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