feedback5 CB3: 乌尔比安's S3 moves only onto a tile of his own board — no longer behind the red gate of the one-helper 联防 map nor onto a boss field's other half (community report of 2026-10-06, item 27)
Official: PRTS 乌尔比安 S3 备注 ③ — the 【移动】 takes the anchor tile if it can be deployed on, else the tile one beyond, else
his own tile. Cause: "deployable" was any standable tile of the battle rect; UNITE_RECT holds both halves of the 联防 map, and
on escaped_single (the lone helper on the left half, the enemies out of the middle gate at col 10) an anchor that met no
enemy flew on to the right half, where no enemy walks, and he stood there for the whole skill. Now the landing must also
lie on his player's board: Battle.onOwnBoard(ps, r, c), the board region GEO.FIELD mapped onto the field (the hunk is
byte-identical to CB4's 219ab31, which uses it for the 突袭 landing, so the two merge cleanly).
Golden: 1 scenario — fields hidden-boss_9-solo: his first cast from (2,5) landed on (2,11), the solo boss field's right
half (checked instrumented); he now stays and the fight differs from there. Tests: test/sim/feedback5-ulpia-own-board.test.js
(4; the one-helper and the two-helper 联防 cases fail on the parent); test/sim/facing-invariance.test.js — its open field
has no boards, so its setup counts every tile as the player's own (the own-board rule is a board-position rule, like the
deployment order the scenario already avoids).
This commit is contained in:
1 parent
212a073af6
commit
9b164fdc66
5 files changed
+126
-30
No files matched your search
@@ -3,6 +3,7 @@
|
||||
// Installed on Battle.prototype by server/sim/Battle.js (a method container: never instantiated; `this` is the battle).
|
||||
|
||||
import { COLS, BOSS_ROW_OFFSET } from '../constants.js';
|
||||
import { GEO } from '../../../shared/constants.js';
|
||||
import { Unit } from '../units.js';
|
||||
import { normDir, mirrorDir } from '../dir.js';
|
||||
import { SkillRuntime } from '../skills.js';
|
||||
@@ -70,6 +71,29 @@ export class BattlePlayers {
|
||||
/** Board → field tile for a player (see header). */
|
||||
mapTile(ps, row, col, abs = false) {
|
||||
if (abs || ps.coords === 'field') return [row, col];
|
||||
return this._boardTile(ps, row, col);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether field tile (r, c) lies on player `ps`'s own board: the board region GEO.FIELD (rows 9–12, cols 2–10, the
|
||||
* tiles its pieces are deployed on) mapped onto this field — the 联防 right-hand helper's +8 columns, the boss field's
|
||||
* rows 2–5, the mirrored right half of a pair (whatever coordinates the player's input uses). Never the hand / 临时
|
||||
* 整备区 rows of a boss field (rows 0–1, inside BOSS_RECT) nor the other half of a 联防 or boss field. Automatic
|
||||
* redeployments that must stay where the player could deploy read it (the 突袭 landing, bonds/addon/battle.js raidTile).
|
||||
*/
|
||||
onOwnBoard(ps, r, c) {
|
||||
if (!ps) return false;
|
||||
let keys = ps._boardKeys;
|
||||
if (!keys) {
|
||||
keys = ps._boardKeys = new Set();
|
||||
for (let br = GEO.FIELD.r0; br <= GEO.FIELD.r1; br++) {
|
||||
for (let bc = GEO.FIELD.c0; bc <= GEO.FIELD.c1; bc++) { const [fr, fc] = this._boardTile(ps, br, bc); keys.add(fr * COLS + fc); }
|
||||
}
|
||||
}
|
||||
return keys.has(r * COLS + c);
|
||||
}
|
||||
|
||||
_boardTile(ps, row, col) {
|
||||
const bossLike = this.kind === 'boss' || this.kind === 'hidden';
|
||||
let r = row;
|
||||
if (ps.rowOffset != null) r = row + ps.rowOffset;
|
||||
|
||||
@@ -13,7 +13,8 @@ export default {
|
||||
// ---------------------------------------------------------------------------------------------------------------
|
||||
// 乌尔比安 — S3 必须开辟的通路 (25 s, CUSTOM_RANGE row ahead): max HP/ATK +, throws an anchor forward that stops on the
|
||||
// first enemy or at max distance — on his own tile while he blocks: 135 % ATK phys + 6 s stun around it
|
||||
// (projectile_range); moves onto the anchor tile when deployable and not his own (a 从不混淆的方向 marker keeps his tile)
|
||||
// (projectile_range); moves onto the anchor tile when deployable (a tile of his own board) and not his own (a 从不混淆的方向
|
||||
// marker keeps his tile)
|
||||
// and returns at skill end — both 【移动】, i.e. free redeploys (Battle.moveRedeploy; the return empties his SP).
|
||||
// T1 本性的坚守: heal 100 (160 below 50 %) on every hit taken. T2 血脉的哺养: per kill +120 max HP / +30 ATK (×9),
|
||||
// other Abyssal Hunters +50 %. Module (elite): healing received ×1.2.
|
||||
@@ -90,8 +91,12 @@ export default {
|
||||
// anchor on his own tile leaves him where he stands, no marker, nothing to return from [ASSUMED: the "tile one
|
||||
// beyond the landing" is not tried when the landing is his own tile]
|
||||
if (stop === 0 || !unit.alive) return;
|
||||
// PRTS 备注: landing tile > the tile one beyond it > his own tile (a deployable, free, unreserved melee tile)
|
||||
const ok = ([r, c]) => (r !== unit.tileR || c !== unit.tileC) && battle.grid.inRect(r, c) && battle.grid.canStand(r, c) && !battle.grid.isObstacle(r, c) && !battle.isReservedTile(r, c);
|
||||
// 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. 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
|
||||
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;
|
||||
const home = [unit.tileR, unit.tileC];
|
||||
|
||||
+25
-26
@@ -1190,38 +1190,37 @@
|
||||
"killed": 3,
|
||||
"total": 3,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": 1870565,
|
||||
"rngDraws": 124,
|
||||
"bossHpLeft": 1903562,
|
||||
"rngDraws": 67,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {"p1":{"killed":3,"total":3,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":2969443,"bossDmg":929435,"heal":76069,"deaths":5,"layerGains":{}}},
|
||||
"players": {"p1":{"killed":3,"total":3,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":2843148,"bossDmg":896438,"heal":73550,"deaths":3,"layerGains":{}}},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_08_b",2,"uniequip_002_horn",2775,1,1,0,377438,9366,0,127,4],
|
||||
["p1",2,"op","chess_char_5_11_b",1,"uniequip_002_demkni",4794.8,1,3,0,832,3417,28568,44,33],
|
||||
["p1",3,"op","chess_char_5_05_b",2,"uniequip_002_ulpia",10629,1,36,0,414083,36990,19701,39,5],
|
||||
["p1",4,"op","chess_char_5_06_b",2,"uniequip_002_etlchi",2567.95,1,7,1,1490203,17115,16047,201,5],
|
||||
["p1",5,"op","chess_char_5_13_b",1,"uniequip_002_ghost2",3364.8,1,9,1,118420,12547,3364,79,3],
|
||||
["p1",6,"op","chess_char_5_07_b",2,"uniequip_002_surtr",2524.94,1,39,0,15153,19432,3930,10,2],
|
||||
["p1",7,"op","chess_char_5_02_b",2,"uniequip_002_titi",1439,1,5,0,0,0,0,0,0],
|
||||
["p1",8,"op","chess_char_5_03_b",2,"uniequip_002_blaze2",968.1,1,6,0,289738,1471,967,189,6],
|
||||
["p1",9,"op","chess_char_5_10_b",2,"uniequip_002_lisa",1480,1,35,0,19621,10891,3491,28,6],
|
||||
["p1",10,"op","chess_char_5_12_b",0,"uniequip_002_dusk",1787,1,2,1,243954,0,0,53,41],
|
||||
["p1","~1","token","token_10039_ulpia_block",null,null,3485,0,17,0,0,15,0,0,null],
|
||||
["p1","~2","token","token_10039_ulpia_block",null,null,3350,0,24,0,0,150,0,0,null],
|
||||
["p1","~3","token","token_10039_ulpia_block",null,null,3335,0,31,0,0,165,0,0,null]
|
||||
["p1",1,"op","chess_char_5_08_b",2,"uniequip_002_horn",2698.4,1,1,0,324837,9374,0,127,4],
|
||||
["p1",2,"op","chess_char_5_11_b",1,"uniequip_002_demkni",4804.8,1,3,0,848,2750,31550,44,34],
|
||||
["p1",3,"op","chess_char_5_05_b",2,"uniequip_002_ulpia",6503.23,1,31,0,255230,37295,14964,28,4],
|
||||
["p1",4,"op","chess_char_5_06_b",2,"uniequip_002_etlchi",3543.16,1,7,2,1502942,13705,13608,201,5],
|
||||
["p1",5,"op","chess_char_5_13_b",1,"uniequip_002_ghost2",3364.8,1,9,1,107792,11970,2438,65,2],
|
||||
["p1",6,"op","chess_char_5_07_b",2,"uniequip_002_surtr",238.16,1,25,0,8629,21618,5871,17,2],
|
||||
["p1",7,"op","chess_char_5_02_b",2,"uniequip_002_titi",1439,1,5,0,6082,0,2705,17,1],
|
||||
["p1",8,"op","chess_char_5_03_b",2,"uniequip_002_blaze2",351.58,1,6,0,364837,2591,1471,188,6],
|
||||
["p1",9,"op","chess_char_5_10_b",2,"uniequip_002_lisa",1480,1,24,0,15428,5383,943,40,5],
|
||||
["p1",10,"op","chess_char_5_12_b",0,"uniequip_002_dusk",1777,1,2,0,256524,80,0,53,41],
|
||||
["p1","~1","token","token_10039_ulpia_block",null,null,3350,0,20,0,0,150,0,0,null],
|
||||
["p1","~2","token","token_10039_ulpia_block",null,null,3335,0,28,0,0,165,0,0,null]
|
||||
],
|
||||
"enemies": {
|
||||
"enemy_10067_ftsjc": [1,1,0,1161,21565],
|
||||
"enemy_5601_entlec": [7,6,0,0,174538],
|
||||
"enemy_9007_acelem": [2,2,0,747,119439],
|
||||
"enemy_9017_achunt_2": [1,0,0,33211,929435],
|
||||
"enemy_9018_actrpa": [1,0,0,16277,517062],
|
||||
"enemy_9019_actrpb": [1,0,0,18530,477352],
|
||||
"enemy_9020_actrpc": [1,0,0,15909,730051]
|
||||
"enemy_5601_entlec": [6,6,0,0,166866],
|
||||
"enemy_9007_acelem": [2,2,0,1054,119439],
|
||||
"enemy_9017_achunt_2": [1,0,0,28371,896438],
|
||||
"enemy_9018_actrpa": [1,0,0,14779,508719],
|
||||
"enemy_9019_actrpb": [1,0,0,19659,453678],
|
||||
"enemy_9020_actrpc": [1,0,0,17888,676442]
|
||||
},
|
||||
"hooks": [1,40,6000,876,876,3547,8777,6397,14,19,105,102,118,25207,1875,10,14,0,1,109,0,2,0],
|
||||
"events": {"atk":1409,"deploy":26,"die":19,"dmg":8204,"engage":9,"fx":850,"heal":383,"skill":207,"spawn":27,"status":303},
|
||||
"fx": {"anchor":5,"aoe":2,"beam":20,"burst":2,"candle":7,"charge":7,"dash":12,"downed":1,"ember":2,"explode":1,"healAoe":33,"link":597,"overload":4,"palsy":4,"phase":12,"revive":1,"shieldBreak":10,"splash":118,"substitute":3,"swap":3,"teleport":6},
|
||||
"snaps": "37ac48f3 092a4bc2 41b7e5d4 56d6fd20 79e6de1e 5c8059ed c19e7138 677e05fa cbd43f62 8f0f65b8 4d76189a 297c0633 3bc4d3c3 9ff783eb 79b3ec5e 5c6f5ce5 a8d3399b 8de60a18 1eaa3427 f2260143"
|
||||
"hooks": [1,33,6000,894,894,3465,8457,4295,12,15,104,100,124,25057,1653,12,13,0,1,78,0,2,0],
|
||||
"events": {"atk":1389,"deploy":20,"die":15,"dmg":7934,"engage":10,"fx":855,"heal":375,"skill":204,"spawn":25,"status":337},
|
||||
"fx": {"anchor":4,"aoe":2,"beam":24,"burst":2,"candle":6,"charge":7,"dash":12,"downed":1,"ember":1,"explode":1,"healAoe":34,"link":597,"overload":4,"palsy":2,"phase":12,"revive":1,"shieldBreak":11,"sleepGuard":1,"splash":124,"substitute":2,"swap":2,"teleport":4,"wake":1},
|
||||
"snaps": "37ac48f3 f657c2f6 e25b0f01 5ea69d76 c3a7469d 9558529b 0fd83c8e 2c035602 b69af21f c2a0eb54 b31f2ad7 8b64e08b bbbd8e09 0dca9aae ce7aae50 b0cd3d8c 35382dd2 d3b4e413 db5e9c68 41e7d434"
|
||||
},
|
||||
"hidden-boss_10-pair": {
|
||||
"about": "假想敌:管 pair pool 15200000: chess_char_5_19_b chess_char_4_01_b chess_char_5_14_b chess_char_5_17_b chess_char_5_15_b chess_char_5_20_b chess_char_5_21_b chess_char_5_22_b chess_char_5_23_b chess_char_4_02_b | chess_char_4_11_b chess_char_4_04_b chess_char_4_07_b chess_char_4_16_b chess_char_4_09_b chess_char_4_06_b chess_char_4_10_b chess_char_4_12_b chess_char_4_13_b chess_char_4_14_b token_10041_cathy_catsld token_10041_cathy_catsld",
|
||||
|
||||
@@ -48,7 +48,12 @@ function scenario(chessId, dir, secs) {
|
||||
e_fly: enemyRec({ key: 'e_fly', hp: 1e6, speed: 0, motion: 'FLY', def: 50, res: 30 }),
|
||||
},
|
||||
},
|
||||
setup: (b) => b.on('deploy', ({ unit }) => { if (unit.uid >= 100) unit.hp = unit.s.maxHp * (0.3 + 0.1 * (unit.uid - 100)); }),
|
||||
setup: (b) => {
|
||||
b.on('deploy', ({ unit }) => { if (unit.uid >= 100) unit.hp = unit.s.maxHp * (0.3 + 0.1 * (unit.uid - 100)); });
|
||||
// the open field has no boards: "a tile of the player's own board" (Battle.onOwnBoard — 乌尔比安 S3's landing) is a
|
||||
// board-position rule (DESIGN), not a facing one — every tile counts here, so the anchor's landing turns with him
|
||||
b.onOwnBoard = () => true;
|
||||
},
|
||||
});
|
||||
h.run(secs);
|
||||
checkInvariants(h.b);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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).
|
||||
// Run: node --test test/sim/feedback5-ulpia-own-board.test.js
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { makeBattle, enemyRec, checkInvariants } from '../helpers/battleHarness.js';
|
||||
|
||||
const ULPIA = 'chess_char_5_05_a';
|
||||
const dummy = enemyRec({ key: 'enemy_anchor_dummy', hp: 1e9, speed: 0, mass: 0, atk: 0 });
|
||||
|
||||
/** 乌尔比安 cast S3 on `tile` of `kind`'s field; `enemy` = a parked enemy's tile or null. Returns where he stands after the cast. */
|
||||
function cast({ kind = 'unite', stageId = 'act1autochess_escaped_single', tile = [10, 7], enemy = null, colOffset = 0 } = {}) {
|
||||
const h = makeBattle({
|
||||
kind, stageId, seed: 5, autoFinish: false, timeLimit: 120, defs: { enemies: { enemy_anchor_dummy: dummy } },
|
||||
players: [{ playerId: 'p1', seat: 0, side: 'L', colOffset, units: [{ uid: 1, kind: 'chess', chessId: ULPIA, row: tile[0], col: tile[1] }], bonds: {} }],
|
||||
});
|
||||
h.step();
|
||||
const u = h.unit(ULPIA);
|
||||
const home = [u.tileR, u.tileC];
|
||||
if (enemy) { h.spawn('enemy_anchor_dummy', { pos: enemy }); h.step(); } // (one tick: the enemy joins the tile index)
|
||||
u.skill.gainSp(999, 'test');
|
||||
// (its CUSTOM_RANGE trigger waits for an enemy on the line: with none the cast is made by hand, as a player's would be)
|
||||
if (!u.skill.active) assert.ok(u.skill.activate('test'), 'S3 cast');
|
||||
h.run(0.2);
|
||||
const at = [u.tileR, u.tileC];
|
||||
checkInvariants(h.b);
|
||||
return { h, u, home, at };
|
||||
}
|
||||
|
||||
test('one-helper 联防: an anchor that meets no enemy flies on past the middle gate — he stays on his own half (until 0.2.0 he moved behind the red gate)', () => {
|
||||
const { h, u, home, at } = cast({ tile: [10, 7] });
|
||||
assert.deepEqual(home, [10, 7]);
|
||||
assert.ok(h.b.rect.c1 >= 13, 'the 联防 field spans both halves');
|
||||
assert.deepEqual(at, home, `stays on (10,7) — not ${at}`);
|
||||
assert.ok(h.b.onOwnBoard(u.player, 10, 7) && !h.b.onOwnBoard(u.player, 10, 13), 'cols 11+ are not his board');
|
||||
assert.equal(h.b.allyUnits.filter((a) => a.kind === 'token' && a.alive).length, 0, 'no 从不混淆的方向: nothing to return from');
|
||||
});
|
||||
|
||||
test('one-helper 联防: an enemy on his half still draws the anchor and he moves next to it (own board)', () => {
|
||||
const { at } = cast({ tile: [10, 4], enemy: [10, 8] });
|
||||
assert.deepEqual(at, [10, 8], 'he moves onto the anchor tile on his half');
|
||||
});
|
||||
|
||||
test('two-helper 联防: the left helper never lands on the right helper\'s half; the right helper moves on its own half', () => {
|
||||
const left = cast({ stageId: 'act1autochess_escaped_multi', tile: [10, 7] });
|
||||
assert.deepEqual(left.at, left.home, `left helper stays (${left.at})`);
|
||||
// the right-hand helper (colOffset 8): board col 3 → field col 11, enemy 3 tiles ahead on its own half
|
||||
const right = cast({ stageId: 'act1autochess_escaped_multi', tile: [10, 3], colOffset: 8, enemy: [10, 14] });
|
||||
assert.deepEqual(right.home, [10, 11]);
|
||||
assert.deepEqual(right.at, [10, 14], 'moves on its own half');
|
||||
assert.ok(right.h.b.onOwnBoard(right.u.player, 10, 14) && !right.h.b.onOwnBoard(right.u.player, 10, 7));
|
||||
});
|
||||
|
||||
test('a normal battle is unchanged: the anchor stops at the field edge and he moves onto the farthest tile of his board', () => {
|
||||
const { at, h, u } = cast({ kind: 'normal', stageId: 'flat', tile: [10, 3] });
|
||||
assert.deepEqual(at, [10, 9], `6 tiles ahead, the last road tile before the gate column (${at})`);
|
||||
assert.ok(h.b.onOwnBoard(u.player, 10, 9));
|
||||
});
|
||||
Reference in new issue
Block a user