mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-21 06:39:59 +08:00
226 lines
11 KiB
JavaScript
226 lines
11 KiB
JavaScript
// Candidate-only engine-derived release. Never replaces Player.prototype or lib.skill.
|
|
// Protected gameplay skills stay guarded; whole-player disposal owns every source.
|
|
import { getDiagnostics } from "../../diagnostics/index.js";
|
|
|
|
const markerFields = new Set(["fixed", "superCharlotte", "charlotte", "forced",
|
|
"unique", "popup", "silent", "ai", "skill_id", "_priority", "priority",
|
|
"locked", "nopop"]);
|
|
const protectedMarker = info => Boolean((info?.fixed || info?.superCharlotte) &&
|
|
Reflect.ownKeys(info).every(key => markerFields.has(key)));
|
|
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 (!wholePlayer && (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 && !wholePlayer) {
|
|
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 (info.onremove == "discard") {
|
|
this.$throw(cards);
|
|
}
|
|
if (info.onremove == "discard" || info.onremove == "lose") {
|
|
await 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, releaseFailed = false;
|
|
root.setContent(async () => {
|
|
try {
|
|
for (const release of releases) {
|
|
await release();
|
|
await root.waitNext();
|
|
}
|
|
} catch (error) {
|
|
releaseFailed = true;
|
|
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 (releaseFailed) 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();
|
|
const diagnostics = getDiagnostics().scope("mengsan.skillTeardown");
|
|
let operation;
|
|
const names = player => [...new Set([
|
|
...(player.skills || []), ...(player.hiddenSkills || []), ...(player.invisibleSkills || []),
|
|
...(player.initedSkills || []),
|
|
...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) {
|
|
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();
|
|
const retained = new Set();
|
|
const equipmentSkills = new Set();
|
|
for (const card of player.getVCards?.("e") || []) {
|
|
const context = { playerId: player.playerid,
|
|
character: player.name, cardName: card.name };
|
|
try {
|
|
const skills = env.get.info(card, false)?.skills || [];
|
|
for (const name of skills) equipmentSkills.add(name);
|
|
const root = env._status.event;
|
|
const existing = new Set(root.next);
|
|
player.removeEquipTrigger(card);
|
|
let callbackError, callbackFailed = false;
|
|
const guarded = new WeakSet();
|
|
const guard = child => {
|
|
if (guarded.has(child)) return;
|
|
guarded.add(child);
|
|
const content = child.content;
|
|
child.content = async event => {
|
|
try { await Reflect.apply(content, child, [event]); }
|
|
catch (error) {
|
|
if (!callbackFailed) callbackError = error;
|
|
callbackFailed = true;
|
|
event.finish();
|
|
}
|
|
};
|
|
const waitNext = child.waitNext;
|
|
child.waitNext = function (...args) {
|
|
for (const next of [...this.next, ...this.after]) {
|
|
guard(next);
|
|
}
|
|
return Reflect.apply(waitNext, this, args);
|
|
};
|
|
};
|
|
for (const child of root.next) {
|
|
if (!existing.has(child)) guard(child);
|
|
}
|
|
await root.waitNext();
|
|
if (callbackFailed) throw callbackError;
|
|
diagnostics.info("equipment.released", context);
|
|
} catch (error) {
|
|
diagnostics.error("equipment.release.failed", error, context);
|
|
throw error;
|
|
}
|
|
}
|
|
const candidates = () => [...new Set([
|
|
...names(player), ...equipmentSkills,
|
|
])].filter(name => {
|
|
const info = env.lib.skill[name];
|
|
if (!protectedMarker(info)) return true;
|
|
if (!retained.has(name)) {
|
|
retained.add(name);
|
|
diagnostics.info("skill.retained", { skill: name,
|
|
playerId: player.playerid, character: player.name,
|
|
fixed: !!info.fixed, superCharlotte: !!info.superCharlotte,
|
|
reason: "metadataOnly" });
|
|
}
|
|
return false;
|
|
});
|
|
while (candidates().length) {
|
|
if (released.size >= 128) throw new Error("Skill teardown exceeded bounded expansion");
|
|
const name = candidates()[0];
|
|
if (released.has(name)) throw new Error("Released skill was re-added: " + name);
|
|
const info = env.lib.skill[name];
|
|
const context = { skill: name, playerId: player.playerid,
|
|
character: player.name, fixed: !!info?.fixed,
|
|
superCharlotte: !!info?.superCharlotte };
|
|
try {
|
|
await removeBattleSkill(player, name, env, true);
|
|
diagnostics.info("skill.released", context);
|
|
} catch (error) {
|
|
diagnostics.error("skill.release.failed", error, context);
|
|
throw error;
|
|
}
|
|
released.add(name);
|
|
equipmentSkills.delete(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;
|
|
};
|
|
}
|