feedback5 WK: 自选编队 on the server — room.diy { picks } (lenient: illegal picks dropped, only malformed input is BAD_MSG; stored per session / seat; a match takes the picks its seat had at the start, a change during a match is for the next one; welcome lists diyKitted), PlayerState.diy with the player's data view (a slotted DIY slot is its operator for every rule: bonds from its factions, no 特质, the pick's skill and module, the slot's tier / price / merge), a per-player stock per slotted piece (8 at tier 5, 5 at tier 6 [ASSUMED]; never in the shared pool; none when every bond of it is banned), DIY pieces in that player's shop rolls and reward offers once the 调度中心 reaches the slot's level, battleInput diy, m.private.diy / diyBanned, prep scouting and results name the operator, 信标 never sends a 自选 piece to a teammate [ASSUMED]

The owner's decisions of 2026-10-05 (picks, slots, shop per player, bans); research 0.2.0 §2. Golden unchanged (no scenario slots a DIY pick).
Tests: test/match/diy-shop.test.js (9), test/lobby-diy.test.js (4).
This commit is contained in:
sganggs committed 2026-10-05 23:35:01 +08:00
1 parent 742f478526
commit 4bee542e61
26 files changed
+1026 -105

No files matched your search

+9 -5
View File
@@ -37,10 +37,12 @@ export class PlayerAcquire {
*/
acquireChess(chessId, { source = 'grant', toTemp = false, fromPool = true, silent = false } = {}) {
const rec = this.gd.chess(chessId);
if (!rec) return null;
// a DIY slot is only ever this player's own 自选 piece: a slot it has not filled has no body (甄选干员) to gain
if (!rec || (rec.isDiy && !rec.diyFor)) return null;
const base = this.gd.baseIdOf(chessId);
const need = rec.isGolden ? this.gd.goldenCopies : 1;
const taken = fromPool ? this.m.pool.take(base, need) : 0;
// the shared pool, or this player's stock of a slotted 自选 piece (player/diy.js poolOf)
const taken = fromPool ? this.poolOf(base).take(base, need) : 0;
const piece = this.newPiece('chess', chessId, { poolCopies: taken });
this.round.gainedChess++;
let owned = piece;
@@ -121,7 +123,7 @@ export class PlayerAcquire {
// equipment, which would be lost in temp — a summon stack removed there comes back at the next round start)
if (where === 'board') this.grantTokensFor(elite);
if (!where) {
this.m.pool.give(baseId, copies);
this.poolOf(baseId).give(baseId, copies);
this.m.toast(this, 'warn', '整备区已满,晋升的精锐干员无法放入');
this.m.log.warn?.(`[match ${this.m.roomCode}] ${this.playerId}: merge result dropped (hand+temp full)`);
this.recompute();
@@ -142,7 +144,7 @@ export class PlayerAcquire {
if (!goldenId) return false;
const base = this.gd.baseIdOf(piece.id);
const extra = Math.max(0, this.gd.goldenCopies - (piece.poolCopies || 0));
piece.poolCopies = (piece.poolCopies || 0) + this.m.pool.take(base, extra);
piece.poolCopies = (piece.poolCopies || 0) + this.poolOf(base).take(base, extra);
piece.id = goldenId;
this.recompute();
return true;
@@ -205,6 +207,8 @@ export class PlayerAcquire {
* copy-weighted roll from the shared pool excluding the ones already drawn; a tier left without another chess tops
* up from the tier below (user playtest #6 item 19: the official promotion reward never offers one operator twice —
* the user's first-hand report; the normal shop's slots may repeat). The offer reserves no copies (the pick takes one).
* The player's slotted 自选 pieces join the draw like in its shop (player/diy.js diyRollEntries: once the 调度中心 is at
* the slot's level) — the reward is a temporary refresh of the shop ("临时刷新3名…干员") [ASSUMED].
*/
pushRewardOffer(source = 'merge', { tier = null, ids = null, label = null } = {}) {
const ro = this.gd.rewardOffer();
@@ -216,7 +220,7 @@ export class PlayerAcquire {
const fresh = (id) => !list.includes(id);
for (let i = 0; i < ro.count; i++) {
let id = null;
for (let tt = t; tt >= 1 && !id; tt--) id = this.m.pool.roll(this.m.rngShop, { tier: tt, filter: fresh });
for (let tt = t; tt >= 1 && !id; tt--) id = this.m.pool.roll(this.m.rngShop, { tier: tt, filter: fresh, extra: this.diyRollEntries() });
if (id) list.push(id);
}
}
+12 -6
View File
@@ -77,13 +77,16 @@ export class PlayerBasics {
/**
* The skill index / module a chess record fights with under this player's loadout (DESIGN §16) — for a chess this
* player fields as its stand-in (0.2.0 补位) the stand-in's backup selection, whatever the loadout says.
* player fields as its stand-in (0.2.0 补位) the stand-in's backup selection, whatever the loadout says; for a slotted
* 自选 slot (0.2.0, player/diy.js) its pick's skill and module (the composed record's defaults — the loadout never names
* a DIY slot: shared/protocol.js checkLoadout).
*/
loadoutFor(chessRecord) {
if (this.fieldsStandIn(chessRecord)) {
return resolveLoadout(null, this.gd.standIn(chessRecord.chessId), (id) => this.gd.standIn(id) || this.gd.chess(id));
}
return resolveLoadout(this.loadout, chessRecord, (id) => this.gd.chess(id));
const rec = chessRecord && chessRecord.isDiy && typeof chessRecord.chessId === 'string' ? this.gd.chess(chessRecord.chessId) || chessRecord : chessRecord;
return resolveLoadout(this.loadout, rec, (id) => this.gd.chess(id));
}
/**
@@ -119,13 +122,16 @@ export class PlayerBasics {
/**
* The record this player's piece of chess record `rec` fights with (0.2.0 补位): the stand-in record
* (gd.standIn — the chess's identity, the stand-in's body) when the player fields its stand-in, else `rec` itself.
* Rules about the unit's body read it (placement class, summon / bot ranges, the scouting art); rules about the chess
* (price, bonds, 特质, merges, pools) keep reading gd.chess.
* (gd.standIn — the chess's identity, the stand-in's body) when the player fields its stand-in; for a DIY slot record
* the player's 自选 record of that slot (this.gd — a caller holding the match's own record of the slot gets the
* operator); else `rec` itself. Rules about the unit's body read it (placement class, summon / bot ranges, the
* scouting art); rules about the chess (price, bonds, 特质, merges, pools) keep reading gd.chess.
* @param {object|null} rec
*/
fieldRecord(rec) {
return rec && this.fieldsStandIn(rec) ? this.gd.standIn(rec.chessId) || rec : rec;
if (rec && this.fieldsStandIn(rec)) return this.gd.standIn(rec.chessId) || rec;
if (rec && rec.isDiy && !rec.diyFor && typeof rec.chessId === 'string') return this.gd.chess(rec.chessId) || rec;
return rec;
}
/**
+209
View File
@@ -0,0 +1,209 @@
// server/match/player/diy.js — PlayerState methods: 自选编队 (0.2.0 DIY; research 0.2.0 §2, the owner's decisions of
// 2026-10-05) — the human's picks for the four DIY slots and what a slotted operator changes in the player's own state.
// * setDiy: the seat's picks when the match started (seats[].diy, server/lobby.js room.diy), re-checked against this
// match's data and kit registry (shared/protocol.js checkDiyPicks, server/sim/content/kits/index.js KITTED_CHARS) and
// fixed for the match — an out-of-match setting: a change during a match applies to the next one. Bots field no 自选
// piece [ASSUMED]. `diy` = { [slotBaseId]: { charId, skillIndex, uniEquipId } } (frozen; uniEquipId null = none).
// * the player's data view (diyGameData): with picks, `ps.gd` is a view of the match's GameData whose chess(id) is the
// composed 自选 record of a slotted slot, both forms (shared/diy.js diyRecord: the slot's identity — tier, price, sell
// price, the 3 → elite merge, status: E2 Lv1 skill rank 4 / E2 Lv60 rank 7 with the module at stage 1 at tier 5, 3 at
// tier 6 — and the operator's body: name, class, position, stats, range, the pick's skill and module, no 特质, the
// bonds derived from its factions; `diyFor` = the slot, `charId` = the operator). Every rule that reads a record
// through ps.gd — bonds (bondsMeta), the 特质 / meta effects (effectsMeta makeCtx), placement and summon ranges, the
// AI 托管 evaluation, names in toasts and tickers — sees the operator. A player without picks keeps the match's
// GameData itself: nothing changes for anyone else. token(id) also finds the 自选 summons (data/backups.json
// `tokens`) and placeableTokens() reads their variant of the owner form (`<charId>@<statusKey>`, shared/diy.js
// diyTokenOwner), so a placeable 自选 summon comes to the hand like any operator's (PRTS 卫戍协议/帮助 §战斗部署).
// * the per-player stock (initDiyStock, called by the match once its bans are drawn): each slotted DIY piece has copies
// of its own — the tier's pool copies, 8 at tier 5 and 5 at tier 6 [ASSUMED: research 0.2.0 §2.5, the excel has no
// stock field] — drawn by this player only; it never enters the shared pool (PR #71's shared shop was the mistake to
// avoid). A piece all of whose bonds are switched off this match (本局禁用: the drawn bans and the mode's static list)
// gets no stock and so leaves the shop, like a preset chess whose bonds are all off (PRTS 卫戍协议:盟约 "禁用盟约有可能
// 影响自选的支援干员"); 协防 (emptyShip) is never banned, so a prototype always stays. poolOf(baseId) routes every copy
// taken or returned for a slotted slot to that stock (buy, reward picks, merges, promotions, sells, temp, elimination).
// * the shop's draws (diyRollEntries): the stock of each slotted slot joins this player's copy-weighted rolls — the
// shop's chess slots and the reward offers' temporary refreshes [ASSUMED] — weighted like any chess of its tier, once
// the 调度中心 has reached the slot's shopLevel (activity_table shopLevelDisplayDataDict lists the tier-5 slots at
// level 5 and the tier-6 slots at level 6; PRTS 帮助 "仅在调度中心等级 ≥ 干员所在等阶"). The price is the slot's (any
// chess of its tier: 4). Effects that grant a random operator from the shared pool never draw it.
// Installed on PlayerState.prototype by server/match/PlayerState.js (a method container: never instantiated; `this` is
// the player state).
import { KITTED_CHARS } from '../../sim/content/kits/index.js';
import { checkDiyPicks } from '../../../shared/protocol.js';
import { diyRecord, diySlot, diyTokenOwner } from '../../../shared/diy.js';
const posIntOr = (v, d) => (Number.isInteger(v) && v > 0 ? v : d);
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
/**
* The copies of a player's slotted DIY pieces — the shared pool's copy interface (has / cap / left / take / give) for
* those base ids only. `entries`: baseId → { cap, left, tier, shopLevel }.
*/
export class DiyStock {
constructor() {
/** @type {Map<string, { cap: number, left: number, tier: number, shopLevel: number }>} */
this.entries = new Map();
}
has(baseId) { return this.entries.has(baseId); }
cap(baseId) { return this.entries.get(baseId)?.cap ?? 0; }
left(baseId) { return this.entries.get(baseId)?.left ?? 0; }
/** Take up to n copies; returns the number taken (0 for a base id without stock). */
take(baseId, n = 1) {
const e = this.entries.get(baseId);
if (!e || !(n > 0)) return 0;
const k = Math.min(e.left, Math.floor(n));
e.left -= k;
return k;
}
/** Return n copies (clamped at the cap); returns the number returned. */
give(baseId, n = 1) {
const e = this.entries.get(baseId);
if (!e || !(n > 0)) return 0;
const k = Math.min(e.cap - e.left, Math.floor(n));
e.left += k;
return k;
}
/** { baseId: left } (tests / diagnostics). */
snapshot() {
const o = {};
for (const [id, e] of this.entries) o[id] = e.left;
return o;
}
}
/**
* The player's view of the match's GameData (see the header): `records` maps the ids of the slotted slots (normal and
* elite) to their composed 自选 records; every other lookup is the match's own.
* @param {import('../gamedata.js').GameData} gd
* @param {Map<string, object>} records
* @returns {import('../gamedata.js').GameData}
*/
export function diyGameData(gd, records) {
const view = Object.create(gd);
const backups = isObj(gd.raw && gd.raw.backups) ? gd.raw.backups : null;
const diyTokens = backups && isObj(backups.tokens) ? backups.tokens : {};
const tokenOf = (id) => (typeof id === 'string' && Object.hasOwn(diyTokens, id) && isObj(diyTokens[id]) ? diyTokens[id] : null);
Object.defineProperties(view, {
/** the match's own GameData (the view is per player) */
matchData: { value: gd },
chess: { value: (id) => (typeof id === 'string' && records.has(id) ? records.get(id) : gd.chess(id)) },
token: { value: (id) => gd.token(id) || tokenOf(id) },
/**
* GameData.placeableTokens for a slotted slot: the summons its record lists (the pick's skill and talents) that are
* placeable, by the variant of the owner form (`bySkill[skillIndex]` sources) — the deploy limit as the count.
*/
placeableTokens: {
value: (chessId, loadout = null) => {
const rec = typeof chessId === 'string' && records.has(chessId) ? records.get(chessId) : null;
if (!rec) return gd.placeableTokens(chessId, loadout);
const owner = diyTokenOwner(rec.charId, rec.status);
const out = [];
for (const tid of Array.isArray(rec.tokens) ? rec.tokens : []) {
const t = tokenOf(tid);
if (!t || t.kind !== 'summon' || t.placeable !== true) continue;
const v = isObj(t.variants) ? t.variants[owner] ?? null : null;
if (v) {
const alt = loadout && Number.isInteger(loadout.skillIndex) && v.bySkill ? v.bySkill[loadout.skillIndex] : null;
const src = Array.isArray(alt?.sources) ? alt.sources : Array.isArray(v.sources) ? v.sources : [];
if (!src.includes('talent') && !src.includes('skill')) continue;
}
out.push({ tokenId: tid, count: Math.min(posIntOr(v?.stats?.deployLimit, posIntOr(t.deployLimit, 1)), 9) });
}
return out;
},
},
});
return view;
}
export class PlayerDiy {
/**
* Fix the 自选 picks for the match (see the header): the seat's picks re-checked against this match's data and kits;
* a slot whose record cannot be composed is left empty. With picks, `this.gd` becomes the player's data view.
* @param {any} picks `{ [slotBaseId]: { charId, skillIndex?, uniEquipId? } | null }`
* @returns {boolean} false when the picks are malformed (nothing changes) or the player is a bot
*/
setDiy(picks) {
if (this.isBot) return false;
const gd = this.m.gd;
const data = gd.raw;
const res = checkDiyPicks(picks, { data, kitted: KITTED_CHARS });
if (!res || !('ok' in res)) {
this.m.log?.warn?.(`[match ${this.m.roomCode}] 自选 picks of ${this.playerId} ignored: ${res && res.detail}`);
return false;
}
const kept = {};
const records = new Map();
for (const [slotId, pick] of Object.entries(res.picks)) {
const slot = diySlot(slotId, data);
let normal;
let golden;
try {
normal = diyRecord(slotId, pick, { elite: false, data });
golden = diyRecord(slotId, pick, { elite: true, data });
} catch { normal = null; golden = null; }
if (!slot || !normal || !golden) continue;
records.set(slot.baseId, Object.freeze(normal));
records.set(slot.goldenId, Object.freeze(golden));
kept[slot.baseId] = Object.freeze({ charId: pick.charId, skillIndex: pick.skillIndex, uniEquipId: pick.uniEquipId ?? null });
}
this.diy = Object.freeze(kept);
this._diyRecords = records;
this.gd = records.size ? diyGameData(gd, records) : gd;
return true;
}
/**
* The stock of each slotted DIY piece (see the header), once the match has drawn its bans: `off` = the bonds switched
* off this match (drawn + the mode's static list). A piece all of whose bonds are off gets none (`diyBanned`).
* @param {Set<string>} off
*/
initDiyStock(off = new Set()) {
this.diyStock = new DiyStock();
const banned = [];
const gd = this.m.gd;
for (const slotId of Object.keys(this.diy || {})) {
const rec = this._diyRecords.get(slotId);
const slot = diySlot(slotId, gd.raw);
if (!rec || !slot) continue;
const bonds = Array.isArray(rec.bonds) ? rec.bonds : [];
if (bonds.length > 0 && bonds.every((b) => off.has(b))) { banned.push(slotId); continue; }
const cap = gd.poolCopies(slotId);
if (!(cap > 0)) continue;
this.diyStock.entries.set(slotId, { cap, left: cap, tier: gd.tierOf(slotId), shopLevel: slot.shopLevel });
}
this.diyBanned = Object.freeze(banned);
}
/** The copy accounting of base chess `baseId` for this player: its own stock for a slotted DIY slot, else the shared pool. */
poolOf(baseId) {
return this.diyStock && this.diyStock.has(baseId) ? this.diyStock : this.m.pool;
}
/**
* The stock entries this player's shop draws from now ([baseId, entry] with copies left and the 调度中心 at the slot's
* shopLevel), or null (none — the shared pool's roll alone, exactly as without picks).
* @returns {Array<[string, { cap: number, left: number, tier: number, shopLevel: number }]> | null}
*/
diyRollEntries() {
if (!this.diyStock || !this.diyStock.entries.size) return null;
const out = [];
for (const [id, e] of this.diyStock.entries) if (e.left > 0 && this.shop.level >= e.shopLevel) out.push([id, e]);
return out.length ? out : null;
}
/**
* The 自选 pick a chess id (normal or elite) of this player fields — its slot's — or null (not a slotted slot).
* @param {string} id
* @returns {{ charId: string, skillIndex: number, uniEquipId: string|null } | null}
*/
diyPickOf(id) {
const rec = this._diyRecords && typeof id === 'string' ? this._diyRecords.get(id) : null;
return rec ? this.diy[rec.diyFor] ?? null : null;
}
}
+8 -5
View File
@@ -1,6 +1,6 @@
// server/match/player/economy.js — PlayerState methods: the economy — funds, spending (onSpend), prep-side bond layer
// gains (clamped by layerGainRoom), the shop (onPrice prices, copy-weighted rolls from the shared pool, freeze) and its
// intents: g.buy, g.refresh, g.freeze, g.levelUp, g.sell.
// gains (clamped by layerGainRoom), the shop (onPrice prices, copy-weighted rolls from the shared pool plus the player's
// own 自选 stock — player/diy.js —, freeze) and its intents: g.buy, g.refresh, g.freeze, g.levelUp, g.sell.
// Installed on PlayerState.prototype by server/match/PlayerState.js (a method container: never instantiated; `this` is
// the player state).
@@ -61,7 +61,8 @@ export class PlayerEconomy {
}
_rollChessSlot() {
const id = this.m.pool.roll(this.m.rngShop, { maxTier: this.shop.level });
// the slotted 自选 pieces join the draw once the 调度中心 reaches their slot's level (player/diy.js diyRollEntries)
const id = this.m.pool.roll(this.m.rngShop, { maxTier: this.shop.level, extra: this.diyRollEntries() });
return id ? { kind: 'chess', id, basePrice: this.gd.chessPrice(id), frozen: false, sold: false } : null;
}
@@ -115,10 +116,12 @@ export class PlayerEconomy {
let piece;
if (slot.kind === 'chess') {
const rec = this.gd.chess(slot.id);
if (!rec) return fail(ERR.BAD_TARGET);
// a DIY slot sells only as this player's own 自选 piece (a slotted slot's record carries `diyFor`)
if (!rec || (rec.isDiy && !rec.diyFor)) return fail(ERR.BAD_TARGET);
const base = this.gd.baseIdOf(slot.id);
const need = rec.isGolden ? this.gd.goldenCopies : 1;
if (this.m.pool.has(base) && this.m.pool.left(base) < need) return fail(ERR.SOLD_OUT);
const pool = this.poolOf(base);
if (pool.has(base) && pool.left(base) < need) return fail(ERR.SOLD_OUT);
if (handFull) return fail(ERR.HAND_FULL);
this.spend(price);
slot.sold = true;
+6 -4
View File
@@ -1,7 +1,8 @@
// server/match/player/pieces.js — PlayerState methods: piece bookkeeping — new pieces, lookup by uid (board / hand /
// temp / equipped), the chess locations, detach and stow (hand first, overflow temp; a free hand slot pulls a temp piece
// in, _fillHandFromTemp), the per-piece round counters (拉普兰德), pool copies back to the shared pool, and summon stacks
// (removal, lifting, counts, the stack granted for an owner).
// in, _fillHandFromTemp), the per-piece round counters (拉普兰德), pool copies back to the shared pool (a 自选 piece's to
// the player's own stock), and summon stacks (removal, lifting, counts, the stack granted for an owner — a 自选 owner's
// from its pick, player/diy.js placeableTokens).
// Installed on PlayerState.prototype by server/match/PlayerState.js (a method container: never instantiated; `this` is
// the player state).
@@ -132,10 +133,11 @@ export class PlayerPieces {
return v;
}
/** Return a piece's pool copies (and its equipped items are handled by the caller). */
/** Return a piece's pool copies — to the shared pool, or a 自选 piece's to this player's stock (poolOf). */
returnCopies(piece) {
if (piece && piece.kind === 'chess' && piece.poolCopies > 0) {
this.m.pool.give(this.gd.baseIdOf(piece.id), piece.poolCopies);
const base = this.gd.baseIdOf(piece.id);
this.poolOf(base).give(base, piece.poolCopies);
piece.poolCopies = 0;
}
}
+3 -2
View File
@@ -34,10 +34,11 @@ export class PlayerPrep {
if (handFull) return fail(ERR.HAND_FULL);
} else {
const rec = this.gd.chess(slot.id);
if (!rec) return fail(ERR.BAD_TARGET);
if (!rec || (rec.isDiy && !rec.diyFor)) return fail(ERR.BAD_TARGET);
const base = this.gd.baseIdOf(slot.id);
const need = rec.isGolden ? this.gd.goldenCopies : 1;
if (this.m.pool.has(base) && this.m.pool.left(base) < need) return fail(ERR.SOLD_OUT);
const pool = this.poolOf(base); // the shared pool, or this player's 自选 stock
if (pool.has(base) && pool.left(base) < need) return fail(ERR.SOLD_OUT);
if (handFull) return fail(ERR.HAND_FULL);
}
const price = Number.isFinite(slot.price) && slot.price > 0 ? Math.trunc(slot.price) : 0;
+8 -2
View File
@@ -1,7 +1,8 @@
// server/match/player/round.js — PlayerState methods: the round lifecycle the match calls — startRound (income, pending
// coins, the shop, summon stacks topped up), endPrep, eliminate (every copy back to the pool), recompute (legality,
// out-of-range summons, temp pieces into free hand slots, bonds), the bond views — and battleInput (the player's
// PlayerBattleInput: board units with their loadout or their 补位 mark, carried 联防 state, the reached layers).
// PlayerBattleInput: board units with their loadout, their 补位 mark or their 自选 pick, carried 联防 state, the reached
// layers).
// Installed on PlayerState.prototype by server/match/PlayerState.js (a method container: never instantiated; `this` is
// the player state).
@@ -102,7 +103,12 @@ export class PlayerRound {
for (const { r, c, piece } of boardOrder(this.board)) {
if (piece.kind === 'chess') {
const u = { uid: piece.uid, kind: 'chess', chessId: piece.id, row: r, col: c, dir: pieceDir(piece), items: (piece.items || []).map((i) => i.id) };
if (this.fieldsStandIn(piece.id)) {
const pick = this.diyPickOf(piece.id);
if (pick) {
// 0.2.0 自选编队: a slotted DIY slot fights as its pick (sim getChess(id, { diy }): the operator's body, the pick's
// skill and module — no loadout fields, docs/SIM.md §12)
u.diy = { charId: pick.charId, skillIndex: pick.skillIndex, uniEquipId: pick.uniEquipId };
} else if (this.fieldsStandIn(piece.id)) {
// 0.2.0 补位: a chess this player does not own fights as its stand-in (sim getChess(id, { standIn: true }): the
// backup skill / module — no loadout fields, docs/SIM.md §12)
u.standIn = true;
+6
View File
@@ -91,6 +91,12 @@ export class PlayerViews {
// 0.2.0 补位: the base chess ids this player fields as their stand-ins in this match (the not-owned list the seat had
// at the match start; [] = every operator owned) — the client's 「替补:X」 badges, the stand-in's art and card
standIns: this.standIns,
// 0.2.0 自选编队: this player's 自选 picks in this match ({ [slotBaseId]: { charId, skillIndex, uniEquipId } } — the seat's
// when the match started; {} = none) — the client composes the operator's record for its own cards and pieces
// (shared/diy.js diyRecord); `diyBanned`: the slotted slots out of the shop this match (every bond of the operator
// switched off)
diy: this.diy,
diyBanned: this.diyBanned,
stats: {
dmgDealt: Math.round(this.stats.dmgDealt), kills: this.stats.kills, leaks: this.stats.leaks, gold: this.stats.gold,
refreshes: this.stats.refreshes, merges: this.stats.merges,