fix: resetGame button doesn't fully reset game state
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m18s
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
# Circular Dependencies
|
# Circular Dependencies
|
||||||
Generated: 2026-06-11T14:10:00.590Z
|
Generated: 2026-06-12T05:02:18.108Z
|
||||||
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
|
Found: 4 circular chain(s) — these MUST be fixed before modifying involved files.
|
||||||
|
|
||||||
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
|
1. 1) data/guardian-encounters.ts > data/guardian-procedural.ts
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"generated": "2026-06-11T14:09:58.499Z",
|
"generated": "2026-06-12T05:02:13.512Z",
|
||||||
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
"description": "Import dependency graph for src/lib/game. Keys are files, values are arrays of files they import.",
|
||||||
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
"usage": "To find what a file affects, search for its path in the VALUES. To find what a file depends on, look at its KEY entry."
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── paused-conversion-dedup.test.ts
|
│ │ │ │ ├── paused-conversion-dedup.test.ts
|
||||||
│ │ │ │ ├── persistence.test.ts
|
│ │ │ │ ├── persistence.test.ts
|
||||||
│ │ │ │ ├── regression-fixes.test.ts
|
│ │ │ │ ├── regression-fixes.test.ts
|
||||||
|
│ │ │ │ ├── reset-game-comprehensive.test.ts
|
||||||
│ │ │ │ ├── room-utils-floor-state.test.ts
|
│ │ │ │ ├── room-utils-floor-state.test.ts
|
||||||
│ │ │ │ ├── room-utils.test.ts
|
│ │ │ │ ├── room-utils.test.ts
|
||||||
│ │ │ │ ├── spell-cast-floorhp-guard.test.ts
|
│ │ │ │ ├── spell-cast-floorhp-guard.test.ts
|
||||||
@@ -380,6 +381,7 @@ Mana-Loop/
|
|||||||
│ │ │ │ ├── combat-actions.ts
|
│ │ │ │ ├── combat-actions.ts
|
||||||
│ │ │ │ ├── combat-damage.ts
|
│ │ │ │ ├── combat-damage.ts
|
||||||
│ │ │ │ ├── combat-descent-actions.ts
|
│ │ │ │ ├── combat-descent-actions.ts
|
||||||
|
│ │ │ │ ├── combat-reset.ts
|
||||||
│ │ │ │ ├── combat-state.types.ts
|
│ │ │ │ ├── combat-state.types.ts
|
||||||
│ │ │ │ ├── combatStore.ts
|
│ │ │ │ ├── combatStore.ts
|
||||||
│ │ │ │ ├── crafting-equipment-tick.ts
|
│ │ │ │ ├── crafting-equipment-tick.ts
|
||||||
|
|||||||
@@ -0,0 +1,303 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { useGameStore } from '../stores/gameStore';
|
||||||
|
import { useManaStore } from '../stores/manaStore';
|
||||||
|
import { useCombatStore } from '../stores/combatStore';
|
||||||
|
import { usePrestigeStore } from '../stores/prestigeStore';
|
||||||
|
import { useCraftingStore } from '../stores/craftingStore';
|
||||||
|
import { useAttunementStore } from '../stores/attunementStore';
|
||||||
|
import { useDisciplineStore } from '../stores/discipline-slice';
|
||||||
|
import { useUIStore } from '../stores/uiStore';
|
||||||
|
|
||||||
|
// Exact localStorage keys matching each store's persist config `name`
|
||||||
|
const ALL_STORE_KEYS = [
|
||||||
|
'mana-loop-game-storage',
|
||||||
|
'mana-loop-mana',
|
||||||
|
'mana-loop-combat',
|
||||||
|
'mana-loop-prestige',
|
||||||
|
'mana-loop-crafting',
|
||||||
|
'mana-loop-attunements',
|
||||||
|
'mana-loop-discipline-store',
|
||||||
|
'mana-loop-ui-storage',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
function clearAllPersistedState() {
|
||||||
|
for (const key of ALL_STORE_KEYS) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set non-default state across ALL stores to simulate a game in progress.
|
||||||
|
* This modifies every field that should be reset by resetGame().
|
||||||
|
*/
|
||||||
|
function setNonDefaultState() {
|
||||||
|
// Game store: advance time
|
||||||
|
useGameStore.setState({ day: 15, hour: 12 });
|
||||||
|
|
||||||
|
// Mana store: add mana, unlock elements
|
||||||
|
useManaStore.setState((s) => ({
|
||||||
|
rawMana: 500,
|
||||||
|
meditateTicks: 100,
|
||||||
|
totalManaGathered: 10000,
|
||||||
|
elements: {
|
||||||
|
...s.elements,
|
||||||
|
fire: { current: 50, max: 100, baseMax: 100, unlocked: true },
|
||||||
|
water: { current: 30, max: 100, baseMax: 100, unlocked: true },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Combat store: change many fields
|
||||||
|
useCombatStore.setState({
|
||||||
|
currentFloor: 25,
|
||||||
|
floorHP: 500,
|
||||||
|
floorMaxHP: 2000,
|
||||||
|
maxFloorReached: 25,
|
||||||
|
activeSpell: 'fireBolt',
|
||||||
|
currentAction: 'climb',
|
||||||
|
castProgress: 0.5,
|
||||||
|
spireMode: true,
|
||||||
|
clearedFloors: { 1: true, 2: true },
|
||||||
|
climbDirection: 'up' as const,
|
||||||
|
isDescending: false,
|
||||||
|
weaponCastProgress: { primary: 0.3 },
|
||||||
|
comboHitCount: 5,
|
||||||
|
floorHitCount: 10,
|
||||||
|
meleeSwordProgress: { sword1: 0.2 },
|
||||||
|
guardianShield: 100,
|
||||||
|
guardianShieldMax: 200,
|
||||||
|
guardianBarrier: 50,
|
||||||
|
guardianBarrierMax: 100,
|
||||||
|
totalSpellsCast: 500,
|
||||||
|
totalDamageDealt: 9999,
|
||||||
|
totalCraftsCompleted: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prestige store: add insight, upgrades, pacts
|
||||||
|
usePrestigeStore.setState({
|
||||||
|
insight: 1000,
|
||||||
|
totalInsight: 5000,
|
||||||
|
loopCount: 3,
|
||||||
|
prestigeUpgrades: { manaWell: 2, manaFlow: 1 },
|
||||||
|
defeatedGuardians: [10, 20, 30],
|
||||||
|
signedPacts: [10, 20],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attunement store: level up
|
||||||
|
useAttunementStore.setState({
|
||||||
|
attunements: {
|
||||||
|
enchanter: { id: 'enchanter', active: true, level: 5, experience: 1000 },
|
||||||
|
invoker: { id: 'invoker', active: true, level: 3, experience: 500 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Discipline store: add disciplines and XP
|
||||||
|
useDisciplineStore.setState({
|
||||||
|
disciplines: {
|
||||||
|
rawManaMastery: { id: 'rawManaMastery', xp: 500, paused: false },
|
||||||
|
},
|
||||||
|
activeIds: ['rawManaMastery'],
|
||||||
|
totalXP: 500,
|
||||||
|
concurrentLimit: 2,
|
||||||
|
processedPerks: ['somePerk'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Crafting store: add designs, unlock effects, add loot
|
||||||
|
useCraftingStore.setState((s) => ({
|
||||||
|
designProgress: {
|
||||||
|
designId: 'design-1',
|
||||||
|
progress: 5,
|
||||||
|
required: 10,
|
||||||
|
name: 'My Design',
|
||||||
|
equipmentType: 'basicStaff',
|
||||||
|
effects: [{ effectId: 'spell_fireBolt', stacks: 1, actualCost: 30 }],
|
||||||
|
},
|
||||||
|
enchantmentDesigns: [{ id: 'design-1', name: 'My Design', equipmentType: 'basicStaff', effects: [], totalCapacityCost: 30, totalStacks: 1 }],
|
||||||
|
unlockedEffects: ['spell_fireBolt', 'spell_iceBolt'],
|
||||||
|
unlockedRecipes: ['recipe1', 'recipe2'],
|
||||||
|
lootInventory: {
|
||||||
|
materials: { ironOre: 100, manaCrystalDust: 50 },
|
||||||
|
blueprints: ['bp1', 'bp2'],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// UI store: set game over, pause
|
||||||
|
useUIStore.setState({
|
||||||
|
paused: true,
|
||||||
|
gameOver: true,
|
||||||
|
victory: true,
|
||||||
|
logs: ['some log message'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// COMPREHENSIVE RESET GAME TESTS
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
describe('resetGame comprehensive', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
clearAllPersistedState();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clearAllPersistedState();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset ALL game store fields to initial values', () => {
|
||||||
|
setNonDefaultState();
|
||||||
|
useGameStore.getState().resetGame();
|
||||||
|
|
||||||
|
const game = useGameStore.getState();
|
||||||
|
expect(game.day).toBe(1);
|
||||||
|
expect(game.hour).toBe(0);
|
||||||
|
expect(game.incursionStrength).toBe(0);
|
||||||
|
expect(game.containmentWards).toBe(0);
|
||||||
|
expect(game.initialized).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset ALL mana store fields to initial values', () => {
|
||||||
|
setNonDefaultState();
|
||||||
|
useGameStore.getState().resetGame();
|
||||||
|
|
||||||
|
const mana = useManaStore.getState();
|
||||||
|
expect(mana.rawMana).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(mana.rawMana).toBeLessThanOrEqual(20); // starting mana should be small
|
||||||
|
expect(mana.meditateTicks).toBe(0);
|
||||||
|
expect(mana.totalManaGathered).toBe(0);
|
||||||
|
expect(mana.elementRegen).toEqual({});
|
||||||
|
|
||||||
|
// Only transference should be unlocked (or base elements depending on config)
|
||||||
|
for (const [key, elem] of Object.entries(mana.elements)) {
|
||||||
|
// Fire and water should NOT be unlocked (they were unlocked by setNonDefaultState)
|
||||||
|
if (key === 'fire' || key === 'water') {
|
||||||
|
// After reset, these should be locked (not unlocked)
|
||||||
|
// The initial state only has certain elements unlocked
|
||||||
|
expect(elem.current).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(elem.max).toBeGreaterThan(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset ALL combat store fields to initial values', () => {
|
||||||
|
setNonDefaultState();
|
||||||
|
useGameStore.getState().resetGame();
|
||||||
|
|
||||||
|
const combat = useCombatStore.getState();
|
||||||
|
expect(combat.currentFloor).toBe(1);
|
||||||
|
expect(combat.maxFloorReached).toBe(1);
|
||||||
|
expect(combat.activeSpell).toBe('manaBolt');
|
||||||
|
expect(combat.currentAction).toBe('meditate');
|
||||||
|
expect(combat.castProgress).toBe(0);
|
||||||
|
|
||||||
|
// These fields should also be reset but might be missed by resetCombat:
|
||||||
|
expect(combat.spireMode).toBe(false);
|
||||||
|
expect(combat.clearedFloors).toEqual({});
|
||||||
|
expect(combat.climbDirection).toBeNull();
|
||||||
|
expect(combat.weaponCastProgress).toEqual({});
|
||||||
|
expect(combat.comboHitCount).toBe(0);
|
||||||
|
expect(combat.floorHitCount).toBe(0);
|
||||||
|
expect(combat.meleeSwordProgress).toEqual({});
|
||||||
|
expect(combat.guardianShield).toBe(0);
|
||||||
|
expect(combat.guardianShieldMax).toBe(0);
|
||||||
|
expect(combat.guardianBarrier).toBe(0);
|
||||||
|
expect(combat.guardianBarrierMax).toBe(0);
|
||||||
|
expect(combat.totalSpellsCast).toBe(0);
|
||||||
|
expect(combat.totalDamageDealt).toBe(0);
|
||||||
|
expect(combat.totalCraftsCompleted).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset ALL prestige store fields to initial values', () => {
|
||||||
|
setNonDefaultState();
|
||||||
|
useGameStore.getState().resetGame();
|
||||||
|
|
||||||
|
const prestige = usePrestigeStore.getState();
|
||||||
|
expect(prestige.insight).toBe(0);
|
||||||
|
expect(prestige.totalInsight).toBe(0);
|
||||||
|
expect(prestige.loopCount).toBe(0);
|
||||||
|
expect(prestige.prestigeUpgrades).toEqual({});
|
||||||
|
expect(prestige.defeatedGuardians).toEqual([]);
|
||||||
|
expect(prestige.signedPacts).toEqual([]);
|
||||||
|
expect(prestige.pactRitualFloor).toBeNull();
|
||||||
|
expect(prestige.pactRitualProgress).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset ALL attunement store fields to initial values', () => {
|
||||||
|
setNonDefaultState();
|
||||||
|
useGameStore.getState().resetGame();
|
||||||
|
|
||||||
|
const att = useAttunementStore.getState();
|
||||||
|
// Should only have enchanter at level 1
|
||||||
|
expect(Object.keys(att.attunements).length).toBe(1);
|
||||||
|
expect(att.attunements.enchanter).toBeDefined();
|
||||||
|
expect(att.attunements.enchanter.level).toBe(1);
|
||||||
|
expect(att.attunements.enchanter.experience).toBe(0);
|
||||||
|
expect(att.attunements.enchanter.active).toBe(true);
|
||||||
|
// Invoker should not be present
|
||||||
|
expect(att.attunements.invoker).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset ALL discipline store fields to initial values', () => {
|
||||||
|
setNonDefaultState();
|
||||||
|
useGameStore.getState().resetGame();
|
||||||
|
|
||||||
|
const disc = useDisciplineStore.getState();
|
||||||
|
expect(disc.disciplines).toEqual({});
|
||||||
|
expect(disc.activeIds).toEqual([]);
|
||||||
|
expect(disc.totalXP).toBe(0);
|
||||||
|
expect(disc.processedPerks).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset ALL crafting store fields to initial values', () => {
|
||||||
|
setNonDefaultState();
|
||||||
|
useGameStore.getState().resetGame();
|
||||||
|
|
||||||
|
const crafting = useCraftingStore.getState();
|
||||||
|
expect(crafting.designProgress).toBeNull();
|
||||||
|
expect(crafting.designProgress2).toBeNull();
|
||||||
|
expect(crafting.enchantmentDesigns).toEqual([]);
|
||||||
|
expect(crafting.unlockedEffects).toEqual([]);
|
||||||
|
expect(crafting.unlockedRecipes).toEqual([]);
|
||||||
|
expect(crafting.lootInventory.materials).toEqual({});
|
||||||
|
expect(crafting.lootInventory.blueprints).toEqual([]);
|
||||||
|
|
||||||
|
// Should still have the starting equipment
|
||||||
|
expect(Object.keys(crafting.equipmentInstances).length).toBe(3); // staff, shirt, shoes
|
||||||
|
expect(crafting.equippedInstances.mainHand).not.toBeNull();
|
||||||
|
expect(crafting.equippedInstances.body).not.toBeNull();
|
||||||
|
expect(crafting.equippedInstances.feet).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset ALL UI store fields to initial values', () => {
|
||||||
|
setNonDefaultState();
|
||||||
|
useGameStore.getState().resetGame();
|
||||||
|
|
||||||
|
const ui = useUIStore.getState();
|
||||||
|
expect(ui.paused).toBe(false);
|
||||||
|
expect(ui.gameOver).toBe(false);
|
||||||
|
expect(ui.victory).toBe(false);
|
||||||
|
expect(ui.logs.length).toBe(1); // fresh game starts with one log message
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear all localStorage keys and not leave stale data', () => {
|
||||||
|
setNonDefaultState();
|
||||||
|
|
||||||
|
// First, force all stores to persist by calling setState on each
|
||||||
|
// (simulating what happens during normal gameplay)
|
||||||
|
const gameState = useGameStore.getState();
|
||||||
|
useGameStore.setState({ day: gameState.day });
|
||||||
|
|
||||||
|
useGameStore.getState().resetGame();
|
||||||
|
|
||||||
|
// After reset, localStorage should contain the reset state, not the old state
|
||||||
|
const combatRaw = localStorage.getItem('mana-loop-combat');
|
||||||
|
if (combatRaw) {
|
||||||
|
const combatParsed = JSON.parse(combatRaw);
|
||||||
|
expect(combatParsed.state.currentFloor).toBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const manaRaw = localStorage.getItem('mana-loop-mana');
|
||||||
|
if (manaRaw) {
|
||||||
|
const manaParsed = JSON.parse(manaRaw);
|
||||||
|
expect(manaParsed.state.totalManaGathered).toBe(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
// ─── Combat Reset Helper ───────────────────────────────────────────────────────
|
||||||
|
// Generates the full default combat store state for resetCombat().
|
||||||
|
// Keeps combatStore.ts under the 400-line limit.
|
||||||
|
|
||||||
|
import type { RuntimeActiveGolem } from '../types';
|
||||||
|
import { getFloorMaxHP } from '../utils';
|
||||||
|
import { generateSpireFloorState, getRoomsForFloor } from '../utils/spire-utils';
|
||||||
|
import { makeInitialSpells } from './combat-actions';
|
||||||
|
import type { CombatState } from './combat-state.types';
|
||||||
|
|
||||||
|
export function createDefaultCombatState(
|
||||||
|
startFloor: number,
|
||||||
|
spellsToKeep: string[] = [],
|
||||||
|
): Partial<CombatState> {
|
||||||
|
const startSpells = makeInitialSpells(spellsToKeep);
|
||||||
|
const seed = startFloor * 12345;
|
||||||
|
const rooms = getRoomsForFloor(startFloor, seed);
|
||||||
|
const startRoom = generateSpireFloorState(startFloor, 0, rooms, 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentFloor: startFloor,
|
||||||
|
floorHP: getFloorMaxHP(startFloor),
|
||||||
|
floorMaxHP: getFloorMaxHP(startFloor),
|
||||||
|
maxFloorReached: startFloor,
|
||||||
|
activeSpell: 'manaBolt',
|
||||||
|
currentAction: 'meditate',
|
||||||
|
castProgress: 0,
|
||||||
|
spireMode: false,
|
||||||
|
currentRoom: startRoom,
|
||||||
|
clearedFloors: {},
|
||||||
|
climbDirection: null,
|
||||||
|
isDescending: false,
|
||||||
|
startFloor,
|
||||||
|
exitFloor: startFloor,
|
||||||
|
currentRoomIndex: 0,
|
||||||
|
roomsPerFloor: rooms,
|
||||||
|
runId: 0,
|
||||||
|
descentPeak: null,
|
||||||
|
roomResetState: {},
|
||||||
|
clearedRooms: {},
|
||||||
|
isDescentComplete: false,
|
||||||
|
golemancy: {
|
||||||
|
golemDesigns: {},
|
||||||
|
golemLoadout: [],
|
||||||
|
activeGolems: [] as RuntimeActiveGolem[],
|
||||||
|
lastSummonFloor: 0,
|
||||||
|
},
|
||||||
|
equipmentSpellStates: [],
|
||||||
|
weaponCastProgress: {},
|
||||||
|
comboHitCount: 0,
|
||||||
|
floorHitCount: 0,
|
||||||
|
meleeSwordProgress: {},
|
||||||
|
guardianShield: 0,
|
||||||
|
guardianShieldMax: 0,
|
||||||
|
guardianBarrier: 0,
|
||||||
|
guardianBarrierMax: 0,
|
||||||
|
spells: startSpells,
|
||||||
|
activityLog: [],
|
||||||
|
achievements: {
|
||||||
|
unlocked: [],
|
||||||
|
progress: {},
|
||||||
|
},
|
||||||
|
totalSpellsCast: 0,
|
||||||
|
totalDamageDealt: 0,
|
||||||
|
totalCraftsCompleted: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import type { CombatStore } from './combat-state.types';
|
|||||||
import {
|
import {
|
||||||
enterDescentMode, advanceRoomOrFloor, onEnterRoomDescend, createEnterSpireMode,
|
enterDescentMode, advanceRoomOrFloor, onEnterRoomDescend, createEnterSpireMode,
|
||||||
} from './combat-descent-actions';
|
} from './combat-descent-actions';
|
||||||
|
import { createDefaultCombatState } from './combat-reset';
|
||||||
import {
|
import {
|
||||||
onEnterLibraryRoom, tickNonCombatRoom, skipNonCombatRoom, stayLongerInRoom,
|
onEnterLibraryRoom, tickNonCombatRoom, skipNonCombatRoom, stayLongerInRoom,
|
||||||
} from './non-combat-room-actions';
|
} from './non-combat-room-actions';
|
||||||
@@ -329,18 +330,7 @@ export const useCombatStore = create<CombatStore>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
resetCombat: (startFloor: number, spellsToKeep: string[] = []) => {
|
resetCombat: (startFloor: number, spellsToKeep: string[] = []) => {
|
||||||
const startSpells = makeInitialSpells(spellsToKeep);
|
set(createDefaultCombatState(startFloor, spellsToKeep));
|
||||||
|
|
||||||
set({
|
|
||||||
currentFloor: startFloor,
|
|
||||||
floorHP: getFloorMaxHP(startFloor),
|
|
||||||
floorMaxHP: getFloorMaxHP(startFloor),
|
|
||||||
maxFloorReached: startFloor,
|
|
||||||
activeSpell: 'manaBolt',
|
|
||||||
currentAction: 'meditate',
|
|
||||||
castProgress: 0,
|
|
||||||
spells: startSpells,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user