diff --git a/packages/common/types/mob.d.ts b/packages/common/types/mob.d.ts index 5de50dec31..c3d0a1e78d 100644 --- a/packages/common/types/mob.d.ts +++ b/packages/common/types/mob.d.ts @@ -1,5 +1,23 @@ import type { Bonuses, Stats } from '@kaetram/common/types/item'; +// The mobs.json drops structure. +export interface MobDrop { + key: string; + count?: number; // Defaults to 1 if undefined. + chance: number; + variable?: number; // Whether or not the drop amount is randomized. + // Quest and achievement requirements. + quest?: string; + achievement?: string; + status?: 'started' | 'notstarted' | 'finished'; // Optional parameter for when to drop item given quest/achievement. +} + +export interface MobDropTable { + drops: MobDrop[]; + achievement?: string; // Achievement that has to be finished to unlock the drop table. + quest?: string; // Quest that must be completed to unlock drop table. +} + export interface MobSkills { accuracy: number; strength: number; @@ -12,7 +30,7 @@ export interface MobData { name: string; description?: string | string[]; hitPoints?: number; - drops?: { [itemKey: string]: number }; + drops?: MobDrop[]; dropTables?: string[]; level?: number; skills?: MobSkills; diff --git a/packages/server/data/items.json b/packages/server/data/items.json index 4d7bb7f806..c4f4c9b5aa 100644 --- a/packages/server/data/items.json +++ b/packages/server/data/items.json @@ -1537,7 +1537,7 @@ "berylgoldring": { "type": "ring", "name": "Beryl Gold Ring", - "price": 200, + "price": 2000, "skill": "defense", "level": 20, "attackStats": { @@ -2693,8 +2693,8 @@ }, "goldnugget": { "type": "object", - "name": "Gold Ore", - "description": "Raw gold ore, will need to be processed before it can be turned into something nice.", + "name": "Gold Nugget", + "description": "A piece of pure gold. For some reason it still has to be processed before it can be used.", "price": 650 }, "iboore": { diff --git a/packages/server/data/mobs.json b/packages/server/data/mobs.json index 9c82577a25..bb51c7936d 100644 --- a/packages/server/data/mobs.json +++ b/packages/server/data/mobs.json @@ -134,7 +134,20 @@ "goblin": { "name": "Goblin", "description": "Didn't I see this same style of goblin in RuneScape?", - "drops": { "tinsword": 10000, "copperhelmet": 10000, "tinchestplate": 10000 }, + "drops": [ + { + "key": "tinsword", + "chance": 10000 + }, + { + "key": "copperhelmet", + "chance": 10000 + }, + { + "key": "tinchestplate", + "chance": 10000 + } + ], "dropTables": ["ordinary", "arrows", "unusual", "vegetables", "fruits", "mushrooms"], "hitPoints": 90, "level": 7, @@ -434,7 +447,16 @@ "adherer": { "name": "Dark Mage", "description": "I heard that these things drop roses that I can then use to hit things with.", - "drops": { "darkmagehood": 600, "darkmagehoodgold": 20 }, + "drops": [ + { + "key": "darkmagehood", + "chance": 600 + }, + { + "key": "darkmagehoodgold", + "chance": 20 + } + ], "dropTables": ["ordinary", "unusual", "shards"], "hitPoints": 200, "level": 38, @@ -2028,7 +2050,12 @@ "icevulture": { "name": "Ice Vulture", "description": "It's like a frozen version of the vicious bird I've seen before.", - "drops": { "feather": 25000 }, + "drops": [ + { + "key": "feather", + "chance": 25000 + } + ], "dropTables": ["ordinary", "shards", "unusual", "arrows"], "hitPoints": 300, "level": 96, @@ -3680,7 +3707,12 @@ "vulture": { "name": "Vulture", - "drops": { "feather": 25000 }, + "drops": [ + { + "key": "feather", + "chance": 25000 + } + ], "dropTables": ["ordinary", "shards", "unusual", "arrows"], "hitPoints": 100, "level": 16, diff --git a/packages/server/data/spawns.json b/packages/server/data/spawns.json index 9cb84edf35..c9410d7acc 100644 --- a/packages/server/data/spawns.json +++ b/packages/server/data/spawns.json @@ -11,12 +11,24 @@ "224-359": { "name": "Hermit Crab Warrior", - "drops": { - "flask": 200, - "gold": 750, - "spear": 75, - "platearmor": 100 - }, + "drops": [ + { + "key": "flask", + "chance": 2000 + }, + { + "key": "gold", + "chance": 7500 + }, + { + "key": "ironspear", + "chance": 750 + }, + { + "key": "platearmor", + "chance": 1000 + } + ], "dropTables": ["ordinary", "unusual", "warriorcrab"], "roaming": true, "miniboss": true, @@ -29,13 +41,12 @@ "197-633": { "name": "Water Guardian", - "drops": { - "redguardarmor": 75, - "redguardarcherarmor": 75, - "ring1": 100, - "scimitar": 100, - "watermelonbow": 100 - }, + "drops": [ + { + "key": "ring1", + "chance": 1000 + } + ], "miniboss": true, "level": 36, "hitPoints": 350, @@ -56,20 +67,63 @@ "175-635": { "name": "Mimic", "achievement": "notwhatitseems", - "drops": { - "gold": 700, - "bigflask": 300, - "spear": 100, - "ring1": 100, - "pendant1": 100, - "goldring": 14 - } + "drops": [ + { + "key": "gold", + "chance": 7000 + }, + { + "key": "bigflask", + "chance": 3000 + }, + { + "key": "crystalspear", + "chance": 1000 + }, + { + "key": "ring1", + "chance": 1000 + }, + { + "key": "pendant1", + "chance": 1000 + }, + { + "key": "goldnugget", + "chance": 1000 + } + ] }, "411-732": { "name": "Mimic", "achievement": "treadcarefully", - "drops": { "gold": 300, "bigflask": 500, "spear": 100, "ring1": 100, "pendant1": 100 } + "drops": [ + { + "key": "gold", + "chance": 7000 + }, + { + "key": "bigflask", + "chance": 3000 + }, + { + "key": "crystalspear", + "chance": 1000 + }, + { + "key": "ring1", + "chance": 1000 + }, + { + "key": "pendant1", + "chance": 1000 + }, + { + "key": "goldnugget", + "chance": 1000 + } + ] }, "228-156": { @@ -78,7 +132,32 @@ "hitPoints": 1500, "level": 30, "defenseLevel": 10, - "drops": { "gold": 800, "bigflask": 400, "silverring": 10, "scimitar": 75, "pendant1": 125 } + "drops": [ + { + "key": "gold", + "chance": 7000 + }, + { + "key": "bigflask", + "chance": 3000 + }, + { + "key": "crystalspear", + "chance": 1000 + }, + { + "key": "ring1", + "chance": 1000 + }, + { + "key": "pendant1", + "chance": 1000 + }, + { + "key": "goldnugget", + "chance": 1000 + } + ] }, "460-725": { @@ -87,13 +166,24 @@ "hitPoints": 2200, "level": 84, "defenseLevel": 10, - "drops": { - "gold": 800, - "manaflask": 100, - "sproutring": 10, - "bluescimitar": 50, - "shardt1": 50 - } + "drops": [ + { + "key": "gold", + "chance": 7000 + }, + { + "key": "manaflask", + "chance": 3000 + }, + { + "key": "sproutring", + "chance": 100 + }, + { + "key": "shardt2", + "chance": 3000 + } + ] }, "855-611": { @@ -102,7 +192,20 @@ "hitPoints": 2200, "level": 84, "defenseLevel": 10, - "drops": { "gold": 800, "cure": 100, "shardt2": 25, "bluescimitar": 50, "greenpendant": 50 } + "drops": [ + { + "key": "gold", + "chance": 8000 + }, + { + "key": "cure", + "chance": 3000 + }, + { + "key": "greenpendant", + "chance": 500 + } + ] }, "677-671": { @@ -111,7 +214,20 @@ "hitPoints": 2200, "level": 84, "defenseLevel": 10, - "drops": { "gold": 800, "cure": 100, "shardt2": 25, "bluescimitar": 50, "greenpendant": 50 } + "drops": [ + { + "key": "gold", + "chance": 8000 + }, + { + "key": "cure", + "chance": 3000 + }, + { + "key": "greenpendant", + "chance": 500 + } + ] }, "706-555": { @@ -120,7 +236,20 @@ "hitPoints": 1300, "level": 62, "defenseLevel": 15, - "drops": { "gold": 500, "cure": 50, "shardt1": 50, "bluescimitar": 50, "greenpendant": 45 } + "drops": [ + { + "key": "gold", + "chance": 8000 + }, + { + "key": "cure", + "chance": 3000 + }, + { + "key": "greenpendant", + "chance": 500 + } + ] }, "168-31": { @@ -164,7 +293,16 @@ "name": "Ice Knight", "miniboss": true, "level": 62, - "drops": { "gold": 300, "icesword": 1000 } + "drops": [ + { + "key": "gold", + "chance": 3000 + }, + { + "key": "icesword", + "chance": 10000 + } + ] }, "784-676": { diff --git a/packages/server/data/tables.json b/packages/server/data/tables.json index a9a25d9586..479a2aa805 100644 --- a/packages/server/data/tables.json +++ b/packages/server/data/tables.json @@ -1,89 +1,286 @@ { "warriorcrab": { - "bead": 100000 + "drops": [ + { + "key": "bead", + "chance": 100000 + } + ] }, "ordinary": { - "gold": 30000, - "flask": 12000, - "bigflask": 7200, - "manaflask": 7600, - "bigmanaflask": 5000, - "burger": 6800, - "string": 12000, - "feather": 10000 + "drops": [ + { + "key": "gold", + "chance": 30000 + }, + { + "key": "flask", + "chance": 12000 + }, + { + "key": "bigflask", + "chance": 7200 + }, + { + "key": "manaflask", + "chance": 7600 + }, + { + "key": "bigmanaflask", + "chance": 5000 + }, + { + "key": "burger", + "chance": 100000 + }, + { + "key": "string", + "chance": 100000 + }, + { + "key": "feather", + "chance": 100000 + } + ] }, "arrows": { - "arrow": 24000, - "firearrow": 7500, - "poisonarrow": 7000, - "lightningarrow": 6500, - "icearrow": 4000, - "nisocarrow": 3000, - "cinnabararrow": 2500, - "pythararrow": 2500, - "iboarrow": 2500 + "drops": [ + { + "key": "arrow", + "chance": 24000 + }, + { + "key": "firearrow", + "chance": 7500 + }, + { + "key": "poisonarrow", + "chance": 7000 + }, + { + "key": "lightningarrow", + "chance": 6500 + }, + { + "key": "icearrow", + "chance": 4000 + }, + { + "key": "nisocarrow", + "chance": 3000 + }, + { + "key": "cinnabararrow", + "chance": 2500 + }, + { + "key": "pythararrow", + "chance": 2500 + }, + { + "key": "iboarrow", + "chance": 2500 + } + ] }, "unusual": { - "blackpotion": 3000, - "hotsauce": 3000, - "cure": 3000, - "logs": 2000, - "stick": 5000, - "bead": 3000, - "banana": 3000, - "watermelon": 3000, - "apple": 3000 + "drops": [ + { + "key": "blackpotion", + "chance": 3000 + }, + { + "key": "cure", + "chance": 3000 + }, + { + "key": "logs", + "chance": 4500 + }, + { + "key": "stick", + "chance": 5000 + }, + { + "key": "bead", + "chance": 2500 + } + ] }, "shards": { - "shardt1": 2000, - "shardt2": 1500, - "shardt3": 1000, - "shardt4": 500, - "shardt5": 200 + "drops": [ + { + "key": "shardt1", + "chance": 2000 + }, + { + "key": "shardt2", + "chance": 1500 + }, + { + "key": "shardt3", + "chance": 1000 + }, + { + "key": "shardt4", + "chance": 500 + }, + { + "key": "shardt5", + "chance": 200 + } + ] }, "vegetables": { - "onion": 5000, - "chilli": 5000, - "potato": 5000, - "tomato": 5000 + "drops": [ + { + "key": "onion", + "chance": 5000 + }, + { + "key": "chilli", + "chance": 5000 + }, + { + "key": "potato", + "chance": 5000 + }, + { + "key": "tomato", + "chance": 5000 + } + ] }, "fruits": { - "strawberry": 8000, - "raspberry": 8000, - "plum": 8000, - "pineapple": 8000, - "peach": 8000, - "mulberry": 8000, - "mango": 8000 + "drops": [ + { + "key": "strawberry", + "chance": 8000 + }, + { + "key": "raspberry", + "chance": 8000 + }, + { + "key": "plum", + "chance": 8000 + }, + { + "key": "pineapple", + "chance": 8000 + }, + { + "key": "peach", + "chance": 8000 + }, + { + "key": "mulberry", + "chance": 8000 + }, + { + "key": "mango", + "chance": 8000 + }, + { + "key": "banana", + "chance": 8000 + }, + { + "key": "watermelon", + "chance": 8000 + }, + { + "key": "apple", + "chance": 8000 + } + ] }, "mushrooms": { - "mushroom1": 6000, - "mushroom2": 6000, - "mushroom3": 6000, - "mushroom4": 6000, - "mushroom5": 6000, - "mushroom6": 6000 + "drops": [ + { + "key": "mushroom1", + "chance": 6000 + }, + { + "key": "mushroom2", + "chance": 6000 + }, + { + "key": "mushroom3", + "chance": 6000 + }, + { + "key": "mushroom4", + "chance": 6000 + }, + { + "key": "mushroom5", + "chance": 6000 + }, + { + "key": "mushroom6", + "chance": 6000 + } + ] }, "manafruits": { - "arcanahypercube": 5000, - "ettispinefruit": 5000, - "galaxylilies": 5000, - "godscroissant": 5000, - "stellarcluster": 5000, - "harmoniccrossband": 5000, - "livinganchorroot": 5000, - "manaberry": 5000, - "nexuscross": 5000, - "orchardsbane": 5000, - "wartberries": 5000, - "zephyrsgaleblade": 5000 + "drops": [ + { + "key": "arcanahypercube", + "chance": 5000 + }, + { + "key": "ettispinefruit", + "chance": 5000 + }, + { + "key": "galaxylilies", + "chance": 5000 + }, + { + "key": "godscroissant", + "chance": 5000 + }, + { + "key": "stellarcluster", + "chance": 5000 + }, + { + "key": "harmoniccrossband", + "chance": 5000 + }, + { + "key": "livinganchorroot", + "chance": 5000 + }, + { + "key": "manaberry", + "chance": 5000 + }, + { + "key": "nexuscross", + "chance": 5000 + }, + { + "key": "orchardsbane", + "chance": 5000 + }, + { + "key": "wartberries", + "chance": 5000 + }, + { + "key": "zephyrsgaleblade", + "chance": 5000 + } + ] } } diff --git a/packages/server/src/game/entity/character/mob/handler.ts b/packages/server/src/game/entity/character/mob/handler.ts index 949e0de44e..16ba9750ad 100644 --- a/packages/server/src/game/entity/character/mob/handler.ts +++ b/packages/server/src/game/entity/character/mob/handler.ts @@ -3,10 +3,11 @@ import Utils from '@kaetram/common/util/utils'; import { Modules } from '@kaetram/common/network'; import { Bubble } from '@kaetram/common/network/impl'; +import type Mob from './mob'; import type Map from '../../../map/map'; import type World from '../../../world'; import type Character from '../character'; -import type Mob from './mob'; +import type Player from '../player/player'; /** * The handler class file for the Mob object. We use this to better @@ -84,7 +85,11 @@ export default class Handler { [instance] = element, entity = this.world.entities.get(instance); - // Ignore non-player entities. + /** + * Ensure that the entity exists and that it's a player. Drops do not occur + * if the entity that kills the mob is non-existent (i.e. if killed via command.) + */ + if (!entity?.isPlayer()) continue; // Kill callback is sent to the player who dealt most amount of damage. @@ -93,7 +98,7 @@ export default class Handler { entity.killCallback?.(this.mob); // Drop the mob's loot and pass the owner's username. - this.mob.drop(entity.username); + this.mob.drop(entity as Player); } } diff --git a/packages/server/src/game/entity/character/mob/mob.ts b/packages/server/src/game/entity/character/mob/mob.ts index 4cdd0bea08..e9670978a1 100644 --- a/packages/server/src/game/entity/character/mob/mob.ts +++ b/packages/server/src/game/entity/character/mob/mob.ts @@ -13,15 +13,15 @@ import { Modules, Opcodes } from '@kaetram/common/network'; import { Heal, Movement } from '@kaetram/common/network/impl'; import { SpecialEntityTypes } from '@kaetram/common/network/modules'; -import type DefaultPlugin from '../../../../../data/plugins/mobs/default'; import type Area from '../../../map/areas/area'; import type Areas from '../../../map/areas/areas'; import type World from '../../../world'; import type Entity from '../../entity'; import type Chest from '../../objects/chest'; import type Player from '../player/player'; +import type DefaultPlugin from '../../../../../data/plugins/mobs/default'; import type { Bonuses, Stats } from '@kaetram/common/types/item'; -import type { RawData, MobData, MobSkills } from '@kaetram/common/types/mob'; +import type { RawData, MobData, MobSkills, MobDrop, MobDropTable } from '@kaetram/common/types/mob'; import type { EntityData, EntityDisplayInfo } from '@kaetram/common/types/entity'; interface ItemDrop { @@ -57,7 +57,7 @@ export default class Mob extends Character { private defenseStats: Stats = Utils.getEmptyStats(); private bonuses: Bonuses = Utils.getEmptyBonuses(); - private drops: { [itemKey: string]: number } = {}; // Empty if not specified. + private drops: MobDrop[] = []; private dropTables: string[] = []; private skills: MobSkills = { @@ -248,17 +248,25 @@ export default class Mob extends Character { /** * Handles the dropping of items from the mob. - * @param owner The leading attacker in the mob's death. Only they will be able - * to pick up the drop for a certain period of time. + * @param player The leading player who dealt the most damage. */ - public drop(owner = ''): void { - let drops = this.getDrops(); + public drop(player: Player): void { + let drops = this.getDrops(player); + // No drops were calculated, so we stop here. if (drops.length === 0) return; for (let drop of drops) - this.world.entities.spawnItem(drop.key, this.x, this.y, true, drop.count, {}, owner); + this.world.entities.spawnItem( + drop.key, + this.x, + this.y, + true, + drop.count, + {}, + player.username + ); } /** @@ -291,10 +299,11 @@ export default class Mob extends Character { * drop chance. We add the item to the list if the roll is successful. We continue * by looking through the mob's drop tables and doing the same thing. We then * return the list of items that the mob will drop. + * @param player Theplayer entity that we use to determine what drops we can use. * @returns A list of items that the mob will drop. */ - public getDrops(): ItemDrop[] { + public getDrops(player: Player): ItemDrop[] { let drops: ItemDrop[] = [], // The items that the mob will drop randomItem = this.getRandomItem(this.drops); @@ -302,25 +311,34 @@ export default class Mob extends Character { if (randomItem) drops.push(randomItem); // Add items from the mob's drop table. - drops = [...drops, ...this.getDropTableItems()]; + drops = [...drops, ...this.getDropTableItems(player)]; return drops; } /** * Looks through the drop tables of the mob and iterates through those to get items. + * @param player The player entity that killed the mob. Used for determining whether or + * not we can use the drop table. * @returns List of items from the drop table. */ - private getDropTableItems(): ItemDrop[] { + private getDropTableItems(player: Player): ItemDrop[] { let drops: ItemDrop[] = []; for (let key of Object.values(this.dropTables)) { - let table = dropTables[key as keyof typeof dropTables]; // Pick the table from the list of drop tables. + let table: MobDropTable = dropTables[key as keyof typeof dropTables]; // Pick the table from the list of drop tables. + + // Player doesn't have the achievement for the drop table completed. + if (table.achievement && !player.achievements.get(table.achievement)?.isFinished()) + continue; + + // Player doesn't have the quest completed to have access to the drop table. + if (table.quest && !player.quests.get(table.quest)?.isFinished()) continue; // Something went wrong. if (table) { - let randomItem = this.getRandomItem(table); + let randomItem = this.getRandomItem(table.drops); // Add a random item from the table. if (randomItem) drops.push(randomItem); @@ -338,17 +356,14 @@ export default class Mob extends Character { * @returns Returns an `ItemDrop` object containing the key and the count. */ - private getRandomItem(items: { [key: string]: number }): ItemDrop | undefined { - let keys = Object.keys(items); - + private getRandomItem(items: MobDrop[]): ItemDrop | undefined { // No items to pick from. - if (keys.length === 0) return undefined; + if (items.length === 0) return undefined; - let key = keys[Utils.randomInt(0, keys.length - 1)], - drop = items[key], - count = 1; + let drop = items[Utils.randomInt(0, items.length - 1)], + count = drop.count || 1; // 1 if not specified. - switch (key) { + switch (drop.key) { case 'gold': { count = Utils.randomInt(this.level, this.level * 10); break; @@ -384,9 +399,9 @@ export default class Mob extends Character { let probability = this.world.getDropProbability(); // If the chance is greater than the drop probability, we adjust the drop - if (drop > probability) drop = probability; + if (drop.chance > probability) drop.chance = probability; - return Utils.randomInt(0, probability) < drop ? { key, count } : undefined; + return Utils.randomInt(0, probability) < drop.chance ? { key: drop.key, count } : undefined; } /**