Persist combat vitals in save data and restore damage on load
- Capture hull/shields/energy from the live CombatSystem into record.combat on save - Stage `combatVitals` in prepareLoad so a panel load restores the damage the run was flying with (clamped, hull floored at 1) instead of always starting full - Add `repairFull()` to CombatSystem and call it on world landing (startLanding) so the dock heals the ship - Set `combatRepair` flag in jumpThroughGate's record so a system arrival also triggers a full repair, while panel loads never carry it - Clear both staged values in resetRunState for fresh runs - Carry `cargoCosts` through jump records to preserve per-bay pricing across systems
This commit is contained in:
parent
ab7cd1857e
commit
aca351b035
|
|
@ -73,6 +73,10 @@ const fakeScene = () => {
|
|||
],
|
||||
},
|
||||
playTimeMs: 123456,
|
||||
// THE SHIP'S VITALS (hull/shields/energy — combat's live state): the
|
||||
// damage the run is flying with rides the save (a load restores it,
|
||||
// the landing is the repair).
|
||||
combat: { hull: 42, shields: 7, energy: 33, xp: { level: 3, xp: 90 } },
|
||||
// THE ORIGIN WORLDS' REMAINING DAILY BATCHES (model v2 — the economy's
|
||||
// only state): a partial batch for the current day rides the save.
|
||||
economyStock: { [system.id]: { day: 1234, left: 2 } },
|
||||
|
|
@ -283,6 +287,8 @@ const makeStorage = (fail = false) => {
|
|||
Array.isArray(rec.comms?.acked) && rec.comms.acked.includes('carol-delivered-1') && rec.comms.acked.includes('oac-delivered-1'));
|
||||
check('capture: the combat done-ledger is carried (record.combat.done)',
|
||||
Array.isArray(rec.combat?.done) && rec.combat.done.includes('pirate_ambush'));
|
||||
check('capture: the ship\u2019s vitals are carried (record.combat hull/shields/energy — the damage the run flies with)',
|
||||
rec.combat?.hull === 42 && rec.combat?.shields === 7 && rec.combat?.energy === 33);
|
||||
check('capture: the story ledger rides (record.story.delivered + capitalReturned)',
|
||||
rec.story?.delivered === true && rec.story?.capitalReturned === true);
|
||||
|
||||
|
|
@ -320,6 +326,10 @@ const makeStorage = (fail = false) => {
|
|||
&& reg.get('commsAcked').size === 2);
|
||||
check('prepare: the combat done-ledger is restored (a Set, same ids)',
|
||||
reg.get('combatDone') instanceof Set && reg.get('combatDone').has('pirate_ambush') && reg.get('combatDone').size === 1);
|
||||
check('prepare: the ship\u2019s vitals are staged (combatVitals — the load restores the damage, not a full ship)',
|
||||
JSON.stringify(reg.get('combatVitals')) === JSON.stringify({ hull: 42, shields: 7, energy: 33 }));
|
||||
check('prepare: a panel load stages NO arrival repair (the damage survives the load)',
|
||||
reg.get('combatRepair') === false);
|
||||
check('prepare: the story ledger is restored (delivered + capitalReturned)',
|
||||
reg.get('story')?.delivered === true && reg.get('story')?.capitalReturned === true);
|
||||
check('prepare: the batch remainders are restored (registry.economyStock)',
|
||||
|
|
@ -360,6 +370,15 @@ const makeStorage = (fail = false) => {
|
|||
regNoBuilds.get('commsAcked') instanceof Set && regNoBuilds.get('commsAcked').size === 0);
|
||||
check('prepare: a legacy record (no combat field) stages an empty done-ledger',
|
||||
regNoBuilds.get('combatDone') instanceof Set && regNoBuilds.get('combatDone').size === 0);
|
||||
check('prepare: a legacy record (no combat vitals) stages null vitals (the fresh ship stands full)',
|
||||
regNoBuilds.get('combatVitals') === null);
|
||||
check('prepare: a legacy record (no combatRepair) stages no arrival repair',
|
||||
regNoBuilds.get('combatRepair') === false);
|
||||
// A JUMP\u2019s record (jumpThroughGate sets the flag) heals on arrival.
|
||||
const regJump = { map: new Map(), set(k, v) { this.map.set(k, v); }, get(k) { return this.map.get(k); } };
|
||||
prepareLoad(regJump, makeRec({ combatRepair: true, combat: { hull: 12, shields: 3, energy: 8 } }));
|
||||
check('prepare: a jump record stages the arrival repair (system landing = full repair)',
|
||||
regJump.get('combatRepair') === true && regJump.get('combatVitals')?.hull === 12);
|
||||
check('prepare: a legacy record (no story field) stages the fresh story ledger',
|
||||
regNoBuilds.get('story')?.capitalLanded === false && regNoBuilds.get('story')?.delivered === false && regNoBuilds.get('story')?.capitalReturned === false);
|
||||
check('prepare: a legacy record (no economyStock) restores an empty batch state',
|
||||
|
|
@ -381,8 +400,12 @@ const makeStorage = (fail = false) => {
|
|||
reg.set('commsPlayed', new Set(['oac-intro']));
|
||||
reg.set('commsAcked', new Set(['oac-intro']));
|
||||
reg.set('combatDone', new Set(['raid']));
|
||||
reg.set('combatVitals', { hull: 1, shields: 1, energy: 1 });
|
||||
reg.set('combatRepair', true);
|
||||
reg.set(PENDING_RESTORE_KEY, { seed: SEED, ship: { x: 0, y: 0, heading: 0 } });
|
||||
resetRunState(reg);
|
||||
check('reset: the combat vitals are cleared (the new run\u2019s ship stands full)', reg.get('combatVitals') === null);
|
||||
check('reset: the arrival repair flag is cleared', reg.get('combatRepair') === false);
|
||||
check('reset: discovery cleared (fresh run)', reg.get('discovery') === null);
|
||||
check('reset: depleted fields cleared (fresh run)', reg.get('depletedClusters') === null);
|
||||
check('reset: staged restore cleared', reg.get(PENDING_RESTORE_KEY) === null);
|
||||
|
|
|
|||
|
|
@ -147,11 +147,27 @@ export class CombatSystem {
|
|||
// the system's share of it.
|
||||
this._levelUnsub = this.xp.on('levelup', (ev) => this._onLevelUp(ev));
|
||||
// Apply the CURRENT level's armor (a restored ledger's level — a load
|
||||
// fires no events, so the bump lands here, not on the event), then a
|
||||
// fresh scene is a fresh ship (full armor, whatever the level).
|
||||
// fires no events, so the bump lands here, not on the event), then
|
||||
// settle the ship's VITALS: o.vitals (SaveData's record.combat
|
||||
// hull/shields/energy, staged by prepareLoad as 'combatVitals')
|
||||
// restores the DAMAGE the run was flying with, clamped to this
|
||||
// level's max — a load is not a repair. Missing or invalid numbers
|
||||
// (a save predating the fields, combat off, a fresh run) fall back
|
||||
// to the fresh ship (full armor). The full repair instead happens on
|
||||
// a WORLD LANDING (GameScene.startLanding → repairFull) and a
|
||||
// SYSTEM ARRIVAL (a jump's 'combatRepair' flag — the destination's
|
||||
// dock stands in for the cut).
|
||||
this._applyArmorToLevel();
|
||||
this.hull = this.maxHull;
|
||||
this.shields = this.maxShields;
|
||||
const vit = o.vitals && typeof o.vitals === 'object' ? o.vitals : null;
|
||||
const settle = (n, max) => {
|
||||
const x = Number(n);
|
||||
return Number.isFinite(x) ? Math.max(0, Math.min(max, x)) : max;
|
||||
};
|
||||
// A restored run is ALIVE — the hull floors at 1 (a hull of 0 is a
|
||||
// death, and death ends the run; a degenerate save captured on the
|
||||
// dying frame must still load as a flying ship).
|
||||
this.hull = Math.max(1, settle(vit?.hull, this.maxHull));
|
||||
this.shields = settle(vit?.shields, this.maxShields);
|
||||
this.playerRespawn = p.respawn !== false;
|
||||
this.playerRespawnDelay = (Number(p.respawnDelay) || 2) * 1000;
|
||||
this.playerDown = false;
|
||||
|
|
@ -163,7 +179,7 @@ export class CombatSystem {
|
|||
// shows it.
|
||||
const en = config.section('combat.energy', {});
|
||||
this.maxEnergy = Math.max(0, Number(en.max) || 100);
|
||||
this.energy = this.maxEnergy;
|
||||
this.energy = settle(vit?.energy, this.maxEnergy); // the drain rides the load too
|
||||
this.energyRegen = Math.max(0, Number(en.regen) || 0);
|
||||
|
||||
// Main-game combat (→ game).
|
||||
|
|
@ -1462,6 +1478,20 @@ export class CombatSystem {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The dock's REPAIR — a WORLD LANDING (planet/station) puts the ship in
|
||||
* the bay: hull + shields + energy back to max (GameScene.startLanding
|
||||
* calls it; the system-arrival repair is the same settle via the
|
||||
* constructor's o.vitals). The respawn's full-heal beat stands on its
|
||||
* own.
|
||||
*/
|
||||
repairFull() {
|
||||
this.hull = this.maxHull;
|
||||
this.shields = this.maxShields;
|
||||
this.energy = this.maxEnergy;
|
||||
this.lastHitAt = -1e9; // the shield-regen grace clock stands fresh
|
||||
}
|
||||
|
||||
/** The clock ran out — the player is back, full armor, in the same seat. */
|
||||
revivePlayer(time) {
|
||||
this.playerDown = false;
|
||||
|
|
|
|||
|
|
@ -47,13 +47,27 @@
|
|||
* // (js/skills/SkillState.js,
|
||||
* // data/skills.json)
|
||||
* combat: { done: [scriptName, …], // the CLEARED wave scripts
|
||||
* level, xp }, // + the run's COMBAT XP
|
||||
* // ledger (js/combat/
|
||||
* level, xp, // + the run's COMBAT XP
|
||||
* hull, shields, energy }, // ledger (js/combat/
|
||||
* // XpManager.js — the
|
||||
* // level + the xp banked
|
||||
* // toward the next; the
|
||||
* // curve is data/
|
||||
* // progression.json)
|
||||
* // + the ship's VITALS
|
||||
* // (CombatSystem's
|
||||
* // hull/shields/energy —
|
||||
* // the LOAD restores the
|
||||
* // damage; the full
|
||||
* // repair is the
|
||||
* // landing's beat)
|
||||
* combatRepair: true, // set by a JUMP's record
|
||||
* // (jumpThroughGate):
|
||||
* // system ARRIVAL is a
|
||||
* // landing — the ship
|
||||
* // heals full. A panel
|
||||
* // load never carries it
|
||||
* // (damage survives).
|
||||
* totalMined: number, // lifetime asteroid mining
|
||||
* pirateKills: { [sysId]: int }, // per-system PIRATE KILL
|
||||
* // tallies (the reputation
|
||||
|
|
@ -320,6 +334,13 @@ export function captureState(scene, now) {
|
|||
done: Array.from(scene.registry.get('combatDone') ?? []),
|
||||
level: Math.max(0, Math.floor(Number(scene.combat?.xp?.level) || 0)),
|
||||
xp: Math.max(0, Math.round(Number(scene.combat?.xp?.xp) || 0)),
|
||||
// THE SHIP'S VITALS (hull / shields / energy — the damage the run
|
||||
// is flying with). A live combat system carries them (null when
|
||||
// combat is off / a save predating the fields → the load falls back
|
||||
// to the fresh ship's full repair — old saves load).
|
||||
hull: scene.combat ? Math.max(0, Math.round(Number(scene.combat.hull) || 0)) : null,
|
||||
shields: scene.combat ? Math.max(0, Math.round(Number(scene.combat.shields) || 0)) : null,
|
||||
energy: scene.combat ? Math.max(0, Math.round(Number(scene.combat.energy) || 0)) : null,
|
||||
},
|
||||
// Lifetime minerals mined from asteroids (the 'Mine 200 Minerals'
|
||||
// requirement's input — the hold is cargo, this is the record).
|
||||
|
|
@ -581,6 +602,27 @@ export function prepareLoad(registry, record) {
|
|||
}
|
||||
: null,
|
||||
);
|
||||
// THE SHIP'S VITALS (record.combat hull/shields/energy) — handed to the
|
||||
// fresh CombatSystem (o.vitals): the LOAD restores the damage the run
|
||||
// was flying with (clamped to its max), NOT a full ship — the full
|
||||
// repair is the landing's beat (a world landing heals; a jump's arrival
|
||||
// heals via the flag below). A save predating the fields (or with
|
||||
// combat off) stages null → the fresh ship's full armor stands.
|
||||
registry.set(
|
||||
'combatVitals',
|
||||
record.combat && typeof record.combat === 'object'
|
||||
? {
|
||||
hull: record.combat.hull ?? null,
|
||||
shields: record.combat.shields ?? null,
|
||||
energy: record.combat.energy ?? null,
|
||||
}
|
||||
: null,
|
||||
);
|
||||
// A jump's ARRIVAL is a LANDING — jumpThroughGate sets the flag on its
|
||||
// record, and the fresh scene heals the ship full instead of restoring
|
||||
// the saved damage (GameScene.create consumes it). A panel load never
|
||||
// carries the key → the damage survives the load.
|
||||
registry.set('combatRepair', record.combatRepair === true);
|
||||
// THE CLASS LEDGER (js/player/ClassState.js) — the class on file, its
|
||||
// granted perks and the banked skill points. A save predating the class
|
||||
// layer has no field → null (a fresh run picks its class at level 5; old
|
||||
|
|
@ -653,6 +695,8 @@ export function resetRunState(registry) {
|
|||
registry.set('commsAcked', null); // the ack gates re-arm on the new run
|
||||
registry.set('combatDone', null); // the cleared waves re-arm on the new run
|
||||
registry.set('combatXp', null); // the new run's level + xp stand fresh
|
||||
registry.set('combatVitals', null); // the new run's ship stands full
|
||||
registry.set('combatRepair', false); // no arrival repair is staged
|
||||
registry.set('playerClass', null); // the new run picks its class again
|
||||
registry.set('playerSkills', null); // …and ranks its tree from scratch
|
||||
registry.set('vignettesPlayed', null); // the scenes re-arm on the new run
|
||||
|
|
|
|||
|
|
@ -1120,9 +1120,18 @@ export class GameScene extends Phaser.Scene {
|
|||
// data/combat.json → enemy.xp / enemyTypes). A save predating the
|
||||
// ledger has no field → the run starts at level 0 (old saves load).
|
||||
const savedXp = this.registry.get('combatXp');
|
||||
// THE SHIP'S VITALS (hull/shields/energy): a LOAD restores the
|
||||
// damage the run was flying with (record.combat → prepareLoad's
|
||||
// 'combatVitals'); a JUMP's arrival is a SYSTEM LANDING — the
|
||||
// 'combatRepair' flag (staged in jumpThroughGate's record) heals
|
||||
// the ship full instead; a fresh run (neither staged) is a fresh
|
||||
// ship (full armor).
|
||||
const arrivalRepair = this.registry.get('combatRepair') === true;
|
||||
this.registry.set('combatRepair', false);
|
||||
this.combat = new CombatSystem(this, {
|
||||
ship: this.ship,
|
||||
xp: savedXp ? XpManager.fromJSON(savedXp) : new XpManager(),
|
||||
vitals: arrivalRepair ? null : (this.registry.get('combatVitals') ?? null),
|
||||
onActiveChange: (active, neutral) => this._setCombatBar(active, neutral),
|
||||
// The readability layer's bookkeeping (js/combat/CombatHud.js —
|
||||
// the same shared layer the range runs): a kill lands in the feed,
|
||||
|
|
@ -3684,6 +3693,10 @@ export class GameScene extends Phaser.Scene {
|
|||
// the field (the nearest-gate fallback).
|
||||
rec.lastJumpFrom = from.id;
|
||||
rec.systemName = dest.name;
|
||||
// SYSTEM ARRIVAL = LANDING — the destination's dock repairs the ship
|
||||
// (hull/shields/energy full) the moment the cut lands. A panel LOAD
|
||||
// never carries the key, so a save's damage survives its load.
|
||||
rec.combatRepair = true;
|
||||
rec.ship = {
|
||||
x: arrival.x,
|
||||
y: arrival.y,
|
||||
|
|
@ -3699,6 +3712,10 @@ export class GameScene extends Phaser.Scene {
|
|||
// restore would re-seed the fresh ship's 0 and shed the goods
|
||||
// aboard (the 4-bay hold would die on the first jump).
|
||||
cargoHold: rec.ship?.cargoHold ?? 0,
|
||||
// The per-bay cost ledger rides the jump too — the manifest prices
|
||||
// exits against what was ACTUALLY paid per bay; dropping it would
|
||||
// re-price every bay at the catalog's own value on the way down.
|
||||
cargoCosts: rec.ship?.cargoCosts ?? null,
|
||||
};
|
||||
rec.tethers = []; // the destination's native set re-forms in create()
|
||||
prepareLoad(this.registry, rec);
|
||||
|
|
@ -4229,6 +4246,11 @@ export class GameScene extends Phaser.Scene {
|
|||
// where they are). A no-op unless the escort is the active contract in
|
||||
// its 'failed' phase (a normal landing — the convoy intact — never resets).
|
||||
this._escortResetForLanding();
|
||||
// THE DOCK'S REPAIR — the world is a landing, not a jump: the bay
|
||||
// puts the hull, shields and energy back to full (a combat-damaged
|
||||
// ship or a drained-energy one alike — the surface is where ships
|
||||
// get fixed; the run's other full-heal beat is the respawn's).
|
||||
this.combat?.repairFull();
|
||||
this.scene.launch('SurfaceScene', {
|
||||
frame: Number(target.frame ?? target.sheetFrame ?? 0),
|
||||
name,
|
||||
|
|
|
|||
Loading…
Reference in New Issue