import { RunService, Workspace } from "@rbxts/services";
import { InstanceComponent } from "engine/shared/component/InstanceComponent";
import { C2SRemoteEvent } from "engine/shared/event/PERemoteEvent";
import { ReplicatedAssets } from "shared/ReplicatedAssets";
import { ModuleCollection } from "shared/weaponProjectiles/WeaponModuleSystem";
import type { damageType } from "engine/shared/BlockDamageController";

export type modifierValue = {
	isRelative?: boolean;
	value: number;
};

export type projectileModifier = Partial<
	Record<keyof damageType, modifierValue> & {
		speedModifier: modifierValue; //<-- velocity modifier
		lifetimeModifier: modifierValue; //<--- time modifier
	}
>;

export type baseWeaponProjectile = {
	Projectile: BasePart;
} & Model;

const CANNON_SHELL = ReplicatedAssets.waitForAsset<baseWeaponProjectile>("WeaponProjectiles", "ShellProjectile");
const PLASMA_BALL = ReplicatedAssets.waitForAsset<baseWeaponProjectile>("WeaponProjectiles", "PlasmaProjectile");
const BULLET = ReplicatedAssets.waitForAsset<baseWeaponProjectile>("WeaponProjectiles", "BulletProjectile");
const LASER = ReplicatedAssets.waitForAsset<baseWeaponProjectile>("WeaponProjectiles", "LaserProjectile");

const projectileFolder = new Instance("Folder", Workspace);
projectileFolder.Name = "Projectiles";

export type DamageType = "KINETIC" | "EXPLOSIVE" | "ENERGY";

export class WeaponProjectile extends InstanceComponent<BasePart> {
	static readonly damageInstance = new C2SRemoteEvent<{
		readonly part: BasePart;
		readonly damage: number;
		readonly modifiers: projectileModifier[];
	}>("projectile_damage", "RemoteEvent");

	rawModifiers: projectileModifier[] = [];
	totalEffect: projectileModifier = {};
	originalLifetime: number | undefined;
	modifiedLifetime: number | undefined;
	currentLifetime: number = 0;
	modifiedVelocity: Vector3;

	readonly projectilePart: BasePart;
	readonly originalProjectileModel;
	static readonly SHELL_PROJECTILE: baseWeaponProjectile = CANNON_SHELL;
	static readonly PLASMA_PROJECTILE: baseWeaponProjectile = PLASMA_BALL;
	static readonly LASER_PROJECTILE: baseWeaponProjectile = LASER;
	static readonly BULLET_PROJECTILE: baseWeaponProjectile = BULLET;

	constructor(
		public startPosition: Vector3,
		readonly projectileType: DamageType,
		originalProjectileModel: baseWeaponProjectile,
		public baseVelocity: Vector3,
		public baseDamage: number,
		readonly baseModifier: projectileModifier,
		lifetime?: number, //<--- seconds
		public color?: Color3,
	) {
		const pmodel: baseWeaponProjectile = originalProjectileModel.Clone();
		const newModel = pmodel.Projectile;
		newModel.Position = startPosition;
		newModel.CanCollide = false;
		newModel.CanTouch = true;
		newModel.Massless = true;
		//newModel.CollisionGroup = "Projectile";
		//newModel.EnableFluidForces = false;
		newModel.AssemblyLinearVelocity = baseVelocity;
		//transform projectile and shit
		//ELONgate the projectile to avoid clipping
		super(newModel);
		this.projectilePart = newModel;
		this.originalLifetime = this.modifiedLifetime = lifetime;
		this.projectilePart.PivotTo(
			CFrame.lookAlong(this.projectilePart.Position, (this.modifiedVelocity = baseVelocity)),
		);
		this.originalProjectileModel = pmodel;
		if (color) pmodel.PrimaryPart!.Color = color;
		pmodel.Parent = projectileFolder;

		this.event.subscribe(this.projectilePart.Touched, (part) => {
			if (part.CollisionGroup === this.projectilePart.CollisionGroup) return;
			this.onHit(part, this.projectilePart?.Position ?? part.Position);
		});
		this.event.subscribe(RunService.PostSimulation, (dt) => {
			const percentage = this.modifiedLifetime === undefined ? 0 : this.currentLifetime / this.modifiedLifetime;
			const reversePercentage = 1 - percentage;
			if (percentage >= 1) return this.destroy();
			this.onTick(dt, percentage, reversePercentage);
		});

		this.onDestroy(() => pmodel.Destroy());

		this.enable();
		recalculateEffects(this);

		if (this.totalEffect.speedModifier) {
			if (this.totalEffect.speedModifier.isRelative)
				newModel.AssemblyLinearVelocity = baseVelocity.mul(this.totalEffect.speedModifier.value);
			else
				newModel.AssemblyLinearVelocity = baseVelocity.add(
					baseVelocity.Unit.mul(this.totalEffect.speedModifier!.value),
				);
		}
	}

	addModifier(...modifiers: projectileModifier[]) {
		for (const mod of modifiers) this.rawModifiers.push(mod);
	}

	onHit(part: BasePart, point: Vector3, destroyOnHit = false): void {
		if (!part.Anchored && !RunService.IsServer())
			WeaponProjectile.damageInstance.send({
				part,
				damage: this.baseDamage,
				modifiers: [this.baseModifier, ...this.rawModifiers],
			});
		if (destroyOnHit) this.destroy();
	}

	onTick(dt: number, percentage: number, reversePercentage: number): void {
		if (this.isDestroyed()) return;
		this.currentLifetime += dt;
		/* :thinking:
		this.projectilePart.CFrame = this.projectilePart.CFrame.add(
			this.modifiedVelocity.mul(new Vector3(dt, dt, dt)),
		);*/
	}
}

function recalculateEffects(projectile: WeaponProjectile) {
	projectile.totalEffect =
		ModuleCollection.calculateTotalModifier([projectile.baseModifier, ...projectile.rawModifiers]) ?? {};

	if (projectile.originalLifetime !== undefined)
		projectile.modifiedLifetime =
			projectile.originalLifetime * (projectile.totalEffect.lifetimeModifier?.value ?? 1);
}

function applyDamageToPart(part: BasePart, damage: number, modifiers: projectileModifier[]) {
	const totalEffect = ModuleCollection.calculateTotalModifier(modifiers) ?? {};

	// todo: implement stuff from the damage system
	// just pass the object to the .applyDamage method
}

/*
	To fire something, you need:
	1. something to load ammo in
	- magazine
	- accumulator (laser, plasma)
	- gas tank (plasma)
	- rocket battery

	2. something to fire loaded
	3. something to modify fired

	kinetic:
		magazine -> some loader (?) -> barrel -> nozzle -> world -> some sparks in the hit spots
		- different calibers?
	rocket:
		???

	bomb:
		built by player -> installed into holder -> released on command

	laser:
		acumulator -> emitter -> some lenses -> focal lens nozzle -> world -> light source until stopped firing

	plasma:
		??? -> emitter (?) -> accelerator magnet (?) -> nozzle -> world -> flash of colored light on hit
		- Strength depends on distance traveled from the emitter
*/


import { Players, Workspace } from "@rbxts/services";
import { C2CRemoteEvent } from "engine/shared/event/PERemoteEvent";
import { WeaponProjectile } from "shared/weaponProjectiles/BaseProjectileLogic";
import type { baseWeaponProjectile, projectileModifier } from "shared/weaponProjectiles/BaseProjectileLogic";

type laserVisualsAmountConstant = 1 | 2 | 3 | 4 | 5;
type laser = baseWeaponProjectile & Record<`LaserProjectileVisual${laserVisualsAmountConstant}`, BasePart>;

export class LaserProjectile extends WeaponProjectile {
	static projectileMap = new Map<Instance, LaserProjectile>();
	static readonly spawnProjectile = new C2CRemoteEvent<{
		readonly color: Color3;
		readonly originPart: BasePart;
		readonly baseDamage: number;
		readonly modifier: projectileModifier;
		readonly owner: Player;
	}>("laser_spawn", "RemoteEvent");

	static readonly destroyProjectile = new C2CRemoteEvent<{
		readonly originPart: BasePart;
	}>("laser_destroy", "RemoteEvent");

	private detectionlessSize = new Vector3(1024, this.projectilePart.Size.Y, this.projectilePart.Size.Z);
	private laserModel: BasePart[] = [];
	private damage;

	constructor(
		private originPart: BasePart,
		baseDamage: number,
		modifier: projectileModifier,
		color: Color3,
		private owner: Player,
	) {
		super(
			originPart.CFrame.Position,
			"ENERGY",
			WeaponProjectile.LASER_PROJECTILE,
			originPart.Rotation,
			baseDamage,
			modifier,
		);
		this.projectilePart.Transparency = 1;
		this.projectilePart.Size = Vector3.one;
		this.damage = this.baseDamage;
		const p = this.originalProjectileModel as laser;
		for (let i = 1; i <= 5; i++) {
			const pr = p[`LaserProjectileVisual${i as laserVisualsAmountConstant}`];
			this.laserModel.push(pr);
			pr.Color = color;
		}
	}

	onTick(dt: number, percentage: number, reversePercentage: number): void {
		const pivo = this.originPart.GetPivot();
		const forwardVector = pivo.XVector.mul(-1);
		this.startPosition = pivo.Position;
		this.projectilePart.PivotTo(pivo);

		let res;
		let iter = 0;
		const length = this.laserModel.size();
		for (iter = 0; iter < length; iter++) {
			const posOffset = 1024 * iter;
			res = Workspace.Shapecast(this.projectilePart, forwardVector.mul(1023));
			this.laserModel[iter].Transparency = 0;
			this.laserModel[iter].PivotTo(this.projectilePart.CFrame);
			if (res === undefined) {
				this.laserModel[iter].Size = this.detectionlessSize;
				this.laserModel[iter].Position = forwardVector.mul(512 + posOffset).add(this.startPosition);
				continue;
			}

			this.laserModel[iter].Size = new Vector3(
				res.Distance,
				this.projectilePart.Size.Y,
				this.projectilePart.Size.Z,
			);
			this.laserModel[iter].Position = forwardVector.mul(res.Distance / 2 + posOffset).add(this.startPosition);
			break;
		}

		// скрыть неактивные части
		for (let i = math.min(iter + 1, length - 1); i < length; i++) this.laserModel[i].Transparency = 1;

		//нанести дамаг
		if (res && Players.LocalPlayer === this.owner) {
			this.baseDamage = this.damage * dt;
			super.onHit(res.Instance, res.Position);
		}
		super.onTick(dt, percentage, reversePercentage);
	}
}

LaserProjectile.spawnProjectile.invoked.Connect(({ color, originPart, baseDamage, modifier, owner }) => {
	print("Laser spawned");
	const v = LaserProjectile.projectileMap.get(originPart);
	if (v !== undefined) {
		v.destroy();
		LaserProjectile.projectileMap.delete(originPart);
	}
	LaserProjectile.projectileMap.set(originPart, new LaserProjectile(originPart, baseDamage, modifier, color, owner));
});

LaserProjectile.destroyProjectile.invoked.Connect(({ originPart }) => {
	print("Laser destroyed");
	const v = LaserProjectile.projectileMap.get(originPart);
	if (v !== undefined) {
		v.destroy();
		LaserProjectile.projectileMap.delete(originPart);
	}
});


