Implement XP-per-skill tracking with persistence via player.info and event sourcing integration #229

Open
opened 2026-03-16 01:40:14 +00:00 by freemo · 0 comments
Owner

Metadata

Key Value
Parent Epic #223 — Skill Tree Data Model & Storage
Legendary #198 — Character Progression & Skills
Branch feature/m4-skill-xp-tracking-persistence
Commit Message feat(skills): implement XP-per-skill tracking with persistence and event sourcing
Points 5
Priority High
MoSCoW Must Have

Background and Context

Skill progression requires tracking XP earned toward each individual skill. When a player performs actions relevant to a skill (e.g., attacking for Combat, foraging for Survival), they earn XP in that specific skill. Once enough XP is accumulated, the skill levels up.

XP data is persisted in player.info["skills"], a hash keyed by skill name. Each entry stores the current level and accumulated XP. Changes to skill XP must be published as Wisper events (:skill_xp_gained) to allow other systems (UI updates, achievements, logging) to react. If event sourcing is enabled in the game configuration, UpdatePlayerSkillXP events are emitted to the event store.

This issue connects the skill data model to the player persistence layer and the event system, completing the data model epic.

Expected Behavior

  1. Player Skill Data Structure in player.info["skills"]:

    player.info["skills"] = {
      "combat" => { "level" => 2, "xp" => 4500 },
      "swordplay" => { "level" => 1, "xp" => 8000 },
      # ...
    }
    
  2. SkillXPManager class at lib/aethyr/core/skills/skill_xp_manager.rb:

    • award_xp(player, skill_name, amount) — adds XP to the specified skill. If XP exceeds threshold, levels up. Returns a result hash: {leveled_up: bool, new_level: N, xp_remaining: N}.
    • get_skill(player, skill_name) — returns {level:, xp:} or nil.
    • get_all_skills(player) — returns the entire skills hash.
    • level_up_threshold(skill_name, current_level) — returns XP needed for next level. Default: 10000 * (current_level + 1) (scaling formula).
    • reset_skill(player, skill_name) — resets a skill to level 0, xp 0 (for respec).
  3. Wisper Event Integration:

    • After XP is awarded, publish :skill_xp_gained with payload: {player_id:, skill_name:, amount:, new_xp:, new_level:, leveled_up:}.
    • After level-up, publish :skill_level_up with payload: {player_id:, skill_name:, new_level:, old_level:}.
  4. Event Sourcing Integration:

    • If ServerConfig[:event_sourcing] is enabled, emit UpdatePlayerSkillXP events with full state delta.
    • Events are stored for replay and auditing.
  5. Persistence:

    • All changes to player.info["skills"] are saved via the existing player save mechanism.
    • On player load, player.info["skills"] is restored from storage.

Acceptance Criteria

  • SkillXPManager class exists at lib/aethyr/core/skills/skill_xp_manager.rb.
  • award_xp correctly accumulates XP and triggers level-ups when threshold is exceeded.
  • award_xp handles overflow (excess XP carries to next level).
  • Level-up threshold scales with current level: 10000 * (current_level + 1).
  • :skill_xp_gained Wisper event is published on every XP award.
  • :skill_level_up Wisper event is published on level-up.
  • UpdatePlayerSkillXP event sourcing events emitted when event sourcing is enabled.
  • player.info["skills"] persists across player save/load cycles.
  • get_skill and get_all_skills return correct data.
  • reset_skill properly clears skill data.

