改动整体底层系统
@@ -2,3 +2,4 @@ log.txt
|
||||
dist/manifest.json
|
||||
tools/__pycache__/
|
||||
/mengsan/.planning/
|
||||
/mengsan/verification
|
||||
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
@@ -12,7 +12,8 @@ const act1Map = {
|
||||
contentId: "act1.battle.liubeiOpening",
|
||||
},
|
||||
],
|
||||
enemies: ["re_xiahoudun", "re_zhangliao", "re_xuzhu"],
|
||||
enemies: ["mengsan_flyconid_shuying"],
|
||||
eliteEnemies: ["re_xiahoudun", "re_zhangliao", "re_xuzhu"],
|
||||
boss: "re_lvbu",
|
||||
baseGold: 20,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// 梦三永久牌组的可获得牌注册表。这里登记牌名与归属,不复制引擎中的效果定义。
|
||||
// 新增武将专属牌时填写 owner 为该武将 ID;通用牌填写 owner: null。
|
||||
const definitions = [
|
||||
{ name: "sha", category: "damage", owner: null },
|
||||
{ name: "shan", category: "utility", owner: null },
|
||||
{ name: "tao", category: "recovery", owner: null },
|
||||
{ name: "jiu", category: "recovery", owner: null },
|
||||
{ name: "guohe", category: "utility", owner: null },
|
||||
{ name: "wuzhong", category: "utility", owner: null },
|
||||
{ name: "juedou", category: "damage", owner: null },
|
||||
{ name: "bagua", category: "utility", owner: null },
|
||||
// 当前仅刘备专属首关的固定奖励会永久获得【的卢】。
|
||||
{ name: "dilu", category: "utility", owner: "mengsan_liubei_shuying" },
|
||||
];
|
||||
|
||||
export function createCardRegistry(entries) {
|
||||
const registry = Object.create(null);
|
||||
for (const entry of entries) {
|
||||
if (!entry || typeof entry.name !== "string" || !/^[a-z][a-z0-9_]*$/.test(entry.name) ||
|
||||
!["damage", "recovery", "utility"].includes(entry.category) ||
|
||||
!(entry.owner === null || typeof entry.owner === "string" && entry.owner.length > 0) ||
|
||||
Object.hasOwn(registry, entry.name)) {
|
||||
throw new Error(`梦三牌定义无效或重复:${entry?.name}`);
|
||||
}
|
||||
registry[entry.name] = Object.freeze({ name: entry.name, category: entry.category, owner: entry.owner });
|
||||
}
|
||||
return Object.freeze(registry);
|
||||
}
|
||||
|
||||
export const cardDefinitions = createCardRegistry(definitions);
|
||||
|
||||
export function canAcquireCard(character, name, registry = cardDefinitions) {
|
||||
const definition = Object.hasOwn(registry, name) ? registry[name] : null;
|
||||
return !!definition && (definition.owner === null || definition.owner === character);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createCardData } from "../runtime/card-data.js";
|
||||
|
||||
// Mode-owned starting decks. Existing saves always retain their actual player.deck.
|
||||
export const defaultDeck = Object.freeze([
|
||||
["spade", 7, "sha"], ["heart", 10, "sha"],
|
||||
@@ -24,8 +26,8 @@ export function getStartingDeck(character) {
|
||||
}
|
||||
|
||||
export function createStartingDeck(character, prefix = `mengsan_card_${Date.now()}`) {
|
||||
return getStartingDeck(character).map((entry, index) => ({
|
||||
return getStartingDeck(character).map((entry, index) => createCardData({
|
||||
id: `${prefix}_${index}`, suit: entry[0], number: entry[1], name: entry[2],
|
||||
nature: entry[3] || null, affixes: entry[4] ? entry[4].slice() : [], upgrade: 0,
|
||||
}));
|
||||
}, { character }));
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ export function createScenarioCharacters(Character) {
|
||||
// 国战男性普通小兵使用原生 shibing1 立绘;实际战斗生命由怪物类或关卡配置决定。
|
||||
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" }),
|
||||
mengsan_sushuang_zhangshiping_shuying: new Character({ sex: "male", group: "shu", hp: 4, maxHp: 4,
|
||||
skills: [], img: "extension/术樱包/mengsan/assets/sushuang-zhangshiping.png", names: "苏|双-张|世平" }),
|
||||
};
|
||||
@@ -13,5 +15,6 @@ export function createScenarioCharacters(Character) {
|
||||
export const scenarioTranslations = {
|
||||
mengsan_liubei_shuying: "界刘备", mengsan_liubei_shuying_title: "50/50生命 · 无初始技能",
|
||||
mengsan_soldier_shuying: "士兵",
|
||||
mengsan_flyconid_shuying: "飞蝇菌子",
|
||||
mengsan_sushuang_zhangshiping_shuying: "苏双&张世平",
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ export const rewards = {
|
||||
},
|
||||
"shared.reward.card.sha": { effectId: "card_sha", name: "获得一张【杀】", description: "加入独立永久牌组" },
|
||||
"shared.reward.card.tao": { effectId: "card_tao", name: "获得一张【桃】", description: "加入独立永久牌组" },
|
||||
"shared.reward.card.wuzhong": { card: { name: "wuzhong" }, name: "获得一张【无中生有】", description: "加入独立永久牌组" },
|
||||
"shared.reward.card.upgrade": { effectId: "upgrade", name: "随机强化", description: "随机一张牌强化一级" },
|
||||
"shared.reward.heal": { effectId: "heal", name: "整顿伤势", description: "回复 8 点生命" },
|
||||
"shared.reward.item.handCharm": { effectId: "item_hand", name: "道具·束带", description: "基础手牌上限额外 +1" },
|
||||
@@ -16,7 +17,7 @@ export const rewardPools = {
|
||||
"shared.pool.battle.normal": [
|
||||
"shared.reward.card.sha",
|
||||
"shared.reward.card.tao",
|
||||
"shared.reward.card.upgrade",
|
||||
"shared.reward.card.wuzhong",
|
||||
],
|
||||
"shared.pool.boss.premium": [
|
||||
"shared.reward.skill.heroic",
|
||||
|
||||
@@ -3,6 +3,8 @@ 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 { selectFlyconidMove, recordFlyconidAction } from "./runtime/flyconid-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";
|
||||
@@ -18,6 +20,9 @@ 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";
|
||||
@@ -71,31 +76,25 @@ const chooseCharacter = async () => {
|
||||
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 = []) => ({
|
||||
id: `mengsan_card_${Date.now()}_${Math.floor(nextRandom(run) * 1e6)}`,
|
||||
suit: ["spade", "heart", "club", "diamond"][Math.floor(nextRandom(run) * 4)],
|
||||
number: 1 + Math.floor(nextRandom(run) * 13),
|
||||
name,
|
||||
nature: null,
|
||||
upgrade,
|
||||
affixes: affixes.slice(),
|
||||
});
|
||||
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 cardReward = config.rewards[rewardId]?.card;
|
||||
if (cardReward) {
|
||||
player.deck.push({ ...createCardInstance(run, cardReward.name), ...copy(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":
|
||||
player.deck.push(createCardInstance(run, "sha"));
|
||||
addCardToDeck(run, createCardInstance(run, "sha"));
|
||||
break;
|
||||
case "card_tao":
|
||||
player.deck.push(createCardInstance(run, "tao"));
|
||||
addCardToDeck(run, createCardInstance(run, "tao"));
|
||||
break;
|
||||
case "upgrade": {
|
||||
const eligible = player.deck.filter(card => !card.affixes?.includes("eternal"));
|
||||
@@ -176,13 +175,14 @@ const finishUtilityNode = async (run, node) => {
|
||||
else if (node.type == "shop") {
|
||||
const price = 20;
|
||||
const choice = await chooseButtons("商店 Demo", [
|
||||
{ id: "card_sha", name: `购买【杀】(${price}金币)`, description: "加入个人牌组", disabled: run.player.gold < price },
|
||||
{ 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") {
|
||||
run.player.gold -= price;
|
||||
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 {
|
||||
@@ -258,6 +258,41 @@ const mountEnergy = (player, session) => {
|
||||
refresh();
|
||||
return refresh;
|
||||
};
|
||||
const isFlyconid = player => player?.name === "mengsan_flyconid_shuying" && player.storage?.mengsanCamp_shuying === "enemy";
|
||||
const planFlyconidIntent = (player, run) => {
|
||||
if (!isFlyconid(player) || !player.isAlive() || player.storage.mengsanFlyconidIntent_shuying) return;
|
||||
const move = selectFlyconidMove(player.storage.mengsanFlyconidState_shuying || {}, () => nextRandom(run));
|
||||
player.storage.mengsanFlyconidIntent_shuying = move;
|
||||
game.mengsanSetEnemyIntent_shuying(player, move);
|
||||
};
|
||||
const applyFlyconidDebuff = (target, move) => {
|
||||
if (!move.debuff) return;
|
||||
const frail = move.id === "frail";
|
||||
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 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 (move.damage != null) {
|
||||
const hit = target.damage(move.damage, player);
|
||||
hit.mengsanAttack_shuying = true;
|
||||
await hit;
|
||||
}
|
||||
if (target.isAlive() && player.isAlive()) applyFlyconidDebuff(target, move);
|
||||
};
|
||||
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));
|
||||
@@ -279,11 +314,15 @@ const createScenario = (plan, current) => {
|
||||
join.setContent(async () => {
|
||||
const monster = initBattleUnit(player, spec);
|
||||
installMonsterPile(player, monster, current);
|
||||
if (monster) current.energyUI.set(player, mountEnergy(player, current.session));
|
||||
if (monster) {
|
||||
current.energyUI.set(player, mountEnergy(player, current.session));
|
||||
current.intentUI.set(player, mountEnemyIntent(player, current.session, STYLE_PATH, document));
|
||||
}
|
||||
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) planFlyconidIntent(player, _status.mengsanRun_shuying);
|
||||
});
|
||||
await join;
|
||||
game.log(player, "作为", spec.camp === "ally" ? "友方支援" : "敌方援军", "加入战斗");
|
||||
@@ -358,6 +397,9 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
|
||||
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));
|
||||
});
|
||||
activeBattle.director = createScenario(plan, currentBattle);
|
||||
activeBattle.director.bind("player", me);
|
||||
plan.units.forEach((spec, index) => activeBattle.director.bind(spec.id, participants[index]));
|
||||
@@ -376,7 +418,7 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
|
||||
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"]) {
|
||||
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"]) {
|
||||
if (!lib.skill.global.includes(skill)) {
|
||||
game.addGlobalSkill(skill);
|
||||
session.ownResource({}, () => game.removeGlobalSkill(skill));
|
||||
@@ -413,6 +455,9 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
|
||||
currentBattle.energyUI.get(player)?.();
|
||||
}
|
||||
await currentBattle.director.beforeTurn(player);
|
||||
if (player === me && session.active) {
|
||||
for (const enemy of game.players) planFlyconidIntent(enemy, run);
|
||||
}
|
||||
game.checkResult();
|
||||
}}));
|
||||
};
|
||||
@@ -437,7 +482,7 @@ const makeFlow = (releaseSkills = async () => {}) => createBattleFlow({
|
||||
releaseSkills,
|
||||
presentVictory: pending => playDialogue(pending.victoryDialogue, { run: pending.base, title: "战后剧情" }),
|
||||
settlement:createBattleSettlement({store:modeStorage, config, getRandomRewardChoices, applyReward, completeNode, enterNextAct}),
|
||||
chooseReward:choices => chooseButtons("战斗奖励(三选一)", choices),
|
||||
chooseReward:chooseBattleReward,
|
||||
showMap,
|
||||
showEnding:async route => { game.over(route === "victory"); },
|
||||
quiesce:() => quiesceEngine(_status),
|
||||
@@ -480,7 +525,7 @@ const setupBattle = async (run, node, encounter = getNodeEncounter(run, node)) =
|
||||
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()};
|
||||
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;
|
||||
@@ -551,6 +596,7 @@ const startJourney = async () => {
|
||||
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,
|
||||
@@ -574,6 +620,13 @@ const createMode = identityMode => {
|
||||
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, limit) {
|
||||
// 脆弱只削减正向获得的护甲,不改变受伤时消耗护甲的数值。
|
||||
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, limit);
|
||||
},
|
||||
getHandcardLimit() {
|
||||
if (this.storage?.mengsanPlayer_shuying) {
|
||||
const run = _status.mengsanRun_shuying;
|
||||
@@ -591,6 +644,12 @@ const createMode = identityMode => {
|
||||
},
|
||||
game: {
|
||||
...(identityMode.game || {}),
|
||||
mengsanSetEnemyIntent_shuying(player, intent) {
|
||||
const update = activeBattle?.intentUI.get(player);
|
||||
if (!update) return false;
|
||||
update(intent);
|
||||
return true;
|
||||
},
|
||||
async mengsanPersistRun_shuying(run) {
|
||||
await saveRun(run);
|
||||
},
|
||||
@@ -663,6 +722,46 @@ const createMode = identityMode => {
|
||||
filter(event, player) { return Boolean(player.storage?.mengsanMonster_shuying); },
|
||||
async content(event, trigger, player) { trigger.num = player.storage.mengsanMonster_shuying.draw; },
|
||||
},
|
||||
mengsan_flyconid_action_shuying: {
|
||||
trigger: { global: "phaseUseBefore" },
|
||||
forced: true, silent: true, popup: false, priority: 100,
|
||||
filter(event) { return Boolean(activeBattle?.session.active && isFlyconid(event.player) && event.player.storage.mengsanFlyconidIntent_shuying); },
|
||||
async content(event, trigger) { await executeFlyconidIntent(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,
|
||||
@@ -757,6 +856,11 @@ const createMode = identityMode => {
|
||||
mengsan_card_use_shuying: "梦三词缀",
|
||||
mengsan_yingyong_shuying: "英勇",
|
||||
mengsan_yingyong_shuying_info: "锁定技,每回合限一次,你于自己的回合内使用牌造成的伤害+1。",
|
||||
mengsan_flyconid_action_shuying: "孢子行动",
|
||||
mengsan_frail_shuying: "脆弱",
|
||||
mengsan_frail_shuying_info: "接下来相应回合获得护甲减少25%(向下取整)。",
|
||||
mengsan_vulnerable_shuying: "易伤",
|
||||
mengsan_vulnerable_shuying_info: "接下来相应回合受到的攻击伤害增加50%(向上取整)。",
|
||||
},
|
||||
config: {},
|
||||
help: {
|
||||
|
||||
@@ -16,8 +16,8 @@ export function createBattleFlow({settlement, chooseReward, showMap, showEnding,
|
||||
if (job.pending.state !== "chosen") {
|
||||
state = "choosing";
|
||||
if (!job.hasChoice) {
|
||||
job.choiceId = job.pending.outcome === "defeat" || !job.pending.choices.length ? null : await chooseReward(copy(job.pending.choices));
|
||||
if (job.pending.outcome !== "defeat" && job.pending.choices.length && !job.pending.choices.some(c => c.id === job.choiceId)) throw new Error("Invalid reward selection");
|
||||
job.choiceId = job.pending.outcome === "defeat" || !job.pending.choices.length ? null : await chooseReward(copy(job.pending.choices), { boss: job.pending.boss });
|
||||
if (job.pending.outcome !== "defeat" && job.pending.choices.length && job.choiceId !== null && !job.pending.choices.some(c => c.id === job.choiceId)) throw new Error("Invalid reward selection");
|
||||
job.hasChoice = true;
|
||||
}
|
||||
job.pending = await settlement.choose(job.input.run.runId, job.pending.id, job.choiceId);
|
||||
|
||||
@@ -19,7 +19,7 @@ export function createBattleSettlement({store, config, getRandomRewardChoices, a
|
||||
// Persist fixed candidates before opening choice UI. No mutation of the live run.
|
||||
async prepare({run: snapshot, node, encounter, hp, outcome = "victory"}) {
|
||||
if (!["victory", "defeat"].includes(outcome)) throw new Error("Unknown battle outcome");
|
||||
if (outcome === "victory" && (!Number.isFinite(hp) || !Number.isFinite(encounter.gold) || encounter.gold < 0)) throw new Error("Invalid battle result");
|
||||
if (outcome === "victory" && (!Number.isFinite(hp) || (!encounter.skipRandomReward && (!Number.isSafeInteger(encounter.gold) || encounter.gold < 0)))) throw new Error("Invalid battle result");
|
||||
const id = idFor(snapshot, node);
|
||||
return store.update(storage => {
|
||||
const run = current(storage, snapshot.runId);
|
||||
@@ -36,12 +36,13 @@ export function createBattleSettlement({store, config, getRandomRewardChoices, a
|
||||
const base = copy(snapshot); delete base.battleFlow;
|
||||
if (outcome === "victory") {
|
||||
base.player.hp = Math.max(1, Math.min(hp, base.player.maxHp ?? hp));
|
||||
base.player.gold += encounter.gold;
|
||||
base.statistics.goldEarned += encounter.gold;
|
||||
base.statistics.defeatedEnemies += encounter.defeatedEnemies ?? 1;
|
||||
}
|
||||
const choices = outcome === "victory" && !encounter.skipRandomReward ? getRandomRewardChoices(base, encounter.rewardPool).filter(c => c.name) : [];
|
||||
if (outcome === "victory" && !encounter.skipRandomReward && !choices.length) throw new Error("Empty battle reward pool");
|
||||
const rewards = outcome === "victory" && !encounter.skipRandomReward ? getRandomRewardChoices(base, encounter.rewardPool).filter(c => c.name) : [];
|
||||
if (outcome === "victory" && !encounter.skipRandomReward && !rewards.length) throw new Error("Empty battle reward pool");
|
||||
// Persist the configured amount as a candidate, never as automatic victory income.
|
||||
const choices = outcome === "victory" && !encounter.skipRandomReward
|
||||
? [{id:"mengsan.reward.gold.shuying", kind:"gold", name:"金币", amount:encounter.gold}, ...rewards] : [];
|
||||
run.battleFlow.pending = {id, state:"awaitingChoice", nodeId:node.id, base, choices:copy(choices), fixedRewards:copy(encounter.fixedRewards || []), victoryDialogue:copy(encounter.victoryDialogue || []), boss:!!encounter.boss, outcome};
|
||||
return run.battleFlow.pending;
|
||||
});
|
||||
@@ -57,9 +58,13 @@ export function createBattleSettlement({store, config, getRandomRewardChoices, a
|
||||
let route = "defeat";
|
||||
if (item.outcome === "victory") {
|
||||
const choice = item.choices.find(c => c.id === choiceId);
|
||||
if (item.choices.length) {
|
||||
if (item.choices.length && choiceId !== null) {
|
||||
if (!choice) throw new Error("Reward is not a fixed candidate");
|
||||
applyReward(result, choice.effectId || choice.id);
|
||||
if (choice.kind === "gold") {
|
||||
if (!Number.isSafeInteger(choice.amount) || choice.amount < 0) throw new Error("Invalid pending gold reward");
|
||||
result.player.gold += choice.amount;
|
||||
result.statistics.goldEarned += choice.amount;
|
||||
} else applyReward(result, choice.effectId || choice.id);
|
||||
} else if (choiceId !== null) throw new Error("Unexpected reward selection");
|
||||
for (const rewardId of item.fixedRewards || []) applyReward(result, rewardId);
|
||||
if (!completeNode(result, item.nodeId)) throw new Error("Node completion failed");
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { cardDefinitions, canAcquireCard } from "../content/card-definitions.js";
|
||||
|
||||
const suits = ["spade", "heart", "club", "diamond"];
|
||||
|
||||
// 仅创建可序列化的牌组数据;战斗中的实体牌仍由 game.createCard 创建。
|
||||
export function createCardData(spec, { character, enemy = false, registry = cardDefinitions } = {}) {
|
||||
if (!spec || typeof spec !== "object" || !Object.hasOwn(registry, spec.name)) {
|
||||
throw new Error(`梦三牌未注册:${spec?.name}`);
|
||||
}
|
||||
if (!enemy && !canAcquireCard(character, spec.name, registry)) {
|
||||
throw new Error(`武将 ${character || "未知"} 无法获得牌:${spec.name}`);
|
||||
}
|
||||
if (typeof spec.id !== "string" || !spec.id || !suits.includes(spec.suit) ||
|
||||
!Number.isInteger(spec.number) || spec.number < 1 || spec.number > 13 ||
|
||||
(spec.nature != null && typeof spec.nature !== "string") ||
|
||||
!Number.isInteger(spec.upgrade ?? 0) || (spec.upgrade ?? 0) < 0 ||
|
||||
!Array.isArray(spec.affixes ?? []) || !(spec.affixes ?? []).every(value => typeof value === "string")) {
|
||||
throw new Error(`梦三牌数据无效:${spec.name}`);
|
||||
}
|
||||
return {
|
||||
id: spec.id, suit: spec.suit, number: spec.number, name: spec.name,
|
||||
nature: spec.nature ?? null, affixes: (spec.affixes ?? []).slice(), upgrade: spec.upgrade ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function createRandomCardData(run, name, { random, now = Date.now, upgrade = 0, affixes = [] } = {}) {
|
||||
if (!run?.player || !Array.isArray(run.player.deck) || typeof random !== "function") throw new Error("梦三随机牌参数无效");
|
||||
// 先检查资格,再消耗随机数;失败的获得操作不改变后续奖励序列。
|
||||
if (!canAcquireCard(run.player.character, name)) throw new Error(`武将 ${run.player.character} 无法获得牌:${name}`);
|
||||
const stamp = now();
|
||||
let id;
|
||||
do { id = `mengsan_card_${stamp}_${Math.floor(random() * 1e6)}`; }
|
||||
while (run.player.deck.some(card => card.id === id));
|
||||
return createCardData({
|
||||
id, suit: suits[Math.floor(random() * suits.length)], number: 1 + Math.floor(random() * 13),
|
||||
name, nature: null, upgrade, affixes,
|
||||
}, { character: run.player.character });
|
||||
}
|
||||
|
||||
export function addCardToDeck(run, spec) {
|
||||
const card = createCardData(spec, { character: run?.player?.character });
|
||||
if (!Array.isArray(run?.player?.deck)) throw new Error("梦三牌组无效");
|
||||
if (run.player.deck.some(existing => existing.id === card.id)) throw new Error(`梦三牌 ID 重复:${card.id}`);
|
||||
run.player.deck.push(card);
|
||||
return card;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// 冷却按飞蝇菌子自己的行动次数计算,不受玩家回合或增援数量影响。
|
||||
export const FLYCONID_MOVES = Object.freeze({
|
||||
smash: Object.freeze({ id: "smash", name: "猛砸", damage: 11 }),
|
||||
frail: Object.freeze({ id: "frail", name: "脆弱孢子", damage: 8, debuff: "脆弱", stacks: 2 }),
|
||||
vulnerable: Object.freeze({ id: "vulnerable", name: "易伤孢子", debuff: "易伤", stacks: 2 }),
|
||||
});
|
||||
|
||||
export function eligibleFlyconidMoves(state = {}) {
|
||||
const upcoming = (state.turnsTaken || 0) + 1;
|
||||
const lastUsed = state.lastUsed || {};
|
||||
const moves = [];
|
||||
if (state.lastMove !== "smash") moves.push(FLYCONID_MOVES.smash);
|
||||
if (lastUsed.frail == null || upcoming - lastUsed.frail > 2) moves.push(FLYCONID_MOVES.frail);
|
||||
if (upcoming >= 2 && (lastUsed.vulnerable == null || upcoming - lastUsed.vulnerable > 3)) moves.push(FLYCONID_MOVES.vulnerable);
|
||||
return moves;
|
||||
}
|
||||
|
||||
export function selectFlyconidMove(state, random) {
|
||||
const moves = eligibleFlyconidMoves(state);
|
||||
if (!moves.length) return null;
|
||||
const roll = random();
|
||||
if (!Number.isFinite(roll) || roll < 0 || roll >= 1) throw new RangeError("怪物随机数必须在 [0,1) 内");
|
||||
return moves[Math.floor(roll * moves.length)];
|
||||
}
|
||||
|
||||
export function recordFlyconidAction(state, move, paid) {
|
||||
const next = {
|
||||
turnsTaken: (state.turnsTaken || 0) + 1,
|
||||
lastMove: state.lastMove || null,
|
||||
lastUsed: { ...(state.lastUsed || {}) },
|
||||
};
|
||||
if (paid && move) {
|
||||
next.lastMove = move.id;
|
||||
next.lastUsed[move.id] = next.turnsTaken;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// 只呈现已经选定的预测技能;不从手牌或原生技能猜测意图。
|
||||
export function attackIconForDamage(damage) {
|
||||
if (!Number.isInteger(damage) || damage < 0) throw new RangeError("预测伤害必须是非负整数");
|
||||
if (damage < 5) return 1;
|
||||
if (damage < 10) return 2;
|
||||
if (damage < 20) return 3;
|
||||
if (damage < 40) return 4;
|
||||
return 5;
|
||||
}
|
||||
|
||||
export function mountEnemyIntent(player, session, assetBase, document) {
|
||||
const badge = document.createElement("div");
|
||||
badge.className = "mengsan-intent-shuying";
|
||||
badge.hidden = true;
|
||||
badge.setAttribute("aria-live", "polite");
|
||||
const name = document.createElement("span");
|
||||
name.className = "mengsan-intent-name-shuying";
|
||||
const damageLine = document.createElement("span");
|
||||
damageLine.className = "mengsan-intent-damage-shuying";
|
||||
const amount = document.createElement("span");
|
||||
amount.className = "mengsan-intent-amount-shuying";
|
||||
const icon = document.createElement("img");
|
||||
icon.className = "mengsan-intent-icon-shuying";
|
||||
icon.alt = "预计伤害";
|
||||
icon.decoding = "async";
|
||||
damageLine.append(amount, icon);
|
||||
const debuffLine = document.createElement("span");
|
||||
debuffLine.className = "mengsan-intent-debuff-shuying";
|
||||
const debuffIcon = document.createElement("img");
|
||||
debuffIcon.className = "mengsan-intent-icon-shuying";
|
||||
debuffIcon.alt = "预计施加负面状态";
|
||||
debuffIcon.decoding = "async";
|
||||
debuffIcon.src = `${assetBase}/assets/intent/Intent_debuff.webp`;
|
||||
const debuffAmount = document.createElement("span");
|
||||
debuffLine.append(debuffIcon, debuffAmount);
|
||||
badge.append(name, damageLine, debuffLine);
|
||||
player.appendChild(badge);
|
||||
session.ownResource(badge, () => badge.remove());
|
||||
return intent => {
|
||||
if (!intent) {
|
||||
badge.hidden = true;
|
||||
badge.removeAttribute("title");
|
||||
return;
|
||||
}
|
||||
if (typeof intent.name !== "string" || !intent.name.trim()) throw new TypeError("预测技能必须有名称");
|
||||
name.textContent = intent.name;
|
||||
const hasDamage = intent.damage != null;
|
||||
damageLine.hidden = !hasDamage;
|
||||
const hasDebuff = typeof intent.debuff === "string" && Number.isInteger(intent.stacks) && intent.stacks > 0;
|
||||
debuffLine.hidden = !hasDebuff;
|
||||
if (hasDebuff) debuffAmount.textContent = `${intent.debuff}×${intent.stacks}`;
|
||||
if (hasDamage) {
|
||||
const tier = attackIconForDamage(intent.damage);
|
||||
amount.textContent = String(intent.damage);
|
||||
const src = `${assetBase}/assets/intent/Intent_attack_${tier}.webp`;
|
||||
if (icon.getAttribute("src") !== src) icon.src = src;
|
||||
badge.title = `${intent.name}:预计造成 ${intent.damage} 点伤害(非最终伤害)${hasDebuff ? `,施加${intent.stacks}层${intent.debuff}` : ""}`;
|
||||
} else {
|
||||
badge.title = hasDebuff ? `${intent.name}:施加${intent.stacks}层${intent.debuff}` : intent.name;
|
||||
}
|
||||
badge.hidden = false;
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { createCardData } from "./card-data.js";
|
||||
|
||||
// 梦三怪物规则只作用于敌方单位;子类可覆盖构造参数或 createDeck()。
|
||||
const TIER_RULES = Object.freeze({
|
||||
normal: Object.freeze({ draw: 1, handLimit: 3, energy: 2 }),
|
||||
@@ -13,6 +15,7 @@ const BASE_DECKS = Object.freeze({
|
||||
|
||||
const HEALTH = Object.freeze({
|
||||
mengsan_soldier_shuying: 24,
|
||||
mengsan_flyconid_shuying: 48,
|
||||
re_xiahoudun: 30, re_zhangliao: 28, re_xuzhu: 34, re_lvbu: 120,
|
||||
re_ganning: 39, re_zhangfei: 42, re_huangzhong: 37, re_weiyan: 40,
|
||||
re_guanyu: 65, dianwei: 62, shen_guanyu: 165,
|
||||
@@ -43,12 +46,12 @@ export class Monster {
|
||||
createDeck(prefix) {
|
||||
return this.deck.map((entry, index) => {
|
||||
const card = typeof entry === "string" ? { name: entry } : entry;
|
||||
return {
|
||||
return createCardData({
|
||||
id: `${prefix}_${index}`, name: card.name,
|
||||
suit: card.suit || ["spade", "heart", "club", "diamond"][index % 4],
|
||||
number: card.number || index % 13 + 1,
|
||||
nature: card.nature || null, affixes: Array.isArray(card.affixes) ? card.affixes.slice() : [], upgrade: card.upgrade || 0,
|
||||
};
|
||||
}, { enemy: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -57,9 +60,12 @@ export class Monster {
|
||||
export class OpeningSoldier extends Monster {
|
||||
constructor(options = {}) { super({ character: "mengsan_soldier_shuying", tier: "normal", hp: 24, ...options }); }
|
||||
}
|
||||
export class Flyconid extends Monster {
|
||||
constructor(options = {}) { super({ ...options, character: "mengsan_flyconid_shuying", tier: "normal", hp: 48 }); }
|
||||
}
|
||||
|
||||
// 新怪物子类在这里注册,未注册的角色使用 Monster 的阶级默认值。
|
||||
export const MONSTER_TYPES = Object.freeze({ mengsan_soldier_shuying: OpeningSoldier });
|
||||
export const MONSTER_TYPES = Object.freeze({ mengsan_soldier_shuying: OpeningSoldier, mengsan_flyconid_shuying: Flyconid });
|
||||
|
||||
export function createMonster(spec) {
|
||||
const Type = Object.hasOwn(MONSTER_TYPES, spec.character) ? MONSTER_TYPES[spec.character] : Monster;
|
||||
|
||||
@@ -13,7 +13,7 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
const draw = document.createElement("div"), discard = document.createElement("div");
|
||||
// Detached nodes: engine get.position recognises these IDs; no duplicate document IDs.
|
||||
draw.id = "cardPile"; discard.id = "discardPile";
|
||||
const owned = new Set(), restores = [];
|
||||
const owned = new Set(), restores = [], costBadges = new Map();
|
||||
const strict = env.strict !== false;
|
||||
let released = false, forcedOwner = false;
|
||||
const personal = () => {
|
||||
@@ -27,6 +27,26 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
return false; // Never infer ownership from whose turn it is.
|
||||
};
|
||||
const refresh = () => { if (!released) env.refresh(); };
|
||||
function showCost(card) {
|
||||
if (!env.showCosts || !card?.name) return;
|
||||
const cost = String(cardCost(card));
|
||||
card.dataset.mengsanCost = cost;
|
||||
let badge = costBadges.get(card);
|
||||
if (!badge) {
|
||||
badge = Array.from(card.children || []).find(child => child.classList?.contains("mengsan-hand-cost-shuying"));
|
||||
if (!badge) {
|
||||
badge = document.createElement("span");
|
||||
badge.className = "mengsan-hand-cost-shuying";
|
||||
badge.setAttribute("aria-hidden", "true");
|
||||
card.appendChild(badge);
|
||||
}
|
||||
costBadges.set(card, badge);
|
||||
}
|
||||
// 十周年 UI scans thrown-card children via innerText when adding “刘备使用”.
|
||||
// Keep this child text-free so its numeric cost cannot match that scan.
|
||||
badge.textContent = "";
|
||||
badge.dataset.mengsanCost = cost;
|
||||
}
|
||||
function adopt(card, data) {
|
||||
if (owned.has(card)) return;
|
||||
owned.add(card); resources.card(card);
|
||||
@@ -37,9 +57,7 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
// Retired affixes from old saves must not acquire new battle behaviour.
|
||||
card.storage.mengsanCard_shuying.affixes = (card.storage.mengsanCard_shuying.affixes || []).filter(key => key !== "annihilate");
|
||||
}
|
||||
if (data && env.showCosts) {
|
||||
card.dataset.mengsanCost = String(cardCost(data));
|
||||
}
|
||||
if (data) showCost(card);
|
||||
// Existing foreign/generated cards retain their original destruction semantics.
|
||||
if (!data) return;
|
||||
card.destroyLog = false;
|
||||
@@ -66,6 +84,27 @@ export function installPersonalPiles(session, owner, battle, run, resources, env
|
||||
const card = game.createCard(data.name, data.suit, data.number, data.nature);
|
||||
adopt(card, data); draw.appendChild(card);
|
||||
}
|
||||
// Native and 十周年 UI both move real card nodes into these hand containers.
|
||||
// Decorate newly gained cards without patching either UI's card factory or layout.
|
||||
const handZones = [owner.node?.handcards1, owner.node?.handcards2].filter(Boolean);
|
||||
const handObserver = new MutationObserver(mutations => {
|
||||
if (released) return;
|
||||
for (const mutation of mutations) for (const card of mutation.addedNodes) {
|
||||
if (card.classList?.contains("card")) showCost(card);
|
||||
}
|
||||
});
|
||||
for (const zone of handZones) {
|
||||
for (const card of zone.childNodes) if (card.classList?.contains("card")) showCost(card);
|
||||
handObserver.observe(zone, { childList: true });
|
||||
}
|
||||
session.ownResource(handObserver, () => {
|
||||
handObserver.disconnect();
|
||||
for (const [card, badge] of costBadges) {
|
||||
if (badge.parentNode === card) badge.remove();
|
||||
delete card.dataset.mengsanCost;
|
||||
}
|
||||
costBadges.clear();
|
||||
});
|
||||
const publicDiscard = ui.discardPile;
|
||||
for (const [key, node] of [["cardPile", draw], ["discardPile", discard]]) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(ui, key);
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { lib, get } from "../../../../noname.js";
|
||||
import { cardCost } from "./combat-rules.js";
|
||||
import { chooseButtons } from "./flow-ui.js";
|
||||
|
||||
const element = (tag, className, text, parent) => {
|
||||
const node = document.createElement(tag);
|
||||
node.className = className;
|
||||
if (text != null) node.textContent = text;
|
||||
parent?.appendChild(node);
|
||||
return node;
|
||||
};
|
||||
|
||||
const plainText = value => {
|
||||
const template = document.createElement("template");
|
||||
template.innerHTML = String(value ?? "").replace(/<br\s*\/?\s*>/gi, "\n");
|
||||
return template.content.textContent.trim();
|
||||
};
|
||||
|
||||
const cardNameOf = choice => choice?.card?.name || ({ card_sha: "sha", card_tao: "tao" })[choice?.effectId] || null;
|
||||
export const isCardReward = choice => Boolean(cardNameOf(choice));
|
||||
|
||||
const viewCard = choice => {
|
||||
const name = cardNameOf(choice);
|
||||
const info = lib.card[name];
|
||||
const imageName = info?.cardimage || name;
|
||||
const image = /^[a-zA-Z0-9_]+$/.test(imageName) ? `${lib.assetURL || ""}image/card/${imageName}.png` : null;
|
||||
const type = { basic: "基本牌", trick: "锦囊牌", delay: "延时锦囊", equip: "装备牌" }[info?.type] || "卡牌";
|
||||
return {
|
||||
name: get.translation(name), type, image, cost: cardCost({ name }),
|
||||
description: plainText(lib.translate[`${name}_info`] || choice.description || "暂无卡牌介绍。"),
|
||||
};
|
||||
};
|
||||
|
||||
const restoreFocus = previous => { if (previous?.isConnected) previous.focus(); };
|
||||
const RETURN_TO_VICTORY = Symbol("return to victory choice");
|
||||
|
||||
// Reusable card-only selector. It accepts three cards today and can lay out four or five later.
|
||||
// Callers supply stable choice IDs; this UI never creates or awards cards itself.
|
||||
export function chooseCardDialog(choices, { title = "选择一张牌", allowSkip = true, skipLabel = "跳过" } = {}) {
|
||||
if (!Array.isArray(choices) || !choices.length || choices.some(choice => !choice?.id || !isCardReward(choice))) {
|
||||
return Promise.reject(new TypeError("chooseCardDialog requires nonempty card choices with IDs"));
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const previousFocus = document.activeElement;
|
||||
const dialog = element("dialog", "mengsan-card-choice-dialog-shuying", null);
|
||||
dialog.setAttribute("aria-label", title);
|
||||
const stage = element("div", "mengsan-card-choice-stage-shuying", null, dialog);
|
||||
const banner = element("header", "mengsan-card-choice-heading-shuying", null, stage);
|
||||
element("span", "mengsan-reward-overline-shuying", "梦三 · 战利品", banner);
|
||||
element("h2", "", title, banner);
|
||||
element("p", "", "择一入阵,余者留于身后", banner);
|
||||
const cards = element("div", "mengsan-card-choice-grid-shuying", null, stage);
|
||||
cards.style.setProperty("--choice-count", String(Math.min(choices.length, 5)));
|
||||
let settled = false;
|
||||
const finish = id => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (dialog.open) dialog.close();
|
||||
dialog.remove();
|
||||
restoreFocus(previousFocus);
|
||||
resolve(id);
|
||||
};
|
||||
for (const choice of choices) {
|
||||
const view = viewCard(choice);
|
||||
const card = element("button", "mengsan-card-choice-tile-shuying", null, cards);
|
||||
card.type = "button";
|
||||
card.setAttribute("aria-label", `${view.name},${view.type},${view.description}。选择此牌`);
|
||||
card.addEventListener("click", () => finish(choice.id));
|
||||
element("span", "mengsan-card-choice-cost-shuying", String(view.cost), card);
|
||||
element("span", "mengsan-card-choice-name-shuying", view.name, card);
|
||||
const artwork = element("span", "mengsan-card-choice-art-shuying", null, card);
|
||||
if (view.image) {
|
||||
const image = element("img", "", null, artwork);
|
||||
image.src = view.image; image.alt = ""; image.loading = "lazy";
|
||||
image.addEventListener("error", () => image.remove(), { once: true });
|
||||
}
|
||||
element("span", "mengsan-card-choice-glyph-shuying", view.name.slice(0, 1), artwork);
|
||||
element("span", "mengsan-card-choice-kind-shuying", view.type, card);
|
||||
element("span", "mengsan-card-choice-description-shuying", view.description, card);
|
||||
}
|
||||
if (allowSkip) {
|
||||
const skip = element("button", "mengsan-card-choice-skip-shuying", skipLabel, stage);
|
||||
skip.type = "button";
|
||||
skip.addEventListener("click", () => finish(null));
|
||||
}
|
||||
dialog.addEventListener("cancel", event => event.preventDefault());
|
||||
dialog.addEventListener("keydown", event => event.stopPropagation());
|
||||
// The engine scales <body>; attach to <html> so viewport-sized modals stay full-screen.
|
||||
document.documentElement.appendChild(dialog);
|
||||
try { dialog.showModal(); cards.querySelector("button")?.focus(); }
|
||||
catch (error) { dialog.remove(); restoreFocus(previousFocus); reject(error); }
|
||||
});
|
||||
}
|
||||
|
||||
// Reusable post-battle choice surface. Each option owns its follow-up; the surface
|
||||
// only resolves a stable candidate ID (or null) and never awards a reward itself.
|
||||
export function chooseVictoryOptions(options, { title = "战后抉择", allowSkip = true } = {}) {
|
||||
if (!Array.isArray(options) || !options.length || options.some(option => !option?.id || !option?.label)) {
|
||||
return Promise.reject(new TypeError("chooseVictoryOptions requires nonempty options with IDs and labels"));
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const previousFocus = document.activeElement;
|
||||
const dialog = element("dialog", "mengsan-victory-dialog-shuying", null);
|
||||
dialog.setAttribute("aria-label", title);
|
||||
const stage = element("div", "mengsan-victory-stage-shuying", null, dialog);
|
||||
const intro = element("header", "mengsan-victory-intro-shuying", null, stage);
|
||||
element("span", "mengsan-reward-overline-shuying", "梦三 · 战后", intro);
|
||||
element("span", "mengsan-victory-seal-shuying", "胜", intro);
|
||||
element("h2", "", title, intro);
|
||||
element("p", "", "此役已定,选择下一段征途的收获。", intro);
|
||||
const board = element("div", "mengsan-victory-options-shuying", null, stage);
|
||||
element("span", "mengsan-victory-eyebrow-shuying", "可选战利品", board);
|
||||
let settled = false, choosing = false;
|
||||
const finish = id => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (dialog.open) dialog.close();
|
||||
dialog.remove();
|
||||
restoreFocus(previousFocus);
|
||||
resolve(id);
|
||||
};
|
||||
for (const [index, option] of options.entries()) {
|
||||
const entry = element("button", "mengsan-victory-option-shuying", null, board);
|
||||
entry.type = "button";
|
||||
entry.setAttribute("aria-label", `${option.label}。${option.description || ""}`);
|
||||
element("span", "mengsan-victory-option-index-shuying", String(index + 1).padStart(2, "0"), entry);
|
||||
const copy = element("span", "mengsan-victory-option-copy-shuying", null, entry);
|
||||
element("strong", "", option.label, copy);
|
||||
if (option.description) element("small", "", option.description, copy);
|
||||
element("span", "mengsan-victory-option-arrow-shuying", "›", entry).setAttribute("aria-hidden", "true");
|
||||
entry.addEventListener("click", async () => {
|
||||
if (settled || choosing) return;
|
||||
choosing = true;
|
||||
entry.disabled = true;
|
||||
if (dialog.open) dialog.close();
|
||||
try {
|
||||
const selected = option.choose ? await option.choose() : option.id;
|
||||
if (selected === RETURN_TO_VICTORY) {
|
||||
dialog.showModal();
|
||||
entry.disabled = false;
|
||||
choosing = false;
|
||||
entry.focus();
|
||||
} else finish(selected);
|
||||
}
|
||||
catch (error) {
|
||||
if (!settled) { settled = true; dialog.remove(); restoreFocus(previousFocus); reject(error); }
|
||||
}
|
||||
});
|
||||
}
|
||||
if (allowSkip) {
|
||||
const skip = element("button", "mengsan-victory-skip-shuying", "放弃本次选择", stage);
|
||||
skip.type = "button";
|
||||
skip.addEventListener("click", () => { if (!choosing) finish(null); });
|
||||
}
|
||||
dialog.addEventListener("cancel", event => event.preventDefault());
|
||||
dialog.addEventListener("keydown", event => event.stopPropagation());
|
||||
document.documentElement.appendChild(dialog);
|
||||
try { dialog.showModal(); board.querySelector("button")?.focus(); }
|
||||
catch (error) { dialog.remove(); restoreFocus(previousFocus); reject(error); }
|
||||
});
|
||||
}
|
||||
|
||||
// Gold is a persisted candidate, not an automatic reward. The card pool remains
|
||||
// a separate Dialog; skipping that Dialog returns to the victory choices.
|
||||
export function chooseBattleReward(choices) {
|
||||
if (!Array.isArray(choices) || !choices.length) return Promise.resolve(null);
|
||||
const gold = choices.find(choice => choice.kind === "gold");
|
||||
const rewards = choices.filter(choice => choice.kind !== "gold");
|
||||
const options = [];
|
||||
if (gold) options.push({
|
||||
id: gold.id,
|
||||
label: `金币 · ${gold.amount}`,
|
||||
description: "将此关金币收入征程钱袋。",
|
||||
});
|
||||
if (rewards.length) {
|
||||
const cardOnly = rewards.every(isCardReward);
|
||||
options.push({
|
||||
id: "choose-reward",
|
||||
label: cardOnly ? "择一张牌" : "挑选一项战利品",
|
||||
description: cardOnly ? "从本次候选卡牌中选择一张,加入牌组。" : "检视本次战利品,再决定带走哪一项。",
|
||||
async choose() {
|
||||
const selected = cardOnly
|
||||
? await chooseCardDialog(rewards, {skipLabel:"跳过选卡"})
|
||||
: await chooseButtons("选择一项奖励", rewards);
|
||||
return selected ?? RETURN_TO_VICTORY;
|
||||
},
|
||||
});
|
||||
}
|
||||
return chooseVictoryOptions(options);
|
||||
}
|
||||
@@ -1,9 +1,17 @@
|
||||
import config from "../config.js";
|
||||
import { nextRandom } from "../state.js";
|
||||
import { canAcquireCard } from "../content/card-definitions.js";
|
||||
|
||||
export const rewardCardName = reward => reward?.card?.name || ({ card_sha: "sha", card_tao: "tao" })[reward?.effectId] || null;
|
||||
export const canAcquireReward = (run, reward) => {
|
||||
const name = rewardCardName(reward);
|
||||
return !name || canAcquireCard(run?.player?.character, name);
|
||||
};
|
||||
|
||||
export const getRandomRewardChoices = (run, poolId, count = 3) => {
|
||||
const fallbackId = "shared.pool.battle.normal";
|
||||
const pool = (config.rewardPools[poolId] || config.rewardPools[fallbackId]).slice();
|
||||
const pool = (config.rewardPools[poolId] || config.rewardPools[fallbackId])
|
||||
.filter(id => canAcquireReward(run, config.rewards[id])).slice();
|
||||
for (let index = pool.length - 1; index > 0; index--) {
|
||||
const target = Math.floor(nextRandom(run) * (index + 1));
|
||||
[pool[index], pool[target]] = [pool[target], pool[index]];
|
||||
|
||||
@@ -979,6 +979,158 @@
|
||||
.mengsan-flow-panel-shuying .mengsan-choice-description-shuying { display: block; margin-top: 6px; color: var(--flow-muted); font-size: 14px; line-height: 1.6; }
|
||||
.mengsan-flow-panel-shuying .mengsan-choice-shuying:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.mengsan-flow-panel-shuying .mengsan-choice-shuying.danger { border-color: #d89586; background: #853d3440; }
|
||||
|
||||
/* Victory choices and card choices are independent, reusable ink-and-jade surfaces. */
|
||||
dialog.mengsan-victory-dialog-shuying, dialog.mengsan-card-choice-dialog-shuying {
|
||||
--reward-ink: #101c20; --reward-jade: #32736c; --reward-celadon: #8db4a5;
|
||||
--reward-bronze: #c7a477; --reward-paper: #eee2c9; --reward-muted: #cabda5;
|
||||
position: fixed !important; inset: 0 !important; width: 100vw !important; height: 100dvh !important;
|
||||
max-width: none !important; max-height: none !important; margin: 0 !important; padding: 0 !important;
|
||||
border: 0 !important; overflow: auto !important; transform: none !important; background: transparent !important;
|
||||
color: var(--reward-paper); font: 16px/1.5 "Microsoft YaHei", sans-serif; text-align: left;
|
||||
}
|
||||
dialog.mengsan-victory-dialog-shuying::backdrop, dialog.mengsan-card-choice-dialog-shuying::backdrop {
|
||||
background: rgba(4, 10, 13, .88);
|
||||
}
|
||||
.mengsan-victory-dialog-shuying *, .mengsan-card-choice-dialog-shuying *,
|
||||
.mengsan-victory-dialog-shuying *::before, .mengsan-card-choice-dialog-shuying *::before,
|
||||
.mengsan-victory-dialog-shuying *::after, .mengsan-card-choice-dialog-shuying *::after { box-sizing: border-box; }
|
||||
.mengsan-reward-overline-shuying {
|
||||
display: block; color: var(--reward-celadon); font: 700 12px/1.4 "Microsoft YaHei",sans-serif;
|
||||
letter-spacing: .22em;
|
||||
}
|
||||
.mengsan-victory-stage-shuying {
|
||||
position: relative !important; display: grid !important; grid-template-columns: minmax(210px,260px) minmax(320px,520px);
|
||||
grid-template-rows: auto auto; align-content: center; justify-content: center; gap: 0 32px;
|
||||
min-height: 100%; width: 100%; padding: 48px 28px;
|
||||
}
|
||||
.mengsan-victory-stage-shuying::before {
|
||||
content: ""; position: absolute !important; z-index: -1; inset: 0;
|
||||
background: radial-gradient(ellipse at 48% 48%,rgba(46,88,80,.25),transparent 62%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.mengsan-victory-intro-shuying {
|
||||
position: relative !important; grid-column: 1; grid-row: 1 / 3; align-self: center;
|
||||
padding: 26px 26px 28px 0; border-right: 1px solid rgba(199,164,119,.45);
|
||||
}
|
||||
.mengsan-victory-intro-shuying::after {
|
||||
content: ""; display: block; width: 66px; height: 3px; margin-top: 28px; background: var(--reward-bronze);
|
||||
}
|
||||
.mengsan-victory-seal-shuying {
|
||||
display: grid !important; place-items: center; width: 88px; height: 88px; margin: 29px 0 24px;
|
||||
border: 3px double var(--reward-bronze); border-radius: 8px; transform: rotate(-7deg);
|
||||
color: var(--reward-paper); background: #6b302b; box-shadow: 0 8px 24px #0007;
|
||||
font: 700 52px/1 "KaiTi","STKaiti",serif;
|
||||
}
|
||||
.mengsan-victory-intro-shuying h2 {
|
||||
margin: 0 0 8px; color: var(--reward-paper); font: 700 clamp(32px,3.5vw,46px)/1.15 "KaiTi","STKaiti",serif;
|
||||
letter-spacing: .09em;
|
||||
}
|
||||
.mengsan-victory-intro-shuying p { max-width: 17em; margin: 0; color: var(--reward-muted); font-size: 15px; line-height: 1.8; }
|
||||
.mengsan-victory-options-shuying {
|
||||
position: relative !important; grid-column: 2; grid-row: 1; display: flex !important; flex-direction: column !important;
|
||||
gap: 12px; max-height: min(66dvh,560px); padding: 24px; overflow-y: auto;
|
||||
border: 1px solid rgba(199,164,119,.65); border-radius: 7px;
|
||||
background: linear-gradient(145deg,#1e3435,#111f24 68%);
|
||||
box-shadow: inset 0 0 0 5px rgba(9,19,22,.8),0 22px 50px #0008;
|
||||
}
|
||||
.mengsan-victory-eyebrow-shuying { color: var(--reward-bronze); font: 700 13px/1.4 "Microsoft YaHei",sans-serif; letter-spacing: .16em; margin: 0 0 5px; }
|
||||
button.mengsan-victory-option-shuying {
|
||||
position: relative !important; display: flex !important; align-items: center; gap: 18px;
|
||||
width: 100%; min-height: 94px; padding: 17px 18px; border: 1px solid #60857a;
|
||||
border-left: 4px solid var(--reward-bronze); border-radius: 3px; color: var(--reward-paper);
|
||||
background: linear-gradient(110deg,#254b49,#1a3238); text-align: left; cursor: pointer;
|
||||
transition: background .18s ease, border-color .18s ease, box-shadow .18s ease;
|
||||
}
|
||||
button.mengsan-victory-option-shuying:hover, button.mengsan-victory-option-shuying:focus-visible {
|
||||
border-color: var(--reward-paper); background: linear-gradient(110deg,#346a60,#24434a);
|
||||
box-shadow: inset 0 0 0 1px #9dbca9,0 6px 20px #0005;
|
||||
}
|
||||
button.mengsan-victory-option-shuying:focus-visible, button.mengsan-victory-skip-shuying:focus-visible,
|
||||
button.mengsan-card-choice-tile-shuying:focus-visible, button.mengsan-card-choice-skip-shuying:focus-visible {
|
||||
outline: 3px solid #d9e9bb; outline-offset: 4px;
|
||||
}
|
||||
button.mengsan-victory-option-shuying:disabled { opacity: .6; cursor: wait; }
|
||||
.mengsan-victory-option-index-shuying { flex: 0 0 44px; color: var(--reward-bronze); font: 700 24px/1 "KaiTi",serif; }
|
||||
.mengsan-victory-option-copy-shuying { display: flex !important; flex: 1; min-width: 0; flex-direction: column; gap: 4px; }
|
||||
.mengsan-victory-option-copy-shuying strong { color: #fff1d5; font: 700 23px/1.2 "KaiTi",serif; }
|
||||
.mengsan-victory-option-copy-shuying small { color: #d3e0d4; font: 14px/1.5 "Microsoft YaHei",sans-serif; }
|
||||
.mengsan-victory-option-arrow-shuying { flex: 0 0 28px; color: var(--reward-bronze); font: 34px/1 "KaiTi",serif; text-align: center; }
|
||||
button.mengsan-victory-skip-shuying {
|
||||
grid-column: 2; grid-row: 2; justify-self: end; min-height: 48px; margin-top: 16px; padding: 8px 4px;
|
||||
border: 0; border-bottom: 1px solid var(--reward-muted); color: var(--reward-muted);
|
||||
background: transparent; font: 16px/1.4 "Microsoft YaHei",sans-serif; cursor: pointer;
|
||||
}
|
||||
button.mengsan-victory-skip-shuying:hover { color: #fff1d5; border-color: #fff1d5; }
|
||||
.mengsan-card-choice-stage-shuying {
|
||||
position: relative !important; display: flex !important; flex-direction: column !important; align-items: center;
|
||||
justify-content: center; gap: clamp(20px,4vh,38px); min-height: 100%; width: 100%; padding: 36px 22px 48px;
|
||||
background: radial-gradient(ellipse at 50% 48%,rgba(47,80,73,.21),transparent 68%);
|
||||
}
|
||||
.mengsan-card-choice-heading-shuying { width: min(620px,100%); text-align: center; }
|
||||
.mengsan-card-choice-heading-shuying::after { content: ""; display: block; width: 132px; height: 2px; margin: 15px auto 0; background: linear-gradient(90deg,transparent,var(--reward-bronze),transparent); }
|
||||
.mengsan-card-choice-heading-shuying h2 { margin: 7px 0 4px; color: var(--reward-paper); font: 700 clamp(28px,3vw,38px)/1.2 "KaiTi","STKaiti",serif; letter-spacing: .13em; }
|
||||
.mengsan-card-choice-heading-shuying p { margin: 0; color: var(--reward-muted); font-size: 14px; }
|
||||
.mengsan-card-choice-grid-shuying {
|
||||
position: relative !important; display: grid !important; grid-template-columns: repeat(var(--choice-count),minmax(0,210px));
|
||||
justify-content: center; align-items: stretch; gap: 18px; width: max-content; max-width: 100%;
|
||||
}
|
||||
button.mengsan-card-choice-tile-shuying {
|
||||
position: relative !important; display: flex !important; flex-direction: column !important; align-items: stretch;
|
||||
min-width: 0; min-height: 322px; padding: 11px; border: 1px solid #9e8261; border-radius: 6px;
|
||||
color: var(--reward-paper); background: linear-gradient(155deg,#355550,#1b3034 48%,#182429);
|
||||
box-shadow: inset 0 0 0 4px #1a282a,inset 0 0 0 5px #57746c,0 12px 25px #0009;
|
||||
font: inherit; text-align: center; appearance: none; cursor: pointer;
|
||||
transition: border-color .18s ease, box-shadow .18s ease, transform .18s ease;
|
||||
}
|
||||
button.mengsan-card-choice-tile-shuying:hover, button.mengsan-card-choice-tile-shuying:focus-visible {
|
||||
border-color: #f4d7a0; box-shadow: inset 0 0 0 4px #1a282a,inset 0 0 0 5px #b59667,0 16px 28px #000a;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
.mengsan-card-choice-cost-shuying {
|
||||
position: absolute !important; z-index: 2; top: -13px; left: -12px; display: grid !important; place-items: center;
|
||||
width: 38px; height: 38px; border: 2px solid #e2c38c; border-radius: 50%; color: #fff1d5;
|
||||
background: #7a3b31; font: 700 19px/1 "KaiTi",serif; box-shadow: 0 3px 9px #0009;
|
||||
}
|
||||
.mengsan-card-choice-name-shuying { display: block !important; min-height: 32px; padding: 2px 6px 5px; color: #f4e6c5; border-bottom: 1px solid #b69a6b; font: 700 20px/1.3 "KaiTi",serif; }
|
||||
.mengsan-card-choice-art-shuying {
|
||||
position: relative !important; display: grid !important; place-items: center; height: 128px; margin: 10px 0 7px;
|
||||
overflow: hidden; border: 1px solid #a98d68; border-radius: 3px;
|
||||
background: radial-gradient(circle,#4f5f55,#192629 70%);
|
||||
}
|
||||
.mengsan-card-choice-art-shuying img { position: absolute !important; z-index: 1; inset: 0; width: 100%; height: 100%; object-fit: contain; background: #263333; }
|
||||
.mengsan-card-choice-glyph-shuying { color: #e9d4ae; font: 700 52px/1 "KaiTi",serif; }
|
||||
.mengsan-card-choice-kind-shuying { align-self: center; margin-bottom: 6px; padding: 1px 12px; border: 1px solid #b99a6b; color: #f0dfba; font-size: 12px; }
|
||||
.mengsan-card-choice-description-shuying { display: block !important; flex: 1; overflow: hidden; padding: 2px 3px 0; color: #e8e5d6; font-size: 13px; line-height: 1.55; }
|
||||
button.mengsan-card-choice-skip-shuying {
|
||||
min-height: 48px; padding: 9px 28px; border: 1px solid #82978b; border-radius: 3px;
|
||||
color: var(--reward-muted); background: #1a2b2f; font: 16px/1.4 "Microsoft YaHei",sans-serif; cursor: pointer;
|
||||
}
|
||||
button.mengsan-card-choice-skip-shuying:hover { color: #fff1d5; border-color: #d4bc91; background: #27433f; }
|
||||
@media (max-width: 1100px) { .mengsan-card-choice-grid-shuying { grid-template-columns: repeat(var(--choice-count),minmax(0,180px)); gap: 14px; } }
|
||||
@media (max-width: 700px) {
|
||||
.mengsan-victory-stage-shuying { grid-template-columns: minmax(0,520px); gap: 18px; align-content: start; padding: 30px 18px; }
|
||||
.mengsan-victory-intro-shuying { grid-column: 1; grid-row: 1; padding: 0 0 15px; border-right: 0; border-bottom: 1px solid rgba(199,164,119,.45); }
|
||||
.mengsan-victory-seal-shuying { display: none !important; }
|
||||
.mengsan-victory-intro-shuying::after { display: none; }
|
||||
.mengsan-victory-intro-shuying h2 { margin-top: 8px; }
|
||||
.mengsan-victory-options-shuying { grid-column: 1; grid-row: 2; max-height: none; padding: 18px; }
|
||||
button.mengsan-victory-skip-shuying { grid-column: 1; grid-row: 3; }
|
||||
.mengsan-card-choice-stage-shuying { justify-content: flex-start; }
|
||||
.mengsan-card-choice-grid-shuying { grid-template-columns: repeat(3,minmax(0,1fr)); width: min(620px,100%); }
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.mengsan-card-choice-grid-shuying { grid-template-columns: repeat(2,minmax(0,1fr)); gap: 15px; }
|
||||
button.mengsan-card-choice-tile-shuying { min-height: 292px; }
|
||||
.mengsan-card-choice-art-shuying { height: 105px; }
|
||||
.mengsan-victory-option-index-shuying { flex-basis: 30px; font-size: 20px; }
|
||||
button.mengsan-victory-option-shuying { gap: 10px; padding: 13px; }
|
||||
}
|
||||
@media (max-width: 350px) { .mengsan-card-choice-grid-shuying { grid-template-columns: minmax(0,210px); } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
button.mengsan-victory-option-shuying, button.mengsan-card-choice-tile-shuying { transition: none; }
|
||||
button.mengsan-card-choice-tile-shuying:hover, button.mengsan-card-choice-tile-shuying:focus-visible { transform: none; }
|
||||
}
|
||||
.mengsan-flow-menu-shuying {
|
||||
position: absolute !important; top: max(16px, env(safe-area-inset-top)); right: max(20px, env(safe-area-inset-right));
|
||||
z-index: 4; min-width: 96px; min-height: 48px; padding: 8px 16px;
|
||||
@@ -1123,11 +1275,14 @@ dialog.mengsan-deck-dialog-shuying::backdrop { background: #080706b8; }
|
||||
.mengsan-gm-dialog-shuying .ms-gm-affixes button:disabled { opacity: .55; cursor: not-allowed; }
|
||||
@media (max-height: 500px) { .mengsan-battle-piles-button-shuying { top: 70px; right: max(8px, env(safe-area-inset-right)); } }
|
||||
|
||||
/* Only session-created Mengsan players; retain internal identity and side. */
|
||||
/* Only session-created Mengsan players; retain internal identity and seat order. */
|
||||
.player.mengsan-hide-identity-shuying > .identity {
|
||||
display: none !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
.player.mengsan-hide-identity-shuying > .seat {
|
||||
display: none !important;
|
||||
}
|
||||
.player > .mengsan-energy-shuying {
|
||||
position: absolute !important;
|
||||
top: -22px !important;
|
||||
@@ -1146,17 +1301,77 @@ dialog.mengsan-deck-dialog-shuying::backdrop { background: #080706b8; }
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.card[data-mengsan-cost]::after {
|
||||
content: attr(data-mengsan-cost) "费";
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
z-index: 5;
|
||||
padding: 1px 3px;
|
||||
border-radius: 3px;
|
||||
background: #173b43e8;
|
||||
color: #fff1c5;
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
/* The selected skill appears below an enemy's frame; number precedes the weapon icon. */
|
||||
.player > .mengsan-intent-shuying {
|
||||
position: absolute !important;
|
||||
bottom: -31px !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%) !important;
|
||||
z-index: 9;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
width: max-content !important;
|
||||
max-width: 145px;
|
||||
min-height: 25px;
|
||||
padding: 2px 5px;
|
||||
border: 1px solid #a87967;
|
||||
border-radius: 6px;
|
||||
background: #1b1719ef;
|
||||
box-shadow: 0 2px 7px #0009;
|
||||
color: #fff0e4;
|
||||
font: 700 12px/1.2 "Microsoft YaHei", sans-serif;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
.player > .mengsan-intent-shuying[hidden],
|
||||
.mengsan-intent-damage-shuying[hidden],
|
||||
.mengsan-intent-debuff-shuying[hidden] { display: none !important; }
|
||||
.mengsan-intent-name-shuying {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mengsan-intent-damage-shuying {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
color: #ffe4d1;
|
||||
font-size: 15px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.mengsan-intent-debuff-shuying {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
color: #d8aee5;
|
||||
font-size: 11px;
|
||||
}
|
||||
.mengsan-intent-icon-shuying {
|
||||
display: block;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
}
|
||||
/* Own a child badge, not card::after (which 十周年 UI reserves and hides). */
|
||||
.handcards > .card[data-mengsan-cost] > .mengsan-hand-cost-shuying {
|
||||
position: absolute !important;
|
||||
top: 55px !important;
|
||||
left: 3px !important;
|
||||
z-index: 7 !important;
|
||||
display: grid !important;
|
||||
place-items: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid #d8ba83;
|
||||
border-radius: 50%;
|
||||
background: #234844f2;
|
||||
box-shadow: 0 2px 4px #0008;
|
||||
color: #fff1cb;
|
||||
font: 700 14px/1 "Microsoft YaHei", sans-serif;
|
||||
pointer-events: none;
|
||||
}
|
||||
.handcards > .card[data-mengsan-cost] > .mengsan-hand-cost-shuying::before {
|
||||
content: attr(data-mengsan-cost);
|
||||
}
|
||||
|
||||