Playtest #6 fixes: summons placed by hand, boss pool stall, bounties, 联防 leak counter, distinct offers, 机变 card text, phone buy tap, skill triggers, blocking, push force, 孤立, enemies, element bar
- #1 #2 skill / device summons (赫默's drone, 巫恋's doll, 凯瑟琳's devices) are hand cards placed by the player; placed summons deploy once at battle start, then a skill summon re-appears at its tile on each cast (the user's call) - #3 叙拉古 隐匿 ends on time (32 + 0.4 × layers s); 忍冬 S3 迷彩 lasts until her next cast (official), kept apart from 隐匿 - #4 multi-round bounty cards last two battles (the user's call; MULTI_ROUND_BOUNTY_BATTLES), blue / red card text - #5 the shared boss pool's float dust (≈3.6e-12 HP) stalled Final Assault leaders at 0 %; < 1 HP now clears the pool, and the team LP reaching 0 first is a defeat (PRTS) - #6 #10 机变 cards show their official effect text; an invisible 详情 button no longer swallows the confirming tap on phones - #7 live ×N counter of a leaker's enemies still on the 联防 field (LP loss stays capped at 10) - #8 #15 #16 skill triggers per 技能策略: skill-range triggers, injured-ally heals (古米, 塞雷娅), tank take-damage triggers with a 3 s cooldown; 新约能天使 S2 shields the highest-aggro operator in range - #9 official per-enemy model scales (威龙) - #11 element bar under the unit's HP bar (white = 元素值 left, element-coloured refill after a burst), floor rounding - #12 枯朽战车 kit per PRTS; every ranged enemy reaches its target's 0.25 collider - #13 恐惧 wander (萨科塔之翼 at half HP, operator fears make enemies flee) with re-planning after pushes - #14 push / pull distance by 力度 − 重量 (PRTS table); #17 every blocker attacks what it blocks; ASPD floor 20 - #18 炎佑 is 孤立: no heals or ally buffs - #19 every free pick-one chess offer (promotion reward, 信标, 寻呼模块) shows different operators - DESIGN §20 and the normative lines, SIM / META / DATA / PLAYING updated; 2747 unit tests and all 27 browser suites green; dev recordings regenerated
This commit is contained in:
1 parent
e6628f0cfe
commit
6464e4b46a
138 files changed
+7946
-1379
No files matched your search
+55
-9
@@ -31,6 +31,8 @@
|
||||
// botPrepBeginSteps / botPrepEndSteps (and planLayoutSteps, arrangeSteps, createRehearsalSteps) are step
|
||||
// generators that yield between whole actions (never with a transient board) — the same actions in the same
|
||||
// order as the one-shot functions (runSteps), hence the same rng draws and decisions.
|
||||
// The summon cards of the placed operators (赫默's 医疗探机, 伺夜's 狼群 …; user playtest #6) are placed after
|
||||
// them on the best remaining tiles, 凯瑟琳's 支援装置 next to the best operator no device faces yet, facing it.
|
||||
// 5. equip items on the strongest deployed damage dealers (consume-on-equip items / Arts only with a handler)
|
||||
// 6. resolve the temp slots, keep one hand slot free, then Ready.
|
||||
// Placement quality (tools/matchrun sweeps, research-faithful waves): the planner beats random layouts by ≈ 8 points
|
||||
@@ -38,7 +40,8 @@
|
||||
|
||||
import { GEO } from '../../shared/constants.js';
|
||||
import { deriveSeed } from '../sim/rng.js';
|
||||
import { freeSlot, legalTiles, positionClass, parseKey, tileKey, FIELD, pieceDir, boardTileOf, BOSS_MIRROR_COL } from './board.js';
|
||||
import { ASPD_MIN } from '../sim/constants.js';
|
||||
import { freeSlot, legalTiles, canPlace, positionClass, parseKey, tileKey, FIELD, pieceDir, boardTileOf, BOSS_MIRROR_COL } from './board.js';
|
||||
import { rotateOffset, normDir, mirrorDir, oppositeDir } from '../sim/dir.js';
|
||||
import { itemKey } from './gamedata.js';
|
||||
import { computeBonds } from './bondsMeta.js';
|
||||
@@ -497,7 +500,7 @@ export function rangeTiles(rec, r, c, dir = 'RIGHT') {
|
||||
function dpsOf(rec) {
|
||||
const st = rec && rec.stats;
|
||||
if (!st || isHealer(rec)) return 0;
|
||||
const interval = Math.max(0.2, (st.bat || 1) * 100 / Math.max(10, st.aspd || 100));
|
||||
const interval = Math.max(0.2, (st.bat || 1) * 100 / Math.max(ASPD_MIN, st.aspd || 100));
|
||||
const d = (st.atk || 0) / interval;
|
||||
return rec.attackKind === 'none' ? d * 0.4 : d;
|
||||
}
|
||||
@@ -920,11 +923,7 @@ export function botPrepEnd(m, ps, job = null) {
|
||||
/** botPrepEnd as a step generator (see botPrepBeginSteps). */
|
||||
export function* botPrepEndSteps(m, ps, job = null) {
|
||||
if (!ps.alive || ps.ready) return;
|
||||
if (job && job.done && job.best !== job.plans[0]) {
|
||||
// summons placed around the default plan would sit on the new plan's tiles: back to their stacks, re-placed after
|
||||
liftTokens(ps);
|
||||
yield* applyPlanSteps(m, ps, job.chosen, job.best);
|
||||
}
|
||||
if (job && job.done && job.best !== job.plans[0]) yield* applyPlanSteps(m, ps, job.chosen, job.best);
|
||||
// 5. temp → hand / sell / destroy; keep one hand slot free for next round's merges
|
||||
resolveTemp(m, ps);
|
||||
if (freeSlot(ps.hand) < 0) sellWeakestHand(m, ps);
|
||||
@@ -1055,8 +1054,13 @@ export function* arrangeSteps(m, ps, { final = false, defer = false } = {}) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Put the chosen pieces on their planned tiles, fill what is still off the board, then place summons. */
|
||||
/**
|
||||
* Put the chosen pieces on their planned tiles, fill what is still off the board, then place summons. Placed summons go
|
||||
* back to their stacks first: they would sit on the plan's tiles (an operator moved onto one swaps it elsewhere), and a
|
||||
* 凯瑟琳 device left beside a tile its operator moved away from would face nothing (QA, playtest #6).
|
||||
*/
|
||||
function* applyPlanSteps(m, ps, chosen, target) {
|
||||
liftTokens(ps);
|
||||
// move pieces onto their targets (board → board moves swap; hand → board may swap an occupant back to the hand)
|
||||
for (let pass = 0; pass < 3; pass++) {
|
||||
let moved = false;
|
||||
@@ -1097,12 +1101,53 @@ function liftTokens(ps) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Summons that help the operator on the tile they face (range 1-1: their own tile + the one in front) instead of
|
||||
* covering the enemy path: 凯瑟琳's 爬行号·防护单元 (a hand piece since user playtest #6).
|
||||
*/
|
||||
const FRONT_SUPPORT_TOKENS = new Set(['token_10041_cathy_catsld']);
|
||||
/** Device tile offsets around an operator (behind it — enemies come from the gates on the right —, above, below, in front) and the facing that points back at it. */
|
||||
const SUPPORT_SPOTS = Object.freeze([[0, -1, 'RIGHT'], [1, 0, 'DOWN'], [-1, 0, 'UP'], [0, 1, 'LEFT']]);
|
||||
|
||||
/**
|
||||
* A tile + direction for a front-support device: next to the most valuable operator no device faces yet (blockers
|
||||
* first — they take the hits —, then DPS), pointing at it; null when none is free.
|
||||
*/
|
||||
function supportSpot(m, ps, p) {
|
||||
const rec = m.gd.token(p.id);
|
||||
if (!rec) return null;
|
||||
const map = ps.deployMap();
|
||||
const pos = positionClass(rec);
|
||||
const faced = new Set();
|
||||
for (const [k, q] of ps.board) {
|
||||
if (q.kind !== 'token' || !FRONT_SUPPORT_TOKENS.has(q.id)) continue;
|
||||
const [r, c] = parseKey(k);
|
||||
const [dr, dc] = rotateOffset(0, 1, pieceDir(q));
|
||||
faced.add(tileKey(r + dr, c + dc));
|
||||
}
|
||||
const ops = [...ps.board.entries()].filter(([k, q]) => q.kind === 'chess' && !faced.has(k)).map(([k, q]) => [k, m.gd.chess(q.id)]).filter(([, c]) => c)
|
||||
.sort((a, b) => Number(isBlocker(b[1])) - Number(isBlocker(a[1])) || dpsOf(b[1]) - dpsOf(a[1]) || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
||||
for (const [k] of ops) {
|
||||
const [r, c] = parseKey(k);
|
||||
for (const [dr, dc, dir] of SUPPORT_SPOTS) {
|
||||
const t = tileKey(r + dr, c + dc);
|
||||
if (!ps.board.has(t) && canPlace(map, pos, r + dr, c + dc)) return [r + dr, c + dc, dir];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Placeable summons from the hand / temp onto the best free tiles (one per stack count). */
|
||||
function* placeTokensSteps(m, ps) {
|
||||
for (const p of [...ps.hand, ...ps.temp]) {
|
||||
if (!p || p.kind !== 'token') continue;
|
||||
for (let n = p.count || 1; n > 0; n--) {
|
||||
yield;
|
||||
if (FRONT_SUPPORT_TOKENS.has(p.id)) {
|
||||
const spot = supportSpot(m, ps, p);
|
||||
if (!spot || !tryDo(() => ps.move(p.uid, { area: 'board', row: spot[0], col: spot[1] }, spot[2]))) break;
|
||||
continue;
|
||||
}
|
||||
const plan = yield* planLayoutSteps(m, ps, [p], LAYOUT_PARAMS, { occupied: new Set(ps.board.keys()) });
|
||||
const k = plan.get(p.uid);
|
||||
if (!k) break;
|
||||
@@ -1156,7 +1201,8 @@ function resolveTemp(m, ps) {
|
||||
tryDo(() => ps.destroy(p.uid));
|
||||
}
|
||||
}
|
||||
// anything left (e.g. sells failed): final sweep — tokens cannot be sold, they are dropped with the temp slot
|
||||
// anything left (e.g. sells failed): final sweep — tokens cannot be sold, they are dropped with the temp slot (and
|
||||
// come back at the next round start, PlayerState.startRound)
|
||||
for (let i = 0; i < ps.temp.length; i++) {
|
||||
const p = ps.temp[i];
|
||||
if (!p) continue;
|
||||
|
||||
Reference in new issue
Block a user