|
|
|
|
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, 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枪 3棍 4弓 5燃烧 6增幅) */
|
|
|
|
|
const WEAPON_TYPE: readonly number[] = [1, 2, 3, 4, 5, 6];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 配表与实现契约对不上时的统一告警。
|
|
|
|
|
* 只打印,不改数值、不补默认、不假装成功。
|
|
|
|
|
*/
|
|
|
|
|
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/燃烧5/增幅6', 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;
|
|
|
|
|
}
|
|
|
|
|
}
|