import { Workspace } from "@rbxts/services";
import { Easing } from "engine/shared/component/Easing";
import { C2CRemoteEvent } from "engine/shared/event/PERemoteEvent";
import { WeaponProjectile } from "shared/weaponProjectiles/BaseProjectileLogic";
import type { modifierValue, projectileModifier } from "shared/weaponProjectiles/BaseProjectileLogic";

type palsmaProjectile = BasePart & { VectorForce: VectorForce };

export class PlasmaProjectile extends WeaponProjectile {
	private startSize = this.projectilePart.Size;
	private readonly vectorForce: VectorForce;
	static readonly spawnProjectile = new C2CRemoteEvent<{
		readonly startPosition: Vector3;
		readonly baseVelocity: Vector3;
		readonly baseDamage: number;
		readonly modifier: projectileModifier;
		readonly color?: Color3;
	}>("plasma_spawn", "RemoteEvent");

	constructor(
		startPosition: Vector3,
		baseVelocity: Vector3,
		baseDamage: number,
		modifier: projectileModifier,
		color?: Color3,
	) {
		super(
			startPosition,
			"ENERGY",
			WeaponProjectile.PLASMA_PROJECTILE,
			baseVelocity,
			baseDamage,
			modifier,
			5,
			color,
		);

		this.vectorForce = (this.projectilePart as unknown as palsmaProjectile).VectorForce;
		this.updateLifetimeModifier(1);
	}

	/**
	 * The projectile gets weaker with time!
	 */
	private updateLifetimeModifier(percentage: number) {
		const nv: modifierValue = { value: percentage, isRelative: true };
		this.rawModifiers[0] = {
			speedModifier: nv,
			heatDamage: nv,
			impactDamage: nv,
			explosiveDamage: nv,
		};
	}

	onHit(part: BasePart, point: Vector3): void {
		const startedWithSize = this.projectilePart.Size;
		this.projectilePart.AssemblyLinearVelocity = Vector3.zero;
		this.projectilePart.Anchored = true;
		this.projectilePart.CanCollide = false;
		this.projectilePart.CanTouch = false;
		this.disable();
		this.projectilePart.Position = this.projectilePart.CFrame.PointToWorldSpace(
			new Vector3(0, startedWithSize.Y / 2, 0),
		);
		//point === hit position (at least should be)

		task.spawn(() => {
			const time = 0.7;
			const startTime = os.clock() / time;
			while (startTime > os.clock() / time - 1) {
				const sz = Easing.ease(os.clock() / time - startTime, "Quint", "Out");
				const revSz = 1 - sz;
				this.projectilePart.Transparency = math.sqrt(sz);
				this.projectilePart.Size = new Vector3(
					sz * startedWithSize.Y,
					math.max(revSz * startedWithSize.Y, 0.1),
					sz * startedWithSize.Y,
				);
				task.wait();
			}

			super.onHit(part, point, true);
		});
	}

	onTick(dt: number, percentage: number, reversePercentage: number): void {
		super.onTick(dt, percentage, reversePercentage);
		//this.projectilePart.AssemblyLinearVelocity = this.baseVelocity;
		this.projectilePart.Transparency = percentage;
		this.updateLifetimeModifier(reversePercentage);
		this.projectilePart.Size = this.startSize.mul(new Vector3(1, 1 + this.baseVelocity.Magnitude / 100, 1));
		this.vectorForce.Force = new Vector3(0, this.projectilePart.Mass * Workspace.Gravity, 0);
	}
}

PlasmaProjectile.spawnProjectile.invoked.Connect(({ startPosition, baseVelocity, baseDamage, modifier, color }) => {
	print("Plasma ball spawned");
	new PlasmaProjectile(startPosition, baseVelocity, baseDamage, modifier, color);
});


import { Workspace } from "@rbxts/services";
import { HostedService } from "engine/shared/di/HostedService";
import { BlockManager } from "shared/building/BlockManager";
import type { SharedPlots } from "shared/building/SharedPlots";
import type { modifierValue, projectileModifier } from "shared/weaponProjectiles/BaseProjectileLogic";

type weaponMarker = {
	markerInstance: BasePart;
	occupiedWith: {
		module: WeaponModule | undefined;
		block: PlacedBlockData | Block | undefined;
	};
};

type markerName = string;
type uuid = string;
type recalcOut = {
	module: WeaponModule;
	extraModifier?: projectileModifier;
	activeOutputs: weaponMarker[];
};

export class WeaponModule {
	static readonly allModules: Record<uuid, WeaponModule> = {};
	readonly block: Block;
	readonly instance: BlockModel;

	readonly allMarkers = new Map<markerName, weaponMarker>();

	pregeneratedCollection: ModuleCollection = new ModuleCollection(this);
	parentCollection: ModuleCollection = this.pregeneratedCollection;

	constructor(
		placedBlock: PlacedBlockData,
		private readonly blockList: BlockList,
	) {
		this.block = blockList.blocks[placedBlock.id]!;
		this.instance = placedBlock.instance;

		const fm = (placedBlock.instance.WaitForChild("moduleMarkers")?.GetChildren() ?? []) as BasePart[];
		const configMarkers = new Set();
		for (const [k, v] of pairs(this.block.weaponConfig!.markers)) {
			configMarkers.add(k);
		}

		for (const m of fm) {
			if (!configMarkers.has(m.Name)) throw `Weapon marker "${m.Name}" not found`;
			this.allMarkers.set(m.Name, {
				markerInstance: m,
				occupiedWith: {
					block: undefined,
					module: undefined,
				},
			});
		}

		if (this.block.weaponConfig) WeaponModule.allModules[placedBlock.uuid] = this;
		this.parentCollection.init();
	}

	getModuleMarkers() {
		const res = [];
		for (const [k, v] of pairs(this.allMarkers)) res.push(v);
		return res;
	}

	update() {
		//get all colided marker touches with
		//iterate through the touched parts
		//	if occupiedWithBlock !== undefined
		//	if a block
		//	if the same type
		//	set module
		const foundMarkers = this.allMarkers;
		const configMarkers = this.block.weaponConfig!.markers;
		const params = new OverlapParams();
		params.CollisionGroup = "Blocks";
		params.FilterType = Enum.RaycastFilterType.Exclude;
		params.AddToFilter(this.instance);
		if (this.instance.PrimaryPart) params.AddToFilter(this.instance.PrimaryPart);

		const allCollidedCollections: Set<ModuleCollection> = new Set();
		for (const [k, v] of pairs(configMarkers)) {
			const marker = foundMarkers.get(k)!;
			const touching = Workspace.GetPartsInPart(marker.markerInstance, params);

			marker.occupiedWith.block = undefined;
			marker.occupiedWith.module = undefined;

			for (const t of touching) {
				const touchingBlock = BlockManager.getBlockDataByPart(t); //get the first one
				marker.occupiedWith.block = touchingBlock;
				if (!touchingBlock) continue;
				const mod = WeaponModule.allModules[touchingBlock.uuid];
				if (!mod) continue;
				const config = this.block.weaponConfig!.markers[k];

				//check if the id of the block is the same as allowed for this module
				if (config.allowedBlockIds === undefined) continue;
				if (config.allowedBlockIds.indexOf(mod.block.id) < 0) continue;
				marker.occupiedWith.module = mod;

				if (marker.occupiedWith.module.parentCollection !== this.parentCollection)
					allCollidedCollections.add(marker.occupiedWith.module.parentCollection);

				break;
			}
		}

		this.parentCollection.combineWithModuleCollections(...allCollidedCollections);
	}
}

export class ModuleCollection {
	readonly modules: Set<WeaponModule> = new Set();
	readonly emitters: Set<WeaponModule> = new Set();
	readonly calculatedOutputs: {
		module: WeaponModule;
		outputs: weaponMarker[];
		modifier: projectileModifier;
	}[] = [];

	constructor(readonly mainModule: WeaponModule) {
		this.modules.add(mainModule);
	}

	init() {
		if (this.mainModule.block.weaponConfig!.type === "CORE") this.emitters.add(this.mainModule);
	}

	combineWithModules(...another: WeaponModule[]) {
		const parentCollections = new Set<ModuleCollection>();
		for (const k of another) parentCollections.add(k.parentCollection);

		this.combineWithModuleCollections(...parentCollections);
	}

	combineWithModuleCollections(...collections: ModuleCollection[]) {
		for (const c of collections) {
			if (c === this) continue;
			for (const k of c.modules) {
				k.parentCollection = this;
				this.modules.add(k);
				if (k.block.weaponConfig!.type === "CORE") this.emitters.add(k);
			}
		}
	}

	removeModules(...another: WeaponModule[]) {
		for (const m of another) {
			m.parentCollection = m.pregeneratedCollection;
			this.modules.delete(m);
			this.emitters.delete(m);
		}
	}

	setMarkersVisibility(isVisible: boolean) {
		isVisible = !isVisible;
		for (const m of this.modules) {
			for (const o of m.getModuleMarkers()) {
				o.markerInstance.Anchored = isVisible;
				o.markerInstance.Transparency = isVisible ? 1 : 0;
			}
		}
	}

	recursivePath(
		outputArray: recalcOut[][],
		nextModule: WeaponModule,
		path: recalcOut[] = [],
	): recalcOut[] | undefined {
		//check if there's a loop
		for (const p of path) if (p.module === nextModule) return;

		const connectedModules: WeaponModule[] = [];
		const activeOutputs: weaponMarker[] = [];

		//get all markers
		for (const [n, e] of pairs(nextModule.allMarkers)) {
			if (e.occupiedWith.module) {
				//get marker rotation
				const [x1, y1, z1] = e.markerInstance.GetPivot().ToEulerAnglesXYZ();
				const markerRotation = new Vector3(x1, y1, z1);

				//get module rotation
				const [x2, y2, z2] = e.occupiedWith.module.instance.GetPivot().ToEulerAnglesXYZ();
				const moduleRotation = new Vector3(x2, y2, z2);

				//get offset in degrees
				const hardcodedRotationOffset = 5;
				const offset = moduleRotation
					.sub(markerRotation)
					.Abs()
					.apply((v) => math.deg(v));

				// print(offset);
				// //add module if offset is lower than "hardcodedRotationOffset"
				if (
					offset.X <= hardcodedRotationOffset &&
					offset.Y <= hardcodedRotationOffset &&
					offset.Z <= hardcodedRotationOffset
				)
					connectedModules.push(e.occupiedWith.module);

				continue;
			}

			if (!e.occupiedWith.block && nextModule.block.weaponConfig!.markers[n].emitsProjectiles) {
				activeOutputs.push(e);
				// print(e);
			}
		}

		const obj: recalcOut = {
			module: nextModule,
			activeOutputs,
		};

		// print(obj.activeOutputs);
		// add modifier because outputs split, i.e. divide output between modules
		if (connectedModules.size() > 0) {
			const baseModifierValue: modifierValue = { value: 1 / activeOutputs.size(), isRelative: true };
			obj.extraModifier = {
				speedModifier: baseModifierValue,
				lifetimeModifier: baseModifierValue,
				heatDamage: baseModifierValue,
				impactDamage: baseModifierValue,
				explosiveDamage: baseModifierValue,
			};
		}

		//if size === 0 then there's only one block
		// therefore just stop iterations on it
		if (path.size() + connectedModules.size() === 0) {
			outputArray.push([obj]);
			return;
		}
		// print(path);
		//just add last module to the path at this point
		path.push(obj);

		//if there are modules attached to the markers
		if (connectedModules.size() === 0) return path;

		for (const e of connectedModules) {
			const p = this.recursivePath(outputArray, e, [...path]);
			if (!p) continue;
			outputArray.push(p);
		}
		return;
	}

