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.
163 lines
6.0 KiB
163 lines
6.0 KiB
import { TableNames } from '../../../game/ConfigTableData';
|
|
import { GEM_SLOT_COUNT } from '../../../game/GameDataEnums';
|
|
import { RankDataManager } from '../../../manager/RankDataManager';
|
|
import { aiLog, aiOp, aiWarn } from './AiLog';
|
|
import type { AiGemSocketStore } from './AiGemSocketStore';
|
|
|
|
/** Rank.aiweapon 六档:无 / 绿 / 蓝 / 紫 / 橙 / 红 */
|
|
export const AI_GEM_WEIGHT_COUNT = 6;
|
|
/** 五行数量 */
|
|
const ELEMENT_COUNT = 5;
|
|
|
|
/**
|
|
* 开战按对手段位 `Rank.aiweapon` 一次性镶嵌 AI 宝石。
|
|
* 20 槽各自独立掷骰:是否镶嵌 + 镶嵌何物;遵守「每五行最多 1 颗专属」。
|
|
*/
|
|
export class AiGemSocketAssigner {
|
|
/**
|
|
* 灌满 store。
|
|
* @param store 局内存档
|
|
* @param opponentRankId 对手段位
|
|
* @param seed 匹配种子
|
|
*/
|
|
fill(store: AiGemSocketStore, opponentRankId: number, seed: number): void {
|
|
store.clear();
|
|
const rank = RankDataManager.getRankConfig(opponentRankId);
|
|
if (!rank) {
|
|
aiWarn(`[AiGem] Rank 缺失 rankId=${opponentRankId},本局 AI 不镶嵌`);
|
|
return;
|
|
}
|
|
const weights = this._parseWeights(rank.aiweapon);
|
|
if (!weights) {
|
|
aiWarn(`[AiGem] aiweapon 无效 rankId=${opponentRankId} raw=${rank.aiweapon}`);
|
|
return;
|
|
}
|
|
const baseSeed = (Math.floor(Number(seed) || 0) >>> 0) || 1;
|
|
let filled = 0;
|
|
for (let element = 1; element <= ELEMENT_COUNT; element++) {
|
|
let hasExclusive = false;
|
|
for (let slot = 0; slot < GEM_SLOT_COUNT; slot++) {
|
|
const salt = element * 100 + slot;
|
|
const band = this._rollBand(weights, baseSeed, salt);
|
|
if (band == null) {
|
|
aiWarn(`[AiGem] 掷骰失败 element=${element} slot=${slot}`);
|
|
continue;
|
|
}
|
|
if (band === 0) {
|
|
store.setSlot(element, slot, 0);
|
|
continue;
|
|
}
|
|
const quality = band;
|
|
const gemId = this._pickGemId(element, quality, hasExclusive, baseSeed, salt ^ 0x9e3779b9);
|
|
if (gemId <= 0) {
|
|
store.setSlot(element, slot, 0);
|
|
continue;
|
|
}
|
|
const row = gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, gemId);
|
|
const gemType = Math.floor(Number(row?.type) || 0);
|
|
if (gemType >= 2 && gemType <= 6) hasExclusive = true;
|
|
store.setSlot(element, slot, gemId);
|
|
filled += 1;
|
|
}
|
|
}
|
|
aiOp(
|
|
`[AiGem] 开战镶嵌完成 rank=${opponentRankId} filled=${filled}/20 weights=${rank.aiweapon}`,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 解析六档权重。
|
|
* @param raw Rank.aiweapon
|
|
*/
|
|
private _parseWeights(raw: string): number[] | null {
|
|
const parts = String(raw ?? '').split(',');
|
|
if (parts.length !== AI_GEM_WEIGHT_COUNT) {
|
|
aiLog('[AiGem] aiweapon 长度须为 6', raw);
|
|
return null;
|
|
}
|
|
const out: number[] = [];
|
|
let total = 0;
|
|
for (let i = 0; i < AI_GEM_WEIGHT_COUNT; i++) {
|
|
const v = Math.max(0, Math.floor(Number(parts[i]) || 0));
|
|
out.push(v);
|
|
total += v;
|
|
}
|
|
if (total <= 0) {
|
|
aiLog('[AiGem] aiweapon 权重和为 0', raw);
|
|
return null;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* 加权抽档:0=空,1~5=品质绿~红。
|
|
* @param weights 六档
|
|
* @param seed 局种子
|
|
* @param salt 槽盐
|
|
*/
|
|
private _rollBand(weights: number[], seed: number, salt: number): number | null {
|
|
let total = 0;
|
|
for (let i = 0; i < weights.length; i++) total += weights[i];
|
|
if (total <= 0) return null;
|
|
let roll = this._unit(seed, salt) * total;
|
|
for (let i = 0; i < weights.length; i++) {
|
|
roll -= weights[i];
|
|
if (roll < 0) return i;
|
|
}
|
|
return AI_GEM_WEIGHT_COUNT - 1;
|
|
}
|
|
|
|
/**
|
|
* 在合法候选中抽一颗(通用 + 未占用专属)。
|
|
* @param element 五行
|
|
* @param quality Weapon.quality 1~5
|
|
* @param hasExclusive 本五行是否已有专属
|
|
* @param seed 局种子
|
|
* @param salt 抽选盐
|
|
*/
|
|
private _pickGemId(element: number, quality: number, hasExclusive: boolean, seed: number, salt: number): number {
|
|
const q = Math.floor(Number(quality) || 0);
|
|
if (q < 1 || q > 5) {
|
|
aiWarn(`[AiGem] 品质越界 q=${q} element=${element}`);
|
|
return 0;
|
|
}
|
|
const needExclusive = element + 1;
|
|
const list = gg.data.table.getTableList<ITableWeapon>(TableNames.Weapon) ?? [];
|
|
const candidates: number[] = [];
|
|
for (let i = 0; i < list.length; i++) {
|
|
const row = list[i];
|
|
const id = Math.floor(Number(row?.id) || 0);
|
|
if (id <= 0) continue;
|
|
if (Math.floor(Number(row.quality) || 0) !== q) continue;
|
|
const t = Math.floor(Number(row.type) || 0);
|
|
if (t === 1) {
|
|
candidates.push(id);
|
|
continue;
|
|
}
|
|
if (t === needExclusive && !hasExclusive) {
|
|
candidates.push(id);
|
|
}
|
|
}
|
|
if (candidates.length <= 0) {
|
|
aiWarn(`[AiGem] 无候选宝石 element=${element} quality=${q} exclusiveUsed=${hasExclusive}`);
|
|
return 0;
|
|
}
|
|
candidates.sort((a, b) => a - b);
|
|
const idx = Math.floor(this._unit(seed, salt) * candidates.length);
|
|
const pick = candidates[Math.min(candidates.length - 1, Math.max(0, idx))];
|
|
return pick;
|
|
}
|
|
|
|
/**
|
|
* 种子均匀 [0,1)。
|
|
* @param seed 局种子
|
|
* @param salt 扰动
|
|
*/
|
|
private _unit(seed: number, salt: number): number {
|
|
let x = ((Math.floor(Number(seed) || 0) ^ Math.floor(Number(salt) || 0)) >>> 0) || 1;
|
|
x ^= x << 13;
|
|
x ^= x >>> 17;
|
|
x ^= x << 5;
|
|
return (x >>> 0) / 4294967296;
|
|
}
|
|
}
|
|
|