feedback5 R3: Battle.js → server/sim/battle/*.js, a pure move — the 131 methods, verbatim with their comments, go to 14 per-concern method containers (players, lifecycle, hooks, spawns, deploy, blocking, status, combat, queries, summons, tiles, displacement, economy, events; fin / clone in util.js) that Battle.js installs on Battle.prototype in a fixed order with class-method descriptors; Battle.js keeps the header, the constructor and the class, and re-exports pushTiles. docs-consistency reads the method modules (battleText) and pins layerGainRoom in battle/economy.js
This commit is contained in:
1 parent
6b489b1776
commit
13224f9483
17 files changed
+2488
-2328
No files matched your search
@@ -0,0 +1,178 @@
|
||||
// server/sim/battle/players.js — Battle methods: players and units: board → field tiles and directions, ally creation
|
||||
// from the PlayerBattleInput, token defs for an owner's loadout, and the kit / profile / skill setup of a unit.
|
||||
// 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 { Unit } from '../units.js';
|
||||
import { normDir, mirrorDir } from '../dir.js';
|
||||
import { SkillRuntime } from '../skills.js';
|
||||
import { resolveProfile } from '../professions.js';
|
||||
import { normalizeToken } from '../simdata.js';
|
||||
import { setupUnitKit } from '../content/index.js';
|
||||
import { clone } from './util.js';
|
||||
|
||||
export class BattlePlayers {
|
||||
_addPlayer(p) {
|
||||
const bossLike = this.kind === 'boss' || this.kind === 'hidden';
|
||||
const side = p.side === 'R' ? 'R' : 'L';
|
||||
const mirror = bossLike && side === 'R';
|
||||
const ps = {
|
||||
playerId: p.playerId,
|
||||
seat: p.seat ?? this.players.length,
|
||||
side,
|
||||
colOffset: Number(p.colOffset) || 0,
|
||||
rowOffset: p.rowOffset,
|
||||
coords: p.coords ?? 'board',
|
||||
mirror,
|
||||
facing: mirror ? -1 : 1,
|
||||
// default direction of this player's units on this field (the FA right side is mirrored: RIGHT ↔ LEFT)
|
||||
dir: mirror ? 'LEFT' : 'RIGHT',
|
||||
half: mirror || (Number(p.colOffset) || 0) >= 8 ? 'R' : 'L',
|
||||
bonds: clone(p.bonds ?? {}),
|
||||
bandId: p.bandId ?? null,
|
||||
playerEffects: clone(p.playerEffects ?? []),
|
||||
lpForBoss: p.lpForBoss ?? null,
|
||||
dp: this.flags.dpInit,
|
||||
units: [],
|
||||
input: p,
|
||||
};
|
||||
this.players.push(ps);
|
||||
this._perPlayer[ps.playerId] = {
|
||||
killed: 0, total: 0, leaked: [], perfect: true, layerGains: {}, coins: 0,
|
||||
damageDealt: 0, bossDamage: 0, healingDone: 0, deaths: 0, unitsEnd: [], unitStats: [],
|
||||
};
|
||||
const late = [];
|
||||
for (const u of p.units ?? []) {
|
||||
try {
|
||||
const unit = this._createAllyFromInput(ps, u);
|
||||
if (unit && unit.kind === 'token' && !unit.ownerUnit && u.ownerUid != null) late.push([unit, u]);
|
||||
} catch (e) { this._internalError('createUnit', e); }
|
||||
}
|
||||
// a token piece listed before its owner (board order is top→bottom) is linked once every unit exists, and takes
|
||||
// the owner-level variant (elite owners: `_b` stats) of the owner's loadout (DESIGN §16)
|
||||
for (const [t, inp] of late) {
|
||||
const owner = ps.units.find((x) => x.kind === 'op' && x.uid === inp.ownerUid);
|
||||
if (!owner) continue;
|
||||
t.ownerUnit = owner;
|
||||
const def = inp.def ? null : this._tokenDef(inp.tokenId ?? inp.chessId, owner, null);
|
||||
if (!def || def === t.def) continue;
|
||||
const st = def.stats;
|
||||
t.def = def; t.defId = def.id; t.name = def.name; t.rangeGrid = def.rangeGrid;
|
||||
Object.assign(t.base, {
|
||||
maxHp: st.maxHp, atk: st.atk, def: st.def, res: st.res, aspd: st.aspd, bat: st.bat, blockCnt: st.blockCnt, spRecovery: st.spRecovery,
|
||||
tauntLevel: st.tauntLevel, massLevel: st.massLevel, hpRecoveryPerSec: st.hpRecoveryPerSec, cost: st.cost, respawnTime: st.respawnTime,
|
||||
});
|
||||
t.markDirty();
|
||||
t.hp = t.s.maxHp;
|
||||
}
|
||||
}
|
||||
|
||||
/** Board → field tile for a player (see header). */
|
||||
mapTile(ps, row, col, abs = false) {
|
||||
if (abs || ps.coords === 'field') return [row, col];
|
||||
const bossLike = this.kind === 'boss' || this.kind === 'hidden';
|
||||
let r = row;
|
||||
if (ps.rowOffset != null) r = row + ps.rowOffset;
|
||||
else if (bossLike && row >= 7) r = row + BOSS_ROW_OFFSET;
|
||||
const c = ps.mirror ? (col <= 10 ? COLS - 1 - col : col) : col + ps.colOffset;
|
||||
return [r, c];
|
||||
}
|
||||
|
||||
/**
|
||||
* Board direction → field direction for a player (DESIGN §3): as given (default RIGHT) except on the mirrored Final
|
||||
* Assault right side, where RIGHT ↔ LEFT (UP / DOWN unchanged). `abs` / field coordinates are taken as they are.
|
||||
*/
|
||||
mapDir(ps, dir, abs = false) {
|
||||
const d = normDir(dir);
|
||||
return ps && ps.mirror && !abs && ps.coords !== 'field' ? mirrorDir(d) : d;
|
||||
}
|
||||
|
||||
_createAllyFromInput(ps, inp) {
|
||||
// coerce (a string row would otherwise concatenate: "10" + -7 → "10-7"); non-integers never deploy
|
||||
const row = Number(inp.row), col = Number(inp.col);
|
||||
if (!Number.isInteger(row) || !Number.isInteger(col)) { this.log(`bad tile ${inp.row},${inp.col} for ${inp.chessId ?? inp.tokenId}`); return null; }
|
||||
const [r, c] = this.mapTile(ps, row, col, !!inp.abs);
|
||||
const dir = this.mapDir(ps, inp.dir, !!inp.abs);
|
||||
if (inp.kind === 'token') {
|
||||
const owner = inp.ownerUid != null ? ps.units.find((x) => x.uid === inp.ownerUid) : null;
|
||||
const def = this._tokenDef(inp.tokenId ?? inp.chessId, owner, inp.def);
|
||||
if (!def) { this.log(`unknown token ${inp.tokenId}`); return null; }
|
||||
const u = this._makeAlly(ps, def, 'token', r, c, { uid: inp.uid, ownerUnit: owner, dir });
|
||||
u.carry = inp.carryState ?? null;
|
||||
return u;
|
||||
}
|
||||
// the unit's own loadout (DESIGN §16): an entry without loadout fields is the DEFAULT — never another player's
|
||||
// choice for the same chess id in a multi-player field (the per-battle data view maps id-only lookups)
|
||||
const def = this.data.getChess(inp.chessId, { skillIndex: inp.skillIndex ?? null, moduleId: inp.moduleId ?? null });
|
||||
if (!def) { this.log(`unknown chess ${inp.chessId}`); return null; }
|
||||
const u = this._makeAlly(ps, def, 'op', r, c, { uid: inp.uid, dir });
|
||||
u.items = [...(inp.items ?? [])];
|
||||
u.carry = inp.carryState ?? null;
|
||||
return u;
|
||||
}
|
||||
|
||||
/**
|
||||
* Token def: an inline def (PlayerBattleInput `def`, spawnToken `opts.def`) as given, else the data record's variant
|
||||
* for the owner — `owner` = the owning ally unit (its chess and its selected skill / module: getToken(id,
|
||||
* owner.defId, owner.def.loadout), DESIGN §16), a chess id, or null.
|
||||
*/
|
||||
_tokenDef(tokenId, owner, inline) {
|
||||
if (inline) return normalizeToken(tokenId, inline);
|
||||
if (!this.data.getToken) return null;
|
||||
if (owner && typeof owner === 'object') return this.data.getToken(tokenId, owner.defId ?? null, owner.def?.loadout ?? null);
|
||||
return this.data.getToken(tokenId, owner ?? null);
|
||||
}
|
||||
|
||||
_makeAlly(ps, def, kind, r, c, extra = {}) {
|
||||
const st = def.stats;
|
||||
const u = new Unit({
|
||||
id: ++this._idSeq, side: 'ally', kind, def, defId: def.id, name: def.name, ownerId: ps ? ps.playerId : null,
|
||||
uid: extra.uid ?? null, ownerUnit: extra.ownerUnit ?? null, x: c, y: r, tileR: r, tileC: c,
|
||||
dir: extra.dir != null ? normDir(extra.dir) : extra.facing != null ? normDir(extra.facing) : ps ? ps.dir : 'RIGHT',
|
||||
base: {
|
||||
maxHp: st.maxHp, atk: st.atk, def: st.def, res: st.res, aspd: st.aspd, bat: st.bat, blockCnt: st.blockCnt,
|
||||
moveSpeed: 0, spRecovery: st.spRecovery, tauntLevel: st.tauntLevel, massLevel: st.massLevel,
|
||||
hpRecoveryPerSec: st.hpRecoveryPerSec, cost: st.cost, respawnTime: st.respawnTime,
|
||||
},
|
||||
});
|
||||
u.alive = false;
|
||||
u.deployed = false;
|
||||
u.items = [];
|
||||
u.rangeGrid = def.rangeGrid;
|
||||
u.player = ps;
|
||||
this.units.push(u);
|
||||
this.allyUnits.push(u);
|
||||
if (ps) ps.units.push(u);
|
||||
return u;
|
||||
}
|
||||
|
||||
/** Resolve kit/profile/skill for a unit (called by content/index.js; safe to call again). */
|
||||
_setupUnit(u, kit = null) {
|
||||
if (!kit) {
|
||||
try { kit = setupUnitKit(this, u, this.contentMode); } catch (e) { this._internalError('setupUnitKit', e); kit = null; }
|
||||
}
|
||||
u.kit = kit || {};
|
||||
u.profile = resolveProfile(u.def, u.kit.trait || null);
|
||||
if (u.def.untargetable) this.addBuff(u, { key: 'trait:untargetable', flags: { untargetable: true }, persist: true, allowDead: true });
|
||||
// abnormal effects a summon holds (tokens.json `abnormal`, PRTS; user playtest #6 item 18): 禁疗 — no heal reaches it;
|
||||
// 孤立 ("无法被同阵营选中") — no ally heal or ally selection (auras over a range) reaches it
|
||||
const ab = u.def.abnormal;
|
||||
if (Array.isArray(ab) && ab.length) {
|
||||
const flags = {};
|
||||
if (ab.includes('healFree')) flags.noHeal = true;
|
||||
if (ab.includes('isolated')) { flags.isolated = true; flags.noHeal = true; }
|
||||
if (Object.keys(flags).length) this.addBuff(u, { key: 'trait:abnormal', flags, persist: true, allowDead: true });
|
||||
}
|
||||
const spec = u.kit.skill || null;
|
||||
u.skill = new SkillRuntime(this, u, u.def.skill, spec, u.def.skill?.bb ?? {});
|
||||
if (u.profile.install && !u._profInstalled) {
|
||||
u._profInstalled = true;
|
||||
this._safe(() => u.profile.install(this, u), 'profile.install', u);
|
||||
}
|
||||
for (const t of u.kit.talents || []) {
|
||||
if (t && typeof t.install === 'function') this._safe(() => t.install(this, u), 'talent.install', u);
|
||||
}
|
||||
if (typeof u.kit.install === 'function') this._safe(() => u.kit.install(this, u), 'kit.install', u);
|
||||
return u.kit;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user