mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-11 03:20:01 +08:00
fixed # 修补缺失资源,修正已知BUG
This commit is contained in:
+4
-1
@@ -10,4 +10,7 @@ tools/test_update.mjs
|
|||||||
tools/test_intent.mjs
|
tools/test_intent.mjs
|
||||||
|
|
||||||
/mengsan/.planning/
|
/mengsan/.planning/
|
||||||
/mengsan/verification
|
/mengsan/verification
|
||||||
|
/mengsan/assets/sts2/
|
||||||
|
|
||||||
|
/Slay the Spire 2
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
@@ -20,7 +20,7 @@ const act1Map = {
|
|||||||
contentId: "act1.battle.liubeiOpening",
|
contentId: "act1.battle.liubeiOpening",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
enemies: ["mengsan_flyconid_shuying"],
|
enemies: ["mengsan_flyconid_shuying", "mengsan_raider_trio_shuying"],
|
||||||
eliteEnemies: ["re_xiahoudun", "re_zhangliao", "re_xuzhu"],
|
eliteEnemies: ["re_xiahoudun", "re_zhangliao", "re_xuzhu"],
|
||||||
boss: "re_lvbu",
|
boss: "re_lvbu",
|
||||||
baseGold: 20,
|
baseGold: 20,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// 灰机 Wiki:五名劫掠者中随机抽取三名,且同场不重复。
|
||||||
|
export const RAIDER_TRIO_ENCOUNTER = "mengsan_raider_trio_shuying";
|
||||||
|
|
||||||
|
export const RAIDER_MEMBERS = Object.freeze([
|
||||||
|
Object.freeze({ character: "mengsan_raider_brute_shuying", minHp: 30, maxHp: 33 }),
|
||||||
|
Object.freeze({ character: "mengsan_raider_assassin_shuying", minHp: 18, maxHp: 23 }),
|
||||||
|
Object.freeze({ character: "mengsan_raider_axe_shuying", minHp: 20, maxHp: 22 }),
|
||||||
|
Object.freeze({ character: "mengsan_raider_crossbow_shuying", minHp: 18, maxHp: 21 }),
|
||||||
|
Object.freeze({ character: "mengsan_raider_tracker_shuying", minHp: 21, maxHp: 25 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function createRaiderTrioBattlePlan(random) {
|
||||||
|
if (typeof random !== "function") throw new TypeError("劫掠者团伙需要随机数生成器");
|
||||||
|
const members = RAIDER_MEMBERS.slice();
|
||||||
|
const pick = count => {
|
||||||
|
const value = random();
|
||||||
|
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("随机数必须在 [0,1) 内");
|
||||||
|
return Math.floor(value * count);
|
||||||
|
};
|
||||||
|
for (let index = members.length - 1; index > 0; index--) {
|
||||||
|
const other = pick(index + 1);
|
||||||
|
[members[index], members[other]] = [members[other], members[index]];
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
units: members.slice(0, 3).map((member, index) => {
|
||||||
|
const hp = member.minHp + pick(member.maxHp - member.minHp + 1);
|
||||||
|
return { id: `raider_${index + 1}`, character: member.character,
|
||||||
|
camp: "enemy", tier: "normal", hp, maxHp: hp, hand: 4 };
|
||||||
|
}),
|
||||||
|
rules: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -8,6 +8,16 @@ export function createScenarioCharacters(Character) {
|
|||||||
skills: [], img: "image/character/shibing1.jpg" }),
|
skills: [], img: "image/character/shibing1.jpg" }),
|
||||||
mengsan_flyconid_shuying: new Character({ sex: "none", group: "qun", hp: 48, maxHp: 48,
|
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/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" }),
|
||||||
|
mengsan_raider_assassin_shuying: new Character({ sex: "none", group: "qun", hp: 21, maxHp: 21,
|
||||||
|
skills: [], img: "extension/术樱包/mengsan/assets/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" }),
|
||||||
|
mengsan_raider_crossbow_shuying: new Character({ sex: "none", group: "qun", hp: 20, maxHp: 20,
|
||||||
|
skills: [], img: "extension/术樱包/mengsan/assets/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" }),
|
||||||
mengsan_sushuang_zhangshiping_shuying: new Character({ sex: "male", group: "shu", hp: 4, maxHp: 4,
|
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/sushuang-zhangshiping.png", names: "苏|双-张|世平" }),
|
||||||
};
|
};
|
||||||
@@ -16,5 +26,10 @@ export const scenarioTranslations = {
|
|||||||
mengsan_liubei_shuying: "界刘备", mengsan_liubei_shuying_title: "50/50生命 · 无初始技能",
|
mengsan_liubei_shuying: "界刘备", mengsan_liubei_shuying_title: "50/50生命 · 无初始技能",
|
||||||
mengsan_soldier_shuying: "士兵",
|
mengsan_soldier_shuying: "士兵",
|
||||||
mengsan_flyconid_shuying: "飞蝇菌子",
|
mengsan_flyconid_shuying: "飞蝇菌子",
|
||||||
|
mengsan_raider_brute_shuying: "劫掠者暴徒",
|
||||||
|
mengsan_raider_assassin_shuying: "劫掠者刺客",
|
||||||
|
mengsan_raider_axe_shuying: "劫掠者斧手",
|
||||||
|
mengsan_raider_crossbow_shuying: "劫掠者弩手",
|
||||||
|
mengsan_raider_tracker_shuying: "劫掠者追踪手",
|
||||||
mengsan_sushuang_zhangshiping_shuying: "苏双&张世平",
|
mengsan_sushuang_zhangshiping_shuying: "苏双&张世平",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ export const showMap = run => new Promise(resolve => {
|
|||||||
const floorCount = Math.max(...run.map.nodes.map(node =>
|
const floorCount = Math.max(...run.map.nodes.map(node =>
|
||||||
Math.floor(node.floor))) + 1;
|
Math.floor(node.floor))) + 1;
|
||||||
map.style.setProperty("--mengsan-map-min-height",
|
map.style.setProperty("--mengsan-map-min-height",
|
||||||
`${floorCount * 100}px`);
|
`${floorCount * 150}px`);
|
||||||
const landscape = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
const landscape = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
landscape.classList.add("mengsan-ink-landscape-shuying");
|
landscape.classList.add("mengsan-ink-landscape-shuying");
|
||||||
landscape.setAttribute("viewBox", "0 0 800 1200");
|
landscape.setAttribute("viewBox", "0 0 800 1200");
|
||||||
|
|||||||
+96
-19
@@ -20,6 +20,7 @@ import {
|
|||||||
skipStunnedAction,
|
skipStunnedAction,
|
||||||
} from "./runtime/stun-intent.js";
|
} from "./runtime/stun-intent.js";
|
||||||
import { selectFlyconidMove, recordFlyconidAction } from "./runtime/flyconid-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 { PLAYER_ENERGY, PLAYER_HAND_LIMIT, canPayCard, payCard, isActiveCardUse } from "./runtime/combat-rules.js";
|
||||||
import { mountBattlePiles } from "./runtime/card-library.js";
|
import { mountBattlePiles } from "./runtime/card-library.js";
|
||||||
import { mountGMManager } from "./runtime/gm-manager.js";
|
import { mountGMManager } from "./runtime/gm-manager.js";
|
||||||
@@ -287,20 +288,39 @@ const mountEnergy = (player, session) => {
|
|||||||
return refresh;
|
return refresh;
|
||||||
};
|
};
|
||||||
const isFlyconid = player => player?.name === "mengsan_flyconid_shuying" && player.storage?.mengsanCamp_shuying === "enemy";
|
const isFlyconid = player => player?.name === "mengsan_flyconid_shuying" && player.storage?.mengsanCamp_shuying === "enemy";
|
||||||
const planFlyconidIntent = (player, run) => {
|
const isRaider = player => player?.storage?.mengsanCamp_shuying === "enemy" && isRaiderCharacter(player.name);
|
||||||
if (!isFlyconid(player) || !player.isAlive() || player.storage.mengsanFlyconidIntent_shuying) return;
|
const planEnemyIntent = (player, run) => {
|
||||||
const move = selectFlyconidMove(player.storage.mengsanFlyconidState_shuying || {}, () => nextRandom(run));
|
if (!player?.isAlive() || isStunned(player)) return;
|
||||||
player.storage.mengsanFlyconidIntent_shuying = move;
|
if (isFlyconid(player)) {
|
||||||
game.mengsanSetEnemyIntent_shuying(player, move);
|
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 applyFlyconidDebuff = (target, move) => {
|
const applyIntentDebuff = (target, move) => {
|
||||||
if (!move.debuff) return;
|
if (!move.debuff) return;
|
||||||
const frail = move.id === "frail";
|
const frail = move.debuff === "脆弱";
|
||||||
const key = frail ? "mengsanFrail_shuying" : "mengsanVulnerable_shuying";
|
const key = frail ? "mengsanFrail_shuying" : "mengsanVulnerable_shuying";
|
||||||
target.storage[key] = (target.storage[key] || 0) + move.stacks;
|
target.storage[key] = (target.storage[key] || 0) + move.stacks;
|
||||||
target.addSkill(frail ? "mengsan_frail_shuying" : "mengsan_vulnerable_shuying");
|
target.addSkill(frail ? "mengsan_frail_shuying" : "mengsan_vulnerable_shuying");
|
||||||
target.markSkill(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 executeFlyconidIntent = async player => {
|
||||||
const move = player.storage.mengsanFlyconidIntent_shuying;
|
const move = player.storage.mengsanFlyconidIntent_shuying;
|
||||||
if (!move) return;
|
if (!move) return;
|
||||||
@@ -318,15 +338,33 @@ const executeFlyconidIntent = async player => {
|
|||||||
await beginDeathBlow(player, target, move);
|
await beginDeathBlow(player, target, move);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (move.damage != null) {
|
await dealIntentDamage(player, target, move);
|
||||||
for (let index = 0; index < attackHitCount(move); index++) {
|
if (target.isAlive() && player.isAlive()) applyIntentDebuff(target, move);
|
||||||
if (!target.isAlive() || !player.isAlive()) break;
|
};
|
||||||
const hit = target.damage(outgoingAttackDamage(move, player), player);
|
const executeRaiderIntent = async player => {
|
||||||
hit.mengsanAttack_shuying = true;
|
const move = player.storage.mengsanRaiderIntent_shuying;
|
||||||
await hit;
|
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");
|
||||||
}
|
}
|
||||||
if (target.isAlive() && player.isAlive()) applyFlyconidDebuff(target, move);
|
|
||||||
};
|
};
|
||||||
const equipBattleUnit = async (player, spec, resources) => {
|
const equipBattleUnit = async (player, spec, resources) => {
|
||||||
for (const info of spec.equipment || []) {
|
for (const info of spec.equipment || []) {
|
||||||
@@ -358,7 +396,7 @@ const createScenario = (plan, current) => {
|
|||||||
await equipBattleUnit(player, spec, current.resources);
|
await equipBattleUnit(player, spec, current.resources);
|
||||||
await player.draw(spec.hand ?? 4);
|
await player.draw(spec.hand ?? 4);
|
||||||
await game.triggerEnter(player);
|
await game.triggerEnter(player);
|
||||||
if (monster && current.session.active) planFlyconidIntent(player, _status.mengsanRun_shuying);
|
if (monster && current.session.active) planEnemyIntent(player, _status.mengsanRun_shuying);
|
||||||
});
|
});
|
||||||
await join;
|
await join;
|
||||||
game.log(player, "作为", spec.camp === "ally" ? "友方支援" : "敌方援军", "加入战斗");
|
game.log(player, "作为", spec.camp === "ally" ? "友方支援" : "敌方援军", "加入战斗");
|
||||||
@@ -400,6 +438,8 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
|
|||||||
if (effect.type === "skill" && !lib.skill[effect.skill]) throw new Error("关卡增益技能未加载:" + effect.skill);
|
if (effect.type === "skill" && !lib.skill[effect.skill]) throw new Error("关卡增益技能未加载:" + effect.skill);
|
||||||
}
|
}
|
||||||
consumeSupports(run);
|
consumeSupports(run);
|
||||||
|
document.body.classList.add("mengsan-battle-ui-shuying");
|
||||||
|
session.ownResource({}, () => document.body.classList.remove("mengsan-battle-ui-shuying"));
|
||||||
resources.field(_status, "mengsanRun_shuying", run);
|
resources.field(_status, "mengsanRun_shuying", run);
|
||||||
resources.field(_status, "mengsanNode_shuying", node);
|
resources.field(_status, "mengsanNode_shuying", node);
|
||||||
resources.field(_status, "mengsanEncounter_shuying", encounter);
|
resources.field(_status, "mengsanEncounter_shuying", encounter);
|
||||||
@@ -460,7 +500,8 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
|
|||||||
"mengsan_draw_shuying", "mengsan_card_use_shuying",
|
"mengsan_draw_shuying", "mengsan_card_use_shuying",
|
||||||
"mengsan_card_affixes_shuying", "mengsan_scenario_shuying",
|
"mengsan_card_affixes_shuying", "mengsan_scenario_shuying",
|
||||||
"mengsan_card_payment_shuying", "mengsan_monster_draw_shuying",
|
"mengsan_card_payment_shuying", "mengsan_monster_draw_shuying",
|
||||||
"mengsan_flyconid_action_shuying",
|
"mengsan_flyconid_action_shuying", "mengsan_raider_action_shuying",
|
||||||
|
"mengsan_raider_card_strength_shuying",
|
||||||
"mengsan_death_blow_finish_shuying",
|
"mengsan_death_blow_finish_shuying",
|
||||||
"mengsan_stun_skip_shuying", "mengsan_stun_recover_shuying",
|
"mengsan_stun_skip_shuying", "mengsan_stun_recover_shuying",
|
||||||
"mengsan_stun_clear_shuying",
|
"mengsan_stun_clear_shuying",
|
||||||
@@ -496,13 +537,14 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
|
|||||||
openingHandChecked = true;
|
openingHandChecked = true;
|
||||||
currentBattle.personalPiles.trimOpeningHand(4);
|
currentBattle.personalPiles.trimOpeningHand(4);
|
||||||
}
|
}
|
||||||
|
if (isRaider(player) && player.hujia > 0) await player.changeHujia(-player.hujia);
|
||||||
if (player.storage.mengsanMaxEnergy_shuying != null) {
|
if (player.storage.mengsanMaxEnergy_shuying != null) {
|
||||||
player.storage.mengsanEnergy_shuying = player.storage.mengsanMaxEnergy_shuying;
|
player.storage.mengsanEnergy_shuying = player.storage.mengsanMaxEnergy_shuying;
|
||||||
currentBattle.energyUI.get(player)?.();
|
currentBattle.energyUI.get(player)?.();
|
||||||
}
|
}
|
||||||
await currentBattle.director.beforeTurn(player);
|
await currentBattle.director.beforeTurn(player);
|
||||||
if (player === me && session.active) {
|
if (player === me && session.active) {
|
||||||
for (const enemy of game.players) planFlyconidIntent(enemy, run);
|
for (const enemy of game.players) planEnemyIntent(enemy, run);
|
||||||
}
|
}
|
||||||
game.checkResult();
|
game.checkResult();
|
||||||
}}));
|
}}));
|
||||||
@@ -586,6 +628,7 @@ const setupBattle = async (run, node, encounter = getNodeEncounter(run, node)) =
|
|||||||
await signal; // External controller, never an engine event content await.
|
await signal; // External controller, never an engine event content await.
|
||||||
if (current.engineError) {
|
if (current.engineError) {
|
||||||
session.requestStop();
|
session.requestStop();
|
||||||
|
document.body.classList.remove("mengsan-battle-ui-shuying");
|
||||||
await chooseButtons("战斗流程异常", [{id:"exit",name:"保留存档并返回模式选择"}], "无法确认旧事件已退出,已停止继续开战。上次成功存档仍保留。");
|
await chooseButtons("战斗流程异常", [{id:"exit",name:"保留存档并返回模式选择"}], "无法确认旧事件已退出,已停止继续开战。上次成功存档仍保留。");
|
||||||
await openModeSelection(); return;
|
await openModeSelection(); return;
|
||||||
}
|
}
|
||||||
@@ -709,8 +752,10 @@ const createMode = identityMode => {
|
|||||||
if (isStunned(player)) return false;
|
if (isStunned(player)) return false;
|
||||||
const { resume = "advance", recover } = options;
|
const { resume = "advance", recover } = options;
|
||||||
const flyconid = isFlyconid(player);
|
const flyconid = isFlyconid(player);
|
||||||
|
const raider = isRaider(player);
|
||||||
const intent = options.intent ?? (flyconid ?
|
const intent = options.intent ?? (flyconid ?
|
||||||
player.storage.mengsanFlyconidIntent_shuying : null);
|
player.storage.mengsanFlyconidIntent_shuying : raider ?
|
||||||
|
player.storage.mengsanRaiderIntent_shuying : null);
|
||||||
const restore = recover || (flyconid ?
|
const restore = recover || (flyconid ?
|
||||||
(choice, original) => {
|
(choice, original) => {
|
||||||
const state =
|
const state =
|
||||||
@@ -720,6 +765,11 @@ const createMode = identityMode => {
|
|||||||
const next = choice === "retry" ? original : null;
|
const next = choice === "retry" ? original : null;
|
||||||
player.storage.mengsanFlyconidIntent_shuying = next;
|
player.storage.mengsanFlyconidIntent_shuying = next;
|
||||||
return next;
|
return next;
|
||||||
|
} : raider ? (choice, original) => {
|
||||||
|
player.storage.mengsanRaiderState_shuying = recordRaiderAction(player.storage.mengsanRaiderState_shuying || {});
|
||||||
|
const next = choice === "retry" ? original : null;
|
||||||
|
player.storage.mengsanRaiderIntent_shuying = next;
|
||||||
|
return next;
|
||||||
} : null);
|
} : null);
|
||||||
const onRecover = async (choice, original) => {
|
const onRecover = async (choice, original) => {
|
||||||
const next = await restore(choice, original);
|
const next = await restore(choice, original);
|
||||||
@@ -818,6 +868,29 @@ const createMode = identityMode => {
|
|||||||
event.player.storage.mengsanFlyconidIntent_shuying); },
|
event.player.storage.mengsanFlyconidIntent_shuying); },
|
||||||
async content(event, trigger) { await executeFlyconidIntent(trigger.player); },
|
async content(event, trigger) { await executeFlyconidIntent(trigger.player); },
|
||||||
},
|
},
|
||||||
|
mengsan_raider_action_shuying: {
|
||||||
|
trigger: { global: "phaseUseBefore" },
|
||||||
|
forced: true, silent: true, popup: false, priority: 100,
|
||||||
|
filter(event) { return Boolean(activeBattle?.session.active &&
|
||||||
|
!isStunned(event.player) && isRaider(event.player) &&
|
||||||
|
event.player.storage.mengsanRaiderIntent_shuying); },
|
||||||
|
async content(event, trigger) { await executeRaiderIntent(trigger.player); },
|
||||||
|
},
|
||||||
|
mengsan_raider_card_strength_shuying: {
|
||||||
|
trigger: { source: "damageBegin1" },
|
||||||
|
forced: true, silent: true, popup: false, priority: 90,
|
||||||
|
filter(event, player) { return Boolean(activeBattle?.session.active &&
|
||||||
|
isRaider(player) && player.storage.mengsanStrength_shuying > 0 &&
|
||||||
|
event.card && !event.mengsanScriptedSkill_shuying); },
|
||||||
|
async content(event, trigger, player) {
|
||||||
|
trigger.num += player.storage.mengsanStrength_shuying;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mengsan_raider_strength_shuying: {
|
||||||
|
mark: true, marktext: "力",
|
||||||
|
intro: { content(storage, player) { return `攻击伤害增加${player.storage.mengsanStrength_shuying || 0}点`; } },
|
||||||
|
onremove(player) { delete player.storage.mengsanStrength_shuying; },
|
||||||
|
},
|
||||||
mengsan_stun_skip_shuying: {
|
mengsan_stun_skip_shuying: {
|
||||||
trigger: { global: "phaseUseBefore" },
|
trigger: { global: "phaseUseBefore" },
|
||||||
forced: true, silent: true, popup: false, priority: 200,
|
forced: true, silent: true, popup: false, priority: 200,
|
||||||
@@ -988,6 +1061,10 @@ const createMode = identityMode => {
|
|||||||
mengsan_yingyong_shuying: "英勇",
|
mengsan_yingyong_shuying: "英勇",
|
||||||
mengsan_yingyong_shuying_info: "锁定技,每回合限一次,你于自己的回合内使用牌造成的伤害+1。",
|
mengsan_yingyong_shuying_info: "锁定技,每回合限一次,你于自己的回合内使用牌造成的伤害+1。",
|
||||||
mengsan_flyconid_action_shuying: "孢子行动",
|
mengsan_flyconid_action_shuying: "孢子行动",
|
||||||
|
mengsan_raider_action_shuying: "劫掠行动",
|
||||||
|
mengsan_raider_card_strength_shuying: "力量加成",
|
||||||
|
mengsan_raider_strength_shuying: "力量",
|
||||||
|
mengsan_raider_strength_shuying_info: "攻击造成的伤害按力量层数增加。",
|
||||||
mengsan_frail_shuying: "脆弱",
|
mengsan_frail_shuying: "脆弱",
|
||||||
mengsan_frail_shuying_info: "接下来相应回合获得护甲减少25%(向下取整)。",
|
mengsan_frail_shuying_info: "接下来相应回合获得护甲减少25%(向下取整)。",
|
||||||
mengsan_vulnerable_shuying: "易伤",
|
mengsan_vulnerable_shuying: "易伤",
|
||||||
|
|||||||
@@ -39,7 +39,25 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
|||||||
debuffIcon.src = `${assetBase}/assets/intent/Intent_debuff.png`;
|
debuffIcon.src = `${assetBase}/assets/intent/Intent_debuff.png`;
|
||||||
const debuffAmount = document.createElement("span");
|
const debuffAmount = document.createElement("span");
|
||||||
debuffLine.append(debuffIcon, debuffAmount);
|
debuffLine.append(debuffIcon, debuffAmount);
|
||||||
badge.append(damageLine, debuffLine);
|
const blockLine = document.createElement("span");
|
||||||
|
blockLine.className = "mengsan-intent-block-shuying";
|
||||||
|
const blockIcon = document.createElement("img");
|
||||||
|
blockIcon.className = "mengsan-intent-icon-shuying";
|
||||||
|
blockIcon.alt = "Block";
|
||||||
|
blockIcon.decoding = "async";
|
||||||
|
blockIcon.src = `${assetBase}/assets/intent/Intent_defend.png`;
|
||||||
|
const blockAmount = document.createElement("span");
|
||||||
|
blockLine.append(blockIcon, blockAmount);
|
||||||
|
const buffLine = document.createElement("span");
|
||||||
|
buffLine.className = "mengsan-intent-buff-shuying";
|
||||||
|
const buffIcon = document.createElement("img");
|
||||||
|
buffIcon.className = "mengsan-intent-icon-shuying";
|
||||||
|
buffIcon.alt = "Buff";
|
||||||
|
buffIcon.decoding = "async";
|
||||||
|
buffIcon.src = `${assetBase}/assets/intent/Intent_buff.png`;
|
||||||
|
const buffAmount = document.createElement("span");
|
||||||
|
buffLine.append(buffIcon, buffAmount);
|
||||||
|
badge.append(damageLine, debuffLine, blockLine, buffLine);
|
||||||
player.appendChild(badge);
|
player.appendChild(badge);
|
||||||
let currentIntent = null;
|
let currentIntent = null;
|
||||||
let timer;
|
let timer;
|
||||||
@@ -56,6 +74,8 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
|||||||
if (isStunIntent(intent)) {
|
if (isStunIntent(intent)) {
|
||||||
damageLine.hidden = false;
|
damageLine.hidden = false;
|
||||||
debuffLine.hidden = true;
|
debuffLine.hidden = true;
|
||||||
|
blockLine.hidden = true;
|
||||||
|
buffLine.hidden = true;
|
||||||
amount.textContent = "";
|
amount.textContent = "";
|
||||||
const src = `${assetBase}/assets/intent/Intent_stun.png`;
|
const src = `${assetBase}/assets/intent/Intent_stun.png`;
|
||||||
if (icon.getAttribute("src") !== src) icon.src = src;
|
if (icon.getAttribute("src") !== src) icon.src = src;
|
||||||
@@ -76,13 +96,20 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
|||||||
Number.isInteger(intent.stacks) && intent.stacks > 0;
|
Number.isInteger(intent.stacks) && intent.stacks > 0;
|
||||||
debuffLine.hidden = !hasDebuff;
|
debuffLine.hidden = !hasDebuff;
|
||||||
if (hasDebuff) {
|
if (hasDebuff) {
|
||||||
const label = intent.id === "frail" ? "Frail" :
|
const label = intent.debuff === "脆弱" ? "Frail" :
|
||||||
intent.id === "vulnerable" ? "Vulnerable" : "Debuff";
|
intent.debuff === "易伤" ? "Vulnerable" : "Debuff";
|
||||||
const debuffText = `${label} ×${intent.stacks}`;
|
const debuffText = `${label} ×${intent.stacks}`;
|
||||||
if (debuffAmount.textContent !== debuffText) {
|
if (debuffAmount.textContent !== debuffText) {
|
||||||
debuffAmount.textContent = debuffText;
|
debuffAmount.textContent = debuffText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const hasBlock = Number.isInteger(intent.block) && intent.block > 0;
|
||||||
|
const hasStrength = Number.isInteger(intent.strength) && intent.strength > 0;
|
||||||
|
blockLine.hidden = !hasBlock;
|
||||||
|
buffLine.hidden = !hasStrength;
|
||||||
|
if (hasBlock) blockAmount.textContent = String(intent.block);
|
||||||
|
if (hasStrength) buffAmount.textContent = `力量 ×${intent.strength}`;
|
||||||
|
const effects = [];
|
||||||
if (hasDamage) {
|
if (hasDamage) {
|
||||||
const hits = attackHitCount(intent);
|
const hits = attackHitCount(intent);
|
||||||
if (deathBlow && hits !== 1) {
|
if (deathBlow && hits !== 1) {
|
||||||
@@ -101,19 +128,13 @@ export function mountEnemyIntent(player, session, assetBase, document,
|
|||||||
`${assetBase}/assets/intent/Intent_attack_${tier}.png`;
|
`${assetBase}/assets/intent/Intent_attack_${tier}.png`;
|
||||||
if (icon.getAttribute("src") !== src) icon.src = src;
|
if (icon.getAttribute("src") !== src) icon.src = src;
|
||||||
icon.alt = deathBlow ? "Death Blow" : "Attack";
|
icon.alt = deathBlow ? "Death Blow" : "Attack";
|
||||||
const label = `${deathBlow ? "Death Blow" : "Attack"}: ` +
|
effects.push(`${deathBlow ? "Death Blow" : "Attack"}: ${damage} damage${hits > 1 ? ` × ${hits} hits` : ""}`);
|
||||||
`${damage} damage${hits > 1 ?
|
|
||||||
` × ${hits} hits` : ""}${hasDebuff ?
|
|
||||||
`, ${debuffAmount.textContent}` : ""}`;
|
|
||||||
if (badge.getAttribute("aria-label") !== label) {
|
|
||||||
badge.setAttribute("aria-label", label);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const label = debuffAmount.textContent;
|
|
||||||
if (badge.getAttribute("aria-label") !== label) {
|
|
||||||
badge.setAttribute("aria-label", label);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (hasDebuff) effects.push(debuffAmount.textContent);
|
||||||
|
if (hasBlock) effects.push(`Block ${intent.block}`);
|
||||||
|
if (hasStrength) effects.push(`Strength +${intent.strength}`);
|
||||||
|
const label = effects.join(", ") || intent.name || "Intent";
|
||||||
|
if (badge.getAttribute("aria-label") !== label) badge.setAttribute("aria-label", label);
|
||||||
badge.hidden = false;
|
badge.hidden = false;
|
||||||
};
|
};
|
||||||
timer = setInterval(refresh, 100);
|
timer = setInterval(refresh, 100);
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ const BASE_DECKS = Object.freeze({
|
|||||||
const HEALTH = Object.freeze({
|
const HEALTH = Object.freeze({
|
||||||
mengsan_soldier_shuying: 24,
|
mengsan_soldier_shuying: 24,
|
||||||
mengsan_flyconid_shuying: 48,
|
mengsan_flyconid_shuying: 48,
|
||||||
|
mengsan_raider_brute_shuying: 32,
|
||||||
|
mengsan_raider_assassin_shuying: 21,
|
||||||
|
mengsan_raider_axe_shuying: 21,
|
||||||
|
mengsan_raider_crossbow_shuying: 20,
|
||||||
|
mengsan_raider_tracker_shuying: 23,
|
||||||
re_xiahoudun: 30, re_zhangliao: 28, re_xuzhu: 34, re_lvbu: 120,
|
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_ganning: 39, re_zhangfei: 42, re_huangzhong: 37, re_weiyan: 40,
|
||||||
re_guanyu: 65, dianwei: 62, shen_guanyu: 165,
|
re_guanyu: 65, dianwei: 62, shen_guanyu: 165,
|
||||||
@@ -63,9 +68,25 @@ export class OpeningSoldier extends Monster {
|
|||||||
export class Flyconid extends Monster {
|
export class Flyconid extends Monster {
|
||||||
constructor(options = {}) { super({ ...options, character: "mengsan_flyconid_shuying", tier: "normal", hp: 48 }); }
|
constructor(options = {}) { super({ ...options, character: "mengsan_flyconid_shuying", tier: "normal", hp: 48 }); }
|
||||||
}
|
}
|
||||||
|
const raiderClass = (character, hp) => class extends Monster {
|
||||||
|
constructor(options = {}) { super({ ...options, character, tier: "normal", hp }); }
|
||||||
|
};
|
||||||
|
export const RaiderBrute = raiderClass("mengsan_raider_brute_shuying", 32);
|
||||||
|
export const RaiderAssassin = raiderClass("mengsan_raider_assassin_shuying", 21);
|
||||||
|
export const RaiderAxe = raiderClass("mengsan_raider_axe_shuying", 21);
|
||||||
|
export const RaiderCrossbow = raiderClass("mengsan_raider_crossbow_shuying", 20);
|
||||||
|
export const RaiderTracker = raiderClass("mengsan_raider_tracker_shuying", 23);
|
||||||
|
|
||||||
// 新怪物子类在这里注册,未注册的角色使用 Monster 的阶级默认值。
|
// 新怪物子类在这里注册,未注册的角色使用 Monster 的阶级默认值。
|
||||||
export const MONSTER_TYPES = Object.freeze({ mengsan_soldier_shuying: OpeningSoldier, mengsan_flyconid_shuying: Flyconid });
|
export const MONSTER_TYPES = Object.freeze({
|
||||||
|
mengsan_soldier_shuying: OpeningSoldier,
|
||||||
|
mengsan_flyconid_shuying: Flyconid,
|
||||||
|
mengsan_raider_brute_shuying: RaiderBrute,
|
||||||
|
mengsan_raider_assassin_shuying: RaiderAssassin,
|
||||||
|
mengsan_raider_axe_shuying: RaiderAxe,
|
||||||
|
mengsan_raider_crossbow_shuying: RaiderCrossbow,
|
||||||
|
mengsan_raider_tracker_shuying: RaiderTracker,
|
||||||
|
});
|
||||||
|
|
||||||
export function createMonster(spec) {
|
export function createMonster(spec) {
|
||||||
const Type = Object.hasOwn(MONSTER_TYPES, spec.character) ? MONSTER_TYPES[spec.character] : Monster;
|
const Type = Object.hasOwn(MONSTER_TYPES, spec.character) ? MONSTER_TYPES[spec.character] : Monster;
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// 普通难度数值;团伙每名成员仅预测并执行自己的一项技能,随后照常出牌。
|
||||||
|
export const RAIDER_MOVES = Object.freeze({
|
||||||
|
beat: Object.freeze({ id: "beat", name: "殴打", damage: 7 }),
|
||||||
|
roar: Object.freeze({ id: "roar", name: "怒吼", strength: 3 }),
|
||||||
|
killshot: Object.freeze({ id: "killshot", name: "致命射击", damage: 10 }),
|
||||||
|
swing: Object.freeze({ id: "swing", name: "挥击", damage: 5, block: 5 }),
|
||||||
|
bigSwing: Object.freeze({ id: "bigSwing", name: "大力挥舞", damage: 12 }),
|
||||||
|
reload: Object.freeze({ id: "reload", name: "装填", block: 3 }),
|
||||||
|
fire: Object.freeze({ id: "fire", name: "射击!", damage: 14 }),
|
||||||
|
track: Object.freeze({ id: "track", name: "追踪", debuff: "脆弱", stacks: 2 }),
|
||||||
|
hounds: Object.freeze({ id: "hounds", name: "放狗", damage: 1, hits: 8 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RAIDER_CYCLES = Object.freeze({
|
||||||
|
mengsan_raider_brute_shuying: Object.freeze([RAIDER_MOVES.beat, RAIDER_MOVES.roar]),
|
||||||
|
mengsan_raider_assassin_shuying: Object.freeze([RAIDER_MOVES.killshot]),
|
||||||
|
mengsan_raider_axe_shuying: Object.freeze([RAIDER_MOVES.swing, RAIDER_MOVES.swing, RAIDER_MOVES.bigSwing]),
|
||||||
|
mengsan_raider_crossbow_shuying: Object.freeze([RAIDER_MOVES.reload, RAIDER_MOVES.fire]),
|
||||||
|
mengsan_raider_tracker_shuying: Object.freeze([RAIDER_MOVES.track, RAIDER_MOVES.hounds]),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function isRaiderCharacter(character) {
|
||||||
|
return Object.hasOwn(RAIDER_CYCLES, character);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectRaiderMove(character, state = {}) {
|
||||||
|
const cycle = RAIDER_CYCLES[character];
|
||||||
|
if (!cycle) return null;
|
||||||
|
const turn = state.turnsTaken || 0;
|
||||||
|
if (!Number.isInteger(turn) || turn < 0) throw new RangeError("劫掠者行动次数无效");
|
||||||
|
if (character === "mengsan_raider_tracker_shuying") {
|
||||||
|
return turn === 0 ? RAIDER_MOVES.track : RAIDER_MOVES.hounds;
|
||||||
|
}
|
||||||
|
return cycle[turn % cycle.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordRaiderAction(state = {}) {
|
||||||
|
return { turnsTaken: (state.turnsTaken || 0) + 1 };
|
||||||
|
}
|
||||||
+6
-3
@@ -1,5 +1,6 @@
|
|||||||
import { createStartingDeck } from "./content/card-library.js";
|
import { createStartingDeck } from "./content/card-library.js";
|
||||||
import config from "./config.js";
|
import config from "./config.js";
|
||||||
|
import { RAIDER_TRIO_ENCOUNTER, createRaiderTrioBattlePlan } from "./content/acts/act1/raider-trio.js";
|
||||||
|
|
||||||
const hashText = text => {
|
const hashText = text => {
|
||||||
let value = 2166136261;
|
let value = 2166136261;
|
||||||
@@ -245,16 +246,18 @@ export const getNodeEncounter = (run, node, override = null) => {
|
|||||||
if (node.type == "boss") {
|
if (node.type == "boss") {
|
||||||
return { battlePlan: encounter?.battlePlan || act.bossBattlePlan || null, enemy: act.boss, tier: "boss", gold: act.baseGold * 3, boss: true, rewardPool: "shared.pool.boss.premium" };
|
return { battlePlan: encounter?.battlePlan || act.bossBattlePlan || null, enemy: act.boss, tier: "boss", gold: act.baseGold * 3, boss: true, rewardPool: "shared.pool.boss.premium" };
|
||||||
}
|
}
|
||||||
|
const enemy = randomGet(run, pool.filter(Boolean));
|
||||||
return {
|
return {
|
||||||
name: encounter?.name || "",
|
name: encounter?.name || (enemy === RAIDER_TRIO_ENCOUNTER ? "劫掠者团伙" : ""),
|
||||||
requiredCharacter: encounter?.requiredCharacter || null,
|
requiredCharacter: encounter?.requiredCharacter || null,
|
||||||
openingDialogue: encounter?.openingDialogue || [],
|
openingDialogue: encounter?.openingDialogue || [],
|
||||||
fixedRewards: encounter?.fixedRewards || [],
|
fixedRewards: encounter?.fixedRewards || [],
|
||||||
skipRandomReward: encounter?.skipRandomReward === true,
|
skipRandomReward: encounter?.skipRandomReward === true,
|
||||||
victoryDialogue: encounter?.victoryDialogue || [],
|
victoryDialogue: encounter?.victoryDialogue || [],
|
||||||
battlePlan: encounter?.battlePlan || null,
|
battlePlan: encounter?.battlePlan || (enemy === RAIDER_TRIO_ENCOUNTER ?
|
||||||
|
createRaiderTrioBattlePlan(() => nextRandom(run)) : null),
|
||||||
tier: encounter?.tier || (node.type === "elite" ? "elite" : "normal"),
|
tier: encounter?.tier || (node.type === "elite" ? "elite" : "normal"),
|
||||||
enemy: randomGet(run, pool.filter(Boolean)),
|
enemy,
|
||||||
gold: encounter?.gold ?? (node.type == "elite" || node.type == "story" ? act.baseGold * 2 : act.baseGold),
|
gold: encounter?.gold ?? (node.type == "elite" || node.type == "story" ? act.baseGold * 2 : act.baseGold),
|
||||||
boss: false,
|
boss: false,
|
||||||
contentId: node.contentId || null,
|
contentId: node.contentId || null,
|
||||||
|
|||||||
+20
-4
@@ -197,7 +197,7 @@
|
|||||||
.mengsan-map-shuying {
|
.mengsan-map-shuying {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
width: min(100%, 860px);
|
width: min(100%, 980px);
|
||||||
height: max(1080px, 160vh,
|
height: max(1080px, 160vh,
|
||||||
var(--mengsan-map-min-height, 1080px)) !important;
|
var(--mengsan-map-min-height, 1080px)) !important;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -673,7 +673,7 @@
|
|||||||
scroll-padding-block: 24px;
|
scroll-padding-block: 24px;
|
||||||
}
|
}
|
||||||
.mengsan-map-shuying {
|
.mengsan-map-shuying {
|
||||||
width: min(100%, 900px);
|
width: min(100%, 980px);
|
||||||
border: 1px solid rgba(185, 170, 134, .78);
|
border: 1px solid rgba(185, 170, 134, .78);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background:
|
background:
|
||||||
@@ -1283,6 +1283,16 @@ dialog.mengsan-deck-dialog-shuying::backdrop { background: #080706b8; }
|
|||||||
.player.mengsan-hide-identity-shuying > .seat {
|
.player.mengsan-hide-identity-shuying > .seat {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
/* 十周年 UI 侧边按钮只在梦三战斗期间隐藏,不影响其他模式。 */
|
||||||
|
body.mengsan-battle-ui-shuying :is(
|
||||||
|
.jiluButton_new, .jiluButton_new1,
|
||||||
|
.huanfuButton_new, .huanfuButton_new1,
|
||||||
|
.meiguiButton_new, .meiguiButton_new1,
|
||||||
|
.xiaolianButton_new, .xiaolianButton_new1
|
||||||
|
) {
|
||||||
|
display: none !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
.player > .mengsan-energy-shuying {
|
.player > .mengsan-energy-shuying {
|
||||||
position: absolute !important;
|
position: absolute !important;
|
||||||
top: -22px !important;
|
top: -22px !important;
|
||||||
@@ -1326,7 +1336,9 @@ dialog.mengsan-deck-dialog-shuying::backdrop { background: #080706b8; }
|
|||||||
}
|
}
|
||||||
.player > .mengsan-intent-shuying[hidden],
|
.player > .mengsan-intent-shuying[hidden],
|
||||||
.mengsan-intent-damage-shuying[hidden],
|
.mengsan-intent-damage-shuying[hidden],
|
||||||
.mengsan-intent-debuff-shuying[hidden] { display: none !important; }
|
.mengsan-intent-debuff-shuying[hidden],
|
||||||
|
.mengsan-intent-block-shuying[hidden],
|
||||||
|
.mengsan-intent-buff-shuying[hidden] { display: none !important; }
|
||||||
.mengsan-intent-damage-shuying {
|
.mengsan-intent-damage-shuying {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex: none;
|
flex: none;
|
||||||
@@ -1336,7 +1348,9 @@ dialog.mengsan-deck-dialog-shuying::backdrop { background: #080706b8; }
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.mengsan-intent-debuff-shuying {
|
.mengsan-intent-debuff-shuying,
|
||||||
|
.mengsan-intent-block-shuying,
|
||||||
|
.mengsan-intent-buff-shuying {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
flex: none;
|
flex: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1344,6 +1358,8 @@ dialog.mengsan-deck-dialog-shuying::backdrop { background: #080706b8; }
|
|||||||
color: #d8aee5;
|
color: #d8aee5;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
.mengsan-intent-block-shuying { color: #b8daf5; }
|
||||||
|
.mengsan-intent-buff-shuying { color: #f5d6a5; }
|
||||||
.mengsan-intent-icon-shuying {
|
.mengsan-intent-icon-shuying {
|
||||||
display: block;
|
display: block;
|
||||||
width: 22px;
|
width: 22px;
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<#
|
||||||
|
Batch-crop Godot AtlasTexture .tres files into PNGs from their referenced atlas images.
|
||||||
|
Requires a FULL RECOVERY output where atlas sources are PNG/WebP. This script does not
|
||||||
|
unpack PCK files or decode .ctex; it only reads .tres/.tpsheet metadata and crops images.
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ProjectRoot,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$OutputDirectory,
|
||||||
|
|
||||||
|
[string]$SpriteDirectory = 'images/atlases/compressed.sprites/card_template',
|
||||||
|
|
||||||
|
[switch]$Overwrite
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
|
||||||
|
$root = [System.IO.Path]::GetFullPath($ProjectRoot).TrimEnd('\')
|
||||||
|
$outRoot = [System.IO.Path]::GetFullPath($OutputDirectory).TrimEnd('\')
|
||||||
|
$relativeSpriteDir = $SpriteDirectory.TrimStart('\', '/').Replace('/', '\')
|
||||||
|
$sourceDir = [System.IO.Path]::GetFullPath((Join-Path $root $relativeSpriteDir))
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
|
||||||
|
throw "ProjectRoot not found: $root"
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $sourceDir -PathType Container)) {
|
||||||
|
throw "SpriteDirectory not found: $sourceDir"
|
||||||
|
}
|
||||||
|
if ($outRoot.Equals($root, [System.StringComparison]::OrdinalIgnoreCase) -or
|
||||||
|
$outRoot.StartsWith($root + '\', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
throw 'OutputDirectory must be outside ProjectRoot so recovered source files stay untouched.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$invariant = [System.Globalization.CultureInfo]::InvariantCulture
|
||||||
|
$files = @(Get-ChildItem -LiteralPath $sourceDir -Filter '*.tres' -File -Recurse)
|
||||||
|
$written = 0
|
||||||
|
$skipped = 0
|
||||||
|
$errors = [System.Collections.Generic.List[string]]::new()
|
||||||
|
|
||||||
|
foreach ($tres in $files) {
|
||||||
|
$sourceImage = $null
|
||||||
|
$crop = $null
|
||||||
|
try {
|
||||||
|
$text = [System.IO.File]::ReadAllText($tres.FullName)
|
||||||
|
if ($text -notmatch 'type="AtlasTexture"') {
|
||||||
|
$skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$atlasIdMatch = [regex]::Match($text, '(?m)^\s*atlas\s*=\s*ExtResource\("([^"]+)"\)')
|
||||||
|
$regionMatch = [regex]::Match($text, '(?m)^\s*region\s*=\s*Rect2i?\(\s*(-?[0-9.]+)\s*,\s*(-?[0-9.]+)\s*,\s*(-?[0-9.]+)\s*,\s*(-?[0-9.]+)\s*\)')
|
||||||
|
if (-not $atlasIdMatch.Success -or -not $regionMatch.Success) {
|
||||||
|
$skipped++
|
||||||
|
$errors.Add("Missing atlas/region fields: $($tres.FullName)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$atlasPath = $null
|
||||||
|
foreach ($blockMatch in [regex]::Matches($text, '(?m)^\s*\[ext_resource[^\]]*\]')) {
|
||||||
|
$block = $blockMatch.Value
|
||||||
|
$idMatch = [regex]::Match($block, '\bid="([^"]+)"')
|
||||||
|
$pathMatch = [regex]::Match($block, '\bpath="([^"]+)"')
|
||||||
|
if ($idMatch.Success -and $pathMatch.Success -and
|
||||||
|
$idMatch.Groups[1].Value -eq $atlasIdMatch.Groups[1].Value) {
|
||||||
|
$atlasPath = $pathMatch.Groups[1].Value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not $atlasPath) {
|
||||||
|
$skipped++
|
||||||
|
$errors.Add("Could not resolve atlas ExtResource: $($tres.FullName)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($atlasPath.StartsWith('res://', [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||||
|
$atlasRelative = $atlasPath.Substring(6).Replace('/', '\')
|
||||||
|
$atlasFile = Join-Path $root $atlasRelative
|
||||||
|
} else {
|
||||||
|
$atlasFile = Join-Path $tres.DirectoryName $atlasPath.Replace('/', '\')
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $atlasFile -PathType Leaf)) {
|
||||||
|
$skipped++
|
||||||
|
$errors.Add("Atlas image missing (Full Recovery should restore it as PNG): $atlasFile ; referenced by $($tres.FullName)")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = @(
|
||||||
|
[double]::Parse($regionMatch.Groups[1].Value, $invariant),
|
||||||
|
[double]::Parse($regionMatch.Groups[2].Value, $invariant),
|
||||||
|
[double]::Parse($regionMatch.Groups[3].Value, $invariant),
|
||||||
|
[double]::Parse($regionMatch.Groups[4].Value, $invariant)
|
||||||
|
)
|
||||||
|
$rounded = @($values | ForEach-Object { [int][Math]::Round($_) })
|
||||||
|
for ($i = 0; $i -lt 4; $i++) {
|
||||||
|
if ([Math]::Abs($values[$i] - $rounded[$i]) -gt 0.001) {
|
||||||
|
throw "Non-integer pixel region in $($tres.FullName)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$x = $rounded[0]; $y = $rounded[1]; $width = $rounded[2]; $height = $rounded[3]
|
||||||
|
if ($x -lt 0 -or $y -lt 0 -or $width -le 0 -or $height -le 0) {
|
||||||
|
throw "Invalid crop region Rect2($x, $y, $width, $height) in $($tres.FullName)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceImage = [System.Drawing.Bitmap]::FromFile($atlasFile)
|
||||||
|
if (($x + $width) -gt $sourceImage.Width -or ($y + $height) -gt $sourceImage.Height) {
|
||||||
|
throw "Crop exceeds atlas bounds ($($sourceImage.Width)x$($sourceImage.Height)): $($tres.FullName)"
|
||||||
|
}
|
||||||
|
$rect = [System.Drawing.Rectangle]::new($x, $y, $width, $height)
|
||||||
|
$crop = $sourceImage.Clone($rect, $sourceImage.PixelFormat)
|
||||||
|
|
||||||
|
$relativeTres = $tres.FullName.Substring($root.Length).TrimStart('\')
|
||||||
|
$relativeOutput = [System.IO.Path]::ChangeExtension($relativeTres, '.png')
|
||||||
|
$target = Join-Path $outRoot $relativeOutput
|
||||||
|
$targetDir = Split-Path -Parent $target
|
||||||
|
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
|
||||||
|
if ((Test-Path -LiteralPath $target) -and -not $Overwrite) {
|
||||||
|
$skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (Test-Path -LiteralPath $target) {
|
||||||
|
Remove-Item -LiteralPath $target -Force
|
||||||
|
}
|
||||||
|
$crop.Save($target, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||||
|
$written++
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$skipped++
|
||||||
|
$errors.Add("$($tres.FullName): $($_.Exception.Message)")
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if ($null -ne $crop) { $crop.Dispose() }
|
||||||
|
if ($null -ne $sourceImage) { $sourceImage.Dispose() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "TRES scanned: $($files.Count)"
|
||||||
|
Write-Output "PNG written: $written"
|
||||||
|
Write-Output "Skipped: $skipped"
|
||||||
|
foreach ($message in $errors) { Write-Warning $message }
|
||||||
Reference in New Issue
Block a user