Subtasks

  • Create lib/aethyr/core/skills/skill_xp_manager.rb with the SkillXPManager class.
  • Implement award_xp(player, skill_name, amount) with XP accumulation and level-up logic.
  • Implement level-up threshold calculation with scaling formula.
  • Implement XP overflow handling (excess XP carries into next level).
  • Implement get_skill(player, skill_name) and get_all_skills(player).
  • Implement reset_skill(player, skill_name).
  • Integrate Wisper event publishing for :skill_xp_gained and :skill_level_up.
  • Integrate event sourcing with UpdatePlayerSkillXP events when enabled.
  • Ensure player.info["skills"] is included in existing player save/load mechanisms.
  • Verify persistence round-trip: save player, reload, verify skill data intact.
  • Docs: Update YARD comments on affected classes and methods. Update relevant Docusaurus documentation pages if applicable.
  • Tests (Cucumber): Add tests/unit/skill_xp_manager.feature covering XP award, level-up trigger, XP overflow, threshold calculation, Wisper event publishing, event sourcing integration, persistence round-trip, reset skill.
  • Tests (Cucumber Integration): Add integration feature in tests/integration/ for XP tracking with persistence and event publishing end-to-end.
  • Tests (Profiling): Run bundle exec rake unit_profile and verify no performance regressions.
  • Quality: Verify coverage >=97% via bundle exec rake unit. If coverage is <97% then review the current unit test coverage report at build/tests/unit/coverage/ and use it to write new Cucumber based unit tests to improve code coverage. Specifically, write Cucumber/Gherkin style unit tests that are descriptively named and specifically improve coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun bundle exec rake unit to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%.
  • Quality: Run bundle exec rake (default task: unit tests with coverage) and bundle exec rake integration, fix any errors if needed ensuring both pass across entire code base, do not ignore any failure even if it seems unrelated to this commit, fix it.

Definition of Done

This issue is complete when:

  • All subtasks above are completed and checked off.
  • A Git commit is created where the first line of the commit message matches the Commit Message in Metadata exactly, followed by a blank line, then additional lines providing relevant details about the implementation.
  • The commit is pushed to the remote on the branch matching the Branch in Metadata exactly.
  • The commit is submitted as a pull request to master, reviewed, and merged before this issue is marked done.
