537 lines
20 KiB
JavaScript
537 lines
20 KiB
JavaScript
import { readFile } from 'node:fs/promises';
|
|
import { createServer } from 'vite';
|
|
|
|
const browserStorage = createStorage();
|
|
globalThis.window = { localStorage: browserStorage };
|
|
|
|
const server = await createServer({
|
|
logLevel: 'error',
|
|
server: { middlewareMode: true, hmr: false },
|
|
appType: 'custom'
|
|
});
|
|
|
|
try {
|
|
const {
|
|
battleSaveStorageBaseKey,
|
|
battleSaveStorageSlotKey,
|
|
campaignBattleResumeStorageKeys,
|
|
clearAllCampaignBattleSaves,
|
|
clearCampaignBattleSavesForSlot,
|
|
clearCampaignBattleResume,
|
|
legacyFirstBattleSaveStorageKey,
|
|
readBattleSaveStorageCandidate,
|
|
readCampaignBattleResume
|
|
} = await server.ssrLoadModule('/src/game/state/battleSaveStorage.ts');
|
|
const {
|
|
createInitialCampaignState,
|
|
resetCampaignState,
|
|
setCampaignState,
|
|
startNewCampaign
|
|
} = await server.ssrLoadModule('/src/game/state/campaignState.ts');
|
|
|
|
const firstBattleId = 'first-battle-zhuo-commandery';
|
|
const campaign = { step: 'first-battle', activeSaveSlot: 2 };
|
|
const storage = createStorage();
|
|
const slotKey = battleSaveStorageSlotKey(firstBattleId, 2);
|
|
storage.setItem(slotKey, JSON.stringify(createBattleSave({ turnNumber: 4, activeFaction: 'enemy' })));
|
|
const directResume = readCampaignBattleResume(campaign, 2, storage);
|
|
assert(
|
|
directResume?.slot === 2 &&
|
|
directResume.battleId === firstBattleId &&
|
|
directResume.turnNumber === 4 &&
|
|
directResume.activeFaction === 'enemy' &&
|
|
directResume.storageKey === slotKey,
|
|
`Expected a matching slotted battle save to become a direct-resume candidate: ${JSON.stringify(directResume)}`
|
|
);
|
|
|
|
const generationCampaign = {
|
|
...campaign,
|
|
battleResumeGeneration: 4
|
|
};
|
|
storage.setItem(
|
|
slotKey,
|
|
JSON.stringify(
|
|
createBattleSave({
|
|
battleResumeGeneration: 3
|
|
})
|
|
)
|
|
);
|
|
assert(
|
|
readCampaignBattleResume(
|
|
generationCampaign,
|
|
2,
|
|
storage
|
|
) === undefined &&
|
|
storage.getItem(slotKey) === null,
|
|
'A superseded battle-save generation must be rejected and cleaned.'
|
|
);
|
|
storage.setItem(
|
|
slotKey,
|
|
JSON.stringify(
|
|
createBattleSave({
|
|
battleResumeGeneration: 4,
|
|
turnNumber: 6
|
|
})
|
|
)
|
|
);
|
|
assert(
|
|
readCampaignBattleResume(
|
|
generationCampaign,
|
|
2,
|
|
storage
|
|
)?.turnNumber === 6,
|
|
'A battle save from the current generation must remain resumable.'
|
|
);
|
|
|
|
const stickyStorage = createStorage({
|
|
refuseRemoval: true
|
|
});
|
|
stickyStorage.setItem(
|
|
slotKey,
|
|
JSON.stringify(
|
|
createBattleSave({
|
|
battleResumeGeneration: 3
|
|
})
|
|
)
|
|
);
|
|
assert(
|
|
readCampaignBattleResume(
|
|
generationCampaign,
|
|
2,
|
|
stickyStorage
|
|
) === undefined &&
|
|
stickyStorage.getItem(slotKey) !== null,
|
|
'A generation mismatch must remain logically blocked when storage refuses physical deletion.'
|
|
);
|
|
|
|
storage.setItem(
|
|
slotKey,
|
|
JSON.stringify(createBattleSave())
|
|
);
|
|
assert(
|
|
readCampaignBattleResume(
|
|
generationCampaign,
|
|
2,
|
|
storage
|
|
) === undefined,
|
|
'A legacy generation-zero battle save must not cross a later battle launch generation.'
|
|
);
|
|
|
|
storage.setItem(slotKey, '{broken-json');
|
|
assert(readCampaignBattleResume(campaign, 2, storage) === undefined, 'Broken JSON must not become a resume candidate.');
|
|
assert(storage.getItem(slotKey) === null, 'Broken slotted battle saves must be removed after validation.');
|
|
|
|
storage.setItem(slotKey, JSON.stringify(createBattleSave({ campaignStep: 'second-battle' })));
|
|
assert(readCampaignBattleResume(campaign, 2, storage) === undefined, 'A stale campaign step must not resume another battle flow.');
|
|
assert(storage.getItem(slotKey) === null, 'A stale campaign-step save must be cleaned safely.');
|
|
|
|
const slotOneCampaign = { step: 'first-battle', activeSaveSlot: 1 };
|
|
const slotOneKey = battleSaveStorageSlotKey(firstBattleId, 1);
|
|
const baseKey = battleSaveStorageBaseKey(firstBattleId);
|
|
const generationFilteredStorage = createStorage({
|
|
refuseRemoval: true
|
|
});
|
|
generationFilteredStorage.setItem(
|
|
slotOneKey,
|
|
JSON.stringify(
|
|
createBattleSave({
|
|
battleResumeGeneration: 3,
|
|
turnNumber: 8
|
|
})
|
|
)
|
|
);
|
|
generationFilteredStorage.setItem(
|
|
baseKey,
|
|
JSON.stringify(
|
|
createBattleSave({
|
|
battleResumeGeneration: 4,
|
|
turnNumber: 5
|
|
})
|
|
)
|
|
);
|
|
const generationFilteredCandidate =
|
|
readBattleSaveStorageCandidate(
|
|
firstBattleId,
|
|
'first-battle',
|
|
1,
|
|
generationFilteredStorage,
|
|
3,
|
|
{},
|
|
{
|
|
...slotOneCampaign,
|
|
battleResumeGeneration: 4
|
|
}
|
|
);
|
|
assert(
|
|
generationFilteredCandidate
|
|
?.storageKey === baseKey &&
|
|
generationFilteredCandidate.state
|
|
.turnNumber === 5 &&
|
|
generationFilteredStorage.getItem(
|
|
slotOneKey
|
|
) !== null,
|
|
`A direct BattleScene-style read must reject a stale generation and continue to a current fallback even when physical deletion is refused: ${JSON.stringify(generationFilteredCandidate)}`
|
|
);
|
|
storage.setItem(slotOneKey, 'corrupted-slot-copy');
|
|
storage.setItem(baseKey, JSON.stringify(createBattleSave({ turnNumber: 3 })));
|
|
const fallbackResume = readCampaignBattleResume(slotOneCampaign, 1, storage);
|
|
assert(
|
|
fallbackResume?.storageKey === baseKey && fallbackResume.turnNumber === 3 && storage.getItem(slotOneKey) === null,
|
|
`A corrupt primary key must be removed before falling back to the compatible base key: ${JSON.stringify(fallbackResume)}`
|
|
);
|
|
|
|
storage.clear();
|
|
storage.setItem(slotOneKey, JSON.stringify(createBattleSave({
|
|
units: [{ ...createBattleSave().units[0], id: 'unknown-unit' }]
|
|
})));
|
|
storage.setItem(baseKey, JSON.stringify(createBattleSave({ turnNumber: 5 })));
|
|
const fullyValidatedFallback = readBattleSaveStorageCandidate(
|
|
firstBattleId,
|
|
'first-battle',
|
|
1,
|
|
storage,
|
|
3,
|
|
{
|
|
mapWidth: 20,
|
|
mapHeight: 20,
|
|
validUnitIds: new Set(['liu-bei']),
|
|
validAllyUnitIds: new Set(['liu-bei']),
|
|
validEnemyUnitIds: new Set()
|
|
}
|
|
);
|
|
assert(
|
|
fullyValidatedFallback?.storageKey === baseKey &&
|
|
fullyValidatedFallback.state.turnNumber === 5 &&
|
|
storage.getItem(slotOneKey) === null &&
|
|
storage.getItem(baseKey) !== null,
|
|
`Full validation must delete only an invalid primary candidate before preserving and loading the valid base fallback: ${JSON.stringify(fullyValidatedFallback)}`
|
|
);
|
|
|
|
storage.clear();
|
|
const alternateCompositionSave = createBattleSave({
|
|
units: [{ ...createBattleSave().units[0], id: 'guan-yu' }]
|
|
});
|
|
storage.setItem(slotKey, JSON.stringify(alternateCompositionSave));
|
|
const looselyValidatedAlternateComposition = readBattleSaveStorageCandidate(
|
|
firstBattleId,
|
|
'first-battle',
|
|
2,
|
|
storage,
|
|
3,
|
|
{ mapWidth: 20, mapHeight: 20 }
|
|
);
|
|
assert(
|
|
looselyValidatedAlternateComposition?.state.units[0]?.id === 'guan-yu' && storage.getItem(slotKey) !== null,
|
|
'Inspecting another slot must preserve a structurally valid save until its campaign composition is loaded.'
|
|
);
|
|
|
|
storage.clear();
|
|
storage.setItem(legacyFirstBattleSaveStorageKey, JSON.stringify(createBattleSave({ turnNumber: 2 })));
|
|
const legacyResume = readCampaignBattleResume(slotOneCampaign, 1, storage);
|
|
assert(
|
|
legacyResume?.storageKey === legacyFirstBattleSaveStorageKey && legacyResume.turnNumber === 2,
|
|
`The first-battle legacy key must remain resumable for slot 1: ${JSON.stringify(legacyResume)}`
|
|
);
|
|
|
|
const keys = campaignBattleResumeStorageKeys(firstBattleId, 1);
|
|
assert(
|
|
JSON.stringify(keys) === JSON.stringify([slotOneKey, baseKey, legacyFirstBattleSaveStorageKey]),
|
|
`Resume keys must retain slotted, base, then legacy fallback order: ${JSON.stringify(keys)}`
|
|
);
|
|
assert(clearCampaignBattleResume(slotOneCampaign, 1, storage) === true, 'Explicit resume cleanup must report removed data.');
|
|
assert(storage.getItem(legacyFirstBattleSaveStorageKey) === null, 'Explicit cleanup must remove the legacy candidate.');
|
|
|
|
storage.setItem(slotOneKey, JSON.stringify(createBattleSave()));
|
|
const campProgress = { step: 'first-camp', activeSaveSlot: 1 };
|
|
assert(readCampaignBattleResume(campProgress, 1, storage) === undefined, 'Camp progress must never consume an old battle save.');
|
|
assert(storage.getItem(slotOneKey) !== null, 'Unrelated camp routing must leave battle data untouched.');
|
|
|
|
const secondBattleKey = battleSaveStorageSlotKey('second-battle-yellow-turban-pursuit', 3);
|
|
const unrelatedKey = 'heros-web:unrelated-setting';
|
|
storage.setItem(baseKey, JSON.stringify(createBattleSave()));
|
|
storage.setItem(secondBattleKey, JSON.stringify(createBattleSave({
|
|
battleId: 'second-battle-yellow-turban-pursuit',
|
|
campaignStep: 'second-battle'
|
|
})));
|
|
storage.setItem(unrelatedKey, 'keep');
|
|
const clearedCount = clearAllCampaignBattleSaves(storage);
|
|
assert(
|
|
clearedCount >= 3 &&
|
|
storage.getItem(slotOneKey) === null &&
|
|
storage.getItem(baseKey) === null &&
|
|
storage.getItem(secondBattleKey) === null &&
|
|
storage.getItem(unrelatedKey) === 'keep',
|
|
`Full cleanup must remove every campaign battle key without touching unrelated settings: ${clearedCount}`
|
|
);
|
|
|
|
browserStorage.setItem(slotOneKey, JSON.stringify(createBattleSave()));
|
|
resetCampaignState();
|
|
assert(browserStorage.getItem(slotOneKey) === null, 'Campaign reset must clear prior-generation battle saves.');
|
|
browserStorage.setItem(secondBattleKey, JSON.stringify(createBattleSave({
|
|
battleId: 'second-battle-yellow-turban-pursuit',
|
|
campaignStep: 'second-battle'
|
|
})));
|
|
browserStorage.setItem(slotOneKey, JSON.stringify(createBattleSave()));
|
|
const priorSlotOneCampaign =
|
|
createInitialCampaignState();
|
|
priorSlotOneCampaign.step = 'first-battle';
|
|
priorSlotOneCampaign.activeSaveSlot = 1;
|
|
priorSlotOneCampaign.battleResumeGeneration = 7;
|
|
setCampaignState(priorSlotOneCampaign);
|
|
const newCampaign = startNewCampaign();
|
|
assert(
|
|
browserStorage.getItem(slotOneKey) === null && browserStorage.getItem(secondBattleKey) !== null,
|
|
'Starting a new campaign must clear slot 1 battle data without deleting resumable saves from preserved campaign slots.'
|
|
);
|
|
assert(
|
|
newCampaign.battleResumeGeneration === 8,
|
|
`A replacement campaign must advance beyond the overwritten slot generation before best-effort battle cleanup: ${newCampaign.battleResumeGeneration}`
|
|
);
|
|
assert(
|
|
clearCampaignBattleSavesForSlot(3, browserStorage) === 1 && browserStorage.getItem(secondBattleKey) === null,
|
|
'Slot-scoped cleanup must remove only battle saves belonging to the requested campaign slot.'
|
|
);
|
|
|
|
storage.clear();
|
|
const slotTwoFirstBattleKey = battleSaveStorageSlotKey(firstBattleId, 2);
|
|
const slotTwoSecondBattleKey = battleSaveStorageSlotKey('second-battle-yellow-turban-pursuit', 2);
|
|
storage.setItem(slotTwoFirstBattleKey, JSON.stringify(createBattleSave({ turnNumber: 8 })));
|
|
storage.setItem(slotTwoSecondBattleKey, JSON.stringify(createBattleSave({
|
|
battleId: 'second-battle-yellow-turban-pursuit',
|
|
campaignStep: 'second-battle',
|
|
turnNumber: 9
|
|
})));
|
|
clearCampaignBattleSavesForSlot(2, storage);
|
|
storage.setItem(slotTwoFirstBattleKey, JSON.stringify(createBattleSave({ turnNumber: 2 })));
|
|
assert(
|
|
readCampaignBattleResume({ step: 'first-battle', activeSaveSlot: 2 }, 2, storage)?.turnNumber === 2 &&
|
|
readCampaignBattleResume({ step: 'second-battle', activeSaveSlot: 2 }, 2, storage) === undefined &&
|
|
storage.getItem(slotTwoSecondBattleKey) === null,
|
|
'Replacing a battle slot must preserve only the newly written current battle and remove stale later-battle resumes.'
|
|
);
|
|
|
|
storage.clear();
|
|
storage.setItem(slotOneKey, JSON.stringify(createBattleSave({ turnNumber: 7 })));
|
|
storage.setItem(baseKey, JSON.stringify(createBattleSave({ turnNumber: 7 })));
|
|
storage.setItem(legacyFirstBattleSaveStorageKey, JSON.stringify(createBattleSave({ turnNumber: 7 })));
|
|
clearCampaignBattleSavesForSlot(1, storage);
|
|
storage.setItem(slotOneKey, JSON.stringify(createBattleSave({ turnNumber: 2 })));
|
|
storage.setItem(baseKey, JSON.stringify(createBattleSave({ turnNumber: 2 })));
|
|
assert(
|
|
storage.getItem(legacyFirstBattleSaveStorageKey) === null &&
|
|
readCampaignBattleResume(slotOneCampaign, 1, storage)?.turnNumber === 2,
|
|
'Replacing slot 1 must remove its legacy generation before writing matching slot and base compatibility copies.'
|
|
);
|
|
|
|
const battleSceneSource = await readFile(new URL('../src/game/scenes/BattleScene.ts', import.meta.url), 'utf8');
|
|
const campSceneSource = await readFile(new URL('../src/game/scenes/CampScene.ts', import.meta.url), 'utf8');
|
|
const campManualSaveSource = methodSource(
|
|
campSceneSource,
|
|
'private saveCampToSlot(',
|
|
'private handleSaveSlotKey('
|
|
);
|
|
const battleManualSaveSource = methodSource(
|
|
battleSceneSource,
|
|
'private saveManualBattleState(',
|
|
'private loadManualBattleState('
|
|
);
|
|
const battleManualLoadSource = methodSource(
|
|
battleSceneSource,
|
|
'private loadManualBattleState(',
|
|
'private restartBattleFromSave('
|
|
);
|
|
const compatibilityBattleSaveSource = methodSource(
|
|
battleSceneSource,
|
|
'private saveBattleState(',
|
|
'private loadBattleState('
|
|
);
|
|
assert(
|
|
/resumeBattleSaveSlot\?: number/.test(battleSceneSource) &&
|
|
/this\.time\.delayedCall\(0, \(\) => this\.loadBattleState\(resumeSlot\)\)/.test(battleSceneSource),
|
|
'BattleScene must consume the title resume slot after the map and unit views are ready.'
|
|
);
|
|
assert(
|
|
/clearBattleSaveStorageForBattle\(battleScenario\.id, getCampaignState\(\)\.activeSaveSlot\)/.test(battleSceneSource),
|
|
'A victory must clear its resumable battle save from the active campaign slot.'
|
|
);
|
|
assert(
|
|
/this\.activeFaction === 'enemy'[\s\S]*?void this\.runEnemyTurn\(\)/.test(battleSceneSource),
|
|
'Resuming an enemy-turn save must restart enemy actions instead of leaving the battle stalled.'
|
|
);
|
|
assert(
|
|
/restartBattleFromSave\(slot: number\)[\s\S]*?loadCampaignState\(normalizedSlot\)[\s\S]*?this\.scene\.restart\([\s\S]*?resumeBattleSaveSlot: normalizedSlot/.test(battleSceneSource) &&
|
|
/readBattleSaveState\(slot: number, validateCurrentComposition = false\)/.test(battleSceneSource) &&
|
|
/readBattleSaveState\(slot, true\)/.test(battleSceneSource),
|
|
'Cross-slot loads must restart after loading the target campaign, then validate against the rebuilt battle composition.'
|
|
);
|
|
assert(
|
|
/writeManualSave\(slot,\s*\{\s*campaign: getCampaignState\(\),\s*context: 'camp'\s*\}\)/.test(
|
|
campManualSaveSource
|
|
) &&
|
|
!campManualSaveSource.includes(
|
|
'clearCampaignBattleSavesForSlot'
|
|
) &&
|
|
!campManualSaveSource.includes(
|
|
'saveCampaignState('
|
|
),
|
|
'Camp user saves must write a protected manual record without changing the active autosave campaign or clearing battle resumes.'
|
|
);
|
|
assert(
|
|
/writeManualSave\(normalizedSlot,\s*\{\s*campaign: getCampaignState\(\),\s*battle: state,\s*context: 'battle'\s*\}\)/.test(
|
|
battleManualSaveSource
|
|
) &&
|
|
!battleManualSaveSource.includes(
|
|
'clearCampaignBattleSavesForSlot'
|
|
) &&
|
|
!battleManualSaveSource.includes(
|
|
'saveCampaignState('
|
|
),
|
|
'Battle user saves must protect campaign and battle in one manual envelope without mutating the active autosave lane.'
|
|
);
|
|
assert(
|
|
/readManualBattleSaveState\(slot, true\)/.test(
|
|
battleManualLoadSource
|
|
) &&
|
|
/restoreManualSave\(slot\)/.test(
|
|
battleManualLoadSource
|
|
) &&
|
|
/restoredCampaign\.activeSaveSlot/.test(
|
|
battleManualLoadSource
|
|
) &&
|
|
/readBattleSaveState\(resumeSlot, true\)/.test(
|
|
battleManualLoadSource
|
|
) &&
|
|
/resumeBattleSaveSlot: resumeSlot/.test(
|
|
battleManualLoadSource
|
|
),
|
|
'Battle manual loads must validate the protected record, restore it into autosave, verify the staged checkpoint, and restart from that lane.'
|
|
);
|
|
assert(
|
|
/clearCampaignBattleSavesForSlot\(normalizedSlot\)/.test(
|
|
compatibilityBattleSaveSource
|
|
) &&
|
|
/localStorage\.setItem\(this\.battleSaveStorageKeyForSlot\(normalizedSlot\)/.test(
|
|
compatibilityBattleSaveSource
|
|
) &&
|
|
(
|
|
battleSceneSource.match(
|
|
/saveBattleState\(/g
|
|
)?.length ?? 0
|
|
) === 1,
|
|
'The legacy direct battle writer may remain for debug compatibility, but no player-facing path may call it.'
|
|
);
|
|
assert(
|
|
/battleResumeGeneration:\s*campaign\.battleResumeGeneration/.test(
|
|
battleSceneSource
|
|
),
|
|
'Every new battle snapshot must carry its campaign battle-resume generation.'
|
|
);
|
|
assert(
|
|
/readBattleSaveState\(slot: number, validateCurrentComposition = false\)[\s\S]*?readBattleSaveStorageCandidate\([\s\S]*?savedCampaign\s*\)/.test(
|
|
battleSceneSource
|
|
),
|
|
'BattleScene compatibility autosave loads must filter candidates through the loaded campaign generation.'
|
|
);
|
|
assert(
|
|
/isSafeBattleAutosaveCheckpoint\(\)[\s\S]*?this\.battleAutosaveReady[\s\S]*?this\.activeFaction === 'ally'[\s\S]*?this\.phase === 'idle'/.test(
|
|
battleSceneSource
|
|
) &&
|
|
/this\.applyBattleSaveState\(state\);[\s\S]*?this\.battleAutosaveReady = true;[\s\S]*?this\.scheduleFirstBattleTutorial\(\);/.test(
|
|
battleSceneSource
|
|
),
|
|
'Autosave must stay disabled until a new or restored battle is complete, and first-battle tutorial scheduling must resume with the checkpoint.'
|
|
);
|
|
assert(
|
|
/persistBattleAutosave\(reason:[\s\S]*?Object\.keys\(battleScenarios\)\.flatMap\([\s\S]*?campaignBattleResumeStorageKeys\([\s\S]*?previousStorageValues[\s\S]*?clearCampaignBattleSavesForSlot\(normalizedSlot\)[\s\S]*?catch \{[\s\S]*?restoreBattleAutosaveStorage\([\s\S]*?previousStorageValues/.test(
|
|
battleSceneSource
|
|
),
|
|
'A failed autosave write must restore every previous battle checkpoint removed from the destination campaign slot.'
|
|
);
|
|
assert(
|
|
/stats: \{ \.\.\.unit\.stats \}/.test(battleSceneSource) &&
|
|
/unit\.stats = \{ \.\.\.savedUnit\.stats \}/.test(battleSceneSource) &&
|
|
/progressUnitStatsForLevelUps\(unit\.stats, unit\.classKey, savedLevelUps\)/.test(battleSceneSource),
|
|
'Battle saves must persist new attributes and reconstruct battle-earned attributes for legacy saves.'
|
|
);
|
|
|
|
console.log('Verified direct battle-save resume routing, durable launch-generation matching, protected manual save/load separation, BattleScene generation filtering, deletion-failure blocking, monotonic new-campaign replacement, restore hooks, stale cleanup, fallback order, generation cleanup, and non-battle isolation.');
|
|
} finally {
|
|
await server.close();
|
|
}
|
|
|
|
function createStorage(options = {}) {
|
|
const values = new Map();
|
|
return {
|
|
getItem(key) {
|
|
return values.has(key) ? values.get(key) : null;
|
|
},
|
|
setItem(key, value) {
|
|
values.set(key, String(value));
|
|
},
|
|
removeItem(key) {
|
|
if (!options.refuseRemoval) {
|
|
values.delete(key);
|
|
}
|
|
},
|
|
clear() {
|
|
values.clear();
|
|
}
|
|
};
|
|
}
|
|
|
|
function createBattleSave(patch = {}) {
|
|
return {
|
|
version: 1,
|
|
battleId: 'first-battle-zhuo-commandery',
|
|
campaignStep: 'first-battle',
|
|
savedAt: '2026-07-19T03:00:00.000Z',
|
|
turnNumber: 2,
|
|
activeFaction: 'ally',
|
|
rosterTab: 'ally',
|
|
actedUnitIds: [],
|
|
attackIntents: [],
|
|
battleLog: ['전투 저장'],
|
|
units: [
|
|
{
|
|
id: 'liu-bei',
|
|
level: 1,
|
|
exp: 0,
|
|
hp: 30,
|
|
maxHp: 30,
|
|
attack: 10,
|
|
move: 4,
|
|
x: 1,
|
|
y: 1,
|
|
direction: 'south',
|
|
equipment: {
|
|
weapon: { itemId: 'training-sword', level: 1, exp: 0 },
|
|
armor: { itemId: 'cloth-armor', level: 1, exp: 0 },
|
|
accessory: { itemId: 'grain-pouch', level: 1, exp: 0 }
|
|
}
|
|
}
|
|
],
|
|
bonds: [],
|
|
...patch
|
|
};
|
|
}
|
|
|
|
function methodSource(
|
|
source,
|
|
startMarker,
|
|
endMarker
|
|
) {
|
|
const start = source.indexOf(startMarker);
|
|
const end = source.indexOf(endMarker, start + startMarker.length);
|
|
assert(
|
|
start >= 0 && end > start,
|
|
`Could not isolate method source between "${startMarker}" and "${endMarker}".`
|
|
);
|
|
return source.slice(start, end);
|
|
}
|
|
|
|
function assert(condition, message) {
|
|
if (!condition) {
|
|
throw new Error(message);
|
|
}
|
|
}
|