You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
AdLike2048/assets/script/game/TableAnomaly.ts

180 lines
7.7 KiB

import { membershipAdd, membershipClear, membershipHas, createMembershipSet, type MembershipSet } from '../../mx/tools/MembershipSet';
/** 读表(与 ConfigTableData 方法对齐,避免循环引用) */
export interface TableRowSource {
/** 表全部行 */
getTableList<T>(tableName: string): T[];
/** 按 id 取一行 */
getTableData<T>(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];
/** Skill.skill 额外效果已落地的头 */
const EXTRA_SKILL: readonly number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9];
/** Weapon.type */
const WEAPON_TYPE: readonly number[] = [1, 2, 3, 4];
/**
* 配表与实现契约对不上时的统一告警。
* 只打印,不改数值、不补默认、不假装成功。
*/
export class TableAnomaly {
/** 策划检索前缀 */
static readonly PREFIX = '【配表异常】';
private static _seen: MembershipSet<string> = 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<ITableSkill>('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<ITableHero>('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 !== 2 && ht !== 3) {
this.warn('Hero', id, 'herotype', '不是基础字1/合成武将2/功能字3', row.herotype);
}
this._checkSkillIds(src, 'Hero', id, 'atk_id', row.atk_id);
this._checkSkillIds(src, 'Hero', id, 'act_skill', row.act_skill);
this._checkWeaponRef(src, 'Hero', id, 'startweapon', Number(row.startweapon) || 0);
this._checkWeaponRef(src, 'Hero', id, 'personalweapon', Number(row.personalweapon) || 0);
const startId = Number(row.startweapon) || 0;
if (ht === 2 && startId > 0) {
const w = src.getTableData<ITableWeapon>('Weapon', startId);
const want = Math.floor(Number(row.weapon) || 0);
if (w && want > 0 && want !== 5 && Math.floor(Number(w.type) || 0) !== want) {
this.warn('Hero', id, 'startweapon', '默认武器 Weapon.type 与 Hero.weapon 不一致', `${startId} type=${w.type} weapon=${want}`);
}
}
const limit = Math.floor(Number(row.level_limit) || 0);
const atkLen = row.atk_id?.length ?? 0;
if (limit > 0 && atkLen > 0 && atkLen < limit) {
this.warn('Hero', id, 'atk_id', '长度小于 level_limit', `len=${atkLen} limit=${limit}`);
}
}
}
private static _scanWeapon(src: TableRowSource): void {
const rows = src.getTableList<ITableWeapon>('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', row.type);
}
const skillId = Number(row.act_skill) || 0;
if (skillId > 0 && !src.getTableData<ITableSkill>('Skill', skillId)) {
this.warn('Weapon', id, 'act_skill', 'Skill 行不存在', skillId);
}
const heroId = Number(row.heroid) || 0;
if (heroId > 0) {
const hero = src.getTableData<ITableHero>('Hero', heroId);
if (!hero) {
this.warn('Weapon', id, 'heroid', 'Hero 行不存在', heroId);
} else if (Math.floor(Number(hero.personalweapon) || 0) !== id) {
this.warn('Weapon', id, 'heroid', 'Hero.personalweapon 不是本武器', `hero=${heroId} personalweapon=${hero.personalweapon}`);
}
}
if (this._in(WEAPON_TYPE, typ) && !String(row.skin ?? '').trim()) {
this.warn('Weapon', id, 'skin', '战斗武器皮肤为空');
}
}
}
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<ITableSkill>('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<ITableWeapon>('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;
}
}