	static calculateTotalModifier(modifiers: projectileModifier[]): projectileModifier | undefined {
		if (modifiers.size() === 0) return;
		const result: projectileModifier = {};

		for (const m of modifiers) {
			for (const [k, v] of pairs(m)) {
				result[k] ??= { value: 0, isRelative: v.isRelative ?? false };
				if ((v.isRelative ?? false) !== result[k].isRelative) {
					result[k].value *= v.value;
					continue;
				}
				result[k].value += v.value;
			}
		}

		return result;
	}

	recalc() {
		const paths: recalcOut[][] = [];
		for (const e of this.emitters) this.recursivePath(paths, e);
		// print("paths:", paths);
		this.calculatedOutputs.clear();

		const getUpgrades = (a: WeaponModule): projectileModifier[] => {
			const result: projectileModifier[] = [];
			const upgradePaths: recalcOut[][] = [];
			this.recursivePath(upgradePaths, a);

			for (const upgradePath of upgradePaths) {
				const buf: projectileModifier[] = [];
				for (const m of upgradePath) {
					if (m.module.block.weaponConfig?.type !== "UPGRADE") continue;
					buf.push(m.module.block.weaponConfig!.modifier);
					if (m.extraModifier) buf.push(m.extraModifier);
				}
				const mod = ModuleCollection.calculateTotalModifier(buf);
				if (!mod) continue;
				result.push(mod);
			}

			return result;
		};

		for (const path of paths) {
			const buf: projectileModifier[] = [];
			for (const p of path) {
				//if upgrade then do not iterate trough it
				if (p.module.block!.weaponConfig!.type === "UPGRADE") continue;

				// add effect from the block itself
				buf.push(p.module.block.weaponConfig!.modifier);

				// add effects from connected upgrades
				for (const u of getUpgrades(p.module)) buf.push(u);

				//if there are no holes to shoot from then skip
				if (p.activeOutputs.size() === 0) continue;

				//otherwise add modifier
				buf.push(p.extraModifier!);

				// add the block to the list of outputs
				this.calculatedOutputs.push({
					module: p.module,
					modifier: ModuleCollection.calculateTotalModifier(buf) ?? {},
					outputs: p.activeOutputs,
				});
			}
		}
	}
}

@injectable
export class WeaponModuleSystem extends HostedService {
	constructor(@inject blockList: BlockList, @inject plots: SharedPlots) {
		super();

		function updateAll() {
			for (const [_, m] of pairs(WeaponModule.allModules)) m.update();

			const arr = new Set<ModuleCollection>();
			for (const [_, m] of pairs(WeaponModule.allModules)) {
				arr.add(m.parentCollection);
			}
			for (const c of arr) c.recalc();
		}

		for (const p of plots.plots) {
			const folder = p.instance.FindFirstChild("Blocks");
			if (folder === undefined) continue;

			this.event.subscribe(folder.ChildAdded, (block) => {
				const blockInfo = BlockManager.getBlockDataByBlockModel(block as BlockModel);
				if (!blockList.blocks[blockInfo.id]?.weaponConfig) return;
				new WeaponModule(blockInfo, blockList);
				updateAll();
			});

			this.event.subscribe(folder.ChildRemoved, (block) => {
				delete WeaponModule.allModules[BlockManager.getBlockDataByBlockModel(block as BlockModel).uuid];
				updateAll();
			});
		}
	}
}


_G.EELogs = _G.EELogs or {}

local function log(...)
    if _G.EELogs == nil then
        _G.EELogs = {}
    end
    table.insert(_G.EELogs, ...)
end

return log


local fs = require("@lune/fs")
local roblox = require("@lune/roblox")

local localGame = roblox.deserializePlace(fs.readFile("out/place.rbxl"))
local upstreamGame = roblox.deserializePlace(fs.readFile("sandbox/upstream.rbxl"))

for _, item in ipairs(upstreamGame.Workspace:GetChildren()) do
    if item.Name == "Terrain" or item.Name == "Camera" then
        continue
    end
    item:Clone().Parent = localGame.Workspace
end

upstreamGame.ReplicatedStorage.Assets:Clone().Parent = localGame.ReplicatedStorage

file = roblox.serializePlace(localGame)
fs.writeFile("sandbox/local.rbxl", file)


import React, { useEffect } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import App from "client/components/App";
import { questState } from "client/components/quest/QuestState";
import StoryMocking from "client/components/StoryMocking";
import { setVisibilityMain } from "client/hooks/useVisibility";
import Packets from "shared/Packets";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
            hasItems: true,
        },
    },
    (props) => {
        StoryMocking.mockData();
        StoryMocking.mockCharacter();
        questState.setTrackedQuest("Quest1");
        if (!props.controls.hasItems) {
            Packets.inventory.set(new Map());
        }

        useEffect(() => {
            setVisibilityMain(props.controls.visible);
        }, [props.controls.visible]);

        return <App />;
    },
);


/// <reference types="@rbxts/types/plugin" />
import React, { Fragment, useEffect } from "@rbxts/react";
import { createRoot, Root } from "@rbxts/react-roblox";
import { ContentProvider, RunService, StarterGui, Workspace } from "@rbxts/services";
import BackpackWindow from "client/components/backpack/BackpackWindow";
import BalanceWindow from "client/components/balance/BalanceWindow";
import CurrencyGainManager from "client/components/balance/CurrencyGainManager";
import BuildWindow from "client/components/build/BuildWindow";
import ChallengeCompletionManager from "client/components/challenge/ChallengeCompletionManager";
import ChallengeHudWindow from "client/components/challenge/ChallengeHudWindow";
import ChestLootManager from "client/components/chest/ChestLootManager";
import CommandsWindow from "client/components/commands/CommandsWindow";
import DebugOverlay from "client/components/debug/DebugOverlay";
import EffectManager from "client/components/effect/EffectManager";
import BrokenItemIndicatorRenderer from "client/components/item/BrokenItemIndicatorRenderer";
import ClientItemReplication from "client/components/item/ClientItemReplication";
import HarvestableGuiRenderer from "client/components/item/HarvestableGuiRenderer";
import InventoryWindow from "client/components/item/inventory/InventoryWindow";
import PortableBeaconWindow from "client/components/item/PortableBeaconWindow";
import PrinterRenderer from "client/components/item/printer/PrinterRenderer";
import RepairedItemEffectRenderer from "client/components/item/RepairedItemEffectRenderer";
import RepairWindow from "client/components/item/RepairWindow";
import PurchaseWindow from "client/components/item/shop/PurchaseWindow";
import ShopGui from "client/components/item/shop/ShopGui";
import UpgradeBoardRenderer from "client/components/item/upgrade/UpgradeBoardRenderer";
import LevelUpManager from "client/components/levelup/LevelUpManager";
import LogsWindow from "client/components/logs/LogsWindow";
import MarketplaceWindow from "client/components/marketplace/MarketplaceWindow";
import DialogueWindow from "client/components/npc/DialogueWindow";
import PlayerListContainer from "client/components/playerlist/PlayerListContainer";
import PositionManager from "client/components/position/PositionManager";
import PillarPuzzle from "client/components/quest/PillarPuzzle";
import QuestCompletionManager from "client/components/quest/QuestCompletionManager";
import QuestWindow from "client/components/quest/QuestWindow";
import TrackedQuestWindow from "client/components/quest/TrackedQuestWindow";
import RenameWindow from "client/components/rename/RenameWindow";
import ResetRenderer from "client/components/reset/ResetRenderer";
import CopyWindow from "client/components/settings/CopyWindow";
import SettingsManager from "client/components/settings/SettingsManager";
import SidebarButtons from "client/components/sidebar/SidebarButtons";
import performNewBeginningsWakeUp from "client/components/start/performNewBeginningsWakeUp";
import TitleScreen from "client/components/start/TitleScreen";
import StatsWindow from "client/components/stats/StatsWindow";
import ToastManager from "client/components/toast/ToastManager";
import TooltipWindow from "client/components/tooltip/TooltipWindow";
import DocumentManager from "client/components/window/DocumentManager";
import WorldRenderer from "client/components/world/WorldRenderer";
import { setVisibilityMain } from "client/hooks/useVisibility";
import MusicManager from "client/MusicManager";
import { assets, getAsset } from "shared/asset/AssetMap";
import { PLAYER_GUI } from "shared/constants";
import { IS_EDIT, IS_PUBLIC_SERVER, IS_STUDIO } from "shared/Context";
import eat from "shared/hamster/eat";
import Sandbox from "shared/Sandbox";
import LoadingScreen from "sharedfirst/LoadingScreen";

function setParent(instance: Instance) {
    instance.Parent = PLAYER_GUI;
    if (IS_EDIT) {
        eat(instance, "Destroy");
    }
}

function createScreenGui(name: string, displayOrder = 0, ignoreGuiInset = true): ScreenGui {
    const screenGui = new Instance("ScreenGui");
    screenGui.IgnoreGuiInset = ignoreGuiInset;
    screenGui.ResetOnSpawn = false;
    screenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling;
    screenGui.Name = name;
    screenGui.DisplayOrder = displayOrder;
    setParent(screenGui);
    return screenGui;
}

function createFolder(name: string): Folder {
    const folder = new Instance("Folder");
    folder.Name = name;
    setParent(folder);
    return folder;
}

function addRoot(roots: Set<Root>, container: Instance): Root {
    const root = createRoot(container);
    roots.add(root);
    return root;
}

function delay(seconds: number): Promise<void> {
    return new Promise((resolve) => {
        task.delay(seconds, () => resolve());
    });
}

// Documents that must register before we reveal the main UI.
const DOCUMENT_IDS_TO_WAIT_FOR = ["Title", "Backpack", "Balance", "Sidebar", "Position", "TrackedQuest"] as const;
const INITIAL_DOCUMENT_WAIT_DELAY = RunService.IsRunning() ? 1 : 0;

function waitForDocuments(
    documentIds: ReadonlyArray<string>,
    timeoutSeconds = 10,
    isCancelled?: () => boolean,
): Promise<void> {
    const pending = new Set<string>();
    for (const id of documentIds) {
        if (!DocumentManager.INFO_PER_DOCUMENT.has(id)) {
            pending.add(id);
        }
    }

    if (pending.size() === 0) {
        return Promise.resolve();
    }

    return new Promise((resolve) => {
        const startedAt = os.clock();
        const connection = RunService.Heartbeat.Connect(() => {
            if (isCancelled?.() === true) {
                connection.Disconnect();
                resolve();
                return;
            }

            const currentPending = new Array<string>();
            for (const id of pending) {
                if (DocumentManager.INFO_PER_DOCUMENT.has(id)) {
                    pending.delete(id);
                } else {
                    currentPending.push(id);
                }
            }

            if (pending.size() === 0) {
                connection.Disconnect();
                resolve();
                return;
            }

            if (timeoutSeconds > 0 && os.clock() - startedAt >= timeoutSeconds) {
                connection.Disconnect();
                let outstandingList = "";
                for (const id of currentPending) {
                    outstandingList = outstandingList === "" ? id : `${outstandingList}, ${id}`;
                }
                if (outstandingList !== "") {
                    warn(`[App] Timed out waiting for documents: ${outstandingList}`);
                }
                resolve();
            }
        });
    });
}

