parent
a63e605506
commit
61f71f8f08
@ -0,0 +1,163 @@ |
||||
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; |
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
{ |
||||
"ver": "4.0.24", |
||||
"importer": "typescript", |
||||
"imported": true, |
||||
"uuid": "9d5c95c8-e3be-4a32-8d68-07af92bb5a26", |
||||
"files": [], |
||||
"subMetas": {}, |
||||
"userData": {} |
||||
} |
||||
@ -0,0 +1,66 @@ |
||||
import { GEM_SLOT_COUNT } from '../../../game/GameDataEnums'; |
||||
|
||||
/** 五行数量 */ |
||||
const ELEMENT_COUNT = 5; |
||||
|
||||
/** |
||||
* 本局 AI 宝石镶嵌存档:五行 × 4 槽。 |
||||
* 开战灌入,局内只读,结束清空。 |
||||
*/ |
||||
export class AiGemSocketStore { |
||||
/** element(1~5) → 长度 4 的宝石 id */ |
||||
private readonly _slots: number[][] = []; |
||||
|
||||
constructor() { |
||||
this.clear(); |
||||
} |
||||
|
||||
/** 清空全部槽 */ |
||||
clear(): void { |
||||
this._slots.length = 0; |
||||
for (let e = 0; e <= ELEMENT_COUNT; e++) { |
||||
this._slots[e] = [0, 0, 0, 0]; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 读某五行 4 槽(副本)。 |
||||
* @param element Hero.herotype 1~5 |
||||
*/ |
||||
getElementSlots(element: number): number[] { |
||||
const e = Math.floor(Number(element) || 0); |
||||
const out = [0, 0, 0, 0]; |
||||
if (e < 1 || e > ELEMENT_COUNT) return out; |
||||
const src = this._slots[e]; |
||||
for (let i = 0; i < GEM_SLOT_COUNT; i++) { |
||||
out[i] = Math.max(0, Math.floor(Number(src[i]) || 0)); |
||||
} |
||||
return out; |
||||
} |
||||
|
||||
/** |
||||
* 写入单槽。 |
||||
* @param element 五行 1~5 |
||||
* @param slot 槽 0~3 |
||||
* @param gemId Weapon.id;0=空 |
||||
*/ |
||||
setSlot(element: number, slot: number, gemId: number): boolean { |
||||
const e = Math.floor(Number(element) || 0); |
||||
const s = Math.floor(Number(slot) || 0); |
||||
if (e < 1 || e > ELEMENT_COUNT || s < 0 || s >= GEM_SLOT_COUNT) return false; |
||||
this._slots[e][s] = Math.max(0, Math.floor(Number(gemId) || 0)); |
||||
return true; |
||||
} |
||||
|
||||
/** 已镶嵌槽数(gemId>0) */ |
||||
countFilled(): number { |
||||
let n = 0; |
||||
for (let e = 1; e <= ELEMENT_COUNT; e++) { |
||||
const row = this._slots[e]; |
||||
for (let s = 0; s < GEM_SLOT_COUNT; s++) { |
||||
if (row[s] > 0) n++; |
||||
} |
||||
} |
||||
return n; |
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
{ |
||||
"ver": "4.0.24", |
||||
"importer": "typescript", |
||||
"imported": true, |
||||
"uuid": "987914d6-2b9b-4799-a189-de1f6765bc51", |
||||
"files": [], |
||||
"subMetas": {}, |
||||
"userData": {} |
||||
} |
||||
Loading…
Reference in new issue