feat: stats & use-based skills, skill-based hit chance (#8) #34

Merged
marco merged 1 commit from feat/stats-skills into main 2026-07-17 17:32:16 +02:00
Owner

Closes #8.

Summary

Character progression (M6): primary stats + use-based skills, wired into combat as a hit-chance roll.

  • Stats (STR/DEX/INT) on every Mobile. HP derives from STR (STR 50 → 50 HP; rat 20 → 20). Stamina/Mana (DEX/INT) are present + derived but not yet combat-wired (declared follow-up).
  • Skills — data-driven (SkillName enum + SkillCatalog + SkillSet, values in tenths, 1000 = 100.0). v1 skills: Wrestling (unarmed) + Swordsmanship (sword). Train use-based: each swing may raise the weapon skill, slower as it climbs (SkillFormulas, quadratic falloff to the cap) — the UO grind. On a gain the player gets a SystemMessage ("Swordsmanship increased 0.1 — now 30.1").
  • Hit chance — combat now rolls to hit from weapon skill (attacker vs defender) (CombatFormulas): equal skill = base, a big skill edge can guarantee the hit, a deficit floors at a sliver. Swings can miss → a floating miss instead of a damage number.
  • Server-authoritative & persisted — the client never sets stats/skills. Skills persist across reconnect (dedicated PlayerSkills table, Sqlite+Postgres migrations); stats are config-constant in v1 so re-seeded on connect.

Design decisions (agreed in-thread)

  • Skills = data table, not a class hierarchy (they differ in data, not behaviour — unlike commands/creatures).
  • Items = class hierarchy (BaseItem → BaseWeapon → Sword; Armor, Coin), each declaring Kind + Graphic (art id, like BaseCreature.Body) + stats, discovered by reflection (ItemCatalog) — replaced the string→WeaponStats dictionary.
  • World stays a thin facade — every combat/skill/death rule lives in CombatSystem; World only delegates. Added a hard gate in CLAUDE.md ("World.cs does not receive logic") + a one-type-per-file rule.

How it was tested

Unit: HitChance (base/advantage/floor), SkillFormulas.GainChance (falloff + zero at cap), SkillSet.TryGain (+0.1, stops at cap). World: attacking trains the weapon skill, STR sets MaxHits, plus the existing combat-loop tests (updated: skill gaps make hits deterministic where needed). Persistence: skills round-trip through the store. 206 tests green, zero warnings, CSharpier clean.

Needs your eyes (GUI)

Fight a rat unarmed: you should see occasional miss, and a stream of "Wrestling increased 0.1 — now 30.x" over your character as it trains. Equip the sword and the messages switch to Swordsmanship. Tell me if the gain rate / hit-chance feel right (all tunable in CombatOptions).

Checklist

  • just lint clean · [x] just test green (206) · [x] whole solution builds, 0 warnings · [x] multi-platform preserved · [x] tests added · [x] EF migrations both providers
Closes #8. ## Summary Character progression (M6): primary stats + use-based skills, wired into combat as a hit-chance roll. - **Stats** (STR/DEX/INT) on every `Mobile`. **HP derives from STR** (STR 50 → 50 HP; rat 20 → 20). Stamina/Mana (DEX/INT) are present + derived but not yet combat-wired (declared follow-up). - **Skills** — data-driven (`SkillName` enum + `SkillCatalog` + `SkillSet`, values in **tenths**, 1000 = 100.0). v1 skills: **Wrestling** (unarmed) + **Swordsmanship** (sword). Train **use-based**: each swing may raise the weapon skill, **slower as it climbs** (`SkillFormulas`, quadratic falloff to the cap) — the UO grind. On a gain the player gets a `SystemMessage` (`"Swordsmanship increased 0.1 — now 30.1"`). - **Hit chance** — combat now rolls to hit from **weapon skill (attacker vs defender)** (`CombatFormulas`): equal skill = base, a big skill edge can guarantee the hit, a deficit floors at a sliver. **Swings can miss** → a floating **`miss`** instead of a damage number. - **Server-authoritative & persisted** — the client never sets stats/skills. Skills persist across reconnect (dedicated **`PlayerSkills`** table, Sqlite+Postgres migrations); stats are config-constant in v1 so re-seeded on connect. ## Design decisions (agreed in-thread) - **Skills = data table**, not a class hierarchy (they differ in data, not behaviour — unlike commands/creatures). - **Items = class hierarchy** (`BaseItem → BaseWeapon → Sword`; `Armor`, `Coin`), each declaring `Kind` + `Graphic` (art id, like `BaseCreature.Body`) + stats, **discovered by reflection** (`ItemCatalog`) — replaced the `string→WeaponStats` dictionary. - **World stays a thin facade** — every combat/skill/death rule lives in `CombatSystem`; World only delegates. Added a **hard gate in CLAUDE.md** ("World.cs does not receive logic") + a **one-type-per-file** rule. ## How it was tested Unit: `HitChance` (base/advantage/floor), `SkillFormulas.GainChance` (falloff + zero at cap), `SkillSet.TryGain` (+0.1, stops at cap). World: **attacking trains the weapon skill**, **STR sets MaxHits**, plus the existing combat-loop tests (updated: skill gaps make hits deterministic where needed). Persistence: skills round-trip through the store. **206 tests green**, zero warnings, CSharpier clean. ## Needs your eyes (GUI) Fight a rat unarmed: you should see occasional **`miss`**, and a stream of **"Wrestling increased 0.1 — now 30.x"** over your character as it trains. Equip the sword and the messages switch to **Swordsmanship**. Tell me if the gain rate / hit-chance feel right (all tunable in `CombatOptions`). ## Checklist - [x] `just lint` clean · [x] `just test` green (206) · [x] whole solution builds, 0 warnings · [x] multi-platform preserved · [x] tests added · [x] EF migrations both providers
feat: stats & use-based skills, hit chance from weapon skill (M6)
All checks were successful
ci / Lint & Test (pull_request) Successful in 1m33s
80d88af605
Primary stats (STR/DEX/INT) on every Mobile; HP derives from STR (Stamina/Mana
present, not yet combat-wired). A data-driven skill system (SkillName enum +
SkillCatalog + SkillSet, values in tenths) that trains use-based: on each swing
the attacker's weapon skill may rise, slower as it climbs (SkillFormulas), and
the player gets a SystemMessage ("Swordsmanship increased 0.1 — now 30.1").

Combat now rolls to hit: weapon skill (attacker vs defender) drives hit chance
(CombatFormulas), so swings can miss — a miss shows a floating "miss" instead of
a damage number. All server-authoritative; the client never sets stats/skills.
Skills persist across reconnect (dedicated PlayerSkills table, dual-provider
migration); stats are config-constant in v1 so re-seeded on connect.

Items become a class hierarchy (BaseItem -> BaseWeapon -> Sword; Armor, Coin),
each declaring its Kind + Graphic + stats, discovered by reflection (ItemCatalog)
— replacing the string->WeaponStats dictionary, mirroring BaseCreature.

World stays a thin facade: all combat/skill/death logic lives in CombatSystem;
World only delegates. Adds a hard gate in CLAUDE.md against putting logic in
World, plus a one-type-per-file rule. Hardened token validation to pin HS256.
marco merged commit 6d11473f32 into main 2026-07-17 17:32:16 +02:00
marco deleted branch feat/stats-skills 2026-07-17 17:32:16 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
marco/IsoMmo!34
No description provided.