merge-system.ts
PURPOSE
Implements the weapon merge mechanic: two level-20 non-legendary weapons combine into a single Legendary weapon determined by the unordered pair of their damage tags. The resulting legendary starts at level 1 and scales 1 to 20 through the normal level-up pool. Merging is single-layer — legendary weapons cannot merge further. Merge cards are surfaced through the level-up reward pool whenever the ship holds two or more eligible weapons.
OWNS
MERGE_ELIGIBLE_LEVELconstant (value 20) — the weapon level at which merging becomes available.MergeCandidateinterface — an unordered pair of owned weapons plus their definitions and the legendary id they would produce.findMergeCandidates(ship)— enumerates every unordered pair of owned, level-20, non-legendary weapons with adamageTag.mergeWeapons(ship, game, weaponIdA, weaponIdB)— mutates the ship’s weapon array: removes both parents, inserts the legendary at the leftmost parent’s slot, and records the legendary ingame.tracking.weaponsFound.
READS FROM
WEAPON_MAPfrom../../data/weapons— looks upWeaponCoreSpecby weapon id, checksisLegendary,damageTag,fireRate.base, anddefaultAngle.getLegendaryForPair(tagA, tagB)from../../data/weapons— resolves the legendary id for a pair ofWeaponTagvalues.ship.weapons— the ship’s weapon instance array; each entry hasid,level, and slot position.GameStatetype from../core/types— imported forgame.trackingaccess.
PUSHES TO
ship.weaponsarray — splices out both parent weapons and splices in the new legendary instance at the lower of the two parent indices.- New weapon instance fields written:
id,cooldown,cooldownMax,fireTimer(randomized toMath.random() * cooldown),level(1),defaultAngle. game.tracking.weaponsFound— appends the legendary id; lazily initializes the array if absent.
DOES NOT
- Does not render UI, generate cards, or open the level-up panel — that is the level-up/reward system’s job; this module only enumerates candidates and executes the merge.
- Does not validate that the legendary id from
getLegendaryForPairexists beyond aWEAPON_MAPlookup guard (returns null on miss). - Does not multi-tier merge — legendaries are filtered out of
findMergeCandidatesand rejected bymergeWeapons. - Does not require matching weapon ids; two copies of the same weapon at level 20 are a valid pair (distinct slot indices, not distinct weapon ids).
- Does not preserve any state from the parent weapons (no level, fire timer, or upgrade history carries over).
- Does not refund XP, emit events, or play VFX/SFX directly.
Signals
- Return value of
findMergeCandidates: array ofMergeCandidateobjects, consumed by the reward/card system to inject merge cards into the level-up pool. - Return value of
mergeWeapons:{ legendaryId }on success,nullon any failure (missing ship, missing slot, legendary parent, missing tag, unresolved legendary id). - Side effect on
game.tracking.weaponsFound— used by tracking/telemetry to record that the legendary was acquired this run.
Entry points
findMergeCandidates(ship)— called by the level-up reward pool builder to detect merge opportunities.mergeWeapons(ship, game, weaponIdA, weaponIdB)— called when a merge card is picked, with the two parent weapon ids from the chosenMergeCandidate.MERGE_ELIGIBLE_LEVELexport — referenced wherever the merge-eligibility threshold is needed outside this module.
Pattern notes
- Eligibility filter is strict: weapon must be in
WEAPON_MAP, must not beisLegendary, must have adamageTag, and(w.level ?? 1)must be at leastMERGE_ELIGIBLE_LEVEL. - Pair enumeration is unordered (
for j = i + 1) so each pair appears exactly once regardless of weapon-id equality. - Distinct-slot lookup in
mergeWeapons: B’s index is found by scanning for a slot whoseid === weaponIdBand whose index is notidxA. This is what allows two same-id weapons to merge. - Splice order matters: higher index is removed first so the lower index stays valid for the second splice.
- The legendary is inserted at
lo(the leftmost parent’s slot) rather than appended, so the HUD weapon bar keeps a stable left-to-right order and the merge cinematic lands the new icon where the leftmost parent used to sit. - Fire timing on the new legendary:
cooldown = 1 / fireRate.base(fallback0.5iffireRateis missing or zero),fireTimeris randomized within[0, cooldown)to avoid synchronized first shots. - Weapon instances are loosely typed as
anyin this module; the canonical specs come throughWeaponCoreSpecfrom the data layer. WEAPON_MAP[legendaryId]is guarded — an unresolved legendary id aborts the merge withnullrather than crashing.