feat(rules): skill-check subsystem foundation - public skill_check helper (d20 + ranks + ability + trained-class-skill +3 + armor check penalty + typed bonuses, one-shot skill track discharged, no natural 1/20 auto-success/fail per SRD Skill Checks), resolve_skill delegates to it, class-skill +3 now requires invested ranks (Table 4-2 Trained Class Skill row), 8 tests
This commit is contained in:
@@ -1277,6 +1277,20 @@ class SaveResult:
|
||||
dc: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SkillCheckResult:
|
||||
"""Outcome of one d20 skill check: the raw die and the full total.
|
||||
|
||||
No success/failure verdict lives here - a skill check only succeeds
|
||||
against a DC (``CombatEngine.resolve_skill``) or against an opposing
|
||||
check (CRB "Skill Checks": "the attempt is successful if your check
|
||||
result exceeds the result of the target").
|
||||
"""
|
||||
|
||||
roll: int
|
||||
total: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Action:
|
||||
"""What a policy wants a combatant to do this turn.
|
||||
@@ -4085,28 +4099,51 @@ class CombatEngine:
|
||||
success = roll != _NATURAL_ONE and (roll == _NATURAL_TWENTY or total >= dc)
|
||||
return SaveResult(success=success, roll=roll, total=total, dc=dc)
|
||||
|
||||
def resolve_skill(
|
||||
self, state: CombatantState, skill: str, dc: int
|
||||
) -> SaveResult:
|
||||
"""Roll a skill check against ``dc``."""
|
||||
def skill_check(
|
||||
self,
|
||||
state: CombatantState,
|
||||
skill: str,
|
||||
*,
|
||||
extra_mods: tuple[StatModifier, ...] = (),
|
||||
) -> SkillCheckResult:
|
||||
"""Roll one d20 skill check: d20 + ranks + ability mod + class-skill
|
||||
+3 (trained class skills only) + armor check penalty + every typed
|
||||
bonus/penalty the effect system carries for the skill (trait, feat,
|
||||
condition, circumstance - all through ``_skill_static_bonus``).
|
||||
|
||||
PF1e (CRB "Skill Checks", KB rules/prpg-core-rulebook/skill-checks.md):
|
||||
"you roll 1d20 and then add your ranks and the appropriate ability
|
||||
score modifier"; "If the skill you're using is a class skill (and you
|
||||
have invested ranks into that skill), you gain a +3 bonus"; "Armor
|
||||
check penalty applies to all Strength- and Dexterity-based skill
|
||||
checks". A skill check has NO natural 1/20 auto-fail/auto-success -
|
||||
the SRD grants those overrides to attack rolls, saves and combat
|
||||
maneuver checks only; success is purely "result equal to or greater
|
||||
than the DC". A PF1e one-shot skill bonus (SRD spells/guidance.md,
|
||||
skill track) discharges on this roll (stacked by bonus type).
|
||||
"""
|
||||
roll = self._rng.d20()
|
||||
# PF1e one-shot skill bonus (SRD spells/guidance.md, skill track):
|
||||
# the first skill check discharges it (stacked by bonus type).
|
||||
shot = self._discharge_one_shot(state, "skill")
|
||||
extra: tuple[StatModifier, ...] = ()
|
||||
extra = list(extra_mods)
|
||||
if shot is not None:
|
||||
if not is_stat_target(skill):
|
||||
msg = f"Unknown skill: {skill}"
|
||||
raise ValueError(msg)
|
||||
extra = (
|
||||
StatModifier(target=skill, value=shot.value, bonus_type=shot.bonus_type),
|
||||
extra.append(
|
||||
StatModifier(target=skill, value=shot.value, bonus_type=shot.bonus_type)
|
||||
)
|
||||
bonus = self._skill_static_bonus(state, skill, extra_mods=extra)
|
||||
total = roll + bonus
|
||||
bonus = self._skill_static_bonus(state, skill, extra_mods=tuple(extra))
|
||||
return SkillCheckResult(roll=roll, total=roll + bonus)
|
||||
|
||||
def resolve_skill(
|
||||
self, state: CombatantState, skill: str, dc: int
|
||||
) -> SaveResult:
|
||||
"""Roll a skill check against ``dc``."""
|
||||
check = self.skill_check(state, skill)
|
||||
# Natural 1/20 are not auto-fail/success for skills
|
||||
success = total >= dc
|
||||
return SaveResult(success=success, roll=roll, total=total, dc=dc)
|
||||
return SaveResult(
|
||||
success=check.total >= dc, roll=check.roll, total=check.total, dc=dc
|
||||
)
|
||||
|
||||
def _skill_static_bonus(
|
||||
self,
|
||||
@@ -4123,8 +4160,12 @@ class CombatEngine:
|
||||
|
||||
skill_rank = state.combatant.skills.get(skill, None)
|
||||
rank = skill_rank.rank if skill_rank else 0
|
||||
# Class skill bonus
|
||||
class_bonus = 3 if skill_rank and skill_rank.class_skill else 0
|
||||
# Class skill bonus. CRB "Skill Checks" (KB rules/prpg-core-rulebook/
|
||||
# skill-checks.md): "If the skill you're using is a class skill (and
|
||||
# you have invested ranks into that skill), you gain a +3 bonus" -
|
||||
# Table 4-2 names the row "Trained Class Skill": an untrained class
|
||||
# skill gets NO synergy bonus.
|
||||
class_bonus = 3 if skill_rank and skill_rank.class_skill and rank > 0 else 0
|
||||
# Armor check penalty
|
||||
armor_penalty = skill_rank.armor_penalty if skill_rank else 0
|
||||
if armor_penalty and skill_spec.armor_check_penalty:
|
||||
|
||||
Reference in New Issue
Block a user