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.
154 lines
6.5 KiB
154 lines
6.5 KiB
import { TableNames } from '../../../game/ConfigTableData';
|
|
import { aiLog, aiOp, aiWarn } from './AiLog';
|
|
import { RankDataManager } from '../../../manager/RankDataManager';
|
|
import type { AiHeroWeaponRecord } from '../types/AiHeroWeaponTypes';
|
|
import { AI_WEAPON_QUALITY_COUNT } from '../types/AiHeroWeaponTypes';
|
|
import type { AiHeroWeaponRegistry } from './AiHeroWeaponRegistry';
|
|
|
|
/**
|
|
* 需求 5.6:Rank.aiweapon 品质加权掷骰 + Hero.weapon 约束下解析 Weapon.id。
|
|
* 各武将独立抽取,结果可重复;已入档的 heroId 直接复用。
|
|
*/
|
|
export class AiHeroWeaponAssigner {
|
|
/**
|
|
* 获取或分配本局 AI 武将武器。
|
|
* @param registry 局内存档
|
|
* @param heroId Hero.id
|
|
* @param opponentRankId 对手段位(读 aiweapon)
|
|
* @param seed 随机种子(子种子含 heroId)
|
|
*/
|
|
resolveOrAssign(registry: AiHeroWeaponRegistry, heroId: number, opponentRankId: number, seed: number): AiHeroWeaponRecord | null {
|
|
const hid = Math.floor(Number(heroId) || 0);
|
|
if (hid <= 0) {
|
|
aiLog('[AiHeroWeaponAssigner] invalid heroId', heroId);
|
|
return null;
|
|
}
|
|
const cached = registry.getWeaponId(hid);
|
|
if (cached != null) {
|
|
return { heroId: hid, weaponId: cached, quality: this._readWeaponQuality(cached) };
|
|
}
|
|
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, hid);
|
|
if (!hero) {
|
|
aiLog('[AiHeroWeaponAssigner] Hero missing', hid);
|
|
return null;
|
|
}
|
|
if (Number(hero.herotype) !== 2) {
|
|
aiWarn(`[AiHeroWeapon] skip: herotype!=2 heroId=${hid} type=${hero.herotype}`);
|
|
return null;
|
|
}
|
|
const rank = RankDataManager.getRankConfig(opponentRankId);
|
|
if (!rank) {
|
|
aiWarn(`[AiHeroWeapon] skip: Rank missing rankId=${opponentRankId} heroId=${hid}`);
|
|
return null;
|
|
}
|
|
const weights = this._parseAiWeaponWeights(rank.aiweapon);
|
|
const quality = this._rollQuality(weights, seed, hid);
|
|
if (quality == null) {
|
|
aiWarn(`[AiHeroWeapon] skip: aiweapon roll failed heroId=${hid} weights=${rank.aiweapon}`);
|
|
return null;
|
|
}
|
|
const weaponId = this._pickWeaponId(hero, quality);
|
|
if (weaponId <= 0) {
|
|
aiWarn(`[AiHeroWeapon] skip: no weapon heroId=${hid} quality=${quality}`);
|
|
return null;
|
|
}
|
|
const record: AiHeroWeaponRecord = { heroId: hid, weaponId, quality };
|
|
registry.put(record);
|
|
const weaponRow = gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, weaponId);
|
|
const weaponName = (weaponRow?.name ?? '').trim() || `id=${weaponId}`;
|
|
const heroName = (hero.name ?? '').trim() || `id=${hid}`;
|
|
const qLabel = this._qualityLabel(quality);
|
|
// 首次分配始终打印,便于核对 aiweapon + Hero.weapon
|
|
aiOp(
|
|
`[AiHeroWeapon] assign hero=${heroName}(${hid}) weaponType=${Math.floor(Number(hero.weapon) || 0)}`
|
|
+ ` -> ${weaponName}(${weaponId}) quality=${qLabel}(${quality}) rank=${opponentRankId}`,
|
|
);
|
|
return record;
|
|
}
|
|
|
|
/** 解析 Rank.aiweapon 五档权重 */
|
|
private _parseAiWeaponWeights(raw: string): number[] {
|
|
const parts = (raw ?? '').split(',');
|
|
const out: number[] = [];
|
|
for (let i = 0; i < AI_WEAPON_QUALITY_COUNT; i++) {
|
|
out.push(Math.max(0, Math.floor(Number(parts[i]) || 0)));
|
|
}
|
|
if (parts.length !== AI_WEAPON_QUALITY_COUNT) {
|
|
aiLog('[AiHeroWeaponAssigner] aiweapon length invalid');
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** 加权随机品质 0~4(白/绿/蓝/紫/橙) */
|
|
private _rollQuality(weights: number[], seed: number, heroId: number): number | null {
|
|
let total = 0;
|
|
for (let i = 0; i < weights.length; i++) total += weights[i];
|
|
if (total <= 0) {
|
|
aiLog('[AiHeroWeaponAssigner] aiweapon total weight <= 0');
|
|
return null;
|
|
}
|
|
let roll = this._seededUnit(seed, heroId) * total;
|
|
for (let i = 0; i < weights.length; i++) {
|
|
roll -= weights[i];
|
|
if (roll < 0) return i;
|
|
}
|
|
return AI_WEAPON_QUALITY_COUNT - 1;
|
|
}
|
|
|
|
/** 按 Hero.weapon 与品质选 Weapon.id */
|
|
private _pickWeaponId(hero: ITableHero, quality: number): number {
|
|
const q = Math.floor(Number(quality) || 0);
|
|
const weaponType = Math.floor(Number(hero.weapon) || 0);
|
|
const personal = Math.floor(Number(hero.personalweapon) || 0);
|
|
if (personal > 0) {
|
|
const pe = gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, personal);
|
|
if (pe && Number(pe.type) === weaponType && Number(pe.quality) === q) return personal;
|
|
}
|
|
const list = gg.data.table.getTableList<ITableWeapon>(TableNames.Weapon) ?? [];
|
|
let best = 0;
|
|
for (let i = 0; i < list.length; i++) {
|
|
const row = list[i];
|
|
if (Number(row.type) !== weaponType) continue;
|
|
if (Number(row.quality) !== q) continue;
|
|
const id = Math.floor(Number(row.id) || 0);
|
|
if (id <= 0) continue;
|
|
if (best <= 0 || id < best) best = id;
|
|
}
|
|
if (best > 0) return best;
|
|
const fallback = Math.floor(Number(hero.startweapon) || 0);
|
|
if (fallback <= 0) {
|
|
aiLog('[AiHeroWeaponAssigner] fallback startweapon missing', hero.id, q);
|
|
return 0;
|
|
}
|
|
aiLog('[AiHeroWeaponAssigner] no weapon for quality, fallback startweapon', hero.id, q, fallback);
|
|
return fallback;
|
|
}
|
|
|
|
private _readWeaponQuality(weaponId: number): number {
|
|
const row = gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, weaponId);
|
|
return Math.floor(Number(row?.quality) || 0);
|
|
}
|
|
|
|
/**
|
|
* 品质档可读名(与 Rank.aiweapon 五档一致)。
|
|
* @param quality 0~4
|
|
*/
|
|
private _qualityLabel(quality: number): string {
|
|
const q = Math.floor(Number(quality) || 0);
|
|
if (q === 0) return '白';
|
|
if (q === 1) return '绿';
|
|
if (q === 2) return '蓝';
|
|
if (q === 3) return '紫';
|
|
if (q === 4) return '橙';
|
|
return `q${q}`;
|
|
}
|
|
|
|
/** seed + heroId 子随机 [0,1) */
|
|
private _seededUnit(seed: number, heroId: number): number {
|
|
let x = (Math.floor(Number(seed) || 0) ^ (Math.floor(Number(heroId) || 0) * 2654435761)) >>> 0;
|
|
x ^= x << 13;
|
|
x ^= x >>> 17;
|
|
x ^= x << 5;
|
|
return (x >>> 0) / 4294967296;
|
|
}
|
|
}
|
|
|