/**
 * Entry point for the app's UI.
 * This creates roots for each major UI section and manages their lifecycle, so
 * do not rely on the returned JSX element for rendering anything.
 */
export default function App() {
    useEffect(() => {
        StarterGui.ResetPlayerGuiOnSpawn = false; // Not having this deletes folders in PlayerGui

        const roots = new Set<Root>();
        addRoot(roots, createScreenGui("DebugOverlay", 100, false)).render(<DebugOverlay />);

        addRoot(roots, createScreenGui("Title", 20)).render(<TitleScreen />);

        addRoot(roots, createScreenGui("PlayerList", 15)).render(<PlayerListContainer />);

        addRoot(roots, createScreenGui("Tooltips", 3)).render(<TooltipWindow />);
        addRoot(roots, createScreenGui("Effects", 2)).render(<EffectManager />);
        addRoot(roots, createScreenGui("Dialogue", 1)).render(<DialogueWindow />);

        // Single document windows
        addRoot(roots, createScreenGui("Build", 0)).render(<BuildWindow />);
        addRoot(roots, createScreenGui("Commands", 0)).render(<CommandsWindow />);
        addRoot(roots, createScreenGui("Copy", 0)).render(<CopyWindow />);
        addRoot(roots, createScreenGui("Inventory", 0)).render(<InventoryWindow />);
        addRoot(roots, createScreenGui("Logs", 0)).render(<LogsWindow />);
        addRoot(roots, createScreenGui("Marketplace", 0)).render(<MarketplaceWindow />);
        addRoot(roots, createScreenGui("PillarPuzzle", 0)).render(<PillarPuzzle />);
        addRoot(roots, createScreenGui("Purchase", 0, false)).render(<PurchaseWindow />);
        addRoot(roots, createScreenGui("PortableBeacon", 0)).render(<PortableBeaconWindow />);
        addRoot(roots, createScreenGui("Quest", 0)).render(<QuestWindow />);
        addRoot(roots, createScreenGui("Rename", 0)).render(<RenameWindow />);
        addRoot(roots, createScreenGui("Repair", 0)).render(<RepairWindow />);
        addRoot(roots, createScreenGui("Settings", 0)).render(<SettingsManager />);
        addRoot(roots, createScreenGui("Stats", 0)).render(<StatsWindow />);
        addRoot(roots, createScreenGui("TrackedQuest", 0)).render(<TrackedQuestWindow />);

        addRoot(roots, createScreenGui("Toasts", -1)).render(<ToastManager />);

        addRoot(roots, createScreenGui("ChallengeCompletion", -3)).render(<ChallengeCompletionManager />);
        addRoot(roots, createScreenGui("ChestLoot", -3)).render(<ChestLootManager />);
        addRoot(roots, createScreenGui("QuestCompletion", -3)).render(<QuestCompletionManager />);

        addRoot(roots, createScreenGui("CurrencyGain", -4, false)).render(<CurrencyGainManager />);

        addRoot(roots, createScreenGui("Balance", -5)).render(<BalanceWindow />);
        addRoot(roots, createScreenGui("LevelUp", -5)).render(<LevelUpManager />);
        addRoot(roots, createScreenGui("Position", -5)).render(<PositionManager />);
        addRoot(roots, createScreenGui("Sidebar", -5)).render(<SidebarButtons />);

        addRoot(roots, createScreenGui("ChallengeHud", -10)).render(<ChallengeHudWindow />);

        addRoot(roots, createScreenGui("Backpack", -15)).render(<BackpackWindow />);

        addRoot(roots, createFolder("BrokenItemIndicators")).render(<BrokenItemIndicatorRenderer />);
        addRoot(roots, createFolder("Harvestable")).render(<HarvestableGuiRenderer />);
        addRoot(roots, createFolder("RepairedItemEffects")).render(<RepairedItemEffectRenderer />);
        addRoot(roots, createFolder("Printer")).render(<PrinterRenderer />);
        addRoot(roots, createFolder("Shop")).render(<ShopGui />);
        addRoot(roots, createFolder("UpgradeBoard")).render(<UpgradeBoardRenderer />);
        addRoot(roots, createFolder("World")).render(<WorldRenderer />);

        Workspace.SetAttribute("Title", IS_PUBLIC_SERVER);
        const cleanup = MusicManager.init();

        let disposed = false;

        Promise.all([
            delay(INITIAL_DOCUMENT_WAIT_DELAY),
            waitForDocuments(DOCUMENT_IDS_TO_WAIT_FOR, 10, () => disposed),
        ])
            .then(() => {
                if (disposed) return;

                LoadingScreen.hideLoadingScreen();

                if (IS_PUBLIC_SERVER) {
                    DocumentManager.setVisible("Title", true);
                } else if (Sandbox.getEnabled()) {
                    setVisibilityMain(true);
                } else {
                    performNewBeginningsWakeUp();
                }
            })
            .catch((err) => {
                warn(`[App] Failed while waiting for initial UI readiness: ${tostring(err)}`);
            });

        task.spawn(() => {
            if (IS_STUDIO && IS_EDIT) {
                const setWaypoint = (key: string) => {
                    game.GetService("ChangeHistoryService").SetWaypoint(key);
                };

                const [success, result] = pcall(() => setWaypoint("SimulationOpen"));
                if (!success) warn(`[ChangeHistoryService] Failed to set waypoint: ${result}`);

                eat(() => {
                    const [success, result] = pcall(() => {
                        setWaypoint("SimulationClosing");
                        task.delay(1, () => setWaypoint("SimulationDone"));
                    });
                    if (!success) warn(`[ChangeHistoryService] Failed to set waypoint: ${result}`);
                });
                return;
            }

            const priority = [
                getAsset("assets/sounds/FabricRustle.mp3"),
                getAsset("assets/sounds/JumpSwish.mp3"),
                getAsset("assets/sounds/DefaultText.mp3"),
                getAsset("assets/sounds/QuestComplete.mp3"),
                getAsset("assets/sounds/QuestNextStage.mp3"),
            ];
            for (const [_, id] of pairs(assets)) {
                if (!priority.includes(id)) priority.push(id);
            }
            ContentProvider.PreloadAsync(priority);
        });

        return () => {
            disposed = true;
            for (const root of roots) {
                root.unmount();
            }
            cleanup();
            LoadingScreen.destroy();
        };
    }, []);

    ClientItemReplication.useManualItemReplication();

    return (
        <Fragment>
            <boolvalue key="mounted" Value={true} />
            <ResetRenderer />
        </Fragment>
    );
}


import { Storybook } from "@rbxts/ui-labs";

const storybook: Storybook = {
    name: "Stories",
    storyRoots: [script.Parent!],
};

export = storybook;


import React, { InstanceProps, memo, useEffect, useRef } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import useHover from "client/hooks/useHover";

interface IconButtonProps {
    image: string;
    onClick?: () => void;
    onEnter?: () => void;
    onLeave?: () => void;
    buttonProps?: InstanceProps<ImageButton>;
}

function IconButton(props: IconButtonProps) {
    const buttonRef = useRef<ImageButton>();
    const scaleRef = useRef<UIScale>();

    const { hovering, events } = useHover(props);
    const color = hovering ? new Color3(0.8, 0.8, 0.8) : new Color3(1, 1, 1);
    const scale = hovering ? 1.05 : 1;
    useEffect(() => {
        const tweenInfo = new TweenInfo(0.2);
        TweenService.Create(buttonRef.current!, tweenInfo, { ImageColor3: color }).Play();
        TweenService.Create(scaleRef.current!, tweenInfo, { Scale: scale }).Play();
    }, [hovering]);

    const handleClick = () => {
        scaleRef.current!.Scale = 0.9;
        TweenService.Create(scaleRef.current!, new TweenInfo(0.5), { Scale: scale }).Play();
        props.onClick?.();
    };

    return (
        <imagebutton
            ref={buttonRef}
            BackgroundTransparency={1}
            Image={props.image}
            Event={{
                Activated: handleClick,
                ...events,
            }}
            {...props.buttonProps}
        >
            <uiscale ref={scaleRef} />
            <uiaspectratioconstraint />
        </imagebutton>
    );
}

export default memo(IconButton);


import React, { Fragment, useEffect, useState } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { StarterGui } from "@rbxts/services";
import { CreateReactStory } from "@rbxts/ui-labs";
import LoadingScreen from "sharedfirst/LoadingScreen";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            text: "Loading...",
            visible: true,
        },
    },
    (props) => {
        const [screenGui, setScreenGui] = useState<ScreenGui>();

        useEffect(() => {
            const screenGui = new Instance("ScreenGui");
            screenGui.IgnoreGuiInset = true;
            screenGui.ResetOnSpawn = false;
            screenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling;
            screenGui.Parent = StarterGui;
            setScreenGui(screenGui);
            return () => {
                screenGui.Destroy();
            };
        }, []);

        if (!screenGui) return <Fragment />;

        if (props.controls.visible) {
            LoadingScreen.showLoadingScreen(props.controls.text, false, screenGui);
        } else {
            LoadingScreen.hideLoadingScreen();
        }
        return <Fragment />;
    },
);


import React, { StrictMode, useEffect, useState } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { Workspace } from "@rbxts/services";
import { CreateReactStory } from "@rbxts/ui-labs";
import BackpackWindow from "client/components/backpack/BackpackWindow";
import HarvestableGuiRenderer from "client/components/item/HarvestableGuiRenderer";
import StoryMocking from "client/components/StoryMocking";
import TooltipWindow from "client/components/tooltip/TooltipWindow";
import useVisibility from "client/hooks/useVisibility";
import Items from "shared/items/Items";
import Packets from "shared/Packets";
import HARVESTABLES from "shared/world/harvestable/Harvestable";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
        },
    },
    (props) => {
        StoryMocking.mockData();
        StoryMocking.mockCharacter();
        useVisibility("Backpack", props.controls.visible);

        const [models, setModels] = useState(new Array<PVInstance>());

        useEffect(() => {
            let i = 0;
            const container = new Instance("Folder");
            container.Name = "Harvestable";
            container.Parent = Workspace;

            const newModels = new Array<PVInstance>();
            for (const [id, harvestable] of pairs(HARVESTABLES)) {
                const item = Items.getItem(id as string);
                if (!item) continue;

                const harvestableModel = item.createModel({
                    item: item.id,
                    posX: 0,
                    posY: 200,
                    posZ: i * 20,
                    rotX: 0,
                    rotY: 0,
                    rotZ: 0,
                });
                if (!harvestableModel) continue;

                harvestableModel.SetAttribute("Health", harvestable.health);
                harvestableModel.Parent = container;
                newModels.push(harvestableModel);
                i++;
            }
            setModels(newModels);
            return () => {
                container.Destroy();
            };
        }, []);

        Packets.useTool.fromClient((player, payload) => {
            const harvestable = payload?.target;
            if (harvestable === undefined) return;
            harvestable.SetAttribute("Health", (harvestable.GetAttribute("Health") as number) - math.random(5, 100));
        });

        return (
            <StrictMode>
                <BackpackWindow />
                <HarvestableGuiRenderer overrideModels={models} />
                <TooltipWindow />
            </StrictMode>
        );
    },
);


