UNPKG

moomooio-client

Version:

MooMooIO-Client is a client-side wrapper API designed for the game MooMoo.io. This package aims to provide a simple yet powerful API for creating clones, mods, or plugins for MooMoo.io.

608 lines (511 loc) 19.6 kB
import ClientPacketOrganizer from "../ClientPacketOrganizer"; import MooMooIOConnection from "../MooMooIOConnection"; import Player, { PlayerEvents } from "./Player"; import { EventsObject } from "unify-emitter"; import accessories from "../data/accessories"; import weapons from "../data/weapons"; import items from "../data/items"; import hats from "../data/hats"; import getAccessory from "../utils/getAccessory"; import getWeapon from "../utils/getWeapon"; import getItem from "../utils/getItem"; import getHat from "../utils/getHat"; import IOInit from "../ServerPackets/IOInit"; import Pong from "../ServerPackets/Pong"; import UpdateProgress from "../ServerPackets/UpdateProgress"; import UpdateUpgrades from "../ServerPackets/UpdateUpgrades"; import UpdateItems from "../ServerPackets/UpdateItems"; import UpdateItemStore from "../ServerPackets/UpdateItemStore"; import UpdateItemUsage from "../ServerPackets/UpdateItemUsage"; import getGroup from "../utils/getGroup"; import UpdateResource from "../ServerPackets/UpdateResource"; import UpdateMiniMap, { Point2D } from "../ServerPackets/UpdateMiniMap"; import UpdateLeaderBoard, { LeaderBoardMember } from "../ServerPackets/UpdateLeaderBoard"; const START_MAX_XP = 300; const MAX_XP_GROWTH = 1.2; const maxXPRelativeToAge = (age: number) => { return START_MAX_XP * MAX_XP_GROWTH ** (age - 1); }; type AccessoryName = (typeof accessories)["0"]["name"]; type HatName = (typeof hats)["0"]["name"]; type GameItemName = (typeof items)["0"]["name"]; type WeaponName = (typeof weapons)["0"]["name"]; export type MyPlayerEvents = EventsObject< PlayerEvents, { spawn: ThisType<MyPlayer>; death: ThisType<MyPlayer>; init: IOInit; pong: Pong; teamSignal: { /** * signal's position on y-axis */ y: number; /** * signal's position on x-axis */ x: number; }; teamNotification: { /** * refers to the name of the player who asked to join */ playerName: string; /** * a unique identifier for the player who asked to join */ playerID: number; }; } >; type BuildGroup = "foodType" | "wallType" | "spikeType" | "millType" | "boostType" | "turretType"; type WeaponType = "primary" | "secondary"; export default class MyPlayer extends Player<MyPlayerEvents> { protected clientPacketOrganizer: ClientPacketOrganizer; protected state = { autoPunchState: false, alive: false, ping: { Connection: Date.now(), MiniMap: Date.now(), }, }; resource = { wood: 100, food: 100, stone: 100, points: 100, kills: 0, }; #currentAge = 1; #currentXP = 0; #maxXP = maxXPRelativeToAge(this.#currentAge); #currentUpgradeLevel = 0; #upgradeLevel = 0; #weaponKit = [0]; #itemsKit = [0, 3, 6, 10]; #purchasedItems = { Accessory: [] as number[], Hat: [] as number[], }; #itemUsage = new Map<number, number>(); // Key: ItemID, Value: ItemUsageCount #mapPoints: Point2D[] = []; #leaderBoard: LeaderBoardMember[] = []; constructor(readonly connection: MooMooIOConnection) { super(); this.clientPacketOrganizer = new ClientPacketOrganizer(connection); } override clear() { super.clear(); this.__reset(); this.#purchasedItems.Accessory = []; this.#purchasedItems.Hat = []; this.clientPacketOrganizer.destroy(); } __reset() { this.state.autoPunchState = false; this.#currentAge = 1; this.#currentXP = 0; this.#maxXP = maxXPRelativeToAge(this.#currentAge); this.#currentUpgradeLevel = 0; this.#upgradeLevel = 0; this.#weaponKit = [0]; this.#itemsKit = [0, 3, 6, 10]; this.resource = { wood: 100, food: 100, stone: 100, points: 100, kills: 0, }; } __init() { this.state.alive = false; this.__reset(); } __spawn() { this.state.alive = true; this.__reset(); this.emit("spawn", this); } __death() { this.state.alive = false; this.emit("death", this); } __updateProgress(packet: UpdateProgress) { const currentAge = packet.currentAge ?? this.#currentAge; const maxXP = maxXPRelativeToAge(currentAge); this.#currentXP = packet.currentXP; this.#currentAge = currentAge; this.#maxXP = maxXP; } __updateUpgrades(packet: UpdateUpgrades) { this.#currentUpgradeLevel = packet.currentUpgradeLevel; this.#upgradeLevel = packet.upgradeLevel; } __updateItems(packet: UpdateItems) { const kit = structuredClone(packet.kit); this.#weaponKit = packet.isWeapon ? kit : this.#weaponKit; this.#itemsKit = packet.isWeapon ? this.#itemsKit : kit; } __updateItemStore(packet: UpdateItemStore) { if (packet.method == "Purchased") { this.#purchasedItems[packet.type].push(packet.itemID); } } __updateItemUsage(packet: UpdateItemUsage) { this.#itemUsage.set(packet.itemID, packet.count); } __updateResource(packet: UpdateResource) { this.resource[packet.resourceType] = packet.resourceValue; } __updateMiniMap(packet: UpdateMiniMap) { this.#mapPoints = structuredClone(packet.points); } __updateLeaderBoard(packet: UpdateLeaderBoard) { this.#leaderBoard = structuredClone(packet.members); } /** * @description This packet is used to create your character in the game. * @param name The name of your character. * @param skinID The skin ID of your character. * @param haveStartPackage Determines whether your character should start with a fast start-up package (having 100 resources for each resource). * @param force is used to FORCE THE spawn. which is helpful when bug occurs */ spawn( name: string, skinID: number, haveStartPackage: boolean = true, force: boolean = false ): MyPlayer { if (!this.state.alive && !force) return this; this.clientPacketOrganizer.spawn(name, skinID, haveStartPackage).parseLast(); return this; } /** * @description this packet is used to ping the game * @param target either `MiniMap` or `Connection` * @note * > if the `target` is `MiniMap` then the server gonna responses with `UpdateMiniMap`....... * > if the `target` is `Connection` then the server gonna responses with `Pong` */ ping(target: "MiniMap" | "Connection"): MyPlayer { const lastRequest = this.state.ping[target]; const now = Date.now(); if (now - lastRequest > 100) { this.clientPacketOrganizer.ping(target).parseLast(); this.state.ping[target] = now; } return this; } /** * @description This packet is used to purchase/buy an `Accessory` or `Hat` in the game. * @ignored if you already own the item or if you don't have enough gold to make the purchase. The packet will be ignored. * @param name - The name of the hat or accessory you want to buy. * @param itemType can be either `Accessory` or `Hat`. * @note If you haven't spawned in the game yet, this packet will be ignored */ buy<Type extends "Hat" | "Accessory", Name extends Type extends "Hat" ? HatName : AccessoryName>( itemType: Type, HatName: Name ): MyPlayer; buy(itemType: "Accessory" | "Hat", AccessoryOrHatName: string): MyPlayer { if (!this.state.alive || !this.isInitialized) return this; const isAccessory = itemType == "Accessory"; const item = (isAccessory ? getAccessory : getHat)(AccessoryOrHatName); if (!item) throw new Error(`Item Not Found ${AccessoryOrHatName} of type ${itemType}`); if (isAccessory && item.id == this.tailType) return this; if (!isAccessory && item.id == this.hatType) return this; if (item.price > this.resource.points) return this; if (this.#purchasedItems[itemType].includes(item.id)) return this; this.clientPacketOrganizer.buy(item.id, itemType).parseLast(); return this; } /** * @description This packet is used to **equip** an `Accessory` or `Hat` in the game. * @ignored if you don't have that item yet. the packet will be ignored. * @param name - The name of the hat or accessory you want to equip. * @param itemType can be either `Accessory` or `Hat`. * @note If you haven't spawned in the game yet, this packet will be ignored. */ equip< Type extends "Hat" | "Accessory", Name extends Type extends "Hat" ? HatName : AccessoryName >(itemType: Type, name: Name | "None"): MyPlayer; /** * @description This packet is used to **equip** an `Accessory` or `Hat` in the game. * @ignored if you don't have that item yet. the packet will be ignored. * @param id - The id of the hat or accessory you want to equip. * @param itemType can be either `Accessory` or `Hat`. * @note If you haven't spawned in the game yet, this packet will be ignored. */ equip<Type extends "Hat" | "Accessory">(itemType: Type, id: number): MyPlayer; equip(itemType: "Accessory" | "Hat", AccessoryOrHatName: string | number): MyPlayer { if (!this.state.alive || !this.isInitialized) return this; const isAccessory = itemType == "Accessory"; const item = (isAccessory ? getAccessory : getHat)(AccessoryOrHatName.toString()); if (AccessoryOrHatName == "None") { this.clientPacketOrganizer.equip(0, itemType).parseLast(); return this; } if (!item) throw new Error(`Item Not Found ${AccessoryOrHatName} of type ${itemType}`); if (item.price != 0 && !this.#purchasedItems[itemType].includes(item.id)) return this; this.clientPacketOrganizer.equip(item.id, itemType).parseLast(); return this; } /** * @description This packet is used to upgrade your chosen items in the game. * @param listOf a list of names of the item you want to upgrade to. * @note If you do not meet the necessary conditions for the upgrade, this packet will be ignored. */ upgrade<Name extends GameItemName | WeaponName>(listOf: Name[]): MyPlayer; /** * @description This packet is used to upgrade your chosen items in the game. * @param name The name of the item you want to upgrade to. * @note If you do not meet the necessary conditions for the upgrade, this packet will be ignored. */ upgrade<Name extends GameItemName | WeaponName>(itemName: Name): MyPlayer; upgrade<Name extends GameItemName | WeaponName>(items: Name | Name[]): MyPlayer { if (items instanceof Array) { for (let itemName of items) { this.upgrade(itemName); } return this; } let item: { id: number; age: number } | undefined = getItem(items); const id = item ? item.id + 16 : (item = getWeapon(items))?.id ?? -1; const age = item?.age ?? -Infinity; if ( this.#upgradeLevel - this.#currentUpgradeLevel == 0 || age < this.#currentUpgradeLevel || age > this.#upgradeLevel ) return this; if (id == -1) throw new Error(`Item Not Found ${items}`); this.clientPacketOrganizer.upgrade(id).parseLast(); return this; } /** * @description This packet is used to punch (hit) in the game * @param direction is the direction the player should face before updating the status (in radian). */ punch(direction: number): MyPlayer { if (!this.state.alive || !this.isInitialized) return this; this.clientPacketOrganizer.setPunchState(true, direction).parseLast(); this.clientPacketOrganizer.setPunchState(false, direction).parseLast(); return this; } /** * @description this packet is used to toggle auto-punch * @notSafe true */ autoPunch(state: boolean): MyPlayer { if (!this.state.alive || !this.isInitialized) return this; if (state == this.state.autoPunchState) return this; const oldState = this.state.autoPunchState; this.state.autoPunchState = state; this.clientPacketOrganizer .toggleAutoPunchState() .parseLast() .then((isSuccess) => { if (!isSuccess) this.state.autoPunchState = oldState; }); return this; } /** * @description This packet is used to send a message for nearby players * @param content the message content you want to send */ chat(message: string): MyPlayer { if (!this.state.alive || !this.isInitialized) return this; this.clientPacketOrganizer.chat(message).parseLast(); return this; } /** * @description this packet is used to update your character direction in the game. * @param direction the direction your character is facing (in radian) * * @note this direction may be overwritten by `punch` */ setDirection(direction: number): MyPlayer { if (!this.state.alive || !this.isInitialized) return this; this.clientPacketOrganizer.setDirection(direction).parseLast(); return this; } /** * @description This packet is used to change the item held by your player. * @param itemName - The name of the weapon or item you want to hold. * @param itemType can be either `Weapon` or `Item`. */ hold< Type extends "Weapon" | "Item", Name extends Type extends "Item" ? GameItemName : WeaponName >(itemType: Type, itemName: Name): MyPlayer; /** * @description This packet is used to change the item held by your player. * @param groupType - The name of the group you want to hold * @param itemType must be `Item`. */ hold(itemType: "Item", groupType: BuildGroup): MyPlayer; /** * @description This packet is used to change the item held by your player. * @param weaponType - The name of the weapon you want to hold. * @param itemType must be `Weapon`. */ hold(itemType: "Weapon", weaponType: WeaponType): MyPlayer; /** * @description This packet is used to change the item held by your player. * @param itemID - The id of the weapon or item you want to hold. * @param itemType can be either `Weapon` or `Item`. */ hold(itemType: "Weapon" | "Item", itemID: number): MyPlayer; hold( itemType: "Weapon" | "Item", itemName: GameItemName | WeaponName | BuildGroup | WeaponType | number ) { return this.#hold(itemType, itemName); } #hold( itemType: "Weapon" | "Item", itemName: GameItemName | WeaponName | BuildGroup | WeaponType | number ): MyPlayer { if (!this.state.alive || !this.isInitialized) return this; const isWeapon = itemType == "Weapon"; const item = isWeapon ? getWeapon( itemName == "primary" ? this.primary : itemName == "secondary" ? this.secondary || 0 : (itemName as number) ) : getItem( itemName.toString().endsWith("Type") ? this[itemName as BuildGroup] ?? -1 : (itemName as number) ); if (!item) throw new Error(`Item Not Found ${itemName} of type ${itemType}`); if (!isWeapon && item.id == this.buildType) return this; if (isWeapon && !this.#weaponKit.includes(item.id)) return this; if (!isWeapon && !this.#itemsKit.includes(item.id)) return this; const group = getGroup("group" in item ? item.group : -1); const ItemUsageCount = isWeapon ? -1 : this.#itemUsage.get(group?.id || -1); if (!isWeapon && group && group.limit <= ItemUsageCount!) return this; this.clientPacketOrganizer.holdItem(item.id, isWeapon).parseLast(); return this; } /** * @description This function is used to perform an action of placing or using an item by the player in the game. * @param itemID The id of the item that the player wants to use or place. * @param backTo The item that the player should return to after completing the task. * @param direction The direction in which the player should place or use the item. */ place(itemID: number, backTo: WeaponName | WeaponType, direction?: number): MyPlayer; /** * @description This function is used to perform an action of placing or using an item by the player in the game. * @param itemName The name of the item that the player wants to use or place. * @param backTo The item that the player should return to after completing the task. * @param direction The direction in which the player should place or use the item. */ place( itemName: GameItemName | BuildGroup, backTo: WeaponName | WeaponType, direction?: number ): MyPlayer; place( itemName: GameItemName | BuildGroup | number, backTo: WeaponName | WeaponType, direction: number = this.angle ): MyPlayer { if (!this.state.alive || !this.isInitialized) return this; this.#hold("Item", itemName); this.punch(direction); this.#hold("Weapon", backTo); return this; } /** * * @description this packet is used to create a team in the game. * @ignored If you are already in a team or if the title you provide is already being used by another team. this packet will be ignored * @param teamTitle unique title for the team. * * @note you can have one team at the same time. */ createTeam(teamTitle: string): MyPlayer { if (!this.state.alive || !this.isInitialized) return this; if (typeof this.teamID == "string") return this; this.clientPacketOrganizer.createTeam(teamTitle).parseLast(); return this; } /** * * @description this packet is used to **leave your current team. * @ignored If you are not in a team. this packet will be ignored. */ leaveTeam(): MyPlayer { if (!this.state.alive || !this.isInitialized) return this; if (typeof this.teamID != "string") return this; this.clientPacketOrganizer.leaveTeam().parseLast(); return this; } /** * * @description This packet is used to ask to join a team in the game. * @ignored If you are already in a team, this packet will be ignored. * @param teamTitle The name of the team you want to join. * @note If the team cannot be found, this packet will be ignored. */ joinRequest(teamTitle: string): MyPlayer { if (!this.state.alive || !this.isInitialized) return this; if (typeof this.teamID == "string") return this; this.clientPacketOrganizer.joinRequest(teamTitle).parseLast(); return this; } // Getters & Setters: get currentAge() { return this.#currentAge; } get currentXP() { return this.#currentXP; } get maxXP() { return this.#maxXP; } get mapPoints() { return this.#mapPoints; } get leaderBoard() { return this.#leaderBoard; } get kit() { return [this.#weaponKit, this.#itemsKit]; } get primary() { return this.#weaponKit[0]!; } get secondary() { return this.#weaponKit[1] ?? null; } private get haveMine() { return this.#itemsKit[4] == 13 || this.#itemsKit[4] == 14; } get foodType() { return this.#itemsKit[0]; } get wallType() { return this.#itemsKit[1]; } get spikeType() { return this.#itemsKit[2]; } get millType() { return this.#itemsKit[2]; } get boostType() { return this.#itemsKit[3] ?? -1; } get turretType() { return this.#itemsKit[4 + +this.haveMine]; } }