mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-21 23:00:06 +08:00
fixed # 重构梦三项目结构
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,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 };
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user