From 7ffb594e731555fc1fd0f85886537b4c78e4c184 Mon Sep 17 00:00:00 2001 From: charles-m-knox <code@charlesmknox.com> Date: Fri, 6 Sep 2024 17:04:51 -0700 Subject: [PATCH] add basic magic teleport plugin for standard spellbook --- src/engine/net/inbound-packets/walk.packet.ts | 4 + src/engine/world/actor/player/metadata.ts | 9 + src/engine/world/actor/player/player.ts | 3 + src/engine/world/actor/skills.ts | 22 +- src/plugins/buttons/magic-teleports.plugin.ts | 280 +++++++++++++++++- 5 files changed, 311 insertions(+), 7 deletions(-) diff --git a/src/engine/net/inbound-packets/walk.packet.ts b/src/engine/net/inbound-packets/walk.packet.ts index 1dcf5d895..89826de7c 100644 --- a/src/engine/net/inbound-packets/walk.packet.ts +++ b/src/engine/net/inbound-packets/walk.packet.ts @@ -9,6 +9,10 @@ const walkPacket = (player: Player, packet: PacketData) => { size -= 14; } + if (!player.canMove()) { + return; + } + const totalSteps = Math.floor((size - 5) / 2); const firstY = buffer.get('short', 'u', 'le'); diff --git a/src/engine/world/actor/player/metadata.ts b/src/engine/world/actor/player/metadata.ts index f8247df1f..de019e145 100644 --- a/src/engine/world/actor/player/metadata.ts +++ b/src/engine/world/actor/player/metadata.ts @@ -105,4 +105,13 @@ export type PlayerMetadata = { * The ID of the player's currently open skill guide. */ activeSkillGuide: number; + + /** + * Whether or not the player is casting a spell that immobilizes them. + * Different from the base Actor class's teleport metadata property. A use + * case for this is to prevent the player from teleporting multiple times + * before the teleport animation finishes (it takes a few ticks). + */ + castingStationarySpell: boolean; + }; diff --git a/src/engine/world/actor/player/player.ts b/src/engine/world/actor/player/player.ts index 665f5acb5..4647d1763 100644 --- a/src/engine/world/actor/player/player.ts +++ b/src/engine/world/actor/player/player.ts @@ -701,6 +701,9 @@ export class Player extends Actor { } public canMove(): boolean { + if (this.metadata?.castingStationarySpell) { + return false; + } return true; } diff --git a/src/engine/world/actor/skills.ts b/src/engine/world/actor/skills.ts index d38df6e13..8d7553180 100644 --- a/src/engine/world/actor/skills.ts +++ b/src/engine/world/actor/skills.ts @@ -3,6 +3,7 @@ import { serverConfig } from '@server/game/game-server'; import { gfxIds } from '@engine/world/config'; import { Actor } from './actor'; import { Player } from './player'; +import { QueueableTask } from '@engine/action/pipe/task/queueable-task'; export enum Skill { ATTACK, @@ -238,9 +239,24 @@ export class Skills extends SkillShortcuts { return; } - this.actor.sendMessage(`Congratulations, you just advanced a ` + - `${ achievementDetails.name.toLowerCase() } level.`); - this.showLevelUpDialogue(skill, finalLevel); + /** + * Note: For skills like casting magic teleports, if the + * level-up dialogue is shown too quickly, it interrupts the + * task processing queue - the dialogue gets shown but the + * teleport doesn't always finish. + * + * Queueing the level-up dialog 1 tick later will result in the + * dialogue being shown after other events get processed on the + * tick that the xp drop occurred. + */ + this.actor.enqueueBaseTask(new QueueableTask([], this.actor, () => { + (this.actor as Player).sendMessage(`Congratulations, you just advanced a ` + `${ achievementDetails.name.toLowerCase() } level.`); + this.showLevelUpDialogue(skill, finalLevel); + return { + callbackResult: false, + shouldContinueLooping: false, + } + } , null, null)) } } } diff --git a/src/plugins/buttons/magic-teleports.plugin.ts b/src/plugins/buttons/magic-teleports.plugin.ts index 79c8ef14e..de1cc023c 100644 --- a/src/plugins/buttons/magic-teleports.plugin.ts +++ b/src/plugins/buttons/magic-teleports.plugin.ts @@ -4,9 +4,18 @@ import { Position } from '@engine/world/position'; import { animationIds } from '@engine/world/config/animation-ids'; import { soundIds } from '@engine/world/config/sound-ids'; import { gfxIds } from '@engine/world/config/gfx-ids'; +import { itemIds } from '@engine/world/config/item-ids'; +import { Item } from '@engine/world/items/item'; +import { widgets } from '@engine/config/config-handler'; +import { TravelLocation } from '@engine/world/config'; +import { activeWorld } from '@engine/world'; +import { Skill } from '@engine/world/actor/skills'; +import { openHouse } from '@plugins/skills/construction/house'; +import { QueueableTask } from '@engine/action/pipe/task/queueable-task'; enum Teleports { Home = 591, + House = 581, Varrock = 12, Lumbridge = 15, Falador = 18, @@ -17,10 +26,121 @@ enum Teleports { Ape_atoll = 569 } +/** + * Keeps track of the cost of performing basic teleport spells. + * + * As of 2024-09-02 the magic system isn't fully implemented, so there isn't + * really a centralized location for storing and processing spell costs. + * Defining it here is the alternative. + * + * If needed, it can be exported, but it's not exported in order to keep this + * plugin self-contained. + */ +const MagicCosts: Record<number, MagicCost> = { + [Teleports.Varrock]: { + [itemIds.runes.air]: 3, + [itemIds.runes.law]: 1, + [itemIds.runes.fire]: 1, + }, + [Teleports.Lumbridge]: { + [itemIds.runes.air]: 3, + [itemIds.runes.law]: 1, + [itemIds.runes.earth]: 1, + }, + [Teleports.Falador]: { + [itemIds.runes.air]: 3, + [itemIds.runes.law]: 1, + [itemIds.runes.water]: 1, + }, + [Teleports.House]: { + [itemIds.runes.air]: 1, + [itemIds.runes.law]: 1, + [itemIds.runes.earth]: 1, + }, + [Teleports.Camelot]: { + [itemIds.runes.air]: 5, + [itemIds.runes.law]: 1, + }, + [Teleports.Ardougne]: { + [itemIds.runes.water]: 2, + [itemIds.runes.law]: 2, + }, + [Teleports.Watchtower]: { + [itemIds.runes.law]: 2, + [itemIds.runes.earth]: 2, + }, + [Teleports.Trollheim]: { + [itemIds.runes.law]: 2, + [itemIds.runes.fire]: 2, + }, + [Teleports.Ape_atoll]: { + [itemIds.runes.fire]: 2, + [itemIds.runes.law]: 2, + [itemIds.runes.water]: 2, + [itemIds.banana]: 1, + }, +} + +/** + * Mapping of the various teleport locations. Some are usable directly from + * the `activeWorld.travelLocations` lookups, but others are not. + */ +const TeleportLocations: Record<number, Position> = { + [Teleports.Home]: new Position(3218, 3218), + [Teleports.Varrock]: new Position(3212, 3424), + [Teleports.Lumbridge]: new Position(3224, 3218), + [Teleports.Falador]: new Position(2965, 3380), + [Teleports.Camelot]: new Position(2757, 3478), + [Teleports.Ardougne]: new Position(2662, 3307), + [Teleports.Watchtower]: new Position(2934, 4714, 2), + [Teleports.Trollheim]: (activeWorld.travelLocations.find('Trollheim') as TravelLocation).position, + [Teleports.Ape_atoll]: new Position(2798, 2798, 1), +} + +const TeleportXP: Record<number, number> = { + [Teleports.Varrock]: 35, + [Teleports.Lumbridge]: 41, + [Teleports.Falador]: 48, + [Teleports.House]: 30, + [Teleports.Camelot]: 55.5, + [Teleports.Ardougne]: 61, + [Teleports.Watchtower]: 68, + [Teleports.Trollheim]: 68, + [Teleports.Ape_atoll]: 74, +} + const buttonIds: number[] = [ - 591, // Home Teleport + Teleports.Home, + Teleports.Varrock, + Teleports.Lumbridge, + Teleports.Falador, + Teleports.House, + Teleports.Camelot, + Teleports.Ardougne, + Teleports.Watchtower, + Teleports.Trollheim, + Teleports.Ape_atoll, ]; +function queueTeleport(player: Player, pos: Position) { + player.enqueueBaseTask(new QueueableTask([], player, () => { + player.teleport(pos); + player.metadata.castingStationarySpell = false; + return { + callbackResult: false, + shouldContinueLooping: false, + } + }, null, null)) +} + +/** + * Casts the home teleport spell (not their player owned home). + * + * @param elapsedTicks A counter of the number of elapsed ticks since the + * teleport started. Used to increment through the teleporting animation up + * until the actual teleport occurs. + * @returns `true` once the teleport finishes, `false` until it finishes + */ function homeTeleport(player: Player, elapsedTicks: number): boolean { if (elapsedTicks === 0) { player.playAnimation(animationIds.homeTeleportDraw); @@ -42,14 +162,133 @@ function homeTeleport(player: Player, elapsedTicks: number): boolean { player.playAnimation(animationIds.homeTeleport); player.playGraphics({ id: gfxIds.homeTeleport, delay: 0, height: 0 }); } else if (elapsedTicks === 22) { - player.teleport(new Position(3218, 3218)); + queueTeleport(player, TeleportLocations[Teleports.Home]) + return true; + } + + return false; +} + +type MagicCost = Record<number, number>; + +/** + * Determines if the player currently has infinite quantities of a resource, + * such as a fire staff for fire runes. + * + * @param resource The item ID for the resource, such as a fire rune. + */ +function hasInfinite(player: Player, resource: number): boolean { + switch (resource) { + case itemIds.runes.air: { + if (player.equipment.has(itemIds.staffs.air)) { + return true; + } + break; + } + case itemIds.runes.fire: { + if (player.equipment.has(itemIds.staffs.fire)) { + return true; + } + break; + } + case itemIds.runes.water: { + if (player.equipment.has(itemIds.staffs.water)) { + return true; + } + break; + } + case itemIds.runes.earth: { + if (player.equipment.has(itemIds.staffs.earth)) { + return true; + } + break; + } + } + + return false; +} + +/** + * Deducts the cost of the spell from the player's inventory. + * + * @returns `false` if the player lacks the required runes + */ +function expenseMagic(player: Player, cost: MagicCost): boolean { + if (!cost) return true; + + const indexesToUpdate: number[] = []; + const itemsToUpdate: Record<number, Item> = []; + + for (const requiredItemId in cost) { + const itemId: number = Number(requiredItemId); + if (hasInfinite(player, itemId)) { + continue; + } + + const itemIndex: number = player.inventory.findIndex(itemId); + if (itemIndex < 0) { + return false; + } + + const newItem: Item = { + amount: player.inventory.amount(itemId) - cost[requiredItemId], + itemId: itemId, + } + + if (newItem.amount < 0) { + return false; + } + + itemsToUpdate[itemIndex] = newItem; + indexesToUpdate.push(itemIndex); + } + + for (let i = 0; i < indexesToUpdate.length; i++) { + if (itemsToUpdate[indexesToUpdate[i]].amount === 0) { + player.inventory.remove(indexesToUpdate[i]) + } else { + player.inventory.set(indexesToUpdate[i], itemsToUpdate[indexesToUpdate[i]]); + } + } + + return true; +} + +function genericTeleport(player: Player, elapsedTicks: number, target: Position, teleportId?: number): boolean { + if (elapsedTicks === 0) { + player.playAnimation(animationIds.teleport) + player.outgoingPackets.playSound(soundIds.teleport, 10); + player.playGraphics({ id: gfxIds.teleport, delay: 0, height: 100 }); + } else if (elapsedTicks === 3) { + switch (teleportId) { + case Teleports.House: { + openHouse(player); + break; + } + default: { + queueTeleport(player, target) + + // warning: undefined xp values cause the xp to reset to 0, + // so make sure to always assert that it's defined + if (teleportId && TeleportXP[teleportId]) { + player.enqueueBaseTask(new QueueableTask([], player, () => { + player.skills.addExp(Skill.MAGIC, TeleportXP[teleportId]) + return { callbackResult: false, shouldContinueLooping: false }; + }, null, null)); + } + break; + } + } + player.playAnimation(animationIds.reset) return true; } return false; } -export const activate = (task: TaskExecutor<ButtonAction>, elapsedTicks: number = 0) => { +const insufficient = 'You do not have enough runes to cast this spell.'; + +const activate = (task: TaskExecutor<ButtonAction>, elapsedTicks: number = 0) => { const { player, buttonId } = task.actionData; let completed: boolean = false; @@ -58,9 +297,42 @@ export const activate = (task: TaskExecutor<ButtonAction>, elapsedTicks: number case Teleports.Home: completed = homeTeleport(player, elapsedTicks); break; + case Teleports.Varrock: + case Teleports.Lumbridge: + case Teleports.Falador: + case Teleports.House: + case Teleports.Camelot: + case Teleports.Ardougne: + case Teleports.Watchtower: + case Teleports.Trollheim: + case Teleports.Ape_atoll: { + if (elapsedTicks === 0) { + // prevents the player from spamming the spell + if (player.metadata?.castingStationarySpell) { + player.sendMessage('You are already teleporting.'); + task.stop(); + return; + } + + player.metadata.castingStationarySpell = true; + + if (!expenseMagic(player, MagicCosts[buttonId])) { + player.sendMessage(insufficient); + completed = true; + break; + } + + player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory); + } + + completed = genericTeleport(player, elapsedTicks, TeleportLocations[buttonId], buttonId); + + break; + } } - if(completed) { + if (completed) { + player.metadata.castingStationarySpell = false; task.stop(); } };