/**
 * @fileoverview Main backpack window React component
 *
 * Displays the player's tool inventory with hotkey numbers and selection states.
 * Manages visibility based on adaptive tab and build mode states.
 */

import { loadAnimation } from "@antivivi/vrldk";
import React, { JSX, useEffect, useRef, useState } from "@rbxts/react";
import { Workspace } from "@rbxts/services";
import { Environment } from "@rbxts/ui-labs";
import GearOption, { layoutOrderFromGear } from "client/components/backpack/GearOption";
import DocumentManager, { useDocument } from "client/components/window/DocumentManager";
import { playSound } from "shared/asset/GameAssets";
import { LOCAL_PLAYER, observeCharacter } from "shared/constants";
import { IS_EDIT } from "shared/Context";
import { getPlayerCharacter } from "shared/hamster/getPlayerCharacter";
import Gear from "shared/item/traits/Gear";
import Items from "shared/items/Items";
import Packets from "shared/Packets";

const KEY_CODES = new Map<number, Enum.KeyCode>([
    [1, Enum.KeyCode.One],
    [2, Enum.KeyCode.Two],
    [3, Enum.KeyCode.Three],
    [4, Enum.KeyCode.Four],
    [5, Enum.KeyCode.Five],
    [6, Enum.KeyCode.Six],
    [7, Enum.KeyCode.Seven],
    [8, Enum.KeyCode.Eight],
    [9, Enum.KeyCode.Nine],
    [10, Enum.KeyCode.Zero],
]);

const equipGear = (itemId: string) => {
    const backpack = LOCAL_PLAYER?.FindFirstChildOfClass("Backpack");
    const character = getPlayerCharacter();
    const humanoid = character?.FindFirstChildOfClass("Humanoid");
    if (character === undefined || humanoid === undefined) return false;

    const currentlyEquippedTool = character?.FindFirstChildOfClass("Tool");
    if (currentlyEquippedTool) {
        humanoid.UnequipTools();
        if (currentlyEquippedTool.Name === itemId) {
            playSound("Unequip.mp3");
            return false;
        }
    }
    let tool: Tool | undefined;
    if (IS_EDIT) {
        tool = Items.getItem(itemId)?.MODEL?.Clone() as Tool | undefined;
    } else {
        tool = backpack?.FindFirstChild(itemId) as Tool | undefined;
    }

    if (tool === undefined) return false;
    humanoid.EquipTool(tool);
    playSound("Equip.mp3");
    return true;
};

/**
 * Main backpack window component that displays tool options
 */
export default function BackpackWindow() {
    const ref = useRef<Frame>();
    const shouldRestoreAfterBuild = useRef(false);
    const expectingBackpackHide = useRef(false);
    const [gears, setGears] = useState<Set<Gear>>(new Set());
    const [equippedGear, setEquippedGear] = useState<Gear | undefined>(undefined);
    const { visible } = useDocument({ id: "Backpack", priority: -1 });
    const openPosition = new UDim2(0.5, 0, 0.985, -5);
    const closedPosition = new UDim2(0.5, 0, 1.2, 0);
    useEffect(() => {
        if (visible) {
            ref.current?.TweenPosition(openPosition, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 1, true);
        } else {
            ref.current?.TweenPosition(closedPosition, Enum.EasingDirection.In, Enum.EasingStyle.Quad, 1, true);
        }
    }, [visible]);

    useEffect(() => {
        let swingAnimation: AnimationTrack | undefined;
        let childAddedConnection: RBXScriptConnection | undefined;
        let childRemovedConnection: RBXScriptConnection | undefined;

        const cleanup = observeCharacter((character: Model) => {
            const humanoid = character.WaitForChild("Humanoid") as Humanoid;
            swingAnimation = loadAnimation(humanoid, 16920778613);

            const onToolAdded = (tool: Instance) => {
                if (tool.IsA("Tool")) {
                    const item = Items.itemsPerId.get(tool.Name);
                    if (item) {
                        const gear = item.findTrait("Gear");
                        setEquippedGear(gear);
                    }
                }
            };

            const onToolRemoved = (tool: Instance) => {
                if (tool.IsA("Tool")) {
                    setEquippedGear(undefined);
                }
            };
            childAddedConnection?.Disconnect();
            childRemovedConnection?.Disconnect();
            childAddedConnection = character.ChildAdded.Connect(onToolAdded);
            childRemovedConnection = character.ChildRemoved.Connect(onToolRemoved);
            const existingTool = character.FindFirstChildOfClass("Tool");
            if (existingTool) {
                onToolAdded(existingTool);
            }
        });

        const OVERLAP_PARAMS = new OverlapParams();
        OVERLAP_PARAMS.CollisionGroup = "HarvestableInquirer";
        /**
         * Checks for a harvestable object in range of the tool.
         * @param tool The tool model to check from.
         * @returns The harvestable model or part, if found.
         */
        const checkHarvestable = (tool: Model) => {
            const blade = (tool.FindFirstChild("Blade") as BasePart | undefined) ?? tool.PrimaryPart;
            if (blade === undefined) return undefined;

            const inside = Workspace.GetPartBoundsInBox(
                blade.CFrame,
                blade.Size.add(new Vector3(1, 5, 1)),
                OVERLAP_PARAMS,
            );
            for (const touching of inside) {
                const tParent = touching.Parent;
                if (tParent === undefined) continue;
                if (tParent.IsA("Model")) {
                    if (tParent.Parent?.Name === "Harvestable") return tParent;
                } else if (tParent.Name === "Harvestable") return touching;
            }
        };

        let lastUse = 0;
        const inputBeganConnection = Environment.UserInput.InputBegan.Connect((input, gameProcessed) => {
            if (gameProcessed === true) return;

            if (
                input.UserInputType === Enum.UserInputType.MouseButton1 ||
                input.UserInputType === Enum.UserInputType.Touch ||
                input.KeyCode === Enum.KeyCode.ButtonL1
            ) {
                const currentTool = getPlayerCharacter()?.FindFirstChildOfClass("Tool");
                if (currentTool === undefined) return;

                const item = Items.getItem(currentTool.Name);
                if (item === undefined) return;
                const gear = item.findTrait("Gear");
                if (gear === undefined) return;
                const hasOnUse = gear.onUse !== undefined;
                if (gear.type === "None" && !hasOnUse) return;

                const t = tick();
                if (lastUse + 8 / (gear.speed ?? 1) > t) return;
                lastUse = t;
                const registerHit = () => {
                    const hit = checkHarvestable(currentTool);
                    if (hit === undefined && !hasOnUse) return;
                    Packets.useTool.toServer({ target: hit });
                };
                if (IS_EDIT) {
                    registerHit();
                } else if (swingAnimation) {
                    swingAnimation.Stopped.Once(registerHit);
                    swingAnimation.Play();
                }
                playSound("ToolSwing.mp3");
            }
        });

        return () => {
            inputBeganConnection.Disconnect();
            childAddedConnection?.Disconnect();
            childRemovedConnection?.Disconnect();
            cleanup();
        };
    }, []);

    useEffect(() => {
        const connection = Environment.UserInput.InputBegan.Connect((input, gameProcessed) => {
            if (gameProcessed === true) return;
            let index = -1;
            for (const [i, v] of KEY_CODES) {
                if (input.KeyCode === v) {
                    index = i;
                    break;
                }
            }

            let sortedGears = new Array<Gear>();
            for (const tool of gears) {
                sortedGears.push(tool);
            }
            sortedGears = sortedGears.sort((a, b) => layoutOrderFromGear(a) < layoutOrderFromGear(b));
            const equipping = sortedGears[index - 1];
            if (equipping === undefined) return;
            equipGear(equipping.item.id);
        });

        return () => connection.Disconnect();
    }, [gears]);

    useEffect(() => {
        const connection = Packets.inventory.observe((inventory) => {
            const [bestTools] = Gear.getBestGearsFromInventory(inventory, Items.itemsPerId);
            setGears(bestTools);
        });
        return () => connection.Disconnect();
    }, []);

    useEffect(() => {
        const connection = DocumentManager.visibilityChanged.connect((id, isVisible) => {
            if (id === "Build") {
                if (isVisible) {
                    if (DocumentManager.isVisible("Backpack")) {
                        shouldRestoreAfterBuild.current = true;
                        expectingBackpackHide.current = true;
                        DocumentManager.setVisible("Backpack", false);
                    } else {
                        shouldRestoreAfterBuild.current = false;
                    }
                } else {
                    if (shouldRestoreAfterBuild.current && !DocumentManager.isVisible("Backpack")) {
                        DocumentManager.setVisible("Backpack", true);
                    }
                    shouldRestoreAfterBuild.current = false;
                }
            } else if (id === "Backpack") {
                if (!isVisible) {
                    if (expectingBackpackHide.current) {
                        expectingBackpackHide.current = false;
                    } else {
                        shouldRestoreAfterBuild.current = false;
                    }
                } else {
                    expectingBackpackHide.current = false;
                }
            }
        });

        return () => connection.Disconnect();
    }, []);

    const gearOptions = new Array<JSX.Element>();
    for (const gear of gears) {
        gearOptions.push(
            <GearOption gear={gear} isEquipped={equippedGear === gear} onClick={() => equipGear(gear.item.id)} />,
        );
    }

    return (
        <frame
            ref={ref}
            AnchorPoint={new Vector2(0.5, 1)}
            BackgroundTransparency={1}
            Position={closedPosition}
            Size={new UDim2(0.45, 200, 0.03, 20)}
            ZIndex={-3}
        >
            {/* Gear options */}
            {gearOptions}

            {/* Layout */}
            <uilistlayout
                FillDirection={Enum.FillDirection.Horizontal}
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0, 10)}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />
        </frame>
    );
}


/**
 * @fileoverview Tool option button component for the backpack system
 *
 * A styled button component representing a tool in the player's backpack.
 * Displays tool icon, hotkey number, and handles selection state visualization.
 */

import React, { memo, useEffect, useRef } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { TooltipManager } from "client/components/tooltip/TooltipWindow";
import { getAsset } from "shared/asset/AssetMap";
import Gear from "shared/item/traits/Gear";

/** Determine layout order based on tool type */
export function layoutOrderFromGear(gear: Gear): number {
    switch (gear.type) {
        case "Pickaxe":
            return 1;
        case "Axe":
            return 2;
        case "Scythe":
            return 3;
        case "Rod":
            return 4;
        case "None":
        default:
            return gear.item.layoutOrder;
    }
}

/**
 * Individual tool option button component
 */
