import { membershipAdd, membershipClear, membershipHas, createMembershipSet, type MembershipSet } from '../../mx/tools/MembershipSet'; /** 读表(与 ConfigTableData 方法对齐,避免循环引用) */ export interface TableRowSource { /** 表全部行 */ getTableList(tableName: string): T[]; /** 按 id 取一行 */ getTableData(tableName: string, id: number): T | null; } /** 索敌已实现的 atk_type 头 */ const ATK_LOGIC: readonly number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; /** Skill.skill 额外效果已落地的头 */ const EXTRA_SKILL: readonly number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; /** Weapon.type(1通用 2~6 金木水火土) */ const WEAPON_TYPE: readonly number[] = [1, 2, 3, 4, 5, 6]; /** * 配表与实现契约对不上时的统一告警。 * 只打印,不改数值、不补默认、不假装成功。 */ export class TableAnomaly { /** 策划检索前缀 */ static readonly PREFIX = '【配表异常】'; private static _seen: MembershipSet = createMembershipSet(); /** * 打印配表异常(同 key 只打一次)。 * @param table 表名 * @param id 行 id * @param field 字段 * @param reason 原因 * @param extra 现场值 */ static warn(table: string, id: number | string, field: string, reason: string, extra?: unknown): void { const key = `${table}\t${id}\t${field}\t${reason}`; if (membershipHas(this._seen, key)) return; membershipAdd(this._seen, key); if (extra !== undefined) { console.warn(TableAnomaly.PREFIX, table, 'id=', id, field, reason, extra); return; } console.warn(TableAnomaly.PREFIX, table, 'id=', id, field, reason); } /** 清空去重(热更后重新扫) */ static reset(): void { membershipClear(this._seen); } /** * 扫描已加载表:引用断裂、类型不在实现集、主动 cd≤0 等。 * @param src 配表源 */ static scan(src: TableRowSource): void { this.reset(); this._scanSkill(src); this._scanHero(src); this._scanWeapon(src); } private static _scanSkill(src: TableRowSource): void { const rows = src.getTableList('Skill'); for (let i = 0; i < rows.length; i++) { const row = rows[i]; if (!row) continue; const id = Number(row.id) || 0; const typ = Math.floor(Number(row.type) || 0); if (typ !== 1 && typ !== 2) { this.warn('Skill', id, 'type', '不是主动1/被动2', row.type); } if (typ === 1 && !(Number(row.cd) > 0)) { this.warn('Skill', id, 'cd', '主动技能 cd≤0,实现不会编造间隔', row.cd); } const atkHead = this._csvHead(row.atk_type); const atkRaw = String(row.atk_type ?? '').trim(); if (atkRaw && !this._in(ATK_LOGIC, atkHead)) { this.warn('Skill', id, 'atk_type', '头类型无索敌实现', atkRaw); } const extraHead = this._csvHead(row.skill); if (String(row.skill ?? '').trim() && extraHead > 0 && !this._in(EXTRA_SKILL, extraHead)) { this.warn('Skill', id, 'skill', '额外效果头未实现', row.skill); } if (typ === 1 && !atkRaw && extraHead <= 0) { this.warn('Skill', id, 'atk_type', '主动技能无 atk_type 且无 skill 额外效果'); } } } private static _scanHero(src: TableRowSource): void { const rows = src.getTableList('Hero'); for (let i = 0; i < rows.length; i++) { const row = rows[i]; if (!row) continue; const id = Number(row.id) || 0; const ht = Math.floor(Number(row.herotype) || 0); if (ht < 1 || ht > 5) { this.warn('Hero', id, 'herotype', '不是金木水火土 1~5', row.herotype); } this._checkSkillIds(src, 'Hero', id, 'atk_id', row.atk_id); const skillId = Math.floor(Number(row.skill_id) || 0); if (skillId > 0 && !src.getTableData('Skill', skillId)) { this.warn('Hero', id, 'skill_id', 'Skill 行不存在', skillId); } } } private static _scanWeapon(src: TableRowSource): void { const rows = src.getTableList('Weapon'); for (let i = 0; i < rows.length; i++) { const row = rows[i]; if (!row) continue; const id = Number(row.id) || 0; const typ = Math.floor(Number(row.type) || 0); if (!this._in(WEAPON_TYPE, typ)) { this.warn('Weapon', id, 'type', '不是通用1/金2/木3/水4/火5/土6', row.type); } const skillId = Number(row.act_skill) || 0; if (skillId > 0 && !src.getTableData('Skill', skillId)) { this.warn('Weapon', id, 'act_skill', 'Skill 行不存在', skillId); } } } private static _checkSkillIds(src: TableRowSource, table: string, id: number, field: string, ids: readonly number[] | undefined): void { if (!ids) return; for (let i = 0; i < ids.length; i++) { const sid = Number(ids[i]) || 0; if (sid <= 0) continue; if (!src.getTableData('Skill', sid)) { this.warn(table, id, field, `第${i}项 Skill 行不存在`, sid); } } } private static _checkWeaponRef(src: TableRowSource, table: string, id: number, field: string, weaponId: number): void { if (weaponId <= 0) return; if (!src.getTableData('Weapon', weaponId)) { this.warn(table, id, field, 'Weapon 行不存在', weaponId); } } private static _csvHead(raw: unknown): number { const s = String(raw ?? '').trim(); if (!s) return 0; return Math.floor(Number(s.split(',')[0]) || 0); } private static _in(list: readonly number[], n: number): boolean { for (let i = 0; i < list.length; i++) { if (list[i] === n) return true; } return false; } }