Files
noname-shuying/mengsan/runtime/skill-teardown.js
T
2026-09-28 20:58:29 +08:00

148 lines
6.5 KiB
JavaScript

// Candidate-only engine-derived release. Never replaces Player.prototype or lib.skill.
// Restricted: additional/fixed/superCharlotte skills require a separate audited adapter.
export async function removeBattleSkill(player, skill, {lib,game,get,_status}, wholePlayer = false) {
const names = Array.isArray(skill) ? skill.slice() : [skill];
for (const name of names) {
if (lib.skill[name]?.fixed || lib.skill[name]?.superCharlotte) throw new Error("Unsupported protected skill: " + name);
}
if (!wholePlayer && Object.values(player.additionalSkills || {}).some(list => list?.length)) throw new Error("Additional skill release adapter required");
const remove = async function removeSkill(skill) {
if (!skill) {
return;
}
_status.event.clearStepCache();
if (Array.isArray(skill)) {
for (var i = 0; i < skill.length; i++) {
await removeSkill.call(this, skill[i]);
}
} else {
if (skill === "counttrigger") {
this.stat[this.stat.length - 1].triggerSkill = {};
return;
} else {
var info = lib.skill[skill];
if (info?.fixed && arguments[1] !== true) {
return skill;
}
this.unmarkSkill(skill);
game.broadcastAll(
function(player, skill2) {
player.skills.remove(skill2);
player.hiddenSkills.remove(skill2);
player.invisibleSkills.remove(skill2);
delete player.tempSkills[skill2];
for (var i2 in player.additionalSkills) {
player.additionalSkills[i2].remove(skill2);
}
},
this,
skill
);
this.checkConflict(skill);
if (info) {
if (info.onremove) {
if (typeof info.onremove == "function") {
await info.onremove(this, skill);
} else if (typeof info.onremove == "string") {
if (info.onremove == "storage") {
delete this.storage[skill];
} else {
var cards = this.storage[skill];
if (get.itemtype(cards) == "card") {
cards = [cards];
}
if (get.itemtype(cards) == "cards") {
if (this.onremove == "discard") {
this.$throw(cards);
}
if (this.onremove == "discard" || this.onremove == "lose") {
game.cardsDiscard(cards);
delete this.storage[skill];
}
}
}
} else if (Array.isArray(info.onremove)) {
for (var i = 0; i < info.onremove.length; i++) {
delete this.storage[info.onremove[i]];
}
} else if (info.onremove === true) {
delete this.storage[skill];
}
}
this.removeSkillTrigger(skill);
if (!wholePlayer && !info.keepSkill) {
this.removeAdditionalSkills(skill);
}
}
this.enableSkill(skill + "_awake");
game.callHook("removeSkillCheck", [skill, this]);
}
}
return skill;
};
return remove.call(player, skill);
}
// Must be called after gameplay root drained, outside all engine event bodies.
export async function runSkillTeardown(releases, {lib,_status}) {
if (_status.eventManager.eventStack.length || _status.eventManager.tempEvent) throw new Error("Gameplay must drain before skill release");
if (!Array.isArray(releases) || releases.some(fn => typeof fn !== "function")) throw new TypeError("Explicit release callbacks required");
const previous = _status.eventManager.rootEvent;
const root = new lib.element.GameEvent("mengsanSkillTeardown", false, _status.eventManager);
let releaseError;
root.setContent(async () => {
try {
for (const release of releases) {
await release();
await root.waitNext();
}
} catch (error) {
releaseError = error;
// Let the engine drain any queued work and pop this root normally.
}
});
// Natural start completion includes next/after and native stack pop. Failure stays fail-closed.
await root.start();
if (_status.eventManager.eventStack.includes(root)) throw new Error("Teardown event still active");
if (_status.eventManager.rootEvent === root) _status.eventManager.rootEvent = previous;
if (releaseError) throw releaseError;
}
// Whole-player disposal differs from removing one source during gameplay: every source is ending.
// Each callback runs at most once; failed/partially executed teardown is NOT replayed blindly.
export function createPlayerTeardown(players, env) {
const captured = players.slice();
let operation;
const names = player => [...new Set([
...(player.skills || []), ...(player.hiddenSkills || []), ...(player.invisibleSkills || []),
...Object.keys(player.tempSkills || {}),
...Object.values(player.additionalSkills || {}).flatMap(list => typeof list === "string" ? [list] : list || []),
])];
return () => {
if (operation) return operation;
operation = (async () => {
// Preflight ALL players before modifying any skill.
for (const player of captured) for (const name of names(player)) {
if (env.lib.skill[name]?.fixed || env.lib.skill[name]?.superCharlotte) throw new Error("Protected skill requires adapter: " + name);
if (Object.values(player.additionalSkills || {}).some(value => !Array.isArray(value))) throw new Error("Unsupported additional skill source shape");
}
await runSkillTeardown(captured.map(player => async () => {
const released = new Set();
while (names(player).length) {
if (released.size >= 128) throw new Error("Skill teardown exceeded bounded expansion");
const name = names(player)[0];
if (released.has(name)) throw new Error("Released skill was re-added: " + name);
await removeBattleSkill(player, name, env, true);
released.add(name);
// Children can add skills. Drain them before taking the next inventory.
await env._status.event.waitNext();
}
for (const [key,list] of Object.entries(player.additionalSkills || {})) {
if (Array.isArray(list) && !list.length) delete player.additionalSkills[key];
}
}), env);
})();
return operation;
};
}