function GearOption({ gear, isEquipped, onClick }: { gear: Gear; isEquipped: boolean; onClick: () => void }) {
    // Color based on equipped state
    const backgroundColor = isEquipped ? Color3.fromRGB(0, 184, 255) : Color3.fromRGB(31, 31, 31);
    const strokeColor = isEquipped ? Color3.fromRGB(0, 184, 255) : Color3.fromRGB(61, 61, 61);
    const scaleRef = useRef<UIScale>();

    useEffect(() => {
        const scale = scaleRef.current;
        if (!scale) return;

        const expand = TweenService.Create(
            scale,
            new TweenInfo(0.12, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
            { Scale: 1.12 },
        );
        const settle = TweenService.Create(scale, new TweenInfo(0.1, Enum.EasingStyle.Quad, Enum.EasingDirection.In), {
            Scale: 1,
        });
        const connection = expand.Completed.Connect(() => settle.Play());

        scale.Scale = 0.92;
        expand.Play();

        return () => {
            connection.Disconnect();
            expand.Destroy();
            settle.Destroy();
            scale.Scale = 1;
        };
    }, [gear.item.id]);

    return (
        <textbutton
            key={gear.item.id}
            BackgroundColor3={backgroundColor}
            BorderColor3={Color3.fromRGB(0, 0, 0)}
            BorderSizePixel={4}
            LayoutOrder={layoutOrderFromGear(gear)}
            Size={new UDim2(1, 0, 1, 0)}
            SizeConstraint={Enum.SizeConstraint.RelativeYY}
            Text=""
            Event={{
                Activated: onClick,
                MouseMoved: () => {
                    TooltipManager.showTooltip({ item: gear.item });
                },
                MouseLeave: () => {
                    TooltipManager.hideTooltip();
                },
            }}
        >
            {/* Background gradient */}
            <uigradient
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, Color3.fromRGB(143, 143, 143)),
                        new ColorSequenceKeypoint(1, Color3.fromRGB(198, 198, 198)),
                    ])
                }
                Rotation={272}
            />

            {/* Tool icon */}
            <imagelabel
                key="ImageLabel"
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundTransparency={1}
                Image={gear.item.image}
                Position={new UDim2(0.5, 0, 0.5, 0)}
                Size={new UDim2(0.75, 0, 0.75, 0)}
                ZIndex={0}
            />

            {/* Background pattern */}
            <imagelabel
                key="Pattern"
                AnchorPoint={new Vector2(0.5, 0)}
                BackgroundTransparency={1}
                Image={getAsset("assets/GridCheckers.png")}
                ImageColor3={Color3.fromRGB(255, 255, 255)}
                ImageTransparency={0.975}
                Position={new UDim2(0.5, 0, 0, 0)}
                ScaleType={Enum.ScaleType.Tile}
                Size={new UDim2(1, 0, 1, 0)}
                TileSize={new UDim2(0, 50, 0, 50)}
                ZIndex={-4}
            />

            {/* Border stroke */}
            <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Color={strokeColor} Thickness={2}>
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(0.587, Color3.fromRGB(173, 173, 173)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 255)),
                        ])
                    }
                    Rotation={75}
                />
            </uistroke>
            <uiscale ref={scaleRef} Scale={1} />
        </textbutton>
    );
}

export default memo(GearOption);


import React, { Fragment, useEffect, useMemo, useRef } from "@rbxts/react";
import { OnoeNum } from "@rbxts/serikanum";
import { Debris, TweenService } from "@rbxts/services";
import StringBuilder from "@rbxts/stringbuilder";
import displayBalanceCurrency from "client/components/balance/displayBalanceCurrency";
import { TooltipManager } from "client/components/tooltip/TooltipWindow";
import { getAsset } from "shared/asset/AssetMap";
import { RobotoSlab, RobotoSlabBold } from "shared/asset/GameFonts";
import { CURRENCY_DETAILS } from "shared/currency/CurrencyDetails";

export function BalanceOptionStyling({ details }: { details: CurrencyDetails }) {
    return (
        <Fragment>
            <uistroke Color={details.color.Lerp(Color3.fromRGB(255, 255, 255), 0.3)}>
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(0.73, Color3.fromRGB(124, 124, 124)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(255, 255, 255)),
                        ])
                    }
                    Rotation={60}
                />
            </uistroke>
            <uigradient
                Color={
                    new ColorSequence([
                        new ColorSequenceKeypoint(0, Color3.fromRGB(125, 125, 125)),
                        new ColorSequenceKeypoint(1, Color3.fromRGB(217, 217, 217)),
                    ])
                }
                Rotation={270}
            />
            <folder>
                <imagelabel
                    AnchorPoint={new Vector2(0.5, 0)}
                    BackgroundTransparency={1}
                    Image={getAsset("assets/GridCheckers.png")}
                    ImageColor3={new Color3(0, 0, 0)}
                    ImageTransparency={0.95}
                    TileSize={new UDim2(0, 75, 0, 75)}
                    Position={new UDim2(0.5, 0, 0, 0)}
                    ScaleType={Enum.ScaleType.Tile}
                    Size={new UDim2(1, 20, 1, 0)}
                    ZIndex={-4}
                />
                <imagelabel
                    AnchorPoint={new Vector2(1, 0)}
                    BackgroundTransparency={1}
                    Image={getAsset("assets/GlassReflection.png")}
                    ImageColor3={new Color3(0, 0, 0)}
                    ImageTransparency={0.9}
                    Rotation={270}
                    Position={new UDim2(1, 0, 0, 0)}
                    ScaleType={Enum.ScaleType.Fit}
                    Size={new UDim2(1, 0, 1, 0)}
                    ZIndex={-2}
                />
            </folder>
        </Fragment>
    );
}

export const balanceOptionImagePerCurrency = new Map<Currency | "none", GuiObject>();

/**
 * Individual currency balance display component.
 * Shows currency icon, amount and income rate information.
 */
export default function BalanceOption({
    currency,
    amount,
    income,
    difference,
    bombBoost,
}: {
    currency: Currency;
    amount: OnoeNum;
    income?: OnoeNum;
    bombBoost?: OnoeNum;
    difference?: OnoeNum;
    layoutOrder: number;
}) {
    const imageRef = useRef<ImageLabel>();
    const wrapperRef = useRef<Frame>();
    const incomeRef = useRef<TextLabel>();
    const details = CURRENCY_DETAILS[currency];

    const tooltipCurrencyColor = useMemo(() => {
        return details.color.Lerp(Color3.fromRGB(255, 255, 255), 0.8).ToHex();
    }, []);

    useEffect(() => {
        const imageLabel = imageRef.current;
        if (imageLabel !== undefined) {
            balanceOptionImagePerCurrency.set(currency, imageLabel);
        }
        return () => {
            if (balanceOptionImagePerCurrency.get(currency) === imageLabel) {
                balanceOptionImagePerCurrency.delete(currency);
            }
        };
    }, []);

    useEffect(() => {
        if (difference === undefined || difference.equals(0)) return;

        const wrapper = wrapperRef.current;
        if (wrapper === undefined) return;
        const incomeLabel = incomeRef.current;
        if (incomeLabel === undefined) return;

        // Show floating difference text
        const diffLabel = incomeLabel.Clone();
        diffLabel.AnchorPoint = new Vector2(1, 0.5);
        diffLabel.Position = new UDim2(0, -25, 0.5, 0);
        diffLabel.Size = new UDim2(0, 0, 0, 0);
        diffLabel.TextSize = 20;
        diffLabel.TextScaled = false;
        diffLabel.TextWrapped = false;
        diffLabel.Text = `${difference.moreThan(0) ? "+" : "-"}${displayBalanceCurrency(currency, difference)}`;
        diffLabel.TextXAlignment = Enum.TextXAlignment.Right;
        diffLabel.Rotation = math.random(-10, 10);
        diffLabel.ZIndex = 3;
        const tweenInfo = new TweenInfo(1, Enum.EasingStyle.Quart, Enum.EasingDirection.In);
        TweenService.Create(diffLabel, tweenInfo, {
            Position: diffLabel.Position.add(new UDim2(0, 0, 0, 50)),
            TextTransparency: 1,
            Rotation: diffLabel.Rotation + math.random(-45, 45),
        }).Play();
        const stroke = diffLabel.FindFirstChildOfClass("UIStroke");
        if (stroke) {
            TweenService.Create(stroke, tweenInfo, { Transparency: 1 }).Play();
        }
        diffLabel.Parent = wrapper;
        Debris.AddItem(diffLabel, 6);
    }, [difference]);

    const showIncome = income && !income.lessEquals(0);

    const isMajor = currency === "Funds" || currency === "Time";
    return (
        <frame
            ref={wrapperRef}
            BackgroundTransparency={1}
            Event={{
                MouseMoved: () => {
                    const tooltipBuilder = new StringBuilder(details.description!)
                        .append("\nYou have ")
                        .append(OnoeNum.toSuffix(amount))
                        .append(" (")
                        .append(OnoeNum.toScientific(amount))
                        .append(") ")
                        .append(currency)
                        .append(".");

                    if (bombBoost) {
                        tooltipBuilder
                            .append("\nA bomb is active, increasing ")
                            .append(currency)
                            .append(" income by x")
                            .append(bombBoost)
                            .append("!");
                    }
                    TooltipManager.showTooltip({
                        message: tooltipBuilder.toString(),
                    });
                },
                MouseLeave: () => {
                    TooltipManager.hideTooltip();
                },
            }}
            LayoutOrder={details.layoutOrder}
            Size={new UDim2(1, 0, 0.045, isMajor ? 30 : 21)}
        >
            <frame
                BackgroundColor3={details.color}
                BorderColor3={Color3.fromRGB(50, 50, 50)}
                BorderSizePixel={3}
                Size={new UDim2(1, 0, 0.8, 0)}
            >
                {/* Currency Icon */}
                <imagelabel
                    ref={imageRef}
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    Image={details.image}
                    LayoutOrder={-2}
                    Position={new UDim2(0, 0, 0.5, 0)}
                    ScaleType={Enum.ScaleType.Fit}
                    Size={new UDim2(0.9, 0, 0.9, 0)}
                    SizeConstraint={Enum.SizeConstraint.RelativeYY}
                    ZIndex={0}
                />

                {/* Amount Container */}
                {/* Balance Label */}
                <textlabel
                    Active={true}
                    AnchorPoint={new Vector2(0, 0.5)}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlabBold}
                    Position={new UDim2(0, 24, 0.5, 0)}
                    Size={new UDim2(1, -30, isMajor ? 0.7 : 0.75, 0)}
                    Text={displayBalanceCurrency(currency, amount)}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextWrapped={true}
                    TextXAlignment={Enum.TextXAlignment.Left}
                >
                    <uistroke Color={details.color.Lerp(Color3.fromRGB(0, 0, 0), 0.3)} Thickness={2}>
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(97, 97, 97)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(63, 63, 63)),
                                ])
                            }
                            Rotation={90}
                        />
                    </uistroke>
                    <uigradient
                        Color={
                            bombBoost
                                ? new ColorSequence(new Color3(1, 0.98, 0.87), new Color3(1, 0.84, 0))
                                : new ColorSequence(new Color3(1, 1, 1))
                        }
                        Rotation={12}
                    />
                </textlabel>

                {/* Styling */}
                <BalanceOptionStyling details={details} />
            </frame>

            {/* Income Container */}
            <frame
                Active={true}
                AnchorPoint={new Vector2(0, 1)}
                AutomaticSize={Enum.AutomaticSize.X}
                BackgroundTransparency={1}
                LayoutOrder={2}
                Position={new UDim2(0, 24, 1, 0)}
                Size={new UDim2(1, -30, 0.35, 0)}
                Visible={showIncome}
            >
                {/* Income Label */}
                <textlabel
                    ref={incomeRef}
                    Active={true}
                    AutomaticSize={Enum.AutomaticSize.X}
                    BackgroundTransparency={1}
                    FontFace={RobotoSlab}
                    LayoutOrder={1}
                    Size={new UDim2(0, 0, 1, 0)}
                    Text={`${displayBalanceCurrency(currency, income!)}/s`}
                    TextColor3={Color3.fromRGB(255, 255, 255)}
                    TextScaled={true}
                    TextWrapped={true}
                    TextXAlignment={Enum.TextXAlignment.Left}
                >
                    <uistroke Color={details.color.Lerp(Color3.fromRGB(0, 0, 0), 0.3)} Thickness={2}>
                        <uigradient
                            Color={
                                new ColorSequence([
                                    new ColorSequenceKeypoint(0, Color3.fromRGB(97, 97, 97)),
                                    new ColorSequenceKeypoint(1, Color3.fromRGB(63, 63, 63)),
                                ])
                            }
                            Rotation={90}
                        />
                    </uistroke>
                </textlabel>

                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    Padding={new UDim(0, 4)}
                    HorizontalAlignment={Enum.HorizontalAlignment.Left}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
            </frame>
        </frame>
    );
}


