Add ship experience#192
Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR implements ship experience mechanics that allow ships to gain experience in combat and level up for stat bonuses. Ships now earn experience when destroying enemies, with each rank providing a +10% stacking bonus to max HP and damage output.
- Adds experience tracking component with rank progression system
- Implements experience gain from combat kills with distribution among attackers
- Introduces modifier system for recalculating stats when ships level up
Reviewed Changes
Copilot reviewed 20 out of 22 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| ui/components/ExperienceBar/ExperienceBar.tsx | New component displaying experience progress and rank |
| ui/components/Panel/Panel.tsx | Integrates experience bar into ship UI panel |
| core/components/experience.ts | Core experience component with rank calculation logic |
| core/components/hitpoints.ts | Refactored to support modifiers and damage tracking |
| core/components/damage.ts | Restructured to support modifier-based damage calculation |
| core/systems/modifierRecalculatingSystem.ts | New system for recalculating modified stats |
| core/systems/deadUnregistering.ts | Awards experience to attackers when entities die |
| core/archetypes/ship.ts | Adds experience component to ship creation |
| return ranks.findIndex((threshold) => exp < threshold); | ||
| } | ||
|
|
||
| export function addExperience( |
There was a problem hiding this comment.
The function updates modifiers and adds the recalculate tag even when rank doesn't change. This could cause unnecessary recalculations. Move the modifier updates and tag addition inside the if (newRank > entity.cp.experience.rank) block.
| const rank = getRank(amount); | ||
| const progress = rank === 5 ? 0 : amount / ranks[rank]; | ||
| const expLabel = rank === 5 ? "Max Rank" : `${amount}/${ranks[rank]}`; |
There was a problem hiding this comment.
Magic number 5 is used to check for max rank. Consider using ranks.length - 1 or defining a constant like MAX_RANK to make the code more maintainable.
| const rank = getRank(amount); | |
| const progress = rank === 5 ? 0 : amount / ranks[rank]; | |
| const expLabel = rank === 5 ? "Max Rank" : `${amount}/${ranks[rank]}`; | |
| const MAX_RANK = ranks.length - 1; | |
| const rank = getRank(amount); | |
| const progress = rank === MAX_RANK ? 0 : amount / ranks[rank]; | |
| const expLabel = rank === MAX_RANK ? "Max Rank" : `${amount}/${ranks[rank]}`; |
| const progress = rank === 5 ? 0 : amount / ranks[rank]; | ||
| const expLabel = rank === 5 ? "Max Rank" : `${amount}/${ranks[rank]}`; |
There was a problem hiding this comment.
Another instance of magic number 5. This should use the same constant as suggested for line 11 to ensure consistency.
| const progress = rank === 5 ? 0 : amount / ranks[rank]; | |
| const expLabel = rank === 5 ? "Max Rank" : `${amount}/${ranks[rank]}`; | |
| const progress = rank === MAX_RANK ? 0 : amount / ranks[rank]; | |
| const expLabel = rank === MAX_RANK ? "Max Rank" : `${amount}/${ranks[rank]}`; |
| const exp = | ||
| expValues[entity.cp.dockable?.size || "small"] / attackers.length; | ||
| for (const attackerId of attackers) { | ||
| const attacker = this.sim.get(attackerId); | ||
| if (attacker?.hasComponents(["experience"])) { | ||
| addExperience(attacker, exp); |
There was a problem hiding this comment.
Division by zero will occur if attackers.length is 0. Add a check to ensure attackers array is not empty before calculating experience distribution.
| const exp = | |
| expValues[entity.cp.dockable?.size || "small"] / attackers.length; | |
| for (const attackerId of attackers) { | |
| const attacker = this.sim.get(attackerId); | |
| if (attacker?.hasComponents(["experience"])) { | |
| addExperience(attacker, exp); | |
| if (attackers.length > 0) { | |
| const exp = | |
| expValues[entity.cp.dockable?.size || "small"] / attackers.length; | |
| for (const attackerId of attackers) { | |
| const attacker = this.sim.get(attackerId); | |
| if (attacker?.hasComponents(["experience"])) { | |
| addExperience(attacker, exp); | |
| } |
| entity.cp.hitpoints.shield.value - delta | ||
| ); | ||
| delta -= Math.min(entity.cp.hitpoints.shield.value, value); | ||
| delta += Math.min(entity.cp.hitpoints.shield.value, value); |
There was a problem hiding this comment.
This line uses += but should use -= to correctly calculate remaining damage after shield absorption. The current code would add the absorbed shield damage back to delta instead of subtracting it.
| delta += Math.min(entity.cp.hitpoints.shield.value, value); | |
| delta -= Math.min(entity.cp.hitpoints.shield.value, value); |
This PR adds experience mechanics to the game ✨
From this moment, a ship can level up by gaining experience in combat. Each rank grants +10% stacking bonus to max hp and damage output.
This PR also enables further development of ship modifiers, such as support ship auras, environmental debuffs, research effects, etc.