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/manager/GemDataManager.ts

473 lines
17 KiB

import { Singleton } from '../../mx/tools/Singleton';
import { TableNames } from '../game/ConfigTableData';
import {
GEM_SLOT_COUNT,
IElementGemSave,
IGemBagEntry,
IGemFuseGain,
OtherDataType,
} from '../game/GameDataEnums';
const LOG = '[GemDataManager]';
/** 五行数量 */
const ELEMENT_COUNT = 5;
/**
* 宝石存档与规则:Item 库存 + ElementGem 镶嵌。
* 可用数 = 背包 − 已镶嵌;合成只消耗可用数。
*/
export class GemDataManager extends Singleton {
/** 确保 Weapon / Item 已加载 */
async ensureTable(): Promise<void> {
await Promise.all([
gg.data.table.ensureTableLoaded(TableNames.Weapon),
gg.data.table.ensureTableLoaded(TableNames.Item),
]);
this._stripIllegalWears();
}
/** 读取宝石配表 */
getGem(gemId: number): ITableWeapon | null {
const n = Math.floor(Number(gemId) || 0);
if (n <= 0) return null;
return gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, n) ?? null;
}
/** 全量宝石配表 */
getAllGemList(): ITableWeapon[] {
return gg.data.table.getTableList<ITableWeapon>(TableNames.Weapon) ?? [];
}
/** 宝石道具 id(Weapon.itemid) */
getItemId(gemId: number): number {
return Math.floor(Number(this.getGem(gemId)?.itemid) || 0);
}
/** 背包库存 */
getOwnCount(gemId: number): number {
const itemId = this.getItemId(gemId);
if (itemId <= 0) return 0;
return Math.max(0, gg.data.getItemNum(itemId) | 0);
}
/** 已镶嵌数量 */
getWornCount(gemId: number): number {
const id = Math.floor(Number(gemId) || 0);
if (id <= 0) return 0;
let n = 0;
for (let e = 1; e <= ELEMENT_COUNT; e++) {
const slots = this.getElementSlots(e);
for (let s = 0; s < slots.length; s++) {
if (slots[s] === id) n++;
}
}
return n;
}
/** 可用数 = 库存 − 已镶嵌 */
getFreeCount(gemId: number): number {
return Math.max(0, this.getOwnCount(gemId) - this.getWornCount(gemId));
}
/**
* 仓库列表。
* @param filterType 0 全部;1 通用;2~6 金木水火土
*/
getBagList(filterType: number): IGemBagEntry[] {
const filter = Math.floor(Number(filterType) || 0);
const list: IGemBagEntry[] = [];
const all = this.getAllGemList();
for (let i = 0; i < all.length; i++) {
const conf = all[i];
const gemId = Math.floor(Number(conf?.id) || 0);
if (gemId <= 0) continue;
if (filter > 0 && Math.floor(Number(conf.type) || 0) !== filter) continue;
const free = this.getFreeCount(gemId);
if (free <= 0) continue;
list.push({ gemId, count: free });
}
list.sort((a, b) => this._compareBag(a, b));
return list;
}
/** 五行 4 槽宝石 id(长度固定 4,空为 0) */
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 row = this._loadMap()[e];
if (!row) return out;
for (let i = 0; i < GEM_SLOT_COUNT; i++) {
out[i] = Math.max(0, Math.floor(Number(row[i]) || 0));
}
return out;
}
/** 某槽宝石 id */
getSlotGem(element: number, slot: number): number {
const s = Math.floor(Number(slot) || 0);
if (s < 0 || s >= GEM_SLOT_COUNT) return 0;
return this.getElementSlots(element)[s] || 0;
}
/**
* 能否镶嵌(含库存;同槽替换自身不额外占库存;替换/交换时排除目标槽旧宝石的专属占用)。
*/
canWear(element: number, slot: number, gemId: number): boolean {
const e = Math.floor(Number(element) || 0);
const s = Math.floor(Number(slot) || 0);
const id = Math.floor(Number(gemId) || 0);
if (e < 1 || e > ELEMENT_COUNT || s < 0 || s >= GEM_SLOT_COUNT) return false;
if (id <= 0) return true;
const conf = this.getGem(id);
if (!conf) {
console.warn(LOG, '宝石配表缺失', id);
return false;
}
if (!this._canWearType(e, conf, s)) return false;
const cur = this.getSlotGem(e, s);
if (cur === id) return true;
if (this.getFreeCount(id) <= 0) return false;
return true;
}
/** 镶嵌或清空(gemId=0 卸下) */
wear(element: number, slot: number, gemId: number): boolean {
const e = Math.floor(Number(element) || 0);
const s = Math.floor(Number(slot) || 0);
const id = Math.floor(Number(gemId) || 0);
if (e < 1 || e > ELEMENT_COUNT || s < 0 || s >= GEM_SLOT_COUNT) return false;
if (id > 0 && !this.canWear(e, s, id)) return false;
const map = this._loadMap();
const slots = map[e] ? map[e].slice() : [0, 0, 0, 0];
while (slots.length < GEM_SLOT_COUNT) slots.push(0);
slots[s] = id;
map[e] = slots;
this._saveMap(map);
return true;
}
/** 卸下 */
unequip(element: number, slot: number): boolean {
return this.wear(element, slot, 0);
}
/**
* 仓库宝石装到槽:目标有宝石则卸旧装新(旧回仓库)。
*/
wearFromBag(element: number, slot: number, gemId: number): boolean {
return this.wear(element, slot, gemId);
}
/**
* 两槽互换;目标空则等同移动。
*/
swapSlots(fromElement: number, fromSlot: number, toElement: number, toSlot: number): boolean {
const fe = Math.floor(Number(fromElement) || 0);
const fs = Math.floor(Number(fromSlot) || 0);
const te = Math.floor(Number(toElement) || 0);
const ts = Math.floor(Number(toSlot) || 0);
if (fe < 1 || fe > ELEMENT_COUNT || te < 1 || te > ELEMENT_COUNT) return false;
if (fs < 0 || fs >= GEM_SLOT_COUNT || ts < 0 || ts >= GEM_SLOT_COUNT) return false;
if (fe === te && fs === ts) return false;
const fromId = this.getSlotGem(fe, fs);
if (fromId <= 0) return false;
const toId = this.getSlotGem(te, ts);
const map = this._loadMap();
const a = this._slotsOf(map, fe);
const b = fe === te ? a : this._slotsOf(map, te);
a[fs] = 0;
b[ts] = 0;
map[fe] = a;
map[te] = b;
this._saveMap(map);
const ok = this.canWear(te, ts, fromId) && (toId <= 0 || this.canWear(fe, fs, toId));
if (!ok) {
a[fs] = fromId;
b[ts] = toId;
map[fe] = a;
map[te] = b;
this._saveMap(map);
return false;
}
b[ts] = fromId;
a[fs] = toId;
map[fe] = a;
map[te] = b;
this._saveMap(map);
return true;
}
/** 是否有可用宝石达到合成门槛(free ≥ synthesisnumber 且有目标) */
canFuseAny(): boolean {
const all = this.getAllGemList();
for (let i = 0; i < all.length; i++) {
const conf = all[i];
const fromId = Math.floor(Number(conf?.id) || 0);
if (fromId <= 0) continue;
const need = Math.floor(Number(conf.synthesisnumber) || 0);
const targets = conf.synthesisitem ?? [];
if (need <= 0 || !targets.length) continue;
if (this.getFreeCount(fromId) >= need) return true;
}
return false;
}
/**
* 多轮一键合成;返回净增可用数 >0 的宝石。
*/
fuseAll(): IGemFuseGain[] {
const before: { [id: number]: number } = {};
this._snapshotFree(before);
let fused = false;
let guard = 0;
while (guard < 64) {
guard++;
if (!this._fuseOneRound()) break;
fused = true;
}
if (guard >= 64) console.warn(LOG, '合成轮次达上限');
if (fused) gg.data.saveLocal();
const after: { [id: number]: number } = {};
this._snapshotFree(after);
const gains: IGemFuseGain[] = [];
for (const key in after) {
if (!Object.prototype.hasOwnProperty.call(after, key)) continue;
const id = Math.floor(Number(key) || 0);
if (id <= 0) continue;
const delta = (after[id] | 0) - (before[id] | 0);
if (delta > 0) gains.push({ gemId: id, count: delta });
}
gains.sort((a, b) => a.gemId - b.gemId);
return gains;
}
/** 五行皮肤名 */
elementSkinName(element: number): string {
switch (Math.floor(Number(element) || 0)) {
case 1: return '金';
case 2: return '木';
case 3: return '水';
case 4: return '火';
case 5: return '土';
default: return '';
}
}
/** 全量镶嵌列表(局内预留) */
getAllElementGems(): IElementGemSave[] {
const list: IElementGemSave[] = [];
for (let e = 1; e <= ELEMENT_COUNT; e++) {
list.push({ element: e, slots: this.getElementSlots(e) });
}
return list;
}
/**
* GM:Weapon 全表每颗宝石再发 1 颗(Item 库存)。每次点击叠加一套。
* @returns 本次发放的宝石种数
*/
async gmGrantAllGems(): Promise<number> {
await this.ensureTable();
const list = this.getAllGemList();
if (!list.length) {
console.warn(LOG, 'GM 一键发宝石:Weapon 表为空或未加载');
return 0;
}
let added = 0;
for (let i = 0; i < list.length; i++) {
const id = Math.floor(Number(list[i]?.id) || 0);
const itemId = Math.floor(Number(list[i]?.itemid) || 0);
if (id <= 0 || itemId <= 0) continue;
gg.data.addItemNum(itemId, 1);
added++;
}
return added;
}
/** 类型 + 专属上限(slot 上原宝石不计入专属占用) */
private _canWearType(element: number, conf: ITableWeapon, slot: number): boolean {
const t = Math.floor(Number(conf.type) || 0);
if (t === 1) return true;
const needType = element + 1;
if (t !== needType) return false;
const slots = this.getElementSlots(element);
for (let i = 0; i < slots.length; i++) {
if (i === slot) continue;
const other = this.getGem(slots[i]);
if (!other) continue;
const ot = Math.floor(Number(other.type) || 0);
if (ot >= 2 && ot <= 6) return false;
}
return true;
}
/** 一轮:按快照合成 */
private _fuseOneRound(): boolean {
const snap: { [id: number]: number } = {};
this._snapshotFree(snap);
const ops: { from: number; to: number; times: number }[] = [];
const all = this.getAllGemList();
for (let i = 0; i < all.length; i++) {
const conf = all[i];
const fromId = Math.floor(Number(conf?.id) || 0);
if (fromId <= 0) continue;
const need = Math.floor(Number(conf.synthesisnumber) || 0);
const targets = conf.synthesisitem ?? [];
if (need <= 0 || !targets.length) continue;
const free = snap[fromId] | 0;
const times = Math.floor(free / need);
if (times <= 0) continue;
for (let n = 0; n < times; n++) {
const toId = this._pickTarget(targets);
if (toId <= 0) {
console.warn(LOG, '合成目标无效', fromId, targets);
continue;
}
ops.push({ from: fromId, to: toId, times: 1 });
}
snap[fromId] = free - times * need;
}
if (!ops.length) return false;
let done = 0;
for (let i = 0; i < ops.length; i++) {
const op = ops[i];
const itemFrom = this.getItemId(op.from);
const itemTo = this.getItemId(op.to);
const need = Math.floor(Number(this.getGem(op.from)?.synthesisnumber) || 0);
if (itemFrom <= 0 || itemTo <= 0 || need <= 0) {
console.warn(LOG, '合成道具缺失', op.from, op.to);
continue;
}
if (gg.data.getItemNum(itemFrom) < need) {
console.warn(LOG, '合成材料不足', op.from);
continue;
}
gg.data.subItemNum(itemFrom, need);
gg.data.addItemNum(itemTo, 1);
done++;
}
return done > 0;
}
/** 多目标随机取一 */
private _pickTarget(targets: readonly number[]): number {
const list: number[] = [];
for (let i = 0; i < targets.length; i++) {
const id = Math.floor(Number(targets[i]) || 0);
if (id > 0) list.push(id);
}
if (!list.length) return 0;
if (list.length === 1) return list[0];
return list[Math.floor(Math.random() * list.length)];
}
/** 快照可用数(含配表全部 id,便于算净增) */
private _snapshotFree(out: { [id: number]: number }): void {
const all = this.getAllGemList();
for (let i = 0; i < all.length; i++) {
const id = Math.floor(Number(all[i]?.id) || 0);
if (id <= 0) continue;
const free = this.getFreeCount(id);
if (free > 0) out[id] = free;
}
}
/** 仓库排序:品质降序,id 升序 */
private _compareBag(a: IGemBagEntry, b: IGemBagEntry): number {
const ca = this.getGem(a.gemId);
const cb = this.getGem(b.gemId);
const qa = Math.floor(Number(ca?.quality) || 0);
const qb = Math.floor(Number(cb?.quality) || 0);
if (qa !== qb) return qb - qa;
return a.gemId - b.gemId;
}
/** 卸掉非法镶嵌 */
private _stripIllegalWears(): void {
let dirty = false;
const map = this._loadMap();
for (let e = 1; e <= ELEMENT_COUNT; e++) {
const slots = map[e] ? map[e].slice() : [0, 0, 0, 0];
while (slots.length < GEM_SLOT_COUNT) slots.push(0);
let exclusive = 0;
for (let s = 0; s < GEM_SLOT_COUNT; s++) {
const id = slots[s];
if (id <= 0) continue;
const conf = this.getGem(id);
if (!conf) {
console.warn(LOG, '卸下无效宝石', e, s, id);
slots[s] = 0;
dirty = true;
continue;
}
const t = Math.floor(Number(conf.type) || 0);
if (t === 1) continue;
if (t !== e + 1) {
console.warn(LOG, '卸下五行不匹配宝石', e, s, id, t);
slots[s] = 0;
dirty = true;
continue;
}
exclusive++;
if (exclusive > 1) {
console.warn(LOG, '卸下多余专属宝石', e, s, id);
slots[s] = 0;
exclusive--;
dirty = true;
}
}
map[e] = slots;
}
if (dirty) this._saveMap(map);
}
/** 取某五行槽位数组副本(长度 4) */
private _slotsOf(map: { [element: number]: number[] }, element: number): number[] {
const src = map[element];
const slots = src ? src.slice() : [0, 0, 0, 0];
while (slots.length < GEM_SLOT_COUNT) slots.push(0);
return slots;
}
/** 解析存档 → element → slots */
private _loadMap(): { [element: number]: number[] } {
const map: { [element: number]: number[] } = {};
const raw = gg.data.getStringData(OtherDataType.ElementGem);
if (!raw) return map;
const parts = raw.split('#');
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (!part) continue;
const sep = part.indexOf('|');
if (sep < 0) continue;
const element = Math.floor(Number(part.substring(0, sep)) || 0);
if (element < 1 || element > ELEMENT_COUNT) continue;
const slotStr = part.substring(sep + 1);
const nums = slotStr.split(',');
const slots = [0, 0, 0, 0];
for (let s = 0; s < GEM_SLOT_COUNT; s++) {
slots[s] = Math.max(0, Math.floor(Number(nums[s]) || 0));
}
map[element] = slots;
}
return map;
}
/** 写回存档 */
private _saveMap(map: { [element: number]: number[] }): void {
const parts: string[] = [];
for (let e = 1; e <= ELEMENT_COUNT; e++) {
const slots = map[e] ?? [0, 0, 0, 0];
const a = Math.max(0, slots[0] | 0);
const b = Math.max(0, slots[1] | 0);
const c = Math.max(0, slots[2] | 0);
const d = Math.max(0, slots[3] | 0);
if (a === 0 && b === 0 && c === 0 && d === 0) continue;
parts.push(`${e}|${a},${b},${c},${d}`);
}
gg.data.setStringData(OtherDataType.ElementGem, parts.join('#'));
gg.data.saveLocal();
}
}