feat: add upgrade scrolls
This commit is contained in:
@@ -26,6 +26,12 @@ export const CONSUMABLES = {
|
||||
throwable: true,
|
||||
stackable: true,
|
||||
},
|
||||
upgrade_scroll: {
|
||||
name: "Upgrade Scroll",
|
||||
textureKey: "items",
|
||||
spriteIndex: 79,
|
||||
stackable: true,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const RANGED_WEAPONS = {
|
||||
@@ -224,6 +230,19 @@ export function createArmour(
|
||||
};
|
||||
}
|
||||
|
||||
export function createUpgradeScroll(quantity = 1): ConsumableItem {
|
||||
const t = CONSUMABLES["upgrade_scroll"];
|
||||
return {
|
||||
id: "upgrade_scroll",
|
||||
name: t.name,
|
||||
type: "Consumable",
|
||||
textureKey: t.textureKey,
|
||||
spriteIndex: t.spriteIndex,
|
||||
stackable: true,
|
||||
quantity,
|
||||
};
|
||||
}
|
||||
|
||||
// Legacy export for backward compatibility during migration
|
||||
export const ITEMS = ALL_TEMPLATES;
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ export interface BaseItem {
|
||||
quantity?: number;
|
||||
stackable?: boolean;
|
||||
variant?: string; // ItemVariantId - stored as string to avoid circular imports
|
||||
upgradeLevel?: number; // Enhancement level (+1, +2, etc.)
|
||||
}
|
||||
|
||||
export interface MeleeWeaponItem extends BaseItem {
|
||||
|
||||
64
src/engine/systems/UpgradeManager.ts
Normal file
64
src/engine/systems/UpgradeManager.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { Item, WeaponItem, ArmourItem } from "../../core/types";
|
||||
|
||||
/**
|
||||
* Manages item upgrade logic for applying upgrade scrolls.
|
||||
*/
|
||||
export class UpgradeManager {
|
||||
|
||||
/**
|
||||
* Checks if an item can be upgraded (weapons and armour only).
|
||||
*/
|
||||
static canUpgrade(item: Item): boolean {
|
||||
return item.type === "Weapon" ||
|
||||
item.type === "BodyArmour" ||
|
||||
item.type === "Helmet" ||
|
||||
item.type === "Gloves" ||
|
||||
item.type === "Boots";
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies an upgrade to an item, increasing all stats by +1.
|
||||
* Returns true if successful.
|
||||
*/
|
||||
static applyUpgrade(item: Item): boolean {
|
||||
if (!this.canUpgrade(item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Increment upgrade level
|
||||
const currentLevel = item.upgradeLevel ?? 0;
|
||||
item.upgradeLevel = currentLevel + 1;
|
||||
|
||||
// Update item name with level suffix
|
||||
// Remove any existing upgrade suffix first
|
||||
const baseName = item.name.replace(/\s*\+\d+$/, "");
|
||||
item.name = `${baseName} +${item.upgradeLevel}`;
|
||||
|
||||
// Increase all numeric stats by +1
|
||||
if (item.type === "Weapon") {
|
||||
const weaponItem = item as WeaponItem;
|
||||
if (weaponItem.stats.attack !== undefined) {
|
||||
weaponItem.stats.attack += 1;
|
||||
}
|
||||
} else if (item.type === "BodyArmour" || item.type === "Helmet" ||
|
||||
item.type === "Gloves" || item.type === "Boots") {
|
||||
const armourItem = item as ArmourItem;
|
||||
if (armourItem.stats.defense !== undefined) {
|
||||
armourItem.stats.defense += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the display name for an item including upgrade level.
|
||||
*/
|
||||
static getDisplayName(item: Item): string {
|
||||
if (item.upgradeLevel && item.upgradeLevel > 0) {
|
||||
const baseName = item.name.replace(/\s*\+\d+$/, "");
|
||||
return `${baseName} +${item.upgradeLevel}`;
|
||||
}
|
||||
return item.name;
|
||||
}
|
||||
}
|
||||
66
src/engine/systems/__tests__/UpgradeManager.test.ts
Normal file
66
src/engine/systems/__tests__/UpgradeManager.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { UpgradeManager } from '../UpgradeManager';
|
||||
import { createMeleeWeapon, createArmour, createConsumable } from '../../../core/config/Items';
|
||||
import type { WeaponItem, ArmourItem } from '../../../core/types';
|
||||
|
||||
describe('UpgradeManager', () => {
|
||||
it('should correctly identify upgradeable items', () => {
|
||||
const sword = createMeleeWeapon("iron_sword");
|
||||
const armor = createArmour("leather_armor");
|
||||
const potion = createConsumable("health_potion");
|
||||
|
||||
expect(UpgradeManager.canUpgrade(sword)).toBe(true);
|
||||
expect(UpgradeManager.canUpgrade(armor)).toBe(true);
|
||||
expect(UpgradeManager.canUpgrade(potion)).toBe(false);
|
||||
});
|
||||
|
||||
it('should upgrade weapon stats and name', () => {
|
||||
const sword = createMeleeWeapon("iron_sword") as WeaponItem;
|
||||
const initialAttack = sword.stats.attack!;
|
||||
const initialName = sword.name;
|
||||
|
||||
const success = UpgradeManager.applyUpgrade(sword);
|
||||
|
||||
expect(success).toBe(true);
|
||||
expect(sword.stats.attack).toBe(initialAttack + 1);
|
||||
expect(sword.upgradeLevel).toBe(1);
|
||||
expect(sword.name).toBe(`${initialName} +1`);
|
||||
});
|
||||
|
||||
it('should upgrade armour stats and name', () => {
|
||||
const armor = createArmour("leather_armor") as ArmourItem;
|
||||
const initialDefense = armor.stats.defense!;
|
||||
const initialName = armor.name;
|
||||
|
||||
const success = UpgradeManager.applyUpgrade(armor);
|
||||
|
||||
expect(success).toBe(true);
|
||||
expect(armor.stats.defense).toBe(initialDefense + 1);
|
||||
expect(armor.upgradeLevel).toBe(1);
|
||||
expect(armor.name).toBe(`${initialName} +1`);
|
||||
});
|
||||
|
||||
it('should handle sequential upgrades', () => {
|
||||
const sword = createMeleeWeapon("iron_sword") as WeaponItem;
|
||||
const initialAttack = sword.stats.attack!;
|
||||
const initialName = sword.name;
|
||||
|
||||
UpgradeManager.applyUpgrade(sword); // +1
|
||||
UpgradeManager.applyUpgrade(sword); // +2
|
||||
|
||||
expect(sword.stats.attack).toBe(initialAttack + 2);
|
||||
expect(sword.upgradeLevel).toBe(2);
|
||||
expect(sword.name).toBe(`${initialName} +2`);
|
||||
});
|
||||
|
||||
it('should not upgrade non-upgradeable items', () => {
|
||||
const potion = createConsumable("health_potion");
|
||||
const initialName = potion.name;
|
||||
|
||||
const success = UpgradeManager.applyUpgrade(potion);
|
||||
|
||||
expect(success).toBe(false);
|
||||
expect(potion.upgradeLevel).toBeUndefined();
|
||||
expect(potion.name).toBe(initialName);
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
createConsumable,
|
||||
createMeleeWeapon,
|
||||
createRangedWeapon,
|
||||
createArmour
|
||||
createArmour,
|
||||
createUpgradeScroll
|
||||
} from "../../core/config/Items";
|
||||
import { seededRandom } from "../../core/math";
|
||||
import * as ROT from "rot-js";
|
||||
@@ -62,7 +63,8 @@ export function generateWorld(floor: number, runState: RunState): { world: World
|
||||
createMeleeWeapon("iron_sword", "sharp"), // Sharp sword variant
|
||||
createConsumable("throwing_dagger", 3),
|
||||
createRangedWeapon("pistol"),
|
||||
createArmour("leather_armor", "heavy") // Heavy armour variant
|
||||
createArmour("leather_armor", "heavy"), // Heavy armour variant
|
||||
createUpgradeScroll(2) // 2 Upgrade scrolls
|
||||
] : [])
|
||||
]
|
||||
},
|
||||
|
||||
@@ -36,6 +36,12 @@ export class ItemSpriteFactory {
|
||||
sprite.setScale(scale);
|
||||
container.add(sprite);
|
||||
|
||||
// Add upgrade level badge if item has been upgraded
|
||||
if (item.upgradeLevel && item.upgradeLevel > 0) {
|
||||
const badge = this.createUpgradeBadge(scene, item.upgradeLevel, scale);
|
||||
container.add(badge);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -109,6 +115,31 @@ export class ItemSpriteFactory {
|
||||
return variant?.glowColor ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a badge displaying the upgrade level (e.g., "+1").
|
||||
*/
|
||||
private static createUpgradeBadge(
|
||||
scene: Phaser.Scene,
|
||||
level: number,
|
||||
scale: number
|
||||
): Phaser.GameObjects.Text {
|
||||
// Position at top-right corner, slightly inset
|
||||
const offset = 5 * scale;
|
||||
|
||||
// Level text with strong outline for readability without background
|
||||
const text = scene.add.text(offset, -offset, `+${level}`, {
|
||||
fontSize: `${9 * scale}px`,
|
||||
color: "#ffd700",
|
||||
fontStyle: "bold",
|
||||
fontFamily: "monospace",
|
||||
stroke: "#000000",
|
||||
strokeThickness: 3
|
||||
});
|
||||
text.setOrigin(0.5);
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an item has a variant with a glow.
|
||||
*/
|
||||
|
||||
@@ -22,6 +22,8 @@ import GameUI from "../ui/GameUI";
|
||||
import { CameraController } from "./systems/CameraController";
|
||||
import { ItemManager } from "./systems/ItemManager";
|
||||
import { TargetingSystem } from "./systems/TargetingSystem";
|
||||
import { UpgradeManager } from "../engine/systems/UpgradeManager";
|
||||
import { InventoryOverlay } from "../ui/components/InventoryOverlay";
|
||||
|
||||
export class GameScene extends Phaser.Scene {
|
||||
private world!: World;
|
||||
@@ -229,6 +231,42 @@ export class GameScene extends Phaser.Scene {
|
||||
return;
|
||||
}
|
||||
|
||||
// Upgrade Scroll Logic
|
||||
if (item.id === "upgrade_scroll") {
|
||||
const uiScene = this.scene.get("GameUI") as GameUI;
|
||||
// Access the public inventory component
|
||||
const inventoryOverlay = uiScene.inventory;
|
||||
|
||||
if (inventoryOverlay && inventoryOverlay instanceof InventoryOverlay) {
|
||||
// Trigger upgrade mode
|
||||
inventoryOverlay.enterUpgradeMode((targetItem: any) => {
|
||||
const success = UpgradeManager.applyUpgrade(targetItem);
|
||||
if (success) {
|
||||
// Consume scroll logic handling stacking
|
||||
const scrollItem = player.inventory?.items.find(it => it.id === "upgrade_scroll");
|
||||
if (scrollItem) {
|
||||
if (scrollItem.stackable && scrollItem.quantity && scrollItem.quantity > 1) {
|
||||
scrollItem.quantity--;
|
||||
} else {
|
||||
this.itemManager.removeFromInventory(player, "upgrade_scroll");
|
||||
}
|
||||
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Upgraded!", "#ffd700");
|
||||
}
|
||||
|
||||
inventoryOverlay.cancelUpgradeMode();
|
||||
this.emitUIUpdate();
|
||||
this.commitPlayerAction({ type: "wait" });
|
||||
} else {
|
||||
// Should technically be prevented by UI highlights, but safety check
|
||||
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Cannot upgrade!", "#ff0000");
|
||||
}
|
||||
});
|
||||
|
||||
this.dungeonRenderer.showFloatingText(player.pos.x, player.pos.y, "Select Item to Upgrade", "#ffffff");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const result = this.itemManager.handleUse(data.itemId, player);
|
||||
|
||||
if (result.success && result.consumed) {
|
||||
|
||||
@@ -9,6 +9,14 @@ export class InventoryOverlay extends OverlayComponent {
|
||||
private dragIcon: Phaser.GameObjects.Sprite | null = null;
|
||||
private draggedItemIndex: number | null = null;
|
||||
private draggedEquipmentKey: string | null = null;
|
||||
private isDragging = false;
|
||||
private dragPayload: any = null;
|
||||
private cachedPlayer: CombatantActor | null = null; // Cache player for local methods
|
||||
|
||||
// Upgrade Mode
|
||||
public isUpgradeMode = false;
|
||||
private onUpgradeSelect?: (item: any) => void;
|
||||
|
||||
private tooltip: Phaser.GameObjects.Container | null = null;
|
||||
private tooltipName: Phaser.GameObjects.Text | null = null;
|
||||
private tooltipStats: Phaser.GameObjects.Text | null = null;
|
||||
@@ -35,6 +43,23 @@ export class InventoryOverlay extends OverlayComponent {
|
||||
this.createEquipmentPanel();
|
||||
this.createBackpackPanel();
|
||||
this.createTooltip();
|
||||
|
||||
// Global input listener to cancel upgrade mode on click outside
|
||||
this.scene.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||
// Only check if visible and in upgrade mode
|
||||
if (this.container.visible && this.isUpgradeMode) {
|
||||
this.handleUpgradeClick(pointer);
|
||||
|
||||
// If clicking outside both panels, cancel
|
||||
const overBackpack = this.getBackpackSlotAt(pointer.x, pointer.y) !== null;
|
||||
const overEquip = this.getEquipmentSlotAt(pointer.x, pointer.y) !== null;
|
||||
|
||||
if (!overBackpack && !overEquip) {
|
||||
console.log("Clicked outside - cancelling (DEBUG: DISABLED to fix interaction)");
|
||||
// this.cancelUpgradeMode();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private createTooltip() {
|
||||
@@ -309,6 +334,7 @@ export class InventoryOverlay extends OverlayComponent {
|
||||
}
|
||||
|
||||
update(player: CombatantActor) {
|
||||
this.cachedPlayer = player;
|
||||
if (!player.inventory) return;
|
||||
|
||||
// Clear existing items from backpack slots
|
||||
@@ -361,8 +387,25 @@ export class InventoryOverlay extends OverlayComponent {
|
||||
slot.setData("equipmentKey", undefined); // Explicitly clear to avoid confusion
|
||||
this.scene.input.setDraggable(slot);
|
||||
|
||||
slot.on("pointerdown", () => {
|
||||
console.log("Clicked item:", item);
|
||||
// Clear previous listeners to avoid accumulation
|
||||
slot.removeAllListeners("pointerdown");
|
||||
slot.removeAllListeners("pointerover");
|
||||
slot.removeAllListeners("pointerout");
|
||||
|
||||
slot.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
|
||||
if (this.isUpgradeMode && this.onUpgradeSelect) {
|
||||
if (item && (item.type === "Weapon" || item.type === "BodyArmour" || item.type === "Helmet" || item.type === "Gloves" || item.type === "Boots")) {
|
||||
this.onUpgradeSelect(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Right click to use item
|
||||
if (pointer.rightButtonDown()) {
|
||||
const gameScene = this.scene.scene.get("GameScene");
|
||||
gameScene.events.emit("use-item", { itemId: item.id });
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
slot.on("pointerover", (pointer: Phaser.Input.Pointer) => {
|
||||
@@ -391,6 +434,19 @@ export class InventoryOverlay extends OverlayComponent {
|
||||
slot.setData("equipmentKey", key);
|
||||
this.scene.input.setDraggable(slot);
|
||||
|
||||
// Clear previous listeners
|
||||
slot.removeAllListeners("pointerdown");
|
||||
slot.removeAllListeners("pointerover");
|
||||
slot.removeAllListeners("pointerout");
|
||||
|
||||
slot.on("pointerdown", () => {
|
||||
if (this.isUpgradeMode && this.onUpgradeSelect) {
|
||||
// All equipped items in valid slots are upgradeable by definition
|
||||
this.onUpgradeSelect(item);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
slot.on("pointerover", (pointer: Phaser.Input.Pointer) => {
|
||||
this.showTooltip(item, pointer.x, pointer.y);
|
||||
});
|
||||
@@ -440,6 +496,7 @@ export class InventoryOverlay extends OverlayComponent {
|
||||
}
|
||||
|
||||
private clearHighlights() {
|
||||
// Reset Equipment Slots
|
||||
this.equipmentSlots.forEach((container, key) => {
|
||||
const graphics = container.list[0] as Phaser.GameObjects.Graphics;
|
||||
if (graphics) {
|
||||
@@ -456,12 +513,142 @@ export class InventoryOverlay extends OverlayComponent {
|
||||
|
||||
graphics.fillStyle(slotBg, 1);
|
||||
graphics.fillRect(-size / 2 + 3, -size / 2 + 3, size - 6, size - 6);
|
||||
|
||||
// Allow interactions again if they were disabled (though we don't disable them currently)
|
||||
container.setAlpha(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Reset Backpack Slots
|
||||
this.backpackSlots.forEach(container => {
|
||||
const graphics = container.list[0] as Phaser.GameObjects.Graphics;
|
||||
if (graphics) {
|
||||
graphics.clear();
|
||||
const slotSize = 44;
|
||||
const slotBorder = 0xd4af37;
|
||||
const slotBg = 0x1a0f1a; // Darker bg for backpack
|
||||
|
||||
graphics.lineStyle(2, slotBorder, 1);
|
||||
graphics.strokeRect(-slotSize / 2, -slotSize / 2, slotSize, slotSize);
|
||||
|
||||
graphics.lineStyle(1, 0x8b7355, 1);
|
||||
graphics.strokeRect(-slotSize / 2 + 2, -slotSize / 2 + 2, slotSize - 4, slotSize - 4);
|
||||
|
||||
graphics.fillStyle(slotBg, 1);
|
||||
graphics.fillRect(-slotSize / 2 + 3, -slotSize / 2 + 3, slotSize - 6, slotSize - 6);
|
||||
|
||||
container.setAlpha(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters upgrade mode, highlighting upgradeable items.
|
||||
*/
|
||||
enterUpgradeMode(onSelect: (item: any) => void) {
|
||||
this.isUpgradeMode = true;
|
||||
this.onUpgradeSelect = onSelect;
|
||||
|
||||
// Highlight all upgradeable items
|
||||
this.highlightUpgradeableItems();
|
||||
}
|
||||
|
||||
cancelUpgradeMode() {
|
||||
this.isUpgradeMode = false;
|
||||
this.onUpgradeSelect = undefined;
|
||||
this.clearHighlights();
|
||||
}
|
||||
|
||||
private highlightUpgradeableItems() {
|
||||
if (!this.cachedPlayer || !this.cachedPlayer.inventory) return;
|
||||
|
||||
// Green highlight for upgradeable items
|
||||
this.backpackSlots.forEach((slot, index) => {
|
||||
const item = this.cachedPlayer!.inventory!.items[index];
|
||||
|
||||
let bg = slot.list.find(c => c instanceof Phaser.GameObjects.Graphics) as Phaser.GameObjects.Graphics;
|
||||
// In this complex container setup, the bg is the first child of the slot container's first child
|
||||
// Actually looking at createBackpackPanel, the slot IS a container with [Graphics] in it.
|
||||
// The Graphics draws border and bg.
|
||||
// We should clear and redraw highlighted
|
||||
|
||||
if (item && (item.type === "Weapon" || item.type === "BodyArmour" || item.type === "Helmet" || item.type === "Gloves" || item.type === "Boots")) {
|
||||
this.drawSlotHighlight(slot, true, 44);
|
||||
} else {
|
||||
this.drawSlotDim(slot, 44);
|
||||
}
|
||||
});
|
||||
|
||||
this.equipmentSlots.forEach((slot, key) => {
|
||||
const item = (this.cachedPlayer!.equipment as any)?.[key];
|
||||
const size = (key === "bodyArmour") ? 58 : (key === "belt") ? 32 : (key === "boots") ? 46 : (key.startsWith("ring")) ? 38 : 46;
|
||||
|
||||
if (item) {
|
||||
this.drawSlotHighlight(slot, true, size);
|
||||
} else {
|
||||
this.drawSlotDim(slot, size);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private drawSlotHighlight(slot: Phaser.GameObjects.Container, active: boolean, size: number) {
|
||||
const g = slot.list[0] as Phaser.GameObjects.Graphics;
|
||||
if (g) {
|
||||
g.clear();
|
||||
// Highlight border
|
||||
g.lineStyle(2, 0x00ff00, 1);
|
||||
g.strokeRect(-size / 2, -size / 2, size, size);
|
||||
|
||||
g.lineStyle(1, 0x00aa00, 1);
|
||||
g.strokeRect(-size / 2 + 2, -size / 2 + 2, size - 4, size - 4);
|
||||
|
||||
g.fillStyle(0x1a2f1a, 1);
|
||||
g.fillRect(-size / 2 + 3, -size / 2 + 3, size - 6, size - 6);
|
||||
}
|
||||
}
|
||||
|
||||
private drawSlotDim(slot: Phaser.GameObjects.Container, size: number) {
|
||||
const g = slot.list[0] as Phaser.GameObjects.Graphics;
|
||||
if (g) {
|
||||
g.setAlpha(0.3);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle clicks for upgrade selection
|
||||
private handleUpgradeClick(pointer: Phaser.Input.Pointer) {
|
||||
if (!this.isUpgradeMode || !this.onUpgradeSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check backpack
|
||||
const backpackIndex = this.getBackpackSlotAt(pointer.x, pointer.y);
|
||||
if (backpackIndex !== null && this.cachedPlayer && this.cachedPlayer.inventory) {
|
||||
const item = this.cachedPlayer.inventory.items[backpackIndex];
|
||||
// Reuse eligibility check
|
||||
if (item && (item.type === "Weapon" || item.type === "BodyArmour" || item.type === "Helmet" || item.type === "Gloves" || item.type === "Boots")) {
|
||||
this.onUpgradeSelect(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check equipment
|
||||
const equipSlot = this.getEquipmentSlotAt(pointer.x, pointer.y);
|
||||
if (equipSlot !== null && this.cachedPlayer && this.cachedPlayer.equipment) {
|
||||
const item = (this.cachedPlayer.equipment as any)[equipSlot];
|
||||
if (item) { // All equipped items are upgradeable types
|
||||
this.onUpgradeSelect(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private setupDragEvents() {
|
||||
this.scene.input.on("dragstart", (pointer: Phaser.Input.Pointer, gameObject: any) => {
|
||||
// Handle Upgrade Mode clicks (prevent drag)
|
||||
if (this.isUpgradeMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gameScene = this.scene.scene.get("GameScene") as any;
|
||||
const player = gameScene.world.actors.get(gameScene.playerId);
|
||||
if (!player) return;
|
||||
|
||||
Reference in New Issue
Block a user