mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-01 17:30:04 +08:00
1176 lines
58 KiB
JavaScript
1176 lines
58 KiB
JavaScript
import { createScenarioCharacters, scenarioTranslations } from "./content/scenario-characters.js";
|
||
import { createMengsanCards } from "./cards/mode-cards.js";
|
||
import { createRelicBattle, createRelicSkills } from "./relics/battle.js";
|
||
import { grantRelic, getRelic, initializeRelicMaxHp }
|
||
from "./relics/definitions.js";
|
||
import { mountRelics } from "./ui/relics.js";
|
||
import { createOpeningEffects, createRelicCombatSkills }
|
||
from "./relics/combat.js";
|
||
import { applyRoomRelics, restRecovery } from "./relics/rooms.js";
|
||
import { buildBattlePlan, consumeSupports, createBattleDirector, grantSupport } from "./battle/battle-director.js";
|
||
import { installPersonalPiles } from "./cards/personal-piles.js";
|
||
import { createMonster } from "./monsters/monster.js";
|
||
import { prepareBondBattle } from "./bonds/state.js";
|
||
import { initializeBondUnit, recordBondDeath } from "./bonds/battle.js";
|
||
import { bondDefinitions } from "./bonds/definitions.js";
|
||
import { createBondIntentActions } from "./bonds/intents.js";
|
||
import {
|
||
createMonsterIntentActions,
|
||
getIntentHostiles,
|
||
isFlyconid,
|
||
isRaider,
|
||
} from "./monsters/actions.js";
|
||
import { mountEnemyIntent } from "./ui/intent-display.js";
|
||
import { mountEnergy } from "./ui/energy-display.js";
|
||
import { mountHandUI } from "./ui/hand/index.js";
|
||
import {
|
||
finishDeathBlow,
|
||
hasPendingDeathBlow,
|
||
} from "./battle/death-blow.js";
|
||
import {
|
||
STUN_INTENT,
|
||
applyStun,
|
||
clearStun,
|
||
finishStunnedTurn,
|
||
isStunned,
|
||
skipStunnedAction,
|
||
} from "./battle/stun-intent.js";
|
||
import { recordFlyconidAction } from "./monsters/flyconid-intent.js";
|
||
import { recordRaiderAction } from "./monsters/raider-intent.js";
|
||
import {
|
||
PLAYER_ENERGY, PLAYER_HAND_LIMIT, cardCost,
|
||
canPayCard, payCard, isActiveCardUse,
|
||
} from "./battle/combat-rules.js";
|
||
import { mountBattlePiles } from "./ui/card-library.js";
|
||
import { mountGMManager } from "./ui/gm-manager.js";
|
||
import { createPlayerTeardown } from "./battle/skill-teardown.js";
|
||
import { createBattleResources } from "./battle/battle-resources.js";
|
||
import { createModeStorage } from "./progression/mode-storage.js";
|
||
import { createBattleSession } from "./battle/battle-session.js";
|
||
import { observeFreshEvent } from "./battle/engine-session.js";
|
||
import { runPhaseLoop, stopBattleTurn } from "./battle/battle-loop.js";
|
||
import { createBattleSettlement } from "./battle/battle-settlement.js";
|
||
import { createBattleFlow, quiesceEngine } from "./battle/battle-flow.js";
|
||
import { lib, game, ui, get, _status } from "../../../noname.js";
|
||
import config from "./config.js";
|
||
import { showMap as renderMap } from "./ui/map/index.js";
|
||
import { getRandomRewardChoices } from "./progression/reward.js";
|
||
import { canAcquireCard } from "./cards/card-definitions.js";
|
||
import { addCardToDeck, createRandomCardData } from "./cards/card-data.js";
|
||
import { hasUpgradeableCard, upgradeRandomCard } from "./cards/upgrades.js";
|
||
import { chooseBattleReward } from "./ui/reward-ui.js";
|
||
import { chooseRewardPackage } from "./ui/reward-package.js";
|
||
import { applyStoryOutcome, getAvailableStoryChoices } from "./progression/story.js";
|
||
import { playDialogue } from "./ui/dialogue.js";
|
||
import { chooseButtons } from "./ui/flow-ui.js";
|
||
import { openModeSelection } from "./ui/navigation.js";
|
||
import {
|
||
completeNode,
|
||
createRun,
|
||
enterNextAct,
|
||
generateActMap,
|
||
getNodeEncounter,
|
||
insertStoryNode,
|
||
nextRandom,
|
||
} from "./progression/state.js";
|
||
|
||
const MODE_ID = config.modeId;
|
||
const STYLE_PATH = `${lib.assetURL}extension/术樱包/mengsan`;
|
||
|
||
const copy = value => JSON.parse(JSON.stringify(value));
|
||
|
||
const modeStorage = createModeStorage({lib, game, config, localStorage});
|
||
const saveRun = async run => {
|
||
const snapshot = copy(run);
|
||
while (true) {
|
||
try { await modeStorage.saveRun(snapshot); return; }
|
||
catch (error) {
|
||
await chooseButtons("存档未完成", [{id:"retry",name:"重试保存"}], "尚未提交本次结果;保留旧存档,不会重新计算奖励或扣款。");
|
||
}
|
||
}
|
||
};
|
||
const clearRun = modeStorage.clearRun;
|
||
const showMap = run => renderMap(run, { saveRun });
|
||
let battleSequence = 0;
|
||
let activeBattle = null;
|
||
const reportFlowError = error => { console.error("梦三流程暂停,未自动清档:", error); };
|
||
|
||
const chooseRun = async savedRun => {
|
||
const available = savedRun?.status === "running";
|
||
const act = available ? config.acts[savedRun.actIndex] : null;
|
||
return chooseButtons("梦三", [
|
||
{ id: "continue", name: "继续征程", disabled: !available,
|
||
description: available ? `${get.translation(savedRun.player.character)} · ${act?.name || "未知区域"} · 已完成 ${savedRun.statistics?.completedNodes || 0} 个节点` : "暂无进行中的征程" },
|
||
{ id: "new", name: "开始新征程", description: available ? "选择角色,确认后替换当前自动存档" : "选择一名武将,踏入第一关" },
|
||
], "单一自动存档 · 完成节点后记录进度", { eyebrow: "水墨行军 · 存档选择" });
|
||
};
|
||
|
||
const chooseCharacter = async () => {
|
||
const choices = config.characters.filter(name => lib.character[name]).map(name => ({
|
||
id: name,
|
||
name: get.translation(name),
|
||
description: lib.translate[`${name}_title`] || "作为本次征程的角色",
|
||
}));
|
||
return chooseButtons("选择出征武将", [...(choices.length ? choices : [{ id: "mengsan_liubei_shuying", name: "界刘备" }]), { id: "back", name: "返回存档选择", description: "不会修改现有征程" }], "此武将将陪伴你完成本次征程。", { back: "back", eyebrow: "出征准备" });
|
||
};
|
||
|
||
const createCardInstance = (run, name = "sha", upgrade = 0, affixes = []) =>
|
||
createRandomCardData(run, name, { random: () => nextRandom(run), upgrade, affixes });
|
||
|
||
const applyReward = (run, rewardId) => {
|
||
const player = run.player;
|
||
const relicId = config.rewards[rewardId]?.relic;
|
||
if (relicId) {
|
||
if (grantRelic(run, relicId)) {
|
||
game.log(`获得遗物【${getRelic(relicId).name}】`);
|
||
}
|
||
return;
|
||
}
|
||
const cardReward = config.rewards[rewardId]?.card;
|
||
if (cardReward) {
|
||
if (!canAcquireCard(player.character, cardReward.name)) throw new Error(`武将 ${player.character} 无法获得牌:${cardReward.name}`);
|
||
addCardToDeck(run, { ...createCardInstance(run, cardReward.name), ...copy(cardReward) });
|
||
return;
|
||
}
|
||
const support = config.rewards[rewardId]?.support;
|
||
if (support) { grantSupport(run, rewardId, support); return; }
|
||
switch (rewardId) {
|
||
case "card_sha":
|
||
addCardToDeck(run, createCardInstance(run, "sha"));
|
||
break;
|
||
case "card_tao":
|
||
addCardToDeck(run, createCardInstance(run, "tao"));
|
||
break;
|
||
case "upgrade": {
|
||
const card = upgradeRandomCard(player.deck, () => nextRandom(run));
|
||
if (card) game.log("梦三:", get.translation(card.name),
|
||
"强化至", card.upgrade, "级");
|
||
else game.log("梦三:没有可强化卡牌");
|
||
break;
|
||
}
|
||
case "heal":
|
||
player.hp = Math.min(player.maxHp, player.hp + 8);
|
||
break;
|
||
case "max_hp":
|
||
player.maxHp += 5;
|
||
player.hp += 5;
|
||
break;
|
||
case "item_hand":
|
||
grantRelic(run, "mengsan_hand_charm_shuying");
|
||
break;
|
||
case "skill_yingyong":
|
||
if (!player.permanentSkills.includes("mengsan_yingyong_shuying")) {
|
||
player.permanentSkills.push("mengsan_yingyong_shuying");
|
||
}
|
||
break;
|
||
}
|
||
};
|
||
|
||
const chooseReward = async (run, rewardConfig = {}) => {
|
||
const choices = getRandomRewardChoices(run, rewardConfig.rewardPool).filter(choice => choice.name);
|
||
const title = rewardConfig.rewardTitle || (rewardConfig.boss ? "极品奖励(三选一)" : "战斗奖励(三选一)");
|
||
const rewardId = await chooseButtons(title, choices, rewardConfig.description);
|
||
const reward = choices.find(choice => choice.id == rewardId);
|
||
if (reward) applyReward(run, reward.effectId || reward.id);
|
||
};
|
||
|
||
const finishStoryNode = async (run, node) => {
|
||
const content = config.nodeContents?.[node.contentId];
|
||
if (!content || content.kind != "dialogue") {
|
||
await setupBattle(run, node);
|
||
return false;
|
||
}
|
||
await playDialogue(content.dialogue, { run, title: content.name });
|
||
const choices = getAvailableStoryChoices(run, content);
|
||
if (!choices.length) {
|
||
choices.push({ id: "continue", name: "继续", description: "当前没有其他可用选项。", outcome: {} });
|
||
}
|
||
const choiceId = await chooseButtons(content.name, choices, content.description);
|
||
const choice = choices.find(current => current.id == choiceId);
|
||
const outcome = applyStoryOutcome(run, choice?.outcome);
|
||
await playDialogue(outcome.dialogue, { run, title: content.name });
|
||
if (outcome.confirmResult && outcome.result) {
|
||
await chooseButtons(content.name, [{ id: "continue", name: "继续" }], outcome.result);
|
||
}
|
||
if (outcome.battle) {
|
||
await setupBattle(run, node, getNodeEncounter(run, node, outcome.battle));
|
||
return false;
|
||
}
|
||
if (outcome.rewardPool) {
|
||
await chooseReward(run, {
|
||
...outcome,
|
||
description: outcome.rewardDescription || (!outcome.confirmResult ? outcome.result : ""),
|
||
});
|
||
}
|
||
completeNode(run, node.id);
|
||
await saveRun(run);
|
||
return true;
|
||
};
|
||
|
||
const finishUtilityNode = async (run, node) => {
|
||
if (node.type == "rest") {
|
||
const choice = await chooseButtons("休息节点", [
|
||
{ id: "heal", name: "休息",
|
||
description: `回复 ${restRecovery(run)} 点生命` },
|
||
{ id: "upgrade", name: "磨砺",
|
||
description: hasUpgradeableCard(run.player.deck)
|
||
? "随机强化一张尚未满级的可强化牌"
|
||
: "没有可强化卡牌",
|
||
disabled: !hasUpgradeableCard(run.player.deck) },
|
||
]);
|
||
if (choice === "heal") {
|
||
const result = applyRoomRelics(run, node, choice);
|
||
if (result) game.log(`休息回复${result.recovered}点生命`,
|
||
...result.relics.map(relic => relic.name));
|
||
} else applyReward(run, choice);
|
||
}
|
||
else if (node.type == "chest") {
|
||
await chooseReward(run, {
|
||
rewardPool: config.acts[run.actIndex].chestRewardPool ||
|
||
"shared.pool.boss.premium",
|
||
rewardTitle: "宝箱奖励(三选一)",
|
||
});
|
||
}
|
||
else if (node.type == "shop") {
|
||
const result = applyRoomRelics(run, node, "enter");
|
||
if (result) {
|
||
game.log(`遗物【餐券】进入商店回复${result.recovered}点生命`);
|
||
await saveRun(run);
|
||
}
|
||
const price = 20;
|
||
const choice = await chooseButtons("商店 Demo", [
|
||
{ id: "card_sha", name: `购买【杀】(${price}金币)`, description: "加入个人牌组", disabled: run.player.gold < price || !canAcquireCard(run.player.character, "sha") },
|
||
{ id: "heal", name: `恢复生命(${price}金币)`, description: "回复 8 点生命", disabled: run.player.gold < price },
|
||
{ id: "leave", name: "离开", description: "暂不购买" },
|
||
]);
|
||
if (choice != "leave" && choice != null) {
|
||
if (run.player.gold < price || (choice === "card_sha" && !canAcquireCard(run.player.character, "sha"))) throw new Error("梦三商店购买资格无效");
|
||
applyReward(run, choice);
|
||
run.player.gold -= price;
|
||
}
|
||
}
|
||
else {
|
||
const choices = [
|
||
{ id: "gold", name: "收下钱袋", description: "获得 15 金币" },
|
||
];
|
||
if (run.actIndex !== 0) {
|
||
choices.unshift({
|
||
id: "branch", name: "调查异象",
|
||
description: "在当前路线后插入一个额外剧情战斗节点",
|
||
});
|
||
}
|
||
const choice = await chooseButtons("随机事件", choices);
|
||
if (choice == "branch") insertStoryNode(run, node.id);
|
||
else if (choice == "gold") {
|
||
run.player.gold += 15;
|
||
run.statistics.goldEarned += 15;
|
||
}
|
||
}
|
||
completeNode(run, node.id);
|
||
await saveRun(run);
|
||
return true;
|
||
};
|
||
|
||
const shuffleBattlePile = (run, cards) => {
|
||
for (let index = cards.length - 1; index > 0; index--) {
|
||
const target = Math.floor(nextRandom(run) * (index + 1));
|
||
[cards[index], cards[target]] = [cards[target], cards[index]];
|
||
}
|
||
};
|
||
|
||
const setBattleCamp = (player, camp) => {
|
||
player.storage.mengsanCamp_shuying = camp;
|
||
player.side = camp === "enemy";
|
||
player.identity = player === game.me ? "zhu" : camp === "ally" ? "zhong" : "fan";
|
||
player.setIdentity(player.identity);
|
||
player.identityShown = true;
|
||
player.ai.shown = 1;
|
||
player.classList.add("mengsan-hide-identity-shuying");
|
||
};
|
||
const initBattleUnit = (player, spec) => {
|
||
const monster = spec.camp === "enemy" ? createMonster(spec) : null;
|
||
player.storage.mengsanUnitId_shuying = spec.id;
|
||
setBattleCamp(player, spec.camp);
|
||
player.init(spec.character, null, spec.inheritSkills !== false);
|
||
setBattleCamp(player, spec.camp);
|
||
player.storage.mengsanUnitId_shuying = spec.id;
|
||
if (monster) {
|
||
player.storage.mengsanMonster_shuying = { tier: monster.tier, draw: monster.draw, handLimit: monster.handLimit };
|
||
player.storage.mengsanMaxEnergy_shuying = monster.energy;
|
||
player.storage.mengsanEnergy_shuying = monster.energy;
|
||
}
|
||
if (spec.maxHp != null || monster) player.maxHp = spec.maxHp ?? monster.hp;
|
||
player.hp = Math.min(spec.hp ?? monster?.hp ?? player.hp, player.maxHp);
|
||
player.storage.mengsanInitialHand_shuying = spec.hand ?? 4;
|
||
for (const skill of spec.skills || []) player.addSkill(skill);
|
||
player.update();
|
||
return monster || initializeBondUnit(player, spec,
|
||
_status.mengsanRun_shuying);
|
||
};
|
||
|
||
const installMonsterPile = (player, monster, current) => {
|
||
if (!monster) return;
|
||
const run = _status.mengsanRun_shuying;
|
||
const battle = { drawPile: monster.createDeck(`${run.runId}_${player.playerid}`), discardPile: [], exhaustPile: [] };
|
||
for (const card of battle.drawPile) if (!lib.card[card.name]) throw new Error("怪物牌堆中的牌未加载:" + card.name);
|
||
shuffleBattlePile(run, battle.drawPile);
|
||
current.monsterPiles.set(player, installPersonalPiles(current.session, player, battle, run, current.resources, {
|
||
game, ui, get, _status, document, MutationObserver, shuffle: shuffleBattlePile,
|
||
strict: false, refresh() {},
|
||
}));
|
||
};
|
||
|
||
const {
|
||
planEnemyIntent,
|
||
executeFlyconidIntent,
|
||
executeRaiderIntent,
|
||
} = createMonsterIntentActions(game, () => activeBattle);
|
||
const { planBondIntent, executeBondIntent } =
|
||
createBondIntentActions(game, () => activeBattle);
|
||
const equipBattleUnit = async (player, spec, resources) => {
|
||
for (const info of spec.equipment || []) {
|
||
const card = resources.card(game.createCard(info.name, info.suit, info.number, info.nature));
|
||
await player.equip(card);
|
||
}
|
||
};
|
||
const createScenario = (plan, current) => {
|
||
const director = createBattleDirector(plan, {
|
||
active: () => current.session.active,
|
||
state: player => ({ hp: player.hp, hand: player.countCards("h"), alive: player.isAlive(),
|
||
camp: player.storage.mengsanCamp_shuying, linked: player.isLinked(), turnedOver: player.isTurnedOver() }),
|
||
dialogue: lines => playDialogue(lines, { run: _status.mengsanRun_shuying, title: "关卡剧情" }),
|
||
async spawn(spec, anchor) {
|
||
const player = game.addPlayer(anchor ? Number(anchor.dataset.position) + 1 : game.players.length + game.dead.length);
|
||
registerPlayers(current.session, [player]);
|
||
current.resources.bindPlayers([player]);
|
||
const join = game.createEvent("mengsanJoinBattle", false);
|
||
join.player = player;
|
||
join.setContent(async () => {
|
||
const monster = initBattleUnit(player, spec);
|
||
installMonsterPile(player, monster, current);
|
||
if (monster) {
|
||
current.energyUI.set(player,
|
||
mountEnergy(player, current.session, document));
|
||
current.intentUI.set(player, mountEnemyIntent(player,
|
||
current.session, STYLE_PATH, document,
|
||
() => getIntentHostiles(game, player)));
|
||
}
|
||
for (const participant of [...game.players, ...game.dead]) participant.setSeatNum(Number(participant.dataset.position) + 1);
|
||
await equipBattleUnit(player, spec, current.resources);
|
||
await player.draw(spec.hand ?? 4);
|
||
await game.triggerEnter(player);
|
||
if (monster && current.session.active) {
|
||
planEnemyIntent(player, _status.mengsanRun_shuying);
|
||
planBondIntent(player);
|
||
}
|
||
});
|
||
await join;
|
||
game.log(player, "作为", spec.camp === "ally" ? "友方支援" : "敌方援军", "加入战斗");
|
||
return player;
|
||
},
|
||
async effect(player, effect) {
|
||
if (effect.type === "draw") await player.draw(effect.amount);
|
||
else if (effect.type === "recover") await player.recover(effect.amount);
|
||
else if (effect.type === "maxHp") await player.gainMaxHp(effect.amount);
|
||
else if (effect.type === "skill") await player.addSkills(effect.skill);
|
||
else if (effect.type === "camp") setBattleCamp(player, effect.camp);
|
||
else if (effect.type === "discardEquipment") {
|
||
const cards = player.getCards("e", card => card.name === effect.name);
|
||
if (cards.length) await player.discard(cards);
|
||
}
|
||
},
|
||
log: name => game.log("关卡条件触发:", name),
|
||
});
|
||
current.session.ownResource(director, () => director.dispose());
|
||
return director;
|
||
};
|
||
|
||
const prepareBattle = async (run, node, encounter, session, resources) => {
|
||
// Old saves keep their deck instances, but the retired affix no longer exists.
|
||
for (const card of run.player.deck) if (Array.isArray(card.affixes)) card.affixes = card.affixes.filter(key => key !== "annihilate");
|
||
if (encounter.requiredCharacter && run.player.character !== encounter.requiredCharacter) throw new Error("该关卡仅限指定主角,请开始刘备的新征程");
|
||
const bondUnit = prepareBondBattle(run, () => nextRandom(run));
|
||
const plan = buildBattlePlan(encounter, run, bondUnit);
|
||
if (run.bondBattle) {
|
||
game.log(`羁绊助战:${bondDefinitions[run.bondBattle.id].name},${
|
||
bondUnit ? "本场将到场" : "本场未到场"}`);
|
||
}
|
||
for (const spec of [...plan.units, ...plan.rules.flatMap(r => r.effects.filter(e => e.type === "spawn").map(e => e.unit))]) {
|
||
if (!lib.character[spec.character]) throw new Error("关卡武将未加载:" + spec.character);
|
||
for (const skill of spec.skills || []) if (!lib.skill[skill]) throw new Error("关卡技能未加载:" + skill);
|
||
const slots = new Set();
|
||
for (const card of spec.equipment || []) {
|
||
const info = lib.card[card.name];
|
||
if (!info || info.type !== "equip" || !info.subtype || slots.has(info.subtype)) throw new Error("初始装备不存在、不是装备或槽位重复:" + card.name);
|
||
slots.add(info.subtype);
|
||
}
|
||
}
|
||
for (const rule of plan.rules) for (const effect of rule.effects) {
|
||
if (effect.type === "skill" && !lib.skill[effect.skill]) throw new Error("关卡增益技能未加载:" + effect.skill);
|
||
}
|
||
consumeSupports(run);
|
||
document.body.classList.add("mengsan-battle-ui-shuying");
|
||
session.ownResource({}, () => document.body.classList.remove("mengsan-battle-ui-shuying"));
|
||
resources.field(_status, "mengsanRun_shuying", run);
|
||
resources.field(_status, "mengsanNode_shuying", node);
|
||
resources.field(_status, "mengsanEncounter_shuying", encounter);
|
||
resources.field(_status, "mengsanBattle_shuying", {
|
||
resolving: false,
|
||
drawPile: copy(run.player.deck),
|
||
discardPile: [],
|
||
exhaustPile: [],
|
||
});
|
||
shuffleBattlePile(run, _status.mengsanBattle_shuying.drawPile);
|
||
|
||
game.prepareArena(1 + plan.units.length);
|
||
resources.capturePreparedArena();
|
||
// Register runtime IDs before card ownership transfers (gain/lose).
|
||
for (const current of game.players) current.getId();
|
||
registerPlayers(session, game.players.slice());
|
||
resources.bindPlayers(game.players);
|
||
const currentBattle = activeBattle;
|
||
let teardown;
|
||
activeBattle.releaseSkills = () => (teardown ||= createPlayerTeardown([...currentBattle.players], {lib,game,get,_status}))();
|
||
const me = game.me;
|
||
|
||
activeBattle.personalPiles = installPersonalPiles(session, me, _status.mengsanBattle_shuying, run, resources, {
|
||
game, ui, get, _status, document, MutationObserver, shuffle: shuffleBattlePile,
|
||
refresh: () => activeBattle?.session === session && activeBattle.pilesUI?.refresh(),
|
||
});
|
||
activeBattle.personalPiles.withOwner(() => me.init(run.player.character));
|
||
me.storage.mengsanUnitId_shuying = "player";
|
||
setBattleCamp(me, "ally");
|
||
const participants = game.players.filter(player => player !== me);
|
||
const monsters = plan.units.map((spec, index) => initBattleUnit(participants[index], spec));
|
||
plan.units.forEach((spec, index) => installMonsterPile(participants[index], monsters[index], currentBattle));
|
||
participants.forEach((player, index) => {
|
||
if (monsters[index]) currentBattle.intentUI.set(player,
|
||
mountEnemyIntent(player, session, STYLE_PATH, document,
|
||
() => getIntentHostiles(game, player)));
|
||
});
|
||
participants.forEach(player => {
|
||
if (player.storage.mengsanBond_shuying) {
|
||
currentBattle.energyUI.set(player,
|
||
mountEnergy(player, session, document));
|
||
}
|
||
});
|
||
activeBattle.director = createScenario(plan, currentBattle);
|
||
activeBattle.director.bind("player", me);
|
||
plan.units.forEach((spec, index) => activeBattle.director.bind(spec.id, participants[index]));
|
||
me.storage.mengsanPlayer_shuying = true;
|
||
me.storage.mengsanMaxEnergy_shuying = PLAYER_ENERGY;
|
||
me.storage.mengsanEnergy_shuying = PLAYER_ENERGY;
|
||
currentBattle.energyUI.set(me, mountEnergy(me, session, document));
|
||
currentBattle.handUI = await mountHandUI(me, session, {
|
||
game, ui, document, window, cardCost,
|
||
styleURL: `${STYLE_PATH}/ui/hand/style.css`,
|
||
log: message => game.log(message),
|
||
});
|
||
if (!session.active) return;
|
||
game.zhu = me;
|
||
if (run.player.maxHp == null) {
|
||
run.player.maxHp = me.maxHp;
|
||
run.player.hp = me.hp;
|
||
}
|
||
initializeRelicMaxHp(run);
|
||
me.maxHp = run.player.maxHp;
|
||
me.hp = Math.max(1, Math.min(run.player.hp, me.maxHp));
|
||
me.update();
|
||
currentBattle.relics = createRelicBattle(run, {
|
||
active: () => session.active && me.hp > 0,
|
||
draw: number => game.mengsanDraw_shuying(me, number),
|
||
effect: createOpeningEffects(game, me,
|
||
() => currentBattle.energyUI.get(me)?.()),
|
||
log: (relic, effect) => {
|
||
game.log(me, `遗物【${relic.name}】发动:${effect}`);
|
||
currentBattle.relicUI?.refresh();
|
||
},
|
||
});
|
||
run.player.permanentSkills.forEach(skill => {
|
||
if (lib.skill[skill] && !me.hasSkill(skill)) me.addSkill(skill);
|
||
});
|
||
for (const skill of [
|
||
"mengsan_draw_shuying", "mengsan_card_use_shuying",
|
||
"mengsan_card_affixes_shuying", "mengsan_scenario_shuying",
|
||
"mengsan_card_payment_shuying", "mengsan_monster_draw_shuying",
|
||
"mengsan_flyconid_action_shuying", "mengsan_raider_action_shuying",
|
||
"mengsan_bond_action_shuying",
|
||
"mengsan_raider_card_strength_shuying",
|
||
"mengsan_death_blow_finish_shuying",
|
||
"mengsan_stun_skip_shuying", "mengsan_stun_recover_shuying",
|
||
"mengsan_stun_clear_shuying",
|
||
"mengsan_relics_shuying",
|
||
"mengsan_relic_attack_shuying",
|
||
]) {
|
||
if (!lib.skill.global.includes(skill)) {
|
||
game.addGlobalSkill(skill);
|
||
session.ownResource({}, () => game.removeGlobalSkill(skill));
|
||
}
|
||
}
|
||
activeBattle.pilesUI = mountBattlePiles(session, _status.mengsanBattle_shuying);
|
||
currentBattle.relicUI = mountRelics(session, run, currentBattle.relics);
|
||
activeBattle.gmUI = mountGMManager(session, me, activeBattle.personalPiles, _status.mengsanBattle_shuying, resources, game);
|
||
for (let index = 0; index < plan.units.length; index++) await equipBattleUnit(participants[index], plan.units[index], resources);
|
||
game.syncState();
|
||
_status.event.trigger("gameStart");
|
||
for (const participant of participants) await participant.draw(participant.storage.mengsanInitialHand_shuying ?? 4);
|
||
for (const participant of participants) currentBattle.monsterPiles.get(participant)?.drawInnate();
|
||
if (!session.active) return;
|
||
activeBattle.personalPiles.resetOpeningHand();
|
||
await game.mengsanDraw_shuying(me, 4);
|
||
if (!session.active) return;
|
||
activeBattle.personalPiles.drawInnate();
|
||
await currentBattle.relics.start();
|
||
if (!session.active) return;
|
||
await playDialogue(encounter.openingDialogue, { run, title: encounter.name || "开场剧情" });
|
||
if (!session.active) return;
|
||
await currentBattle.director.start();
|
||
if (!session.active) return;
|
||
game.checkResult();
|
||
const loop = game.createEvent("phaseLoop", false);
|
||
loop.player = me;
|
||
loop._isStandardLoop = true;
|
||
let openingHandChecked = false;
|
||
loop.setContent(async event => runPhaseLoop(event, me, session, {game, lib, _status, get, beforeTurn: async player => {
|
||
if (player === me && !openingHandChecked) {
|
||
openingHandChecked = true;
|
||
currentBattle.personalPiles.trimOpeningHand(
|
||
4 + currentBattle.relics.openingHandBonus);
|
||
}
|
||
if ((isRaider(player) || player.storage.mengsanBond_shuying) &&
|
||
player.hujia > 0) await player.changeHujia(-player.hujia);
|
||
if (player.storage.mengsanMaxEnergy_shuying != null) {
|
||
player.storage.mengsanEnergy_shuying = player.storage.mengsanMaxEnergy_shuying;
|
||
currentBattle.energyUI.get(player)?.();
|
||
}
|
||
await currentBattle.director.beforeTurn(player);
|
||
if (player === me && session.active) {
|
||
for (const participant of game.players) {
|
||
planEnemyIntent(participant, run);
|
||
planBondIntent(participant);
|
||
}
|
||
}
|
||
game.checkResult();
|
||
}}));
|
||
};
|
||
|
||
|
||
const registerPlayers = (session, players) => {
|
||
for (const player of players) {
|
||
activeBattle.players.add(player);
|
||
const id = player.playerid;
|
||
session.ownResource(player, () => {
|
||
if (game.playerMap[id] === player) delete game.playerMap[id];
|
||
for (const list of [game.players, game.dead]) {
|
||
const index = list.indexOf(player); if (index >= 0) list.splice(index, 1);
|
||
}
|
||
if (game.me === player) game.me = null;
|
||
if (game.zhu === player) game.zhu = null;
|
||
player.remove();
|
||
});
|
||
}
|
||
};
|
||
const makeFlow = (releaseSkills = async () => {}) => createBattleFlow({
|
||
releaseSkills,
|
||
presentVictory: pending => {
|
||
if (pending.bondGrowth) {
|
||
const growth = pending.bondGrowth;
|
||
game.log(`羁绊【${bondDefinitions[growth.id].name}】进度增加${
|
||
growth.gain}%,当前${growth.level}级(${growth.progress}%)`);
|
||
}
|
||
if (pending.relicRecovery > 0) {
|
||
game.log(`遗物【燃烧之血】战后回复${pending.relicRecovery}点生命`);
|
||
}
|
||
return playDialogue(pending.victoryDialogue,
|
||
{ run: pending.base, title: "战后剧情" });
|
||
},
|
||
settlement:createBattleSettlement({store:modeStorage, config,
|
||
getRandomRewardChoices, applyReward, completeNode, enterNextAct,
|
||
logRewardPackage: (pack, selection) => {
|
||
const card = pack.upgradeChoices.find(card =>
|
||
card.id === selection.cardId);
|
||
game.log(`梦三:过关奖励 ${pack.gold} 金币,` +
|
||
(card ? `强化【${get.translation(card.name)}】` : "无可强化牌"));
|
||
},
|
||
}),
|
||
chooseReward: (choices, options) => (options.rewardPackage
|
||
? chooseRewardPackage : chooseBattleReward)(choices, {
|
||
...options,
|
||
fixedRewards: options.fixedRewards.map(id => ({
|
||
...config.rewards[id], id,
|
||
})),
|
||
}),
|
||
showMap,
|
||
showEnding:async route => { game.over(route === "victory"); },
|
||
quiesce:() => quiesceEngine(_status),
|
||
});
|
||
const requestBattleFinish = outcome => {
|
||
const current = activeBattle;
|
||
if (!current) return false;
|
||
for (const player of game.dead) {
|
||
recordBondDeath(player, _status.mengsanRun_shuying,
|
||
message => game.log(message));
|
||
}
|
||
const accepted = current.flow.requestFinish({session:current.session, hp:game.me?.hp || 0, outcome, defeatedEnemies:current.director?.defeatedEnemies ?? 1});
|
||
if (accepted) {
|
||
current.pilesUI?.dispose();
|
||
current.gmUI?.dispose();
|
||
current.relicUI?.dispose();
|
||
current.handUI?.dispose();
|
||
stopBattleTurn(current.session, _status.eventManager);
|
||
if (_status.mengsanBattle_shuying) _status.mengsanBattle_shuying.resolving = true;
|
||
current.finished();
|
||
}
|
||
return accepted;
|
||
};
|
||
const awaitSettlement = async (flow, initial = flow.wait()) => {
|
||
let operation = initial;
|
||
while (true) {
|
||
try { return await operation; }
|
||
catch (error) {
|
||
if (flow.state !== "retryable") throw error;
|
||
const action = await chooseButtons("结算未完成", [
|
||
{id:"retry",name:"重试保存与结算"},
|
||
{id:"exit",name:"保留已保存进度并返回模式选择"},
|
||
], "已提交的奖励不会重发。技能释放失败时请返回模式选择,避免重复调用释放回调。");
|
||
if (action === "exit") { await openModeSelection(); throw error; }
|
||
operation = flow.retry();
|
||
}
|
||
}
|
||
};
|
||
const setupBattle = async (run, node, encounter = getNodeEncounter(run, node)) => {
|
||
// Wait for engine boot callbacks BEFORE arena creation; cardsAsync then builds synchronously.
|
||
if (lib.onfree) await new Promise(resolve => lib.onfree.push(resolve));
|
||
await quiesceEngine(_status);
|
||
const session = createBattleSession(`${run.runId}:${++battleSequence}`);
|
||
const resources = createBattleResources(session, {game,ui,_status});
|
||
let current;
|
||
const flow = makeFlow(() => current.releaseSkills?.());
|
||
let finished;
|
||
const signal = new Promise(resolve => { finished = resolve; });
|
||
current = {session, flow, finished, resources, players: new Set(), monsterPiles: new Map(), energyUI: new Map(), intentUI: new Map()};
|
||
activeBattle = current;
|
||
const root = new lib.element.GameEvent("mengsanBattle", false, _status.eventManager);
|
||
const previousRoot = _status.eventManager.rootEvent;
|
||
session.ownResource({}, () => {
|
||
if (_status.eventManager.rootEvent === root) _status.eventManager.rootEvent = previousRoot;
|
||
});
|
||
root.setContent(async () => { await prepareBattle(run, node, encounter, session, resources); });
|
||
observeFreshEvent(session, root);
|
||
await flow.enterBattle({session,run,node,encounter,start() {
|
||
root.start().catch(error => { current.engineError = error; finished(); });
|
||
}});
|
||
await signal; // External controller, never an engine event content await.
|
||
if (current.engineError) {
|
||
session.requestStop();
|
||
current.handUI?.dispose();
|
||
document.body.classList.remove("mengsan-battle-ui-shuying");
|
||
await chooseButtons("战斗流程异常", [{id:"exit",name:"保留存档并返回模式选择"}], "无法确认旧事件已退出,已停止继续开战。上次成功存档仍保留。");
|
||
await openModeSelection(); return;
|
||
}
|
||
const result = await awaitSettlement(flow);
|
||
if (activeBattle === current) activeBattle = null;
|
||
if (result.route === "map") await routeRun(result.run, result.nextNode, true);
|
||
};
|
||
const routeRun = async (run, selected = null, supplied = false) => {
|
||
while (true) {
|
||
const node = supplied ? selected : await showMap(run);
|
||
supplied = false;
|
||
if (!node) { await openModeSelection(); return; }
|
||
if (["event", "rest", "shop", "chest"].includes(node.type)) {
|
||
await finishUtilityNode(run, node);
|
||
continue;
|
||
}
|
||
if (node.type === "story" && config.nodeContents?.[node.contentId]?.kind === "dialogue") {
|
||
if (await finishStoryNode(run, node)) continue;
|
||
return;
|
||
}
|
||
await setupBattle(run, node); return;
|
||
}
|
||
};
|
||
const startJourney = async () => {
|
||
lib.translate.restart = "返回";
|
||
let run = copy(lib.storage[config.saveKey] || null);
|
||
while (true) {
|
||
const action = await chooseRun(run);
|
||
if (action === "continue") break;
|
||
const character = await chooseCharacter();
|
||
if (character === "back") continue;
|
||
if (run) {
|
||
const decision = await chooseButtons("替换当前存档?", [
|
||
{ id: "cancel", name: "保留原征程", description: "返回存档选择,不作任何修改" },
|
||
{ id: "confirm", name: "确认覆盖并出征", danger: true, description: `以${get.translation(character)}开始新征程,原进度无法撤销` },
|
||
], "只有确认后才会创建并写入新征程。", { back: "cancel", eyebrow: "覆盖确认" });
|
||
if (decision !== "confirm") continue;
|
||
}
|
||
run = createRun(character);
|
||
await saveRun(run);
|
||
break;
|
||
}
|
||
if (run.battleFlow?.pending) {
|
||
const flow = makeFlow();
|
||
const result = await awaitSettlement(flow, flow.resumePending({session:createBattleSession(`recovery-${++battleSequence}`)}));
|
||
if (result.route === "map") await routeRun(result.run, result.nextNode, true);
|
||
return;
|
||
}
|
||
const canRefreshOldMap = config.characters.includes(run.player.character) && !run.map?.completedNodeIds?.length && run.map?.layoutVersion != config.mapLayoutVersion;
|
||
if (!run.map || run.map.actIndex != run.actIndex || canRefreshOldMap) {
|
||
run.map = generateActMap(run, run.actIndex);
|
||
await saveRun(run);
|
||
}
|
||
await routeRun(run);
|
||
};
|
||
|
||
const createMode = identityMode => {
|
||
const identityElement = identityMode.element || {};
|
||
const identityPlayer = identityElement.player || {};
|
||
const nativeChangeHujia = identityPlayer.changeHujia || lib.element.Player.prototype.changeHujia;
|
||
const modeCards = createMengsanCards();
|
||
return {
|
||
...identityMode,
|
||
name: MODE_ID,
|
||
character: { ...(identityMode.character || {}), ...createScenarioCharacters(lib.element.Character) },
|
||
card: { ...(identityMode.card || {}), ...modeCards.card },
|
||
connect: false,
|
||
start: [
|
||
async () => {
|
||
// Let the engine start event finish before starting the external controller.
|
||
setTimeout(() => startJourney().catch(reportFlowError), 0);
|
||
},
|
||
],
|
||
element: {
|
||
...identityElement,
|
||
player: {
|
||
...identityPlayer,
|
||
dieAfter() {
|
||
recordBondDeath(this, _status.mengsanRun_shuying,
|
||
message => game.log(message));
|
||
game.checkResult();
|
||
},
|
||
dieAfter2() {}, // Identity-mode kill rewards/loyalist penalties do not apply here.
|
||
isFriendOf(player) { return this === player || (this.storage.mengsanCamp_shuying === player?.storage?.mengsanCamp_shuying); },
|
||
isEnemyOf(player) { return Boolean(player && this.storage.mengsanCamp_shuying !== player.storage?.mengsanCamp_shuying); },
|
||
getEnemies(filter, includeDie) { return game[includeDie ? "filterPlayer2" : "filterPlayer"](p => this.isEnemyOf(p) && (!filter || filter(p))); },
|
||
getFriends(filter, includeDie) { const self = filter === true; return game[includeDie ? "filterPlayer2" : "filterPlayer"](p => (p !== this || self) && this.isFriendOf(p) && (typeof filter !== "function" || filter(p))); },
|
||
changeHujia(num, type) {
|
||
// 脆弱只削减正向获得的护甲,不改变受伤时消耗护甲的数值。
|
||
if (this.storage?.mengsanFrail_shuying > 0 && (num == null || num > 0) && type !== "damage") {
|
||
num = Math.floor((num ?? 1) * 0.75);
|
||
}
|
||
return nativeChangeHujia.call(this, num, type, false);
|
||
},
|
||
getHandcardLimit() {
|
||
if (this.storage?.mengsanPlayer_shuying) {
|
||
const run = _status.mengsanRun_shuying;
|
||
const bonus = Number(run?.player?.handLimitBonus || 0);
|
||
return PLAYER_HAND_LIMIT + bonus;
|
||
}
|
||
if (this.storage?.mengsanMonster_shuying) return this.storage.mengsanMonster_shuying.handLimit;
|
||
if (this.storage?.mengsanBond_shuying) {
|
||
return this.storage.mengsanBond_shuying.handLimit;
|
||
}
|
||
let number = Math.max(this.hp, 0);
|
||
number = game.checkMod(this, number, "maxHandcardBase", this);
|
||
number = game.checkMod(this, number, "maxHandcard", this);
|
||
number = game.checkMod(this, number, "maxHandcardFinal", this);
|
||
return Math.max(0, number);
|
||
},
|
||
},
|
||
},
|
||
game: {
|
||
...(identityMode.game || {}),
|
||
mengsanSetEnemyIntent_shuying(player, intent) {
|
||
const update = activeBattle?.intentUI.get(player);
|
||
if (!update) return false;
|
||
update(intent);
|
||
return true;
|
||
},
|
||
mengsanStunEnemy_shuying(player, options = {}) {
|
||
const update = activeBattle?.intentUI.get(player);
|
||
if (!activeBattle?.session.active || !update ||
|
||
!player?.isAlive() ||
|
||
player.storage?.mengsanCamp_shuying !== "enemy") {
|
||
return false;
|
||
}
|
||
if (isStunned(player)) return false;
|
||
const { resume = "advance", recover } = options;
|
||
const flyconid = isFlyconid(player);
|
||
const raider = isRaider(player);
|
||
const intent = options.intent ?? (flyconid ?
|
||
player.storage.mengsanFlyconidIntent_shuying : raider ?
|
||
player.storage.mengsanRaiderIntent_shuying : null);
|
||
const restore = recover || (flyconid ?
|
||
(choice, original) => {
|
||
const state =
|
||
player.storage.mengsanFlyconidState_shuying || {};
|
||
player.storage.mengsanFlyconidState_shuying =
|
||
recordFlyconidAction(state, null, false);
|
||
const next = choice === "retry" ? original : null;
|
||
player.storage.mengsanFlyconidIntent_shuying = next;
|
||
return next;
|
||
} : raider ? (choice, original) => {
|
||
player.storage.mengsanRaiderState_shuying = recordRaiderAction(player.storage.mengsanRaiderState_shuying || {});
|
||
const next = choice === "retry" ? original : null;
|
||
player.storage.mengsanRaiderIntent_shuying = next;
|
||
return next;
|
||
} : null);
|
||
const onRecover = async (choice, original) => {
|
||
const next = await restore(choice, original);
|
||
update(next === undefined ?
|
||
(choice === "retry" ? original : null) : next);
|
||
};
|
||
if (typeof restore !== "function") {
|
||
throw new TypeError("此怪物需要击晕恢复回调");
|
||
}
|
||
if (!applyStun(player, intent, { resume, recover: onRecover })) {
|
||
return false;
|
||
}
|
||
update(STUN_INTENT);
|
||
return true;
|
||
},
|
||
async mengsanPersistRun_shuying(run) {
|
||
await saveRun(run);
|
||
},
|
||
async mengsanClearRun_shuying() {
|
||
await clearRun();
|
||
},
|
||
mengsanFailRun_shuying(reason = "征程失败") {
|
||
game.log(reason);
|
||
return requestBattleFinish("defeat");
|
||
},
|
||
async mengsanDraw_shuying(player, number = 1) {
|
||
if (!player?.storage?.mengsanPlayer_shuying || number <= 0) return [];
|
||
const battle = _status.mengsanBattle_shuying;
|
||
const run = _status.mengsanRun_shuying;
|
||
if (!battle || battle.drawPile.length + battle.discardPile.length < number) {
|
||
await game.mengsanFailRun_shuying("个人牌堆与弃牌堆不足,挑战失败");
|
||
return [];
|
||
}
|
||
const result = activeBattle.personalPiles.take(number);
|
||
if (!result.length) return [];
|
||
player.directgain(result);
|
||
player.$draw(result.length);
|
||
game.log(player, "从个人牌堆摸了", get.cnNumber(result.length), "张牌");
|
||
return result;
|
||
},
|
||
mengsanFinishBattle_shuying() {
|
||
return requestBattleFinish("victory");
|
||
},
|
||
checkResult() {
|
||
const current = activeBattle;
|
||
if (!current?.session.active || !current.director?.ready || current.director.busy || current.resultCheckQueued) return;
|
||
current.resultCheckQueued = true;
|
||
const next = game.createEvent("mengsanScenarioResult", false);
|
||
next.setContent(async () => {
|
||
try {
|
||
await current.director.evaluate("state");
|
||
if (!current.session.active) return;
|
||
if (!game.me || game.me.isDead()) game.mengsanFailRun_shuying("角色死亡,征程结束");
|
||
else if (!game.players.some(p => p.storage.mengsanCamp_shuying === "enemy" && p.isAlive()) && !current.director.pendingEnemies) game.mengsanFinishBattle_shuying();
|
||
} finally { current.resultCheckQueued = false; }
|
||
});
|
||
},
|
||
},
|
||
get: {
|
||
...(identityMode.get || {}),
|
||
rawAttitude(from, to) { return from === to || from.storage.mengsanCamp_shuying === to.storage.mengsanCamp_shuying ? 8 : -8; },
|
||
},
|
||
skill: {
|
||
...(identityMode.skill || {}),
|
||
...createRelicSkills(() => activeBattle, () => game.me),
|
||
...createRelicCombatSkills(() => activeBattle, () => game.me),
|
||
mengsan_taoyuan_bond_shuying: {
|
||
locked: true, mark: true, marktext: "义",
|
||
intro: { content: "先天结识关羽、张飞,初始羁绊均为8级。助战角色在行军菜单中选择。" },
|
||
},
|
||
mengsan_card_payment_shuying: {
|
||
forced: true, silent: true, popup: false,
|
||
firstDo: true, priority: 10000,
|
||
trigger: { global: "useCard0" },
|
||
filter(event) { return event.player?.storage?.mengsanMaxEnergy_shuying != null &&
|
||
lib.card[event.card?.name]?.mengsanCost_shuying != null && isActiveCardUse(event, event.player); },
|
||
async content(event, trigger) {
|
||
if (trigger.mengsanPaid_shuying) return;
|
||
if (!payCard(trigger.player, trigger.card)) {
|
||
trigger.cancel();
|
||
game.log(trigger.player, "费用不足,不能使用", trigger.card);
|
||
return;
|
||
}
|
||
trigger.mengsanPaid_shuying = true;
|
||
activeBattle?.energyUI.get(trigger.player)?.();
|
||
},
|
||
},
|
||
mengsan_monster_draw_shuying: {
|
||
trigger: { player: "phaseDrawBegin2" },
|
||
forced: true, silent: true, popup: false,
|
||
filter(event, player) {
|
||
return Boolean(player.storage?.mengsanMonster_shuying ||
|
||
player.storage?.mengsanBond_shuying);
|
||
},
|
||
async content(event, trigger, player) {
|
||
trigger.num = isStunned(player) ? 0 :
|
||
(player.storage.mengsanMonster_shuying ||
|
||
player.storage.mengsanBond_shuying).draw;
|
||
},
|
||
},
|
||
mengsan_flyconid_action_shuying: {
|
||
trigger: { global: "phaseUseBefore" },
|
||
forced: true, silent: true, popup: false, priority: 100,
|
||
filter(event) { return Boolean(activeBattle?.session.active &&
|
||
!isStunned(event.player) && isFlyconid(event.player) &&
|
||
event.player.storage.mengsanFlyconidIntent_shuying); },
|
||
async content(event, trigger) { await executeFlyconidIntent(trigger.player); },
|
||
},
|
||
mengsan_raider_action_shuying: {
|
||
trigger: { global: "phaseUseBefore" },
|
||
forced: true, silent: true, popup: false, priority: 100,
|
||
filter(event) { return Boolean(activeBattle?.session.active &&
|
||
!isStunned(event.player) && isRaider(event.player) &&
|
||
event.player.storage.mengsanRaiderIntent_shuying); },
|
||
async content(event, trigger) { await executeRaiderIntent(trigger.player); },
|
||
},
|
||
mengsan_bond_action_shuying: {
|
||
trigger: { global: "phaseUseBefore" },
|
||
forced: true, silent: true, popup: false, priority: 100,
|
||
filter(event) {
|
||
return Boolean(activeBattle?.session.active &&
|
||
!isStunned(event.player) &&
|
||
event.player.storage?.mengsanBond_shuying?.intent);
|
||
},
|
||
async content(event, trigger) {
|
||
await executeBondIntent(trigger.player);
|
||
},
|
||
},
|
||
mengsan_raider_card_strength_shuying: {
|
||
trigger: { source: "damageBegin1" },
|
||
forced: true, silent: true, popup: false, priority: 90,
|
||
filter(event, player) { return Boolean(activeBattle?.session.active &&
|
||
(isRaider(player) || player.storage.mengsanBond_shuying) &&
|
||
player.storage.mengsanStrength_shuying > 0 &&
|
||
event.card && !event.mengsanScriptedSkill_shuying); },
|
||
async content(event, trigger, player) {
|
||
trigger.num += player.storage.mengsanStrength_shuying;
|
||
},
|
||
},
|
||
mengsan_raider_strength_shuying: {
|
||
mark: true, marktext: "力",
|
||
intro: { content(storage, player) { return `攻击伤害增加${player.storage.mengsanStrength_shuying || 0}点`; } },
|
||
onremove(player) { delete player.storage.mengsanStrength_shuying; },
|
||
},
|
||
mengsan_stun_skip_shuying: {
|
||
trigger: { global: "phaseUseBefore" },
|
||
forced: true, silent: true, popup: false, priority: 200,
|
||
filter(event) {
|
||
return Boolean(activeBattle?.session.active &&
|
||
isStunned(event.player));
|
||
},
|
||
async content(event, trigger) {
|
||
skipStunnedAction(trigger.player, trigger);
|
||
},
|
||
},
|
||
mengsan_stun_recover_shuying: {
|
||
trigger: { global: "phaseAfter" },
|
||
forced: true, silent: true, popup: false, priority: 200,
|
||
filter(event) {
|
||
return Boolean(activeBattle?.session.active &&
|
||
isStunned(event.player));
|
||
},
|
||
async content(event, trigger) {
|
||
await finishStunnedTurn(trigger.player);
|
||
},
|
||
},
|
||
mengsan_stun_clear_shuying: {
|
||
trigger: { global: "dieAfter" },
|
||
forced: true, silent: true, popup: false,
|
||
filter(event) { return isStunned(event.player); },
|
||
async content(event, trigger) {
|
||
clearStun(trigger.player);
|
||
},
|
||
},
|
||
mengsan_death_blow_finish_shuying: {
|
||
trigger: { global: "phaseUseAfter" },
|
||
forced: true, silent: true, popup: false,
|
||
filter(event) {
|
||
return Boolean(activeBattle?.session.active &&
|
||
hasPendingDeathBlow(event.player));
|
||
},
|
||
async content(event, trigger) {
|
||
await finishDeathBlow(trigger.player);
|
||
},
|
||
},
|
||
mengsan_frail_shuying: {
|
||
mark: true, marktext: "脆",
|
||
onremove(player) { delete player.storage.mengsanFrail_shuying; },
|
||
intro: { content(storage, player) { return `剩余${player.storage.mengsanFrail_shuying || 0}回合:获得护甲减少25%(向下取整)`; } },
|
||
trigger: { player: "phaseAfter" },
|
||
forced: true, silent: true, popup: false,
|
||
async content(event, trigger, player) {
|
||
const remaining = Math.max(0, (player.storage.mengsanFrail_shuying || 0) - 1);
|
||
player.storage.mengsanFrail_shuying = remaining;
|
||
if (remaining) player.markSkill("mengsan_frail_shuying");
|
||
else player.removeSkill("mengsan_frail_shuying");
|
||
},
|
||
},
|
||
mengsan_vulnerable_shuying: {
|
||
mark: true, marktext: "易",
|
||
onremove(player) { delete player.storage.mengsanVulnerable_shuying; },
|
||
intro: { content(storage, player) { return `剩余${player.storage.mengsanVulnerable_shuying || 0}回合:受到攻击伤害增加50%(向上取整)`; } },
|
||
trigger: { player: ["damageBegin1", "phaseAfter"] },
|
||
forced: true, silent: true, popup: false,
|
||
filter(event, player) {
|
||
if (!player.storage?.mengsanVulnerable_shuying) return false;
|
||
return event.name !== "damage" || Boolean(event.card || event.mengsanAttack_shuying);
|
||
},
|
||
async content(event, trigger, player) {
|
||
if (event.triggername === "damageBegin1") {
|
||
trigger.num = Math.ceil(trigger.num * 1.5);
|
||
return;
|
||
}
|
||
const remaining = Math.max(0, (player.storage.mengsanVulnerable_shuying || 0) - 1);
|
||
player.storage.mengsanVulnerable_shuying = remaining;
|
||
if (remaining) player.markSkill("mengsan_vulnerable_shuying");
|
||
else player.removeSkill("mengsan_vulnerable_shuying");
|
||
},
|
||
},
|
||
mengsan_scenario_shuying: {
|
||
trigger: { global: ["changeHpAfter", "gainAfter", "loseAfter", "dieAfter", "turnOverAfter", "linkAfter", "phaseAfter"] },
|
||
forced: true, silent: true, popup: false, priority: -100,
|
||
filter() { return Boolean(activeBattle?.session.active && activeBattle.director?.ready); },
|
||
async content(event, trigger) {
|
||
if (event.triggername === "phaseAfter") await activeBattle.director.afterTurn(trigger.player, trigger);
|
||
else await activeBattle.director.evaluate("state");
|
||
game.checkResult();
|
||
},
|
||
},
|
||
mengsan_draw_shuying: {
|
||
trigger: { player: "drawBegin" },
|
||
firstDo: true,
|
||
forced: true,
|
||
silent: true,
|
||
priority: 10000,
|
||
filter(event, player) {
|
||
return Boolean(player.storage?.mengsanPlayer_shuying && event.num > 0);
|
||
},
|
||
async content(event, trigger, player) {
|
||
const number = trigger.num;
|
||
trigger.cancel();
|
||
await game.mengsanDraw_shuying(player, number);
|
||
},
|
||
},
|
||
mengsan_card_use_shuying: {
|
||
trigger: { global: ["useCard1", "respond"] },
|
||
forced: true,
|
||
silent: true,
|
||
popup: false,
|
||
filter(event) {
|
||
return Boolean(activeBattle?.session.active && event.cards?.some(card => card.storage?.mengsanCard_shuying));
|
||
},
|
||
async content(event, trigger) {
|
||
trigger.cards.forEach(card => {
|
||
if (card.storage?.mengsanCard_shuying) card.storage.mengsanConsumed_shuying = true;
|
||
});
|
||
},
|
||
},
|
||
mengsan_card_affixes_shuying: {
|
||
forced: true, silent: true, popup: false, priority: 100,
|
||
trigger: { global: ["phaseDiscardBegin", "cardsDiscardAfter"] },
|
||
filter(event) {
|
||
if (!activeBattle?.session.active) return false;
|
||
if (event.name === "phaseDiscard") return event.player?.getCards("h").some(card => card.storage?.mengsanCard_shuying?.affixes?.includes("void"));
|
||
return event.cards?.some(card => card.storage?.mengsanCard_shuying?.affixes?.includes("ingenious"));
|
||
},
|
||
async content(event, trigger) {
|
||
if (event.triggername === "phaseDiscardBegin") {
|
||
const cards = trigger.player.getCards("h").filter(card => card.storage?.mengsanCard_shuying?.affixes?.includes("void"));
|
||
if (cards.length) {
|
||
await trigger.player.lose(cards, ui.special);
|
||
for (const card of cards) card.remove();
|
||
activeBattle.pilesUI?.refresh();
|
||
}
|
||
return;
|
||
}
|
||
if (trigger.getParent("phaseDiscard")) return;
|
||
for (const card of trigger.cards || []) {
|
||
if (!activeBattle?.session.active || get.position(card, true) !== "d" || !card.storage?.mengsanCard_shuying?.affixes?.includes("ingenious") || card.storage.mengsanIngeniousResolving_shuying) continue;
|
||
const use = trigger.getParent("useCard"), respond = trigger.getParent("respond");
|
||
if (use?.cards?.includes(card) || respond?.cards?.includes(card)) continue;
|
||
const owner = game.playerMap[card.storage.mengsanOwnerId_shuying];
|
||
if (!owner?.isAlive() || !lib.filter.cardEnabled(card, owner) || !canPayCard(owner, card) || !owner.hasUseTarget(card)) continue;
|
||
card.storage.mengsanIngeniousResolving_shuying = true;
|
||
try { await owner.chooseUseTarget({ card, cards: [card], forced: true, prompt: "奇巧:使用此牌" }); }
|
||
finally { card.storage.mengsanIngeniousResolving_shuying = false; }
|
||
}
|
||
},
|
||
mod: {
|
||
ignoredHandcard(card) { if (card.storage?.mengsanCard_shuying?.affixes?.includes("retain")) return true; },
|
||
cardEnabled2(card) { if (card.storage?.mengsanCard_shuying?.affixes?.includes("unplayable")) return false; },
|
||
},
|
||
},
|
||
mengsan_yingyong_shuying: {
|
||
trigger: { source: "damageBegin1" },
|
||
forced: true,
|
||
usable: 1,
|
||
filter(event, player) {
|
||
return Boolean(event.card && _status.currentPhase == player);
|
||
},
|
||
async content(event, trigger) {
|
||
trigger.num++;
|
||
},
|
||
},
|
||
},
|
||
translate: {
|
||
...(identityMode.translate || {}),
|
||
...modeCards.translate,
|
||
...scenarioTranslations,
|
||
mengsan_draw_shuying: "梦三牌组",
|
||
mengsan_card_use_shuying: "梦三词缀",
|
||
mengsan_yingyong_shuying: "英勇",
|
||
mengsan_yingyong_shuying_info: "锁定技,每回合限一次,你于自己的回合内使用牌造成的伤害+1。",
|
||
mengsan_flyconid_action_shuying: "孢子行动",
|
||
mengsan_raider_action_shuying: "劫掠行动",
|
||
mengsan_weak_shuying: "虚弱",
|
||
mengsan_weak_shuying_info:
|
||
"攻击伤害减少25%(向下取整),自身回合结束减少1层。",
|
||
mengsan_relic_attack_shuying: "遗物攻击修正",
|
||
mengsan_raider_card_strength_shuying: "力量加成",
|
||
mengsan_raider_strength_shuying: "力量",
|
||
mengsan_raider_strength_shuying_info: "攻击造成的伤害按力量层数增加。",
|
||
mengsan_frail_shuying: "脆弱",
|
||
mengsan_frail_shuying_info: "接下来相应回合获得护甲减少25%(向下取整)。",
|
||
mengsan_vulnerable_shuying: "易伤",
|
||
mengsan_vulnerable_shuying_info: "接下来相应回合受到的攻击伤害增加50%(向上取整)。",
|
||
},
|
||
config: {},
|
||
help: {
|
||
梦三: "梦三:玩家、敌方怪物和羁绊助战各有独立牌堆与弃牌堆,其他友方支援使用公共牌堆。主动出牌消耗费用,转化牌按转化后牌名计费;响应不耗费。DIY须在技能所属角色事件内访问牌堆,不可缓存公共牌堆节点。",
|
||
},
|
||
};
|
||
};
|
||
|
||
export async function createMengsanMode() {
|
||
if (!lib.group.includes("han")) {
|
||
game.addGroup("han", "汉", "汉", { color: [
|
||
[255, 252, 250, 1], [255, 241, 238, 1],
|
||
[245, 196, 193, 1], [192, 85, 91, 1],
|
||
] });
|
||
}
|
||
await new Promise(resolve => {
|
||
const style = lib.init.css(`${STYLE_PATH}/ui`, "style", resolve);
|
||
style.addEventListener("error", resolve, { once: true });
|
||
});
|
||
const identityMode = await game.loadModeAsync("identity");
|
||
const mode = createMode(identityMode);
|
||
mode.splash = "ext:术樱包/pve/images/tianshu.jpg";
|
||
return mode;
|
||
}
|
||
|
||
export default async function initMengsan() {
|
||
if (lib.config.all.mode.includes(MODE_ID)) return;
|
||
const mode = await createMengsanMode();
|
||
game.addMode(MODE_ID, mode, {
|
||
translate: "梦三",
|
||
config: {},
|
||
extension: "术樱包",
|
||
});
|
||
}
|