import React, { StrictMode, useEffect } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { OnoeNum } from "@rbxts/serikanum";
import { Debris, Workspace } from "@rbxts/services";
import { CreateReactStory, Number } from "@rbxts/ui-labs";
import BalanceWindow from "client/components/balance/BalanceWindow";
import CurrencyGainManager from "client/components/balance/CurrencyGainManager";
import StoryMocking from "client/components/StoryMocking";
import TooltipWindow from "client/components/tooltip/TooltipWindow";
import useVisibility from "client/hooks/useVisibility";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import { CURRENCIES } from "shared/currency/CurrencyDetails";
import Droplet from "shared/item/Droplet";
import Packets from "shared/Packets";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
            visibleCurrencies: Number(CURRENCIES.size(), 0, CURRENCIES.size(), 1, true),
            currencyBombTime: 0,
            difference: 0,
        },
    },
    (props) => {
        const [gainAmount, setGainAmount] = React.useState(new OnoeNum(1));
        StoryMocking.mockData();

        const removingCurrencies = CURRENCIES.size() - props.controls.visibleCurrencies;
        if (removingCurrencies > 0) {
            const newBalance = Packets.balance.get();
            let count = 0;
            const reversedSortedCurrencies = new Array<Currency>();
            for (const [currency] of CurrencyBundle.SORTED_DETAILS) {
                reversedSortedCurrencies.unshift(currency);
            }
            for (const currency of reversedSortedCurrencies) {
                newBalance.delete(currency);
                count++;
                if (count >= removingCurrencies) break;
            }

            Packets.balance.set(newBalance);
        }

        useEffect(() => {
            const bombEndTimes = Packets.bombEndTimes.get();
            for (const currency of CURRENCIES) {
                bombEndTimes.set(currency, os.time() + props.controls.currencyBombTime);
            }
            Packets.bombEndTimes.set(bombEndTimes);
        }, [props.controls.currencyBombTime]);

        useEffect(() => {
            const newDifference = new Map<Currency, OnoeNum>();
            for (const currency of CURRENCIES) {
                newDifference.set(currency, new OnoeNum(props.controls.difference));
            }
            Packets.showDifference.toAllClients(newDifference);
        }, [props.controls.difference]);

        useEffect(() => {
            const mockDroplet = Droplet.TheFirstDroplet.model.Clone();
            mockDroplet.Parent = Workspace;
            mockDroplet.Position = new Vector3(0, 0, 0);
            Debris.AddItem(mockDroplet, 5);

            const amountPerCurrency = new Map<Currency, OnoeNum>();
            for (const currency of CURRENCIES) {
                amountPerCurrency.set(currency, new OnoeNum(gainAmount));
            }
            Packets.dropletBurnt.toAllClients(mockDroplet.Name, amountPerCurrency);
        }, [gainAmount]);

        useVisibility("Balance", props.controls.visible);
        return (
            <StrictMode>
                <BalanceWindow />
                <CurrencyGainManager />
                <TooltipWindow />
                <textbutton
                    AnchorPoint={new Vector2(0.5, 1)}
                    Position={new UDim2(0.5, 0, 1, -50)}
                    Size={new UDim2(0, 200, 0, 50)}
                    Text="+1 Gain"
                    Event={{
                        MouseButton1Click: () => {
                            setGainAmount(gainAmount.add(1));
                        },
                    }}
                />
            </StrictMode>
        );
    },
);


import React, { Fragment, useEffect, useRef, useState } from "@rbxts/react";
import { OnoeNum } from "@rbxts/serikanum";
import BalanceOption, { balanceOptionImagePerCurrency } from "client/components/balance/BalanceOption";
import NavigationControls from "client/components/balance/NavigationControls";
import { useDocument } from "client/components/window/DocumentManager";
import useInterval from "client/hooks/useInterval";
import useProperty from "client/hooks/useProperty";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import { CURRENCY_DETAILS } from "shared/currency/CurrencyDetails";
import CurrencyBomb from "shared/currency/mechanics/CurrencyBomb";
import Packets from "shared/Packets";

const ZERO = new OnoeNum(0);

/**
 * React component for displaying player currency balances with pagination support.
 *
 * Features:
 * - Displays all player currencies with their current amounts
 * - Shows income rates when available
 * - Pagination support for different currency categories
 * - Real-time updates via Packets observation
 * - Configurable formatting options
 */
export default function BalanceWindow() {
    const wrapperRef = useRef<Frame>();
    const navigationRef = useRef<Frame>();
    const balance = useProperty(Packets.balance);
    const revenue = useProperty(Packets.revenue);
    const [difference, setDifference] = useState<BaseCurrencyMap>(new Map());
    const [currentPage, setCurrentPage] = useState(1);
    const [maxPage, setMaxPage] = useState(1);
    const [bombBoosts, setBombBoosts] = useState<CurrencyBundle | undefined>(new CurrencyBundle());
    const { visible } = useDocument({ id: "Balance", priority: -1 });

    const openPosition = new UDim2(1, 0, 1, -70);
    const closePosition = openPosition.add(new UDim2(0, 250, 0, 0));
    useEffect(() => {
        if (visible) {
            wrapperRef.current?.TweenPosition(openPosition, Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 1, true);
        } else {
            wrapperRef.current?.TweenPosition(closePosition, Enum.EasingDirection.In, Enum.EasingStyle.Quad, 1, true);
        }
    }, [visible]);

    useInterval(() => {
        setBombBoosts(CurrencyBomb.getBombBoosts(Packets.bombEndTimes.get(), os.time()));
        return 1;
    }, []);

    // Subscribe to revenue updates
    useEffect(() => {
        const differenceConnection = Packets.showDifference.fromServer(setDifference);

        return () => {
            differenceConnection.Disconnect();
        };
    }, []);

    // Calculate max pages and filter currencies
    useEffect(() => {
        let calculatedMaxPage = 1;
        for (const [currency, details] of pairs(CURRENCY_DETAILS)) {
            if (details.page >= 999) continue; // Skip unobtainable/internal currencies
            const amount = balance.get(currency);
            if (amount === undefined || ZERO.moreEquals(amount)) continue; // Skip if no balance
            if (details.page !== undefined && details.page > calculatedMaxPage) {
                calculatedMaxPage = details.page;
            }
        }
        setMaxPage(calculatedMaxPage);

        // Reset to page 1 if we're on a page that no longer exists
        if (calculatedMaxPage === 1 && currentPage > 1) {
            setCurrentPage(1);
        }
    }, [balance, currentPage]);

    // Get currencies for current page
    const getCurrentPageCurrencies = (): Currency[] => {
        const currencies: Currency[] = [];
        for (const [currency, details] of pairs(CURRENCY_DETAILS)) {
            const currencyBalance = balance.get(currency);
            const amount = new OnoeNum(currencyBalance ?? 0);

            const shouldShow = !amount.lessEquals(0) && currentPage === details.page;

            if (shouldShow) {
                currencies.push(currency);
            }
        }

        // Sort by layout order
        currencies.sort((a, b) => {
            const orderA = CURRENCY_DETAILS[a].layoutOrder ?? 0;
            const orderB = CURRENCY_DETAILS[b].layoutOrder ?? 0;
            return orderA < orderB;
        });

        return currencies;
    };

    const handlePageChange = (newPage: number) => {
        if (newPage < 1) {
            setCurrentPage(maxPage);
        } else if (newPage > maxPage) {
            setCurrentPage(1);
        } else {
            setCurrentPage(newPage);
        }
    };

    const currentCurrencies = getCurrentPageCurrencies();

    if (navigationRef.current) {
        balanceOptionImagePerCurrency.set("none", navigationRef.current);
    }

    return (
        <frame
            ref={wrapperRef}
            AnchorPoint={new Vector2(1, 1)}
            BackgroundTransparency={1}
            Position={closePosition}
            Size={new UDim2(0.025, 150, 0.5, -10)}
            ZIndex={-1}
        >
            <uilistlayout
                FillDirection={Enum.FillDirection.Vertical}
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0, 10)}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Top}
            />

            <frame ref={navigationRef} BackgroundTransparency={1} LayoutOrder={-999} Size={new UDim2(1, 0, 0, 20)}>
                {/* Navigation Controls */}
                {maxPage > 1 ? (
                    <NavigationControls
                        currentPage={currentPage}
                        maxPage={maxPage}
                        currentPageName={CurrencyBundle.getCategory(currentPage) ?? "Main"}
                        onPageChange={handlePageChange}
                    />
                ) : (
                    <Fragment />
                )}

                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalAlignment={Enum.HorizontalAlignment.Right}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
            </frame>

            {/* Currency Options */}
            {currentCurrencies.map((currency, index) => {
                const currencyBalance = balance.get(currency);
                const incomeAmount = revenue.get(currency);
                const differenceAmount = difference.get(currency);
                let amount = currencyBalance === undefined ? undefined : new OnoeNum(currencyBalance);
                if (amount === undefined || amount.lessEquals(0)) {
                    amount = new OnoeNum(0);
                }

                return (
                    <BalanceOption
                        key={currency}
                        currency={currency}
                        amount={amount}
                        income={incomeAmount ? new OnoeNum(incomeAmount) : undefined}
                        difference={differenceAmount ? new OnoeNum(differenceAmount) : undefined}
                        bombBoost={bombBoosts?.get(currency)}
                        layoutOrder={index}
                    />
                );
            })}

            <uipadding PaddingLeft={new UDim(0, 10)} PaddingRight={new UDim(0, 20)} />
        </frame>
    );
}


import React, { memo, useEffect, useRef } from "@rbxts/react";
import { BaseOnoeNum, OnoeNum } from "@rbxts/serikanum";
import { Debris, TweenService, Workspace } from "@rbxts/services";
import { balanceOptionImagePerCurrency } from "client/components/balance/BalanceOption";
import displayBalanceCurrency from "client/components/balance/displayBalanceCurrency";
import { PingManager } from "client/components/stats/StatsWindow";
import { UISignals } from "shared/api/APIExpose";
import UserGameSettings from "shared/api/UserGameSettings";
import { getSound, SOUND_EFFECTS_GROUP } from "shared/asset/GameAssets";
import { RobotoSlabExtraBold } from "shared/asset/GameFonts";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import { CURRENCY_DETAILS } from "shared/currency/CurrencyDetails";
import Droplet from "shared/item/Droplet";
import Packets from "shared/Packets";

/**
 * Creates and animates a currency gain element imperatively for better performance
 */
