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.
467 lines
17 KiB
467 lines
17 KiB
|
3 weeks ago
|
import { Weapon2TileType } from 'db://assets/mx/module/merge';
|
||
|
|
import { TableNames } from '../../game/ConfigTableData';
|
||
|
|
import { ConstNumType } from '../../game/GameDataEnums';
|
||
|
|
import { MatchStreakStats } from '../../manager/MatchStreakStats';
|
||
|
|
import { heroCharacterIds, isAttackGlyph, isComboHero } from '../config/BattleHeroTable';
|
||
|
|
import { formatIdList, formatWeapon2Brief, logBagRefreshDebug } from '../debug/BagRefreshDebugLog';
|
||
|
|
import type { BagRollEntry } from './BagWeaponPool';
|
||
|
|
import type { PlayerBagTextPool } from './PlayerBagTextPool';
|
||
|
|
import { WeaponComposeConfigProvider } from './WeaponComposeConfigProvider';
|
||
|
|
|
||
|
|
/** 新号首局保底神将(配表 Hero.name) */
|
||
|
|
const FIRST_BATTLE_HERO_NAMES = ['哪吒', '杨戬', '悟空'];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 玩家连败组合字保底。
|
||
|
|
* 地图碎字 + 本次已抽出的组合字满 4 个、且本局尚无完整组合英雄时,
|
||
|
|
* 第 5 个及之后每个组合字按「连败次数 × Const30」判定;命中则换成已有碎字所缺的另一半。
|
||
|
|
* 凑齐任意一对(含未拼上的散字)后本局停止。
|
||
|
|
*/
|
||
|
|
export class ComboLosePity {
|
||
|
|
/** 本局已因完整组合英雄停止 */
|
||
|
|
private _stopped = false;
|
||
|
|
/** herotype=2 且 text 长度>1 的英雄缓存 */
|
||
|
|
private _comboHeroes: ITableHero[] | null = null;
|
||
|
|
|
||
|
|
/** 本局是否已停止 */
|
||
|
|
get stopped(): boolean {
|
||
|
|
return this._stopped;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 快照恢复停止标记。
|
||
|
|
* @param stopped 是否已停止
|
||
|
|
*/
|
||
|
|
setStopped(stopped: boolean): void {
|
||
|
|
this._stopped = !!stopped;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 按抽取顺序尝试保底替换(就地改 slots)。
|
||
|
|
* @param slots 本次抽出的槽(抽取顺序,未打乱)
|
||
|
|
* @param mapComboIds 玩家地图上的组合字 Text.id
|
||
|
|
* @param pool 字池(替换时扣减/归还 remaining)
|
||
|
|
*/
|
||
|
|
applyToSlots(slots: BagRollEntry[], mapComboIds: readonly number[], pool: PlayerBagTextPool): void {
|
||
|
|
if (this._stopped) {
|
||
|
|
logBagRefreshDebug('连败保底: 跳过(本局已停止)');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const owned: number[] = [];
|
||
|
|
for (let i = 0; i < mapComboIds.length; i++) owned.push(mapComboIds[i]);
|
||
|
|
if (this._hasCompleteHero(owned)) {
|
||
|
|
this._stopped = true;
|
||
|
|
logBagRefreshDebug('连败保底: 本局停止(地图已有完整组合英雄)', formatIdList(owned));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
for (let i = 0; i < slots.length; i++) {
|
||
|
|
if (this._stopped) return;
|
||
|
|
const id = slots[i].weapon2Id;
|
||
|
|
if (!this._isCombo(id)) continue;
|
||
|
|
owned.push(id);
|
||
|
|
if (this._hasCompleteHero(owned)) {
|
||
|
|
this._stopped = true;
|
||
|
|
logBagRefreshDebug('连败保底: 本局停止(刷新凑齐完整英雄)', formatWeapon2Brief(id));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
owned.pop();
|
||
|
|
if (owned.length < 4) {
|
||
|
|
owned.push(id);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const hit = this._rollHit();
|
||
|
|
logBagRefreshDebug(
|
||
|
|
'连败保底: 判定',
|
||
|
|
`碎字=${owned.length}`,
|
||
|
|
`连败=${MatchStreakStats.getLoseStreak()}`,
|
||
|
|
hit ? '→ 命中' : '→ 未命中',
|
||
|
|
);
|
||
|
|
if (!hit) {
|
||
|
|
owned.push(id);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const pick = this._consumeMissing(owned, pool);
|
||
|
|
if (pick == null) {
|
||
|
|
owned.push(id);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
if (!pool.tryRefundWeapon2Id(id)) {
|
||
|
|
console.log('[ComboLosePity] 归还原抽字 remaining 失败', id);
|
||
|
|
}
|
||
|
|
slots[i] = { weapon2Id: pick };
|
||
|
|
this._stopped = true;
|
||
|
|
logBagRefreshDebug(
|
||
|
|
'连败保底: 替换',
|
||
|
|
`${formatWeapon2Brief(id)} → ${formatWeapon2Brief(pick)}`,
|
||
|
|
`候选源=${formatIdList(owned)}`,
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 当前字是否为组合碎字 */
|
||
|
|
private _isCombo(weapon2Id: number): boolean {
|
||
|
|
return WeaponComposeConfigProvider.instance.getWeapon2Type(weapon2Id) === Weapon2TileType.Combo;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 已有字是否凑齐任意多格组合英雄(含未拼上) */
|
||
|
|
private _hasCompleteHero(owned: readonly number[]): boolean {
|
||
|
|
const heroes = this._comboHeroList();
|
||
|
|
for (let i = 0; i < heroes.length; i++) {
|
||
|
|
if (this._ownsAll(owned, heroCharacterIds(heroes[i]))) return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 从已有碎字所缺另一半中随机一个并扣池。
|
||
|
|
* @returns 成功扣减的 Text.id;无候选或 remaining 耗尽时 null
|
||
|
|
*/
|
||
|
|
private _consumeMissing(owned: readonly number[], pool: PlayerBagTextPool): number | null {
|
||
|
|
const missing = this._collectMissing(owned);
|
||
|
|
if (missing.length === 0) {
|
||
|
|
console.log('[ComboLosePity] 碎字所缺另一半为空', owned.join(','));
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
this._shuffleIds(missing);
|
||
|
|
for (let i = 0; i < missing.length; i++) {
|
||
|
|
if (pool.tryConsumeWeapon2Id(missing[i])) return missing[i];
|
||
|
|
}
|
||
|
|
console.log('[ComboLosePity] 所缺字 remaining 均耗尽', missing.join(','));
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 已有碎字对应英雄中尚未持有的另一半(去重) */
|
||
|
|
private _collectMissing(owned: readonly number[]): number[] {
|
||
|
|
const missing: number[] = [];
|
||
|
|
const heroes = this._comboHeroList();
|
||
|
|
for (let i = 0; i < heroes.length; i++) {
|
||
|
|
const chars = heroCharacterIds(heroes[i]);
|
||
|
|
if (!this._ownsAny(owned, chars)) continue;
|
||
|
|
for (let c = 0; c < chars.length; c++) {
|
||
|
|
if (this._owns(owned, chars[c])) continue;
|
||
|
|
this._addUnique(missing, chars[c]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return missing;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 连败次数 × Const30 万分比,上限 10000 */
|
||
|
|
private _rollHit(): boolean {
|
||
|
|
const arr = gg.data.table.getConstArray(ConstNumType.LoseComboPityRate);
|
||
|
|
const wan = Math.floor(Number(arr[0]) || 0);
|
||
|
|
if (!(wan > 0)) {
|
||
|
|
console.log('[ComboLosePity] Const30 概率无效', arr);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const chance = Math.min(10000, MatchStreakStats.getLoseStreak() * wan);
|
||
|
|
return Math.random() * 10000 < chance;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 组合神将列表 */
|
||
|
|
private _comboHeroList(): ITableHero[] {
|
||
|
|
if (this._comboHeroes) return this._comboHeroes;
|
||
|
|
const list = gg.data.table.getTableList<ITableHero>(TableNames.Hero) ?? [];
|
||
|
|
const out: ITableHero[] = [];
|
||
|
|
for (let i = 0; i < list.length; i++) {
|
||
|
|
const hero = list[i];
|
||
|
|
if (!isComboHero(hero)) continue;
|
||
|
|
if (heroCharacterIds(hero).length <= 1) continue;
|
||
|
|
out.push(hero);
|
||
|
|
}
|
||
|
|
this._comboHeroes = out;
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 是否持有序列中全部 id */
|
||
|
|
private _ownsAll(owned: readonly number[], chars: readonly number[]): boolean {
|
||
|
|
for (let i = 0; i < chars.length; i++) {
|
||
|
|
if (!this._owns(owned, chars[i])) return false;
|
||
|
|
}
|
||
|
|
return chars.length > 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 是否持有序列中任一 id */
|
||
|
|
private _ownsAny(owned: readonly number[], chars: readonly number[]): boolean {
|
||
|
|
for (let i = 0; i < chars.length; i++) {
|
||
|
|
if (this._owns(owned, chars[i])) return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 是否持有该 Text.id */
|
||
|
|
private _owns(owned: readonly number[], id: number): boolean {
|
||
|
|
for (let i = 0; i < owned.length; i++) {
|
||
|
|
if (owned[i] === id) return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 去重追加。
|
||
|
|
* @param ids 目标数组
|
||
|
|
* @param id 待加入 id
|
||
|
|
*/
|
||
|
|
private _addUnique(ids: number[], id: number): void {
|
||
|
|
for (let i = 0; i < ids.length; i++) {
|
||
|
|
if (ids[i] === id) return;
|
||
|
|
}
|
||
|
|
ids.push(id);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 打乱 id 列表。
|
||
|
|
* @param ids 待打乱数组
|
||
|
|
*/
|
||
|
|
private _shuffleIds(ids: number[]): void {
|
||
|
|
for (let i = ids.length - 1; i > 0; i--) {
|
||
|
|
const j = Math.floor(Math.random() * (i + 1));
|
||
|
|
const tmp = ids[i];
|
||
|
|
ids[i] = ids[j];
|
||
|
|
ids[j] = tmp;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 新号第一局前两次刷新:先给哪吒/杨戬/悟空六字之一,再给能合成的另一半。
|
||
|
|
* 与是否拖到地图无关。仅玩家;GM 不走。
|
||
|
|
*/
|
||
|
|
export class FirstBattleHeroPity {
|
||
|
|
/** 本局是否启用 */
|
||
|
|
private readonly _enabled: boolean;
|
||
|
|
/** 第二次要给的另一半 Text.id;0=无需再给 */
|
||
|
|
private _pending = 0;
|
||
|
|
/** 三对神将字,扁平 [a,b, a,b, a,b] */
|
||
|
|
private _pairs: number[] | null = null;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param enabled 开战时是否新号第一局
|
||
|
|
*/
|
||
|
|
constructor(enabled: boolean) {
|
||
|
|
this._enabled = enabled;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 第二次待给的另一半 */
|
||
|
|
get pendingPartner(): number {
|
||
|
|
return this._pending;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 快照恢复待给另一半。
|
||
|
|
* @param id Text.id;0 表示无需再给
|
||
|
|
*/
|
||
|
|
setPendingPartner(id: number): void {
|
||
|
|
this._pending = Math.floor(Number(id) || 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 按本局第几次刷新就地改 slots。
|
||
|
|
* @param slots 本次抽出的槽
|
||
|
|
* @param refreshUsed 本局已成功普通刷新次数(本次尚未 +1)
|
||
|
|
* @param pool 字池
|
||
|
|
*/
|
||
|
|
applyToSlots(slots: BagRollEntry[], refreshUsed: number, pool: PlayerBagTextPool): void {
|
||
|
|
if (!this._enabled) return;
|
||
|
|
if (refreshUsed === 0) {
|
||
|
|
this._ensureFirst(slots, pool);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (refreshUsed === 1) this._ensureSecond(slots, pool);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 第一次:保证六字之一并记下另一半 */
|
||
|
|
private _ensureFirst(slots: BagRollEntry[], pool: PlayerBagTextPool): void {
|
||
|
|
const pairs = this._heroPairs();
|
||
|
|
if (pairs.length < 2) {
|
||
|
|
console.log('[FirstBattleHeroPity] 神将字对为空,跳过首次保底');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (this._hasCompletePair(slots, pairs)) {
|
||
|
|
this._pending = 0;
|
||
|
|
logBagRefreshDebug('首局神将: 首次已成对,第二次不补', formatIdList(this._slotIds(slots)));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const present = this._presentGlyphs(slots, pairs);
|
||
|
|
if (present.length > 0) {
|
||
|
|
const pick = present[Math.floor(Math.random() * present.length)];
|
||
|
|
this._pending = this._partnerOf(pick, pairs);
|
||
|
|
logBagRefreshDebug(
|
||
|
|
'首局神将: 首次已有六字',
|
||
|
|
formatWeapon2Brief(pick),
|
||
|
|
`下一半=${formatWeapon2Brief(this._pending)}`,
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const glyphs = this._allGlyphs(pairs);
|
||
|
|
const pick = glyphs[Math.floor(Math.random() * glyphs.length)];
|
||
|
|
if (!this._replaceOne(slots, pick, pool, true)) return;
|
||
|
|
this._pending = this._partnerOf(pick, pairs);
|
||
|
|
logBagRefreshDebug(
|
||
|
|
'首局神将: 首次替换',
|
||
|
|
formatWeapon2Brief(pick),
|
||
|
|
`下一半=${formatWeapon2Brief(this._pending)}`,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 第二次:保证给记下的另一半 */
|
||
|
|
private _ensureSecond(slots: BagRollEntry[], pool: PlayerBagTextPool): void {
|
||
|
|
if (this._pending <= 0) return;
|
||
|
|
const want = this._pending;
|
||
|
|
this._pending = 0;
|
||
|
|
if (this._contains(slots, want)) {
|
||
|
|
logBagRefreshDebug('首局神将: 第二次已抽出', formatWeapon2Brief(want));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!this._replaceOne(slots, want, pool, false)) return;
|
||
|
|
logBagRefreshDebug('首局神将: 第二次替换', formatWeapon2Brief(want));
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 替换一槽为指定字:扣新字 remaining、归还原字。remaining 不足仍强制给字。
|
||
|
|
* @param keepAttacker true 时不替换本批唯一攻击字
|
||
|
|
*/
|
||
|
|
private _replaceOne(slots: BagRollEntry[], newId: number, pool: PlayerBagTextPool, keepAttacker: boolean): boolean {
|
||
|
|
const idx = this._pickReplaceIndex(slots, keepAttacker);
|
||
|
|
if (idx < 0) {
|
||
|
|
console.log('[FirstBattleHeroPity] 无槽可替换', newId);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const oldId = slots[idx].weapon2Id;
|
||
|
|
if (oldId === newId) return true;
|
||
|
|
if (!pool.tryConsumeWeapon2Id(newId)) {
|
||
|
|
console.log('[FirstBattleHeroPity] 字池 remaining 不足仍强制给字', newId);
|
||
|
|
}
|
||
|
|
if (!pool.tryRefundWeapon2Id(oldId)) {
|
||
|
|
console.log('[FirstBattleHeroPity] 归还原抽字 remaining 失败', oldId);
|
||
|
|
}
|
||
|
|
slots[idx] = { weapon2Id: newId };
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 随机选替换下标。
|
||
|
|
* @param keepAttacker true 时跳过本批唯一攻击字
|
||
|
|
*/
|
||
|
|
private _pickReplaceIndex(slots: BagRollEntry[], keepAttacker: boolean): number {
|
||
|
|
let atkCount = 0;
|
||
|
|
for (let i = 0; i < slots.length; i++) {
|
||
|
|
if (isAttackGlyph(slots[i].weapon2Id)) atkCount++;
|
||
|
|
}
|
||
|
|
const cand: number[] = [];
|
||
|
|
for (let i = 0; i < slots.length; i++) {
|
||
|
|
if (keepAttacker && atkCount <= 1 && isAttackGlyph(slots[i].weapon2Id)) continue;
|
||
|
|
cand.push(i);
|
||
|
|
}
|
||
|
|
if (cand.length <= 0) {
|
||
|
|
console.log('[FirstBattleHeroPity] 无可替换槽', `atk=${atkCount}`, `n=${slots.length}`);
|
||
|
|
return -1;
|
||
|
|
}
|
||
|
|
return cand[Math.floor(Math.random() * cand.length)];
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 读三对神将字;缺失 Hero 或非两字则告警跳过该将 */
|
||
|
|
private _heroPairs(): number[] {
|
||
|
|
if (this._pairs) return this._pairs;
|
||
|
|
const out: number[] = [];
|
||
|
|
const list = gg.data.table.getTableList<ITableHero>(TableNames.Hero) ?? [];
|
||
|
|
for (let n = 0; n < FIRST_BATTLE_HERO_NAMES.length; n++) {
|
||
|
|
const name = FIRST_BATTLE_HERO_NAMES[n];
|
||
|
|
let found: ITableHero | null = null;
|
||
|
|
for (let i = 0; i < list.length; i++) {
|
||
|
|
if (String(list[i].name ?? '') !== name) continue;
|
||
|
|
found = list[i];
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
if (!found) {
|
||
|
|
console.log('[FirstBattleHeroPity] Hero 缺失', name);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const chars = heroCharacterIds(found);
|
||
|
|
if (chars.length !== 2) {
|
||
|
|
console.log('[FirstBattleHeroPity] Hero.text 不是两字', name, chars.join(','));
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const a = Math.floor(Number(chars[0]) || 0);
|
||
|
|
const b = Math.floor(Number(chars[1]) || 0);
|
||
|
|
if (!(a > 0) || !(b > 0)) {
|
||
|
|
console.log('[FirstBattleHeroPity] Hero.text 非法', name, chars.join(','));
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
out.push(a, b);
|
||
|
|
}
|
||
|
|
this._pairs = out;
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 本批是否已含任一对的两个字 */
|
||
|
|
private _hasCompletePair(slots: readonly BagRollEntry[], pairs: readonly number[]): boolean {
|
||
|
|
for (let i = 0; i + 1 < pairs.length; i += 2) {
|
||
|
|
if (this._contains(slots, pairs[i]) && this._contains(slots, pairs[i + 1])) return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 本批已出现的六字(去重) */
|
||
|
|
private _presentGlyphs(slots: readonly BagRollEntry[], pairs: readonly number[]): number[] {
|
||
|
|
const out: number[] = [];
|
||
|
|
for (let i = 0; i < slots.length; i++) {
|
||
|
|
const id = slots[i].weapon2Id;
|
||
|
|
if (this._isGlyph(id, pairs)) this._addUnique(out, id);
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 六字全集 */
|
||
|
|
private _allGlyphs(pairs: readonly number[]): number[] {
|
||
|
|
const out: number[] = [];
|
||
|
|
for (let i = 0; i < pairs.length; i++) this._addUnique(out, pairs[i]);
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 同一对里的另一半;找不到返回 0 */
|
||
|
|
private _partnerOf(id: number, pairs: readonly number[]): number {
|
||
|
|
for (let i = 0; i + 1 < pairs.length; i += 2) {
|
||
|
|
if (pairs[i] === id) return pairs[i + 1];
|
||
|
|
if (pairs[i + 1] === id) return pairs[i];
|
||
|
|
}
|
||
|
|
console.log('[FirstBattleHeroPity] 无配对另一半', id);
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** id 是否属于保底六字 */
|
||
|
|
private _isGlyph(id: number, pairs: readonly number[]): boolean {
|
||
|
|
for (let i = 0; i < pairs.length; i++) {
|
||
|
|
if (pairs[i] === id) return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 本批是否含该 Text.id */
|
||
|
|
private _contains(slots: readonly BagRollEntry[], id: number): boolean {
|
||
|
|
for (let i = 0; i < slots.length; i++) {
|
||
|
|
if (slots[i].weapon2Id === id) return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 槽位 Text.id 列表(调试) */
|
||
|
|
private _slotIds(slots: readonly BagRollEntry[]): number[] {
|
||
|
|
const ids: number[] = [];
|
||
|
|
for (let i = 0; i < slots.length; i++) ids.push(slots[i].weapon2Id);
|
||
|
|
return ids;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 去重追加。
|
||
|
|
* @param ids 目标数组
|
||
|
|
* @param id 待加入 id
|
||
|
|
*/
|
||
|
|
private _addUnique(ids: number[], id: number): void {
|
||
|
|
for (let i = 0; i < ids.length; i++) {
|
||
|
|
if (ids[i] === id) return;
|
||
|
|
}
|
||
|
|
ids.push(id);
|
||
|
|
}
|
||
|
|
}
|