## Metadata | Key | Value | |-----|-------| | **Parent Epic** | #223 — Skill Tree Data Model & Storage | | **Legendary** | #198 — Character Progression & Skills | | **Branch** | `feature/m4-skill-xp-tracking-persistence` | | **Commit Message** | `feat(skills): implement XP-per-skill tracking with persistence and event sourcing` | | **Points** | 5 | | **Priority** | High | | **MoSCoW** | Must Have | ## Background and Context Skill progression requires tracking XP earned toward each individual skill. When a player performs actions relevant to a skill (e.g., attacking for Combat, foraging for Survival), they earn XP in that specific skill. Once enough XP is accumulated, the skill levels up. XP data is persisted in `player.info["skills"]`, a hash keyed by skill name. Each entry stores the current level and accumulated XP. Changes to skill XP must be published as Wisper events (`:skill_xp_gained`) to allow other systems (UI updates, achievements, logging) to react. If event sourcing is enabled in the game configuration, `UpdatePlayerSkillXP` events are emitted to the event store. This issue connects the skill data model to the player persistence layer and the event system, completing the data model epic. ## Expected Behavior 1. **Player Skill Data Structure** in `player.info["skills"]`: ```ruby player.info["skills"] = { "combat" => { "level" => 2, "xp" => 4500 }, "swordplay" => { "level" => 1, "xp" => 8000 }, # ... } ``` 2. **SkillXPManager** class at `lib/aethyr/core/skills/skill_xp_manager.rb`: - `award_xp(player, skill_name, amount)` — adds XP to the specified skill. If XP exceeds threshold, levels up. Returns a result hash: `{leveled_up: bool, new_level: N, xp_remaining: N}`. - `get_skill(player, skill_name)` — returns `{level:, xp:}` or `nil`. - `get_all_skills(player)` — returns the entire skills hash. - `level_up_threshold(skill_name, current_level)` — returns XP needed for next level. Default: `10000 * (current_level + 1)` (scaling formula). - `reset_skill(player, skill_name)` — resets a skill to level 0, xp 0 (for respec). 3. **Wisper Event Integration:** - After XP is awarded, publish `:skill_xp_gained` with payload: `{player_id:, skill_name:, amount:, new_xp:, new_level:, leveled_up:}`. - After level-up, publish `:skill_level_up` with payload: `{player_id:, skill_name:, new_level:, old_level:}`. 4. **Event Sourcing Integration:** - If `ServerConfig[:event_sourcing]` is enabled, emit `UpdatePlayerSkillXP` events with full state delta. - Events are stored for replay and auditing. 5. **Persistence:** - All changes to `player.info["skills"]` are saved via the existing player save mechanism. - On player load, `player.info["skills"]` is restored from storage. ## Acceptance Criteria - [ ] `SkillXPManager` class exists at `lib/aethyr/core/skills/skill_xp_manager.rb`. - [ ] `award_xp` correctly accumulates XP and triggers level-ups when threshold is exceeded. - [ ] `award_xp` handles overflow (excess XP carries to next level). - [ ] Level-up threshold scales with current level: `10000 * (current_level + 1)`. - [ ] `:skill_xp_gained` Wisper event is published on every XP award. - [ ] `:skill_level_up` Wisper event is published on level-up. - [ ] `UpdatePlayerSkillXP` event sourcing events emitted when event sourcing is enabled. - [ ] `player.info["skills"]` persists across player save/load cycles. - [ ] `get_skill` and `get_all_skills` return correct data. - [ ] `reset_skill` properly clears skill data. ## Subtasks - [ ] Create `lib/aethyr/core/skills/skill_xp_manager.rb` with the `SkillXPManager` class. - [ ] Implement `award_xp(player, skill_name, amount)` with XP accumulation and level-up logic. - [ ] Implement level-up threshold calculation with scaling formula. - [ ] Implement XP overflow handling (excess XP carries into next level). - [ ] Implement `get_skill(player, skill_name)` and `get_all_skills(player)`. - [ ] Implement `reset_skill(player, skill_name)`. - [ ] Integrate Wisper event publishing for `:skill_xp_gained` and `:skill_level_up`. - [ ] Integrate event sourcing with `UpdatePlayerSkillXP` events when enabled. - [ ] Ensure `player.info["skills"]` is included in existing player save/load mechanisms. - [ ] Verify persistence round-trip: save player, reload, verify skill data intact. - [ ] Docs: Update YARD comments on affected classes and methods. Update relevant Docusaurus documentation pages if applicable. - [ ] Tests (Cucumber): Add `tests/unit/skill_xp_manager.feature` covering XP award, level-up trigger, XP overflow, threshold calculation, Wisper event publishing, event sourcing integration, persistence round-trip, reset skill. - [ ] Tests (Cucumber Integration): Add integration feature in `tests/integration/` for XP tracking with persistence and event publishing end-to-end. - [ ] Tests (Profiling): Run `bundle exec rake unit_profile` and verify no performance regressions. - [ ] Quality: Verify coverage >=97% via `bundle exec rake unit`. If coverage is <97% then review the current unit test coverage report at `build/tests/unit/coverage/` and use it to write new Cucumber based unit tests to improve code coverage. Specifically, write Cucumber/Gherkin style unit tests that are descriptively named and specifically improve coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `bundle exec rake unit` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%. - [ ] Quality: Run `bundle exec rake` (default task: unit tests with coverage) and `bundle exec rake integration`, fix any errors if needed ensuring both pass across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it. ## Definition of Done This issue is complete when: - All subtasks above are completed and checked off. - A Git commit is created where the **first line** of the commit message matches the Commit Message in Metadata exactly, followed by a blank line, then additional lines providing relevant details about the implementation. - The commit is pushed to the remote on the branch matching the **Branch** in Metadata exactly. - The commit is submitted as a **pull request** to `master`, reviewed, and **merged** before this issue is marked done.
freemo added this to the v1.3.0 milestone 2026-03-16 01:40:14 +00:00
freemo self-assigned this 2026-03-16 01:40:14 +00:00
Sign in to join this conversation.
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.

Reference: aethyr/Aethyr#229
No description provided.