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/battle/ai/runtime/AiHeroWeaponAssigner.ts

155 lines
6.5 KiB

3 weeks ago
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;
}
}