function createCurrencyGain(
    parent: GuiObject,
    currency: Currency,
    amount: BaseOnoeNum,
    start: Vector3,
    offsetIndex: number,
) {
    const details = CURRENCY_DETAILS[currency];

    // Create the main frame
    const frame = new Instance("Frame");
    frame.AnchorPoint = new Vector2(0.5, 0.5);
    frame.AutomaticSize = Enum.AutomaticSize.XY;
    frame.BackgroundTransparency = 1;
    frame.Parent = parent;

    // Create the currency image
    const imageLabel = new Instance("ImageLabel");
    imageLabel.AnchorPoint = new Vector2(0, 0.5);
    imageLabel.BackgroundTransparency = 1;
    imageLabel.Image = details.image;
    imageLabel.LayoutOrder = 2;
    imageLabel.Size = new UDim2(0, 20, 0, 20);
    imageLabel.ZIndex = 4;
    imageLabel.Parent = frame;

    // Create the currency amount text
    const textLabel = new Instance("TextLabel");
    textLabel.AutomaticSize = Enum.AutomaticSize.X;
    textLabel.BackgroundTransparency = 1;
    textLabel.Font = Enum.Font.Unknown;
    textLabel.FontFace = RobotoSlabExtraBold;
    textLabel.Size = new UDim2(0, 0, 0, 20);
    textLabel.Text = displayBalanceCurrency(currency, new OnoeNum(amount));
    textLabel.TextColor3 = details.color;
    textLabel.TextScaled = true;
    textLabel.TextSize = 14;
    textLabel.TextWrapped = true;
    textLabel.Parent = frame;

    // Create the text stroke
    const stroke = new Instance("UIStroke");
    stroke.Thickness = 2;
    stroke.Parent = textLabel;

    // Create the layout
    const layout = new Instance("UIListLayout");
    layout.FillDirection = Enum.FillDirection.Horizontal;
    layout.Padding = new UDim(0, 3);
    layout.SortOrder = Enum.SortOrder.LayoutOrder;
    layout.VerticalAlignment = Enum.VerticalAlignment.Center;
    layout.Parent = frame;

    // Set up cleanup
    Debris.AddItem(frame, 1);

    // Position the frame
    frame.Position = UDim2.fromOffset(start.X, start.Y + offsetIndex * 20);

    // Get destination for animation
    const balanceOptionImage = balanceOptionImagePerCurrency.get(currency) ?? balanceOptionImagePerCurrency.get("none");
    if (balanceOptionImage === undefined) return;
    const destination = balanceOptionImage.AbsolutePosition.add(balanceOptionImage.AbsoluteSize.div(2));

    // Create and play animation
    const tweenInfo = new TweenInfo(1, Enum.EasingStyle.Quart, Enum.EasingDirection.In);

    TweenService.Create(frame, tweenInfo, {
        Position: UDim2.fromOffset(destination.X, destination.Y),
        Rotation: frame.Rotation + math.random(-45, 45),
    }).Play();

    // Animate transparency if quality is high enough
    if (UserGameSettings!.SavedQualityLevel.Value > 5) {
        TweenService.Create(imageLabel, tweenInfo, { ImageTransparency: 1 }).Play();
        TweenService.Create(textLabel, tweenInfo, { TextTransparency: 1 }).Play();
        TweenService.Create(stroke, tweenInfo, { Transparency: 1 }).Play();
    }
}

function CurrencyGainManager() {
    const containerRef = useRef<Frame>();

    useEffect(() => {
        const container = containerRef.current;
        if (!container) return;

        const showCurrencyGain = (at: Vector3 | undefined, amountPerCurrency: BaseCurrencyMap) => {
            const camera = Workspace.CurrentCamera;
            if (camera === undefined) return;
            let location: Vector3;
            if (at) {
                if (at.sub(camera.CFrame.Position).Magnitude > 50) {
                    return;
                }
                const [loc, withinBounds] = camera.WorldToScreenPoint(at);
                if (!withinBounds) return;
                location = loc;
            } else {
                location = new Vector3(camera.ViewportSize.X / 2, camera.ViewportSize.Y / 2, 0);
            }
            // Create currency gains directly without React components
            let index = 0;
            for (const [currency] of CurrencyBundle.SORTED_DETAILS) {
                const amount = amountPerCurrency.get(currency);
                if (amount === undefined || new OnoeNum(amount).lessEquals(0)) continue;

                createCurrencyGain(container, currency, amount, location, index);
                index++;
            }
        };

        const connection = UISignals.showCurrencyGain.connect(showCurrencyGain);
        const gainConnection = Packets.dropletBurnt.fromServer((dropletModelId, amountPerCurrency) => {
            const dropletModel = Droplet.MODEL_PER_SPAWN_ID.get(dropletModelId);
            if (dropletModel === undefined) {
                return;
            }

            const t = os.clock();
            let burnSound: Sound;
            const sizeMagnitude = dropletModel.Size.Magnitude / 2;
            const tweenInfo = new TweenInfo(sizeMagnitude / 2);

            const light = dropletModel.FindFirstChildOfClass("PointLight") as PointLight | undefined;
            if (light !== undefined) {
                TweenService.Create(light, tweenInfo, { Range: 0 }).Play();
                burnSound = getSound("LuckyDropletBurn.mp3");
            } else {
                burnSound = getSound("DropletBurn.mp3");
            }

            burnSound.PlaybackSpeed = (math.random() * 0.3 + 0.85) / sizeMagnitude;
            if (sizeMagnitude > 0.666) {
                const reverb = new Instance("ReverbSoundEffect");
                reverb.DecayTime = sizeMagnitude / 2;
                reverb.DryLevel = 0.5;
                reverb.WetLevel = 0.5;
                reverb.Parent = burnSound;
            }
            burnSound.SoundGroup = SOUND_EFFECTS_GROUP;
            burnSound.Parent = dropletModel;
            burnSound.Play();
            TweenService.Create(dropletModel, tweenInfo, { Color: new Color3(), Size: new Vector3() }).Play();

            Debris.AddItem(dropletModel, 6);
            dropletModel.Anchored = true; // NOTE: causes desync, server sees it stop without even touching lava

            PingManager.logPing(os.clock() - t);

            showCurrencyGain(dropletModel.Position, amountPerCurrency);
        });

        return () => {
            connection.Disconnect();
            gainConnection.Disconnect();
        };
    }, []);

    return <frame ref={containerRef} BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)} ZIndex={1000} />;
}

export default memo(CurrencyGainManager);


import React, { createRef, Fragment } from "@rbxts/react";
import useHotkeyWithTooltip from "client/components/hotkeys/useHotkeyWithTooltip";
import DocumentManager from "client/components/window/DocumentManager";
import { RobotoMonoBold } from "shared/asset/GameFonts";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";

interface NavigationControlsProps {
    currentPage: number;
    maxPage: number;
    currentPageName: string;
    onPageChange: (newPage: number) => void;
}

/**
 * Navigation controls for switching between currency pages/categories.
 */
export default function NavigationControls({
    currentPage,
    maxPage,
    currentPageName,
    onPageChange,
}: NavigationControlsProps) {
    const leftRef = createRef<ImageButton>();
    const rightRef = createRef<ImageButton>();
    const leftTooltipProps = useHotkeyWithTooltip({
        label: "Previous Page",
        action: () => {
            if (!DocumentManager.isVisible("Balance")) return false;
            onPageChange(currentPage - 1);
            playSound("MenuClick.mp3");
            return true;
        },
        onEnter: () => {
            leftRef.current!.ImageTransparency = 0;
        },
        onLeave: () => {
            leftRef.current!.ImageTransparency = 0.5;
        },
    });
    const rightTooltipProps = useHotkeyWithTooltip({
        label: "Next Page",
        action: () => {
            if (!DocumentManager.isVisible("Balance")) return false;
            onPageChange(currentPage + 1);
            playSound("MenuClick.mp3");
            return true;
        },
        onEnter: () => {
            rightRef.current!.ImageTransparency = 0;
        },
        onLeave: () => {
            rightRef.current!.ImageTransparency = 0.5;
        },
    });

    return (
        <Fragment>
            {/* Left Arrow */}
            <frame
                Active={true}
                BackgroundTransparency={1}
                LayoutOrder={-5}
                Selectable={true}
                Size={new UDim2(0.8, 0, 0.8, 0)}
                SizeConstraint={Enum.SizeConstraint.RelativeYY}
            >
                <imagebutton
                    ref={leftRef}
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    Image={getAsset("assets/Dropdown.png")}
                    ImageTransparency={0.5}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Rotation={90}
                    ScaleType={Enum.ScaleType.Fit}
                    Selectable={false}
                    Size={new UDim2(1, 0, 1, 0)}
                    Event={{
                        ...leftTooltipProps.events,
                    }}
                />
            </frame>

            {/* Page Label */}
            <textlabel
                Active={true}
                AutomaticSize={Enum.AutomaticSize.X}
                AnchorPoint={new Vector2(0.5, 1)}
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                Position={new UDim2(0.5, 0, 1, -5)}
                Size={new UDim2(0, 0, 1, 0)}
                Text={currentPageName}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={14}
                TextWrapped={true}
            >
                <uistroke Thickness={2} Transparency={0.2} />
                <uipadding PaddingLeft={new UDim(0, 5)} PaddingRight={new UDim(0, 5)} />
            </textlabel>

            {/* Right Arrow */}
            <frame
                Active={true}
                BackgroundTransparency={1}
                LayoutOrder={5}
                Selectable={true}
                Size={new UDim2(0.8, 0, 0.8, 0)}
                SizeConstraint={Enum.SizeConstraint.RelativeYY}
            >
                <imagebutton
                    ref={rightRef}
                    AnchorPoint={new Vector2(0.5, 0.5)}
                    BackgroundTransparency={1}
                    Image={getAsset("assets/Dropdown.png")}
                    ImageTransparency={0.5}
                    Position={new UDim2(0.5, 0, 0.5, 0)}
                    Rotation={270}
                    ScaleType={Enum.ScaleType.Fit}
                    Selectable={false}
                    Size={new UDim2(1, 0, 1, 0)}
                    Event={{
                        ...rightTooltipProps.events,
                    }}
                />
            </frame>

            <uilistlayout
                FillDirection={Enum.FillDirection.Horizontal}
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />
        </Fragment>
    );
}


//!native
//!optimize 2
import { OnoeNum } from "@rbxts/serikanum";
import { Workspace } from "@rbxts/services";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Packets from "shared/Packets";

let lastRecalculation = 0;
let isFormatting = false;
const recalculate = () => {
    const camera = Workspace.CurrentCamera;
    isFormatting = camera !== undefined && camera.ViewportSize.X < 1000 && Packets.settings.get()?.FormatCurrencies;
    return isFormatting;
};
isFormatting = recalculate();

/**
 * Displays the balance of a currency in the appropriate format.
 * @param currency The currency to display.
 * @param amount The amount of currency to display.
 * @returns The formatted currency string
 */
export default function displayBalanceCurrency(currency: Currency, amount: OnoeNum | number): string {
    if (tick() - lastRecalculation > 5) {
        lastRecalculation = tick();
        isFormatting = recalculate();
    }

    if (isFormatting) {
        return tostring(amount);
    }

    return CurrencyBundle.getFormatted(currency, amount, true);
}