fixed # 重构梦三项目结构

This commit is contained in:
2026-09-30 10:23:58 +08:00
parent 3fdaf6d218
commit f2da9bce65
53 changed files with 267 additions and 175 deletions
+131
View File
@@ -0,0 +1,131 @@
import {
attackHitCount,
outgoingAttackDamage,
} from "../battle/intent-damage.js";
import { beginDeathBlow, isDeathBlowIntent } from "../battle/death-blow.js";
import { isStunned } from "../battle/stun-intent.js";
import { nextRandom } from "../progression/state.js";
import { selectFlyconidMove, recordFlyconidAction } from "./flyconid-intent.js";
import {
isRaiderCharacter,
selectRaiderMove,
recordRaiderAction,
} from "./raider-intent.js";
export const isFlyconid = player =>
player?.name === "mengsan_flyconid_shuying" &&
player.storage?.mengsanCamp_shuying === "enemy";
export const isRaider = player =>
player?.storage?.mengsanCamp_shuying === "enemy" &&
isRaiderCharacter(player.name);
export function createMonsterIntentActions(game, getActiveBattle) {
const planEnemyIntent = (player, run) => {
if (!player?.isAlive() || isStunned(player)) return;
if (isFlyconid(player)) {
if (player.storage.mengsanFlyconidIntent_shuying) return;
const move = selectFlyconidMove(
player.storage.mengsanFlyconidState_shuying || {},
() => nextRandom(run));
player.storage.mengsanFlyconidIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isRaider(player)) {
if (player.storage.mengsanRaiderIntent_shuying) return;
const move = selectRaiderMove(player.name,
player.storage.mengsanRaiderState_shuying || {});
player.storage.mengsanRaiderIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
}
};
const applyIntentDebuff = (target, move) => {
if (!move.debuff) return;
const frail = move.debuff === "脆弱";
const key = frail ? "mengsanFrail_shuying" :
"mengsanVulnerable_shuying";
target.storage[key] = (target.storage[key] || 0) + move.stacks;
target.addSkill(frail ? "mengsan_frail_shuying" :
"mengsan_vulnerable_shuying");
target.markSkill(frail ? "mengsan_frail_shuying" :
"mengsan_vulnerable_shuying");
};
const dealIntentDamage = async (source, target, move) => {
if (move.damage == null) return;
for (let index = 0; index < attackHitCount(move); index++) {
if (!target.isAlive() || !source.isAlive()) break;
const hit = target.damage(
outgoingAttackDamage(move, source), source);
hit.mengsanAttack_shuying = true;
hit.mengsanScriptedSkill_shuying = true;
await hit;
}
};
const executeFlyconidIntent = async player => {
const move = player.storage.mengsanFlyconidIntent_shuying;
if (!move) return;
player.storage.mengsanFlyconidIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
const paid = player.storage.mengsanEnergy_shuying >= 1;
player.storage.mengsanFlyconidState_shuying =
recordFlyconidAction(
player.storage.mengsanFlyconidState_shuying || {},
move, paid);
if (!paid) {
game.log(player, "费用不足,未发动", move.name);
return;
}
player.storage.mengsanEnergy_shuying--;
getActiveBattle()?.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
const target = game.me;
if (!target?.isAlive() || !player.isAlive()) return;
if (isDeathBlowIntent(move)) {
await beginDeathBlow(player, target, move);
return;
}
await dealIntentDamage(player, target, move);
if (target.isAlive() && player.isAlive()) {
applyIntentDebuff(target, move);
}
};
const executeRaiderIntent = async player => {
const move = player.storage.mengsanRaiderIntent_shuying;
if (!move) return;
player.storage.mengsanRaiderIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanRaiderState_shuying = recordRaiderAction(
player.storage.mengsanRaiderState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name);
return;
}
player.storage.mengsanEnergy_shuying--;
getActiveBattle()?.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
const target = game.me;
if (!target?.isAlive() || !player.isAlive()) return;
await dealIntentDamage(player, target, move);
if (target.isAlive() && player.isAlive()) {
applyIntentDebuff(target, move);
}
if (!player.isAlive() || !getActiveBattle()?.session.active) return;
if (move.block) await player.changeHujia(move.block);
if (move.strength) {
player.storage.mengsanStrength_shuying =
(player.storage.mengsanStrength_shuying || 0) +
move.strength;
player.addSkill("mengsan_raider_strength_shuying");
player.markSkill("mengsan_raider_strength_shuying");
}
};
return {
planEnemyIntent,
executeFlyconidIntent,
executeRaiderIntent,
};
}
+37
View File
@@ -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;
}
+96
View File
@@ -0,0 +1,96 @@
import { createCardData } from "../cards/card-data.js";
// 梦三怪物规则只作用于敌方单位;子类可覆盖构造参数或 createDeck()。
const TIER_RULES = Object.freeze({
normal: Object.freeze({ draw: 1, handLimit: 3, energy: 2 }),
elite: Object.freeze({ draw: 2, handLimit: 5, energy: 3 }),
boss: Object.freeze({ draw: 3, handLimit: 7, energy: 4 }),
});
const BASE_DECKS = Object.freeze({
normal: Object.freeze(["sha", "sha", "sha", "sha", "shan", "shan", "tao", "jiu", "guohe", "sha"]),
elite: Object.freeze(["sha", "sha", "sha", "sha", "shan", "shan", "shan", "tao", "jiu", "juedou", "wuzhong", "guohe"]),
boss: Object.freeze(["sha", "sha", "sha", "sha", "sha", "shan", "shan", "shan", "tao", "tao", "jiu", "jiu", "juedou", "wuzhong", "guohe"]),
});
const HEALTH = Object.freeze({
mengsan_soldier_shuying: 24,
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_ganning: 39, re_zhangfei: 42, re_huangzhong: 37, re_weiyan: 40,
re_guanyu: 65, dianwei: 62, shen_guanyu: 165,
re_simayi: 47, re_sunben: 49, dongzhuo: 55, re_yuanshao: 52,
shen_zhaoyun: 76, shen_simayi: 80, shen_caocao: 210,
});
const validDeckCard = entry => typeof entry === "string" ? Boolean(entry) : Boolean(entry &&
typeof entry.name === "string" && entry.name &&
(entry.suit == null || ["spade", "heart", "club", "diamond"].includes(entry.suit)) &&
(entry.number == null || Number.isInteger(entry.number) && entry.number >= 1 && entry.number <= 13));
export class Monster {
constructor({ character, tier = "normal", hp, deck, draw, handLimit, energy } = {}) {
if (!TIER_RULES[tier]) throw new Error("梦三怪物阶级无效:" + tier);
if (typeof character !== "string" || !character) throw new Error("梦三怪物缺少武将ID");
const defaults = TIER_RULES[tier];
this.character = character;
this.tier = tier;
this.hp = hp ?? HEALTH[character] ?? (tier === "boss" ? 110 : tier === "elite" ? 60 : 32);
this.draw = draw ?? defaults.draw;
this.handLimit = handLimit ?? defaults.handLimit;
this.energy = energy ?? defaults.energy;
this.deck = deck || BASE_DECKS[tier];
if (![this.hp, this.draw, this.handLimit, this.energy].every(value => Number.isInteger(value) && value > 0)) throw new Error("梦三怪物数值无效");
if (!Array.isArray(this.deck) || !this.deck.length || this.deck.some(entry => !validDeckCard(entry))) throw new Error("梦三怪物牌堆无效");
}
createDeck(prefix) {
return this.deck.map((entry, index) => {
const card = typeof entry === "string" ? { name: entry } : entry;
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 });
});
}
}
// 专属怪物示例:以后可在子类中覆写任意默认行为。
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 }); }
}
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 的阶级默认值。
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) {
const Type = Object.hasOwn(MONSTER_TYPES, spec.character) ? MONSTER_TYPES[spec.character] : Monster;
return new Type(spec);
}
export { TIER_RULES, BASE_DECKS };
+39
View File
@@ -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 };
}