fixed # 重构梦三项目结构
@@ -0,0 +1,17 @@
|
||||
# 梦三模式目录
|
||||
|
||||
`register.js` 是扩展加载入口,`mode.js` 负责组装模式、注册技能并连接各领域模块,`config.js` 提供模式配置。发布器和外部扩展应继续从 `register.js` 加载,避免依赖内部文件路径。
|
||||
|
||||
| 目录 | 职责 |
|
||||
| --- | --- |
|
||||
| `battle/` | 战斗会话、回合、结算、费用与通用战斗效果 |
|
||||
| `monsters/` | 怪物阶级、牌堆默认值和各怪物意图选择 |
|
||||
| `cards/` | 卡牌定义、词缀、生成和个人牌堆 |
|
||||
| `progression/` | 征程状态、存档、剧情选择和奖励逻辑 |
|
||||
| `content/` | 章节、遭遇、剧本、角色与内容注册表 |
|
||||
| `ui/` | 地图、对话、意图、奖励、图鉴及样式 |
|
||||
| `assets/intent/` | 意图图标 |
|
||||
| `assets/portraits/` | 角色立绘 |
|
||||
| `docs/` | 战斗和关卡配置说明 |
|
||||
|
||||
新增功能优先放入所属领域;界面代码放在 `ui/`,怪物行动逻辑放在 `monsters/`。跨领域引用使用相对路径,避免新增一个涵盖所有功能的扁平 `runtime/` 目录。模块资源文件随 `tools/generate_manifest.py` 自动收录,发布入口仍为 `mengsan/register.js`。
|
||||
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 2.8 MiB |
@@ -1,4 +1,4 @@
|
||||
import { cardDefinitions, canAcquireCard } from "../content/card-definitions.js";
|
||||
import { cardDefinitions, canAcquireCard } from "./card-definitions.js";
|
||||
|
||||
const suits = ["spade", "heart", "club", "diamond"];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import standard from "../../../../card/standard.js";
|
||||
import extra from "../../../../card/extra.js";
|
||||
import { _status } from "../../../../noname.js";
|
||||
import { cardCost, canPayCard, isActiveCardUse, SHA_DAMAGE } from "../runtime/combat-rules.js";
|
||||
import { cardCost, canPayCard, isActiveCardUse, SHA_DAMAGE } from "../battle/combat-rules.js";
|
||||
|
||||
const packs = [standard, extra];
|
||||
const names = [...new Set(packs.flatMap(pack => pack.list.map(entry => entry[2])))];
|
||||
@@ -1,7 +1,7 @@
|
||||
// Mode-owned piles use real Card nodes so native searches/gain/insert operations agree.
|
||||
// DIY must access ui piles inside the acting player's engine event, never cache global nodes.
|
||||
import { cardCost } from "./combat-rules.js";
|
||||
import { AFFIX_INFO } from "../content/affixes.js";
|
||||
import { cardCost } from "../battle/combat-rules.js";
|
||||
import { AFFIX_INFO } from "./affixes.js";
|
||||
|
||||
|
||||
export function installPersonalPiles(session, owner, battle, run, resources, env) {
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createCardData } from "../runtime/card-data.js";
|
||||
import { createCardData } from "./card-data.js";
|
||||
|
||||
// Mode-owned starting decks. Existing saves always retain their actual player.deck.
|
||||
export const defaultDeck = Object.freeze([
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defaultDeck } from "./content/card-library.js";
|
||||
import { defaultDeck } from "./cards/starting-deck.js";
|
||||
import contentRegistry from "./content/registry.js";
|
||||
|
||||
const mengsanConfig = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { validateBattlePlan } from "../runtime/battle-director.js";
|
||||
import { validateBattlePlan } from "../battle/battle-director.js";
|
||||
import act1 from "./acts/act1/index.js";
|
||||
import act2 from "./acts/act2/index.js";
|
||||
import act3 from "./acts/act3/index.js";
|
||||
|
||||
@@ -7,19 +7,19 @@ export function createScenarioCharacters(Character) {
|
||||
mengsan_soldier_shuying: new Character({ sex: "male", group: "qun", hp: 4, maxHp: 4,
|
||||
skills: [], img: "image/character/shibing1.jpg" }),
|
||||
mengsan_flyconid_shuying: new Character({ sex: "none", group: "qun", hp: 48, maxHp: 48,
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/flyconid-portrait.png" }),
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/portraits/flyconid-portrait.png" }),
|
||||
mengsan_raider_brute_shuying: new Character({ sex: "none", group: "qun", hp: 32, maxHp: 32,
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/raider-brute-portrait.png" }),
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/portraits/raider-brute-portrait.png" }),
|
||||
mengsan_raider_assassin_shuying: new Character({ sex: "none", group: "qun", hp: 21, maxHp: 21,
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/raider-assassin-portrait.png" }),
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/portraits/raider-assassin-portrait.png" }),
|
||||
mengsan_raider_axe_shuying: new Character({ sex: "none", group: "qun", hp: 21, maxHp: 21,
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/raider-axe-portrait.png" }),
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/portraits/raider-axe-portrait.png" }),
|
||||
mengsan_raider_crossbow_shuying: new Character({ sex: "none", group: "qun", hp: 20, maxHp: 20,
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/raider-crossbow-portrait.png" }),
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/portraits/raider-crossbow-portrait.png" }),
|
||||
mengsan_raider_tracker_shuying: new Character({ sex: "none", group: "qun", hp: 23, maxHp: 23,
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/raider-tracker-portrait.png" }),
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/portraits/raider-tracker-portrait.png" }),
|
||||
mengsan_sushuang_zhangshiping_shuying: new Character({ sex: "male", group: "shu", hp: 4, maxHp: 4,
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/sushuang-zhangshiping.png", names: "苏|双-张|世平" }),
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/portraits/sushuang-zhangshiping.png", names: "苏|双-张|世平" }),
|
||||
};
|
||||
}
|
||||
export const scenarioTranslations = {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
# 梦三战斗规则(测试阶段)
|
||||
|
||||
只影响梦三模式,不修改 noname 的标准/军争原始牌包。`content/mode-cards.js` 从标准、军争实体牌列表逐一复制模式专属定义,沿用原牌名以保留响应、装备、转化兼容性;图鉴说明在梦三模式覆盖。新征程的界刘备为 50/50 生命,默认每回合摸 2 张、手牌上限 3、回合费用恢复至 3。主动使用牌消耗费用,转化牌按转化后的牌名计费;响应【闪】、救援【桃】等不消耗费用。默认主动牌 1 费,`runtime/combat-rules.js` 中列出例外。【杀】1 费、可多次使用,牌定义的基础伤害为 4;【桃】及【桃园结义】回复 8 点生命;【南蛮入侵】、【万箭齐发】、【决斗】、【火攻】基础伤害 4,【闪电】命中伤害 12。其余卡牌保留原有效果,并加上梦三费用与图鉴说明。
|
||||
只影响梦三模式,不修改 noname 的标准/军争原始牌包。`cards/mode-cards.js` 从标准、军争实体牌列表逐一复制模式专属定义,沿用原牌名以保留响应、装备、转化兼容性;图鉴说明在梦三模式覆盖。新征程的界刘备为 50/50 生命,默认每回合摸 2 张、手牌上限 3、回合费用恢复至 3。主动使用牌消耗费用,转化牌按转化后的牌名计费;响应【闪】、救援【桃】等不消耗费用。默认主动牌 1 费,`battle/combat-rules.js` 中列出例外。【杀】1 费、可多次使用,牌定义的基础伤害为 4;【桃】及【桃园结义】回复 8 点生命;【南蛮入侵】、【万箭齐发】、【决斗】、【火攻】基础伤害 4,【闪电】命中伤害 12。其余卡牌保留原有效果,并加上梦三费用与图鉴说明。
|
||||
|
||||
## 怪物
|
||||
|
||||
`runtime/monster.js` 的 `Monster` 类统一提供阶级默认值。普通/精英/Boss 分别每回合摸 1/2/3 张、手牌上限 3/5/7、回合费用 2/3/4。每名敌人拥有自己的抽牌堆、弃牌堆与手牌;怪物默认使用阶级牌堆,不再从公共牌堆摸牌。友方支援仍用公共牌堆。
|
||||
`monsters/monster.js` 的 `Monster` 类统一提供阶级默认值。普通/精英/Boss 分别每回合摸 1/2/3 张、手牌上限 3/5/7、回合费用 2/3/4。每名敌人拥有自己的抽牌堆、弃牌堆与手牌;怪物默认使用阶级牌堆,不再从公共牌堆摸牌。友方支援仍用公共牌堆。
|
||||
|
||||
`HEALTH` 为目前各敌人的独立生命值;新怪物未配置时使用阶级兜底生命值。`OpeningSoldier extends Monster` 是继承示例;新子类在 `MONSTER_TYPES` 注册。关卡单位可以写 `tier`、`hp/maxHp`、`draw`、`handLimit`、`energy`、`deck` 覆盖默认值;`deck` 可为牌名数组(如 `deck:["sha","shan","tao"]`),也可用 `{name,suit,number}` 指定单张牌。牌名必须在 noname 中存在。
|
||||
|
||||
## 配置位置
|
||||
|
||||
- 玩家费用、手牌上限、卡牌费用与【杀】伤害:`runtime/combat-rules.js`
|
||||
- 梦三模式卡牌复制、内容与图鉴文案:`content/mode-cards.js`
|
||||
- 怪物阶级、默认牌堆、生命与继承示例:`runtime/monster.js`
|
||||
- 玩家费用、手牌上限、卡牌费用与【杀】伤害:`battle/combat-rules.js`
|
||||
- 梦三模式卡牌复制、内容与图鉴文案:`cards/mode-cards.js`
|
||||
- 怪物阶级、默认牌堆、生命与继承示例:`monsters/monster.js`
|
||||
- 刘备角色初始生命:`content/scenario-characters.js`
|
||||
- 刘备专属初始牌组:`content/card-library.js`
|
||||
- 刘备专属初始牌组:`cards/starting-deck.js`
|
||||
- 首关小兵覆盖数值:`content/acts/act1/stories/liubei-opening.js`
|
||||
|
||||
旧存档不作迁移。源码检查不等于游戏内、十周年 UI 或 AI 行为验收。
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
- 提前击败敌人允许正常获胜,此时未到第四次回合的支援不会触发。不强行等待、不添加免死。
|
||||
|
||||
## 新角色立绘
|
||||
`assets/sushuang-zhangshiping.png` 为本次 image_gen 生成的原创双人立绘,不是官方素材。
|
||||
`assets/portraits/sushuang-zhangshiping.png` 为本次 image_gen 生成的原创双人立绘,不是官方素材。
|
||||
|
||||
## 验证边界
|
||||
隔离脚本检查配置、回合计数、效果顺序及奖励幂等,不等于游戏/十周年UI验收。请开始新征程检查真实引擎的装备、座次、AI和剧情显示。
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { createScenarioCharacters, scenarioTranslations } from "./content/scenario-characters.js";
|
||||
import { createMengsanCards } from "./content/mode-cards.js";
|
||||
import { buildBattlePlan, consumeSupports, createBattleDirector, grantSupport } from "./runtime/battle-director.js";
|
||||
import { installPersonalPiles } from "./runtime/personal-piles.js";
|
||||
import { createMonster } from "./runtime/monster.js";
|
||||
import { mountEnemyIntent } from "./runtime/intent-display.js";
|
||||
import { attackHitCount, outgoingAttackDamage } from "./runtime/intent-damage.js";
|
||||
import { createMengsanCards } from "./cards/mode-cards.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 {
|
||||
createMonsterIntentActions,
|
||||
isFlyconid,
|
||||
isRaider,
|
||||
} from "./monsters/actions.js";
|
||||
import { mountEnemyIntent } from "./ui/intent-display.js";
|
||||
import { mountEnergy } from "./ui/energy-display.js";
|
||||
import {
|
||||
beginDeathBlow,
|
||||
finishDeathBlow,
|
||||
hasPendingDeathBlow,
|
||||
isDeathBlowIntent,
|
||||
} from "./runtime/death-blow.js";
|
||||
} from "./battle/death-blow.js";
|
||||
import {
|
||||
STUN_INTENT,
|
||||
applyStun,
|
||||
@@ -18,31 +21,31 @@ import {
|
||||
finishStunnedTurn,
|
||||
isStunned,
|
||||
skipStunnedAction,
|
||||
} from "./runtime/stun-intent.js";
|
||||
import { selectFlyconidMove, recordFlyconidAction } from "./runtime/flyconid-intent.js";
|
||||
import { isRaiderCharacter, selectRaiderMove, recordRaiderAction } from "./runtime/raider-intent.js";
|
||||
import { PLAYER_ENERGY, PLAYER_HAND_LIMIT, canPayCard, payCard, isActiveCardUse } from "./runtime/combat-rules.js";
|
||||
import { mountBattlePiles } from "./runtime/card-library.js";
|
||||
import { mountGMManager } from "./runtime/gm-manager.js";
|
||||
import { createPlayerTeardown } from "./runtime/skill-teardown.js";
|
||||
import { createBattleResources } from "./runtime/battle-resources.js";
|
||||
import { createModeStorage } from "./runtime/mode-storage.js";
|
||||
import { createBattleSession } from "./runtime/battle-session.js";
|
||||
import { observeFreshEvent } from "./runtime/engine-session.js";
|
||||
import { runPhaseLoop, stopBattleTurn } from "./runtime/battle-loop.js";
|
||||
import { createBattleSettlement } from "./runtime/battle-settlement.js";
|
||||
import { createBattleFlow, quiesceEngine } from "./runtime/battle-flow.js";
|
||||
} 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 } from "./map/index.js";
|
||||
import { getRandomRewardChoices } from "./runtime/reward.js";
|
||||
import { canAcquireCard } from "./content/card-definitions.js";
|
||||
import { addCardToDeck, createRandomCardData } from "./runtime/card-data.js";
|
||||
import { chooseBattleReward } from "./runtime/reward-ui.js";
|
||||
import { applyStoryOutcome, getAvailableStoryChoices } from "./runtime/story.js";
|
||||
import { playDialogue } from "./runtime/dialogue.js";
|
||||
import { chooseButtons } from "./runtime/flow-ui.js";
|
||||
import { openModeSelection } from "./runtime/navigation.js";
|
||||
import { showMap } 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 { chooseBattleReward } from "./ui/reward-ui.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,
|
||||
@@ -51,7 +54,7 @@ import {
|
||||
getNodeEncounter,
|
||||
insertStoryNode,
|
||||
nextRandom,
|
||||
} from "./state.js";
|
||||
} from "./progression/state.js";
|
||||
|
||||
const MODE_ID = config.modeId;
|
||||
const STYLE_PATH = `${lib.assetURL}extension/术樱包/mengsan`;
|
||||
@@ -278,94 +281,11 @@ const installMonsterPile = (player, monster, current) => {
|
||||
}));
|
||||
};
|
||||
|
||||
const mountEnergy = (player, session) => {
|
||||
const badge = document.createElement("div");
|
||||
badge.className = "mengsan-energy-shuying";
|
||||
const refresh = () => { badge.textContent = `费用 ${player.storage.mengsanEnergy_shuying}/${player.storage.mengsanMaxEnergy_shuying}`; };
|
||||
player.appendChild(badge);
|
||||
session.ownResource(badge, () => badge.remove());
|
||||
refresh();
|
||||
return refresh;
|
||||
};
|
||||
const isFlyconid = player => player?.name === "mengsan_flyconid_shuying" && player.storage?.mengsanCamp_shuying === "enemy";
|
||||
const isRaider = player => player?.storage?.mengsanCamp_shuying === "enemy" && isRaiderCharacter(player.name);
|
||||
const planEnemyIntent = (player, run) => {
|
||||
if (!player?.isAlive() || isStunned(player)) return;
|
||||
if (isFlyconid(player)) {
|
||||
if (player.storage.mengsanFlyconidIntent_shuying) return;
|
||||
const move = selectFlyconidMove(player.storage.mengsanFlyconidState_shuying || {}, () => nextRandom(run));
|
||||
player.storage.mengsanFlyconidIntent_shuying = move;
|
||||
game.mengsanSetEnemyIntent_shuying(player, move);
|
||||
} else if (isRaider(player)) {
|
||||
if (player.storage.mengsanRaiderIntent_shuying) return;
|
||||
const move = selectRaiderMove(player.name, player.storage.mengsanRaiderState_shuying || {});
|
||||
player.storage.mengsanRaiderIntent_shuying = move;
|
||||
game.mengsanSetEnemyIntent_shuying(player, move);
|
||||
}
|
||||
};
|
||||
const applyIntentDebuff = (target, move) => {
|
||||
if (!move.debuff) return;
|
||||
const frail = move.debuff === "脆弱";
|
||||
const key = frail ? "mengsanFrail_shuying" : "mengsanVulnerable_shuying";
|
||||
target.storage[key] = (target.storage[key] || 0) + move.stacks;
|
||||
target.addSkill(frail ? "mengsan_frail_shuying" : "mengsan_vulnerable_shuying");
|
||||
target.markSkill(frail ? "mengsan_frail_shuying" : "mengsan_vulnerable_shuying");
|
||||
};
|
||||
const dealIntentDamage = async (source, target, move) => {
|
||||
if (move.damage == null) return;
|
||||
for (let index = 0; index < attackHitCount(move); index++) {
|
||||
if (!target.isAlive() || !source.isAlive()) break;
|
||||
const hit = target.damage(outgoingAttackDamage(move, source), source);
|
||||
hit.mengsanAttack_shuying = true;
|
||||
hit.mengsanScriptedSkill_shuying = true;
|
||||
await hit;
|
||||
}
|
||||
};
|
||||
const executeFlyconidIntent = async player => {
|
||||
const move = player.storage.mengsanFlyconidIntent_shuying;
|
||||
if (!move) return;
|
||||
player.storage.mengsanFlyconidIntent_shuying = null;
|
||||
game.mengsanSetEnemyIntent_shuying(player, null);
|
||||
const paid = player.storage.mengsanEnergy_shuying >= 1;
|
||||
player.storage.mengsanFlyconidState_shuying = recordFlyconidAction(player.storage.mengsanFlyconidState_shuying || {}, move, paid);
|
||||
if (!paid) { game.log(player, "费用不足,未发动", move.name); return; }
|
||||
player.storage.mengsanEnergy_shuying--;
|
||||
activeBattle?.energyUI.get(player)?.();
|
||||
game.log(player, "消耗1费用发动", move.name);
|
||||
const target = game.me;
|
||||
if (!target?.isAlive() || !player.isAlive()) return;
|
||||
if (isDeathBlowIntent(move)) {
|
||||
await beginDeathBlow(player, target, move);
|
||||
return;
|
||||
}
|
||||
await dealIntentDamage(player, target, move);
|
||||
if (target.isAlive() && player.isAlive()) applyIntentDebuff(target, move);
|
||||
};
|
||||
const executeRaiderIntent = async player => {
|
||||
const move = player.storage.mengsanRaiderIntent_shuying;
|
||||
if (!move) return;
|
||||
player.storage.mengsanRaiderIntent_shuying = null;
|
||||
game.mengsanSetEnemyIntent_shuying(player, null);
|
||||
player.storage.mengsanRaiderState_shuying = recordRaiderAction(player.storage.mengsanRaiderState_shuying || {});
|
||||
if (player.storage.mengsanEnergy_shuying < 1) {
|
||||
game.log(player, "费用不足,未发动", move.name);
|
||||
return;
|
||||
}
|
||||
player.storage.mengsanEnergy_shuying--;
|
||||
activeBattle?.energyUI.get(player)?.();
|
||||
game.log(player, "消耗1费用发动", move.name);
|
||||
const target = game.me;
|
||||
if (!target?.isAlive() || !player.isAlive()) return;
|
||||
await dealIntentDamage(player, target, move);
|
||||
if (target.isAlive() && player.isAlive()) applyIntentDebuff(target, move);
|
||||
if (!player.isAlive() || !activeBattle?.session.active) return;
|
||||
if (move.block) await player.changeHujia(move.block);
|
||||
if (move.strength) {
|
||||
player.storage.mengsanStrength_shuying = (player.storage.mengsanStrength_shuying || 0) + move.strength;
|
||||
player.addSkill("mengsan_raider_strength_shuying");
|
||||
player.markSkill("mengsan_raider_strength_shuying");
|
||||
}
|
||||
};
|
||||
const {
|
||||
planEnemyIntent,
|
||||
executeFlyconidIntent,
|
||||
executeRaiderIntent,
|
||||
} = createMonsterIntentActions(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));
|
||||
@@ -388,7 +308,8 @@ const createScenario = (plan, current) => {
|
||||
const monster = initBattleUnit(player, spec);
|
||||
installMonsterPile(player, monster, current);
|
||||
if (monster) {
|
||||
current.energyUI.set(player, mountEnergy(player, current.session));
|
||||
current.energyUI.set(player,
|
||||
mountEnergy(player, current.session, document));
|
||||
current.intentUI.set(player, mountEnemyIntent(player,
|
||||
current.session, STYLE_PATH, document, () => game.me));
|
||||
}
|
||||
@@ -484,7 +405,7 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
|
||||
me.storage.mengsanPlayer_shuying = true;
|
||||
me.storage.mengsanMaxEnergy_shuying = PLAYER_ENERGY;
|
||||
me.storage.mengsanEnergy_shuying = PLAYER_ENERGY;
|
||||
currentBattle.energyUI.set(me, mountEnergy(me, session));
|
||||
currentBattle.energyUI.set(me, mountEnergy(me, session, document));
|
||||
game.zhu = me;
|
||||
if (run.player.maxHp == null) {
|
||||
run.player.maxHp = me.maxHp;
|
||||
@@ -1085,7 +1006,7 @@ export async function createMengsanMode() {
|
||||
] });
|
||||
}
|
||||
await new Promise(resolve => {
|
||||
const style = lib.init.css(STYLE_PATH, "style", resolve);
|
||||
const style = lib.init.css(`${STYLE_PATH}/ui`, "style", resolve);
|
||||
style.addEventListener("error", resolve, { once: true });
|
||||
});
|
||||
const identityMode = await game.loadModeAsync("identity");
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
attackHitCount,
|
||||
outgoingAttackDamage,
|
||||
} from "../battle/intent-damage.js";
|
||||
import { beginDeathBlow, isDeathBlowIntent } from "../battle/death-blow.js";
|
||||
import { isStunned } from "../battle/stun-intent.js";
|
||||
import { nextRandom } from "../progression/state.js";
|
||||
import { selectFlyconidMove, recordFlyconidAction } from "./flyconid-intent.js";
|
||||
import {
|
||||
isRaiderCharacter,
|
||||
selectRaiderMove,
|
||||
recordRaiderAction,
|
||||
} from "./raider-intent.js";
|
||||
|
||||
export const isFlyconid = player =>
|
||||
player?.name === "mengsan_flyconid_shuying" &&
|
||||
player.storage?.mengsanCamp_shuying === "enemy";
|
||||
|
||||
export const isRaider = player =>
|
||||
player?.storage?.mengsanCamp_shuying === "enemy" &&
|
||||
isRaiderCharacter(player.name);
|
||||
|
||||
export function createMonsterIntentActions(game, getActiveBattle) {
|
||||
const planEnemyIntent = (player, run) => {
|
||||
if (!player?.isAlive() || isStunned(player)) return;
|
||||
if (isFlyconid(player)) {
|
||||
if (player.storage.mengsanFlyconidIntent_shuying) return;
|
||||
const move = selectFlyconidMove(
|
||||
player.storage.mengsanFlyconidState_shuying || {},
|
||||
() => nextRandom(run));
|
||||
player.storage.mengsanFlyconidIntent_shuying = move;
|
||||
game.mengsanSetEnemyIntent_shuying(player, move);
|
||||
} else if (isRaider(player)) {
|
||||
if (player.storage.mengsanRaiderIntent_shuying) return;
|
||||
const move = selectRaiderMove(player.name,
|
||||
player.storage.mengsanRaiderState_shuying || {});
|
||||
player.storage.mengsanRaiderIntent_shuying = move;
|
||||
game.mengsanSetEnemyIntent_shuying(player, move);
|
||||
}
|
||||
};
|
||||
|
||||
const applyIntentDebuff = (target, move) => {
|
||||
if (!move.debuff) return;
|
||||
const frail = move.debuff === "脆弱";
|
||||
const key = frail ? "mengsanFrail_shuying" :
|
||||
"mengsanVulnerable_shuying";
|
||||
target.storage[key] = (target.storage[key] || 0) + move.stacks;
|
||||
target.addSkill(frail ? "mengsan_frail_shuying" :
|
||||
"mengsan_vulnerable_shuying");
|
||||
target.markSkill(frail ? "mengsan_frail_shuying" :
|
||||
"mengsan_vulnerable_shuying");
|
||||
};
|
||||
|
||||
const dealIntentDamage = async (source, target, move) => {
|
||||
if (move.damage == null) return;
|
||||
for (let index = 0; index < attackHitCount(move); index++) {
|
||||
if (!target.isAlive() || !source.isAlive()) break;
|
||||
const hit = target.damage(
|
||||
outgoingAttackDamage(move, source), source);
|
||||
hit.mengsanAttack_shuying = true;
|
||||
hit.mengsanScriptedSkill_shuying = true;
|
||||
await hit;
|
||||
}
|
||||
};
|
||||
|
||||
const executeFlyconidIntent = async player => {
|
||||
const move = player.storage.mengsanFlyconidIntent_shuying;
|
||||
if (!move) return;
|
||||
player.storage.mengsanFlyconidIntent_shuying = null;
|
||||
game.mengsanSetEnemyIntent_shuying(player, null);
|
||||
const paid = player.storage.mengsanEnergy_shuying >= 1;
|
||||
player.storage.mengsanFlyconidState_shuying =
|
||||
recordFlyconidAction(
|
||||
player.storage.mengsanFlyconidState_shuying || {},
|
||||
move, paid);
|
||||
if (!paid) {
|
||||
game.log(player, "费用不足,未发动", move.name);
|
||||
return;
|
||||
}
|
||||
player.storage.mengsanEnergy_shuying--;
|
||||
getActiveBattle()?.energyUI.get(player)?.();
|
||||
game.log(player, "消耗1费用发动", move.name);
|
||||
const target = game.me;
|
||||
if (!target?.isAlive() || !player.isAlive()) return;
|
||||
if (isDeathBlowIntent(move)) {
|
||||
await beginDeathBlow(player, target, move);
|
||||
return;
|
||||
}
|
||||
await dealIntentDamage(player, target, move);
|
||||
if (target.isAlive() && player.isAlive()) {
|
||||
applyIntentDebuff(target, move);
|
||||
}
|
||||
};
|
||||
|
||||
const executeRaiderIntent = async player => {
|
||||
const move = player.storage.mengsanRaiderIntent_shuying;
|
||||
if (!move) return;
|
||||
player.storage.mengsanRaiderIntent_shuying = null;
|
||||
game.mengsanSetEnemyIntent_shuying(player, null);
|
||||
player.storage.mengsanRaiderState_shuying = recordRaiderAction(
|
||||
player.storage.mengsanRaiderState_shuying || {});
|
||||
if (player.storage.mengsanEnergy_shuying < 1) {
|
||||
game.log(player, "费用不足,未发动", move.name);
|
||||
return;
|
||||
}
|
||||
player.storage.mengsanEnergy_shuying--;
|
||||
getActiveBattle()?.energyUI.get(player)?.();
|
||||
game.log(player, "消耗1费用发动", move.name);
|
||||
const target = game.me;
|
||||
if (!target?.isAlive() || !player.isAlive()) return;
|
||||
await dealIntentDamage(player, target, move);
|
||||
if (target.isAlive() && player.isAlive()) {
|
||||
applyIntentDebuff(target, move);
|
||||
}
|
||||
if (!player.isAlive() || !getActiveBattle()?.session.active) return;
|
||||
if (move.block) await player.changeHujia(move.block);
|
||||
if (move.strength) {
|
||||
player.storage.mengsanStrength_shuying =
|
||||
(player.storage.mengsanStrength_shuying || 0) +
|
||||
move.strength;
|
||||
player.addSkill("mengsan_raider_strength_shuying");
|
||||
player.markSkill("mengsan_raider_strength_shuying");
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
planEnemyIntent,
|
||||
executeFlyconidIntent,
|
||||
executeRaiderIntent,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createCardData } from "./card-data.js";
|
||||
import { createCardData } from "../cards/card-data.js";
|
||||
|
||||
// 梦三怪物规则只作用于敌方单位;子类可覆盖构造参数或 createDeck()。
|
||||
const TIER_RULES = Object.freeze({
|
||||
@@ -1,6 +1,6 @@
|
||||
import config from "../config.js";
|
||||
import { nextRandom } from "../state.js";
|
||||
import { canAcquireCard } from "../content/card-definitions.js";
|
||||
import { nextRandom } from "./state.js";
|
||||
import { canAcquireCard } from "../cards/card-definitions.js";
|
||||
|
||||
export const rewardCardName = reward => reward?.card?.name || ({ card_sha: "sha", card_tao: "tao" })[reward?.effectId] || null;
|
||||
export const canAcquireReward = (run, reward) => {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createStartingDeck } from "./content/card-library.js";
|
||||
import config from "./config.js";
|
||||
import { RAIDER_TRIO_ENCOUNTER, createRaiderTrioBattlePlan } from "./content/acts/act1/raider-trio.js";
|
||||
import { createStartingDeck } from "../cards/starting-deck.js";
|
||||
import config from "../config.js";
|
||||
import { RAIDER_TRIO_ENCOUNTER, createRaiderTrioBattlePlan } from "../content/acts/act1/raider-trio.js";
|
||||
|
||||
const hashText = text => {
|
||||
let value = 2166136261;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { lib, get, ui } from "../../../../noname.js";
|
||||
import { AFFIX_INFO } from "../content/affixes.js";
|
||||
import { AFFIX_INFO } from "../cards/affixes.js";
|
||||
|
||||
const suits = { spade: "♠ 黑桃", heart: "♥ 红桃", club: "♣ 梅花", diamond: "♦ 方块" };
|
||||
const types = { basic: "基本牌", trick: "锦囊牌", delay: "延时锦囊", equip: "装备牌" };
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mountMenu } from "./navigation.js";
|
||||
import { ui, get } from "../../../../noname.js";
|
||||
import { isDialogueLineVisible, applyDialogueChoice, applySkippedDialogueChoices } from "./dialogue-choice.js";
|
||||
import { isDialogueLineVisible, applyDialogueChoice, applySkippedDialogueChoices } from "../progression/dialogue-choice.js";
|
||||
|
||||
const TYPE_NAMES = {
|
||||
character: "角色",
|
||||
@@ -0,0 +1,13 @@
|
||||
export function mountEnergy(player, session, document) {
|
||||
const badge = document.createElement("div");
|
||||
badge.className = "mengsan-energy-shuying";
|
||||
const refresh = () => {
|
||||
const current = player.storage.mengsanEnergy_shuying;
|
||||
const maximum = player.storage.mengsanMaxEnergy_shuying;
|
||||
badge.textContent = `费用 ${current}/${maximum}`;
|
||||
};
|
||||
player.appendChild(badge);
|
||||
session.ownResource(badge, () => badge.remove());
|
||||
refresh();
|
||||
return refresh;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AFFIX_INFO } from "../content/affixes.js";
|
||||
import { AFFIX_INFO } from "../cards/affixes.js";
|
||||
import { describeLibraryCard } from "./card-library.js";
|
||||
|
||||
const node = (tag, className, label, parent) => {
|
||||
@@ -1,7 +1,7 @@
|
||||
// 只呈现已经选定的预测技能;不从手牌或原生技能猜测意图。
|
||||
import { attackHitCount, previewAttackDamage } from "./intent-damage.js";
|
||||
import { isDeathBlowIntent } from "./death-blow.js";
|
||||
import { isStunIntent } from "./stun-intent.js";
|
||||
import { attackHitCount, previewAttackDamage } from "../battle/intent-damage.js";
|
||||
import { isDeathBlowIntent } from "../battle/death-blow.js";
|
||||
import { isStunIntent } from "../battle/stun-intent.js";
|
||||
|
||||
export function attackIconForDamage(damage) {
|
||||
if (!Number.isInteger(damage) || damage < 0) {
|
||||
@@ -27,14 +27,14 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
||||
amount.className = "mengsan-intent-amount-shuying";
|
||||
const icon = document.createElement("img");
|
||||
icon.className = "mengsan-intent-icon-shuying";
|
||||
icon.alt = "Attack";
|
||||
icon.alt = "";
|
||||
icon.decoding = "async";
|
||||
damageLine.append(icon, amount);
|
||||
const debuffLine = document.createElement("span");
|
||||
debuffLine.className = "mengsan-intent-debuff-shuying";
|
||||
const debuffIcon = document.createElement("img");
|
||||
debuffIcon.className = "mengsan-intent-icon-shuying";
|
||||
debuffIcon.alt = "Debuff";
|
||||
debuffIcon.alt = "";
|
||||
debuffIcon.decoding = "async";
|
||||
debuffIcon.src = `${assetBase}/assets/intent/Intent_debuff.png`;
|
||||
const debuffAmount = document.createElement("span");
|
||||
@@ -43,7 +43,7 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
||||
blockLine.className = "mengsan-intent-block-shuying";
|
||||
const blockIcon = document.createElement("img");
|
||||
blockIcon.className = "mengsan-intent-icon-shuying";
|
||||
blockIcon.alt = "Block";
|
||||
blockIcon.alt = "";
|
||||
blockIcon.decoding = "async";
|
||||
blockIcon.src = `${assetBase}/assets/intent/Intent_defend.png`;
|
||||
const blockAmount = document.createElement("span");
|
||||
@@ -52,7 +52,7 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
||||
buffLine.className = "mengsan-intent-buff-shuying";
|
||||
const buffIcon = document.createElement("img");
|
||||
buffIcon.className = "mengsan-intent-icon-shuying";
|
||||
buffIcon.alt = "Buff";
|
||||
buffIcon.alt = "";
|
||||
buffIcon.decoding = "async";
|
||||
buffIcon.src = `${assetBase}/assets/intent/Intent_buff.png`;
|
||||
const buffAmount = document.createElement("span");
|
||||
@@ -79,7 +79,6 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
||||
amount.textContent = "";
|
||||
const src = `${assetBase}/assets/intent/Intent_stun.png`;
|
||||
if (icon.getAttribute("src") !== src) icon.src = src;
|
||||
icon.alt = "Stunned";
|
||||
if (badge.getAttribute("aria-label") !== "Stunned") {
|
||||
badge.setAttribute("aria-label", "Stunned");
|
||||
}
|
||||
@@ -96,9 +95,7 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
||||
Number.isInteger(intent.stacks) && intent.stacks > 0;
|
||||
debuffLine.hidden = !hasDebuff;
|
||||
if (hasDebuff) {
|
||||
const label = intent.debuff === "脆弱" ? "Frail" :
|
||||
intent.debuff === "易伤" ? "Vulnerable" : "Debuff";
|
||||
const debuffText = `${label} ×${intent.stacks}`;
|
||||
const debuffText = `×${intent.stacks}`;
|
||||
if (debuffAmount.textContent !== debuffText) {
|
||||
debuffAmount.textContent = debuffText;
|
||||
}
|
||||
@@ -108,7 +105,9 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
||||
blockLine.hidden = !hasBlock;
|
||||
buffLine.hidden = !hasStrength;
|
||||
if (hasBlock) blockAmount.textContent = String(intent.block);
|
||||
if (hasStrength) buffAmount.textContent = `力量 ×${intent.strength}`;
|
||||
if (hasStrength) {
|
||||
buffAmount.textContent = `+${intent.strength}`;
|
||||
}
|
||||
const effects = [];
|
||||
if (hasDamage) {
|
||||
const hits = attackHitCount(intent);
|
||||
@@ -127,13 +126,23 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
||||
`${assetBase}/assets/intent/Intent_death_blow.png` :
|
||||
`${assetBase}/assets/intent/Intent_attack_${tier}.png`;
|
||||
if (icon.getAttribute("src") !== src) icon.src = src;
|
||||
icon.alt = deathBlow ? "Death Blow" : "Attack";
|
||||
effects.push(`${deathBlow ? "Death Blow" : "Attack"}: ${damage} damage${hits > 1 ? ` × ${hits} hits` : ""}`);
|
||||
}
|
||||
if (hasDebuff) effects.push(debuffAmount.textContent);
|
||||
if (hasDebuff) {
|
||||
const label = intent.debuff === "脆弱" ? "Frail" :
|
||||
intent.debuff === "易伤" ? "Vulnerable" : "Debuff";
|
||||
effects.push(`${label} ×${intent.stacks}`);
|
||||
}
|
||||
if (hasBlock) effects.push(`Block ${intent.block}`);
|
||||
if (hasStrength) effects.push(`Strength +${intent.strength}`);
|
||||
const label = effects.join(", ") || intent.name || "Intent";
|
||||
if (!effects.length) {
|
||||
damageLine.hidden = false;
|
||||
amount.textContent = "";
|
||||
const src = `${assetBase}/assets/intent/Intent_unknown.png`;
|
||||
if (icon.getAttribute("src") !== src) icon.src = src;
|
||||
effects.push("Unknown");
|
||||
}
|
||||
const label = effects.join(", ");
|
||||
if (badge.getAttribute("aria-label") !== label) badge.setAttribute("aria-label", label);
|
||||
badge.hidden = false;
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import { openCardLibrary } from "../runtime/card-library.js";
|
||||
import { askMenu, openModeSelection } from "../runtime/navigation.js";
|
||||
import { ui, get } from "../../../../noname.js";
|
||||
import config from "../config.js";
|
||||
import { getSelectableNodes } from "../state.js";
|
||||
import { openCardLibrary } from "../card-library.js";
|
||||
import { askMenu, openModeSelection } from "../navigation.js";
|
||||
import { ui, get } from "../../../../../noname.js";
|
||||
import config from "../../config.js";
|
||||
import { getSelectableNodes } from "../../progression/state.js";
|
||||
|
||||
// Original vector pictograms; no external image/font requests.
|
||||
const iconPaths = {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { lib, get } from "../../../../noname.js";
|
||||
import { cardCost } from "./combat-rules.js";
|
||||
import { cardCost } from "../battle/combat-rules.js";
|
||||
import { chooseButtons } from "./flow-ui.js";
|
||||
|
||||
const element = (tag, className, text, parent) => {
|
||||
@@ -12,6 +12,7 @@ EXCLUDE_DIRS = {
|
||||
".gitea",
|
||||
".github",
|
||||
".update_tmp",
|
||||
".planning",
|
||||
"dist",
|
||||
"tools",
|
||||
"__pycache__",
|
||||
|
||||