fixed # 补全梦三怪物、卡牌

fixed # 修复天书乱斗中,过关技能不会重置次数的问题
This commit is contained in:
2026-10-03 04:42:19 +08:00
parent b005a7ff15
commit c448da39c2
131 changed files with 5021 additions and 246 deletions
+445
View File
@@ -1,14 +1,37 @@
import { addConstrict } from "./strangler-constrict.js";
import { applyShrink } from "./shrinker-status.js";
import { getIntentHostiles } from "../battle/intent-effects.js";
import { applyLeafslimeGoop } from "./leafslime-goop.js";
import { createIntentExecutor } from "../battle/intent-effects.js";
export { getIntentHostiles } from "../battle/intent-effects.js";
import { isStunned } from "../battle/stun-intent.js";
import { nextRandom } from "../progression/state.js";
import { selectFlyconidMove, recordFlyconidAction } from "./flyconid-intent.js";
import { isMawler, selectMawlerMove, recordMawlerAction } from "./mawler-intent.js";
import { isVine, selectVineMove, recordVineAction } from "./vine-intent.js";
import { isCubex, selectCubexMove, recordCubexAction } from "./cubex-intent.js";
import { isByrdonis, selectByrdonisMove, recordByrdonisAction } from "./byrdonis-intent.js";
import { isEffigy, selectEffigyMove, recordEffigyAction } from "./effigy-intent.js";
import { isPhrog, isPhrogActor, selectPhrogMove, selectWrigglerMove, recordPhrogAction } from "./phrog-intent.js";
import { applyInfection } from "../cards/infection-card.js";
import { isNibbit, selectNibbitMove, recordNibbitAction, bindNibbitOpenings } from "./nibbit-intent.js";
import { isShrinker, selectShrinkerMove, recordShrinkerAction } from "./shrinker-intent.js";
import { isTwigmedium, selectTwigmediumMove, recordTwigmediumAction } from "./twigmedium-intent.js";
import { isTwigslime, selectTwigslimeMove, recordTwigslimeAction } from "./twigslime-intent.js";
import { isLeafslimeMedium, selectLeafslimeMediumMove, recordLeafslimeMediumAction } from "./leafmedium-intent.js";
import { isLeafslime, selectLeafslimeMove, recordLeafslimeAction } from "./leafslime-intent.js";
import { isStrangler, selectStranglerMove, recordStranglerAction } from "./strangler-intent.js";
import { isJaxfruit, selectJaxfruitMove, recordJaxfruitAction } from "./jaxfruit-intent.js";
import { isCrawler, selectCrawlerMove, recordCrawlerAction } from "./crawler-intent.js";
import { isInklet, selectInkletMove, recordInkletAction } from "./inklet-intent.js";
import {
isRaiderCharacter,
selectRaiderMove,
recordRaiderAction,
} from "./raider-intent.js";
import { isFogmog, isFogmogActor, selectFogmogMove, recordFogmogAction, EYE_INTENT } from "./fogmog-intent.js";
export const isFlyconid = player =>
player?.name === "mengsan_flyconid_shuying" &&
player.storage?.mengsanCamp_shuying === "enemy";
@@ -28,6 +51,94 @@ export function createMonsterIntentActions(game, getActiveBattle) {
() => nextRandom(run));
player.storage.mengsanFlyconidIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isInklet(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanInkletIntent_shuying) return;
const move = selectInkletMove(player.storage.mengsanInkletState_shuying || {}, () => nextRandom(run));
player.storage.mengsanInkletIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isCrawler(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanCrawlerIntent_shuying) return;
const move = selectCrawlerMove(player.storage.mengsanCrawlerState_shuying || {});
player.storage.mengsanCrawlerIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isJaxfruit(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanJaxfruitIntent_shuying) return;
const move = selectJaxfruitMove(player.storage.mengsanJaxfruitState_shuying || {});
player.storage.mengsanJaxfruitIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isStrangler(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanStranglerIntent_shuying) return;
const move = selectStranglerMove(player.storage.mengsanStranglerState_shuying || {}, () => nextRandom(run));
player.storage.mengsanStranglerIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isLeafslime(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanLeafslimeIntent_shuying) return;
const move = selectLeafslimeMove(player.storage.mengsanLeafslimeState_shuying || {}, () => nextRandom(run));
player.storage.mengsanLeafslimeIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isLeafslimeMedium(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanLeafslimeMediumIntent_shuying) return;
const move = selectLeafslimeMediumMove(player.storage.mengsanLeafslimeMediumState_shuying || {});
player.storage.mengsanLeafslimeMediumIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isTwigslime(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanTwigslimeIntent_shuying) return;
const move = selectTwigslimeMove(player.storage.mengsanTwigslimeState_shuying || {});
player.storage.mengsanTwigslimeIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isTwigmedium(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanTwigmediumIntent_shuying) return;
const move = selectTwigmediumMove(player.storage.mengsanTwigmediumState_shuying || {}, () => nextRandom(run));
player.storage.mengsanTwigmediumIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isShrinker(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanShrinkerIntent_shuying) return;
const move = selectShrinkerMove(player.storage.mengsanShrinkerState_shuying || {});
player.storage.mengsanShrinkerIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isNibbit(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanNibbitIntent_shuying) return;
bindNibbitOpenings(game.players);
const move = selectNibbitMove(player.storage.mengsanNibbitState_shuying || {});
player.storage.mengsanNibbitIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isEffigy(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanEffigyIntent_shuying) return;
const move = selectEffigyMove(player.storage.mengsanEffigyState_shuying || {});
player.storage.mengsanEffigyIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isPhrogActor(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanPhrogIntent_shuying) return;
const move = (isPhrog(player) ? selectPhrogMove : selectWrigglerMove)(player.storage.mengsanPhrogState_shuying || {});
player.storage.mengsanPhrogIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isByrdonis(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanByrdonisIntent_shuying) return;
const move = selectByrdonisMove(player.storage.mengsanByrdonisState_shuying || {});
player.storage.mengsanByrdonisIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isCubex(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanCubexIntent_shuying) return;
const move = selectCubexMove(player.storage.mengsanCubexState_shuying || {});
player.storage.mengsanCubexIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isVine(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanVineIntent_shuying) return;
const move = selectVineMove(player.storage.mengsanVineState_shuying || {});
player.storage.mengsanVineIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isMawler(player)) {
if (!getActiveBattle()?.session.active || player.storage.mengsanMawlerIntent_shuying) return;
const move = selectMawlerMove(player.storage.mengsanMawlerState_shuying || {},
() => nextRandom(run));
player.storage.mengsanMawlerIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isFogmogActor(player)) {
if (player.storage.mengsanFogmogIntent_shuying || !getActiveBattle()?.fogmog?.canAct(player)) return;
const move = isFogmog(player) ? selectFogmogMove(
player.storage.mengsanFogmogState_shuying || {}, () => nextRandom(run)) : EYE_INTENT;
player.storage.mengsanFogmogIntent_shuying = move;
game.mengsanSetEnemyIntent_shuying(player, move);
} else if (isRaider(player)) {
if (player.storage.mengsanRaiderIntent_shuying) return;
const move = selectRaiderMove(player.name,
@@ -74,7 +185,341 @@ export function createMonsterIntentActions(game, getActiveBattle) {
await executeEffects(player, move);
};
const executeInkletIntent = async player => {
const current = getActiveBattle();
if (!isInklet(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanInkletIntent_shuying;
if (!move) return;
player.storage.mengsanInkletIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanInkletState_shuying = recordInkletAction(player.storage.mengsanInkletState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeCrawlerIntent = async player => {
const current = getActiveBattle();
if (!isCrawler(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanCrawlerIntent_shuying;
if (!move) return;
player.storage.mengsanCrawlerIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanCrawlerState_shuying =
recordCrawlerAction(player.storage.mengsanCrawlerState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeJaxfruitIntent = async player => {
const current = getActiveBattle();
if (!isJaxfruit(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanJaxfruitIntent_shuying;
if (!move) return;
player.storage.mengsanJaxfruitIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanJaxfruitState_shuying =
recordJaxfruitAction(player.storage.mengsanJaxfruitState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeStranglerIntent = async player => {
const current = getActiveBattle();
if (!isStrangler(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanStranglerIntent_shuying;
if (!move) return;
player.storage.mengsanStranglerIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanStranglerState_shuying =
recordStranglerAction(player.storage.mengsanStranglerState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.constrict) {
for (const target of getIntentHostiles(game, player)) addConstrict(current, player, target, move.constrict);
} else await executeEffects(player, move);
};
const executeLeafslimeIntent = async player => {
const current = getActiveBattle();
if (!isLeafslime(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanLeafslimeIntent_shuying;
if (!move) return;
player.storage.mengsanLeafslimeIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanLeafslimeState_shuying =
recordLeafslimeAction(player.storage.mengsanLeafslimeState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.slimed) applyLeafslimeGoop(game, current, player);
else await executeEffects(player, move);
};
const executeLeafslimeMediumIntent = async player => {
const current = getActiveBattle();
if (!isLeafslimeMedium(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanLeafslimeMediumIntent_shuying;
if (!move) return;
player.storage.mengsanLeafslimeMediumIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanLeafslimeMediumState_shuying =
recordLeafslimeMediumAction(player.storage.mengsanLeafslimeMediumState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.slimed) applyLeafslimeGoop(game, current, player, move.slimed);
else await executeEffects(player, move);
};
const executeTwigslimeIntent = async player => {
const current = getActiveBattle();
if (!isTwigslime(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanTwigslimeIntent_shuying;
if (!move) return;
player.storage.mengsanTwigslimeIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanTwigslimeState_shuying =
recordTwigslimeAction(player.storage.mengsanTwigslimeState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeTwigmediumIntent = async player => {
const current = getActiveBattle();
if (!isTwigmedium(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanTwigmediumIntent_shuying;
if (!move) return;
player.storage.mengsanTwigmediumIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanTwigmediumState_shuying =
recordTwigmediumAction(player.storage.mengsanTwigmediumState_shuying || {}, move);
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.slimed) applyLeafslimeGoop(game, current, player, move.slimed);
else await executeEffects(player, move);
};
const executeShrinkerIntent = async player => {
const current = getActiveBattle();
if (!isShrinker(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanShrinkerIntent_shuying;
if (!move) return;
player.storage.mengsanShrinkerIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanShrinkerState_shuying =
recordShrinkerAction(player.storage.mengsanShrinkerState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.shrink) {
for (const target of getIntentHostiles(game, player)) applyShrink(current, player, target);
} else await executeEffects(player, move);
};
const executeNibbitIntent = async player => {
const current = getActiveBattle();
if (!isNibbit(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanNibbitIntent_shuying;
if (!move) return;
player.storage.mengsanNibbitIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanNibbitState_shuying =
recordNibbitAction(player.storage.mengsanNibbitState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeEffigyIntent = async (player, phase) => {
const current = getActiveBattle();
if (!isEffigy(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanEffigyIntent_shuying;
if (!move) return;
player.storage.mengsanEffigyIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanEffigyState_shuying =
recordEffigyAction(player.storage.mengsanEffigyState_shuying || {});
if (move.sleep) {
phase?.cancel();
game.log(player, "沉睡,本回合不行动"); return;
}
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executePhrogIntent = async player => {
const current = getActiveBattle();
if (!isPhrogActor(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanPhrogIntent_shuying;
if (!move || move.id === "spawned") return;
player.storage.mengsanPhrogIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanPhrogState_shuying =
recordPhrogAction(player.storage.mengsanPhrogState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.infection) applyInfection(game, current, player, move.infection);
await executeEffects(player, move);
};
const executeByrdonisIntent = async player => {
const current = getActiveBattle();
if (!isByrdonis(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanByrdonisIntent_shuying;
if (!move) return;
player.storage.mengsanByrdonisIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanByrdonisState_shuying =
recordByrdonisAction(player.storage.mengsanByrdonisState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeCubexIntent = async player => {
const current = getActiveBattle();
if (!isCubex(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanCubexIntent_shuying;
if (!move) return;
player.storage.mengsanCubexIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanCubexState_shuying =
recordCubexAction(player.storage.mengsanCubexState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeVineIntent = async player => {
const current = getActiveBattle();
if (!isVine(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanVineIntent_shuying;
if (!move) return;
player.storage.mengsanVineIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
player.storage.mengsanVineState_shuying =
recordVineAction(player.storage.mengsanVineState_shuying || {});
if (player.storage.mengsanEnergy_shuying < 1) {
game.log(player, "费用不足,未发动", move.name); return;
}
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeMawlerIntent = async player => {
const current = getActiveBattle();
if (!isMawler(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return;
const move = player.storage.mengsanMawlerIntent_shuying;
if (!move) return;
player.storage.mengsanMawlerIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
const paid = player.storage.mengsanEnergy_shuying >= 1;
player.storage.mengsanMawlerState_shuying =
recordMawlerAction(player.storage.mengsanMawlerState_shuying || {}, move, paid);
if (!paid) { game.log(player, "费用不足,未发动", move.name); return; }
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
await executeEffects(player, move);
};
const executeFogmogIntent = async player => {
const current = getActiveBattle();
if (!isFogmogActor(player) || isStunned(player) || !current?.fogmog?.canAct(player)) return;
const move = player.storage.mengsanFogmogIntent_shuying;
if (!move) return;
player.storage.mengsanFogmogIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
const paid = player.storage.mengsanEnergy_shuying >= 1;
if (isFogmog(player)) player.storage.mengsanFogmogState_shuying =
recordFogmogAction(player.storage.mengsanFogmogState_shuying || {}, move, paid);
if (!paid) { game.log(player, "费用不足,未发动", move.name); return; }
player.storage.mengsanEnergy_shuying--;
current.energyUI.get(player)?.();
game.log(player, "消耗1费用发动", move.name);
if (move.summon) await current.fogmog.summon(player);
else if (move.dazed) current.fogmog.distract(player);
else await executeEffects(player, move);
};
return {
executeFogmogIntent,
executeMawlerIntent,
executeVineIntent,
executeCubexIntent,
executeByrdonisIntent,
executeEffigyIntent,
executePhrogIntent,
executeNibbitIntent,
executeShrinkerIntent,
executeTwigmediumIntent,
executeTwigslimeIntent,
executeLeafslimeMediumIntent,
executeLeafslimeIntent,
executeStranglerIntent,
executeJaxfruitIntent,
executeCrawlerIntent,
executeInkletIntent,
planEnemyIntent,
executeFlyconidIntent,
executeRaiderIntent,
+39
View File
@@ -0,0 +1,39 @@
const KEY = "mengsanArtifact_shuying", SKILL = "mengsan_artifact_shuying";
const bindings = new WeakMap();
const DEBUFFS = Object.freeze({
vulnerable: ["mengsanVulnerable_shuying", "mengsan_vulnerable_shuying"],
weak: ["mengsanWeak_shuying", "mengsan_weak_shuying"],
frail: ["mengsanFrail_shuying", "mengsan_frail_shuying"],
});
export const clearArtifact = player => { delete player.storage[KEY]; player.removeSkill(SKILL); };
export function setArtifact(player, layers, current = null) {
if (!Number.isSafeInteger(layers) || layers < 0) throw new RangeError("人工制品层数无效");
if (!layers) { clearArtifact(player); return; }
if (current?.session.active && bindings.get(player)?.battle !== current) {
const entry = { battle: current };
current.session.ownResource(entry, () => {
if (bindings.get(player) !== entry) return;
bindings.delete(player); clearArtifact(player);
});
bindings.set(player, entry);
}
player.storage[KEY] = layers; player.addSkill(SKILL); player.markSkill(SKILL);
}
export function consumeArtifact(player) {
if (!player?.isAlive() || !player.storage) return false;
const layers = player.storage[KEY] ?? 0;
if (!Number.isSafeInteger(layers) || layers < 0) throw new RangeError("人工制品层数无效");
if (!layers) return false;
if (layers === 1) clearArtifact(player);
else { player.storage[KEY] = layers - 1; player.markSkill(SKILL); }
return true;
}
// 一次施加抵消一次,不按负面层数逐层消耗;伤害和正面增益不走此入口。
export function applyMengsanDebuff(player, kind, amount) {
if (!Object.hasOwn(DEBUFFS, kind) || !Number.isSafeInteger(amount) || amount < 0) throw new RangeError("负面状态参数无效");
if (!amount || !player?.isAlive()) return false;
const [key, skill] = DEBUFFS[kind], old = player.storage[key] ?? 0;
if (!Number.isSafeInteger(old) || old < 0 || amount > Number.MAX_SAFE_INTEGER - old) throw new RangeError("负面状态层数越界");
if (consumeArtifact(player)) return false;
player.storage[key] = old + amount; player.addSkill(skill); player.markSkill(skill); return true;
}
+57
View File
@@ -0,0 +1,57 @@
// 普通数值;网页SVG:开始→俯冲→啄击→俯冲。领地意识在回合结束结算。
export const BYRDONIS_CHARACTER = "mengsan_byrdonis_shuying";
export const BYRDONIS_MOVES = Object.freeze({
swoop: Object.freeze({ id: "swoop", name: "俯冲", damage: 17 }),
peck: Object.freeze({ id: "peck", name: "啄击", damage: 3, hits: 3 }),
});
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("多尼斯异鸟行动记录无效");
return turns;
};
export const selectByrdonisMove = (state = {}) => turnsOf(state) % 2 ? BYRDONIS_MOVES.peck : BYRDONIS_MOVES.swoop;
export function recordByrdonisAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("多尼斯异鸟行动推进无效");
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export function rollByrdonisHp(random) {
const value = random();
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("多尼斯异鸟随机数必须在[0,1)内");
return 81 + Math.floor(value * 4);
}
export const isByrdonis = player => player?.name === BYRDONIS_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
const bindings = new WeakMap();
export function initializeByrdonis(player, current = null) {
if (!isByrdonis(player)) return false;
if (current?.session.active && bindings.get(player)?.battle !== current) {
const entry = { battle: current };
current.session.ownResource(entry, () => {
if (bindings.get(player) !== entry) return;
bindings.delete(player);
delete player.storage.mengsanByrdonisState_shuying;
delete player.storage.mengsanByrdonisIntent_shuying;
player.removeSkill("mengsan_territorial_shuying");
player.removeSkill("mengsan_raider_strength_shuying");
});
bindings.set(player, entry);
}
player.storage.mengsanByrdonisState_shuying = { turnsTaken: 0 };
player.storage.mengsanByrdonisIntent_shuying = null;
player.storage.mengsanTerritorial_shuying = 1;
player.addSkill("mengsan_territorial_shuying");
player.markSkill("mengsan_territorial_shuying");
return true;
}
export function resolveTerritorial(player, current) {
if (!isByrdonis(player) || !player.isAlive() || !current?.session.active) return false;
const layers = player.storage.mengsanTerritorial_shuying ?? 0;
const strength = player.storage.mengsanStrength_shuying ?? 0;
if (!Number.isSafeInteger(layers) || layers < 0 || !Number.isSafeInteger(strength) ||
!Number.isSafeInteger(strength + layers)) throw new RangeError("领地意识或力量数值无效");
if (!layers) return false;
player.storage.mengsanStrength_shuying = strength + layers;
player.addSkill("mengsan_raider_strength_shuying");
player.markSkill("mengsan_raider_strength_shuying");
return true;
}
+32
View File
@@ -0,0 +1,32 @@
// 毛绒伏地虫:酸液 -> 吸入 -> 酸液 -> 回到开头,不是两招交替。
export const CRAWLER_CHARACTER = "mengsan_fuzzy_wurm_crawler_shuying";
export const CRAWLER_MOVES = Object.freeze({
acid: Object.freeze({ id: "acid", name: "酸液黏球", damage: 4 }),
inhale: Object.freeze({ id: "inhale", name: "吸入", strength: 7 }),
});
export const CRAWLER_CYCLE = Object.freeze([
CRAWLER_MOVES.acid, CRAWLER_MOVES.inhale, CRAWLER_MOVES.acid,
]);
const cycleIndex = state => {
const index = state.cycleIndex ?? 0;
if (!Number.isInteger(index) || index < 0 || index >= CRAWLER_CYCLE.length) {
throw new RangeError("毛绒伏地虫循环位置无效");
}
return index;
};
export const selectCrawlerMove = (state = {}) => CRAWLER_CYCLE[cycleIndex(state)];
export function recordCrawlerAction(state = {}, advance = true) {
const index = cycleIndex(state), turn = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turn) || turn < 0 || turn === Number.MAX_SAFE_INTEGER || typeof advance !== "boolean") {
throw new RangeError("毛绒伏地虫行动记录无效");
}
return { turnsTaken: turn + 1,
cycleIndex: (index + (advance ? 1 : 0)) % CRAWLER_CYCLE.length };
}
export function rollCrawlerHp(random) {
const roll = random();
if (!Number.isFinite(roll) || roll < 0 || roll >= 1) throw new RangeError("怪物随机数必须在 [0,1) 内");
return 55 + Math.floor(roll * 3);
}
export const isCrawler = player => player?.name === CRAWLER_CHARACTER &&
player.storage?.mengsanCamp_shuying === "enemy";
+43
View File
@@ -0,0 +1,43 @@
import { setArtifact } from "./artifact-status.js";
// SVG明确:蓄能只在开场一次,排出回到重复轰击,不回到蓄能。
export const CUBEX_CHARACTER = "mengsan_cubex_construct_shuying";
export const CUBEX_MOVES = Object.freeze({
charge: Object.freeze({ id: "charge", name: "蓄能", strength: 2 }),
repeater: Object.freeze({ id: "repeater", name: "重复轰击", damage: 7, strength: 2 }),
expel: Object.freeze({ id: "expel", name: "排出", damage: 5, hits: 2 }),
});
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("立柱构造体行动记录无效");
return turns;
};
const CYCLE = Object.freeze([CUBEX_MOVES.repeater, CUBEX_MOVES.repeater, CUBEX_MOVES.expel]);
export const selectCubexMove = (state = {}) => {
const turns = turnsOf(state);
return turns === 0 ? CUBEX_MOVES.charge : CYCLE[(turns - 1) % CYCLE.length];
};
export function recordCubexAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("立柱构造体行动推进无效");
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export const isCubex = player => player?.name === CUBEX_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
const bindings = new WeakMap();
export function initializeCubex(player, current = null) {
if (!isCubex(player)) return false;
if (current?.session.active && bindings.get(player)?.battle !== current) {
const entry = { battle: current };
current.session.ownResource(entry, () => {
if (bindings.get(player) !== entry) return;
bindings.delete(player);
delete player.storage.mengsanCubexState_shuying;
delete player.storage.mengsanCubexIntent_shuying;
});
bindings.set(player, entry);
}
player.hujia = 13;
player.storage.mengsanCubexState_shuying = { turnsTaken: 0 };
player.storage.mengsanCubexIntent_shuying = null;
setArtifact(player, 1, current);
return true;
}
+29
View File
@@ -0,0 +1,29 @@
import { initializeSlow } from "./effigy-slow.js";
export const EFFIGY_CHARACTER = "mengsan_bygone_effigy_shuying";
export const EFFIGY_MOVES = Object.freeze({
sleep: Object.freeze({ id: "sleep", name: "沉睡", sleep: true }),
wake: Object.freeze({ id: "wake", name: "苏醒", strength: 10 }),
slashes: Object.freeze({ id: "slashes", name: "斩击", damage: 13 }),
});
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("旧日雕像行动记录无效");
return turns;
};
export const selectEffigyMove = (state = {}) => {
const turns = turnsOf(state);
return turns === 0 ? EFFIGY_MOVES.sleep : turns === 1 ? EFFIGY_MOVES.wake : EFFIGY_MOVES.slashes;
};
export function recordEffigyAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("旧日雕像行动推进无效");
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export const isEffigy = player => player?.name === EFFIGY_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
export function initializeEffigy(player, current = null) {
if (!isEffigy(player)) return false;
initializeSlow(player, current);
player.storage.mengsanEffigyState_shuying = { turnsTaken: 0 };
player.storage.mengsanEffigyIntent_shuying = null;
return true;
}
+62
View File
@@ -0,0 +1,62 @@
import { isAttackCard } from "./vine-tangled.js";
const bindings = new WeakMap();
const layersOf = player => {
const value = player?.storage?.mengsanSlow_shuying ?? 0;
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError("缓慢层数无效");
return value;
};
export function slowPercent(player) {
const count = player?.storage?.mengsanSlowCards_shuying ?? 0;
const percent = count * layersOf(player) * 10;
if (!Number.isSafeInteger(count) || count < 0 || !Number.isSafeInteger(percent)) throw new RangeError("缓慢计数溢出");
return percent;
}
export function initializeSlow(player, current) {
if (current?.session.active && bindings.get(player)?.battle !== current) {
const entry = { battle: current, seen: new WeakSet() };
current.session.ownResource(entry, () => {
if (bindings.get(player) !== entry) return;
bindings.delete(player);
delete player.storage.mengsanEffigyState_shuying;
delete player.storage.mengsanEffigyIntent_shuying;
player.removeSkill("mengsan_slow_shuying");
player.removeSkill("mengsan_raider_strength_shuying");
});
bindings.set(player, entry);
}
player.storage.mengsanSlow_shuying = 1;
player.storage.mengsanSlowCards_shuying = 0;
player.addSkill("mengsan_slow_shuying");
player.markSkill("mengsan_slow_shuying");
}
export function canCountSlowCard(player, current, event, phasePlayer) {
const binding = bindings.get(player);
return Boolean(current?.session.active && binding?.battle === current && player.isAlive() &&
layersOf(player) > 0 && event?.name === "useCard" && event.card &&
!event.cancelled && !event._cancelled && event.player?.storage?.mengsanCamp_shuying === "ally" &&
phasePlayer?.storage?.mengsanCamp_shuying === "ally" && current.players?.has(event.player) &&
!binding.seen.has(event));
}
// AfterCardPlayed: the attack being resolved does not count itself. A virtual card is one play.
export function recordSlowCard(player, current, event, phasePlayer) {
if (!canCountSlowCard(player, current, event, phasePlayer)) return false;
const count = (player.storage.mengsanSlowCards_shuying ?? 0) + 1;
if (!Number.isSafeInteger(count) || !Number.isSafeInteger(count * layersOf(player) * 10)) throw new RangeError("缓慢计数溢出");
bindings.get(player).seen.add(event);
player.storage.mengsanSlowCards_shuying = count;
player.markSkill("mengsan_slow_shuying");
return true;
}
export function resetSlow(player) {
player.storage.mengsanSlowCards_shuying = 0;
player.markSkill("mengsan_slow_shuying");
}
export const slowApplies = (player, event) => Boolean(layersOf(player) > 0 && slowPercent(player) > 0 &&
(event?.mengsanAttack_shuying || isAttackCard(event?.card)) && !event.mengsanSlowApplied_shuying);
// 整数伤害适配:力量/虚弱/易伤之后乘(1+10%×已使用牌数),向下取整,护甲之后由引擎处理。
export function slowAttackDamage(damage, target) {
if (!Number.isSafeInteger(damage) || damage < 0) throw new RangeError("缓慢攻击伤害无效");
const value = damage * (100 + slowPercent(target));
if (!Number.isSafeInteger(value)) throw new RangeError("缓慢攻击伤害溢出");
return Math.floor(value / 100);
}
+3 -2
View File
@@ -8,10 +8,11 @@ export const FLYCONID_MOVES = Object.freeze({
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);
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;
}
+75
View File
@@ -0,0 +1,75 @@
import { EYE_CHARACTER, isFogmog, isToothedEye } from "./fogmog-intent.js";
import { createDazedData } from "../cards/status-cards.js";
import { getIntentHostiles } from "../battle/intent-effects.js";
// 生命周期属于单场战斗;复活复用原席位,不追加玩家或定时器。
export function createFogmogBattle(game, current, spawn) {
const owners = new Map(), eyes = new Map(), pending = new Map();
let round = 0, sequence = 0, cardSequence = 0;
const active = () => current.session.active;
const canAct = player => active() && player.isAlive() &&
(isFogmog(player) || isToothedEye(player) && eyes.get(player)?.isAlive());
current.session.ownResource(owners, () => { owners.clear(); eyes.clear(); pending.clear(); });
return {
canAct,
register(player, spec) {
if (!isToothedEye(player)) return;
const owner = game.players.find(p => p.playerid === spec.fogmogOwner && isFogmog(p));
if (!owner || owners.has(owner)) throw new Error("利齿之眼缺少唯一雾菇召唤者");
owners.set(owner, player); eyes.set(player, owner);
player.storage.mengsanFogmogOwner_shuying = owner.playerid;
player.addSkill("mengsan_fogmog_illusion_shuying");
player.markSkill("mengsan_fogmog_illusion_shuying");
},
async summon(owner) {
if (!canAct(owner) || !isFogmog(owner) || owners.has(owner)) return;
if (game.players.length + game.dead.length >= 8) throw new Error("雾菇召唤需要一个空席位");
await spawn({ id: `fogmog_eye_${++sequence}`, character: EYE_CHARACTER,
camp: "enemy", tier: "normal", hp: 6, maxHp: 6, hand: 0,
inheritSkills: false, fogmogOwner: owner.playerid }, owner);
},
distract(source) {
if (!canAct(source)) return;
for (const target of getIntentHostiles(game, source)) {
const pile = target === game.me ? current.personalPiles : current.monsterPiles.get(target);
if (!pile) continue; // 无个人牌堆的剧情援军不向公共牌堆塞入状态牌。
for (let index = 0; index < 3 && canAct(source); index++) {
pile.addToDiscard(createDazedData(`dazed_${source.playerid}_${++cardSequence}`));
}
game.log(target, "的个人弃牌堆加入3张【晕眩】");
}
},
onDeath(player) {
if (eyes.has(player)) {
if (eyes.get(player).isAlive()) pending.set(player, round + 1);
player.storage.mengsanFogmogIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(player, null);
}
if (owners.has(player)) {
const eye = owners.get(player);
pending.delete(eye);
eye.storage.mengsanFogmogIntent_shuying = null;
game.mengsanSetEnemyIntent_shuying(eye, null);
}
},
beforeRound() {
if (!active()) return;
round++;
for (const [eye, due] of pending) {
const owner = eyes.get(eye);
if (!owner?.isAlive()) { pending.delete(eye); continue; }
if (round < due) continue;
pending.delete(eye);
if (!eye.isAlive()) {
eye.revive(6, false);
eye.hujia = 0;
eye.storage.mengsanEnergy_shuying = eye.storage.mengsanMaxEnergy_shuying;
eye.addSkill("mengsan_fogmog_illusion_shuying");
eye.markSkill("mengsan_fogmog_illusion_shuying");
eye.update();
game.log(eye, "以6点生命复活");
}
}
},
};
}
+35
View File
@@ -0,0 +1,35 @@
// 灰机 Wiki 雾菇页的普通难度:74生命,重击8、头槌14。
export const FOGMOG_CHARACTER = "mengsan_fogmog_shuying";
export const EYE_CHARACTER = "mengsan_eye_with_teeth_shuying";
export const FOGMOG_MOVES = Object.freeze({
spores: Object.freeze({ id: "spores", name: "虚幻孢子", summon: EYE_CHARACTER }),
thwack: Object.freeze({ id: "thwack", name: "重击", damage: 8, strength: 1 }),
headbutt: Object.freeze({ id: "headbutt", name: "头槌", damage: 14 }),
});
export const EYE_INTENT = Object.freeze({ id: "distract", name: "牵制", dazed: 3 });
export function selectFogmogMove(state = {}, random) {
const turn = state.turnsTaken || 0;
if (!Number.isSafeInteger(turn) || turn < 0) throw new RangeError("雾菇行动次数无效");
if (!turn) return FOGMOG_MOVES.spores;
if (state.lastMove !== "thwack") return FOGMOG_MOVES.thwack;
if ((state.thwacks || 0) >= 2) return FOGMOG_MOVES.headbutt;
const roll = random();
if (!Number.isFinite(roll) || roll < 0 || roll >= 1) throw new RangeError("怪物随机数必须在 [0,1) 内");
return roll < 0.4 ? FOGMOG_MOVES.thwack : FOGMOG_MOVES.headbutt;
}
export function recordFogmogAction(state = {}, move, paid) {
return {
turnsTaken: (state.turnsTaken || 0) + 1,
lastMove: paid && move ? move.id : state.lastMove || null,
thwacks: paid && move ? (move.id === "thwack" ?
(state.lastMove === "thwack" ? (state.thwacks || 0) + 1 : 1) : 0) : state.thwacks || 0,
};
}
export const isFogmog = player => player?.name === FOGMOG_CHARACTER &&
player.storage?.mengsanCamp_shuying === "enemy";
export const isToothedEye = player => player?.name === EYE_CHARACTER &&
player.storage?.mengsanCamp_shuying === "enemy";
export const isFogmogActor = player => isFogmog(player) || isToothedEye(player);
// 爪牙不单独构成胜利目标;雾菇死亡后无需继续杀死或等待其幻象。
export const isEncounterEnemy = player => player?.isAlive() &&
player.storage?.mengsanCamp_shuying === "enemy" && !isToothedEye(player);
+52
View File
@@ -0,0 +1,52 @@
// 灰机 Wiki 墨宝:奇数位刺击起手;偶数位旋风起手后刺击;以后随机招与刺击交替。
export const INKLET_CHARACTER = "mengsan_inklet_shuying";
export const INKLET_MOVES = Object.freeze({
jab: Object.freeze({ id: "jab", name: "刺击", damage: 3 }),
whirlwind: Object.freeze({ id: "whirlwind", name: "旋风斩", damage: 2, hits: 3 }),
gaze: Object.freeze({ id: "gaze", name: "锐利凝视", damage: 10 }),
});
const roll = random => {
const value = random();
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("墨宝随机数必须在[0,1)内");
return value;
};
const positionOf = state => {
const position = state.position ?? 1;
if (!Number.isInteger(position) || position < 1 || position > 7) throw new RangeError("墨宝站位必须为1~7");
return position;
};
const stageOf = state => {
const stage = state.stage ?? "opening";
if (!["opening", "jab", "random"].includes(stage)) throw new RangeError("墨宝行动节点无效");
return stage;
};
export function selectInkletMove(state = {}, random) {
const position = positionOf(state), stage = stageOf(state);
if (stage === "opening") return position % 2 ? INKLET_MOVES.jab : INKLET_MOVES.whirlwind;
if (stage === "jab") return INKLET_MOVES.jab;
return roll(random) < 0.5 ? INKLET_MOVES.gaze : INKLET_MOVES.whirlwind;
}
export function recordInkletAction(state = {}, advance = true) {
const position = positionOf(state), stage = stageOf(state), turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0 || turns === Number.MAX_SAFE_INTEGER || typeof advance !== "boolean") {
throw new RangeError("墨宝行动记录无效");
}
const next = stage === "opening" ? (position % 2 ? "random" : "jab") : stage === "jab" ? "random" : "jab";
return { position, stage: advance ? next : stage, turnsTaken: turns + 1 };
}
export const rollInkletHp = random => 11 + Math.floor(roll(random) * 7);
export const isInklet = player => player?.name === INKLET_CHARACTER &&
player.storage?.mengsanCamp_shuying === "enemy";
// 在梦三的原生changeHp事件创建后限幅;保留护甲消耗量,只将穿透后的生命损失限制为1。
// 层数由实际changeHp通知消耗,完全格挡、零伤害及取消事件不消耗。
export function capInkletHpLoss(player, event) {
if (!isInklet(player) || !(player.storage.mengsanSlippery_shuying > 0) ||
!Number.isFinite(event.num) || event.num >= 0) return event;
const parent = event.getParent();
if (parent?.unreal) return event;
const armor = parent?.name === "damage" && !parent.nohujia && !player.hasSkillTag("nohujia") ?
Math.max(0, player.hujia || 0) : 0;
if (-event.num > armor) event.num = -Math.min(-event.num, armor + 1);
return event;
}
+26
View File
@@ -0,0 +1,26 @@
// 闪光贾克斯果:每回合能量球,伤害结算完成后获得2力量;不随机选择行动。
export const JAXFRUIT_CHARACTER = "mengsan_snapping_jaxfruit_shuying";
export const JAXFRUIT_MOVE = Object.freeze({ id: "energyOrb", name: "能量球", damage: 3, strength: 2 });
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("贾克斯果行动记录无效");
return turns;
};
export function selectJaxfruitMove(state = {}) {
turnsOf(state);
return JAXFRUIT_MOVE;
}
export function recordJaxfruitAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) {
throw new RangeError("贾克斯果行动推进无效");
}
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export function rollJaxfruitHp(random) {
const value = random();
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("贾克斯果随机数必须在[0,1)内");
return 31 + Math.floor(value * 3);
}
export const isJaxfruit = player => player?.name === JAXFRUIT_CHARACTER &&
player.storage?.mengsanCamp_shuying === "enemy";
+26
View File
@@ -0,0 +1,26 @@
// 灰机Wiki普通数值:第一回合黏糊射击,之后与团块射击严格交替。
export const LEAFMEDIUM_CHARACTER = "mengsan_leaf_slime_m_shuying";
export const LEAFMEDIUM_MOVES = Object.freeze({
clump: Object.freeze({ id: "clump", name: "团块射击", damage: 8 }),
sticky: Object.freeze({ id: "sticky", name: "黏糊射击", slimed: 2 }),
});
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("树叶史莱姆行动记录无效");
return turns;
};
const roll = random => {
const value = random();
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("树叶史莱姆随机数必须在[0,1)内");
return value;
};
export function selectLeafslimeMediumMove(state = {}) {
return turnsOf(state) % 2 === 0 ? LEAFMEDIUM_MOVES.sticky : LEAFMEDIUM_MOVES.clump;
}
export function recordLeafslimeMediumAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("树叶史莱姆行动推进无效");
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export const rollLeafslimeMediumHp = random => 32 + Math.floor(roll(random) * 4);
export const isLeafslimeMedium = player => player?.name === LEAFMEDIUM_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
+26
View File
@@ -0,0 +1,26 @@
import { createSlimedData } from "../cards/slimed-card.js";
import { getIntentHostiles } from "../battle/intent-effects.js";
import { isLeafslime } from "./leafslime-intent.js";
import { isLeafslimeMedium } from "./leafmedium-intent.js";
import { isTwigmedium } from "./twigmedium-intent.js";
export function applyLeafslimeGoop(game, current, source, count = 1) {
if (!current?.session.active || !source.isAlive() || !(isLeafslime(source) || isLeafslimeMedium(source) || isTwigmedium(source))) return;
const expected = isLeafslimeMedium(source) ? 2 : 1;
if (count !== expected) throw new RangeError("黏液牌数量与怪物配置不符");
if (!current.leafslimeCards) {
const counter = current.leafslimeCards = { sequence: 0 };
current.session.ownResource(counter, () => { delete current.leafslimeCards; });
}
for (const target of getIntentHostiles(game, source)) {
if (!current.session.active || !source.isAlive()) break;
const pile = target === game.me ? current.personalPiles : current.monsterPiles.get(target);
if (!pile) continue; // 无个人牌堆的剧情援军不污染公共牌堆。
for (let index = 0; index < count; index++) {
if (!current.session.active || !source.isAlive()) break;
const sequence = ++current.leafslimeCards.sequence;
if (!Number.isSafeInteger(sequence)) throw new RangeError("黏液牌序号溢出");
pile.addToDiscard(createSlimedData(`slimed_${source.playerid}_${sequence}`));
}
game.log(target, `的个人弃牌堆加入${count}张【黏液】`);
}
}
+27
View File
@@ -0,0 +1,27 @@
// 灰机Wiki普通数值:每次行动在冲撞/黏液之间等权重随机。
export const LEAFSLIME_CHARACTER = "mengsan_leaf_slime_s_shuying";
export const LEAFSLIME_MOVES = Object.freeze({
tackle: Object.freeze({ id: "tackle", name: "冲撞", damage: 3 }),
goop: Object.freeze({ id: "goop", name: "黏液", slimed: 1 }),
});
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("树叶史莱姆行动记录无效");
return turns;
};
const roll = random => {
const value = random();
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("树叶史莱姆随机数必须在[0,1)内");
return value;
};
export function selectLeafslimeMove(state = {}, random) {
turnsOf(state);
return roll(random) < 0.5 ? LEAFSLIME_MOVES.tackle : LEAFSLIME_MOVES.goop;
}
export function recordLeafslimeAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("树叶史莱姆行动推进无效");
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export const rollLeafslimeHp = random => 11 + Math.floor(roll(random) * 5);
export const isLeafslime = player => player?.name === LEAFSLIME_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
+38
View File
@@ -0,0 +1,38 @@
// 灰机 Wiki 蛮兽页普通数值;冷却1表示攻击不能连续使用。
export const MAWLER_CHARACTER = "mengsan_mawler_shuying";
export const MAWLER_MOVES = Object.freeze({
claw: Object.freeze({ id: "claw", name: "爪击", damage: 4, hits: 2 }),
rip: Object.freeze({ id: "rip", name: "狂乱撕扯", damage: 14 }),
roar: Object.freeze({ id: "roar", name: "怒吼", debuff: "易伤", stacks: 3 }),
});
export function eligibleMawlerMoves(state = {}) {
const turn = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turn) || turn < 0) throw new RangeError("蛮兽行动次数无效");
if (turn === 0) return [MAWLER_MOVES.claw];
const moves = [];
if (state.lastMove !== "claw") moves.push(MAWLER_MOVES.claw);
if (state.lastMove !== "rip") moves.push(MAWLER_MOVES.rip);
if (!state.roarUsed) moves.push(MAWLER_MOVES.roar);
return moves;
}
export function selectMawlerMove(state = {}, random) {
const moves = eligibleMawlerMoves(state);
if (moves.length === 1) return moves[0];
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 recordMawlerAction(state = {}, move, paid) {
const turn = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turn) || turn < 0 || turn === Number.MAX_SAFE_INTEGER) {
throw new RangeError("蛮兽行动次数无效");
}
if (paid && (!move || !Object.hasOwn(MAWLER_MOVES, move.id))) throw new TypeError("蛮兽行动无效");
return {
turnsTaken: turn + 1,
lastMove: paid ? move.id : state.lastMove || null,
roarUsed: Boolean(state.roarUsed || paid && move.id === "roar"),
};
}
export const isMawler = player => player?.name === MAWLER_CHARACTER &&
player.storage?.mengsanCamp_shuying === "enemy";
+126
View File
@@ -16,6 +16,24 @@ const BASE_DECKS = Object.freeze({
const HEALTH = Object.freeze({
mengsan_soldier_shuying: 24,
mengsan_flyconid_shuying: 48,
mengsan_fogmog_shuying: 74, mengsan_eye_with_teeth_shuying: 6,
mengsan_mawler_shuying: 72,
mengsan_vine_shambler_shuying: 61,
mengsan_nibbit_shuying: 44,
mengsan_cubex_construct_shuying: 65,
mengsan_byrdonis_shuying: 82,
mengsan_bygone_effigy_shuying: 127,
mengsan_phrog_parasite_shuying: 62,
mengsan_wriggler_shuying: 19,
mengsan_shrinker_beetle_shuying: 39,
mengsan_twig_slime_m_shuying: 27,
mengsan_twig_slime_s_shuying: 9,
mengsan_leaf_slime_m_shuying: 34,
mengsan_leaf_slime_s_shuying: 13,
mengsan_slithering_strangler_shuying: 54,
mengsan_snapping_jaxfruit_shuying: 32,
mengsan_inklet_shuying: 14,
mengsan_fuzzy_wurm_crawler_shuying: 56,
mengsan_raider_brute_shuying: 32,
mengsan_raider_assassin_shuying: 21,
mengsan_raider_axe_shuying: 21,
@@ -68,6 +86,95 @@ export class OpeningSoldier extends Monster {
export class Flyconid extends Monster {
constructor(options = {}) { super({ ...options, character: "mengsan_flyconid_shuying", tier: "normal", hp: 48 }); }
}
export class FuzzyWurmCrawler extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_fuzzy_wurm_crawler_shuying", tier: "normal", hp: options.hp ?? 56 });
}
}
export class Inklet extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_inklet_shuying", tier: "normal", hp: options.hp ?? 14 });
}
}
export class SnappingJaxfruit extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_snapping_jaxfruit_shuying", tier: "normal", hp: options.hp ?? 32 });
}
}
export class SlitheringStrangler extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_slithering_strangler_shuying", tier: "normal", hp: options.hp ?? 54 });
}
}
export class LeafSlimeSmall extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_leaf_slime_s_shuying", tier: "normal", hp: options.hp ?? 13 });
}
}
export class LeafSlimeMedium extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_leaf_slime_m_shuying", tier: "normal", hp: options.hp ?? 34 });
}
}
export class TwigSlimeSmall extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_twig_slime_s_shuying", tier: "normal", hp: options.hp ?? 9 });
}
}
export class TwigSlimeMedium extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_twig_slime_m_shuying", tier: "normal", hp: options.hp ?? 27 });
}
}
export class ShrinkerBeetle extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_shrinker_beetle_shuying", tier: "normal", hp: options.hp ?? 39 });
}
}
export class PhrogParasite extends Monster {
constructor(options = {}) { super({ ...options, character: "mengsan_phrog_parasite_shuying", tier: "elite", hp: options.hp ?? 62 }); }
}
export class Wriggler extends Monster {
constructor(options = {}) { super({ ...options, character: "mengsan_wriggler_shuying", tier: "elite", hp: options.hp ?? 19 }); }
}
export class BygoneEffigy extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_bygone_effigy_shuying", tier: "elite", hp: options.hp ?? 127 });
}
}
export class Byrdonis extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_byrdonis_shuying", tier: "elite", hp: options.hp ?? 82 });
}
}
export class CubexConstruct extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_cubex_construct_shuying", tier: "normal", hp: options.hp ?? 65 });
}
}
export class Nibbit extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_nibbit_shuying", tier: "normal", hp: options.hp ?? 44 });
}
}
export class VineShambler extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_vine_shambler_shuying", tier: "normal", hp: options.hp ?? 61 });
}
}
export class Mawler extends Monster {
constructor(options = {}) { super({ ...options, character: "mengsan_mawler_shuying", tier: "normal", hp: 72 }); }
}
export class Fogmog extends Monster {
constructor(options = {}) { super({ ...options, character: "mengsan_fogmog_shuying", tier: "normal", hp: 74 }); }
}
export class EyeWithTeeth extends Monster {
constructor(options = {}) {
super({ ...options, character: "mengsan_eye_with_teeth_shuying", tier: "normal", hp: 6, energy: 1, handLimit: 1 });
this.draw = 0;
}
createDeck() { return []; } // 爪牙仅发动牵制,不额外抽取或打出三国杀牌。
}
const raiderClass = (character, hp) => class extends Monster {
constructor(options = {}) { super({ ...options, character, tier: "normal", hp }); }
};
@@ -81,6 +188,25 @@ export const RaiderTracker = raiderClass("mengsan_raider_tracker_shuying", 23);
export const MONSTER_TYPES = Object.freeze({
mengsan_soldier_shuying: OpeningSoldier,
mengsan_flyconid_shuying: Flyconid,
mengsan_fogmog_shuying: Fogmog,
mengsan_mawler_shuying: Mawler,
mengsan_vine_shambler_shuying: VineShambler,
mengsan_nibbit_shuying: Nibbit,
mengsan_cubex_construct_shuying: CubexConstruct,
mengsan_byrdonis_shuying: Byrdonis,
mengsan_bygone_effigy_shuying: BygoneEffigy,
mengsan_phrog_parasite_shuying: PhrogParasite,
mengsan_wriggler_shuying: Wriggler,
mengsan_shrinker_beetle_shuying: ShrinkerBeetle,
mengsan_twig_slime_m_shuying: TwigSlimeMedium,
mengsan_twig_slime_s_shuying: TwigSlimeSmall,
mengsan_leaf_slime_m_shuying: LeafSlimeMedium,
mengsan_leaf_slime_s_shuying: LeafSlimeSmall,
mengsan_slithering_strangler_shuying: SlitheringStrangler,
mengsan_snapping_jaxfruit_shuying: SnappingJaxfruit,
mengsan_inklet_shuying: Inklet,
mengsan_fuzzy_wurm_crawler_shuying: FuzzyWurmCrawler,
mengsan_eye_with_teeth_shuying: EyeWithTeeth,
mengsan_raider_brute_shuying: RaiderBrute,
mengsan_raider_assassin_shuying: RaiderAssassin,
mengsan_raider_axe_shuying: RaiderAxe,
+44
View File
@@ -0,0 +1,44 @@
// 普通难度:顶撞→犹豫斩击→哈气。双只分别从斩击、哈气进入同一循环。
export const NIBBIT_CHARACTER = "mengsan_nibbit_shuying";
export const NIBBIT_MOVES = Object.freeze({
butt: Object.freeze({ id: "butt", name: "顶撞", damage: 12 }),
slice: Object.freeze({ id: "slice", name: "犹豫斩击", damage: 6, block: 5 }),
hiss: Object.freeze({ id: "hiss", name: "哈气", strength: 2 }),
});
const CYCLE = Object.freeze([NIBBIT_MOVES.butt, NIBBIT_MOVES.slice, NIBBIT_MOVES.hiss]);
const openingOf = state => {
const opening = state.opening ?? "butt";
if (!Object.hasOwn(NIBBIT_MOVES, opening)) throw new RangeError("小啃兽起手无效");
return opening;
};
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("小啃兽行动记录无效");
return turns;
};
export const selectNibbitMove = (state = {}) => {
const offset = openingOf(state) === "slice" ? 1 : openingOf(state) === "hiss" ? 2 : 0;
return CYCLE[(turnsOf(state) % CYCLE.length + offset) % CYCLE.length];
};
export function recordNibbitAction(state = {}, advance = true) {
const opening = openingOf(state), turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("小啃兽行动推进无效");
return { opening, turnsTaken: turns + (advance ? 1 : 0) };
}
export function rollNibbitHp(random) {
const roll = random();
if (!Number.isFinite(roll) || roll < 0 || roll >= 1) throw new RangeError("怪物随机数必须在[0,1)内");
return 42 + Math.floor(roll * 5);
}
export const isNibbit = player => player?.name === NIBBIT_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
export function bindNibbitOpenings(players) {
// 开战时按关卡前后顺序固定起手;死亡、席位变更或重复规划不重置循环。
const group = players.filter(isNibbit);
group.forEach((player, index) => {
if (player.storage.mengsanNibbitState_shuying != null) return;
player.storage.mengsanNibbitState_shuying = {
opening: group.length === 2 ? (index === 0 ? "slice" : "hiss") : "butt",
turnsTaken: 0,
};
});
}
+38
View File
@@ -0,0 +1,38 @@
import { INFESTED_COUNT, isPhrog, WRIGGLER_CHARACTER } from "./phrog-intent.js";
// 死亡钩子被原生die事件await;所有衍生物入场后才继续原伤害/卡牌结算。
export function createPhrogBattle(game, current, spawn) {
const consumed = new WeakSet();
let pending = 0, sequence = 0;
const controller = {
get pending() {
// 原生die先移入dead,再调整负生命至0,最后才调用dieAfter。
// changeHpAfter可能在这段间隙检查胜利;死体本身也须预留亡语。
return pending > 0 || current.session.active && game.dead.some(player =>
current.players.has(player) && isPhrog(player) && !consumed.has(player) &&
player.storage.mengsanInfested_shuying === INFESTED_COUNT);
},
async onDeath(player) {
if (!current.session.active || !isPhrog(player) || player.isAlive() ||
!current.players.has(player) || consumed.has(player)) return false;
const count = player.storage.mengsanInfested_shuying ?? 0;
if (count !== INFESTED_COUNT) throw new RangeError("寄生物召唤配置无效");
if (game.players.length + game.dead.length + count > 8) throw new Error("异蛙寄生虫召唤席位不足");
if (!Number.isSafeInteger(sequence + 1)) throw new RangeError("寄生物召唤序号溢出");
consumed.add(player); pending++;
const group = ++sequence;
try {
let anchor = player;
for (let position = 1; position <= count; position++) {
if (!current.session.active) break;
anchor = await spawn({ id: `wriggler_${player.playerid}_${group}_${position}`,
character: WRIGGLER_CHARACTER, camp: "enemy", inheritSkills: false,
hand: 4, wrigglerPosition: position, wrigglerSpawned: true }, anchor);
}
return true;
} finally { pending--; }
},
};
current.session.ownResource(controller, () => { if (current.phrog === controller) delete current.phrog; });
return controller;
}
+75
View File
@@ -0,0 +1,75 @@
// 灰机Wiki普通数值;不混入A8生命和A9攻击。
export const PHROG_CHARACTER = "mengsan_phrog_parasite_shuying";
export const WRIGGLER_CHARACTER = "mengsan_wriggler_shuying";
export const INFESTED_COUNT = 4;
export const PHROG_MOVES = Object.freeze({
infect: Object.freeze({ id: "infect", name: "感染", infection: 3 }),
lash: Object.freeze({ id: "lash", name: "甩动", damage: 4, hits: 4 }),
});
export const WRIGGLER_MOVES = Object.freeze({
spawned: Object.freeze({ id: "spawned", name: "生成", intentType: "stun" }),
bite: Object.freeze({ id: "bite", name: "污秽啃咬", damage: 6 }),
wriggle: Object.freeze({ id: "wriggle", name: "扭动", infection: 1, strength: 2 }),
});
const turnsOf = state => {
const value = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(value) || value < 0) throw new RangeError("异蛙行动次数无效");
return value;
};
const positionOf = state => {
const position = state.position ?? 1;
if (!Number.isInteger(position) || position < 1 || position > 4) throw new RangeError("扭动虫站位必须为1~4");
return position;
};
export const selectPhrogMove = (state = {}) => turnsOf(state) % 2 ? PHROG_MOVES.lash : PHROG_MOVES.infect;
export function selectWrigglerMove(state = {}) {
const position = positionOf(state), turn = turnsOf(state);
if (state.spawned) return WRIGGLER_MOVES.spawned;
return (turn + (position % 2 ? 0 : 1)) % 2 ? WRIGGLER_MOVES.wriggle : WRIGGLER_MOVES.bite;
}
export function recordPhrogAction(state = {}, advance = true) {
const turn = turnsOf(state);
if (typeof advance !== "boolean" || advance && turn === Number.MAX_SAFE_INTEGER) throw new RangeError("异蛙行动推进无效");
return { ...state, turnsTaken: turn + (advance ? 1 : 0) };
}
const rollHp = (random, base, range) => {
const value = random();
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("异蛙随机数必须在[0,1)内");
return base + Math.floor(value * range);
};
export const rollPhrogHp = random => rollHp(random, 61, 4);
export const rollWrigglerHp = random => rollHp(random, 17, 5);
export const isPhrog = player => player?.name === PHROG_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
export const isWriggler = player => player?.name === WRIGGLER_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
export const isPhrogActor = player => isPhrog(player) || isWriggler(player);
const bindings = new WeakMap();
export function initializePhrog(player, spec, current = null) {
if (!isPhrogActor(player)) return false;
const position = isWriggler(player) ? positionOf({ position: spec.wrigglerPosition }) : 1;
if (current?.session.active && bindings.get(player)?.battle !== current) {
const entry = { battle: current };
current.session.ownResource(entry, () => {
if (bindings.get(player) !== entry) return;
bindings.delete(player);
delete player.storage.mengsanPhrogState_shuying;
delete player.storage.mengsanPhrogIntent_shuying;
player.removeSkill("mengsan_infested_shuying");
player.removeSkill("mengsan_raider_strength_shuying");
});
bindings.set(player, entry);
}
player.storage.mengsanPhrogState_shuying = { turnsTaken: 0, position, spawned: isWriggler(player) && spec.wrigglerSpawned === true };
player.storage.mengsanPhrogIntent_shuying = null;
if (isPhrog(player)) {
player.storage.mengsanInfested_shuying = INFESTED_COUNT;
player.addSkill("mengsan_infested_shuying"); player.markSkill("mengsan_infested_shuying");
}
return true;
}
export function skipSpawnedWriggler(player, phase) {
if (!isWriggler(player) || !player.isAlive() || !player.storage.mengsanPhrogState_shuying?.spawned) return false;
phase.cancel();
player.storage.mengsanPhrogState_shuying = { ...player.storage.mengsanPhrogState_shuying, spawned: false };
player.storage.mengsanPhrogIntent_shuying = null;
return true;
}
+27
View File
@@ -0,0 +1,27 @@
// 缩小甲虫,灰机Wiki普通数值;缩小后大啃、践踏严格交替。
export const SHRINKER_CHARACTER = "mengsan_shrinker_beetle_shuying";
export const SHRINKER_MOVES = Object.freeze({
shrink: Object.freeze({ id: "shrink", name: "缩小", debuff: "缩小", stacks: 1, shrink: true }),
chomp: Object.freeze({ id: "chomp", name: "大啃", damage: 7 }),
stomp: Object.freeze({ id: "stomp", name: "践踏", damage: 13 }),
});
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("缩小甲虫行动记录无效");
return turns;
};
export function selectShrinkerMove(state = {}) {
const turns = turnsOf(state);
return turns === 0 ? SHRINKER_MOVES.shrink : turns % 2 ? SHRINKER_MOVES.chomp : SHRINKER_MOVES.stomp;
}
export function recordShrinkerAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("缩小甲虫行动推进无效");
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export function rollShrinkerHp(random) {
const value = random();
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("缩小甲虫随机数必须在[0,1)内");
return 38 + Math.floor(value * 3);
}
export const isShrinker = player => player?.name === SHRINKER_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
+45
View File
@@ -0,0 +1,45 @@
import { consumeArtifact } from "./artifact-status.js";
import { isShrinker } from "./shrinker-intent.js";
const SKILL = "mengsan_shrink_shuying";
const KEY = "mengsanShrink_shuying";
const clear = target => { delete target.storage[KEY]; target.removeSkill(SKILL); };
// 来源只保留在战斗对象中;多只甲虫不累乘减伤,死亡逐个解除来源。
export function isShrunk(current, target) {
const records = current?.shrinkerSources;
const sources = records?.get(target);
if (!sources) return false;
if (!current.session.active || !target.isAlive()) {
records.delete(target); clear(target); return false;
}
for (const source of sources) {
if (!source.isAlive() || !isShrinker(source) || !current.players.has(source) ||
source.storage.mengsanCamp_shuying === target.storage.mengsanCamp_shuying) sources.delete(source);
}
if (sources.size) { target.storage[KEY] = true; return true; }
records.delete(target); clear(target); return false;
}
export function applyShrink(current, source, target) {
if (!current?.session.active || !isShrinker(source) || !source.isAlive() ||
!current.players.has(source) || !target.isAlive() ||
source.storage.mengsanCamp_shuying === target.storage.mengsanCamp_shuying) return false;
if (consumeArtifact(target)) return false;
if (!current.shrinkerSources) {
const records = current.shrinkerSources = new Map();
current.session.ownResource(records, () => {
for (const target of records.keys()) clear(target);
records.clear(); delete current.shrinkerSources;
});
}
isShrunk(current, target);
const sources = current.shrinkerSources.get(target) || new Set();
sources.add(source); current.shrinkerSources.set(target, sources);
target.storage[KEY] = true; target.addSkill(SKILL); target.markSkill(SKILL);
return true;
}
export function clearShrinkSource(current, source) {
const records = current?.shrinkerSources;
if (!records) return;
if (records.has(source)) { records.delete(source); clear(source); }
for (const [target, sources] of records) { sources.delete(source); isShrunk(current, target); }
}
export const shrinkAttackDamage = (damage, source) => source?.storage?.mengsanShrink_shuying ? Math.floor(damage * 7 / 10) : damage;
+62
View File
@@ -0,0 +1,62 @@
import { consumeArtifact } from "./artifact-status.js";
import { isStrangler } from "./strangler-intent.js";
const SKILL = "mengsan_constrict_shuying";
const KEY = "mengsanConstrict_shuying";
const clear = target => { delete target.storage[KEY]; target.removeSkill(SKILL); };
// 来源引用仅保存在本场战斗中,不进入永久牌堆/存档;同名多只怪物分别计层。
export function constrictTotal(current, target) {
const records = current?.stranglerConstrict;
const sources = records?.get(target);
if (!sources) return 0;
if (!current.session.active || !target.isAlive()) {
records.delete(target); clear(target); return 0;
}
let total = 0;
for (const [source, layers] of sources) {
if (!source.isAlive() || !isStrangler(source) || !current.players.has(source) ||
source.storage.mengsanCamp_shuying === target.storage.mengsanCamp_shuying) sources.delete(source);
else total += layers;
}
if (total) { target.storage[KEY] = total; target.markSkill(SKILL); }
else { records.delete(target); clear(target); }
return total;
}
export function addConstrict(current, source, target, layers) {
if (!current?.session.active || !isStrangler(source) || !source.isAlive() ||
!current.players.has(source) || !target.isAlive() ||
target.storage.mengsanCamp_shuying === source.storage.mengsanCamp_shuying) return false;
if (!Number.isSafeInteger(layers) || layers <= 0) throw new RangeError("紧缠层数无效");
if (!current.stranglerConstrict) {
const records = current.stranglerConstrict = new Map();
current.session.ownResource(records, () => {
for (const target of records.keys()) clear(target);
records.clear(); delete current.stranglerConstrict;
});
}
constrictTotal(current, target);
const records = current.stranglerConstrict;
const sources = records.get(target) || new Map();
const total = (target.storage[KEY] || 0) + layers;
if (!Number.isSafeInteger(total)) throw new RangeError("紧缠层数溢出");
if (consumeArtifact(target)) return false;
sources.set(source, (sources.get(source) || 0) + layers); records.set(target, sources);
target.storage[KEY] = total; target.addSkill(SKILL); target.markSkill(SKILL);
return true;
}
export function clearConstrictSource(current, source) {
const records = current?.stranglerConstrict;
if (!records) return;
if (records.has(source)) { records.delete(source); clear(source); }
for (const [target, sources] of records) {
sources.delete(source); constrictTotal(current, target);
}
}
export async function resolveConstrict(current, target) {
const amount = constrictTotal(current, target);
if (!amount) return;
// 非攻击伤害:可由原生护甲抵扣,不受力量/虚弱/易伤的攻击修正。
const hit = target.damage(amount, "nosource");
hit.mengsanConstrictDamage_shuying = true;
hit.mengsanScriptedSkill_shuying = true;
await hit;
}
+31
View File
@@ -0,0 +1,31 @@
// 蛇行扼杀者,普通数值:缠身与等权重随机攻击严格交替。
export const STRANGLER_CHARACTER = "mengsan_slithering_strangler_shuying";
export const STRANGLER_MOVES = Object.freeze({
constrict: Object.freeze({ id: "constrict", name: "缠身", debuff: "紧缠", stacks: 3, constrict: 3 }),
thwack: Object.freeze({ id: "thwack", name: "重击", damage: 7, block: 5 }),
lash: Object.freeze({ id: "lash", name: "甩动", damage: 12 }),
});
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("扼杀者行动记录无效");
return turns;
};
const roll = random => {
const value = random();
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("扼杀者随机数必须在[0,1)内");
return value;
};
export function selectStranglerMove(state = {}, random) {
return turnsOf(state) % 2 === 0 ? STRANGLER_MOVES.constrict :
roll(random) < 0.5 ? STRANGLER_MOVES.thwack : STRANGLER_MOVES.lash;
}
export function recordStranglerAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) {
throw new RangeError("扼杀者行动推进无效");
}
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export const rollStranglerHp = random => 53 + Math.floor(roll(random) * 3);
export const isStrangler = player => player?.name === STRANGLER_CHARACTER &&
player.storage?.mengsanCamp_shuying === "enemy";
+47
View File
@@ -0,0 +1,47 @@
// 灰机Wiki普通数值:首回合黏糊射击;随机分支等权,黏液不连用,扑击最多连用两次。
export const TWIGMEDIUM_CHARACTER = "mengsan_twig_slime_m_shuying";
export const TWIGMEDIUM_MOVES = Object.freeze({
sticky: Object.freeze({ id: "sticky", name: "黏糊射击", slimed: 1 }),
pounce: Object.freeze({ id: "pounce", name: "戳刺扑击", damage: 11 }),
});
const stateOf = state => {
const turnsTaken = state.turnsTaken ?? 0;
const lastMove = state.lastMove ?? null;
const consecutive = state.consecutive ?? 0;
if (!Number.isSafeInteger(turnsTaken) || turnsTaken < 0 ||
!Number.isSafeInteger(consecutive) || consecutive < 0 ||
(turnsTaken === 0 ? lastMove !== null || consecutive !== 0 :
!Object.hasOwn(TWIGMEDIUM_MOVES, lastMove) || consecutive < 1 ||
consecutive > turnsTaken || consecutive > (lastMove === "sticky" ? 1 : 2))) {
throw new RangeError("中型树枝史莱姆行动记录无效");
}
return { turnsTaken, lastMove, consecutive };
};
const roll = random => {
const value = random();
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("随机数必须在[0,1)内");
return value;
};
export function selectTwigmediumMove(state = {}, random) {
const { turnsTaken, lastMove, consecutive } = stateOf(state);
if (turnsTaken === 0) return TWIGMEDIUM_MOVES.sticky;
if (lastMove === "sticky") return TWIGMEDIUM_MOVES.pounce;
if (consecutive === 2) return TWIGMEDIUM_MOVES.sticky;
return roll(random) < 0.5 ? TWIGMEDIUM_MOVES.sticky : TWIGMEDIUM_MOVES.pounce;
}
export function recordTwigmediumAction(state = {}, move, advance = true) {
const previous = stateOf(state);
if (typeof advance !== "boolean") throw new RangeError("行动推进参数无效");
if (!advance) return previous;
const id = move?.id;
if (previous.turnsTaken === Number.MAX_SAFE_INTEGER || !Object.hasOwn(TWIGMEDIUM_MOVES, id) ||
(previous.turnsTaken === 0 && id !== "sticky") ||
(previous.lastMove === id && previous.consecutive >= (id === "sticky" ? 1 : 2))) {
throw new RangeError("中型树枝史莱姆行动推进无效");
}
return { turnsTaken: previous.turnsTaken + 1, lastMove: id,
consecutive: previous.lastMove === id ? previous.consecutive + 1 : 1 };
}
export const rollTwigmediumHp = random => 26 + Math.floor(roll(random) * 3);
export const isTwigmedium = player => player?.name === TWIGMEDIUM_CHARACTER &&
player.storage?.mengsanCamp_shuying === "enemy";
+26
View File
@@ -0,0 +1,26 @@
// 灰机Wiki普通数值:每回合冲撞,造成4点攻击伤害。
export const TWIGSLIME_CHARACTER = "mengsan_twig_slime_s_shuying";
export const TWIGSLIME_MOVES = Object.freeze({
tackle: Object.freeze({ id: "tackle", name: "冲撞", damage: 4 }),
});
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("树叶史莱姆行动记录无效");
return turns;
};
const roll = random => {
const value = random();
if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("树叶史莱姆随机数必须在[0,1)内");
return value;
};
export function selectTwigslimeMove(state = {}) {
turnsOf(state);
return TWIGSLIME_MOVES.tackle;
}
export function recordTwigslimeAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("树叶史莱姆行动推进无效");
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export const rollTwigslimeHp = random => 7 + Math.floor(roll(random) * 5);
export const isTwigslime = player => player?.name === TWIGSLIME_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
+20
View File
@@ -0,0 +1,20 @@
// 灰机Wiki普通数值,SVG箭头:挥击→紧绕藤蔓→大啃→挥击。
export const VINE_CHARACTER = "mengsan_vine_shambler_shuying";
export const VINE_MOVES = Object.freeze({
swipe: Object.freeze({ id: "swipe", name: "挥击", damage: 6, hits: 2 }),
vines: Object.freeze({ id: "vines", name: "紧绕藤蔓", damage: 8, debuff: "缠结", stacks: 1 }),
chomp: Object.freeze({ id: "chomp", name: "大啃", damage: 16 }),
});
const turnsOf = state => {
const turns = state.turnsTaken ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("藤蔓蹒跚者行动记录无效");
return turns;
};
const CYCLE = Object.freeze([VINE_MOVES.swipe, VINE_MOVES.vines, VINE_MOVES.chomp]);
export const selectVineMove = (state = {}) => CYCLE[turnsOf(state) % CYCLE.length];
export function recordVineAction(state = {}, advance = true) {
const turns = turnsOf(state);
if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("藤蔓蹒跚者行动推进无效");
return { turnsTaken: turns + (advance ? 1 : 0) };
}
export const isVine = player => player?.name === VINE_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy";
+37
View File
@@ -0,0 +1,37 @@
import { consumeArtifact } from "./artifact-status.js";
import { cardDefinitions } from "../cards/card-definitions.js";
const KEY = "mengsanTangled_shuying";
const SKILL = "mengsan_tangled_shuying";
// 梦三伤害牌类别;未进永久奖励注册表的原生攻击锦囊也适用。
const nativeAttacks = new Set(["nanman", "wanjian", "huogong"]);
export const isAttackCard = card => Boolean(card && (nativeAttacks.has(card.name) ||
Object.hasOwn(cardDefinitions, card.name) && cardDefinitions[card.name].category === "damage"));
const clear = target => { delete target.storage[KEY]; target.removeSkill(SKILL); };
const turnsOf = target => {
const turns = target.storage?.[KEY] ?? 0;
if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("缠结持续回合无效");
return turns;
};
export function applyTangled(current, target, turns) {
if (!current?.session.active || !target.isAlive() || !current.players.has(target)) return false;
if (!Number.isSafeInteger(turns) || turns < 1) throw new RangeError("缠结回合数必须为正整数");
const total = turnsOf(target) + turns;
if (!Number.isSafeInteger(total)) throw new RangeError("缠结回合数溢出");
if (consumeArtifact(target)) return false;
if (!current.tangledTargets) {
const targets = current.tangledTargets = new Set();
current.session.ownResource(targets, () => {
for (const target of targets) clear(target);
targets.clear(); delete current.tangledTargets;
});
}
current.tangledTargets.add(target); target.storage[KEY] = total;
target.addSkill(SKILL); target.markSkill(SKILL); current.handUI?.refresh(); return true;
}
export function advanceTangled(current, target) {
if (!current?.session.active || !current.tangledTargets?.has(target)) return;
const remaining = Math.max(0, turnsOf(target) - 1);
if (remaining) { target.storage[KEY] = remaining; target.markSkill(SKILL); }
else { current.tangledTargets.delete(target); clear(target); }
current.handUI?.refresh();
}