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.
436 lines
16 KiB
436 lines
16 KiB
|
1 week ago
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 多轮一键合成;返回净增可用数 >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;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 类型 + 专属上限(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();
|
||
|
|
}
|
||
|
|
}
|