fixed # 修复梦三战斗异常及退场技能清理

This commit is contained in:
2026-10-13 10:42:21 +08:00
parent 28f70964ca
commit 9f485a1401
7 changed files with 603 additions and 21 deletions
+3
View File
@@ -92,6 +92,9 @@ export function createBattleFlow({settlement, chooseReward, chooseDefeat, showMa
return Object.freeze({
get state() { return state; },
get error() { return error; },
get settlementStatus() {
return { committed: !!job?.committed, phase };
},
async enterBattle({session, run, node, encounter, start}) {
if (state !== "map") throw new Error("Map handoff not ready");
const saved = settlement.readRun();
+91 -13
View File
@@ -1,9 +1,16 @@
// Candidate-only engine-derived release. Never replaces Player.prototype or lib.skill.
// Restricted: additional/fixed/superCharlotte skills require a separate audited adapter.
// 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 (lib.skill[name]?.fixed || lib.skill[name]?.superCharlotte) throw new Error("Unsupported protected skill: " + name);
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) {
@@ -21,7 +28,7 @@ export async function removeBattleSkill(player, skill, {lib,game,get,_status}, w
return;
} else {
var info = lib.skill[skill];
if (info?.fixed && arguments[1] !== true) {
if (info?.fixed && !wholePlayer) {
return skill;
}
this.unmarkSkill(skill);
@@ -52,11 +59,11 @@ export async function removeBattleSkill(player, skill, {lib,game,get,_status}, w
cards = [cards];
}
if (get.itemtype(cards) == "cards") {
if (this.onremove == "discard") {
if (info.onremove == "discard") {
this.$throw(cards);
}
if (this.onremove == "discard" || this.onremove == "lose") {
game.cardsDiscard(cards);
if (info.onremove == "discard" || info.onremove == "lose") {
await game.cardsDiscard(cards);
delete this.storage[skill];
}
}
@@ -89,7 +96,7 @@ export async function runSkillTeardown(releases, {lib,_status}) {
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;
let releaseError, releaseFailed = false;
root.setContent(async () => {
try {
for (const release of releases) {
@@ -97,6 +104,7 @@ export async function runSkillTeardown(releases, {lib,_status}) {
await root.waitNext();
}
} catch (error) {
releaseFailed = true;
releaseError = error;
// Let the engine drain any queued work and pop this root normally.
}
@@ -105,16 +113,18 @@ export async function runSkillTeardown(releases, {lib,_status}) {
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;
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 || []),
])];
@@ -122,18 +132,86 @@ export function createPlayerTeardown(players, env) {
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);
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();
while (names(player).length) {
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 = names(player)[0];
const name = candidates()[0];
if (released.has(name)) throw new Error("Released skill was re-added: " + name);
await removeBattleSkill(player, name, env, true);
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();
}