diff --git a/docs/DATA.md b/docs/DATA.md index b146061d..5b92e6cc 100644 --- a/docs/DATA.md +++ b/docs/DATA.md @@ -418,19 +418,22 @@ hidden core `h08_0X`; 联防 `act1autochess_escaped_single|multi`; training `tr0 Timing: templates have one wave with one fragment, so `time = wave.preDelay + fragment.preDelay + action.preDelay` exactly (the builder warns if a multi-fragment template ever appears). -## 12. `stages.json` — `{ [stageId]: Stage }` (11 terrains, 8 active, + the 2 联防 maps) +## 12. `stages.json` — `{ [stageId]: Stage }` (11 terrains, 8 active, + the 2 escaped levels' maps) -The 11 battle stages of `stageDatasDict`, then the two **联防 maps** (GitHub #41): act2autochess constData -`escapedBattleTemplateMapSinglePlayer` / `MultiPlayer` name the level of the 联防 battle (`level_act1autochess_escaped_single` -/ `_multi`, the wave templates of the same id in `waves.json`), and its own map is the 联防 field — tile for tile the same -on both: two road halves (cols 3–9 and 11–17, rows 9–12) joined at col 10, the objective at (9,2), no devices or special -terrain. `server/match/unite.js uniteStageId` fields the 联防 battle on the one of its helper count. +The 11 battle stages of `stageDatasDict` — every field of a match is fought on its stage: the own boards, the boss fields +and the 联防 field (both halves, `GEO.UNITE_RECT`; the owner's decision of 2026-10-07) — then the maps of the two escaped +levels: act2autochess constData `escapedBattleTemplateMapSinglePlayer` / `MultiPlayer` name the level of the 联防 wave +(`level_act1autochess_escaped_single` / `_multi`, the wave templates of the same id in `waves.json`). Their map is the +placeholder grid every wave template level carries, tile for tile (the round templates `01…07` and `h01…h08`, the +training `trXX` too): two road halves (cols 3–9 and 11–17, rows 9–12) joined at col 10, the objective at (9,2), no devices or +special terrain. 0.2.0 fought the 联防 battle on it (GitHub #41); no field uses these two records since 0.2.1 — they stay +as official level data, and sim tests use them as a plain two-halves road. | Field | Example | Meaning | |---|---|---| -| `id`, `name` | `"act2autochess_m01"`, `"战场#05(下半) 源石流发生装置"` | player-facing name from research 05 (the official tables carry none; falls back to id). The build drops bracketed segments containing Latin letters (research notes such as `战场#01 (upper half #01)` → `战场#01`), logs a warning, and fails validation if Latin text remains. The 联防 maps: `联防阵地(1名玩家)` / `联防阵地(2名玩家)`, the remake's own label [ASSUMED] (no table or PRTS page names them; no screen shows it) | -| `weight`, `active`, `modes` | `50`, `true`, `["mode_single_normal",…]` | match-start pick weight (act1 m05–m07 weight 0); the 联防 maps: `0`, `false`, `[]` (never a match stage) | -| `kind`, `helpers` | `"unite"`, `1` | only on the two 联防 maps: the helper count whose 联防 field it is (= `config.unite.templates`; the build fails when they disagree) | +| `id`, `name` | `"act2autochess_m01"`, `"战场#05(下半) 源石流发生装置"` | player-facing name from research 05 (the official tables carry none; falls back to id). The build drops bracketed segments containing Latin letters (research notes such as `战场#01 (upper half #01)` → `战场#01`), logs a warning, and fails validation if Latin text remains. The escaped levels' maps: `联防阵地(1名玩家)` / `联防阵地(2名玩家)`, the remake's own label [ASSUMED] (no table or PRTS page names them; no screen shows it) | +| `weight`, `active`, `modes` | `50`, `true`, `["mode_single_normal",…]` | match-start pick weight (act1 m05–m07 weight 0); the escaped levels' maps: `0`, `false`, `[]` (never a match stage) | +| `kind`, `helpers` | `"unite"`, `1` | only on the escaped levels' two maps: the helper count of their wave template (= `config.unite.templates`; the build fails when they disagree); no field is fought on them | | `size` | `[19,21]` | | | `rows[]` | `rows[9] = "##Errr#rrrSrrr#rrrS##"` | 19 strings, **index = row (0 = bottom)**, one glyph per col | | `tiles[glyph]` | `{"tileKey":"tile_road","height":"LOW","buildable":"ALL","passable":"ALL","groundPassable":true,"flyPassable":true,"special":null,"bb":{}}` | actual tile properties of each glyph used. `buildable` is the **effective** deploy type: the level's buildableType, except a tile whose mechanism refuses deployment — 深水区 `tile_deepsea` (PRTS 深水区 地形信息 "部署类型 全部位 … 地形机制 拒绝部署(待补充)"; player report #3 after 0.1.0) — which is `NONE` and keeps the level's value in `buildableType` (`server/sim/grid.js DEPLOY_REFUSED_TILES`, shared with the builder) | diff --git a/docs/I18N.md b/docs/I18N.md index 187d4815..ef9acb5a 100644 --- a/docs/I18N.md +++ b/docs/I18N.md @@ -157,7 +157,7 @@ rebuilt. same as in Chinese, so `ui/richText.js` renders the English unchanged (a few official EN texts leave a tag unclosed; the parser runs it to the end of the text, as the game does). - **Fallbacks**, in order: `tools/i18n/fallback-remake.json` (the remake's own strings: stage-name parts, item notes, - the 联防 map names, the season name), `tools/i18n/fallback-pr70.json` (PR #70's translations of texts the EN client + the escaped levels' map names, the season name), `tools/i18n/fallback-pr70.json` (PR #70's translations of texts the EN client lacks: modules of CN-only operators, the 甄选 slots, 结算称号 criteria, emote labels), then `public/i18n/en.json`; anything else stays Chinese. Research notes (`bonds.*.spec`, `implFormula`, `assumed` …) are never translated. - **Format** (`shared/i18nData.js`): `{ version, lang, meta: { source, coverage, totals }, names, files }`. `files[file][id]` diff --git a/docs/META.md b/docs/META.md index b5cd6288..4d6573e6 100644 --- a/docs/META.md +++ b/docs/META.md @@ -689,10 +689,10 @@ helpers = `unite.js helperOrder` (research 08 §5, PRTS 卫戍协议/帮助 § chosen by most units on the field (downed included) > an active bond > most standing units > seat; the pair ordered by units > active bond > Σ active layers > standing > seat (LP plays no part), the first one on the right-hand field (colOffset +8, where escaped_multi enters), the other colOffset 0; the escaped template of that size routes the leaked -enemies by slot class, and its own map is the field (`unite.js uniteStageId`: stages.json `act1autochess_escaped_single` / -`_multi`, two road halves joined at col 10 — no water, crates or devices of the round's stage, no band map characters: -the level has no predefined ones; the round's stage only with data that lacks them; GitHub #41), each helper's pieces -on their prep tiles; helpers' operators carry +enemies by slot class on the round's battlefield — the match stage opened to both halves (`GEO.UNITE_RECT`, cols 0–20), +its terrain, crates, water, devices, runes and band map characters included, as in 0.1.x (the owner's decision of +2026-10-07, 「官服保留地形」; 0.2.0 fought it on the escaped levels' own map, the placeholder road every wave template +carries — GitHub #41, withdrawn), each helper's pieces on their prep tiles; helpers' operators carry `{ hpPct, sp }` from `unitsEnd` ("阵地以其当前状态": the HP ratio and the 技力 only — a skill running at the end enters switched off; summon pieces `{ sp }`, "召唤物仅修改技力"); an operator knocked out at the end of the helper's own combat carries `{ down: true }` (PRTS 卫戍协议/帮助: "部署完成后…上一阶段为退场状态的干员强制退场"): deployed, then forced out diff --git a/docs/PLAYING.md b/docs/PLAYING.md index 90eb333e..7d602d6a 100644 --- a/docs/PLAYING.md +++ b/docs/PLAYING.md @@ -90,7 +90,7 @@ 某回合**有人漏怪、又有人完美作战**(全歼且无漏怪)时触发:最多 2 名完美作战的队友带着自己的阵容(与官方相同,只保留作战结束时的生命比例和技力:作战结束时还开着的技能在联防开场是关着的,技力是开技能后剩下的值,通常为 0;召唤物只保留技力;作战结束时已被击倒的干员在原位倒地(阿戈尔的开场吞噬仍把它当作站着的干员计算标记与基础攻击力、阻挡数,之后它照常退场),按官方的「强制退场」以完整的再部署时间重新倒计时,时间到且部署费用足够后照常再部署——联防较短时可能来不及再部署;机变「征召」这类开战时生效的再部署时间减免同样计入),迎击其他玩家漏掉的全部敌人。盟约层数按本回合自己作战结束时达到的层数(即盟约栏显示的层数)计算,联防中不再叠加层数;这些层数在结算时只计入一次。 - 参加者按官方规则挑选:场上单位最多 > 有已激活的盟约 > 未倒下的单位最多 > 座位靠前(与 LP 无关);两人参加时,排在前面的一位(再比较激活盟约的层数总和)在右半场「率先迎敌」。 -- 联防在官方的**联防专用地图**上进行(与官方相同,不是本回合的战场):一整片道路,没有水池、箱子、装置等地形;干员按休整期的位置站好。一人协防时敌人从中间的入口进入左半场;两人协防时敌人从最右侧进入,先经过右半场,再穿过中间进入左半场。 +- 联防在**本回合的战场**上进行(与官方相同:地形、水池、箱子和装置都保留),左右两个半场都在场上;干员按休整期的位置站好,两人协防时排在前面的一位整体右移 8 格、站在右半场。一人协防时敌人从中间的入口进入左半场;两人协防时敌人从最右侧进入,先经过右半场,再穿过中间进入左半场。被推开的敌人可能越过中间的入口退到右半场,之后会沿路线走回来。(0.2.0 曾把联防改到一整片空道路上,0.2.1 起改回本回合的战场。) - 只有完美作战的队友能参与联防;联防中**盟约不会叠加层数**。 - 联防的结果**只在结算的「作战结束」框里报**(与官方一样,没有「联防成功 / 联防失败」之类的字幕)。框里的数字是服务器结算时**真正扣到你头上**的点数:漏怪的玩家按联防后还剩下的敌人扣,不是自己那场漏了几只。**「全员无伤!」只在联防一只都没放过去时出现**(这一轮谁都没被扣);联防没守住、而你自己没被扣(你是去帮忙的队友,或你漏的敌人都被拦下了),框里只有「作战结束」这个标题——掉血的队友并不是无伤,官方对话框里也没有别的句子。框的颜色:青色=联防全部拦下,橙色=联防没守住但你没掉血,红色=你被扣了。漏了几只仍然看顶栏的橙色「×N」。 diff --git a/docs/design/geometry-time.md b/docs/design/geometry-time.md index b38e710b..82639926 100644 --- a/docs/design/geometry-time.md +++ b/docs/design/geometry-time.md @@ -14,7 +14,7 @@ Part of [DESIGN.md](../DESIGN.md) (the index; section numbers are global). - Tile semantics come from `data/stages.json` legend: height (LOW/HIGH), buildable (ALL/MELEE/RANGED/NONE — the **effective** deploy type: the 深水区 `tile_deepsea` refuses deployment, NONE with the level's `buildableType` ALL kept, PRTS 深水区 地形信息 "拒绝部署(待补充)"; an active 特制水上平台 makes its tile deployable in the prep deploy map only, §21.3), ground-passable, fly-only, special terrain (mire, smog, deepsea, infection). The fenced tiles (围墙 `tile_fence_bound` / 围栏 `tile_fence`, legend `b`: LOW, buildable ALL, passable to flyers only) take units, which attack normally from there but block no ground enemy (PRTS 地形机制, §21.23). Melee chess may stand only on LOW tiles with buildable ALL/MELEE — except a melee chess whose trait reads 「可以放置于远程位」 (the 钩索师 / 推击手 branch trait: 歌蕾蒂娅, 崖心, 见行者, normal and elite, with any module or none), which may use the ranged tiles too (`shared/highGround.js`; the owner's decision of 2026-10-05, following PRTS — it reverses the 2026-10-04 one that allowed only elite 歌蕾蒂娅 with HOK-Y; on a 高台 it attacks and blocks nothing); ranged chess on ALL/RANGED **and** on LOW ALL/MELEE tiles ("所有行动内远程干员可部署在近战位"). Tokens follow their own `position`; a summon marked `ownerRange` (狼群, 流形: "只能部署在召唤者攻击范围内"; Mon3tr's 重构体 by her talent) goes only on a tile of its owner's attack range — the loadout grid rotated by the owner's facing around its tile (`PlayerState.summonRange`, §21.3) —, one marked `ownerRangeOutside` / `rangedTilesOnly` (凯尔希·思衡托's 战术锚点: "仅可以部署在凯尔希·思衡托攻击范围外的远程位") only on a 高台 outside it (`summonExcluded`, placement class `high`; 0.2.0). - **Facing (corrected, see research 09 §1.2):** like standard Arknights, every board piece has a direction `dir ∈ UP|RIGHT|DOWN|LEFT` chosen with the 4-direction deploy wheel after dropping it on a tile (also re-orientable in place); default `RIGHT`; persists across rounds. Range grid entries are `[dRow, dCol]` relative to facing RIGHT and are rotated: RIGHT `(dr,dc)`, UP `(dc,−dr)`, LEFT `(−dr,−dc)`, DOWN `(−dc,dr)` (row 0 = bottom); `rangeExtend` applies along +dCol before rotating — to a running skill's range too, unless that skill ignores 攻击距离 (`targeting.noRangeExtend`, 信仰搅拌机 S3, §21.16). The right-side Final Assault player is mirrored col `c→20−c` with RIGHT↔LEFT (UP/DOWN unchanged). - **Range test:** an enemy is inside a grid range if the tile containing it (`round(y), round(x)`) is one of the range tiles; a huge enemy (data `hitArea`: 巨型单位, a 4.95 × 2.95 rectangle moved 1.0 up) if any tile its hit rectangle overlaps is one (PRTS 作战机制 "巨型BOSS单位的每一个占据的格子都可以让其本身通过格子判定"; `sim/body.js`, §19.4). Radius ranges (enemies, auras, AoE) use Euclidean distance in tiles — to a huge enemy's rectangle [ASSUMED] — except splash around a struck target, a 中点判定 on positions (PRTS 作战机制 "中点判定…案例:阻挡,酒神1天赋的1.3溅射半径"). A ranged enemy's normal attack takes an ally whose collider (radius 0.25, PRTS 作战机制 §碰撞体积 "我方干员碰撞体积基本均为以0.25格为半径的圆形") touches its range circle: centre distance ≤ `rangeRadius` + 0.25 (`constants.js ALLY_COLLIDER_RADIUS`, `ai.js enemyAttack`; §20.4). -- **Fields at runtime:** a `Battle` receives a **field rect** (rows/cols subset) and the full stage grid. Normal battles use rows 9–12 × cols 0–10; unite uses rows 9–12 × cols 0–20 — on its own map, the escaped template's (stages.json `kind: 'unite'`, `unite.js uniteStageId`; GitHub #41), not the round's stage; boss uses rows 0–5 × cols 0–20 (solo boss uses the left half only). Pathfinding never leaves the rect. +- **Fields at runtime:** a `Battle` receives a **field rect** (rows/cols subset) and the full stage grid. Normal battles use rows 9–12 × cols 0–10; unite uses rows 9–12 × cols 0–20 of the round's stage, both halves with their terrain and devices (0.2.0's escaped-level map, GitHub #41, withdrawn in 0.2.1: the owner's decision of 2026-10-07); boss uses rows 0–5 × cols 0–20 (solo boss uses the left half only). Pathfinding never leaves the rect. - **Speed:** `tilesPerSecond = moveSpeed × MOVE_SCALE` with `MOVE_SCALE = 0.5` (in `sim/constants.js`, tunable). --- diff --git a/docs/history/0.2.0.md b/docs/history/0.2.0.md index 35ca03b0..593a75f6 100644 --- a/docs/history/0.2.0.md +++ b/docs/history/0.2.0.md @@ -419,6 +419,8 @@ machine-translated with the official terms, marked as such (§25.23). false`) carry the 联防 battle in both combat modes; the client draws that map and restores the board after. No water, crates, devices or terrain in 联防; the BGM (PR #110) is unchanged. [ASSUMED]: the records' names 联防阵地(1名玩家) / (2名玩家). Golden: unite-1, unite-2 and the co-op matches. Tests: `test/match/feedback5-unite-map.test.js`. +- **Withdrawn in 0.2.1**, see §26 — 联防 plays on the round's battlefield again (the owner's decision of 2026-10-07, + 「官服保留地形」: the escaped levels' map is the placeholder road every wave template level carries). #### 25.6.5 伺夜's 狼群 has the official 战术点形态; S1 acts by the pack's state (PRTS 备注; the owner's decision of 2026-10-05) — `content/tokens.js wolfPack / installWolfTacticalPoint / wolfReturnNow`, `kits/ops/chess_char_3_19-vigil.js` diff --git a/public/dev/game-mock.js b/public/dev/game-mock.js index dce22adf..0c4f218b 100644 --- a/public/dev/game-mock.js +++ b/public/dev/game-mock.js @@ -472,8 +472,8 @@ function startCombat(phase) { const e = data.lookup('enemies', bossRec.enemyKey); enemies.push({ id: id++, kind: 'enemy', side: 'enemy', ownerId: ME, defId: bossRec.enemyKey, name: bossRec.name, spine: bossRec.enemyKey, avatar: bossRec.enemyKey, x: 10, y: 3, facing: -1, maxHp: e?.stats?.maxHp || 1e5, hp: (e?.stats?.maxHp || 1e5) * 0.62, spawnAt: 0, dir: 0, boss: true, dead: false }); } - // the 联防 field fights on its own map (server unite.js uniteStageId: 2 helpers → escaped_multi; GitHub #41) - const field = { fieldId, kind, rect, stageId: kind === 'unite' ? 'act1autochess_escaped_multi' : pub.stageId, units: units.map((u) => ({ ...u })) }; + // every field — 联防 too — is fought on the round's battlefield (server unite.js; 0.2.0's escaped-level map withdrawn) + const field = { fieldId, kind, rect, stageId: pub.stageId, units: units.map((u) => ({ ...u })) }; store.patch('match', { field }); const allyState = units.map((u) => ({ ...u, hp: u.maxHp * (0.55 + rnd() * 0.45), sp: rnd() * 20, spMax: 20 })); const t0 = performance.now(); diff --git a/public/js/screens/game.js b/public/js/screens/game.js index 603cfd57..e7c589d3 100644 --- a/public/js/screens/game.js +++ b/public/js/screens/game.js @@ -356,20 +356,19 @@ function MatchScreen() { const shownMembers = shownField ? ((Array.isArray(pub?.fields) ? pub.fields : []).find((f) => f && f.fieldId === shownField.fieldId)?.players || (Array.isArray(shownField.players) ? shownField.players : null)) : null; const ownView = showPrep || (shownField ? (shownMembers ? shownMembers.includes(myId) : shownField.fieldId === ownFieldId(myId)) : !watchingOther); - // a field on a map of its own (联防: the escaped template's map, GitHub #41) shows that map; leaving it restores the board's - const fieldStage = gd.ready && shownField && shownField.stageId && shownField.stageId !== pub?.stageId ? gd.stage(shownField.stageId) : null; + // every field is fought on the round's battlefield — 联防 too, its terrain, crates and water included (0.2.1, the + // owner's decision of 2026-10-07; 0.2.0 drew the escaped levels' empty road there and restored the board on leaving) useEffect(() => { if (!view) return; - const st = fieldStage || (ownView ? ownStage : baseStage); + const st = ownView ? ownStage : baseStage; if (st) view.setStage(st); - }, [view, ownView, ownStage, baseStage, fieldStage]); - // the stage behind the board ON SCREEN — the one view.setStage was just given: a field's own map (联防, GitHub #41), - // else the own one (机变 overrides applied) or, while watching a teammate, the plain one — and how a tapped BOARD tile - // maps to it (GitHub issue #184: tileClick → gameLogic.terrainInfo). Everywhere but a boss-prep board the two spaces - // are the same: a 最终攻势 / 隐秘核心 battle renders the stage's own rows (GEO.BOSS_RECT), 联防 / normal rects are - // stage rows; the boss PREP draws the player's half (stage rows 2–5) as board rows 9–12 (render/prepfield.js toDisp), - // which is exactly gameLogic.fieldTile. - live.current.terrainStage = fieldStage || (ownView ? ownStage : baseStage); + }, [view, ownView, ownStage, baseStage]); + // the stage behind the board ON SCREEN — the own one (机变 overrides applied) or, while watching a teammate, the plain + // one — and how a tapped BOARD tile maps to it (GitHub issue #184: tileClick → gameLogic.terrainInfo). Everywhere but a + // boss-prep board the two spaces are the same: a 最终攻势 / 隐秘核心 battle renders the stage's own rows (GEO.BOSS_RECT), + // 联防 / normal rects are stage rows; the boss PREP draws the player's half (stage rows 2–5) as board rows 9–12 + // (render/prepfield.js toDisp), which is exactly gameLogic.fieldTile. + live.current.terrainStage = ownView ? ownStage : baseStage; live.current.terrainTile = showPrep && (deployField === 'bossL' || deployField === 'bossR') ? (row, col) => fieldTile(deployField, row, col) : (row, col) => [row, col]; diff --git a/server/match/match/unitePhase.js b/server/match/match/unitePhase.js index 4f1093af..31bbedac 100644 --- a/server/match/match/unitePhase.js +++ b/server/match/match/unitePhase.js @@ -5,7 +5,7 @@ import { PHASE, GEO } from '../../../shared/constants.js'; import { deriveSeed } from '../../sim/rng.js'; -import { uniteBattleOpts, uniteSurvivors, uniteStageId } from '../unite.js'; +import { uniteBattleOpts, uniteSurvivors } from '../unite.js'; import { FieldRunner, timelineAt, uniteBillBounds } from '../fields.js'; import { uniteLeft } from '../../sim/spec.js'; import { FLOW_TICKER_PRIORITY, DELAYS } from './common.js'; @@ -40,9 +40,10 @@ export class MatchUnite { } /** - * Battle options of the 联防 field (helpers' carried end state, the leakers' enemies) on its own map — the escaped - * template's (unite.js uniteStageId; GitHub #41), the round's stage only when the data lacks it. The field meta and the - * client-run spec carry that stageId, so every viewer draws the 联防 map. + * Battle options of the 联防 field (helpers' carried end state, the leakers' enemies) on the round's battlefield, its + * terrain, crates, water, devices and runes included (unite.js header; the owner's decision of 2026-10-07 — 0.2.0's + * escaped-level map is withdrawn). The field meta and the client-run spec carry the match stageId, so every viewer + * draws the battlefield the boards stand on. */ _uniteOpts(plan, limit) { const { wave, players } = uniteBattleOpts(this, plan, limit); @@ -51,7 +52,7 @@ export class MatchUnite { kind: 'unite', modeId: this.modeId, round: this.round, - stageId: uniteStageId(this.gd, plan.helpers.length) ?? this.stageId, + stageId: this.stageId, rect: { ...GEO.UNITE_RECT }, timeLimit: limit, players, diff --git a/server/match/unite.js b/server/match/unite.js index 66c7d96d..270689d2 100644 --- a/server/match/unite.js +++ b/server/match/unite.js @@ -5,11 +5,13 @@ // players chosen by most units on the field (downed included) > has an active bond (存疑) > most undowned units, then // seat; with 2 helpers the one ranked first by most units > active bond > Σ active bond layers (存疑) > most undowned // units "率先迎敌" on the RIGHT-hand field (colOffset +8, where the escaped_multi routes enter), the other keeps the -// left half (colOffset 0); a lone helper plays escaped_single on its own field. The field is the template's own map -// (uniteStageId: data/stages.json act1autochess_escaped_single / _multi, kind 'unite' — two road halves joined at col 10, -// the right half's gates at col 18; GitHub #41: until 0.1.3 it was the round's stage, its water, crates and devices -// included), every helper's pieces on their prep tiles ("按休整期位置部署在场"), the right-hand one shifted 8 columns -// (the official maps' halves, = the stage config's player_map_lr_offset 8). Their operators keep the HP ratio and +// left half (colOffset 0); a lone helper plays escaped_single on its own field. The field is the round's battlefield — +// the match stage with its terrain, crates, water, devices and runes — opened to both halves (GEO.UNITE_RECT, cols +// 0–20; the stage's right half is its left half + 8 columns), every helper's pieces on their prep tiles +// ("按休整期位置部署在场"), the right-hand one shifted 8 columns (= the stage config's player_map_lr_offset 8): the +// owner's knowledge of the official mode, 2026-10-07 「官服保留地形」. 0.2.0 fielded it on the escaped levels' own map, +// an empty road (GitHub #41) — withdrawn in 0.2.1; data/stages.json keeps those two records (kind 'unite'), which no +// match fields. Their operators keep the HP ratio and // the SP (技力, stored charges included) from the end of their own combat, nothing else — a skill still running then // enters switched off (BattleResult.unitsEnd → PlayerBattleInput.units[].carryState `{ hpPct, sp }`, "阵地以其当前状态"; // community report #34 / GitHub #82: it used to restart for free). An operator knocked out at the end of its own combat (alive false) is fielded with @@ -120,22 +122,6 @@ export function helperOrder(m, perfects, results) { return select.sort((a, b) => S(b).units - S(a).units || (S(b).active - S(a).active) || S(b).layers - S(a).layers || S(b).standing - S(a).standing || a.seat - b.seat); } -/** - * The map of the 联防 field (GitHub #41): act2autochess constData escapedBattleTemplateMapSinglePlayer (1 helper) / - * MultiPlayer (2 helpers) name one level for both the 联防 wave and its map — config.unite.templates[n], built into - * data/stages.json as the stage of the same id (kind 'unite'). null when the data has no such stage (degraded data: - * the caller keeps the round's stage). - * @param {import('./gamedata.js').GameData} gd - * @param {number} helperCount - * @returns {string|null} - */ -export function uniteStageId(gd, helperCount) { - const templates = gd.unite.templates || {}; - const id = templates[String(helperCount)] ?? templates[helperCount] ?? null; - const st = id ? gd.stage(id) : null; - return st && st.kind === 'unite' ? id : null; -} - /** Battle options for the unite field (without data/logger, added by the match). */ export function uniteBattleOpts(m, plan, timeLimit) { const wave = buildUniteWave(m.gd, plan.leaked, plan.helpers.length, timeLimit); diff --git a/server/sim/battle/economy.js b/server/sim/battle/economy.js index f2b96078..ca913877 100644 --- a/server/sim/battle/economy.js +++ b/server/sim/battle/economy.js @@ -57,7 +57,8 @@ export class BattleEconomy { * whose half of the field it fell on: PRTS 卫戍协议:盟约 决策 "该敌人于对应玩家所属区域倒下时,使相应玩家获得额外资金" * [ASSUMED for 联防: no source names the payee of a death nobody caused there]. Until 0.2.0 that fallback was the * leaker, who has no entry in a 联防 battle, and addCoins dropped the coins (community report of 2026-10-06 - * 「被源石地板烫死的悬赏没给赏金」: until 0.1.3 the 联防 field was the round's map, 战场#04's 活性源石 included). + * 「被源石地板烫死的悬赏没给赏金」: in 0.1.x the 联防 field was the round's map, as it is again since 0.2.1, 战场#04's + * 活性源石 included). */ _bountyPayee(unit, killer = null) { if (killer && killer.side === 'ally' && killer.ownerId != null && this._pp(killer.ownerId)) return killer.ownerId; diff --git a/server/sim/content/kits/ops/chess_char_5_05-ulpia.js b/server/sim/content/kits/ops/chess_char_5_05-ulpia.js index cf7ea860..edd67120 100644 --- a/server/sim/content/kits/ops/chess_char_5_05-ulpia.js +++ b/server/sim/content/kits/ops/chess_char_5_05-ulpia.js @@ -96,10 +96,9 @@ export default { // PRTS 备注: landing tile > the tile one beyond it > his own tile (a deployable, free, unreserved melee tile). // "可部署" is a tile his player may deploy on: his own board (Battle.onOwnBoard) — never the other half of a 联防 or // boss field nor a boss field's hand / 临时整备区 rows; a teammate's half stays closed to him even where the 突袭 - // landing may take it (Battle.onFieldBoard, DESIGN §26.1). Community report of 2026-10-06 (item 27) - // 「乌尔比安使用3技能会在联防阶段跳到红门后」: on the one-helper 联防 map (escaped_single, enemies from the middle gate at - // col 10) an anchor that met no enemy flew on to the right half, where no enemy ever walks, and he moved there for - // the whole skill + // landing may take it (Battle.onFieldBoard, DESIGN §26.1). Community report of 2026-10-06 (item 27) 「乌尔比安使用3技能会在 + // 联防阶段跳到红门后」: in a one-helper 联防 (escaped_single's enemies come out of the middle gate at col 10) an anchor + // that met no enemy flew on to the right half, where no enemy ever walks, and he moved there for the whole skill const ok = ([r, c]) => (r !== unit.tileR || c !== unit.tileC) && battle.grid.inRect(r, c) && battle.onOwnBoard(unit.player, r, c) && battle.grid.canStand(r, c) && !battle.grid.isObstacle(r, c) && !battle.isReservedTile(r, c); const dest = [[sr, sc], frontOf(unit.tileR, unit.tileC, unit.dir, stop + 1)].find(ok); if (dest == null) return; diff --git a/test/data.test.js b/test/data.test.js index c4f27b5c..263f7cf7 100644 --- a/test/data.test.js +++ b/test/data.test.js @@ -664,7 +664,7 @@ test('stages: player-facing names are clean Chinese "战场#NN…" labels (no re // Regression: research 05 named act1 m01 '战场#01 (upper half #01)'; the English note reached the // briefing BATTLEFIELD row. Names come from research only, so the build must strip such notes. for (const s of Object.values(stages)) { - if (s.kind === 'unite') continue; // the two 联防 maps: the remake's own label, checked in the 联防 test below + if (s.kind === 'unite') continue; // the escaped levels' two maps: the remake's own label, checked in the test below assert.match(s.name, /^战场#\d{2}(?:\((?:上半|下半)\))?(?: \S.*)?$/, `${s.id}: name ${JSON.stringify(s.name)}`); assert.doesNotMatch(s.name, /[A-Za-z]/, `${s.id}: Latin text in name ${JSON.stringify(s.name)}`); assert.equal(s.name, s.name.trim().replace(/\s+/g, ' '), `${s.id}: stray whitespace in name`); @@ -673,8 +673,10 @@ test('stages: player-facing names are clean Chinese "战场#NN…" labels (no re assert.equal(stages.act2autochess_m01.name, '战场#05(下半) 源石流发生装置'); }); -test('stages: the 联防 maps (GitHub #41) — the escaped template levels, one per helper count, never a match stage', () => { - // act2autochess constData escapedBattleTemplateMapSinglePlayer / MultiPlayer → config.unite.templates { 1, 2 } +test('stages: the escaped levels\' maps — one per helper count, never a match stage (and since 0.2.1 never the 联防 field)', () => { + // act2autochess constData escapedBattleTemplateMapSinglePlayer / MultiPlayer → config.unite.templates { 1, 2 }. 0.2.0 + // fought the 联防 battle on these maps (GitHub #41); 联防 plays on the round's battlefield again (the owner's decision of + // 2026-10-07, test/match/feedback5-unite-map.test.js) and the records stay as the official level data assert.deepEqual(config.unite.templates, { 1: 'act1autochess_escaped_single', 2: 'act1autochess_escaped_multi' }); for (const [n, id] of Object.entries(config.unite.templates)) { const s = stages[id]; @@ -688,13 +690,14 @@ test('stages: the 联防 maps (GitHub #41) — the escaped template levels, one assert.equal(s.name, `联防阵地(${n}名玩家)`); assert.deepEqual(s.devices, [], 'no crates, platforms or other devices'); assert.deepEqual(s.special, {}, 'no water, mire, smog or infection'); - // the official level map (level_act1autochess_escaped_*.json): two road halves joined at col 10, not the round's stage + // the official level map (level_act1autochess_escaped_*.json): two road halves joined at col 10 assert.deepEqual(s.rows.slice(9, 13), ['##ErrrrrrrSrrrrrrrS##', '###rrrrrrr#rrrrrrr###', '###rrrrrrr#rrrrrrr###', '###rrrrrrrSrrrrrrrS##']); for (let r = 9; r <= 12; r++) for (const c of [19, 20]) assert.equal(s.tiles[s.rows[r][c]].groundPassable, false, `${id} (${r},${c}) is no ground`); assert.ok(s.groundPaths['9,10->9,2'] && s.groundPaths['9,18->9,2'], 'walkable from both gates'); } // the two maps are tile for tile the same: they differ in their routes (data/waves.json — 1 helper enters at col 10, - // 2 helpers at col 18 through the checkpoint (9,10)) + // 2 helpers at col 18 through the checkpoint (9,10)), which the 联防 field follows on the round's stage (its gates + // (9,10) / (12,10) and (9,18) / (12,18)) assert.deepEqual(stages.act1autochess_escaped_single.rows, stages.act1autochess_escaped_multi.rows); assert.deepEqual(waves.act1autochess_escaped_single.routes.map((r) => r.start), [[9, 10], [9, 10], [12, 10], [9, 10], [9, 10], [9, 10], [9, 10], [9, 10]]); assert.deepEqual(waves.act1autochess_escaped_multi.routes.map((r) => r.start), [[9, 18], [9, 18], [12, 18], [9, 18], [9, 18], [9, 18], [9, 18], [9, 18]]); diff --git a/test/e2e/coop.e2e.mjs b/test/e2e/coop.e2e.mjs index bfd67e3c..e2eb487d 100644 --- a/test/e2e/coop.e2e.mjs +++ b/test/e2e/coop.e2e.mjs @@ -1198,6 +1198,16 @@ describe('browser E2E against the real server', { skip: !ENABLED && 'needs Chrom } else if (hs.phase === 'UNITE') { const rs = await runnerState(host); assert.ok(rs && rs.kind === 'unite', 'every human simulates the 联防 field locally'); + // 0.2.1 (the owner's decision of 2026-10-07): the 联防 field is the round's battlefield, terrain and crates + // included — the field meta names the match stage and the view draws it (0.2.0 drew the escaped levels' road) + for (const c of both) { + const drawn = await c.page.evaluate(() => { + const s = globalThis.__SP__.store.get(); + return { match: s.match.public?.stageId ?? null, field: s.match.field?.stageId ?? null, shown: globalThis.__SP_VIEW__?.raw?.debug?.tiles?.stage?.id ?? null }; + }); + assert.ok(drawn.match && drawn.field === drawn.match && drawn.shown === drawn.match, `${c.label}: the 联防 field shows the round's battlefield (${JSON.stringify(drawn)})`); + c.note(`联防 round ${hs.round} drawn on ${drawn.shown}`); + } } if (hs.round >= ROUNDS && hs.phase === 'COMBAT') { await sleep(2000); break; } } else if (hs.phase === 'SETTLE' && !seen.has(`settle-${hs.round}`)) { diff --git a/test/golden/README.md b/test/golden/README.md index 2e9cef61..c7b0028e 100644 --- a/test/golden/README.md +++ b/test/golden/README.md @@ -16,7 +16,7 @@ through. |---|---| | `roster` | 49 battles: every visible chess record (normal and elite) with every selectable skill and module (DESIGN §16), 12 operators per battle on a real stage (all 11 in turn) against the round's real wave three times over, every non-leader enemy kind of `data/enemies.json` as extra spawns (half of them bounties), placeable summons on the board, every equipment item, band, battle-side 机变 card and stage map card in turn, bonds from the board | | `bonds` | 46 battles: every bond at its activation threshold (1 layer) and at its top tier (999 layers) | -| `fields` | 22 battles: every Final Assault / Hidden Core leader on a pair and a solo template (shared pool, 200 s cap) and the 联防 field with 1 and 2 helpers on its own map (carried HP / SP, a knocked-out operator, two leakers' enemies) | +| `fields` | 22 battles: every Final Assault / Hidden Core leader on a pair and a solo template (shared pool, 200 s cap) and the 联防 field with 1 and 2 helpers on the round's stage, both halves (carried HP / SP, a knocked-out operator, two leakers' enemies) | | `matches` | 18 matches to the end in virtual time: 16 bot-only (solo 标准 / 险境 / 绝境 / 终极 × 2 seeds, co-op 2 / 3 / 4, one server-run combat match, two runs boosted to the Hidden Core), one co-op match whose human seat (AI 托管, offline) does not own 9 NORMAL chess — they fight as their 补位 stand-ins (its digest lists them per round, `standIns`) — and one whose human seat slots 自选 picks (推进之王 and prototypes) with its 调度中心 at level 5 from the first prep: its own shop draws them and its AI fields 推进之王 (its digest lists per round the 自选 shop draws and the fielded pieces, `diy`) | | `standins` | 10 battles: every NORMAL chess record (normal and elite, 110) fielded as its 补位 stand-in (`standIn: true`: the stand-in's body, backup skill / module and kit — all 17 stand-ins, every skill a chess names for them), 12 per battle by strength band, laid out by the stand-in's position on a real stage, against the round's real wave three times over plus 8 ground enemy kinds (melee stand-ins always meet an enemy), an item each, bonds from the board | | `diy` | battles of 自选 pieces (a DIY slot with its `diy` pick, `shared/diy.js`): every owned 6★ with an operator kit (`kits/index.js OPERATOR_KITS`) in each form of tiers 5 and 6 (normal; elite with no module and with each module) under each skill, then every prototype pick with a kit at its locked selection, normal and elite, 12 per battle against the round's real wave; a new operator kit adds its scenarios (the battle count grows with the kits) | diff --git a/test/golden/fields.json b/test/golden/fields.json index be5ee8d0..9da91a83 100644 --- a/test/golden/fields.json +++ b/test/golden/fields.json @@ -1370,7 +1370,7 @@ "about": "联防 1 helper(s), 11 leaked: chess_char_6_03_b chess_char_6_06_b chess_char_6_07_b chess_char_6_01_b chess_char_6_02_b chess_char_6_04_b chess_char_6_05_b chess_char_6_08_b token_10017_skadi2_dedant", "fast": true, "kind": "unite", - "stageId": "act1autochess_escaped_single", + "stageId": "act1autochess_m02", "modeId": "mode_multi_normal", "round": 7, "seed": 2621626757, @@ -1382,33 +1382,34 @@ "total": 10, "unspawned": 0, "bossHpLeft": null, - "rngDraws": 19, + "rngDraws": 24, "errors": {"count":0,"kinds":[]}, - "players": {"p1":{"killed":10,"total":10,"perfect":true,"leaks":0,"leaksUncounted":1,"coins":3,"dmg":70502,"bossDmg":0,"heal":4439,"deaths":2,"layerGains":{}}}, + "players": {"p1":{"killed":10,"total":10,"perfect":true,"leaks":0,"leaksUncounted":1,"coins":3,"dmg":70502,"bossDmg":0,"heal":7551,"deaths":0,"layerGains":{}}}, "units": [ - ["p1",1,"op","chess_char_6_03_b",2,"uniequip_002_yu",3430,1,2,1,912,0,0,1,0], + ["p1",1,"op","chess_char_6_03_b",2,"uniequip_002_yu",3430,1,2,0,0,0,0,0,0], ["p1",2,"op","chess_char_6_06_b",2,"uniequip_002_pepe",1616.45,1,1,0,0,0,0,0,0], - ["p1",3,"op","chess_char_6_07_b",2,"uniequip_002_siege2",3101,1,22,0,0,0,0,0,0], - ["p1",4,"op","chess_char_6_01_b",2,"uniequip_002_lemuen",2352,1,4,3,16632,0,1646,6,1], - ["p1",5,"op","chess_char_6_02_b",2,"uniequip_002_sbell2",1933,1,7,2,24053,126,512,3,1], - ["p1",6,"op","chess_char_6_04_b",2,"uniequip_002_skadi2",333.86,1,23,0,2871,4463,1627,0,3], - ["p1",7,"op","chess_char_6_05_b",2,"uniequip_002_pasngr",1442,1,25,4,26035,1446,653,0,1], - ["p1",8,"op","chess_char_6_08_b",2,"uniequip_002_reed2",1668,1,6,0,0,0,0,0,0], + ["p1",3,"op","chess_char_6_07_b",2,"uniequip_002_siege2",3101,1,23,0,0,0,0,0,0], + ["p1",4,"op","chess_char_6_01_b",2,"uniequip_002_lemuen",2352,1,4,3,17564,0,1646,4,1], + ["p1",5,"op","chess_char_6_02_b",2,"uniequip_002_sbell2",1933,1,7,1,7345,0,387,1,1], + ["p1",6,"op","chess_char_6_04_b",2,"uniequip_002_skadi2",75.38,1,5,1,632,6392,4869,0,4], + ["p1",7,"op","chess_char_6_05_b",2,"uniequip_002_pasngr",1442,1,8,4,29637,0,649,0,1], + ["p1",8,"op","chess_char_6_08_b",2,"uniequip_002_reed2",1668,1,6,1,15325,0,0,8,1], ["p1",9,"token","token_10017_skadi2_dedant",null,null,100,0,9,0,0,0,0,0,null], - ["p1","~1","token","token_10017_skadi2_dedant",null,null,100,0,24,0,0,0,0,0,null], - ["p1","~2","token","token_10017_skadi2_dedant",null,null,100,1,26,0,0,0,0,0,null] + ["p1","~1","token","token_10017_skadi2_dedant",null,null,100,0,22,0,0,0,0,0,null], + ["p1","~2","token","token_10017_skadi2_dedant",null,null,100,0,24,0,0,0,0,0,null], + ["p1","~3","token","token_10017_skadi2_dedant",null,null,100,1,25,0,0,0,0,0,null] ], - "enemies": {"enemy_10028_vtswd":[1,0,0,0,0],"enemy_1158_divman":[4,4,0,322,15206],"enemy_1160_hvyslr":[3,3,0,0,34560],"enemy_1161_tidmag":[3,3,0,1639,20736]}, - "hooks": [1,25,5100,20,20,57,129,2358,12,15,6,5,7,9981,13,1,11,0,1,12,0,2,0], - "events": {"atk":28,"bounty":1,"deploy":14,"die":15,"dmg":78,"engage":3,"fx":57,"skill":11,"spawn":22,"status":43}, - "fx": {"bombard":7,"bombardShell":7,"burst":2,"camouflage":1,"lightning":14,"lock":7,"lure":1,"snow":12,"storm":1,"talent":1,"tide":3,"wanted":1}, - "snaps": "c558a4df 5e12c668 02dad932 00fbc7af bc41cf43 ad41bea1 6e07fbe6 27dde52f 53b9a18c a640dd14 452ad2b3 46658458 8c81ee61 4c70f7fe c0fba143 3b8d81c6 aa0fc004" + "enemies": {"enemy_10028_vtswd":[1,0,0,0,0],"enemy_1158_divman":[4,4,0,0,15206],"enemy_1160_hvyslr":[3,3,0,0,34560],"enemy_1161_tidmag":[3,3,0,0,20736]}, + "hooks": [1,24,5100,13,13,48,140,3883,10,14,8,8,7,11668,21,0,11,0,1,10,0,1,0], + "events": {"atk":15,"bounty":1,"deploy":13,"die":14,"dmg":60,"engage":3,"fx":58,"skill":16,"spawn":23,"status":65}, + "fx": {"bombard":7,"bombardShell":7,"burst":1,"lightning":15,"lock":7,"lure":1,"scorchBurst":4,"snow":10,"storm":1,"talent":1,"tide":4}, + "snaps": "24e18075 c08e2f71 f5edcfeb 0c324c6b fd360730 254c3c71 8375ccc6 24ac7d99 00239a36 da6ce132 05710646 d34c3d36 a02b720e 749b9c2b a2d13804 15645614 d947c90f" }, "unite-2": { "about": "联防 2 helper(s), 11 leaked: chess_char_6_15_b chess_char_6_17_b chess_char_6_09_b chess_char_6_11_b chess_char_6_12_b chess_char_6_13_b chess_char_6_14_b chess_char_6_16_b token_10030_mlyss_wtrman | chess_char_6_19_b chess_char_5_01_b chess_char_5_05_b chess_char_5_06_b chess_char_6_18_b chess_char_6_20_b chess_char_5_02_b chess_char_5_03_b", "fast": true, "kind": "unite", - "stageId": "act1autochess_escaped_multi", + "stageId": "act1autochess_m07", "modeId": "mode_multi_hard", "round": 11, "seed": 1995382690, @@ -1427,30 +1428,34 @@ "p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":10528,"bossDmg":0,"heal":9524,"deaths":0,"layerGains":{}} }, "units": [ - ["p1",1,"op","chess_char_6_15_b",2,"uniequip_002_qiubai",4608,1,32,2,33012,4080,0,21,1], - ["p1",2,"op","chess_char_6_17_b",2,"uniequip_002_nearl2",3593,1,35,0,27083,4928,0,17,1], - ["p1",3,"op","chess_char_6_09_b",0,"uniequip_002_cello",1322,1,33,0,0,0,0,0,0], - ["p1",4,"op","chess_char_6_11_b",2,"uniequip_002_mlyss",1893,1,9,0,0,0,0,0,0], - ["p1",5,"op","chess_char_6_12_b",1,"uniequip_002_rosmon",2944.54,1,12,6,72628,0,0,15,1], - ["p1",6,"op","chess_char_6_13_b",1,"uniequip_002_angel2",2192.85,1,14,0,0,5746,0,0,0], - ["p1",7,"op","chess_char_6_14_b",2,"uniequip_002_lumen",3323.25,1,16,0,0,0,5334,72,1], - ["p1",8,"op","chess_char_6_16_b",2,"uniequip_002_halo2",1439,1,7,0,4962,0,0,5,1], - ["p1",9,"token","token_10030_mlyss_wtrman",null,null,2062,1,10,0,0,0,0,0,1], - ["p2",10,"op","chess_char_6_19_b",2,"uniequip_002_blkkgt",2767,1,2,0,0,0,0,0,0], - ["p2",11,"op","chess_char_5_01_b",1,"uniequip_002_excu2",1272.7,1,6,0,0,0,0,0,0], - ["p2",12,"op","chess_char_5_05_b",2,"uniequip_002_ulpia",5905,1,34,0,0,0,0,0,0], - ["p2",13,"op","chess_char_5_06_b",2,"uniequip_002_etlchi",776.63,1,15,0,0,0,0,0,0], - ["p2",14,"op","chess_char_6_18_b",2,"uniequip_002_whitw2",1541,1,4,1,10528,0,0,0,1], - ["p2",15,"op","chess_char_6_20_b",2,"uniequip_002_agoat2",1631.88,1,8,0,0,0,9524,25,2], - ["p2",16,"op","chess_char_5_02_b",2,"uniequip_002_titi",1554.12,1,13,0,0,0,0,0,0], - ["p2",17,"op","chess_char_5_03_b",2,"uniequip_002_blaze2",1589.76,1,17,0,0,0,0,0,0], - ["p1","~1","token","token_10019_nearl2_sword",null,null,5441.59,0,31,1,27960,558,0,7,null] + ["p1",1,"op","chess_char_6_15_b",2,"uniequip_002_qiubai",4608,1,36,2,33012,4080,0,21,1], + ["p1",2,"op","chess_char_6_17_b",2,"uniequip_002_nearl2",3593,1,39,0,27083,4928,0,17,1], + ["p1",3,"op","chess_char_6_09_b",0,"uniequip_002_cello",1322,1,37,0,0,0,0,0,0], + ["p1",4,"op","chess_char_6_11_b",2,"uniequip_002_mlyss",1893,1,13,0,0,0,0,0,0], + ["p1",5,"op","chess_char_6_12_b",1,"uniequip_002_rosmon",2944.54,1,16,6,72628,0,0,15,1], + ["p1",6,"op","chess_char_6_13_b",1,"uniequip_002_angel2",2192.85,1,18,0,0,5746,0,0,0], + ["p1",7,"op","chess_char_6_14_b",2,"uniequip_002_lumen",3323.25,1,20,0,0,0,5334,72,1], + ["p1",8,"op","chess_char_6_16_b",2,"uniequip_002_halo2",1439,1,11,0,4962,0,0,5,1], + ["p1",9,"token","token_10030_mlyss_wtrman",null,null,2062,1,14,0,0,0,0,0,1], + ["p2",10,"op","chess_char_6_19_b",2,"uniequip_002_blkkgt",2767,1,6,0,0,0,0,0,0], + ["p2",11,"op","chess_char_5_01_b",1,"uniequip_002_excu2",1272.7,1,10,0,0,0,0,0,0], + ["p2",12,"op","chess_char_5_05_b",2,"uniequip_002_ulpia",5905,1,38,0,0,0,0,0,0], + ["p2",13,"op","chess_char_5_06_b",2,"uniequip_002_etlchi",776.63,1,19,0,0,0,0,0,0], + ["p2",14,"op","chess_char_6_18_b",2,"uniequip_002_whitw2",1541,1,8,1,10528,0,0,0,1], + ["p2",15,"op","chess_char_6_20_b",2,"uniequip_002_agoat2",1631.88,1,12,0,0,0,9524,25,2], + ["p2",16,"op","chess_char_5_02_b",2,"uniequip_002_titi",1554.12,1,17,0,0,0,0,0,0], + ["p2",17,"op","chess_char_5_03_b",2,"uniequip_002_blaze2",1589.76,1,21,0,0,0,0,0,0], + ["-","~1","device","trap_1105_accrate",null,null,100,1,1,0,0,0,0,0,null], + ["-","~2","device","trap_1105_accrate",null,null,100,1,2,0,0,0,0,0,null], + ["-","~3","device","trap_1105_accrate",null,null,100,1,3,0,0,0,0,0,null], + ["-","~4","device","trap_1105_accrate",null,null,100,1,4,0,0,0,0,0,null], + ["p1","~1","token","token_10019_nearl2_sword",null,null,5441.59,0,35,1,27960,558,0,7,null] ], "enemies": {"enemy_10029_vtshld":[1,0,0,0,0],"enemy_1249_lysdb_2":[3,3,0,1139,53748],"enemy_1251_lysyta":[4,4,0,0,32846],"enemy_1329_cbshld":[3,3,0,8428,89580]}, - "hooks": [1,34,6900,175,175,310,536,506,11,16,9,7,0,19291,418,5,11,0,1,11,0,0,0], - "events": {"atk":275,"bounty":1,"deploy":23,"die":16,"dmg":307,"engage":5,"fx":166,"heal":197,"skill":16,"spawn":29,"status":87}, + "hooks": [1,38,6900,175,175,310,536,506,11,16,9,7,0,19291,418,5,11,0,1,11,0,0,0], + "events": {"atk":275,"bounty":1,"deploy":23,"die":16,"dmg":307,"engage":5,"fx":166,"heal":197,"skill":16,"spawn":33,"status":87}, "fx": {"blink":2,"copy":1,"drone":141,"droneLock":9,"drones":2,"garrisonGrant":1,"link":1,"radiantSword":1,"shieldBreak":3,"summon":1,"sunBurst":1,"talent":3}, - "snaps": "87a560e8 8cf58762 c766f850 d32294f6 295e925c 44a2d0b0 80ea41cf 5b4cd7c2 993fdc2e 3171686a fab27835 978a7b48 e45c0811 5a5f2ed3 3d500d80 addffc1a f295f658 ae43f351 436ae4d0 214a93f0 1b8bc39d 44a2436b e448417b" + "snaps": "c4705ca6 7ac5e1f4 7b93c781 1a4d420a c1ddfa30 66889e17 58d20bb6 0a8d750d f2fed186 eada2980 a257e8a5 66c38c0d 3ab9a260 3462b6ee 9abc9798 5949be5e 691d70b6 ba404db0 9eb04453 328af32e a81e9442 4c690478 bcda8066" } } } diff --git a/test/golden/matches.json b/test/golden/matches.json index c31bb6bf..cae9f70a 100644 --- a/test/golden/matches.json +++ b/test/golden/matches.json @@ -516,7 +516,7 @@ "combat": "client", "boost": null, "setup": {"stageId":"act2autochess_m04","bossId":"boss_3","hiddenBossId":"boss_8","factions":["FLY","TIMES","DOT"]}, - "end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":46,"bossPoolLeft":0,"virtualMs":118400}, + "end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":56,"bossPoolLeft":0,"virtualMs":118400}, "rng": {"setup":25,"shop":456,"waves":0,"draft":40,"bots":21608,"meta":4}, "errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0}, "rounds": [ @@ -582,7 +582,7 @@ }, { "round": 13, - "ai_0": [1,27,12,0,0,6,8,"f3619a79",269,"arcaneShip 2/43, egirShip 1/124, preciShip 1/17, raidShip 0/7, sargonShip 0/10, siracusaShip 1/44, skillfulShip 0/8, soloShip 1/6, steadShip 1/0, suntShip 1/0, swiftShip 1/35",13,34,21,126], + "ai_0": [1,37,12,0,0,6,8,"f3619a79",269,"arcaneShip 2/43, egirShip 1/124, preciShip 1/17, raidShip 0/7, sargonShip 0/10, siracusaShip 1/44, skillfulShip 0/8, soloShip 1/6, steadShip 1/0, suntShip 1/0, swiftShip 1/35",13,34,21,126], "ai_1": [1,19,12,0,0,6,8,"187f8019",183,"arcaneShip 2/28, indomShip 0/2, kazimierzShip 1/53, kjeragShip 0/2, preciShip 2/102, sargonShip 0/10, steadShip 0/6, suntShip 1/0, swiftShip 0/11",34,34,0,123] } ], @@ -592,12 +592,12 @@ "victory": true, "roundsPassed": 14, "eliminatedRound": null, - "lp": 27, + "lp": 37, "bandId": "band_sarkazb", "title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"}, "trophies": 4, "reward": 400, - "stats": {"activatedLayers":303,"bossDamage":283279,"dmgDealt":2679730,"gold":138,"itemsEquipped":17,"kills":310,"leaks":32,"lpLost":18,"merges":3,"perfectRounds":9,"refreshes":23}, + "stats": {"activatedLayers":303,"bossDamage":283279,"dmgDealt":2679730,"gold":138,"itemsEquipped":17,"kills":310,"leaks":32,"lpLost":8,"merges":3,"perfectRounds":9,"refreshes":23}, "lineup": ["1_17_a@11,7","4_14_a@11,8[3_01_e_a+4_08_e_a]","4_10_b@10,5[6_09_e_a+1_02_e_b]","3_18_a@10,7[4_05_e_a+3_05_e_a]","5_13_a@10,8[4_06_e_a]","3_05_a@9,4","3_09_a@9,8[1_05_e_a+4_07_e_a]","1_08_b@9,9[4_03_e_a+4_04_e_a]"], "bonds": ["arcaneShip 2/55","egirShip 1/124","preciShip 1/22","raidShip 0/7","sargonShip 0/10","siracusaShip 1/56","skillfulShip 0/8","soloShip 1/6","steadShip 1/0","suntShip 1/0","swiftShip 1/40"] }, @@ -611,7 +611,7 @@ "title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"}, "trophies": 4, "reward": 400, - "stats": {"activatedLayers":185,"bossDamage":1134221,"dmgDealt":4214714,"gold":135,"itemsEquipped":19,"kills":324,"leaks":9,"lpLost":9,"merges":4,"perfectRounds":11,"refreshes":14}, + "stats": {"activatedLayers":185,"bossDamage":1134221,"dmgDealt":4858908,"gold":135,"itemsEquipped":19,"kills":344,"leaks":9,"lpLost":9,"merges":4,"perfectRounds":11,"refreshes":14}, "lineup": ["6_08_a@12,8","1_17_b@11,7[3_04_e_a+4_08_e_b]","4_20_a@11,8[5_02_e_a+4_04_e_a]","4_10_a@10,5[1_02_e_b+4_10_e_a]","6_17_a@10,8","6_08_a@9,7[4_07_e_a+3_01_e_a]","4_02_a@9,8","1_19_b@9,9[2_04_e_a+1_05_e_b]"], "bonds": ["arcaneShip 2/30","indomShip 0/2","kazimierzShip 1/53","kjeragShip 0/2","preciShip 2/102","sargonShip 0/10","steadShip 0/6","suntShip 1/0","swiftShip 0/11"] } @@ -708,7 +708,7 @@ "title": {"id":"comment_5","name":"万事俱备","picId":"comment_icon_5","text":"装备配发数量最多"}, "trophies": 2, "reward": 255, - "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1458318,"gold":113,"itemsEquipped":10,"kills":161,"leaks":72,"lpLost":28,"merges":2,"perfectRounds":5,"refreshes":9}, + "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1461472,"gold":113,"itemsEquipped":10,"kills":161,"leaks":72,"lpLost":28,"merges":2,"perfectRounds":5,"refreshes":9}, "lineup": [], "bonds": ["deputShip 0/2","indomShip 0/8","kazimierzShip 0/2","kjeragShip 0/2","raidShip 0/2","siracusaShip 0/32","skillfulShip 0/12","soloShip 0/5","steadShip 0/14","suntShip 0/2","swiftShip 0/12"] }, @@ -722,7 +722,7 @@ "title": {"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"}, "trophies": 3, "reward": 298, - "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":3670137,"gold":136,"itemsEquipped":11,"kills":257,"leaks":40,"lpLost":31,"merges":4,"perfectRounds":8,"refreshes":22}, + "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":3663883,"gold":136,"itemsEquipped":11,"kills":257,"leaks":40,"lpLost":31,"merges":4,"perfectRounds":8,"refreshes":22}, "lineup": [], "bonds": ["arcaneShip 0/40","kazimierzShip 0/2","preciShip 0/84","sargonShip 0/80","skillfulShip 0/14","swiftShip 0/10"] } @@ -762,37 +762,37 @@ }, { "round": 4, - "ai_0": [1,22,8,0,1,2,8,"248ea0fd",12,"deputShip 1/8, kazimierzShip 0/2, swiftShip 1/4",13,14,1,23], + "ai_0": [1,23,8,0,1,2,8,"248ea0fd",12,"deputShip 1/8, kazimierzShip 0/2, swiftShip 1/4",13,14,1,23], "ai_1": [1,43,9,0,2,2,8,"4c004aa4",16,"deputShip 1/2, kazimierzShip 0/7, swiftShip 1/14",7,12,5,24], "ai_2": [1,27,8,0,1,2,8,"e66e483b",6,"indomShip 1/2, siracusaShip 1/0, soloShip 1/4, yanShip 0/1",12,12,0,23] }, { "round": 5, - "ai_0": [1,22,9,0,0,3,8,"61eb7d90",20,"deputShip 1/12, kazimierzShip 0/2, soloShip 1/2, swiftShip 1/6",53,53,0,32], + "ai_0": [1,23,9,0,0,3,8,"61eb7d90",20,"deputShip 1/12, kazimierzShip 0/2, soloShip 1/2, swiftShip 1/6",53,53,0,32], "ai_1": [1,43,10,0,0,3,8,"b4417f25",27,"deputShip 2/6, kazimierzShip 0/7, swiftShip 1/21",53,53,0,34], "ai_2": [1,27,9,0,2,3,8,"a15e1321",10,"indomShip 1/4, siracusaShip 1/0, soloShip 1/6, yanShip 0/1",53,53,0,31] }, { "round": 6, - "ai_0": [1,22,9,0,1,3,8,"4faaf967",44,"deputShip 2/20, kazimierzShip 0/2, lateranoShip 1/7, skillfulShip 1/7, soloShip 0/2, swiftShip 1/10",23,26,3,42], + "ai_0": [1,23,9,0,1,3,8,"4faaf967",44,"deputShip 2/20, kazimierzShip 0/2, lateranoShip 1/7, skillfulShip 1/7, soloShip 0/2, swiftShip 1/10",23,26,3,42], "ai_1": [1,43,9,0,0,3,8,"85c81f35",46,"deputShip 1/8, kazimierzShip 0/7, siracusaShip 1/8, soloShip 1/2, suntShip 1/2, swiftShip 1/26",26,26,0,43], "ai_2": [1,27,11,0,2,3,8,"fa398791",18,"indomShip 1/8, miraShip 1/0, siracusaShip 1/4, soloShip 1/6, suntShip 1/0, yanShip 0/1",12,26,14,43] }, { "round": 7, - "ai_0": [1,22,11,0,0,4,8,"0ae8946b",78,"deputShip 2/28, kazimierzShip 0/4, lateranoShip 1/17, skillfulShip 1/17, soloShip 0/2, swiftShip 1/16",22,25,3,55], + "ai_0": [1,23,11,0,0,4,8,"0ae8946b",78,"deputShip 2/28, kazimierzShip 0/4, lateranoShip 1/17, skillfulShip 1/17, soloShip 0/2, swiftShip 1/16",22,25,3,55], "ai_1": [1,43,10,0,0,4,8,"bf120dea",65,"deputShip 1/10, kazimierzShip 0/7, siracusaShip 1/16, soloShip 1/4, suntShip 1/4, swiftShip 1/31",25,25,0,52], "ai_2": [1,27,12,0,0,4,8,"162f421b",31,"deputShip 1/2, indomShip 0/8, miraShip 1/5, siracusaShip 1/12, soloShip 1/10, suntShip 1/2, yanShip 0/1",22,25,3,55] }, { "round": 8, - "ai_0": [1,12,11,0,0,4,8,"f1f06cea",104,"deputShip 2/39, kazimierzShip 0/4, lateranoShip 1/24, skillfulShip 1/27, soloShip 1/6, steadShip 1/8, swiftShip 0/16",10,20,10,66], + "ai_0": [1,13,11,0,0,4,8,"f1f06cea",104,"deputShip 2/39, kazimierzShip 0/4, lateranoShip 1/24, skillfulShip 1/27, soloShip 1/6, steadShip 1/8, swiftShip 0/16",10,20,10,66], "ai_1": [1,33,11,0,0,4,8,"e84c71c4",84,"deputShip 1/12, kazimierzShip 0/7, raidShip 1/7, siracusaShip 1/18, soloShip 0/4, suntShip 1/6, swiftShip 1/41",6,20,14,63], "ai_2": [1,17,11,0,4,4,8,"7904d229",68,"deputShip 1/4, indomShip 0/8, miraShip 1/7, siracusaShip 1/32, skillfulShip 1/2, soloShip 1/12, suntShip 1/4, swiftShip 1/7, yanShip 0/1",7,20,13,67] }, { "round": 9, - "ai_0": [1,2,12,0,1,4,8,"7e93ff85",153,"deputShip 1/45, emptyShip 1/2, kazimierzShip 0/4, lateranoShip 1/29, skillfulShip 1/35, soloShip 1/8, steadShip 1/14, suntShip 1/2, swiftShip 1/18",48,58,10,78], + "ai_0": [1,3,12,0,1,4,8,"7e93ff85",153,"deputShip 1/45, emptyShip 1/2, kazimierzShip 0/4, lateranoShip 1/29, skillfulShip 1/35, soloShip 1/8, steadShip 1/14, suntShip 1/2, swiftShip 1/18",48,58,10,78], "ai_1": [1,29,12,0,1,4,8,"1c039dc7",113,"deputShip 1/14, kazimierzShip 0/7, raidShip 1/14, siracusaShip 1/26, soloShip 0/4, suntShip 1/8, swiftShip 1/51",56,60,4,76], "ai_2": [1,9,16,0,0,4,8,"a9a38e4e",84,"deputShip 0/4, indomShip 0/8, miraShip 1/10, siracusaShip 2/58, skillfulShip 0/4, soloShip 1/12, suntShip 1/4, swiftShip 0/9, yanShip 0/1",41,49,8,82] }, @@ -826,7 +826,7 @@ "title": {"id":"comment_6","name":"挥金如土","picId":"comment_icon_6","text":"消耗资金最多"}, "trophies": 2, "reward": 191, - "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1395734,"gold":91,"itemsEquipped":6,"kills":212,"leaks":37,"lpLost":31,"merges":2,"perfectRounds":4,"refreshes":11}, + "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1395734,"gold":91,"itemsEquipped":6,"kills":212,"leaks":37,"lpLost":30,"merges":2,"perfectRounds":4,"refreshes":11}, "lineup": [], "bonds": ["arcaneShip 0/4","deputShip 0/48","emptyShip 0/5","kazimierzShip 0/4","lateranoShip 0/36","skillfulShip 0/38","soloShip 0/8","steadShip 0/18","suntShip 0/2","swiftShip 0/23"] }, @@ -854,7 +854,7 @@ "title": {"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"}, "trophies": 2, "reward": 213, - "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1840422,"gold":105,"itemsEquipped":12,"kills":180,"leaks":72,"lpLost":36,"merges":4,"perfectRounds":3,"refreshes":14}, + "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1845779,"gold":105,"itemsEquipped":12,"kills":181,"leaks":72,"lpLost":36,"merges":4,"perfectRounds":3,"refreshes":14}, "lineup": [], "bonds": ["deputShip 0/4","indomShip 0/8","miraShip 0/12","siracusaShip 0/84","skillfulShip 0/4","soloShip 0/12","suntShip 0/4","swiftShip 0/16","yanShip 0/1"] } @@ -1036,84 +1036,84 @@ "boost": null, "setup": {"stageId":"act2autochess_m01","bossId":"boss_1","hiddenBossId":"boss_8","factions":["DOT","INVISIBLE","REFLECTION"]}, "end": {"victory":false,"roundsPassed":10,"hiddenReached":false,"hiddenCleared":false,"reason":"eliminated","teamLp":null,"bossPoolLeft":null,"virtualMs":111300}, - "rng": {"setup":25,"shop":495,"waves":0,"draft":56,"bots":18468,"meta":57}, + "rng": {"setup":25,"shop":471,"waves":0,"draft":56,"bots":18450,"meta":59}, "errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0}, "rounds": [ { "round": 1, "ai_0": [1,45,4,0,0,1,2,"cf5de4b4",0,"kazimierzShip 0/2, swiftShip 0/2",5,5,0,4], "ai_1": [1,26,4,0,0,1,2,"59374416",0,"kazimierzShip 0/2, sargonShip 0/2, swiftShip 0/2",5,5,0,4], - "ai_2": [1,25,4,0,0,1,2,"eb0c1458",0,"deputShip 0/2, victoriaShip 0/2",2,5,3,4], + "ai_2": [1,28,4,0,0,1,2,"eb0c1458",0,"deputShip 0/2, victoriaShip 0/2",2,5,3,4], "ai_3": [1,26,4,0,0,1,2,"ba03322a",0,"soloShip 1/0",5,5,0,4] }, { "round": 2, "ai_0": [1,45,5,0,0,1,4,"62de86cd",4,"kazimierzShip 0/4, swiftShip 1/4",8,8,0,9], "ai_1": [1,26,5,0,0,1,4,"a2ec5116",5,"kazimierzShip 0/5, sargonShip 0/2, soloShip 1/0, swiftShip 1/5",8,8,0,9], - "ai_2": [1,24,5,0,0,1,3,"70d74d61",0,"deputShip 0/4, kjeragShip 0/2, soloShip 1/0, victoriaShip 0/6",7,8,1,9], + "ai_2": [1,28,5,0,0,1,3,"70d74d61",0,"deputShip 0/4, kjeragShip 0/2, soloShip 1/0, victoriaShip 0/6",7,8,1,9], "ai_3": [1,26,5,0,0,1,4,"9e98d014",0,"sargonShip 0/2, soloShip 1/0",8,8,0,9] }, { "round": 3, "ai_0": [1,45,6,0,1,1,7,"a7d37d87",4,"arcaneShip 0/2, kazimierzShip 0/4, preciShip 0/2, swiftShip 1/4",9,9,0,15], "ai_1": [1,26,6,0,1,1,7,"bf8932c2",5,"kazimierzShip 0/5, kjeragShip 0/2, sargonShip 0/4, soloShip 1/0, steadShip 0/2, swiftShip 1/5",11,11,0,15], - "ai_2": [1,24,6,0,1,1,6,"7363c3d8",0,"deputShip 0/4, kazimierzShip 0/2, kjeragShip 0/8, soloShip 1/0, steadShip 0/4, swiftShip 0/2, victoriaShip 0/8, yanShip 0/1",9,9,0,15], + "ai_2": [1,28,6,0,1,1,6,"7363c3d8",0,"deputShip 0/4, kazimierzShip 0/2, kjeragShip 0/8, soloShip 1/0, steadShip 0/4, swiftShip 0/2, victoriaShip 0/8, yanShip 0/1",9,9,0,15], "ai_3": [1,26,6,0,2,1,7,"5051466d",1,"arcaneShip 0/2, preciShip 0/2, sargonShip 0/2, soloShip 1/1",9,9,0,15] }, { "round": 4, - "ai_0": [1,45,8,0,1,2,8,"822a03c7",6,"arcaneShip 0/8, kazimierzShip 0/4, preciShip 0/8, swiftShip 1/6",10,10,0,23], + "ai_0": [1,45,8,0,3,2,8,"822a03c7",6,"arcaneShip 0/8, kazimierzShip 0/4, preciShip 0/8, swiftShip 1/6",10,10,0,23], "ai_1": [1,26,8,0,1,2,8,"c29eb727",12,"arcaneShip 1/2, kazimierzShip 0/5, kjeragShip 0/2, preciShip 0/2, sargonShip 0/4, soloShip 1/0, steadShip 1/2, swiftShip 1/8",12,12,0,23], - "ai_2": [1,24,8,0,1,2,8,"c8d8c0f0",9,"deputShip 1/7, kazimierzShip 0/2, kjeragShip 0/10, soloShip 1/2, steadShip 0/4, swiftShip 0/2, victoriaShip 0/10, yanShip 0/4",10,10,0,22], - "ai_3": [1,24,9,0,0,2,8,"3fd210ed",10,"arcaneShip 1/5, deputShip 1/2, preciShip 0/2, sargonShip 0/2, soloShip 1/3",8,10,2,24] + "ai_2": [1,28,8,0,1,2,8,"c8d8c0f0",9,"deputShip 1/7, kazimierzShip 0/2, kjeragShip 0/10, soloShip 1/2, steadShip 0/4, swiftShip 0/2, victoriaShip 0/10, yanShip 0/4",10,10,0,22], + "ai_3": [1,26,9,0,0,2,8,"3fd210ed",10,"arcaneShip 1/5, deputShip 1/2, preciShip 0/2, sargonShip 0/2, soloShip 1/3",8,10,2,24] }, { "round": 5, - "ai_0": [1,36,9,0,0,3,8,"0e6eee6e",8,"arcaneShip 0/8, kazimierzShip 0/4, preciShip 0/8, soloShip 1/0, suntShip 1/0, swiftShip 1/8",8,17,9,32], + "ai_0": [1,36,11,0,0,3,8,"7628b380",8,"arcaneShip 0/8, kazimierzShip 0/4, preciShip 0/8, soloShip 1/0, suntShip 1/0, swiftShip 1/8",8,17,9,33], "ai_1": [1,18,9,0,0,3,8,"8d338f5b",14,"arcaneShip 1/4, kazimierzShip 0/5, kjeragShip 0/2, preciShip 0/4, sargonShip 0/4, soloShip 1/0, steadShip 1/2, swiftShip 1/8",9,17,8,32], - "ai_2": [1,16,9,0,0,3,8,"8e449278",22,"deputShip 1/10, kazimierzShip 0/2, kjeragShip 0/14, soloShip 1/4, steadShip 1/8, swiftShip 0/2, victoriaShip 0/12, yanShip 0/4",9,17,8,31], - "ai_3": [1,14,8,0,0,3,8,"46c3fdd0",17,"arcaneShip 1/8, deputShip 1/4, preciShip 0/2, sargonShip 0/2, soloShip 1/5",5,17,12,32] + "ai_2": [1,20,9,0,0,3,8,"8e449278",22,"deputShip 1/10, kazimierzShip 0/2, kjeragShip 0/14, soloShip 1/4, steadShip 1/8, swiftShip 0/2, victoriaShip 0/12, yanShip 0/4",9,17,8,31], + "ai_3": [1,16,8,0,0,3,8,"46c3fdd0",17,"arcaneShip 1/8, deputShip 1/4, preciShip 0/2, sargonShip 0/2, soloShip 1/5",5,17,12,32] }, { "round": 6, - "ai_0": [1,36,9,0,0,3,8,"6f9e5953",31,"arcaneShip 1/11, indomShip 1/2, kazimierzShip 1/8, preciShip 0/8, soloShip 1/0, suntShip 1/0, swiftShip 1/10",13,14,1,41], + "ai_0": [1,36,9,0,0,3,8,"f71e9e29",31,"arcaneShip 1/11, indomShip 1/2, kazimierzShip 1/8, preciShip 0/8, soloShip 1/0, suntShip 1/0, swiftShip 1/10",13,14,1,42], "ai_1": [1,18,9,0,0,3,8,"0d78d413",35,"arcaneShip 1/4, kazimierzShip 1/9, kjeragShip 0/2, preciShip 1/8, sargonShip 0/4, soloShip 1/0, steadShip 2/6, swiftShip 1/8",14,14,0,41], - "ai_2": [1,16,9,0,0,3,8,"d76ab210",59,"deputShip 1/12, kazimierzShip 0/4, kjeragShip 1/19, raidShip 1/14, soloShip 0/4, steadShip 2/14, swiftShip 0/2, victoriaShip 0/14, yanShip 0/4",14,14,0,41], - "ai_3": [1,14,9,0,0,3,8,"b07fd748",46,"arcaneShip 1/11, deputShip 1/6, kjeragShip 1/9, preciShip 1/9, sargonShip 0/2, soloShip 1/7, steadShip 1/4",12,14,2,41] + "ai_2": [1,20,9,0,0,3,8,"d76ab210",59,"deputShip 1/12, kazimierzShip 0/4, kjeragShip 1/19, raidShip 1/14, soloShip 0/4, steadShip 2/14, swiftShip 0/2, victoriaShip 0/14, yanShip 0/4",14,14,0,41], + "ai_3": [1,16,9,0,0,3,8,"b07fd748",46,"arcaneShip 1/11, deputShip 1/6, kjeragShip 1/9, preciShip 1/9, sargonShip 0/2, soloShip 1/7, steadShip 1/4",12,14,2,41] }, { "round": 7, - "ai_0": [1,26,10,0,0,4,8,"3e2f54ac",43,"arcaneShip 1/13, indomShip 1/4, kazimierzShip 1/16, preciShip 0/8, soloShip 1/0, suntShip 1/0, swiftShip 1/10",7,21,14,52], + "ai_0": [1,26,10,0,0,4,8,"9bcfb186",43,"arcaneShip 1/13, indomShip 1/4, kazimierzShip 1/16, preciShip 0/8, soloShip 1/0, suntShip 1/0, swiftShip 1/10",7,21,14,53], "ai_1": [1,8,10,0,0,4,8,"c08bbdb7",47,"arcaneShip 1/4, kazimierzShip 1/13, kjeragShip 0/2, preciShip 1/12, sargonShip 0/4, soloShip 1/0, steadShip 2/10, swiftShip 1/8",10,21,11,52], - "ai_2": [1,7,10,0,0,3,8,"2fd8f260",101,"deputShip 2/20, kazimierzShip 0/4, kjeragShip 1/31, raidShip 0/14, skillfulShip 0/2, soloShip 0/4, steadShip 1/22, suntShip 1/4, swiftShip 0/2, victoriaShip 1/24, yanShip 0/10",12,21,9,51], - "ai_3": [1,4,10,0,0,4,8,"07f52be4",54,"arcaneShip 1/15, deputShip 1/8, kjeragShip 1/11, preciShip 1/11, sargonShip 0/2, soloShip 1/9, steadShip 0/4",4,21,17,52] + "ai_2": [1,11,10,0,0,3,8,"2fd8f260",101,"deputShip 2/20, kazimierzShip 0/4, kjeragShip 1/31, raidShip 0/14, skillfulShip 0/2, soloShip 0/4, steadShip 1/22, suntShip 1/4, swiftShip 0/2, victoriaShip 1/24, yanShip 0/10",12,21,9,51], + "ai_3": [1,6,10,0,0,4,8,"07f52be4",54,"arcaneShip 1/15, deputShip 1/8, kjeragShip 1/11, preciShip 1/11, sargonShip 0/2, soloShip 1/9, steadShip 0/4",4,21,17,52] }, { "round": 8, - "ai_0": [1,20,11,0,0,4,8,"76bea343",35,"arcaneShip 2/13, emptyShip 1/0, indomShip 0/4, kazimierzShip 1/22, preciShip 0/8, soloShip 1/0, suntShip 1/0, swiftShip 0/14",3,9,6,63], + "ai_0": [1,20,11,0,0,4,8,"85689579",35,"arcaneShip 2/13, emptyShip 1/0, indomShip 0/4, kazimierzShip 1/22, preciShip 0/8, soloShip 1/0, suntShip 1/0, swiftShip 0/14",3,9,6,64], "ai_1": [1,1,11,0,0,4,8,"5be276b8",69,"arcaneShip 2/4, kazimierzShip 1/17, kjeragShip 0/2, preciShip 2/21, sargonShip 0/4, soloShip 1/0, steadShip 1/14, swiftShip 1/13",2,9,7,63], - "ai_2": [1,1,11,0,0,4,8,"029d7c62",110,"deputShip 2/28, kazimierzShip 0/4, kjeragShip 1/40, preciShip 0/2, raidShip 0/14, skillfulShip 0/2, soloShip 0/4, steadShip 0/22, suntShip 1/8, swiftShip 0/4, victoriaShip 1/34, yanShip 0/10",3,9,6,62], + "ai_2": [1,5,11,0,0,4,8,"029d7c62",110,"deputShip 2/28, kazimierzShip 0/4, kjeragShip 1/40, preciShip 0/2, raidShip 0/14, skillfulShip 0/2, soloShip 0/4, steadShip 0/22, suntShip 1/8, swiftShip 0/4, victoriaShip 1/34, yanShip 0/10",3,9,6,62], "ai_3": [0,0,11,0,0,4,0,"811c9dc5",0,"arcaneShip 0/20, deputShip 0/10, kjeragShip 0/19, preciShip 0/11, sargonShip 0/2, skillfulShip 0/7, soloShip 0/11, steadShip 0/4",1,9,8,63] }, { "round": 9, - "ai_0": [1,10,12,0,3,4,8,"a3659a4d",41,"arcaneShip 2/17, emptyShip 1/0, indomShip 0/4, kazimierzShip 1/24, preciShip 0/8, soloShip 1/0, suntShip 1/0, swiftShip 0/14",4,19,15,73], - "ai_1": [0,0,12,0,0,4,0,"811c9dc5",0,"arcaneShip 0/4, kazimierzShip 0/21, kjeragShip 0/2, preciShip 0/30, sargonShip 0/4, steadShip 0/20, swiftShip 0/18",6,19,13,75], - "ai_2": [1,1,12,0,1,4,8,"138cef2e",169,"deputShip 2/36, kazimierzShip 0/4, kjeragShip 1/54, preciShip 0/2, raidShip 0/14, skillfulShip 1/13, soloShip 1/8, steadShip 0/22, suntShip 1/12, swiftShip 0/4, victoriaShip 1/46, yanShip 0/10",19,19,0,73], + "ai_0": [1,10,12,0,0,4,8,"92351ce9",37,"arcaneShip 2/13, emptyShip 1/0, indomShip 0/4, kazimierzShip 1/24, preciShip 0/8, soloShip 1/0, suntShip 1/0, swiftShip 0/14",4,19,15,75], + "ai_1": [0,0,12,0,0,4,0,"811c9dc5",0,"arcaneShip 0/4, kazimierzShip 0/23, kjeragShip 0/2, preciShip 0/30, sargonShip 0/4, steadShip 0/18, swiftShip 0/20",6,19,13,76], + "ai_2": [1,5,12,0,1,4,8,"8e97ba4c",161,"deputShip 2/36, kazimierzShip 0/4, kjeragShip 1/54, preciShip 0/2, raidShip 0/14, skillfulShip 1/13, soloShip 0/4, steadShip 0/22, suntShip 1/12, swiftShip 0/4, victoriaShip 1/46, yanShip 0/10",19,19,0,75], "ai_3": [0,0,0,0,0,4,0,"811c9dc5",0,"arcaneShip 0/20, deputShip 0/10, kjeragShip 0/19, preciShip 0/11, sargonShip 0/2, skillfulShip 0/7, soloShip 0/11, steadShip 0/4",null,null,null,63] }, { "round": 10, - "ai_0": [1,5,15,0,0,5,8,"a650b1b8",51,"arcaneShip 2/25, emptyShip 1/0, indomShip 0/4, kazimierzShip 1/26, preciShip 0/8, soloShip 1/0, suntShip 1/0, swiftShip 0/14",16,25,9,87], - "ai_1": [0,0,0,0,0,4,0,"811c9dc5",0,"arcaneShip 0/4, kazimierzShip 0/21, kjeragShip 0/2, preciShip 0/30, sargonShip 0/4, steadShip 0/20, swiftShip 0/18",null,null,null,75], - "ai_2": [1,1,13,0,0,5,8,"4d632433",206,"deputShip 2/42, kazimierzShip 0/4, kjeragShip 1/63, preciShip 0/2, raidShip 0/14, skillfulShip 1/17, soloShip 1/14, steadShip 0/22, suntShip 1/16, swiftShip 0/4, victoriaShip 1/54, yanShip 0/12",25,25,0,86], + "ai_0": [1,7,12,0,0,5,8,"28ed99c4",42,"arcaneShip 2/16, emptyShip 1/0, indomShip 0/4, kazimierzShip 1/26, preciShip 0/8, soloShip 1/0, suntShip 1/0, swiftShip 0/14",14,25,11,86], + "ai_1": [0,0,0,0,0,4,0,"811c9dc5",0,"arcaneShip 0/4, kazimierzShip 0/23, kjeragShip 0/2, preciShip 0/30, sargonShip 0/4, steadShip 0/18, swiftShip 0/20",null,null,null,76], + "ai_2": [1,5,13,0,0,5,8,"7348a417",233,"arcaneShip 0/2, deputShip 1/40, egirShip 0/2, kazimierzShip 0/4, kjeragShip 1/59, preciShip 0/2, raidShip 1/47, skillfulShip 1/17, soloShip 0/4, steadShip 0/24, suntShip 1/14, swiftShip 0/4, victoriaShip 1/56, yanShip 0/10",25,25,0,88], "ai_3": [0,0,0,0,0,4,0,"811c9dc5",0,"arcaneShip 0/20, deputShip 0/10, kjeragShip 0/19, preciShip 0/11, sargonShip 0/2, skillfulShip 0/7, soloShip 0/11, steadShip 0/4",null,null,null,63] }, { "round": 11, - "ai_0": [0,0,12,0,0,5,0,"811c9dc5",0,"arcaneShip 0/29, indomShip 0/4, kazimierzShip 0/26, preciShip 0/8, swiftShip 0/14",4,24,20,99], - "ai_1": [0,0,0,0,0,4,0,"811c9dc5",0,"arcaneShip 0/4, kazimierzShip 0/21, kjeragShip 0/2, preciShip 0/30, sargonShip 0/4, steadShip 0/20, swiftShip 0/18",null,null,null,75], - "ai_2": [0,0,12,0,0,5,0,"811c9dc5",0,"deputShip 0/48, emptyShip 0/2, investShip 0/2, kazimierzShip 0/4, kjeragShip 0/72, preciShip 0/16, raidShip 0/14, skillfulShip 0/21, soloShip 0/14, steadShip 0/22, suntShip 0/18, swiftShip 0/18, victoriaShip 0/64, visiShip 0/4, yanShip 0/12",20,24,4,102], + "ai_0": [0,0,12,0,0,5,0,"811c9dc5",0,"arcaneShip 0/22, indomShip 0/4, kazimierzShip 0/28, preciShip 0/8, swiftShip 0/14",5,24,19,98], + "ai_1": [0,0,0,0,0,4,0,"811c9dc5",0,"arcaneShip 0/4, kazimierzShip 0/23, kjeragShip 0/2, preciShip 0/30, sargonShip 0/4, steadShip 0/18, swiftShip 0/20",null,null,null,76], + "ai_2": [0,0,12,0,0,5,0,"811c9dc5",0,"arcaneShip 0/12, deputShip 0/46, egirShip 0/2, kazimierzShip 0/4, kjeragShip 0/67, preciShip 0/2, raidShip 0/47, skillfulShip 0/21, soloShip 0/4, steadShip 0/24, suntShip 0/16, swiftShip 0/4, victoriaShip 0/64, yanShip 0/12",12,24,12,104], "ai_3": [0,0,0,0,0,4,0,"811c9dc5",0,"arcaneShip 0/20, deputShip 0/10, kjeragShip 0/19, preciShip 0/11, sargonShip 0/2, skillfulShip 0/7, soloShip 0/11, steadShip 0/4",null,null,null,63] } ], @@ -1128,9 +1128,9 @@ "title": {"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"}, "trophies": 2, "reward": 213, - "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1585533,"gold":99,"itemsEquipped":10,"kills":87,"leaks":74,"lpLost":50,"merges":4,"perfectRounds":4,"refreshes":12}, + "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1582681,"gold":98,"itemsEquipped":11,"kills":86,"leaks":75,"lpLost":48,"merges":3,"perfectRounds":4,"refreshes":13}, "lineup": [], - "bonds": ["arcaneShip 0/29","indomShip 0/4","kazimierzShip 0/26","preciShip 0/8","swiftShip 0/14"] + "bonds": ["arcaneShip 0/22","indomShip 0/4","kazimierzShip 0/28","preciShip 0/8","swiftShip 0/14"] }, "ai_1": { "alive": false, @@ -1142,9 +1142,9 @@ "title": {"id":"comment_6","name":"挥金如土","picId":"comment_icon_6","text":"消耗资金最多"}, "trophies": 1, "reward": 170, - "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1199483,"gold":75,"itemsEquipped":9,"kills":77,"leaks":39,"lpLost":35,"merges":1,"perfectRounds":5,"refreshes":12}, + "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1254589,"gold":76,"itemsEquipped":9,"kills":80,"leaks":39,"lpLost":35,"merges":1,"perfectRounds":5,"refreshes":10}, "lineup": [], - "bonds": ["arcaneShip 0/4","kazimierzShip 0/21","kjeragShip 0/2","preciShip 0/30","sargonShip 0/4","steadShip 0/20","swiftShip 0/18"] + "bonds": ["arcaneShip 0/4","kazimierzShip 0/23","kjeragShip 0/2","preciShip 0/30","sargonShip 0/4","steadShip 0/18","swiftShip 0/20"] }, "ai_2": { "alive": false, @@ -1156,9 +1156,9 @@ "title": {"id":"comment_5","name":"万事俱备","picId":"comment_icon_5","text":"装备配发数量最多"}, "trophies": 2, "reward": 213, - "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":2779516,"gold":102,"itemsEquipped":11,"kills":137,"leaks":31,"lpLost":31,"merges":3,"perfectRounds":5,"refreshes":7}, + "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":2872888,"gold":104,"itemsEquipped":13,"kills":137,"leaks":39,"lpLost":33,"merges":3,"perfectRounds":5,"refreshes":5}, "lineup": [], - "bonds": ["deputShip 0/48","emptyShip 0/2","investShip 0/2","kazimierzShip 0/4","kjeragShip 0/72","preciShip 0/16","raidShip 0/14","skillfulShip 0/21","soloShip 0/14","steadShip 0/22","suntShip 0/18","swiftShip 0/18","victoriaShip 0/64","visiShip 0/4","yanShip 0/12"] + "bonds": ["arcaneShip 0/12","deputShip 0/46","egirShip 0/2","kazimierzShip 0/4","kjeragShip 0/67","preciShip 0/2","raidShip 0/47","skillfulShip 0/21","soloShip 0/4","steadShip 0/24","suntShip 0/16","swiftShip 0/4","victoriaShip 0/64","yanShip 0/12"] }, "ai_3": { "alive": false, @@ -1170,7 +1170,7 @@ "title": null, "trophies": 1, "reward": 149, - "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":768250,"gold":63,"itemsEquipped":8,"kills":52,"leaks":41,"lpLost":30,"merges":1,"perfectRounds":3,"refreshes":3}, + "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":777705,"gold":63,"itemsEquipped":8,"kills":55,"leaks":41,"lpLost":28,"merges":1,"perfectRounds":3,"refreshes":3}, "lineup": [], "bonds": ["arcaneShip 0/20","deputShip 0/10","kjeragShip 0/19","preciShip 0/11","sargonShip 0/2","skillfulShip 0/7","soloShip 0/11","steadShip 0/4"] } @@ -1186,7 +1186,7 @@ "combat": "server", "boost": null, "setup": {"stageId":"act1autochess_m01","bossId":"boss_1","hiddenBossId":"boss_9","factions":["FLY","DOT","REFLECTION"]}, - "end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":22,"bossPoolLeft":0,"virtualMs":119900}, + "end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":32,"bossPoolLeft":0,"virtualMs":119900}, "rng": {"setup":25,"shop":511,"waves":0,"draft":34,"bots":18062,"meta":112}, "errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0}, "rounds": [ @@ -1223,37 +1223,37 @@ { "round": 7, "ai_0": [1,29,13,0,2,4,8,"ae8b19bc",59,"arcaneShip 1/8, deputShip 2/6, kjeragShip 0/2, preciShip 0/4, siracusaShip 1/27, soloShip 1/18, steadShip 0/2",27,27,0,53], - "ai_1": [1,23,11,0,3,4,8,"c7d258e1",15,"arcaneShip 0/2, kjeragShip 0/13, preciShip 2/7, siracusaShip 1/0, soloShip 1/0, steadShip 1/8",22,27,5,49] + "ai_1": [1,28,11,0,3,4,8,"c7d258e1",15,"arcaneShip 0/2, kjeragShip 0/13, preciShip 2/7, siracusaShip 1/0, soloShip 1/0, steadShip 1/8",22,27,5,49] }, { "round": 8, "ai_0": [1,29,13,0,1,4,8,"6bbcf02a",90,"arcaneShip 2/18, deputShip 2/10, kjeragShip 0/2, preciShip 0/4, siracusaShip 1/40, soloShip 1/22, steadShip 0/2",24,24,0,66], - "ai_1": [1,17,14,0,0,4,8,"5589bffd",24,"arcaneShip 0/2, emptyShip 1/0, kjeragShip 1/13, preciShip 1/7, siracusaShip 1/4, soloShip 1/0, steadShip 0/8",17,23,6,63] + "ai_1": [1,27,14,0,0,4,8,"5589bffd",24,"arcaneShip 0/2, emptyShip 1/0, kjeragShip 1/13, preciShip 1/7, siracusaShip 1/4, soloShip 1/0, steadShip 0/8",17,23,6,63] }, { "round": 9, "ai_0": [1,29,13,0,0,4,8,"5de9a4ca",153,"arcaneShip 2/38, deputShip 1/12, indomShip 0/8, kjeragShip 0/2, miraShip 1/2, preciShip 0/4, siracusaShip 1/64, soloShip 1/24, steadShip 0/2, suntShip 1/2, victoriaShip 1/11, visiShip 0/8",15,15,0,79], - "ai_1": [1,17,12,0,0,4,8,"90947485",62,"arcaneShip 0/2, indomShip 0/8, kjeragShip 1/28, preciShip 2/26, siracusaShip 1/8, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",15,15,0,75] + "ai_1": [1,27,12,0,0,4,8,"90947485",62,"arcaneShip 0/2, indomShip 0/8, kjeragShip 1/28, preciShip 2/26, siracusaShip 1/8, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",15,15,0,75] }, { "round": 10, "ai_0": [1,29,12,0,2,5,8,"c0721c81",178,"arcaneShip 2/42, deputShip 1/14, indomShip 0/8, kjeragShip 0/2, miraShip 1/4, preciShip 0/4, siracusaShip 1/74, soloShip 1/26, steadShip 0/2, suntShip 1/4, victoriaShip 1/14, visiShip 0/8",24,24,0,92], - "ai_1": [1,17,12,0,0,5,8,"5b7d990c",72,"arcaneShip 0/2, indomShip 0/8, kjeragShip 1/28, preciShip 2/36, siracusaShip 1/8, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",23,23,0,86] + "ai_1": [1,27,12,0,0,5,8,"5b7d990c",72,"arcaneShip 0/2, indomShip 0/8, kjeragShip 1/28, preciShip 2/36, siracusaShip 1/8, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",23,23,0,86] }, { "round": 11, "ai_0": [1,29,14,0,0,5,8,"1de29f06",219,"arcaneShip 2/50, deputShip 1/16, indomShip 0/8, kjeragShip 0/2, miraShip 1/6, preciShip 0/4, siracusaShip 1/96, soloShip 1/28, steadShip 0/2, suntShip 1/6, victoriaShip 1/17, visiShip 0/8",23,23,0,107], - "ai_1": [1,17,12,0,0,5,8,"c68c2f7d",103,"arcaneShip 0/2, indomShip 0/8, kjeragShip 1/28, preciShip 2/49, siracusaShip 1/26, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",23,23,0,97] + "ai_1": [1,27,12,0,0,5,8,"c68c2f7d",103,"arcaneShip 0/2, indomShip 0/8, kjeragShip 1/28, preciShip 2/49, siracusaShip 1/26, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",23,23,0,97] }, { "round": 12, "ai_0": [1,19,12,0,0,6,8,"06501643",236,"arcaneShip 2/52, deputShip 1/18, indomShip 0/8, kjeragShip 0/2, miraShip 1/8, preciShip 0/4, siracusaShip 1/100, soloShip 1/30, steadShip 0/2, suntShip 1/8, victoriaShip 1/20, visiShip 0/8",28,47,19,119], - "ai_1": [1,16,12,0,0,6,8,"de894d12",130,"arcaneShip 0/2, indomShip 0/8, kjeragShip 1/28, preciShip 2/62, siracusaShip 1/40, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",45,46,1,108] + "ai_1": [1,26,12,0,0,6,8,"de894d12",130,"arcaneShip 0/2, indomShip 0/8, kjeragShip 1/28, preciShip 2/62, siracusaShip 1/40, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",45,46,1,108] }, { "round": 13, "ai_0": [1,9,12,0,0,6,8,"e26e0bca",240,"arcaneShip 2/60, deputShip 0/18, indomShip 0/8, kjeragShip 0/2, maniShip 1/0, miraShip 1/8, preciShip 0/4, siracusaShip 1/114, soloShip 1/30, steadShip 0/2, suntShip 1/8, victoriaShip 1/20, visiShip 0/8",16,39,23,129], - "ai_1": [1,13,12,0,0,6,8,"95ed45eb",166,"arcaneShip 0/4, indomShip 0/8, kjeragShip 1/28, preciShip 2/88, siracusaShip 1/50, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",36,39,3,120] + "ai_1": [1,23,12,0,0,6,8,"95ed45eb",166,"arcaneShip 0/4, indomShip 0/8, kjeragShip 1/28, preciShip 2/88, siracusaShip 1/50, soloShip 1/0, steadShip 0/8, suntShip 1/0, victoriaShip 0/8, visiShip 0/8",36,39,3,120] } ], "players": { @@ -1267,7 +1267,7 @@ "title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"}, "trophies": 4, "reward": 400, - "stats": {"activatedLayers":285,"bossDamage":757732,"dmgDealt":3562183,"gold":142,"itemsEquipped":20,"kills":255,"leaks":43,"lpLost":21,"merges":4,"perfectRounds":10,"refreshes":23}, + "stats": {"activatedLayers":285,"bossDamage":757732,"dmgDealt":3727425,"gold":142,"itemsEquipped":20,"kills":265,"leaks":43,"lpLost":21,"merges":4,"perfectRounds":10,"refreshes":23}, "lineup": ["5_20_a@11,3[4_07_e_a+1_01_e_a]","5_12_a@11,6[6_09_e_a+3_08_e_b]","6_11_a@11,7","2_10_a@10,3[3_05_e_a+3_11_e_b]","4_10_a@10,7[4_09_e_a+1_05_e_a]","1_08_b@10,8[1_02_e_a+5_03_e_a]","4_11_b@9,8[4_08_e_a]","6_07_a@9,9[4_10_e_a+4_05_e_a]"], "bonds": ["arcaneShip 2/74","deputShip 0/18","indomShip 0/8","kjeragShip 0/2","maniShip 1/0","miraShip 1/12","preciShip 0/4","siracusaShip 1/132","soloShip 1/30","steadShip 0/2","suntShip 1/8","victoriaShip 1/29","visiShip 0/8"] }, @@ -1276,12 +1276,12 @@ "victory": true, "roundsPassed": 14, "eliminatedRound": null, - "lp": 13, + "lp": 23, "bandId": "band_malkie", "title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"}, "trophies": 4, "reward": 400, - "stats": {"activatedLayers":166,"bossDamage":592268,"dmgDealt":3911952,"gold":132,"itemsEquipped":21,"kills":253,"leaks":15,"lpLost":15,"merges":3,"perfectRounds":9,"refreshes":22}, + "stats": {"activatedLayers":166,"bossDamage":592268,"dmgDealt":3922620,"gold":132,"itemsEquipped":21,"kills":253,"leaks":15,"lpLost":5,"merges":3,"perfectRounds":9,"refreshes":22}, "lineup": ["3_20_a@12,3[4_03_e_a+5_01_e_a]","5_10_a@11,3[1_01_e_a+3_04_e_b]","5_02_a@11,6","1_17_a@11,7[5_07_e_a+1_02_e_b]","3_11_b@10,3[3_03_e_a+5_02_e_b]","1_08_b@10,8[3_06_e_a+4_11_e_a]","1_02_b@9,4[4_10_e_a+4_08_e_b]","6_18_a@9,9[3_11_e_a+5_02_e_a]"], "bonds": ["arcaneShip 0/4","indomShip 0/8","kjeragShip 1/28","preciShip 2/88","siracusaShip 1/50","soloShip 1/0","steadShip 0/8","suntShip 1/0","victoriaShip 0/8","visiShip 0/8"] } @@ -1492,8 +1492,8 @@ "combat": "client", "boost": null, "setup": {"stageId":"act1autochess_m01","bossId":"boss_5","hiddenBossId":"boss_10","factions":["DOT","INVISIBLE","REFLECTION"]}, - "end": {"victory":false,"roundsPassed":13,"hiddenReached":false,"hiddenCleared":false,"reason":"defeat","teamLp":0,"bossPoolLeft":59326,"virtualMs":119550}, - "rng": {"setup":25,"shop":484,"waves":0,"draft":34,"bots":17816,"meta":19}, + "end": {"victory":true,"roundsPassed":14,"hiddenReached":false,"hiddenCleared":false,"reason":"victory","teamLp":14,"bossPoolLeft":0,"virtualMs":119900}, + "rng": {"setup":25,"shop":525,"waves":0,"draft":34,"bots":18074,"meta":23}, "errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0}, "rounds": [ { @@ -1538,57 +1538,57 @@ }, { "round": 9, - "h_0": [1,25,12,0,0,4,8,"f78a00e2",63,"arcaneShip 1/7, kazimierzShip 0/8, preciShip 1/14, siracusaShip 1/18, suntShip 1/0, swiftShip 1/24, yanShip 0/13",17,23,6,76], + "h_0": [1,28,12,0,0,4,8,"f78a00e2",63,"arcaneShip 1/7, kazimierzShip 0/8, preciShip 1/14, siracusaShip 1/18, suntShip 1/0, swiftShip 1/24, yanShip 0/13",17,23,6,76], "ai_0": [1,23,12,0,0,4,8,"ae419bd2",132,"arcaneShip 0/2, deputShip 1/14, kazimierzShip 0/8, lateranoShip 1/11, preciShip 0/2, raidShip 0/2, sargonShip 0/2, siracusaShip 1/24, skillfulShip 1/34, suntShip 1/8, swiftShip 1/41",23,23,0,75] }, { "round": 10, - "h_0": [1,25,12,0,0,5,8,"1a942a0c",66,"arcaneShip 0/7, kazimierzShip 0/8, preciShip 1/19, siracusaShip 1/18, skillfulShip 1/0, suntShip 1/0, swiftShip 1/29, yanShip 0/13",31,31,0,88], + "h_0": [1,28,12,0,0,5,8,"1a942a0c",66,"arcaneShip 0/7, kazimierzShip 0/8, preciShip 1/19, siracusaShip 1/18, skillfulShip 1/0, suntShip 1/0, swiftShip 1/29, yanShip 0/13",31,31,0,88], "ai_0": [1,23,12,0,1,5,8,"82fa7de0",166,"arcaneShip 0/2, deputShip 1/22, kazimierzShip 0/8, lateranoShip 1/19, miraShip 1/2, preciShip 0/2, raidShip 0/2, sargonShip 0/2, siracusaShip 1/26, skillfulShip 1/42, suntShip 1/10, swiftShip 1/45",31,31,0,87] }, { "round": 11, - "h_0": [1,20,12,0,2,5,8,"a1840a82",60,"arcaneShip 0/7, kazimierzShip 0/8, miraShip 1/4, preciShip 0/19, siracusaShip 1/18, skillfulShip 1/0, soloShip 1/0, suntShip 1/0, swiftShip 1/38, yanShip 0/13",20,25,5,102], + "h_0": [1,25,12,0,2,5,8,"a1840a82",60,"arcaneShip 0/7, kazimierzShip 0/8, miraShip 1/4, preciShip 0/19, siracusaShip 1/18, skillfulShip 1/0, soloShip 1/0, suntShip 1/0, swiftShip 1/38, yanShip 0/13",20,25,5,102], "ai_0": [1,23,13,0,0,5,8,"c0b11e3b",214,"arcaneShip 0/2, deputShip 1/30, kazimierzShip 0/8, lateranoShip 1/29, miraShip 1/4, preciShip 0/2, raidShip 0/2, sargonShip 0/2, siracusaShip 1/40, skillfulShip 1/52, suntShip 1/12, swiftShip 1/47",25,25,0,98] }, { "round": 12, - "h_0": [1,10,14,0,0,6,8,"efcfaab5",123,"arcaneShip 0/7, kazimierzShip 0/8, miraShip 1/18, preciShip 1/24, siracusaShip 1/34, soloShip 1/0, suntShip 1/0, swiftShip 1/47, yanShip 0/13",15,31,16,116], + "h_0": [1,25,14,0,0,6,8,"efcfaab5",123,"arcaneShip 0/7, kazimierzShip 0/8, miraShip 1/18, preciShip 1/24, siracusaShip 1/34, soloShip 1/0, suntShip 1/0, swiftShip 1/47, yanShip 0/13",15,31,16,116], "ai_0": [1,23,12,0,1,6,8,"97827397",254,"arcaneShip 0/2, deputShip 1/32, kazimierzShip 0/8, lateranoShip 1/49, miraShip 1/6, preciShip 0/2, raidShip 0/2, sargonShip 0/2, siracusaShip 1/42, skillfulShip 1/62, suntShip 1/14, swiftShip 1/49",31,31,0,112] }, { "round": 13, - "h_0": [0,0,12,0,0,6,0,"811c9dc5",0,"arcaneShip 0/7, kazimierzShip 0/8, miraShip 0/30, preciShip 0/33, sargonShip 0/9, siracusaShip 0/46, swiftShip 0/51, yanShip 0/13",8,57,49,130], + "h_0": [1,15,12,0,2,6,8,"31530f99",169,"arcaneShip 0/7, kazimierzShip 0/8, miraShip 1/30, preciShip 1/33, raidShip 1/0, sargonShip 1/9, siracusaShip 1/46, suntShip 1/0, swiftShip 1/51, yanShip 0/13",8,57,49,130], "ai_0": [1,13,13,0,2,6,8,"c4308d5d",194,"arcaneShip 0/2, deputShip 0/32, kazimierzShip 0/8, lateranoShip 1/63, miraShip 1/6, preciShip 1/2, raidShip 0/2, sargonShip 0/2, siracusaShip 1/54, skillfulShip 0/68, suntShip 1/14, swiftShip 1/55",10,57,47,127] } ], "players": { "h_0": { - "alive": false, - "victory": false, - "roundsPassed": 12, - "eliminatedRound": 13, - "lp": 0, + "alive": true, + "victory": true, + "roundsPassed": 14, + "eliminatedRound": null, + "lp": 8, "bandId": "band_amedic", - "title": {"id":"comment_6","name":"挥金如土","picId":"comment_icon_6","text":"消耗资金最多"}, - "trophies": 3, - "reward": 280, - "stats": {"activatedLayers":0,"bossDamage":0,"dmgDealt":1950591,"gold":130,"itemsEquipped":14,"kills":218,"leaks":76,"lpLost":28,"merges":4,"perfectRounds":9,"refreshes":20}, - "lineup": [], - "bonds": ["arcaneShip 0/7","kazimierzShip 0/8","miraShip 0/30","preciShip 0/33","sargonShip 0/9","siracusaShip 0/46","swiftShip 0/51","yanShip 0/13"] + "title": {"id":"comment_4","name":"精英云集","picId":"comment_icon_4","text":"晋升精锐次数最多"}, + "trophies": 4, + "reward": 400, + "stats": {"activatedLayers":189,"bossDamage":281870,"dmgDealt":2422133,"gold":144,"itemsEquipped":20,"kills":241,"leaks":76,"lpLost":13,"merges":6,"perfectRounds":9,"refreshes":24}, + "lineup": ["5_10_a@12,3[1_02_e_b]","5_15_a@11,3[3_11_e_a]","3_13_b@11,8[1_01_e_a]","3_19_b@10,3[4_07_e_a+3_01_e_a]","4_16_a@10,7[4_03_e_b+3_07_e_a]","1_08_b@10,8[4_11_e_b+4_02_e_a]","6_05_a@9,8[6_09_e_a+6_01_e_b]","5_02_a@9,9"], + "bonds": ["arcaneShip 0/11","kazimierzShip 0/16","kjeragShip 0/8","miraShip 1/34","preciShip 1/37","sargonShip 1/9","siracusaShip 1/46","soloShip 1/0","suntShip 1/0","swiftShip 1/63","yanShip 0/13"] }, "ai_0": { "alive": true, - "victory": false, - "roundsPassed": 13, + "victory": true, + "roundsPassed": 14, "eliminatedRound": null, - "lp": 0, + "lp": 6, "bandId": "band_chiave", - "title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"}, - "trophies": 3, - "reward": 320, - "stats": {"activatedLayers":280,"bossDamage":330674,"dmgDealt":3048187,"gold":142,"itemsEquipped":22,"kills":259,"leaks":47,"lpLost":10,"merges":6,"perfectRounds":12,"refreshes":21}, - "lineup": ["1_01_b@12,3[2_01_e_a+1_02_e_a]","4_21_a@12,7","3_21_b@11,3[3_01_e_a+1_05_e_b]","5_20_a@11,8[3_07_e_b]","3_01_b@10,3[4_08_e_b+6_05_e_a]","3_18_a@10,7[4_11_e_b+4_02_e_b]","4_16_a@10,8","6_01_a@9,8[5_09_e_a+3_08_e_a]"], + "title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"}, + "trophies": 4, + "reward": 400, + "stats": {"activatedLayers":280,"bossDamage":498130,"dmgDealt":3639604,"gold":140,"itemsEquipped":22,"kills":271,"leaks":47,"lpLost":10,"merges":6,"perfectRounds":12,"refreshes":21}, + "lineup": ["1_01_b@12,3[2_01_e_a+3_08_e_b]","4_21_a@12,7","3_21_b@11,3[3_01_e_a+1_05_e_b]","5_20_a@11,8[3_07_e_b]","3_01_b@10,3[4_08_e_b+6_05_e_a]","3_18_a@10,7","4_16_a@10,8[4_11_e_a+4_02_e_a]","6_01_a@9,8[5_09_e_a+3_07_e_a]"], "bonds": ["arcaneShip 0/2","deputShip 0/32","kazimierzShip 0/8","lateranoShip 1/71","miraShip 1/6","preciShip 0/2","raidShip 0/2","sargonShip 0/2","siracusaShip 1/66","skillfulShip 1/68","suntShip 1/14","swiftShip 1/55"] } }, @@ -1607,7 +1607,7 @@ [10,"3_18_a→char_609_acguad 4_16_a→char_615_acspec 5_10_a→char_606_csuppo"], [11,"4_16_a→char_615_acspec 5_10_a→char_606_csuppo"], [12,"4_16_a→char_615_acspec 5_10_a→char_606_csuppo 6_05_a→char_611_acnipe"], - [13,""] + [13,"4_16_a→char_615_acspec 5_10_a→char_606_csuppo 6_05_a→char_611_acnipe"] ] } }, diff --git a/test/match/feedback5-unite-map.test.js b/test/match/feedback5-unite-map.test.js index 42ce2054..8e26a4a4 100644 --- a/test/match/feedback5-unite-map.test.js +++ b/test/match/feedback5-unite-map.test.js @@ -1,16 +1,17 @@ -// GitHub #41 (item 3): the 联防 field was the round's own stage — its water, crates and devices included — opened to both -// halves. Official: act2autochess constData escapedBattleTemplateMapSinglePlayer / MultiPlayer = level_act1autochess_ -// escaped_single / _multi, the level of the 联防 battle and its map (two road halves joined at col 10). One helper → -// escaped_single (enemies enter at col 10), two helpers → escaped_multi (enemies enter at col 18 and pass (9,10)); the -// helpers' pieces stand on their prep tiles ("按休整期位置部署在场"), the first of two shifted 8 columns onto the right half -// ("率先迎敌(即位于右侧阵地)"). data/stages.json holds both maps (kind 'unite'); server/match/unite.js uniteStageId. +// 联防 plays on the round's battlefield again (0.2.1, the owner's decision of 2026-10-07 after the report 「联防阶段地形全消失 +// 了」: 「官服保留地形,改回去」). The 联防 field is the match stage opened to both halves (GEO.UNITE_RECT, cols 0–20) with its +// terrain, crates, water, devices and runes, as in 0.1.x; the escaped wave templates route the leaked enemies (one helper: +// escaped_single, entering at the middle gate (9,10); two helpers: escaped_multi, entering at col 18 through (9,10)); +// every helper's pieces stand on their prep tiles, the first of two shifted 8 columns onto the right half ("率先迎敌(即位于右侧 +// 阵地)"). 0.2.0 fought it on the escaped levels' own map, an empty road (GitHub #41 item 3, docs/history/0.2.0.md +// §25.6.4) — withdrawn; data/stages.json keeps those two records (kind 'unite'), which no field uses. Every test here +// fails on 0.2.0 (the field was act1autochess_escaped_single / _multi there). import { test } from 'node:test'; import assert from 'node:assert/strict'; import { GEO, PHASE } from '../../shared/constants.js'; import { Battle } from '../../server/sim/Battle.js'; import { createBattleFromSpec } from '../../server/sim/spec.js'; import { DataSource } from '../../server/sim/simdata.js'; -import { uniteStageId } from '../../server/match/unite.js'; import { FakeBattle } from './fakeBattle.js'; import { DATA, makeMatch, give, chessOfTier, legalTileFor, checkInvariants } from './harness.js'; @@ -19,18 +20,23 @@ class NoFinish extends Battle { constructor(o) { super({ ...o, autoFinish: false }); } } +const CRATE = 'trap_1105_accrate'; +/** 战场#01's active 阻隔工事 on the 联防 rows: three on the left half (#001–#003), three on the right half (#101–#103). */ +const M01_CRATES = [[10, 5], [10, 6], [10, 13], [10, 14], [11, 5], [11, 13]]; + /** - * Co-op on 战场#01 (its row 9 is fenced off at cols 5–7: "##Err###rrSrr###rrS##"): p_0 leaks 3 enemies, the other - * players are perfect — 1 helper with 2 humans, 2 helpers with 3. Each helper fields one ranged operator in its corner. + * Co-op on `stageId` (战场#01's row 9 is walled off at cols 5–7: "##Err###rrSrr###rrS##"): p_0 leaks 3 enemies, the other + * players are perfect — 1 helper with 2 humans, 2 helpers with 3. Each helper fields one ranged operator on the first + * legal tile in reading order (战场#01: the 围墙 (12,3)). */ -function scenario({ humans, clientCombat }) { +function scenario({ humans, clientCombat, stageId = 'act1autochess_m01', data = DATA, seed = 4101 + humans }) { const h = makeMatch({ - mode: 'coop', humans, seed: 4101 + humans, fake: true, clientCombat, + mode: 'coop', humans, seed, fake: true, clientCombat, data, script: (b) => (b.kind === 'normal' ? { leaks: { p_0: 3 } } : {}), }).start(); const m = h.m; h.toPrep(1); - h.setStage('act1autochess_m01'); + h.setStage(stageId); const ranged = chessOfTier(1, (c) => c.position === 'RANGED').filter((x) => m.pool.has(x)); const helpers = []; for (let i = 1; i < humans; i++) { @@ -52,78 +58,112 @@ function uniteField(m, clientCombat) { return { opts: u.opts, battle: new NoFinish({ ...u.opts, data: m.ds, logger: { warn() {}, error() {}, info() {}, debug() {} } }) }; } -for (const clientCombat of [true, false]) { - test(`联防 with 1 helper (${clientCombat ? 'client-side combat' : 'server-run'}): escaped_single, not the round's stage — enemies enter at col 10 and walk row 9`, () => { +const tileOf = (ps, piece) => [...ps.board.entries()].find(([, p]) => p === piece)[0].split(',').map(Number); +const startOf = (r) => r.start ?? [r.startPosition?.row, r.startPosition?.col]; +/** The crates standing on the field (device units, sorted by tile). */ +const crates = (b) => b.allyUnits.filter((u) => u.kind === 'device' && u.defId === CRATE && u.alive).map((u) => [u.tileR, u.tileC]) + .sort((a, b2) => a[0] - b2[0] || a[1] - b2[1]); +const MODES = [[true, 'client-side combat'], [false, 'server-run']]; + +for (const [clientCombat, label] of MODES) { + test(`联防 with 1 helper (${label}): the round's battlefield — 战场#01's walls and crates on both halves, the helper on its 围墙, walkers from the middle gate go round the walls`, () => { const { m, helpers } = scenario({ humans: 2, clientCombat }); assert.deepEqual(m.unitePlan.helpers.map((p) => p.playerId), ['p_1']); const { opts, battle: b } = uniteField(m, clientCombat); - assert.equal(opts.stageId, 'act1autochess_escaped_single'); - assert.equal(uniteStageId(m.gd, 1), 'act1autochess_escaped_single'); - assert.deepEqual(opts.rect, GEO.UNITE_RECT, 'the whole 19×21 map\'s field rows (both halves are road on it)'); - assert.equal(b.stage.id, 'act1autochess_escaped_single'); - assert.deepEqual(b.stage.devices, [], 'no crates or devices of 战场#01'); - // the map: row 9 is road from the gate (9,10) to the objective (9,2); on 战场#01 cols 5–7 are fenced off - for (let c = 3; c <= 9; c++) assert.ok(b.grid.groundPassable(9, c), `(9,${c}) is road`); - assert.equal(m.stage.rows[9].slice(5, 8), '###', '战场#01 itself has no ground there'); - for (const c of [19, 20]) assert.ok(!b.grid.groundPassable(9, c), `(9,${c}) is no ground`); - // the routes of escaped_single: every one starts at col 10 - assert.ok(opts.routes.every((r) => (r.start ?? [r.startPosition?.row, r.startPosition?.col])[1] === 10)); - // the helper's piece stands on its prep tile - const { ps, piece } = helpers[0]; - const [r, c] = [...ps.board.entries()].find(([, p]) => p === piece)[0].split(',').map(Number); + assert.equal(opts.stageId, 'act1autochess_m01', 'the match stage (0.2.0: act1autochess_escaped_single)'); + assert.equal(opts.stageId, m.stageId); + assert.deepEqual(opts.rect, GEO.UNITE_RECT, 'both halves of the stage'); + assert.ok(opts.routes.every((r) => startOf(r)[1] === 10), 'escaped_single\'s routes enter at the middle gate (col 10)'); + assert.equal(b.stage.id, 'act1autochess_m01'); + assert.deepEqual(b.stage.rows, m.stage.rows, 'tile for tile the stage the boards stand on'); + for (const c of [5, 6, 7]) assert.ok(!b.grid.groundPassable(9, c), `(9,${c}) is 战场#01's wall, no road`); b.step(); + assert.deepEqual(crates(b), M01_CRATES, 'the 阻隔工事 of both halves stand (0.2.0: none)'); + // the helper's piece on its prep tile — the same 围墙 it stood on in prep + const { ps, piece } = helpers[0]; + const [r, c] = tileOf(ps, piece); + assert.deepEqual([r, c], [12, 3]); const u = b.allyUnits.find((x) => x.uid === piece.uid && x.ownerId === 'p_1'); assert.deepEqual([u.tileR, u.tileC], [r, c]); - // a walker crosses the tiles 战场#01 fences off - const crossed = new Set(); + assert.equal(b.grid.tile(r, c).key, 'tile_fence_bound', 'its tile is 战场#01\'s 围墙 (0.2.0: road)'); + // walkers go round 战场#01's walls: never on row 9 cols 5–7, through the top row instead + const seen = new Set(); while (b.time < 60 && !b.finished) { b.step(); - for (const e of b.enemies) if (e.alive && e.motion !== 'FLY') crossed.add(`${Math.round(e.y)},${Math.round(e.x)}`); + for (const e of b.enemies) if (e.alive && e.motion !== 'FLY') seen.add(`${Math.round(e.y)},${Math.round(e.x)}`); } - assert.ok(['9,5', '9,6', '9,7'].some((k) => crossed.has(k)), `a walker on row 9 cols 5–7 (${[...crossed].sort().join(' ')})`); + assert.ok(!['9,5', '9,6', '9,7'].some((k) => seen.has(k)), `no walker on the wall (${[...seen].sort().join(' ')})`); + assert.ok([4, 5, 6, 7].some((cc) => seen.has(`12,${cc}`)), `a walker on the top row (${[...seen].sort().join(' ')})`); assert.equal(b.errorCount || 0, 0); checkInvariants(m); m.dispose(); }); + + test(`联防 with 2 helpers (${label}): the round's battlefield — the first helper on the right half (+8 columns) on the same 围墙 as in prep, crates on both halves, every viewer gets the match stage`, () => { + const { h, m, helpers } = scenario({ humans: 3, clientCombat }); + const order = m.unitePlan.helpers.map((p) => p.playerId); + assert.equal(order.length, 2); + const { opts, battle: b } = uniteField(m, clientCombat); + assert.equal(opts.stageId, 'act1autochess_m01', 'the match stage (0.2.0: act1autochess_escaped_multi)'); + assert.deepEqual(opts.players.map((p) => [p.playerId, p.colOffset]), [[order[0], 8], [order[1], 0]]); + assert.ok(opts.routes.every((r) => startOf(r)[1] === 18), 'escaped_multi\'s routes enter at col 18'); + assert.ok(opts.routes.filter((r) => r.motion === 'WALK').every((r) => r.checkpoints.some(([rr, cc]) => rr === 9 && cc === 10)), 'walkers pass (9,10)'); + assert.deepEqual(b.stage.rows, m.stage.rows); + b.step(); + assert.deepEqual(crates(b), M01_CRATES, 'the 阻隔工事 of both halves stand (0.2.0: none)'); + for (const { ps, piece } of helpers) { + const [r, c] = tileOf(ps, piece); + const u = b.allyUnits.find((x) => x.uid === piece.uid && x.ownerId === ps.playerId); + const off = ps.playerId === order[0] ? 8 : 0; + assert.deepEqual([u.tileR, u.tileC], [r, c + off], `${ps.playerId}: its prep tile${off ? ' on the right half' : ''}`); + // the stage's right half is its left half + 8 columns: the shifted helper stands on the tile kind it prepared on + assert.equal(b.grid.tile(r, c + off).key, m.stage.tiles[m.stage.rows[r][c]].tileKey, `${ps.playerId}: the prep tile's kind`); + assert.equal(b.grid.tile(r, c + off).key, 'tile_fence_bound', `${ps.playerId}: 战场#01's 围墙 (0.2.0: road)`); + } + // what the other browsers are given: the 联防 field names the match stage, the one their boards are drawn on + if (clientCombat) { + const start = h.lastTo('p_0', 'b.start'); + assert.equal(start && start.spec && start.spec.stageId, m.stageId, 'the leaker simulates the 联防 spec on the match stage'); + } else { + m.handle('p_0', { t: 'g.watch', fieldId: 'u' }); + const meta = h.lastTo('p_0', 'm.field'); + assert.equal(meta && meta.stageId, m.stageId, 'a watcher\'s m.field names the match stage'); + } + assert.equal(m.publicView().stageId, 'act1autochess_m01'); + checkInvariants(m); + m.dispose(); + }); } -test('联防 with 2 helpers: escaped_multi — the first helper on the right half (col + 8), enemies enter at col 18 through (9,10); the client draws the field\'s map', () => { - const { h, m, helpers } = scenario({ humans: 3, clientCombat: false }); - const order = m.unitePlan.helpers.map((p) => p.playerId); - assert.equal(order.length, 2); +test('联防 on 战场#04: the round\'s special terrain is on the field — walkers pick up the 活性源石 on their way round the walls', () => { + const { m } = scenario({ humans: 2, clientCombat: false, stageId: 'act1autochess_m04' }); const { opts, battle: b } = uniteField(m, false); - assert.equal(opts.stageId, 'act1autochess_escaped_multi'); - assert.equal(b.stage.id, 'act1autochess_escaped_multi'); - assert.deepEqual(opts.players.map((p) => [p.playerId, p.colOffset]), [[order[0], 8], [order[1], 0]]); - assert.ok(opts.routes.every((r) => r.start[1] === 18), 'every route enters at col 18'); - assert.ok(opts.routes.filter((r) => r.motion === 'WALK').every((r) => r.checkpoints.some(([rr, cc]) => rr === 9 && cc === 10)), 'walkers pass (9,10)'); - b.step(); - for (const { ps, piece } of helpers) { - const [r, c] = [...ps.board.entries()].find(([, p]) => p === piece)[0].split(',').map(Number); - const u = b.allyUnits.find((x) => x.uid === piece.uid && x.ownerId === ps.playerId); - const off = ps.playerId === order[0] ? 8 : 0; - assert.deepEqual([u.tileR, u.tileC], [r, c + off], `${ps.playerId}: its prep tile${off ? ' on the right half' : ''}`); - assert.equal(b.stage.rows[r][c + off], 'r', 'a road tile of the 联防 map'); + assert.equal(opts.stageId, 'act1autochess_m04'); + assert.ok(b.stage.special && b.stage.special.infection, 'the stage\'s 活性源石 parameters (0.2.0: none)'); + const infection = []; + for (let r = GEO.UNITE_RECT.r0; r <= GEO.UNITE_RECT.r1; r++) for (let c = GEO.UNITE_RECT.c0; c <= GEO.UNITE_RECT.c1; c++) if (b.grid.tile(r, c).terrain === 'infection') infection.push([r, c]); + assert.deepEqual(infection, [[10, 6], [10, 14], [11, 6], [11, 14]], '战场#04\'s 活性源石 tiles on both halves'); + let burnt = null; + while (b.time < 60 && !b.finished && !burnt) { + b.step(); + burnt = b.enemies.find((e) => e.alive && e.findBuff && e.findBuff('terrain:infection')) || null; } - // what a watching browser receives: the m.field of the 联防 carries the map it is drawn on - m.handle('p_0', { t: 'g.watch', fieldId: 'u' }); - const meta = h.lastTo('p_0', 'm.field'); - assert.equal(meta && meta.stageId, 'act1autochess_escaped_multi'); - assert.equal(m.stageId, 'act1autochess_m01', 'the match stage (m.public stageId, the boards) stays the round\'s'); - checkInvariants(m); + assert.ok(burnt, 'a walker carries the 活性源石 effect'); + assert.equal(b.errorCount || 0, 0); m.dispose(); }); -test('degraded data without the 联防 maps: the field keeps the round\'s stage', () => { +test('the 联防 field does not depend on the escaped levels\' map records: with or without them it is the same battlefield', () => { const stages = Object.fromEntries(Object.entries(DATA.stages).filter(([, s]) => s.kind !== 'unite')); - const h = makeMatch({ mode: 'coop', humans: 2, seed: 4199, fake: true, data: { ...DATA, stages }, script: (b) => (b.kind === 'normal' ? { leaks: { p_0: 2 } } : {}) }).start(); - const m = h.m; - h.toPrep(1); - assert.equal(uniteStageId(m.gd, 1), null); - const ps = h.ps('p_1'); - const id = chessOfTier(1, (c) => c.position === 'RANGED').find((x) => m.pool.has(x)); - give(m, ps, id, 'board', legalTileFor(m, ps, id)); - h.drive(() => m.phase === PHASE.UNITE); - assert.equal(FakeBattle.instances.find((b) => b.kind === 'unite').opts.stageId, m.stageId); - m.dispose(); + const run = (data) => { + const { m } = scenario({ humans: 2, clientCombat: false, data, seed: 4199 }); + const { opts } = uniteField(m, false); + const out = { stageId: opts.stageId, routes: opts.routes, offsets: opts.players.map((p) => [p.playerId, p.colOffset]) }; + m.dispose(); + return out; + }; + const full = run(DATA); + const degraded = run({ ...DATA, stages }); + assert.equal(full.stageId, 'act1autochess_m01'); + assert.deepEqual(full, degraded, 'the same stage, routes and helper offsets'); }); diff --git a/test/match/playtest5-deploy.test.js b/test/match/playtest5-deploy.test.js index a96e174a..b81a1132 100644 --- a/test/match/playtest5-deploy.test.js +++ b/test/match/playtest5-deploy.test.js @@ -225,7 +225,7 @@ describe('#7 audit: every stage × deploy field (server = official level data = // buildableType (PRTS 深水区 地形信息 "地形机制:拒绝部署(待补充)" — player report #3 after 0.1.0, 战场#08's pool) const ROLE = { trap_1105_accrate: 'block', trap_032_mound: 'block', trap_1106_achplat: 'platform', trap_040_canoe: 'water' }; for (const [id, st] of Object.entries(DATA.stages)) { - if (st.kind === 'unite') continue; // the 联防 maps: nobody deploys or prepares on them (no bench, no boss halves) + if (st.kind === 'unite') continue; // the escaped levels' maps: no field is fought on them (no bench, no boss halves) const lv = JSON.parse(readFileSync(levelFile(id), 'utf8')); const map = lv.mapData.map, tiles = lv.mapData.tiles, H = map.length; const dev = new Map(); @@ -262,7 +262,7 @@ describe('#7 audit: every stage × deploy field (server = official level data = test('carry-over: a tile legal on the normal board is legal (same class) on both boss halves of every active stage', () => { for (const [id, st] of Object.entries(DATA.stages)) { - if (st.kind === 'unite') continue; // the 联防 maps have no boss field + if (st.kind === 'unite') continue; // the escaped levels' maps have no boss field const n = buildDeployMap(st); for (const field of ['bossL', 'bossR']) { const b = buildDeployMap(st, { field }); diff --git a/test/render/pen.test.js b/test/render/pen.test.js index 421dfb5e..26f4fd49 100644 --- a/test/render/pen.test.js +++ b/test/render/pen.test.js @@ -12,7 +12,7 @@ import { bossFieldPlacement } from '../../server/match/finalAssault.js'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); const STAGES = JSON.parse(readFileSync(path.join(ROOT, 'data/stages.json'), 'utf8')); -// the battle stages (the two 联防 maps, kind 'unite', have no preview pen: nobody prepares on them) +// the battle stages (the escaped levels' two maps, kind 'unite', have no preview pen: nobody prepares on them) const stageList = Object.values(STAGES.stages || STAGES).filter((s) => s.kind !== 'unite'); const stage = (STAGES.stages || STAGES).act2autochess_m01; diff --git a/test/render/tiles.test.js b/test/render/tiles.test.js index e9a4600e..21de0e7b 100644 --- a/test/render/tiles.test.js +++ b/test/render/tiles.test.js @@ -26,8 +26,8 @@ describe('parseStage', () => { assert.ok(t.alpha > 0 && t.alpha <= 1); } } - // the own board is always drawn and playable; hand slots are raised benches (the two 联防 maps, kind 'unite', have - // no bench: nobody prepares on them) + // the own board is always drawn and playable; hand slots are raised benches (the escaped levels' two maps, kind + // 'unite', have no bench: nobody prepares on them) for (let r = 9; r <= 12; r++) for (let c = 3; c <= 8; c++) assert.ok(g[r][c].drawn, `${id} board ${r},${c}`); for (let c = 0; c < 10; c++) { if (st.kind === 'unite') { assert.equal(g[7][c].glyph, '#', `${id} no hand ${c}`); continue; } diff --git a/test/sim/feedback5-bounty-payee.test.js b/test/sim/feedback5-bounty-payee.test.js index 037f18e1..0ce92bf5 100644 --- a/test/sim/feedback5-bounty-payee.test.js +++ b/test/sim/feedback5-bounty-payee.test.js @@ -5,7 +5,8 @@ // death pays the card's owner when that player fights there — on its own field a 活性源石 death always paid (pinned) — // and in 联防, where the owner is the leaker (unite.js planUnite) and fights elsewhere, the helper whose half it fell on. // Until 0.2.0 that last case fell back to the leaker, who has no per-player entry in the 联防 battle: the coins were -// dropped (until 0.1.3 the 联防 field was the round's map — 战场#04's 活性源石 burned leaked bounty enemies for nothing). +// dropped (in 0.1.x the 联防 field was the round's map, as it is again since 0.2.1, so 战场#04's 活性源石 burned leaked +// bounty enemies for nothing). // Run: node --test test/sim/feedback5-bounty-payee.test.js import { test } from 'node:test'; diff --git a/test/sim/feedback5-ulpia-own-board.test.js b/test/sim/feedback5-ulpia-own-board.test.js index 8f317d62..f3bcfa2d 100644 --- a/test/sim/feedback5-ulpia-own-board.test.js +++ b/test/sim/feedback5-ulpia-own-board.test.js @@ -1,9 +1,10 @@ // test/sim/feedback5-ulpia-own-board.test.js — community report of 2026-10-06 (item 27) 「乌尔比安使用3技能会在联防阶段跳到红门 // 后」. 乌尔比安 S3 必须开辟的通路 throws the anchor up to 6 tiles ahead (the nearest enemy of the line, else the farthest tile) and -// moves there when the tile "可以部署" (PRTS 备注 ③: the landing tile > the tile one beyond > his own tile). On the one-helper 联防 -// map (act1autochess_escaped_single: the helper's pieces on the left half, the enemies out of the middle gate at col 10) the -// right half is no tile his player deploys on, yet an anchor that met no enemy flew there and he moved behind the red gate for -// the whole skill. Now the landing must lie on his own board (Battle.onOwnBoard, the board region mapped onto the field). +// moves there when the tile "可以部署" (PRTS 备注 ③: the landing tile > the tile one beyond > his own tile). In a one-helper 联防 +// (the helper's pieces on the left half, the enemies out of the middle gate at col 10; the fixture here is the escaped level's +// plain road, act1autochess_escaped_single — the 联防 field itself is the round's battlefield since 0.2.1) the right half is no +// tile his player deploys on, yet an anchor that met no enemy flew there and he moved behind the red gate for the whole +// skill. Now the landing must lie on his own board (Battle.onOwnBoard, the board region mapped onto the field). // Run: node --test test/sim/feedback5-ulpia-own-board.test.js import { test } from 'node:test'; diff --git a/tools/build-data.mjs b/tools/build-data.mjs index 6ed26c48..cb1597f7 100644 --- a/tools/build-data.mjs +++ b/tools/build-data.mjs @@ -2768,17 +2768,20 @@ function stageDisplayName(stageId, raw) { } /** - * The 联防 maps' names: the tables and PRTS name neither map, so the label is the remake's own [ASSUMED] (PRTS - * 卫戍协议/帮助 §联防阶段 calls the joined field "一处阵地"); no screen shows it today. + * The escaped levels' map names: the tables and PRTS name neither map, so the label is the remake's own [ASSUMED] (PRTS + * 卫戍协议/帮助 §联防阶段 calls the joined field "一处阵地"); no screen shows it. */ const UNITE_STAGE_NAMES = { 1: '联防阵地(1名玩家)', 2: '联防阵地(2名玩家)' }; /** * Build data/stages.json: 19×21 terrain grids (row 0 = bottom), legend, devices, special terrain - * parameters, deployable tiles and helper ground paths — the 11 battle stages of stageDatasDict, then the two 联防 - * maps: act2autochess constData escapedBattleTemplateMapSinglePlayer / MultiPlayer name the level of the 联防 battle - * (level_act1autochess_escaped_single / _multi), whose own map is the 联防 field — a road, not the round's stage (GitHub - * #41). They carry `kind: 'unite'` and `helpers` (1 / 2), weight 0, no modes, and are never drawn as a match stage. + * parameters, deployable tiles and helper ground paths — the 11 battle stages of stageDatasDict, then the maps of the + * two escaped levels: act2autochess constData escapedBattleTemplateMapSinglePlayer / MultiPlayer name the level of the + * 联防 wave (level_act1autochess_escaped_single / _multi), whose own map is an empty road — the placeholder grid every + * wave template level carries (01…07, h01…h08, tr…: tile for tile the same). They carry `kind: 'unite'` + * and `helpers` (1 / 2), weight 0, no modes, and are never a match stage. 0.2.0 fought the 联防 battle on them (GitHub + * #41); since 0.2.1 the 联防 field is the round's battlefield again (server/match/unite.js, the owner's decision of + * 2026-10-07), and the two records stay as the official level data — sim tests use them as a plain two-halves road. */ function buildStages(ctx, modesById) { const researchStages = ctx.research.maps?.stages || {}; @@ -3826,7 +3829,7 @@ function validateAll(f) { if (s.rows.some((r) => r.includes('?'))) err(`stage ${s.id}: unknown tile glyph`); if (s.name !== s.id && /[A-Za-z]/.test(s.name)) err(`stage ${s.id}: Latin text in player-facing name "${s.name}"`); } - // the 联防 field of 1 / 2 helpers is the map of its template (server/match/unite.js uniteStageId) + // the escaped level of 1 / 2 helpers (config.unite.templates) has its map record (kind 'unite'; no field uses it) for (const [n, id] of Object.entries(config.unite.templates)) { if (stages[id]?.kind !== 'unite' || stages[id].helpers !== Number(n) || stages[id].active) err(`unite template ${id}: no inactive 联防 stage for ${n} helper(s)`); } diff --git a/tools/golden.mjs b/tools/golden.mjs index 95ddf30b..1677d731 100644 --- a/tools/golden.mjs +++ b/tools/golden.mjs @@ -22,9 +22,8 @@ // turn; bonds from the board (bondsMeta.computeBonds) with 0–120 layers // bonds 46 battles: every bond at its activation threshold (1 layer) and at its top tier (999 layers), 8 operators // fields 22 battles: every Final Assault / Hidden Core leader on its pair and its solo template (a shared boss pool, -// ended at 200 game s) and 联防 fields (1 and 2 helpers on the 联防 map of their count — boards laid out on a -// battle stage —, carried HP / SP, a knocked-out operator, two leakers' enemies with a summoned-only kind and -// a bounty) +// ended at 200 game s) and 联防 fields (1 and 2 helpers on a battle stage, both halves, carried HP / SP, a +// knocked-out operator, two leakers' enemies with a summoned-only kind and a bounty) // matches 18 matches run to the end in virtual time with the match's default bot rehearsal: 16 bot-only (solo 标准 / // 险境 / 绝境 / 终极 ×2 seeds, co-op 2 / 3 / 4, one server-run combat match, two with LP and layers raised at the // first prep so they reach the Hidden Core), one co-op match whose human seat (AI 托管, offline: its @@ -69,7 +68,6 @@ import { buildBattleSpec, createBattleFromSpec } from '../server/sim/spec.js'; import { createRng, deriveSeed } from '../server/sim/rng.js'; import { GameData } from '../server/match/gamedata.js'; import { setupMatchWaves, buildNormalWave, buildBossWave, buildUniteWave, isFlyKey, routeByMotion } from '../server/match/waves.js'; -import { uniteStageId } from '../server/match/unite.js'; import { buildDeployMap, positionClass, canPlace, ownerRangeKeys, tileKey } from '../server/match/board.js'; import { computeBonds, bondSnapshot } from '../server/match/bondsMeta.js'; import { Match } from '../server/match/Match.js'; @@ -246,7 +244,7 @@ const ENEMY_COLS = Object.freeze(['spawned', 'killed', 'leaked', 'dmg', 'taken'] const CHESS = Object.values(data.chess).sort((a, b) => byId(a.chessId, b.chessId)); const VISIBLE = CHESS.filter((c) => c.visible); -// the battle stages (the two 联防 maps, stages.json kind 'unite', are the 联防 fields' own: server/match/unite.js uniteStageId) +// the battle stages (data/stages.json also keeps the escaped levels' two maps, kind 'unite', which no field uses) const STAGES = Object.keys(data.stages).filter((id) => data.stages[id].kind !== 'unite').sort(byId); const BANDS = Object.keys(data.bands).sort(byId); const EQUIPS = Object.values(data.items).filter((i) => i.itemType === 'EQUIP').map((i) => i.id).sort(byId); @@ -607,9 +605,8 @@ export function fieldScenarios() { const modeId = helpers === 1 ? 'mode_multi_normal' : 'mode_multi_hard'; const gd = gdFor(modeId); const round = helpers === 1 ? 7 : 11; - // the helpers' boards are laid out on the round's stage; the 联防 battle runs on its own map (GitHub #41) - const boardStageId = STAGES[(i * 5) % STAGES.length]; - const stageId = uniteStageId(gd, helpers) ?? boardStageId; + // the 联防 battle runs on the round's stage, the helpers' boards on its two halves (0.2.0's escaped-level map withdrawn) + const stageId = STAGES[(i * 5) % STAGES.length]; const seed = deriveSeed(20261005, `unite:${helpers}`); const src = normalWave(gd, round, seed); const leakers = helpers === 1 ? ['p2', 'p3'] : ['p3', 'p4']; @@ -624,7 +621,7 @@ export function fieldScenarios() { let uid = 1; const players = []; for (let h = 0; h < helpers; h++) { - const t = team(gd, boardStageId, 'normal', uid, 8, helpers > 1 && h === 0 ? 8 : 0); + const t = team(gd, stageId, 'normal', uid, 8, helpers > 1 && h === 0 ? 8 : 0); uid = t.nextUid; // carried end state of the helper's own battle: HP ratio + SP, one knocked out, summons with SP only t.units.forEach((u, k) => {