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 { 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, 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, showCosts: true, 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)); 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(); 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(); 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: "术樱包", }); }