# Commit Message
## Wolfenstein: Expand E1M2 level and persist per-mission weapon loadouts ### Level Data (assets/gamedata/wolfenstein/level-e1m2.json) - Increase map dimensions from 33×24 to 39×29 cells, adding new corridors, rooms, and wall structures - Add tile type `5` (likely a new floor or decorative element) throughout the expanded areas - Reposition existing doors, guards, items, objects, and wall art to match the new layout - Add new entities: additional guards with patrol routes, machine gun pickups, ammo, health kits, yellow key, and secret door in the expanded sections - Update player start position and exit location for the larger map ### Game Logic (src/games/wolfenstein/WolfensteinGame.js) - Introduce `LOADOUTS_KEY` localStorage persistence to record which weapons the player owned when each mission was unlocked - Replace `_freshCampaignCarry()` with `_carryForUnlockedStart()`, which loads the recorded weapon loadout for a given mission (falling back to fists-only for mission 0 or unrecorded missions) and tops up ammo to full - Add `_fullAmmoFor()` helper that computes max ammo reserves for all ammo-consuming weapons in a loadout - Update `_recordMissionCleared()` to capture the player's current weapon list at clear-time and store it as the unlock loadout for the next mission (only advancing monotonically) - Change "Next Mission" flow to rely on the recorded loadout rather than extracting live state, ensuring consistent full-ammo/full-health starts - Update `_retry()` to restore the original attempt's weapons but top up ammo to full per the "die → come back with full ammo" spec
This commit is contained in:
parent
9fa3896b5b
commit
3bc8b69f1f
File diff suppressed because it is too large
Load Diff
|
|
@ -42,6 +42,14 @@ const SAVE_KEY = 'wolfenstein-save';
|
|||
const SAVE_SLOT_COUNT = 4;
|
||||
const saveSlotKey = (i) => `wolfenstein-save-slot-${i}`;
|
||||
const PROGRESS_KEY = 'wolfenstein-progress';
|
||||
// Per-campaign, per-mission-index snapshot of the weapons the player owned
|
||||
// at the moment that mission was unlocked (i.e. captured from the winning
|
||||
// player right as the PRECEDING mission was cleared — see
|
||||
// _recordMissionCleared/_loadoutForMission). Lets picking any already-
|
||||
// unlocked mission straight off the campaign list (not just "Next Mission"
|
||||
// right after a win) start with the right weapons instead of always
|
||||
// falling back to fists-only.
|
||||
const LOADOUTS_KEY = 'wolfenstein-loadouts';
|
||||
|
||||
export default class WolfensteinGame extends Phaser.Scene {
|
||||
constructor() { super('WolfensteinGame'); }
|
||||
|
|
@ -159,20 +167,23 @@ export default class WolfensteinGame extends Phaser.Scene {
|
|||
|
||||
// `carry` is optional — the loadout (weapons/weapon/ammo/health, but
|
||||
// deliberately never keys — see Logic.createState's own doc comment) to
|
||||
// resume with instead of a fresh campaign start. Passed explicitly by
|
||||
// onNext (this._extractCarry of the just-won state) and by _retry
|
||||
// (replaying whatever this SAME mission attempt itself started with,
|
||||
// stored on `this.meta.carry` below — not the failed attempt's live
|
||||
// state, which may have less health/ammo than it started with). Any
|
||||
// other call — picking a mission straight off the campaign list, or a
|
||||
// fresh "New Game" — passes nothing, which _freshCampaignCarry resolves
|
||||
// to fists-only/full-health, matching "starting a new campaign begins
|
||||
// with just fists."
|
||||
// resume with instead of a fresh unlocked-mission start. Passed
|
||||
// explicitly only by _retry (replaying whatever this SAME mission
|
||||
// attempt itself started with, stored on `this.meta.carry` below, with
|
||||
// ammo topped back up to full — not the failed attempt's live state,
|
||||
// which may have less health/ammo than it started with, and not the
|
||||
// exact carried-in ammo either, per Brian's "die → come back full ammo"
|
||||
// spec). Every other call — "Next Mission", picking a mission straight
|
||||
// off the campaign list, or a fresh "New Game" — passes nothing, which
|
||||
// _carryForUnlockedStart resolves to "whatever weapons this mission's
|
||||
// player had the moment it was unlocked, full ammo, full health" (mission
|
||||
// 0 of a campaign has never been unlocked by anyone, so that's always
|
||||
// fists-only, matching "starting a new campaign begins with just fists").
|
||||
async startMission(campaignId, missionIndex, carry = null) {
|
||||
const camp = this.campaigns.campaigns.find((c) => c.id === campaignId);
|
||||
const mission = camp.missions[missionIndex];
|
||||
const levelJson = await this._loadLevelJson(`wolfenstein-level-${mission.id}`, `assets/gamedata/wolfenstein/${mission.levelFile}`);
|
||||
this._beginLevel(levelJson, { mode: 'campaign', campaignId, missionIndex, carry: carry ?? this._freshCampaignCarry() });
|
||||
this._beginLevel(levelJson, { mode: 'campaign', campaignId, missionIndex, carry: carry ?? this._carryForUnlockedStart(campaignId, missionIndex) });
|
||||
}
|
||||
|
||||
_loadLevelJson(key, path) {
|
||||
|
|
@ -184,9 +195,21 @@ export default class WolfensteinGame extends Phaser.Scene {
|
|||
});
|
||||
}
|
||||
|
||||
/** Fresh-campaign starting loadout: fists only, no ammo, full health — see startMission's doc comment. */
|
||||
_freshCampaignCarry() {
|
||||
return { weapons: ['fists'], weapon: 'fists', ammo: {}, health: this.rules.constants.playerMaxHealth };
|
||||
/**
|
||||
* Starting loadout for a mission the player hasn't begun an attempt at
|
||||
* yet: the weapons owned at the moment it was unlocked (see
|
||||
* _loadoutForMission/_recordMissionCleared), topped up to full ammo and
|
||||
* full health rather than whatever exact amounts were left over — see
|
||||
* startMission's doc comment.
|
||||
*/
|
||||
_carryForUnlockedStart(campaignId, missionIndex) {
|
||||
const weapons = this._loadoutForMission(campaignId, missionIndex);
|
||||
return {
|
||||
weapons: weapons.slice(),
|
||||
weapon: weapons[weapons.length - 1],
|
||||
ammo: this._fullAmmoFor(weapons),
|
||||
health: this.rules.constants.playerMaxHealth,
|
||||
};
|
||||
}
|
||||
|
||||
/** Extracts a carry-over loadout from a live player object (see startMission's `carry` param). */
|
||||
|
|
@ -194,6 +217,17 @@ export default class WolfensteinGame extends Phaser.Scene {
|
|||
return { weapons: p.weapons.slice(), weapon: p.weapon, ammo: { ...p.ammo }, health: p.health };
|
||||
}
|
||||
|
||||
/** Full (maxAmmo) reserves for every ammo-consuming weapon in `weapons`. */
|
||||
_fullAmmoFor(weapons) {
|
||||
const ammo = {};
|
||||
for (const wid of weapons) {
|
||||
const w = this.rules.weaponById[wid];
|
||||
const at = w?.ammoType ? this.rules.ammoTypeById[w.ammoType] : null;
|
||||
if (at) ammo[at.id] = at.maxAmmo;
|
||||
}
|
||||
return ammo;
|
||||
}
|
||||
|
||||
_beginLevel(levelJson, meta) {
|
||||
const model = Logic.buildLevelModel(levelJson);
|
||||
this.state = Logic.createState(model, this.rules, meta.carry ?? null);
|
||||
|
|
@ -344,14 +378,18 @@ export default class WolfensteinGame extends Phaser.Scene {
|
|||
let hasNext = false;
|
||||
if (this.meta?.mode === 'campaign') {
|
||||
const camp = this.campaigns.campaigns.find((c) => c.id === this.meta.campaignId);
|
||||
this._recordMissionCleared(this.meta.campaignId, this.meta.missionIndex);
|
||||
this._recordMissionCleared(this.meta.campaignId, this.meta.missionIndex, this._extractCarry(this.state.player).weapons);
|
||||
hasNext = this.meta.missionIndex + 1 < camp.missions.length;
|
||||
this.clearSave();
|
||||
}
|
||||
this.swapScreen(Screens.resultScreen(this, {
|
||||
won: true, missionName: this.state.levelMeta.name, hasNext,
|
||||
onRetry: () => this._retry(),
|
||||
onNext: () => this.startMission(this.meta.campaignId, this.meta.missionIndex + 1, this._extractCarry(this.state.player)),
|
||||
// No explicit carry — the loadout just recorded by _recordMissionCleared
|
||||
// above is exactly the weapons this mission was unlocked with, so
|
||||
// startMission's own _carryForUnlockedStart default (full ammo, full
|
||||
// health) applies here the same as picking this mission off the list.
|
||||
onNext: () => this.startMission(this.meta.campaignId, this.meta.missionIndex + 1),
|
||||
onMenu: () => { this._teardownLevel(); this.showMainMenu(); },
|
||||
}));
|
||||
}
|
||||
|
|
@ -374,8 +412,15 @@ export default class WolfensteinGame extends Phaser.Scene {
|
|||
// (this.meta.carry, set when it began — see startMission/_resumeState),
|
||||
// not the just-died state, which may hold less health/ammo than it did
|
||||
// at the start (and, for a mid-level pickup since then, more weapons —
|
||||
// both wrong to hand back on a failed attempt).
|
||||
if (this.meta?.mode === 'campaign') this.startMission(this.meta.campaignId, this.meta.missionIndex, this.meta.carry);
|
||||
// both wrong to hand back on a failed attempt). Ammo is topped back up
|
||||
// to full rather than reused verbatim, per Brian's "die → come back
|
||||
// with full ammo" spec — same top-up startMission's own
|
||||
// _carryForUnlockedStart default applies for a fresh unlocked-mission
|
||||
// start, just applied on top of this attempt's own weapons/health.
|
||||
if (this.meta?.mode === 'campaign') {
|
||||
const carry = { ...this.meta.carry, ammo: this._fullAmmoFor(this.meta.carry.weapons) };
|
||||
this.startMission(this.meta.campaignId, this.meta.missionIndex, carry);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- HUD
|
||||
|
|
@ -578,10 +623,35 @@ export default class WolfensteinGame extends Phaser.Scene {
|
|||
|
||||
_readProgress() { try { return JSON.parse(window.localStorage.getItem(PROGRESS_KEY)) ?? {}; } catch { return {}; } }
|
||||
_writeProgress(obj) { try { window.localStorage.setItem(PROGRESS_KEY, JSON.stringify(obj)); } catch { /* storage may be unavailable */ } }
|
||||
_recordMissionCleared(campaignId, missionIndex) {
|
||||
/**
|
||||
* `unlockedWeapons`, if given, is the weapons the player held the instant
|
||||
* `missionIndex` was cleared — i.e. exactly what the NEXT mission
|
||||
* (`missionIndex + 1`) should be considered unlocked with. Stored keyed
|
||||
* by the mission it applies to (not the one that was just cleared) so
|
||||
* `_loadoutForMission` can look it up directly. Only ever moves a given
|
||||
* mission's recorded loadout forward monotonically (`Math.max`-style, via
|
||||
* cleared-count comparison) — replaying an earlier mission and losing
|
||||
* doesn't downgrade a later mission's already-recorded unlock loadout.
|
||||
*/
|
||||
_recordMissionCleared(campaignId, missionIndex, unlockedWeapons) {
|
||||
const prog = this._readProgress();
|
||||
prog[campaignId] = Math.max(prog[campaignId] ?? 0, missionIndex + 1);
|
||||
const wasCleared = prog[campaignId] ?? 0;
|
||||
prog[campaignId] = Math.max(wasCleared, missionIndex + 1);
|
||||
this._writeProgress(prog);
|
||||
if (unlockedWeapons && missionIndex + 1 > wasCleared) {
|
||||
const loadouts = this._readLoadouts();
|
||||
loadouts[campaignId] = loadouts[campaignId] ?? {};
|
||||
loadouts[campaignId][missionIndex + 1] = unlockedWeapons.slice();
|
||||
this._writeLoadouts(loadouts);
|
||||
}
|
||||
}
|
||||
|
||||
_readLoadouts() { try { return JSON.parse(window.localStorage.getItem(LOADOUTS_KEY)) ?? {}; } catch { return {}; } }
|
||||
_writeLoadouts(obj) { try { window.localStorage.setItem(LOADOUTS_KEY, JSON.stringify(obj)); } catch { /* storage may be unavailable */ } }
|
||||
/** Weapons owned at the moment `missionIndex` was unlocked — see startMission/_carryForUnlockedStart. */
|
||||
_loadoutForMission(campaignId, missionIndex) {
|
||||
if (missionIndex === 0) return ['fists'];
|
||||
return this._readLoadouts()[campaignId]?.[missionIndex] ?? ['fists'];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- save/load
|
||||
|
|
|
|||
Loading…
Reference in New Issue