Repository: runejs/server Branch: develop Commit: 3c1e6a757e73 Files: 488 Total size: 31.1 MB Directory structure: gitextract__j1tb7oh/ ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.md │ │ ├── content-feature-plugin.md │ │ └── game-engine-feature.md │ └── workflows/ │ └── build.yml ├── .gitignore ├── .swcrc ├── .vscode/ │ └── settings.json ├── CONTRIBUTING.md ├── Dockerfile ├── FEATURES.md ├── LICENSE ├── README.md ├── biome.json ├── cache/ │ ├── file-names.properties │ ├── main_file_cache.dat0 │ ├── main_file_cache.dat1 │ ├── main_file_cache.dat2 │ ├── main_file_cache.idx0 │ ├── main_file_cache.idx1 │ ├── main_file_cache.idx10 │ ├── main_file_cache.idx11 │ ├── main_file_cache.idx12 │ ├── main_file_cache.idx2 │ ├── main_file_cache.idx255 │ ├── main_file_cache.idx3 │ ├── main_file_cache.idx4 │ ├── main_file_cache.idx5 │ ├── main_file_cache.idx6 │ ├── main_file_cache.idx7 │ ├── main_file_cache.idx8 │ └── main_file_cache.idx9 ├── config/ │ ├── file-names.properties │ └── server-config.example.json ├── data/ │ ├── config/ │ │ ├── combat-styles.json │ │ ├── examine-item-data.yaml │ │ ├── item-spawns/ │ │ │ └── lumbridge/ │ │ │ └── lumbridge.json │ │ ├── items/ │ │ │ ├── barrows/ │ │ │ │ └── dharoks.json │ │ │ ├── bolts.json │ │ │ ├── bones.json │ │ │ ├── containers.json │ │ │ ├── currency.json │ │ │ ├── equipment/ │ │ │ │ ├── amulets.json │ │ │ │ ├── axes.json │ │ │ │ ├── bows.json │ │ │ │ ├── capes.json │ │ │ │ ├── daggers.json │ │ │ │ ├── darts.json │ │ │ │ ├── halberd.json │ │ │ │ ├── hammers.json │ │ │ │ ├── hatchets.json │ │ │ │ ├── hats.json │ │ │ │ ├── javelins.json │ │ │ │ ├── leather-armour.json │ │ │ │ ├── longswords.json │ │ │ │ ├── mauls.json │ │ │ │ ├── pickaxes.json │ │ │ │ ├── shortswords.json │ │ │ │ ├── skillscapes.json │ │ │ │ ├── slayer.json │ │ │ │ ├── spears.json │ │ │ │ ├── staffs.json │ │ │ │ ├── standard-metals/ │ │ │ │ │ ├── adamant-armour.json │ │ │ │ │ ├── black-armour.json │ │ │ │ │ ├── bronze-armour.json │ │ │ │ │ ├── bronze-weapons.json │ │ │ │ │ ├── iron-armour.json │ │ │ │ │ ├── iron-weapons.json │ │ │ │ │ ├── mithril-armour.json │ │ │ │ │ ├── mithril-weapons.json │ │ │ │ │ ├── rune-armour.json │ │ │ │ │ ├── rune-god-armour.json │ │ │ │ │ ├── steel-armour.json │ │ │ │ │ ├── steel-weapons.json │ │ │ │ │ └── white-armour.json │ │ │ │ ├── tiaras.json │ │ │ │ ├── training.json │ │ │ │ └── whips.json │ │ │ ├── food.json │ │ │ ├── holiday/ │ │ │ │ └── partyhats.json │ │ │ ├── icons.json │ │ │ ├── logs.json │ │ │ ├── other.json │ │ │ ├── quests/ │ │ │ │ ├── lost-city.json │ │ │ │ └── witchs-potion.json │ │ │ └── skills/ │ │ │ ├── artisan-tools.json │ │ │ ├── baking.json │ │ │ ├── cooking.json │ │ │ ├── crafting/ │ │ │ │ ├── gems.json │ │ │ │ └── jewelery-moulds.json │ │ │ ├── firemaking.json │ │ │ ├── fishing.json │ │ │ ├── fletching/ │ │ │ │ ├── arrows.json │ │ │ │ └── bolts.json │ │ │ ├── herblore/ │ │ │ │ ├── herbs.json │ │ │ │ ├── ingredients.json │ │ │ │ └── tools.json │ │ │ ├── herblore.json │ │ │ ├── mining.json │ │ │ ├── prayer.json │ │ │ └── runecrafting.json │ │ ├── music/ │ │ │ └── musicRegions.json │ │ ├── npc-spawns/ │ │ │ ├── alkharid/ │ │ │ │ └── alkharid-general.json │ │ │ ├── ardougne/ │ │ │ │ ├── bankers.json │ │ │ │ ├── guards.json │ │ │ │ └── market.json │ │ │ ├── camelot/ │ │ │ │ └── bankers.json │ │ │ ├── canifis/ │ │ │ │ └── bankers.json │ │ │ ├── catherby/ │ │ │ │ └── bankers.json │ │ │ ├── draynor/ │ │ │ │ └── bankers.json │ │ │ ├── edgeville/ │ │ │ │ └── bankers.json │ │ │ ├── falador/ │ │ │ │ ├── bankers.json │ │ │ │ └── guards.json │ │ │ ├── keldagrim/ │ │ │ │ └── bankers.json │ │ │ ├── lumbridge/ │ │ │ │ ├── bankers.json │ │ │ │ ├── lumbridge-general.json │ │ │ │ ├── lumbridge-goblins.json │ │ │ │ └── lumbridge-sheep.json │ │ │ ├── magebank/ │ │ │ │ └── bankers.json │ │ │ ├── nardah/ │ │ │ │ └── bankers.json │ │ │ ├── pestcontrol/ │ │ │ │ └── bankers.json │ │ │ ├── portsarim/ │ │ │ │ └── port-sarim-general.json │ │ │ ├── rimmington/ │ │ │ │ └── rimmington.json │ │ │ ├── shilovillage/ │ │ │ │ └── bankers.json │ │ │ ├── varrock/ │ │ │ │ ├── bankers.json │ │ │ │ ├── blue-moon-inn.json │ │ │ │ └── varrock-general.json │ │ │ └── yanille/ │ │ │ └── bankers.json │ │ ├── npcs/ │ │ │ ├── alkharid.json │ │ │ ├── ardougne.json │ │ │ ├── bankers.json │ │ │ ├── barbarians.json │ │ │ ├── general.json │ │ │ ├── generic-humans.json │ │ │ ├── goblins.json │ │ │ ├── guards.json │ │ │ ├── lumbridge.json │ │ │ ├── port-sarim.json │ │ │ ├── rimmington.json │ │ │ ├── sheep.json │ │ │ └── varrock.json │ │ ├── scenery-spawns.yaml │ │ ├── shops/ │ │ │ ├── alkharid/ │ │ │ │ ├── alkharid-gem-trader.json │ │ │ │ ├── dommiks-crafting-store.json │ │ │ │ ├── louies-armored-legs.json │ │ │ │ └── ranaels-skirt-store.json │ │ │ ├── lumbridge/ │ │ │ │ ├── bobs-axes.json │ │ │ │ └── lumbridge-general-store.json │ │ │ ├── portsarim/ │ │ │ │ └── bettys-magic-emporium.json │ │ │ ├── shilo-village/ │ │ │ │ └── oblis-general-store.json │ │ │ └── varrock/ │ │ │ └── zaffs-staffs.json │ │ ├── travel-locations-data.yaml │ │ ├── widgets.json │ │ └── xteas/ │ │ └── 435.json │ └── saves/ │ └── .gitkeep ├── docker-compose.yml ├── jest.config.ts ├── nodemon.json ├── package.json ├── src/ │ ├── engine/ │ │ ├── action/ │ │ │ ├── action-pipeline.ts │ │ │ ├── hook/ │ │ │ │ ├── action-hook.ts │ │ │ │ ├── hook-filters.test.ts │ │ │ │ ├── hook-filters.ts │ │ │ │ └── task.ts │ │ │ ├── loader.ts │ │ │ └── pipe/ │ │ │ ├── button.action.ts │ │ │ ├── equipment-change.action.ts │ │ │ ├── item-interaction.action.ts │ │ │ ├── item-on-item.action.ts │ │ │ ├── item-on-npc.action.ts │ │ │ ├── item-on-object.action.ts │ │ │ ├── item-on-player.action.ts │ │ │ ├── item-on-world-item.action.ts │ │ │ ├── item-swap.action.ts │ │ │ ├── magic-on-npc.action.ts │ │ │ ├── move-item.action.ts │ │ │ ├── npc-init.action.ts │ │ │ ├── npc-interaction.action.ts │ │ │ ├── object-interaction.action.ts │ │ │ ├── player-command.action.ts │ │ │ ├── player-init.action.ts │ │ │ ├── player-interaction.action.ts │ │ │ ├── prayer.action.ts │ │ │ ├── region-change.action.ts │ │ │ ├── spawned-item-interaction.action.ts │ │ │ ├── task/ │ │ │ │ ├── queueable-task.ts │ │ │ │ ├── walk-to-actor-plugin-task.ts │ │ │ │ ├── walk-to-item-plugin-task.ts │ │ │ │ └── walk-to-object-plugin-task.ts │ │ │ └── widget-interaction.action.ts │ │ ├── config/ │ │ │ ├── config-handler.ts │ │ │ ├── data-dump.ts │ │ │ ├── directories.ts │ │ │ ├── item-config.ts │ │ │ ├── item-spawn-config.ts │ │ │ ├── music-regions-config.ts │ │ │ ├── npc-config.ts │ │ │ ├── npc-spawn-config.ts │ │ │ ├── quest-config.ts │ │ │ ├── shop-config.testskip.ts │ │ │ └── shop-config.ts │ │ ├── interface/ │ │ │ └── interface-state.ts │ │ ├── net/ │ │ │ ├── inbound-packet-handler.ts │ │ │ ├── inbound-packets/ │ │ │ │ ├── add-friend.packet.ts │ │ │ │ ├── add-ignore.packet.ts │ │ │ │ ├── blinking-tab-click.packet.ts │ │ │ │ ├── button-click.packet.ts │ │ │ │ ├── character-design.packet.ts │ │ │ │ ├── chat.packet.ts │ │ │ │ ├── command.packet.ts │ │ │ │ ├── drop-item.packet.ts │ │ │ │ ├── examine.packet.ts │ │ │ │ ├── item-interaction.packet.ts │ │ │ │ ├── item-on-item.packet.ts │ │ │ │ ├── item-on-npc.packet.ts │ │ │ │ ├── item-on-object.packet.ts │ │ │ │ ├── item-on-player.packet.ts │ │ │ │ ├── item-on-world-item.packet.ts │ │ │ │ ├── item-swap.packet.ts │ │ │ │ ├── junk.packet.ts │ │ │ │ ├── magic-attack.packet.ts │ │ │ │ ├── npc-interaction.packet.ts │ │ │ │ ├── number-input.packet.ts │ │ │ │ ├── object-interaction.packet.ts │ │ │ │ ├── pickup-item.packet.ts │ │ │ │ ├── player-interaction.packet.ts │ │ │ │ ├── private-message.packet.ts │ │ │ │ ├── remove-friend.packet.ts │ │ │ │ ├── remove-ignore.packet.ts │ │ │ │ ├── social-button.packet.ts │ │ │ │ ├── walk.packet.ts │ │ │ │ ├── widget-interaction.packet.ts │ │ │ │ └── widgets-closed.packet.ts │ │ │ ├── isaac.ts │ │ │ ├── outbound-packet-handler.ts │ │ │ └── packet.ts │ │ ├── plugins/ │ │ │ ├── content-plugin.ts │ │ │ ├── loader.ts │ │ │ └── reload-content.ts │ │ ├── task/ │ │ │ ├── README.md │ │ │ ├── impl/ │ │ │ │ ├── actor-actor-interaction-task.ts │ │ │ │ ├── actor-landscape-object-interaction-task.ts │ │ │ │ ├── actor-task.ts │ │ │ │ ├── actor-teleport-task.ts │ │ │ │ ├── actor-walk-to-task.ts │ │ │ │ └── actor-world-item-interaction-task.ts │ │ │ ├── task-scheduler.test.ts │ │ │ ├── task-scheduler.ts │ │ │ ├── task.test.ts │ │ │ ├── task.ts │ │ │ ├── types.ts │ │ │ └── utils/ │ │ │ └── _testing.ts │ │ ├── util/ │ │ │ ├── address.ts │ │ │ ├── colors.ts │ │ │ ├── data.ts │ │ │ ├── error-handling.ts │ │ │ ├── files.ts │ │ │ ├── num.ts │ │ │ ├── objects.ts │ │ │ ├── queue.test.ts │ │ │ ├── queue.ts │ │ │ ├── strings.ts │ │ │ ├── time.ts │ │ │ └── varbits.ts │ │ └── world/ │ │ ├── actor/ │ │ │ ├── actor.ts │ │ │ ├── combat.ts │ │ │ ├── dialogue.ts │ │ │ ├── magic.ts │ │ │ ├── metadata.ts │ │ │ ├── npc.ts │ │ │ ├── pathfinding.ts │ │ │ ├── player/ │ │ │ │ ├── achievements.ts │ │ │ │ ├── attack.ts │ │ │ │ ├── cutscenes.ts │ │ │ │ ├── dialogue-action.ts │ │ │ │ ├── metadata.ts │ │ │ │ ├── model.ts │ │ │ │ ├── player-data.ts │ │ │ │ ├── player.ts │ │ │ │ ├── private-messaging.ts │ │ │ │ ├── quest.ts │ │ │ │ └── sync/ │ │ │ │ ├── actor-sync.ts │ │ │ │ ├── npc-sync-task.ts │ │ │ │ └── player-sync-task.ts │ │ │ ├── prayer.ts │ │ │ ├── skills.ts │ │ │ ├── update-flags.ts │ │ │ ├── util.ts │ │ │ └── walking-queue.ts │ │ ├── config/ │ │ │ ├── animation-ids.ts │ │ │ ├── examine-data.ts │ │ │ ├── gfx-ids.ts │ │ │ ├── harvest-tool.ts │ │ │ ├── harvestable-object.ts │ │ │ ├── item-ids.ts │ │ │ ├── object-ids.ts │ │ │ ├── scenery-spawns.ts │ │ │ ├── songs.ts │ │ │ ├── sound-ids.ts │ │ │ ├── travel-locations.ts │ │ │ └── widget.ts │ │ ├── direction.ts │ │ ├── index.ts │ │ ├── instances.ts │ │ ├── items/ │ │ │ ├── item-container.ts │ │ │ ├── item.ts │ │ │ └── world-item.ts │ │ ├── map/ │ │ │ ├── chunk-manager.ts │ │ │ ├── chunk.ts │ │ │ ├── collision-map.ts │ │ │ ├── landscape-object.ts │ │ │ └── region.ts │ │ ├── position.ts │ │ ├── skill-util/ │ │ │ ├── glory-boost.ts │ │ │ ├── harvest-roll.ts │ │ │ └── harvest-skill.ts │ │ ├── sound/ │ │ │ └── music.ts │ │ ├── task.ts │ │ └── world.ts │ ├── plugins/ │ │ ├── buttons/ │ │ │ ├── logout-button.plugin.ts │ │ │ ├── magic-attack.plugin.ts │ │ │ ├── magic-teleports.plugin.ts │ │ │ ├── player-emotes.plugin.ts │ │ │ └── player-setting-button.plugin.ts │ │ ├── combat/ │ │ │ └── combat-styles.plugin.ts │ │ ├── commands/ │ │ │ ├── bank-command.plugin.ts │ │ │ ├── camera-commands.plugin.ts │ │ │ ├── clear-inventory-command.plugin.ts │ │ │ ├── client-config-command.plugin.ts │ │ │ ├── current-position-command.plugin.ts │ │ │ ├── data-dump-command.plugin.ts │ │ │ ├── dump-metadata-command.plugin.ts │ │ │ ├── give-item-command.plugin.ts │ │ │ ├── groups-debug.plugin.ts │ │ │ ├── pathing-commands.plugin.ts │ │ │ ├── player-animation-command.plugin.ts │ │ │ ├── player-graphics-command.plugin.ts │ │ │ ├── quest-list-command.plugin.ts │ │ │ ├── region-debug-commands.plugin.ts │ │ │ ├── reset-camera-command.plugin.ts │ │ │ ├── sound-song-commands.plugin.ts │ │ │ ├── spawn-npc-command.plugin.ts │ │ │ ├── spawn-scenery-command.plugin.ts │ │ │ ├── spawn-test-players-command.plugin.ts │ │ │ ├── stat-commands.plugin.ts │ │ │ ├── teleport-command.plugin.ts │ │ │ ├── transform-command.plugin.ts │ │ │ ├── travel-back-command.plugin.ts │ │ │ ├── travel-command.plugin.ts │ │ │ └── widget-commands.plugin.ts │ │ ├── dialogue/ │ │ │ ├── dialogue-option.plugin.ts │ │ │ └── item-selection.plugin.ts │ │ ├── items/ │ │ │ ├── buckets/ │ │ │ │ ├── empty-container.plugin.ts │ │ │ │ └── fill-container.plugin.ts │ │ │ ├── capes/ │ │ │ │ └── skillcape-emotes.plugin.ts │ │ │ ├── consumables/ │ │ │ │ └── eating.plugin.ts │ │ │ ├── drop-item.plugin.ts │ │ │ ├── equipment/ │ │ │ │ ├── equip-item.plugin.ts │ │ │ │ ├── equipment-stats.plugin.ts │ │ │ │ └── unequip-item.plugin.ts │ │ │ ├── herblore/ │ │ │ │ └── clean-herb.ts │ │ │ ├── move-item.plugin.ts │ │ │ ├── pickup-item.plugin.ts │ │ │ ├── pots/ │ │ │ │ └── empty-pot.plugin.ts │ │ │ ├── rotten-potato/ │ │ │ │ ├── helpers/ │ │ │ │ │ ├── rotten-potato-helpers.ts │ │ │ │ │ └── rotten-potato-travel.ts │ │ │ │ ├── hooks/ │ │ │ │ │ ├── rotten-potato-command-hook.ts │ │ │ │ │ ├── rotten-potato-eat.ts │ │ │ │ │ ├── rotten-potato-item-on-item.ts │ │ │ │ │ ├── rotten-potato-item-on-player.ts │ │ │ │ │ └── rotten-potato-peel.ts │ │ │ │ └── rotten-potato.plugin.ts │ │ │ ├── runecrafting/ │ │ │ │ └── tiaras.plugin.ts │ │ │ ├── shopping/ │ │ │ │ ├── buy-from-shop.plugin.ts │ │ │ │ ├── item-value.plugin.ts │ │ │ │ └── sell-to-shop.plugin.ts │ │ │ └── swap-items.plugin.ts │ │ ├── music/ │ │ │ ├── music-regions.plugin.ts │ │ │ └── music-tab.plugin.ts │ │ ├── npcs/ │ │ │ ├── al-kharid/ │ │ │ │ ├── dommik-crafting-shop.plugin.ts │ │ │ │ ├── gem-trader.plugin.ts │ │ │ │ ├── karim.plugin.ts │ │ │ │ ├── louie-armoured-legs.plugin.ts │ │ │ │ └── ranael-super-skirt.plugin.ts │ │ │ ├── falador/ │ │ │ │ └── custom-guards.plugin.ts │ │ │ ├── lumbridge/ │ │ │ │ ├── bob.plugin.ts │ │ │ │ ├── hans.plugin.ts │ │ │ │ ├── lumbridge-farm-helpers.plugin.ts │ │ │ │ └── shopkeeper.plugin.ts │ │ │ ├── port-sarim/ │ │ │ │ └── betty.plugin.ts │ │ │ └── varrock/ │ │ │ ├── blue-moon-inn.plugin.ts │ │ │ ├── master-smithing-tutor.plugin.ts │ │ │ ├── wilough.plugin.ts │ │ │ └── zaff-superior-staffs.plugin.ts │ │ ├── objects/ │ │ │ ├── bank/ │ │ │ │ ├── bank.plugin.ts │ │ │ │ └── deposit-box.plugin.ts │ │ │ ├── cows/ │ │ │ │ └── cow.plugin.ts │ │ │ ├── crates/ │ │ │ │ └── crates.plugin.ts │ │ │ ├── doors/ │ │ │ │ ├── door.plugin.ts │ │ │ │ ├── double-door.plugin.ts │ │ │ │ └── gate.plugin.ts │ │ │ ├── dungeon-entrances/ │ │ │ │ └── taverly-dungeon-ladder.plugin.ts │ │ │ ├── item-spawns/ │ │ │ │ └── take-axe.plugin.ts │ │ │ ├── ladders/ │ │ │ │ └── ladder.plugin.ts │ │ │ ├── mill/ │ │ │ │ ├── flour-bin.plugin.ts │ │ │ │ ├── hopper-controls.plugin.ts │ │ │ │ └── hopper.plugin.ts │ │ │ └── pickables/ │ │ │ └── pickables.plugin.ts │ │ ├── player/ │ │ │ ├── follow-player.plugin.js │ │ │ ├── login-unlock-emotes.plugin.ts │ │ │ ├── login-update-settings.plugin.ts │ │ │ └── update-friends-list.plugin.ts │ │ ├── quests/ │ │ │ ├── cooks-assistant-quest.plugin.ts │ │ │ ├── goblin-diplomacy-tutorial/ │ │ │ │ ├── goblin-diplomacy-quest.plugin.ts │ │ │ │ ├── melee-tutor-dialogue.ts │ │ │ │ ├── runescape-guide-dialogue.ts │ │ │ │ └── stage-handler.ts │ │ │ ├── quest-journal.plugin.ts │ │ │ └── witchs-potion-quest.plugin.ts │ │ └── skills/ │ │ ├── construction/ │ │ │ ├── con-constants.ts │ │ │ ├── home-saver.ts │ │ │ ├── house.ts │ │ │ ├── index.ts │ │ │ ├── room-builder.ts │ │ │ └── util.ts │ │ ├── crafting/ │ │ │ ├── sheep-plugin.plugin.ts │ │ │ └── spinning-wheel.plugin.ts │ │ ├── firemaking/ │ │ │ ├── chance.ts │ │ │ ├── data.ts │ │ │ ├── firemaking-task.ts │ │ │ ├── index.ts │ │ │ ├── light-fire.ts │ │ │ └── types.ts │ │ ├── fletching/ │ │ │ ├── fletching-constants.ts │ │ │ ├── fletching-types.ts │ │ │ └── fletching.plugin.ts │ │ ├── level-up-dialogue.plugin.ts │ │ ├── mining/ │ │ │ ├── chance.ts │ │ │ ├── mining-task.ts │ │ │ ├── mining.plugin.ts │ │ │ └── prospecting.plugin.ts │ │ ├── prayer/ │ │ │ └── bury-bones.plugin.ts │ │ ├── runecrafting/ │ │ │ ├── runecrafting-altar.plugin.ts │ │ │ ├── runecrafting-constants.ts │ │ │ ├── runecrafting-crafting.plugin.ts │ │ │ ├── runecrafting-tiara.plugin.ts │ │ │ └── runecrafting-types.ts │ │ ├── skill-guides/ │ │ │ ├── Strength.json │ │ │ ├── agility.json │ │ │ ├── attack.json │ │ │ ├── construction.json │ │ │ ├── cooking.json │ │ │ ├── crafting.json │ │ │ ├── defence.json │ │ │ ├── farming.json │ │ │ ├── firemaking.json │ │ │ ├── fishing.json │ │ │ ├── fletching.json │ │ │ ├── herblore.json │ │ │ ├── hitpoint.json │ │ │ ├── magic.json │ │ │ ├── mining.json │ │ │ ├── prayer.json │ │ │ ├── ranged.json │ │ │ ├── runecrafting.json │ │ │ ├── skill-guide-config.ts │ │ │ ├── skill-guides.plugin.ts │ │ │ ├── slayer.json │ │ │ ├── smithing.json │ │ │ ├── thieving.json │ │ │ └── woodcutting.json │ │ ├── smithing/ │ │ │ ├── forging-constants.ts │ │ │ ├── forging-task.ts │ │ │ ├── forging-types.ts │ │ │ ├── forging.plugin.ts │ │ │ ├── smelting-constants.ts │ │ │ ├── smelting-task.ts │ │ │ ├── smelting-types.ts │ │ │ └── smelting.plugin.ts │ │ └── woodcutting/ │ │ ├── chance.ts │ │ ├── index.ts │ │ └── woodcutting-task.ts │ └── server/ │ ├── game/ │ │ ├── game-server-config.ts │ │ ├── game-server-connection.ts │ │ └── game-server.ts │ ├── gateway/ │ │ └── gateway-server.ts │ └── runner.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ node_modules npm-debug.log .gitignore ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: Bug Report about: Create a report to help us improve title: "[BUG]" labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/content-feature-plugin.md ================================================ --- name: Content Feature/Plugin about: New game content features or plugin requests title: "[CONTENT]" labels: enhancement, needs workshopping assignees: '' --- ### Content/Plugin Description > Example: As a player, I expect to receive a bucket of milk when I click to milk a cow while having a bucket in my inventory. ### Acceptance Criteria > Any criteria required to consider this new plugin or feature completed. - [ ] required item 1 - [ ] required item 2 - [ ] required item 3 ## Additional Information > Other additional details relevant to the new feature that were not described above. ================================================ FILE: .github/ISSUE_TEMPLATE/game-engine-feature.md ================================================ --- name: Game Engine Feature about: New game engine or content API features title: "[ENGINE] " labels: needs workshopping, new feature assignees: '' --- ### Game Engine Feature Description > Example: As a plugin developer, I expect to be able to be able to hook into the player init event. ### Acceptance Criteria > Any criteria required to consider this new feature completed. - [ ] required item 1 - [ ] required item 2 - [ ] required item 3 ## Additional Information > Other additional details relevant to the new feature that were not described above. ================================================ FILE: .github/workflows/build.yml ================================================ name: Check & Build Project on: pull_request: branches: [ master, develop, feature/** ] push: branches: [ master, develop ] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v2 - name: Setup Node uses: actions/setup-node@v1 with: node-version: '24.x' - name: Install Node Modules run: npm i - name: Run Linter run: npm run lint:fin - name: Run Formatter run: npm run format:fin - name: Run Tests run: npm run test:fin - name: Run Typecheck run: npm run typecheck - name: Build Project run: npm run build ================================================ FILE: .gitignore ================================================ node_modules .DS_Store /dist /data/dump /data/houses /data/saves/*.json /config/server-config.json server-config.yaml # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories.ts and files .idea *.suo *.ntvs* *.njsproj *.sln *.sw? /.vs /coverage/ ================================================ FILE: .swcrc ================================================ { "$schema": "https://swc.rs/schema.json", "exclude": "node_modules/", "sourceMaps": true, "module": { "type": "commonjs" }, "jsc": { "target": "esnext", "baseUrl": "./src", "paths": { "@engine/*": ["./engine/*"], "@plugins/*": ["./plugins/*"], "@server/*": ["./server/*"] }, "parser": { "syntax": "typescript", "tsx": false, "decorators": true, "dynamicImport": false } } } ================================================ FILE: .vscode/settings.json ================================================ { "editor.tabSize": 4, "files.trimTrailingWhitespace": true, "files.trimFinalNewlines": true, "files.insertFinalNewline": true } ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to RuneJS RuneJS was created with the intention of utilizing JavaScript/TypeScript and Node's innovative features. RxJS was imported for reactive programming as well, opening up opportunities for easy content development. As such, there are a few things we're looking to avoid... 1. Direct ports/copying from Java servers - This defeats the purpose of RuneJS by implementing basic flows that any regular Java-based server would use. Think outside the box and really utilize ES6, TypeScript, Node, and RxJS! :) 2. Additional/outside dependencies - Sometimes additional dependencies cannot be avoided, but we'd like to avoid them as much as possible. RuneJS intends to be simple and easy for anyone to pick up, without requiring the user to set up any databases or additional third party systems. - In some cases this is of course unavoidable, as such we'll handle them on a case-by-case basis. Ultimately if you're looking to contribute, it's best to check in with us on Discord to see if we're already working on a specific feature or have plans for it already. Add us at **Tyn#0001** ## Code Style Code style (linting and formatting) are handled by [Biome](https://biomejs.dev/). It is recommended that you install [the Biome extension for your IDE](https://biomejs.dev/guides/editors/first-party-extensions/). Running `npm run fin` will perform all necessary checks (linting, formatting, typechecking and tests). ## Testing Unit tests can be written using Jest. To execute the test suite, run `npm test` - Test files should be located next to the file under test, and called `file-name.test.ts` - Tests should use the `when / then` pattern made up of composable `describe` statements - Make use of `beforeEach` to set up state before each test After running the tests, you can find code coverage in the `./coverage/` folder. ### When / Then testing pattern Tests should be broken down into a series of `describe` statements, which set up their own internal state when possible. ```ts describe('when there is a player', () => { let player: Player beforeEach(() => { player = createMockPlayer() }) describe('when player is wearing a hat', () => { beforeEach(() => { player.equipment().set(0, someHatItem) }) test('should return true', () => { const result = isWearingHat(player) expect(result).toEqual(true) }) }) }) ``` There are two main benefits to this kind of design: - It serves as living documentation: from reading the `beforeEach` block you can clearly see how the prerequisite of "player is wearing a hat" is achieved - It allows for easy expansion of test cases: future developers can add further statements inside "when player is wearing a hat" if they want to make use of that setup ================================================ FILE: Dockerfile ================================================ FROM node:24 WORKDIR /usr/src/app COPY package.json ./ COPY package-lock.json ./ RUN apt update RUN apt install -y libsdl-pango-dev RUN npm ci COPY src ./src COPY tsconfig.json ./ COPY .swcrc ./ RUN npm run build EXPOSE 43594 CMD [ "npm", "run", "start:standalone" ] ================================================ FILE: FEATURES.md ================================================ [![RuneJS Discord Server](https://img.shields.io/discord/678751302297059336?label=RuneJS%20Discord&logo=discord)](https://discord.gg/5P74nSh) ![RuneJS](https://i.imgur.com/pmkdSfc.png) # RuneJS Feature List ## Game Server * RSA + ISAAC ciphering :heavy_check_mark: * Game Update Server :heavy_check_mark: * Authentication Server :heavy_check_mark: * Server side cache loading :heavy_check_mark: * Client pathing validation via cache mapdata :heavy_check_mark: * Item/object/npc definitions :heavy_check_mark: * Packet queueing :heavy_check_mark: ### Technical Features * Asynchronous server infrastructure w/ Promises & RxJS Observables * A diverse TypeScript plugin system for easily writing new content based off of in-game actions * A simplified JavaScript plugin system for quickly and easily bootstrapping game content * Flexible quest and dialogue systems for more advanced content development * Code compilation via Babel, offering more seamless compilation and redeployment of plugins ## Game World * Private & group Player Instances :heavy_check_mark: * Personal player instance objects and world items :heavy_check_mark: * Bank :heavy_check_mark: * Withdraw/Deposit 1,5,10,All :heavy_check_mark: * As note :heavy_check_mark: * Swap slot :heavy_check_mark: * Insert mode: :heavy_check_mark: * Deposit box :heavy_check_mark: * Audio :yellow_square: * Music :yellow_square: * Playing music :heavy_check_mark: * Music Regions :x: * Music Player tab :x: * Sounds :yellow_square: * Playing sounds :heavy_check_mark: * Sound effects for actions :yellow_square: * Home Teleport :heavy_check_mark: * Emotes :heavy_check_mark: * Skillcape emotes :heavy_check_mark: * Unlockable emotes w/ requirements :heavy_check_mark: * Shop support :heavy_check_mark: * Inventory support :heavy_check_mark: * Swapping items :heavy_check_mark: * Dropping items :heavy_check_mark: * Picking up ground items :heavy_check_mark: * Equipping items :heavy_check_mark: * Doors/gates :yellow_square: * NSEW doors :heavy_check_mark: * Diagonal doors :yellow_square: * Double doors :heavy_check_mark: * Wooden gates :heavy_check_mark: * Climbing ladders & stairs :yellow_square: * Clue Scrolls :x: ### Quests * Cook's Assistant :heavy_check_mark: ### Skills * Combat :yellow_square: * Melee :yellow_square: * Ranged :x: * Magic :x: * Prayer :x: * Cooking :x: * Fletching :x: * Fishing :x: * Firemaking :yellow_square: * Fire lighting :yellow_square: * Chain fires w/ movement :yellow_square: * Herblore :x: * Agility :x: * Thieving :x: * Slayer :x: * Farming :x: * Runecrafting :x: * Construction :x: * Woodcutting :yellow_square: * Formula for success :heavy_check_mark: * Chopping Trees :heavy_check_mark: * Axes :heavy_check_mark: * Birds nests :heavy_check_mark: * Stump ids :yellow_square: * Canoes :x: * Mining :yellow_square: * Formula for success :heavy_check_mark: * Mining ores :heavy_check_mark: * Pickaxes :heavy_check_mark: * Random gems :heavy_check_mark: * Gem ores :heavy_check_mark: * Essence mining :heavy_check_mark: * Empty Rock ids :yellow_square: * Crafting :yellow_square: * Spinning wheel :heavy_check_mark: * Smithing :yellow_square: * Smelting ore to bars :heavy_check_mark: * Forging :yellow_square: * Correct items :heavy_check_mark: * Hiding non-applicable items :yellow_square: ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ [![RuneJS Discord Server](https://img.shields.io/discord/678751302297059336?label=RuneJS%20Discord&logo=discord)](https://discord.gg/5P74nSh) [![RuneJS](https://i.imgur.com/QSXNzwC.png)](https://github.com/runejs/) # RuneJS Game Server RuneJS is a RuneScape game server written in TypeScript and JavaScript. The aim of this project is to create a game server that is both fun and easy to use, while also providing simple content development systems. The game server currently runs a build of RuneScape from October 30th-31st, 2006 (game build #435). No other builds are supported at this time, but may become available in the future. **RuneJS is completely open-source and open to all pull requests and/or issues. Many plugins have been added by contributor pull requests and we're always happy to have more!** ![RuneJS Lumbridge](https://i.imgur.com/KVCqKSb.png) ## Setup ### Prerequisites - [`docker`](https://docs.docker.com/get-docker/) and [`docker-compose`](https://docs.docker.com/compose/install/) - If on Windows, `docker-compose` comes with `docker` ### Running the Game Server 1. Copy the `config/server-config.example.json` and paste it into the same folder using the name `server-config.json` 2. Go into your new `server-config.json` file and modify your RSA modulus and exponent with the ones matching your game client - You may also modify the server's port and host address from this configuration file 3. Build the docker image with `docker-compose build` 4. Run the game server with `docker-compose up` The game server will spin up and be accessible via port 43594. ## Game Client The [RuneScape Java Client #435](https://github.com/runejs/refactored-client-435) must be used to log into a RuneJS game server. ## Additional Commands Before running these commands, you must: 1. have [NodeJS **version 24 or higher**](https://nodejs.org/en/) installed on your machine 2. run `npm install` from the root of this project * `npm run game` Launches the game server by itself without building * `npm run game:dev` Builds and launches the game server by itself in watch mode * `npm run login` Launches the login server by itself without building * `npm run update` Launches the update server by itself without building * `npm run infra` Launches both the login and update server without building * `npm run standalone` Launches all three servers concurrently without building * `npm run build:watch` Builds the application and watches for changes * `npm run build` Builds the application * `npm run lint` Runs Biome in linting mode, use `lint:fix` to autofix * `npm run format` Runs Biome in formatting mode, use `format:fix` to autofix * `npm run test` Runs all tests with Jest * `npm run typecheck` Typechecks the project * `npm run fin` Combines lint:fix, format:fix, test and typecheck into a single command ================================================ FILE: biome.json ================================================ { "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, "files": { "ignoreUnknown": false, "ignore": [] }, "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 4, "lineWidth": 140 }, "organizeImports": { "enabled": true }, "linter": { "enabled": true, "rules": { "recommended": true, "complexity": { "recommended": true, "noBannedTypes": "off", "noForEach": "off", "noStaticOnlyClass": "off", "noUselessConstructor": "off", "noUselessSwitchCase": "off", "useLiteralKeys": "off", "useOptionalChain": "off" }, "correctness": { "recommended": true, "noSwitchDeclarations": "off" }, "performance": { "recommended": true, "noAccumulatingSpread": "off", "noDelete": "off" }, "suspicious": { "recommended": true, "noAssignInExpressions": "off", "noConfusingVoidType": "off", "noDoubleEquals": "off", "noDuplicateObjectKeys": "off", "noExplicitAny": "off", "noGlobalIsNan": "off", "noImplicitAnyLet": "off", "noSelfCompare": "off", "noUnsafeDeclarationMerging": "off" }, "style": { "recommended": true, "noInferrableTypes": "off", "noNonNullAssertion": "off", "noParameterAssign": "off", "noUnusedTemplateLiteral": "off", "noUselessElse": "off", "useEnumInitializers": "off", "useImportType": "off", "useNodejsImportProtocol": "off", "useNumberNamespace": "off", "useSingleVarDeclarator": "off", "useTemplate": "off" } } }, "javascript": { "formatter": { "quoteStyle": "single", "arrowParentheses": "asNeeded" } } } ================================================ FILE: cache/file-names.properties ================================================ -1863637185=miscgraphics,4 -1863637184=miscgraphics,5 -1863637187=miscgraphics,2 -1863637186=miscgraphics,3 -1863637181=miscgraphics,8 -1863637180=miscgraphics,9 -440204630=alls fairy in love n war -1863637183=miscgraphics,6 -1253085654=scorpia_dances -1863637182=miscgraphics,7 1356196826=backvmid3 -751102526=high seas 1356196825=backvmid2 -1863637189=miscgraphics,0 549358875=camelot -1863637188=miscgraphics,1 -1619800378=emotes_locked,19 -1619800379=emotes_locked,18 792536868=understanding 1356196824=backvmid1 -1773559904=title_mute -342013218=p11_full 1523653533=pirates of peril 696768774=harmony -1701556831=magicoff2,27 281586976=orb_icon,3 286265996=ready for battle -1701556832=magicoff2,26 281586977=orb_icon,4 -645977478=scape scared -1701556833=magicoff2,25 281586978=orb_icon,5 -1701556834=magicoff2,24 -1701556830=magicoff2,28 -1052794696=dance of death -1619800383=emotes_locked,14 -1619800384=emotes_locked,13 -1619800385=emotes_locked,12 -1556842207=sl_flags -1619800386=emotes_locked,11 1614826739=mapletree -1619800387=emotes_locked,10 -1701556835=magicoff2,23 -1701556836=magicoff2,22 1363656441=serenade -1701556837=magicoff2,21 3089326=door -1701556838=magicoff2,20 -1619800380=emotes_locked,17 -1619800381=emotes_locked,16 -1619800382=emotes_locked,15 -1037172987=tomorrow 825974316=melodrama 281586973=orb_icon,0 281586974=orb_icon,1 281586975=orb_icon,2 -1701556864=magicoff2,15 -1701556865=magicoff2,14 -1701556866=magicoff2,13 1509400204=sarcophagus -1701556867=magicoff2,12 -1701556860=magicoff2,19 -1701556861=magicoff2,18 -1701556862=magicoff2,17 1868377358=lower_depths -1701556863=magicoff2,16 1086036315=reggae2 -1220755677=hermit -1544597765=sl_stars -1619800350=emotes_locked,26 -1619800351=emotes_locked,25 -1636062434=tex_brown -1619800352=emotes_locked,24 -1619800353=emotes_locked,23 -2122174648=back to life -1619800354=emotes_locked,22 -1701556868=magicoff2,11 -1619800355=emotes_locked,21 -1701556869=magicoff2,10 -1619800356=emotes_locked,20 1619539773=side_icons,7 1121239524=scape wild 368271413=diango's little helpers 1619539772=side_icons,6 1619539775=side_icons,9 1619539774=side_icons,8 -1938172321=miscgraphics2,0 1619539771=side_icons,5 1619539770=side_icons,4 -926977577=the enchanter 1202794514=doorways 1343649581=schools out -1097177625=q8_full 756012174=wornicons,4 756012173=wornicons,3 756012176=wornicons,6 756012175=wornicons,5 756012170=wornicons,0 827249681=ogre the top 756012172=wornicons,2 756012171=wornicons,1 -1741764817=poles apart 756012178=wornicons,8 -1938172320=miscgraphics2,1 756012177=wornicons,7 -43136286=the last shanty 756012179=wornicons,9 837131331=mapback -1938172313=miscgraphics2,8 907740319=the depths -1938172312=miscgraphics2,9 -2099722614=cave of beasts -1938172317=miscgraphics2,4 -1938172316=miscgraphics2,5 -1938172315=miscgraphics2,6 -1938172314=miscgraphics2,7 1619539769=side_icons,3 -1367483767=cavern -1938172319=miscgraphics2,2 50474489=treestump -1938172318=miscgraphics2,3 1619539766=side_icons,0 1619539768=side_icons,2 1619539767=side_icons,1 -607416954=prayeron,11 1837251043=melzars maze -607416953=prayeron,12 1318893900=have an ice day -607416952=prayeron,13 -1701556800=magicoff2,37 -607416951=prayeron,14 -1701556801=magicoff2,36 -56804840=woodland -607416950=prayeron,15 1120636327=scape cave 358884868=button_red -1701556806=magicoff2,31 -1701556807=magicoff2,30 -1701556802=magicoff2,35 -1701556803=magicoff2,34 -1701556804=magicoff2,33 -1858265682=monster melee -1701556805=magicoff2,32 -649484675=land of the dwarves -1789903512=golden touch 1328851780=close_buttons,6 1080306793=shayzien_march -1339126929=damage 4820960=monkey sadness -1951786153=bone dance 1328851781=close_buttons,7 939546513=forlorn_homestead -1019905269=shadowland -607416955=prayeron,10 -2048535896=pheasant peasant -1025233830=monkey madness 1121125956=scape soft -1562452687=etcetera 1969878996=emotes,47 1969878994=emotes,45 -1228279872=ge_icons,3 1969878995=emotes,46 -1228279871=ge_icons,4 1969878992=emotes,43 -1228279870=ge_icons,5 1969878993=emotes,44 1959803992=invback 399048409=mage arena 1969878990=emotes,41 -47057524=lasting 1969878991=emotes,42 -1228279875=ge_icons,0 -1228279874=ge_icons,1 -1228279873=ge_icons,2 -1860080918=inspiration 1997757502=redstone2 1997757503=redstone3 -1060046352=tribal2 1997757501=redstone1 -1701556829=magicoff2,29 1890607210=magicon2,37 1890607211=magicon2,38 1890607212=magicon2,39 -1408684838=ascent -848436598=fishing -119984250=combaticons2,17 -119984251=combaticons2,16 1769177816=jungle island -119984252=combaticons2,15 -119984253=combaticons2,14 -119984254=combaticons2,13 -119984255=combaticons2,12 -119984256=combaticons2,11 -119984257=combaticons2,10 1890607205=magicon2,32 1890607206=magicon2,33 1890607207=magicon2,34 1890607208=magicon2,35 135141185=zeah_combat 1890607209=magicon2,36 -180851958=norse code 112903447=water 922007495=talking forest -672706748=miracle dance -1110089645=lament -1237461365=grotto 1890607203=magicon2,30 1890607204=magicon2,31 -1073910849=mirror -988841056=still night -1857025509=sunburn -468596910=easter jig 796868952=major miner -1066798491=trawler 1640556978=wonderous -1624274920=emperor 740093634=find my way 1890607238=magicon2,44 1890607239=magicon2,45 3559837=tick -1839713245=sideicons 1029455878=hells bells 1890607234=magicon2,40 1890607235=magicon2,41 1890607236=magicon2,42 1890607237=magicon2,43 1503566841=forbidden -895763669=spooky -276138668=ham attack 500433071=combaticons,5 -1021014225=catch me if you can 500433070=combaticons,4 500433073=combaticons,7 500433072=combaticons,6 500433075=combaticons,9 1533565119=mind over matter 500433074=combaticons,8 2025958358=emotes_locked,4 500433066=combaticons,0 2025958357=emotes_locked,3 862821975=far away -1228392498=artistry 2025958356=emotes_locked,2 500433068=combaticons,2 2025958355=emotes_locked,1 500433067=combaticons,1 1085444827=refresh 500433069=combaticons,3 2025958359=emotes_locked,5 1427043851=on the up 2025958354=emotes_locked,0 1316697938=whistle 347955347=venture 1959211608=mapfunction,77 1959211609=mapfunction,78 881850881=the chosen 1959211601=mapfunction,70 1959211602=mapfunction,71 584643951=lost soul 582140282=rising damp 1959211603=mapfunction,72 1740872686=soulfall 1959211604=mapfunction,73 1959211605=mapfunction,74 -119984248=combaticons2,19 1959211606=mapfunction,75 -119984249=combaticons2,18 1609255038=slither and thither 82917947=sarim's vermin 1959211607=mapfunction,76 1728911401=natural -1189743137=duel arena 108698078=roof2 214634021=head to head 2025958361=emotes_locked,7 -448773288=isle of everywhere 2025958360=emotes_locked,6 -1869996941=titlebox -338347745=showdown -2075972251=long ago 2025958363=emotes_locked,9 2025958362=emotes_locked,8 -1487589606=7th realm -1253087691=garden -2133902017=zeah_farming -492926285=impetuous 3314014=lair 907815588=the desert -1960860275=barbarianism 1890607241=magicon2,47 1890607242=magicon2,48 1890607243=magicon2,49 -919642451=jungle bells 795515487=underground 561438836=fountain -1418827919=illusive -634763748=fruits de mer 1890607240=magicon2,46 1694458038=large_button 1393517697=bandit camp 1959211632=mapfunction,80 1884773718=magicoff2,2 1884773719=magicoff2,3 1884773716=magicoff2,0 2121900771=backtop1 1884773717=magicoff2,1 -103077377=gnomeball -1947119982=blistering barnacles 828650857=autumn voyage 92909147=alone 1691516951=undead dungeon 122265833=expecting -1320617626=dunjun 1959211633=mapfunction,81 1959211634=mapfunction,82 1959211635=mapfunction,83 1959211636=mapfunction,84 1959211637=mapfunction,85 777534707=army of darkness 1959211638=mapfunction,86 1959211639=mapfunction,87 1884773721=magicoff2,5 1884773722=magicoff2,6 1884773720=magicoff2,4 1959211640=mapfunction,88 1959211641=mapfunction,89 -327707013=anywhere 1884773725=magicoff2,9 1884773723=magicoff2,7 1884773724=magicoff2,8 1116844876=incantation -728886272=temple of light 685934899=in the clink -1237289460=grumpy 1945133711=inferno 466902883=strange place -418223472=phasmatys 817472004=zombiism 106578554=zeah_book,0 106578555=zeah_book,1 1509070203=eagle peak -485932799=expedition 1171923143=emotes,8 -675357975=attack1 1171923144=emotes,9 1959211610=mapfunction,79 -675357974=attack2 -675357973=attack3 -675357972=attack4 -675357971=attack5 40246002=masquerade -675357970=attack6 -734206983=arrival -1980407601=sea shanty xmas 284435223=pharoah's tomb -148552909=down below 1171923141=emotes,6 1171923142=emotes,7 1171923140=emotes,5 -1077789440=mellow -710537653=kingdom 1171923138=emotes,3 1171923139=emotes,4 1171923136=emotes,1 -2098286081=venture2 1171923137=emotes,2 1171923135=emotes,0 -1094248165=sigmunds showdown -271106892=rat a tat tat 3288564=keys -143163121=ham fisted -900633031=medieval 944208821=life's a beach\! -1228279453=riverside -1666444059=combaticons,10 825919125=options_icons,24 825919126=options_icons,25 1179379180=the trade parade -1666444057=combaticons,12 825919123=options_icons,22 -1666444058=combaticons,11 825919124=options_icons,23 825919121=options_icons,20 1884768169=magicoff,32 825919122=options_icons,21 1884768167=magicoff,30 1884768168=magicoff,31 1318818808=chainmail 582031337=intrepid 783525419=beetle juice 432605856=untouchable -969918857=neverland 79789174=narnode's theme -705938181=zealot 117588=web -1666444051=combaticons,18 -1666444052=combaticons,17 1687654733=troubled -1666444050=combaticons,19 -1666444055=combaticons,14 825919129=options_icons,28 -1666444056=combaticons,13 -1666444053=combaticons,16 825919127=options_icons,26 -1666444054=combaticons,15 825919128=options_icons,27 1320694328=magical journey 364185053=roll the bones -1254483584=jungly1 981183822=right on track -1254483583=jungly2 -1254483582=jungly3 3075958=dark -2038936746=deep down 1512143976=everlasting fire -1392319985=beyond 46273615=tale of keldagrim -651951461=goblin game 3522941=save 104084791=mossy 1250935993=the monsters below 794539501=garden of summer 1814277765=elven mist 2110556093=the golem -1475251658=where eagles lair 1884768143=magicoff,27 1884768144=magicoff,28 1884768141=magicoff,25 1529837717=bubble and squeak 1884768142=magicoff,26 -1679325940=technology 1884768145=magicoff,29 -826562194=troubled_waters 1884768140=magicoff,24 -1359348243=painting1 1267356434=the power of tears -1359348242=painting2 -860755690=jungle hunt 1884768138=magicoff,22 -1197347961=magic magic magic 1884768139=magicoff,23 1134405764=hypnotized 1959211539=mapfunction,50 1381363755=my arms journey -1644401602=complication 1959211540=mapfunction,51 1959211541=mapfunction,52 1884768136=magicoff,20 1959211542=mapfunction,53 2111304827=warning_icons,0 1884768137=magicoff,21 -440187560=zogre dance 1959211543=mapfunction,54 2111304828=warning_icons,1 -2002535437=corridors of power 825919130=options_icons,29 580384095=jungle troubles 1301622585=slice of station 1959211544=mapfunction,55 2111304829=warning_icons,2 -1294172031=escape -1309477156=expanse -1526067851=alternative root 2124773424=dynasty 1743765602=leftarrow -1482676188=romancing the crone -1891851953=island of the trolls 736457293=small_button_pressed -1106172890=letter 986170990=dreamstate 1959211545=mapfunction,56 1959211546=mapfunction,57 1765722413=spirits of elid 1959211547=mapfunction,58 1959211548=mapfunction,59 -2075333010=lonesome 3314400=lava 1355033875=worldmap_icon,1 1814357716=knightmare 1690742645=nox_irae 94935104=cross -1249495153=frogland -1642689926=athletes foot 107944162=quest 1355033874=worldmap_icon,0 -2130741313=joy of the hunt -28982081=labyrinth 250959119=marooned -1522984472=altar_ego 1326424637=the lost melody -1779111734=arabique -398925062=sea shanty2 1884768110=magicoff,15 1884768111=magicoff,16 1817249074=woe of the wyvern 1884768114=magicoff,19 1884768112=magicoff,17 -1624760229=emotion 1884768113=magicoff,18 -353951458=attention 279431252=garden of autumn 422652266=small_button 1884768107=magicoff,12 1884768108=magicoff,13 375695247=the far side 1884768105=magicoff,10 1884768106=magicoff,11 -528864109=crystal sword 1884768109=magicoff,14 1959211570=mapfunction,60 -158141423=prayeron,7 -158141424=prayeron,6 1959211571=mapfunction,61 -158141421=prayeron,9 1959211572=mapfunction,62 -158141422=prayeron,8 1959211573=mapfunction,63 1959211574=mapfunction,64 688840255=piscarilius_sigil 1959211575=mapfunction,65 1959211576=mapfunction,66 1170407052=headicons_prayer 1959211577=mapfunction,67 -324496873=soundscape -1418445703=tex_red 1961540869=wornicons,10 -1028580907=that_sullen_hall 397136995=elfpainting -2092714094=haunted mine 1959211578=mapfunction,68 1959211579=mapfunction,69 -158141429=prayeron,1 -158141427=prayeron,3 -140492390=bunny_sugar_rush -158141428=prayeron,2 -158141425=prayeron,5 1827366203=righteousness -649601274=darkness_in_the_depths -158141426=prayeron,4 1961540870=wornicons,11 -158141430=prayeron,0 910299584=principality -734028978=arrow,1 -734028979=arrow,0 790067275=garden of spring 35762567=workshop -1095396929=competition 96463963=egypt -1154441378=jollyr -1685231711=cave background -2078908549=time out -1172405897=wildwood -170561624=spookyjungle 2110231453=mod_icons 2061491048=shining -1063411723=tremble 94627585=chest -520702427=ice melody 1346720899=backleft1 -607416919=prayeron,25 -607416918=prayeron,26 900197712=staticons,6 -607416917=prayeron,27 900197713=staticons,7 -607416916=prayeron,28 900197710=staticons,4 -607416915=prayeron,29 900197711=staticons,5 957931606=courage 900197714=staticons,8 900197715=staticons,9 -720253066=the other side -1655721374=prayeroff,31 -607416921=prayeron,23 -1655721375=prayeroff,30 -607416920=prayeron,24 -710515142=the mad mole -1350228392=stratosphere -1666438445=combaticons2,3 -1666438446=combaticons2,2 -1666438443=combaticons2,5 -1666438444=combaticons2,4 -1106570438=legion -1666438441=combaticons2,7 -1666438442=combaticons2,6 1398587265=flute salad -1666438440=combaticons2,8 837223705=mapedge 900197709=staticons,3 -243680393=peng_emotes,7 900197707=staticons,1 378300078=everlasting 900197708=staticons,2 -944748869=witching -243680396=peng_emotes,4 -1335336992=logo_deadman_mode -1368714737=small_button_blue -795140435=wander -243680397=peng_emotes,3 -243680394=peng_emotes,6 -1666438447=combaticons2,1 -243680395=peng_emotes,5 -1666438448=combaticons2,0 -243680398=peng_emotes,2 -243680399=peng_emotes,1 -607416924=prayeron,20 900197706=staticons,0 -607416923=prayeron,21 -607416922=prayeron,22 3016376=bark -89244313=romper chomper 346288985=dorgeshun deep 1585002399=magicon,21 -1725263140=chef surprize 1585002398=magicon,20 -993528987=making waves -628963539=ham and seek -1666438439=combaticons2,9 -333224315=baroque -1236252722=prime time 280241284=waking dream -564582358=distant land 115411843=castlewars 1513246078=al kharid -1377700863=unknown land 1264132816=miles away 1185785872=barbassault_icons,3 1185785873=barbassault_icons,4 -1655721397=prayeroff,29 -1655721398=prayeroff,28 1185785874=barbassault_icons,5 -1655721399=prayeroff,27 1185785875=barbassault_icons,6 1711341885=fight or flight 1185785870=barbassault_icons,1 1185785871=barbassault_icons,2 3530505=sire -309570839=pick_and_shovel 647234089=distillery hilarity -127408236=gnome_village_party 1337378554=backbase2 109757537=stars 1337378553=backbase1 109757538=start 819884325=wilderness3 1328851775=close_buttons,1 658759958=side_background 1328851774=close_buttons,0 -782211141=wonder 1328851777=close_buttons,3 -1995718284=wall_white 819884324=wilderness2 1328851776=close_buttons,2 1328851779=close_buttons,5 -943885542=scape hunter 1328851778=close_buttons,4 1185785869=barbassault_icons,0 -607416949=prayeron,16 -607416948=prayeron,17 -1779684630=rune essence -607416947=prayeron,18 -607416946=prayeron,19 1119460311=bandos battalion -967559823=creature cruelty -1904094243=zeah_fishing -1913214770=wilderness 460367020=village 825919161=options_icons,39 1585002375=magicon,18 94839810=coins 1585002376=magicon,19 825919160=options_icons,38 -1282090556=faerie -521895311=the adventurer 788399136=tree spirits -1902858744=beneath_the_stronghold -356730043=pirates of penance 1185785876=barbassault_icons,7 825919158=options_icons,36 825919159=options_icons,37 825919156=options_icons,34 -1455241861=victory is mine 825919157=options_icons,35 -1333874720=side_icons,17 825919154=options_icons,32 825919155=options_icons,33 825919152=options_icons,30 825919153=options_icons,31 1437805631=chatback -1623296531=ground scape 685190118=in the brine 1366257555=nightfall 110327241=theme -1333874725=side_icons,12 -1333874726=side_icons,11 -1333874727=side_icons,10 -1333874721=side_icons,16 -1333874722=side_icons,15 -1124681475=darkly_altared -8976533=throne of the demon -1333874723=side_icons,14 -1333874724=side_icons,13 -1989106719=assault and battery 1958759012=greatness 1057075019=b12_full 1143353537=chain of command -51091830=desert voyage -1073927447=mirage -91048728=number_button 3165239=gaol -1380919269=breeze 445640248=rugged_terrain 106079=key -655784411=overlay_multiway -1025835715=backright2 -1025835716=backright1 1120933843=scape main 3225350=iban -956253112=title fight -123912401=la mort 1585002367=magicon,10 -2128736428=startgame 1585002368=magicon,11 -925031874=royale 1585002369=magicon,12 1585002370=magicon,13 1585002373=magicon,16 1585002374=magicon,17 -1307116191=superstition 1585002371=magicon,14 1585002372=magicon,15 -587569902=path of peril 3392903=null -1601127242=inadequacy 2136330800=staticons2,0 2136330801=staticons2,1 344336468=grip of the talon 2136330804=staticons2,4 2136330805=staticons2,5 2136330802=staticons2,2 2136330803=staticons2,3 1960215130=barking mad -43712789=scape original 621171714=cellar song 1585002461=magicon,41 1585002462=magicon,42 1585002460=magicon,40 111485446=upass 2136330808=staticons2,8 694847251=in the manor 2136330809=staticons2,9 2136330806=staticons2,6 2136330807=staticons2,7 -1385847955=rightarrow 1343200077=the slayer 1585002465=magicon,45 1585002466=magicon,46 1585002463=magicon,43 1585002464=magicon,44 1585002469=magicon,49 -95571520=volcanic vikings 1585002467=magicon,47 1585002468=magicon,48 -1032629963=shipwrecked 93330745=aztec -881372797=tabs,1 1377351472=oriental 121641580=headicons_hint -881372798=tabs,0 1585002438=magicon,39 -1661605940=elfwood -607416893=prayeron,30 -822106577=jungle island xmas -607416892=prayeron,31 332368736=mad eadgar 1585002432=magicon,33 -143368781=side_background_right 1585002433=magicon,34 -1081494434=malady 1585002430=magicon,31 1585002431=magicon,32 1585002436=magicon,37 1585002437=magicon,38 1585002434=magicon,35 1585002435=magicon,36 -1938171360=miscgraphics3,0 -1059680853=trinity 781557721=dies_irae -1938171359=miscgraphics3,1 497375231=stillness -1938171358=miscgraphics3,2 -1938171357=miscgraphics3,3 -960709976=dogs of war 2129339089=magicon,1 2129339088=magicon,0 755433248=headicons_pk 108392383=regal -213632750=waterfall -1367706280=canvas 73828649=settlement 848123561=into the abyss 478781900=last stand 1339486127=the shadow -1055503808=roc and roll 837204902=mapdots 950484242=compass -1082154559=fanfare 747848680=nether_realm 788224888=dead quiet 1532279978=monarch waltz -149029727=side_background_left1 812947089=fanfare2 -149029726=side_background_left2 1006643748=high spirits -2136059388=starlight 2122572442=the tower -1998869913=spooky2 1411067174=gnome village2 -2065077267=wild side 812947090=fanfare3 1585002429=magicon,30 1294629755=on the wing 2097127567=monkey badness -2032107216=sojourn 1020264019=pest control 3237038=info 1473393027=fe fi fo fum -1686202291=upper_depths 3540994=stop 1742080803=darkwood 740392969=little cave of horrors -158379532=prayerglow -691855347=in between -200702983=the noble rodent 1652745754=forgotten -1895307673=hitmark,3 -1895307674=hitmark,2 -1895307675=hitmark,1 -1895307676=hitmark,0 -1895307670=hitmark,6 -1895307671=hitmark,5 -1895307672=hitmark,4 1968917071=bone dry -850506182=trawler minor 197029040=mapscene -808772318=in the pits -1165315580=looking back -1763090403=scape_ape -1938177931=miscgraphics,11 -1938177932=miscgraphics,10 1936130561=thrall_of_the_serpent 1258863383=yesteryear 1994744000=slice of silent movie -1691854169=dead can dance 1585002407=magicon,29 1585002405=magicon,27 1585002406=magicon,28 271319484=frostbite -499867199=meridian 1585002400=magicon,22 -1938177930=miscgraphics,12 -84626226=mudskipper melody 1585002403=magicon,25 1585002404=magicon,26 3641802=wall 1585002401=magicon,23 1585002402=magicon,24 -606457701=wolf mountain 1276599785=button_brown 1969878899=emotes,13 1969878897=emotes,11 1969878898=emotes,12 1969878896=emotes,10 -1938177928=miscgraphics,14 -907669678=brew hoo hoo -1938177929=miscgraphics,13 72999866=subterranea 619237947=the galleon -1764950404=scape sad 295831445=heart and mind 908430134=dangerous road 738888631=tradebacking -174800339=verdana_11pt_regular 686705631=lightwalk -601591436=side_background_bottom -1479412376=the navigator -359173459=zamorak zoo 744536246=null and void -1701556798=magicoff2,39 -1701556799=magicoff2,38 -1396384012=bamboo -200388662=lighthouse 133626717=suspicious -810515425=voyage 3061973=crag 1802291895=big chords -1661619479=elfwall 113315621=wood2 1813041183=steelborder2,0 429244831=slug a bug ball 1813041184=steelborder2,1 -1658386264=shining_spirit 738909086=chamber 526264239=verdana_13pt_regular -877351859=temple 2142215577=the mollusc menace 1124498189=warpath -2136649922=no way out -339706871=grimly_fiendish 547534551=wrath and ruin 544229147=lore and order 3327206=load 1610073470=lovakengj_sigil -419218284=long way home -662489856=food for thought 1306461568=stagnant -1662171955=elfdoor -1043985601=meddling kids 947464074=titlebutton -1309055712=exposed -1487348923=ambient jungle -1829469821=lament of meiyerditch 233203434=leftarrow_small -1216167350=dangerous 114464611=railings -1106574323=legend -1701556767=magicoff2,49 -999707515=time to mine -1701556768=magicoff2,48 2129339097=magicon,9 2129339096=magicon,8 1959211510=mapfunction,42 2129339095=magicon,7 1959211511=mapfunction,43 2129339094=magicon,6 2129339093=magicon,5 2129339092=magicon,4 2129339091=magicon,3 2129339090=magicon,2 3522472=saga -1701556769=magicoff2,47 -544722449=rellekka 1033441676=tribal background 1915718129=the desolate isle 1890607150=magicon2,19 1787618597=stranded 1717999087=forgettable melody 1959211512=mapfunction,44 -243680400=peng_emotes,0 1959211513=mapfunction,45 1959211514=mapfunction,46 1959211515=mapfunction,47 1959211516=mapfunction,48 1959211517=mapfunction,49 1705947058=the cellar dwellers 1216634785=landlubber 1884768198=magicoff,40 -1588113323=the rogues den 1884768199=magicoff,41 -905842564=serene -607599698=prayeroff,2 -607599699=prayeroff,1 1890607142=magicon2,11 1389384362=monkey trouble 1890607143=magicon2,12 1890607144=magicon2,13 1890607145=magicon2,14 1890607146=magicon2,15 1890607147=magicon2,16 1890607148=magicon2,17 1966766798=mausoleum 1890607149=magicon2,18 1808345541=armadyl alliance 290391725=options_slider,7 1890607141=magicon2,10 290391722=options_slider,4 -607599696=prayeroff,4 290391721=options_slider,3 -607599697=prayeroff,3 290391724=options_slider,6 -607599694=prayeroff,6 290391723=options_slider,5 -607599695=prayeroff,5 -607599692=prayeroff,8 -607599693=prayeroff,7 290391720=options_slider,2 -607599691=prayeroff,9 1334775925=chat_background -1779127378=arabian2 -1779127377=arabian3 528722471=island life 1890607175=magicon2,23 1890607176=magicon2,24 949634504=mouse trap 1890607177=magicon2,25 1890607178=magicon2,26 1890607179=magicon2,27 3327403=logo 290391719=options_slider,1 1092249049=storm brew 290391718=options_slider,0 404357804=everywhere 1890607172=magicon2,20 1890607173=magicon2,21 951530772=contest 1890607174=magicon2,22 -1701556776=magicoff2,40 1884768176=magicoff,39 -395250469=corporal punishment 1749113330=newbie melody 1884768174=magicoff,37 1884768175=magicoff,38 -1701556772=magicoff2,44 -1701556773=magicoff2,43 -1701556774=magicoff2,42 -1701556775=magicoff2,41 -858121616=tzhaar 1884768172=magicoff,35 1884768173=magicoff,36 666772244=combat_shield 1884768170=magicoff,33 1884768171=magicoff,34 1959211509=mapfunction,41 1639695510=mapmarker -1661748240=friends_icons -552301350=knightly -1918044851=mastermindless -1701556770=magicoff2,46 -1701556771=magicoff2,45 201526300=corporealbeast 1959211508=mapfunction,40 105001967=nomad -70910145=clickcross,3 -1737914947=mapfunction,5 1801745440=staticons2,11 -70910146=clickcross,2 -865479038=tribal -1737914946=mapfunction,6 1801745441=staticons2,12 -70910147=clickcross,1 -1737914945=mapfunction,7 1801745442=staticons2,13 -70910148=clickcross,0 -1737914944=mapfunction,8 1801745443=staticons2,14 -1737914943=mapfunction,9 1801745444=staticons2,15 1801745445=staticons2,16 1801745446=staticons2,17 -1877545169=land down under 93921962=books -1737914949=mapfunction,3 -1737914948=mapfunction,4 -1655721428=prayeroff,19 -2136884405=title.jpg -1655721429=prayeroff,18 1584819628=magicoff,6 1584819629=magicoff,7 437480876=voodoo cult 1584819624=magicoff,2 1584819625=magicoff,3 1584819626=magicoff,4 -1737914952=mapfunction,0 1584819627=magicoff,5 -1737914951=mapfunction,1 124995564=harmony2 1584819622=magicoff,0 1801745439=staticons2,10 1584819623=magicoff,1 346263512=dorgeshun city -1665011705=down and out 1890607180=magicon2,28 1890607181=magicon2,29 1417471781=titlescroll 1959211446=mapfunction,20 1956141536=options_radio_buttons,0 736568812=ballad of enchantment -1737914950=mapfunction,2 1959211447=mapfunction,21 1959211448=mapfunction,22 1959211449=mapfunction,23 -1890130256=morytania -70910141=clickcross,7 -70910142=clickcross,6 1956141539=options_radio_buttons,3 -70910143=clickcross,5 1956141538=options_radio_buttons,2 284766976=splendour -70910144=clickcross,4 1956141537=options_radio_buttons,1 196677638=the quizmaster 530068296=overture -1123094568=sl_button -700552779=hosidius_sigil -614076819=sad meadow 1956141543=options_radio_buttons,7 1956141542=options_radio_buttons,6 1956141541=options_radio_buttons,5 1956141540=options_radio_buttons,4 1584819631=magicoff,9 1846633612=gnome village -2128560371=sl_back 1969878905=emotes,19 306819362=crystal castle 1584819630=magicoff,8 1969878903=emotes,17 303737220=options_icons,7 1969878904=emotes,18 1969878901=emotes,15 -78220817=devils_may_care 1969878902=emotes,16 -40521666=dimension x 1969878900=emotes,14 673424924=the lunar isle 789609582=brimstail's scales 303737222=options_icons,9 303737221=options_icons,8 1959211415=mapfunction,10 3059343=coil 1959211416=mapfunction,11 1959211417=mapfunction,12 -1256560486=last_man_standing 1959211418=mapfunction,13 336238005=rightarrow_small 1959211419=mapfunction,14 -677662361=forever -1655721430=prayeroff,17 -1655721431=prayeroff,16 -1655721432=prayeroff,15 1959211420=mapfunction,15 1959211663=mapfunction,90 1959211421=mapfunction,16 1959211422=mapfunction,17 1959211423=mapfunction,18 303737217=options_icons,4 303737216=options_icons,3 303737215=options_icons,2 -1665005042=funny bunnies 303737214=options_icons,1 303737219=options_icons,6 303737218=options_icons,5 303737213=options_icons,0 95997798=we are the fairies 2001751835=desert heat 1959211424=mapfunction,19 -1655721437=prayeroff,10 687938017=clanwars -1776024210=desolate_mage -650944128=strength of saradomin -1655721433=prayeroff,14 1160873524=aye car rum ba -1655721434=prayeroff,13 -1655721435=prayeroff,12 -1081314499=marble -1655721436=prayeroff,11 1097075475=reset,0 1959211477=mapfunction,30 -693313916=warriors guild 3506388=roof 1959211478=mapfunction,31 1097075476=reset,1 -2134967800=dagannoth dawn -985763247=planks 1959211479=mapfunction,32 1999746381=fenkenstrain's refrain 898010371=garden of winter 359174830=rat hunt 1959211482=mapfunction,35 686441581=lightness 1959211483=mapfunction,36 1959211484=mapfunction,37 1959211485=mapfunction,38 1959211486=mapfunction,39 2023201035=dwarf theme 1959211480=mapfunction,33 1959211481=mapfunction,34 -1065532022=combatboxes,1 -1065532021=combatboxes,2 -1065532020=combatboxes,3 1867160429=old_tiles 394756979=scape santa 25205919=elfroof2 -663428071=dangerous way -1065532023=combatboxes,0 1959211450=mapfunction,24 -895939599=spirit 1959211451=mapfunction,25 1959211452=mapfunction,26 1959211453=mapfunction,27 1959211454=mapfunction,28 1959211455=mapfunction,29 -275310687=undercurrent 212205923=goblin village -303898981=faithless -1381531001=tomb raider 260940912=marzipan 1343267530=backhmid1 1343267531=backhmid2 1097468315=horizon -1655721404=prayeroff,22 623451622=kourend_the_magnificent -1655721405=prayeroff,21 -1655721406=prayeroff,20 -1655721400=prayeroff,26 -313384067=p12_full -1655721401=prayeroff,25 -1655721402=prayeroff,24 -1655721403=prayeroff,23 95848451=dream 1966781751=maws_jaws_claws -995428255=parade 95734525=method of madness -1308064877=hitmarks 1030045177=mutant medley 1333034828=blackmark 851641665=davy jones locker 417793574=scrollbar 1346720900=backleft2 1884768206=magicoff,48 1884768207=magicoff,49 1801140808=fangs for the memory 1884768204=magicoff,46 1345432055=pinball wizard 1884768205=magicoff,47 -783693496=dance of the undead 1274780903=chompy hunt 465278529=the lost tribe -1666437481=combaticons3,6 2032696205=cabin fever -1666437480=combaticons3,7 1825640471=borderland 415928477=zeah_mining 1884768202=magicoff,44 -607599700=prayeroff,0 1884768203=magicoff,45 1884768200=magicoff,42 1884768201=magicoff,43 -1666437487=combaticons3,0 -1666437486=combaticons3,1 813726263=crystal cave 1235442953=pathways 518814479=lullaby -1666437483=combaticons3,4 1585007985=magicon2,7 -1666437482=combaticons3,5 1585007986=magicon2,8 -1666437485=combaticons3,2 1585007987=magicon2,9 -1666437484=combaticons3,3 104080482=moody 1969878967=emotes,39 -665666447=work work work 1364992651=evil bobs island 1969878965=emotes,37 1227328817=verdana_15pt_regular 1969878966=emotes,38 1969878963=emotes,35 1581724013=monkey business 1969878964=emotes,36 -74307138=miscellania 1969878961=emotes,33 1969878962=emotes,34 1969878960=emotes,32 1131171307=wayward -1154558416=sl_arrows 529929957=overpass 1258058669=huffman -694094064=adventure 1171698653=orb_xp,1 1171698652=orb_xp,0 1171698655=orb_xp,3 1171698654=orb_xp,2 1802171733=arceuus_sigil 221109227=tears of guthix -1661754893=elfroof -1666437478=combaticons3,9 1124565314=warrior 1969878958=emotes,30 1969878959=emotes,31 -934797897=reggae 2110260221=the genie -1666437479=combaticons3,8 104257585=mummy -1923924724=sworddecor,0 2136325196=staticons,17 -1332194002=background 2136164423=homescape -1923924721=sworddecor,3 2136325194=staticons,15 -1923924722=sworddecor,2 -873564465=tiptoe 2136325195=staticons,16 -1923924723=sworddecor,1 123560953=espionage 2136325192=staticons,13 2136325193=staticons,14 1038911415=gnome king 2136325190=staticons,11 2136325191=staticons,12 1854274741=karamja jam 1969878989=emotes,40 -1242708793=glyphs 563269755=the terrible tower 1650323088=twilight -12868552=sea shanty 289742397=book of spells 1880989696=dragontooth island 2136325189=staticons,10 110873=pen -454421102=out of the deep 825919099=options_icons,19 825919097=options_icons,17 825919098=options_icons,18 -1349119470=cursed 1585007978=magicon2,0 825919095=options_icons,15 1585007979=magicon2,1 825919096=options_icons,16 825919093=options_icons,13 825919094=options_icons,14 825919091=options_icons,11 825919092=options_icons,12 -895977880=sphinx -874529881=city of the dead 825919090=options_icons,10 1585007981=magicon2,3 1585007982=magicon2,4 1585007983=magicon2,5 -1618729246=body parts 1585007984=magicon2,6 1585007980=magicon2,2 1086075866=shayzien_sigil 2103661451=jester minute -911346307=steelborder,2 -1809781334=button_brown_big -911346306=steelborder,3 -816227352=vision -911346309=steelborder,0 109407595=shine -911346308=steelborder,1 -119954464=combaticons3,12 -119954465=combaticons3,11 -1846853118=armageddon -119954462=combaticons3,14 -119954463=combaticons3,13 -119954460=combaticons3,16 -908183966=scarab -119954461=combaticons3,15 -282886672=home sweet home 103666243=march 1969878929=emotes,22 1643875326=fire and brimstone 1969878927=emotes,20 -119954466=combaticons3,10 1969878928=emotes,21 -839455633=close quarters 941457503=way of the enchanter -1453405761=mor-ul-rek -415134015=have a blast -1658514874=floating free 1213477442=chickened out -1619800349=emotes_locked,27 -599680631=fear and loathing -1081041422=insect queen -1268786147=forest -119954459=combaticons3,17 -119954457=combaticons3,19 -119954458=combaticons3,18 -850395529=trouble brewing -1773920521=cave of the goblins -771284962=claustrophobia 1310729739=bankbuttons,2 1306691868=upcoming 1465443077=over to nardah 1310729744=bankbuttons,7 1310729743=bankbuttons,6 1310729742=bankbuttons,5 1234827707=deep wildy -90350772=xenophobe -750127868=arabian 1310729741=bankbuttons,4 1310729740=bankbuttons,3 1041911129=waterlogged 108875897=runes 1447063382=barb wire -378865792=magic dance 285466503=overlay_duel 1814287296=zeah_magic 1976894499=down to earth 1969878936=emotes,29 1969878934=emotes,27 -1567437308=deadlands 1969878935=emotes,28 1310729738=bankbuttons,1 1969878932=emotes,25 -957019274=too many cooks 1310729737=bankbuttons,0 2092627105=silence 1969878933=emotes,26 1969878930=emotes,23 1969878931=emotes,24 -901674570=well of voyage 1787935731=quill_oblique_large -365283881=quill_caps_large 1157777820=lunar_alphabet 24702590=lunar_alphabet_lrg ================================================ FILE: cache/main_file_cache.dat2 ================================================ [File too large to display: 28.8 MB] ================================================ FILE: config/file-names.properties ================================================ -1863637185=miscgraphics,4 -1863637184=miscgraphics,5 -1863637187=miscgraphics,2 -1863637186=miscgraphics,3 -1863637181=miscgraphics,8 -1863637180=miscgraphics,9 -440204630=alls fairy in love n war -1863637183=miscgraphics,6 -1253085654=scorpia_dances -1863637182=miscgraphics,7 1356196826=backvmid3 -751102526=high seas 1356196825=backvmid2 -1863637189=miscgraphics,0 549358875=camelot -1863637188=miscgraphics,1 -1619800378=emotes_locked,19 -1619800379=emotes_locked,18 792536868=understanding 1356196824=backvmid1 -1773559904=title_mute -342013218=p11_full 1523653533=pirates of peril 696768774=harmony -1701556831=magicoff2,27 281586976=orb_icon,3 286265996=ready for battle -1701556832=magicoff2,26 281586977=orb_icon,4 -645977478=scape scared -1701556833=magicoff2,25 281586978=orb_icon,5 -1701556834=magicoff2,24 -1701556830=magicoff2,28 -1052794696=dance of death -1619800383=emotes_locked,14 -1619800384=emotes_locked,13 -1619800385=emotes_locked,12 -1556842207=sl_flags -1619800386=emotes_locked,11 1614826739=mapletree -1619800387=emotes_locked,10 -1701556835=magicoff2,23 -1701556836=magicoff2,22 1363656441=serenade -1701556837=magicoff2,21 3089326=door -1701556838=magicoff2,20 -1619800380=emotes_locked,17 -1619800381=emotes_locked,16 -1619800382=emotes_locked,15 -1037172987=tomorrow 825974316=melodrama 281586973=orb_icon,0 281586974=orb_icon,1 281586975=orb_icon,2 -1701556864=magicoff2,15 -1701556865=magicoff2,14 -1701556866=magicoff2,13 1509400204=sarcophagus -1701556867=magicoff2,12 -1701556860=magicoff2,19 -1701556861=magicoff2,18 -1701556862=magicoff2,17 1868377358=lower_depths -1701556863=magicoff2,16 1086036315=reggae2 -1220755677=hermit -1544597765=sl_stars -1619800350=emotes_locked,26 -1619800351=emotes_locked,25 -1636062434=tex_brown -1619800352=emotes_locked,24 -1619800353=emotes_locked,23 -2122174648=back to life -1619800354=emotes_locked,22 -1701556868=magicoff2,11 -1619800355=emotes_locked,21 -1701556869=magicoff2,10 -1619800356=emotes_locked,20 1619539773=side_icons,7 1121239524=scape wild 368271413=diango's little helpers 1619539772=side_icons,6 1619539775=side_icons,9 1619539774=side_icons,8 -1938172321=miscgraphics2,0 1619539771=side_icons,5 1619539770=side_icons,4 -926977577=the enchanter 1202794514=doorways 1343649581=schools out -1097177625=q8_full 756012174=wornicons,4 756012173=wornicons,3 756012176=wornicons,6 756012175=wornicons,5 756012170=wornicons,0 827249681=ogre the top 756012172=wornicons,2 756012171=wornicons,1 -1741764817=poles apart 756012178=wornicons,8 -1938172320=miscgraphics2,1 756012177=wornicons,7 -43136286=the last shanty 756012179=wornicons,9 837131331=mapback -1938172313=miscgraphics2,8 907740319=the depths -1938172312=miscgraphics2,9 -2099722614=cave of beasts -1938172317=miscgraphics2,4 -1938172316=miscgraphics2,5 -1938172315=miscgraphics2,6 -1938172314=miscgraphics2,7 1619539769=side_icons,3 -1367483767=cavern -1938172319=miscgraphics2,2 50474489=treestump -1938172318=miscgraphics2,3 1619539766=side_icons,0 1619539768=side_icons,2 1619539767=side_icons,1 -607416954=prayeron,11 1837251043=melzars maze -607416953=prayeron,12 1318893900=have an ice day -607416952=prayeron,13 -1701556800=magicoff2,37 -607416951=prayeron,14 -1701556801=magicoff2,36 -56804840=woodland -607416950=prayeron,15 1120636327=scape cave 358884868=button_red -1701556806=magicoff2,31 -1701556807=magicoff2,30 -1701556802=magicoff2,35 -1701556803=magicoff2,34 -1701556804=magicoff2,33 -1858265682=monster melee -1701556805=magicoff2,32 -649484675=land of the dwarves -1789903512=golden touch 1328851780=close_buttons,6 1080306793=shayzien_march -1339126929=damage 4820960=monkey sadness -1951786153=bone dance 1328851781=close_buttons,7 939546513=forlorn_homestead -1019905269=shadowland -607416955=prayeron,10 -2048535896=pheasant peasant -1025233830=monkey madness 1121125956=scape soft -1562452687=etcetera 1969878996=emotes,47 1969878994=emotes,45 -1228279872=ge_icons,3 1969878995=emotes,46 -1228279871=ge_icons,4 1969878992=emotes,43 -1228279870=ge_icons,5 1969878993=emotes,44 1959803992=invback 399048409=mage arena 1969878990=emotes,41 -47057524=lasting 1969878991=emotes,42 -1228279875=ge_icons,0 -1228279874=ge_icons,1 -1228279873=ge_icons,2 -1860080918=inspiration 1997757502=redstone2 1997757503=redstone3 -1060046352=tribal2 1997757501=redstone1 -1701556829=magicoff2,29 1890607210=magicon2,37 1890607211=magicon2,38 1890607212=magicon2,39 -1408684838=ascent -848436598=fishing -119984250=combaticons2,17 -119984251=combaticons2,16 1769177816=jungle island -119984252=combaticons2,15 -119984253=combaticons2,14 -119984254=combaticons2,13 -119984255=combaticons2,12 -119984256=combaticons2,11 -119984257=combaticons2,10 1890607205=magicon2,32 1890607206=magicon2,33 1890607207=magicon2,34 1890607208=magicon2,35 135141185=zeah_combat 1890607209=magicon2,36 -180851958=norse code 112903447=water 922007495=talking forest -672706748=miracle dance -1110089645=lament -1237461365=grotto 1890607203=magicon2,30 1890607204=magicon2,31 -1073910849=mirror -988841056=still night -1857025509=sunburn -468596910=easter jig 796868952=major miner -1066798491=trawler 1640556978=wonderous -1624274920=emperor 740093634=find my way 1890607238=magicon2,44 1890607239=magicon2,45 3559837=tick -1839713245=sideicons 1029455878=hells bells 1890607234=magicon2,40 1890607235=magicon2,41 1890607236=magicon2,42 1890607237=magicon2,43 1503566841=forbidden -895763669=spooky -276138668=ham attack 500433071=combaticons,5 -1021014225=catch me if you can 500433070=combaticons,4 500433073=combaticons,7 500433072=combaticons,6 500433075=combaticons,9 1533565119=mind over matter 500433074=combaticons,8 2025958358=emotes_locked,4 500433066=combaticons,0 2025958357=emotes_locked,3 862821975=far away -1228392498=artistry 2025958356=emotes_locked,2 500433068=combaticons,2 2025958355=emotes_locked,1 500433067=combaticons,1 1085444827=refresh 500433069=combaticons,3 2025958359=emotes_locked,5 1427043851=on the up 2025958354=emotes_locked,0 1316697938=whistle 347955347=venture 1959211608=mapfunction,77 1959211609=mapfunction,78 881850881=the chosen 1959211601=mapfunction,70 1959211602=mapfunction,71 584643951=lost soul 582140282=rising damp 1959211603=mapfunction,72 1740872686=soulfall 1959211604=mapfunction,73 1959211605=mapfunction,74 -119984248=combaticons2,19 1959211606=mapfunction,75 -119984249=combaticons2,18 1609255038=slither and thither 82917947=sarim's vermin 1959211607=mapfunction,76 1728911401=natural -1189743137=duel arena 108698078=roof2 214634021=head to head 2025958361=emotes_locked,7 -448773288=isle of everywhere 2025958360=emotes_locked,6 -1869996941=titlebox -338347745=showdown -2075972251=long ago 2025958363=emotes_locked,9 2025958362=emotes_locked,8 -1487589606=7th realm -1253087691=garden -2133902017=zeah_farming -492926285=impetuous 3314014=lair 907815588=the desert -1960860275=barbarianism 1890607241=magicon2,47 1890607242=magicon2,48 1890607243=magicon2,49 -919642451=jungle bells 795515487=underground 561438836=fountain -1418827919=illusive -634763748=fruits de mer 1890607240=magicon2,46 1694458038=large_button 1393517697=bandit camp 1959211632=mapfunction,80 1884773718=magicoff2,2 1884773719=magicoff2,3 1884773716=magicoff2,0 2121900771=backtop1 1884773717=magicoff2,1 -103077377=gnomeball -1947119982=blistering barnacles 828650857=autumn voyage 92909147=alone 1691516951=undead dungeon 122265833=expecting -1320617626=dunjun 1959211633=mapfunction,81 1959211634=mapfunction,82 1959211635=mapfunction,83 1959211636=mapfunction,84 1959211637=mapfunction,85 777534707=army of darkness 1959211638=mapfunction,86 1959211639=mapfunction,87 1884773721=magicoff2,5 1884773722=magicoff2,6 1884773720=magicoff2,4 1959211640=mapfunction,88 1959211641=mapfunction,89 -327707013=anywhere 1884773725=magicoff2,9 1884773723=magicoff2,7 1884773724=magicoff2,8 1116844876=incantation -728886272=temple of light 685934899=in the clink -1237289460=grumpy 1945133711=inferno 466902883=strange place -418223472=phasmatys 817472004=zombiism 106578554=zeah_book,0 106578555=zeah_book,1 1509070203=eagle peak -485932799=expedition 1171923143=emotes,8 -675357975=attack1 1171923144=emotes,9 1959211610=mapfunction,79 -675357974=attack2 -675357973=attack3 -675357972=attack4 -675357971=attack5 40246002=masquerade -675357970=attack6 -734206983=arrival -1980407601=sea shanty xmas 284435223=pharoah's tomb -148552909=down below 1171923141=emotes,6 1171923142=emotes,7 1171923140=emotes,5 -1077789440=mellow -710537653=kingdom 1171923138=emotes,3 1171923139=emotes,4 1171923136=emotes,1 -2098286081=venture2 1171923137=emotes,2 1171923135=emotes,0 -1094248165=sigmunds showdown -271106892=rat a tat tat 3288564=keys -143163121=ham fisted -900633031=medieval 944208821=life's a beach\! -1228279453=riverside -1666444059=combaticons,10 825919125=options_icons,24 825919126=options_icons,25 1179379180=the trade parade -1666444057=combaticons,12 825919123=options_icons,22 -1666444058=combaticons,11 825919124=options_icons,23 825919121=options_icons,20 1884768169=magicoff,32 825919122=options_icons,21 1884768167=magicoff,30 1884768168=magicoff,31 1318818808=chainmail 582031337=intrepid 783525419=beetle juice 432605856=untouchable -969918857=neverland 79789174=narnode's theme -705938181=zealot 117588=web -1666444051=combaticons,18 -1666444052=combaticons,17 1687654733=troubled -1666444050=combaticons,19 -1666444055=combaticons,14 825919129=options_icons,28 -1666444056=combaticons,13 -1666444053=combaticons,16 825919127=options_icons,26 -1666444054=combaticons,15 825919128=options_icons,27 1320694328=magical journey 364185053=roll the bones -1254483584=jungly1 981183822=right on track -1254483583=jungly2 -1254483582=jungly3 3075958=dark -2038936746=deep down 1512143976=everlasting fire -1392319985=beyond 46273615=tale of keldagrim -651951461=goblin game 3522941=save 104084791=mossy 1250935993=the monsters below 794539501=garden of summer 1814277765=elven mist 2110556093=the golem -1475251658=where eagles lair 1884768143=magicoff,27 1884768144=magicoff,28 1884768141=magicoff,25 1529837717=bubble and squeak 1884768142=magicoff,26 -1679325940=technology 1884768145=magicoff,29 -826562194=troubled_waters 1884768140=magicoff,24 -1359348243=painting1 1267356434=the power of tears -1359348242=painting2 -860755690=jungle hunt 1884768138=magicoff,22 -1197347961=magic magic magic 1884768139=magicoff,23 1134405764=hypnotized 1959211539=mapfunction,50 1381363755=my arms journey -1644401602=complication 1959211540=mapfunction,51 1959211541=mapfunction,52 1884768136=magicoff,20 1959211542=mapfunction,53 2111304827=warning_icons,0 1884768137=magicoff,21 -440187560=zogre dance 1959211543=mapfunction,54 2111304828=warning_icons,1 -2002535437=corridors of power 825919130=options_icons,29 580384095=jungle troubles 1301622585=slice of station 1959211544=mapfunction,55 2111304829=warning_icons,2 -1294172031=escape -1309477156=expanse -1526067851=alternative root 2124773424=dynasty 1743765602=leftarrow -1482676188=romancing the crone -1891851953=island of the trolls 736457293=small_button_pressed -1106172890=letter 986170990=dreamstate 1959211545=mapfunction,56 1959211546=mapfunction,57 1765722413=spirits of elid 1959211547=mapfunction,58 1959211548=mapfunction,59 -2075333010=lonesome 3314400=lava 1355033875=worldmap_icon,1 1814357716=knightmare 1690742645=nox_irae 94935104=cross -1249495153=frogland -1642689926=athletes foot 107944162=quest 1355033874=worldmap_icon,0 -2130741313=joy of the hunt -28982081=labyrinth 250959119=marooned -1522984472=altar_ego 1326424637=the lost melody -1779111734=arabique -398925062=sea shanty2 1884768110=magicoff,15 1884768111=magicoff,16 1817249074=woe of the wyvern 1884768114=magicoff,19 1884768112=magicoff,17 -1624760229=emotion 1884768113=magicoff,18 -353951458=attention 279431252=garden of autumn 422652266=small_button 1884768107=magicoff,12 1884768108=magicoff,13 375695247=the far side 1884768105=magicoff,10 1884768106=magicoff,11 -528864109=crystal sword 1884768109=magicoff,14 1959211570=mapfunction,60 -158141423=prayeron,7 -158141424=prayeron,6 1959211571=mapfunction,61 -158141421=prayeron,9 1959211572=mapfunction,62 -158141422=prayeron,8 1959211573=mapfunction,63 1959211574=mapfunction,64 688840255=piscarilius_sigil 1959211575=mapfunction,65 1959211576=mapfunction,66 1170407052=headicons_prayer 1959211577=mapfunction,67 -324496873=soundscape -1418445703=tex_red 1961540869=wornicons,10 -1028580907=that_sullen_hall 397136995=elfpainting -2092714094=haunted mine 1959211578=mapfunction,68 1959211579=mapfunction,69 -158141429=prayeron,1 -158141427=prayeron,3 -140492390=bunny_sugar_rush -158141428=prayeron,2 -158141425=prayeron,5 1827366203=righteousness -649601274=darkness_in_the_depths -158141426=prayeron,4 1961540870=wornicons,11 -158141430=prayeron,0 910299584=principality -734028978=arrow,1 -734028979=arrow,0 790067275=garden of spring 35762567=workshop -1095396929=competition 96463963=egypt -1154441378=jollyr -1685231711=cave background -2078908549=time out -1172405897=wildwood -170561624=spookyjungle 2110231453=mod_icons 2061491048=shining -1063411723=tremble 94627585=chest -520702427=ice melody 1346720899=backleft1 -607416919=prayeron,25 -607416918=prayeron,26 900197712=staticons,6 -607416917=prayeron,27 900197713=staticons,7 -607416916=prayeron,28 900197710=staticons,4 -607416915=prayeron,29 900197711=staticons,5 957931606=courage 900197714=staticons,8 900197715=staticons,9 -720253066=the other side -1655721374=prayeroff,31 -607416921=prayeron,23 -1655721375=prayeroff,30 -607416920=prayeron,24 -710515142=the mad mole -1350228392=stratosphere -1666438445=combaticons2,3 -1666438446=combaticons2,2 -1666438443=combaticons2,5 -1666438444=combaticons2,4 -1106570438=legion -1666438441=combaticons2,7 -1666438442=combaticons2,6 1398587265=flute salad -1666438440=combaticons2,8 837223705=mapedge 900197709=staticons,3 -243680393=peng_emotes,7 900197707=staticons,1 378300078=everlasting 900197708=staticons,2 -944748869=witching -243680396=peng_emotes,4 -1335336992=logo_deadman_mode -1368714737=small_button_blue -795140435=wander -243680397=peng_emotes,3 -243680394=peng_emotes,6 -1666438447=combaticons2,1 -243680395=peng_emotes,5 -1666438448=combaticons2,0 -243680398=peng_emotes,2 -243680399=peng_emotes,1 -607416924=prayeron,20 900197706=staticons,0 -607416923=prayeron,21 -607416922=prayeron,22 3016376=bark -89244313=romper chomper 346288985=dorgeshun deep 1585002399=magicon,21 -1725263140=chef surprize 1585002398=magicon,20 -993528987=making waves -628963539=ham and seek -1666438439=combaticons2,9 -333224315=baroque -1236252722=prime time 280241284=waking dream -564582358=distant land 115411843=castlewars 1513246078=al kharid -1377700863=unknown land 1264132816=miles away 1185785872=barbassault_icons,3 1185785873=barbassault_icons,4 -1655721397=prayeroff,29 -1655721398=prayeroff,28 1185785874=barbassault_icons,5 -1655721399=prayeroff,27 1185785875=barbassault_icons,6 1711341885=fight or flight 1185785870=barbassault_icons,1 1185785871=barbassault_icons,2 3530505=sire -309570839=pick_and_shovel 647234089=distillery hilarity -127408236=gnome_village_party 1337378554=backbase2 109757537=stars 1337378553=backbase1 109757538=start 819884325=wilderness3 1328851775=close_buttons,1 658759958=side_background 1328851774=close_buttons,0 -782211141=wonder 1328851777=close_buttons,3 -1995718284=wall_white 819884324=wilderness2 1328851776=close_buttons,2 1328851779=close_buttons,5 -943885542=scape hunter 1328851778=close_buttons,4 1185785869=barbassault_icons,0 -607416949=prayeron,16 -607416948=prayeron,17 -1779684630=rune essence -607416947=prayeron,18 -607416946=prayeron,19 1119460311=bandos battalion -967559823=creature cruelty -1904094243=zeah_fishing -1913214770=wilderness 460367020=village 825919161=options_icons,39 1585002375=magicon,18 94839810=coins 1585002376=magicon,19 825919160=options_icons,38 -1282090556=faerie -521895311=the adventurer 788399136=tree spirits -1902858744=beneath_the_stronghold -356730043=pirates of penance 1185785876=barbassault_icons,7 825919158=options_icons,36 825919159=options_icons,37 825919156=options_icons,34 -1455241861=victory is mine 825919157=options_icons,35 -1333874720=side_icons,17 825919154=options_icons,32 825919155=options_icons,33 825919152=options_icons,30 825919153=options_icons,31 1437805631=chatback -1623296531=ground scape 685190118=in the brine 1366257555=nightfall 110327241=theme -1333874725=side_icons,12 -1333874726=side_icons,11 -1333874727=side_icons,10 -1333874721=side_icons,16 -1333874722=side_icons,15 -1124681475=darkly_altared -8976533=throne of the demon -1333874723=side_icons,14 -1333874724=side_icons,13 -1989106719=assault and battery 1958759012=greatness 1057075019=b12_full 1143353537=chain of command -51091830=desert voyage -1073927447=mirage -91048728=number_button 3165239=gaol -1380919269=breeze 445640248=rugged_terrain 106079=key -655784411=overlay_multiway -1025835715=backright2 -1025835716=backright1 1120933843=scape main 3225350=iban -956253112=title fight -123912401=la mort 1585002367=magicon,10 -2128736428=startgame 1585002368=magicon,11 -925031874=royale 1585002369=magicon,12 1585002370=magicon,13 1585002373=magicon,16 1585002374=magicon,17 -1307116191=superstition 1585002371=magicon,14 1585002372=magicon,15 -587569902=path of peril 3392903=null -1601127242=inadequacy 2136330800=staticons2,0 2136330801=staticons2,1 344336468=grip of the talon 2136330804=staticons2,4 2136330805=staticons2,5 2136330802=staticons2,2 2136330803=staticons2,3 1960215130=barking mad -43712789=scape original 621171714=cellar song 1585002461=magicon,41 1585002462=magicon,42 1585002460=magicon,40 111485446=upass 2136330808=staticons2,8 694847251=in the manor 2136330809=staticons2,9 2136330806=staticons2,6 2136330807=staticons2,7 -1385847955=rightarrow 1343200077=the slayer 1585002465=magicon,45 1585002466=magicon,46 1585002463=magicon,43 1585002464=magicon,44 1585002469=magicon,49 -95571520=volcanic vikings 1585002467=magicon,47 1585002468=magicon,48 -1032629963=shipwrecked 93330745=aztec -881372797=tabs,1 1377351472=oriental 121641580=headicons_hint -881372798=tabs,0 1585002438=magicon,39 -1661605940=elfwood -607416893=prayeron,30 -822106577=jungle island xmas -607416892=prayeron,31 332368736=mad eadgar 1585002432=magicon,33 -143368781=side_background_right 1585002433=magicon,34 -1081494434=malady 1585002430=magicon,31 1585002431=magicon,32 1585002436=magicon,37 1585002437=magicon,38 1585002434=magicon,35 1585002435=magicon,36 -1938171360=miscgraphics3,0 -1059680853=trinity 781557721=dies_irae -1938171359=miscgraphics3,1 497375231=stillness -1938171358=miscgraphics3,2 -1938171357=miscgraphics3,3 -960709976=dogs of war 2129339089=magicon,1 2129339088=magicon,0 755433248=headicons_pk 108392383=regal -213632750=waterfall -1367706280=canvas 73828649=settlement 848123561=into the abyss 478781900=last stand 1339486127=the shadow -1055503808=roc and roll 837204902=mapdots 950484242=compass -1082154559=fanfare 747848680=nether_realm 788224888=dead quiet 1532279978=monarch waltz -149029727=side_background_left1 812947089=fanfare2 -149029726=side_background_left2 1006643748=high spirits -2136059388=starlight 2122572442=the tower -1998869913=spooky2 1411067174=gnome village2 -2065077267=wild side 812947090=fanfare3 1585002429=magicon,30 1294629755=on the wing 2097127567=monkey badness -2032107216=sojourn 1020264019=pest control 3237038=info 1473393027=fe fi fo fum -1686202291=upper_depths 3540994=stop 1742080803=darkwood 740392969=little cave of horrors -158379532=prayerglow -691855347=in between -200702983=the noble rodent 1652745754=forgotten -1895307673=hitmark,3 -1895307674=hitmark,2 -1895307675=hitmark,1 -1895307676=hitmark,0 -1895307670=hitmark,6 -1895307671=hitmark,5 -1895307672=hitmark,4 1968917071=bone dry -850506182=trawler minor 197029040=mapscene -808772318=in the pits -1165315580=looking back -1763090403=scape_ape -1938177931=miscgraphics,11 -1938177932=miscgraphics,10 1936130561=thrall_of_the_serpent 1258863383=yesteryear 1994744000=slice of silent movie -1691854169=dead can dance 1585002407=magicon,29 1585002405=magicon,27 1585002406=magicon,28 271319484=frostbite -499867199=meridian 1585002400=magicon,22 -1938177930=miscgraphics,12 -84626226=mudskipper melody 1585002403=magicon,25 1585002404=magicon,26 3641802=wall 1585002401=magicon,23 1585002402=magicon,24 -606457701=wolf mountain 1276599785=button_brown 1969878899=emotes,13 1969878897=emotes,11 1969878898=emotes,12 1969878896=emotes,10 -1938177928=miscgraphics,14 -907669678=brew hoo hoo -1938177929=miscgraphics,13 72999866=subterranea 619237947=the galleon -1764950404=scape sad 295831445=heart and mind 908430134=dangerous road 738888631=tradebacking -174800339=verdana_11pt_regular 686705631=lightwalk -601591436=side_background_bottom -1479412376=the navigator -359173459=zamorak zoo 744536246=null and void -1701556798=magicoff2,39 -1701556799=magicoff2,38 -1396384012=bamboo -200388662=lighthouse 133626717=suspicious -810515425=voyage 3061973=crag 1802291895=big chords -1661619479=elfwall 113315621=wood2 1813041183=steelborder2,0 429244831=slug a bug ball 1813041184=steelborder2,1 -1658386264=shining_spirit 738909086=chamber 526264239=verdana_13pt_regular -877351859=temple 2142215577=the mollusc menace 1124498189=warpath -2136649922=no way out -339706871=grimly_fiendish 547534551=wrath and ruin 544229147=lore and order 3327206=load 1610073470=lovakengj_sigil -419218284=long way home -662489856=food for thought 1306461568=stagnant -1662171955=elfdoor -1043985601=meddling kids 947464074=titlebutton -1309055712=exposed -1487348923=ambient jungle -1829469821=lament of meiyerditch 233203434=leftarrow_small -1216167350=dangerous 114464611=railings -1106574323=legend -1701556767=magicoff2,49 -999707515=time to mine -1701556768=magicoff2,48 2129339097=magicon,9 2129339096=magicon,8 1959211510=mapfunction,42 2129339095=magicon,7 1959211511=mapfunction,43 2129339094=magicon,6 2129339093=magicon,5 2129339092=magicon,4 2129339091=magicon,3 2129339090=magicon,2 3522472=saga -1701556769=magicoff2,47 -544722449=rellekka 1033441676=tribal background 1915718129=the desolate isle 1890607150=magicon2,19 1787618597=stranded 1717999087=forgettable melody 1959211512=mapfunction,44 -243680400=peng_emotes,0 1959211513=mapfunction,45 1959211514=mapfunction,46 1959211515=mapfunction,47 1959211516=mapfunction,48 1959211517=mapfunction,49 1705947058=the cellar dwellers 1216634785=landlubber 1884768198=magicoff,40 -1588113323=the rogues den 1884768199=magicoff,41 -905842564=serene -607599698=prayeroff,2 -607599699=prayeroff,1 1890607142=magicon2,11 1389384362=monkey trouble 1890607143=magicon2,12 1890607144=magicon2,13 1890607145=magicon2,14 1890607146=magicon2,15 1890607147=magicon2,16 1890607148=magicon2,17 1966766798=mausoleum 1890607149=magicon2,18 1808345541=armadyl alliance 290391725=options_slider,7 1890607141=magicon2,10 290391722=options_slider,4 -607599696=prayeroff,4 290391721=options_slider,3 -607599697=prayeroff,3 290391724=options_slider,6 -607599694=prayeroff,6 290391723=options_slider,5 -607599695=prayeroff,5 -607599692=prayeroff,8 -607599693=prayeroff,7 290391720=options_slider,2 -607599691=prayeroff,9 1334775925=chat_background -1779127378=arabian2 -1779127377=arabian3 528722471=island life 1890607175=magicon2,23 1890607176=magicon2,24 949634504=mouse trap 1890607177=magicon2,25 1890607178=magicon2,26 1890607179=magicon2,27 3327403=logo 290391719=options_slider,1 1092249049=storm brew 290391718=options_slider,0 404357804=everywhere 1890607172=magicon2,20 1890607173=magicon2,21 951530772=contest 1890607174=magicon2,22 -1701556776=magicoff2,40 1884768176=magicoff,39 -395250469=corporal punishment 1749113330=newbie melody 1884768174=magicoff,37 1884768175=magicoff,38 -1701556772=magicoff2,44 -1701556773=magicoff2,43 -1701556774=magicoff2,42 -1701556775=magicoff2,41 -858121616=tzhaar 1884768172=magicoff,35 1884768173=magicoff,36 666772244=combat_shield 1884768170=magicoff,33 1884768171=magicoff,34 1959211509=mapfunction,41 1639695510=mapmarker -1661748240=friends_icons -552301350=knightly -1918044851=mastermindless -1701556770=magicoff2,46 -1701556771=magicoff2,45 201526300=corporealbeast 1959211508=mapfunction,40 105001967=nomad -70910145=clickcross,3 -1737914947=mapfunction,5 1801745440=staticons2,11 -70910146=clickcross,2 -865479038=tribal -1737914946=mapfunction,6 1801745441=staticons2,12 -70910147=clickcross,1 -1737914945=mapfunction,7 1801745442=staticons2,13 -70910148=clickcross,0 -1737914944=mapfunction,8 1801745443=staticons2,14 -1737914943=mapfunction,9 1801745444=staticons2,15 1801745445=staticons2,16 1801745446=staticons2,17 -1877545169=land down under 93921962=books -1737914949=mapfunction,3 -1737914948=mapfunction,4 -1655721428=prayeroff,19 -2136884405=title.jpg -1655721429=prayeroff,18 1584819628=magicoff,6 1584819629=magicoff,7 437480876=voodoo cult 1584819624=magicoff,2 1584819625=magicoff,3 1584819626=magicoff,4 -1737914952=mapfunction,0 1584819627=magicoff,5 -1737914951=mapfunction,1 124995564=harmony2 1584819622=magicoff,0 1801745439=staticons2,10 1584819623=magicoff,1 346263512=dorgeshun city -1665011705=down and out 1890607180=magicon2,28 1890607181=magicon2,29 1417471781=titlescroll 1959211446=mapfunction,20 1956141536=options_radio_buttons,0 736568812=ballad of enchantment -1737914950=mapfunction,2 1959211447=mapfunction,21 1959211448=mapfunction,22 1959211449=mapfunction,23 -1890130256=morytania -70910141=clickcross,7 -70910142=clickcross,6 1956141539=options_radio_buttons,3 -70910143=clickcross,5 1956141538=options_radio_buttons,2 284766976=splendour -70910144=clickcross,4 1956141537=options_radio_buttons,1 196677638=the quizmaster 530068296=overture -1123094568=sl_button -700552779=hosidius_sigil -614076819=sad meadow 1956141543=options_radio_buttons,7 1956141542=options_radio_buttons,6 1956141541=options_radio_buttons,5 1956141540=options_radio_buttons,4 1584819631=magicoff,9 1846633612=gnome village -2128560371=sl_back 1969878905=emotes,19 306819362=crystal castle 1584819630=magicoff,8 1969878903=emotes,17 303737220=options_icons,7 1969878904=emotes,18 1969878901=emotes,15 -78220817=devils_may_care 1969878902=emotes,16 -40521666=dimension x 1969878900=emotes,14 673424924=the lunar isle 789609582=brimstail's scales 303737222=options_icons,9 303737221=options_icons,8 1959211415=mapfunction,10 3059343=coil 1959211416=mapfunction,11 1959211417=mapfunction,12 -1256560486=last_man_standing 1959211418=mapfunction,13 336238005=rightarrow_small 1959211419=mapfunction,14 -677662361=forever -1655721430=prayeroff,17 -1655721431=prayeroff,16 -1655721432=prayeroff,15 1959211420=mapfunction,15 1959211663=mapfunction,90 1959211421=mapfunction,16 1959211422=mapfunction,17 1959211423=mapfunction,18 303737217=options_icons,4 303737216=options_icons,3 303737215=options_icons,2 -1665005042=funny bunnies 303737214=options_icons,1 303737219=options_icons,6 303737218=options_icons,5 303737213=options_icons,0 95997798=we are the fairies 2001751835=desert heat 1959211424=mapfunction,19 -1655721437=prayeroff,10 687938017=clanwars -1776024210=desolate_mage -650944128=strength of saradomin -1655721433=prayeroff,14 1160873524=aye car rum ba -1655721434=prayeroff,13 -1655721435=prayeroff,12 -1081314499=marble -1655721436=prayeroff,11 1097075475=reset,0 1959211477=mapfunction,30 -693313916=warriors guild 3506388=roof 1959211478=mapfunction,31 1097075476=reset,1 -2134967800=dagannoth dawn -985763247=planks 1959211479=mapfunction,32 1999746381=fenkenstrain's refrain 898010371=garden of winter 359174830=rat hunt 1959211482=mapfunction,35 686441581=lightness 1959211483=mapfunction,36 1959211484=mapfunction,37 1959211485=mapfunction,38 1959211486=mapfunction,39 2023201035=dwarf theme 1959211480=mapfunction,33 1959211481=mapfunction,34 -1065532022=combatboxes,1 -1065532021=combatboxes,2 -1065532020=combatboxes,3 1867160429=old_tiles 394756979=scape santa 25205919=elfroof2 -663428071=dangerous way -1065532023=combatboxes,0 1959211450=mapfunction,24 -895939599=spirit 1959211451=mapfunction,25 1959211452=mapfunction,26 1959211453=mapfunction,27 1959211454=mapfunction,28 1959211455=mapfunction,29 -275310687=undercurrent 212205923=goblin village -303898981=faithless -1381531001=tomb raider 260940912=marzipan 1343267530=backhmid1 1343267531=backhmid2 1097468315=horizon -1655721404=prayeroff,22 623451622=kourend_the_magnificent -1655721405=prayeroff,21 -1655721406=prayeroff,20 -1655721400=prayeroff,26 -313384067=p12_full -1655721401=prayeroff,25 -1655721402=prayeroff,24 -1655721403=prayeroff,23 95848451=dream 1966781751=maws_jaws_claws -995428255=parade 95734525=method of madness -1308064877=hitmarks 1030045177=mutant medley 1333034828=blackmark 851641665=davy jones locker 417793574=scrollbar 1346720900=backleft2 1884768206=magicoff,48 1884768207=magicoff,49 1801140808=fangs for the memory 1884768204=magicoff,46 1345432055=pinball wizard 1884768205=magicoff,47 -783693496=dance of the undead 1274780903=chompy hunt 465278529=the lost tribe -1666437481=combaticons3,6 2032696205=cabin fever -1666437480=combaticons3,7 1825640471=borderland 415928477=zeah_mining 1884768202=magicoff,44 -607599700=prayeroff,0 1884768203=magicoff,45 1884768200=magicoff,42 1884768201=magicoff,43 -1666437487=combaticons3,0 -1666437486=combaticons3,1 813726263=crystal cave 1235442953=pathways 518814479=lullaby -1666437483=combaticons3,4 1585007985=magicon2,7 -1666437482=combaticons3,5 1585007986=magicon2,8 -1666437485=combaticons3,2 1585007987=magicon2,9 -1666437484=combaticons3,3 104080482=moody 1969878967=emotes,39 -665666447=work work work 1364992651=evil bobs island 1969878965=emotes,37 1227328817=verdana_15pt_regular 1969878966=emotes,38 1969878963=emotes,35 1581724013=monkey business 1969878964=emotes,36 -74307138=miscellania 1969878961=emotes,33 1969878962=emotes,34 1969878960=emotes,32 1131171307=wayward -1154558416=sl_arrows 529929957=overpass 1258058669=huffman -694094064=adventure 1171698653=orb_xp,1 1171698652=orb_xp,0 1171698655=orb_xp,3 1171698654=orb_xp,2 1802171733=arceuus_sigil 221109227=tears of guthix -1661754893=elfroof -1666437478=combaticons3,9 1124565314=warrior 1969878958=emotes,30 1969878959=emotes,31 -934797897=reggae 2110260221=the genie -1666437479=combaticons3,8 104257585=mummy -1923924724=sworddecor,0 2136325196=staticons,17 -1332194002=background 2136164423=homescape -1923924721=sworddecor,3 2136325194=staticons,15 -1923924722=sworddecor,2 -873564465=tiptoe 2136325195=staticons,16 -1923924723=sworddecor,1 123560953=espionage 2136325192=staticons,13 2136325193=staticons,14 1038911415=gnome king 2136325190=staticons,11 2136325191=staticons,12 1854274741=karamja jam 1969878989=emotes,40 -1242708793=glyphs 563269755=the terrible tower 1650323088=twilight -12868552=sea shanty 289742397=book of spells 1880989696=dragontooth island 2136325189=staticons,10 110873=pen -454421102=out of the deep 825919099=options_icons,19 825919097=options_icons,17 825919098=options_icons,18 -1349119470=cursed 1585007978=magicon2,0 825919095=options_icons,15 1585007979=magicon2,1 825919096=options_icons,16 825919093=options_icons,13 825919094=options_icons,14 825919091=options_icons,11 825919092=options_icons,12 -895977880=sphinx -874529881=city of the dead 825919090=options_icons,10 1585007981=magicon2,3 1585007982=magicon2,4 1585007983=magicon2,5 -1618729246=body parts 1585007984=magicon2,6 1585007980=magicon2,2 1086075866=shayzien_sigil 2103661451=jester minute -911346307=steelborder,2 -1809781334=button_brown_big -911346306=steelborder,3 -816227352=vision -911346309=steelborder,0 109407595=shine -911346308=steelborder,1 -119954464=combaticons3,12 -119954465=combaticons3,11 -1846853118=armageddon -119954462=combaticons3,14 -119954463=combaticons3,13 -119954460=combaticons3,16 -908183966=scarab -119954461=combaticons3,15 -282886672=home sweet home 103666243=march 1969878929=emotes,22 1643875326=fire and brimstone 1969878927=emotes,20 -119954466=combaticons3,10 1969878928=emotes,21 -839455633=close quarters 941457503=way of the enchanter -1453405761=mor-ul-rek -415134015=have a blast -1658514874=floating free 1213477442=chickened out -1619800349=emotes_locked,27 -599680631=fear and loathing -1081041422=insect queen -1268786147=forest -119954459=combaticons3,17 -119954457=combaticons3,19 -119954458=combaticons3,18 -850395529=trouble brewing -1773920521=cave of the goblins -771284962=claustrophobia 1310729739=bankbuttons,2 1306691868=upcoming 1465443077=over to nardah 1310729744=bankbuttons,7 1310729743=bankbuttons,6 1310729742=bankbuttons,5 1234827707=deep wildy -90350772=xenophobe -750127868=arabian 1310729741=bankbuttons,4 1310729740=bankbuttons,3 1041911129=waterlogged 108875897=runes 1447063382=barb wire -378865792=magic dance 285466503=overlay_duel 1814287296=zeah_magic 1976894499=down to earth 1969878936=emotes,29 1969878934=emotes,27 -1567437308=deadlands 1969878935=emotes,28 1310729738=bankbuttons,1 1969878932=emotes,25 -957019274=too many cooks 1310729737=bankbuttons,0 2092627105=silence 1969878933=emotes,26 1969878930=emotes,23 1969878931=emotes,24 -901674570=well of voyage 1787935731=quill_oblique_large -365283881=quill_caps_large 1157777820=lunar_alphabet 24702590=lunar_alphabet_lrg ================================================ FILE: config/server-config.example.json ================================================ { "configDir": "./config", "cacheDir": "./cache", "host": "0.0.0.0", "port": 43594, "updateServerHost": "0.0.0.0", "updateServerPort": 43592, "loginServerHost": "0.0.0.0", "loginServerPort": 43591, "rsaMod": "119568088839203297999728368933573315070738693395974011872885408638642676871679245723887367232256427712869170521351089799352546294030059890127723509653145359924771433131004387212857375068629466435244653901851504845054452735390701003613803443469723435116497545687393297329052988014281948392136928774011011998343", "rsaExp": "12747337179295870166838611986189126026507945904720545965726999254744592875817063488911622974072289858092633084100280214658532446654378876853112046049506789703022033047774294965255097838909779899992870910011426403494610880634275141204442441976355383839981584149269550057129306515912021704593400378690444280161", "encryptionEnabled": true, "playerSavePath": "./data/saves", "showWelcome": true, "expRate": 1, "giveAchievements": true, "checkCredentials": true, "tutorialEnabled": false, "adminDropsEnabled": true, "bypassTeleportRequirements": false } ================================================ FILE: data/config/combat-styles.json ================================================ { "unarmed": [ { "type": "crush", "exp": "attack", "anim": "punch", "button_id": 2 }, { "type": "crush", "exp": "strength", "anim": "kick", "button_id": 3 }, { "type": "crush", "exp": "defence", "anim": "punch", "button_id": 4 } ], "axe": [ { "type": "slash", "exp": "attack", "anim": "slash", "button_id": 2 }, { "type": "slash", "exp": "strength", "anim": "slash", "button_id": 5 }, { "type": "crush", "exp": "strength", "anim": "slash", "button_id": 4 }, { "type": "slash", "exp": "defence", "anim": "slash", "button_id": 3 } ], "dagger": [ { "type": "stab", "exp": "attack", "anim": "stab", "button_id": 2 }, { "type": "stab", "exp": "strength", "anim": "stab", "button_id": 3 }, { "type": "slash", "exp": "strength", "anim": "slash", "button_id": 4 }, { "type": "stab", "exp": "defence", "anim": "stab", "button_id": 5 } ] } ================================================ FILE: data/config/examine-item-data.yaml ================================================ - id: 0 # Dwarf remains examine: "The body of a Dwarf savaged by Goblins." - id: 1 # Toolkit examine: "Good for repairing broken cannons." - id: 2 # Cannonball examine: "Ammo for the Dwarf Cannon." - id: 3 # Nulodion's notes examine: "Construction notes for Dwarf cannon ammo." - id: 4 # Ammo mould examine: "Used to make cannon ammunition." - id: 5 # Instruction manual examine: "An old note book." - id: 6 # Cannon base examine: "The cannon is built on this." - id: 8 # Cannon stand examine: "The mounting for the multicannon." - id: 10 # Cannon barrels examine: "The barrels of the multicannon." - id: 12 # Cannon furnace examine: "This powers the multicannon." - id: 14 # Railing examine: "A metal railing replacement." - id: 15 # Holy table napkin examine: "A cloth given to me by Sir Galahad." - id: 16 # Magic whistle examine: "A small tin whistle." - id: 17 # Grail bell examine: "I wonder what happens when I ring it?" - id: 18 # Magic gold feather examine: "It will point the way for me." - id: 19 # Holy grail examine: "A holy and powerful artefact." - id: 20 # White cog examine: "A cog from some machinery." - id: 21 # Black cog examine: "A cog from some machinery." - id: 22 # Blue cog examine: "A cog from some machinery." - id: 23 # Red cog examine: "A cog from some machinery." - id: 24 # Rat poison examine: "Doesn't look very tasty." - id: 25 # Red vine worm examine: "Wormy." - id: 26 # Fishing trophy examine: "Hemenster fishing contest trophy." - id: 27 # Fishing pass examine: "Pass to the Hemenster fishing contest." - id: 28 # Insect repellent examine: "Drives away all known 6 legged creatures." - id: 30 # Bucket of wax examine: "It's a bucket of wax." - id: 32 # Lit black candle examine: "A lit spooky candle." - id: 33 # Lit candle examine: "A lit candle." - id: 35 # Excalibur examine: "This used to belong to King Arthur." - id: 36 # Candle examine: "A candle." - id: 38 # Black candle examine: "A spooky candle." - id: 39 # Bronze arrowtips examine: "I can make some arrows with these." - id: 40 # Iron arrowtips examine: "I can make some arrows with these." - id: 41 # Steel arrowtips examine: "I can make some arrows with these." - id: 42 # Mithril arrowtips examine: "I can make some arrows with these." - id: 43 # Adamant arrowtips examine: "I can make some arrows with these." - id: 44 # Rune arrowtips examine: "I can make some arrows with these." - id: 45 # Opal bolt tips examine: "Opal bolt tips." - id: 46 # Pearl bolt tips examine: "Pearl bolt tips." - id: 47 # Barb bolttips examine: "I can make bolts with these." - id: 48 # Longbow (u) examine: "I need to find a string for this." - id: 50 # Shortbow (u) examine: "I need to find a string for this." - id: 52 # Arrow shaft examine: "A wooden arrow shaft." - id: 53 # Headless arrow examine: "A wooden arrow shaft with flights attached." - id: 54 # Oak shortbow (u) examine: "An unstrung oak shortbow; I need a bowstring for this." - id: 56 # Oak longbow (u) examine: "An unstrung oak longbow; I need a bowstring for this." - id: 58 # Willow longbow (u) examine: "An unstrung willow longbow; I need a bowstring for this." - id: 60 # Willow shortbow (u) examine: "An unstrung willow shortbow; I need a bowstring for this." - id: 62 # Maple longbow (u) examine: "An unstrung maple longbow; I need a bowstring for this." - id: 64 # Maple shortbow (u) examine: "An unstrung maple shortbow; I need a bowstring for this." - id: 66 # Yew longbow (u) examine: "An unstrung yew longbow; I need a bowstring for this." - id: 68 # Yew shortbow (u) examine: "An unstrung yew shortbow; I need a bowstring for this." - id: 70 # Magic longbow (u) examine: "An unstrung magic longbow; I need a bowstring for this." - id: 72 # Magic shortbow (u) examine: "An unstrung magic shortbow; I need a bowstring for this." - id: 74 # Khazard helmet examine: "A helmet, as worn by the minions of General Khazard." - id: 75 # Khazard armour examine: "Armour, as worn by the minions of General Khazard." - id: 76 # Khazard cell keys examine: "These keys open the cells at the Khazard fight arena." - id: 77 # Khali brew examine: "A bottle of Khazard's worst brew." - id: 78 # Ice arrows examine: "Can only be fired from yew, magic, dark or twisted bows." - id: 83 # Lever examine: "A lever to open something perhaps?" - id: 84 # Staff of armadyl examine: "The power in this staff causes it to vibrate gently." - id: 85 # Shiny key examine: "It catches the light!." - id: 86 # Pendant of lucien examine: "An amulet made by Lucien." - id: 87 # Armadyl pendant examine: "Worn by followers of Armadyl." - id: 88 # Boots of lightness examine: "Magic boots that make you lighter than normal." - id: 89 # Boots of lightness examine: "Magic boots that make you lighter than normal." - id: 90 # Child's blanket examine: "It's very soft!" - id: 113 # Strength potion(4) examine: "4 doses of Strength potion." - id: 115 # Strength potion(3) examine: "3 doses of Strength potion." - id: 117 # Strength potion(2) examine: "2 doses of Strength potion." - id: 119 # Strength potion(1) examine: "1 dose of Strength potion." - id: 121 # Attack potion(3) examine: "3 doses of Attack potion." - id: 123 # Attack potion(2) examine: "2 doses of Attack potion." - id: 125 # Attack potion(1) examine: "1 dose of Attack potion." - id: 127 # Restore potion(3) examine: "3 doses of restore potion." - id: 129 # Restore potion(2) examine: "2 doses of restore potion." - id: 131 # Restore potion(1) examine: "1 dose of restore potion." - id: 133 # Defence potion(3) examine: "3 doses of Defence potion." - id: 135 # Defence potion(2) examine: "2 doses of Defence potion." - id: 137 # Defence potion(1) examine: "1 dose of Defence potion." - id: 139 # Prayer potion(3) examine: "3 doses of Prayer restore potion." - id: 141 # Prayer potion(2) examine: "2 doses of Prayer restore potion." - id: 143 # Prayer potion(1) examine: "1 dose of Prayer restore potion." - id: 145 # Super attack(3) examine: "3 doses of super Attack potion." - id: 147 # Super attack(2) examine: "2 doses of super Attack potion." - id: 149 # Super attack(1) examine: "1 dose of super Attack potion." - id: 151 # Fishing potion(3) examine: "3 doses of Fishing potion." - id: 153 # Fishing potion(2) examine: "2 doses of Fishing potion." - id: 155 # Fishing potion(1) examine: "1 dose of Fishing potion." - id: 157 # Super strength(3) examine: "3 doses of super Strength potion." - id: 159 # Super strength(2) examine: "2 doses of super Strength potion." - id: 161 # Super strength(1) examine: "1 dose of super Strength potion." - id: 163 # Super defence(3) examine: "3 doses of super Defence potion." - id: 165 # Super defence(2) examine: "2 doses of super Defence potion." - id: 167 # Super defence(1) examine: "1 dose of super Defence potion." - id: 169 # Ranging potion(3) examine: "3 doses of ranging potion." - id: 171 # Ranging potion(2) examine: "2 doses of ranging potion." - id: 173 # Ranging potion(1) examine: "1 dose of ranging potion." - id: 175 # Antipoison(3) examine: "3 doses of antipoison potion." - id: 177 # Antipoison(2) examine: "2 doses of antipoison potion." - id: 179 # Antipoison(1) examine: "1 dose of antipoison potion." - id: 181 # Superantipoison(3) examine: "3 doses of super antipoison potion." - id: 183 # Superantipoison(2) examine: "2 doses of super antipoison potion." - id: 185 # Superantipoison(1) examine: "1 dose of super antipoison potion." - id: 187 # Weapon poison examine: "For use on daggers and projectiles." - id: 189 # Zamorak brew(3) examine: "3 doses of Zamorak brew." - id: 191 # Zamorak brew(2) examine: "2 doses of Zamorak brew." - id: 193 # Zamorak brew(1) examine: "1 dose of Zamorak brew." - id: 195 # Potion examine: "This is meant to be good for spots." - id: 197 # Poison chalice examine: "A cup of a strange brew..." - id: 221 # Eye of newt examine: "It seems to be looking at me." - id: 223 # Red spiders' eggs examine: "Ewww!" - id: 225 # Limpwurt root examine: "The root of a limpwurt plant." - id: 227 # Vial of water examine: "A glass vial containing water." - id: 229 # Vial examine: "An empty glass vial." - id: 231 # Snape grass examine: "Strange spiky grass." - id: 233 # Pestle and mortar examine: "I can grind things for potions in this." - id: 235 # Unicorn horn dust examine: "Finely ground horn of Unicorn." - id: 237 # Unicorn horn examine: "This horn has restorative properties." - id: 239 # White berries examine: "Sour berries, used in potions." - id: 241 # Dragon scale dust examine: "Finely ground scale of Dragon." - id: 243 # Blue dragon scale examine: "A large shiny scale." - id: 245 # Wine of zamorak examine: "An evil wine for an evil god." - id: 247 # Jangerberries examine: "They don't look very ripe." - id: 249 # Guam leaf examine: "A bitter green herb." - id: 251 # Marrentill examine: "A herb used in poison cures." - id: 253 # Tarromin examine: "A useful herb." - id: 255 # Harralander examine: "A useful herb." - id: 257 # Ranarr weed examine: "A useful herb." - id: 259 # Irit leaf examine: "A useful herb." - id: 261 # Avantoe examine: "A useful herb." - id: 263 # Kwuarm examine: "A powerful herb." - id: 265 # Cadantine examine: "A powerful herb." - id: 267 # Dwarf weed examine: "A powerful herb." - id: 269 # Torstol examine: "A powerful herb." - id: 271 # Pressure gauge examine: "It looks like part of a machine." - id: 272 # Fish food examine: "Keeps your pet fish strong and healthy." - id: 273 # Poison examine: "This stuff looks nasty." - id: 274 # Poisoned fish food examine: "Doesn't seem very nice to the poor fishes." - id: 275 # Key examine: "A slightly smelly key." - id: 276 # Rubber tube examine: "It's slightly charred." - id: 277 # Oil can examine: "It's pretty full." - id: 278 # Cattleprod examine: "A sharp cattleprod." - id: 279 # Sheep feed examine: "Councillor Halgrive gave me this to kill some sheep." - id: 280 # Sheep bones (1) examine: "The suspicious-looking remains of a suspicious-looking sheep." - id: 281 # Sheep bones (2) examine: "The suspicious-looking remains of a suspicious-looking sheep." - id: 282 # Sheep bones (3) examine: "The suspicious-looking remains of a suspicious-looking sheep." - id: 283 # Sheep bones (4) examine: "The suspicious-looking remains of a suspicious-looking sheep." - id: 284 # Plague jacket examine: "This should protect me from the plague, I hope!" - id: 285 # Plague trousers examine: "These should protect me from the plague, I hope!" - id: 286 # Orange goblin mail examine: "Armour designed to fit goblins." - id: 287 # Blue goblin mail examine: "Armour designed to fit goblins." - id: 288 # Goblin mail examine: "Armour designed to fit goblins." - id: 290 # Research package examine: "This contains some vital research results." - id: 291 # Notes examine: "It seems to be written in some kind of code." - id: 292 # Book on baxtorian examine: "A book on elven history in northern Gielinor." - id: 294 # Glarial's pebble examine: "A small pebble with elven inscription." - id: 295 # Glarial's amulet examine: "A bright green gem set in a necklace." - id: 296 # Glarial's urn examine: "An urn containing Glarial's ashes." - id: 299 # Mithril seeds examine: "Magical seeds in a mithril case." - id: 300 # Rat's tail examine: "A bit of rat." - id: 301 # Lobster pot examine: "Useful for catching lobsters." - id: 303 # Small fishing net examine: "Useful for catching small fish." - id: 305 # Big fishing net examine: "Useful for catching lots of fish." - id: 307 # Fishing rod examine: "Useful for catching sardine or herring." - id: 309 # Fly fishing rod examine: "Useful for catching salmon or trout." - id: 311 # Harpoon examine: "Useful for catching really big fish." - id: 313 # Fishing bait examine: "For use with a fishing rod." - id: 314 # Feather examine: "Used for fly fishing." - id: 315 # Shrimps examine: "Some nicely cooked shrimp." - id: 317 # Raw shrimps examine: "I should try cooking this." - id: 319 # Anchovies examine: "Some nicely cooked anchovies." - id: 321 # Raw anchovies examine: "I should try cooking this." - id: 323 # Burnt fish examine: "Oops!" - id: 325 # Sardine examine: "Some nicely cooked sardines." - id: 327 # Raw sardine examine: "I should try cooking this." - id: 329 # Salmon examine: "Some nicely cooked salmon." - id: 331 # Raw salmon examine: "I should try cooking this." - id: 333 # Trout examine: "Some nicely cooked trout." - id: 335 # Raw trout examine: "I should try cooking this." - id: 337 # Giant carp examine: "Some nicely cooked giant carp." - id: 338 # Raw giant carp examine: "I should try cooking this." - id: 339 # Cod examine: "Some nicely cooked cod." - id: 341 # Raw cod examine: "I should try cooking this." - id: 343 # Burnt fish examine: "Oops!" - id: 345 # Raw herring examine: "I should try cooking this." - id: 347 # Herring examine: "Some nicely cooked herring." - id: 349 # Raw pike examine: "I should try cooking this." - id: 351 # Pike examine: "Some nicely cooked pike." - id: 353 # Raw mackerel examine: "I should try cooking this." - id: 355 # Mackerel examine: "Some nicely cooked mackerel." - id: 357 # Burnt fish examine: "Oops!" - id: 359 # Raw tuna examine: "I should try cooking this." - id: 361 # Tuna examine: "Wow, this is a big fish." - id: 363 # Raw bass examine: "I should try cooking this." - id: 365 # Bass examine: "Wow, this is a big fish." - id: 367 # Burnt fish examine: "Oops!" - id: 369 # Burnt fish examine: "Oops!" - id: 371 # Raw swordfish examine: "I should try cooking this." - id: 373 # Swordfish examine: "I'd better be careful eating this!" - id: 375 # Burnt swordfish examine: "Oops!" - id: 377 # Raw lobster examine: "I should try cooking this." - id: 379 # Lobster examine: "This looks tricky to eat." - id: 381 # Burnt lobster examine: "Oops!" - id: 383 # Raw shark examine: "I should try cooking this." - id: 385 # Shark examine: "I'd better be careful eating this." - id: 387 # Burnt shark examine: "Oops!" - id: 389 # Raw manta ray examine: "A rare catch." - id: 391 # Manta ray examine: "A rare catch." - id: 393 # Burnt manta ray examine: "Oops!" - id: 395 # Raw sea turtle examine: "A rare catch." - id: 397 # Sea turtle examine: "Tasty!" - id: 399 # Burnt sea turtle examine: "Oops!" - id: 401 # Seaweed examine: "Slightly damp seaweed." - id: 403 # Edible seaweed examine: "Slightly damp seaweed." - id: 405 # Casket examine: "I hope there's treasure in it." - id: 407 # Oyster examine: "Maybe there are pearls inside?" - id: 409 # Empty oyster examine: "Aww, it's empty." - id: 411 # Oyster pearl examine: "I could work wonders with a chisel on this pearl." - id: 413 # Oyster pearls examine: "I could work wonders with a chisel on these pearls." - id: 415 # Ethenea examine: "An expensive colourless liquid." - id: 416 # Liquid honey examine: "This isn't worth much." - id: 417 # Sulphuric broline examine: "It's highly poisonous." - id: 418 # Plague sample examine: "Probably best I don't keep this too long." - id: 419 # Touch paper examine: "A special kind of paper." - id: 420 # Distillator examine: "Apparently it distills." - id: 422 # Bird feed examine: "Birds love this stuff!" - id: 423 # Key examine: "Opens things." - id: 424 # Pigeon cage examine: "It's full of pigeons." - id: 425 # Pigeon cage examine: "It's empty..." - id: 426 # Priest gown examine: "Top half of a priest suit." - id: 428 # Priest gown examine: "Bottom half of a priest suit." - id: 431 # Karamjan rum examine: "A very strong spirit brewed in Karamja." - id: 432 # Chest key examine: "A key to One-Eyed Hector's chest." - id: 433 # Pirate message examine: "Pirates don't have the best handwriting..." - id: 434 # Clay examine: "Some hard dry clay." - id: 436 # Copper ore examine: "This needs refining." - id: 438 # Tin ore examine: "This needs refining." - id: 440 # Iron ore examine: "This needs refining." - id: 442 # Silver ore examine: "This needs refining." - id: 444 # Gold ore examine: "This needs refining." - id: 446 # 'perfect' gold ore examine: "This needs refining." - id: 447 # Mithril ore examine: "This needs refining." - id: 449 # Adamantite ore examine: "This needs refining." - id: 451 # Runite ore examine: "This needs refining." - id: 453 # Coal examine: "Hmm a non-renewable energy source!" - id: 455 # Barcrawl card examine: "The official Alfred Grimhand bar crawl card." - id: 456 # Scorpion cage examine: "It's empty!" - id: 457 # Scorpion cage examine: "There is 1 scorpion inside." - id: 458 # Scorpion cage examine: "There are 2 scorpions inside." - id: 459 # Scorpion cage examine: "There are 2 scorpions inside." - id: 460 # Scorpion cage examine: "There is 1 scorpion inside." - id: 461 # Scorpion cage examine: "There are 2 scorpions inside." - id: 462 # Scorpion cage examine: "There is 1 scorpion inside." - id: 463 # Scorpion cage examine: "There are 3 scorpions inside." - id: 464 # Strange fruit examine: "I wonder what this tastes like?" - id: 466 # Pickaxe handle examine: "Useless without the head." - id: 468 # Broken pickaxe examine: "Nurmof can fix this for me." - id: 470 # Broken pickaxe examine: "Nurmof can fix this for me." - id: 472 # Broken pickaxe examine: "Nurmof can fix this for me." - id: 474 # Broken pickaxe examine: "Nurmof can fix this for me." - id: 476 # Broken pickaxe examine: "Nurmof can fix this for me." - id: 478 # Broken pickaxe examine: "Nurmof can fix this for me." - id: 480 # Bronze pick head examine: "It's missing a handle." - id: 482 # Iron pick head examine: "It's missing a handle." - id: 484 # Steel pick head examine: "It's missing a handle." - id: 486 # Mithril pick head examine: "It's missing a handle." - id: 488 # Adamant pick head examine: "It's missing a handle." - id: 490 # Rune pick head examine: "It's missing a handle." - id: 492 # Axe handle examine: "Useless without the head." - id: 494 # Broken axe examine: "Bob can fix this for me." - id: 496 # Broken axe examine: "Bob can fix this for me." - id: 498 # Broken axe examine: "Bob can fix this for me." - id: 500 # Broken axe examine: "Bob can fix this for me." - id: 502 # Broken axe examine: "Bob can fix this for me." - id: 504 # Broken axe examine: "Bob can fix this for me." - id: 506 # Broken axe examine: "Bob can fix this for me." - id: 522 # Enchanted beef examine: "I don't fancy eating this now." - id: 523 # Enchanted rat examine: "I don't fancy eating this now." - id: 524 # Enchanted bear examine: "I don't fancy eating this now." - id: 525 # Enchanted chicken examine: "I don't fancy eating this now." - id: 526 # Bones examine: "Bones are for burying!" - id: 528 # Burnt bones examine: "Bones are for burying!" - id: 530 # Bat bones examine: "Ew it's a pile of bones." - id: 532 # Big bones examine: "Ew it's a pile of bones." - id: 534 # Babydragon bones examine: "Ew it's a pile of bones." - id: 536 # Dragon bones examine: "These would feed a dog for months!" - id: 538 # Druid's robe examine: "Keeps a druid's knees nice and warm." - id: 542 # Monk's robe examine: "Keeps a monk's knees nice and warm." - id: 548 # Shade robe examine: "If a shade had knees, this would keep them nice and warm." - id: 550 # Newcomer map examine: "Issued to all new citizens of Gielinor." - id: 552 # Ghostspeak amulet examine: "It lets me talk to ghosts." - id: 553 # Ghost's skull examine: "Ooooh spooky!" - id: 554 # Fire rune examine: "One of the 4 basic elemental Runes." - id: 555 # Water rune examine: "One of the 4 basic elemental Runes." - id: 556 # Air rune examine: "One of the 4 basic elemental Runes." - id: 557 # Earth rune examine: "One of the 4 basic elemental Runes." - id: 558 # Mind rune examine: "Used for basic level missile spells." - id: 559 # Body rune examine: "Used for curse spells." - id: 560 # Death rune examine: "Used for medium level missile spells." - id: 561 # Nature rune examine: "Used for alchemy spells." - id: 562 # Chaos rune examine: "Used for low level missile spells." - id: 563 # Law rune examine: "Used for teleport spells." - id: 564 # Cosmic rune examine: "Used for enchant spells." - id: 565 # Blood rune examine: "Used for high level missile spells." - id: 566 # Soul rune examine: "Used for high level curse spells." - id: 567 # Unpowered orb examine: "I'd prefer it if it was powered." - id: 569 # Fire orb examine: "A magic glowing orb." - id: 571 # Water orb examine: "A magic glowing orb." - id: 573 # Air orb examine: "A magic glowing orb." - id: 575 # Earth orb examine: "A magic glowing orb." - id: 581 # Black robe examine: "I can do magic better in this." - id: 583 # Bailing bucket examine: "It's a bailing bucket." - id: 585 # Bailing bucket examine: "It's a bailing bucket full of salty water." - id: 587 # Orb of protection examine: "A strange glowing green orb." - id: 588 # Orbs of protection examine: "Two strange glowing green orbs." - id: 589 # Gnome amulet examine: "It's an amulet of protection given to me by the gnomes." - id: 590 # Tinderbox examine: "Useful for lighting a fire." - id: 592 # Ashes examine: "A heap of ashes." - id: 594 # Lit torch examine: "A lit home-made torch." - id: 595 # Torch examine: "An unlit home-made torch." - id: 596 # Unlit torch examine: "An unlit home-made torch." - id: 602 # Lens mould examine: "An unusual mould in the shape of a disc." - id: 604 # Bone shard examine: "A slender bone shard given to you by Zadimus." - id: 605 # Bone key examine: "A bone key fashioned from a shard of bone." - id: 606 # Stone-plaque examine: "A stone plaque with carved letters in it." - id: 607 # Tattered scroll examine: "An ancient tattered scroll." - id: 608 # Crumpled scroll examine: "An ancient crumpled scroll." - id: 609 # Rashiliyia corpse examine: "The remains of the Zombie Queen." - id: 610 # Zadimus corpse examine: "The remains of Zadimus." - id: 611 # Locating crystal examine: "A magical crystal sphere." - id: 612 # Locating crystal examine: "A magical crystal sphere." - id: 613 # Locating crystal examine: "A magical crystal sphere." - id: 614 # Locating crystal examine: "A magical crystal sphere." - id: 615 # Locating crystal examine: "A magical crystal sphere." - id: 616 # Beads of the dead examine: "A curious looking neck ornament." - id: 617 # Coins examine: "Lovely money!" - id: 618 # Bone beads examine: "Beads carved out of a bone." - id: 619 # Paramaya ticket examine: "Allows you to rest in the luxurious Paramayer Inn." - id: 621 # Ship ticket examine: "Allows you passage on the 'Lady of the waves' ship." - id: 623 # Sword pommel examine: "An ivory sword pommel." - id: 624 # Bervirius notes examine: "Notes taken from the tomb of Bervirius." - id: 625 # Wampum belt examine: "A decorated belt used to trade information between distant villages." - id: 666 # Portrait examine: "Picture of a posing Paladin." - id: 667 # Blurite sword examine: "A Faladian Knight's sword." - id: 668 # Blurite ore examine: "Definitely blue." - id: 669 # Specimen jar examine: "A receptacle for specimens!" - id: 670 # Specimen brush examine: "A small brush used to clean rock samples." - id: 671 # Animal skull examine: "A carefully-kept-safe skull sample." - id: 672 # Special cup examine: "A special cup." - id: 673 # Teddy examine: "A lucky mascot." - id: 674 # Cracked sample examine: "A roughly shaped piece of rock." - id: 675 # Rock pick examine: "A small pick for digging." - id: 676 # Trowel examine: "Used for digging!" - id: 677 # Panning tray examine: "An empty tray for panning." - id: 678 # Panning tray examine: "This tray contains gold." - id: 679 # Panning tray examine: "This tray contains mud." - id: 680 # Nuggets examine: "Pure, lovely gold!" - id: 681 # Ancient talisman examine: "An unusual symbol as yet unidentified by the archaeological expert." - id: 682 # Unstamped letter examine: "A letter waiting to be stamped." - id: 683 # Sealed letter examine: "A sealed letter of recommendation." - id: 684 # Belt buckle examine: "Used to hold up trousers!" - id: 685 # Old boot examine: "Phew!" - id: 686 # Rusty sword examine: "A decent-enough weapon gone rusty." - id: 687 # Broken arrow examine: "This must have been shot at high speed." - id: 688 # Buttons examine: "Not Dick Whittington's helper at all!" - id: 689 # Broken staff examine: "I pity the poor person beaten with this!" - id: 690 # Broken glass examine: "Smashed glass." - id: 691 # Level 1 certificate examine: "The owner has passed the Earth Sciences level 1 exam." - id: 692 # Level 2 certificate examine: "The owner has passed Earth Sciences level 2 exam." - id: 693 # Level 3 certificate examine: "The owner has passed Earth Sciences level 3 exam." - id: 694 # Ceramic remains examine: "Smashing!" - id: 695 # Old tooth examine: "Now, if I can just find a tooth fairy to sell this to..." - id: 696 # Invitation letter examine: "A letter inviting me to use the private dig shafts." - id: 697 # Damaged armour examine: "It would be hard to repair this!" - id: 698 # Broken armour examine: "No use to me in this state..." - id: 699 # Stone tablet examine: "An old stone slab with writing on it." - id: 700 # Chemical powder examine: "An acrid chemical." - id: 701 # Ammonium nitrate examine: "An acrid chemical." - id: 702 # Unidentified liquid examine: "A strong chemical." - id: 703 # Nitroglycerin examine: "A strong chemical." - id: 704 # Ground charcoal examine: "Charcoal - crushed to small pieces!" - id: 705 # Mixed chemicals examine: "A mixture of strong chemicals." - id: 706 # Mixed chemicals examine: "A mixture of strong chemicals." - id: 707 # Chemical compound examine: "A mixture of strong chemicals." - id: 708 # Arcenia root examine: "The root of an arcenia plant." - id: 709 # Chest key examine: "This fits a chest." - id: 710 # Vase examine: "A vessel for holding plants." - id: 711 # Book on chemicals examine: "It's about chemicals, judging from its cover." - id: 712 # Cup of tea examine: "A nice cup of tea." - id: 714 # Radimus notes examine: "Notes given to you by Radimus Erkle, it includes a partially completed map." - id: 715 # Radimus notes examine: "Notes given to you by Radimus Erkle, it includes a partially completed map." - id: 716 # Bull roarer examine: "It makes a loud but interesting sound when swung in the air." - id: 717 # Scrawled note examine: "A scrawled note with spidery writing on it." - id: 718 # A scribbled note examine: "A scrawled note with spidery writing on it." - id: 719 # Scrumpled note examine: "A scrawled note with spidery writing on it." - id: 720 # Sketch examine: "A rough sketch of a bowl shaped vessel given to you by Gujuo." - id: 721 # Gold bowl examine: "A specially made bowl constructed out of pure gold." - id: 722 # Blessed gold bowl examine: "A specially made bowl constructed out of pure gold and blessed." - id: 723 # Golden bowl examine: "A specially made golden bowl with water." - id: 724 # Golden bowl examine: "A specially made bowl constructed out of pure gold. It has pure water in it." - id: 725 # Golden bowl examine: "A blessed golden bowl. It has water in it." - id: 726 # Golden bowl examine: "A blessed golden bowl. It has pure sacred water in it." - id: 727 # Hollow reed examine: "One of nature's pipes." - id: 729 # Shamans tome examine: "It looks like the Shamans personal notes..." - id: 731 # Enchanted vial examine: "An enchanted empty glass vial." - id: 732 # Holy water examine: "A vial of holy water, good against certain demons." - id: 733 # Smashed glass examine: "Fragments of a broken container." - id: 735 # Yommi tree seeds examine: "These need to be germinated before they can be used." - id: 736 # Yommi tree seeds examine: "These are germinated and ready to be planted in fertile soil." - id: 737 # Snakeweed mixture examine: "It's a mixture of Snakeweed and water. Needs another ingredient." - id: 738 # Ardrigal mixture examine: "It's a mixture of Ardrigal and water. Needs another ingredient." - id: 739 # Bravery potion examine: "A bravery potion for which Gujuo gave you the details, let's hope it works." - id: 740 # Blue hat examine: "A strange blue wizards hat." - id: 741 # Chunk of crystal examine: "It looks like it's been snapped off of something." - id: 742 # Hunk of crystal examine: "It looks like it's been snapped off of something." - id: 743 # Lump of crystal examine: "It looks like it's been snapped off of something." - id: 744 # Heart crystal examine: "A heart shaped crystal." - id: 745 # Heart crystal examine: "A heart shaped crystal." - id: 746 # Dark dagger examine: "A black obsidian dagger, it has a strange aura about it." - id: 747 # Glowing dagger examine: "A black obsidian dagger, it has a strange aura about it - it seems to be glowing." - id: 748 # Holy force examine: "A powerful spell for good." - id: 749 # Yommi totem examine: "A well carved totem pole made from the trunk of a Yommi tree." - id: 750 # Gilded totem examine: "A gilded totem pole from the Kharazi tribe." - id: 751 # Gnomeball examine: "A ball used in Gnomeball." - id: 753 # Cadava berries examine: "Poisonous berries." - id: 755 # Message examine: "A message from Juliet to Romeo." - id: 756 # Cadava potion examine: "I'm meant to give this to Juliet." - id: 757 # Book examine: "The Shield of Arrav by A R Wright." - id: 763 # Broken shield examine: "Half of the Shield of Arrav." - id: 765 # Broken shield examine: "Half of the Shield of Arrav." - id: 767 # Phoenix crossbow examine: "Former property of the Phoenix Gang." - id: 769 # Certificate examine: "I can use this to claim a reward from the King." - id: 771 # Dramen branch examine: "A limb of the fabled Dramen tree." - id: 772 # Dramen staff examine: "Crafted from a Dramen tree branch." - id: 773 # 'perfect' ring examine: "A perfect ruby ring." - id: 774 # 'perfect' necklace examine: "A perfect ruby necklace." - id: 775 # Cooking gauntlets examine: "These gauntlets empower with a greater ability to cook fish." - id: 777 # Chaos gauntlets examine: "These gauntlets empower spell casters." - id: 778 # Steel gauntlets examine: "My reward for assisting the Fitzharmon family." - id: 779 # Crest part examine: "A fragment of the Fitzharmon family crest." - id: 780 # Crest part examine: "A fragment of the Fitzharmon family crest." - id: 781 # Crest part examine: "A fragment of the Fitzharmon family crest." - id: 782 # Family crest examine: "The Fitzharmon family crest." - id: 783 # Bark sample examine: "A sample of the bark from the Grand Tree." - id: 784 # Translation book examine: "A book to translate the ancient gnome language into English." - id: 785 # Glough's journal examine: "Perhaps I should read it and see what Glough is up to!" - id: 786 # Hazelmere's scroll examine: "Hazelmere wrote something down on this scroll." - id: 787 # Lumber order examine: "An order from the Karamja shipyard." - id: 788 # Glough's key examine: "The key to Glough's chest." - id: 789 # Twigs examine: "Twigs bound together in the shape of a T." - id: 790 # Twigs examine: "Twigs bound together in the shape of a U." - id: 791 # Twigs examine: "Twigs bound together in the shape of a Z." - id: 792 # Twigs examine: "Twigs bound together in the shape of a O." - id: 793 # Daconia rock examine: "An ancient rock with strange magical properties." - id: 794 # Invasion plans examine: "These are plans for an invasion!" - id: 795 # War ship examine: "A model of a Karamja warship." - id: 800 # Bronze thrownaxe examine: "A finely balanced throwing axe." - id: 801 # Iron thrownaxe examine: "A finely balanced throwing axe." - id: 802 # Steel thrownaxe examine: "A finely balanced throwing axe." - id: 803 # Mithril thrownaxe examine: "A finely balanced throwing axe." - id: 805 # Rune thrownaxe examine: "A finely balanced throwing axe." - id: 806 # Bronze dart examine: "A deadly throwing dart with a bronze tip." - id: 807 # Iron dart examine: "A deadly throwing dart with an iron tip." - id: 808 # Steel dart examine: "A deadly throwing dart with a steel tip." - id: 809 # Mithril dart examine: "A deadly throwing dart with a mithril tip." - id: 810 # Adamant dart examine: "A deadly throwing dart with an adamant tip." - id: 811 # Rune dart examine: "A deadly throwing dart with a rune tip." - id: 812 # Bronze dart(p) examine: "A deadly poisoned dart with a bronze tip." - id: 813 # Iron dart(p) examine: "A deadly poisoned dart with an iron tip." - id: 814 # Steel dart(p) examine: "A deadly poisoned dart with a steel tip." - id: 815 # Mithril dart(p) examine: "A deadly poisoned dart with a mithril tip." - id: 816 # Adamant dart(p) examine: "A deadly poisoned dart with an adamant tip." - id: 817 # Rune dart(p) examine: "A deadly poisoned dart with a rune tip." - id: 819 # Bronze dart tip examine: "A deadly looking dart tip made of bronze - needs feathers for flight." - id: 820 # Iron dart tip examine: "A deadly looking dart tip made of iron - needs feathers for flight." - id: 821 # Steel dart tip examine: "A deadly-looking dart tip made of steel - needs feathers for flight." - id: 822 # Mithril dart tip examine: "A deadly looking dart tip made of mithril - needs feathers for flight." - id: 823 # Adamant dart tip examine: "A deadly-looking dart tip made of adamant - needs feathers for flight." - id: 824 # Rune dart tip examine: "A deadly-looking dart tip made of runite - needs feathers for flight." - id: 825 # Bronze javelin examine: "A bronze tipped javelin." - id: 826 # Iron javelin examine: "An iron tipped javelin." - id: 827 # Steel javelin examine: "A steel tipped javelin." - id: 828 # Mithril javelin examine: "A mithril tipped javelin." - id: 829 # Adamant javelin examine: "An adamant tipped javelin." - id: 830 # Rune javelin examine: "A rune tipped javelin." - id: 831 # Bronze javelin(p) examine: "A bronze tipped javelin." - id: 832 # Iron javelin(p) examine: "An iron tipped javelin." - id: 833 # Steel javelin(p) examine: "A steel tipped javelin." - id: 834 # Mithril javelin(p) examine: "A mithril tipped javelin." - id: 835 # Adamant javelin(p) examine: "An adamant tipped javelin." - id: 836 # Rune javelin(p) examine: "A rune tipped javelin." - id: 837 # Crossbow examine: "This fires crossbow bolts." - id: 839 # Longbow examine: "A nice sturdy bow." - id: 841 # Shortbow examine: "Short but effective." - id: 843 # Oak shortbow examine: "A shortbow made out of oak, still effective." - id: 845 # Oak longbow examine: "A nice sturdy bow made out of oak." - id: 847 # Willow longbow examine: "A nice sturdy bow made out of willow." - id: 849 # Willow shortbow examine: "A shortbow made out of willow, still effective." - id: 851 # Maple longbow examine: "A nice sturdy bow made out of Maple." - id: 853 # Maple shortbow examine: "A shortbow made out of Maple, still effective." - id: 855 # Yew longbow examine: "A nice sturdy bow made out of yew." - id: 857 # Yew shortbow examine: "A shortbow made out of yew, still effective." - id: 859 # Magic longbow examine: "A nice sturdy magical bow." - id: 861 # Magic shortbow examine: "Short and magical, but still effective." - id: 863 # Iron knife examine: "A finely balanced throwing knife." - id: 864 # Bronze knife examine: "A finely balanced throwing knife." - id: 865 # Steel knife examine: "A finely balanced throwing knife." - id: 866 # Mithril knife examine: "A finely balanced throwing knife." - id: 867 # Adamant knife examine: "A finely balanced throwing knife." - id: 868 # Rune knife examine: "A finely balanced throwing knife." - id: 869 # Black knife examine: "A finely balanced throwing knife." - id: 870 # Bronze knife(p) examine: "A finely balanced throwing knife." - id: 871 # Iron knife(p) examine: "A finely balanced throwing knife." - id: 872 # Steel knife(p) examine: "A finely balanced throwing knife." - id: 873 # Mithril knife(p) examine: "A finely balanced throwing knife." - id: 874 # Black knife(p) examine: "A finely balanced throwing knife." - id: 875 # Adamant knife(p) examine: "A finely balanced throwing knife." - id: 876 # Rune knife(p) examine: "A finely balanced throwing knife." - id: 877 # Bronze bolts examine: "Bronze crossbow bolts." - id: 879 # Opal bolts examine: "Opal tipped Bronze crossbow bolts." - id: 880 # Pearl bolts examine: "Pearl tipped Iron crossbow bolts." - id: 881 # Barbed bolts examine: "Great if you have a crossbow!" - id: 882 # Bronze arrow examine: "Arrows with bronze heads." - id: 883 # Bronze arrow(p) examine: "Venomous-looking arrows." - id: 884 # Iron arrow examine: "Arrows with iron heads." - id: 885 # Iron arrow(p) examine: "Venomous-looking arrows." - id: 886 # Steel arrow examine: "Arrows with steel heads." - id: 887 # Steel arrow(p) examine: "Venomous-looking arrows." - id: 888 # Mithril arrow examine: "Arrows with mithril heads." - id: 889 # Mithril arrow(p) examine: "Venomous-looking arrows." - id: 890 # Adamant arrow examine: "Arrows with adamant heads." - id: 891 # Adamant arrow(p) examine: "Venomous-looking arrows." - id: 892 # Rune arrow examine: "Arrows with rune heads." - id: 893 # Rune arrow(p) examine: "Venomous-looking arrows." - id: 946 # Knife examine: "A dangerous looking knife." - id: 948 # Bear fur examine: "This would make warm clothing." - id: 950 # Silk examine: "It's a sheet of silk." - id: 952 # Spade examine: "A slightly muddy spade." - id: 954 # Rope examine: "A coil of rope." - id: 956 # Flier examine: "Get your axes from Bob's Axes." - id: 958 # Grey wolf fur examine: "This would make warm clothing." - id: 960 # Plank examine: "A plank of wood!" - id: 962 # Christmas cracker examine: "I need to pull this." - id: 964 # Skull examine: "Ooooh spooky!" - id: 966 # Tile examine: "A fraction of a roof." - id: 968 # Rock examine: "A rock" - id: 970 # Papyrus examine: "Used for making notes." - id: 972 # Papyrus examine: "Used for making notes." - id: 973 # Charcoal examine: "A lump of charcoal." - id: 975 # Machete examine: "A jungle specific slashing device." - id: 981 # Disk of returning examine: "Used to get out of Thordur's blackhole." - id: 983 # Brass key examine: "Opens a door that leads into a dungeon." - id: 989 # Crystal key examine: "A mysterious key for a mysterious chest." - id: 991 # Muddy key examine: "It looks like the key to a chest." - id: 993 # Sinister key examine: "You get a sense of dread from this key." - id: 995 # Coins examine: "Lovely money!" - id: 1005 # White apron examine: "A mostly clean apron." - id: 1009 # Brass necklace examine: "I'd prefer a gold one." - id: 1011 # Blue skirt examine: "Leg covering favoured by women and wizards." - id: 1013 # Pink skirt examine: "A ladies skirt." - id: 1015 # Black skirt examine: "Clothing favoured by women and dark wizards." - id: 1017 # Wizard hat examine: "A silly pointed hat." - id: 1037 # Bunny ears examine: "A rabbit-like adornment." - id: 1038 # Red partyhat examine: "A nice hat from a cracker." - id: 1040 # Yellow partyhat examine: "A nice hat from a cracker." - id: 1042 # Blue partyhat examine: "A nice hat from a cracker." - id: 1044 # Green partyhat examine: "A nice hat from a cracker." - id: 1046 # Purple partyhat examine: "A nice hat from a cracker." - id: 1048 # White partyhat examine: "A nice hat from a cracker." - id: 1050 # Santa hat examine: "It's a Santa hat." - id: 1052 # Cape of legends examine: "The cape worn by members of the Legends Guild." - id: 1059 # Leather gloves examine: "These will keep my hands warm!" - id: 1061 # Leather boots examine: "Comfortable leather boots." - id: 1063 # Leather vambraces examine: "Better than no armour!" - id: 1067 # Iron platelegs examine: "These look pretty heavy." - id: 1069 # Steel platelegs examine: "These look pretty heavy." - id: 1071 # Mithril platelegs examine: "These look pretty heavy." - id: 1073 # Adamant platelegs examine: "These look pretty heavy." - id: 1075 # Bronze platelegs examine: "These look pretty heavy." - id: 1077 # Black platelegs examine: "Big, black and heavy looking." - id: 1079 # Rune platelegs examine: "These look pretty heavy." - id: 1081 # Iron plateskirt examine: "Designer leg protection." - id: 1083 # Steel plateskirt examine: "Designer leg protection." - id: 1085 # Mithril plateskirt examine: "Designer leg protection." - id: 1087 # Bronze plateskirt examine: "Designer leg protection." - id: 1089 # Black plateskirt examine: "Big, black and heavy looking." - id: 1091 # Adamant plateskirt examine: "Designer leg protection." - id: 1093 # Rune plateskirt examine: "Designer leg protection." - id: 1095 # Leather chaps examine: "Better than no armour!" - id: 1097 # Studded chaps examine: "Those studs should provide a bit more protection." - id: 1099 # Green d'hide chaps examine: "Made from 100% real dragonhide." - id: 1101 # Iron chainbody examine: "A series of connected metal rings." - id: 1103 # Bronze chainbody examine: "A series of connected metal rings." - id: 1105 # Steel chainbody examine: "A series of connected metal rings." - id: 1107 # Black chainbody examine: "A series of connected metal rings." - id: 1109 # Mithril chainbody examine: "A series of connected metal rings." - id: 1111 # Adamant chainbody examine: "A series of connected metal rings." - id: 1113 # Rune chainbody examine: "A series of connected metal rings." - id: 1115 # Iron platebody examine: "Provides excellent protection." - id: 1117 # Bronze platebody examine: "Provides excellent protection." - id: 1119 # Steel platebody examine: "Provides excellent protection." - id: 1121 # Mithril platebody examine: "Provides excellent protection." - id: 1123 # Adamant platebody examine: "Provides excellent protection." - id: 1125 # Black platebody examine: "Provides excellent protection." - id: 1127 # Rune platebody examine: "Provides excellent protection." - id: 1129 # Leather body examine: "Better than no armour!" - id: 1131 # Hardleather body examine: "Harder than normal leather." - id: 1133 # Studded body examine: "Those studs should provide a bit more protection." - id: 1135 # Green d'hide body examine: "Made from 100% real dragonhide." - id: 1137 # Iron med helm examine: "A medium sized helmet." - id: 1139 # Bronze med helm examine: "A medium sized helmet." - id: 1141 # Steel med helm examine: "A medium sized helmet." - id: 1143 # Mithril med helm examine: "A medium sized helmet." - id: 1145 # Adamant med helm examine: "A medium sized helmet." - id: 1147 # Rune med helm examine: "A medium sized helmet." - id: 1149 # Dragon med helm examine: "Makes the wearer pretty intimidating." - id: 1151 # Black med helm examine: "A medium sized helmet." - id: 1153 # Iron full helm examine: "A full face helmet." - id: 1155 # Bronze full helm examine: "A full face helmet." - id: 1157 # Steel full helm examine: "A full face helmet." - id: 1159 # Mithril full helm examine: "A full face helmet." - id: 1161 # Adamant full helm examine: "A full face helmet." - id: 1163 # Rune full helm examine: "A full face helmet." - id: 1165 # Black full helm examine: "A full face helmet." - id: 1167 # Leather cowl examine: "Better than no armour!" - id: 1169 # Coif examine: "Light weight head protection." - id: 1171 # Wooden shield examine: "A solid wooden shield." - id: 1173 # Bronze sq shield examine: "A medium square shield." - id: 1175 # Iron sq shield examine: "A medium square shield." - id: 1177 # Steel sq shield examine: "A medium square shield." - id: 1179 # Black sq shield examine: "A medium square shield." - id: 1181 # Mithril sq shield examine: "A medium square shield." - id: 1183 # Adamant sq shield examine: "A medium square shield." - id: 1185 # Rune sq shield examine: "A medium square shield." - id: 1187 # Dragon sq shield examine: "An ancient and powerful looking Dragon Square shield." - id: 1189 # Bronze kiteshield examine: "A large metal shield." - id: 1191 # Iron kiteshield examine: "A large metal shield." - id: 1193 # Steel kiteshield examine: "A large metal shield." - id: 1195 # Black kiteshield examine: "A large metal shield." - id: 1197 # Mithril kiteshield examine: "A large metal shield." - id: 1199 # Adamant kiteshield examine: "A large metal shield." - id: 1201 # Rune kiteshield examine: "A large metal shield." - id: 1203 # Iron dagger examine: "Short but pointy." - id: 1205 # Bronze dagger examine: "Short but pointy." - id: 1207 # Steel dagger examine: "Short but pointy." - id: 1209 # Mithril dagger examine: "A dangerous dagger." - id: 1211 # Adamant dagger examine: "Short and deadly." - id: 1213 # Rune dagger examine: "A powerful dagger." - id: 1215 # Dragon dagger examine: "A powerful dagger." - id: 1217 # Black dagger examine: "A vicious black dagger." - id: 1219 # Iron dagger(p) examine: "The blade is covered with poison." - id: 1221 # Bronze dagger(p) examine: "This dagger is poisoned." - id: 1223 # Steel dagger(p) examine: "The blade has been poisoned." - id: 1225 # Mithril dagger(p) examine: "A poisoned Mithril dagger." - id: 1227 # Adamant dagger(p) examine: "A very dangerous poisoned dagger." - id: 1229 # Rune dagger(p) examine: "The blade is covered with a nasty poison." - id: 1231 # Dragon dagger(p) examine: "A powerful dagger." - id: 1233 # Black dagger(p) examine: "This dagger is poisoned." - id: 1237 # Bronze spear examine: "A bronze tipped spear." - id: 1239 # Iron spear examine: "An iron tipped spear." - id: 1241 # Steel spear examine: "A steel tipped spear." - id: 1243 # Mithril spear examine: "A mithril tipped spear." - id: 1245 # Adamant spear examine: "An adamantite tipped spear." - id: 1247 # Rune spear examine: "A rune tipped spear." - id: 1249 # Dragon spear examine: "A dragon tipped spear." - id: 1251 # Bronze spear(p) examine: "A poisoned bronze tipped spear." - id: 1253 # Iron spear(p) examine: "A poisoned iron tipped spear." - id: 1255 # Steel spear(p) examine: "A poisoned steel tipped spear." - id: 1257 # Mithril spear(p) examine: "A poisoned mithril tipped spear." - id: 1259 # Adamant spear(p) examine: "A poisoned adamantite tipped spear." - id: 1261 # Rune spear(p) examine: "A poisoned rune tipped spear." - id: 1263 # Dragon spear(p) examine: "A poisoned dragon tipped spear." - id: 1265 # Bronze pickaxe examine: "Used for mining." - id: 1267 # Iron pickaxe examine: "Used for mining." - id: 1269 # Steel pickaxe examine: "Used for mining." - id: 1271 # Adamant pickaxe examine: "Used for mining." - id: 1273 # Mithril pickaxe examine: "Used for mining." - id: 1275 # Rune pickaxe examine: "Used for mining." - id: 1277 # Bronze sword examine: "A razor sharp sword." - id: 1279 # Iron sword examine: "A razor sharp sword." - id: 1281 # Steel sword examine: "A razor sharp sword." - id: 1283 # Black sword examine: "A razor sharp sword." - id: 1285 # Mithril sword examine: "A razor sharp sword." - id: 1287 # Adamant sword examine: "A razor sharp sword." - id: 1289 # Rune sword examine: "A razor sharp sword." - id: 1291 # Bronze longsword examine: "A razor sharp longsword." - id: 1293 # Iron longsword examine: "A razor sharp longsword." - id: 1295 # Steel longsword examine: "A razor sharp longsword." - id: 1297 # Black longsword examine: "A razor sharp longsword." - id: 1299 # Mithril longsword examine: "A razor sharp longsword." - id: 1301 # Adamant longsword examine: "A razor sharp longsword." - id: 1303 # Rune longsword examine: "A razor sharp longsword." - id: 1305 # Dragon longsword examine: "A very powerful sword." - id: 1307 # Bronze 2h sword examine: "A two handed sword." - id: 1309 # Iron 2h sword examine: "A two handed sword." - id: 1311 # Steel 2h sword examine: "A two handed sword." - id: 1313 # Black 2h sword examine: "A two handed sword." - id: 1315 # Mithril 2h sword examine: "A two handed sword." - id: 1317 # Adamant 2h sword examine: "A two handed sword." - id: 1319 # Rune 2h sword examine: "A two handed sword." - id: 1321 # Bronze scimitar examine: "A vicious, curved sword." - id: 1323 # Iron scimitar examine: "A vicious, curved sword." - id: 1325 # Steel scimitar examine: "A vicious, curved sword." - id: 1327 # Black scimitar examine: "A vicious, curved sword." - id: 1329 # Mithril scimitar examine: "A vicious, curved sword." - id: 1331 # Adamant scimitar examine: "A vicious, curved sword." - id: 1333 # Rune scimitar examine: "A vicious, curved sword." - id: 1335 # Iron warhammer examine: "I don't think it's intended for joinery." - id: 1337 # Bronze warhammer examine: "I don't think it's intended for joinery." - id: 1339 # Steel warhammer examine: "I don't think it's intended for joinery." - id: 1341 # Black warhammer examine: "I don't think it's intended for joinery." - id: 1343 # Mithril warhammer examine: "I don't think it's intended for joinery." - id: 1347 # Rune warhammer examine: "I don't think it's intended for joinery." - id: 1349 # Iron axe examine: "A woodcutter's axe." - id: 1351 # Bronze axe examine: "A woodcutter's axe." - id: 1353 # Steel axe examine: "A woodcutter's axe." - id: 1355 # Mithril axe examine: "A powerful axe." - id: 1357 # Adamant axe examine: "A powerful axe." - id: 1359 # Rune axe examine: "A powerful axe." - id: 1361 # Black axe examine: "A sinister looking axe." - id: 1363 # Iron battleaxe examine: "A vicious looking axe." - id: 1365 # Steel battleaxe examine: "A vicious looking axe." - id: 1367 # Black battleaxe examine: "A vicious looking axe." - id: 1369 # Mithril battleaxe examine: "A vicious looking axe." - id: 1371 # Adamant battleaxe examine: "A vicious looking axe." - id: 1373 # Rune battleaxe examine: "A vicious looking axe." - id: 1375 # Bronze battleaxe examine: "A vicious looking axe." - id: 1377 # Dragon battleaxe examine: "A vicious looking axe." - id: 1379 # Staff examine: "It's a slightly magical stick." - id: 1381 # Staff of air examine: "A Magical staff." - id: 1383 # Staff of water examine: "A Magical staff." - id: 1385 # Staff of earth examine: "A Magical staff." - id: 1387 # Staff of fire examine: "A Magical staff." - id: 1389 # Magic staff examine: "A Magical staff." - id: 1391 # Battlestaff examine: "It's a slightly magical stick." - id: 1393 # Fire battlestaff examine: "It's a slightly magical stick." - id: 1395 # Water battlestaff examine: "It's a slightly magical stick." - id: 1397 # Air battlestaff examine: "It's a slightly magical stick." - id: 1399 # Earth battlestaff examine: "It's a slightly magical stick." - id: 1401 # Mystic fire staff examine: "It's a slightly magical stick." - id: 1403 # Mystic water staff examine: "It's a slightly magical stick." - id: 1405 # Mystic air staff examine: "It's a slightly magical stick." - id: 1407 # Mystic earth staff examine: "It's a slightly magical stick." - id: 1409 # Iban's staff examine: "An ancient staff, formerly the property of Iban." - id: 1410 # Iban's staff examine: "I'll need to get this repaired before I can use it." - id: 1419 # Scythe examine: "It's a Scythe." - id: 1420 # Iron mace examine: "A spiky mace." - id: 1422 # Bronze mace examine: "A spiky mace." - id: 1424 # Steel mace examine: "A spiky mace." - id: 1426 # Black mace examine: "A spiky mace." - id: 1428 # Mithril mace examine: "A spiky mace." - id: 1430 # Adamant mace examine: "A spiky mace." - id: 1432 # Rune mace examine: "A spiky mace." - id: 1434 # Dragon mace examine: "A spiky mace." - id: 1436 # Rune essence examine: "An uncharged Rune Stone." - id: 1438 # Air talisman examine: "A mysterious power emanates from the talisman..." - id: 1440 # Earth talisman examine: "A mysterious power emanates from the talisman..." - id: 1442 # Fire talisman examine: "A mysterious power emanates from the talisman..." - id: 1444 # Water talisman examine: "A mysterious power emanates from the talisman..." - id: 1446 # Body talisman examine: "A mysterious power emanates from the talisman..." - id: 1448 # Mind talisman examine: "A mysterious power emanates from the talisman..." - id: 1452 # Chaos talisman examine: "A mysterious power emanates from the talisman..." - id: 1454 # Cosmic talisman examine: "A mysterious power emanates from the talisman..." - id: 1456 # Death talisman examine: "A mysterious power emanates from the talisman..." - id: 1458 # Law talisman examine: "A mysterious power emanates from the talisman..." - id: 1462 # Nature talisman examine: "A mysterious power emanates from the talisman..." - id: 1464 # Archery ticket examine: "I can exchange this for equipment." - id: 1465 # Weapon poison examine: "For use on daggers and projectiles." - id: 1466 # Sea slug examine: "A rather nasty looking crustacean." - id: 1467 # Damp sticks examine: "Some damp wooden sticks." - id: 1468 # Dry sticks examine: "Some dry wooden sticks." - id: 1469 # Broken glass examine: "Smashed glass." - id: 1470 # Red bead examine: "A small round red bead." - id: 1472 # Yellow bead examine: "A small round yellow bead." - id: 1474 # Black bead examine: "A small round black bead." - id: 1476 # White bead examine: "A small round white bead." - id: 1478 # Amulet of accuracy examine: "It increases my aim." - id: 1480 # Rock examine: "A chunk of rock." - id: 1481 # Orb of light examine: "A magical sphere that glimmers within." - id: 1482 # Orb of light examine: "A magical sphere that glimmers within." - id: 1483 # Orb of light examine: "A magical sphere that glimmers within." - id: 1484 # Orb of light examine: "A magical sphere that glimmers within." - id: 1486 # Piece of railing examine: "A broken piece of railing." - id: 1487 # Unicorn horn examine: "A withered unicorn horn." - id: 1488 # Paladin's badge examine: "A coat of arms of the Ardougne Paladins." - id: 1489 # Paladin's badge examine: "A coat of arms of the Ardougne Paladins." - id: 1490 # Paladin's badge examine: "A coat of arms of the Ardougne Paladins." - id: 1491 # Witch's cat examine: "A cat." - id: 1492 # Doll of iban examine: "A simple doll with Iban's likeness." - id: 1493 # Old journal examine: "An account of the last times of someone." - id: 1494 # History of iban examine: "The tale of Iban." - id: 1495 # Klank's gauntlets examine: "Strong dwarvish gloves." - id: 1496 # Iban's dove examine: "I thought you only saw these in pairs?" - id: 1497 # Amulet of othanian examine: "A mystical demonic amulet." - id: 1498 # Amulet of doomion examine: "A mystical demonic amulet." - id: 1499 # Amulet of holthion examine: "A mystical demonic amulet." - id: 1500 # Iban's shadow examine: "A strange dark liquid." - id: 1501 # Dwarf brew examine: "Smells stronger than most spirits." - id: 1502 # Iban's ashes examine: "The burnt remains of Iban." - id: 1503 # Warrant examine: "A search warrant for a house in Ardougne." - id: 1504 # Hangover cure examine: "It doesn't look very tasty." - id: 1506 # Gas mask examine: "Stops me from breathing nasty stuff!" - id: 1507 # A small key examine: "Quite a small key." - id: 1508 # A scruffy note examine: "It seems to say \"hongorer lure\"..." - id: 1509 # Book examine: "Turnip growing for beginners." - id: 1510 # Picture examine: "A picture of a lady called Elena." - id: 1511 # Logs examine: "A number of wooden logs." - id: 1513 # Magic logs examine: "Logs cut from a magic tree." - id: 1515 # Yew logs examine: "Logs cut from a yew tree." - id: 1517 # Maple logs examine: "Logs cut from a maple tree." - id: 1519 # Willow logs examine: "Logs cut from a willow tree." - id: 1521 # Oak logs examine: "Logs cut from an oak tree." - id: 1523 # Lockpick examine: "For picking tough locks." - id: 1526 # Snake weed examine: "This herb is Snake Weed." - id: 1528 # Ardrigal examine: "This herb is Ardrigal." - id: 1530 # Sito foil examine: "This herb is Sito Foil." - id: 1532 # Volencia moss examine: "This herb is Volencia Moss." - id: 1534 # Rogue's purse examine: "This herb is Rogue's Purse." - id: 1535 # Map part examine: "A piece of the map to Crandor." - id: 1536 # Map part examine: "A piece of the map to Crandor." - id: 1537 # Map part examine: "A piece of the map to Crandor." - id: 1538 # Crandor map examine: "A map of the route to Crandor." - id: 1539 # Steel nails examine: "Keeps things in place fairly permanently." - id: 1540 # Anti-dragon shield examine: "This provides partial protection from dragonbreath attacks." - id: 1542 # Maze key examine: "A key to Melzar's Maze." - id: 1543 # Key examine: "A red key from Melzar's Maze." - id: 1544 # Key examine: "An orange key from Melzar's Maze." - id: 1545 # Key examine: "A yellow key from Melzar's Maze." - id: 1546 # Key examine: "A blue key from Melzar's Maze." - id: 1547 # Key examine: "A magenta key from Melzar's Maze." - id: 1548 # Key examine: "A green key from Melzar's Maze." - id: 1549 # Stake examine: "A very pointy stick." - id: 1550 # Garlic examine: "A clove of garlic." - id: 1552 # Seasoned sardine examine: "Sardine flavoured with doogle leaves." - id: 1554 # Fluffs' kitten examine: "It looks like it's lost." - id: 1555 # Pet kitten examine: "This kitten seems to like you." - id: 1556 # Pet kitten examine: "This kitten seems to like you." - id: 1557 # Pet kitten examine: "This kitten seems to like you." - id: 1558 # Pet kitten examine: "This kitten seems to like you." - id: 1559 # Pet kitten examine: "This kitten seems to like you." - id: 1560 # Pet kitten examine: "This kitten seems to like you." - id: 1561 # Pet cat examine: "This cat definitely likes you." - id: 1562 # Pet cat examine: "This cat definitely likes you." - id: 1563 # Pet cat examine: "This cat definitely likes you." - id: 1564 # Pet cat examine: "This cat definitely likes you." - id: 1565 # Pet cat examine: "This cat definitely likes you." - id: 1566 # Pet cat examine: "This cat definitely likes you." - id: 1567 # Pet cat examine: "This cat is so well fed it can hardly move." - id: 1568 # Pet cat examine: "This cat is so well fed it can hardly move." - id: 1569 # Pet cat examine: "This cat is so well fed it can hardly move." - id: 1570 # Pet cat examine: "This cat is so well fed it can hardly move." - id: 1571 # Pet cat examine: "This cat is so well fed it can hardly move." - id: 1572 # Pet cat examine: "This cat is so well fed it can hardly move." - id: 1573 # Doogle leaves examine: "A tasty herb, good for seasoning." - id: 1575 # Cat training medal examine: "For feline training expertise." - id: 1577 # Pete's candlestick examine: "Scarface Pete's Candlestick." - id: 1579 # Thieves' armband examine: "This denotes a Master Thief." - id: 1580 # Ice gloves examine: "These will keep my hands cold!" - id: 1581 # Blamish snail slime examine: "Yuck." - id: 1582 # Blamish oil examine: "Made from the finest snail slime." - id: 1583 # Fire feather examine: "Firebird feather." - id: 1584 # Id papers examine: "Apparently my name is Hartigen." - id: 1585 # Oily fishing rod examine: "Useful for catching lava eels." - id: 1586 # Miscellaneous key examine: "I wonder what this unlocks?." - id: 1590 # Dusty key examine: "Never let your home get like this." - id: 1591 # Jail key examine: "Key to a cell." - id: 1592 # Ring mould examine: "Used to make gold rings." - id: 1594 # Unholy mould examine: "Used to make unholy symbols." - id: 1595 # Amulet mould examine: "Used to make gold amulets." - id: 1597 # Necklace mould examine: "Used to make gold necklaces." - id: 1599 # Holy mould examine: "Used to make holy symbols of Saradomin." - id: 1601 # Diamond examine: "This looks valuable." - id: 1603 # Ruby examine: "This looks valuable." - id: 1605 # Emerald examine: "This looks valuable." - id: 1607 # Sapphire examine: "This looks valuable." - id: 1609 # Opal examine: "A semi precious stone." - id: 1611 # Jade examine: "A semi precious stone." - id: 1613 # Red topaz examine: "A semi precious stone." - id: 1615 # Dragonstone examine: "This looks valuable." - id: 1617 # Uncut diamond examine: "This would be worth more cut." - id: 1619 # Uncut ruby examine: "This would be worth more cut." - id: 1621 # Uncut emerald examine: "This would be worth more cut." - id: 1623 # Uncut sapphire examine: "This would be worth more cut." - id: 1625 # Uncut opal examine: "This would be worth more cut." - id: 1627 # Uncut jade examine: "This would be worth more cut." - id: 1629 # Uncut red topaz examine: "This would be worth more cut." - id: 1631 # Uncut dragonstone examine: "This would be worth more cut." - id: 1635 # Gold ring examine: "A valuable ring." - id: 1637 # Sapphire ring examine: "A valuable ring." - id: 1639 # Emerald ring examine: "A valuable ring." - id: 1641 # Ruby ring examine: "A valuable ring." - id: 1643 # Diamond ring examine: "A valuable ring." - id: 1645 # Dragonstone ring examine: "A valuable ring." - id: 1654 # Gold necklace examine: "I wonder if this is valuable." - id: 1656 # Sapphire necklace examine: "I wonder if this is valuable." - id: 1658 # Emerald necklace examine: "I wonder if this is valuable." - id: 1660 # Ruby necklace examine: "I wonder if this is valuable." - id: 1662 # Diamond necklace examine: "I wonder if this is valuable." - id: 1664 # Dragon necklace examine: "I wonder if this is valuable." - id: 1692 # Gold amulet examine: "A plain gold amulet." - id: 1694 # Sapphire amulet examine: "I wonder if I can get this enchanted." - id: 1696 # Emerald amulet examine: "I wonder if I can get this enchanted." - id: 1698 # Ruby amulet examine: "I wonder if I can get this enchanted." - id: 1700 # Diamond amulet examine: "I wonder if I can get this enchanted." - id: 1704 # Amulet of glory examine: "A very powerful dragonstone amulet." - id: 1706 # Amulet of glory(1) examine: "A dragonstone amulet with 1 magic charge." - id: 1708 # Amulet of glory(2) examine: "A dragonstone amulet with 2 magic charges." - id: 1710 # Amulet of glory(3) examine: "A dragonstone amulet with 3 magic charges." - id: 1712 # Amulet of glory(4) examine: "A dragonstone amulet with 4 magic charges." - id: 1714 # Unstrung symbol examine: "It needs a string so I can wear it." - id: 1716 # Unblessed symbol examine: "A symbol of Saradomin." - id: 1718 # Holy symbol examine: "A blessed holy symbol of Saradomin." - id: 1720 # Unstrung emblem examine: "It needs a string so I can wear it." - id: 1722 # Unpowered symbol examine: "An unblessed symbol of Zamorak." - id: 1724 # Unholy symbol examine: "An unholy symbol of Zamorak." - id: 1725 # Amulet of strength examine: "An enchanted ruby amulet." - id: 1727 # Amulet of magic examine: "An enchanted sapphire amulet of magic." - id: 1729 # Amulet of defence examine: "An enchanted emerald amulet of protection." - id: 1731 # Amulet of power examine: "An enchanted diamond amulet of power." - id: 1733 # Needle examine: "Used with a thread to make clothes." - id: 1734 # Thread examine: "Used with a needle to make clothes." - id: 1735 # Shears examine: "For shearing sheep." - id: 1737 # Wool examine: "I think this came from a sheep." - id: 1739 # Cowhide examine: "I should take this to the tannery." - id: 1741 # Leather examine: "It's a piece of leather." - id: 1743 # Hard leather examine: "It's a piece of hard leather." - id: 1747 # Black dragonhide examine: "The scaly rough hide from a Black Dragon." - id: 1749 # Red dragonhide examine: "The scaly rough hide from a Red Dragon." - id: 1751 # Blue dragonhide examine: "The scaly rough hide from a Blue Dragon." - id: 1753 # Green dragonhide examine: "The scaly rough hide from a Green Dragon." - id: 1755 # Chisel examine: "Good for detailed Crafting." - id: 1757 # Brown apron examine: "A mostly clean apron." - id: 1759 # Ball of wool examine: "Spun from sheeps' wool." - id: 1761 # Soft clay examine: "Clay soft enough to mould." - id: 1763 # Red dye examine: "A little bottle of red dye." - id: 1765 # Yellow dye examine: "A little bottle of yellow dye." - id: 1767 # Blue dye examine: "A little bottle of blue dye." - id: 1769 # Orange dye examine: "A little bottle of orange dye." - id: 1771 # Green dye examine: "A little bottle of green dye." - id: 1773 # Purple dye examine: "A little bottle of purple dye." - id: 1775 # Molten glass examine: "Hot glass ready to be blown into useful objects." - id: 1777 # Bow string examine: "I need a bow stave to attach this to." - id: 1779 # Flax examine: "I should use this with a spinning wheel." - id: 1781 # Soda ash examine: "One of the ingredients for making glass." - id: 1783 # Bucket of sand examine: "One of the ingredients for making glass." - id: 1785 # Glassblowing pipe examine: "Used to form molten glass into useful items." - id: 1787 # Unfired pot examine: "I need to put this in a pottery oven." - id: 1789 # Unfired pie dish examine: "I need to put this in a pottery oven." - id: 1791 # Unfired bowl examine: "I need to put this in a pottery oven." - id: 1793 # Woad leaf examine: "A slightly bluish leaf." - id: 1794 # Bronze wire examine: "Useful for Crafting items." - id: 1796 # Silver necklace examine: "Anna's shiny silver coated necklace." - id: 1797 # Silver necklace examine: "Anna's shiny silver coated necklace coated with a thin layer of flour." - id: 1798 # Silver cup examine: "Bob's shiny silver coated tea cup." - id: 1799 # Silver cup examine: "Bob's shiny silver coated tea cup coated with a thin layer of flour." - id: 1800 # Silver bottle examine: "Carol's shiny silver coated bottle." - id: 1801 # Silver bottle examine: "Carol's shiny silver coated bottle coated with a thin layer of flour." - id: 1802 # Silver book examine: "David's shiny silver coated book." - id: 1803 # Silver book examine: "David's shiny silver coated book coated with a thin layer of flour." - id: 1804 # Silver needle examine: "Elizabeth's shiny silver coated needle." - id: 1805 # Silver needle examine: "Elizabeth's shiny silver coated needle coated with a thin layer of flour." - id: 1806 # Silver pot examine: "Frank's shiny silver coated pot." - id: 1807 # Silver pot examine: "Frank's shiny silver coated pot coated with a thin layer of flour." - id: 1808 # Criminal's thread examine: "Some red thread found at the murder scene." - id: 1809 # Criminal's thread examine: "Some green thread found at the murder scene." - id: 1810 # Criminal's thread examine: "Some blue thread found at the murder scene." - id: 1811 # Flypaper examine: "A piece of fly paper. It's sticky." - id: 1812 # Pungent pot examine: "A pot found at the murder scene, with a sickly odour." - id: 1813 # Criminal's dagger examine: "A flimsy-looking dagger found at the crime scene." - id: 1814 # Criminal's dagger examine: "A flimsy-looking dagger found at the crime scene coated with a thin layer of flour." - id: 1815 # Killer's print examine: "The fingerprints of the murderer." - id: 1816 # Anna's print examine: "An imprint of Anna's fingerprint." - id: 1817 # Bob's print examine: "An imprint of Bob's fingerprint." - id: 1818 # Carol's print examine: "An imprint of Carol's fingerprint." - id: 1819 # David's print examine: "An imprint of David's fingerprint." - id: 1820 # Elizabeth's print examine: "An imprint of Elizabeth's fingerprint." - id: 1821 # Frank's print examine: "An imprint of Frank's fingerprint." - id: 1822 # Unknown print examine: "An unidentified fingerprint taken from the murder weapon." - id: 1823 # Waterskin(4) examine: "A full waterskin with four portions of water." - id: 1825 # Waterskin(3) examine: "A nearly full waterskin with three portions of water." - id: 1827 # Waterskin(2) examine: "A half empty waterskin with two portions of water." - id: 1829 # Waterskin(1) examine: "A nearly empty waterskin with one portion of water." - id: 1831 # Waterskin(0) examine: "A completely empty waterskin - you'll need to fill it up." - id: 1833 # Desert shirt examine: "A cool, light desert shirt." - id: 1835 # Desert robe examine: "A cool, light desert robe." - id: 1837 # Desert boots examine: "Comfortable desert shoes." - id: 1839 # Metal key examine: "This key is crudely made. It came from the mining camp Mercenary Captain." - id: 1840 # Cell door key examine: "A metallic key, usually used by prison guards." - id: 1841 # Barrel examine: "An empty mining barrel." - id: 1842 # Ana in a barrel examine: "A mining barrel with Ana in it." - id: 1843 # Wrought iron key examine: "This key unlocks a very sturdy gate of some sort. Ana gave me this key." - id: 1844 # Slave shirt examine: "A filthy, smelly, flea infested shirt." - id: 1845 # Slave robe examine: "A filthy, smelly, flea infested robe." - id: 1846 # Slave boots examine: "A set of filthy, smelly, flea infested desert slave boots." - id: 1847 # Scrumpled paper examine: "A piece of paper with barely legible writing - looks like a recipe!" - id: 1849 # Prototype dart examine: "A prototype throwing dart." - id: 1850 # Technical plans examine: "Plans of a technical nature." - id: 1851 # Tenti pineapple examine: "The most delicious of pineapples." - id: 1852 # Bedabin key examine: "A key to the chest in Captain Siad's room." - id: 1853 # Prototype dart tip examine: "A prototype dart tip - it looks deadly." - id: 1854 # Shantay pass examine: "Allows you to pass through the Shantay pass into the Kharid Desert." - id: 1855 # Rock examine: "Looks like a plain rock, must have some ore in it?" - id: 1856 # Guide book examine: "A Tourist's Guide To Ardougne." - id: 1857 # Totem examine: "The Rantuki tribe's totem." - id: 1858 # Address label examine: "It says 'To Lord Handelmort, Handelmort Mansion'." - id: 1859 # Raw ugthanki meat examine: "I need to cook this first." - id: 1861 # Ugthanki meat examine: "Freshly cooked ugthanki meat." - id: 1863 # Pitta dough examine: "I need to cook this." - id: 1865 # Pitta bread examine: "Nicely baked pitta bread. Needs more ingredients to make a kebab." - id: 1869 # Chopped tomato examine: "A mixture of tomatoes in a bowl." - id: 1871 # Chopped onion examine: "A mixture of onions in a bowl." - id: 1873 # Chopped ugthanki examine: "Strips of ugthanki meat in a bowl." - id: 1875 # Onion & tomato examine: "A mixture of chopped onions and tomatoes in a bowl." - id: 1877 # Ugthanki & onion examine: "A mixture of chopped onions and ugthanki meat in a bowl." - id: 1879 # Ugthanki & tomato examine: "A mixture of chopped tomatoes and ugthanki meat in a bowl." - id: 1881 # Kebab mix examine: "A mixture of chopped tomatoes, onions and ugthanki meat in a bowl." - id: 1883 # Ugthanki kebab examine: "A strange smelling kebab made from ugthanki meat." - id: 1885 # Ugthanki kebab examine: "A fresh kebab made from ugthanki meat." - id: 1887 # Cake tin examine: "Useful for baking cakes." - id: 1889 # Uncooked cake examine: "Now all I need to do is cook it." - id: 1891 # Cake examine: "A plain sponge cake." - id: 1893 # 2/3 cake examine: "Someone has eaten a big chunk of this cake." - id: 1895 # Slice of cake examine: "I'd rather have a whole cake." - id: 1897 # Chocolate cake examine: "This looks very tasty." - id: 1899 # 2/3 chocolate cake examine: "Someone has eaten a big chunk of this cake." - id: 1901 # Chocolate slice examine: "I'd rather have a whole cake." - id: 1903 # Burnt cake examine: "Argh what a mess!" - id: 1905 # Asgarnian ale examine: "Probably the finest ale in Asgarnia." - id: 1907 # Wizard's mind bomb examine: "It's got strange bubbles in it." - id: 1909 # Greenman's ale examine: "A glass of frothy ale." - id: 1911 # Dragon bitter examine: "A glass of bitter." - id: 1913 # Dwarven stout examine: "A pint of thick dark beer." - id: 1915 # Grog examine: "A murky glass of some sort of drink." - id: 1917 # Beer examine: "A glass of frothy ale." - id: 1919 # Beer glass examine: "I need to fill this with beer." - id: 1921 # Bowl of water examine: "It's a bowl of water." - id: 1923 # Bowl examine: "Useful for mixing things." - id: 1925 # Bucket examine: "It's a wooden bucket." - id: 1927 # Bucket of milk examine: "It's a bucket of milk." - id: 1929 # Bucket of water examine: "It's a bucket of water." - id: 1931 # Pot examine: "This pot is empty." - id: 1933 # Pot of flour examine: "There is flour in this pot." - id: 1935 # Jug examine: "This jug is empty." - id: 1937 # Jug of water examine: "It's full of water." - id: 1939 # Swamp tar examine: "A foul smelling thick tar-like substance." - id: 1940 # Raw swamp paste examine: "A thick tar-like substance mixed with flour." - id: 1941 # Swamp paste examine: "A tar-like substance mixed with flour and warmed." - id: 1942 # Potato examine: "This could be used to make a good stew." - id: 1944 # Egg examine: "A nice fresh egg." - id: 1947 # Grain examine: "Some wheat heads." - id: 1949 # Chef's hat examine: "What a silly hat." - id: 1951 # Redberries examine: "Very bright red berries." - id: 1953 # Pastry dough examine: "Potentially pastry." - id: 1955 # Cooking apple examine: "Keeps the doctor away." - id: 1957 # Onion examine: "A strong smelling onion." - id: 1959 # Pumpkin examine: "Happy Halloween." - id: 1961 # Easter egg examine: "Happy Easter." - id: 1963 # Banana examine: "Mmm this looks tasty." - id: 1965 # Cabbage examine: "Yuck I don't like cabbage." - id: 1967 # Cabbage examine: "Yuck I don't like cabbage." - id: 1969 # Spinach roll examine: "A home made spinach thing." - id: 1971 # Kebab examine: "A meaty kebab." - id: 1973 # Chocolate bar examine: "Mmmmmmm chocolate." - id: 1975 # Chocolate dust examine: "It's ground up chocolate." - id: 1977 # Chocolatey milk examine: "Milk with chocolate in it." - id: 1978 # Cup of tea examine: "A nice cup of tea." - id: 1980 # Empty cup examine: "An empty cup." - id: 1982 # Tomato examine: "This would make good ketchup." - id: 1984 # Rotten apple examine: "Rotten to the core!" - id: 1985 # Cheese examine: "It's got holes in it." - id: 1987 # Grapes examine: "Good grapes for wine making." - id: 1989 # Half full wine jug examine: "An optimist would say it's half full." - id: 1991 # Jug of bad wine examine: "Oh dear, this wine is terrible!" - id: 1993 # Jug of wine examine: "It's full of wine." - id: 1995 # Unfermented wine examine: "This wine needs to ferment before it can be drunk." - id: 1997 # Incomplete stew examine: "I need to add some meat too." - id: 1999 # Incomplete stew examine: "I need to add some potato too." - id: 2001 # Uncooked stew examine: "I need to cook this." - id: 2003 # Stew examine: "It's a meat and potato stew." - id: 2005 # Burnt stew examine: "Eew, it's horribly burnt." - id: 2007 # Spice examine: "This could liven up an otherwise bland stew." - id: 2009 # Uncooked curry examine: "I need to cook this." - id: 2011 # Curry examine: "It's a spicy hot curry." - id: 2013 # Burnt curry examine: "Eew, it's horribly burnt." - id: 2015 # Vodka examine: "An absolutely clear spirit." - id: 2017 # Whisky examine: "A bottle of Draynor Malt." - id: 2019 # Gin examine: "A strong spirit that tastes of Juniper." - id: 2021 # Brandy examine: "A strong spirit best served in a large glass." - id: 2023 # Cocktail guide examine: "A book on tree gnome cocktails." - id: 2025 # Cocktail shaker examine: "Used for mixing cocktails." - id: 2026 # Cocktail glass examine: "For sipping cocktails." - id: 2030 # Premade choc s'dy examine: "A premade Chocolate Saturday." - id: 2032 # Premade dr' dragon examine: "A premade Drunk Dragon." - id: 2034 # Premade fr' blast examine: "A premade Fruit Blast." - id: 2036 # Premade p' punch examine: "A premade Pineapple punch." - id: 2038 # Premade sgg examine: "A premade Short Green Guy." - id: 2040 # Premade wiz blz'd examine: "A Premade Wizard Blizzard." - id: 2048 # Pineapple punch examine: "A fresh healthy fruit mix." - id: 2054 # Wizard blizzard examine: "This looks like a strange mix." - id: 2064 # Blurberry special examine: "Looks good... smells strong." - id: 2074 # Choc saturday examine: "A warm creamy alcoholic beverage." - id: 2080 # Short green guy examine: "A Short Green Guy... looks good." - id: 2084 # Fruit blast examine: "A cool refreshing fruit mix." - id: 2092 # Drunk dragon examine: "A warm creamy alcoholic beverage." - id: 2102 # Lemon examine: "A fresh lemon." - id: 2104 # Lemon chunks examine: "Fresh chunks of lemon." - id: 2106 # Lemon slices examine: "Fresh lemon slices." - id: 2108 # Orange examine: "A fresh orange." - id: 2110 # Orange chunks examine: "Fresh chunks of orange." - id: 2112 # Orange slices examine: "Fresh orange slices." - id: 2114 # Pineapple examine: "It can be cut up into something more manageable with a knife." - id: 2116 # Pineapple chunks examine: "Fresh chunks of pineapple." - id: 2118 # Pineapple ring examine: "Exotic fruit." - id: 2120 # Lime examine: "A fresh lime." - id: 2122 # Lime chunks examine: "Fresh chunks of lime." - id: 2124 # Lime slices examine: "Fresh lime slices." - id: 2126 # Dwellberries examine: "Some rather pretty blue berries." - id: 2128 # Equa leaves examine: "Small sweet smelling leaves." - id: 2130 # Pot of cream examine: "Fresh cream." - id: 2132 # Raw beef examine: "I need to cook this first." - id: 2134 # Raw rat meat examine: "I need to cook this first." - id: 2136 # Raw bear meat examine: "I need to cook this first." - id: 2138 # Raw chicken examine: "I need to cook this first." - id: 2140 # Cooked chicken examine: "Mmm this looks tasty." - id: 2142 # Cooked meat examine: "Mmm this looks tasty." - id: 2144 # Burnt chicken examine: "Oh dear, it's totally burnt!" - id: 2146 # Burnt meat examine: "Oh dear, it's totally burnt!" - id: 2148 # Raw lava eel examine: "A very strange eel." - id: 2149 # Lava eel examine: "Strange, it looks cooler now it's been cooked." - id: 2150 # Swamp toad examine: "A slippery little blighter." - id: 2152 # Toad's legs examine: "They're a gnome delicacy apparently." - id: 2162 # King worm examine: "They're a gnome delicacy apparently." - id: 2164 # Batta tin examine: "A deep tin used for baking gnome battas in." - id: 2165 # Crunchy tray examine: "A shallow tray used for baking crunchies in." - id: 2166 # Gnomebowl mould examine: "A large ovenproof bowl." - id: 2167 # Gianne's cook book examine: "Aluft Gianne's favorite dishes." - id: 2169 # Gnome spice examine: "It's Aluft Gianne's secret mix of spices." - id: 2171 # Gianne dough examine: "It's made from a secret recipe." - id: 2175 # Burnt gnomebowl examine: "This gnome bowl has been burnt to a cinder." - id: 2177 # Half baked bowl examine: "This gnome bowl is in the early stages of preparation." - id: 2178 # Raw gnomebowl examine: "This gnome bowl needs cooking." - id: 2185 # Chocolate bomb examine: "Full of creamy, chocolately goodness." - id: 2187 # Tangled toad's legs examine: "It actually smells quite good." - id: 2191 # Worm hole examine: "It actually smells quite good." - id: 2195 # Veg ball examine: "This looks pretty healthy." - id: 2199 # Burnt crunchies examine: "These crunchies have been burnt to a cinder." - id: 2201 # Half baked crunchy examine: "This crunchy is in the early stages of preparation." - id: 2202 # Raw crunchies examine: "These crunchies need cooking." - id: 2205 # Worm crunchies examine: "It actually smells quite good." - id: 2209 # Chocchip crunchies examine: "Yum... smells good." - id: 2213 # Spicy crunchies examine: "Yum... smells spicy." - id: 2217 # Toad crunchies examine: "It actually smells quite good." - id: 2219 # Premade w'm batta examine: "A premade Worm Batta." - id: 2221 # Premade t'd batta examine: "A Premade Toad Batta." - id: 2223 # Premade c+t batta examine: "A Premade Cheese and Tomato Batta." - id: 2225 # Premade fr't batta examine: "A premade Fruit Batta." - id: 2227 # Premade veg batta examine: "A Premade Vegetable Batta." - id: 2229 # Premade choc bomb examine: "A premade Chocolate Bomb." - id: 2231 # Premade ttl examine: "A premade Tangled Toads Legs." - id: 2233 # Premade worm hole examine: "A premade Worm Hole." - id: 2235 # Premade veg ball examine: "A premade Vegetable Ball." - id: 2237 # Premade w'm crun' examine: "Some Premade Worm Crunchies." - id: 2239 # Premade ch' crunch examine: "Some Premade chocchip crunchies." - id: 2241 # Premade s'y crunch examine: "Some premade Spicy Crunchies." - id: 2243 # Premade t'd crunch examine: "Some premade Toad Crunchies." - id: 2247 # Burnt batta examine: "This batta has been burnt to a cinder." - id: 2249 # Half baked batta examine: "This gnome batta is in the early stages of preparation." - id: 2250 # Raw batta examine: "This gnome batta needs cooking." - id: 2251 # Unfinished batta examine: "This batta is just missing those little finishing touches." - id: 2253 # Worm batta examine: "It actually smells quite good." - id: 2255 # Toad batta examine: "It actually smells quite good." - id: 2257 # Unfinished batta examine: "This batta is just missing those little finishing touches." - id: 2259 # Cheese+tom batta examine: "This smells really good." - id: 2277 # Fruit batta examine: "It actually smells quite good." - id: 2279 # Unfinished batta examine: "This batta is just missing those little finishing touches." - id: 2281 # Vegetable batta examine: "Well... it looks healthy." - id: 2283 # Pizza base examine: "I need to add some tomato next." - id: 2285 # Incomplete pizza examine: "I need to add some cheese next." - id: 2287 # Uncooked pizza examine: "This needs cooking." - id: 2289 # Plain pizza examine: "A cheese and tomato pizza." - id: 2291 # 1/2 plain pizza examine: "Half of this plain pizza has been eaten." - id: 2293 # Meat pizza examine: "A pizza with bits of meat on it." - id: 2295 # 1/2 meat pizza examine: "Half of this meat pizza has been eaten." - id: 2297 # Anchovy pizza examine: "A pizza with anchovies." - id: 2299 # 1/2 anchovy pizza examine: "Half of this anchovy pizza has been eaten." - id: 2301 # Pineapple pizza examine: "A tropicana pizza." - id: 2305 # Burnt pizza examine: "Oh dear!" - id: 2307 # Bread dough examine: "Some uncooked dough." - id: 2309 # Bread examine: "Nice crispy bread." - id: 2311 # Burnt bread examine: "Nice crispy bread. Possibly too crispy." - id: 2313 # Pie dish examine: "Deceptively pie shaped." - id: 2315 # Pie shell examine: "I need to find a filling for this pie." - id: 2317 # Uncooked apple pie examine: "This would be much tastier cooked." - id: 2319 # Uncooked meat pie examine: "This would be much healthier cooked." - id: 2321 # Uncooked berry pie examine: "This would be much more appetising cooked." - id: 2323 # Apple pie examine: "Mmm Apple pie." - id: 2325 # Redberry pie examine: "Looks tasty." - id: 2327 # Meat pie examine: "Not for vegetarians." - id: 2329 # Burnt pie examine: "I think I left it on the stove too long." - id: 2331 # Half a meat pie examine: "Half of it is suitable for vegetarians." - id: 2333 # Half a redberry pie examine: "So tasty I kept some for later." - id: 2335 # Half an apple pie examine: "Mmm half an apple pie." - id: 2337 # Raw oomlie examine: "Raw meat from the oomlie bird." - id: 2339 # Palm leaf examine: "A thick green palm leaf used by natives to cook meat." - id: 2341 # Wrapped oomlie examine: "Oomlie meat in a palm leaf pouch. It just needs to be cooked." - id: 2343 # Cooked oomlie wrap examine: "Deliciously cooked oomlie meat in a palm leaf pouch." - id: 2347 # Hammer examine: "Good for hitting things!" - id: 2349 # Bronze bar examine: "It's a bar of bronze." - id: 2351 # Iron bar examine: "It's a bar of iron." - id: 2353 # Steel bar examine: "It's a bar of steel." - id: 2355 # Silver bar examine: "It's a bar of silver." - id: 2357 # Gold bar examine: "It's a bar of gold." - id: 2359 # Mithril bar examine: "It's a bar of mithril." - id: 2361 # Adamantite bar examine: "It's a bar of adamantite." - id: 2363 # Runite bar examine: "It's a bar of runite." - id: 2365 # 'perfect' gold bar examine: "It's a bar of 'perfect' gold." - id: 2366 # Shield left half examine: "The left half of a dragon square shield." - id: 2368 # Shield right half examine: "The right half of a dragon square shield." - id: 2370 # Steel studs examine: "A set of studs for leather armour." - id: 2372 # Ogre relic examine: "An old statue of an ogre warrior." - id: 2373 # Relic part 1 examine: "Part of an ogre relic." - id: 2374 # Relic part 2 examine: "Part of an ogre relic." - id: 2375 # Relic part 3 examine: "Part of an ogre relic." - id: 2376 # Skavid map examine: "It's a map." - id: 2377 # Ogre tooth examine: "Very tooth-like." - id: 2378 # Toban's key examine: "Formerly the property of the ogre, Toban." - id: 2379 # Rock cake examine: "Handy if you want to break all your teeth." - id: 2380 # Crystal examine: "A yellow crystal that's meant to power the Watchtower in Yanille." - id: 2381 # Crystal examine: "A magenta crystal that's meant to power the Watchtower in Yanille." - id: 2382 # Crystal examine: "A cyan crystal that's meant to power the Watchtower in Yanille." - id: 2383 # Crystal examine: "A grey crystal that's meant to power the Watchtower in Yanille." - id: 2385 # Old robe examine: "I can't wear this old thing." - id: 2386 # Unusual armour examine: "Looks kind of useless." - id: 2387 # Damaged dagger examine: "Pointy." - id: 2388 # Tattered eye patch examine: "Useless as an eye patch." - id: 2389 # Vial examine: "An infusion of water and jangerberries." - id: 2390 # Vial examine: "A mixture of jangerberries and a guam leaf in a vial." - id: 2391 # Ground bat bones examine: "Let's see it fly, now!" - id: 2394 # Potion examine: "A strange brew." - id: 2395 # Magic ogre potion examine: "A dangerous magical liquid." - id: 2396 # Spell scroll examine: "A spell is written on this parchment." - id: 2397 # Shaman robe examine: "A tattered old robe." - id: 2399 # Silverlight key examine: "The Wizard Traiborn gave me this key to Silverlight's case." - id: 2400 # Silverlight key examine: "Captain Rovin gave me this key to Silverlight's case." - id: 2401 # Silverlight key examine: "Sir Prysin dropped this key down the drain." - id: 2402 # Silverlight examine: "The magical sword 'Silverlight'." - id: 2403 # Hazeel scroll examine: "Scroll containing a powerful enchantment of restoration." - id: 2404 # Chest key examine: "This key opens a chest in the Carnillean household." - id: 2405 # Carnillean armour examine: "Decorative armour; an heirloom of the Carnillean family." - id: 2406 # Hazeel's mark examine: "A sign of my commitment to Hazeel." - id: 2407 # Ball examine: "A child's ball." - id: 2408 # Diary examine: "A daily journal." - id: 2409 # Door key examine: "A key to the Witch's house's front door." - id: 2410 # Magnet examine: "A very attractive magnet." - id: 2411 # Key examine: "A key to the Witch's shed." - id: 2412 # Saradomin cape examine: "A cape from the almighty god Saradomin." - id: 2413 # Guthix cape examine: "A cape from the almighty god Guthix." - id: 2414 # Zamorak cape examine: "A cape from the almighty god Zamorak." - id: 2415 # Saradomin staff examine: "A magical staff imbued with the power of Saradomin." - id: 2416 # Guthix staff examine: "A magical staff imbued with the power of Guthix." - id: 2417 # Zamorak staff examine: "A magical staff imbued with the power of Zamorak." - id: 2418 # Bronze key examine: "A heavy key made of bronze." - id: 2419 # Wig examine: "A wig that has been dyed slightly blonde." - id: 2421 # Wig examine: "A grey woollen wig." - id: 2422 # Blue partyhat examine: "A nice hat from a cracker." - id: 2423 # Key print examine: "An imprint of a key in a lump of clay." - id: 2424 # Paste examine: "A bottle of skin coloured paste." - id: 2426 # Burnt oomlie examine: "Oh dear, it's totally burnt!" - id: 2428 # Attack potion(4) examine: "4 doses of Attack potion." - id: 2430 # Restore potion(4) examine: "4 doses of restore potion." - id: 2432 # Defence potion(4) examine: "4 doses of Defence potion." - id: 2434 # Prayer potion(4) examine: "4 doses of Prayer restore potion." - id: 2436 # Super attack(4) examine: "4 doses of super Attack potion." - id: 2438 # Fishing potion(4) examine: "4 doses of Fishing potion." - id: 2440 # Super strength(4) examine: "4 doses of super Strength potion." - id: 2442 # Super defence(4) examine: "4 doses of super Defence potion." - id: 2444 # Ranging potion(4) examine: "4 doses of ranging potion." - id: 2446 # Antipoison(4) examine: "4 doses of antipoison potion." - id: 2448 # Superantipoison(4) examine: "4 doses of super antipoison potion." - id: 2450 # Zamorak brew(4) examine: "4 doses of Zamorak brew." - id: 2452 # Antifire potion(4) examine: "4 doses of anti-firebreath potion." - id: 2454 # Antifire potion(3) examine: "3 doses of anti-firebreath potion." - id: 2456 # Antifire potion(2) examine: "2 doses of anti-firebreath potion." - id: 2458 # Antifire potion(1) examine: "1 dose of anti-firebreath potion." - id: 2481 # Lantadyme examine: "A powerful herb." - id: 2493 # Blue d'hide chaps examine: "Made from 100% real dragonhide." - id: 2495 # Red d'hide chaps examine: "Made from 100% real dragonhide." - id: 2497 # Black d'hide chaps examine: "Made from 100% real dragonhide." - id: 2499 # Blue d'hide body examine: "Made from 100% real dragonhide." - id: 2501 # Red d'hide body examine: "Made from 100% real dragonhide." - id: 2503 # Black d'hide body examine: "Made from 100% real dragonhide." - id: 2507 # Red dragon leather examine: "It's a piece of prepared red dragonhide." - id: 2511 # Logs examine: "A number of wooden logs." - id: 2513 # Dragon chainbody examine: "A series of connected metal rings." - id: 2514 # Raw shrimps examine: "I should try cooking this." - id: 2516 # Pot of flour examine: "There is flour in this pot." - id: 2518 # Rotten tomato examine: "Pretty smelly." - id: 2528 # Lamp examine: "Wonder what happens if I rub it..." - id: 2530 # Bones examine: "Bones are for burying!" - id: 2550 # Ring of recoil examine: "An enchanted ring." - id: 2552 # Ring of dueling(8) examine: "An enchanted ring." - id: 2554 # Ring of dueling(7) examine: "An enchanted ring." - id: 2556 # Ring of dueling(6) examine: "An enchanted ring." - id: 2558 # Ring of dueling(5) examine: "An enchanted ring." - id: 2560 # Ring of dueling(4) examine: "An enchanted ring." - id: 2562 # Ring of dueling(3) examine: "An enchanted ring." - id: 2564 # Ring of dueling(2) examine: "An enchanted ring." - id: 2566 # Ring of dueling(1) examine: "An enchanted ring." - id: 2568 # Ring of forging examine: "An enchanted ring." - id: 2570 # Ring of life examine: "An enchanted ring." - id: 2572 # Ring of wealth examine: "It can be charged at the Fountain of Rune." - id: 2574 # Sextant examine: "Used by navigators to find their position in Gielinor." - id: 2575 # Watch examine: "A fine looking time piece." - id: 2576 # Chart examine: "A navigator's chart of Gielinor." - id: 2577 # Ranger boots examine: "Lightweight boots ideal for rangers." - id: 2579 # Wizard boots examine: "Slightly magical boots." - id: 2581 # Robin hood hat examine: "Endorsed by Robin Hood." - id: 2583 # Black platebody (t) examine: "Black platebody with trim." - id: 2585 # Black platelegs (t) examine: "Black platelegs with trim." - id: 2587 # Black full helm (t) examine: "Black full helmet with trim." - id: 2589 # Black kiteshield (t) examine: "Black kiteshield with trim." - id: 2591 # Black platebody (g) examine: "Black platebody with gold trim." - id: 2593 # Black platelegs (g) examine: "Black platelegs with gold trim." - id: 2595 # Black full helm (g) examine: "Black full helmet with gold trim." - id: 2597 # Black kiteshield (g) examine: "Black kiteshield with gold trim." - id: 2615 # Rune platebody (g) examine: "Rune platebody with gold trim." - id: 2617 # Rune platelegs (g) examine: "Rune platelegs with gold trim." - id: 2619 # Rune full helm (g) examine: "Rune full helmet with gold trim." - id: 2621 # Rune kiteshield (g) examine: "Rune kiteshield with gold trim." - id: 2623 # Rune platebody (t) examine: "Rune platebody with trim." - id: 2625 # Rune platelegs (t) examine: "Rune platelegs with trim." - id: 2627 # Rune full helm (t) examine: "Rune full helmet with trim." - id: 2629 # Rune kiteshield (t) examine: "Rune kiteshield with trim." - id: 2631 # Highwayman mask examine: "Your money or your life!" - id: 2633 # Blue beret examine: "Parlez-vous francais?" - id: 2635 # Black beret examine: "Parlez-vous francais?" - id: 2637 # White beret examine: "Parlez-vous francais?" - id: 2639 # Tan cavalier examine: "All for one and one for all!" - id: 2641 # Dark cavalier examine: "All for one and one for all!" - id: 2643 # Black cavalier examine: "All for one and one for all!" - id: 2645 # Red headband examine: "A minimalist's hat." - id: 2647 # Black headband examine: "A minimalist's hat." - id: 2649 # Brown headband examine: "A minimalist's hat." - id: 2651 # Pirate's hat examine: "Shiver me timbers!" - id: 2653 # Zamorak platebody examine: "Rune platebody in the colours of Zamorak." - id: 2655 # Zamorak platelegs examine: "Rune platelegs in the colours of Zamorak." - id: 2657 # Zamorak full helm examine: "Rune full helmet in the colours of Zamorak." - id: 2659 # Zamorak kiteshield examine: "Rune kiteshield in the colours of Zamorak." - id: 2669 # Guthix platebody examine: "Rune platebody in the colours of Guthix." - id: 2671 # Guthix platelegs examine: "Rune plate legs in the colours of Guthix." - id: 2673 # Guthix full helm examine: "A rune full face helmet in the colours of Guthix." - id: 2675 # Guthix kiteshield examine: "Rune kiteshield in the colours of Guthix." - id: 2859 # Wolf bones examine: "Bones of a recently slain wolf." - id: 2861 # Wolfbone arrowtips examine: "I can make an ogre arrow with these." - id: 2862 # Achey tree logs examine: "These logs are longer than normal." - id: 2864 # Ogre arrow shaft examine: "A wooden arrow shaft." - id: 2865 # Flighted ogre arrow examine: "A wooden arrow shaft with four flights attached." - id: 2866 # Ogre arrow examine: "A large ogre arrow with a bone tip." - id: 2871 # Ogre bellows examine: "A large pair of ogre bellows." - id: 2872 # Ogre bellows (3) examine: "A large pair of ogre bellows, it has three loads of swamp gas in it." - id: 2873 # Ogre bellows (2) examine: "A large pair of ogre bellows, it has two loads of swamp gas in it." - id: 2874 # Ogre bellows (1) examine: "A large pair of ogre bellows, it has one load of swamp gas in it." - id: 2875 # Bloated toad examine: "An inflated toad." - id: 2876 # Raw chompy examine: "I need to cook this first." - id: 2878 # Cooked chompy examine: "It might look delicious to an ogre." - id: 2880 # Ruined chompy examine: "It's really burnt." - id: 2882 # Seasoned chompy examine: "It has been deliciously seasoned to taste wonderful for ogres." - id: 2883 # Ogre bow examine: "More powerful than a normal bow, useful against large game birds." - id: 2886 # Battered book examine: "Book of the elemental shield." - id: 2887 # Battered key examine: "An old battered key." - id: 2888 # A stone bowl examine: "This is an empty stone bowl." - id: 2889 # A stone bowl examine: "This is a stone bowl full of lava." - id: 2890 # Elemental shield examine: "A magic shield." - id: 2892 # Elemental ore examine: "This needs refining." - id: 2893 # Elemental metal examine: "It's a bar of refined elemental ore." - id: 2944 # Golden key examine: "A replica key made of solid gold." - id: 2945 # Iron key examine: "A key made of solid Iron." - id: 2946 # Golden tinderbox examine: "A replica tinderbox made of solid gold." - id: 2947 # Golden candle examine: "A replica candle made of solid gold." - id: 2948 # Golden pot examine: "A replica pot made of solid gold." - id: 2949 # Golden hammer examine: "A replica hammer made of solid gold." - id: 2950 # Golden feather examine: "A replica feather made of solid gold." - id: 2951 # Golden needle examine: "A replica needle made of solid gold." - id: 2952 # Wolfbane examine: "A silver dagger that can prevent werewolves changing form." - id: 2955 # Moonlight mead examine: "A foul smelling brew." - id: 2957 # Druid pouch examine: "An empty druid pouch." - id: 2958 # Druid pouch examine: "A druid pouch." - id: 2959 # Rotten food examine: "Erhhh! It stinks." - id: 2961 # Silver sickle examine: "It's a silver sickle." - id: 2964 # Washing bowl examine: "Used for washing your face, amongst other things." - id: 2966 # Mirror examine: "A small mirror, probably used for grooming." - id: 2967 # Journal examine: "This must be Filliman Tarlocks personal journal." - id: 2968 # Druidic spell examine: "A druidic spell given to you freely by the spirit of Filliman Tarlock." - id: 2969 # A used spell examine: "A used druidic spell given to you freely by the spirit of Filliman Tarlock." - id: 2972 # Mort myre stem examine: "A cutting from a budding branch." - id: 2974 # Mort myre pear examine: "A pear picked from a dying bush in Mort Myre." - id: 2976 # Sickle mould examine: "Used to make sickles." - id: 2978 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Ogre Bowman" - id: 2979 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Bowman" - id: 2980 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Ogre Yeoman" - id: 2981 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Yeoman" - id: 2982 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Ogre Marksman" - id: 2983 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Marksman" - id: 2984 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Ogre Woodsman" - id: 2985 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Woodsman" - id: 2986 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess:Ogre Forester" - id: 2987 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Forester" - id: 2988 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Ogre Bowmaster" - id: 2989 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Bowmaster" - id: 2990 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Ogre Expert" - id: 2991 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Expert" - id: 2992 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Ogre Dragon Archer" - id: 2993 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Dragon Archer" - id: 2994 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Expert Ogre Dragon Archer" - id: 2995 # Chompy bird hat examine: "A symbol of your chompy bird hunting prowess: Expert Dragon Archer" - id: 2996 # Agility arena ticket examine: "I can exchange these for further experience or items." - id: 2997 # Pirate's hook examine: "You should see the shark..." - id: 2998 # Toadflax examine: "A useful herb." - id: 3000 # Snapdragon examine: "A powerful herb." - id: 3008 # Energy potion(4) examine: "4 doses of energy potion." - id: 3010 # Energy potion(3) examine: "3 doses of energy potion." - id: 3012 # Energy potion(2) examine: "2 doses of energy potion." - id: 3014 # Energy potion(1) examine: "1 dose of energy potion." - id: 3016 # Super energy(4) examine: "4 doses of super energy potion." - id: 3018 # Super energy(3) examine: "3 doses of super energy potion." - id: 3020 # Super energy(2) examine: "2 doses of super energy potion." - id: 3022 # Super energy(1) examine: "1 dose of super energy potion." - id: 3024 # Super restore(4) examine: "4 doses of super restore potion." - id: 3026 # Super restore(3) examine: "3 doses of super restore potion." - id: 3028 # Super restore(2) examine: "2 doses of super restore potion." - id: 3030 # Super restore(1) examine: "1 dose of super restore potion." - id: 3032 # Agility potion(4) examine: "4 doses of Agility potion." - id: 3034 # Agility potion(3) examine: "3 doses of Agility potion." - id: 3036 # Agility potion(2) examine: "2 doses of Agility potion." - id: 3038 # Agility potion(1) examine: "1 dose of Agility potion." - id: 3040 # Magic potion(4) examine: "4 doses of Magic potion." - id: 3042 # Magic potion(3) examine: "3 doses of Magic potion." - id: 3044 # Magic potion(2) examine: "2 doses of Magic potion." - id: 3046 # Magic potion(1) examine: "1 dose of Magic potion." - id: 3053 # Lava battlestaff examine: "It's a slightly magical stick." - id: 3054 # Mystic lava staff examine: "It's a slightly magical stick." - id: 3057 # Mime mask examine: "A mime would wear this." - id: 3058 # Mime top examine: "A mime would wear this." - id: 3059 # Mime legs examine: "A mime would wear these." - id: 3060 # Mime gloves examine: "A mime would wear these." - id: 3061 # Mime boots examine: "A mime would wear these." - id: 3062 # Strange box examine: "It seems to be humming..." - id: 3093 # Black dart examine: "A deadly throwing dart with a black tip." - id: 3094 # Black dart(p) examine: "A deadly poisoned dart with a black tip." - id: 3095 # Bronze claws examine: "A set of fighting claws." - id: 3096 # Iron claws examine: "A set of fighting claws." - id: 3097 # Steel claws examine: "A set of fighting claws." - id: 3098 # Black claws examine: "A set of fighting claws." - id: 3099 # Mithril claws examine: "A set of fighting claws." - id: 3100 # Adamant claws examine: "A set of fighting claws." - id: 3101 # Rune claws examine: "A set of fighting claws." - id: 3102 # Combination examine: "The combination to Burthorpe Castle's equipment room." - id: 3103 # Iou examine: "The guard wrote the IOU on the back of some paper." - id: 3104 # Secret way map examine: "This map shows the secret way up to Death Plateau." - id: 3105 # Climbing boots examine: "Boots made for climbing." - id: 3107 # Spiked boots examine: "Climbing boots with spikes." - id: 3109 # Stone ball examine: "Place on the stone mechanism in the right order to open the door." - id: 3110 # Stone ball examine: "Place on the stone mechanism in the right order to open the door." - id: 3111 # Stone ball examine: "Place on the stone mechanism in the right order to open the door." - id: 3112 # Stone ball examine: "Place on the stone mechanism in the right order to open the door." - id: 3113 # Stone ball examine: "Place on the stone mechanism in the right order to open the door." - id: 3114 # Certificate examine: "Entrance certificate to the Imperial Guard." - id: 3122 # Granite shield examine: "A solid stone shield." - id: 3123 # Shaikahan bones examine: "Large glistening bones which glow with a pale yellow aura." - id: 3125 # Jogre bones examine: "Fairly big bones which smell distinctly of Jogre." - id: 3127 # Burnt jogre bones examine: "These blackened Jogre bones have been somehow burnt." - id: 3128 # Pasty jogre bones examine: "Burnt Jogre bones smothered with raw Karambwanji Paste." - id: 3129 # Pasty jogre bones examine: "Burnt Jogre bones smothered with cooked Karambwanji paste." - id: 3130 # Marinated j' bones examine: "Burnt Jogre bones marinated in a lovely Karambwanji sauce. Perfect." - id: 3131 # Pasty jogre bones examine: "Jogre bones smothered with raw Karambwanji paste." - id: 3132 # Pasty jogre bones examine: "Jogre bones smothered with cooked Karambwanji paste." - id: 3133 # Marinated j' bones examine: "Jogre Bones marinated in Karambwanji sauce. Not quite right." - id: 3135 # Prison key examine: "The key to the troll prison." - id: 3136 # Cell key 1 examine: "The key to Godric's cell in the troll prison." - id: 3137 # Cell key 2 examine: "The key to Mad Eadgar's cell in the troll prison." - id: 3138 # Potato cactus examine: "How am I supposed to eat that?!" - id: 3140 # Dragon chainbody examine: "A series of connected metal rings." - id: 3142 # Raw karambwan examine: "A raw green octopus." - id: 3144 # Cooked karambwan examine: "Cooked octopus. It looks very nutritious." - id: 3147 # Cooked karambwan examine: "Cooked octopus. It looks very nutritious." - id: 3148 # Burnt karambwan examine: "Burnt octopus." - id: 3150 # Raw karambwanji examine: "Small brightly coloured tropical fish." - id: 3152 # Karambwan paste examine: "Freshly made octopus paste." - id: 3153 # Karambwan paste examine: "Freshly made octopus paste. This smells quite nauseating." - id: 3154 # Karambwan paste examine: "Freshly made octopus paste." - id: 3155 # Karambwanji paste examine: "This paste smells of raw fish." - id: 3156 # Karambwanji paste examine: "This paste smells of cooked fish." - id: 3157 # Karambwan vessel examine: "A wide bodied and thin necked vessel, encrusted with sea salt." - id: 3159 # Karambwan vessel examine: "This Karambwan Vessel is loaded with Karambwanji." - id: 3161 # Crafting manual examine: "A set of instructions explaining how to construct a Karambwan vessel." - id: 3162 # Sliced banana examine: "You swear you had more than three slices before." - id: 3164 # Karamjan rum examine: "The Karamjan rum has slices of banana floating in it." - id: 3165 # Karamjan rum examine: "A banana has been stuffed into the neck of this bottle." - id: 3166 # Monkey corpse examine: "It's the body of a dead monkey." - id: 3167 # Monkey skin examine: "It's the skin of a dead monkey." - id: 3168 # Seaweed sandwich examine: "A 'Seaweed in Monkey Skin' sandwich. Perfect for statue repair." - id: 3169 # Stuffed monkey examine: "A body of a dead monkey, tastefully stuffed with seaweed." - id: 3170 # Bronze spear(kp) examine: "A Karambwan poisoned bronze tipped spear." - id: 3171 # Iron spear(kp) examine: "A Karambwan poisoned iron tipped spear." - id: 3172 # Steel spear(kp) examine: "A Karambwan poisoned steel tipped spear." - id: 3173 # Mithril spear(kp) examine: "A Karambwan poisoned mithril tipped spear." - id: 3174 # Adamant spear(kp) examine: "A Karambwan poisoned adamantite tipped spear." - id: 3175 # Rune spear(kp) examine: "A Karambwan poisoned rune tipped spear." - id: 3176 # Dragon spear(kp) examine: "A Karambwan poisoned dragon tipped spear." - id: 3179 # Monkey bones examine: "These are smallish monkey bones." - id: 3180 # Monkey bones examine: "These are medium sized monkey bones." - id: 3181 # Monkey bones examine: "These are quite large monkey bones." - id: 3182 # Monkey bones examine: "These are quite large monkey bones." - id: 3183 # Monkey bones examine: "These are small monkey bones." - id: 3185 # Monkey bones examine: "These are smallish monkey bones. They smell extremely nauseating." - id: 3186 # Monkey bones examine: "These are smallish monkey bones. They smell extremely nauseating." - id: 3187 # Bones examine: "They seem to shake slightly... It might be a good idea to bury them." - id: 3188 # Cleaning cloth examine: "A spirit soaked piece of silk which can be used to remove poison." - id: 3190 # Bronze halberd examine: "A bronze halberd." - id: 3192 # Iron halberd examine: "An iron halberd." - id: 3194 # Steel halberd examine: "A steel halberd." - id: 3196 # Black halberd examine: "A black halberd." - id: 3198 # Mithril halberd examine: "A mithril halberd." - id: 3200 # Adamant halberd examine: "An adamant halberd." - id: 3202 # Rune halberd examine: "A rune halberd." - id: 3204 # Dragon halberd examine: "A dragon halberd." - id: 3206 # King's message examine: "A summons from King Lathas." - id: 3208 # Crystal pendant examine: "Lord Iorwerth's crystal pendant." - id: 3209 # Sulphur examine: "A piece of sulphur formation." - id: 3211 # Limestone examine: "Some limestone." - id: 3213 # Quicklime examine: "Some quicklime." - id: 3214 # Pot of quicklime examine: "A pot of ground quicklime." - id: 3215 # Ground sulphur examine: "A pile of ground sulphur." - id: 3216 # Barrel examine: "An empty barrel." - id: 3218 # Barrel bomb examine: "A barrel full of fire oil." - id: 3219 # Barrel bomb examine: "A fused barrel full of fire oil." - id: 3221 # Barrel of naphtha examine: "A barrel full of naphtha." - id: 3222 # Naphtha mix examine: "A barrel full of naphtha and sulphur." - id: 3223 # Naphtha mix examine: "A barrel full of naphtha and quicklime." - id: 3224 # Strip of cloth examine: "A strip of cloth." - id: 3226 # Raw rabbit examine: "Might taste better cooked." - id: 3228 # Cooked rabbit examine: "Mmm this looks tasty." - id: 3230 # Big book of bangs examine: "A book by Mel Achy." - id: 3239 # Bark examine: "Bark from a hollow tree." - id: 3261 # Goutweed examine: "A pale, tough looking herb." - id: 3262 # Troll thistle examine: "It's tough and spiky." - id: 3263 # Dried thistle examine: "It'll be easier to grind now." - id: 3264 # Ground thistle examine: "It's ready for mixing." - id: 3265 # Troll potion examine: "It's part of Eadgar's plan." - id: 3266 # Drunk parrot examine: "It's rather drunk." - id: 3267 # Dirty robe examine: "It's dirty and smelly." - id: 3268 # Fake man examine: "It's good enough to fool a troll." - id: 3269 # Storeroom key examine: "The key to the Troll storeroom." - id: 3270 # Alco-chunks examine: "Pineapple chunks dipped in strong liquor." - id: 3327 # Myre snelm examine: "A marshy coloured snail shell helmet." - id: 3329 # Blood'n'tar snelm examine: "A red and black Snail shell helmet." - id: 3331 # Ochre snelm examine: "A muddy yellow snail shell helmet." - id: 3333 # Bruise blue snelm examine: "A moody blue snail shell helmet." - id: 3335 # Broken bark snelm examine: "An orange and bark coloured snail shell helmet." - id: 3337 # Myre snelm examine: "A swamp coloured pointed snail shell helmet." - id: 3339 # Blood'n'tar snelm examine: "A red and black pointed snail shell helmet." - id: 3341 # Ochre snelm examine: "A muddy yellow coloured pointed snail shell helmet." - id: 3343 # Bruise blue snelm examine: "A moody blue pointed snail shell helmet." - id: 3345 # Blamish myre shell examine: "A large 'Myre' coloured blamish snail shell, looks protective." - id: 3347 # Blamish red shell examine: "A large red and black blamish snail shell, looks protective." - id: 3349 # Blamish ochre shell examine: "A large muddy yellow coloured blamish snail shell, looks protective." - id: 3351 # Blamish blue shell examine: "A large blue coloured blamish snail shell, looks protective." - id: 3353 # Blamish bark shell examine: "A large bark coloured blamish snail shell, looks protective." - id: 3355 # Blamish myre shell examine: "A large 'Myre' coloured blamish snail shell, looks protective." - id: 3357 # Blamish red shell examine: "A large red coloured blamish snail shell, looks protective." - id: 3359 # Blamish ochre shell examine: "A large ochre coloured blamish snail shell, looks protective." - id: 3361 # Blamish blue shell examine: "A large blue coloured blamish snail shell, looks protective." - id: 3363 # Thin snail examine: "The thin, slimy corpse of a deceased giant snail." - id: 3365 # Lean snail examine: "The lean, slimy corpse of a deceased giant snail." - id: 3367 # Fat snail examine: "The fat, slimy corpse of a deceased giant snail." - id: 3369 # Thin snail meat examine: "A succulently slimy slice of sumptuous snail." - id: 3371 # Lean snail meat examine: "A succulently slimy slice of sumptuous snail." - id: 3373 # Fat snail meat examine: "A succulently slimy slice of sumptuous snail." - id: 3375 # Burnt snail examine: "A slightly super-saute'ed snail." - id: 3377 # Sample bottle examine: "An empty sample bottle." - id: 3381 # Cooked slimy eel examine: "A cooked slimy eel - not delicious, but pretty nutritious." - id: 3383 # Burnt eel examine: "It looks like it's seen one too many fires." - id: 3385 # Splitbark helm examine: "A wooden helmet." - id: 3387 # Splitbark body examine: "Provides good protection." - id: 3389 # Splitbark legs examine: "These should protect my legs." - id: 3391 # Splitbark gauntlets examine: "These should keep my hands safe." - id: 3395 # Diary examine: "A diary belonging to Herbi Flax." - id: 3396 # Loar remains examine: "The remains of a deadly shade." - id: 3398 # Phrin remains examine: "The remains of a deadly shade." - id: 3400 # Riyl remains examine: "The remains of a deadly shade." - id: 3402 # Asyn remains examine: "The remains of a deadly shade." - id: 3404 # Fiyr remains examine: "The remains of a deadly shade." - id: 3406 # Unfinished potion examine: "I need another ingredient to finish this potion." - id: 3408 # Serum 207 (4) examine: "4 doses serum 207 as described in Herbi Flax's diary." - id: 3410 # Serum 207 (3) examine: "3 doses serum 207 as described in Herbi Flax's diary." - id: 3412 # Serum 207 (2) examine: "2 doses serum 207 as described in Herbi Flax's diary." - id: 3414 # Serum 207 (1) examine: "1 dose serum 207 as described in Herbi Flax's diary." - id: 3416 # Serum 208 (4) examine: "4 doses permanent serum 208 as described in Herbi Flax's diary." - id: 3417 # Serum 208 (3) examine: "3 doses permanent serum 208 as described in Herbi Flax's diary." - id: 3418 # Serum 208 (2) examine: "2 doses permanent serum 208 as described in Herbi Flax's diary." - id: 3419 # Serum 208 (1) examine: "1 dose permanent serum 208 as described in Herbi Flax's diary." - id: 3420 # Limestone brick examine: "A well carved limestone brick." - id: 3422 # Olive oil(4) examine: "4 doses of olive oil." - id: 3424 # Olive oil(3) examine: "3 doses of olive oil." - id: 3426 # Olive oil(2) examine: "2 doses of olive oil." - id: 3428 # Olive oil(1) examine: "1 dose of olive oil." - id: 3430 # Sacred oil(4) examine: "4 doses of sacred Oil." - id: 3432 # Sacred oil(3) examine: "3 doses of sacred Oil." - id: 3434 # Sacred oil(2) examine: "2 doses of sacred Oil." - id: 3436 # Sacred oil(1) examine: "1 dose of sacred Oil." - id: 3438 # Pyre logs examine: "Logs prepared with sacred oil for a funeral pyre." - id: 3440 # Oak pyre logs examine: "Oak logs prepared with sacred oil for a funeral pyre." - id: 3442 # Willow pyre logs examine: "Willow logs prepared with sacred oil for a funeral pyre." - id: 3444 # Maple pyre logs examine: "Maple logs prepared with sacred oil for a funeral pyre." - id: 3446 # Yew pyre logs examine: "Yew logs prepared with sacred oil for a funeral pyre." - id: 3448 # Magic pyre logs examine: "Magic logs prepared with sacred oil for a funeral pyre." - id: 3450 # Bronze key red examine: "A bronze key with a blood-red painted eyelet." - id: 3451 # Bronze key brown examine: "A bronze key with a brown painted eyelet." - id: 3452 # Bronze key crimson examine: "A bronze key with a crimson painted eyelet." - id: 3453 # Bronze key black examine: "A bronze key with a black painted eyelet." - id: 3454 # Bronze key purple examine: "A bronze key with a purple painted eyelet." - id: 3455 # Steel key red examine: "A steel key with a blood-red painted eyelet." - id: 3456 # Steel key brown examine: "A steel key with a brown painted eyelet." - id: 3457 # Steel key crimson examine: "A steel key with a crimson painted eyelet." - id: 3458 # Steel key black examine: "A steel key with a black painted eyelet." - id: 3459 # Steel key purple examine: "A steel key with a purple painted eyelet." - id: 3460 # Black key red examine: "A black key with a blood-red painted eyelet." - id: 3461 # Black key brown examine: "A black key with a brown painted eyelet." - id: 3462 # Black key crimson examine: "A black key with a crimson painted eyelet." - id: 3463 # Black key black examine: "A black key with a black painted eyelet." - id: 3464 # Black key purple examine: "A black key with a purple painted eyelet." - id: 3465 # Silver key red examine: "A silver key with a blood-red painted eyelet." - id: 3466 # Silver key brown examine: "A silver key with a brown painted eyelet." - id: 3467 # Silver key crimson examine: "A silver key with a crimson painted eyelet." - id: 3468 # Silver key black examine: "A silver key with a black painted eyelet." - id: 3469 # Silver key purple examine: "A silver key with a purple painted eyelet." - id: 3470 # Fine cloth examine: "Amazingly untouched by time." - id: 3472 # Black plateskirt (t) examine: "Black plateskirt with trim." - id: 3473 # Black plateskirt (g) examine: "Black plateskirt with gold trim." - id: 3476 # Rune plateskirt (g) examine: "Rune plateskirt with gold trim." - id: 3477 # Rune plateskirt (t) examine: "Rune plateskirt with trim." - id: 3478 # Zamorak plateskirt examine: "Rune plateskirt in the colours of Zamorak." - id: 3480 # Guthix plateskirt examine: "Rune plateskirt in the colours of Guthix." - id: 3481 # Gilded platebody examine: "Rune platebody with gold plate." - id: 3483 # Gilded platelegs examine: "Rune platelegs with gold plate." - id: 3485 # Gilded plateskirt examine: "Rune plateskirt with gold plate." - id: 3486 # Gilded full helm examine: "Rune full helmet with gold plate." - id: 3488 # Gilded kiteshield examine: "Rune kiteshield with gold plate." - id: 3678 # Flamtaer hammer examine: "An exquisitely shaped tool specially designed for fixing temples." - id: 3688 # Unstrung lyre examine: "It's almost a musical instrument." - id: 3689 # Lyre examine: "It's a musical instrument I don't know how to play." - id: 3690 # Enchanted lyre examine: "A musical instrument that I can magically play." - id: 3691 # Enchanted lyre(1) examine: "This will teleport me to the Fremennik province when I play it." - id: 3692 # Branch examine: "I can use this to make a lyre." - id: 3693 # Golden fleece examine: "I can spin this into golden wool..." - id: 3694 # Golden wool examine: "I can use this to make a lyre." - id: 3695 # Pet rock examine: "The lowest maintenance pet you will ever have." - id: 3696 # Hunters' talisman examine: "Talisman to bind the Draugen." - id: 3697 # Hunters' talisman examine: "Talisman to bind the Draugen." - id: 3698 # Exotic flower examine: "Some flowers from a distant land." - id: 3699 # Fremennik ballad examine: "A hauntingly beautiful love ballad." - id: 3700 # Sturdy boots examine: "A pair of sturdy custom made boots." - id: 3702 # Custom bow string examine: "A finely crafted string for a custom bow." - id: 3703 # Unusual fish examine: "An extremely odd, non-edible fish." - id: 3704 # Sea fishing map examine: "Map showing the best fishing spots out at sea." - id: 3705 # Weather forecast examine: "An estimate of expected local weather patterns." - id: 3706 # Champions token examine: "Shows the wearer is worthy of the Champions table." - id: 3707 # Legendary cocktail examine: "Probably the greatest cocktail in the world." - id: 3708 # Fiscal statement examine: "A signed statement promising a reduction on sales tax." - id: 3709 # Promissory note examine: "A legally binding contract promising not to enter the longhall." - id: 3710 # Warriors' contract examine: "This employment contract is for a warrior to act as a bodyguard." - id: 3711 # Keg of beer examine: "A lot of beer in a barrel." - id: 3712 # Low alcohol keg examine: "Suspiciously close to beer, but without the side effects." - id: 3713 # Strange object examine: "It's some kind of weird little parcel thing." - id: 3714 # Lit strange object examine: "It's some kind of weird little parcel thing. On fire." - id: 3715 # Red disk examine: "A red coloured disk, apparently made of wood." - id: 3716 # Red disk examine: "A red coloured disk, apparently made of wood." - id: 3718 # Magnet examine: "A very attractive magnet." - id: 3719 # Blue thread examine: "Some blue thread." - id: 3720 # Small pick examine: "A small pick for cracking small objects." - id: 3721 # Toy ship examine: "Might be fun to play with in the bath." - id: 3722 # Full bucket examine: "This bucket is completely full. It has a 5 painted on its side." - id: 3723 # 4/5ths full bucket examine: "This bucket is eighty percent full. It has a 5 painted on its side." - id: 3724 # 3/5ths full bucket examine: "This bucket is sixty percent full. It has a 5 painted on its side." - id: 3725 # 2/5ths full bucket examine: "This bucket is forty percent full. It has a 5 painted on its side." - id: 3726 # 1/5ths full bucket examine: "This bucket is twenty percent full. It has a 5 painted on its side." - id: 3727 # Empty bucket examine: "This bucket is completely empty. It has a 5 painted on its side." - id: 3728 # Frozen bucket examine: "This bucket of water is frozen solid." - id: 3729 # Full jug examine: "This jug is completely full. It has a 3 painted on its side." - id: 3730 # 2/3rds full jug examine: "This jug is two thirds full. It has a 3 painted on its side." - id: 3731 # 1/3rds full jug examine: "This jug is one thirds full. It has a 3 painted on its side." - id: 3732 # Empty jug examine: "This jug is completely empty. It has a 3 painted on its side." - id: 3733 # Frozen jug examine: "This jug of water is frozen solid." - id: 3734 # Vase examine: "An unusually shaped vase. You can see something glinting inside." - id: 3735 # Vase of water examine: "An unusually shaped vase full of water. You can see something glinting inside." - id: 3736 # Frozen vase examine: "An unusually shaped vase full of ice. You can see something glinting inside." - id: 3737 # Vase lid examine: "This looks like a lid to some kind of container..." - id: 3738 # Sealed vase examine: "The lid is screwed on tightly." - id: 3739 # Sealed vase examine: "The lid is screwed on tightly. It is very cold." - id: 3740 # Sealed vase examine: "The lid is screwed on tightly. It is full of water." - id: 3741 # Frozen key examine: "A key encased in ice." - id: 3742 # Red herring examine: "The colouring on it seems to be some kind of sticky goop..." - id: 3743 # Red disk examine: "A red coloured disk, apparently made of wood." - id: 3744 # Wooden disk examine: "A simple looking disk made of wood." - id: 3745 # Seer's key examine: "The key to leave the Seer's house." - id: 3746 # Sticky red goop examine: "Yup, it's sticky, it's red and it's goop." - id: 3748 # Fremennik helm examine: "A sturdy helm worn only by Fremennik clan members." - id: 3749 # Archer helm examine: "This helmet is worn by archers." - id: 3751 # Berserker helm examine: "This helmet is worn by berserkers." - id: 3753 # Warrior helm examine: "This helmet is worn by warriors." - id: 3755 # Farseer helm examine: "This helmet is worn by farseers." - id: 3757 # Fremennik blade examine: "A sword used only by Fremennik warriors." - id: 3758 # Fremennik shield examine: "A shield worn by Fremennik warriors." - id: 3791 # Fremennik boots examine: "Very stylish!" - id: 3793 # Fremennik robe examine: "The latest fashion in Rellekka." - id: 3795 # Fremennik skirt examine: "The latest fashion in Rellekka." - id: 3797 # Fremennik hat examine: "A silly pointed hat." - id: 3801 # Keg of beer examine: "A lot of beer in a barrel." - id: 3805 # Tankard examine: "A big cup for a big thirst." - id: 3827 # Saradomin page 1 examine: "This seems to have been torn from a book..." - id: 3828 # Saradomin page 2 examine: "This seems to have been torn from a book..." - id: 3829 # Saradomin page 3 examine: "This seems to have been torn from a book..." - id: 3830 # Saradomin page 4 examine: "This seems to have been torn from a book..." - id: 3831 # Zamorak page 1 examine: "This seems to have been torn from a book..." - id: 3832 # Zamorak page 2 examine: "This seems to have been torn from a book..." - id: 3833 # Zamorak page 3 examine: "This seems to have been torn from a book..." - id: 3834 # Zamorak page 4 examine: "This seems to have been torn from a book..." - id: 3835 # Guthix page 1 examine: "This seems to have been torn from a book..." - id: 3836 # Guthix page 2 examine: "This seems to have been torn from a book..." - id: 3837 # Guthix page 3 examine: "This seems to have been torn from a book..." - id: 3838 # Guthix page 4 examine: "This seems to have been torn from a book..." - id: 3839 # Damaged book examine: "An incomplete book of Saradomin." - id: 3840 # Holy book examine: "The holy book of Saradomin." - id: 3841 # Damaged book examine: "An incomplete book of Zamorak." - id: 3842 # Unholy book examine: "The unholy book of Zamorak." - id: 3843 # Damaged book examine: "An incomplete book of Guthix." - id: 3844 # Book of balance examine: "The holy book of Guthix." - id: 3845 # Journal examine: "A daily journal." - id: 3846 # Diary examine: "Someone's Diary." - id: 3847 # Manual examine: "Looks like some kind of manual." - id: 3848 # Lighthouse key examine: "The key to the front door of the lighthouse." - id: 3849 # Rusty casket examine: "Looks old and rusty..." - id: 3853 # Games necklace(8) examine: "An enchanted necklace." - id: 3855 # Games necklace(7) examine: "An enchanted necklace." - id: 3857 # Games necklace(6) examine: "An enchanted necklace." - id: 3859 # Games necklace(5) examine: "An enchanted necklace." - id: 3861 # Games necklace(4) examine: "An enchanted necklace." - id: 3863 # Games necklace(3) examine: "An enchanted necklace." - id: 3865 # Games necklace(2) examine: "An enchanted necklace." - id: 3867 # Games necklace(1) examine: "An enchanted necklace." - id: 3894 # Awful anthem examine: "It's not very good." - id: 3895 # Good anthem examine: "Much better." - id: 3896 # Treaty examine: "Just needs the King's signature." - id: 3897 # Giant nib examine: "For making a giant pen." - id: 3898 # Giant pen examine: "The King should be able to use this." - id: 3899 # Iron sickle examine: "Not as good as a pet frog." - id: 3901 # Ghrim's book examine: "Managing Thine Kingdom for Noobes by A. Ghrim." - id: 4001 # Hardy gout tuber examine: "A hardy gout tuber." - id: 4002 # Spare controls examine: "It looks like some kind of control panel." - id: 4004 # Gnome royal seal examine: "It's the official Gnome Royal Seal, signed by King Narnode Shareen." - id: 4005 # Narnode's orders examine: "Unreadable orders handwritten by King Narnode Shareen." - id: 4006 # Monkey dentures examine: "Magical monkey talking dentures! What more can we say? Ook!" - id: 4007 # Enchanted bar examine: "A gold bar invested with a talkative monkey spirit." - id: 4008 # Eye of gnome examine: "It's... the eye of a gnome! Now what on earth could one do with this?" - id: 4012 # Monkey nuts examine: "These are monkey nuts. Yummy." - id: 4014 # Monkey bar examine: "It's a monkey bar. It looks highly nutritious." - id: 4016 # Banana stew examine: "It's a bowl full of mushy banana." - id: 4020 # M'amulet mould examine: "It's an amulet mould shaped like a monkey head." - id: 4021 # M'speak amulet examine: "It's an Amulet of Monkey Speak. It makes vague chattering noises." - id: 4022 # M'speak amulet examine: "It's an unstrung Amulet of Monkey Speak. It makes vague chattering noises." - id: 4023 # Monkey talisman examine: "A magical talisman in the shape of a monkey head." - id: 4033 # Monkey examine: "It's a monkey in your backpack. As you look it pokes you." - id: 4034 # Monkey skull examine: "It's a very ancient skull from some kind of ape." - id: 4035 # 10th squad sigil examine: "It's the official sigil of the 10th squad of the Royal Guard." - id: 4037 # Saradomin banner examine: "The Saradomin Team Standard." - id: 4039 # Zamorak banner examine: "The Zamorak Team Standard." - id: 4041 # Hooded cloak examine: "The colours of Saradomin." - id: 4042 # Hooded cloak examine: "The colours of Zamorak." - id: 4043 # Rock examine: "I can use this with the catapult." - id: 4045 # Explosive potion examine: "I could use this to destroy things..." - id: 4047 # Climbing rope examine: "Should be long enough to scale castle walls." - id: 4049 # Bandages examine: "A box of bandages for healing." - id: 4051 # Toolkit examine: "I can use this to repair things." - id: 4053 # Barricade examine: "Use these to block enemy team movement." - id: 4055 # Castlewars manual examine: "It's a manual for castlewars." - id: 4067 # Castle wars ticket examine: "I can exchange these for further items." - id: 4068 # Decorative sword examine: "A very decorative sword." - id: 4069 # Decorative armour examine: "Very decorative armour." - id: 4070 # Decorative armour examine: "Very decorative armour." - id: 4071 # Decorative helm examine: "A very decorative helm." - id: 4072 # Decorative shield examine: "A very decorative shield." - id: 4073 # Damp tinderbox examine: "Not so useful for lighting a fire." - id: 4075 # Glowing fungus examine: "A bizarre fungus. It glows with a pale blue light." - id: 4077 # Crystal-mine key examine: "A key I found in the lower levels of the Morytanian mines." - id: 4078 # Zealot's key examine: "I stole this from a Saradominist I met South of Mort'ton." - id: 4079 # Yo-yo examine: "A gift from Santa." - id: 4081 # Salve amulet examine: "Increases the wearer's strength and accuracy by 15%{{Sic|actually 1/6th}} when fighting the undead." - id: 4082 # Salve shard examine: "An unstrung crystal imbued with the power of Saradomin." - id: 4083 # Sled examine: "It needs waxing before I can use it." - id: 4084 # Sled examine: "A waxed sled." - id: 4085 # Wax examine: "I can use this to wax my sled." - id: 4086 # Trollweiss examine: "These are very rare flowers with a pungent odour." - id: 4087 # Dragon platelegs examine: "These look pretty heavy." - id: 4089 # Mystic hat examine: "A magical hat." - id: 4091 # Mystic robe top examine: "The upper half of a magical robe." - id: 4093 # Mystic robe bottom examine: "The lower half of a magical robe." - id: 4095 # Mystic gloves examine: "Magical gloves." - id: 4097 # Mystic boots examine: "Magical boots." - id: 4119 # Bronze boots examine: "These will protect my feet." - id: 4121 # Iron boots examine: "These will protect my feet." - id: 4123 # Steel boots examine: "These will protect my feet." - id: 4125 # Black boots examine: "These will protect my feet." - id: 4127 # Mithril boots examine: "These will protect my feet." - id: 4129 # Adamant boots examine: "These will protect my feet." - id: 4131 # Rune boots examine: "These will protect my feet." - id: 4150 # Broad arrows examine: "Arrows with a wider than normal tip." - id: 4151 # Abyssal whip examine: "A weapon from the abyss." - id: 4153 # Granite maul examine: "Simplicity is the best weapon." - id: 4155 # Enchanted gem examine: "I can contact the Slayer Masters with this." - id: 4156 # Mirror shield examine: "I can just about see things in this shield's reflection." - id: 4158 # Leaf-bladed spear examine: "A spear with a leaf-shaped point." - id: 4160 # Broad arrows examine: "Arrows with a wider than normal tip." - id: 4161 # Bag of salt examine: "A bag of salt." - id: 4162 # Rock hammer examine: "I can even smash stone with this." - id: 4164 # Facemask examine: "Stops me breathing in too much dust." - id: 4166 # Earmuffs examine: "These will protect my ears from loud noise." - id: 4168 # Nose peg examine: "Protects me from any bad smells." - id: 4170 # Slayer's staff examine: "An old and magical staff." - id: 4172 # Broad arrows examine: "Arrows with a wider than normal tip." - id: 4173 # Broad arrows examine: "Arrows with a wider than normal tip." - id: 4174 # Broad arrows examine: "Arrows with a wider than normal tip." - id: 4175 # Broad arrows examine: "Arrows with a wider than normal tip." - id: 4179 # Stick examine: "For playing fetch with." - id: 4180 # Dragon platelegs examine: "These look pretty heavy." - id: 4182 # Goutweed examine: "A pale, tough looking herb." - id: 4183 # Star amulet examine: "A six-pointed marble and obsidian amulet" - id: 4184 # Cavern key examine: "Upon close examination, this seems to be a key." - id: 4185 # Tower key examine: "Upon close examination, this seems to be a key." - id: 4186 # Shed key examine: "Upon close examination, this seems to be a key." - id: 4187 # Marble amulet examine: "Triangular in shape, made from marble, and as large as your hand." - id: 4188 # Obsidian amulet examine: "Triangular in shape, made from obsidian, and as large as your hand." - id: 4189 # Garden cane examine: "A length of garden cane." - id: 4190 # Garden brush examine: "A typical garden brush." - id: 4191 # Extended brush examine: "A typical garden brush, with a cane tied to it." - id: 4192 # Extended brush examine: "A typical garden brush, with two canes tied to it." - id: 4193 # Extended brush examine: "A typical garden brush, with three canes tied to it." - id: 4194 # Torso examine: "A decomposing torso, from which issues the acrid stench of the grave." - id: 4195 # Arms examine: "A pair of limp, dead arms." - id: 4196 # Legs examine: "A pair of lifeless, rotting legs." - id: 4197 # Decapitated head examine: "A gruesome, decapitated head, whose brain has rotted away." - id: 4198 # Decapitated head examine: "A gruesome, decapitated head - its eyes stare lifelessly at nothing." - id: 4199 # Pickled brain examine: "A pickled brain, submerged inside a jar of vinegar." - id: 4200 # Conductor mould examine: "A mould for making silver lightning conductors." - id: 4201 # Conductor examine: "A silver lightning conductor." - id: 4202 # Ring of charos examine: "The Ring of Charos." - id: 4203 # Journal examine: "A book." - id: 4204 # Letter examine: "A letter, clearly hastily written." - id: 4205 # Consecration seed examine: "This consecration seed looks grey and dead." - id: 4206 # Consecration seed examine: "This consecration seed glows with a warm light." - id: 4209 # Cadarn lineage examine: "A book on Cadarn clan history." - id: 4212 # New crystal bow examine: "A nice sturdy magical bow." - id: 4224 # New crystal shield examine: "A nice sturdy crystal shield." - id: 4236 # Signed oak bow examine: "This bow has been signed by Robin, Master Bowman." - id: 4237 # Nettle-water examine: "It's a bowl of water, with some nettles in it." - id: 4239 # Nettle tea examine: "It's a bowl of nettle tea." - id: 4240 # Nettle tea examine: "It's a bowl of milky nettle tea." - id: 4241 # Nettles examine: "A handful of nettles." - id: 4242 # Cup of tea examine: "A nice cup of nettle tea." - id: 4243 # Cup of tea examine: "A milky cup of nettle tea." - id: 4244 # Porcelain cup examine: "A porcelain cup." - id: 4245 # Cup of tea examine: "Some nettle tea in a porcelain cup." - id: 4246 # Cup of tea examine: "Some milky nettle tea in a porcelain cup." - id: 4247 # Mystical robes examine: "The Robes of Necrovarus." - id: 4248 # Book of haricanto examine: "The Book of Haricanto." - id: 4249 # Translation manual examine: "A translation manual." - id: 4250 # Ghostspeak amulet examine: "The amulet of ghostspeak glows green from the crone's enchantment." - id: 4251 # Ectophial examine: "The Ectophial." - id: 4252 # Ectophial examine: "The Ectophial." - id: 4253 # Model ship examine: "A small wooden ship." - id: 4254 # Model ship examine: "A small wooden ship with a silk flag." - id: 4255 # Bonemeal examine: "A pot of crushed bones." - id: 4256 # Bonemeal examine: "A pot of crushed bat bones." - id: 4257 # Bonemeal examine: "A pot of crushed big bones." - id: 4258 # Bonemeal examine: "A pot of crushed burnt bones." - id: 4259 # Bonemeal examine: "A pot of crushed burnt jogre bones." - id: 4260 # Bonemeal examine: "A pot of crushed baby dragon bones." - id: 4261 # Bonemeal examine: "A pot of crushed dragon bones." - id: 4262 # Bonemeal examine: "A pot of crushed wolf bones." - id: 4263 # Bonemeal examine: "A pot of crushed small ninja monkey bones." - id: 4264 # Bonemeal examine: "A pot of crushed medium ninja monkey bones." - id: 4265 # Bonemeal examine: "A pot of crushed gorilla monkey bones." - id: 4266 # Bonemeal examine: "A pot of crushed bearded gorilla monkey bones." - id: 4267 # Bonemeal examine: "A pot of crushed monkey bones." - id: 4268 # Bonemeal examine: "A pot of crushed small zombie monkey bones." - id: 4269 # Bonemeal examine: "A pot of crushed large zombie monkey bones." - id: 4270 # Bonemeal examine: "A pot of crushed skeleton bones." - id: 4271 # Bonemeal examine: "A pot of crushed jogre bones." - id: 4272 # Bone key examine: "A key dropped by Necrovarus." - id: 4273 # Chest key examine: "A key to a chest." - id: 4274 # Map scrap examine: "A section from some kind of map." - id: 4275 # Map scrap examine: "A section from some kind of map." - id: 4276 # Map scrap examine: "A section from some kind of map." - id: 4277 # Treasure map examine: "A complete treasure map." - id: 4278 # Ecto-token examine: "A token with ectoplasm on it." - id: 4283 # Petition form examine: "A scroll of paper containing signatures." - id: 4284 # Bedsheet examine: "It's a bedsheet." - id: 4285 # Bedsheet examine: "It's an ectoplasm-covered bedsheet." - id: 4286 # Bucket of slime examine: "It's a bucket of ectoplasm." - id: 4287 # Raw beef examine: "This raw beef is rancid." - id: 4289 # Raw chicken examine: "This raw chicken is rancid." - id: 4291 # Cooked chicken examine: "This cooked chicken looks disgusting." - id: 4293 # Cooked meat examine: "I wouldn't eat that if I were you." - id: 4298 # Ham shirt examine: "The label says 'Vivid Crimson', but it looks pink to me!" - id: 4300 # Ham robe examine: "The label says 'Vivid Crimson', but it looks pink to me!" - id: 4302 # Ham hood examine: "Light-weight head protection and eye shield." - id: 4304 # Ham cloak examine: "A HAM cape." - id: 4315 # Team-1 cape examine: "Ooohhh look at the pretty colours..." - id: 4317 # Team-2 cape examine: "Ooohhh look at the pretty colours..." - id: 4319 # Team-3 cape examine: "Ooohhh look at the pretty colours..." - id: 4321 # Team-4 cape examine: "Ooohhh look at the pretty colours..." - id: 4323 # Team-5 cape examine: "Ooohhh look at the pretty colours..." - id: 4325 # Team-6 cape examine: "Ooohhh look at the pretty colours..." - id: 4327 # Team-7 cape examine: "Ooohhh look at the pretty colours..." - id: 4329 # Team-8 cape examine: "Ooohhh look at the pretty colours..." - id: 4331 # Team-9 cape examine: "Ooohhh look at the pretty colours..." - id: 4333 # Team-10 cape examine: "Ooohhh look at the pretty colours..." - id: 4335 # Team-11 cape examine: "Ooohhh look at the pretty colours..." - id: 4337 # Team-12 cape examine: "Ooohhh look at the pretty colours..." - id: 4339 # Team-13 cape examine: "Ooohhh look at the pretty colours..." - id: 4341 # Team-14 cape examine: "Ooohhh look at the pretty colours..." - id: 4343 # Team-15 cape examine: "Ooohhh look at the pretty colours..." - id: 4345 # Team-16 cape examine: "Ooohhh look at the pretty colours..." - id: 4347 # Team-17 cape examine: "Ooohhh look at the pretty colours..." - id: 4349 # Team-18 cape examine: "Ooohhh look at the pretty colours..." - id: 4351 # Team-19 cape examine: "Ooohhh look at the pretty colours..." - id: 4353 # Team-20 cape examine: "Ooohhh look at the pretty colours..." - id: 4355 # Team-21 cape examine: "Ooohhh look at the pretty colours..." - id: 4357 # Team-22 cape examine: "Ooohhh look at the pretty colours..." - id: 4359 # Team-23 cape examine: "Ooohhh look at the pretty colours..." - id: 4361 # Team-24 cape examine: "Ooohhh look at the pretty colours..." - id: 4363 # Team-25 cape examine: "Ooohhh look at the pretty colours..." - id: 4365 # Team-26 cape examine: "Ooohhh look at the pretty colours..." - id: 4367 # Team-27 cape examine: "Ooohhh look at the pretty colours..." - id: 4369 # Team-28 cape examine: "Ooohhh look at the pretty colours..." - id: 4371 # Team-29 cape examine: "Ooohhh look at the pretty colours..." - id: 4373 # Team-30 cape examine: "Ooohhh look at the pretty colours..." - id: 4375 # Team-31 cape examine: "Ooohhh look at the pretty colours..." - id: 4377 # Team-32 cape examine: "Ooohhh look at the pretty colours..." - id: 4379 # Team-33 cape examine: "Ooohhh look at the pretty colours..." - id: 4381 # Team-34 cape examine: "Ooohhh look at the pretty colours..." - id: 4383 # Team-35 cape examine: "Ooohhh look at the pretty colours..." - id: 4385 # Team-36 cape examine: "Ooohhh look at the pretty colours..." - id: 4387 # Team-37 cape examine: "Ooohhh look at the pretty colours..." - id: 4389 # Team-38 cape examine: "Ooohhh look at the pretty colours..." - id: 4391 # Team-39 cape examine: "Ooohhh look at the pretty colours..." - id: 4393 # Team-40 cape examine: "Ooohhh look at the pretty colours..." - id: 4395 # Team-41 cape examine: "Ooohhh look at the pretty colours..." - id: 4397 # Team-42 cape examine: "Ooohhh look at the pretty colours..." - id: 4399 # Team-43 cape examine: "Ooohhh look at the pretty colours..." - id: 4401 # Team-44 cape examine: "Ooohhh look at the pretty colours..." - id: 4403 # Team-45 cape examine: "Ooohhh look at the pretty colours..." - id: 4405 # Team-46 cape examine: "Ooohhh look at the pretty colours..." - id: 4407 # Team-47 cape examine: "Ooohhh look at the pretty colours..." - id: 4409 # Team-48 cape examine: "Ooohhh look at the pretty colours..." - id: 4411 # Team-49 cape examine: "Ooohhh look at the pretty colours..." - id: 4413 # Team-50 cape examine: "Ooohhh look at the pretty colours..." - id: 4415 # Blunt axe examine: "A jungle forester's blunt axe." - id: 4416 # Herbal tincture examine: "A strong medicinal brew for heavy chests." - id: 4417 # Guthix rest(4) examine: "A cup of Guthix Rest." - id: 4419 # Guthix rest(3) examine: "A cup of Guthix Rest." - id: 4421 # Guthix rest(2) examine: "A cup of Guthix Rest." - id: 4423 # Guthix rest(1) examine: "A cup of Guthix Rest." - id: 4425 # Stodgy mattress examine: "A half-filled feather mattress." - id: 4426 # Comfy mattress examine: "A comfy-looking feather mattress." - id: 4427 # Iron oxide examine: "Looks like a bunch of rust to me." - id: 4428 # Animate rock scroll examine: "An animate rock spell is written on this parchment." - id: 4429 # Broken vane part examine: "These weathervane directionals are broken." - id: 4430 # Directionals examine: "The weathervane directionals should work now." - id: 4431 # Broken vane part examine: "This weathervane ornament is damaged." - id: 4432 # Ornament examine: "A fixed Weathervane ornament." - id: 4433 # Broken vane part examine: "A broken Weathervane pillar." - id: 4434 # Weathervane pillar examine: "A fixed weathervane rotating pillar." - id: 4435 # Weather report examine: "Clear skies ahead, with some chance of showers, thunderstorms, ice and hail." - id: 4436 # Airtight pot examine: "This is pretty well sealed." - id: 4438 # Unfired pot lid examine: "This needs firing, then it should fit on a normal-sized pot." - id: 4440 # Pot lid examine: "This should fit on a normal-sized pot." - id: 4442 # Breathing salts examine: "An airtight pot with something inside, most likely breathing salts." - id: 4443 # Chicken cage examine: "A large cage for transporting chickens." - id: 4444 # Sharpened axe examine: "A jungle forester's super sharp axe." - id: 4445 # Red mahogany log examine: "Some mahogany logs which have been professionally cured." - id: 4446 # Steel key ring examine: "I can store my keys here." - id: 4447 # Antique lamp examine: "I wonder what happens if I rub it..." - id: 4456 # Bowl of hot water examine: "It's a bowl of hot water." - id: 4458 # Cup of water examine: "A cup of water." - id: 4460 # Cup of hot water examine: "It's hot!" - id: 4462 # Ruined herb tea examine: "A ruined herb tea." - id: 4464 # Herb tea mix examine: "An unfinished herb tea made up of water and harralander." - id: 4466 # Herb tea mix examine: "An unfinished herb tea made up of water and guam." - id: 4468 # Herb tea mix examine: "An unfinished herb tea made up of water and marrentill." - id: 4470 # Herb tea mix examine: "An unfinished herb tea made up of water, harralander and marrentill." - id: 4472 # Herb tea mix examine: "An unfinished herb tea made up of water, harralander and guam." - id: 4474 # Herb tea mix examine: "An unfinished herb tea made up of water and 2 doses of guam." - id: 4476 # Herb tea mix examine: "An unfinished herb tea made up of water, guam and marrentill." - id: 4478 # Herb tea mix examine: "An unfinished herb tea made up of water, harralander, marrentill and guam." - id: 4480 # Herb tea mix examine: "An unfinished herb tea made up of water, 2 doses of guam and marrentill." - id: 4482 # Herb tea mix examine: "An unfinished herb tea made up of water, 2 doses of guam and harralander." - id: 4484 # Safety guarantee examine: "The strange characters supposedly grant Svidi safe passage into Rellekka." - id: 4485 # White pearl examine: "This fruit is known as White Pearl. Should taste good." - id: 4486 # White pearl seed examine: "You can grow this seed even in cold mountain ranges!" - id: 4487 # Half a rock examine: "It's a piece of the Ancient Rock of the mountain people. It's still just a stone." - id: 4488 # Corpse of woman examine: "The corpse of a woman who died long ago." - id: 4489 # Asleif's necklace examine: "This used to belong to Asleif, daughter of the mountain camp chieftain." - id: 4490 # Mud examine: "Yuck, it's sticky, dirty mud." - id: 4492 # Muddy rock examine: "A muddy rock." - id: 4494 # Pole examine: "It's just a long stick, really." - id: 4496 # Broken pole examine: "Splintered into pieces, it has become completely useless to you." - id: 4498 # Rope examine: "A coil of rope." - id: 4500 # Pole examine: "It's just a long stick, really." - id: 4502 # Bearhead examine: "Quite ferocious looking." - id: 4503 # Decorative sword examine: "A very decorative sword." - id: 4504 # Decorative armour examine: "Very decorative armour." - id: 4505 # Decorative armour examine: "Very decorative armour." - id: 4506 # Decorative helm examine: "A very decorative helm." - id: 4507 # Decorative shield examine: "A very decorative shield." - id: 4508 # Decorative sword examine: "A very decorative sword." - id: 4509 # Decorative armour examine: "Very decorative armour." - id: 4510 # Decorative armour examine: "Very decorative armour." - id: 4511 # Decorative helm examine: "A very decorative helm." - id: 4512 # Decorative shield examine: "A very decorative shield." - id: 4513 # Castlewars hood examine: "The colours of Saradomin." - id: 4514 # Castlewars cloak examine: "A fine castlewars Cape." - id: 4515 # Castlewars hood examine: "The colours of Zamorak." - id: 4516 # Castlewars cloak examine: "A fine castlewars Cape." - id: 4517 # Giant frog legs examine: "This could feed a family of gnomes for a week!" - id: 4522 # Oil lamp examine: "Not the genie sort." - id: 4524 # Oil lamp examine: "Not the genie sort." - id: 4529 # Candle lantern examine: "A candle in a glass cage." - id: 4531 # Candle lantern examine: "A flickering candle in a glass cage." - id: 4532 # Candle lantern examine: "A candle in a glass cage." - id: 4534 # Candle lantern examine: "A flickering candle in a glass cage." - id: 4537 # Oil lantern examine: "An unlit oil lantern." - id: 4539 # Oil lantern examine: "It lights your way through the dark places of the earth." - id: 4540 # Oil lantern frame examine: "Add the glass to complete." - id: 4542 # Lantern lens examine: "A roughly circular disc of glass." - id: 4548 # Bullseye lantern examine: "A sturdy steel lantern." - id: 4550 # Bullseye lantern examine: "A sturdy steel lantern casting a bright beam." - id: 4551 # Spiny helmet examine: "You don't want to wear it inside-out." - id: 4561 # Purple sweets examine: "Not likely to last until next Halloween." - id: 4566 # Rubber chicken examine: "Perhaps not the most powerful weapon in Gielinor." - id: 4567 # Gold helmet examine: "Made of gold and white gold." - id: 4568 # Dwarven lore examine: "The book is almost falling apart, you'll have to handle it quite carefully." - id: 4569 # Book page 1 examine: "A missing page from Rolad's book! It seems to be the first one." - id: 4570 # Book page 2 examine: "A missing page from Rolad's book! It seems to be the second one." - id: 4571 # Book page 3 examine: "A missing page from Rolad's book! It seems to be the third one." - id: 4572 # Pages examine: "A collection of missing pages from Rolad's book!" - id: 4573 # Pages examine: "A collection of missing pages from Rolad's book!" - id: 4574 # Base schematics examine: "These are the base schematics of a dwarven multicannon" - id: 4575 # Schematic examine: "A transparent overlay - details of something?" - id: 4576 # Schematics examine: "Transparent overlays - details of something?" - id: 4577 # Schematics examine: "Transparent overlays - details of something?" - id: 4578 # Schematic examine: "The assembled schematic for modifying the dwarven multicannon." - id: 4579 # Cannon ball examine: "A heavy gold metal sphere." - id: 4580 # Black spear examine: "A black tipped spear." - id: 4582 # Black spear(p) examine: "A poisoned black tipped spear." - id: 4584 # Black spear(kp) examine: "A Karambwan poisoned black tipped spear." - id: 4585 # Dragon plateskirt examine: "This looks pretty heavy." - id: 4587 # Dragon scimitar examine: "A vicious, curved sword." - id: 4589 # Keys examine: "Keys to the Mayor's house.." - id: 4590 # Jewels examine: "The Mayor of Pollnivneach's wife's jewels." - id: 4593 # Fake beard examine: "Makes me itch." - id: 4597 # Note examine: "A note found in the Mayor's bedroom mentioning the word 'Fibonacci'." - id: 4598 # Note examine: "A list of 5 numbers." - id: 4601 # Ugthanki dung examine: "Dung of the Camelus Horribleus variety." - id: 4602 # Ugthanki dung examine: "Dung of the Camelus Horribleus variety." - id: 4603 # Receipt examine: "A receipt for one 'Camelus Horribleus'." - id: 4604 # Hag's poison examine: "A red viscous liquid in a vial." - id: 4605 # Snake charm examine: "Makes a hissing sound." - id: 4606 # Snake basket examine: "This is used to hold snakes." - id: 4607 # Snake basket full examine: "This basket contains a snake." - id: 4608 # Super kebab examine: "A meaty and very hot kebab." - id: 4610 # Red hot sauce examine: "The bottle feels warm." - id: 4611 # Desert disguise examine: "A disguise suitable for the desert." - id: 4613 # Spinning plate examine: "It has a picture of a dragon on it." - id: 4615 # Letter examine: "An old faded letter." - id: 4616 # Varmen's notes examine: "An archaeologist's notes." - id: 4617 # Display cabinet key examine: "The museum curator's key." - id: 4618 # Statuette examine: "A beautifully-carved stone statuette." - id: 4619 # Strange implement examine: "It's pretty, but you wish you knew what it was." - id: 4620 # Black mushroom examine: "It looks horrible." - id: 4621 # Phoenix feather examine: "A long feather patterned like a flame." - id: 4622 # Black mushroom ink examine: "Black ink made out of mushrooms." - id: 4623 # Phoenix quill pen examine: "A phoenix feather dipped in ink." - id: 4624 # Golem program examine: "It reads 'YOUR TASK IS DONE'." - id: 4627 # Bandit's brew examine: "A cheeky little lager." - id: 4654 # Etchings examine: "A copy of the engravings found on a mysterious stone tablet." - id: 4655 # Translation examine: "A rough translation made from archaeological etchings." - id: 4656 # Warm key examine: "This key is unusually warm to the touch." - id: 4657 # Ring of visibility examine: "A ring that allows you to see things that are normally invisible..." - id: 4658 # Silver pot examine: "A silver pot made by Ruantun." - id: 4659 # Blessed pot examine: "A silver pot made by Ruantun and blessed on Entrana." - id: 4660 # Silver pot examine: "A silver pot made by Ruantun filled with your blood." - id: 4661 # Blessed pot examine: "A blessed silver pot made by Ruantun filled with your blood." - id: 4662 # Silver pot examine: "A silver pot made by Ruantun filled with blood and garlic." - id: 4663 # Blessed pot examine: "A blessed silver pot filled with blood and garlic." - id: 4664 # Silver pot examine: "A silver pot made by Ruantun filled with blood, garlic and spices." - id: 4665 # Blessed pot examine: "A blessed silver pot filled with blood, garlic and spices." - id: 4666 # Silver pot examine: "A silver pot made by Ruantun filled with blood and spices." - id: 4667 # Blessed pot examine: "A blessed silver pot filled with blood and spices." - id: 4668 # Garlic powder examine: "Finely ground garlic powder." - id: 4670 # Blood diamond examine: "The Diamond of Blood." - id: 4671 # Ice diamond examine: "The Diamond of Ice." - id: 4672 # Smoke diamond examine: "The Diamond of Smoke." - id: 4673 # Shadow diamond examine: "The Diamond of Shadow." - id: 4674 # Gilded cross examine: "An old and strangely shaped metal cross." - id: 4675 # Ancient staff examine: "A magical staff of ancient origin..." - id: 4677 # Catspeak amulet examine: "It's an amulet of cat speak. It makes vague purring noises." - id: 4678 # Canopic jar examine: "Has a lid shaped like a man. I think it contains someone's liver. Yuck." - id: 4679 # Canopic jar examine: "Has a lid shaped like an ape. Eeew! I think it contains someone's intestines." - id: 4680 # Canopic jar examine: "Has a lid shaped like a bug. Disgusting! I think there's a stomach inside." - id: 4681 # Canopic jar examine: "Has a lid shaped like a crocodile. Yuck, I think there are lungs inside." - id: 4682 # Holy symbol examine: "Menaphite lucky charm." - id: 4683 # Unholy symbol examine: "Sign of the devourer." - id: 4684 # Linen examine: "One sheet of mummy wrap." - id: 4686 # Embalming manual examine: "Little book of embalming by Bod E. Wrapper." - id: 4687 # Bucket of sap examine: "It's a bucket of sap." - id: 4689 # Pile of salt examine: "A little heap of salt." - id: 4691 # Sphinx's token examine: "Miniature golden statue of a sphinx." - id: 4693 # Full bucket examine: "It's a bucket of salty water." - id: 4694 # Steam rune examine: "A combined Water and Fire Rune." - id: 4695 # Mist rune examine: "A combined Air and Water Rune." - id: 4696 # Dust rune examine: "A combined Air and Earth Rune." - id: 4697 # Smoke rune examine: "A combined Air and Fire Rune." - id: 4698 # Mud rune examine: "A combined Earth and Water Rune." - id: 4699 # Lava rune examine: "A combined Earth and Fire Rune." - id: 4700 # Sapphire lantern examine: "You need to add lamp oil before you can use it." - id: 4701 # Sapphire lantern examine: "A bullseye lantern with a sapphire for a lens." - id: 4702 # Sapphire lantern examine: "A lantern casting a bright blue beam." - id: 4703 # Magic stone examine: "Doesn't look very special." - id: 4704 # Stone bowl examine: "A magic stone bowl for catching the tears of Guthix." - id: 4707 # Crumbling tome examine: "This book must be really old!" - id: 4740 # Bolt rack examine: "Must need a special type of crossbow to use this." - id: 4773 # Bronze brutal examine: "Blunt bronze arrow... ouch." - id: 4778 # Iron brutal examine: "Blunt iron arrow... ouch." - id: 4783 # Steel brutal examine: "Blunt steel arrow... ouch." - id: 4788 # Black brutal examine: "Blunt black arrow... ouch." - id: 4793 # Mithril brutal examine: "Blunt mithril arrow... ouch." - id: 4798 # Adamant brutal examine: "Blunt adamantite arrow... ouch." - id: 4803 # Rune brutal examine: "Blunt rune arrow... ouch." - id: 4808 # Black prism examine: "A very black prism." - id: 4809 # Torn page examine: "A half torn necromantic page." - id: 4810 # Ruined backpack examine: "A broken and useless looking backpack with the moniker,'B.Vahn' in it." - id: 4811 # Dragon inn tankard examine: "A white ceramic mug with a dragon insignia." - id: 4812 # Zogre bones examine: "A pile of Zombie Ogre bones." - id: 4814 # Sithik portrait examine: "A classic realist charcoal portrait of Sithik." - id: 4815 # Sithik portrait examine: "A semi-nihilistic, pseudo-impressionistic, half-squarist charcoal sketch of Sithik." - id: 4816 # Signed portrait examine: "A signed classic realist charcoal portrait of Sithik." - id: 4817 # Book of portraiture examine: "A book explaining the art of portraiture." - id: 4818 # Ogre artefact examine: "An ancient ogre artefact - resembling a heavy large helm." - id: 4819 # Bronze nails examine: "Keeps things in place fairly permanently." - id: 4820 # Iron nails examine: "Keeps things in place fairly permanently." - id: 4821 # Black nails examine: "Keeps things in place fairly permanently." - id: 4822 # Mithril nails examine: "Keeps things in place fairly permanently." - id: 4823 # Adamantite nails examine: "Keeps things in place fairly permanently." - id: 4824 # Rune nails examine: "Keeps things in place fairly permanently." - id: 4825 # Unstrung comp bow examine: "An unstrung composite ogre bow." - id: 4827 # Comp ogre bow examine: "A composite ogre bow." - id: 4829 # Book of 'h.a.m' examine: "A book of H.A.M affiliation." - id: 4830 # Fayrg bones examine: "Ancient ogre bones from the ogre burial tomb." - id: 4832 # Raurg bones examine: "Ancient ogre bones from the ogre burial tomb." - id: 4834 # Ourg bones examine: "Ancient ogre bones from the ogre burial tomb." - id: 4836 # Strange potion examine: "Some strange liquid given to you by Zavistic Rarve." - id: 4837 # Necromancy book examine: "A book of necromantic spells." - id: 4839 # Ogre gate key examine: "A key to some sort of special tomb area." - id: 4840 # Unfinished potion examine: "I need another ingredient to finish this rogue's purse potion." - id: 4842 # Relicym's balm(4) examine: "4 doses of Relicym's balm, which helps cure disease." - id: 4844 # Relicym's balm(3) examine: "3 doses of Relicym's balm, which helps cure disease." - id: 4846 # Relicym's balm(2) examine: "2 doses of Relicym's balm, which helps cure disease." - id: 4848 # Relicym's balm(1) examine: "1 dose of Relicym's balm, which helps cure disease." - id: 4850 # Ogre coffin key examine: "A key which opens coffins!." - id: 4852 # Bonemeal examine: "A pot of crushed zogre bones." - id: 4853 # Bonemeal examine: "A pot of crushed fayrg bones." - id: 4854 # Bonemeal examine: "A pot of crushed raurg bones." - id: 4855 # Bonemeal examine: "A pot of crushed ourg bones." - id: 5001 # Raw cave eel examine: "It's incredibly slimy." - id: 5002 # Burnt cave eel examine: "It's no longer slimy, or edible." - id: 5003 # Cave eel examine: "It's a bit slimy." - id: 5004 # Frog spawn examine: "That's disgusting!" - id: 5008 # Brooch examine: "A stone brooch with a symbol on it." - id: 5009 # Goblin symbol book examine: "A book about the ancient goblin tribes." - id: 5010 # Key examine: "The key you stole from Sigmund." - id: 5011 # Silverware examine: "You found the Lumbridge silverware in the HAM cave." - id: 5012 # Peace treaty examine: "A peace treaty between Lumbridge and the Cave Goblins." - id: 5013 # Mining helmet examine: "A helmet with a lamp on it." - id: 5014 # Mining helmet examine: "A helmet with an unlit lamp on it." - id: 5016 # Bone spear examine: "Basic but brutal!" - id: 5018 # Bone club examine: "Basic but brutal!" - id: 5020 # Minecart ticket examine: "A ticket to take you from Keldagrim to the dwarven mines under Ice Mountain." - id: 5021 # Minecart ticket examine: "A ticket to take you from the dwarven mines under Ice Mountain to Keldagrim." - id: 5022 # Minecart ticket examine: "A ticket to take you from Keldagrim to the passage under White Wolf Mountain." - id: 5023 # Minecart ticket examine: "A ticket to take you from the passage under White Wolf Mountain to Keldagrim." - id: 5024 # Woven top examine: "Far too small to wear." - id: 5026 # Woven top examine: "Yellow top, too small for me." - id: 5028 # Woven top examine: "Blue top, very tiny." - id: 5030 # Shirt examine: "Tiny!" - id: 5032 # Shirt examine: "Tiny!" - id: 5034 # Shirt examine: "Tiny!" - id: 5036 # Trousers examine: "A pair of long dwarven trousers... long for dwarves, of course." - id: 5038 # Trousers examine: "A pair of long dwarven trousers... long for dwarves, of course." - id: 5040 # Trousers examine: "A pair of long dwarven trousers... long for dwarves, of course." - id: 5042 # Shorts examine: "These look great, on dwarves!" - id: 5044 # Shorts examine: "Yellow shorts. Far too small for you." - id: 5046 # Shorts examine: "Blue shorts, these would look great on dwarves!" - id: 5048 # Skirt examine: "A brown skirt. Size small!" - id: 5050 # Skirt examine: "Lilac skirt." - id: 5052 # Skirt examine: "A blue skirt." - id: 5056 # Dwarven battleaxe examine: "This looks very rusty and worn." - id: 5057 # Dwarven battleaxe examine: "Three sapphires have been crafted onto the hilt." - id: 5058 # Dwarven battleaxe examine: "This axe blade has been sharpened." - id: 5059 # Dwarven battleaxe examine: "This axe has a sharp blade and there are sapphires in the hilt." - id: 5062 # Left boot examine: "One of a pair I assume." - id: 5063 # Right boot examine: "A good looking boot, for the right foot. Literally." - id: 5064 # Exquisite boots examine: "A lovely pair of boots." - id: 5065 # Book on costumes examine: "An old library book. It bears the title 'Scholars Guide to Dwarven Costumes'." - id: 5066 # Meeting notes examine: "These notes are from a meeting of the Keldagrim Consortium." - id: 5067 # Exquisite clothes examine: "Clothes for the sculptor's model." - id: 5070 # Bird nest examine: "It's a bird's nest with an egg in it." - id: 5071 # Bird nest examine: "It's a bird's nest with an egg in it." - id: 5072 # Bird nest examine: "It's a bird's nest with an egg in it." - id: 5073 # Bird nest examine: "It's a bird's nest with some seeds in it." - id: 5074 # Bird nest examine: "It's a bird's nest with a ring in it." - id: 5075 # Bird nest examine: "It's an empty bird's nest." - id: 5076 # Bird's egg examine: "A red bird's egg." - id: 5077 # Bird's egg examine: "A blue bird's egg." - id: 5078 # Bird's egg examine: "A green bird's egg." - id: 5096 # Marigold seed examine: "A marigold seed - plant in a flower patch." - id: 5097 # Rosemary seed examine: "A rosemary seed - plant in a flower patch." - id: 5098 # Nasturtium seed examine: "A nasturtium seed - plant in a flower patch." - id: 5099 # Woad seed examine: "A woad seed - plant in a flower patch." - id: 5100 # Limpwurt seed examine: "A limpwurt seed - plant in a flower patch." - id: 5101 # Redberry seed examine: "A redberry bush seed - plant in a bush patch." - id: 5102 # Cadavaberry seed examine: "A cadavaberry bush seed - plant in a bush patch." - id: 5103 # Dwellberry seed examine: "A dwellberry bush seed - plant in a bush patch." - id: 5104 # Jangerberry seed examine: "A jangerberry bush seed - plant in a bush patch." - id: 5105 # Whiteberry seed examine: "A whiteberry bush seed - plant in a bush patch." - id: 5106 # Poison ivy seed examine: "A poison ivy bush seed - plant in a bush patch." - id: 5280 # Cactus seed examine: "A Cactus seed - plant in a cactus patch." - id: 5281 # Belladonna seed examine: "Also known as Deadly Nightshade - plant in a belladonna patch." - id: 5282 # Mushroom spore examine: "A Bittercap mushroom spore - plant in a mushroom patch." - id: 5283 # Apple tree seed examine: "Plant in a plantpot of soil to grow a sapling." - id: 5284 # Banana tree seed examine: "Plant in a plantpot of soil to grow a sapling." - id: 5285 # Orange tree seed examine: "Plant in a plantpot of soil to grow a sapling." - id: 5286 # Curry tree seed examine: "Plant in a plantpot of soil to grow a sapling." - id: 5287 # Pineapple seed examine: "Plant in a plantpot of soil to grow a sapling." - id: 5288 # Papaya tree seed examine: "Plant in a plantpot of soil to grow a sapling." - id: 5289 # Palm tree seed examine: "Plant in a plantpot of soil to grow a sapling." - id: 5290 # Calquat tree seed examine: "Plant in a plantpot of soil to grow a sapling." - id: 5291 # Guam seed examine: "A guam seed - plant in a herb patch." - id: 5292 # Marrentill seed examine: "A marrentill seed - plant in a herb patch." - id: 5293 # Tarromin seed examine: "A tarromin seed - plant in a herb patch." - id: 5294 # Harralander seed examine: "A harralander seed - plant in a herb patch." - id: 5295 # Ranarr seed examine: "A ranarr seed - plant in a herb patch." - id: 5296 # Toadflax seed examine: "A toadflax seed - plant in a herb patch." - id: 5297 # Irit seed examine: "An irit seed - plant in a herb patch." - id: 5298 # Avantoe seed examine: "An avantoe seed - plant in a herb patch." - id: 5299 # Kwuarm seed examine: "A kwuarm seed - plant in a herb patch." - id: 5300 # Snapdragon seed examine: "A snapdragon seed - plant in a herb patch." - id: 5301 # Cadantine seed examine: "A cadantine seed - plant in a herb patch." - id: 5302 # Lantadyme seed examine: "A lantadyme seed - plant in a herb patch." - id: 5303 # Dwarf weed seed examine: "A dwarf weed seed - plant in a herb patch." - id: 5304 # Torstol seed examine: "A torstol seed - plant in a herb patch." - id: 5305 # Barley seed examine: "A barley seed - plant in a hops patch." - id: 5306 # Jute seed examine: "A jute plant seed - plant in a hops patch." - id: 5307 # Hammerstone seed examine: "A Hammerstone hop seed - plant in a hops patch." - id: 5308 # Asgarnian seed examine: "An Asgarnian hop seed - plant in a hops patch." - id: 5309 # Yanillian seed examine: "A Yanillian hop seed - plant in a hops patch." - id: 5310 # Krandorian seed examine: "A Krandorian hop seed - plant in a hops patch." - id: 5311 # Wildblood seed examine: "A Wildblood hop seed - plant in a hops patch." - id: 5312 # Acorn examine: "Plant this in a plantpot of soil to grow a sapling." - id: 5313 # Willow seed examine: "Plant this in a plantpot of soil to grow a sapling." - id: 5314 # Maple seed examine: "Plant this in a plantpot of soil to grow a sapling." - id: 5315 # Yew seed examine: "Plant this in a plantpot of soil to grow a sapling." - id: 5316 # Magic seed examine: "Plant this in a plantpot of soil to grow a sapling." - id: 5317 # Spirit seed examine: "Plant this in a plantpot of soil to grow a sapling." - id: 5318 # Potato seed examine: "A potato seed - plant in an allotment." - id: 5319 # Onion seed examine: "An onion seed - plant in an allotment." - id: 5320 # Sweetcorn seed examine: "A sweetcorn seed - plant in an allotment." - id: 5321 # Watermelon seed examine: "A watermelon seed - plant in an allotment." - id: 5322 # Tomato seed examine: "A tomato seed - plant in an allotment." - id: 5323 # Strawberry seed examine: "A strawberry seed - plant in an allotment." - id: 5324 # Cabbage seed examine: "A cabbage seed - plant in an allotment." - id: 5325 # Gardening trowel examine: "Not suitable for archaeological digs." - id: 5327 # Spade handle examine: "I need to attach this to its head." - id: 5328 # Spade head examine: "I need to attach this to its handle." - id: 5329 # Secateurs examine: "Good for pruning away diseased leaves." - id: 5331 # Watering can examine: "This watering can is empty." - id: 5333 # Watering can(1) examine: "This watering can is almost empty." - id: 5334 # Watering can(2) examine: "This watering can is three-quarters empty." - id: 5335 # Watering can(3) examine: "This watering can is just under half-full." - id: 5336 # Watering can(4) examine: "Some would say this watering can is half-full, others half-empty." - id: 5337 # Watering can(5) examine: "This watering can is just over half-full." - id: 5338 # Watering can(6) examine: "This watering can is three quarters full." - id: 5339 # Watering can(7) examine: "This watering can is almost completely full." - id: 5340 # Watering can(8) examine: "This watering can is completely full." - id: 5341 # Rake examine: "Use this to clear weeds." - id: 5343 # Seed dibber examine: "Use this to plant seeds with." - id: 5345 # Gardening boots examine: "A pair of gardening boots." - id: 5347 # Rake handle examine: "I need to reattach this to its head." - id: 5348 # Rake head examine: "I need to reattach this to its handle." - id: 5352 # Unfired plant pot examine: "An unfired plant pot." - id: 5358 # Oak seedling examine: "An acorn has been sown in this plant pot." - id: 5359 # Willow seedling examine: "A willow tree seed has been sown in this plant pot." - id: 5360 # Maple seedling examine: "A maple tree seed has been sown in this plant pot." - id: 5361 # Yew seedling examine: "A yew tree seed has been sown in this plant pot." - id: 5362 # Magic seedling examine: "A magic tree seed has been sown in this plant pot." - id: 5363 # Spirit seedling examine: "A spirit tree seed has been sown in this plant pot." - id: 5370 # Oak sapling examine: "This sapling is ready to be replanted in a tree patch." - id: 5371 # Willow sapling examine: "This sapling is ready to be replanted in a tree patch." - id: 5372 # Maple sapling examine: "This sapling is ready to be replanted in a tree patch." - id: 5373 # Yew sapling examine: "This sapling is ready to be replanted in a tree patch." - id: 5374 # Magic sapling examine: "This sapling is ready to be replanted in a tree patch." - id: 5375 # Spirit sapling examine: "This sapling is ready to be replanted in a Spirit patch." - id: 5376 # Basket examine: "An empty fruit basket." - id: 5378 # Apples(1) examine: "A fruit basket filled with apples." - id: 5380 # Apples(2) examine: "A fruit basket filled with apples." - id: 5382 # Apples(3) examine: "A fruit basket filled with apples." - id: 5384 # Apples(4) examine: "A fruit basket filled with apples." - id: 5386 # Apples(5) examine: "A fruit basket filled with apples." - id: 5388 # Oranges(1) examine: "A fruit basket filled with oranges." - id: 5390 # Oranges(2) examine: "A fruit basket filled with oranges." - id: 5392 # Oranges(3) examine: "A fruit basket filled with oranges." - id: 5394 # Oranges(4) examine: "A fruit basket filled with oranges." - id: 5396 # Oranges(5) examine: "A fruit basket filled with oranges." - id: 5398 # Strawberries(1) examine: "A fruit basket filled with strawberries." - id: 5400 # Strawberries(2) examine: "A fruit basket filled with strawberries." - id: 5402 # Strawberries(3) examine: "A fruit basket filled with strawberries." - id: 5404 # Strawberries(4) examine: "A fruit basket filled with strawberries." - id: 5406 # Strawberries(5) examine: "A fruit basket filled with strawberries." - id: 5408 # Bananas(1) examine: "A fruit basket filled with bananas." - id: 5410 # Bananas(2) examine: "A fruit basket filled with bananas." - id: 5412 # Bananas(3) examine: "A fruit basket filled with bananas." - id: 5414 # Bananas(4) examine: "A fruit basket filled with bananas." - id: 5416 # Bananas(5) examine: "A fruit basket filled with bananas." - id: 5418 # Empty sack examine: "An empty vegetable sack." - id: 5420 # Potatoes(1) examine: "There is 1 potato in this sack." - id: 5422 # Potatoes(2) examine: "There are 2 potatoes in this sack." - id: 5424 # Potatoes(3) examine: "There are 3 potatoes in this sack." - id: 5426 # Potatoes(4) examine: "There are 4 potatoes in this sack." - id: 5428 # Potatoes(5) examine: "There are 5 potatoes in this sack." - id: 5430 # Potatoes(6) examine: "There are 6 potatoes in this sack." - id: 5432 # Potatoes(7) examine: "There are 7 potatoes in this sack." - id: 5434 # Potatoes(8) examine: "There are 8 potatoes in this sack." - id: 5436 # Potatoes(9) examine: "There are 9 potatoes in this sack." - id: 5438 # Potatoes(10) examine: "There are 10 potatoes in this sack." - id: 5440 # Onions(1) examine: "There is 1 onion in this sack." - id: 5442 # Onions(2) examine: "There are 2 onions in this sack." - id: 5444 # Onions(3) examine: "There are 3 onions in this sack." - id: 5446 # Onions(4) examine: "There are 4 onions in this sack." - id: 5448 # Onions(5) examine: "There are 5 onions in this sack." - id: 5450 # Onions(6) examine: "There are 6 onions in this sack." - id: 5452 # Onions(7) examine: "There are 7 onions in this sack." - id: 5454 # Onions(8) examine: "There are 8 onions in this sack." - id: 5456 # Onions(9) examine: "There are 9 onions in this sack." - id: 5458 # Onions(10) examine: "There are 10 onions in this sack." - id: 5460 # Cabbages(1) examine: "There is 1 cabbage in this sack." - id: 5462 # Cabbages(2) examine: "There are 2 cabbages in this sack." - id: 5464 # Cabbages(3) examine: "There are 3 cabbages in this sack." - id: 5466 # Cabbages(4) examine: "There are 4 cabbages in this sack." - id: 5468 # Cabbages(5) examine: "There are 5 cabbages in this sack." - id: 5470 # Cabbages(6) examine: "There are 6 cabbages in this sack." - id: 5472 # Cabbages(7) examine: "There are 7 cabbages in this sack." - id: 5474 # Cabbages(8) examine: "There are 8 cabbages in this sack." - id: 5476 # Cabbages(9) examine: "There are 9 cabbages in this sack." - id: 5478 # Cabbages(10) examine: "There are 10 cabbages in this sack." - id: 5480 # Apple seedling examine: "An apple tree seed has been sown in this plant pot." - id: 5481 # Banana seedling examine: "A banana tree seed has been sown in this plant pot." - id: 5482 # Orange seedling examine: "An orange tree seed has been sown in this plant pot." - id: 5483 # Curry seedling examine: "A curry tree seed has been sown in this plant pot." - id: 5484 # Pineapple seedling examine: "A pineapple plant seed has been sown in this plant pot." - id: 5485 # Papaya seedling examine: "A papaya tree seed has been sown in this plant pot." - id: 5486 # Palm seedling examine: "A palm tree seed has been sown in this plant pot." - id: 5487 # Calquat seedling examine: "A Calquat tree seed has been sown in this plant pot." - id: 5496 # Apple sapling examine: "This sapling is ready to be replanted in a fruit tree patch." - id: 5497 # Banana sapling examine: "This sapling is ready to be replanted in a fruit tree patch." - id: 5498 # Orange sapling examine: "This sapling is ready to be replanted in a fruit tree patch." - id: 5499 # Curry sapling examine: "This sapling is ready to be replanted in a fruit tree patch." - id: 5500 # Pineapple sapling examine: "This sapling is ready to be replanted in a fruit tree patch." - id: 5501 # Papaya sapling examine: "This sapling is ready to be replanted in a fruit tree patch." - id: 5502 # Palm sapling examine: "This sapling is ready to be replanted in a fruit tree patch." - id: 5503 # Calquat sapling examine: "This sapling is ready to be replanted in a Calquat tree patch." - id: 5504 # Strawberry examine: "A freshly picked strawberry." - id: 5506 # Old man's message examine: "The Wise Old Man of Draynor Village asked you to take this to someone." - id: 5507 # Strange book examine: "A tatty old book belonging to the Wise Old Man of Draynor Village." - id: 5508 # Book of folklore examine: "A tatty old book belonging to the Wise Old Man of Draynor Village." - id: 5509 # Small pouch examine: "A small pouch used for storing essence." - id: 5510 # Medium pouch examine: "A medium-sized pouch used for storing essence." - id: 5511 # Medium pouch examine: "A damaged medium-sized pouch used for storing essence." - id: 5512 # Large pouch examine: "A large pouch used for storing essence." - id: 5513 # Large pouch examine: "A large damaged pouch used for storing essence." - id: 5514 # Giant pouch examine: "A giant-sized pouch used for storing essence." - id: 5515 # Giant pouch examine: "A damaged giant-sized pouch used for storing essence." - id: 5516 # Elemental talisman examine: "A mysterious power emanates from the talisman..." - id: 5518 # Scrying orb examine: "This contains mystical teleport information..." - id: 5519 # Scrying orb examine: "This orb apparently contains a cypher spell." - id: 5520 # Abyssal book examine: "Some research notes on abyssal space." - id: 5521 # Binding necklace examine: "A necklace embedded with mystical power." - id: 5523 # Tiara mould examine: "A mould for tiaras." - id: 5525 # Tiara examine: "Makes me feel like a Princess." - id: 5527 # Air tiara examine: "A tiara infused with the properties of air." - id: 5529 # Mind tiara examine: "A tiara infused with the properties of the mind." - id: 5531 # Water tiara examine: "A tiara infused with the properties of water." - id: 5533 # Body tiara examine: "A tiara infused with the properties of the body." - id: 5535 # Earth tiara examine: "A tiara infused with the properties of the earth." - id: 5537 # Fire tiara examine: "A tiara infused with the properties of fire." - id: 5539 # Cosmic tiara examine: "A tiara infused with the properties of the cosmos." - id: 5541 # Nature tiara examine: "A tiara infused with the properties of nature." - id: 5543 # Chaos tiara examine: "A tiara infused with the properties of chaos." - id: 5545 # Law tiara examine: "A tiara infused with the properties of law." - id: 5547 # Death tiara examine: "A tiara infused with the properties of death." - id: 5553 # Rogue top examine: "Black banded leather armour, a rogue's dream!" - id: 5554 # Rogue mask examine: "Black banded leather armour, a rogue's dream!" - id: 5555 # Rogue trousers examine: "Black banded leather armour, a rogue's dream!" - id: 5556 # Rogue gloves examine: "Black banded leather gloves, a rogue's dream!" - id: 5557 # Rogue boots examine: "Black banded leather boots, a rogue's dream!" - id: 5558 # Rogue kit examine: "It can do almost anything!" - id: 5559 # Flash powder examine: "A small satchel of bright powder!" - id: 5560 # Stethoscope examine: "A useful hearing aid." - id: 5561 # Mystic jewel examine: "I can escape the Maze with this!" - id: 5568 # Tile examine: "For a mosaic." - id: 5569 # Tiles examine: "For a mosaic." - id: 5570 # Tiles examine: "For a mosaic." - id: 5571 # Tiles examine: "For a mosaic." - id: 5574 # Initiate sallet examine: "An initiate Temple Knight's helm." - id: 5575 # Initiate hauberk examine: "An initiate Temple Knight's Armour." - id: 5576 # Initiate cuisse examine: "An initiate Temple Knight's leg armour." - id: 5577 # Cupric sulfate examine: "A vial of something labelled 'Cupric Sulfate'." - id: 5578 # Acetic acid examine: "A vial of something labelled 'Acetic Acid'." - id: 5579 # Gypsum examine: "A vial of something labelled 'Gypsum'." - id: 5580 # Sodium chloride examine: "A vial of something labelled 'Sodium Chloride'." - id: 5581 # Nitrous oxide examine: "A vial of something labelled 'Nitrous Oxide'." - id: 5582 # Vial of liquid examine: "A vial of something labelled 'Dihydrogen Monoxide'." - id: 5583 # Tin ore powder examine: "A vial of something labelled 'Powdered Tin Ore'." - id: 5584 # Cupric ore powder examine: "A vial of something labelled 'Powdered Cupric Ore'." - id: 5585 # Bronze key examine: "I hope the mould was accurate enough...." - id: 5586 # Metal spade examine: "It's a metal spade with a wooden handle." - id: 5587 # Metal spade examine: "It's a metal spade without a handle." - id: 5588 # Alchemical notes examine: "Looks like a pretty boring read." - id: 5589 # ??? mixture examine: "A very hot vial of something or other. The label says 'Cupric Sulfate'." - id: 5590 # ??? mixture examine: "A very warm vial of something or other. It's a bit lumpy." - id: 5591 # ??? mixture examine: "It looks horrible. I think I messed something up." - id: 5592 # Tin examine: "I could probably pour something into this." - id: 5593 # Tin examine: "It's full of a white lumpy mixture that seems to be hardening." - id: 5594 # Tin examine: "There is an impression of a key embedded in it." - id: 5595 # Tin examine: "There is an impression of a key, filled with tin ore." - id: 5596 # Tin examine: "There is an impression of a key, filled with copper ore." - id: 5597 # Tin examine: "There is an impression of a key, filled with tin and copper ore." - id: 5598 # Tin examine: "There is a bronze key surrounded by plaster in this tin." - id: 5599 # Tin examine: "There is a strange concoction filling this tin." - id: 5600 # Tin examine: "A tin layered with some stuff from a vial." - id: 5601 # Chisel examine: "Good for detailed crafting." - id: 5602 # Bronze wire examine: "Useful for crafting items." - id: 5603 # Shears examine: "For shearing sheep." - id: 5604 # Magnet examine: "A very attractive magnet." - id: 5605 # Knife examine: "A dangerous looking knife." - id: 5606 # Makeover voucher examine: "I can exchange this for one free makeover with the makeover mage." - id: 5607 # Grain examine: "A sack full of grain." - id: 5608 # Fox examine: "I don't think he likes being carried." - id: 5609 # Chicken examine: "He'd be easier to carry if I cooked and ate him first..." - id: 5610 # Hourglass examine: "It's an hourglass." - id: 5615 # Bonemeal examine: "A pot of crushed Shaikahan bones." - id: 5616 # Bronze arrow(p+) examine: "Venomous-looking arrows." - id: 5617 # Iron arrow(p+) examine: "Venomous-looking arrows." - id: 5618 # Steel arrow(p+) examine: "Venomous-looking arrows." - id: 5619 # Mithril arrow(p+) examine: "Venomous-looking arrows." - id: 5620 # Adamant arrow(p+) examine: "Venomous-looking arrows." - id: 5621 # Rune arrow(p+) examine: "Venomous-looking arrows." - id: 5622 # Bronze arrow(p++) examine: "Venomous-looking arrows." - id: 5623 # Iron arrow(p++) examine: "Venomous-looking arrows." - id: 5624 # Steel arrow(p++) examine: "Venomous-looking arrows." - id: 5625 # Mithril arrow(p++) examine: "Venomous-looking arrows." - id: 5627 # Rune arrow(p++) examine: "Venomous-looking arrows." - id: 5628 # Bronze dart(p+) examine: "A deadly poisoned dart with a bronze tip." - id: 5629 # Iron dart(p+) examine: "A deadly poisoned dart with an iron tip." - id: 5630 # Steel dart(p+) examine: "A deadly poisoned dart with a steel tip." - id: 5631 # Black dart(p+) examine: "A deadly poisoned dart with a black tip." - id: 5632 # Mithril dart(p+) examine: "A deadly poisoned dart with a mithril tip." - id: 5633 # Adamant dart(p+) examine: "A deadly poisoned dart with an adamant tip." - id: 5634 # Rune dart(p+) examine: "A deadly poisoned dart with a rune tip." - id: 5635 # Bronze dart(p++) examine: "A deadly poisoned dart with a bronze tip." - id: 5636 # Iron dart(p++) examine: "A deadly poisoned dart with an iron tip." - id: 5637 # Steel dart(p++) examine: "A deadly poisoned dart with a steel tip." - id: 5638 # Black dart(p++) examine: "A deadly poisoned dart with a black tip." - id: 5639 # Mithril dart(p++) examine: "A deadly poisoned dart with a mithril tip." - id: 5640 # Adamant dart(p++) examine: "A deadly poisoned dart with an adamant tip." - id: 5641 # Rune dart(p++) examine: "A deadly poisoned dart with a rune tip." - id: 5642 # Bronze javelin(p+) examine: "A bronze tipped javelin." - id: 5643 # Iron javelin(p+) examine: "An iron tipped javelin." - id: 5644 # Steel javelin(p+) examine: "A steel tipped javelin." - id: 5645 # Mithril javelin(p+) examine: "A mithril tipped javelin." - id: 5647 # Rune javelin(p+) examine: "A rune tipped javelin." - id: 5649 # Iron javelin(p++) examine: "An iron tipped javelin." - id: 5650 # Steel javelin(p++) examine: "A steel tipped javelin." - id: 5651 # Mithril javelin(p++) examine: "A mithril tipped javelin." - id: 5653 # Rune javelin(p++) examine: "A rune tipped javelin." - id: 5654 # Bronze knife(p+) examine: "A finely balanced throwing knife." - id: 5655 # Iron knife(p+) examine: "A finely balanced throwing knife." - id: 5656 # Steel knife(p+) examine: "A finely balanced throwing knife." - id: 5657 # Mithril knife(p+) examine: "A finely balanced throwing knife." - id: 5658 # Black knife(p+) examine: "A finely balanced throwing knife." - id: 5659 # Adamant knife(p+) examine: "A finely balanced throwing knife." - id: 5660 # Rune knife(p+) examine: "A finely balanced throwing knife." - id: 5661 # Bronze knife(p++) examine: "A finely balanced throwing knife." - id: 5662 # Iron knife(p++) examine: "A finely balanced throwing knife." - id: 5663 # Steel knife(p++) examine: "A finely balanced throwing knife." - id: 5664 # Mithril knife(p++) examine: "A finely balanced throwing knife." - id: 5665 # Black knife(p++) examine: "A finely balanced throwing knife." - id: 5667 # Rune knife(p++) examine: "A finely balanced throwing knife." - id: 5668 # Iron dagger(p+) examine: "The blade is covered with poison." - id: 5670 # Bronze dagger(p+) examine: "This dagger is poisoned." - id: 5672 # Steel dagger(p+) examine: "The blade has been poisoned." - id: 5674 # Mithril dagger(p+) examine: "A poisoned Mithril dagger." - id: 5678 # Rune dagger(p+) examine: "The blade is covered with a nasty poison." - id: 5680 # Dragon dagger(p+) examine: "A powerful dagger." - id: 5682 # Black dagger(p+) examine: "This dagger is poisoned." - id: 5686 # Iron dagger(p++) examine: "The blade is covered with poison." - id: 5690 # Steel dagger(p++) examine: "The blade has been poisoned." - id: 5692 # Mithril dagger(p++) examine: "A poisoned Mithril dagger." - id: 5696 # Rune dagger(p++) examine: "The blade is covered with a nasty poison." - id: 5700 # Black dagger(p++) examine: "This dagger is poisoned." - id: 5704 # Bronze spear(p+) examine: "A poisoned bronze tipped spear." - id: 5706 # Iron spear(p+) examine: "A poisoned iron tipped spear." - id: 5708 # Steel spear(p+) examine: "A poisoned steel tipped spear." - id: 5710 # Mithril spear(p+) examine: "A poisoned mithril tipped spear." - id: 5712 # Adamant spear(p+) examine: "A poisoned adamantite tipped spear." - id: 5714 # Rune spear(p+) examine: "A poisoned rune tipped spear." - id: 5716 # Dragon spear(p+) examine: "A poisoned dragon tipped spear." - id: 5718 # Bronze spear(p++) examine: "A poisoned bronze tipped spear." - id: 5720 # Iron spear(p++) examine: "A poisoned iron tipped spear." - id: 5722 # Steel spear(p++) examine: "A poisoned steel tipped spear." - id: 5724 # Mithril spear(p++) examine: "A poisoned mithril tipped spear." - id: 5728 # Rune spear(p++) examine: "A poisoned rune tipped spear." - id: 5730 # Dragon spear(p++) examine: "A poisoned dragon tipped spear." - id: 5734 # Black spear(p+) examine: "A poisoned black tipped spear." - id: 5736 # Black spear(p++) examine: "A poisoned black tipped spear." - id: 5738 # Woad leaf examine: "A slightly bluish leaf." - id: 5739 # Asgarnian ale(m) examine: "This looks a good deal stronger than normal Asgarnian Ale." - id: 5741 # Mature wmb examine: "This looks a good deal stronger than normal Wizards Mind Bomb." - id: 5743 # Greenman's ale(m) examine: "This looks a good deal stronger than normal Greenman's Ale." - id: 5745 # Dragon bitter(m) examine: "This looks a good deal stronger than normal Dragon Bitter." - id: 5747 # Dwarven stout(m) examine: "This looks a good deal stronger than normal Dwarven Stout." - id: 5749 # Moonlight mead(m) examine: "This looks a good deal stronger than normal Moonlight Mead." - id: 5751 # Axeman's folly examine: "This might help me chop harder." - id: 5753 # Axeman's folly(m) examine: "This looks a good deal stronger than normal Axeman's Folly." - id: 5755 # Chef's delight examine: "A fruity, full-bodied ale." - id: 5757 # Chef's delight(m) examine: "This looks a good deal stronger than normal Chef's Delight." - id: 5759 # Slayer's respite examine: "Ale with bite." - id: 5761 # Slayer's respite(m) examine: "This looks a good deal stronger than normal Slayer's Respite." - id: 5763 # Cider examine: "A glass of cider." - id: 5765 # Mature cider examine: "This looks a good deal stronger than normal cider." - id: 5767 # Ale yeast examine: "A pot filled with ale yeast." - id: 5769 # Calquat keg examine: "Sliced and hollowed out to form a keg." - id: 5771 # Dwarven stout(1) examine: "This keg contains 1 pint of Dwarven Stout." - id: 5773 # Dwarven stout(2) examine: "This keg contains 2 pints of Dwarven Stout." - id: 5775 # Dwarven stout(3) examine: "This keg contains 3 pints of Dwarven Stout." - id: 5777 # Dwarven stout(4) examine: "This keg contains 4 pints of Dwarven Stout." - id: 5779 # Asgarnian ale(1) examine: "This keg contains 1 pint of Asgarnian Ale." - id: 5781 # Asgarnian ale(2) examine: "This keg contains 2 pints of Asgarnian Ale." - id: 5783 # Asgarnian ale(3) examine: "This keg contains 3 pints of Asgarnian Ale." - id: 5785 # Asgarnian ale(4) examine: "This keg contains 4 pints of Asgarnian Ale." - id: 5787 # Greenmans ale(1) examine: "This keg contains 1 pint of Greenmans Ale." - id: 5789 # Greenmans ale(2) examine: "This keg contains 2 pints of Greenmans Ale." - id: 5791 # Greenmans ale(3) examine: "This keg contains 3 pints of Greenmans Ale." - id: 5793 # Greenmans ale(4) examine: "This keg contains 4 pints of Greenmans Ale." - id: 5795 # Mind bomb(1) examine: "This keg contains 1 pint of Wizards Mind Bomb." - id: 5797 # Mind bomb(2) examine: "This keg contains 2 pints of Wizards Mind Bomb." - id: 5799 # Mind bomb(3) examine: "This keg contains 3 pints of Wizards Mind Bomb." - id: 5801 # Mind bomb(4) examine: "This keg contains 4 pints of Wizards Mind Bomb." - id: 5803 # Dragon bitter(1) examine: "This keg contains 1 pint of Dragon Bitter." - id: 5805 # Dragon bitter(2) examine: "This keg contains 2 pints of Dragon Bitter." - id: 5807 # Dragon bitter(3) examine: "This keg contains 3 pints of Dragon Bitter." - id: 5809 # Dragon bitter(4) examine: "This keg contains 4 pints of Dragon Bitter." - id: 5811 # Moonlight mead(1) examine: "This keg contains 1 pint of Moonlight Mead." - id: 5813 # Moonlight mead(2) examine: "This keg contains 2 pints of Moonlight Mead." - id: 5815 # Moonlight mead(3) examine: "This keg contains 3 pints of Moonlight Mead." - id: 5817 # Moonlight mead(4) examine: "This keg contains 4 pints of Moonlight Mead." - id: 5819 # Axeman's folly(1) examine: "This keg contains 1 pint of Axeman's Folly." - id: 5821 # Axeman's folly(2) examine: "This keg contains 2 pints of Axeman's Folly." - id: 5823 # Axeman's folly(3) examine: "This keg contains 3 pints of Axeman's Folly." - id: 5825 # Axeman's folly(4) examine: "This keg contains 4 pints of Axeman's Folly." - id: 5827 # Chef's delight(1) examine: "This keg contains 1 pint of Chef's Delight." - id: 5829 # Chef's delight(2) examine: "This keg contains 2 pints of Chef's Delight." - id: 5831 # Chef's delight(3) examine: "This keg contains 3 pints of Chef's Delight." - id: 5833 # Chef's delight(4) examine: "This keg contains 4 pints of Chef's Delight." - id: 5835 # Slayer's respite(1) examine: "This keg contains 1 pint of Slayer's Respite." - id: 5837 # Slayer's respite(2) examine: "This keg contains 2 pints of Slayer's Respite." - id: 5839 # Slayer's respite(3) examine: "This keg contains 3 pints of Slayer's Respite." - id: 5841 # Slayer's respite(4) examine: "This keg contains 4 pints of Slayer's Respite." - id: 5843 # Cider(1) examine: "This keg contains 1 pint of Cider." - id: 5845 # Cider(2) examine: "This keg contains 2 pints of Cider." - id: 5847 # Cider(3) examine: "This keg contains 3 pints of Cider." - id: 5849 # Cider(4) examine: "This keg contains 4 pints of Cider." - id: 5851 # Dwarven stout(m1) examine: "This keg contains 1 pint of mature Dwarven Stout." - id: 5853 # Dwarven stout(m2) examine: "This keg contains 2 pints of mature Dwarven Stout." - id: 5855 # Dwarven stout(m3) examine: "This keg contains 3 pints of mature Dwarven Stout." - id: 5857 # Dwarven stout(m4) examine: "This keg contains 4 pints of mature Dwarven Stout." - id: 5859 # Asgarnian ale(m1) examine: "This keg contains 1 pint of mature Asgarnian Ale." - id: 5861 # Asgarnian ale(m2) examine: "This keg contains 2 pints of mature Asgarnian Ale." - id: 5863 # Asgarnian ale(m3) examine: "This keg contains 3 pints of mature Asgarnian Ale." - id: 5865 # Asgarnian ale(m4) examine: "This keg contains 4 pints of mature Asgarnian Ale." - id: 5867 # Greenmans ale(m1) examine: "This keg contains 1 pint of mature Greenmans Ale." - id: 5869 # Greenmans ale(m2) examine: "This keg contains 2 pints of mature Greenmans Ale." - id: 5871 # Greenmans ale(m3) examine: "This keg contains 3 pints of mature Greenmans Ale." - id: 5873 # Greenmans ale(m4) examine: "This keg contains 4 pints of mature Greenmans Ale." - id: 5875 # Mind bomb(m1) examine: "This keg contains 1 pint of mature Wizards Mind Bomb." - id: 5877 # Mind bomb(m2) examine: "This keg contains 2 pints of mature Wizards Mind Bomb." - id: 5879 # Mind bomb(m3) examine: "This keg contains 3 pints of mature Wizards Mind Bomb." - id: 5881 # Mind bomb(m4) examine: "This keg contains 4 pints of mature Wizards Mind Bomb." - id: 5883 # Dragon bitter(m1) examine: "This keg contains 1 pint of mature Dragon Bitter." - id: 5885 # Dragon bitter(m2) examine: "This keg contains 2 pints of mature Dragon Bitter." - id: 5887 # Dragon bitter(m3) examine: "This keg contains 3 pints of mature Dragon Bitter." - id: 5889 # Dragon bitter(m4) examine: "This keg contains 4 pints of mature Dragon Bitter." - id: 5899 # Axeman's folly(m1) examine: "This keg contains 1 pint of mature Axeman's Folly." - id: 5901 # Axeman's folly(m2) examine: "This keg contains 2 pints of mature Axeman's Folly." - id: 5903 # Axeman's folly(m3) examine: "This keg contains 3 pints of mature Axeman's Folly." - id: 5905 # Axeman's folly(m4) examine: "This keg contains 4 pints of mature Axeman's Folly." - id: 5907 # Chef's delight(m1) examine: "This keg contains 1 pint of mature Chef's Delight." - id: 5909 # Chef's delight(m2) examine: "This keg contains 2 pints of mature Chef's Delight." - id: 5911 # Chef's delight(m3) examine: "This keg contains 3 pints of mature Chef's Delight." - id: 5913 # Chef's delight(m4) examine: "This keg contains 4 pints of mature Chef's Delight." - id: 5923 # Cider(m1) examine: "This keg contains 1 pint of mature Cider." - id: 5925 # Cider(m2) examine: "This keg contains 2 pints of mature Cider." - id: 5927 # Cider(m3) examine: "This keg contains 3 pints of mature Cider." - id: 5929 # Cider(m4) examine: "This keg contains 4 pints of mature Cider." - id: 5931 # Jute fibre examine: "I can weave this to make sacks." - id: 5933 # Willow branch examine: "A branch from a willow tree." - id: 5935 # Coconut milk examine: "A vial filled with coconut milk" - id: 5937 # Weapon poison(+) examine: "A vial of extra strong weapon poison, for spears and daggers." - id: 5940 # Weapon poison(++) examine: "A vial of super strong weapon poison, for spears and daggers." - id: 5943 # Antidote+(4) examine: "4 doses of extra strong antipoison potion." - id: 5945 # Antidote+(3) examine: "3 doses of extra strong antipoison potion." - id: 5947 # Antidote+(2) examine: "2 doses of extra strong antipoison potion." - id: 5949 # Antidote+(1) examine: "1 dose of extra strong antipoison potion." - id: 5952 # Antidote++(4) examine: "4 doses of super strong antipoison potion." - id: 5954 # Antidote++(3) examine: "3 doses of super strong antipoison potion." - id: 5956 # Antidote++(2) examine: "2 doses of super strong antipoison potion." - id: 5958 # Antidote++(1) examine: "1 dose of super strong antipoison potion." - id: 5960 # Tomatoes(1) examine: "A fruit basket filled with tomatoes." - id: 5962 # Tomatoes(2) examine: "A fruit basket filled with tomatoes." - id: 5964 # Tomatoes(3) examine: "A fruit basket filled with tomatoes." - id: 5966 # Tomatoes(4) examine: "A fruit basket filled with tomatoes." - id: 5968 # Tomatoes(5) examine: "A fruit basket filled with tomatoes." - id: 5970 # Curry leaf examine: "I could make a spicy curry with this." - id: 5972 # Papaya fruit examine: "Looks delicious." - id: 5974 # Coconut examine: "It's a coconut." - id: 5978 # Coconut shell examine: "All the milk has been removed." - id: 5980 # Calquat fruit examine: "This is the largest fruit I've ever seen." - id: 5982 # Watermelon examine: "A juicy watermelon." - id: 5984 # Watermelon slice examine: "A slice of watermelon." - id: 5986 # Sweetcorn examine: "Raw sweetcorn." - id: 5988 # Cooked sweetcorn examine: "Delicious cooked sweetcorn." - id: 5990 # Burnt sweetcorn examine: "This sweetcorn has been cooked for too long." - id: 5992 # Apple mush examine: "A bucket of apple mush." - id: 5994 # Hammerstone hops examine: "A handful of Hammerstone Hops." - id: 5996 # Asgarnian hops examine: "A handful of Asgarnian Hops." - id: 5998 # Yanillian hops examine: "A handful of Yanillian Hops." - id: 6000 # Krandorian hops examine: "A handful of Krandorian Hops." - id: 6002 # Wildblood hops examine: "A handful of Wildblood Hops." - id: 6004 # Mushroom examine: "A Bittercap Mushroom" - id: 6006 # Barley examine: "A handful of Barley." - id: 6008 # Barley malt examine: "A handful of barley malt." - id: 6010 # Marigolds examine: "A bunch of marigolds." - id: 6012 # Nasturtiums examine: "A bunch of nasturtiums." - id: 6014 # Rosemary examine: "Some rosemary." - id: 6016 # Cactus spine examine: "Don't prick yourself with this." - id: 6020 # Leaves examine: "A pile of leaves." - id: 6022 # Leaves examine: "A pile of Oak tree leaves." - id: 6024 # Leaves examine: "A pile of Willow tree leaves." - id: 6026 # Leaves examine: "A pile of Yew tree leaves." - id: 6028 # Leaves examine: "A pile of Maple tree leaves." - id: 6030 # Leaves examine: "A pile of Magic tree leaves." - id: 6032 # Compost examine: "Good for plants, helps them grow." - id: 6036 # Plant cure examine: "Use this on plants to cure disease." - id: 6038 # Magic string examine: "I could use this to make jewellery." - id: 6040 # Amulet of nature examine: "An Amulet of Nature." - id: 6043 # Oak roots examine: "The roots of the Oak tree." - id: 6045 # Willow roots examine: "The roots of the Willow tree." - id: 6047 # Maple roots examine: "The roots of the Maple tree." - id: 6049 # Yew roots examine: "The roots of the Yew tree." - id: 6051 # Magic roots examine: "The roots of the Magic tree." - id: 6055 # Weeds examine: "A handful of weeds." - id: 6057 # Hay sack examine: "A sack filled with hay." - id: 6058 # Hay sack examine: "This sack of hay has a bronze spear sticking through it." - id: 6059 # Scarecrow examine: "This should scare the birds." - id: 6064 # Bloody mourner top examine: "How do I wash blood stains out?" - id: 6065 # Mourner top examine: "A thick heavy leather top." - id: 6067 # Mourner trousers examine: "A pair of mourner trousers." - id: 6068 # Mourner gloves examine: "These will keep my hands warm!" - id: 6069 # Mourner boots examine: "Comfortable leather boots." - id: 6070 # Mourner cloak examine: "A dull brown cape." - id: 6071 # Mourner letter examine: "A letter of recommendation." - id: 6072 # Tegid's soap examine: "A bar of soap taken from Tegid." - id: 6073 # Prifddinas' history examine: "A book on the history of Prifddinas." - id: 6075 # Eastern discovery examine: "A book on the exploration of the eastern realm." - id: 6077 # Eastern settlement examine: "A book on the settlement of the eastern realm." - id: 6079 # The great divide examine: "A book about the great divide." - id: 6081 # Broken device examine: "A strange broken device of gnomic design." - id: 6082 # Fixed device examine: "A device for firing dye." - id: 6083 # Tarnished key examine: "A key that Essyllt gave to me." - id: 6085 # Red dye bellows examine: "A large pair of ogre bellows filled with red dye." - id: 6086 # Blue dye bellows examine: "A large pair of ogre bellows filled with blue dye." - id: 6087 # Yellow dye bellows examine: "A large pair of ogre bellows filled with yellow dye." - id: 6088 # Green dye bellows examine: "A large pair of ogre bellows filled with green dye." - id: 6089 # Blue toad examine: "A blue dye filled toad." - id: 6090 # Red toad examine: "A red dye filled toad." - id: 6091 # Yellow toad examine: "A yellow dye filled toad." - id: 6092 # Green toad examine: "A green dye filled toad." - id: 6093 # Rotten apples examine: "A barrel full of rotten apples." - id: 6094 # Apple barrel examine: "A barrel full of mushed apples." - id: 6095 # Naphtha apple mix examine: "A barrel full of rotten apples and naphtha." - id: 6096 # Toxic naphtha examine: "A barrel full of toxic naphtha." - id: 6097 # Sieve examine: "It's a sieve." - id: 6098 # Toxic powder examine: "A pile of toxic powder." - id: 6099 # Teleport crystal (4) examine: "An enchanted teleportation crystal." - id: 6100 # Teleport crystal (3) examine: "An enchanted teleportation crystal." - id: 6101 # Teleport crystal (2) examine: "An enchanted teleportation crystal." - id: 6102 # Teleport crystal (1) examine: "An enchanted teleportation crystal." - id: 6104 # New key examine: "A newly cut key that Essyllt gave to me." - id: 6106 # Ghostly boots examine: "They seem to be not quite of this world..." - id: 6107 # Ghostly robe examine: "A particularly spooky robe top." - id: 6108 # Ghostly robe examine: "An unearthly set of robe bottoms." - id: 6109 # Ghostly hood examine: "A ghostly hood, fit for a ghostly head." - id: 6110 # Ghostly gloves examine: "They seem to fade in and out of existence..." - id: 6111 # Ghostly cloak examine: "Made of a strange ghostly material..." - id: 6112 # Kelda seed examine: "Kelda hop seeds can only be grown underground!" - id: 6113 # Kelda hops examine: "A handful of Kelda Hops." - id: 6118 # Kelda stout examine: "A pint of bluish beer." - id: 6119 # Square stone examine: "There is a strange yellow marking on this stone." - id: 6120 # Square stone examine: "There is a strange green marking on this stone." - id: 6121 # Letter examine: "A letter addressed to Elstan of Falador." - id: 6122 # A chair examine: "For sitting on." - id: 6123 # Beer glass examine: "For drinking... if it were filled." - id: 6125 # Enchanted lyre(2) examine: "This will teleport me to the Fremennik province when I play it." - id: 6126 # Enchanted lyre(3) examine: "This will teleport me to the Fremennik province when I play it." - id: 6127 # Enchanted lyre(4) examine: "This will teleport me to the Fremennik province when I play it." - id: 6128 # Rock-shell helm examine: "Protective headwear made from crabs. Better than that sounds." - id: 6129 # Rock-shell plate examine: "A sturdy body armour made from rock crab pieces." - id: 6130 # Rock-shell legs examine: "Some tough leggings made from rock crab parts." - id: 6131 # Spined helm examine: "A helm fit for any Fremennik ranger." - id: 6133 # Spined body examine: "A constant reminder that I'm above a Dagannoth in the food chain." - id: 6135 # Spined chaps examine: "Stylish leg armour for rangers with a lingering smell of raw fish..." - id: 6137 # Skeletal helm examine: "Make your foes cower by wearing a skull as a helmet!" - id: 6139 # Skeletal top examine: "The bones in this armour seem to vibrate with a magical quality..." - id: 6141 # Skeletal bottoms examine: "A superior set of strengthened slacks for any self respecting seer." - id: 6143 # Spined boots examine: "Some finely crafted Fremennik boots, made from spined dagannoth hide." - id: 6145 # Rock-shell boots examine: "Some Fremennik boots, made from the shards of a rock crab's shell." - id: 6147 # Skeletal boots examine: "Some finely crafted Fremennik boots, made from the bones of a wallasalki." - id: 6149 # Spined gloves examine: "Fremennik gloves stitched together from spined dagannoth hide." - id: 6151 # Rock-shell gloves examine: "Fremennik gloves stitched together from rock crab shell shards." - id: 6153 # Skeletal gloves examine: "Fremennik gloves stitched together from wallasalki bones fragments." - id: 6155 # Dagannoth hide examine: "A sturdy piece of dagannoth hide." - id: 6157 # Rock-shell chunk examine: "A spherical chunk of rock-shell." - id: 6159 # Rock-shell shard examine: "A curved piece of rock-shell." - id: 6161 # Rock-shell splinter examine: "A slim piece of rock-shell." - id: 6163 # Skull piece examine: "A fearsome looking skull." - id: 6165 # Ribcage piece examine: "A slightly damaged ribcage." - id: 6167 # Fibula piece examine: "An interesting looking bone shard." - id: 6169 # Circular hide examine: "A toughened chunk of dagannoth hide." - id: 6171 # Flattened hide examine: "A tattered chunk of dagannoth hide." - id: 6173 # Stretched hide examine: "A weathered chunk of dagannoth hide." - id: 6178 # Raw pheasant examine: "I need to cook this first." - id: 6179 # Raw pheasant examine: "I need to cook this first." - id: 6180 # Lederhosen top examine: "A leather strapped top." - id: 6181 # Lederhosen shorts examine: "Brown leather shorts with bright white socks?" - id: 6182 # Lederhosen hat examine: "A hat with a goat's hair attached." - id: 6183 # Frog token examine: "I can use this at the Varrock clothes shop." - id: 6184 # Prince tunic examine: "Very posh!" - id: 6185 # Prince leggings examine: "Very posh!" - id: 6186 # Princess blouse examine: "Very posh!" - id: 6187 # Princess skirt examine: "Very posh!" - id: 6188 # Frog mask examine: "Now that's just silly." - id: 6199 # Mystery box examine: "Oooh, I wonder what could be inside?" - id: 6200 # Raw fishlike thing examine: "A raw... fish? Is this a fish??" - id: 6202 # Fishlike thing examine: "It's a fishlike thing that appears to already be cooked. It looks disgusting." - id: 6204 # Raw fishlike thing examine: "A raw... fish? Is this a fish??" - id: 6206 # Fishlike thing examine: "It's a fishlike thing that appears to already be cooked. It looks disgusting." - id: 6209 # Small fishing net examine: "Useful for catching small fish." - id: 6211 # Teak pyre logs examine: "Teak logs prepared with sacred oil for a funeral pyre." - id: 6213 # Mahogany pyre log examine: "Mahogany logs prepared with sacred oil for a funeral pyre." - id: 6215 # Broodoo shield (10) examine: "A scary broodoo shield." - id: 6217 # Broodoo shield (9) examine: "A scary broodoo shield." - id: 6219 # Broodoo shield (8) examine: "A scary broodoo shield." - id: 6221 # Broodoo shield (7) examine: "A scary broodoo shield." - id: 6223 # Broodoo shield (6) examine: "A scary broodoo shield." - id: 6225 # Broodoo shield (5) examine: "A scary broodoo shield." - id: 6227 # Broodoo shield (4) examine: "A scary broodoo shield." - id: 6229 # Broodoo shield (3) examine: "A scary broodoo shield." - id: 6231 # Broodoo shield (2) examine: "A scary broodoo shield." - id: 6233 # Broodoo shield (1) examine: "A scary broodoo shield." - id: 6235 # Broodoo shield examine: "A scary broodoo shield." - id: 6237 # Broodoo shield (10) examine: "A scary broodoo shield." - id: 6239 # Broodoo shield (9) examine: "A scary broodoo shield." - id: 6241 # Broodoo shield (8) examine: "A scary broodoo shield." - id: 6243 # Broodoo shield (7) examine: "A scary broodoo shield." - id: 6245 # Broodoo shield (6) examine: "A scary broodoo shield." - id: 6247 # Broodoo shield (5) examine: "A scary broodoo shield." - id: 6249 # Broodoo shield (4) examine: "A scary broodoo shield." - id: 6251 # Broodoo shield (3) examine: "A scary broodoo shield." - id: 6253 # Broodoo shield (2) examine: "A scary broodoo shield." - id: 6255 # Broodoo shield (1) examine: "A scary broodoo shield." - id: 6257 # Broodoo shield examine: "A scary broodoo shield." - id: 6259 # Broodoo shield (10) examine: "A scary broodoo shield." - id: 6261 # Broodoo shield (9) examine: "A scary broodoo shield." - id: 6263 # Broodoo shield (8) examine: "A scary broodoo shield." - id: 6265 # Broodoo shield (7) examine: "A scary broodoo shield." - id: 6267 # Broodoo shield (6) examine: "A scary broodoo shield." - id: 6269 # Broodoo shield (5) examine: "A scary broodoo shield." - id: 6271 # Broodoo shield (4) examine: "A scary broodoo shield." - id: 6273 # Broodoo shield (3) examine: "A scary broodoo shield." - id: 6275 # Broodoo shield (2) examine: "A scary broodoo shield." - id: 6277 # Broodoo shield (1) examine: "A scary broodoo shield." - id: 6279 # Broodoo shield examine: "A scary broodoo shield." - id: 6281 # Thatch spar light examine: "A wooden pole for use in primitive construction." - id: 6283 # Thatch spar med examine: "A wooden pole for use in primitive construction." - id: 6285 # Thatch spar dense examine: "A wooden pole for use in primitive construction." - id: 6287 # Snake hide examine: "Scaly but not slimy! It could be a useful material if it were tanned." - id: 6289 # Snakeskin examine: "Nicely tanned skin from a snake." - id: 6291 # Spider carcass examine: "Its creeping days are over!" - id: 6293 # Spider on stick examine: "A raw spider threaded onto a skewer stick." - id: 6295 # Spider on shaft examine: "A raw spider threaded onto an arrow shaft." - id: 6297 # Spider on stick examine: "A nicely roasted spider threaded onto a skewer stick." - id: 6299 # Spider on shaft examine: "A nicely roasted spider threaded onto an arrow shaft." - id: 6301 # Burnt spider examine: "A badly burnt spider threaded onto a charred skewer stick." - id: 6303 # Spider on shaft examine: "A badly burnt spider threaded onto a charred arrow shaft." - id: 6305 # Skewer stick examine: "A sharp pointed stick, quite resistant to fire." - id: 6306 # Trading sticks examine: "Karamja currency." - id: 6311 # Gout tuber examine: "Plant this in a herb patch to grow Goutweed." - id: 6313 # Opal machete examine: "A jungle specific slashing device." - id: 6315 # Jade machete examine: "A jungle specific slashing device." - id: 6317 # Red topaz machete examine: "A jungle specific slashing device." - id: 6319 # Proboscis examine: "A giant mosquito's proboscis, aerodynamic and sharp!" - id: 6322 # Snakeskin body examine: "Made from 100% real snakeskin." - id: 6324 # Snakeskin chaps examine: "Made from 100% real snake." - id: 6326 # Snakeskin bandana examine: "Lightweight head protection." - id: 6328 # Snakeskin boots examine: "Made from snakes." - id: 6332 # Mahogany logs examine: "Some well-cut mahogany logs." - id: 6333 # Teak logs examine: "Some well-cut teak logs." - id: 6335 # Tribal mask examine: "A ceremonial wooden mask." - id: 6337 # Tribal mask examine: "A ceremonial wooden mask." - id: 6339 # Tribal mask examine: "A ceremonial wooden mask." - id: 6341 # Tribal top examine: "Local dress." - id: 6343 # Villager robe examine: "A brightly coloured robe prized by the Tai Bwo Wannai peoples." - id: 6345 # Villager hat examine: "A brightly coloured hat prized by the Tai Bwo Wannai peoples." - id: 6347 # Villager armband examine: "A brown armband, as worn by the Tai Bwo Wannai locals." - id: 6349 # Villager sandals examine: "A brightly coloured pair of local sandals." - id: 6351 # Tribal top examine: "Local dress." - id: 6353 # Villager robe examine: "A brightly coloured robe prized by the Tai Bwo Wannai peoples." - id: 6355 # Villager hat examine: "A brightly coloured hat prized by the Tai Bwo Wannai peoples." - id: 6357 # Villager sandals examine: "A brightly coloured pair of local sandals." - id: 6359 # Villager armband examine: "A light blue armband, as worn by the Tai Bwo Wannai locals." - id: 6361 # Tribal top examine: "Local dress." - id: 6363 # Villager robe examine: "A brightly coloured robe prized by the Tai Bwo Wannai peoples." - id: 6365 # Villager hat examine: "A brightly coloured hat prized by the Tai Bwo Wannai peoples." - id: 6367 # Villager sandals examine: "A brightly coloured pair of local sandals." - id: 6369 # Villager armband examine: "A dark blue armband, as worn by the Tai Bwo Wannai locals." - id: 6371 # Tribal top examine: "Local dress." - id: 6373 # Villager robe examine: "A brightly coloured robe prized by the Tai Bwo Wannai peoples." - id: 6375 # Villager hat examine: "A brightly coloured hat prized by the Tai Bwo Wannai peoples." - id: 6377 # Villager sandals examine: "A brightly coloured pair of local sandals." - id: 6379 # Villager armband examine: "A white armband, as worn by the Tai Bwo Wannai locals." - id: 6382 # Fez examine: "A Fez hat. Juss like that." - id: 6384 # Desert top examine: "A bit itchy." - id: 6386 # Desert robes examine: "Has a coarse hard wearing texture." - id: 6388 # Desert top examine: "Good for those cold desert nights." - id: 6390 # Desert legs examine: "Better than factor 50 sun cream." - id: 6408 # Oak blackjack(o) examine: "An offensive blackjack." - id: 6410 # Oak blackjack(d) examine: "A defensive blackjack." - id: 6412 # Willow blackjack(o) examine: "An offensive blackjack." - id: 6414 # Willow blackjack(d) examine: "A defensive blackjack." - id: 6416 # Maple blackjack examine: "A solid bit of maple." - id: 6418 # Maple blackjack(o) examine: "An offensive blackjack." - id: 6420 # Maple blackjack(d) examine: "A defensive blackjack." - id: 6422 # Air rune examine: "One of the 4 basic elemental Runes." - id: 6424 # Water rune examine: "One of the 4 basic elemental Runes." - id: 6426 # Earth rune examine: "One of the 4 basic elemental Runes." - id: 6428 # Fire rune examine: "One of the 4 basic elemental Runes." - id: 6430 # Chaos rune examine: "Used for low level missile spells." - id: 6432 # Death rune examine: "Used for medium level missile spells." - id: 6434 # Law rune examine: "Used for teleport spells." - id: 6436 # Mind rune examine: "Used for basic level missile spells." - id: 6438 # Body rune examine: "Used for curse spells." - id: 6448 # Spadeful of coke examine: "A spadeful of refined coal." - id: 6453 # White rose seed examine: "A white rosebush seed." - id: 6454 # Red rose seed examine: "A red rosebush seed." - id: 6455 # Pink rose seed examine: "A pink rosebush seed." - id: 6456 # Vine seed examine: "A grapevine seed." - id: 6457 # Delphinium seed examine: "A delphinium seed." - id: 6458 # Orchid seed examine: "A pink orchid seed." - id: 6459 # Orchid seed examine: "A yellow orchid seed." - id: 6460 # Snowdrop seed examine: "A snowdrop seed." - id: 6461 # White tree shoot examine: "A shoot that has been cut from a dying White Tree." - id: 6462 # White tree shoot examine: "A shoot that has been cut from a dying White Tree." - id: 6464 # White tree sapling examine: "A young White Tree sapling." - id: 6465 # Ring of charos(a) examine: "The power within this ring has been activated." - id: 6466 # Rune shards examine: "A rune essence chip that has been broken into shards." - id: 6467 # Rune dust examine: "Crushed rune essence." - id: 6468 # Plant cure examine: "This plant cure emits potency." - id: 6469 # White tree fruit examine: "Looks delicious." - id: 6470 # Compost potion(4) examine: "Pour this on compost to turn it into super-compost." - id: 6472 # Compost potion(3) examine: "Pour this on compost to turn it into super-compost." - id: 6474 # Compost potion(2) examine: "Pour this on compost to turn it into super-compost." - id: 6476 # Compost potion(1) examine: "Pour this on compost to turn it into super-compost." - id: 6478 # Trolley examine: "I can use this to move heavy objects." - id: 6479 # List examine: "A list of things that I must collect for Queen Ellamaria." - id: 6522 # Toktz-xil-ul examine: "A razor sharp ring of obsidian." - id: 6523 # Toktz-xil-ak examine: "A razor sharp sword of obsidian." - id: 6524 # Toktz-ket-xil examine: "A spiked shield of obsidian." - id: 6525 # Toktz-xil-ek examine: "A large knife of obsidian." - id: 6526 # Toktz-mej-tal examine: "A staff of obsidian." - id: 6527 # Tzhaar-ket-em examine: "A mace of obsidian." - id: 6528 # Tzhaar-ket-om examine: "A maul of obsidian." - id: 6529 # Tokkul examine: "It's a token of some kind made from obsidian." - id: 6541 # Mouse toy examine: "An Advanced Combat Training Device." - id: 6542 # Present examine: "Thanks for all your help! Love, Bob & Neite." - id: 6543 # Antique lamp examine: "I wonder what happens if I rub it..." - id: 6544 # Catspeak amulet(e) examine: "It's an amulet of cat speak. It makes vague purring noises." - id: 6545 # Chores examine: "A list of chores that Bob gave you to do." - id: 6546 # Recipe examine: "It says on the back 'My favourite recipe.'" - id: 6548 # Nurse hat examine: "A nurse's hat, but does it have healing powers?" - id: 6549 # Lazy cat examine: "Lethargic." - id: 6550 # Lazy cat examine: "Lethargic." - id: 6551 # Lazy cat examine: "Lethargic." - id: 6552 # Lazy cat examine: "Lethargic." - id: 6553 # Lazy cat examine: "Lethargic." - id: 6554 # Lazy cat examine: "Lethargic." - id: 6555 # Wily cat examine: "Wild." - id: 6556 # Wily cat examine: "Wild." - id: 6557 # Wily cat examine: "Wild." - id: 6558 # Wily cat examine: "Wild." - id: 6559 # Wily cat examine: "Wild." - id: 6560 # Wily cat examine: "Wild." - id: 6562 # Mud battlestaff examine: "It's a slightly magical stick." - id: 6563 # Mystic mud staff examine: "It's a slightly magical stick." - id: 6568 # Obsidian cape examine: "A cape of woven obsidian plates." - id: 6570 # Fire cape examine: "A cape of fire." - id: 6571 # Uncut onyx examine: "This would be worth more cut." - id: 6573 # Onyx examine: "This looks valuable." - id: 6575 # Onyx ring examine: "A valuable ring." - id: 6577 # Onyx necklace examine: "I wonder if this is valuable." - id: 6581 # Onyx amulet examine: "I wonder if I can get this enchanted." - id: 6583 # Ring of stone examine: "An enchanted ring." - id: 6585 # Amulet of fury examine: "A very powerful onyx amulet." - id: 6587 # White claws examine: "A set of fighting claws." - id: 6589 # White battleaxe examine: "A vicious looking axe." - id: 6591 # White dagger examine: "A vicious white dagger." - id: 6593 # White dagger(p) examine: "This dagger is poisoned." - id: 6595 # White dagger(p+) examine: "This dagger is poisoned." - id: 6597 # White dagger(p++) examine: "This dagger is poisoned." - id: 6599 # White halberd examine: "A white halberd." - id: 6601 # White mace examine: "A spiky mace." - id: 6603 # White magic staff examine: "A Magical staff." - id: 6605 # White sword examine: "A razor sharp sword." - id: 6607 # White longsword examine: "A razor sharp longsword." - id: 6609 # White 2h sword examine: "A two handed sword." - id: 6611 # White scimitar examine: "A vicious, curved sword." - id: 6613 # White warhammer examine: "I don't think it's intended for joinery." - id: 6615 # White chainbody examine: "A series of connected metal rings." - id: 6617 # White platebody examine: "Provides excellent protection." - id: 6619 # White boots examine: "These will protect my feet." - id: 6621 # White med helm examine: "A medium sized helmet." - id: 6623 # White full helm examine: "A full face helmet." - id: 6625 # White platelegs examine: "Big, White and heavy looking." - id: 6627 # White plateskirt examine: "Big, White and heavy looking." - id: 6629 # White gloves examine: "These will keep my hands warm!" - id: 6631 # White sq shield examine: "A medium square shield." - id: 6633 # White kiteshield examine: "A large metal shield." - id: 6635 # Commorb examine: "A Temple Knight Communication Orb. Top Secret!" - id: 6636 # Solus's hat examine: "Proof that I have defeated the evil mage Solus." - id: 6638 # Colour wheel examine: "A key to the nature of light itself." - id: 6639 # Hand mirror examine: "A small hand mirror." - id: 6641 # Yellow crystal examine: "A yellow crystal." - id: 6643 # Cyan crystal examine: "A cyan crystal." - id: 6644 # Blue crystal examine: "A blue crystal." - id: 6646 # Fractured crystal examine: "A fractured crystal, one of the edges is clear." - id: 6647 # Fractured crystal examine: "A fractured crystal, one of the edges is clear." - id: 6648 # Item list examine: "It's a list of items I need to collect." - id: 6649 # Edern's journal examine: "The journal of Nissyen Edern." - id: 6650 # Blackened crystal examine: "A blackened crystal sample." - id: 6651 # Newly made crystal examine: "A newly formed crystal." - id: 6652 # Newly made crystal examine: "A warm energy radiates from this crystal." - id: 6653 # Crystal trinket examine: "A small crystal trinket." - id: 6654 # Camo top examine: "Examine what?" - id: 6655 # Camo bottoms examine: "Examine what?" - id: 6656 # Camo helmet examine: "Examine what?" - id: 6657 # Camo top examine: "Examine what?" - id: 6658 # Camo bottoms examine: "Examine what?" - id: 6659 # Camo helmet examine: "Examine what?" - id: 6660 # Fishing explosive examine: "The jar keeps shaking...I'm scared." - id: 6662 # Broken fishing rod examine: "This fishing rod seems to have been bitten in half..." - id: 6663 # Forlorn boot examine: "It seems someone vacated this boot in a hurry..." - id: 6664 # Fishing explosive examine: "The jar keeps shaking...I'm scared." - id: 6665 # Mudskipper hat examine: "Fishy, damp and smelly." - id: 6666 # Flippers examine: "Strangely uncomfortable flippers." - id: 6668 # Fishbowl examine: "A fishless fishbowl." - id: 6669 # Fishbowl examine: "A fishless fishbowl with some seaweed." - id: 6670 # Fishbowl examine: "A fishbowl with a Tiny Bluefish in it." - id: 6671 # Fishbowl examine: "A fishbowl with a Tiny Greenfish in it." - id: 6672 # Fishbowl examine: "A fishbowl with a Tiny Spinefish in it." - id: 6673 # Fishbowl and net examine: "An empty fishbowl in a net." - id: 6674 # Tiny net examine: "A tiny net for grabbing tiny fish." - id: 6675 # An empty box examine: "'Ingredients; Ground Guam and Ground Seaweed.'" - id: 6677 # Guam in a box examine: "'Ingredients; Ground Guam and Ground Seaweed.' Well, I have the Guam Leaf..." - id: 6678 # Guam in a box? examine: "'Ingredients; Ground Guam and Ground Seaweed.' Well, I have the Guam Leaf..." - id: 6679 # Seaweed in a box examine: "'Ingredients; Ground Guam and Ground Seaweed.' Well, I have the Seaweed..." - id: 6680 # Seaweed in a box? examine: "'Ingredients; Ground Guam and Ground Seaweed.' Well, I have the Seaweed..." - id: 6681 # Ground guam examine: "One of the ingredients for making fish food." - id: 6683 # Ground seaweed examine: "One of the ingredients for making fish food." - id: 6685 # Saradomin brew(4) examine: "4 doses of Saradomin brew." - id: 6687 # Saradomin brew(3) examine: "3 doses of Saradomin brew." - id: 6689 # Saradomin brew(2) examine: "2 doses of Saradomin brew." - id: 6691 # Saradomin brew(1) examine: "1 dose of Saradomin brew." - id: 6693 # Crushed nest examine: "A crushed bird's nest." - id: 6696 # Ice cooler examine: "Contains ice-cold water." - id: 6697 # Pat of butter examine: "A pat of freshly churned butter." - id: 6699 # Burnt potato examine: "This potato doesn't look edible." - id: 6701 # Baked potato examine: "It'd taste even better with some toppings." - id: 6703 # Potato with butter examine: "A baked potato with butter." - id: 6705 # Potato with cheese examine: "A baked potato with butter and cheese." - id: 6707 # Camulet examine: "An amulet of Camel-speak. It makes vague braying noises." - id: 6708 # Slayer gloves examine: "Especially good against diseased arachnids." - id: 6710 # Blindweed seed examine: "A Blindweed seed - plant in a Blindweed patch." - id: 6711 # Blindweed examine: "An inedible, foul smelling herb." - id: 6712 # Bucket of water examine: "It's a bucket of... water?" - id: 6713 # Wrench examine: "A heavy metal wrench." - id: 6714 # Holy wrench examine: "A shining paragon of wrenchly virtue." - id: 6715 # Sluglings examine: "They look at you balefully. 'Feed us...'" - id: 6716 # Karamthulhu examine: "A sinister looking squid." - id: 6717 # Karamthulhu examine: "A sinister looking squid." - id: 6718 # Fever spider body examine: "A diseased deceased Fever Spider. Handle with care." - id: 6719 # Unsanitary swill examine: "Sorry, I mean a bucket of 'rum'." - id: 6720 # Slayer gloves examine: "Especially good against diseased arachnids." - id: 6721 # Rusty scimitar examine: "A decent enough weapon gone rusty." - id: 6722 # Zombie head examine: "Alas...I hardly knew him." - id: 6724 # Seercull examine: "An ancient Fremennik bow that was once used to battle the Moon Clan." - id: 6728 # Bonemeal examine: "A pot of crushed Dagannoth-king bones." - id: 6729 # Dagannoth bones examine: "These would feed a dogfish for months!" - id: 6731 # Seers ring examine: "A mysterious ring that can fill the wearer with magical power..." - id: 6733 # Archers ring examine: "A fabled ring that improves the wearer's skill with a bow..." - id: 6735 # Warrior ring examine: "A legendary ring once worn by Fremennik warriors." - id: 6737 # Berserker ring examine: "A ring reputed to bring out a berserk fury in its wearer." - id: 6739 # Dragon axe examine: "A very powerful axe." - id: 6741 # Broken axe examine: "Bob can fix this for me." - id: 6745 # Silverlight examine: "The magical sword 'Silverlight', stained black with mushroom ink." - id: 6746 # Darklight examine: "The magical sword 'Silverlight', enhanced with the blood of Agrith-Naar." - id: 6747 # Demonic sigil mould examine: "Used to make the sigil of the demon Agrith-Naar." - id: 6748 # Demonic sigil examine: "A sigil used for the summoning of the demon Agrith-Naar." - id: 6749 # Demonic tome examine: "Will this book help in summoning Agrith-Naar?" - id: 6750 # Black desert shirt examine: "A desert shirt stained black with mushroom ink." - id: 6752 # Black desert robe examine: "A desert robe stained black with mushroom ink." - id: 6754 # Enchanted key examine: "It seems to change temperature as I walk." - id: 6755 # Journal examine: "Somebody's private journal." - id: 6756 # Letter examine: "A sealed letter to the king." - id: 6757 # Letter examine: "A sealed letter to Jorral." - id: 6758 # Scroll examine: "A timeline of the outpost." - id: 6759 # Chest examine: "A dirty chest." - id: 6760 # Guthix mjolnir examine: "A Guthix Mjolnir." - id: 6762 # Saradomin mjolnir examine: "A Saradomin Mjolnir." - id: 6764 # Zamorak mjolnir examine: "A Zamorak Mjolnir." - id: 6766 # Cat antipoison examine: "Antipoison for Pox." - id: 6767 # Book examine: "The book of Rats." - id: 6768 # Poisoned cheese examine: "A little more smelly than usual." - id: 6769 # Music scroll examine: "Charming." - id: 6770 # Directions examine: "Jimmy Dazzler's directions." - id: 6771 # Pot of weeds examine: "Contains garden weeds." - id: 6772 # Smouldering pot examine: "Contains slowly burning garden weeds." - id: 6773 # Rat pole examine: "A pole for putting rats on." - id: 6774 # Rat pole examine: "A pole with one rat on it." - id: 6775 # Rat pole examine: "A pole with two rats on it." - id: 6776 # Rat pole examine: "A pole with three rats on it." - id: 6777 # Rat pole examine: "A pole with four rats on it." - id: 6778 # Rat pole examine: "A pole with five rats on it." - id: 6779 # Rat pole examine: "A pole with six rats on it." - id: 6785 # Statuette examine: "A statue of the goddess Elidinis." - id: 6786 # Robe of elidinis examine: "This looks quite old." - id: 6787 # Robe of elidinis examine: "A patched up robe." - id: 6788 # Torn robe examine: "This robe is too torn to wear." - id: 6789 # Torn robe examine: "This robe is too torn to wear." - id: 6790 # Shoes examine: "Awusah's Shoes." - id: 6791 # Sole examine: "Awusah's Sole." - id: 6792 # Ancestral key examine: "An ancient key from the shrine in Nardah." - id: 6793 # Ballad examine: "The Ballad of Jareesh." - id: 6794 # Choc-ice examine: "Better eat this before it melts." - id: 6796 # Lamp examine: "Ooh a nice shiny lamp." - id: 6797 # Watering can examine: "This watering can is empty." - id: 6809 # Granite legs examine: "These look pretty heavy." - id: 6810 # Bonemeal examine: "A pot of crushed wyvern bones." - id: 6812 # Wyvern bones examine: "Bones of a large flying creature!" - id: 6814 # Fur examine: "This would make warm clothing." - id: 6817 # Slender blade examine: "A slender two-handed sword." - id: 6818 # Bow-sword examine: "A sharp sword that can also fire arrows." - id: 6819 # Large pouch examine: "A large pouch used for storing essence." - id: 6821 # Orb examine: "A glowing orb." - id: 6822 # Star bauble examine: "An unpainted bauble shaped like a star." - id: 6823 # Star bauble examine: "A bauble shaped like a star painted yellow." - id: 6824 # Star bauble examine: "A bauble shaped like a star painted red." - id: 6825 # Star bauble examine: "A bauble shaped like a star painted blue." - id: 6826 # Star bauble examine: "A bauble shaped like a star painted green." - id: 6827 # Star bauble examine: "A bauble shaped like a star painted pink." - id: 6828 # Box bauble examine: "An unpainted bauble shaped like a gift." - id: 6829 # Box bauble examine: "A bauble shaped like a gift painted yellow." - id: 6830 # Box bauble examine: "A bauble shaped like a gift painted red." - id: 6831 # Box bauble examine: "A bauble shaped like a gift painted blue." - id: 6832 # Box bauble examine: "A bauble shaped like a gift painted green." - id: 6833 # Box bauble examine: "A bauble shaped like a gift painted pink." - id: 6834 # Diamond bauble examine: "An unpainted bauble shaped like a diamond." - id: 6835 # Diamond bauble examine: "A bauble shaped like a diamond painted yellow." - id: 6836 # Diamond bauble examine: "A bauble shaped like a diamond painted red." - id: 6837 # Diamond bauble examine: "A bauble shaped like a diamond painted blue." - id: 6838 # Diamond bauble examine: "A bauble shaped like a diamond painted green." - id: 6839 # Diamond bauble examine: "A bauble shaped like a diamond painted pink." - id: 6840 # Tree bauble examine: "An unpainted bauble shaped like a wintumber tree." - id: 6841 # Tree bauble examine: "A bauble shaped like a wintumber tree painted yellow." - id: 6842 # Tree bauble examine: "A bauble shaped like a wintumber tree painted red." - id: 6843 # Tree bauble examine: "A bauble shaped like a wintumber tree painted blue." - id: 6844 # Tree bauble examine: "A bauble shaped like a wintumber tree painted green." - id: 6845 # Tree bauble examine: "A bauble shaped like a wintumber tree painted pink." - id: 6846 # Bell bauble examine: "An unpainted bauble shaped like a bell." - id: 6847 # Bell bauble examine: "A bauble shaped like a bell painted yellow." - id: 6848 # Bell bauble examine: "A bauble shaped like a bell painted red." - id: 6849 # Bell bauble examine: "A bauble shaped like a bell painted blue." - id: 6850 # Bell bauble examine: "A bauble shaped like a bell painted green." - id: 6851 # Bell bauble examine: "A bauble shaped like a bell painted pink." - id: 6852 # Puppet box examine: "A box for storing completed puppets." - id: 6853 # Bauble box examine: "A box for storing painted baubles." - id: 6854 # Puppet box examine: "A box full of puppets. Bring to the Taverly members gate." - id: 6855 # Bauble box examine: "A box full of painted baubles. Give to a Pixie or use to decorate a tree." - id: 6856 # Bobble hat examine: "A woolly bobble hat." - id: 6857 # Bobble scarf examine: "A woolly scarf." - id: 6858 # Jester hat examine: "A woolly jester hat." - id: 6859 # Jester scarf examine: "A woolly jester scarf." - id: 6860 # Tri-jester hat examine: "A woolly triple bobble jester hat." - id: 6861 # Tri-jester scarf examine: "A woolly jester scarf." - id: 6862 # Woolly hat examine: "A woolly tobogganing hat." - id: 6863 # Woolly scarf examine: "A woolly tobogganing scarf." - id: 6864 # Marionette handle examine: "The controlling part of a marionette." - id: 6865 # Blue marionette examine: "I've got no strings ... oh hang on!" - id: 6866 # Green marionette examine: "I've got no strings ... oh hang on!" - id: 6867 # Red marionette examine: "I've got no strings ... oh hang on!" - id: 6868 # Blue marionette examine: "I've got no strings ... oh hang on!" - id: 6869 # Green marionette examine: "I've got no strings ... oh hang on!" - id: 6870 # Red marionette examine: "I've got no strings ... oh hang on!" - id: 6871 # Red marionette examine: "I've got no strings ... oh hang on!" - id: 6872 # Red marionette examine: "I've got no strings ... oh hang on!" - id: 6873 # Red marionette examine: "I've got no strings ... oh hang on!" - id: 6874 # Red marionette examine: "I've got no strings ... oh hang on!" - id: 6875 # Blue marionette examine: "I've got no strings ... oh hang on!" - id: 6876 # Blue marionette examine: "I've got no strings ... oh hang on!" - id: 6877 # Blue marionette examine: "I've got no strings ... oh hang on!" - id: 6878 # Blue marionette examine: "I've got no strings ... oh hang on!" - id: 6879 # Green marionette examine: "I've got no strings ... oh hang on!" - id: 6880 # Green marionette examine: "I've got no strings ... oh hang on!" - id: 6881 # Green marionette examine: "I've got no strings ... oh hang on!" - id: 6882 # Green marionette examine: "I've got no strings ... oh hang on!" - id: 6883 # Peach examine: "A tasty fruit." - id: 6885 # Progress hat examine: "A magic training arena progress hat." - id: 6886 # Progress hat examine: "A magic training arena progress hat." - id: 6887 # Progress hat examine: "A magic training arena progress hat." - id: 6889 # Mage's book examine: "The magical book of the Mage." - id: 6891 # Arena book examine: "A book about the Training Arena." - id: 6893 # Leather boots examine: "Comfortable leather boots." - id: 6894 # Adamant kiteshield examine: "A large metal shield." - id: 6895 # Adamant med helm examine: "A medium sized helmet." - id: 6896 # Emerald examine: "This looks valuable." - id: 6897 # Rune longsword examine: "A razor sharp longsword." - id: 6898 # Cylinder examine: "A green cylinder." - id: 6899 # Cube examine: "A yellow cube." - id: 6900 # Icosahedron examine: "A blue icosahedron." - id: 6901 # Pentamid examine: "A red pentamid." - id: 6902 # Orb examine: "A white sphere." - id: 6903 # Dragonstone examine: "This looks valuable." - id: 6904 # Animals' bones examine: "Various animals' bones." - id: 6905 # Animals' bones examine: "Various animals' bones." - id: 6906 # Animals' bones examine: "Various animals' bones." - id: 6907 # Animals' bones examine: "Various animals' bones." - id: 6908 # Beginner wand examine: "A beginner level wand." - id: 6910 # Apprentice wand examine: "An apprentice level wand." - id: 6912 # Teacher wand examine: "A teacher level wand." - id: 6914 # Master wand examine: "A master level wand." - id: 6916 # Infinity top examine: "Mystical robes." - id: 6918 # Infinity hat examine: "A mystic hat." - id: 6920 # Infinity boots examine: "Mystical boots." - id: 6922 # Infinity gloves examine: "Mystical gloves." - id: 6924 # Infinity bottoms examine: "Mystical robes." - id: 6945 # Sandy hand examine: "A severed hand covered with sand." - id: 6946 # Beer soaked hand examine: "A severed hand dripping with beer." - id: 6947 # Bert's rota examine: "A copy of a work rota." - id: 6948 # Sandy's rota examine: "An original work rota." - id: 6949 # A magic scroll examine: "This scroll glows with an inner light." - id: 6950 # Magical orb examine: "An ordinary looking magical scrying orb." - id: 6951 # Magical orb (a) examine: "This magical scrying orb pulsates as it stores information." - id: 6952 # Truth serum examine: "Fluid sloshes innocently in this vial." - id: 6953 # Bottled water examine: "A bottle of water." - id: 6954 # Redberry juice examine: "Redberry Juice sloshes around in this vial waiting for white berries to be added." - id: 6955 # Pink dye examine: "A vial of pink dye." - id: 6956 # Rose tinted lens examine: "This lens has a pinkish tinge to it." - id: 6957 # Wizard's head examine: "A decapitated, sand covered head." - id: 6958 # Sand examine: "A handful of sand from Sandy's pocket." - id: 6961 # Baguette examine: "A freshly baked baguette." - id: 6962 # Triangle sandwich examine: "A freshly made triangle sandwich." - id: 6963 # Roll examine: "A freshly made roll." - id: 6964 # Coins examine: "Lovely money!" - id: 6965 # Square sandwich examine: "A freshly made square sandwich." - id: 6966 # Prison key examine: "A key to the prison." - id: 6967 # Dragon med helm examine: "Makes the wearer pretty intimidating." - id: 6969 # Shark examine: "I'd better be careful eating this." - id: 6970 # Pyramid top examine: "It's a solid gold pyramid!" - id: 6971 # Sandstone (1kg) examine: "A tiny chunk of sandstone." - id: 6973 # Sandstone (2kg) examine: "A small chunk of sandstone." - id: 6975 # Sandstone (5kg) examine: "A medium-sized chunk of sandstone." - id: 6977 # Sandstone (10kg) examine: "A large chunk of sandstone." - id: 6979 # Granite (500g) examine: "A tiny chunk of granite." - id: 6981 # Granite (2kg) examine: "A small chunk of granite." - id: 6983 # Granite (5kg) examine: "A medium-sized chunk of granite." - id: 6985 # Sandstone (20kg) examine: "A huge twenty-kilo block of sandstone." - id: 6986 # Sandstone (32kg) examine: "A huge thirty-two-kilo block of sandstone." - id: 6987 # Sandstone body examine: "The body of a sandstone statue." - id: 6988 # Sandstone base examine: "The base and legs of a sandstone statue." - id: 6989 # Stone head examine: "A granite head shaped like the sculptor Lazim." - id: 6990 # Stone head examine: "A granite head shaped like the god Zamorak." - id: 6991 # Stone head examine: "A granite head shaped like the god Icthlarin." - id: 6992 # Stone head examine: "A granite head shaped like a camel." - id: 6993 # Z sigil examine: "A metal sigil in the shape of a Z." - id: 6994 # M sigil examine: "A metal sigil in the shape of an M." - id: 6995 # R sigil examine: "A metal sigil in the shape of an R." - id: 6996 # K sigil examine: "A metal sigil in the shape of a K." - id: 6997 # Stone left arm examine: "The left arm of a large stone statue." - id: 6998 # Stone right arm examine: "The right arm of a large stone statue." - id: 6999 # Stone left leg examine: "The left leg of a large stone statue." - id: 7000 # Stone right leg examine: "The right leg of a large stone statue." - id: 7001 # Camel mould (p) examine: "A positive clay mould of a camel's head." - id: 7002 # Stone head examine: "A granite head that will fit exactly into the pedestal." - id: 7003 # Camel mask examine: "Blend in in the desert." - id: 7051 # Unlit bug lantern examine: "A lantern to aid attacking Harpie bugs." - id: 7053 # Lit bug lantern examine: "A lantern to aid attacking Harpie bugs." - id: 7054 # Chilli potato examine: "A baked potato with chilli con carne." - id: 7056 # Egg potato examine: "A baked potato with egg and tomato." - id: 7058 # Mushroom potato examine: "A baked potato with mushroom and onions." - id: 7060 # Tuna potato examine: "A baked potato with tuna and sweetcorn." - id: 7062 # Chilli con carne examine: "A bowl of meat in chilli-con-carne sauce." - id: 7064 # Egg and tomato examine: "A bowl of scrambled eggs and tomato." - id: 7066 # Mushroom & onion examine: "A bowl of fried mushroom and onions." - id: 7068 # Tuna and corn examine: "A bowl of cooked tuna and sweetcorn." - id: 7070 # Minced meat examine: "A bowl of finely minced meat." - id: 7072 # Spicy sauce examine: "A bowl of spicy sauce." - id: 7074 # Chopped garlic examine: "A bowl of chopped garlic." - id: 7076 # Uncooked egg examine: "A bowl of uncooked egg." - id: 7078 # Scrambled egg examine: "A bowl of scrambled egg." - id: 7080 # Sliced mushrooms examine: "A bowl of sliced Bittercap mushrooms." - id: 7082 # Fried mushrooms examine: "A bowl of fried Bittercap mushrooms." - id: 7084 # Fried onions examine: "A bowl of sliced, fried onions." - id: 7086 # Chopped tuna examine: "A bowl of finely chopped tuna." - id: 7088 # Sweetcorn examine: "A bowl of cooked sweetcorn." - id: 7090 # Burnt egg examine: "A bowl of burnt, overcooked egg." - id: 7092 # Burnt onion examine: "A bowl of blackened onions." - id: 7094 # Burnt mushroom examine: "A bowl of burnt sliced mushroom." - id: 7108 # Gunpowder examine: "Best keep this away from naked flames." - id: 7109 # Fuse examine: "Burns very well." - id: 7110 # Stripy pirate shirt examine: "A sea worthy shirt." - id: 7112 # Pirate bandana examine: "Essential pirate wear." - id: 7114 # Pirate boots examine: "Not for land lubbers." - id: 7116 # Pirate leggings examine: "A sea worthy pair of trousers." - id: 7118 # Canister examine: "A cannister holding shrapnel." - id: 7119 # Cannon ball examine: "A heavy metal ball." - id: 7120 # Ramrod examine: "For cleaning and packing the cannon." - id: 7121 # Repair plank examine: "A plank of wood to repair the hull with." - id: 7122 # Stripy pirate shirt examine: "A sea worthy shirt." - id: 7124 # Pirate bandana examine: "Essential pirate wear." - id: 7126 # Pirate leggings examine: "A sea worthy pair of trousers." - id: 7128 # Stripy pirate shirt examine: "A sea worthy shirt." - id: 7130 # Pirate bandana examine: "Essential pirate wear." - id: 7132 # Pirate leggings examine: "A sea worthy pair of trousers." - id: 7134 # Stripy pirate shirt examine: "A sea worthy shirt." - id: 7136 # Pirate bandana examine: "Essential pirate wear." - id: 7138 # Pirate leggings examine: "A sea worthy pair of trousers." - id: 7140 # Lucky cutlass examine: "Feels quite lucky." - id: 7141 # Harry's cutlass examine: "I hope he doesn't want it back." - id: 7142 # Rapier examine: "The very butcher of a silk button." - id: 7143 # Plunder examine: "Looks valuable." - id: 7144 # Book o' piracy examine: "By Cap'n Hook-Hand Morrisane." - id: 7145 # Cannon barrel examine: "A working cannon barrel." - id: 7146 # Broken cannon examine: "Not likely to work again." - id: 7148 # Repair plank examine: "A plank of wood to repair the hull with." - id: 7149 # Canister examine: "A cannister holding shrapnel." - id: 7150 # Tacks examine: "Useful for pinning up paintings." - id: 7155 # Rope examine: "A coil of rope." - id: 7156 # Tinderbox examine: "Useful for lighting a fire." - id: 7157 # Braindeath 'rum' examine: "I think it is eating through the bottle." - id: 7158 # Dragon 2h sword examine: "A two-handed Dragon Sword." - id: 7159 # Insulated boots examine: "They're heavily insulated wellies." - id: 7162 # Pie recipe book examine: "Lots of pie recipes for me to try." - id: 7164 # Part mud pie examine: "Still needs two more ingredients." - id: 7166 # Part mud pie examine: "Still needs one more ingredient." - id: 7168 # Raw mud pie examine: "Needs to be baked before I can use it." - id: 7170 # Mud pie examine: "All the good of the earth." - id: 7172 # Part garden pie examine: "Still needs two more ingredients." - id: 7174 # Part garden pie examine: "Still needs one more ingredient." - id: 7176 # Raw garden pie examine: "Needs cooking before I eat it." - id: 7178 # Garden pie examine: "What I wouldn't give for a good steak about now..." - id: 7180 # Half a garden pie examine: "What I wouldn't give for a good steak about now..." - id: 7182 # Part fish pie examine: "Still needs two more ingredients." - id: 7184 # Part fish pie examine: "Still needs one more ingredient." - id: 7186 # Raw fish pie examine: "Raw fish is risky, better cook it." - id: 7188 # Fish pie examine: "Bounty of the sea." - id: 7190 # Half a fish pie examine: "Bounty of the sea." - id: 7192 # Part admiral pie examine: "Still needs two more ingredients." - id: 7194 # Part admiral pie examine: "Still needs one more ingredient." - id: 7196 # Raw admiral pie examine: "This would taste a lot better cooked." - id: 7198 # Admiral pie examine: "Much tastier than a normal fish pie." - id: 7200 # Half an admiral pie examine: "Much tastier than a normal fish pie." - id: 7202 # Part wild pie examine: "Still needs two more ingredients." - id: 7204 # Part wild pie examine: "Still needs one more ingredient." - id: 7206 # Raw wild pie examine: "Good as it looks, I'd better cook it." - id: 7208 # Wild pie examine: "A triumph of man over nature." - id: 7210 # Half a wild pie examine: "A triumph of man over nature." - id: 7212 # Part summer pie examine: "Still needs two more ingredients." - id: 7214 # Part summer pie examine: "Still needs one more ingredient." - id: 7216 # Raw summer pie examine: "Fresh fruit may be good for you, but I should really cook this." - id: 7218 # Summer pie examine: "All the fruits of a very small forest." - id: 7220 # Half a summer pie examine: "All the fruits of a very small forest." - id: 7222 # Burnt rabbit examine: "This could be mistaken for charcoal." - id: 7223 # Roast rabbit examine: "A delicious looking piece of roast rabbit." - id: 7224 # Skewered rabbit examine: "All ready to be used on a fire." - id: 7225 # Iron spit examine: "An iron spit." - id: 7230 # Skewered chompy examine: "A skewered chompy bird." - id: 7319 # Red boater examine: "Stylish!" - id: 7321 # Orange boater examine: "Stylish!" - id: 7323 # Green boater examine: "Stylish!" - id: 7325 # Blue boater examine: "Stylish!" - id: 7327 # Black boater examine: "Stylish!" - id: 7329 # Red firelighter examine: "Makes firelighting a lot easier." - id: 7330 # Green firelighter examine: "Makes firelighting a lot easier." - id: 7331 # Blue firelighter examine: "Makes firelighting a lot easier." - id: 7362 # Studded body (g) examine: "Those studs should provide a bit more protection. Nice trim too!" - id: 7364 # Studded body (t) examine: "Those studs should provide a bit more protection. Nice trim too!" - id: 7366 # Studded chaps (g) examine: "Those studs should provide a bit more protection. Nice trim too!" - id: 7368 # Studded chaps (t) examine: "Those studs should provide a bit more protection. Nice trim too!" - id: 7386 # Blue skirt (g) examine: "Leg covering favoured by women and wizards. With a colourful trim!" - id: 7388 # Blue skirt (t) examine: "Leg covering favoured by women and wizards. With a colourful trim!" - id: 7398 # Enchanted robe examine: "Enchanted Wizards robes." - id: 7399 # Enchanted top examine: "Enchanted Wizards robes." - id: 7400 # Enchanted hat examine: "A three pointed hat of magic." - id: 7404 # Red logs examine: "A number of chemical covered wooden logs." - id: 7405 # Green logs examine: "A number of chemical covered wooden logs." - id: 7406 # Blue logs examine: "A number of chemical covered wooden logs." - id: 7408 # Draynor skull examine: "I shouldn't joke, this is a grave matter." - id: 7409 # Magic secateurs examine: "The only way to kill a Tanglefoot." - id: 7410 # Queen's secateurs examine: "Contains the Fairy Queen's magic essence." - id: 7411 # Symptoms list examine: "A list of the Fairy Queen's symptoms." - id: 7413 # Bird nest examine: "It's a bird's nest with some seeds in it." - id: 7416 # Mole claw examine: "A mole claw." - id: 7418 # Mole skin examine: "The skin of a large mole." - id: 7421 # Fungicide spray 10 examine: "Pumps fungicide." - id: 7422 # Fungicide spray 9 examine: "Pumps fungicide." - id: 7423 # Fungicide spray 8 examine: "Pumps fungicide." - id: 7424 # Fungicide spray 7 examine: "Pumps fungicide." - id: 7425 # Fungicide spray 6 examine: "Pumps fungicide." - id: 7426 # Fungicide spray 5 examine: "Pumps fungicide." - id: 7427 # Fungicide spray 4 examine: "Pumps fungicide." - id: 7428 # Fungicide spray 3 examine: "Pumps fungicide." - id: 7429 # Fungicide spray 2 examine: "Pumps fungicide." - id: 7430 # Fungicide spray 1 examine: "Pumps fungicide." - id: 7431 # Fungicide spray 0 examine: "Pumps fungicide." - id: 7432 # Fungicide examine: "Does exactly what it says on the tin." - id: 7433 # Wooden spoon examine: "Spoooooon!" - id: 7435 # Egg whisk examine: "A large whisk of death." - id: 7437 # Spork examine: "Use the spork." - id: 7439 # Spatula examine: "A large spatula... of doom!" - id: 7441 # Frying pan examine: "Looks like it's non-stick too!" - id: 7443 # Skewer examine: "Generally used for impaling fresh meat." - id: 7445 # Rolling pin examine: "That's how I roll!" - id: 7447 # Kitchen knife examine: "A sharp, dependable knife, for filleting meat." - id: 7449 # Meat tenderiser examine: "Often used to soften tough meat up." - id: 7451 # Cleaver examine: "An effective tool for chopping tough meat." - id: 7463 # Cornflour examine: "A little heap of cornflour." - id: 7464 # Book on chickens examine: "A tatty old book belonging to the Wise Old Man of Draynor Village." - id: 7465 # Vanilla pod examine: "Surprise, it looks like a vanilla pod." - id: 7468 # Pot of cornflour examine: "It's cornflour in a pot." - id: 7470 # Cornflour mixture examine: "A mixture of milk, cream and cornflour." - id: 7471 # Milky mixture examine: "It's a bucket of milk and cream." - id: 7472 # Cinnamon examine: "Some cinnamon sticks." - id: 7473 # Brulee examine: "It's just missing a sprinkling of cinnamon." - id: 7474 # Brulee examine: "A pot of brulee mixture - needs egg." - id: 7475 # Brulee examine: "Perfect, it just needs flambeing." - id: 7476 # Brulee supreme examine: "A pot of brulee supreme." - id: 7477 # Evil chicken's egg examine: "What came first, the chicken or..." - id: 7478 # Dragon token examine: "It's got a dragon on it." - id: 7479 # Spicy stew examine: "It's a meat and potato stew with fancy seasoning." - id: 7480 # Red spice (4) examine: "Allows for equal distribution of spice." - id: 7481 # Red spice (3) examine: "Allows for equal distribution of spice." - id: 7482 # Red spice (2) examine: "Allows for equal distribution of spice." - id: 7483 # Red spice (1) examine: "Allows for equal distribution of spice." - id: 7484 # Orange spice (4) examine: "Allows for equal distribution of spice." - id: 7485 # Orange spice (3) examine: "Allows for equal distribution of spice." - id: 7486 # Orange spice (2) examine: "Allows for equal distribution of spice." - id: 7487 # Orange spice (1) examine: "Allows for equal distribution of spice." - id: 7488 # Brown spice (4) examine: "Allows for equal distribution of spice." - id: 7489 # Brown spice (3) examine: "Allows for equal distribution of spice." - id: 7490 # Brown spice (2) examine: "Allows for equal distribution of spice." - id: 7491 # Brown spice (1) examine: "Allows for equal distribution of spice." - id: 7492 # Yellow spice (4) examine: "Allows for equal distribution of spice." - id: 7493 # Yellow spice (3) examine: "Allows for equal distribution of spice." - id: 7494 # Yellow spice (2) examine: "Allows for equal distribution of spice." - id: 7495 # Yellow spice (1) examine: "Allows for equal distribution of spice." - id: 7496 # Empty spice shaker examine: "Allows for equal distribution of spice." - id: 7497 # Dirty blast examine: "A cool refreshing fruit mix. With ash in for some reason." - id: 7498 # Antique lamp examine: "I wonder what happens if I rub it..." - id: 7508 # Asgoldian ale examine: "There appears to be a coin in the bottom. Liked by dwarves." - id: 7509 # Dwarven rock cake examine: "Red hot and glowing, ouch! Only for dwarf consumption." - id: 7510 # Dwarven rock cake examine: "Cool and heavy as a brick. Only for dwarf consumption." - id: 7511 # Slop of compromise examine: "Two out of two goblin generals prefer it!" - id: 7512 # Soggy bread examine: "Previously a nice crispy loaf of bread. Now just kind of icky." - id: 7513 # Spicy maggots examine: "They clearly taste so much better this way!" - id: 7514 # Dyed orange examine: "Orange slices which have been dyed, but it looks more like they died." - id: 7515 # Breadcrumbs examine: "Glad these aren't in my bed." - id: 7516 # Kelp examine: "Slightly damp seaweed." - id: 7517 # Ground kelp examine: "Kelp flakes. Smells of the sea." - id: 7518 # Crab meat examine: "A smelly meat." - id: 7519 # Crab meat examine: "A smelly meat." - id: 7520 # Burnt crab meat examine: "Oh dear, it's burnt." - id: 7521 # Cooked crab meat examine: "Nice and Tasty!" - id: 7523 # Cooked crab meat examine: "Nice and Tasty!" - id: 7524 # Cooked crab meat examine: "Nice and Tasty!" - id: 7525 # Cooked crab meat examine: "Nice and Tasty!" - id: 7526 # Cooked crab meat examine: "Nice and Tasty!" - id: 7527 # Ground crab meat examine: "A smelly paste." - id: 7528 # Ground cod examine: "A smelly paste." - id: 7529 # Raw fishcake examine: "Would taste nicer if I cooked it." - id: 7530 # Cooked fishcake examine: "Mmmm, reminds me of the seaside." - id: 7531 # Burnt fishcake examine: "Darn thing's all burnt!" - id: 7532 # Mudskipper hide examine: "Hmmm, what can I use this for?" - id: 7533 # Rock examine: "A rock." - id: 7534 # Fishbowl helmet examine: "You'll look daft, but at least you won't drown!" - id: 7535 # Diving apparatus examine: "I'll need a helmet to make this work." - id: 7536 # Fresh crab claw examine: "Fresh off the crab itself." - id: 7537 # Crab claw examine: "If it's good enough for crabs, it's good enough for me!" - id: 7538 # Fresh crab shell examine: "Fresh off the crab itself." - id: 7539 # Crab helmet examine: "If it's good enough for crabs, it's good enough for me!" - id: 7540 # Broken crab claw examine: "Darn, it's useless now." - id: 7541 # Broken crab shell examine: "Darn, it's useless now." - id: 7542 # Cake of guidance examine: "Imbued with knowledge itself." - id: 7543 # Raw guide cake examine: "Now all I need to do is cook it." - id: 7544 # Enchanted egg examine: "Egg containing knowledge." - id: 7545 # Enchanted milk examine: "Guiding milk." - id: 7546 # Enchanted flour examine: "A pot of special flour." - id: 7547 # Druid pouch examine: "An empty druid pouch." - id: 7548 # Potato seed examine: "A potato seed - plant in an allotment." - id: 7550 # Onion seed examine: "An onion seed - plant in an allotment." - id: 7552 # Mithril arrow examine: "Arrows with mithril heads." - id: 7554 # Fire rune examine: "One of the 4 basic elemental Runes." - id: 7556 # Water rune examine: "One of the 4 basic elemental Runes." - id: 7558 # Air rune examine: "One of the 4 basic elemental Runes." - id: 7560 # Chaos rune examine: "Used for low level missile spells." - id: 7562 # Tomato seed examine: "A tomato seed - plant in an allotment." - id: 7564 # Balloon toad examine: "An inflated toad tied to a rock like a balloon." - id: 7565 # Balloon toad examine: "An inflated toad tied to a rock like a balloon." - id: 7566 # Raw jubbly examine: "The uncooked meat of a Jubbly bird." - id: 7568 # Cooked jubbly examine: "Lovely Jubbly!" - id: 7570 # Burnt jubbly examine: "The burnt meat of a Jubbly bird." - id: 7572 # Red banana examine: "Like a banana only redder." - id: 7573 # Tchiki monkey nuts examine: "Like Monkey Nuts only Tchikier." - id: 7574 # Sliced red banana examine: "Perfect for stuffing snakes." - id: 7575 # Tchiki nut paste examine: "Mixing this with jam would just be wrong." - id: 7576 # Snake corpse examine: "Like a snake only not alive." - id: 7577 # Raw stuffed snake examine: "This snake is stuffed right up." - id: 7578 # Odd stuffed snake examine: "Is this really what you wanted to do?" - id: 7579 # Stuffed snake examine: "Fit for a Monkey King." - id: 7580 # Snake over-cooked examine: "It's a burnt snake." - id: 7581 # Overgrown hellcat examine: "Your hellish pet cat!!" - id: 7582 # Hell cat examine: "Your hellish pet cat!!" - id: 7583 # Hell-kitten examine: "Your hellish pet cat!!" - id: 7584 # Lazy hell cat examine: "Your hellish pet cat!!" - id: 7585 # Wily hellcat examine: "Your hellish pet cat!!" - id: 7587 # Coffin examine: "Filled with items. Like a bank, but spookier!" - id: 7588 # Coffin examine: "Filled with items. Like a bank, but spookier!" - id: 7589 # Coffin examine: "Filled with items. Like a bank, but spookier!" - id: 7590 # Coffin examine: "Filled with items. Like a bank, but spookier!" - id: 7591 # Coffin examine: "Filled with items. Like a bank, but spookier!" - id: 7592 # Zombie shirt examine: "Aside from the braaaains on the lapel, it's still quite good." - id: 7593 # Zombie trousers examine: "Good for a shamble about town." - id: 7594 # Zombie mask examine: "I look 40,000 years old in this..." - id: 7595 # Zombie gloves examine: "Smells pretty funky." - id: 7596 # Zombie boots examine: "Thrilling." - id: 7622 # Bucket of rubble examine: "A bucket partially filled with rubble." - id: 7624 # Bucket of rubble examine: "A bucket almost full of rubble." - id: 7626 # Bucket of rubble examine: "A bucket totally filled with rubble." - id: 7628 # Plaster fragment examine: "A fragment of plaster with some impressions on it." - id: 7629 # Dusty scroll examine: "An ancient tattered scroll." - id: 7630 # Crate examine: "An old and musty looking crate." - id: 7632 # Temple library key examine: "A key for the Temple Library." - id: 7633 # Ancient book examine: "A book about seven warrior priests, written about 200 years ago." - id: 7634 # Battered tome examine: "An ancient history book." - id: 7635 # Leather book examine: "An ancient leather-bound tome." - id: 7636 # Rod dust examine: "Rod of Ivandis dust." - id: 7637 # Silvthrill rod examine: "A silvery rod of mithril and silver with a sapphire on the top." - id: 7638 # Silvthrill rod examine: "An enchanted rod of mithril and silver with a sapphire on the top." - id: 7649 # Rod clay mould examine: "Rod of Ivandis mould." - id: 7650 # Silver dust examine: "It's ground up silver." - id: 7660 # Guthix balance(4) examine: "A potion of harralander, red spiders eggs, garlic and silver dust." - id: 7662 # Guthix balance(3) examine: "A potion of harralander, red spiders eggs, garlic and silver dust." - id: 7664 # Guthix balance(2) examine: "A potion of harralander, red spiders eggs, garlic and silver dust." - id: 7666 # Guthix balance(1) examine: "A potion of harralander, red spiders eggs, garlic and silver dust." - id: 7668 # Gadderhammer examine: "A specially crafted hammer with strange markings on it." - id: 7671 # Boxing gloves examine: "I think they look a bit silly." - id: 7673 # Boxing gloves examine: "I think they look a bit silly." - id: 7675 # Wooden sword examine: "A less-than razor sharp sword." - id: 7676 # Wooden shield examine: "A less-than strong shield." - id: 7678 # Prize key examine: "You can use this to open the prize chest!" - id: 7679 # Pugel examine: "A good tool for bashing someone." - id: 7681 # Game book examine: "Party Pete's Bumper Book Of Games" - id: 7688 # Kettle examine: "The kettle is empty" - id: 7690 # Full kettle examine: "It's full of cold water." - id: 7691 # Hot kettle examine: "It's full of boiling water." - id: 7692 # Pot of tea (4) examine: "I'd really like a nice cup of tea." - id: 7694 # Pot of tea (3) examine: "I'd really like a nice cup of tea." - id: 7696 # Pot of tea (2) examine: "I'd really like a nice cup of tea." - id: 7698 # Pot of tea (1) examine: "I'd really like a nice cup of tea." - id: 7700 # Teapot with leaves examine: "Add boiling water to make a tea." - id: 7702 # Teapot examine: "This teapot is empty." - id: 7704 # Pot of tea (4) examine: "I'd really like a nice cup of tea." - id: 7706 # Pot of tea (3) examine: "I'd really like a nice cup of tea." - id: 7708 # Pot of tea (2) examine: "I'd really like a nice cup of tea." - id: 7710 # Pot of tea (1) examine: "I'd really like a nice cup of tea." - id: 7712 # Teapot with leaves examine: "Add boiling water to make a tea." - id: 7714 # Teapot examine: "This teapot is empty." - id: 7716 # Pot of tea (4) examine: "I'd really like a nice cup of tea." - id: 7718 # Pot of tea (3) examine: "I'd really like a nice cup of tea." - id: 7720 # Pot of tea (2) examine: "I'd really like a nice cup of tea." - id: 7722 # Pot of tea (1) examine: "I'd really like a nice cup of tea." - id: 7724 # Teapot with leaves examine: "Add boiling water to make a tea." - id: 7726 # Teapot examine: "This teapot is empty." - id: 7728 # Empty cup examine: "An empty cup." - id: 7730 # Cup of tea examine: "A nice cup of nettle tea." - id: 7731 # Cup of tea examine: "A milky cup of nettle tea." - id: 7732 # Porcelain cup examine: "A porcelain cup." - id: 7733 # Cup of tea examine: "Some nettle tea in a porcelain cup." - id: 7734 # Cup of tea examine: "Some milky nettle tea in a porcelain cup." - id: 7735 # Porcelain cup examine: "A porcelain cup." - id: 7736 # Cup of tea examine: "Some nettle tea in a porcelain cup." - id: 7737 # Cup of tea examine: "Some milky nettle tea in a porcelain cup." - id: 7738 # Tea leaves examine: "Mmm, how about a nice cup of tea?" - id: 7740 # Beer examine: "A glass of frothy ale." - id: 7742 # Beer glass examine: "I need to fill this with beer." - id: 7744 # Asgarnian ale examine: "Probably the finest ale in Asgarnia." - id: 7746 # Greenman's ale examine: "A glass of frothy ale." - id: 7748 # Dragon bitter examine: "A glass of bitter." - id: 7750 # Moonlight mead examine: "A foul smelling brew." - id: 7752 # Cider examine: "A glass of cider." - id: 7754 # Chef's delight examine: "A fruity, full-bodied ale." - id: 7759 # Toy soldier examine: "Nice bit of crafting!" - id: 7761 # Toy soldier (wound) examine: "Nice bit of crafting!" - id: 7763 # Toy doll examine: "Nice bit of crafting!" - id: 7765 # Toy doll (wound) examine: "Nice bit of crafting!" - id: 7767 # Toy mouse examine: "Nice bit of crafting!" - id: 7769 # Toy mouse (wound) examine: "Nice bit of crafting!" - id: 7771 # Toy cat examine: "Nice bit of crafting!" - id: 7773 # Branch examine: "Leafless bush branch." - id: 7774 # Reward token examine: "Yellow reward token earned from helping people journey to Burgh de Rott." - id: 7775 # Reward token examine: "Red reward token earned from helping people journey to Burgh de Rott." - id: 7776 # Reward token examine: "Blue reward token earned from helping people journey to Burgh de Rott." - id: 7777 # Long vine examine: "A long section of vine made up of lots of shorter sections." - id: 7778 # Short vine examine: "A short section of vines." - id: 7779 # Fishing tome examine: "A tome of learning which focuses on Fishing." - id: 7780 # Fishing tome examine: "A tome of learning which focuses on Fishing." - id: 7781 # Fishing tome examine: "A tome of learning which focuses on Fishing." - id: 7782 # Agility tome examine: "A tome of learning which focuses on Agility." - id: 7783 # Agility tome examine: "A tome of learning which focuses on Agility." - id: 7784 # Agility tome examine: "A tome of learning which focuses on Agility." - id: 7785 # Thieving tome examine: "A tome of learning which focuses on Thieving." - id: 7786 # Thieving tome examine: "A tome of learning which focuses on Thieving." - id: 7787 # Thieving tome examine: "A tome of learning which focuses on Thieving." - id: 7788 # Slayer tome examine: "A tome of learning which focuses on the Slayer skill." - id: 7789 # Slayer tome examine: "A tome of learning which focuses on the Slayer skill." - id: 7790 # Slayer tome examine: "A tome of learning which focuses on the Slayer skill." - id: 7791 # Mining tome examine: "A tome of learning which focuses on the Mining skill." - id: 7792 # Mining tome examine: "A tome of learning which focuses on the Mining skill." - id: 7793 # Mining tome examine: "A tome of learning which focuses on the Mining skill." - id: 7794 # Firemaking tome examine: "A tome of learning which focuses on the Firemaking skill." - id: 7795 # Firemaking tome examine: "A tome of learning which focuses on the Firemaking skill." - id: 7796 # Firemaking tome examine: "A tome of learning which focuses on the Firemaking skill." - id: 7797 # Woodcutting tome examine: "A tome of learning which focuses on the Woodcutting skill." - id: 7798 # Woodcutting tome examine: "A tome of learning which focuses on the Woodcutting skill." - id: 7799 # Woodcutting tome examine: "A tome of learning which focuses on the Woodcutting skill." - id: 7800 # Snail shell examine: "A shell from a giant snail." - id: 7801 # Snake hide examine: "Scaly but not slimy!" - id: 7803 # Yin yang amulet examine: "A non-magical copy of the make-over mage's amulet." - id: 7806 # Anger sword examine: "A heavy duty sword." - id: 7807 # Anger battleaxe examine: "A heavy duty axe." - id: 7808 # Anger mace examine: "A heavy duty mace." - id: 7809 # Anger spear examine: "A heavy duty spear." - id: 7810 # Jug of vinegar examine: "This wine clearly did not age well." - id: 7811 # Pot of vinegar examine: "Well, this pot is certainly full of vinegar and no mistake." - id: 7812 # Goblin skull examine: "This needs a good polish." - id: 7813 # Bone in vinegar examine: "There is a goblin bone in here." - id: 7814 # Goblin skull examine: "This bone belongs in a museum!" - id: 7815 # Bear ribs examine: "This needs a good polish." - id: 7816 # Bone in vinegar examine: "There is a bear bone in here." - id: 7817 # Bear ribs examine: "This bone belongs in a museum!" - id: 7818 # Ram skull examine: "This needs a good polish." - id: 7819 # Bone in vinegar examine: "There is a ram bone in here." - id: 7820 # Ram skull examine: "This bone belongs in a museum!" - id: 7821 # Unicorn bone examine: "This needs a good polish." - id: 7822 # Bone in vinegar examine: "There is a unicorn bone in here." - id: 7823 # Unicorn bone examine: "This bone belongs in a museum!" - id: 7824 # Giant rat bone examine: "This needs a good polish." - id: 7825 # Bone in vinegar examine: "There is a giant rat bone in here." - id: 7826 # Giant rat bone examine: "This bone belongs in a museum!" - id: 7827 # Giant bat wing examine: "This needs a good polish." - id: 7828 # Bone in vinegar examine: "There is a giant bat bone in here." - id: 7829 # Giant bat wing examine: "This bone belongs in a museum!" - id: 7830 # Wolf bone examine: "This needs a good polish." - id: 7831 # Bone in vinegar examine: "There is a wolf bone in here." - id: 7832 # Wolf bone examine: "This bone belongs in a museum!" - id: 7833 # Bat wing examine: "This needs a good polish." - id: 7834 # Bone in vinegar examine: "There is a bat bone in here." - id: 7835 # Bat wing examine: "This bone belongs in a museum!" - id: 7836 # Rat bone examine: "This needs a good polish." - id: 7837 # Bone in vinegar examine: "There is a rat bone in here." - id: 7838 # Rat bone examine: "This bone belongs in a museum!" - id: 7839 # Baby dragon bone examine: "This needs a good polish." - id: 7840 # Bone in vinegar examine: "There is a baby blue dragon bone in here." - id: 7841 # Baby dragon bone examine: "This bone belongs in a museum!" - id: 7842 # Ogre ribs examine: "This needs a good polish." - id: 7843 # Bone in vinegar examine: "There is an ogre bone in here." - id: 7844 # Ogre ribs examine: "This bone belongs in a museum!" - id: 7845 # Jogre bone examine: "This needs a good polish." - id: 7846 # Bone in vinegar examine: "There is a jogre bone in here." - id: 7847 # Jogre bone examine: "This bone belongs in a museum!" - id: 7848 # Zogre bone examine: "This needs a good polish." - id: 7849 # Bone in vinegar examine: "There is a zogre bone in here." - id: 7850 # Zogre bone examine: "This bone belongs in a museum!" - id: 7851 # Mogre bone examine: "This needs a good polish." - id: 7852 # Bone in vinegar examine: "There is a mogre bone in here." - id: 7853 # Mogre bone examine: "This bone belongs in a museum!" - id: 7854 # Monkey paw examine: "This needs a good polish." - id: 7855 # Bone in vinegar examine: "There is a monkey bone in here." - id: 7856 # Monkey paw examine: "This bone belongs in a museum!" - id: 7857 # Dagannoth ribs examine: "This needs a good polish." - id: 7858 # Bone in vinegar examine: "There is a Dagannoth bone in here." - id: 7859 # Dagannoth ribs examine: "This bone belongs in a museum!" - id: 7860 # Snake spine examine: "This needs a good polish." - id: 7861 # Bone in vinegar examine: "There is a snake bone in here." - id: 7862 # Snake spine examine: "This bone belongs in a museum!" - id: 7863 # Zombie bone examine: "This needs a good polish." - id: 7864 # Bone in vinegar examine: "There is a zombie bone in here." - id: 7865 # Zombie bone examine: "This bone belongs in a museum!" - id: 7866 # Werewolf bone examine: "This needs a good polish." - id: 7867 # Bone in vinegar examine: "There is a werewolf bone in here." - id: 7868 # Werewolf bone examine: "This bone belongs in a museum!" - id: 7869 # Moss giant bone examine: "This needs a good polish." - id: 7870 # Bone in vinegar examine: "A Moss Giant's bone" - id: 7871 # Moss giant bone examine: "This bone belongs in a museum!" - id: 7872 # Fire giant bone examine: "This needs a good polish." - id: 7873 # Bone in vinegar examine: "There is a fire giant bone in here." - id: 7874 # Fire giant bone examine: "This bone belongs in a museum!" - id: 7875 # Ice giant ribs examine: "This needs a good polish." - id: 7876 # Bone in vinegar examine: "There is an ice giant bone in here." - id: 7877 # Ice giant ribs examine: "This bone belongs in a museum!" - id: 7878 # Terrorbird wing examine: "This needs a good polish." - id: 7879 # Bone in vinegar examine: "There is a terrorbird bone in here." - id: 7880 # Terrorbird wing examine: "This bone belongs in a museum!" - id: 7881 # Ghoul bone examine: "This needs a good polish." - id: 7882 # Bone in vinegar examine: "There is a ghoul bone in here." - id: 7883 # Ghoul bone examine: "This bone belongs in a museum!" - id: 7884 # Troll bone examine: "This needs a good polish." - id: 7885 # Bone in vinegar examine: "There is a troll bone in here." - id: 7886 # Troll bone examine: "This bone belongs in a museum!" - id: 7887 # Seagull wing examine: "This needs a good polish." - id: 7888 # Bone in vinegar examine: "There is a seagull bone in here." - id: 7889 # Seagull wing examine: "This bone belongs in a museum!" - id: 7890 # Undead cow ribs examine: "This needs a good polish." - id: 7891 # Bone in vinegar examine: "There is an undead cow bone in here." - id: 7892 # Undead cow ribs examine: "This bone belongs in a museum!" - id: 7893 # Experiment bone examine: "This needs a good polish." - id: 7894 # Bone in vinegar examine: "There is an experiment bone in here." - id: 7895 # Experiment bone examine: "This bone belongs in a museum!" - id: 7896 # Rabbit bone examine: "This needs a good polish." - id: 7897 # Bone in vinegar examine: "There is a rabbit bone in here." - id: 7898 # Rabbit bone examine: "This bone belongs in a museum!" - id: 7899 # Basilisk bone examine: "This needs a good polish." - id: 7900 # Bone in vinegar examine: "There is a basilisk bone in here." - id: 7901 # Basilisk bone examine: "This bone belongs in a museum!" - id: 7902 # Desert lizard bone examine: "This needs a good polish." - id: 7903 # Bone in vinegar examine: "There is a desert lizard bone in here." - id: 7904 # Desert lizard bone examine: "This bone belongs in a museum!" - id: 7905 # Cave goblin skull examine: "This needs a good polish." - id: 7906 # Bone in vinegar examine: "There is a cave goblin bone in here." - id: 7907 # Cave goblin skull examine: "This bone belongs in a museum!" - id: 7908 # Big frog leg examine: "This needs a good polish." - id: 7909 # Bone in vinegar examine: "There is a big frog bone in here." - id: 7910 # Big frog leg examine: "This bone belongs in a museum!" - id: 7911 # Vulture wing examine: "This needs a good polish." - id: 7912 # Bone in vinegar examine: "There is a vulture bone in here." - id: 7913 # Vulture wing examine: "This bone belongs in a museum!" - id: 7914 # Jackal bone examine: "This needs a good polish." - id: 7915 # Bone in vinegar examine: "There is a jackal bone in here." - id: 7916 # Jackal bone examine: "This bone belongs in a museum!" - id: 7917 # Ram skull helm examine: "Makes me feel baaad to the bone." - id: 7918 # Bonesack examine: "The Bonesack is a little old item that protects like leather." - id: 7919 # Bottle of wine examine: "A very good vintage." - id: 7921 # Empty wine bottle examine: "This one has clearly been taken down and passed around." - id: 7922 # Al kharid flyer examine: "The money off voucher has expired." - id: 7927 # Easter ring examine: "A ring given to you by the Easter Bunny." - id: 7928 # Easter egg examine: "\"Best before 1st May 2014.\"" - id: 7929 # Easter egg examine: "\"Best before 1st May 2014.\"" - id: 7930 # Easter egg examine: "\"Best before 1st May 2014.\"" - id: 7931 # Easter egg examine: "\"Best before 1st May 2014.\"" - id: 7932 # Easter egg examine: "\"Best before 1st May 2014.\"" - id: 7933 # Easter egg examine: "\"Best before 1st May 2014.\"" - id: 7934 # Field ration examine: "A field ration to help your wounds go away." - id: 7936 # Pure essence examine: "An uncharged Rune Stone of extra capability." - id: 7939 # Tortoise shell examine: "A word in your shell-like." - id: 7941 # Iron sheet examine: "A sturdy sheet of iron." - id: 7942 # Fresh monkfish examine: "Freshly caught. Needs cooking." - id: 7943 # Fresh monkfish examine: "Freshly caught and cooked - perfect for storing. Not so good for eating." - id: 7944 # Raw monkfish examine: "I should try cooking this." - id: 7946 # Monkfish examine: "A tasty fish." - id: 7948 # Burnt monkfish examine: "Maybe a little less heat next time." - id: 7950 # Bone seeds examine: "A highly portable army of skeletal magi." - id: 7951 # Herman's book examine: "A book taken from the desk of Herman Caranos." - id: 7952 # Axe handle examine: "Useless without the head." - id: 7954 # Burnt shrimp examine: "Oops!" - id: 7956 # Casket examine: "I hope there's treasure in it." - id: 7957 # White apron examine: "A mostly clean apron." - id: 7958 # Mining prop examine: "A prop for holding up a tunnel roof." - id: 7959 # Heavy box examine: "A box full of stolen Etceterian items." - id: 7960 # Empty box examine: "It says 'To the dungeons' on the side." - id: 7961 # Burnt diary examine: "A diary with one page." - id: 7962 # Burnt diary examine: "A diary with two pages." - id: 7963 # Burnt diary examine: "A diary with three pages." - id: 7964 # Burnt diary examine: "A diary with four pages." - id: 7965 # Burnt diary examine: "A diary with five pages." - id: 7966 # Letter examine: "Sigrid's letter to Vargas." - id: 7967 # Engine examine: "A dwarf-made coal engine. It looks very sturdy." - id: 7968 # Scroll examine: "An official-looking scroll." - id: 7969 # Pulley beam examine: "A beam with a pulley attached." - id: 7970 # Long pulley beam examine: "A long beam with a pulley attached." - id: 7971 # Longer pulley beam examine: "A very long beam with a pulley attached." - id: 7972 # Lift manual examine: "The manual for an AMCE Lift-In-A-Box™." - id: 7973 # Beam examine: "A wooden beam." - id: 7975 # Crawling hand examine: "I should get it stuffed!" - id: 7976 # Cockatrice head examine: "I should get it stuffed!" - id: 7977 # Basilisk head examine: "I should get it stuffed!" - id: 7978 # Kurask head examine: "I should get it stuffed!" - id: 7979 # Abyssal head examine: "I should get it stuffed!" - id: 7980 # Kbd heads examine: "I should get them stuffed!" - id: 7981 # Kq head examine: "I should get it stuffed!" - id: 7989 # Big bass examine: "Whopper! I should get this stuffed!" - id: 7991 # Big swordfish examine: "Whopper! I should get this stuffed!" - id: 7993 # Big shark examine: "It's a monster! I should get this stuffed!" - id: 7995 # Arthur portrait examine: "A portrait of King Arthur." - id: 7996 # Elena portrait examine: "A portrait of Elena." - id: 7997 # Keldagrim portrait examine: "A painting of the statue of King Alvis of Keldagrim." - id: 7998 # Misc. portrait examine: "A portrait of Prince Brand and Princess Astrid of Miscellania." - id: 7999 # Desert painting examine: "The searing Kharid Desert." - id: 8000 # Isafdar painting examine: "The exotic land of the Elves." - id: 8001 # Karamja painting examine: "The tropical coast of Karamja." - id: 8002 # Lumbridge painting examine: "Oxtable's famous painting of the Lumbridge water mill." - id: 8003 # Morytania painting examine: "A painting of the spooky forests of Morytania." - id: 8004 # Small map examine: "A small map of Gielinor." - id: 8005 # Medium map examine: "A medium map of Gielinor." - id: 8006 # Large map examine: "A large map of Gielinor." - id: 8007 # Varrock teleport examine: "A teleport to Varrock." - id: 8008 # Lumbridge teleport examine: "A teleport to Lumbridge." - id: 8009 # Falador teleport examine: "A teleport to Falador." - id: 8010 # Camelot teleport examine: "A teleport to Camelot." - id: 8011 # Ardougne teleport examine: "A teleport to East Ardougne." - id: 8013 # Teleport to house examine: "A teleport to one's own house." - id: 8014 # Bones to bananas examine: "A tablet containing a magic spell." - id: 8015 # Bones to peaches examine: "A tablet containing a magic spell." - id: 8019 # Enchant diamond examine: "A tablet containing a magic spell." - id: 8021 # Enchant onyx examine: "A tablet containing a magic spell." - id: 8261 # Cockatrice head examine: "I should get it stuffed!" - id: 8262 # Basilisk head examine: "I should get it stuffed!" - id: 8263 # Kurask head examine: "I should get it stuffed!" - id: 8264 # Abyssal head examine: "I should get it stuffed!" - id: 8265 # Kbd heads examine: "I should get them stuffed!" - id: 8266 # Kq head examine: "I should get it stuffed!" - id: 8279 # Silverlight examine: "The magical sword 'Silverlight'." - id: 8280 # Excalibur examine: "This used to belong to King Arthur." - id: 8281 # Darklight examine: "The magical sword 'Silverlight', enhanced with the blood of Agrith-Naar." - id: 8417 # Bagged dead tree examine: "You can plant this in your garden." - id: 8419 # Bagged nice tree examine: "You can plant this in your garden." - id: 8421 # Bagged oak tree examine: "You can plant this in your garden." - id: 8423 # Bagged willow tree examine: "You can plant this in your garden." - id: 8425 # Bagged maple tree examine: "You can plant this in your garden." - id: 8427 # Bagged yew tree examine: "You can plant this in your garden." - id: 8429 # Bagged magic tree examine: "You can plant this in your garden." - id: 8431 # Bagged plant 1 examine: "You can plant this in your garden." - id: 8433 # Bagged plant 2 examine: "You can plant this in your garden." - id: 8435 # Bagged plant 3 examine: "You can plant this in your garden." - id: 8437 # Thorny hedge examine: "You can plant this in your garden." - id: 8439 # Nice hedge examine: "You can plant this in your garden." - id: 8441 # Small box hedge examine: "You can plant this in your garden." - id: 8443 # Topiary hedge examine: "You can plant this in your garden." - id: 8445 # Fancy hedge examine: "You can plant this in your garden." - id: 8447 # Tall fancy hedge examine: "You can plant this in your garden." - id: 8449 # Tall box hedge examine: "You can plant this in your garden." - id: 8451 # Bagged flower examine: "You can plant this in your garden." - id: 8453 # Bagged daffodils examine: "You can plant this in your garden." - id: 8455 # Bagged bluebells examine: "You can plant this in your garden." - id: 8457 # Bagged sunflower examine: "You can plant this in your garden." - id: 8459 # Bagged marigolds examine: "You can plant this in your garden." - id: 8461 # Bagged roses examine: "You can plant this in your garden." - id: 8463 # Construction guide examine: "How to build a house." - id: 8464 # Rune heraldic helm examine: "The colours represent the symbol of Arrav." - id: 8466 # Rune heraldic helm examine: "The colours represent Asgarnia." - id: 8468 # Rune heraldic helm examine: "The colours represent the Dorgeshuun brooch." - id: 8470 # Rune heraldic helm examine: "The colours represent a dragon." - id: 8472 # Rune heraldic helm examine: "The colours represent a fairy." - id: 8474 # Rune heraldic helm examine: "The colours represent Guthix." - id: 8476 # Rune heraldic helm examine: "The colours represent the HAM cult." - id: 8478 # Rune heraldic helm examine: "The colours represent the mythical 'horse'." - id: 8480 # Rune heraldic helm examine: "The colours represent a Jogre." - id: 8482 # Rune heraldic helm examine: "The colours represent Kandarin." - id: 8484 # Rune heraldic helm examine: "The colours represent Misthalin." - id: 8486 # Rune heraldic helm examine: "The colours represent money." - id: 8488 # Rune heraldic helm examine: "The colours represent Saradomin." - id: 8490 # Rune heraldic helm examine: "The colours represent a skull." - id: 8492 # Rune heraldic helm examine: "The colours represent Varrock." - id: 8494 # Rune heraldic helm examine: "The colours represent Zamorak." - id: 8496 # Crude chair examine: "How does it all fit in there?" - id: 8498 # Wooden chair examine: "How does it all fit in there?" - id: 8500 # Rocking chair examine: "How does it all fit in there?" - id: 8502 # Oak chair examine: "How does it all fit in there?" - id: 8504 # Oak armchair examine: "How does it all fit in there?" - id: 8506 # Teak armchair examine: "How does it all fit in there?" - id: 8508 # Mahogany armchair examine: "How does it all fit in there?" - id: 8510 # Bookcase examine: "How does it all fit in there?" - id: 8512 # Oak bookcase examine: "How does it all fit in there?" - id: 8516 # Beer barrel examine: "How does it all fit in there?" - id: 8518 # Cider barrel examine: "How does it all fit in there?" - id: 8520 # Asgarnian ale examine: "How does it all fit in there?" - id: 8522 # Greenman's ale examine: "How does it all fit in there?" - id: 8524 # Dragon bitter examine: "How does it all fit in there?" - id: 8526 # Chef's delight examine: "How does it all fit in there?" - id: 8528 # Kitchen table examine: "How does it all fit in there?" - id: 8530 # Oak kitchen table examine: "How does it all fit in there?" - id: 8532 # Teak kitchen table examine: "How does it all fit in there?" - id: 8548 # Wood dining table examine: "How does it all fit in there?" - id: 8550 # Oak dining table examine: "How does it all fit in there?" - id: 8552 # Carved oak table examine: "How does it all fit in there?" - id: 8554 # Teak table examine: "How does it all fit in there?" - id: 8556 # Carved teak table examine: "How does it all fit in there?" - id: 8558 # Mahogany table examine: "How does it all fit in there?" - id: 8560 # Opulent table examine: "How does it all fit in there?" - id: 8562 # Wooden bench examine: "How does it all fit in there?" - id: 8564 # Oak bench examine: "How does it all fit in there?" - id: 8566 # Carved oak bench examine: "How does it all fit in there?" - id: 8568 # Teak dining bench examine: "How does it all fit in there?" - id: 8570 # Carved teak bench examine: "How does it all fit in there?" - id: 8572 # Mahogany bench examine: "How does it all fit in there?" - id: 8574 # Gilded bench examine: "How does it all fit in there?" - id: 8576 # Wooden bed examine: "How does it all fit in there?" - id: 8578 # Oak bed examine: "How does it all fit in there?" - id: 8580 # Large oak bed examine: "How does it all fit in there?" - id: 8582 # Teak bed examine: "How does it all fit in there?" - id: 8584 # Large teak bed examine: "How does it all fit in there?" - id: 8586 # Four-poster bed examine: "How does it all fit in there?" - id: 8588 # Gilded four-poster examine: "How does it all fit in there?" - id: 8590 # Oak clock examine: "How does it all fit in there?" - id: 8592 # Teak clock examine: "How does it all fit in there?" - id: 8594 # Gilded clock examine: "How does it all fit in there?" - id: 8596 # Shaving stand examine: "How does it all fit in there?" - id: 8598 # Oak shaving stand examine: "How does it all fit in there?" - id: 8600 # Oak dresser examine: "How does it all fit in there?" - id: 8602 # Teak dresser examine: "How does it all fit in there?" - id: 8604 # Fancy teak dresser examine: "How does it all fit in there?" - id: 8606 # Mahogany dresser examine: "How does it all fit in there?" - id: 8608 # Gilded dresser examine: "How does it all fit in there?" - id: 8610 # Shoe box examine: "How does it all fit in there?" - id: 8612 # Oak drawers examine: "How does it all fit in there?" - id: 8614 # Oak wardrobe examine: "How does it all fit in there?" - id: 8616 # Teak drawers examine: "How does it all fit in there?" - id: 8618 # Teak wardrobe examine: "How does it all fit in there?" - id: 8622 # Gilded wardrobe examine: "How does it all fit in there?" - id: 8650 # Banner examine: "A banner with the symbol of Arrav." - id: 8652 # Banner examine: "A banner with the symbol of Asgarnia." - id: 8654 # Banner examine: "A banner with a picture of the Dorgeshuun brooch." - id: 8656 # Banner examine: "A banner with a picture of a dragon." - id: 8658 # Banner examine: "A banner with a picture of a fairy." - id: 8660 # Banner examine: "A banner with the symbol of Guthix." - id: 8662 # Banner examine: "A banner with the symbol of the HAM cult." - id: 8664 # Banner examine: "A banner with a picture of the mythical 'horse'." - id: 8666 # Banner examine: "A banner with a picture of a Jogre." - id: 8668 # Banner examine: "A banner with the symbol of Kandarin." - id: 8670 # Banner examine: "A banner with the symbol of Misthalin." - id: 8672 # Banner examine: "A banner with a picture of a money-bag." - id: 8674 # Banner examine: "A banner with the symbol of Saradomin." - id: 8676 # Banner examine: "A banner with a picture of a skull." - id: 8678 # Banner examine: "A banner with the symbol of Varrock." - id: 8680 # Banner examine: "A banner with the symbol of Zamorak." - id: 8682 # Steel heraldic helm examine: "The colours represent the symbol of Arrav." - id: 8684 # Steel heraldic helm examine: "The colours represent Asgarnia." - id: 8686 # Steel heraldic helm examine: "The colours represent the Dorgeshuun brooch." - id: 8688 # Steel heraldic helm examine: "The colours represent a dragon." - id: 8690 # Steel heraldic helm examine: "The colours represent a fairy." - id: 8692 # Steel heraldic helm examine: "The colours represent Guthix." - id: 8694 # Steel heraldic helm examine: "The colours represent the HAM cult." - id: 8696 # Steel heraldic helm examine: "The colours represent the mythical 'horse'." - id: 8698 # Steel heraldic helm examine: "The colours represent a Jogre." - id: 8700 # Steel heraldic helm examine: "The colours represent Kandarin." - id: 8702 # Steel heraldic helm examine: "The colours represent Misthalin." - id: 8704 # Steel heraldic helm examine: "The colours represent money." - id: 8706 # Steel heraldic helm examine: "The colours represent Saradomin." - id: 8708 # Steel heraldic helm examine: "The colours represent a skull." - id: 8710 # Steel heraldic helm examine: "The colours represent Varrock." - id: 8712 # Steel heraldic helm examine: "The colours represent Zamorak." - id: 8714 # Rune kiteshield examine: "A shield with the symbol of Arrav." - id: 8716 # Rune kiteshield examine: "A shield with the symbol of Asgarnia." - id: 8718 # Rune kiteshield examine: "A shield with a picture of the Dorgeshuun brooch." - id: 8720 # Rune kiteshield examine: "A shield with a picture of a dragon." - id: 8722 # Rune kiteshield examine: "A shield with a picture of a fairy." - id: 8724 # Rune kiteshield examine: "A shield with the symbol of Guthix." - id: 8726 # Rune kiteshield examine: "A shield with the symbol of the HAM cult." - id: 8728 # Rune kiteshield examine: "A shield with a picture of the mythical 'horse'." - id: 8730 # Rune kiteshield examine: "A shield with a picture of a Jogre." - id: 8732 # Rune kiteshield examine: "A shield with the symbol of Kandarin." - id: 8734 # Rune kiteshield examine: "A shield with the symbol of Misthalin." - id: 8736 # Rune kiteshield examine: "A shield with a picture of a money-bag." - id: 8738 # Rune kiteshield examine: "A shield with the symbol of Saradomin." - id: 8740 # Rune kiteshield examine: "A shield with a picture of a skull." - id: 8742 # Rune kiteshield examine: "A shield with the symbol of Varrock." - id: 8744 # Rune kiteshield examine: "A shield with the symbol of Zamorak." - id: 8746 # Steel kiteshield examine: "A shield with the symbol of Arrav." - id: 8748 # Steel kiteshield examine: "A shield with the symbol of Asgarnia." - id: 8750 # Steel kiteshield examine: "A shield with a picture of the Dorgeshuun brooch." - id: 8752 # Steel kiteshield examine: "A shield with a picture of a dragon." - id: 8754 # Steel kiteshield examine: "A shield with a picture of a fairy." - id: 8756 # Steel kiteshield examine: "A shield with the symbol of Guthix." - id: 8758 # Steel kiteshield examine: "A shield with the symbol of the HAM cult." - id: 8760 # Steel kiteshield examine: "A shield with a picture of the mythical 'horse'." - id: 8762 # Steel kiteshield examine: "A shield with a picture of a Jogre." - id: 8764 # Steel kiteshield examine: "A shield with the symbol of Kandarin." - id: 8766 # Steel kiteshield examine: "A shield with the symbol of Misthalin." - id: 8768 # Steel kiteshield examine: "A shield with a picture of a money-bag." - id: 8770 # Steel kiteshield examine: "A shield with the symbol of Saradomin." - id: 8772 # Steel kiteshield examine: "A shield with a picture of a skull." - id: 8774 # Steel kiteshield examine: "A shield with the symbol of Varrock." - id: 8776 # Steel kiteshield examine: "A shield with the symbol of Zamorak." - id: 8778 # Oak plank examine: "A plank of sturdy oak." - id: 8780 # Teak plank examine: "A plank of fine teak." - id: 8782 # Mahogany plank examine: "A plank of expensive mahogany." - id: 8784 # Gold leaf examine: "A very delicate sheet of gold." - id: 8786 # Marble block examine: "A beautifully carved marble block." - id: 8788 # Magic stone examine: "A magic stone to make high-level furniture." - id: 8790 # Bolt of cloth examine: "A bolt of ordinary cloth." - id: 8792 # Clockwork examine: "A clockwork mechanism." - id: 8794 # Saw examine: "Good for cutting wood." - id: 8837 # Timber beam examine: "A hefty beam of timber, perfect for building temples." - id: 8839 # Void knight top examine: "Torso armour from the order of the Void Knights." - id: 8840 # Void knight robe examine: "Leg armour from the order of the Void Knights." - id: 8841 # Void knight mace examine: "A mace used by the order of the Void Knights." - id: 8842 # Void knight gloves examine: "Gloves as used by the order of the Void Knights." - id: 8844 # Bronze defender examine: "A defensive weapon." - id: 8845 # Iron defender examine: "A defensive weapon." - id: 8846 # Steel defender examine: "A defensive weapon." - id: 8847 # Black defender examine: "A defensive weapon." - id: 8848 # Mithril defender examine: "A defensive weapon." - id: 8849 # Adamant defender examine: "A defensive weapon." - id: 8850 # Rune defender examine: "A defensive weapon." - id: 8851 # Warrior guild token examine: "Warrior Guild Token." - id: 8856 # Defensive shield examine: "Large, round, heavy shield." - id: 8858 # 18lb shot examine: "Just landed 18lb shot." - id: 8859 # 22lb shot examine: "Just landed 22lb shot." - id: 8860 # One barrel examine: "To put on your head." - id: 8861 # Two barrels examine: "To put on your head." - id: 8862 # Three barrels examine: "To put on your head." - id: 8863 # Four barrels examine: "To put on your head." - id: 8864 # Five barrels examine: "To put on your head." - id: 8865 # Ground ashes examine: "A heap of finely ground ashes." - id: 8866 # Steel key examine: "You stole this key from a HAM guard." - id: 8867 # Bronze key examine: "You stole this key from a HAM guard." - id: 8868 # Silver key examine: "You stole this key from a HAM guard." - id: 8869 # Iron key examine: "You stole this key from a HAM guard." - id: 8870 # Zanik examine: "She's dead, but the mark on her forehead is glowing brightly." - id: 8871 # Crate with zanik examine: "It's got Zanik in it." - id: 8872 # Bone dagger examine: "A powerful dagger." - id: 8874 # Bone dagger (p) examine: "A powerful dagger." - id: 8876 # Bone dagger (p+) examine: "A powerful dagger." - id: 8878 # Bone dagger (p++) examine: "A powerful dagger." - id: 8882 # Bone bolts examine: "Good if you have a bone crossbow!" - id: 8887 # Zanik examine: "She's dead, but the mark on her forehead is glowing brightly." - id: 8890 # Coins examine: "Lovely money!" - id: 8901 # Black mask (10) examine: "A magic cave horror mask." - id: 8903 # Black mask (9) examine: "A magic cave horror mask." - id: 8905 # Black mask (8) examine: "A magic cave horror mask." - id: 8907 # Black mask (7) examine: "A magic cave horror mask." - id: 8909 # Black mask (6) examine: "A magic cave horror mask." - id: 8911 # Black mask (5) examine: "A magic cave horror mask." - id: 8913 # Black mask (4) examine: "A magic cave horror mask." - id: 8915 # Black mask (3) examine: "A magic cave horror mask." - id: 8917 # Black mask (2) examine: "A magic cave horror mask." - id: 8919 # Black mask (1) examine: "A magic cave horror mask." - id: 8921 # Black mask examine: "An inert-seeming cave horror mask." - id: 8923 # Witchwood icon examine: "A stick on a string... pure style." - id: 8924 # Bandana eyepatch examine: "Essential pirate wear." - id: 8925 # Bandana eyepatch examine: "Essential pirate wear." - id: 8926 # Bandana eyepatch examine: "Essential pirate wear." - id: 8927 # Bandana eyepatch examine: "Essential pirate wear." - id: 8928 # Hat eyepatch examine: "Essential pirate wear." - id: 8929 # Crabclaw hook examine: "Tied together so they don't come apart." - id: 8930 # Pipe section examine: "Crude wooden pipe section." - id: 8932 # Lumber patch examine: "Repairs made with this will be patchy at best." - id: 8934 # Scrapey tree logs examine: "Slimy logs from the Scrapey tree." - id: 8936 # Blue flowers examine: "Very blue." - id: 8938 # Red flowers examine: "Very red." - id: 8940 # Rum examine: "Alcohol in the loosest sense of the word." - id: 8941 # Rum examine: "Alcohol in the loosest sense of the word." - id: 8942 # Monkey examine: "A confused looking monkey." - id: 8943 # Blue monkey examine: "Bluuuuuuuue Monkeeeeeey!" - id: 8944 # Blue monkey examine: "Bluuuuuuuue Monkeeeeeey!" - id: 8945 # Blue monkey examine: "Bluuuuuuuue Monkeeeeeey!" - id: 8946 # Red monkey examine: "A well red monkey." - id: 8947 # Red monkey examine: "A well red monkey." - id: 8948 # Red monkey examine: "A well red monkey." - id: 8949 # Pirate bandana examine: "A blue bandana." - id: 8950 # Pirate hat examine: "A red pirate hat." - id: 8951 # Pieces of eight examine: "Piratical currency." - id: 8952 # Blue naval shirt examine: "...You can sail the seven seas..." - id: 8953 # Green naval shirt examine: "...You can sail the seven seas..." - id: 8954 # Red naval shirt examine: "...You can sail the seven seas..." - id: 8955 # Brown naval shirt examine: "...You can sail the seven seas..." - id: 8956 # Black naval shirt examine: "...You can sail the seven seas..." - id: 8957 # Purple naval shirt examine: "...You can sail the seven seas..." - id: 8958 # Grey naval shirt examine: "...You can sail the seven seas..." - id: 8959 # Blue tricorn hat examine: "I could never look square in this." - id: 8960 # Green tricorn hat examine: "I could never look square in this." - id: 8961 # Red tricorn hat examine: "I could never look square in this." - id: 8962 # Brown tricorn hat examine: "I could never look square in this." - id: 8963 # Black tricorn hat examine: "I could never look square in this." - id: 8964 # Purple tricorn hat examine: "I could never look square in this." - id: 8965 # Grey tricorn hat examine: "I could never look square in this." - id: 8966 # Cutthroat flag examine: "The flag of The Cutthroat." - id: 8967 # Guilded smile flag examine: "The flag of The Guilded Smile." - id: 8968 # Bronze fist flag examine: "The flag of The Bronze Fist." - id: 8969 # Lucky shot flag examine: "The flag of The Lucky Shot." - id: 8970 # Treasure flag examine: "The flag of The Treasure Trove." - id: 8971 # Phasmatys flag examine: "The flag of The Phasmatys Pride." - id: 8972 # Bowl of red water examine: "A bowl of red water." - id: 8974 # Bowl of blue water examine: "A bowl of blue water." - id: 8976 # Bitternut examine: "Monkeys seem to like throwing these." - id: 8977 # Scrapey bark examine: "Greasy bark from the Scrapey Tree." - id: 8979 # Bridge section examine: "Caution; not for use over troubled water." - id: 8981 # Sweetgrubs examine: "Well, at least they aren't trying mind-control." - id: 8986 # Bucket examine: "It's a wooden bucket." - id: 8987 # Torch examine: "An unlit home-made torch." - id: 8988 # The stuff examine: "Apparently good for brewing." - id: 8989 # Brewin' guide examine: "A how-to of brewing and arson." - id: 8990 # Brewin' guide examine: "A how-to of brewing and arson." - id: 8991 # Blue navy slacks examine: "Not for slackers." - id: 8992 # Green navy slacks examine: "Not for slackers." - id: 8993 # Red navy slacks examine: "Not for slackers." - id: 8994 # Brown navy slacks examine: "Not for slackers." - id: 8995 # Black navy slacks examine: "Not for slackers." - id: 8996 # Purple navy slacks examine: "Not for slackers." - id: 8997 # Grey navy slacks examine: "Not for slackers." - id: 9003 # Security book examine: "WARNING: Contains information which could make your account secure!" - id: 9004 # Stronghold notes examine: "Information regarding the Stronghold of Security." - id: 9005 # Fancy boots examine: "Very nice boots from the Stronghold of Security." - id: 9006 # Fighting boots examine: "Very nice boots from the Stronghold of Security." - id: 9007 # Right skull half examine: "Ooooh spooky!" - id: 9008 # Left skull half examine: "Ooooh spooky!" - id: 9009 # Strange skull examine: "Seems to be for use with a staff or sceptre of some sort." - id: 9010 # Top of sceptre examine: "Top half of a broken Sceptre." - id: 9011 # Bottom of sceptre examine: "Bottom half of a broken Sceptre." - id: 9012 # Runed sceptre examine: "Sceptre with runes on, it appears to be missing something." - id: 9013 # Skull sceptre examine: "A fragile magical Sceptre." - id: 9016 # Gorak claws examine: "Oversized nail clippings." - id: 9017 # Star flower examine: "A rare flower with magical properties." - id: 9018 # Gorak claw powder examine: "Ground-down Gorak claws." - id: 9020 # Queen's secateurs examine: "Contains the Fairy Queen's magic essence." - id: 9021 # Magic essence(4) examine: "4 doses of magic essence potion." - id: 9022 # Magic essence(3) examine: "3 doses of magic essence potion." - id: 9023 # Magic essence(2) examine: "2 doses of magic essence potion." - id: 9024 # Magic essence(1) examine: "1 dose of magic essence potion." - id: 9025 # Nuff's certificate examine: "A scroll that says she's a healer, that's Fairy Nuff." - id: 9026 # Ivory comb examine: "Gets knots and kinks out of your hair." - id: 9028 # Golden scarab examine: "Little ornament in the shape of a scarab." - id: 9030 # Stone scarab examine: "Little ornament in the shape of a scarab." - id: 9032 # Pottery scarab examine: "Little ornament in the shape of a scarab." - id: 9034 # Golden statuette examine: "A small golden statuette." - id: 9036 # Pottery statuette examine: "A small statuette." - id: 9038 # Stone statuette examine: "A small statuette." - id: 9040 # Gold seal examine: "A seal. It's gold." - id: 9042 # Stone seal examine: "A seal. Made out of stone obviously." - id: 9052 # Locust meat examine: "Delicious and nutritious. Well, nutritious anyway." - id: 9054 # Red goblin mail examine: "Armour designed to fit goblins." - id: 9055 # Black goblin mail examine: "Armour designed to fit goblins." - id: 9056 # Yellow goblin mail examine: "Armour designed to fit goblins." - id: 9057 # Green goblin mail examine: "Armour designed to fit goblins." - id: 9058 # Purple goblin mail examine: "Armour designed to fit goblins." - id: 9059 # Pink goblin mail examine: "Armour designed to fit goblins." - id: 9064 # Emerald lantern examine: "A mystical lantern." - id: 9065 # Emerald lantern examine: "A mystical lantern casting a green beam." - id: 9066 # Emerald lens examine: "A roughly circular disc of glass." - id: 9067 # Dream log examine: "A log of my thoughts..." - id: 9068 # Moonclan helm examine: "Mystical headgear." - id: 9069 # Moonclan hat examine: "A mystical hat." - id: 9070 # Moonclan armour examine: "Provides good protection." - id: 9071 # Moonclan skirt examine: "This should protect my legs." - id: 9072 # Moonclan gloves examine: "These should keep my hands safe." - id: 9073 # Moonclan boots examine: "Groovy foot protection." - id: 9074 # Moonclan cape examine: "A mystical cape." - id: 9075 # Astral rune examine: "Used for Lunar spells." - id: 9076 # Lunar ore examine: "This needs refining." - id: 9077 # Lunar bar examine: "It's a bar of magic metal." - id: 9078 # Moonclan manual examine: "A book of Moonclan history" - id: 9079 # Suqah tooth examine: "The tooth, the whole tooth, and nothing but the tooth." - id: 9080 # Suqah hide examine: "An untanned piece of Suqah hide." - id: 9081 # Suqah leather examine: "A piece of Suqah hide that has been expertly tanned into leather." - id: 9082 # Ground tooth examine: "A ground Suqah tooth." - id: 9083 # Seal of passage examine: "A seal of passage issued by Brundt the Chieftain of the Fremennik." - id: 9084 # Lunar staff examine: "A Moonclan staff." - id: 9085 # Empty vial examine: "A vessel for holding liquid." - id: 9086 # Vial of water examine: "A vessel containing water." - id: 9087 # Waking sleep vial examine: "A vessel for dreaming while awake!" - id: 9088 # Guam vial examine: "A vessel with water and Guam inside." - id: 9089 # Marr vial examine: "A vessel with water and Marrentill inside." - id: 9090 # Guam-marr vial examine: "A vessel with water, Guam and Marrentill inside." - id: 9091 # Lunar staff - pt1 examine: "A staff enchanted by air." - id: 9092 # Lunar staff - pt2 examine: "A staff enchanted by air and fire." - id: 9093 # Lunar staff - pt3 examine: "A staff enchanted by air, fire and water." - id: 9094 # Kindling examine: "Small bits of wood from the first magic tree!" - id: 9095 # Soaked kindling examine: "Magic wood soaked with a potion of waking sleep. Groovy." - id: 9096 # Lunar helm examine: "A mystical helmet." - id: 9097 # Lunar torso examine: "Provides good protection." - id: 9098 # Lunar legs examine: "These should protect my legs." - id: 9099 # Lunar gloves examine: "These should keep my hands safe." - id: 9100 # Lunar boots examine: "Mystical foot protection." - id: 9101 # Lunar cape examine: "Oooo pretty!" - id: 9102 # Lunar amulet examine: "Awesome." - id: 9103 # A special tiara examine: "I'll be the talk of the town with this... maybe." - id: 9104 # Lunar ring examine: "A mysterious ring that can fill the wearer with magical power..." - id: 9139 # Blurite bolts examine: "Blurite crossbow bolts." - id: 9140 # Iron bolts examine: "Iron crossbow bolts." - id: 9141 # Steel bolts examine: "Steel crossbow bolts." - id: 9142 # Mithril bolts examine: "Mithril crossbow bolts." - id: 9143 # Adamant bolts examine: "Adamantite crossbow bolts." - id: 9144 # Runite bolts examine: "Runite crossbow bolts." - id: 9145 # Silver bolts examine: "Silver crossbow bolts." - id: 9187 # Jade bolt tips examine: "Jade bolt tips." - id: 9188 # Topaz bolt tips examine: "Red Topaz bolt tips." - id: 9189 # Sapphire bolt tips examine: "Sapphire bolt tips." - id: 9190 # Emerald bolt tips examine: "Emerald bolt tips." - id: 9191 # Ruby bolt tips examine: "Ruby bolt tips." - id: 9192 # Diamond bolt tips examine: "Diamond bolt tips." - id: 9194 # Onyx bolt tips examine: "Onyx bolt tips." - id: 9236 # Opal bolts (e) examine: "Enchanted Opal tipped Bronze Crossbow Bolts." - id: 9237 # Jade bolts (e) examine: "Enchanted Jade tipped Blurite Crossbow Bolts." - id: 9238 # Pearl bolts (e) examine: "Enchanted Pearl tipped Iron Crossbow Bolts." - id: 9239 # Topaz bolts (e) examine: "Enchanted Red Topaz tipped Steel Crossbow Bolts." - id: 9240 # Sapphire bolts (e) examine: "Enchanted Sapphire tipped Mithril Crossbow Bolts." - id: 9241 # Emerald bolts (e) examine: "Enchanted Emerald tipped Mithril Crossbow Bolts." - id: 9242 # Ruby bolts (e) examine: "Enchanted Ruby tipped Adamantite Crossbow Bolts." - id: 9243 # Diamond bolts (e) examine: "Enchanted Diamond tipped Adamantite Crossbow Bolts." - id: 9245 # Onyx bolts (e) examine: "Enchanted Onyx tipped Runite Crossbow Bolts." - id: 9287 # Iron bolts (p) examine: "Some poisoned iron bolts." - id: 9288 # Steel bolts (p) examine: "Some poisoned steel bolts." - id: 9289 # Mithril bolts (p) examine: "Some poisoned mithril bolts." - id: 9291 # Runite bolts (p) examine: "Some poisoned runite bolts." - id: 9292 # Silver bolts (p) examine: "Some poisoned silver bolts." - id: 9335 # Jade bolts examine: "Jade tipped Blurite crossbow bolts." - id: 9336 # Topaz bolts examine: "Topaz tipped Steel crossbow bolts." - id: 9337 # Sapphire bolts examine: "Sapphire tipped Mithril crossbow bolts." - id: 9338 # Emerald bolts examine: "Emerald tipped Mithril crossbow bolts." - id: 9339 # Ruby bolts examine: "Ruby tipped Adamantite crossbow bolts." - id: 9340 # Diamond bolts examine: "Diamond tipped Adamantite crossbow bolts." - id: 9342 # Onyx bolts examine: "Onyx tipped Runite crossbow bolts." - id: 9375 # Bronze bolts (unf) examine: "Unfeathered bronze crossbow bolts." - id: 9376 # Blurite bolts (unf) examine: "Unfeathered blurite crossbow bolts." - id: 9377 # Iron bolts (unf) examine: "Unfeathered iron crossbow bolts." - id: 9378 # Steel bolts (unf) examine: "Unfeathered steel crossbow bolts." - id: 9379 # Mithril bolts (unf) examine: "Unfeathered mithril crossbow bolts." - id: 9380 # Adamant bolts(unf) examine: "Unfeathered adamantite crossbow bolts." - id: 9381 # Runite bolts (unf) examine: "Unfeathered runite crossbow bolts." - id: 9382 # Silver bolts (unf) examine: "Unfeathered silver crossbow bolts." - id: 9416 # Mith grapple tip examine: "A mithril grapple tip." - id: 9418 # Mith grapple examine: "A mithril grapple tipped bolt - needs a rope." - id: 9419 # Mith grapple examine: "A mithril grapple tipped bolt with a rope." - id: 9420 # Bronze limbs examine: "A pair of bronze crossbow limbs." - id: 9422 # Blurite limbs examine: "A pair of blurite crossbow limbs." - id: 9423 # Iron limbs examine: "A pair of iron crossbow limbs." - id: 9425 # Steel limbs examine: "A pair of steel crossbow limbs." - id: 9427 # Mithril limbs examine: "A pair of mithril crossbow limbs." - id: 9429 # Adamantite limbs examine: "A pair of adamantite crossbow limbs." - id: 9431 # Runite limbs examine: "A pair of runite crossbow limbs." - id: 9433 # Bolt pouch examine: "A pouch for storing crossbow bolts." - id: 9434 # Bolt mould examine: "A mould for creating silver crossbow bolts." - id: 9436 # Sinew examine: "I can use this to make a crossbow string." - id: 9438 # Crossbow string examine: "A string for a crossbow." - id: 9440 # Wooden stock examine: "A wooden crossbow stock." - id: 9442 # Oak stock examine: "An oak crossbow stock." - id: 9444 # Willow stock examine: "A willow crossbow stock." - id: 9446 # Teak stock examine: "A teak crossbow stock." - id: 9448 # Maple stock examine: "A maple crossbow stock." - id: 9450 # Mahogany stock examine: "A mahogany crossbow stock." - id: 9452 # Yew stock examine: "A yew crossbow stock." - id: 9467 # Blurite bar examine: "It's a bar of blurite." - id: 9468 # Sawdust examine: "What is left over when a log is made into a plank." - id: 9469 # Grand seed pod examine: "A seed pod of the Grand Tree." - id: 9470 # Gnome scarf examine: "A scarf. You feel your upper lip stiffening." - id: 9472 # Gnome goggles examine: "Tally Ho!" - id: 9474 # Reward token examine: "This entitles you to one free gnome food delivery." - id: 9475 # Mint cake examine: "It looks very minty." - id: 9477 # Aluft aloft box examine: "You can check on your delivery details here." - id: 9478 # Half made batta examine: "This fruit batta needs baking and garnishing with spice." - id: 9479 # Unfinished batta examine: "This fruit batta needs garnishing with spice." - id: 9480 # Half made batta examine: "This worm batta needs baking and garnishing with equa leaves." - id: 9482 # Half made batta examine: "This toad batta just requires baking to complete." - id: 9483 # Half made batta examine: "This cheese and tom batta needs baking and garnishing with equa leaves." - id: 9485 # Half made batta examine: "This veg batta needs baking and garnishing with equa leaves." - id: 9487 # Wizard blizzard examine: "This looks like a strange mix." - id: 9489 # Wizard blizzard examine: "This looks like a strange mix." - id: 9508 # Wizard blizzard examine: "This looks like a strange mix." - id: 9510 # Short green guy examine: "A Short Green Guy... looks good." - id: 9512 # Pineapple punch examine: "A fresh healthy fruit mix." - id: 9514 # Fruit blast examine: "A cool refreshing fruit mix." - id: 9516 # Drunk dragon examine: "A warm creamy alcoholic beverage." - id: 9518 # Choc saturday examine: "A warm creamy alcoholic beverage." - id: 9520 # Blurberry special examine: "Looks good... smells strong." - id: 9522 # Batta tin examine: "A deep tin used for baking gnome battas in." - id: 9524 # Batta tin examine: "A deep tin used for baking gnome battas in." - id: 9527 # Fruit batta examine: "It actually smells quite good." - id: 9529 # Toad batta examine: "It actually smells quite good." - id: 9531 # Worm batta examine: "It actually smells quite good." - id: 9533 # Vegetable batta examine: "Well... it looks healthy." - id: 9535 # Cheese+tom batta examine: "This smells really good." - id: 9538 # Toad crunchies examine: "It actually smells quite good." - id: 9540 # Spicy crunchies examine: "Yum... smells spicy." - id: 9542 # Worm crunchies examine: "It actually smells quite good." - id: 9544 # Chocchip crunchies examine: "Yum... smells good." - id: 9547 # Worm hole examine: "It actually smells quite good." - id: 9549 # Veg ball examine: "This looks pretty healthy." - id: 9553 # Chocolate bomb examine: "Full of creamy, chocolately goodness." - id: 9558 # Half made bowl examine: "This unfinished tangled toads legs requires baking." - id: 9559 # Half made bowl examine: "This unfinished worm hole needs baking and garnishing with equa leaves." - id: 9560 # Unfinished bowl examine: "This unfinished worm hole needs garnishing with equa leaves." - id: 9561 # Half made bowl examine: "This unfinished veg ball needs baking and garnishing with equa leaves." - id: 9562 # Unfinished bowl examine: "This unfinished veg ball needs garnishing with equa leaves." - id: 9563 # Half made bowl examine: "This unfinished choc bomb needs baking, two pots of cream and choc dust." - id: 9564 # Unfinished bowl examine: "This unfinished choc bomb needs two pots of cream and chocolate dust." - id: 9566 # Mixed blizzard examine: "This Wizzard Blizzard needs pouring, a lime slice and pineapple chunks." - id: 9567 # Mixed sgg examine: "This Short Green Guy cocktail needs pouring, a lime slice and equa leaves.." - id: 9568 # Mixed blast examine: "This Fruit Blast cocktail needs pouring and a lemon slice." - id: 9569 # Mixed punch examine: "This Pineapple Punch needs pouring, lime and pineapple chunks and a orange slice." - id: 9570 # Mixed special examine: "This Blurberry Special needs pouring, orange and lemon chunks, a lime slice and equa leaves." - id: 9571 # Mixed saturday examine: "This Choc Saturday needs pouring, heating and cream and chocolate dust." - id: 9572 # Mixed saturday examine: "This Choc Saturday needs heating, and cream and chocolate dust." - id: 9573 # Mixed saturday examine: "This Choc Saturday needs cream and chocolate dust to finish." - id: 9574 # Mixed dragon examine: "This Drunk Dragon needs pouring, pineapple chunks, cream and heating." - id: 9575 # Mixed dragon examine: "This Drunk Dragon needs pineapple chunks, cream and heating." - id: 9576 # Mixed dragon examine: "This Drunk Dragon needs heating to finish." - id: 9577 # Half made crunchy examine: "This choc chip crunchy needs baking and garnishing with chocolate dust." - id: 9578 # Unfinished crunchy examine: "This choc chip crunchy needs garnishing with chocolate dust." - id: 9579 # Half made crunchy examine: "This spicy crunchy needs baking and garnishing with spice." - id: 9580 # Unfinished crunchy examine: "This spicy crunchy needs garnishing with spice." - id: 9581 # Half made crunchy examine: "This toad crunchy needs baking and garnishing with equa leaves." - id: 9582 # Unfinished crunchy examine: "This toad crunchy needs garnishing with equa leaves." - id: 9583 # Half made crunchy examine: "This worm crunchy needs baking and garnishing with gnome spices." - id: 9584 # Unfinished crunchy examine: "This worm crunchy needs garnishing with gnome spices." - id: 9589 # Dossier examine: "A dossier containing info on the Black Knight plot." - id: 9590 # Dossier examine: "A dossier containing info on the Black Knight plot." - id: 9592 # Magic glue examine: "Glue made from tree sap and ground mud runes." - id: 9593 # Weird gloop examine: "This doesn't look like it will do anything interesting." - id: 9594 # Ground mud runes examine: "Mud runes ground into a powder." - id: 9597 # A red circle examine: "A red circular crystalline disc." - id: 9598 # A red triangle examine: "A red triangular crystalline disc." - id: 9599 # A red square examine: "A red square-shaped crystalline disc." - id: 9600 # A red pentagon examine: "A red pentagon-shaped crystalline disc." - id: 9601 # An orange circle examine: "An orange circular crystalline disc." - id: 9602 # An orange triangle examine: "An orange triangular crystalline disc." - id: 9603 # An orange square examine: "An orange square-shaped crystalline disc." - id: 9604 # Orange pentagon examine: "An orange pentagon-shaped crystalline disc." - id: 9605 # A yellow circle examine: "A yellow circular crystalline disc." - id: 9606 # A yellow triangle examine: "A yellow triangular crystalline disc." - id: 9607 # A yellow square examine: "A yellow square-shaped crystalline disc." - id: 9608 # A yellow pentagon examine: "A yellow pentagon-shaped crystalline disc." - id: 9609 # A green circle examine: "A green circular crystalline disc." - id: 9610 # A green triangle examine: "A green triangular crystalline disc." - id: 9611 # A green square examine: "A green square-shaped crystalline disc." - id: 9612 # A green pentagon examine: "A green pentagon-shaped crystalline disc." - id: 9613 # A blue circle examine: "A blue circular crystalline disc." - id: 9614 # A blue triangle examine: "A blue triangular crystalline disc." - id: 9615 # A blue square examine: "A blue square-shaped crystalline disc." - id: 9616 # A blue pentagon examine: "A blue pentagon-shaped crystalline disc." - id: 9617 # An indigo circle examine: "An indigo circular crystalline disc." - id: 9618 # An indigo triangle examine: "An indigo triangular crystalline disc." - id: 9619 # An indigo square examine: "An indigo square-shaped crystalline disc." - id: 9620 # An indigo pentagon examine: "An indigo pentagon-shaped crystalline disc." - id: 9621 # A violet circle examine: "A violet circular crystalline disc." - id: 9622 # A violet triangle examine: "A violet triangular crystalline disc." - id: 9623 # A violet square examine: "A violet square-shaped crystalline disc." - id: 9624 # A violet pentagon examine: "A violet pentagon-shaped crystalline disc." - id: 9625 # Crystal saw examine: "A magical saw." - id: 9627 # A handwritten book examine: "A book on elven crystal." - id: 9629 # Tyras helm examine: "As used by King Tyras' personal guard." - id: 9632 # Daeyalt ore examine: "This needs refining." - id: 9633 # Message examine: "A message for Veliaf." - id: 9634 # Vyrewatch top examine: "Dress like the powerful vyrewatch!" - id: 9636 # Vyrewatch legs examine: "Dress like the powerful vyrewatch!" - id: 9638 # Vyrewatch shoes examine: "Dress like the powerful vyrewatch!" - id: 9640 # Citizen top examine: "Ghetto disguise!" - id: 9642 # Citizen trousers examine: "Ghetto disguise!" - id: 9644 # Citizen shoes examine: "Ghetto disguise!" - id: 9646 # Castle sketch 1 examine: "Northern approach of the castle." - id: 9647 # Castle sketch 2 examine: "Western approach of the castle." - id: 9648 # Castle sketch 3 examine: "Southern approach of the castle." - id: 9649 # Message examine: "A message found behind a loose tile." - id: 9651 # Large ornate key examine: "A key to some large, strange door." - id: 9652 # Haemalchemy examine: "A book called Haemalchemy Volume 1." - id: 9653 # Sealed message examine: "A sealed message from Safalaan to Veliaf." - id: 9654 # Door key examine: "A key to some door." - id: 9655 # Ladder top examine: "The top of a ladder." - id: 9665 # Torch examine: "An unlit home-made torch." - id: 9668 # Initiate harness m examine: "Initiate level armour pack." - id: 9672 # Proselyte sallet examine: "A Proselyte Temple Knight's helm." - id: 9674 # Proselyte hauberk examine: "A Proselyte Temple Knight's armour." - id: 9676 # Proselyte cuisse examine: "A Proselyte Temple Knight's leg armour." - id: 9678 # Proselyte tasset examine: "A Proselyte Temple Knight's leg armour." - id: 9680 # Sea slug glue examine: "A rendered down baby sea slug." - id: 9681 # Commorb v2 examine: "A Temple Knight Communication Orb. Top Secret!" - id: 9682 # Door transcription examine: "A copy of the mysterious glyphs." - id: 9683 # Dead sea slug examine: "Dead sea slug, very sticky." - id: 9684 # Page 1 examine: "A page from Maledict's holy book." - id: 9685 # Page 2 examine: "A page from Maledict's holy book." - id: 9686 # Page 3 examine: "A page from Maledict's holy book." - id: 9687 # Fragment 1 examine: "A piece of a torn page." - id: 9688 # Fragment 2 examine: "A piece of a torn page." - id: 9689 # Fragment 3 examine: "A piece of a torn page." - id: 9690 # Blank water rune examine: "A blank water rune." - id: 9691 # Water rune examine: "A water rune." - id: 9692 # Blank air rune examine: "A blank air rune." - id: 9693 # Air rune examine: "An air rune." - id: 9694 # Blank earth rune examine: "A blank earth rune." - id: 9695 # Earth rune examine: "An earth rune." - id: 9696 # Blank mind rune examine: "A blank mind rune." - id: 9697 # Mind rune examine: "A mind rune." - id: 9698 # Blank fire rune examine: "A blank fire rune." - id: 9699 # Fire rune examine: "A fire rune." - id: 9703 # Training sword examine: "Basic training sword." - id: 9704 # Training shield examine: "Made of flimsy painted wood." - id: 9705 # Training bow examine: "Light and flexible, good for a beginner." - id: 9706 # Training arrows examine: "Standard training arrows." - id: 9715 # Slashed book examine: "Book of the elemental shield." - id: 9717 # Beaten book examine: "Book of the Elemental Helm." - id: 9718 # Crane schematic examine: "On the subject of lava dippers." - id: 9719 # Lever schematic examine: "A scroll with a lever schematic drawn on it." - id: 9720 # Crane claw examine: "A crane claw." - id: 9721 # Scroll examine: "A scroll with some writing on it." - id: 9722 # Key examine: "Quite a small key." - id: 9723 # Pipe examine: "A spare section of pipe." - id: 9724 # Large cog examine: "A large cog." - id: 9725 # Medium cog examine: "A medium cog." - id: 9726 # Small cog examine: "A small cog." - id: 9727 # Primed bar examine: "A primed elemental ingot." - id: 9728 # Primed mind bar examine: "An elemental mind ingot." - id: 9729 # Elemental helmet examine: "A magic helmet." - id: 9731 # Mind shield examine: "A magic shield." - id: 9733 # Mind helmet examine: "A magic helmet." - id: 9735 # Desert goat horn examine: "Not much good for blowing." - id: 9736 # Goat horn dust examine: "Finely ground desert goat horn." - id: 9739 # Combat potion(4) examine: "4 doses of combat potion." - id: 9741 # Combat potion(3) examine: "3 doses of combat potion." - id: 9743 # Combat potion(2) examine: "2 doses of combat potion." - id: 9745 # Combat potion(1) examine: "1 dose of combat potion." - id: 9747 # Attack cape examine: "The cape worn by masters of Attack." - id: 9748 # Attack cape(t) examine: "The cape worn by masters of Attack." - id: 9749 # Attack hood examine: "Attack skillcape hood." - id: 9750 # Strength cape examine: "The cape worn by only the strongest people." - id: 9751 # Strength cape(t) examine: "The cape worn by only the strongest people." - id: 9752 # Strength hood examine: "Strength skillcape hood." - id: 9753 # Defence cape examine: "The cape worn by masters of the art of Defence." - id: 9754 # Defence cape(t) examine: "The cape worn by masters of the art of Defence." - id: 9755 # Defence hood examine: "Defence skillcape hood." - id: 9756 # Ranging cape examine: "The cape worn by master archers." - id: 9757 # Ranging cape(t) examine: "The cape worn by master archers." - id: 9758 # Ranging hood examine: "Range skillcape hood." - id: 9759 # Prayer cape examine: "The cape worn by the most pious of heroes." - id: 9760 # Prayer cape(t) examine: "The cape worn by the most pious of heroes." - id: 9761 # Prayer hood examine: "Prayer skillcape hood." - id: 9762 # Magic cape examine: "The cape worn by the most powerful mages." - id: 9763 # Magic cape(t) examine: "The cape worn by the most powerful mages." - id: 9764 # Magic hood examine: "Magic skillcape hood." - id: 9765 # Runecraft cape examine: "The cape worn by master runecrafters." - id: 9766 # Runecraft cape(t) examine: "The cape worn by master runecrafters." - id: 9768 # Hitpoints cape examine: "The cape worn by the healthiest adventurers." - id: 9769 # Hitpoints cape(t) examine: "The cape worn by the healthiest adventurers." - id: 9770 # Hitpoints hood examine: "Hitpoints skillcape hood." - id: 9771 # Agility cape examine: "The cape worn by the most agile of heroes." - id: 9772 # Agility cape(t) examine: "The cape worn by the most agile of heroes." - id: 9773 # Agility hood examine: "Agility skillcape hood." - id: 9774 # Herblore cape examine: "The cape worn by the most skilled at the art of Herblore." - id: 9775 # Herblore cape(t) examine: "The cape worn by the most skilled at the art of Herblore." - id: 9776 # Herblore hood examine: "Herblore skillcape hood." - id: 9777 # Thieving cape examine: "The cape worn by master thieves." - id: 9778 # Thieving cape(t) examine: "The cape worn by master thieves." - id: 9779 # Thieving hood examine: "Thieving skillcape hood." - id: 9780 # Crafting cape examine: "The cape worn by master craftworkers." - id: 9781 # Crafting cape(t) examine: "The cape worn by master craftworkers." - id: 9782 # Crafting hood examine: "Crafting skillcape hood." - id: 9783 # Fletching cape examine: "The cape worn by the best of fletchers." - id: 9784 # Fletching cape(t) examine: "The cape worn by the best of fletchers." - id: 9785 # Fletching hood examine: "Fletching skillcape hood." - id: 9786 # Slayer cape examine: "The cape worn by Slayer masters." - id: 9787 # Slayer cape(t) examine: "The cape worn by Slayer masters." - id: 9788 # Slayer hood examine: "Slayer skillcape hood." - id: 9789 # Construct. cape examine: "The cape worn by master builders." - id: 9790 # Construct. cape(t) examine: "The cape worn by master builders." - id: 9791 # Construct. hood examine: "Construction skillcape hood." - id: 9792 # Mining cape examine: "The cape worn by the most skilled miners." - id: 9793 # Mining cape(t) examine: "The cape worn by the most skilled miners." - id: 9794 # Mining hood examine: "Mining skillcape hood." - id: 9795 # Smithing cape examine: "The cape worn by master smiths." - id: 9796 # Smithing cape(t) examine: "The cape worn by master smiths." - id: 9797 # Smithing hood examine: "Smithing skillcape hood." - id: 9798 # Fishing cape examine: "The cape worn by the best fishermen." - id: 9799 # Fishing cape(t) examine: "The cape worn by the best fishermen." - id: 9800 # Fishing hood examine: "Fishing skillcape hood." - id: 9801 # Cooking cape examine: "The cape worn by the world's best chefs." - id: 9802 # Cooking cape(t) examine: "The cape worn by the world's best chefs." - id: 9803 # Cooking hood examine: "Cooking skillcape hood." - id: 9804 # Firemaking cape examine: "The cape worn by master firelighters." - id: 9805 # Firemaking cape(t) examine: "The cape worn by master firelighters." - id: 9806 # Firemaking hood examine: "Firemaking skillcape hood." - id: 9807 # Woodcutting cape examine: "The cape worn by master woodcutters." - id: 9808 # Woodcut. cape(t) examine: "The cape worn by master woodcutters." - id: 9809 # Woodcutting hood examine: "Woodcutting skillcape hood." - id: 9810 # Farming cape examine: "The cape worn by master farmers." - id: 9811 # Farming cape(t) examine: "The cape worn by master farmers." - id: 9812 # Farming hood examine: "Farming skillcape hood." - id: 9813 # Quest point cape examine: "The cape worn by only the most experienced adventurers." - id: 9814 # Quest point hood examine: "Quest point cape hood." - id: 9815 # Bobble hat examine: "A woolly bobble hat." - id: 9816 # Bobble scarf examine: "A woolly scarf." - id: 9843 # Oak cape rack examine: "How does it all fit in there?" - id: 9844 # Teak cape rack examine: "How does it all fit in there?" - id: 9846 # Gilded cape rack examine: "How does it all fit in there?" - id: 9847 # Marble cape rack examine: "How does it all fit in there?" - id: 9848 # Magic cape rack examine: "How does it all fit in there?" - id: 9849 # Oak toy box examine: "How does it all fit in there?" - id: 9850 # Teak toy box examine: "How does it all fit in there?" - id: 9851 # Mahogany toy box examine: "How does it all fit in there?" - id: 9859 # Oak armour case examine: "How does it all fit in there?" - id: 9860 # Teak armour case examine: "How does it all fit in there?" - id: 9862 # Oak treasure chest examine: "How does it all fit in there?" - id: 9864 # M. treasure chest examine: "Mahogany treasure chest." - id: 9901 # Goutweedy lump examine: "A lump that might at some point have been a gout tuber." - id: 9902 # Hardy gout tubers examine: "A pile of gout tubers suitable for use in mountainous terrain." - id: 9903 # Farming manual examine: "Farmer Gricoller's Farming Manual." - id: 9906 # Ghost buster 500 examine: "A Ghost Buster 500 loaded with a white destabiliser." - id: 9907 # Ghost buster 500 examine: "A Ghost Buster 500 loaded with a red destabiliser." - id: 9908 # Ghost buster 500 examine: "A Ghost Buster 500 loaded with a blue destabiliser." - id: 9909 # Ghost buster 500 examine: "A Ghost Buster 500 loaded with a green destabiliser." - id: 9910 # Ghost buster 500 examine: "A Ghost Buster 500 loaded with a yellow destabiliser." - id: 9911 # Ghost buster 500 examine: "A Ghost Buster 500 loaded with a black destabiliser." - id: 9912 # Ghost buster 500 examine: "A Ghost Buster 500 without a destabiliser loaded." - id: 9913 # White destabiliser examine: "A white destabiliser for use in a Ghost Buster." - id: 9914 # Red destabiliser examine: "A red destabiliser for use in a Ghost Buster." - id: 9915 # Blue destabiliser examine: "A blue destabiliser for use in a Ghost Buster." - id: 9916 # Green destabiliser examine: "A green destabiliser for use in a Ghost Buster." - id: 9917 # Yellow destabiliser examine: "A yellow destabiliser for use in a Ghost Buster." - id: 9918 # Black destabiliser examine: "A black destabiliser for use in a Ghost Buster." - id: 9919 # Evil root examine: "A freshly cut root of all evil." - id: 9920 # Jack lantern mask examine: "Better not light it!" - id: 9921 # Skeleton boots examine: "Skeleton feet." - id: 9922 # Skeleton gloves examine: "Some skeletal gloves." - id: 9923 # Skeleton leggings examine: "Does my pelvis look big in this?" - id: 9924 # Skeleton shirt examine: "The shirt of a full body skeleton costume." - id: 9925 # Skeleton mask examine: "A scary skeleton mask." ================================================ FILE: data/config/item-spawns/lumbridge/lumbridge.json ================================================ [ { "item": "rs:coins", "amount": 25, "spawn_x": 3211, "spawn_y": 3240, "instance": "global", "respawn": 25, "metadata": { "hello": "world" } }, { "item": "rs:egg", "spawn_x": 3191, "spawn_y": 3276 }, { "item": "rs:egg", "spawn_x": 3226, "spawn_y": 3300 }, { "item": "rs:egg", "spawn_x": 3228, "spawn_y": 3299 }, { "item": "rs:egg", "spawn_x": 3231, "spawn_y": 3301, "instance": "player" } ] ================================================ FILE: data/config/items/barrows/dharoks.json ================================================ { "rs:dharoks_axe": { "tradable": true, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "two_handed", "requirements": { "skills": { "attack": 70, "strength": 70 } }, "offensive_bonuses": { "speed": 7, "stab": -4, "slash": 103, "crush": 95, "magic": -4 }, "defensive_bonuses": { "ranged": -1 }, "skill_bonuses": { "strength": 105 } }, "variations": [ { "game_id": 4718, "suffix": "0" }, { "tradable": false, "game_id": 4886, "suffix": "1" }, { "tradable": false, "game_id": 4887, "suffix": "2" }, { "tradable": false, "game_id": 4888, "suffix": "3" }, { "tradable": false, "game_id": 4889, "suffix": "4" }, { "game_id": 4890, "suffix": "5" } ] } } ================================================ FILE: data/config/items/bolts.json ================================================ { "rs:bronze_bolt": { "game_id": 877 }, "rs:opal_bolt": { "game_id": 879 }, "rs:blurite_bolt": { "game_id": 9139 }, "rs:jade_bolt": { "game_id": 9237 }, "rs:iron_bolt": { "game_id": 9140 }, "rs:silver_bolt": { "game_id": 9145 }, "rs:steel_bolt": { "game_id": 9141 }, "rs:mithril_bolt": { "game_id": 9142 }, "rs:sapphire_bolt": { "game_id": 9337 }, "rs:emerald_bolt": { "game_id": 9338 }, "rs:adamant_bolt": { "game_id": 9143 }, "rs:runite_bolt": { "game_id": 9144 }, "rs:onyx_bolt": { "game_id": 9342 } } ================================================ FILE: data/config/items/bones.json ================================================ { "rs:bones": { "game_id": 526, "tradable": true, "weight": 0.5, "metadata": { "prayerBuryXp": 4.5, "wikiId": "Bones" } }, "rs:bones_burnt": { "game_id": 528, "tradable": true, "weight": 0.5, "metadata": { "prayerBuryXp": 4.5, "wikiId": "Burnt_bones" } }, "rs:bones_wolf": { "game_id": 2859, "tradable": true, "weight": 0.5, "metadata": { "prayerBuryXp": 4.5, "wikiId": "Wolf_bones" } }, "rs:bones_bat": { "game_id": 530, "tradable": true, "weight": 0.5, "metadata": { "prayerBuryXp": 5.3, "wikiId": "Bat_bones" } }, "rs:bones_big": { "game_id": 532, "tradable": true, "weight": 0.8, "metadata": { "prayerBuryXp": 15, "wikiId": "Big_bones" } }, "rs:bones_dagannoth": { "game_id": 6729, "tradable": true, "weight": 1.5, "metadata": { "prayerBuryXp": 125, "wikiId": "Dagannoth_bones" } }, "rs:bones_babydragon": { "game_id": 534, "tradable": true, "weight": 0.8, "metadata": { "prayerBuryXp": 30, "wikiId": "Babydragon_bones" } }, "rs:bones_dragon": { "game_id": 536, "tradable": true, "weight": 1.5, "metadata": { "prayerBuryXp": 72, "wikiId": "Dragon_bones" } }, "rs:bones_wyvern": { "game_id": 6812, "tradable": true, "weight": 0.5, "metadata": { "prayerBuryXp": 72, "wikiId": "Wyvern_bones" } }, "rs:bones_monkey_normal": { "game_id": 3183, "tradable": true, "weight": 0.5, "metadata": { "prayerBuryXp": 5, "wikiId": "Monkey_bones" } }, "rs:bones_monkey_small_zombie": { "game_id": 3185, "tradable": false, "weight": 0.5, "metadata": { "prayerBuryXp": 5, "wikiId": "Small_zombie_monkey_bones" } }, "rs:bones_monkey_large_zombie": { "game_id": 3186, "tradable": false, "weight": 0.5, "metadata": { "prayerBuryXp": 5, "wikiId": "Large_zombie_monkey_bones" } }, "rs:bones_monkey_gorilla": { "game_id": 3181, "tradable": false, "weight": 0.5, "metadata": { "prayerBuryXp": 18, "wikiId": "Gorilla_bones" } }, "rs:bones_monkey_bearded_gorilla": { "game_id": 3182, "tradable": false, "weight": 0.8, "metadata": { "prayerBuryXp": 18, "wikiId": "Bearded_gorilla_bones" } }, "rs:bones_monkey_small_ninja": { "game_id": 3179, "tradable": false, "weight": 0.5, "metadata": { "prayerBuryXp": 16, "wikiId": "Small_ninja_monkey_bones" } }, "rs:bones_monkey_medium_ninja": { "game_id": 3180, "tradable": false, "weight": 0.5, "metadata": { "prayerBuryXp": 18, "wikiId": "Medium_ninja_monkey_bones" } }, "rs:bones_monkey_skeleton_gorilla": { "game_id": 3187, "tradable": false, "weight": 0.5, "metadata": { "prayerBuryXp": 3, "wikiId": "Bones_(Ape_Atoll)" } }, "rs:bones_jogre": { "game_id": 3152, "tradable": true, "weight": 0.8, "metadata": { "prayerBuryXp": 15, "wikiId": "Jogre_bones" } }, "rs:bones_zogre": { "game_id": 4812, "tradable": true, "weight": 0.8, "metadata": { "prayerBuryXp": 22.5, "wikiId": "Zogre_bones" } }, "rs:bones_fayrg": { "game_id": 4830, "tradable": true, "weight": 0.8, "metadata": { "prayerBuryXp": 84, "wikiId": "Fayrg_bones" } }, "rs:bones_raurg": { "game_id": 4832, "tradable": true, "weight": 0.8, "metadata": { "prayerBuryXp": 96, "wikiId": "Raurg_bones" } }, "rs:bones_ourg": { "game_id": 4834, "tradable": true, "weight": 0.8, "metadata": { "prayerBuryXp": 140, "wikiId": "Ourg_bones" } } } ================================================ FILE: data/config/items/containers.json ================================================ { "rs:pot": { "game_id": 1931, "examine": "This pot is empty.", "tradable": true, "weight": 0.453 }, "rs:vial": { "game_id": 229, "examine": "An empty glass vial.", "tradable": true, "weight": 0.015, "variations": [ { "game_id": 227, "suffix": "water", "examine": "A glass vial containing water." } ] }, "rs:jug": { "game_id": 1935, "examine": "This jug is empty.", "tradable": true, "weight": 0.453 }, "rs:bucket": { "game_id": 1925, "examine": "It's a wooden bucket.", "tradable": true, "weight": 1 }, "rs:bowl": { "game_id": 1923, "examine": "Useful for mixing things.", "tradable": true }, "rs:pie_dish": { "game_id": 2313, "examine": "Deceptively pie shaped.", "tradable": true } } ================================================ FILE: data/config/items/currency.json ================================================ { "rs:coins": { "game_id": 995, "tradable": true } } ================================================ FILE: data/config/items/equipment/amulets.json ================================================ { "rs:amulet_of_glory": { "game_id": 1704, "examine": "A very powerful dragonstone amulet.", "tradable": true, "weight": 0.01, "equippable": true, "equipment_data": { "equipment_slot": "neck", "offensive_bonuses": { "stab": 10, "slash": 10, "crush": 10, "magic": 10, "ranged": 10 }, "defensive_bonuses": { "stab": 3, "slash": 3, "crush": 3, "magic": 3, "ranged": 3 }, "skill_bonuses": { "prayer": 3, "strength": 6, "ranged": 0, "magic": 0 } }, "variations": [ { "game_id": 1706, "examine": "A dragonstone amulet with 1 magic charge.", "suffix": "charged_1" }, { "game_id": 1708, "examine": "A dragonstone amulet with 2 magic charges.", "suffix": "charged_2" }, { "game_id": 1710, "examine": "A dragonstone amulet with 3 magic charges.", "suffix": "charged_3" }, { "game_id": 1712, "examine": "A dragonstone amulet with 4 magic charges.", "suffix": "charged_4" } ] } } ================================================ FILE: data/config/items/equipment/axes.json ================================================ { "rs:dharoks_greataxe": { "game_id": 4718, "examine": "Dharok the Wretched's greataxe.", "tradable": true, "weight": 3.175, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "one_handed", "offensive_bonuses": { "speed": 7, "stab": -4, "slash": 103, "crush": 95, "magic": -4 }, "defensive_bonuses": { "ranged": -1 }, "skill_bonuses": { "strength": 105 }, "weapon_info": { "style": "axe" } } } } ================================================ FILE: data/config/items/equipment/bows.json ================================================ { "rs:shortbow": { "game_id": 50 }, "rs:longbow": { "game_id": 48 }, "rs:oak_shortbow": { "game_id": 54 }, "rs:oak_longbow": { "game_id": 56 }, "rs:willow_shortbow": { "game_id": 60 }, "rs:willow_longbow": { "game_id": 58 }, "rs:maple_shortbow": { "game_id": 64 }, "rs:maple_longbow": { "game_id": 62 }, "rs:yew_shortbow": { "game_id": 68 }, "rs:yew_longbow": { "game_id": 66 }, "rs:magic_shortbow": { "game_id": 72 }, "rs:magic_longbow": { "game_id": 70 } } ================================================ FILE: data/config/items/equipment/capes.json ================================================ { "rs:cape_of_legends": { "game_id": 1052, "examine": "The cape worn by members of the Legends Guild.", "tradable": false, "weight": 1.814, "equippable": true, "equipment_data": { "equipment_slot": "back", "defensive_bonuses": { "stab": 7, "slash": 7, "crush": 7, "magic": 7, "ranged": 7 } } } } ================================================ FILE: data/config/items/equipment/daggers.json ================================================ { "rs:iron_dagger": { "game_id": 1203, "examine": "Short but pointy.", "tradable": true, "weight": 0.453, "equippable": true }, "rs:steel_dagger": { "game_id": 1207, "examine": "Short but pointy.", "tradable": true, "weight": 0.453, "equippable": true }, "rs:black_dagger": { "game_id": 1217, "examine": "Short but pointy.", "tradable": true, "weight": 0.453, "equippable": true }, "rs:white_dagger": { "game_id": 6591, "examine": "A vicious white dagger.", "tradable": true, "weight": 0.453, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "one_handed", "offensive_bonuses": { "speed": 4, "stab": 10, "slash": 5, "crush": -4, "magic": 1 }, "defensive_bonuses": { "magic": 1 }, "skill_bonuses": { "strength": 3 }, "weapon_info": { "style": "dagger" } } }, "rs:mithril_dagger": { "game_id": 1209, "examine": "A dangerous dagger.", "tradable": true, "weight": 0.396, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "one_handed", "offensive_bonuses": { "speed": 4, "stab": 11, "slash": 5, "crush": -4, "magic": 1 }, "defensive_bonuses": { "magic": 1 }, "skill_bonuses": { "strength": 10 }, "weapon_info": { "style": "dagger" } } }, "rs:adamant_dagger": { "game_id": 1211, "examine": "Short and deadly.", "tradable": true, "weight": 0.51, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "one_handed", "offensive_bonuses": { "speed": 4, "stab": 15, "slash": 8, "crush": -4, "magic": 1 }, "defensive_bonuses": { "magic": 1 }, "skill_bonuses": { "strength": 14 }, "weapon_info": { "style": "dagger" } } }, "rs:rune_dagger": { "game_id": 1213, "examine": "Short and deadly.", "tradable": true, "weight": 0.51, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "one_handed", "offensive_bonuses": { "speed": 4, "stab": 25, "slash": 12, "crush": -4, "magic": 1 }, "defensive_bonuses": { "magic": 1 }, "skill_bonuses": { "strength": 24 }, "weapon_info": { "style": "dagger" } } }, "rs:dragon_dagger": { "game_id": 1215, "examine": "A powerful dagger.", "tradable": true, "weight": 0.453, "equippable": true } } ================================================ FILE: data/config/items/equipment/darts.json ================================================ { "rs:bronze_dart": { "game_id": 806 }, "rs:iron_dart": { "game_id": 807 }, "rs:steel_dart": { "game_id": 808 }, "rs:mithril_dart": { "game_id": 809 }, "rs:adamant_dart": { "game_id": 810 }, "rs:rune_dart": { "game_id": 811 } } ================================================ FILE: data/config/items/equipment/halberd.json ================================================ { "rs:iron_halberd": { "game_id": 3192, "examine": "Short but pointy.", "tradable": true, "weight": 3.175, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "two_handed", "offensive_bonuses": { "speed": 7, "stab": 9, "slash": 12, "crush": 0, "magic": -4, "ranged": 0 }, "defensive_bonuses": { "stab": -1, "slash": 1, "crush": 2, "magic": 0, "ranged": 0 }, "skill_bonuses": { "strength": 12, "prayer": 0 }, "weapon_info": { "style": "halberd" } } }, "rs:steel_halberd": { "game_id": 3194, "examine": "Short but pointy.", "tradable": true, "weight": 3.175, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "two_handed", "offensive_bonuses": { "speed": 7, "stab": 14, "slash": 19, "crush": 0, "magic": -4, "ranged": 0 }, "defensive_bonuses": { "stab": -1, "slash": 1, "crush": 2, "magic": 0, "ranged": 0 }, "skill_bonuses": { "strength": 20, "prayer": 0 }, "weapon_info": { "style": "halberd" } } }, "rs:black_halberd": { "game_id": 3196, "examine": "Short but pointy.", "tradable": true, "weight": 3.175, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "two_handed", "offensive_bonuses": { "speed": 7, "stab": 19, "slash": 25, "crush": 0, "magic": -4, "ranged": 0 }, "defensive_bonuses": { "stab": -1, "slash": 2, "crush": 3, "magic": 0, "ranged": 0 }, "skill_bonuses": { "strength": 20, "prayer": 0 }, "weapon_info": { "style": "halberd" } } }, "rs:white_halberd": { "game_id": 6599, "examine": "Short but pointy.", "tradable": true, "weight": 3.175, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "two_handed", "offensive_bonuses": { "speed": 7, "stab": 19, "slash": 25, "crush": 0, "magic": -4, "ranged": 0 }, "defensive_bonuses": { "stab": -1, "slash": 2, "crush": 3, "magic": 0, "ranged": 0 }, "skill_bonuses": { "strength": 20, "prayer": 0 }, "weapon_info": { "style": "halberd" } } }, "rs:mithril_halberd": { "game_id": 3198, "examine": "Short but pointy.", "tradable": true, "weight": 3.175, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "two_handed", "offensive_bonuses": { "speed": 7, "stab": 22, "slash": 28, "crush": 0, "magic": -4, "ranged": 0 }, "defensive_bonuses": { "stab": -1, "slash": 2, "crush": 4, "magic": 0, "ranged": 0 }, "skill_bonuses": { "strength": 29, "prayer": 0 }, "weapon_info": { "style": "halberd" } } }, "rs:adamant_halberd": { "game_id": 3200, "examine": "Short but pointy.", "tradable": true, "weight": 3.175, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "two_handed", "offensive_bonuses": { "speed": 7, "stab": 28, "slash": 41, "crush": 0, "magic": -4, "ranged": 0 }, "defensive_bonuses": { "stab": -1, "slash": 3, "crush": 4, "magic": 0, "ranged": 0 }, "skill_bonuses": { "strength": 42, "prayer": 0 }, "weapon_info": { "style": "halberd" } } }, "rs:rune_halberd": { "game_id": 3202, "examine": "Short but pointy.", "tradable": true, "weight": 3.175, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "two_handed", "offensive_bonuses": { "speed": 7, "stab": 48, "slash": 67, "crush": 0, "magic": -4, "ranged": 0 }, "defensive_bonuses": { "stab": -1, "slash": 4, "crush": 5, "magic": 0, "ranged": 0 }, "skill_bonuses": { "strength": 68, "prayer": 0 }, "weapon_info": { "style": "halberd" } } }, "rs:dragon_halberd": { "game_id": 3204, "examine": "Short but pointy.", "tradable": true, "weight": 3.175, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "two_handed", "offensive_bonuses": { "speed": 7, "stab": 70, "slash": 95, "crush": 0, "magic": -4, "ranged": 0 }, "defensive_bonuses": { "stab": -1, "slash": 4, "crush": 5, "magic": 0, "ranged": 0 }, "skill_bonuses": { "strength": 89, "prayer": 0 }, "weapon_info": { "style": "halberd" } } } } ================================================ FILE: data/config/items/equipment/hammers.json ================================================ { "rs:torags_hammers": { "game_id": 4747, "examine": "Torag the Corrupted's twin hammers.", "tradable": true, "notable": true, "stackable": false, "equippable": true } } ================================================ FILE: data/config/items/equipment/hatchets.json ================================================ { "presets": { "rs:woodcutting_axe": { "tradable": true, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "one_handed", "offensive_bonuses": { "speed": 5, "stab": -2 }, "defensive_bonuses": { "slash": 1 }, "weapon_info": { "style": "axe" } } } }, "rs:bronze_axe": { "extends": "rs:woodcutting_axe", "game_id": 1351, "weight": 1.35, "equipment_data": { "offensive_bonuses": { "slash": 4, "crush": 2 }, "skill_bonuses": { "strength": 5 } } }, "rs:iron_axe": { "extends": "rs:woodcutting_axe", "game_id": 1349, "weight": 1.36, "equipment_data": { "offensive_bonuses": { "slash": 5, "crush": 3 }, "skill_bonuses": { "strength": 7 } } }, "rs:black_axe": { "extends": "rs:woodcutting_axe", "game_id": 1361, "weight": 1.36, "equipment_data": { "offensive_bonuses": { "slash": 5, "crush": 3 }, "skill_bonuses": { "strength": 7 } } }, "rs:dragon_axe": { "extends": "rs:woodcutting_axe", "game_id": 6739, "weight": 1.36, "equipment_data": { "offensive_bonuses": { "slash": 5, "crush": 3 }, "skill_bonuses": { "strength": 7 } } }, "rs:steel_axe": { "extends": "rs:woodcutting_axe", "game_id": 1353, "weight": 1.36, "equipment_data": { "requirements": { "skills": { "attack": 5 } }, "offensive_bonuses": { "slash": 8, "crush": 6 }, "skill_bonuses": { "strength": 9 } } }, "rs:mithril_axe": { "extends": "rs:woodcutting_axe", "game_id": 1355, "weight": 1.133, "equipment_data": { "requirements": { "skills": { "attack": 20 } }, "offensive_bonuses": { "slash": 12, "crush": 10 }, "skill_bonuses": { "strength": 13 } } }, "rs:adamant_axe": { "extends": "rs:woodcutting_axe", "game_id": 1357, "weight": 1.587, "equipment_data": { "requirements": { "skills": { "attack": 30 } }, "offensive_bonuses": { "slash": 17, "crush": 15 }, "skill_bonuses": { "strength": 19 } } }, "rs:rune_axe": { "extends": "rs:woodcutting_axe", "game_id": 1359, "weight": 1, "equipment_data": { "requirements": { "skills": { "attack": 40 } }, "offensive_bonuses": { "slash": 26, "crush": 24 }, "skill_bonuses": { "strength": 29 } } } } ================================================ FILE: data/config/items/equipment/hats.json ================================================ { "rs:wizard_hat_(blue)": { "game_id": 579, "weight": 0.453, "equipment_data": { "offensive_bonuses": { "magic": 2 }, "skill_bonuses": { "magic": 2 } } }, "rs:wizard_hat_(black)": { "game_id": 1017, "weight": 0.453, "equipment_data": { "offensive_bonuses": { "magic": 2 }, "skill_bonuses": { "magic": 2 } } } } ================================================ FILE: data/config/items/equipment/javelins.json ================================================ { "rs:bronze_javelin": { "game_id": 52 }, "rs:iron_javelin": { "game_id": 52 }, "rs:steel_javelin": { "game_id": 52 }, "rs:mithril_javelin": { "game_id": 52 }, "rs:adamant_javelin": { "game_id": 52 }, "rs:rune_javelin": { "game_id": 52 } } ================================================ FILE: data/config/items/equipment/leather-armour.json ================================================ { "rs:leather_cowl": { "game_id": 1167, "examine": "Better than no armour!", "tradable": true, "weight": 0.907, "equippable": true, "equipment_data": { "equipment_slot": "head", "requirements": { "skills": { "ranged": 1, "defence": 1 } }, "offensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 1 }, "defensive_bonuses": { "stab": 2, "slash": 3, "crush": 4, "magic": 2, "ranged": 3 }, "skill_bonuses": { "prayer": 0, "strength": 0, "ranged": 0, "magic": 0 } } }, "rs:leather_body": { "game_id": 1129, "examine": "Better than no armour!", "tradable": true, "weight": 2.721, "equippable": true, "equipment_data": { "equipment_slot": "torso", "requirements": { "skills": { "ranged": 1, "defence": 1 } }, "offensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": -2, "ranged": 2 }, "defensive_bonuses": { "stab": 8, "slash": 9, "crush": 10, "magic": 4, "ranged": 9 }, "skill_bonuses": { "prayer": 0, "strength": 0, "ranged": 0, "magic": 0 } } }, "rs:leather_chaps": { "game_id": 1095, "examine": "Better than no armour!", "tradable": true, "weight": 2.721, "equippable": true, "equipment_data": { "equipment_slot": "legs", "requirements": { "skills": { "ranged": 1, "defence": 1 } }, "offensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 4 }, "defensive_bonuses": { "stab": 2, "slash": 2, "crush": 1, "magic": 0, "ranged": 0 }, "skill_bonuses": { "prayer": 0, "strength": 0, "ranged": 0, "magic": 0 } } }, "rs:leather_vambraces": { "game_id": 1063, "examine": "Better than no armour!", "tradable": true, "weight": 0.226, "equippable": true, "equipment_data": { "equipment_slot": "hands", "requirements": { "skills": { "defence": 1 } }, "offensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 4 }, "defensive_bonuses": { "stab": 2, "slash": 2, "crush": 1, "magic": 0, "ranged": 0 }, "skill_bonuses": { "prayer": 0, "strength": 0, "ranged": 0, "magic": 0 } } }, "rs:leather_boots": { "game_id": 1061, "examine": "Comfortable leather boots.", "tradable": true, "weight": 0.34, "equippable": true, "equipment_data": { "equipment_slot": "feet", "requirements": { "skills": { "defence": 1 } }, "offensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "defensive_bonuses": { "stab": 0, "slash": 1, "crush": 1, "magic": 0, "ranged": 0 }, "skill_bonuses": { "prayer": 0, "strength": 0, "ranged": 0, "magic": 0 } } } } ================================================ FILE: data/config/items/equipment/longswords.json ================================================ { "rs:dragon_longsword": { "game_id": 1305, "examine": "A very powerful sword.", "tradable": true, "weight": 1.814, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "requirements": { "skills": { "attack": 60 } }, "offensive_bonuses": { "speed": 5, "stab": 58, "slash": 69, "crush": -2, "magic": 0, "ranged": 0 }, "defensive_bonuses": { "stab": 0, "slash": 3, "crush": 2, "magic": 0, "ranged": 0 }, "skill_bonuses": { "prayer": 0, "strength": 71, "ranged": 0, "magic": 0 } } } } ================================================ FILE: data/config/items/equipment/mauls.json ================================================ { "rs:granite_maul": { "game_id": 4153, "examine": "Simplicity is the best weapon.", "tradable": true, "weight": 4.535, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "two_handed", "offensive_bonuses": { "speed": 7, "stab": 0, "slash": 0, "crush": 81, "magic": 1 }, "defensive_bonuses": { "magic": 1 }, "skill_bonuses": { "strength": 79 }, "weapon_info": { "style": "mace" } } } } ================================================ FILE: data/config/items/equipment/pickaxes.json ================================================ { "presets": { "rs:pickaxe_base": { "examine": "Used for mining.", "tradable": true, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "one_handed", "offensive_bonuses": { "speed": 5, "slash": -2 }, "defensive_bonuses": { "slash": 1 }, "weapon_info": { "style": "pickaxe" } } } }, "rs:bronze_pickaxe": { "extends": "rs:pickaxe_base", "game_id": 1265, "weight": 2.267, "equipment_data": { "offensive_bonuses": { "stab": 4, "crush": 2 }, "skill_bonuses": { "strength": 5 } } }, "rs:iron_pickaxe": { "extends": "rs:pickaxe_base", "game_id": 1267, "weight": 2.267, "equipment_data": { "offensive_bonuses": { "stab": 5, "crush": 3 }, "skill_bonuses": { "strength": 7 } } }, "rs:steel_pickaxe": { "extends": "rs:pickaxe_base", "game_id": 1269, "weight": 2.267, "equipment_data": { "requirements": { "skills": { "attack": 5 } }, "offensive_bonuses": { "stab": 8, "crush": 6 }, "skill_bonuses": { "strength": 9 } } }, "rs:mithril_pickaxe": { "extends": "rs:pickaxe_base", "game_id": 1273, "weight": 1.814, "equipment_data": { "requirements": { "skills": { "attack": 20 } }, "offensive_bonuses": { "stab": 12, "crush": 10 }, "skill_bonuses": { "strength": 13 } } }, "rs:adamant_pickaxe": { "extends": "rs:pickaxe_base", "game_id": 1271, "weight": 2.721, "equipment_data": { "requirements": { "skills": { "attack": 30 } }, "offensive_bonuses": { "stab": 17, "crush": 15 }, "skill_bonuses": { "strength": 19 } } }, "rs:rune_pickaxe": { "extends": "rs:pickaxe_base", "game_id": 1275, "weight": 2.267, "equipment_data": { "requirements": { "skills": { "attack": 40 } }, "offensive_bonuses": { "stab": 26, "crush": 24 }, "skill_bonuses": { "strength": 29 } } } } ================================================ FILE: data/config/items/equipment/shortswords.json ================================================ { "rs:toktz_xil_ak": { "game_id": 6523, "examine": "A razor sharp sword of obsidian.", "tradable": true, "weight": 0.453, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "one_handed", "offensive_bonuses": { "speed": 4, "stab": 10, "slash": 5, "crush": -4, "magic": 1 }, "defensive_bonuses": { "magic": 1 }, "skill_bonuses": { "strength": 3 }, "weapon_info": { "style": "longsword" } } } } ================================================ FILE: data/config/items/equipment/skillscapes.json ================================================ { "presets": { "rs:skillcape": { "tradable": false, "weight": 0.453, "equippable": true, "equipment_data": { "equipment_slot": "back", "defensive_bonuses": { "stab": 9, "slash": 9, "crush": 9, "magic": 9, "ranged": 9 } } }, "rs:skillcape_hood": { "tradable": false, "weight": 0.453, "equippable": true, "equipment_data": { "equipment_slot": "head" } } }, "rs:attack_cape": { "extends": "rs:skillcape", "game_id": 9747, "equipment_data": { "requirements": { "skills": { "attack": 99 } } } }, "rs:attack_cape_t": { "extends": "rs:skillcape", "game_id": 9748, "equipment_data": { "requirements": { "skills": { "attack": 99 } } } }, "rs:attack_hood": { "extends": "rs:skillcape_hood", "game_id": 9749, "equipment_data": { "requirements": { "skills": { "attack": 99 } } } }, "rs:strength_cape": { "extends": "rs:skillcape", "game_id": 9750, "equipment_data": { "requirements": { "skills": { "strength": 99 } } } }, "rs:strength_cape_t": { "extends": "rs:skillcape", "game_id": 9751, "equipment_data": { "requirements": { "skills": { "strength": 99 } } } }, "rs:strength_hood": { "extends": "rs:skillcape_hood", "game_id": 9752, "equipment_data": { "requirements": { "skills": { "strength": 99 } } } }, "rs:defence_cape": { "extends": "rs:skillcape", "game_id": 9753, "equipment_data": { "requirements": { "skills": { "defence": 99 } } } }, "rs:defence_cape_t": { "extends": "rs:skillcape", "game_id": 9754, "equipment_data": { "requirements": { "skills": { "defence": 99 } } } }, "rs:defence_hood": { "extends": "rs:skillcape_hood", "game_id": 9755, "equipment_data": { "requirements": { "skills": { "defence": 99 } } } }, "rs:ranged_cape": { "extends": "rs:skillcape", "game_id": 9756, "equipment_data": { "requirements": { "skills": { "ranged": 99 } } } }, "rs:ranged_cape_t": { "extends": "rs:skillcape", "game_id": 9757, "equipment_data": { "requirements": { "skills": { "ranged": 99 } } } }, "rs:ranged_hood": { "extends": "rs:skillcape_hood", "game_id": 9758, "equipment_data": { "requirements": { "skills": { "ranged": 99 } } } }, "rs:prayer_cape": { "extends": "rs:skillcape", "game_id": 9759, "equipment_data": { "requirements": { "skills": { "prayer": 99 } } } }, "rs:prayer_cape_t": { "extends": "rs:skillcape", "game_id": 9760, "equipment_data": { "requirements": { "skills": { "prayer": 99 } } } }, "rs:prayer_hood": { "extends": "rs:skillcape_hood", "game_id": 9761, "equipment_data": { "requirements": { "skills": { "prayer": 99 } } } }, "rs:magic_cape": { "extends": "rs:skillcape", "game_id": 9762, "equipment_data": { "requirements": { "skills": { "magic": 99 } } } }, "rs:magic_cape_t": { "extends": "rs:skillcape", "game_id": 9763, "equipment_data": { "requirements": { "skills": { "magic": 99 } } } }, "rs:magic_hood": { "extends": "rs:skillcape_hood", "game_id": 9764, "equipment_data": { "requirements": { "skills": { "magic": 99 } } } }, "rs:runecrafting_cape": { "extends": "rs:skillcape", "game_id": 9765, "equipment_data": { "requirements": { "skills": { "runecrafting": 99 } } } }, "rs:runecrafting_cape_t": { "extends": "rs:skillcape", "game_id": 9766, "equipment_data": { "requirements": { "skills": { "runecrafting": 99 } } } }, "rs:runecrafting_hood": { "extends": "rs:skillcape_hood", "game_id": 9767, "equipment_data": { "requirements": { "skills": { "runecrafting": 99 } } } }, "rs:hitpoints_cape": { "extends": "rs:skillcape", "game_id": 9768, "equipment_data": { "requirements": { "skills": { "hitpoints": 99 } } } }, "rs:hitpoints_cape_t": { "extends": "rs:skillcape", "game_id": 9769, "equipment_data": { "requirements": { "skills": { "hitpoints": 99 } } } }, "rs:hitpoints_hood": { "extends": "rs:skillcape_hood", "game_id": 9770, "equipment_data": { "requirements": { "skills": { "hitpoints": 99 } } } }, "rs:agility_cape": { "extends": "rs:skillcape", "game_id": 9771, "equipment_data": { "requirements": { "skills": { "agility": 99 } } } }, "rs:agility_cape_t": { "extends": "rs:skillcape", "game_id": 9772, "equipment_data": { "requirements": { "skills": { "agility": 99 } } } }, "rs:agility_hood": { "extends": "rs:skillcape_hood", "game_id": 9773, "equipment_data": { "requirements": { "skills": { "agility": 99 } } } }, "rs:herblore_cape": { "extends": "rs:skillcape", "game_id": 9774, "equipment_data": { "requirements": { "skills": { "herblore": 99 } } } }, "rs:herblore_cape_t": { "extends": "rs:skillcape", "game_id": 9775, "equipment_data": { "requirements": { "skills": { "herblore": 99 } } } }, "rs:herblore_hood": { "extends": "rs:skillcape_hood", "game_id": 9776, "equipment_data": { "requirements": { "skills": { "herblore": 99 } } } }, "rs:thieving_cape": { "extends": "rs:skillcape", "game_id": 9777, "equipment_data": { "requirements": { "skills": { "thieving": 99 } } } }, "rs:thieving_cape_t": { "extends": "rs:skillcape", "game_id": 9778, "equipment_data": { "requirements": { "skills": { "thieving": 99 } } } }, "rs:thieving_hood": { "extends": "rs:skillcape_hood", "game_id": 9779, "equipment_data": { "requirements": { "skills": { "thieving": 99 } } } }, "rs:crafting_cape": { "extends": "rs:skillcape", "game_id": 9780, "equipment_data": { "requirements": { "skills": { "crafting": 99 } } } }, "rs:crafting_cape_t": { "extends": "rs:skillcape", "game_id": 9781, "equipment_data": { "requirements": { "skills": { "crafting": 99 } } } }, "rs:crafting_hood": { "extends": "rs:skillcape_hood", "game_id": 9782, "equipment_data": { "requirements": { "skills": { "crafting": 99 } } } }, "rs:fletching_cape": { "extends": "rs:skillcape", "game_id": 9783, "equipment_data": { "requirements": { "skills": { "fletching": 99 } } } }, "rs:fletching_cape_t": { "extends": "rs:skillcape", "game_id": 9784, "equipment_data": { "requirements": { "skills": { "fletching": 99 } } } }, "rs:fletching_hood": { "extends": "rs:skillcape_hood", "game_id": 9785, "equipment_data": { "requirements": { "skills": { "fletching": 99 } } } }, "rs:slayer_cape": { "extends": "rs:skillcape", "game_id": 9786, "equipment_data": { "requirements": { "skills": { "slayer": 99 } } } }, "rs:slayer_cape_t": { "extends": "rs:skillcape", "game_id": 9787, "equipment_data": { "requirements": { "skills": { "slayer": 99 } } } }, "rs:slayer_hood": { "extends": "rs:skillcape_hood", "game_id": 9788, "equipment_data": { "requirements": { "skills": { "slayer": 99 } } } }, "rs:construction_cape": { "extends": "rs:skillcape", "game_id": 9789, "equipment_data": { "requirements": { "skills": { "construction": 99 } } } }, "rs:construction_cape_t": { "extends": "rs:skillcape", "game_id": 9790, "equipment_data": { "requirements": { "skills": { "construction": 99 } } } }, "rs:construction_hood": { "extends": "rs:skillcape_hood", "game_id": 9791, "equipment_data": { "requirements": { "skills": { "construction": 99 } } } }, "rs:mining_cape": { "extends": "rs:skillcape", "game_id": 9792, "equipment_data": { "requirements": { "skills": { "mining": 99 } } } }, "rs:mining_cape_t": { "extends": "rs:skillcape", "game_id": 9793, "equipment_data": { "requirements": { "skills": { "mining": 99 } } } }, "rs:mining_hood": { "extends": "rs:skillcape_hood", "game_id": 9794, "equipment_data": { "requirements": { "skills": { "mining": 99 } } } }, "rs:smithing_cape": { "extends": "rs:skillcape", "game_id": 9795, "equipment_data": { "requirements": { "skills": { "smithing": 99 } } } }, "rs:smithing_cape_t": { "extends": "rs:skillcape", "game_id": 9796, "equipment_data": { "requirements": { "skills": { "smithing": 99 } } } }, "rs:smithing_hood": { "extends": "rs:skillcape_hood", "game_id": 9797, "equipment_data": { "requirements": { "skills": { "smithing": 99 } } } }, "rs:fishing_cape": { "extends": "rs:skillcape", "game_id": 9798, "equipment_data": { "requirements": { "skills": { "fishing": 99 } } } }, "rs:fishing_cape_t": { "extends": "rs:skillcape", "game_id": 9799, "equipment_data": { "requirements": { "skills": { "fishing": 99 } } } }, "rs:fishing_hood": { "extends": "rs:skillcape_hood", "game_id": 9800, "equipment_data": { "requirements": { "skills": { "fishing": 99 } } } }, "rs:cooking_cape": { "extends": "rs:skillcape", "game_id": 9801, "equipment_data": { "requirements": { "skills": { "cooking": 99 } } } }, "rs:cooking_cape_t": { "extends": "rs:skillcape", "game_id": 9802, "equipment_data": { "requirements": { "skills": { "cooking": 99 } } } }, "rs:cooking_hood": { "extends": "rs:skillcape_hood", "game_id": 9803, "equipment_data": { "requirements": { "skills": { "cooking": 99 } } } }, "rs:firemaking_cape": { "extends": "rs:skillcape", "game_id": 9804, "equipment_data": { "requirements": { "skills": { "firemaking": 99 } } } }, "rs:firemaking_cape_t": { "extends": "rs:skillcape", "game_id": 9805, "equipment_data": { "requirements": { "skills": { "firemaking": 99 } } } }, "rs:firemaking_hood": { "extends": "rs:skillcape_hood", "game_id": 9806, "equipment_data": { "requirements": { "skills": { "firemaking": 99 } } } }, "rs:woodcutting_cape": { "extends": "rs:skillcape", "game_id": 9807, "equipment_data": { "requirements": { "skills": { "woodcutting": 99 } } } }, "rs:woodcutting_cape_t": { "extends": "rs:skillcape", "game_id": 9808, "equipment_data": { "requirements": { "skills": { "woodcutting": 99 } } } }, "rs:woodcutting_hood": { "extends": "rs:skillcape_hood", "game_id": 9809, "equipment_data": { "requirements": { "skills": { "woodcutting": 99 } } } }, "rs:farming_cape": { "extends": "rs:skillcape", "game_id": 9810, "equipment_data": { "requirements": { "skills": { "farming": 99 } } } }, "rs:farming_cape_t": { "extends": "rs:skillcape", "game_id": 9811, "equipment_data": { "requirements": { "skills": { "farming": 99 } } } }, "rs:farming_hood": { "extends": "rs:skillcape_hood", "game_id": 9812, "equipment_data": { "requirements": { "skills": { "farming": 99 } } } }, "rs:quest_cape": { "extends": "rs:skillcape", "game_id": 9813 }, "rs:quest_hood": { "extends": "rs:skillcape_hood", "game_id": 9814 } } ================================================ FILE: data/config/items/equipment/slayer.json ================================================ { "rs:black_mask": { "game_id": 8921, "examine": "An inert-seeming cave horror mask.", "tradable": true, "weight": 10, "equippable": true, "equipment_data": { "equipment_slot": "head", "requirements": { "skills": { "defence": 10, "strength": 20, "combat": 40 } }, "offensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": -3, "ranged": -1 }, "defensive_bonuses": { "stab": 9, "slash": 10, "crush": 8, "magic": -1, "ranged": 9 }, "skill_bonuses": { "prayer": 0, "strength": 0, "ranged": 0, "magic": 0 } }, "variations": [ { "game_id": 8919, "suffix": "1" }, { "game_id": 8917, "suffix": "2" }, { "game_id": 8915, "suffix": "3" }, { "game_id": 8913, "suffix": "4" }, { "game_id": 8911, "suffix": "5" }, { "game_id": 8909, "suffix": "6" }, { "game_id": 8907, "suffix": "7" }, { "game_id": 8905, "suffix": "8" }, { "game_id": 8903, "suffix": "9" }, { "game_id": 8901, "suffix": "10" } ] }, "rs:witchwood_icon": { "game_id": 8923, "examine": "A stick on a string... pure style.", "tradable": false, "weight": 0.007, "equippable": true, "equipment_data": { "equipment_slot": "neck", "requirements": { "skills": { "slayer": 35 } }, "offensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 1, "ranged": 0 }, "defensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "skill_bonuses": { "prayer": 1, "strength": 0, "ranged": 0, "magic": 0 } } }, "rs:slayer_staff": { "game_id": 4170, "examine": "An old and magical staff.", "tradable": true, "weight": 1.814, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "requirements": { "skills": { "slayer": 55, "magic": 50 } }, "offensive_bonuses": { "speed": 4, "stab": 7, "slash": -1, "crush": 25, "magic": 12, "ranged": 0 }, "defensive_bonuses": { "stab": 2, "slash": 3, "crush": 1, "magic": 10, "ranged": 0 }, "skill_bonuses": { "prayer": 0, "strength": 35, "ranged": 0, "magic": 0 }, "weapon_info": { "style": "magical_staff" } } }, "rs:nose_peg": { "game_id": 4168, "examine": "Protects me from any bad smells.", "tradable": true, "weight": 0.001, "equippable": true, "equipment_data": { "equipment_slot": "head", "requirements": { "skills": { "slayer": 60 } }, "offensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "defensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "skill_bonuses": { "prayer": 0, "strength": 0, "ranged": 0, "magic": 0 } } }, "rs:ear_muffs": { "game_id": 4166, "examine": "These will protect my ears from loud noise.", "tradable": true, "weight": 0.113, "equippable": true, "equipment_data": { "equipment_slot": "head", "requirements": { "skills": { "slayer": 15 } }, "offensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "defensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "skill_bonuses": { "prayer": 0, "strength": 0, "ranged": 0, "magic": 0 } } }, "rs:face_mask": { "game_id": 4164, "examine": "Stops me breathing in too much dust.", "tradable": true, "weight": 0.113, "equippable": true, "equipment_data": { "equipment_slot": "head", "requirements": { "skills": { "slayer": 10 } }, "offensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "defensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "skill_bonuses": { "prayer": 0, "strength": 0, "ranged": 0, "magic": 0 } } }, "rs:rock_hammer": { "game_id": 4162, "examine": "I can even smash stone with this.", "tradable": true, "weight": 2.267, "equippable": false }, "rs:bag_of_salt": { "game_id": 4161, "examine": "A bag of salt.", "tradable": true, "weight": 0, "equippable": false }, "rs:broad_arrows": { "game_id": 4160, "examine": "Arrows with a wider than normal tip.", "tradable": false, "weight": 0, "equippable": true, "equipment_data": { "equipment_slot": "quiver", "requirements": { "skills": { "slayer": 55, "ranged": 50 } }, "offensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "defensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "skill_bonuses": { "prayer": 0, "strength": 0, "ranged": 28, "magic": 0 } } }, "rs:leaf_bladed_spear": { "game_id": 4158, "examine": "A spear with a leaf-shaped point.", "tradable": false, "weight": 2.267, "equippable": true, "equipment_data": { "equipment_slot": "2h", "requirements": { "skills": { "slayer": 55, "attack": 50 } }, "offensive_bonuses": { "speed": 5, "stab": 47, "slash": 42, "crush": 36, "magic": 0, "ranged": 0 }, "defensive_bonuses": { "stab": 1, "slash": 1, "crush": 0, "magic": 0, "ranged": 0 }, "skill_bonuses": { "prayer": 0, "strength": 50, "ranged": 0, "magic": 0 }, "weapon_info": { "style": "spear" } } }, "rs:mirror_shield": { "game_id": 4156, "examine": "I can just about see things in this shield's reflection.", "tradable": true, "weight": 2.267, "equippable": true, "equipment_data": { "equipment_slot": "off_hand", "requirements": { "skills": { "slayer": 25, "defence": 20 } }, "offensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "defensive_bonuses": { "stab": 10, "slash": 15, "crush": 5, "magic": 5, "ranged": 10 }, "skill_bonuses": { "prayer": 0, "strength": 0, "ranged": 0, "magic": 0 } } }, "rs:enchanted_gem": { "game_id": 4155, "examine": "I can contact the Slayer Masters with this.", "tradable": false, "weight": 0.002, "equippable": false }, "rs:granite_maul": { "game_id": 4153, "examine": "Simplicity is the best weapon.", "tradable": true, "weight": 4.535, "equippable": true, "equipment_data": { "equipment_slot": "2h", "requirements": { "skills": { "attack": 50 } }, "offensive_bonuses": { "speed": 7, "stab": 0, "slash": 0, "crush": 81, "magic": 0, "ranged": 0 }, "defensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "skill_bonuses": { "prayer": 0, "strength": 79, "ranged": 0, "magic": 0 }, "weapon_info": { "style": "hammer" } } } } ================================================ FILE: data/config/items/equipment/spears.json ================================================ { "rs:guthans_warspear": { "game_id": 4726, "tradable": true, "notable": true, "stackable": false, "equippable": true } } ================================================ FILE: data/config/items/equipment/staffs.json ================================================ { "presets": { "rs:staff_base": { "tradable": true, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "two_handed", "weapon_info": { "style": "magical_staff" } } } }, "rs:battlestaff": { "extends": "rs:staff_base", "game_id": 1391, "examine": "It's a slightly magical stick.", "weight": 2.267, "equipment_data": { "offensive_bonuses": { "speed": 5, "stab": 7, "slash": -1, "crush": 25, "magic": 10 }, "defensive_bonuses": { "stab": 2, "slash": 3, "crush": 1, "magic": 10 }, "skill_bonuses": { "strength": 32 }, "requirements": { "skills": { "attack": 30, "magic": 30 } } } }, "rs:staff": { "extends": "rs:staff_base", "game_id": 1379, "examine": "It's a slightly magical stick.", "weight": 1.814, "equipment_data": { "offensive_bonuses": { "speed": 5, "stab": 0, "slash": -1, "crush": 7, "magic": 4 }, "defensive_bonuses": { "stab": 2, "slash": 3, "crush": 1, "magic": 4 }, "skill_bonuses": { "strength": 3 } } }, "rs:magic_staff": { "extends": "rs:staff_base", "game_id": 1389, "examine": "A Magical staff.", "weight": 2.267, "equipment_data": { "offensive_bonuses": { "speed": 5, "stab": 2, "slash": -1, "crush": 10, "magic": 10 }, "defensive_bonuses": { "stab": 2, "slash": 3, "crush": 1, "magic": 10 }, "skill_bonuses": { "strength": 7 } } }, "rs:staff_of_air": { "extends": "rs:staff_base", "game_id": 1381, "examine": "A Magical staff.", "weight": 2.267, "equipment_data": { "offensive_bonuses": { "speed": 5, "stab": 0, "slash": -1, "crush": 7, "magic": 10 }, "defensive_bonuses": { "stab": 2, "slash": 3, "crush": 1, "magic": 10 }, "skill_bonuses": { "strength": 3 } } }, "rs:staff_of_water": { "extends": "rs:staff_base", "game_id": 1383, "examine": "A Magical staff.", "weight": 2.267, "equipment_data": { "offensive_bonuses": { "speed": 5, "stab": 0, "slash": -1, "crush": 7, "magic": 10 }, "defensive_bonuses": { "stab": 2, "slash": 3, "crush": 1, "magic": 10 }, "skill_bonuses": { "strength": 3 } } }, "rs:staff_of_earth": { "extends": "rs:staff_base", "game_id": 1385, "examine": "A Magical staff.", "weight": 2.267, "equipment_data": { "offensive_bonuses": { "speed": 5, "stab": 1, "slash": -1, "crush": 9, "magic": 10 }, "defensive_bonuses": { "stab": 2, "slash": 3, "crush": 1, "magic": 10 }, "skill_bonuses": { "strength": 5 } } }, "rs:staff_of_fire": { "extends": "rs:staff_base", "game_id": 1387, "examine": "A Magical staff.", "weight": 2.267, "equipment_data": { "offensive_bonuses": { "speed": 5, "stab": 3, "slash": -1, "crush": 9, "magic": 10 }, "defensive_bonuses": { "stab": 2, "slash": 3, "crush": 1, "magic": 10 }, "skill_bonuses": { "strength": 6 } } } } ================================================ FILE: data/config/items/equipment/standard-metals/adamant-armour.json ================================================ { "presets": { "rs:adamant_armour_base": { "tradable": true, "equippable": true, "equipment_data": { "requirements": { "skills": { "defence": 30 } } }, "groups": ["adamant_metal", "equipment"] } }, "rs:adamant_platelegs": { "extends": "rs:adamant_armour_base", "game_id": 1073, "examine": "These look pretty heavy.", "weight": 10.432, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -7 }, "defensive_bonuses": { "stab": 33, "slash": 31, "crush": 29, "magic": -4, "ranged": 31 } }, "groups": ["legs"] }, "rs:adamant_plateskirt": { "extends": "rs:adamant_armour_base", "game_id": 1091, "examine": "Designer leg protection.", "weight": 9.071, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -7 }, "defensive_bonuses": { "stab": 33, "slash": 31, "crush": 29, "magic": -4, "ranged": 31 } }, "groups": ["legs"] } } ================================================ FILE: data/config/items/equipment/standard-metals/black-armour.json ================================================ { "presets": { "rs:black_armour_base": { "tradable": true, "equippable": true, "equipment_data": { "requirements": { "skills": { "defence": 10 } } }, "groups": ["black_metal", "equipment"] } }, "rs:black_platelegs": { "extends": "rs:black_armour_base", "game_id": 1077, "examine": "Big, black and heavy looking.", "weight": 9.071, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -7 }, "defensive_bonuses": { "stab": 21, "slash": 20, "crush": 19, "magic": -4, "ranged": 20 } }, "groups": ["legs"] }, "rs:black_plateskirt": { "extends": "rs:black_armour_base", "game_id": 1089, "examine": "Big, black and heavy looking.", "weight": 8.164, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -7 }, "defensive_bonuses": { "stab": 21, "slash": 20, "crush": 19, "magic": -4, "ranged": 20 } }, "groups": ["legs"] } } ================================================ FILE: data/config/items/equipment/standard-metals/bronze-armour.json ================================================ { "presets": { "rs:bronze_armour_base": { "tradable": true, "equippable": true, "equipment_data": { "requirements": { "skills": { "defence": 1 } } }, "groups": ["bronze_metal", "equipment"] } }, "rs:bronze_platelegs": { "extends": "rs:bronze_armour_base", "game_id": 1075, "examine": "These look pretty heavy.", "weight": 9.071, "tradable": true, "equippable": true, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -7 }, "defensive_bonuses": { "stab": 8, "slash": 7, "crush": 6, "magic": -4, "ranged": 7 } }, "groups": ["legs"] }, "rs:bronze_plateskirt": { "game_id": 1087, "extends": "rs:bronze_armour_base", "examine": "Designer leg protection.", "weight": 8.164, "tradable": true, "equippable": true, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -7 }, "defensive_bonuses": { "stab": 8, "slash": 7, "crush": 6, "magic": -4, "ranged": 7 } }, "groups": ["legs"] } } ================================================ FILE: data/config/items/equipment/standard-metals/bronze-weapons.json ================================================ { "presets": { "rs:bronze_weapon_base": { "tradable": true, "equippable": true, "equipment_data": { "requirements": { "skills": { "defence": 1 } } }, "groups": ["bronze_metal", "equipment", "weapon"] } }, "rs:bronze_dagger": { "game_id": 1205, "extends": "rs:bronze_weapon_base", "tradable": true, "weight": 0.453, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "one_handed", "requirements": { "quests": { "tyn:goblin_diplomacy": 20 } }, "offensive_bonuses": { "speed": 4, "stab": 4, "slash": 2, "crush": -4, "magic": 1 }, "defensive_bonuses": { "magic": 1 }, "skill_bonuses": { "strength": 3 }, "weapon_info": { "style": "dagger" } }, "groups": ["main_hand", "dagger"] } } ================================================ FILE: data/config/items/equipment/standard-metals/iron-armour.json ================================================ { "rs:iron_platelegs": { "game_id": 1067, "examine": "These look pretty heavy.", "weight": 9.071, "tradable": true, "equippable": true, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -7 }, "defensive_bonuses": { "stab": 11, "slash": 10, "crush": 10, "magic": -4, "ranged": 10 } } }, "rs:iron_plateskirt": { "game_id": 1081, "examine": "Designer leg protection.", "weight": 8.164, "tradable": true, "equippable": true, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -7 }, "defensive_bonuses": { "stab": 11, "slash": 10, "crush": 10, "magic": -4, "ranged": 10 } } } } ================================================ FILE: data/config/items/equipment/standard-metals/iron-weapons.json ================================================ { "rs:iron_battleaxe": { "game_id": 1363, "examine": "A vicious looking axe.", "tradable": true, "weight": 2.721, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "one_handed", "offensive_bonuses": { "speed": 6, "stab": -2, "slash": 8, "crush": 5 }, "defensive_bonuses": { "ranged": -1 }, "skill_bonuses": { "strength": 13 }, "weapon_info": { "style": "axe" } } } } ================================================ FILE: data/config/items/equipment/standard-metals/mithril-armour.json ================================================ { "presets": { "rs:mithril_armour_base": { "tradable": true, "equippable": true, "equipment_data": { "requirements": { "skills": { "defence": 20 } } } } }, "rs:mithril_platelegs": { "extends": "rs:mithril_armour_base", "game_id": 1071, "examine": "These look pretty heavy.", "weight": 7.711, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -7 }, "defensive_bonuses": { "stab": 24, "slash": 22, "crush": 20, "magic": -4, "ranged": 22 } } }, "rs:mithril_plateskirt": { "extends": "rs:mithril_armour_base", "game_id": 1085, "examine": "Designer leg protection.", "weight": 7.257, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -7 }, "defensive_bonuses": { "stab": 24, "slash": 22, "crush": 20, "magic": -4, "ranged": 22 } } } } ================================================ FILE: data/config/items/equipment/standard-metals/mithril-weapons.json ================================================ { "rs:mithril_battleaxe": { "game_id": 1369, "examine": "A vicious looking axe.", "tradable": true, "weight": 2.267, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "one_handed", "requirements": { "skills": { "attack": 20 } }, "offensive_bonuses": { "speed": 6, "stab": -2, "slash": 22, "crush": 17 }, "defensive_bonuses": { "ranged": -1 }, "skill_bonuses": { "strength": 29 }, "weapon_info": { "style": "axe" } } } } ================================================ FILE: data/config/items/equipment/standard-metals/rune-armour.json ================================================ { "presets": { "rs:rune_armour_base": { "tradable": true, "equippable": true, "equipment_data": { "requirements": { "skills": { "defence": 40 } } } } }, "rs:rune_gloves": { "game_id": 7460, "examine": "A pair of very nice gloves.", "weight": 0.226, "tradable": false, "equipment_data": { "equipment_slot": "hands", "offensive_bonuses": { "stab": 8, "slash": 8, "crush": 8, "magic": 4, "ranged": 8 }, "defensive_bonuses": { "stab": 8, "slash": 8, "crush": 8, "magic": 4, "ranged": 8 }, "skill_bonuses": { "strength": 8 } } }, "rs:rune_defender": { "extends": "rs:rune_armour_base", "game_id": 8850, "examine": "A defensive weapon.", "weight": 0.453, "equipment_data": { "equipment_slot": "off_hand", "requirements": { "skills": { "attack": 40 } }, "offensive_bonuses": { "stab": 20, "slash": 19, "crush": 18, "magic": -3, "ranged": -2 }, "defensive_bonuses": { "stab": 20, "slash": 19, "crush": 18, "magic": -3, "ranged": -2 }, "skill_bonuses": { "strength": 5 } } }, "rs:rune_boots": { "extends": "rs:rune_armour_base", "game_id": 4131, "examine": "These will protect my feet.", "weight": 1.36, "equipment_data": { "equipment_slot": "feet", "offensive_bonuses": { "magic": -3, "ranged": -1 }, "defensive_bonuses": { "stab": 12, "slash": 13, "crush": 14 }, "skill_bonuses": { "strength": 2 } } }, "rs:rune_med_helm": { "extends": "rs:rune_armour_base", "game_id": 1147, "examine": "A medium sized helmet.", "weight": 1.814, "equipment_data": { "equipment_slot": "head", "offensive_bonuses": { "magic": -3 }, "defensive_bonuses": { "stab": 22, "slash": 23, "crush": 21, "magic": -1, "ranged": 22 } } }, "rs:rune_full_helm": { "extends": "rs:rune_armour_base", "game_id": 1163, "examine": "A full face helmet.", "weight": 2.721, "equipment_data": { "equipment_slot": "head", "offensive_bonuses": { "magic": -6, "ranged": -3 }, "defensive_bonuses": { "stab": 30, "slash": 32, "crush": 27, "magic": -1, "ranged": 30 } } }, "rs:rune_sq_shield": { "extends": "rs:rune_armour_base", "game_id": 1185, "examine": "A medium square shield.", "weight": 3.628, "equipment_data": { "equipment_slot": "off_hand", "offensive_bonuses": { "magic": -6 }, "defensive_bonuses": { "stab": 38, "slash": 40, "crush": 36, "ranged": 38 } } }, "rs:rune_kiteshield": { "extends": "rs:rune_armour_base", "game_id": 1201, "examine": "A large metal shield.", "weight": 5.443, "equipment_data": { "equipment_slot": "off_hand", "offensive_bonuses": { "magic": -8, "ranged": -3 }, "defensive_bonuses": { "stab": 44, "slash": 48, "crush": 46, "magic": -1, "ranged": 46 } } }, "rs:rune_platelegs": { "extends": "rs:rune_armour_base", "game_id": 1079, "examine": "These look pretty heavy.", "weight": 9.071, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -11 }, "defensive_bonuses": { "stab": 51, "slash": 49, "crush": 47, "magic": -4, "ranged": 49 } } }, "rs:rune_plateskirt": { "extends": "rs:rune_armour_base", "game_id": 1093, "examine": "Designer leg protection.", "weight": 8.164, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -11 }, "defensive_bonuses": { "stab": 51, "slash": 49, "crush": 47, "magic": -4, "ranged": 49 } } }, "rs:rune_chainbody": { "extends": "rs:rune_armour_base", "game_id": 1113, "examine": "A series of connected metal rings.", "weight": 6.803, "equipment_data": { "equipment_slot": "torso", "offensive_bonuses": { "magic": -15 }, "defensive_bonuses": { "stab": 63, "slash": 72, "crush": 78, "magic": -3, "ranged": 65 } } }, "rs:rune_platebody": { "extends": "rs:rune_armour_base", "game_id": 1127, "examine": "Provides excellent protection.", "weight": 9.979, "equipment_data": { "equipment_slot": "torso", "offensive_bonuses": { "magic": -30, "ranged": -15 }, "defensive_bonuses": { "stab": 82, "slash": 80, "crush": 72, "magic": -6, "ranged": 80 }, "requirements": { "quests": { "rs:dragon_slayer": "complete" } } } } } ================================================ FILE: data/config/items/equipment/standard-metals/rune-god-armour.json ================================================ { "presets": { "rs:rune_god_armour_base": { "tradable": true, "equippable": true, "equipment_data": { "requirements": { "skills": { "defence": 40 } }, "skill_bonuses": { "prayer": 1 } } } }, "rs:guthix_full_helm": { "extends": "rs:rune_god_armour_base", "game_id": 2673, "examine": "A rune full face helmet in the colours of Guthix.", "weight": 2.721, "equipment_data": { "equipment_slot": "head", "offensive_bonuses": { "magic": -6, "ranged": -3 }, "defensive_bonuses": { "stab": 30, "slash": 32, "crush": 27, "magic": -1, "ranged": 30 } } }, "rs:guthix_kiteshield": { "extends": "rs:rune_god_armour_base", "game_id": 2675, "examine": "Rune kiteshield in the colours of Guthix.", "weight": 5.443, "equipment_data": { "equipment_slot": "off_hand", "offensive_bonuses": { "magic": -8, "ranged": -3 }, "defensive_bonuses": { "stab": 44, "slash": 48, "crush": 46, "magic": -1, "ranged": 46 } } }, "rs:guthix_platelegs": { "extends": "rs:rune_god_armour_base", "game_id": 2671, "examine": "Rune platelegs in the colours of Guthix.", "weight": 9.071, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -11 }, "defensive_bonuses": { "stab": 51, "slash": 49, "crush": 47, "magic": -4, "ranged": 49 } } }, "rs:guthix_plateskirt": { "extends": "rs:rune_god_armour_base", "game_id": 3480, "examine": "Rune plateskirt in the colours of Guthix.", "weight": 8.164, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -11 }, "defensive_bonuses": { "stab": 51, "slash": 49, "crush": 47, "magic": -4, "ranged": 49 } } }, "rs:guthix_platebody": { "extends": "rs:rune_god_armour_base", "game_id": 2669, "examine": "Rune platebody in the colours of Guthix.", "weight": 9.979, "equipment_data": { "equipment_slot": "torso", "offensive_bonuses": { "magic": -30, "ranged": -15 }, "defensive_bonuses": { "stab": 82, "slash": 80, "crush": 72, "magic": -6, "ranged": 80 }, "requirements": { "quests": { "rs:dragon_slayer": "complete" } } } }, "rs:zamorak_full_helm": { "extends": "rs:rune_god_armour_base", "game_id": 2657, "examine": "A rune full face helmet in the colours of Zamorak.", "weight": 2.721, "equipment_data": { "equipment_slot": "head", "offensive_bonuses": { "magic": -6, "ranged": -3 }, "defensive_bonuses": { "stab": 30, "slash": 32, "crush": 27, "magic": -1, "ranged": 30 } } }, "rs:zamorak_kiteshield": { "extends": "rs:rune_god_armour_base", "game_id": 2659, "examine": "Rune kiteshield in the colours of Zamorak.", "weight": 5.443, "equipment_data": { "equipment_slot": "off_hand", "offensive_bonuses": { "magic": -8, "ranged": -3 }, "defensive_bonuses": { "stab": 44, "slash": 48, "crush": 46, "magic": -1, "ranged": 46 } } }, "rs:zamorak_platelegs": { "extends": "rs:rune_god_armour_base", "game_id": 2655, "examine": "Rune platelegs in the colours of Zamorak.", "weight": 9.071, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -11 }, "defensive_bonuses": { "stab": 51, "slash": 49, "crush": 47, "magic": -4, "ranged": 49 } } }, "rs:zamorak_plateskirt": { "extends": "rs:rune_god_armour_base", "game_id": 3478, "examine": "Rune plateskirt in the colours of Zamorak.", "weight": 8.164, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -11 }, "defensive_bonuses": { "stab": 51, "slash": 49, "crush": 47, "magic": -4, "ranged": 49 } } }, "rs:zamorak_platebody": { "extends": "rs:rune_god_armour_base", "game_id": 2653, "examine": "Rune platebody in the colours of Zamorak.", "weight": 9.979, "equipment_data": { "equipment_slot": "torso", "offensive_bonuses": { "magic": -30, "ranged": -15 }, "defensive_bonuses": { "stab": 82, "slash": 80, "crush": 72, "magic": -6, "ranged": 80 }, "requirements": { "quests": { "rs:dragon_slayer": "complete" } } } }, "rs:saradomin_full_helm": { "extends": "rs:rune_god_armour_base", "game_id": 2665, "examine": "A rune full face helmet in the colours of Saradomin.", "weight": 2.721, "equipment_data": { "equipment_slot": "head", "offensive_bonuses": { "magic": -6, "ranged": -3 }, "defensive_bonuses": { "stab": 30, "slash": 32, "crush": 27, "magic": -1, "ranged": 30 } } }, "rs:saradomin_kiteshield": { "extends": "rs:rune_god_armour_base", "game_id": 2667, "examine": "Rune kiteshield in the colours of Saradomin.", "weight": 5.443, "equipment_data": { "equipment_slot": "off_hand", "offensive_bonuses": { "magic": -8, "ranged": -3 }, "defensive_bonuses": { "stab": 44, "slash": 48, "crush": 46, "magic": -1, "ranged": 46 } } }, "rs:saradomin_platelegs": { "extends": "rs:rune_god_armour_base", "game_id": 2663, "examine": "Rune platelegs in the colours of Saradomin.", "weight": 9.071, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -11 }, "defensive_bonuses": { "stab": 51, "slash": 49, "crush": 47, "magic": -4, "ranged": 49 } } }, "rs:saradomin_plateskirt": { "extends": "rs:rune_god_armour_base", "game_id": 3479, "examine": "Rune plateskirt in the colours of Saradomin.", "weight": 8.164, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -11 }, "defensive_bonuses": { "stab": 51, "slash": 49, "crush": 47, "magic": -4, "ranged": 49 } } }, "rs:saradomin_platebody": { "extends": "rs:rune_god_armour_base", "game_id": 2661, "examine": "Rune platebody in the colours of Saradomin.", "weight": 9.979, "equipment_data": { "equipment_slot": "torso", "offensive_bonuses": { "magic": -30, "ranged": -15 }, "defensive_bonuses": { "stab": 82, "slash": 80, "crush": 72, "magic": -6, "ranged": 80 }, "requirements": { "quests": { "rs:dragon_slayer": "complete" } } } } } ================================================ FILE: data/config/items/equipment/standard-metals/steel-armour.json ================================================ { "presets": { "rs:steel_armour_base": { "tradable": true, "equippable": true, "equipment_data": { "requirements": { "skills": { "defence": 5 } } } } }, "rs:steel_platelegs": { "extends": "rs:steel_armour_base", "game_id": 1069, "examine": "These look pretty heavy.", "weight": 9.071, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -7 }, "defensive_bonuses": { "stab": 17, "slash": 16, "crush": 15, "magic": -4, "ranged": 16 } } }, "rs:steel_plateskirt": { "extends": "rs:steel_armour_base", "game_id": 1083, "examine": "Designer leg protection.", "weight": 8.164, "equipment_data": { "equipment_slot": "legs", "offensive_bonuses": { "magic": -21, "ranged": -7 }, "defensive_bonuses": { "stab": 17, "slash": 16, "crush": 15, "magic": -4, "ranged": 16 } } } } ================================================ FILE: data/config/items/equipment/standard-metals/steel-weapons.json ================================================ { "rs:steel_battleaxe": { "game_id": 1365, "examine": "A vicious looking axe.", "tradable": true, "weight": 2.721, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "one_handed", "requirements": { "skills": { "attack": 5 } }, "offensive_bonuses": { "speed": 6, "stab": -2, "slash": 16, "crush": 11 }, "defensive_bonuses": { "ranged": -1 }, "skill_bonuses": { "strength": 20 }, "weapon_info": { "style": "axe" } } } } ================================================ FILE: data/config/items/equipment/standard-metals/white-armour.json ================================================ { "rs:white_full_helm": { "game_id": 6623, "tradable": true, "weight": 2.721, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "helmet", "requirements": { "skills": { "defense": 10 } }, "offensive_bonuses": { "magic": -6, "ranged": -2 }, "defensive_bonuses": { "stab": 12, "slash": 13, "crush": 10, "magic": -1, "ranged": 12 }, "skill_bonuses": { "prayer": 1 } } } } ================================================ FILE: data/config/items/equipment/tiaras.json ================================================ { "rs:air_tiara": { "game_id": 5527, "tradable": true, "weight": 1, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } }, "rs:mind_tiara": { "game_id": 5529, "tradable": true, "weight": 1, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } }, "rs:water_tiara": { "game_id": 5531, "tradable": true, "weight": 1, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } }, "rs:body_tiara": { "game_id": 5533, "tradable": true, "weight": 1, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } }, "rs:earth_tiara": { "game_id": 5535, "tradable": true, "weight": 1, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } }, "rs:fire_tiara": { "game_id": 5537, "tradable": true, "weight": 1, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } }, "rs:cosmic_tiara": { "game_id": 5539, "tradable": true, "weight": 1, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } }, "rs:nature_tiara": { "game_id": 5541, "tradable": true, "weight": 1, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } }, "rs:chaos_tiara": { "game_id": 5543, "tradable": true, "weight": 1, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } }, "rs:law_tiara": { "game_id": 5545, "tradable": true, "weight": 1, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } }, "rs:death_tiara": { "game_id": 5547, "tradable": true, "weight": 1, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } }, "rs:blood_tiara": { "game_id": 5549, "tradable": true, "weight": 1, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } }, "rs:soul_tiara": { "game_id": 5551, "tradable": true, "weight": 1, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } } } ================================================ FILE: data/config/items/equipment/training.json ================================================ { "rs:training_sword": { "game_id": 9703, "examine": "Basic training sword.", "tradable": false, "weight": 1.814, "equippable": true, "destroy": "You can get another Training sword by talking to the Melee Combat skill tutor north of Lumbridge Castle.", "equipment_data": { "equipment_slot": "main_hand", "equipment_type": "one_handed", "offensive_bonuses": { "speed": 4, "stab": 4, "slash": 3, "crush": -2 }, "defensive_bonuses": { "slash": 2, "crush": 1 }, "skill_bonuses": { "strength": 5 }, "weapon_info": { "style": "dagger" } } }, "rs:training_shield": { "game_id": 9704, "examine": "Made of flimsy painted wood.", "tradable": false, "weight": 5.443, "equippable": true, "destroy": "You can get another Training shield by talking to the Melee Combat skill tutor north of Lumbridge Castle.", "equipment_data": { "equipment_slot": "off_hand", "defensive_bonuses": { "stab": 4, "slash": 5, "crush": 3, "magic": 1, "ranged": 4 } } } } ================================================ FILE: data/config/items/equipment/whips.json ================================================ { "rs:abyssal_whip": { "game_id": 4151, "examine": "A weapon from the Abyss.", "tradable": true, "weight": 0.453, "equippable": true, "equipment_data": { "equipment_slot": "main_hand", "requirements": { "skills": { "attack": 70 } }, "offensive_bonuses": { "speed": 4, "stab": 0, "slash": 82, "crush": 0, "magic": 0, "ranged": 0 }, "defensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "skill_bonuses": { "prayer": 0, "strength": 82, "ranged": 0, "magic": 0 }, "weapon_info": { "style": "whip" } } }, "rs:veracs_flail": { "game_id": 4755, "examine": "Verac the Defiled's flail.", "tradable": true, "weight": 2.267, "equippable": true, "equipment_data": { "equipment_slot": "2h", "requirements": { "skills": { "attack": 70 } }, "offensive_bonuses": { "speed": 5, "stab": 68, "slash": -2, "crush": 82, "magic": 0, "ranged": 0 }, "defensive_bonuses": { "stab": 0, "slash": 0, "crush": 0, "magic": 0, "ranged": 0 }, "skill_bonuses": { "prayer": 6, "strength": 72, "ranged": 0, "magic": 0 }, "weapon_info": { "style": "mace" } } } } ================================================ FILE: data/config/items/food.json ================================================ { "presets": { "rs:food": { "tradable": true, "consumable": true, "metadata": { "consume_effects": { "clock": "food" }, "special": false } }, "rs:beverage": { "tradable": true, "consumable": true, "metadata": { "consume_effects": { "clock": "food" }, "special": false } } }, "rs:meat": { "extends": "rs:food", "game_id": 2142, "metadata": { "consume_effects": { "skills": { "hitpoints": 3 } } } }, "rs:shrimps": { "extends": "rs:food", "game_id": 315, "metadata": { "consume_effects": { "skills": { "hitpoints": 3 } } } }, "rs:chicken": { "extends": "rs:food", "game_id": 2140, "metadata": { "consume_effects": { "skills": { "hitpoints": 3 } } } }, "rs:anchovies": { "extends": "rs:food", "game_id": 319, "metadata": { "consume_effects": { "skills": { "hitpoints": 1 } } } }, "rs:sardine": { "extends": "rs:food", "game_id": 325, "metadata": { "consume_effects": { "skills": { "hitpoints": 4 } } } }, "rs:karambwan": { "extends": "rs:food", "game_id": 3142, "metadata": { "consume_effects": { "clock": "special_food", "skills": { "hitpoints": 18 } } } }, "rs:ugthanki_kebab": { "extends": "rs:food", "game_id": 1883, "metadata": { "consume_effects": { "skills": { "hitpoints": 19 } } } }, "rs:herring": { "extends": "rs:food", "game_id": 345, "metadata": { "consume_effects": { "skills": { "hitpoints": 5 } } } }, "rs:mackerel": { "extends": "rs:food", "game_id": 355, "metadata": { "consume_effects": { "skills": { "hitpoints": 6 } } } }, "rs:thin_snail_meat": { "extends": "rs:food", "game_id": 3369, "metadata": { "consume_effects": { "skills": { "hitpoints": [5, 7] } } } }, "rs:trout": { "extends": "rs:food", "game_id": 333, "metadata": { "consume_effects": { "skills": { "hitpoints": 7 } } } }, "rs:lean_snail_meat": { "extends": "rs:food", "game_id": 3371, "metadata": { "consume_effects": { "skills": { "hitpoints": [5, 8] } } } }, "rs:cod": { "extends": "rs:food", "game_id": 339, "metadata": { "consume_effects": { "skills": { "hitpoints": 7 } } } }, "rs:pike": { "extends": "rs:food", "game_id": 351, "metadata": { "consume_effects": { "skills": { "hitpoints": 8 } } } }, "rs:salmon": { "extends": "rs:food", "game_id": 329, "metadata": { "consume_effects": { "skills": { "hitpoints": 9 } } } }, "rs:tuna": { "extends": "rs:food", "game_id": 361, "metadata": { "consume_effects": { "skills": { "hitpoints": 10 } } } }, "rs:cooked_chompy": { "extends": "rs:food", "game_id": 2878, "metadata": { "consume_effects": { "skills": { "hitpoints": 10 } } } }, "rs:fish_cake": { "extends": "rs:food", "game_id": 7530, "metadata": { "consume_effects": { "skills": { "hitpoints": 11 } } } }, "rs:cave_eel": { "extends": "rs:food", "game_id": 5003, "metadata": { "consume_effects": { "skills": { "hitpoints": [8, 12] } } } }, "rs:lobster": { "extends": "rs:food", "game_id": 379, "metadata": { "consume_effects": { "skills": { "hitpoints": 12 } } } }, "rs:jubbly": { "extends": "rs:food", "game_id": 7568, "metadata": { "consume_effects": { "skills": { "hitpoints": 15 } } } }, "rs:bass": { "extends": "rs:food", "game_id": 365, "metadata": { "consume_effects": { "skills": { "hitpoints": 13 } } } }, "rs:swordfish": { "extends": "rs:food", "game_id": 373, "metadata": { "consume_effects": { "skills": { "hitpoints": 14 } } } }, "rs:lava_eel": { "extends": "rs:food", "game_id": 2149, "metadata": { "consume_effects": { "skills": { "hitpoints": 11 } } } }, "rs:shark": { "extends": "rs:food", "game_id": 385, "metadata": { "consume_effects": { "skills": { "hitpoints": 20 } } } }, "rs:sea_turtle": { "extends": "rs:food", "game_id": 397, "metadata": { "consume_effects": { "skills": { "hitpoints": 21 } } } }, "rs:manta_ray": { "extends": "rs:food", "game_id": 391, "metadata": { "consume_effects": { "skills": { "hitpoints": 22 } } } }, "rs:bread": { "extends": "rs:food", "game_id": 2309, "metadata": { "consume_effects": { "skills": { "hitpoints": 5 } } } }, "rs:uncooked_meat_pie": { "extends": "rs:food", "game_id": 2320 }, "rs:redberry_pie": { "extends": "rs:food", "metadata": { "consume_effects": { "skills": { "hitpoints": 5 } } }, "variations": [ { "metadata": { "consume_effects": { "replaced_by": "rs:redberry_pie:1" } }, "suffix": "0", "game_id": 2325 }, { "metadata": { "consume_effects": { "replaced_by": "rs:pie_dish" } }, "suffix": "1", "tradable": false, "game_id": 2333 } ] }, "rs:meat_pie": { "extends": "rs:food", "game_id": 2327, "metadata": { "consume_effects": { "skills": { "hitpoints": 6 } } }, "variations": [ { "metadata": { "consume_effects": { "replaced_by": "rs:meat_pie:1" } }, "suffix": "0", "game_id": 2327 }, { "metadata": { "consume_effects": { "replaced_by": "rs:pie_dish" } }, "suffix": "1", "tradable": false, "game_id": 2331 } ] }, "rs:apple_pie": { "extends": "rs:food", "metadata": { "consume_effects": { "skills": { "hitpoints": 7 } } }, "variations": [ { "metadata": { "consume_effects": { "replaced_by": "rs:apple_pie:1" } }, "suffix": "0", "game_id": 2323 }, { "metadata": { "consume_effects": { "replaced_by": "rs:pie_dish" } }, "suffix": "1", "tradable": false, "game_id": 2335 } ] }, "rs:garden_pie": { "extends": "rs:food", "metadata": { "consume_effects": { "skills": { "hitpoints": 6, "farming": 3 } } }, "variations": [ { "metadata": { "consume_effects": { "replaced_by": "rs:garden_pie:1" } }, "suffix": "0", "game_id": 7178 }, { "metadata": { "consume_effects": { "replaced_by": "rs:pie_dish" } }, "suffix": "1", "tradable": false, "game_id": 7180 } ] }, "rs:fish_pie": { "extends": "rs:food", "game_id": 7188, "metadata": { "consume_effects": { "skills": { "hitpoints": 6, "fishing": 3 } } }, "variations": [ { "metadata": { "consume_effects": { "replaced_by": "rs:fish_pie:1" } }, "suffix": "0", "game_id": 7188 }, { "metadata": { "consume_effects": { "replaced_by": "rs:pie_dish" } }, "suffix": "1", "tradable": false, "game_id": 7190 } ] }, "rs:admiral_pie": { "extends": "rs:food", "metadata": { "consume_effects": { "skills": { "hitpoints": 8, "fishing": 5 } } }, "variations": [ { "metadata": { "consume_effects": { "replaced_by": "rs:admiral_pie:1" } }, "suffix": "0", "game_id": 7198 }, { "metadata": { "consume_effects": { "replaced_by": "rs:pie_dish" } }, "suffix": "1", "tradable": false, "game_id": 7200 } ] }, "rs:wild_pie": { "extends": "rs:food", "metadata": { "consume_effects": { "skills": { "hitpoints": 11, "slayer": 5, "ranged": 4 } } }, "variations": [ { "metadata": { "consume_effects": { "replaced_by": "rs:wild_pie:1" } }, "suffix": "0", "game_id": 7208 }, { "metadata": { "consume_effects": { "replaced_by": "rs:pie_dish" } }, "suffix": "1", "tradable": false, "game_id": 7210 } ] }, "rs:summer_pie": { "extends": "rs:food", "metadata": { "consume_effects": { "skills": { "hitpoints": 11, "agility": 5 }, "energy": 10 } }, "variations": [ { "game_id": 7218, "suffix": "0", "metadata": { "consume_effects": { "replaced_by": "rs:summer_pie:1" } } }, { "suffix": "1", "tradable": false, "game_id": 7220, "metadata": { "consume_effects": { "replaced_by": "rs:pie_dish" } } } ] }, "rs:stew": { "extends": "rs:food", "game_id": 2003, "metadata": { "consume_effects": { "replaced_by": "rs:bowl", "skills": { "hitpoints": 11 } } } }, "rs:banana_stew": { "extends": "rs:food", "game_id": 4016, "metadata": { "consume_effects": { "replaced_by": "rs:bowl", "skills": { "hitpoints": 11 } } } }, "rs:spicy_stew": { "extends": "rs:food", "game_id": 7479, "metadata": { "consume_effects": { "special": true } } }, "rs:plain_pizza": { "extends": "rs:food", "metadata": { "consume_effects": { "skills": { "hitpoints": 7 } } }, "variations": [ { "metadata": { "consume_effects": { "replaced_by": "rs:plain_pizza:1" } }, "suffix": "0", "game_id": 2289 }, { "metadata": { "consume_effects": { "replaced_by": null } }, "suffix": "1", "tradable": false, "game_id": 2291 } ] }, "rs:meat_pizza": { "extends": "rs:food", "metadata": { "consume_effects": { "skills": { "hitpoints": 8 } } }, "variations": [ { "metadata": { "consume_effects": { "replaced_by": "rs:meat_pizza:1" } }, "suffix": "0", "game_id": 2293 }, { "metadata": { "consume_effects": { "replaced_by": null } }, "suffix": "1", "tradable": false, "game_id": 2295 } ] }, "rs:anchovy_pizza": { "extends": "rs:food", "metadata": { "consume_effects": { "skills": { "hitpoints": 9 } } }, "variations": [ { "metadata": { "consume_effects": { "replaced_by": "rs:anchovy_pizza:1" } }, "suffix": "0", "game_id": 2297 }, { "metadata": { "consume_effects": { "replaced_by": null } }, "suffix": "1", "tradable": false, "game_id": 2299 } ] }, "rs:pineapple_pizza": { "extends": "rs:food", "metadata": { "consume_effects": { "skills": { "hitpoints": 11 } } }, "variations": [ { "metadata": { "consume_effects": { "replaced_by": "rs:pineapple_pizza:1" } }, "suffix": "0", "game_id": 2301 }, { "metadata": { "consume_effects": { "replaced_by": null } }, "suffix": "1", "tradable": false, "game_id": 2303 } ] }, "rs:cake": { "extends": "rs:food", "metadata": { "consume_effects": { "skills": { "hitpoints": 4 } } }, "variations": [ { "metadata": { "consume_effects": { "replaced_by": "rs:cake:1" } }, "suffix": "0", "game_id": 1891 }, { "metadata": { "consume_effects": { "replaced_by": "rs:cake:2" } }, "suffix": "1", "tradable": false, "game_id": 1893 }, { "metadata": { "consume_effects": { "replaced_by": null } }, "suffix": "2", "tradable": false, "game_id": 1895 } ] }, "rs:chocolate_cake": { "extends": "rs:food", "metadata": { "consume_effects": { "skills": { "hitpoints": 5 } } }, "variations": [ { "metadata": { "consume_effects": { "replaced_by": "rs:chocolate_cake:1" } }, "suffix": "0", "game_id": 1897 }, { "metadata": { "consume_effects": { "replaced_by": "rs:chocolate_cake:2" } }, "suffix": "1", "tradable": false, "game_id": 1899 }, { "metadata": { "consume_effects": { "replaced_by": null } }, "suffix": "2", "tradable": false, "game_id": 1901 } ] }, "rs:wine": { "extends": "rs:beverage", "game_id": 1993, "metadata": { "consume_effects": { "skills": { "hitpoints": 11, "attack": -2 } } } }, "rs:nettle_tea": { "extends": "rs:beverage", "game_id": 4239, "metadata": { "consume_effects": { "skills": { "hitpoints": 3 }, "energy": 5 } } }, "rs:cider": { "extends": "rs:beverage", "game_id": 5763, "metadata": { "consume_effects": { "skills": { "hitpoints": 2, "farming": 1 } } } }, "rs:dwarven_stout": { "extends": "rs:beverage", "game_id": 1913, "metadata": { "consume_effects": { "special": true } } }, "rs:asgarnian_ale": { "extends": "rs:beverage", "game_id": 1905, "metadata": { "consume_effects": { "skills": { "hitpoints": 2, "strength": 2, "attack": -4 } } } }, "rs:greenmans_ale": { "extends": "rs:beverage", "game_id": 7746, "metadata": { "consume_effects": { "skills": { "hitpoints": 1, "herblore": 1, "attack": -3, "strength": -3, "defence": -3 } } } }, "rs:wizards_mind_bomb": { "extends": "rs:beverage", "game_id": 1907, "metadata": { "consume_effects": { "special": true } } }, "rs:dragon_bitter": { "extends": "rs:beverage", "game_id": 1911, "metadata": { "consume_effects": { "skills": { "hitpoints": 1, "attack": -3, "strength": 2 } } } }, "rs:moonlight_mead": { "extends": "rs:beverage", "game_id": 2955, "metadata": { "consume_effects": { "skills": { "hitpoints": 4 } } } }, "rs:axemans_folly": { "extends": "rs:beverage", "game_id": 5751, "metadata": { "consume_effects": { "skills": { "hitpoints": 1, "woodcutting": 1, "strength": -3, "attack": -3 } } } }, "rs:chefs_delight": { "extends": "rs:beverage", "game_id": 5755, "metadata": { "consume_effects": { "special": true } } }, "rs:slayers_respite": { "extends": "rs:beverage", "game_id": 5759, "metadata": { "consume_effects": { "skills": { "hitpoints": 1, "slayer": 2, "strength": -2, "attack": -2 } } } }, "rs:spicy_sauce": { "extends": "rs:food", "game_id": 7072, "metadata": { "consume_effects": { "skills": { "hitpoints": 2 } } } }, "rs:scrambled_egg": { "extends": "rs:food", "game_id": 7078, "metadata": { "consume_effects": { "skills": { "hitpoints": 5 } } } }, "rs:scrambled_egg_and_tomato": { "extends": "rs:food", "game_id": 7064, "metadata": { "consume_effects": { "skills": { "hitpoints": 8 } } } }, "rs:sweetcorn": { "extends": "rs:food", "game_id": 7088, "metadata": { "consume_effects": { "special": true } } }, "rs:baked_potato_with_butter": { "extends": "rs:food", "game_id": 6703, "metadata": { "consume_effects": { "skills": { "hitpoints": 14 } } } }, "rs:fried_onion": { "extends": "rs:food", "game_id": 7084, "metadata": { "consume_effects": { "skills": { "hitpoints": 5 } } } }, "rs:fried_mushroom": { "extends": "rs:food", "game_id": 7082, "metadata": { "consume_effects": { "skills": { "hitpoints": 5 } } } }, "rs:baked_potato_with_butter_and_cheese": { "extends": "rs:food", "game_id": 6705, "metadata": { "consume_effects": { "skills": { "hitpoints": 16 } } } }, "rs:baked_potato_with_egg_and_tomato": { "extends": "rs:food", "game_id": 7056, "metadata": { "consume_effects": { "skills": { "hitpoints": 16 } } } }, "rs:fried_mushroom_and_onion": { "extends": "rs:food", "game_id": 7066, "metadata": { "consume_effects": { "skills": { "hitpoints": 11 } } } }, "rs:baked_potato_with_mushroom_and_onion": { "extends": "rs:food", "game_id": 7058, "metadata": { "consume_effects": { "skills": { "hitpoints": 20 } } } }, "rs:tuna_and_sweetcorn": { "extends": "rs:food", "game_id": 7068, "metadata": { "consume_effects": { "skills": { "hitpoints": 13 } } } }, "rs:baked_potato_with_tuna_and_sweetcorn": { "extends": "rs:food", "game_id": 7060, "metadata": { "consume_effects": { "skills": { "hitpoints": 22 } } } }, "rs:butter": { "tradable": true, "game_id": 6697 }, "rs:raw_shrimp": { "extends": "rs:food", "game_id": 317 }, "rs:raw_sardine": { "extends": "rs:food", "game_id": 327 }, "rs:raw_karambwanji": { "extends": "rs:food", "game_id": 3150 }, "rs:raw_herring": { "extends": "rs:food", "game_id": 345 }, "rs:raw_anchovies": { "extends": "rs:food", "game_id": 321 }, "rs:raw_mackerel": { "extends": "rs:food", "game_id": 353 }, "rs:raw_trout": { "extends": "rs:food", "game_id": 335 }, "rs:raw_cod": { "extends": "rs:food", "game_id": 341 }, "rs:raw_pike": { "extends": "rs:food", "game_id": 349 }, "rs:raw_slimy_eel": { "extends": "rs:food", "game_id": 3379 }, "rs:raw_salmon": { "extends": "rs:food", "game_id": 331 }, "rs:raw_tuna": { "extends": "rs:food", "game_id": 359 }, "rs:raw_cave_eel": { "extends": "rs:food", "game_id": 5001 }, "rs:raw_bass": { "extends": "rs:food", "game_id": 363 }, "rs:raw_lobster": { "extends": "rs:food", "game_id": 377 }, "rs:raw_swordfish": { "extends": "rs:food", "game_id": 371 }, "rs:raw_lava_eel": { "extends": "rs:food", "game_id": 2148 }, "rs:raw_monkfish": { "extends": "rs:food", "game_id": 7944 }, "rs:raw_karambwan": { "extends": "rs:food", "game_id": 3142 }, "rs:raw_shark": { "extends": "rs:food", "game_id": 383 }, "rs:raw_sea_turtle": { "extends": "rs:food", "game_id": 395 }, "rs:raw_manta_ray": { "extends": "rs:food", "game_id": 389 }, "rs:raw_manta_ray": { "extends": "rs:food", "game_id": 389 }, "rs:raw_leaping_trout": { "extends": "rs:food", "game_id": 389 }, "rs:raw_leaping_salmon": { "extends": "rs:food", "game_id": 11330 }, "rs:raw_leaping_sturgeon": { "extends": "rs:food", "game_id": 11332 }, "rs:cheese": { "tradable": true, "game_id": 1985 }, "rs:pitta_bread": { "tradable": true, "game_id": 1865 }, "rs:wine_of_zamorak": { "game_id": 245, "tradable": true }, "rs:baked_potato": { "tradable": "true", "game_id": 6701 } } ================================================ FILE: data/config/items/holiday/partyhats.json ================================================ { "presets": { "rs:partyhat": { "tradable": true, "weight": 0.056, "equippable": true, "equipment_data": { "equipment_slot": "head", "equipment_type": "hat" } } }, "rs:red_partyhat": { "extends": "rs:partyhat", "game_id": 1038 }, "rs:yellow_partyhat": { "extends": "rs:partyhat", "game_id": 1040 }, "rs:blue_partyhat": { "extends": "rs:partyhat", "game_id": 1042 }, "rs:green_partyhat": { "extends": "rs:partyhat", "game_id": 1034 }, "rs:purple_partyhat": { "extends": "rs:partyhat", "game_id": 1046 }, "rs:white_partyhat": { "extends": "rs:partyhat", "game_id": 1048 } } ================================================ FILE: data/config/items/icons.json ================================================ { "rs:gnome_stronghold_agility_course": { "game_id": 2150 }, "rs:gnomeball_game": { "game_id": 751 }, "rs:werewolf_skullball_game": { "game_id": 1061 }, "rs:agility_pyramid": { "game_id": 6970 }, "rs:barbarian_outpost_agility_course": { "game_id": 1365 }, "rs:ape_atoll_agility_course": { "game_id": 4024 }, "rs:wilderness_course": { "game_id": 553 }, "rs:agility_jump_green": { "game_id": 6518 }, "rs:agility_contortion_green": { "game_id": 6520 }, "rs:agility_balance_green": { "game_id": 6519 }, "rs:agility_climb": { "game_id": 6517 }, "rs:agility_jump_yellow": { "game_id": 6514 }, "rs:agility_contortion_yellow": { "game_id": 6516 }, "rs:agility_balance_yellow": { "game_id": 6515 } } ================================================ FILE: data/config/items/logs.json ================================================ { "presets": { "rs:log": { "tradable": true, "weight": 2 } }, "rs:logs": { "extends": "rs:log", "game_id": 1511 }, "rs:achey_logs": { "extends": "rs:log", "game_id": 2862 }, "rs:pyre_logs": { "extends": "rs:log", "game_id": 3438 }, "rs:oak_logs": { "extends": "rs:log", "game_id": 1521 }, "rs:oak_pyre_logs": { "extends": "rs:log", "game_id": 3440 }, "rs:willow_logs": { "extends": "rs:log", "game_id": 1519 }, "rs:teak_logs": { "extends": "rs:log", "game_id": 6333 }, "rs:willow_pyre_logs": { "extends": "rs:log", "game_id": 3442 }, "rs:teak_pyre_logs": { "extends": "rs:log", "game_id": 6211 }, "rs:maple_logs": { "extends": "rs:log", "game_id": 1517 }, "rs:mahogany_logs": { "extends": "rs:log", "game_id": 6332 }, "rs:maple_pyre_logs": { "extends": "rs:log", "game_id": 3444 }, "rs:mahogany_pyre_logs": { "extends": "rs:log", "game_id": 6213 }, "rs:yew_logs": { "extends": "rs:log", "game_id": 1515 }, "rs:yew_pyre_logs": { "extends": "rs:log", "game_id": 3446 }, "rs:magic_logs": { "extends": "rs:log", "game_id": 1513 }, "rs:magic_pyre_logs": { "extends": "rs:log", "game_id": 1513 }, "rs:bark": { "game_id": 3239, "examine": "Bark from a hollow tree.", "tradable": true, "weight": 1, "equippable": false } } ================================================ FILE: data/config/items/other.json ================================================ { "rs:dwarf_remains": { "game_id": 0, "tradable": false }, "rs:rotten_potato": { "game_id": 5733, "tradable": false, "examine": "Yuk!" } } ================================================ FILE: data/config/items/quests/lost-city.json ================================================ { "rs:dramen_branch": { "game_id": 771, "examine": "A limb of the fabled Dramen tree.", "tradable": false, "weight": 2.267, "equippable": false } } ================================================ FILE: data/config/items/quests/witchs-potion.json ================================================ { "rs:rats_tail": { "game_id": 300, "tradable": false, "weight": 0.003 } } ================================================ FILE: data/config/items/skills/artisan-tools.json ================================================ { "rs:shears": { "game_id": 5603, "examine": "For shearing sheep.", "tradable": true, "weight": 0.113 }, "rs:chisel": { "game_id": 1755, "examine": "Good for detailed Crafting.", "tradable": true, "weight": 0.453 }, "rs:spade": { "game_id": 952, "examine": "A slightly muddy spade.", "tradable": true, "weight": 1.814 }, "rs:hammer": { "game_id": 2347, "examine": "Good for hitting things!", "tradable": true, "weight": 0.907 }, "rs:needle": { "game_id": 1733, "examine": "Used with a thread to make clothes.", "tradable": true, "weight": 0 }, "rs:thread": { "game_id": 1734, "examine": "Used with a needle to make clothes.", "tradable": true, "weight": 0 } } ================================================ FILE: data/config/items/skills/baking.json ================================================ { "rs:cake_tin": { "game_id": 1887, "examine": "Useful for baking cakes.", "tradable": true, "weight": 0.1 }, "rs:egg": { "game_id": 1944, "examine": "A nice fresh egg.", "tradable": true, "weight": 0.02 } } ================================================ FILE: data/config/items/skills/cooking.json ================================================ { "rs:raw_rat_meat": { "game_id": 2134 }, "rs:cooked_meat": { "game_id": 2142 }, "rs:burnt_meat": { "game_id": 2146 } } ================================================ FILE: data/config/items/skills/crafting/gems.json ================================================ { "presets": { "rs:uncut_gem_base": { "examine": "This would be worth more cut.", "weight": 0.003, "tradable": true, "equippable": false }, "rs:cut_gem_base": { "examine": "A precious gem.", "weight": 0.002, "tradable": true, "equippable": false } }, "rs:uncut_opal": { "extends": "rs:uncut_gem_base", "game_id": 1625 }, "rs:uncut_jade": { "extends": "rs:uncut_gem_base", "game_id": 1627 }, "rs:uncut_red_topaz": { "extends": "rs:uncut_gem_base", "game_id": 1629 }, "rs:uncut_sapphire": { "extends": "rs:uncut_gem_base", "game_id": 1623 }, "rs:uncut_emerald": { "extends": "rs:uncut_gem_base", "game_id": 1621 }, "rs:uncut_ruby": { "extends": "rs:uncut_gem_base", "game_id": 1619 }, "rs:uncut_diamond": { "extends": "rs:uncut_gem_base", "game_id": 1617 }, "rs:opal": { "extends": "rs:cut_gem_base", "game_id": 1609 }, "rs:jade": { "extends": "rs:cut_gem_base", "game_id": 1611 }, "rs:red_topaz": { "extends": "rs:cut_gem_base", "game_id": 1613 }, "rs:sapphire": { "extends": "rs:cut_gem_base", "game_id": 1607 }, "rs:emerald": { "extends": "rs:cut_gem_base", "game_id": 1605 }, "rs:ruby": { "extends": "rs:cut_gem_base", "game_id": 1603 }, "rs:diamond": { "extends": "rs:cut_gem_base", "game_id": 1601 } } ================================================ FILE: data/config/items/skills/crafting/jewelery-moulds.json ================================================ { "presets": { "rs:mould_base": { "weight": 0.453, "tradable": true } }, "rs:ring_mould": { "extends": "rs:mould_base", "game_id": 1592, "examine": "Used to make gold rings." }, "rs:necklace_mould": { "extends": "rs:mould_base", "game_id": 1597, "examine": "Used to make gold necklaces." }, "rs:amulet_mould": { "extends": "rs:mould_base", "game_id": 1595, "examine": "Used to make gold amulets." }, "rs:holy_mould": { "extends": "rs:mould_base", "game_id": 1599, "examine": "Used to make holy symbols of Saradomin." }, "rs:sickle_mould": { "extends": "rs:mould_base", "game_id": 2976, "examine": "Used to make sickles." }, "rs:tiara_mould": { "game_id": 5523, "examine": "A mould for tiaras.", "weight": 1, "tradable": true } } ================================================ FILE: data/config/items/skills/firemaking.json ================================================ { "rs:tinderbox": { "game_id": 590, "tradable": true, "weight": 0.035 }, "rs:normal_pyre_ships": { "game_id": 3438 }, "rs:oak_pyre_ships": { "game_id": 3440 }, "rs:willow_pyre_ships": { "game_id": 3442 }, "rs:teak_pyre_ships": { "game_id": 6211 }, "rs:maple_pyre_ships": { "game_id": 3444 }, "rs:mahogany_pyre_ships": { "game_id": 6213 }, "rs:yew_pyre_ships": { "game_id": 3446 }, "rs:magic_pyre_ships": { "game_id": 3446 }, "rs:candle": { "game_id": 36 }, "rs:candle_lanterns": { "game_id": 4527 }, "rs:oil_lamps": { "game_id": 4522 }, "rs:iron_spits": { "game_id": 7225 }, "rs:empty_oil_lantern": { "game_id": 4535 }, "rs:harpie_bug_lanterns": { "game_id": 7051 }, "rs:bullseye_lanterns": { "game_id": 4544 }, "rs:sapphire_lanterns": { "game_id": 4700 } } ================================================ FILE: data/config/items/skills/fishing.json ================================================ { "rs:small_fishing_net": { "game_id": 303 }, "rs:big_fishing_net": { "game_id": 305 }, "rs:fishing_rod": { "game_id": 307 }, "rs:fly_fishing_rod": { "game_id": 309 }, "rs:harpoon": { "game_id": 311 }, "rs:lobster_pot": { "game_id": 301 }, "rs:oily_fishing_rod": { "game_id": 1585 }, "rs:karambwan_vessel": { "game_id": 3157 } } ================================================ FILE: data/config/items/skills/fletching/arrows.json ================================================ { "rs:arrow_shaft": { "game_id": 52 }, "rs:bronze_arrow": { "game_id": 882 }, "rs:iron_arrow": { "game_id": 884 }, "rs:steel_arrow": { "game_id": 886 }, "rs:mithril_arrow": { "game_id": 888 }, "rs:broad_arrow": { "game_id": 4150 }, "rs:adamant_arrow": { "game_id": 890 }, "rs:rune_arrow": { "game_id": 892 } } ================================================ FILE: data/config/items/skills/fletching/bolts.json ================================================ { "rs:bronze_bolt": { "game_id": 877 }, "rs:opal_bolt": { "game_id": 879 }, "rs:blurite_bolt": { "game_id": 9139 }, "rs:jade_bolt": { "game_id": 9237 }, "rs:iron_bolt": { "game_id": 9140 }, "rs:silver_bolt": { "game_id": 9145 }, "rs:steel_bolt": { "game_id": 9141 }, "rs:mithril_bolt": { "game_id": 9142 }, "rs:sapphire_bolt": { "game_id": 9337 }, "rs:emerald_bolt": { "game_id": 9338 }, "rs:adamant_bolt": { "game_id": 9143 }, "rs:runite_bolt": { "game_id": 9144 }, "rs:onyx_bolt": { "game_id": 9342 } } ================================================ FILE: data/config/items/skills/herblore/herbs.json ================================================ { "presets": { "rs:grimy_herb_base": { "examine": "It needs cleaning.", "weight": 0.007, "tradable": true }, "rs:clean_herb_base": { "weight": 0.007, "tradable": true } }, "rs:grimy_guam": { "extends": "rs:grimy_herb_base", "game_id": 199 }, "rs:grimy_marrentill": { "extends": "rs:grimy_herb_base", "game_id": 201 }, "rs:grimy_tarromin": { "extends": "rs:grimy_herb_base", "game_id": 203 }, "rs:grimy_harralander": { "extends": "rs:grimy_herb_base", "game_id": 205 }, "rs:grimy_ranarr": { "extends": "rs:grimy_herb_base", "game_id": 207 }, "rs:grimy_irit": { "extends": "rs:grimy_herb_base", "game_id": 209 }, "rs:grimy_avantoe": { "extends": "rs:grimy_herb_base", "game_id": 211 }, "rs:grimy_kwuarm": { "extends": "rs:grimy_herb_base", "game_id": 213 }, "rs:grimy_cadantine": { "extends": "rs:grimy_herb_base", "game_id": 215 }, "rs:grimy_dwarf_weed": { "extends": "rs:grimy_herb_base", "game_id": 217 }, "rs:grimy_torstol": { "extends": "rs:grimy_herb_base", "game_id": 219 }, "rs:grimy_toadflax": { "extends": "rs:grimy_herb_base", "game_id": 3049 }, "rs:grimy_snapdragon": { "extends": "rs:grimy_herb_base", "game_id": 3051 }, "rs:grimy_lantadyme": { "extends": "rs:grimy_herb_base", "game_id": 2485 }, "rs:herb_guam": { "extends": "rs:clean_herb_base", "game_id": 249 }, "rs:herb_marrentill": { "extends": "rs:clean_herb_base", "game_id": 251 }, "rs:herb_tarromin": { "extends": "rs:clean_herb_base", "game_id": 253 }, "rs:herb_harralander": { "extends": "rs:clean_herb_base", "game_id": 255 }, "rs:herb_ranarr": { "extends": "rs:clean_herb_base", "game_id": 257 }, "rs:herb_irit": { "extends": "rs:clean_herb_base", "game_id": 259 }, "rs:herb_avantoe": { "extends": "rs:clean_herb_base", "game_id": 261 }, "rs:herb_kwuarm": { "extends": "rs:clean_herb_base", "game_id": 263 }, "rs:herb_cadantine": { "extends": "rs:clean_herb_base", "game_id": 265 }, "rs:herb_dwarf_weed": { "extends": "rs:clean_herb_base", "game_id": 267 }, "rs:herb_torstol": { "extends": "rs:clean_herb_base", "game_id": 269 }, "rs:herb_toadflax": { "extends": "rs:clean_herb_base", "game_id": 2998 }, "rs:herb_snapdragon": { "extends": "rs:clean_herb_base", "game_id": 3000 }, "rs:herb_lantadyme": { "extends": "rs:clean_herb_base", "game_id": 2481 } } ================================================ FILE: data/config/items/skills/herblore/ingredients.json ================================================ { "rs:eye_of_newt": { "game_id": 221 } } ================================================ FILE: data/config/items/skills/herblore/tools.json ================================================ { "rs:pestle_and_mortar": { "game_id": 233, "examine": "I can grind things for potions in this.", "tradable": true, "weight": 0.056 } } ================================================ FILE: data/config/items/skills/herblore.json ================================================ { "rs:herblore_attack_potion": { "game_id": 221 }, "rs:herblore_anti_poison_potion": { "game_id": 235 }, "rs:herblore_relicyms_balm": { "game_id": 1534 }, "rs:herblore_strength_potion": { "game_id": 225 }, "rs:herblore_serum_207": { "game_id": 592 }, "rs:herblore_stat_restore_potion": { "game_id": 223 }, "rs:guthix_balance_potion": { "game_id": 592 }, "rs:herblore_energy_potion": { "game_id": 1975 }, "rs:herblore_defence_potion": { "game_id": 239 }, "rs:herblore_agility_potion": { "game_id": 2152 }, "rs:herblore_combat_potion": { "game_id": 9736 }, "rs:herblore_prayer_restore_potion": { "game_id": 231 }, "rs:herblore_super_attack_potion": { "game_id": 221 }, "rs:herblore_super_anti_poison_potion": { "game_id": 235 }, "rs:herblore_fishing_potion": { "game_id": 231 }, "rs:herblore_super_energy_potion": { "game_id": 2970 }, "rs:herblore_hunting_potion": { "game_id": 10109 }, "rs:herblore_super_strength_potion": { "game_id": 225 }, "rs:herblore_magic_essence_potion": { "game_id": 9016 }, "rs:herblore_weapon_poison": { "game_id": 243 }, "rs:herblore_super_restore_poison": { "game_id": 223 }, "rs:herblore_super_defence_potion": { "game_id": 239 }, "rs:herblore_antidote_plus": { "game_id": 6049 }, "rs:herblore_anti_firebreath_potion": { "game_id": 243 }, "rs:herblore_ranging_potion": { "game_id": 245 }, "rs:herblore_weapon_poison_plus": { "game_id": 6016 }, "rs:herblore_magic_potion": { "game_id": 3138 }, "rs:herblore_zamorak_brew": { "game_id": 247 }, "rs:herblore_antidote_plus_plus": { "game_id": 6051 }, "rs:herblore_saradomin_brew": { "game_id": 6693 }, "rs:herblore_weapon_poison_plus_plus": { "game_id": 6018 }, "rs:guam_leaf": { "game_id": 199 }, "rs:rogues_purse": { "game_id": 1534 }, "rs:snake_weed": { "game_id": 1526 }, "rs:marrentill": { "game_id": 251 }, "rs:tarromin": { "game_id": 253 }, "rs:harralander": { "game_id": 255 }, "rs:ranarr_weed": { "game_id": 257 }, "rs:toadflax": { "game_id": 2998 }, "rs:irit_leaf": { "game_id": 259 }, "rs:avantoe": { "game_id": 261 }, "rs:kwuarm": { "game_id": 263 }, "rs:snapdragon": { "game_id": 3000 }, "rs:cadantine": { "game_id": 265 }, "rs:lantadyme": { "game_id": 2481 }, "rs:dwarf_weed": { "game_id": 267 }, "rs:torstol": { "game_id": 269 } } ================================================ FILE: data/config/items/skills/mining.json ================================================ { "rs:rune_essence": { "game_id": 1436, "examine": "An unimbued rune.", "tradable": true, "weight": 0.002, "equippable": false }, "rs:clay": { "game_id": 434, "examine": "Some hard dry clay.", "tradable": true, "weight": 1, "equippable": false }, "rs:copper_ore": { "game_id": 436, "examine": "This needs refining.", "tradable": true, "weight": 2.267, "equippable": false }, "rs:tin_ore": { "game_id": 438, "examine": "This needs refining.", "tradable": true, "weight": 2.267, "equippable": false }, "rs:blurite_ore": { "game_id": 668, "examine": "Definitely blue.", "tradable": false, "weight": 2.267, "equippable": false }, "rs:limestone": { "game_id": 3211, "examine": "Some limestone.", "tradable": true, "weight": 2.267, "equippable": false }, "rs:iron_ore": { "game_id": 440, "examine": "This needs refining.", "tradable": true, "weight": 2.267, "equippable": false }, "rs:silver_ore": { "game_id": 442, "examine": "This needs refining.", "tradable": true, "weight": 2.267, "equippable": false }, "rs:coal": { "game_id": 453, "examine": "Hmm a non-renewable energy source!", "tradable": true, "weight": 2.267, "equippable": false }, "rs:gold_ore": { "game_id": 444, "examine": "This needs refining.", "tradable": true, "weight": 2.267, "equippable": false }, "rs:mithril_ore": { "game_id": 447, "examine": "This needs refining.", "tradable": true, "weight": 1.814, "equippable": false }, "rs:adamantite_ore": { "game_id": 449, "examine": "This needs refining.", "tradable": true, "weight": 2.721, "equippable": false }, "rs:soft_clay": { "game_id": 1761, "examine": "Clay soft enough to mould.", "tradable": true, "weight": 0.907, "equippable": false }, "rs:runite_ore": { "game_id": 451, "examine": "This needs refining.", "tradable": true, "weight": 2.267, "equippable": false }, "rs:pure_essence": { "game_id": 7936, "examine": "An unimbued rune of extra capability.", "tradable": true, "weight": 0.002, "equippable": false }, "rs:granite": { "game_id": 6979, "examine": "A tiny chunk of granite.", "tradable": true, "weight": 0.5, "equippable": false, "variations": [ { "game_id": 6981, "examine": "A small chunk of granite.", "weight": 2, "suffix": "2kg" }, { "game_id": 6983, "examine": "A medium-sized chunk of granite.", "weight": 5, "suffix": "5kg" } ] } } ================================================ FILE: data/config/items/skills/prayer.json ================================================ { "rs:prayer_guide": { "game_id": 1714 }, "rs:herblore_anti_poison_potion": { "game_id": 235 } } ================================================ FILE: data/config/items/skills/runecrafting.json ================================================ { "rs:binding_necklace": { "game_id": 5521, "tradable": true, "weight": 0.01, "equippable": true, "equipment_data": { "equipment_slot": "neck" } }, "rs:pure_essence": { "game_id": 7936, "tradable": true, "stackable": true, "examine": "An uncharged Rune Stone of extra capability.", "weight": 0.01 }, "rs:rune_essence": { "game_id": 1436, "tradable": true, "examine": "An uncharged Rune Stone.", "weight": 0.01 }, "rs:air_rune": { "game_id": 556, "tradable": true, "stackable": true, "examine": "One of the 4 basic elemental Runes.", "weight": 0.01 }, "rs:mind_rune": { "game_id": 558, "tradable": true, "stackable": true, "exmine": "Used for basic level missile spells.", "weight": 0.01 }, "rs:water_rune": { "game_id": 555, "tradable": true, "stackable": true, "examine": "One of the 4 basic elemental Runes.", "weight": 0.01 }, "rs:earth_rune": { "game_id": 557, "tradable": true, "stackable": true, "examine": "One of the 4 basic elemental Runes.", "weight": 0.01 }, "rs:fire_rune": { "game_id": 554, "tradable": true, "stackable": true, "examine": "One of the 4 basic elemental Runes.", "weight": 0.01 }, "rs:body_rune": { "game_id": 559, "tradable": true, "stackable": true, "examine": "Used for curse spells.", "weight": 0.01 }, "rs:cosmic_rune": { "game_id": 564, "tradable": true, "stackable": true, "examine": "Used for enchant spells.", "weight": 0.01 }, "rs:chaos_rune": { "game_id": 562, "tradable": true, "stackable": true, "examine": "Used for low level missile spells.", "weight": 0.01 }, "rs:nature_rune": { "game_id": 561, "tradable": true, "stackable": true, "examine": "Used for alchemy spells.", "weight": 0.01 }, "rs:law_rune": { "game_id": 563, "tradable": true, "stackable": true, "examine": "Used for teleport spells.", "weight": 0.01 }, "rs:death_rune": { "game_id": 560, "tradable": true, "stackable": true, "examine": "Used for medium level missile spells.", "weight": 0.01 }, "rs:mist_rune": { "game_id": 4695, "tradable": true, "stackable": true, "examine": "A combined Air and Water Rune.", "weight": 0.01 }, "rs:dust_rune": { "game_id": 4696, "tradable": true, "stackable": true, "examine": "A combined Air and Earth Rune.", "weight": 0.01 }, "rs:mud_rune": { "game_id": 4698, "tradable": true, "stackable": true, "examine": "A combined Earth and Water Rune.", "weight": 0.01 }, "rs:smoke_rune": { "game_id": 4697, "tradable": true, "stackable": true, "examine": "A combined Air and Fire Rune.", "weight": 0.01 }, "rs:steam_rune": { "game_id": 4694, "tradable": true, "stackable": true, "examine": "A combined Water and Fire Rune.", "weight": 0.01 }, "rs:lava_rune": { "game_id": 4699, "tradable": true, "stackable": true, "examine": "A combined Earth and Fire Rune.", "weight": 0.01 }, "rs:astral_rune": { "game_id": 9075, "examine": "Used for Lunar spells.", "weight": 0.01 }, "rs:blood_rune": { "game_id": 565, "examine": "Used for high level missile spells.", "weight": 0.01 }, "rs:soul_rune": { "game_id": 566, "examine": "Used for high level curse spells.", "weight": 0.01 }, "rs:air_talisman": { "game_id": 1438, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:earth_talisman": { "game_id": 1440, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:fire_talisman": { "game_id": 1442, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:water_talisman": { "game_id": 1444, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:body_talisman": { "game_id": 1446, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:mind_talisman": { "game_id": 1448, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:blood_talisman": { "game_id": 1450, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:chaos_talisman": { "game_id": 1452, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:cosmic_talisman": { "game_id": 1454, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:death_talisman": { "game_id": 1456, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:law_talisman": { "game_id": 1458, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:soul_talisman": { "game_id": 1460, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:nature_talisman": { "game_id": 1462, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:elemental_talisman": { "game_id": 5516, "tradable": true, "stackable": false, "examine": "A mysterious power emanates from the talisman...", "weight": 0.015 }, "rs:air_tiara": { "game_id": 5523, "tradable": true, "stackable": false, "examine": "A tiara infused with the properties of air.", "weight": 1.0, "equippable": true, "equipment_data": { "equipment_slot": "head" } }, "rs:body_tiara": { "game_id": 5533, "tradable": true, "stackable": false, "examine": "A tiara infused with the properties of the body.", "weight": 1.0, "equippable": true, "equipment_data": { "equipment_slot": "head" } }, "rs:chaos_tiara": { "game_id": 5543, "tradable": true, "stackable": false, "examine": "A tiara infused with the properties of chaos.", "weight": 1.0, "equippable": true, "equipment_data": { "equipment_slot": "head" } }, "rs:cosmic_tiara": { "game_id": 5539, "tradable": true, "stackable": false, "examine": "A tiara infused with the properties of the cosmos.", "weight": 1.0, "equippable": true, "equipment_data": { "equipment_slot": "head" } }, "rs:death_tiara": { "game_id": 5547, "tradable": true, "stackable": false, "examine": "A tiara infused with the properties of death.", "weight": 1.0, "equippable": true, "equipment_data": { "equipment_slot": "head" } }, "rs:earth_tiara": { "game_id": 5535, "tradable": true, "stackable": false, "examine": "A tiara infused with the properties of the earth.", "weight": 1.0, "equippable": true, "equipment_data": { "equipment_slot": "head" } }, "rs:fire_tiara": { "game_id": 5537, "tradable": true, "stackable": false, "examine": "A tiara infused with the properties of fire.", "weight": 1.0, "equippable": true, "equipment_data": { "equipment_slot": "head" } }, "rs:law_tiara": { "game_id": 5545, "tradable": true, "stackable": false, "examine": "A tiara infused with the properties of law.", "weight": 1.0, "equippable": true, "equipment_data": { "equipment_slot": "head" } }, "rs:mind_tiara": { "game_id": 5529, "tradable": true, "stackable": false, "examine": "A tiara infused with the properties of the mind.", "weight": 1.0, "equippable": true, "equipment_data": { "equipment_slot": "head" } }, "rs:nature_tiara": { "game_id": 5541, "tradable": true, "stackable": false, "examine": "A tiara infused with the properties of nature.", "weight": 1.0, "equippable": true, "equipment_data": { "equipment_slot": "head" } }, "rs:water_tiara": { "game_id": 5531, "tradable": true, "stackable": false, "examine": "A tiara infused with the properties of water.", "weight": 1.0, "equippable": true, "equipment_data": { "equipment_slot": "head" } }, "rs:blood_tiara": { "game_id": 5549, "tradable": true, "stackable": false, "examine": "A tiara infused with the properties of blood.", "weight": 1.0, "equippable": true, "equipment_data": { "equipment_slot": "head" } }, "rs:soul_tiara": { "game_id": 5551, "tradable": true, "stackable": false, "examine": "A tiara infused with the properties of the soul.", "weight": 1.0, "equippable": true, "equipment_data": { "equipment_slot": "head" } }, "rs:small_pouch": { "game_id": 5509 }, "rs:medium_pouch": { "game_id": 5510 }, "rs:large_pouch": { "game_id": 5512 }, "rs:giant_pouch": { "game_id": 5514 } } ================================================ FILE: data/config/music/musicRegions.json ================================================ { "musicRegions": [ { "songId": 363, "songName": "7th Realm", "musicTabButtonId": 299, "regionIds": [10645, 10644] }, { "songId": 177, "songName": "Adventure", "musicTabButtonId": 25, "regionIds": [12854] }, { "songId": 50, "songName": "Al-Kharid", "musicTabButtonId": 26, "regionIds": [13105, 13361] }, { "songId": 73, "songName": "All's Fairy In Love'N'War", "musicTabButtonId": 443, "regionIds": [0] }, { "songId": 102, "songName": "Alone", "musicTabButtonId": 27, "regionIds": [12086, 10390, 10134] }, { "songId": 90, "songName": "Ambient Jungle", "musicTabButtonId": 28, "regionIds": [11310] }, { "songId": 305, "songName": "Anywhere", "musicTabButtonId": 276, "regionIds": [10795] }, { "songId": 123, "songName": "Arabian 2", "musicTabButtonId": 30, "regionIds": [13107] }, { "songId": 124, "songName": "Arabian 3", "musicTabButtonId": 31, "regionIds": [12848] }, { "songId": 36, "songName": "Arabian", "musicTabButtonId": 29, "regionIds": [13617, 13106] }, { "songId": 19, "songName": "Arabique", "musicTabButtonId": 32, "regionIds": [11417] }, { "songId": 160, "songName": "Army of Darkness", "musicTabButtonId": 33, "regionIds": [12088] }, { "songId": 186, "songName": "Arrival", "musicTabButtonId": 34, "regionIds": [11572] }, { "songId": 247, "songName": "Artistry", "musicTabButtonId": 245, "regionIds": [8010] }, { "songId": 24, "songName": "Attack 1", "musicTabButtonId": 35, "regionIds": [10034] }, { "songId": 25, "songName": "Attack 2", "musicTabButtonId": 36, "regionIds": [11414] }, { "songId": 26, "songName": "Attack 3", "musicTabButtonId": 37, "regionIds": [0] }, { "songId": 27, "songName": "Attack 4", "musicTabButtonId": 38, "regionIds": [10289] }, { "songId": 28, "songName": "Attack 5", "musicTabButtonId": 39, "regionIds": [9033] }, { "songId": 29, "songName": "Attack 6", "musicTabButtonId": 40, "regionIds": [10387] }, { "songId": 180, "songName": "Attention", "musicTabButtonId": 41, "regionIds": [11825] }, { "songId": 2, "songName": "Autumn Voyage", "musicTabButtonId": 42, "regionIds": [12851] }, { "songId": 497, "songName": "Aye Car Rum Ba", "musicTabButtonId": 373, "regionIds": [8527] }, { "songId": 248, "songName": "Aztec", "musicTabButtonId": 217, "regionIds": [11157, 11158, 11156, 10902] }, { "songId": 324, "songName": "Background", "musicTabButtonId": 43, "regionIds": [11060, 11316, 7758] }, { "songId": 152, "songName": "Ballad of Enchantment", "musicTabButtonId": 44, "regionIds": [10290] }, { "songId": 263, "songName": "Bandit Camp", "musicTabButtonId": 243, "regionIds": [12590] }, { "songId": 141, "songName": "Barbarianism", "musicTabButtonId": 253, "regionIds": [12341, 12441] }, { "songId": 345, "songName": "Barking Mad", "musicTabButtonId": 305, "regionIds": [14234] }, { "songId": 99, "songName": "Baroque", "musicTabButtonId": 45, "regionIds": [10547] }, { "songId": 100, "songName": "Beyond", "musicTabButtonId": 46, "regionIds": [11675, 11419, 11418] }, { "songId": 83, "songName": "Big Chords", "musicTabButtonId": 47, "regionIds": [10032, 11593] }, { "songId": 498, "songName": "Blistering Barnacles", "musicTabButtonId": 372, "regionIds": [8528] }, { "songId": 342, "songName": "Body Parts", "musicTabButtonId": 304, "regionIds": [13979, 14235] }, { "songId": 154, "songName": "Bone Dance", "musicTabButtonId": 248, "regionIds": [13619] }, { "songId": 266, "songName": "Bone Dry", "musicTabButtonId": 322, "regionIds": [12946, 13202] }, { "songId": 64, "songName": "Book of Spells", "musicTabButtonId": 48, "regionIds": [12593] }, { "songId": 291, "songName": "Borderland", "musicTabButtonId": 258, "regionIds": [10809, 10810] }, { "songId": 132, "songName": "Breeze", "musicTabButtonId": 229, "regionIds": [9010] }, { "songId": 471, "songName": "Brew Hoo Hoo", "musicTabButtonId": 357, "regionIds": [14747] }, { "songId": 194, "songName": "Brimstail's Scales", "musicTabButtonId": 451, "regionIds": [9625] }, { "songId": 489, "songName": "Bubble And Squeak", "musicTabButtonId": 380, "regionIds": [7753] }, { "songId": 545, "songName": "Cabin Fever", "musicTabButtonId": 400, "regionIds": [0] }, { "songId": 104, "songName": "Camelot", "musicTabButtonId": 49, "regionIds": [11062, 11063] }, { "songId": 314, "songName": "Castlewars", "musicTabButtonId": 285, "regionIds": [9520, 9620] }, { "songId": 481, "songName": "Catch Me If You Can", "musicTabButtonId": 379, "regionIds": [10646] }, { "songId": 325, "songName": "Cave Background", "musicTabButtonId": 50, "regionIds": [12185, 11929, 12184] }, { "songId": 357, "songName": "Cave of Beasts", "musicTabButtonId": 311, "regionIds": [11165] }, { "songId": 389, "songName": "Cave of The Goblins", "musicTabButtonId": 313, "regionIds": [12949, 12693] }, { "songId": 68, "songName": "Cavern", "musicTabButtonId": 51, "regionIds": [10388, 10389, 12193] }, { "songId": 330, "songName": "Cellar Song", "musicTabButtonId": 197, "regionIds": [12697] }, { "songId": 63, "songName": "Chain of Command", "musicTabButtonId": 52, "regionIds": [10905, 10651, 10648, 10649, 10650] }, { "songId": 282, "songName": "Chamber", "musicTabButtonId": 291, "regionIds": [11078, 10821] }, { "songId": 583, "songName": "Chef Surprise", "musicTabButtonId": 406, "regionIds": [0] }, { "songId": 575, "songName": "Chickened Out", "musicTabButtonId": 405, "regionIds": [9796] }, { "songId": 71, "songName": "Chompy Hunt", "musicTabButtonId": 202, "regionIds": [10542, 10642] }, { "songId": 383, "songName": "City of The Dead", "musicTabButtonId": 327, "regionIds": [12844, 12843, 13099] }, { "songId": 373, "songName": "Claustrophobia", "musicTabButtonId": 316, "regionIds": [9549, 9293] }, { "songId": 67, "songName": "Close Quarters", "musicTabButtonId": 199, "regionIds": [12602] }, { "songId": 269, "songName": "Competition", "musicTabButtonId": 254, "regionIds": [8781] }, { "songId": 142, "songName": "Complication", "musicTabButtonId": 270, "regionIds": [9035] }, { "songId": 258, "songName": "Contest", "musicTabButtonId": 222, "regionIds": [11576] }, { "songId": 418, "songName": "Corporal Punishment", "musicTabButtonId": 370, "regionIds": [12619] }, { "songId": 509, "songName": "Corridors of Power", "musicTabButtonId": 423, "regionIds": [0] }, { "songId": 178, "songName": "Courage", "musicTabButtonId": 256, "regionIds": [11673, 11674] }, { "songId": 259, "songName": "Crystal Castle", "musicTabButtonId": 231, "regionIds": [9011, 9012] }, { "songId": 181, "songName": "Crystal Cave", "musicTabButtonId": 53, "regionIds": [9797] }, { "songId": 169, "songName": "Crystal Sword", "musicTabButtonId": 54, "regionIds": [12855, 10647] }, { "songId": 59, "songName": "Cursed", "musicTabButtonId": 205, "regionIds": [9879, 9623] }, { "songId": 198, "songName": "Dagannoth Dawn", "musicTabButtonId": 374, "regionIds": [7748, 7236] }, { "songId": 560, "songName": "Dance of Death", "musicTabButtonId": 436, "regionIds": [0] }, { "songId": 380, "songName": "Dance of The Undead", "musicTabButtonId": 334, "regionIds": [14131] }, { "songId": 336, "songName": "Dangerous Road", "musicTabButtonId": 264, "regionIds": [11413] }, { "songId": 381, "songName": "Dangerous Way", "musicTabButtonId": 335, "regionIds": [14231] }, { "songId": 182, "songName": "Dangerous", "musicTabButtonId": 55, "regionIds": [12343, 13115, 13371] }, { "songId": 326, "songName": "Dark", "musicTabButtonId": 56, "regionIds": [13113, 13369] }, { "songId": 576, "songName": "Davy Jones Locker", "musicTabButtonId": 404, "regionIds": [11924] }, { "songId": 476, "songName": "Dead Can Dance", "musicTabButtonId": 359, "regionIds": [12601] }, { "songId": 84, "songName": "Dead Quiet", "musicTabButtonId": 216, "regionIds": [13621, 9294, 9550] }, { "songId": 288, "songName": "Deadlands", "musicTabButtonId": 246, "regionIds": [14134, 14390] }, { "songId": 278, "songName": "Deep Down", "musicTabButtonId": 290, "regionIds": [11079, 10822, 10823] }, { "songId": 37, "songName": "Deep Wildy", "musicTabButtonId": 57, "regionIds": [11835, 11836] }, { "songId": 465, "songName": "Desert Heat", "musicTabButtonId": 385, "regionIds": [13615, 13614] }, { "songId": 174, "songName": "Desert Voyage", "musicTabButtonId": 58, "regionIds": [13103, 13359, 13102] }, { "songId": 532, "songName": "Diango's Little Helpers", "musicTabButtonId": 392, "regionIds": [8005] }, { "songId": 86, "songName": "Dimension X", "musicTabButtonId": 442, "regionIds": [0] }, { "songId": 501, "songName": "Distant Land", "musicTabButtonId": 409, "regionIds": [13874, 14130, 13873, 14129] }, { "songId": 610, "songName": "Distillery Hilarity", "musicTabButtonId": 432, "regionIds": [0] }, { "songId": 537, "songName": "Dogs of War", "musicTabButtonId": 437, "regionIds": [0] }, { "songId": 56, "songName": "Doorways", "musicTabButtonId": 59, "regionIds": [12598] }, { "songId": 361, "songName": "Down Below", "musicTabButtonId": 326, "regionIds": [12439, 12438] }, { "songId": 143, "songName": "Down To Earth", "musicTabButtonId": 255, "regionIds": [10571] }, { "songId": 358, "songName": "Dragontooth Island", "musicTabButtonId": 309, "regionIds": [15159] }, { "songId": 327, "songName": "Dream", "musicTabButtonId": 60, "regionIds": [12594] }, { "songId": 623, "songName": "Dreamstate", "musicTabButtonId": 446, "regionIds": [0] }, { "songId": 47, "songName": "Duel Arena", "musicTabButtonId": 189, "regionIds": [13362] }, { "songId": 173, "songName": "Dunjun", "musicTabButtonId": 61, "regionIds": [11928, 11672] }, { "songId": 351, "songName": "Dynasty", "musicTabButtonId": 317, "regionIds": [13358] }, { "songId": 69, "songName": "Egypt", "musicTabButtonId": 62, "regionIds": [13104, 13360] }, { "songId": 252, "songName": "Elven Mist", "musicTabButtonId": 239, "regionIds": [9266] }, { "songId": 148, "songName": "Emotion", "musicTabButtonId": 63, "regionIds": [10033, 10133, 10309, 11081] }, { "songId": 138, "songName": "Emperor", "musicTabButtonId": 179, "regionIds": [11570, 11670] }, { "songId": 17, "songName": "Escape", "musicTabButtonId": 198, "regionIds": [10903] }, { "songId": 285, "songName": "Etceteria", "musicTabButtonId": 273, "regionIds": [10300] }, { "songId": 586, "songName": "Everlasting Fire", "musicTabButtonId": 418, "regionIds": [13373] }, { "songId": 268, "songName": "Everywhere", "musicTabButtonId": 236, "regionIds": [8499, 8755] }, { "songId": 411, "songName": "Evil Bob's Island", "musicTabButtonId": 358, "regionIds": [10058] }, { "songId": 106, "songName": "Expanse", "musicTabButtonId": 64, "regionIds": [12852, 12605, 12952] }, { "songId": 41, "songName": "Expecting", "musicTabButtonId": 65, "regionIds": [9522, 9778, 9878] }, { "songId": 153, "songName": "Expedition", "musicTabButtonId": 175, "regionIds": [11676, 9619] }, { "songId": 270, "songName": "Exposed", "musicTabButtonId": 232, "regionIds": [8752] }, { "songId": 118, "songName": "Faerie", "musicTabButtonId": 66, "regionIds": [9541, 9540] }, { "songId": 337, "songName": "Faithless", "musicTabButtonId": 265, "regionIds": [12856, 13112] }, { "songId": 72, "songName": "Fanfare", "musicTabButtonId": 67, "regionIds": [11828] }, { "songId": 166, "songName": "Fanfare2", "musicTabButtonId": 68, "regionIds": [11823] }, { "songId": 167, "songName": "Fanfare3", "musicTabButtonId": 69, "regionIds": [10545] }, { "songId": 504, "songName": "Fangs For The Memory", "musicTabButtonId": 410, "regionIds": [0] }, { "songId": 372, "songName": "Far Away", "musicTabButtonId": 348, "regionIds": [9265] }, { "songId": 602, "songName": "Fear And Loathing", "musicTabButtonId": 411, "regionIds": [0] }, { "songId": 344, "songName": "Fenkenstrain's Refrain", "musicTabButtonId": 303, "regionIds": [13879, 14135] }, { "songId": 375, "songName": "Fight Or Flight", "musicTabButtonId": 349, "regionIds": [8008, 7752] }, { "songId": 312, "songName": "Find My Way", "musicTabButtonId": 284, "regionIds": [10894, 11150] }, { "songId": 463, "songName": "Fire And Brimstone", "musicTabButtonId": 365, "regionIds": [9552] }, { "songId": 119, "songName": "Fishing", "musicTabButtonId": 70, "regionIds": [11317] }, { "songId": 163, "songName": "Flute Salad", "musicTabButtonId": 71, "regionIds": [12595] }, { "songId": 558, "songName": "Food For Thought", "musicTabButtonId": 438, "regionIds": [0] }, { "songId": 121, "songName": "Forbidden", "musicTabButtonId": 204, "regionIds": [13111, 13367] }, { "songId": 251, "songName": "Forest", "musicTabButtonId": 238, "regionIds": [9009] }, { "songId": 98, "songName": "Forever", "musicTabButtonId": 72, "regionIds": [12342, 12444, 12443, 12442] }, { "songId": 436, "songName": "Forgettable Melody", "musicTabButtonId": 350, "regionIds": [7501] }, { "songId": 378, "songName": "Forgotten", "musicTabButtonId": 320, "regionIds": [10828] }, { "songId": 409, "songName": "Frogland", "musicTabButtonId": 355, "regionIds": [9802] }, { "songId": 294, "songName": "Frostbite", "musicTabButtonId": 325, "regionIds": [11323, 11579] }, { "songId": 347, "songName": "Fruits De Mer", "musicTabButtonId": 272, "regionIds": [11059] }, { "songId": 603, "songName": "Funny Bunnies", "musicTabButtonId": 415, "regionIds": [9810] }, { "songId": 159, "songName": "Gaol", "musicTabButtonId": 73, "regionIds": [10031, 12090, 10131] }, { "songId": 125, "songName": "Garden", "musicTabButtonId": 74, "regionIds": [12853, 12953] }, { "songId": 22, "songName": "Gnome King", "musicTabButtonId": 75, "regionIds": [9782, 9783] }, { "songId": 150, "songName": "Gnome Theme", "musicTabButtonId": 76, "regionIds": [12085] }, { "songId": 33, "songName": "Gnome Village", "musicTabButtonId": 77, "regionIds": [9781] }, { "songId": 101, "songName": "Gnome Village2", "musicTabButtonId": 78, "regionIds": [9269, 9525] }, { "songId": 23, "songName": "Gnome", "musicTabButtonId": 79, "regionIds": [11830] }, { "songId": 112, "songName": "Gnomeball", "musicTabButtonId": 80, "regionIds": [9270, 9526, 9271, 9527] }, { "songId": 346, "songName": "Goblin Game", "musicTabButtonId": 271, "regionIds": [10393] }, { "songId": 535, "songName": "Golden Touch", "musicTabButtonId": 394, "regionIds": [0] }, { "songId": 116, "songName": "Greatness", "musicTabButtonId": 81, "regionIds": [12596] }, { "songId": 520, "songName": "Grip of The Talon", "musicTabButtonId": 377, "regionIds": [0] }, { "songId": 246, "songName": "Grotto", "musicTabButtonId": 212, "regionIds": [13720] }, { "songId": 466, "songName": "Ground Scape", "musicTabButtonId": 347, "regionIds": [0] }, { "songId": 128, "songName": "Grumpy", "musicTabButtonId": 203, "regionIds": [10286] }, { "songId": 638, "songName": "Ham Fisted", "musicTabButtonId": 430, "regionIds": [0] }, { "songId": 76, "songName": "Harmony", "musicTabButtonId": 82, "regionIds": [12850, 7507] }, { "songId": 46, "songName": "Harmony2", "musicTabButtonId": 192, "regionIds": [12950] }, { "songId": 277, "songName": "Haunted Mine", "musicTabButtonId": 288, "regionIds": [11077] }, { "songId": 434, "songName": "Have A Blast", "musicTabButtonId": 362, "regionIds": [7757] }, { "songId": 612, "songName": "Head To Head", "musicTabButtonId": 421, "regionIds": [0] }, { "songId": 190, "songName": "Heart And Mind", "musicTabButtonId": 200, "regionIds": [10059] }, { "songId": 4, "songName": "Hells Bells", "musicTabButtonId": 293, "regionIds": [11066, 11067] }, { "songId": 97, "songName": "Hermit", "musicTabButtonId": 220, "regionIds": [9034] }, { "songId": 55, "songName": "High Seas", "musicTabButtonId": 83, "regionIds": [11057] }, { "songId": 205, "songName": "High Spirits", "musicTabButtonId": 459, "regionIds": [0] }, { "songId": 454, "songName": "Home Sweet Home", "musicTabButtonId": 428, "regionIds": [0] }, { "songId": 621, "songName": "HomeScape", "musicTabButtonId": 427, "regionIds": [0] }, { "songId": 18, "songName": "Horizon", "musicTabButtonId": 84, "regionIds": [11573] }, { "songId": 384, "songName": "Hypnotized", "musicTabButtonId": 330, "regionIds": [0] }, { "songId": 1, "songName": "Iban", "musicTabButtonId": 85, "regionIds": [8519, 8520, 8521] }, { "songId": 87, "songName": "Ice Melody", "musicTabButtonId": 190, "regionIds": [11318] }, { "songId": 370, "songName": "In Between", "musicTabButtonId": 315, "regionIds": [10317, 10061] }, { "songId": 530, "songName": "In The Brine", "musicTabButtonId": 401, "regionIds": [14638] }, { "songId": 511, "songName": "In The Clink", "musicTabButtonId": 397, "regionIds": [8261] }, { "songId": 188, "songName": "In The Manor", "musicTabButtonId": 86, "regionIds": [10287] }, { "songId": 469, "songName": "In The Pits", "musicTabButtonId": 366, "regionIds": [9808] }, { "songId": 519, "songName": "Incantation", "musicTabButtonId": 378, "regionIds": [0] }, { "songId": 260, "songName": "Insect Queen", "musicTabButtonId": 228, "regionIds": [13972, 14228] }, { "songId": 96, "songName": "Inspiration", "musicTabButtonId": 87, "regionIds": [12087] }, { "songId": 412, "songName": "Into The Abyss", "musicTabButtonId": 343, "regionIds": [12108, 12107] }, { "songId": 95, "songName": "Intrepid", "musicTabButtonId": 88, "regionIds": [9369, 9370] }, { "songId": 306, "songName": "Island Life", "musicTabButtonId": 280, "regionIds": [10794, 11050] }, { "songId": 627, "songName": "Isle of Everywhere", "musicTabButtonId": 448, "regionIds": [0] }, { "songId": 6, "songName": "Jolly-R", "musicTabButtonId": 89, "regionIds": [11058] }, { "songId": 172, "songName": "Jungle Island", "musicTabButtonId": 90, "regionIds": [11309, 11313] }, { "songId": 479, "songName": "Jungle Troubles", "musicTabButtonId": 363, "regionIds": [11568, 10055] }, { "songId": 114, "songName": "Jungly 1", "musicTabButtonId": 91, "regionIds": [11054, 11154] }, { "songId": 115, "songName": "Jungly 2", "musicTabButtonId": 92, "regionIds": [10802] }, { "songId": 117, "songName": "Jungly 3", "musicTabButtonId": 93, "regionIds": [11055] }, { "songId": 362, "songName": "Karamja Jam", "musicTabButtonId": 221, "regionIds": [10900, 10899] }, { "songId": 9, "songName": "Kingdom", "musicTabButtonId": 298, "regionIds": [11319] }, { "songId": 191, "songName": "Knightly", "musicTabButtonId": 94, "regionIds": [10291] }, { "songId": 134, "songName": "La Mort", "musicTabButtonId": 369, "regionIds": [8779] }, { "songId": 287, "songName": "Lair", "musicTabButtonId": 247, "regionIds": [13975] }, { "songId": 197, "songName": "Lament of Meiyerditch", "musicTabButtonId": 452, "regionIds": [0] }, { "songId": 542, "songName": "Lament", "musicTabButtonId": 399, "regionIds": [12433] }, { "songId": 506, "songName": "Land Down Under", "musicTabButtonId": 424, "regionIds": [0] }, { "songId": 396, "songName": "Land of the Dwarves", "musicTabButtonId": 342, "regionIds": [11423] }, { "songId": 164, "songName": "Landlubber", "musicTabButtonId": 194, "regionIds": [10801] }, { "songId": 546, "songName": "Last Stand", "musicTabButtonId": 419, "regionIds": [0] }, { "songId": 60, "songName": "Lasting", "musicTabButtonId": 95, "regionIds": [10549] }, { "songId": 293, "songName": "Legend", "musicTabButtonId": 241, "regionIds": [10808, 11064] }, { "songId": 66, "songName": "Legion", "musicTabButtonId": 96, "regionIds": [10039, 10295, 12089] }, { "songId": 631, "songName": "Life's A Beach!", "musicTabButtonId": 433, "regionIds": [0] }, { "songId": 320, "songName": "Lighthouse", "musicTabButtonId": 267, "regionIds": [10040, 9799] }, { "songId": 113, "songName": "Lightness", "musicTabButtonId": 97, "regionIds": [12599] }, { "songId": 74, "songName": "Lightwalk", "musicTabButtonId": 98, "regionIds": [11061] }, { "songId": 632, "songName": "Little Cave of Horrors", "musicTabButtonId": 434, "regionIds": [0] }, { "songId": 168, "songName": "Lonesome", "musicTabButtonId": 187, "regionIds": [13203] }, { "songId": 161, "songName": "Long Ago", "musicTabButtonId": 99, "regionIds": [10544] }, { "songId": 12, "songName": "Long Way Home", "musicTabButtonId": 100, "regionIds": [11826] }, { "songId": 253, "songName": "Lost Soul", "musicTabButtonId": 233, "regionIds": [9008, 9264] }, { "songId": 20, "songName": "Lullaby", "musicTabButtonId": 102, "regionIds": [13365, 10551] }, { "songId": 264, "songName": "Mad Eadgar", "musicTabButtonId": 225, "regionIds": [11677] }, { "songId": 13, "songName": "Mage Arena", "musicTabButtonId": 103, "regionIds": [12348, 12349, 10057] }, { "songId": 185, "songName": "Magic Dance", "musicTabButtonId": 104, "regionIds": [10288] }, { "songId": 184, "songName": "Magical Journey", "musicTabButtonId": 105, "regionIds": [10805] }, { "songId": 544, "songName": "Making Waves", "musicTabButtonId": 420, "regionIds": [9272, 9528, 9273] }, { "songId": 559, "songName": "Malady", "musicTabButtonId": 439, "regionIds": [0] }, { "songId": 328, "songName": "March", "musicTabButtonId": 101, "regionIds": [10036] }, { "songId": 304, "songName": "Marooned", "musicTabButtonId": 279, "regionIds": [11562, 12117] }, { "songId": 261, "songName": "Marzipan", "musicTabButtonId": 226, "regionIds": [11421, 11167, 11166] }, { "songId": 340, "songName": "Masquerade", "musicTabButtonId": 300, "regionIds": [10908] }, { "songId": 577, "songName": "Mastermindless", "musicTabButtonId": 407, "regionIds": [0] }, { "songId": 156, "songName": "Mausoleum", "musicTabButtonId": 209, "regionIds": [13722] }, { "songId": 508, "songName": "Meddling Kids", "musicTabButtonId": 425, "regionIds": [0] }, { "songId": 157, "songName": "Medieval", "musicTabButtonId": 106, "regionIds": [13109] }, { "songId": 193, "songName": "Mellow", "musicTabButtonId": 107, "regionIds": [10293] }, { "songId": 317, "songName": "Melodrama", "musicTabButtonId": 286, "regionIds": [9776] }, { "songId": 254, "songName": "Meridian", "musicTabButtonId": 234, "regionIds": [8497, 8753, 9287] }, { "songId": 600, "songName": "Method of Madness", "musicTabButtonId": 412, "regionIds": [0] }, { "songId": 107, "songName": "Miles Away", "musicTabButtonId": 108, "regionIds": [11571, 10569] }, { "songId": 534, "songName": "Mind Over Matter", "musicTabButtonId": 395, "regionIds": [0] }, { "songId": 65, "songName": "Miracle Dance", "musicTabButtonId": 109, "regionIds": [11083] }, { "songId": 388, "songName": "Mirage", "musicTabButtonId": 331, "regionIds": [13199] }, { "songId": 284, "songName": "Miscellania", "musicTabButtonId": 274, "regionIds": [10044] }, { "songId": 200, "songName": "The Mollusc Menace", "musicTabButtonId": 455, "regionIds": [0] }, { "songId": 21, "songName": "Monarch Waltz", "musicTabButtonId": 110, "regionIds": [10807] }, { "songId": 303, "songName": "Monkey Madness", "musicTabButtonId": 278, "regionIds": [11051] }, { "songId": 343, "songName": "Monster Melee", "musicTabButtonId": 310, "regionIds": [12694] }, { "songId": 10, "songName": "Moody", "musicTabButtonId": 111, "regionIds": [9523, 9779, 12600] }, { "songId": 48, "songName": "Morytania", "musicTabButtonId": 210, "regionIds": [13622] }, { "songId": 515, "songName": "Mudskipper Melody", "musicTabButtonId": 371, "regionIds": [11824] }, { "songId": 203, "songName": "My Arm's Journey", "musicTabButtonId": 457, "regionIds": [0] }, { "songId": 348, "songName": "Narnode's Theme", "musicTabButtonId": 275, "regionIds": [9882] }, { "songId": 245, "songName": "Natural", "musicTabButtonId": 213, "regionIds": [13620, 9038] }, { "songId": 155, "songName": "Neverland", "musicTabButtonId": 112, "regionIds": [9780] }, { "songId": 62, "songName": "Newbie Melody", "musicTabButtonId": 113, "regionIds": [12080, 12336, 12592, 12079, 12335] }, { "songId": 646, "songName": "Night of The Vampyre", "musicTabButtonId": 454, "regionIds": [0] }, { "songId": 127, "songName": "Nightfall", "musicTabButtonId": 114, "regionIds": [11827, 12861] }, { "songId": 594, "songName": "No Way Out", "musicTabButtonId": 413, "regionIds": [0] }, { "songId": 58, "songName": "Nomad", "musicTabButtonId": 201, "regionIds": [11056] }, { "songId": 587, "songName": "Null And Void", "musicTabButtonId": 416, "regionIds": [10537] }, { "songId": 633, "songName": "On The Wing", "musicTabButtonId": 440, "regionIds": [0] }, { "songId": 103, "songName": "Oriental", "musicTabButtonId": 115, "regionIds": [11666, 11668] }, { "songId": 322, "songName": "Out of The Deep", "musicTabButtonId": 268, "regionIds": [10140, 10056] }, { "songId": 447, "songName": "Over To Nardah", "musicTabButtonId": 387, "regionIds": [13613] }, { "songId": 256, "songName": "Overpass", "musicTabButtonId": 237, "regionIds": [9267] }, { "songId": 7, "songName": "Overture", "musicTabButtonId": 116, "regionIds": [10806] }, { "songId": 93, "songName": "Parade", "musicTabButtonId": 117, "regionIds": [13110, 13366] }, { "songId": 393, "songName": "Path of Peril", "musicTabButtonId": 323, "regionIds": [10575, 10831] }, { "songId": 364, "songName": "Pathways", "musicTabButtonId": 297, "regionIds": [10901] }, { "songId": 588, "songName": "Pest Control", "musicTabButtonId": 417, "regionIds": [10536] }, { "songId": 505, "songName": "Pharaoh's Tomb", "musicTabButtonId": 398, "regionIds": [13356, 12105] }, { "songId": 354, "songName": "Phasmatys", "musicTabButtonId": 307, "regionIds": [14746] }, { "songId": 419, "songName": "Pheasant Peasant", "musicTabButtonId": 354, "regionIds": [10314] }, { "songId": 614, "songName": "Pinball Wizard", "musicTabButtonId": 422, "regionIds": [0] }, { "songId": 149, "songName": "Principality", "musicTabButtonId": 219, "regionIds": [11575] }, { "songId": 334, "songName": "Pirates of Peril", "musicTabButtonId": 263, "regionIds": [12093] }, { "songId": 158, "songName": "Quest", "musicTabButtonId": 118, "regionIds": [10315] }, { "songId": 482, "songName": "Rat A Tat Tat", "musicTabButtonId": 382, "regionIds": [11599] }, { "songId": 491, "songName": "Rat Hunt", "musicTabButtonId": 383, "regionIds": [11343] }, { "songId": 318, "songName": "Ready For Battle", "musicTabButtonId": 287, "regionIds": [0] }, { "songId": 329, "songName": "Regal", "musicTabButtonId": 119, "regionIds": [13117] }, { "songId": 78, "songName": "Reggae", "musicTabButtonId": 120, "regionIds": [11565, 11821] }, { "songId": 89, "songName": "Reggae2", "musicTabButtonId": 121, "regionIds": [11567] }, { "songId": 289, "songName": "Rellekka", "musicTabButtonId": 259, "regionIds": [10297, 10553, 10554] }, { "songId": 44, "songName": "Right On Track", "musicTabButtonId": 351, "regionIds": [7501] }, { "songId": 262, "songName": "Righteousness", "musicTabButtonId": 227, "regionIds": [9803] }, { "songId": 91, "songName": "Riverside", "musicTabButtonId": 122, "regionIds": [8496, 10803] }, { "songId": 204, "songName": "Roc and Roll", "musicTabButtonId": 458, "regionIds": [0] }, { "songId": 533, "songName": "Roll The Bones", "musicTabButtonId": 396, "regionIds": [0] }, { "songId": 335, "songName": "Romancing The Crone", "musicTabButtonId": 294, "regionIds": [11068] }, { "songId": 390, "songName": "Romper Chomper", "musicTabButtonId": 338, "regionIds": [9263, 9519] }, { "songId": 53, "songName": "Royale", "musicTabButtonId": 123, "regionIds": [11671] }, { "songId": 57, "songName": "Rune Essence", "musicTabButtonId": 124, "regionIds": [11595] }, { "songId": 5, "songName": "Sad Meadow", "musicTabButtonId": 125, "regionIds": [10035] }, { "songId": 290, "songName": "Saga", "musicTabButtonId": 240, "regionIds": [10296, 10552] }, { "songId": 359, "songName": "Sarcophagus", "musicTabButtonId": 324, "regionIds": [12945] }, { "songId": 490, "songName": "Sarim's Vermin", "musicTabButtonId": 384, "regionIds": [11926] }, { "songId": 144, "songName": "Scape Cave", "musicTabButtonId": 126, "regionIds": [12436, 12698, 13210, 12954] }, { "songId": 0, "songName": "Scape Main", "musicTabButtonId": 318, "regionIds": [0] }, { "songId": 400, "songName": "Scape Original", "musicTabButtonId": 127, "regionIds": [0] }, { "songId": 331, "songName": "Scape Sad", "musicTabButtonId": 128, "regionIds": [13116, 13372] }, { "songId": 547, "songName": "Scape Santa", "musicTabButtonId": 292, "regionIds": [0] }, { "songId": 321, "songName": "Scape Scared", "musicTabButtonId": 262, "regionIds": [0] }, { "songId": 54, "songName": "Scape Soft", "musicTabButtonId": 184, "regionIds": [11829] }, { "songId": 332, "songName": "Scape Wild", "musicTabButtonId": 129, "regionIds": [12857, 12604, 12860] }, { "songId": 352, "songName": "Scarab", "musicTabButtonId": 328, "regionIds": [12589, 12845, 13101, 11085, 11341, 11597] }, { "songId": 35, "songName": "Sea Shanty 2", "musicTabButtonId": 131, "regionIds": [12082] }, { "songId": 92, "songName": "Sea Shanty", "musicTabButtonId": 130, "regionIds": [11569] }, { "songId": 110, "songName": "Serenade", "musicTabButtonId": 132, "regionIds": [9521, 9777] }, { "songId": 52, "songName": "Serene", "musicTabButtonId": 133, "regionIds": [11936, 11937, 11339, 11837] }, { "songId": 356, "songName": "Settlement", "musicTabButtonId": 312, "regionIds": [11065] }, { "songId": 286, "songName": "Shadowland", "musicTabButtonId": 249, "regionIds": [13618, 13875, 8526] }, { "songId": 122, "songName": "Shine", "musicTabButtonId": 134, "regionIds": [13363] }, { "songId": 120, "songName": "Shining", "musicTabButtonId": 186, "regionIds": [12858] }, { "songId": 353, "songName": "Shipwrecked", "musicTabButtonId": 306, "regionIds": [14391] }, { "songId": 311, "songName": "Showdown", "musicTabButtonId": 283, "regionIds": [10895] }, { "songId": 640, "songName": "Sigmund's Showdown", "musicTabButtonId": 431, "regionIds": [0] }, { "songId": 257, "songName": "Sojourn", "musicTabButtonId": 224, "regionIds": [11321, 11577] }, { "songId": 80, "songName": "Soundscape", "musicTabButtonId": 135, "regionIds": [9774, 10030] }, { "songId": 387, "songName": "Sphinx", "musicTabButtonId": 329, "regionIds": [13100] }, { "songId": 175, "songName": "Spirit", "musicTabButtonId": 136, "regionIds": [12597] }, { "songId": 462, "songName": "Spirits of The Elid", "musicTabButtonId": 388, "regionIds": [13461] }, { "songId": 77, "songName": "Splendour", "musicTabButtonId": 137, "regionIds": [11574] }, { "songId": 129, "songName": "Spooky Jungle", "musicTabButtonId": 139, "regionIds": [11053] }, { "songId": 333, "songName": "Spooky", "musicTabButtonId": 138, "regionIds": [12340] }, { "songId": 11, "songName": "Spooky2", "musicTabButtonId": 289, "regionIds": [13718, 13974] }, { "songId": 241, "songName": "Stagnant", "musicTabButtonId": 215, "regionIds": [13876, 8782] }, { "songId": 108, "songName": "Starlight", "musicTabButtonId": 140, "regionIds": [12181, 11925] }, { "songId": 151, "songName": "Start", "musicTabButtonId": 172, "regionIds": [12339] }, { "songId": 111, "songName": "Still Night", "musicTabButtonId": 141, "regionIds": [13108] }, { "songId": 319, "songName": "Stillness", "musicTabButtonId": 296, "regionIds": [13977] }, { "songId": 292, "songName": "Stranded", "musicTabButtonId": 295, "regionIds": [11322, 11578] }, { "songId": 470, "songName": "Strange Place", "musicTabButtonId": 364, "regionIds": [7494] }, { "songId": 243, "songName": "Stratosphere", "musicTabButtonId": 207, "regionIds": [8523] }, { "songId": 568, "songName": "Storm Brew", "musicTabButtonId": 402, "regionIds": [10577] }, { "songId": 517, "songName": "Subterranea", "musicTabButtonId": 375, "regionIds": [10142, 10398] }, { "songId": 267, "songName": "Sunburn", "musicTabButtonId": 242, "regionIds": [12846, 13357] }, { "songId": 265, "songName": "Superstition", "musicTabButtonId": 257, "regionIds": [11153] }, { "songId": 308, "songName": "Suspicious", "musicTabButtonId": 282, "regionIds": [10567, 10311] }, { "songId": 395, "songName": "Tale of Keldagrim", "musicTabButtonId": 341, "regionIds": [11678, 11679] }, { "songId": 140, "songName": "Talking Forest", "musicTabButtonId": 173, "regionIds": [10550] }, { "songId": 397, "songName": "Tears of Guthix", "musicTabButtonId": 332, "regionIds": [12948] }, { "songId": 296, "songName": "Technology", "musicTabButtonId": 277, "regionIds": [10566, 10310, 9626] }, { "songId": 376, "songName": "Temple of Light", "musicTabButtonId": 368, "regionIds": [7496] }, { "songId": 307, "songName": "Temple", "musicTabButtonId": 281, "regionIds": [11151] }, { "songId": 478, "songName": "The Cellar Dwellers", "musicTabButtonId": 361, "regionIds": [10135, 10391] }, { "songId": 425, "songName": "The Chosen", "musicTabButtonId": 346, "regionIds": [9805] }, { "songId": 79, "songName": "The Desert", "musicTabButtonId": 142, "regionIds": [12591, 12847] }, { "songId": 461, "songName": "The Desolate Isle", "musicTabButtonId": 352, "regionIds": [10042] }, { "songId": 541, "songName": "The Enchanter", "musicTabButtonId": 393, "regionIds": [13462] }, { "songId": 403, "songName": "The Far Side", "musicTabButtonId": 345, "regionIds": [12111] }, { "songId": 630, "songName": "The Galleon", "musicTabButtonId": 450, "regionIds": [0] }, { "songId": 464, "songName": "The Genie", "musicTabButtonId": 386, "regionIds": [13457] }, { "songId": 377, "songName": "The Golem", "musicTabButtonId": 319, "regionIds": [13616, 13872] }, { "songId": 643, "songName": "The Last Shanty", "musicTabButtonId": 453, "regionIds": [0] }, { "songId": 407, "songName": "The Lost Melody", "musicTabButtonId": 339, "regionIds": [13206] }, { "songId": 420, "songName": "The Lost Tribe", "musicTabButtonId": 340, "regionIds": [0] }, { "songId": 625, "songName": "The Lunar Isle", "musicTabButtonId": 447, "regionIds": [0] }, { "songId": 573, "songName": "The Mad Mole", "musicTabButtonId": 403, "regionIds": [6992, 6993] }, { "songId": 448, "songName": "The Monsters Below", "musicTabButtonId": 353, "regionIds": [9886] }, { "songId": 316, "songName": "The Navigator", "musicTabButtonId": 261, "regionIds": [0] }, { "songId": 485, "songName": "The Noble Rodent", "musicTabButtonId": 381, "regionIds": [0] }, { "songId": 355, "songName": "The Other Side", "musicTabButtonId": 308, "regionIds": [14646, 14647] }, { "songId": 398, "songName": "The Power of Tears", "musicTabButtonId": 333, "regionIds": [0] }, { "songId": 202, "songName": "Prime Time", "musicTabButtonId": 456, "regionIds": [0] }, { "songId": 413, "songName": "The Quizmaster", "musicTabButtonId": 356, "regionIds": [7754] }, { "songId": 402, "songName": "The Rogues Den", "musicTabButtonId": 344, "regionIds": [11855, 11854, 12110, 12109] }, { "songId": 170, "songName": "The Shadow", "musicTabButtonId": 143, "regionIds": [11314, 11315] }, { "songId": 341, "songName": "The Slayer", "musicTabButtonId": 302, "regionIds": [11164] }, { "songId": 510, "songName": "Slither and Thither", "musicTabButtonId": 426, "regionIds": [0] }, { "songId": 201, "songName": "Slug a bug Ball", "musicTabButtonId": 266, "regionIds": [0] }, { "songId": 339, "songName": "The Terrible Tower", "musicTabButtonId": 301, "regionIds": [13623] }, { "songId": 133, "songName": "The Tower", "musicTabButtonId": 144, "regionIds": [10292, 10136, 10392] }, { "songId": 109, "songName": "Theme", "musicTabButtonId": 174, "regionIds": [10294, 10138, 10394] }, { "songId": 379, "songName": "Throne of The Demon", "musicTabButtonId": 321, "regionIds": [0] }, { "songId": 242, "songName": "Time Out", "musicTabButtonId": 218, "regionIds": [11591] }, { "songId": 369, "songName": "Time To Mine", "musicTabButtonId": 314, "regionIds": [11422] }, { "songId": 338, "songName": "Tiptoe", "musicTabButtonId": 269, "regionIds": [12440] }, { "songId": 525, "songName": "Title Fight", "musicTabButtonId": 389, "regionIds": [12696] }, { "songId": 591, "songName": "Tomb Raider", "musicTabButtonId": 444, "regionIds": [0] }, { "songId": 105, "songName": "Tomorrow", "musicTabButtonId": 188, "regionIds": [12081] }, { "songId": 582, "songName": "Too Many Cooks...", "musicTabButtonId": 408, "regionIds": [11930] }, { "songId": 51, "songName": "Trawler Minor", "musicTabButtonId": 146, "regionIds": [7755, 8011] }, { "songId": 38, "songName": "Trawler", "musicTabButtonId": 145, "regionIds": [7499, 8012] }, { "songId": 130, "songName": "Tree Spirits", "musicTabButtonId": 147, "regionIds": [9268, 9524] }, { "songId": 187, "songName": "Tremble", "musicTabButtonId": 223, "regionIds": [11320] }, { "songId": 94, "songName": "Tribal 2", "musicTabButtonId": 150, "regionIds": [11566, 11822] }, { "songId": 162, "songName": "Tribal Background", "musicTabButtonId": 148, "regionIds": [11312, 11412] }, { "songId": 165, "songName": "Tribal", "musicTabButtonId": 149, "regionIds": [11311] }, { "songId": 192, "songName": "Trinity", "musicTabButtonId": 151, "regionIds": [10804, 10904] }, { "songId": 611, "songName": "Trouble Brewing", "musicTabButtonId": 435, "regionIds": [0] }, { "songId": 183, "songName": "Troubled", "musicTabButtonId": 152, "regionIds": [11833] }, { "songId": 88, "songName": "Twilight", "musicTabButtonId": 208, "regionIds": [10906] }, { "songId": 473, "songName": "Tzhaar!", "musicTabButtonId": 367, "regionIds": [9551] }, { "songId": 176, "songName": "Undercurrent", "musicTabButtonId": 195, "regionIds": [12345] }, { "songId": 179, "songName": "Underground", "musicTabButtonId": 153, "regionIds": [13368, 11416] }, { "songId": 323, "songName": "Underground Pass", "musicTabButtonId": 157, "regionIds": [9622, 9621] }, { "songId": 131, "songName": "Understanding", "musicTabButtonId": 206, "regionIds": [9547] }, { "songId": 3, "songName": "Unknown Land", "musicTabButtonId": 156, "regionIds": [12338, 8524] }, { "songId": 70, "songName": "Upcoming", "musicTabButtonId": 158, "regionIds": [10546] }, { "songId": 75, "songName": "Venture", "musicTabButtonId": 159, "regionIds": [13364] }, { "songId": 45, "songName": "Venture2", "musicTabButtonId": 193, "regionIds": [13465, 13464] }, { "songId": 528, "songName": "Victory Is Mine", "musicTabButtonId": 390, "regionIds": [0] }, { "songId": 61, "songName": "Village", "musicTabButtonId": 211, "regionIds": [13878] }, { "songId": 85, "songName": "Vision", "musicTabButtonId": 160, "regionIds": [12337, 12437] }, { "songId": 30, "songName": "Voodoo Cult", "musicTabButtonId": 161, "regionIds": [11665, 9545] }, { "songId": 32, "songName": "Voyage", "musicTabButtonId": 162, "regionIds": [10038] }, { "songId": 622, "songName": "Waking Dream", "musicTabButtonId": 445, "regionIds": [0] }, { "songId": 49, "songName": "Wander", "musicTabButtonId": 163, "regionIds": [12083] }, { "songId": 295, "songName": "Warrior", "musicTabButtonId": 260, "regionIds": [10653] }, { "songId": 634, "songName": "Warriors' Guild", "musicTabButtonId": 429, "regionIds": [0] }, { "songId": 82, "songName": "Waterfall", "musicTabButtonId": 164, "regionIds": [10037, 10137] }, { "songId": 244, "songName": "Waterlogged", "musicTabButtonId": 214, "regionIds": [13877, 14133, 8014, 8270] }, { "songId": 626, "songName": "Way of The Enchanter", "musicTabButtonId": 449, "regionIds": [0] }, { "songId": 394, "songName": "Wayward", "musicTabButtonId": 336, "regionIds": [9875] }, { "songId": 126, "songName": "We Are The Fairies", "musicTabButtonId": 441, "regionIds": [0] }, { "songId": 271, "songName": "Well of Voyage", "musicTabButtonId": 230, "regionIds": [9366] }, { "songId": 475, "songName": "Wild Side", "musicTabButtonId": 360, "regionIds": [12092, 12348] }, { "songId": 435, "songName": "Wilderness", "musicTabButtonId": 165, "regionIds": [11832, 12346] }, { "songId": 42, "songName": "Wilderness2", "musicTabButtonId": 166, "regionIds": [12091, 12347] }, { "songId": 43, "songName": "Wilderness3", "musicTabButtonId": 167, "regionIds": [11834] }, { "songId": 8, "songName": "Wildwood", "musicTabButtonId": 250, "regionIds": [12344] }, { "songId": 14, "songName": "Witching", "musicTabButtonId": 168, "regionIds": [13114, 13370] }, { "songId": 529, "songName": "Woe of The Wyvern", "musicTabButtonId": 391, "regionIds": [0] }, { "songId": 189, "songName": "Wolf Mountain", "musicTabButtonId": 191, "regionIds": [12603, 12859] }, { "songId": 34, "songName": "Wonder", "musicTabButtonId": 169, "regionIds": [11831] }, { "songId": 81, "songName": "Wonderous", "musicTabButtonId": 170, "regionIds": [10548] }, { "songId": 255, "songName": "Woodland", "musicTabButtonId": 235, "regionIds": [8498, 8754] }, { "songId": 15, "songName": "Workshop", "musicTabButtonId": 171, "regionIds": [12084] }, { "songId": 565, "songName": "Wrath And Ruin", "musicTabButtonId": 414, "regionIds": [0] }, { "songId": 524, "songName": "Xenophobe", "musicTabButtonId": 376, "regionIds": [7492, 11589] }, { "songId": 145, "songName": "Yesteryear", "musicTabButtonId": 185, "regionIds": [12849] }, { "songId": 146, "songName": "Zealot", "musicTabButtonId": 196, "regionIds": [10827] }, { "songId": 392, "songName": "Zogre Dance", "musicTabButtonId": 337, "regionIds": [9775] } ] } ================================================ FILE: data/config/npc-spawns/alkharid/alkharid-general.json ================================================ [ { "npc": "rs:alkharid_gem_trader", "spawn_x": 3288, "spawn_y": 3212, "movement_radius": 1 }, { "npc": "rs:alkharid_dommik", "spawn_x": 3320, "spawn_y": 3194, "movement_radius": 3 }, { "npc": "rs:alkharid_louie", "spawn_x": 3317, "spawn_y": 3174, "movement_radius": 3 }, { "npc": "rs:alkharid_ranael", "spawn_x": 3315, "spawn_y": 3161, "movement_radius": 3 }, { "npc": "rs:alkharid_karim", "spawn_x": 3272, "spawn_y": 3182, "movement_radius": 2 } ] ================================================ FILE: data/config/npc-spawns/ardougne/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 2657, "spawn_y": 3286, "movement_radius": 0 }, { "npc": "rs:banker", "spawn_x": 2657, "spawn_y": 3283, "movement_radius": 0 }, { "npc": "rs:banker", "spawn_x": 2657, "spawn_y": 3280, "movement_radius": 0 }, { "npc": "rs:banker", "spawn_x": 2619, "spawn_y": 3330, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 2618, "spawn_y": 3330, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 2615, "spawn_y": 3330, "movement_radius": 0, "face": "NORTH" } ] ================================================ FILE: data/config/npc-spawns/ardougne/guards.json ================================================ [ { "npc": "rs:guard:2", "spawn_x": 2635, "spawn_y": 3339, "movement_radius": 5, "face": "NORTH" }, { "npc": "rs:guard:2", "spawn_x": 2636, "spawn_y": 3340, "movement_radius": 5, "face": "NORTH" }, { "npc": "rs:guard:2", "spawn_x": 2637, "spawn_y": 3339, "movement_radius": 5, "face": "NORTH" }, { "npc": "rs:guard:2", "spawn_x": 2651, "spawn_y": 3307, "movement_radius": 10, "face": "NORTH" }, { "npc": "rs:guard:2", "spawn_x": 2659, "spawn_y": 3309, "movement_radius": 10, "face": "WEST" }, { "npc": "rs:guard:2", "spawn_x": 2661, "spawn_y": 3309, "movement_radius": 10, "face": "NORTH" }, { "npc": "rs:guard:2", "spawn_x": 2663, "spawn_y": 3301, "movement_radius": 10, "face": "NORTH" }, { "npc": "rs:guard:2", "spawn_x": 2665, "spawn_y": 3300, "movement_radius": 10, "face": "NORTH" }, { "npc": "rs:guard:2", "spawn_x": 2664, "spawn_y": 3318, "movement_radius": 10, "face": "NORTH" } ] ================================================ FILE: data/config/npc-spawns/ardougne/market.json ================================================ [ { "npc": "rs:ardougne_baker:0", "spawn_x": 2654, "spawn_y": 3311, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:ardougne_baker:1", "spawn_x": 2669, "spawn_y": 3310, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:knight:0", "spawn_x": 2671, "spawn_y": 3313, "movement_radius": 7, "face": "NORTH" }, { "npc": "rs:knight:0", "spawn_x": 2652, "spawn_y": 3318, "movement_radius": 7, "face": "NORTH" }, { "npc": "rs:knight:0", "spawn_x": 2669, "spawn_y": 3298, "movement_radius": 7, "face": "NORTH" }, { "npc": "rs:knight:0", "spawn_x": 2653, "spawn_y": 3300, "movement_radius": 7, "face": "NORTH" }, { "npc": "rs:hero", "spawn_x": 2667, "spawn_y": 3316, "movement_radius": 7, "face": "NORTH" }, { "npc": "rs:hero", "spawn_x": 2647, "spawn_y": 3306, "movement_radius": 7, "face": "NORTH" }, { "npc": "rs:hero", "spawn_x": 2630, "spawn_y": 3288, "movement_radius": 7, "face": "NORTH" }, { "npc": "rs:silver_merchant", "spawn_x": 2658, "spawn_y": 3316, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:spice_seller", "spawn_x": 2658, "spawn_y": 3296, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:fur_trader", "spawn_x": 2666, "spawn_y": 3295, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:gem_merchant", "spawn_x": 2669, "spawn_y": 3303, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:silk_merchant", "spawn_x": 2656, "spawn_y": 3301, "movement_radius": 0, "face": "WEST" } ] ================================================ FILE: data/config/npc-spawns/camelot/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 2807, "spawn_y": 3443, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:banker", "spawn_x": 2809, "spawn_y": 3443, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:banker", "spawn_x": 2810, "spawn_y": 3443, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:banker", "spawn_x": 2811, "spawn_y": 3443, "movement_radius": 0, "face": "SOUTH" } ] ================================================ FILE: data/config/npc-spawns/canifis/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 3514, "spawn_y": 3482, "movement_radius": 0, "face": "WEST" }, { "npc": "rs:banker", "spawn_x": 3514, "spawn_y": 3481, "movement_radius": 0, "face": "WEST" }, { "npc": "rs:banker", "spawn_x": 3514, "spawn_y": 3480, "movement_radius": 0, "face": "WEST" }, { "npc": "rs:banker", "spawn_x": 3514, "spawn_y": 3479, "movement_radius": 0, "face": "WEST" } ] ================================================ FILE: data/config/npc-spawns/catherby/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 2729, "spawn_y": 3495, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:banker", "spawn_x": 2728, "spawn_y": 3495, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:banker", "spawn_x": 2727, "spawn_y": 3495, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:banker", "spawn_x": 2724, "spawn_y": 3495, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:banker", "spawn_x": 2722, "spawn_y": 3495, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:banker", "spawn_x": 2721, "spawn_y": 3495, "movement_radius": 0, "face": "SOUTH" } ] ================================================ FILE: data/config/npc-spawns/draynor/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 3090, "spawn_y": 3242, "movement_radius": 0, "face": "EAST" }, { "npc": "rs:banker", "spawn_x": 3090, "spawn_y": 3243, "movement_radius": 0, "face": "EAST" }, { "npc": "rs:banker", "spawn_x": 3090, "spawn_y": 3245, "movement_radius": 0, "face": "EAST" } ] ================================================ FILE: data/config/npc-spawns/edgeville/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 3096, "spawn_y": 3491, "movement_radius": 0, "face": "WEST" }, { "npc": "rs:banker", "spawn_x": 3096, "spawn_y": 3489, "movement_radius": 0, "face": "WEST" }, { "npc": "rs:banker", "spawn_x": 3096, "spawn_y": 3492, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 3098, "spawn_y": 3492, "movement_radius": 0, "face": "NORTH" } ] ================================================ FILE: data/config/npc-spawns/falador/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 2947, "spawn_y": 3366, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 2946, "spawn_y": 3366, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 2948, "spawn_y": 3366, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 2949, "spawn_y": 3366, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 2945, "spawn_y": 3366, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 3015, "spawn_y": 3353, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 3014, "spawn_y": 3353, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 3013, "spawn_y": 3353, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 3012, "spawn_y": 3353, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 3011, "spawn_y": 3353, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 3010, "spawn_y": 3353, "movement_radius": 0, "face": "NORTH" } ] ================================================ FILE: data/config/npc-spawns/falador/guards.json ================================================ [ { "npc": "rs:guard:0", "spawn_x": 2967, "spawn_y": 3388, "movement_radius": 5, "face": "NORTH" }, { "npc": "rs:guard:0", "spawn_x": 2965, "spawn_y": 3383, "movement_radius": 5, "face": "NORTH" }, { "npc": "rs:guard:1", "spawn_x": 2968, "spawn_y": 3377, "movement_radius": 5, "face": "NORTH" } ] ================================================ FILE: data/config/npc-spawns/keldagrim/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 2836, "spawn_y": 10205, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 2838, "spawn_y": 10205, "movement_radius": 0, "face": "NORTH" } ] ================================================ FILE: data/config/npc-spawns/lumbridge/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 3209, "spawn_y": 3222, "spawn_level": 2, "movement_radius": 0, "face": "SOUTH" }, { "npc": "rs:banker", "spawn_x": 3210, "spawn_y": 3222, "spawn_level": 2, "movement_radius": 0, "face": "SOUTH" } ] ================================================ FILE: data/config/npc-spawns/lumbridge/lumbridge-general.json ================================================ [ { "npc": "rs:hans", "spawn_x": 3222, "spawn_y": 3222, "movement_radius": 8 }, { "npc": "rs:man", "spawn_x": 3222, "spawn_y": 3218, "movement_radius": 12 }, { "npc": "rs:lumbridge_bob", "spawn_x": 3230, "spawn_y": 3203, "movement_radius": 1 }, { "npc": "rs:lumbridge_shop_keeper", "spawn_x": 3211, "spawn_y": 3247, "movement_radius": 1 }, { "npc": "rs:gillie_groats", "spawn_x": 3253, "spawn_y": 3274, "movement_radius": 1 }, { "npc": "rs:millie_miller", "spawn_x": 3168, "spawn_y": 3306, "movement_radius": 3 }, { "npc": "rs:lumbridge_castle_cook", "spawn_x": 3210, "spawn_y": 3215, "movement_radius": 4 }, { "npc": "rs:runescape_guide", "spawn_x": 3230, "spawn_y": 3238, "movement_radius": 1 } ] ================================================ FILE: data/config/npc-spawns/lumbridge/lumbridge-goblins.json ================================================ [ { "npc": "rs:goblin", "spawn_x": 3263, "spawn_y": 3232, "movement_radius": 8 }, { "npc": "rs:goblin", "spawn_x": 3262, "spawn_y": 3235, "movement_radius": 8 }, { "npc": "rs:goblin", "spawn_x": 3259, "spawn_y": 3225, "movement_radius": 8 }, { "npc": "rs:goblin", "spawn_x": 3254, "spawn_y": 3231, "movement_radius": 8 }, { "npc": "rs:goblin", "spawn_x": 3249, "spawn_y": 3229, "movement_radius": 8 }, { "npc": "rs:goblin", "spawn_x": 3251, "spawn_y": 3237, "movement_radius": 8 }, { "npc": "rs:goblin", "spawn_x": 3246, "spawn_y": 3238, "movement_radius": 8 }, { "npc": "rs:goblin", "spawn_x": 3252, "spawn_y": 3243, "movement_radius": 8 }, { "npc": "rs:goblin", "spawn_x": 3263, "spawn_y": 3239, "movement_radius": 8 }, { "npc": "rs:goblin", "spawn_x": 3259, "spawn_y": 3235, "movement_radius": 8 }, { "npc": "rs:goblin", "spawn_x": 3257, "spawn_y": 3245, "movement_radius": 8 }, { "npc": "rs:goblin", "spawn_x": 3252, "spawn_y": 3250, "movement_radius": 8 } ] ================================================ FILE: data/config/npc-spawns/lumbridge/lumbridge-sheep.json ================================================ [ { "npc": "rs:spy_penguin_sheep", "spawn_x": 3197, "spawn_y": 3262, "movement_radius": 10 }, { "npc": "rs:sheep", "spawn_x": 3199, "spawn_y": 3267, "movement_radius": 10 }, { "npc": "rs:sheep", "spawn_x": 3203, "spawn_y": 3271, "movement_radius": 10 }, { "npc": "rs:sheep", "spawn_x": 3199, "spawn_y": 3273, "movement_radius": 10 }, { "npc": "rs:sheep", "spawn_x": 3208, "spawn_y": 3273, "movement_radius": 10 }, { "npc": "rs:sheep", "spawn_x": 3209, "spawn_y": 3260, "movement_radius": 10 } ] ================================================ FILE: data/config/npc-spawns/magebank/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 2534, "spawn_y": 4713, "movement_radius": 0, "face": "WEST" } ] ================================================ FILE: data/config/npc-spawns/nardah/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 3425, "spawn_y": 2889, "movement_radius": 0, "face": "EAST" }, { "npc": "rs:banker", "spawn_x": 3425, "spawn_y": 2891, "movement_radius": 0, "face": "EAST" }, { "npc": "rs:banker", "spawn_x": 3425, "spawn_y": 2893, "movement_radius": 0, "face": "EAST" }, { "npc": "rs:banker", "spawn_x": 3425, "spawn_y": 2894, "movement_radius": 0, "face": "EAST" } ] ================================================ FILE: data/config/npc-spawns/pestcontrol/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 2665, "spawn_y": 2651, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 2666, "spawn_y": 2651, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 2667, "spawn_y": 2651, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 2668, "spawn_y": 2651, "movement_radius": 0, "face": "NORTH" } ] ================================================ FILE: data/config/npc-spawns/portsarim/port-sarim-general.json ================================================ [ { "npc": "rs:betty", "spawn_x": 3012, "spawn_y": 3259, "movement_radius": 5 }, { "npc": "rs:Giant_rat", "spawn_level": 0, "spawn_x": 2995, "spawn_y": 3191, "movement_radius": 6, "face": "WEST" }, { "npc": "rs:Giant_rat_2", "spawn_level": 0, "spawn_x": 2999, "spawn_y": 3194, "movement_radius": 6, "face": "WEST" } ] ================================================ FILE: data/config/npc-spawns/rimmington/rimmington.json ================================================ [ { "npc": "rs:hetty", "spawn_x": 2968, "spawn_y": 3205, "spawn_level": 0, "movement_radius": 4, "face": "SOUTH" }, { "npc": "rs:rat", "spawn_x": 2953, "spawn_y": 3204, "spawn_level": 0, "movement_radius": 4, "face": "EAST" }, { "npc": "rs:rat", "spawn_x": 2955, "spawn_y": 3202, "spawn_level": 0, "movement_radius": 4, "face": "NORTH" }, { "npc": "rs:rat", "spawn_x": 2959, "spawn_y": 3204, "spawn_level": 0, "movement_radius": 4, "face": "SOUTH" }, { "npc": "rs:rat", "spawn_x": 2958, "spawn_y": 3202, "spawn_level": 0, "movement_radius": 4, "face": "WEST" } ] ================================================ FILE: data/config/npc-spawns/shilovillage/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 2852, "spawn_y": 2955, "movement_radius": 0, "face": "WEST" } ] ================================================ FILE: data/config/npc-spawns/varrock/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 3252, "spawn_y": 3418, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 3253, "spawn_y": 3418, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 3254, "spawn_y": 3418, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 3256, "spawn_y": 3418, "movement_radius": 0, "face": "NORTH" }, { "npc": "rs:banker", "spawn_x": 3187, "spawn_y": 3436, "movement_radius": 0, "face": "WEST" }, { "npc": "rs:banker", "spawn_x": 3187, "spawn_y": 3438, "movement_radius": 0, "face": "WEST" }, { "npc": "rs:banker", "spawn_x": 3187, "spawn_y": 3440, "movement_radius": 0, "face": "WEST" }, { "npc": "rs:banker", "spawn_x": 3187, "spawn_y": 3442, "movement_radius": 0, "face": "WEST" }, { "npc": "rs:banker", "spawn_x": 3187, "spawn_y": 3444, "movement_radius": 0, "face": "WEST" }, { "npc": "rs:banker", "spawn_x": 3187, "spawn_y": 3446, "movement_radius": 0, "face": "WEST" } ] ================================================ FILE: data/config/npc-spawns/varrock/blue-moon-inn.json ================================================ [ { "npc": "rs:barbarian_woman", "spawn_x": 3224, "spawn_y": 3402, "movement_radius": 1 }, { "npc": "rs:blue_moon_inn_bartender", "spawn_x": 3226, "spawn_y": 3399, "movement_radius": 2 }, { "npc": "rs:blue_moon_inn_charlie", "spawn_x": 3230, "spawn_y": 3402, "movement_radius": 1 }, { "npc": "rs:jonny_the_beard", "spawn_x": 3225, "spawn_y": 3395, "movement_radius": 5 }, { "npc": "rs:dr_harlow", "spawn_x": 3222, "spawn_y": 3397, "movement_radius": 3 } ] ================================================ FILE: data/config/npc-spawns/varrock/varrock-general.json ================================================ [ { "npc": "rs:varrock_zaff", "spawn_x": 3203, "spawn_y": 3432, "movement_radius": 2 }, { "npc": "rs:varrock_horvic", "spawn_x": 3229, "spawn_y": 3437, "movement_radius": 2 }, { "npc": "rs:varrock_wilough", "spawn_x": 3221, "spawn_y": 3437, "movement_radius": 2 }, { "npc": "rs:varrock_shilop", "spawn_x": 3220, "spawn_y": 3432, "movement_radius": 2 }, { "npc": "rs:varrock_baraek", "spawn_x": 3220, "spawn_y": 3433, "movement_radius": 1 }, { "npc": "rs:romeo", "spawn_x": 3211, "spawn_y": 3424, "movement_radius": 4 }, { "npc": "rs:varrock_shop_keeper", "spawn_x": 3214, "spawn_y": 3417, "movement_radius": 2 }, { "npc": "rs:varrock_shop_assistant", "spawn_x": 3214, "spawn_y": 3417, "movement_radius": 2 }, { "npc": "rs:master_smithing_tutor", "spawn_x": 3187, "spawn_y": 3423, "movement_radius": 3 } ] ================================================ FILE: data/config/npc-spawns/yanille/bankers.json ================================================ [ { "npc": "rs:banker", "spawn_x": 2615, "spawn_y": 3091, "movement_radius": 0, "face": "WEST" }, { "npc": "rs:banker", "spawn_x": 2615, "spawn_y": 3092, "movement_radius": 0, "face": "WEST" }, { "npc": "rs:banker", "spawn_x": 2615, "spawn_y": 3094, "movement_radius": 0, "face": "WEST" } ] ================================================ FILE: data/config/npcs/alkharid.json ================================================ { "rs:alkharid_gem_trader": { "game_id": 540 }, "rs:alkharid_dommik": { "game_id": 545 }, "rs:alkharid_louie": { "game_id": 542 }, "rs:alkharid_ranael": { "game_id": 544 }, "rs:alkharid_karim": { "game_id": 543 } } ================================================ FILE: data/config/npcs/ardougne.json ================================================ { "rs:ardougne_baker": { "variations": [ { "suffix": 0, "game_id": 571 }, { "suffix": 1, "game_id": 571 } ] }, "rs:knight": { "variations": [ { "suffix": 0, "game_id": 23 }, { "suffix": 1, "game_id": 26 } ] }, "rs:hero": { "game_id": 21 }, "rs:silk_merchant": { "game_id": 574 }, "rs:fur_trader": { "game_id": 573 }, "rs:spice_seller": { "game_id": 572 }, "rs:gem_merchant": { "game_id": 570 }, "rs:silver_merchant": { "game_id": 569 } } ================================================ FILE: data/config/npcs/bankers.json ================================================ { "rs:banker": { "game_id": 494 } } ================================================ FILE: data/config/npcs/barbarians.json ================================================ { "rs:barbarian_woman": { "game_id": 17 } } ================================================ FILE: data/config/npcs/general.json ================================================ { "rs:rat": { "game_id": 47, "drop_table": [ { "itemKey": "rs:rats_tail", "frequency": "always", "amount": 1, "amountMax": 1, "questRequirement": { "questId": "rs:witchs_potion", "stage": 50 } }, { "itemKey": "rs:bones", "frequency": "always", "amount": 1 } ], "skills": { "hitpoints": 1 } } } ================================================ FILE: data/config/npcs/generic-humans.json ================================================ { "presets": { "rs:human_male": { "killable": true, "respawn_time": 3, "skills": { "hitpoints": 7 }, "offensive_stats": { "speed": 4 }, "defensive_stats": { "stab": -21, "slash": -21, "crush": -21, "magic": -21, "ranged": -21 }, "animations": { "attack": 422, "defend": 424, "death": 512 }, "drop_table": [ { "itemKey": "rs:bones", "frequency": "always", "amount": 1 }, { "itemKey": "rs:bronze_dagger", "frequency": "1/128", "amount": 1 } ] } }, "rs:man": { "extends": "rs:human_male", "game_id": 1, "variations": [ { "suffix": "0", "game_id": 2 }, { "suffix": "1", "game_id": 3 } ] } } ================================================ FILE: data/config/npcs/goblins.json ================================================ { "presets": { "rs:goblin_base_l2": { "killable": true, "respawn_time": 3, "skills": { "hitpoints": 5 }, "offensive_stats": { "speed": 4, "attack": -21, "strength": -15, "magic": 0, "magic_strength": 0, "ranged": 0, "ranged_strength": 0 }, "defensive_stats": { "stab": -15, "slash": -15, "crush": -15, "magic": -15, "ranged": -15 }, "animations": { "attack": [310, 309], "defend": 312, "death": 313 }, "drop_table": [{ "itemKey": "rs:bones", "frequency": "always", "amount": 1 }] } }, "rs:goblin": { "extends": "rs:goblin_base_l2", "game_id": 100, "metadata": { "level": 2 } }, "rs:goblin_with_spear": { "extends": "rs:goblin_base_l2", "game_id": 101, "metadata": { "level": 5 } }, "rs:goblin_with_helmet": { "extends": "rs:goblin_base_l2", "game_id": 102, "metadata": { "level": 13 } }, "rs:goblin_green_with_sprear": { "extends": "rs:goblin_base_l2", "game_id": 298, "metadata": { "level": 5 } }, "rs:goblin_red_with_sprear": { "extends": "rs:goblin_base_l2", "game_id": 299, "metadata": { "level": 5 } }, "rs:goblin_red_with_shield": { "extends": "rs:goblin_base_l2", "game_id": 444, "metadata": { "level": 5 } }, "rs:goblin_green_with_shield": { "extends": "rs:goblin_base_l2", "game_id": 445, "metadata": { "level": 5 } }, "rs:goblin_guard": { "extends": "rs:goblin_base_l2", "game_id": 489, "metadata": { "level": 42 } } } ================================================ FILE: data/config/npcs/guards.json ================================================ { "rs:guard": { "variations": [ { "suffix": 0, "game_id": 9 }, { "suffix": 1, "game_id": 10 }, { "suffix": 2, "game_id": 32 }, { "suffix": 3, "game_id": 206 }, { "suffix": 4, "game_id": 344 }, { "suffix": 5, "game_id": 345 }, { "suffix": 6, "game_id": 346 }, { "suffix": 7, "game_id": 368 } ] } } ================================================ FILE: data/config/npcs/lumbridge.json ================================================ { "rs:hans": { "game_id": 0, "skills": { "hitpoints": 10, "herblore": 99 } }, "rs:runescape_guide": { "game_id": 945 }, "rs:melee_combat_tutor": { "game_id": 705 }, "rs:lumbridge_shop_keeper": { "game_id": 520 }, "rs:lumbridge_bob": { "game_id": 519 }, "rs:gillie_groats": { "game_id": 3807 }, "rs:millie_miller": { "game_id": 3806 }, "rs:lumbridge_castle_cook": { "game_id": 278 } } ================================================ FILE: data/config/npcs/port-sarim.json ================================================ { "rs:betty": { "game_id": 583 }, "rs:Giant_rat": { "game_id": 86, "drop_table": [ { "itemKey": "rs:bones", "frequency": "always", "amount": 1 }, { "itemKey": "rs:raw_rat_meat", "frequency": "always", "amount": 1 } ] }, "rs:Giant_rat_2": { "game_id": 4924, "drop_table": [ { "itemKey": "rs:bones", "frequency": "always", "amount": 1 }, { "itemKey": "rs:raw_rat_meat", "frequency": "always", "amount": 1 } ] } } ================================================ FILE: data/config/npcs/rimmington.json ================================================ { "rs:hetty": { "game_id": 307 } } ================================================ FILE: data/config/npcs/sheep.json ================================================ { "rs:spy_penguin_sheep": { "game_id": 3579 }, "rs:sheep": { "game_id": 43 }, "rs:naked_sheep": { "game_id": 42 } } ================================================ FILE: data/config/npcs/varrock.json ================================================ { "rs:varrock_zaff": { "game_id": 546 }, "rs:blue_moon_inn_bartender": { "game_id": 734 }, "rs:blue_moon_inn_charlie": { "game_id": 794 }, "rs:jonny_the_beard": { "game_id": 645 }, "rs:dr_harlow": { "game_id": 756 }, "rs:varrock_horvic": { "game_id": 549 }, "rs:varrock_wilough": { "game_id": 783 }, "rs:varrock_shilop": { "game_id": 781 }, "rs:varrock_baraek": { "game_id": 547 }, "rs:romeo": { "game_id": 639 }, "rs:varrock_shop_keeper": { "game_id": 520 }, "rs:varrock_shop_assistant": { "game_id": 521 }, "rs:master_smithing_tutor": { "game_id": 4905 } } ================================================ FILE: data/config/scenery-spawns.yaml ================================================ ================================================ FILE: data/config/shops/alkharid/alkharid-gem-trader.json ================================================ { "rs:alkharid_gem_trader": { "name": "Gem Trader", "shop_sell_rate": 1.0, "shop_buy_rate": 0.7, "rate_modifier": 0.03, "stock": [ { "itemKey": "rs:uncut_sapphire", "amount": 1, "restock": 25000 }, { "itemKey": "rs:uncut_emerald", "amount": 1, "restock": 40000 }, { "itemKey": "rs:uncut_ruby", "amount": 0, "restock": 2000 }, { "itemKey": "rs:uncut_diamond", "amount": 0, "restock": 4000 }, { "itemKey": "rs:sapphire", "amount": 1, "restock": 15000 }, { "itemKey": "rs:emerald", "amount": 1, "restock": 35000 }, { "itemKey": "rs:ruby", "amount": 0, "restock": 2000 }, { "itemKey": "rs:diamond", "amount": 0, "restock": 4000 } ] } } ================================================ FILE: data/config/shops/alkharid/dommiks-crafting-store.json ================================================ { "rs:dommiks_crafting_store": { "name": "Dommik's Crafting Store", "shop_sell_rate": 1.0, "shop_buy_rate": 0.65, "rate_modifier": 0.02, "stock": [ { "itemKey": "rs:chisel", "amount": 2, "restock": 100 }, { "itemKey": "rs:ring_mould", "amount": 4, "restock": 100 }, { "itemKey": "rs:necklace_mould", "amount": 2, "restock": 100 }, { "itemKey": "rs:amulet_mould", "amount": 2, "restock": 100 }, { "itemKey": "rs:needle", "amount": 3, "restock": 100 }, { "itemKey": "rs:thread", "amount": 100, "restock": 5 }, { "itemKey": "rs:holy_mould", "amount": 3, "restock": 100 }, { "itemKey": "rs:sickle_mould", "amount": 6, "restock": 15 }, { "itemKey": "rs:tiara_mould", "amount": 10, "restock": 10 } ] } } ================================================ FILE: data/config/shops/alkharid/louies-armored-legs.json ================================================ { "rs:louies_armored_legs": { "name": "Louie's Armoured Legs Bazaar", "shop_sell_rate": 1.0, "shop_buy_rate": 0.65, "rate_modifier": 0.01, "stock": [ { "itemKey": "rs:bronze_platelegs", "amount": 5, "restock": 100 }, { "itemKey": "rs:iron_platelegs", "amount": 3, "restock": 400 }, { "itemKey": "rs:steel_platelegs", "amount": 2, "restock": 900 }, { "itemKey": "rs:black_platelegs", "amount": 1, "restock": 1200 }, { "itemKey": "rs:mithril_platelegs", "amount": 1, "restock": 2000 }, { "itemKey": "rs:adamant_platelegs", "amount": 1, "restock": 13000 } ] } } ================================================ FILE: data/config/shops/alkharid/ranaels-skirt-store.json ================================================ { "rs:ranaels_skirt_store": { "name": "Ranael's Super Skirt Store", "shop_sell_rate": 1.0, "shop_buy_rate": 0.65, "rate_modifier": 0.01, "stock": [ { "itemKey": "rs:bronze_plateskirt", "amount": 5, "restock": 100 }, { "itemKey": "rs:iron_plateskirt", "amount": 3, "restock": 400 }, { "itemKey": "rs:steel_plateskirt", "amount": 2, "restock": 900 }, { "itemKey": "rs:black_plateskirt", "amount": 1, "restock": 1200 }, { "itemKey": "rs:mithril_plateskirt", "amount": 1, "restock": 2000 }, { "itemKey": "rs:adamant_plateskirt", "amount": 1, "restock": 13000 } ] } } ================================================ FILE: data/config/shops/lumbridge/bobs-axes.json ================================================ { "rs:lumbridge_bobs_axes": { "name": "Bob's Brilliant Axes.", "shop_sell_rate": 1.0, "shop_buy_rate": 0.6, "rate_modifier": 0.02, "stock": [ { "itemKey": "rs:bronze_pickaxe", "amount": 5, "restock": 100 }, { "itemKey": "rs:bronze_axe", "amount": 10, "restock": 100 }, { "itemKey": "rs:iron_axe", "amount": 5, "restock": 200 }, { "itemKey": "rs:steel_axe", "amount": 3, "restock": 400 }, { "itemKey": "rs:iron_battleaxe", "amount": 5, "restock": 100 }, { "itemKey": "rs:steel_battleaxe", "amount": 2, "restock": 200 }, { "itemKey": "rs:mithril_battleaxe", "amount": 1, "restock": 3000 } ] } } ================================================ FILE: data/config/shops/lumbridge/lumbridge-general-store.json ================================================ { "rs:lumbridge_general_store": { "name": "Lumbridge General Store", "general_store": true, "shop_sell_rate": 1.3, "shop_buy_rate": 0.4, "rate_modifier": 0.03, "stock": [ { "itemKey": "rs:pot", "amount": 5, "restock": 10 }, { "itemKey": "rs:jug", "amount": 2, "restock": 100 }, { "itemKey": "rs:shears", "amount": 2, "restock": 100 }, { "itemKey": "rs:knife", "amount": 5, "restock": 100 }, { "itemKey": "rs:bucket", "amount": 3, "restock": 10 }, { "itemKey": "rs:bowl", "amount": 2, "restock": 50 }, { "itemKey": "rs:cake_tin", "amount": 2, "restock": 50 }, { "itemKey": "rs:tinderbox", "amount": 2, "restock": 100 }, { "itemKey": "rs:chisel", "amount": 2, "restock": 100 }, { "itemKey": "rs:spade", "amount": 5, "restock": 100 }, { "itemKey": "rs:hammer", "amount": 5, "restock": 100 } ] } } ================================================ FILE: data/config/shops/portsarim/bettys-magic-emporium.json ================================================ { "rs:bettys_magic_emporium": { "name": "Betty's Magic Emporium", "shop_sell_rate": 1.0, "shop_buy_rate": 0.6, "rate_modifier": 0.001, "stock": [ { "itemKey": "rs:fire_rune", "amount": 5000, "restock": 10 }, { "itemKey": "rs:water_rune", "amount": 5000, "restock": 10 }, { "itemKey": "rs:air_rune", "amount": 5000, "restock": 10 }, { "itemKey": "rs:earth_rune", "amount": 5000, "restock": 10 }, { "itemKey": "rs:mind_rune", "amount": 5000, "restock": 10 }, { "itemKey": "rs:body_rune", "amount": 5000, "restock": 10 }, { "itemKey": "rs:chaos_rune", "amount": 250, "restock": 10 }, { "itemKey": "rs:death_rune", "amount": 250, "restock": 15 }, { "itemKey": "rs:eye_of_newt", "amount": 300, "restock": 10 }, { "itemKey": "rs:wizard_hat_(black)", "amount": 1, "restock": 100 }, { "itemKey": "rs:wizard_hat_(blue)", "amount": 1, "restock": 100 } ] } } ================================================ FILE: data/config/shops/shilo-village/oblis-general-store.json ================================================ { "rs:oblis-general-store": { "name": "Obli's General Store", "general_store": true, "shop_sell_rate": 1.5, "rate_modifier": 0.02, "shop_buy_rate": 0.5, "stock": [ { "itemKey": "rs:tinderbox", "amount": 2, "restock": 500 }, { "itemKey": "rs:vial", "amount": 10, "restock": 500 }, { "itemKey": "rs:pestle_and_mortar", "amount": 3, "restock": 500 }, { "itemKey": "rs:pot", "amount": 3, "restock": 500 }, { "itemKey": "rs:bronze_axe", "amount": 3, "restock": 500 }, { "itemKey": "rs:bronze_pickaxe", "amount": 2, "restock": 500 }, { "itemKey": "rs:iron_axe", "amount": 5, "restock": 700 }, { "itemKey": "rs:leather_body", "amount": 12, "restock": 500 }, { "itemKey": "rs:leather_gloves", "amount": 10, "restock": 500 }, { "itemKey": "rs:leather_boots", "amount": 10, "restock": 500 }, { "itemKey": "rs:cooked_meat", "amount": 2, "restock": 500 }, { "itemKey": "rs:bread", "amount": 10, "restock": 500 }, { "itemKey": "rs:bronze_bar", "amount": 10, "restock": 500 }, { "itemKey": "rs:spade", "amount": 10, "restock": 500 }, { "itemKey": "rs:candle", "amount": 10, "restock": 500 }, { "itemKey": "rs:unlit_torch", "amount": 10, "restock": 500 }, { "itemKey": "rs:chisel", "amount": 10, "restock": 500 }, { "itemKey": "rs:hammer", "amount": 10, "restock": 500 }, { "itemKey": "rs:papyrus", "amount": 50, "restock": 200 }, { "itemKey": "rs:charcoal", "amount": 50, "restock": 200 }, { "itemKey": "rs:vial:water", "amount": 50, "restock": 200 }, { "itemKey": "rs:machete", "amount": 50, "restock": 200 }, { "itemKey": "rs:rope", "amount": 10, "restock": 200 } ] } } ================================================ FILE: data/config/shops/varrock/zaffs-staffs.json ================================================ { "rs:zaffs_superior_staffs": { "name": "Zaff's Superior Staffs", "shop_sell_rate": 1.0, "shop_buy_rate": 0.55, "rate_modifier": 0.02, "stock": [ { "itemKey": "rs:battlestaff", "amount": 5, "restock": 100 }, { "itemKey": "rs:staff", "amount": 5, "restock": 100 }, { "itemKey": "rs:magic_staff", "amount": 5, "restock": 200 }, { "itemKey": "rs:staff_of_air", "amount": 2, "restock": 1000 }, { "itemKey": "rs:staff_of_water", "amount": 2, "restock": 1000 }, { "itemKey": "rs:staff_of_earth", "amount": 2, "restock": 1000 }, { "itemKey": "rs:staff_of_fire", "amount": 2, "restock": 1000 } ] } } ================================================ FILE: data/config/travel-locations-data.yaml ================================================ - name: Abandoned Mine x: 3441 y: 3236 z: 0 - name: Agility Arena x: 2809 y: 3191 z: 0 - name: Agility Pyramid x: 3364 y: 2840 z: 0 - name: Agility Training Area 1 x: 2481 y: 3424 z: 0 - name: Agility Training Area 2 x: 2533 y: 3538 z: 0 - name: Agility Training Area 3 x: 2998 y: 3952 z: 0 - name: Al Kharid x: 3293 y: 3151 z: 0 - name: Ape Atoll x: 2747 y: 2751 z: 0 - name: Arandar x: 2342 y: 3294 z: 0 - name: Ardougne Sewers x: 2567 y: 9682 z: 0 - name: Ardougne Sewers Mine x: 2655 y: 9677 z: 0 - name: Ardougne Zoo x: 2612 y: 3275 z: 0 - name: Asgarnian Ice Dungeon x: 3038 y: 9580 z: 0 - name: Ah Za Rhoon x: 2908 y: 9336 z: 0 - name: Bandit Camp x: 3037 y: 3699 z: 0 - name: Bandit Camp x: 3171 y: 2979 z: 0 - name: Barbarian Outpost x: 2552 y: 3561 z: 0 - name: Barbarian Village x: 3080 y: 3419 z: 0 - name: Barrows x: 3564 y: 3288 z: 0 - name: Barrows Crypt x: 3551 y: 9695 z: 0 - name: Battlefield x: 2520 y: 3232 z: 0 - name: Baxtorian Falls x: 2513 y: 3461 z: 0 - name: Bear x: 3285 y: 3838 z: 0 - name: Bedabin Camp x: 3169 y: 3036 z: 0 - name: Beehives x: 2759 y: 3442 z: 0 - name: Black Knights' Fortress x: 3025 y: 3514 z: 0 - name: Bone Yard x: 3236 y: 3746 z: 0 - name: Brimhaven x: 2773 y: 3176 z: 0 - name: Brimhaven Dungeon x: 2668 y: 9520 z: 0 - name: Burgh de Rott x: 3495 y: 3218 z: 0 - name: Burthorpe x: 2893 y: 3541 z: 0 - name: Cairn Isle x: 2765 y: 2976 z: 0 - name: Cairn Island Dungeon x: 2764 y: 9376 z: 0 - name: Camelot Castle x: 2758 y: 3507 z: 0 - name: Canifis x: 3495 y: 3487 z: 0 - name: Castle Wars x: 2430 y: 3104 z: 0 - name: Catherby x: 2821 y: 3433 z: 0 - name: Champions' Guild x: 3191 y: 3360 z: 0 - name: Chaos Druid Tower Dungeon x: 2580 y: 9743 z: 0 - name: Chaos Temple x: 2933 y: 3514 z: 0 - name: Chaos Temple x: 3240 y: 3608 z: 0 - name: Clan Wars x: 3371 y: 3162 z: 0 - name: Clan Wars x: 3422 y: 4735 z: 0 - name: Clocktower x: 2571 y: 3240 z: 0 - name: Clocktower Dungeon x: 2590 y: 9630 z: 0 - name: Coal Trucks x: 2598 y: 3489 z: 0 - name: Combat Training Camp x: 2515 y: 3369 z: 0 - name: Cooks' Guild x: 3143 y: 3447 z: 0 - name: Cosmic entity's plane x: 2079 y: 4828 z: 0 - name: Crafting Guild x: 2926 y: 3281 z: 0 - name: Crandor x: 2836 y: 3271 z: 0 - name: Crandor Dungeon x: 2849 y: 9636 z: 0 - name: Crash Island x: 2914 y: 2720 z: 0 - name: Dark Warriors' Fortress x: 3029 y: 3630 z: 0 - name: Dark Wizards' Tower x: 2908 y: 3334 z: 0 - name: Death Plateau x: 2863 y: 3590 z: 0 - name: Deep Wilderness Dungeon x: 3040 y: 10336 z: 0 - name: Demonic Ruins x: 3289 y: 3885 z: 0 - name: Desert Mining Camp x: 3288 y: 3021 z: 0 - name: Deserted Keep x: 3153 y: 3931 z: 0 - name: Digsite x: 3362 y: 3417 z: 0 - name: Dragontooth Island x: 3806 y: 3554 z: 0 - name: Draynor Manor x: 3104 y: 3341 z: 0 - name: Draynor Sewers x: 3107 y: 9672 z: 0 - name: Draynor Village x: 3105 y: 3258 z: 0 - name: Druids' Circle x: 2925 y: 3482 z: 0 - name: Duel Arena x: 3361 y: 3232 z: 0 - name: Dwarven Mine x: 3015 y: 3445 z: 0 - name: Dwarven Mine Dungeon x: 3024 y: 9791 z: 0 - name: Eagles' Peak x: 2332 y: 3486 z: 0 - name: East Ardougne x: 2598 y: 3295 z: 0 - name: Ectofuntus x: 3659 y: 3519 z: 0 - name: Edgeville Dungeon x: 3114 y: 9917 z: 0 - name: Edgeville x: 3086 y: 3497 z: 0 - name: Elemental Workshop x: 1963 y: 5149 z: 0 - name: Elf Camp x: 2196 y: 3251 z: 0 - name: Enakhra's Temple Bottom Floor x: 3104 y: 9312 z: 0 - name: Entrana x: 2843 y: 3378 z: 0 - name: Etceteria x: 2609 y: 3874 z: 0 - name: Exam Centre x: 3363 y: 3339 z: 0 - name: Falador x: 3004 y: 3361 z: 0 - name: Falador Mole Lair x: 1760 y: 5190 z: 0 - name: Falconer x: 2374 y: 3604 z: 0 - name: Feldip Hills x: 2556 y: 2982 z: 0 - name: Ferox Enclave x: 3141 y: 3629 z: 0 - name: Fenkenstrain's Castle x: 3548 y: 3554 z: 0 - name: Fenkenstrain's Dungeon x: 3519 y: 9952 z: 0 - name: Fight Arena x: 2592 y: 3161 z: 0 - name: Fishing Guild x: 2604 y: 3400 z: 0 - name: Fishing Platform x: 2772 y: 3283 z: 0 - name: Flax x: 2744 y: 3443 z: 0 - name: Forinthry Dungeon x: 3211 y: 10150 z: 0 - name: Fountain of Rune x: 3378 y: 3891 z: 0 - name: Fremennik Province x: 2666 y: 3632 z: 0 - name: Fremennik Slayer Dungeon x: 2805 y: 10001 z: 0 - name: Frozen Waste Plateau x: 2962 y: 3917 z: 0 - name: 2006 Easter Event x: 2464 y: 5280 z: 0 - name: Glarial's Tomb x: 2543 y: 9827 z: 0 - name: Gnome Ball Field x: 2395 y: 3486 z: 0 - name: Goblin Cave x: 2587 y: 9830 z: 0 - name: Goblin Village x: 2956 y: 3505 z: 0 - name: Golden Apple Tree x: 2766 y: 3607 z: 0 - name: Grand Tree x: 2464 y: 3501 z: 0 - name: Grand Tree Tunnels x: 2463 y: 9887 z: 0 - name: Graveyard of Shadows x: 3164 y: 3672 z: 0 - name: Graveyard x: 3569 y: 3404 z: 0 - name: Gu'Tanoth x: 2521 y: 3043 z: 0 - name: Haunted Woods x: 3564 y: 3490 z: 0 - name: Hemenster x: 2634 y: 3437 z: 0 - name: Heroes' Guild x: 2896 y: 3510 z: 0 - name: Iban's Lair x: 2335 y: 9855 z: 0 - name: Ice Mountain x: 3007 y: 3481 z: 0 - name: Ice path x: 2854 y: 3808 z: 0 - name: Ice Queen's lair x: 2865 y: 9954 z: 0 - name: Isafdar x: 2244 y: 3180 z: 0 - name: Jail x: 3125 y: 3242 z: 0 - name: Jiggig x: 2465 y: 3045 z: 0 - name: Lower Jiggig Dungeon x: 2465 y: 9441 z: 0 - name: Middle Jiggig Dungeon x: 2465 y: 9441 z: 2 - name: Kalphite Lair x: 3226 y: 3106 z: 0 - name: Karamja x: 2859 y: 3043 z: 0 - name: Karamja Dungeon x: 2840 y: 9571 z: 0 - name: Keep Le Faye x: 2769 y: 3399 z: 0 - name: Keldagrim Entrance x: 2725 y: 3712 z: 0 - name: Keldagrim x: 2855 y: 10175 z: 0 - name: Kharazi Jungle x: 2833 y: 2922 z: 0 - name: Kharidian Desert x: 3264 y: 2960 z: 0 - name: Kingdom of Asgarnia x: 2991 y: 3405 z: 0 - name: Kingdom of Kandarin x: 2572 y: 3445 z: 0 - name: Kingdom of Misthalin x: 3215 y: 3318 z: 0 - name: Lava Dragon Isle x: 3197 y: 3825 z: 0 - name: Lava Maze x: 3075 y: 3845 z: 0 - name: Lava Maze Dungeon x: 3040 y: 10272 z: 0 - name: Legends' Guild x: 2730 y: 3377 z: 0 - name: Legends' Guild Dungeon x: 2720 y: 9754 z: 0 - name: Lighthouse x: 2510 y: 3626 z: 0 - name: Lighthouse Dungeon x: 2518 y: 10021 z: 0 - name: Lizards x: 3421 y: 3041 z: 0 - name: Lletya x: 2346 y: 3177 z: 0 - name: Lumber Yard x: 3305 y: 3505 z: 0 - name: Lumbridge Swamp Caves x: 3169 y: 9571 z: 0 - name: Lumbridge Swamp x: 3184 y: 3179 z: 0 - name: Lumbridge x: 3224 y: 3218 z: 0 - name: Mage Arena x: 3105 y: 3932 z: 0 - name: Mage Training Arena Rooms x: 3357 y: 9666 z: 0 - name: Mage Training Arena x: 3363 y: 3304 z: 0 - name: Marim x: 2760 y: 2783 z: 0 - name: Market x: 3082 y: 3246 z: 0 - name: Mausoleum x: 3503 y: 3572 z: 0 - name: McGrubor's Wood x: 2641 y: 3480 z: 0 - name: Melzar's Maze x: 2933 y: 3248 z: 0 - name: Menaphos x: 3233 y: 2780 z: 0 - name: Miscellania x: 2537 y: 3875 z: 0 - name: Miscellania dungeon x: 2558 y: 10276 z: 0 - name: Mogre Camp x: 2974 y: 9496 z: 1 - name: Monastery 1 x: 2602 y: 3215 z: 0 - name: Monastery 2 x: 3052 y: 3487 z: 0 - name: Mort Myre Swamp x: 3440 y: 3380 z: 0 - name: Mort'ton x: 3487 y: 3283 z: 0 - name: Morytania x: 3467 y: 3441 z: 0 - name: Mor Ul Rek x: 2494 y: 5124 z: 0 - name: Mountain Camp x: 2801 y: 3670 z: 0 - name: Mouse Hole x: 2280 y: 5535 z: 0 - name: Mudskipper Point x: 2992 y: 3116 z: 0 - name: Musa Point x: 2897 y: 3161 z: 0 - name: Nardah x: 3427 y: 2903 z: 0 - name: Necromancer x: 2669 y: 3241 z: 0 - name: Nightmare Zone x: 2603 y: 3115 z: 0 - name: Observatory x: 2441 y: 3157 z: 0 - name: Observatory Dungeon x: 2398 y: 9441 z: 0 - name: Ogre Enclave x: 2592 y: 9444 z: 0 - name: Otto's Grotto x: 2502 y: 3488 z: 0 - name: Outpost x: 2441 y: 3345 z: 0 - name: Palace x: 3212 y: 3479 z: 0 - name: Pest Control x: 2656 y: 2593 z: 0 - name: Pirates' Hideout x: 3041 y: 3950 z: 0 - name: Piscatoris Fishing Colony x: 2343 y: 3690 z: 0 - name: Poision Waste x: 2232 y: 3096 z: 0 - name: Pollnivneach x: 3352 y: 2977 z: 0 - name: Port Khazard x: 2655 y: 3185 z: 0 - name: Port Phasmatys x: 3674 y: 3486 z: 0 - name: Port Sarim x: 3044 y: 3218 z: 0 - name: Pothole Dungeon x: 2845 y: 9505 z: 0 - name: Prifddinas x: 2235 y: 3317 z: 0 - name: Pyramid x: 3233 y: 2896 z: 0 - name: Quarry x: 3172 y: 2908 z: 0 - name: Ranging Guild x: 2666 y: 3429 z: 0 - name: Ratcatchers Mansion x: 2847 y: 5086 z: 0 - name: Rellekka x: 2668 y: 3676 z: 0 - name: Resource Area x: 3185 y: 3934 z: 0 - name: Rimmington x: 2957 y: 3215 z: 0 - name: River Elid x: 3372 y: 3074 z: 0 - name: River Lum x: 3167 y: 3346 z: 0 - name: River Salve x: 3403 y: 3442 z: 0 - name: Rogue's Den x: 3047 y: 4976 z: 1 - name: Rogues' Castle x: 3286 y: 3931 z: 0 - name: Ruins of Uzer x: 3479 y: 3098 z: 0 - name: Ruins x: 2967 y: 3695 z: 0 - name: Ruins x: 3164 y: 3734 z: 0 - name: Scorpion Pit x: 3232 y: 3942 z: 0 - name: Secret Hangar x: 2391 y: 9890 z: 0 - name: Seers' Village x: 2701 y: 3483 z: 0 - name: Shadow Dungeon x: 2687 y: 5088 z: 0 - name: Shantay Pass x: 3304 y: 3122 z: 0 - name: Shilo Village x: 2844 y: 2982 z: 0 - name: Ship Yard x: 2987 y: 3055 z: 0 - name: Sinclair Mansion x: 2742 y: 3549 z: 0 - name: Slayer Tower x: 3428 y: 3554 z: 0 - name: Smoke Dungeon x: 3265 y: 9373 z: 0 - name: Sophanem x: 3296 y: 2780 z: 0 - name: Sorcerer's Tower x: 2702 y: 3404 z: 0 - name: Spider x: 3320 y: 3756 z: 0 - name: Stronghold Slayer Cave x: 2435 y: 9806 z: 0 - name: Swamp x: 2418 y: 3511 z: 0 - name: Tai Bwo Wannai x: 2789 y: 3063 z: 0 - name: Taverley Dungeon x: 2886 y: 9811 z: 0 - name: Taverly x: 2896 y: 3455 z: 0 - name: Temple x: 3414 y: 3487 z: 0 - name: Temple of Ikov x: 2649 y: 9854 z: 0 - name: Temple of Marimbo Dungeon x: 2784 y: 9184 z: 0 - name: The Forgotten Cemetery x: 2976 y: 3750 z: 0 - name: The Hollows x: 3498 y: 3381 z: 0 - name: "The Warrens" x: 1776 y: 10143 z: 0 - name: Tirannwn x: 2240 y: 3263 z: 0 - name: Toll Gate x: 3271 y: 3226 z: 0 - name: Tower of Life x: 2648 y: 3215 z: 0 - name: Trawler x: 2683 y: 3166 z: 0 - name: Tree Gnome Stronghold x: 2430 y: 3447 z: 0 - name: Tree Gnome Village x: 2527 y: 3166 z: 0 - name: Troll Stronghold x: 2832 y: 3682 z: 0 - name: Trollheim x: 2891 y: 3676 z: 0 - name: Trollweiss Mountain x: 2782 y: 3862 z: 0 - name: Tutorial Island x: 3101 y: 3094 z: 0 - name: Tyras Camp x: 2186 y: 3146 z: 0 - name: TzHaar City x: 2451 y: 5146 z: 0 - name: Underground Pass x: 2449 y: 3312 z: 0 - name: Underground Pass Area 1 x: 2464 y: 9700 z: 0 - name: Underground Pass Area 2 x: 2399 y: 9637 z: 0 - name: Underground Pass Area 3 x: 2398 y: 9710 z: 0 - name: Varrock x: 3213 y: 3449 z: 0 - name: Viyeldi caves x: 2398 y: 4717 z: 0 - name: Viyeldi caves (2) x: 2782 y: 9315 z: 0 - name: Void Knights' Outpost x: 2639 y: 2674 z: 0 - name: Vultures x: 3337 y: 2868 z: 0 - name: Waterbirth Island x: 2521 y: 3757 z: 0 - name: Waterbirth Dungeon 1 x: 2495 y: 10144 z: 0 - name: Waterbirth Dungeon 2 x: 1895 y: 4367 z: 0 - name: Waterbirth Dungeon 3 x: 1895 y: 4367 z: 1 - name: Waterbirth Dungeon 4 x: 1895 y: 4367 z: 2 - name: Waterbirth Dungeon 5 x: 1895 y: 4367 z: 3 - name: Waterbirth Dungeon 6 x: 2912 y: 4448 z: 0 - name: Waterfall Dungeon x: 2577 y: 9890 z: 0 - name: West Ardougne x: 2524 y: 3305 z: 0 - name: White Knights' Castle x: 2969 y: 3341 z: 0 - name: White Wolf Mountain x: 2847 y: 3494 z: 0 - name: Wilderness x: 3144 y: 3775 z: 0 - name: Witchaven x: 2709 y: 3289 z: 0 - name: Witchaven Dungeon x: 2722 y: 9689 z: 0 - name: Witchaven Dungeon (2) x: 2329 y: 5097 z: 0 - name: Wizards' Guild x: 2583 y: 3078 z: 0 - name: Wizards' Tower x: 3110 y: 3157 z: 0 - name: Yanille x: 2554 y: 3089 z: 0 - name: Yanille Agility Dungeon x: 2581 y: 9499 z: 0 - name: Yanille Agility Dungeon (2) x: 2580 y: 9577 z: 0 - name: Zanaris x: 2415 y: 4455 z: 0 ================================================ FILE: data/config/widgets.json ================================================ { "characterDesign": 269, "furnace": { "widgetId": 311, "slots": { "slot0": { "modelId": 4, "titleId": 16 }, "slot1": { "modelId": 5, "titleId": 20 }, "slot2": { "modelId": 6, "titleId": 24 }, "slot3": { "modelId": 7, "titleId": 28 }, "slot4": { "modelId": 8, "titleId": 32 }, "slot5": { "modelId": 9, "titleId": 36 }, "slot6": { "modelId": 10, "titleId": 40 }, "slot7": { "modelId": 11, "titleId": 44 }, "slot8": { "modelId": 12, "titleId": 48 } } }, "anvil": { "widgetId": 312 }, "inventory": { "widgetId": 149, "containerId": 0 }, "disabledTabs": 151, "equipment": { "widgetId": 387, "containerId": 25 }, "equipmentStats": { "widgetId": 465, "containerId": 103 }, "equipmentStatsInventory": { "widgetId": 336, "containerId": 0 }, "bank": { "depositBoxWidget": { "widgetId": 11, "containerId": 61, "titleText": 60 }, "pinSettingsWidget": { "widgetId": 14 }, "screenWidget": { "widgetId": 12, "containerId": 89 }, "tabWidget": { "widgetId": 266, "containerId": 0 } }, "defaultCombatStyle": 92, "skillGuide": 308, "skillsTab": 320, "friendsList": 131, "ignoreList": 148, "logoutTab": 182, "settingsTab": 261, "emotesTab": 464, "musicPlayerTab": 239, "prayerTab": 271, "standardSpellbookTab": 192, "questTab": 274, "shop": { "widgetId": 300, "containerId": 75, "title": 76 }, "shopPlayerInventory": { "widgetId": 301, "containerId": 0 }, "questJournal": 275, "questReward": 277, "welcomeScreen": 378, "welcomeScreenChildren": { "cogs": 16, "question": 17, "drama": 18, "bankPin": 19, "bankPinQuestion": 20, "scamming": 21, "bankPinKey": 22, "christmas": 23, "killcount": 24 }, "whatWouldYouLikeToSpin": 459 } ================================================ FILE: data/config/xteas/435.json ================================================ [ { "archive": 5, "group": 1, "name_hash": -1153472937, "name": "l40_55", "mapsquare": 10295, "key": [ -1920480496, -1423914110, 951774544, -1419269290 ] }, { "archive": 5, "group": 5, "name_hash": -1155051772, "name": "l29_80", "mapsquare": 7504, "key": [ 1847230655, 1366615901, 817013928, -639754200 ] }, { "archive": 5, "group": 9, "name_hash": -1153323920, "name": "l45_75", "mapsquare": 11595, "key": [ -1714471127, 803338900, 314885542, 1310938544 ] }, { "archive": 5, "group": 13, "name_hash": -1418243906, "name": "l39_160", "mapsquare": 10144, "key": [ 265530509, 2033515489, -2022406749, -591072091 ] }, { "archive": 5, "group": 21, "name_hash": -1153472876, "name": "l40_74", "mapsquare": 10314, "key": [ -2020182865, -861201399, 1793241744, -541294677 ] }, { "archive": 5, "group": 23, "name_hash": -1153472875, "name": "l40_75", "mapsquare": 10315, "key": [ 874541054, 1607050822, 800324422, 1994534030 ] }, { "archive": 5, "group": 25, "name_hash": -1397926444, "name": "l40_160", "mapsquare": 10400, "key": [ -1656816922, 1318311812, -811481661, 1625916625 ] }, { "archive": 5, "group": 37, "name_hash": -1154366605, "name": "l31_75", "mapsquare": 8011, "key": [ -1287670691, 690712165, -1180498360, -813635980 ] }, { "archive": 5, "group": 39, "name_hash": -1154366601, "name": "l31_79", "mapsquare": 8015, "key": [ -1320874262, 1937053221, 220952674, 1903403210 ] }, { "archive": 5, "group": 41, "name_hash": -1395155938, "name": "l43_145", "mapsquare": 11153, "key": [ -483259316, 1922539567, 1755917448, -1522301387 ] }, { "archive": 5, "group": 43, "name_hash": -1395155937, "name": "l43_146", "mapsquare": 11154, "key": [ 865563030, -1947773050, 322324580, 1127332895 ] }, { "archive": 5, "group": 49, "name_hash": -1153472848, "name": "l40_81", "mapsquare": 10321, "key": [ -1416786366, -183512307, 774862574, 1306347169 ] }, { "archive": 5, "group": 53, "name_hash": -1420091002, "name": "l37_148", "mapsquare": 9620, "key": [ 17247799, -1105459916, 1430084566, -466128103 ] }, { "archive": 5, "group": 57, "name_hash": -1420090979, "name": "l37_150", "mapsquare": 9622, "key": [ 56315964, 322433230, -2111187201, -1087892507 ] }, { "archive": 5, "group": 59, "name_hash": -1420090978, "name": "l37_151", "mapsquare": 9623, "key": [ 1173395984, 1622423564, 1684894916, 949843813 ] }, { "archive": 5, "group": 63, "name_hash": -1420090976, "name": "l37_153", "mapsquare": 9625, "key": [ -1160175957, -906620860, -585287228, -1756474591 ] }, { "archive": 5, "group": 65, "name_hash": -1420090975, "name": "l37_154", "mapsquare": 9626, "key": [ -1086856428, 1097853921, -94537716, -1037836120 ] }, { "archive": 5, "group": 67, "name_hash": -1152519655, "name": "l51_46", "mapsquare": 13102, "key": [ -585304244, -85656032, -1672180123, -889752362 ] }, { "archive": 5, "group": 69, "name_hash": -1152519654, "name": "l51_47", "mapsquare": 13103, "key": [ 664202950, 9942731, 526981448, 1961317213 ] }, { "archive": 5, "group": 75, "name_hash": -1152519630, "name": "l51_50", "mapsquare": 13106, "key": [ -143377915, -1243385951, -2076474290, 2005128831 ] }, { "archive": 5, "group": 77, "name_hash": -1152519629, "name": "l51_51", "mapsquare": 13107, "key": [ 282236429, 12837044, 477635096, -1973557870 ] }, { "archive": 5, "group": 81, "name_hash": -1152519627, "name": "l51_53", "mapsquare": 13109, "key": [ 1764736868, -790106346, -2113719433, 100735736 ] }, { "archive": 5, "group": 87, "name_hash": -1152519624, "name": "l51_56", "mapsquare": 13112, "key": [ -294331294, -882184793, 66656303, -1818669310 ] }, { "archive": 5, "group": 91, "name_hash": -1152519622, "name": "l51_58", "mapsquare": 13114, "key": [ -1266416096, 120695750, -1880861340, 2120698563 ] }, { "archive": 5, "group": 93, "name_hash": -1153264429, "name": "l47_47", "mapsquare": 12079, "key": [ 339960494, -1350930341, -272469140, 318766784 ] }, { "archive": 5, "group": 95, "name_hash": -1152519621, "name": "l51_59", "mapsquare": 13115, "key": [ 1818342784, -642975152, -355404480, 132805372 ] }, { "archive": 5, "group": 97, "name_hash": -1153264428, "name": "l47_48", "mapsquare": 12080, "key": [ -1938428459, -1838574783, 131585942, -1297008627 ] }, { "archive": 5, "group": 99, "name_hash": -1153264427, "name": "l47_49", "mapsquare": 12081, "key": [ 613255413, -1717776939, -179584559, -1310165510 ] }, { "archive": 5, "group": 101, "name_hash": -1152519599, "name": "l51_60", "mapsquare": 13116, "key": [ 1370550295, -820804475, 1895373272, 1832997410 ] }, { "archive": 5, "group": 103, "name_hash": -1152519598, "name": "l51_61", "mapsquare": 13117, "key": [ -1752848915, -1242872593, 1012298354, -1076490243 ] }, { "archive": 5, "group": 105, "name_hash": -1153264405, "name": "l47_50", "mapsquare": 12082, "key": [ 978817309, -1069180717, -1957128971, -476497920 ] }, { "archive": 5, "group": 109, "name_hash": -1153264403, "name": "l47_52", "mapsquare": 12084, "key": [ -1358781692, -1981539499, -2131133338, -221063772 ] }, { "archive": 5, "group": 111, "name_hash": -1153264402, "name": "l47_53", "mapsquare": 12085, "key": [ 866634875, 2126690507, 1264568376, 2107515492 ] }, { "archive": 5, "group": 117, "name_hash": -1153264399, "name": "l47_56", "mapsquare": 12088, "key": [ 344960338, -452527992, 1340005990, -1328736855 ] }, { "archive": 5, "group": 119, "name_hash": -1153264398, "name": "l47_57", "mapsquare": 12089, "key": [ -551791250, -2032976862, -1354246732, 1795367998 ] }, { "archive": 5, "group": 121, "name_hash": -1153264397, "name": "l47_58", "mapsquare": 12090, "key": [ -2012109696, -607715631, -468949176, 1368365998 ] }, { "archive": 5, "group": 123, "name_hash": -1153264396, "name": "l47_59", "mapsquare": 12091, "key": [ -2111919909, -1125782501, -1802382602, 2137630247 ] }, { "archive": 5, "group": 125, "name_hash": -1153413382, "name": "l42_49", "mapsquare": 10801, "key": [ -1831859863, -1836056891, 342489401, 1312886995 ] }, { "archive": 5, "group": 127, "name_hash": -1153264374, "name": "l47_60", "mapsquare": 12092, "key": [ 111448945, 1997179939, -1992758838, -904080104 ] }, { "archive": 5, "group": 129, "name_hash": -1153264373, "name": "l47_61", "mapsquare": 12093, "key": [ -901249604, 892308020, 1075683735, -148430952 ] }, { "archive": 5, "group": 131, "name_hash": -1153413360, "name": "l42_50", "mapsquare": 10802, "key": [ 1999669309, 1732706412, 1414266618, -2034805718 ] }, { "archive": 5, "group": 133, "name_hash": -1153413359, "name": "l42_51", "mapsquare": 10803, "key": [ 632564181, 298843585, -1472601465, -1065203254 ] }, { "archive": 5, "group": 135, "name_hash": -1153413358, "name": "l42_52", "mapsquare": 10804, "key": [ 844993707, -1910359056, -477807005, 1502341650 ] }, { "archive": 5, "group": 137, "name_hash": -1153413357, "name": "l42_53", "mapsquare": 10805, "key": [ -381810387, 1628315077, 690097561, -713577288 ] }, { "archive": 5, "group": 141, "name_hash": -1153413355, "name": "l42_55", "mapsquare": 10807, "key": [ 698638677, -1565681878, 635760984, -1240425436 ] }, { "archive": 5, "group": 143, "name_hash": -1153413354, "name": "l42_56", "mapsquare": 10808, "key": [ -960669862, 212910530, -1874628754, 1886016422 ] }, { "archive": 5, "group": 145, "name_hash": -1369297323, "name": "l50_151", "mapsquare": 12951, "key": [ -700278939, 1204517853, 912770506, 985515932 ] }, { "archive": 5, "group": 147, "name_hash": -1154158160, "name": "l38_46", "mapsquare": 9774, "key": [ -525517154, -170441725, -288082119, -1442581115 ] }, { "archive": 5, "group": 149, "name_hash": -1154158159, "name": "l38_47", "mapsquare": 9775, "key": [ 358467, -1032890524, -323905369, 672132700 ] }, { "archive": 5, "group": 153, "name_hash": -1154158157, "name": "l38_49", "mapsquare": 9777, "key": [ 31736957, -2137121125, 248765559, -757570002 ] }, { "archive": 5, "group": 159, "name_hash": -1154158133, "name": "l38_52", "mapsquare": 9780, "key": [ 23047955, 374207630, -14357742, 487844353 ] }, { "archive": 5, "group": 161, "name_hash": -1153264338, "name": "l47_75", "mapsquare": 12107, "key": [ -90455586, 506470665, -1991061709, 1699265764 ] }, { "archive": 5, "group": 167, "name_hash": -1154158130, "name": "l38_55", "mapsquare": 9783, "key": [ -697645230, -219261751, 668010484, -637416792 ] }, { "archive": 5, "group": 175, "name_hash": -1153413293, "name": "l42_75", "mapsquare": 10827, "key": [ -837303631, -267210523, -2043344605, 99880496 ] }, { "archive": 5, "group": 177, "name_hash": -1368373827, "name": "l51_147", "mapsquare": 13203, "key": [ -1342258113, -2106603709, 395365404, -973258459 ] }, { "archive": 5, "group": 179, "name_hash": -1152370701, "name": "l56_45", "mapsquare": 14381, "key": [ -534048527, 1614741928, -469012711, 1064350100 ] }, { "archive": 5, "group": 183, "name_hash": -1154158069, "name": "l38_74", "mapsquare": 9802, "key": [ -1207560898, -2121890421, -1280361853, 1268736089 ] }, { "archive": 5, "group": 185, "name_hash": -1368373799, "name": "l51_154", "mapsquare": 13210, "key": [ -1504547443, 1455578463, -1808313124, -45471826 ] }, { "archive": 5, "group": 189, "name_hash": -1154307027, "name": "l33_71", "mapsquare": 8519, "key": [ -2051430502, -746663069, -593970049, 379050420 ] }, { "archive": 5, "group": 191, "name_hash": -1154307026, "name": "l33_72", "mapsquare": 8520, "key": [ 576327208, 1659610549, -1319545488, -1984379615 ] }, { "archive": 5, "group": 193, "name_hash": -1154307025, "name": "l33_73", "mapsquare": 8521, "key": [ -1909603451, -596972288, -446203583, -1017538492 ] }, { "archive": 5, "group": 199, "name_hash": -1154307022, "name": "l33_76", "mapsquare": 8524, "key": [ 1989551519, 1836116867, 185458553, 223032295 ] }, { "archive": 5, "group": 201, "name_hash": -1393308896, "name": "l45_145", "mapsquare": 11665, "key": [ 862638927, -1128276245, -2128612744, 77024238 ] }, { "archive": 5, "group": 203, "name_hash": -1393308895, "name": "l45_146", "mapsquare": 11666, "key": [ 714846754, 200141282, -207402626, 2133043283 ] }, { "archive": 5, "group": 205, "name_hash": -1393308893, "name": "l45_148", "mapsquare": 11668, "key": [ 1094922095, -1747387328, 1439302672, 1510179242 ] }, { "archive": 5, "group": 209, "name_hash": -1393308869, "name": "l45_151", "mapsquare": 11671, "key": [ 517807890, -107570269, 1542962182, -1495382393 ] }, { "archive": 5, "group": 211, "name_hash": -1393308868, "name": "l45_152", "mapsquare": 11672, "key": [ 59956468, -1316944326, -1514599528, 979714228 ] }, { "archive": 5, "group": 213, "name_hash": -1393308867, "name": "l45_153", "mapsquare": 11673, "key": [ 343838952, -181461714, 1044748667, 1241583057 ] }, { "archive": 5, "group": 215, "name_hash": -1393308866, "name": "l45_154", "mapsquare": 11674, "key": [ -1389259398, 1840036337, 732526621, -1831580013 ] }, { "archive": 5, "group": 217, "name_hash": -1155051798, "name": "l29_75", "mapsquare": 7499, "key": [ 389242534, 1468756327, 1739858109, -397529213 ] }, { "archive": 5, "group": 225, "name_hash": -1418243959, "name": "l39_149", "mapsquare": 10133, "key": [ -1295008659, 266597781, -348722649, -387821353 ] }, { "archive": 5, "group": 227, "name_hash": -1418243937, "name": "l39_150", "mapsquare": 10134, "key": [ -169090672, -418150474, -663319294, 345797130 ] }, { "archive": 5, "group": 233, "name_hash": -1418243934, "name": "l39_153", "mapsquare": 10137, "key": [ 309023294, 10701046, 752820323, 320724862 ] }, { "archive": 5, "group": 235, "name_hash": -1418243933, "name": "l39_154", "mapsquare": 10138, "key": [ -1266932509, 1159824249, 1189711903, 1903826998 ] }, { "archive": 5, "group": 243, "name_hash": -1152460047, "name": "l53_51", "mapsquare": 13619, "key": [ 712676739, -1616631519, -393806187, 1432842778 ] }, { "archive": 5, "group": 245, "name_hash": -1152460046, "name": "l53_52", "mapsquare": 13620, "key": [ -1340730148, -1509744228, -755242060, 937448072 ] }, { "archive": 5, "group": 247, "name_hash": -1152460045, "name": "l53_53", "mapsquare": 13621, "key": [ -58207297, 135788372, -1606570127, 662309804 ] }, { "archive": 5, "group": 249, "name_hash": -1153204848, "name": "l49_46", "mapsquare": 12590, "key": [ 1057515769, -334000230, -1142782469, 1285186544 ] }, { "archive": 5, "group": 251, "name_hash": -1153204847, "name": "l49_47", "mapsquare": 12591, "key": [ 1153167116, 1162748714, -2111043500, -1049253880 ] }, { "archive": 5, "group": 253, "name_hash": -1153204846, "name": "l49_48", "mapsquare": 12592, "key": [ -794001650, -436110669, 1193837402, 1311524977 ] }, { "archive": 5, "group": 255, "name_hash": -1153204845, "name": "l49_49", "mapsquare": 12593, "key": [ -544590439, -1841162484, -767714061, 1725894803 ] }, { "archive": 5, "group": 257, "name_hash": -1153204823, "name": "l49_50", "mapsquare": 12594, "key": [ -120447093, 579654861, -527409529, 1730758809 ] }, { "archive": 5, "group": 259, "name_hash": -1153204822, "name": "l49_51", "mapsquare": 12595, "key": [ -1713627476, -189383107, 2075238616, 630646748 ] }, { "archive": 5, "group": 261, "name_hash": -1153204821, "name": "l49_52", "mapsquare": 12596, "key": [ 1748061888, 601884795, -571814788, -1524221044 ] }, { "archive": 5, "group": 265, "name_hash": -1152370700, "name": "l56_46", "mapsquare": 14382, "key": [ -2047578817, 357439085, 332136595, 1757066622 ] }, { "archive": 5, "group": 271, "name_hash": -1153204817, "name": "l49_56", "mapsquare": 12600, "key": [ 2086237847, -2000241337, 787272168, -1342875227 ] }, { "archive": 5, "group": 273, "name_hash": -1153353804, "name": "l44_45", "mapsquare": 11309, "key": [ 349211408, 1529654711, -1227331504, 531945093 ] }, { "archive": 5, "group": 275, "name_hash": -1153204816, "name": "l49_57", "mapsquare": 12601, "key": [ -360556539, 1198026803, -609365963, -1563087824 ] }, { "archive": 5, "group": 279, "name_hash": -1153204815, "name": "l49_58", "mapsquare": 12602, "key": [ -1597607397, -2146719506, -1185304014, 1704592002 ] }, { "archive": 5, "group": 281, "name_hash": -1153353802, "name": "l44_47", "mapsquare": 11311, "key": [ 255517588, -1240190218, -693538893, 52321524 ] }, { "archive": 5, "group": 283, "name_hash": -1153204814, "name": "l49_59", "mapsquare": 12603, "key": [ 1903637882, -2078269745, 1154595732, 2025457352 ] }, { "archive": 5, "group": 289, "name_hash": -1153204792, "name": "l49_60", "mapsquare": 12604, "key": [ -1812120898, -1662909112, -1432124543, 571589094 ] }, { "archive": 5, "group": 291, "name_hash": -1153204791, "name": "l49_61", "mapsquare": 12605, "key": [ 794830896, -1814271030, 54960303, 1749986908 ] }, { "archive": 5, "group": 293, "name_hash": -1153353778, "name": "l44_50", "mapsquare": 11314, "key": [ 1619729167, 1824886381, 1092421269, -1188344727 ] }, { "archive": 5, "group": 295, "name_hash": -1153353777, "name": "l44_51", "mapsquare": 11315, "key": [ -202635393, 778738800, 432999637, 1992018816 ] }, { "archive": 5, "group": 299, "name_hash": -1153353775, "name": "l44_53", "mapsquare": 11317, "key": [ 1589220603, 1966511687, -1108893479, -635825550 ] }, { "archive": 5, "group": 307, "name_hash": -1391461850, "name": "l47_149", "mapsquare": 12181, "key": [ -1585897411, -486973222, 976725192, 587846488 ] }, { "archive": 5, "group": 313, "name_hash": -1391461825, "name": "l47_153", "mapsquare": 12185, "key": [ -1833444484, -1358796470, -2064057632, 1209694095 ] }, { "archive": 5, "group": 317, "name_hash": -1391461797, "name": "l47_160", "mapsquare": 12192, "key": [ 152792546, 1064551985, 823336050, -12082873 ] }, { "archive": 5, "group": 319, "name_hash": -1391461796, "name": "l47_161", "mapsquare": 12193, "key": [ -593888487, 92505902, -268347225, 2144548258 ] }, { "archive": 5, "group": 325, "name_hash": -1397926499, "name": "l40_147", "mapsquare": 10387, "key": [ 1215785755, 210499977, -2093512319, -625901574 ] }, { "archive": 5, "group": 327, "name_hash": -1152340910, "name": "l57_45", "mapsquare": 14637, "key": [ 1954698915, 587858534, -1998840957, -860751267 ] }, { "archive": 5, "group": 329, "name_hash": -1397926498, "name": "l40_148", "mapsquare": 10388, "key": [ 1415243362, 801135032, -385381783, -83383530 ] }, { "archive": 5, "group": 331, "name_hash": -1153353711, "name": "l44_75", "mapsquare": 11339, "key": [ -1717666269, 1594420520, -1751131464, -154240780 ] }, { "archive": 5, "group": 343, "name_hash": -1397926471, "name": "l40_154", "mapsquare": 10394, "key": [ -2102331685, 781435808, 1774321038, -660278831 ] }, { "archive": 5, "group": 347, "name_hash": -1152340908, "name": "l57_47", "mapsquare": 14639, "key": [ -1842058479, -1554929196, 440799576, -1954881277 ] }, { "archive": 5, "group": 349, "name_hash": -1154396396, "name": "l30_75", "mapsquare": 7755, "key": [ 1467763642, -2057666779, 1692811, 1234049587 ] }, { "archive": 5, "group": 351, "name_hash": -1152340907, "name": "l57_48", "mapsquare": 14640, "key": [ -95254230, -1094148730, -1768319507, 105210786 ] }, { "archive": 5, "group": 357, "name_hash": -1389614782, "name": "l49_154", "mapsquare": 12698, "key": [ 1313997139, 413705980, -1786270630, -1211819932 ] }, { "archive": 5, "group": 363, "name_hash": -1361909181, "name": "l58_146", "mapsquare": 14994, "key": [ 96028866, 945858112, 1932735023, 1432905132 ] }, { "archive": 5, "group": 365, "name_hash": -1361909180, "name": "l58_147", "mapsquare": 14995, "key": [ 452468371, 863200236, 1512019095, 410208849 ] }, { "archive": 5, "group": 367, "name_hash": -1152311119, "name": "l58_45", "mapsquare": 14893, "key": [ -102394113, 1484262111, 933695487, -283213546 ] }, { "archive": 5, "group": 369, "name_hash": -1396079432, "name": "l42_151", "mapsquare": 10903, "key": [ -1260800958, -1306263170, 319256612, 1478365211 ] }, { "archive": 5, "group": 371, "name_hash": -1152549446, "name": "l50_46", "mapsquare": 12846, "key": [ -1128107273, -1379368189, -1166299202, 1367493265 ] }, { "archive": 5, "group": 373, "name_hash": -1396079431, "name": "l42_152", "mapsquare": 10904, "key": [ 299433082, 1093303265, 1627642083, 1856381978 ] }, { "archive": 5, "group": 377, "name_hash": -1396079430, "name": "l42_153", "mapsquare": 10905, "key": [ 15962844, -263608354, -1698286677, -1506930528 ] }, { "archive": 5, "group": 379, "name_hash": -1152549444, "name": "l50_48", "mapsquare": 12848, "key": [ -472467501, 942301843, -1235756555, 1092807961 ] }, { "archive": 5, "group": 381, "name_hash": -1152549443, "name": "l50_49", "mapsquare": 12849, "key": [ 856523891, -869724785, 1850232409, -674722748 ] }, { "archive": 5, "group": 387, "name_hash": -1152549419, "name": "l50_52", "mapsquare": 12852, "key": [ -383547364, -1207189748, 1322204269, -1173579306 ] }, { "archive": 5, "group": 389, "name_hash": -1152549418, "name": "l50_53", "mapsquare": 12853, "key": [ -1734454891, 559576609, 2053124820, 179220049 ] }, { "archive": 5, "group": 395, "name_hash": -1152549415, "name": "l50_56", "mapsquare": 12856, "key": [ 1065837316, 566874993, -395031456, -1005765069 ] }, { "archive": 5, "group": 397, "name_hash": -1153294222, "name": "l46_45", "mapsquare": 11821, "key": [ -1676526445, -459611821, 895613340, -1034819584 ] }, { "archive": 5, "group": 399, "name_hash": -1152549414, "name": "l50_57", "mapsquare": 12857, "key": [ 1315061238, 798718066, -593804887, 1674278988 ] }, { "archive": 5, "group": 401, "name_hash": -1153294221, "name": "l46_46", "mapsquare": 11822, "key": [ 1634541811, 1149149862, -1352773374, 843504994 ] }, { "archive": 5, "group": 403, "name_hash": -1152311118, "name": "l58_46", "mapsquare": 14894, "key": [ -699464927, -423502070, -1690485752, 1901016133 ] }, { "archive": 5, "group": 405, "name_hash": -1152549413, "name": "l50_58", "mapsquare": 12858, "key": [ 1739165090, 1380036717, 1760229212, -243206298 ] }, { "archive": 5, "group": 407, "name_hash": -1153294220, "name": "l46_47", "mapsquare": 11823, "key": [ -256018875, 2059756735, 1142698660, 1129906231 ] }, { "archive": 5, "group": 409, "name_hash": -1152311117, "name": "l58_47", "mapsquare": 14895, "key": [ 1294223700, 778677766, -2141486577, 1402077049 ] }, { "archive": 5, "group": 411, "name_hash": -1152549412, "name": "l50_59", "mapsquare": 12859, "key": [ 426800430, 789609597, 2049374976, 800682309 ] }, { "archive": 5, "group": 413, "name_hash": -1153294219, "name": "l46_48", "mapsquare": 11824, "key": [ -1116727390, -659788658, 307002260, 354360287 ] }, { "archive": 5, "group": 417, "name_hash": -1153294218, "name": "l46_49", "mapsquare": 11825, "key": [ 1525999114, 1162595366, 465333685, 1306030426 ] }, { "archive": 5, "group": 421, "name_hash": -1152549390, "name": "l50_60", "mapsquare": 12860, "key": [ 189035026, -14034698, -394067455, -734636364 ] }, { "archive": 5, "group": 423, "name_hash": -1152549389, "name": "l50_61", "mapsquare": 12861, "key": [ 1816963481, -496926322, 1214068433, 1190940799 ] }, { "archive": 5, "group": 427, "name_hash": -1421014500, "name": "l36_150", "mapsquare": 9366, "key": [ 1443456963, 1791165211, 59698870, 1492849730 ] }, { "archive": 5, "group": 435, "name_hash": -1153294193, "name": "l46_53", "mapsquare": 11829, "key": [ 1880703134, 1096367920, -438608238, -2070635879 ] }, { "archive": 5, "group": 437, "name_hash": -1421014497, "name": "l36_153", "mapsquare": 9369, "key": [ 1046237398, 2034378264, -2111613007, 1419925615 ] }, { "archive": 5, "group": 441, "name_hash": -1421014496, "name": "l36_154", "mapsquare": 9370, "key": [ 1560310225, -1912646908, -21541393, -690057286 ] }, { "archive": 5, "group": 445, "name_hash": -1153294190, "name": "l46_56", "mapsquare": 11832, "key": [ 1106039923, -761356647, -2114299754, 948992246 ] }, { "archive": 5, "group": 453, "name_hash": -1153294188, "name": "l46_58", "mapsquare": 11834, "key": [ 173215366, -1499360859, 472196248, 1020719132 ] }, { "archive": 5, "group": 457, "name_hash": -1153294187, "name": "l46_59", "mapsquare": 11835, "key": [ 1629126437, 1652550905, 841609762, -317888116 ] }, { "archive": 5, "group": 459, "name_hash": -1153443174, "name": "l41_48", "mapsquare": 10544, "key": [ -895303896, 744949463, -2033369513, 338766696 ] }, { "archive": 5, "group": 463, "name_hash": -1153294165, "name": "l46_60", "mapsquare": 11836, "key": [ 1310410800, 423198468, -1302324807, 259991487 ] }, { "archive": 5, "group": 465, "name_hash": -1153294164, "name": "l46_61", "mapsquare": 11837, "key": [ 1906296384, 292432464, 594794920, -1546603049 ] }, { "archive": 5, "group": 471, "name_hash": -1153443149, "name": "l41_52", "mapsquare": 10548, "key": [ 1215763226, 2050714422, -2048665259, 230062419 ] }, { "archive": 5, "group": 473, "name_hash": -1153443148, "name": "l41_53", "mapsquare": 10549, "key": [ 1856240647, -1307099618, -1197945308, -2122254516 ] }, { "archive": 5, "group": 475, "name_hash": -1153443147, "name": "l41_54", "mapsquare": 10550, "key": [ 957349475, -496721990, 1780878747, -1948737082 ] }, { "archive": 5, "group": 477, "name_hash": -1153443146, "name": "l41_55", "mapsquare": 10551, "key": [ 1042296612, -799710697, -433924400, -596565458 ] }, { "archive": 5, "group": 479, "name_hash": -1153443145, "name": "l41_56", "mapsquare": 10552, "key": [ 1428473548, -513790027, 959922611, 2094550429 ] }, { "archive": 5, "group": 483, "name_hash": -1154187948, "name": "l37_49", "mapsquare": 9521, "key": [ -1204382617, 894320578, -2017151095, -679668405 ] }, { "archive": 5, "group": 485, "name_hash": -1154187926, "name": "l37_50", "mapsquare": 9522, "key": [ 1252542168, -1448059709, 1122042436, -1636001244 ] }, { "archive": 5, "group": 487, "name_hash": -1154187925, "name": "l37_51", "mapsquare": 9523, "key": [ -1310187207, -834726064, -1799766719, 1699616161 ] }, { "archive": 5, "group": 495, "name_hash": -1154187922, "name": "l37_54", "mapsquare": 9526, "key": [ -2064252262, -887239839, 1167799401, 858861043 ] }, { "archive": 5, "group": 501, "name_hash": -1153443086, "name": "l41_73", "mapsquare": 10569, "key": [ 501975646, 1931710158, -452188231, -1541425293 ] }, { "archive": 5, "group": 505, "name_hash": -1153443084, "name": "l41_75", "mapsquare": 10571, "key": [ 1446859333, -1375675929, -760014134, 1113381222 ] }, { "archive": 5, "group": 507, "name_hash": -1360985659, "name": "l59_147", "mapsquare": 15251, "key": [ 128349827, -1935626457, -20863709, 460738721 ] }, { "archive": 5, "group": 509, "name_hash": -1154187861, "name": "l37_73", "mapsquare": 9545, "key": [ 219605670, 414657294, -2055023109, -49887033 ] }, { "archive": 5, "group": 513, "name_hash": -1154187859, "name": "l37_75", "mapsquare": 9547, "key": [ -157587105, -806602516, 1521207142, -727421759 ] }, { "archive": 5, "group": 525, "name_hash": -1152281328, "name": "l59_45", "mapsquare": 15149, "key": [ -1200628316, 15132614, -465260385, -1525542932 ] }, { "archive": 5, "group": 535, "name_hash": -1369297320, "name": "l50_154", "mapsquare": 12954, "key": [ 1051871996, 1141268606, -1818400310, 1415407155 ] }, { "archive": 5, "group": 537, "name_hash": -1152281327, "name": "l59_46", "mapsquare": 15150, "key": [ -1763056537, 1265265926, 379787904, 598675540 ] }, { "archive": 5, "group": 541, "name_hash": -1152281326, "name": "l59_47", "mapsquare": 15151, "key": [ -2030556277, -1247226369, -1858099479, -1486886413 ] }, { "archive": 5, "group": 543, "name_hash": -1394232414, "name": "l44_148", "mapsquare": 11412, "key": [ -119590785, -577007136, 324812522, 2041325243 ] }, { "archive": 5, "group": 545, "name_hash": -1394232413, "name": "l44_149", "mapsquare": 11413, "key": [ -95977595, -2110282911, 1926811747, -767376929 ] }, { "archive": 5, "group": 547, "name_hash": -1394232391, "name": "l44_150", "mapsquare": 11414, "key": [ -460014032, 2024565076, -2104679952, -443498989 ] }, { "archive": 5, "group": 551, "name_hash": -1394232389, "name": "l44_152", "mapsquare": 11416, "key": [ 698674506, 1358331305, 913172931, 765272229 ] }, { "archive": 5, "group": 553, "name_hash": -1394232388, "name": "l44_153", "mapsquare": 11417, "key": [ 397843162, 248774218, -228700444, -531210715 ] }, { "archive": 5, "group": 557, "name_hash": -1394232386, "name": "l44_155", "mapsquare": 11419, "key": [ 1015780380, -702872379, -62881570, -362521171 ] }, { "archive": 5, "group": 561, "name_hash": -1419167482, "name": "l38_147", "mapsquare": 9875, "key": [ -266211268, 1666415569, -594364895, -521555462 ] }, { "archive": 5, "group": 575, "name_hash": -1419167454, "name": "l38_154", "mapsquare": 9882, "key": [ 1405534886, 1766737228, -31518279, -1845608276 ] }, { "archive": 5, "group": 585, "name_hash": -1152489861, "name": "l52_49", "mapsquare": 13361, "key": [ 1703044360, 924675594, 1161742507, -1174930763 ] }, { "archive": 5, "group": 587, "name_hash": -1152489839, "name": "l52_50", "mapsquare": 13362, "key": [ 1001397515, 373830398, 1721578513, -710173574 ] }, { "archive": 5, "group": 589, "name_hash": -1152489838, "name": "l52_51", "mapsquare": 13363, "key": [ -1632087500, -620280348, 22360696, 1386365966 ] }, { "archive": 5, "group": 591, "name_hash": -1152489837, "name": "l52_52", "mapsquare": 13364, "key": [ 1727399192, -217492117, 1391929576, 1966780437 ] }, { "archive": 5, "group": 595, "name_hash": -1152489835, "name": "l52_54", "mapsquare": 13366, "key": [ -1592783139, -1871719576, 721348207, 67703995 ] }, { "archive": 5, "group": 599, "name_hash": -1152489833, "name": "l52_56", "mapsquare": 13368, "key": [ -922499048, 1143654396, 1593518953, 1160947087 ] }, { "archive": 5, "group": 601, "name_hash": -1152489832, "name": "l52_57", "mapsquare": 13369, "key": [ -857763581, -71502428, -1316897039, -786191624 ] }, { "archive": 5, "group": 603, "name_hash": -1152489831, "name": "l52_58", "mapsquare": 13370, "key": [ -1259912756, -640452721, 1196310747, 269878509 ] }, { "archive": 5, "group": 605, "name_hash": -1153234638, "name": "l48_47", "mapsquare": 12335, "key": [ -387260899, -2132243418, 654609855, -1092457792 ] }, { "archive": 5, "group": 607, "name_hash": -1152489830, "name": "l52_59", "mapsquare": 13371, "key": [ -398274614, 1285418048, 876964091, -1818498048 ] }, { "archive": 5, "group": 609, "name_hash": -1153234637, "name": "l48_48", "mapsquare": 12336, "key": [ 1152832719, 1703327384, -664371384, 1662584632 ] }, { "archive": 5, "group": 611, "name_hash": -1153234636, "name": "l48_49", "mapsquare": 12337, "key": [ -750073424, 748796796, -1188784945, 127988493 ] }, { "archive": 5, "group": 613, "name_hash": -1152489808, "name": "l52_60", "mapsquare": 13372, "key": [ 865568678, -2124731942, 491055641, 1024391843 ] }, { "archive": 5, "group": 615, "name_hash": -1152489807, "name": "l52_61", "mapsquare": 13373, "key": [ -2063060546, 1762500570, 940695155, -1216823417 ] }, { "archive": 5, "group": 617, "name_hash": -1153234614, "name": "l48_50", "mapsquare": 12338, "key": [ -314247680, -1975556448, 815744465, 686683170 ] }, { "archive": 5, "group": 623, "name_hash": -1153234611, "name": "l48_53", "mapsquare": 12341, "key": [ 771717693, 2109827194, 2070396643, 121903263 ] }, { "archive": 5, "group": 629, "name_hash": -1153234608, "name": "l48_56", "mapsquare": 12344, "key": [ 1004655261, 1112441676, -1346215742, -481727651 ] }, { "archive": 5, "group": 631, "name_hash": -1153383595, "name": "l43_45", "mapsquare": 11053, "key": [ 347789951, -648108134, -480504172, -1661473430 ] }, { "archive": 5, "group": 633, "name_hash": -1153234607, "name": "l48_57", "mapsquare": 12345, "key": [ -1123116470, -1886379076, 75321832, 214948598 ] }, { "archive": 5, "group": 637, "name_hash": -1153234606, "name": "l48_58", "mapsquare": 12346, "key": [ -715432683, -1936681920, -1020543828, 1309349058 ] }, { "archive": 5, "group": 641, "name_hash": -1153234605, "name": "l48_59", "mapsquare": 12347, "key": [ -1936094430, -1392774395, -458238889, -1734657503 ] }, { "archive": 5, "group": 647, "name_hash": -1153234583, "name": "l48_60", "mapsquare": 12348, "key": [ 1498945330, -1007588865, 989046743, -1246005481 ] }, { "archive": 5, "group": 651, "name_hash": -1153383569, "name": "l43_50", "mapsquare": 11058, "key": [ 1248059648, -909154233, -2017783073, -1490858018 ] }, { "archive": 5, "group": 657, "name_hash": -1153383566, "name": "l43_53", "mapsquare": 11061, "key": [ -1835613132, -798666747, 2068157339, -2010441483 ] }, { "archive": 5, "group": 659, "name_hash": -1153383565, "name": "l43_54", "mapsquare": 11062, "key": [ 341057778, 537106684, 410932276, -1693151115 ] }, { "archive": 5, "group": 661, "name_hash": -1153383564, "name": "l43_55", "mapsquare": 11063, "key": [ 230190523, -541989310, 756088519, 651538143 ] }, { "archive": 5, "group": 663, "name_hash": -1153383563, "name": "l43_56", "mapsquare": 11064, "key": [ -1650524835, -1814768390, -97991881, -243448484 ] }, { "archive": 5, "group": 665, "name_hash": -1152281325, "name": "l59_48", "mapsquare": 15152, "key": [ 608495786, -1850745649, 1481459068, 44047411 ] }, { "archive": 5, "group": 673, "name_hash": -1154128366, "name": "l39_49", "mapsquare": 10033, "key": [ 1965668434, 1587425243, 966917721, 2010341526 ] }, { "archive": 5, "group": 677, "name_hash": -1154128343, "name": "l39_51", "mapsquare": 10035, "key": [ -1407868267, -2023338060, 1465071532, -1638236197 ] }, { "archive": 5, "group": 683, "name_hash": -1154128340, "name": "l39_54", "mapsquare": 10038, "key": [ -1492144861, 1907137836, -518005672, 990043883 ] }, { "archive": 5, "group": 685, "name_hash": -1367450280, "name": "l52_152", "mapsquare": 13464, "key": [ -1367805857, 1879344111, -360193890, -1649253702 ] }, { "archive": 5, "group": 695, "name_hash": -1153383504, "name": "l43_73", "mapsquare": 11081, "key": [ -1262440825, -866171020, -669660830, 1117529492 ] }, { "archive": 5, "group": 697, "name_hash": -1153383503, "name": "l43_74", "mapsquare": 11082, "key": [ -1048566284, -1888694855, 1387988701, 1385807517 ] }, { "archive": 5, "group": 701, "name_hash": -1392385371, "name": "l46_149", "mapsquare": 11925, "key": [ -302404963, 277321688, 1579550568, 112114901 ] }, { "archive": 5, "group": 703, "name_hash": -1392385349, "name": "l46_150", "mapsquare": 11926, "key": [ 1068449903, -150593625, 336172166, -629999737 ] }, { "archive": 5, "group": 705, "name_hash": -1154128280, "name": "l39_72", "mapsquare": 10056, "key": [ -1242668132, 243125991, 408826199, -1580449429 ] }, { "archive": 5, "group": 707, "name_hash": -1392385347, "name": "l46_152", "mapsquare": 11928, "key": [ 926476326, -742526377, -1211115608, -1389832255 ] }, { "archive": 5, "group": 709, "name_hash": -1154128279, "name": "l39_73", "mapsquare": 10057, "key": [ 2098955587, 1915815888, -1487119512, 2000380500 ] }, { "archive": 5, "group": 711, "name_hash": -1392385346, "name": "l46_153", "mapsquare": 11929, "key": [ 1557493911, -1795871852, 842977119, -1273925544 ] }, { "archive": 5, "group": 713, "name_hash": -1154128278, "name": "l39_74", "mapsquare": 10058, "key": [ -1722103890, -365638741, -1142855167, 2038282466 ] }, { "archive": 5, "group": 717, "name_hash": -1154128277, "name": "l39_75", "mapsquare": 10059, "key": [ 888791770, 446863035, 1420300298, 1689801625 ] }, { "archive": 5, "group": 719, "name_hash": -1155051771, "name": "l29_81", "mapsquare": 7505, "key": [ 1128826986, -1208716500, 1252781850, 1529005118 ] }, { "archive": 5, "group": 733, "name_hash": -1154277232, "name": "l34_75", "mapsquare": 8779, "key": [ -678956803, 2085783757, 650059580, 1450034986 ] }, { "archive": 5, "group": 735, "name_hash": -1154366578, "name": "l31_81", "mapsquare": 8017, "key": [ 1937423379, 797654757, -418006613, -1412491464 ] }, { "archive": 5, "group": 737, "name_hash": -1390538330, "name": "l48_148", "mapsquare": 12436, "key": [ 399007290, 768017220, 1423287782, -1886380141 ] }, { "archive": 5, "group": 739, "name_hash": -1390538329, "name": "l48_149", "mapsquare": 12437, "key": [ -1441809522, 719788383, 401674935, 735960337 ] }, { "archive": 5, "group": 743, "name_hash": -1390538304, "name": "l48_153", "mapsquare": 12441, "key": [ -2117208237, -719237621, 1574397150, 760135387 ] }, { "archive": 5, "group": 745, "name_hash": -1390538303, "name": "l48_154", "mapsquare": 12442, "key": [ 1291079634, -1775973138, -22379340, -1049278279 ] }, { "archive": 5, "group": 747, "name_hash": -1390538302, "name": "l48_155", "mapsquare": 12443, "key": [ 185729295, -1473640650, 785055601, 1540127286 ] }, { "archive": 5, "group": 753, "name_hash": -1153324012, "name": "l45_46", "mapsquare": 11566, "key": [ 1440870269, 1497945697, -1109133632, -2052747028 ] }, { "archive": 5, "group": 755, "name_hash": -1153324011, "name": "l45_47", "mapsquare": 11567, "key": [ 1608156752, 653692997, -1552888712, -68059986 ] }, { "archive": 5, "group": 757, "name_hash": -1153324010, "name": "l45_48", "mapsquare": 11568, "key": [ 1434739536, -965821980, 1477351656, -1116923723 ] }, { "archive": 5, "group": 761, "name_hash": -1153323987, "name": "l45_50", "mapsquare": 11570, "key": [ -1161155387, 1152701504, -1530532036, -216336018 ] }, { "archive": 5, "group": 763, "name_hash": -1397002979, "name": "l41_146", "mapsquare": 10642, "key": [ -1862930636, -112759523, -1776313742, 286385794 ] }, { "archive": 5, "group": 771, "name_hash": -1397002976, "name": "l41_149", "mapsquare": 10645, "key": [ 2023797650, 819145234, -2109392672, 1648146732 ] }, { "archive": 5, "group": 775, "name_hash": -1153323982, "name": "l45_55", "mapsquare": 11575, "key": [ -945127161, -1910820945, -1457442680, -838340972 ] }, { "archive": 5, "group": 777, "name_hash": -1153323981, "name": "l45_56", "mapsquare": 11576, "key": [ -653186907, 1743653193, 1590707166, 841079490 ] }, { "archive": 5, "group": 781, "name_hash": -1397002953, "name": "l41_151", "mapsquare": 10647, "key": [ -805201729, 122084157, -1599110597, -1319888179 ] }, { "archive": 5, "group": 785, "name_hash": -1153472967, "name": "l40_46", "mapsquare": 10286, "key": [ 153699565, -613131576, -377838747, -1493212656 ] }, { "archive": 5, "group": 789, "name_hash": -1153323979, "name": "l45_58", "mapsquare": 11578, "key": [ 899841295, -1673793442, -444221245, -1412324058 ] }, { "archive": 5, "group": 793, "name_hash": -1397002951, "name": "l41_153", "mapsquare": 10649, "key": [ -42874604, 1755845042, -28694142, -1223794235 ] }, { "archive": 5, "group": 795, "name_hash": -1153323978, "name": "l45_59", "mapsquare": 11579, "key": [ -1812176752, -317754254, -1196521232, -764437892 ] }, { "archive": 5, "group": 799, "name_hash": -1397002950, "name": "l41_154", "mapsquare": 10650, "key": [ 688979136, 1749672831, 630260962, 1478721348 ] }, { "archive": 5, "group": 801, "name_hash": -1153472964, "name": "l40_49", "mapsquare": 10289, "key": [ 661370115, 1720102, -155777581, 1516736681 ] }, { "archive": 5, "group": 803, "name_hash": -1154217623, "name": "l36_81", "mapsquare": 9297, "key": [ -713608515, -67472639, -1826048194, 544718437 ] }, { "archive": 5, "group": 815, "name_hash": -1153472938, "name": "l40_54", "mapsquare": 10294, "key": [ -1453542294, -12979792, -48294325, -2009299224 ] }, { "archive": 5, "group": 817, "name_hash": -1396079429, "name": "l42_154", "mapsquare": 10906, "key": [ 578750625, -1699758530, 981674787, -1430062823 ] }, { "archive": 5, "group": 869, "name_hash": -1152460043, "name": "l53_55", "mapsquare": 13623, "key": [ 1573282650, -1418634195, -1148095035, -1144615238 ] }, { "archive": 5, "group": 871, "name_hash": -1152400462, "name": "l55_54", "mapsquare": 14134, "key": [ -1102558063, 817524163, -892019492, -1799662614 ] }, { "archive": 5, "group": 875, "name_hash": -1152430252, "name": "l54_55", "mapsquare": 13879, "key": [ -489064370, 2145976597, -1121899197, 1908605309 ] }, { "archive": 5, "group": 877, "name_hash": -1152430254, "name": "l54_53", "mapsquare": 13877, "key": [ -2102225396, -2039402279, -1361955467, -263013399 ] }, { "archive": 5, "group": 887, "name_hash": -1152430256, "name": "l54_51", "mapsquare": 13875, "key": [ 1430738652, -42941568, -775986981, 1000814725 ] }, { "archive": 5, "group": 889, "name_hash": -1152430255, "name": "l54_52", "mapsquare": 13876, "key": [ -225305190, 2101410213, -13023121, 617681501 ] }, { "archive": 5, "group": 891, "name_hash": -1396079456, "name": "l42_148", "mapsquare": 10900, "key": [ 305961345, -1339000220, 2053506211, -1898778662 ] }, { "archive": 5, "group": 899, "name_hash": -1395155934, "name": "l43_149", "mapsquare": 11157, "key": [ 1464071264, -103928758, 1681823375, -1178129652 ] }, { "archive": 5, "group": 907, "name_hash": -1154247442, "name": "l35_74", "mapsquare": 9034, "key": [ 385694264, 2091635410, 1191598764, -2130030706 ] }, { "archive": 5, "group": 909, "name_hash": -1153353772, "name": "l44_56", "mapsquare": 11320, "key": [ -1608050593, -413428259, 2059848881, -2038252682 ] }, { "archive": 5, "group": 911, "name_hash": -1394232384, "name": "l44_157", "mapsquare": 11421, "key": [ 1786548109, -823015733, 1963056537, -1997978489 ] }, { "archive": 5, "group": 913, "name_hash": -1153353771, "name": "l44_57", "mapsquare": 11321, "key": [ 1281968466, -998167738, 684914723, -1121178555 ] }, { "archive": 5, "group": 915, "name_hash": -1393308864, "name": "l45_156", "mapsquare": 11676, "key": [ 568565427, 1504215833, 1399137388, 792018103 ] }, { "archive": 5, "group": 921, "name_hash": -1369297347, "name": "l50_148", "mapsquare": 12948, "key": [ 1629198458, 696032791, -1386317415, 445054408 ] }, { "archive": 5, "group": 925, "name_hash": -1154128281, "name": "l39_71", "mapsquare": 10055, "key": [ -1765314579, -920999711, 703810353, 554270505 ] }, { "archive": 5, "group": 931, "name_hash": -1154307113, "name": "l33_48", "mapsquare": 8496, "key": [ 1343253631, 1982572295, -1601521319, 2107012341 ] }, { "archive": 5, "group": 933, "name_hash": -1154307112, "name": "l33_49", "mapsquare": 8497, "key": [ 2127424498, 1622469596, -1889017351, -1927651722 ] }, { "archive": 5, "group": 935, "name_hash": -1154307090, "name": "l33_50", "mapsquare": 8498, "key": [ -1323684593, -1102390619, 1456317314, -2027993898 ] }, { "archive": 5, "group": 937, "name_hash": -1154307089, "name": "l33_51", "mapsquare": 8499, "key": [ 1158999293, 2131770333, -861122128, 1149793504 ] }, { "archive": 5, "group": 939, "name_hash": -1154277322, "name": "l34_48", "mapsquare": 8752, "key": [ 50858443, 684152471, 698071810, -175585203 ] }, { "archive": 5, "group": 941, "name_hash": -1154277321, "name": "l34_49", "mapsquare": 8753, "key": [ 1402352663, -1285733714, 396097692, -321602106 ] }, { "archive": 5, "group": 943, "name_hash": -1154277299, "name": "l34_50", "mapsquare": 8754, "key": [ 1210656247, 128694686, -574675206, -409220905 ] }, { "archive": 5, "group": 945, "name_hash": -1154277298, "name": "l34_51", "mapsquare": 8755, "key": [ -1165798584, 772244190, -402783978, 541755309 ] }, { "archive": 5, "group": 947, "name_hash": -1154247531, "name": "l35_48", "mapsquare": 9008, "key": [ 2084752401, -1394447522, 2084945842, 1474596929 ] }, { "archive": 5, "group": 949, "name_hash": -1154247530, "name": "l35_49", "mapsquare": 9009, "key": [ 259679100, 1332627111, 610006293, 1556652072 ] }, { "archive": 5, "group": 951, "name_hash": -1154247508, "name": "l35_50", "mapsquare": 9010, "key": [ 1975408960, 888933152, -1564560503, 1102048305 ] }, { "archive": 5, "group": 953, "name_hash": -1154247507, "name": "l35_51", "mapsquare": 9011, "key": [ -135370293, 700991137, -1302596410, -1183796338 ] }, { "archive": 5, "group": 955, "name_hash": -1154247506, "name": "l35_52", "mapsquare": 9012, "key": [ 1204696185, -486414605, -1016110497, 134649113 ] }, { "archive": 5, "group": 957, "name_hash": -1154217740, "name": "l36_48", "mapsquare": 9264, "key": [ 1166653059, 1243560308, 851316348, -2052894389 ] }, { "archive": 5, "group": 961, "name_hash": -1154217717, "name": "l36_50", "mapsquare": 9266, "key": [ -829139011, 1967146062, 396751774, 842207093 ] }, { "archive": 5, "group": 963, "name_hash": -1154217716, "name": "l36_51", "mapsquare": 9267, "key": [ 245074215, -415791406, 175943925, 572779390 ] }, { "archive": 5, "group": 965, "name_hash": -1154217654, "name": "l36_71", "mapsquare": 9287, "key": [ 765657606, 140057268, 871191797, -1188018037 ] }, { "archive": 5, "group": 969, "name_hash": -1394232383, "name": "l44_158", "mapsquare": 11422, "key": [ 1604300407, 2105717333, -1276306766, -1021710559 ] }, { "archive": 5, "group": 971, "name_hash": -1152370671, "name": "l56_54", "mapsquare": 14390, "key": [ -1631704360, 1281079278, -1549067757, -2034268623 ] }, { "archive": 5, "group": 973, "name_hash": -1153472936, "name": "l40_56", "mapsquare": 10296, "key": [ 352471788, -1609938193, -266114897, -1904210854 ] }, { "archive": 5, "group": 977, "name_hash": -1153413353, "name": "l42_57", "mapsquare": 10809, "key": [ -256458821, -522789093, -547929725, 1236572696 ] }, { "archive": 5, "group": 981, "name_hash": -1153383562, "name": "l43_57", "mapsquare": 11065, "key": [ -1487802839, 856470179, 1889283583, 1157865105 ] }, { "archive": 5, "group": 987, "name_hash": -1153472935, "name": "l40_57", "mapsquare": 10297, "key": [ -465349039, -896523889, 319217459, -1898915679 ] }, { "archive": 5, "group": 991, "name_hash": -1153443143, "name": "l41_58", "mapsquare": 10554, "key": [ 1188457126, -1729374406, -799893645, 1217674507 ] }, { "archive": 5, "group": 995, "name_hash": -1418243931, "name": "l39_156", "mapsquare": 10140, "key": [ 1403932265, 753452087, 1731472685, -767209354 ] }, { "archive": 5, "group": 1001, "name_hash": -1154158129, "name": "l38_56", "mapsquare": 9784, "key": [ 1874487660, 1635569708, -1949316626, -1920350613 ] }, { "archive": 5, "group": 1003, "name_hash": -1154158128, "name": "l38_57", "mapsquare": 9785, "key": [ -65486938, -1485596203, -1327236566, 408455718 ] }, { "archive": 5, "group": 1005, "name_hash": -1154128337, "name": "l39_57", "mapsquare": 10041, "key": [ 1187862613, -117754960, 130411769, -1192628982 ] }, { "archive": 5, "group": 1007, "name_hash": -1154277230, "name": "l34_77", "mapsquare": 8781, "key": [ -1424107598, 264936559, -56759783, -902529771 ] }, { "archive": 5, "group": 1011, "name_hash": -1154158104, "name": "l38_60", "mapsquare": 9788, "key": [ 432135203, 92254316, -1619550296, 76065412 ] }, { "archive": 5, "group": 1015, "name_hash": -1154128335, "name": "l39_59", "mapsquare": 10043, "key": [ 1912880022, -1723964516, 2062562401, -1162129761 ] }, { "archive": 5, "group": 1017, "name_hash": -1154128313, "name": "l39_60", "mapsquare": 10044, "key": [ -1224862407, 1865311910, 1786772636, 18123079 ] }, { "archive": 5, "group": 1023, "name_hash": -1153472911, "name": "l40_60", "mapsquare": 10300, "key": [ 1607882720, 243924351, 1945561299, -898345204 ] }, { "archive": 5, "group": 1025, "name_hash": -1153472910, "name": "l40_61", "mapsquare": 10301, "key": [ -158146381, 1698585677, -910760378, 815929788 ] }, { "archive": 5, "group": 1031, "name_hash": -1153443119, "name": "l41_61", "mapsquare": 10557, "key": [ -807815903, 958215082, -647601710, 1350072419 ] }, { "archive": 5, "group": 1033, "name_hash": -1153472879, "name": "l40_71", "mapsquare": 10311, "key": [ 1791115, -578135422, 542207831, -1947800181 ] }, { "archive": 5, "group": 1035, "name_hash": -1396079462, "name": "l42_142", "mapsquare": 10894, "key": [ 513365075, 1947090381, 355292589, 1088691299 ] }, { "archive": 5, "group": 1037, "name_hash": -1396079461, "name": "l42_143", "mapsquare": 10895, "key": [ -444031659, -2140928581, -1156609421, -1171551448 ] }, { "archive": 5, "group": 1039, "name_hash": -1153413389, "name": "l42_42", "mapsquare": 10794, "key": [ 311561230, 1919568586, -437925589, -1702919060 ] }, { "archive": 5, "group": 1041, "name_hash": -1153413388, "name": "l42_43", "mapsquare": 10795, "key": [ -725090005, -117997344, 1120373857, 260003830 ] }, { "archive": 5, "group": 1043, "name_hash": -1395155941, "name": "l43_142", "mapsquare": 11150, "key": [ -2076855267, 798384451, -78177982, -255258967 ] }, { "archive": 5, "group": 1045, "name_hash": -1395155940, "name": "l43_143", "mapsquare": 11151, "key": [ 755494593, -1531347130, 657780541, 1078696226 ] }, { "archive": 5, "group": 1047, "name_hash": -1153383598, "name": "l43_42", "mapsquare": 11050, "key": [ -2135414572, 787126361, -721453549, -544629872 ] }, { "archive": 5, "group": 1049, "name_hash": -1153383597, "name": "l43_43", "mapsquare": 11051, "key": [ -509769132, -2037740887, -214716357, 1020989120 ] }, { "archive": 5, "group": 1051, "name_hash": -1153324016, "name": "l45_42", "mapsquare": 11562, "key": [ -353749627, 319334308, -1999130422, 435767930 ] }, { "archive": 5, "group": 1053, "name_hash": -1153472880, "name": "l40_70", "mapsquare": 10310, "key": [ -1690006852, 1771615113, -1889368758, 2144832231 ] }, { "archive": 5, "group": 1055, "name_hash": -1153443089, "name": "l41_70", "mapsquare": 10566, "key": [ -1919693075, -1761530062, -740698891, -697859240 ] }, { "archive": 5, "group": 1057, "name_hash": -1153443088, "name": "l41_71", "mapsquare": 10567, "key": [ 693572165, -414346822, 966267694, -1410395721 ] }, { "archive": 5, "group": 1059, "name_hash": -1154187950, "name": "l37_47", "mapsquare": 9519, "key": [ 859829242, 1044182946, -1333414478, 1254064132 ] }, { "archive": 5, "group": 1069, "name_hash": -1154128336, "name": "l39_58", "mapsquare": 10042, "key": [ 736722071, 75967325, -775764394, 106769937 ] }, { "archive": 5, "group": 1075, "name_hash": -1153413297, "name": "l42_71", "mapsquare": 10823, "key": [ -626951011, -1729338061, -758591782, 439072305 ] }, { "archive": 5, "group": 1087, "name_hash": -1395155904, "name": "l43_158", "mapsquare": 11166, "key": [ 431616711, -1774935949, -1016388232, -1018749983 ] }, { "archive": 5, "group": 1089, "name_hash": -1395155903, "name": "l43_159", "mapsquare": 11167, "key": [ -1959015492, -1104071, -1016386588, 1365103001 ] }, { "archive": 5, "group": 1091, "name_hash": -1153383561, "name": "l43_58", "mapsquare": 11066, "key": [ 291008624, -1992710142, -1831766032, 1925022398 ] }, { "archive": 5, "group": 1093, "name_hash": -1153383560, "name": "l43_59", "mapsquare": 11067, "key": [ 1192664190, -341042618, -820464572, 602716622 ] }, { "archive": 5, "group": 1095, "name_hash": -1153383538, "name": "l43_60", "mapsquare": 11068, "key": [ 1956851882, 1582504141, -1090835806, 1413275925 ] }, { "archive": 5, "group": 1097, "name_hash": -1153353770, "name": "l44_58", "mapsquare": 11322, "key": [ -631747543, -196712359, -1853026593, -1746530974 ] }, { "archive": 5, "group": 1101, "name_hash": -1153413329, "name": "l42_60", "mapsquare": 10812, "key": [ 90198727, 258723745, 748197903, -426406231 ] }, { "archive": 5, "group": 1103, "name_hash": -1365603237, "name": "l54_153", "mapsquare": 13977, "key": [ 692126287, 1949546323, -1604362290, -896929747 ] }, { "archive": 5, "group": 1105, "name_hash": -1152400465, "name": "l55_51", "mapsquare": 14131, "key": [ 1016975630, -301160039, -1036603507, 114074749 ] }, { "archive": 5, "group": 1107, "name_hash": -1397002977, "name": "l41_148", "mapsquare": 10644, "key": [ -1850188218, -1521064184, 1993643055, -554430753 ] }, { "archive": 5, "group": 1109, "name_hash": -1396079457, "name": "l42_147", "mapsquare": 10899, "key": [ 997083034, 1445099511, -1372563312, -1611746786 ] }, { "archive": 5, "group": 1111, "name_hash": -1396079427, "name": "l42_156", "mapsquare": 10908, "key": [ -1337354744, 405036483, 751544363, -1235603400 ] }, { "archive": 5, "group": 1113, "name_hash": -1395155906, "name": "l43_156", "mapsquare": 11164, "key": [ 1660453528, 243788278, -653472483, -435200336 ] }, { "archive": 5, "group": 1115, "name_hash": -1395155905, "name": "l43_157", "mapsquare": 11165, "key": [ -1356346572, -511238576, 1759371669, 874983287 ] }, { "archive": 5, "group": 1117, "name_hash": -1365603235, "name": "l54_155", "mapsquare": 13979, "key": [ 1489416025, -1142280467, 1808198885, 137033499 ] }, { "archive": 5, "group": 1119, "name_hash": -1364679714, "name": "l55_155", "mapsquare": 14235, "key": [ 2114439316, -345963084, -1331796597, 275182841 ] }, { "archive": 5, "group": 1121, "name_hash": -1364679715, "name": "l55_154", "mapsquare": 14234, "key": [ 237295045, 867264725, -1732973561, -1010980224 ] }, { "archive": 5, "group": 1123, "name_hash": -1153472902, "name": "l40_69", "mapsquare": 10309, "key": [ 1450418080, -2118438156, -1409971045, -1573015729 ] }, { "archive": 5, "group": 1125, "name_hash": -1152370670, "name": "l56_55", "mapsquare": 14391, "key": [ 956732210, 1669781219, 1907068552, -1303552057 ] }, { "archive": 5, "group": 1129, "name_hash": -1152340880, "name": "l57_54", "mapsquare": 14646, "key": [ -1240067889, -383570142, -558581485, 786219832 ] }, { "archive": 5, "group": 1131, "name_hash": -1152340879, "name": "l57_55", "mapsquare": 14647, "key": [ -1420591871, -1781801975, 1517176402, -1599286074 ] }, { "archive": 5, "group": 1133, "name_hash": -1154187887, "name": "l37_68", "mapsquare": 9540, "key": [ 2133656685, -1448323734, -310319541, -725382856 ] }, { "archive": 5, "group": 1135, "name_hash": -1154187886, "name": "l37_69", "mapsquare": 9541, "key": [ 478180212, -332955947, -540535501, 2054288254 ] }, { "archive": 5, "group": 1139, "name_hash": -1152281297, "name": "l59_55", "mapsquare": 15159, "key": [ -1251872791, -1612592239, 23799253, 2059004542 ] }, { "archive": 5, "group": 1141, "name_hash": -1389614786, "name": "l49_150", "mapsquare": 12694, "key": [ 1038510617, -584206625, 1955760201, 602453049 ] }, { "archive": 5, "group": 1145, "name_hash": -1154217648, "name": "l36_77", "mapsquare": 9293, "key": [ -1806573381, -426990884, -1261881306, 1666660032 ] }, { "archive": 5, "group": 1147, "name_hash": -1154187857, "name": "l37_77", "mapsquare": 9549, "key": [ 1895759075, -843340286, 1429492719, 295020002 ] }, { "archive": 5, "group": 1149, "name_hash": -1154128275, "name": "l39_77", "mapsquare": 10061, "key": [ -99111554, 1883772725, 1465462119, 1523468364 ] }, { "archive": 5, "group": 1151, "name_hash": -1153472873, "name": "l40_77", "mapsquare": 10317, "key": [ -289613151, -1921022679, 1386892226, 145070936 ] }, { "archive": 5, "group": 1153, "name_hash": -1394232382, "name": "l44_159", "mapsquare": 11423, "key": [ 1414693295, -863955294, -2034093411, 1818980885 ] }, { "archive": 5, "group": 1155, "name_hash": -1393308862, "name": "l45_158", "mapsquare": 11678, "key": [ 350404025, 770298785, 1215100269, -965606935 ] }, { "archive": 5, "group": 1165, "name_hash": -1152430279, "name": "l54_49", "mapsquare": 13873, "key": [ -1098656620, -1288920207, 1465590763, 1004731135 ] }, { "archive": 5, "group": 1167, "name_hash": -1153443080, "name": "l41_79", "mapsquare": 10575, "key": [ -5261221, -598782629, -390496163, -1439990955 ] }, { "archive": 5, "group": 1169, "name_hash": -1153413289, "name": "l42_79", "mapsquare": 10831, "key": [ -1201758536, -1084481964, -461943093, -1283229064 ] }, { "archive": 5, "group": 1173, "name_hash": -1153353769, "name": "l44_59", "mapsquare": 11323, "key": [ -1788142439, -1669893367, 1168059730, -14615580 ] }, { "archive": 5, "group": 1179, "name_hash": -1390538307, "name": "l48_150", "mapsquare": 12438, "key": [ 1545165254, 945298048, 244672985, -2058714291 ] }, { "archive": 5, "group": 1181, "name_hash": -1390538306, "name": "l48_151", "mapsquare": 12439, "key": [ 737806768, -1467792108, 1644942216, -2040381961 ] }, { "archive": 5, "group": 1187, "name_hash": -1152549448, "name": "l50_44", "mapsquare": 12844, "key": [ 2093476291, -208179048, 86110726, -3825057 ] }, { "archive": 5, "group": 1189, "name_hash": -1152549447, "name": "l50_45", "mapsquare": 12845, "key": [ -1421717474, -733669692, 192925225, 1604038145 ] }, { "archive": 5, "group": 1193, "name_hash": -1152519657, "name": "l51_44", "mapsquare": 13100, "key": [ -1830823075, 1215145937, -525456385, -257417215 ] }, { "archive": 5, "group": 1195, "name_hash": -1369297349, "name": "l50_146", "mapsquare": 12946, "key": [ 14919176, -1004409181, -1076064226, 261553907 ] }, { "archive": 5, "group": 1197, "name_hash": -1368373828, "name": "l51_146", "mapsquare": 13202, "key": [ 1474806838, 1338118442, -1146258577, -655181935 ] }, { "archive": 5, "group": 1199, "name_hash": -1152519656, "name": "l51_45", "mapsquare": 13101, "key": [ -1137100843, 1099082163, -1318648712, 1999295524 ] }, { "archive": 5, "group": 1205, "name_hash": -1154247443, "name": "l35_73", "mapsquare": 9033, "key": [ -340750931, -1897990035, 1835646040, 1466647905 ] }, { "archive": 5, "group": 1209, "name_hash": -1364679718, "name": "l55_151", "mapsquare": 14231, "key": [ 1658757346, -2019485271, -1955365006, 895906099 ] }, { "archive": 5, "group": 1211, "name_hash": -1154217741, "name": "l36_47", "mapsquare": 9263, "key": [ -937938813, 1777088166, 1291736808, 1827292190 ] }, { "archive": 5, "group": 1229, "name_hash": -1362832672, "name": "l57_155", "mapsquare": 14747, "key": [ 702153225, 1229706904, 725926382, -1037645198 ] }, { "archive": 5, "group": 1231, "name_hash": -1154366608, "name": "l31_72", "mapsquare": 8008, "key": [ -661798367, -1057089809, -575371589, 936390357 ] }, { "archive": 5, "group": 1233, "name_hash": -1155051796, "name": "l29_77", "mapsquare": 7501, "key": [ 1108184632, 2136992911, -832195021, 495111280 ] }, { "archive": 5, "group": 1235, "name_hash": -1418243929, "name": "l39_158", "mapsquare": 10142, "key": [ 1872120942, -765842959, 449655176, -1168971827 ] }, { "archive": 5, "group": 1237, "name_hash": -1419167450, "name": "l38_158", "mapsquare": 9886, "key": [ -1621103539, 1580907615, -2010887870, 683082929 ] }, { "archive": 5, "group": 1241, "name_hash": -1154396394, "name": "l30_77", "mapsquare": 7757, "key": [ -1928468960, 1136494639, -1967058052, 602105509 ] }, { "archive": 5, "group": 1243, "name_hash": -1154187833, "name": "l37_80", "mapsquare": 9552, "key": [ 1401891457, -1899495202, -773917688, 1780158441 ] }, { "archive": 5, "group": 1245, "name_hash": -1154158042, "name": "l38_80", "mapsquare": 9808, "key": [ -923585668, -574638285, 500509980, -960811077 ] }, { "archive": 5, "group": 1247, "name_hash": -1154366604, "name": "l31_76", "mapsquare": 8012, "key": [ -1309713498, 82935011, -1681291522, -685742249 ] }, { "archive": 5, "group": 1249, "name_hash": -1155051803, "name": "l29_70", "mapsquare": 7494, "key": [ -462058213, -2060727055, 781172327, 1942354602 ] }, { "archive": 5, "group": 1251, "name_hash": -1154187855, "name": "l37_79", "mapsquare": 9551, "key": [ 1566715405, -1011565479, -979871653, -2050870795 ] }, { "archive": 5, "group": 1255, "name_hash": -1154396399, "name": "l30_72", "mapsquare": 7752, "key": [ -1840222511, -1889016815, -1769816321, 1649680459 ] }, { "archive": 5, "group": 1259, "name_hash": -1152460072, "name": "l53_47", "mapsquare": 13615, "key": [ 1478333842, -146576907, -285634093, 361637473 ] }, { "archive": 5, "group": 1261, "name_hash": -1154307019, "name": "l33_79", "mapsquare": 8527, "key": [ -2043864193, 484228359, -673534993, 1382486523 ] }, { "archive": 5, "group": 1263, "name_hash": -1154306997, "name": "l33_80", "mapsquare": 8528, "key": [ -1141049958, 1626814655, 206252937, 924426084 ] }, { "archive": 5, "group": 1267, "name_hash": -1155051826, "name": "l29_68", "mapsquare": 7492, "key": [ -342030979, 1222986839, 511788549, 910172876 ] }, { "archive": 5, "group": 1269, "name_hash": -1154396424, "name": "l30_68", "mapsquare": 7748, "key": [ 1860227470, -290409954, 274550911, -1155783956 ] }, { "archive": 5, "group": 1273, "name_hash": -1154396398, "name": "l30_73", "mapsquare": 7753, "key": [ 1896168736, -1223044138, 779149869, -1759332197 ] }, { "archive": 5, "group": 1275, "name_hash": -1397002954, "name": "l41_150", "mapsquare": 10646, "key": [ 1341282074, 345412696, -2043666158, -457216576 ] }, { "archive": 5, "group": 1277, "name_hash": -1153353707, "name": "l44_79", "mapsquare": 11343, "key": [ 1133993629, 697001923, 34692910, 944315660 ] }, { "archive": 5, "group": 1279, "name_hash": -1153323916, "name": "l45_79", "mapsquare": 11599, "key": [ 858055497, -1049380333, 537304023, -1427503453 ] }, { "archive": 5, "group": 1283, "name_hash": -1152460073, "name": "l53_46", "mapsquare": 13614, "key": [ -1661089893, 60198635, 829308397, -1835163720 ] }, { "archive": 5, "group": 1287, "name_hash": -1367450308, "name": "l52_145", "mapsquare": 13457, "key": [ -445447674, 886051920, 1838544120, -1401595505 ] }, { "archive": 5, "group": 1289, "name_hash": -1367450304, "name": "l52_149", "mapsquare": 13461, "key": [ -126183647, -25151941, 868809662, 1423445530 ] }, { "archive": 5, "group": 1291, "name_hash": -1389614784, "name": "l49_152", "mapsquare": 12696, "key": [ 1955953194, -1456762745, -1903871849, -1325043037 ] }, { "archive": 5, "group": 1299, "name_hash": -1154336841, "name": "l32_69", "mapsquare": 8261, "key": [ -67002792, 1244037268, -1488682033, -924470690 ] }, { "archive": 5, "group": 1301, "name_hash": -1153264340, "name": "l47_73", "mapsquare": 12105, "key": [ 1153497516, -449169462, -635419798, -1471828315 ] }, { "archive": 5, "group": 1303, "name_hash": -1152489866, "name": "l52_44", "mapsquare": 13356, "key": [ -62403158, -259993960, 1312599048, 602862976 ] }, { "archive": 5, "group": 1307, "name_hash": -1153204849, "name": "l49_45", "mapsquare": 12589, "key": [ 1867222552, -1279268543, 219776982, -1133356510 ] }, { "archive": 5, "group": 1315, "name_hash": -1154396393, "name": "l30_78", "mapsquare": 7758, "key": [ -623741461, -108703380, 1436176787, -834745239 ] }, { "archive": 5, "group": 1319, "name_hash": -1153294225, "name": "l46_42", "mapsquare": 11818, "key": [ -2034608097, 1147002393, -720701760, -1045127369 ] }, { "archive": 5, "group": 1323, "name_hash": -1155111353, "name": "l27_81", "mapsquare": 6993, "key": [ 487810245, -1442562976, -1813756859, 201393142 ] }, { "archive": 5, "group": 1325, "name_hash": -1155081594, "name": "l28_70", "mapsquare": 7238, "key": [ -853120516, 1697831503, -12188711, 770411534 ] }, { "archive": 5, "group": 1329, "name_hash": -1392385372, "name": "l46_148", "mapsquare": 11924, "key": [ 1132625310, 132067456, -328236024, 510687418 ] }, { "archive": 5, "group": 1333, "name_hash": -1154158096, "name": "l38_68", "mapsquare": 9796, "key": [ -392461938, 562137949, 1796705877, -650819019 ] }, { "archive": 5, "group": 1335, "name_hash": -1152400488, "name": "l55_49", "mapsquare": 14129, "key": [ 2079314312, -89031344, 495299597, -2042107512 ] }, { "archive": 5, "group": 1337, "name_hash": -1152400466, "name": "l55_50", "mapsquare": 14130, "key": [ 1237228106, -349369148, -1811607373, -882226393 ] }, { "archive": 5, "group": 1339, "name_hash": -1365603240, "name": "l54_150", "mapsquare": 13974, "key": [ -606147120, 1392509835, -1748925993, -2005068147 ] }, { "archive": 5, "group": 1341, "name_hash": -1153353715, "name": "l44_71", "mapsquare": 11335, "key": [ -133492081, -915358326, -714649892, -2093709476 ] }, { "archive": 5, "group": 1343, "name_hash": -1154366602, "name": "l31_78", "mapsquare": 8014, "key": [ 677229402, -1392165236, -1290075373, 1127375497 ] }, { "archive": 5, "group": 1347, "name_hash": -1154307020, "name": "l33_78", "mapsquare": 8526, "key": [ 88056195, 1264510314, -1772486211, 1316184148 ] }, { "archive": 5, "group": 1349, "name_hash": -1154277229, "name": "l34_78", "mapsquare": 8782, "key": [ -1516093424, -688279690, -1680379897, -341995244 ] }, { "archive": 5, "group": 1351, "name_hash": -1154247438, "name": "l35_78", "mapsquare": 9038, "key": [ -1589976068, -956669136, 615859679, -1852090031 ] }, { "archive": 5, "group": 1353, "name_hash": -1154217647, "name": "l36_78", "mapsquare": 9294, "key": [ 123780264, 435568451, -2011105306, 1736505938 ] }, { "archive": 5, "group": 1355, "name_hash": -1154187856, "name": "l37_78", "mapsquare": 9550, "key": [ 1890635758, 373622658, 194491547, -1040541455 ] }, { "archive": 5, "group": 1357, "name_hash": -1153294102, "name": "l46_81", "mapsquare": 11857, "key": [ -1225499683, -2000595018, -612194825, -2014162349 ] }, { "archive": 5, "group": 1359, "name_hash": -1368373800, "name": "l51_153", "mapsquare": 13209, "key": [ -795447804, 1305344381, 98581556, 2008745861 ] }, { "archive": 5, "group": 1361, "name_hash": -1153264311, "name": "l47_81", "mapsquare": 12113, "key": [ 1668148064, 1058673313, -1746348692, 847341704 ] }, { "archive": 5, "group": 1363, "name_hash": -1153234520, "name": "l48_81", "mapsquare": 12369, "key": [ -832628753, 1107609195, 262567666, -1274474550 ] }, { "archive": 5, "group": 1369, "name_hash": -1153443182, "name": "l41_40", "mapsquare": 10536, "key": [ -1644243947, 2011830533, 1900755455, -920681409 ] }, { "archive": 5, "group": 1371, "name_hash": -1153443181, "name": "l41_41", "mapsquare": 10537, "key": [ 1496743910, 592404003, 619983952, 616035197 ] }, { "archive": 5, "group": 1373, "name_hash": -1154247501, "name": "l35_57", "mapsquare": 9017, "key": [ -518439975, -1388520826, 203536985, 1155539914 ] }, { "archive": 5, "group": 1387, "name_hash": -1154217710, "name": "l36_57", "mapsquare": 9273, "key": [ 889946754, -775396017, 49756200, 1934859028 ] }, { "archive": 5, "group": 1391, "name_hash": -1152430251, "name": "l54_56", "mapsquare": 13880, "key": [ -672032867, -104766980, -780173326, -1476119873 ] }, { "archive": 5, "group": 1393, "name_hash": -1152400489, "name": "l55_48", "mapsquare": 14128, "key": [ 1907787432, -241593923, 1152767085, -541422403 ] }, { "archive": 5, "group": 1395, "name_hash": -1152400460, "name": "l55_56", "mapsquare": 14136, "key": [ -1104533465, 1792124817, -1564247177, 79706641 ] }, { "archive": 5, "group": 1397, "name_hash": -1152370669, "name": "l56_56", "mapsquare": 14392, "key": [ 993603906, -1477196579, 593064486, 166369140 ] }, { "archive": 5, "group": 1399, "name_hash": -1152340878, "name": "l57_56", "mapsquare": 14648, "key": [ -1757352130, -1996516705, 263017190, 1647356258 ] }, { "archive": 5, "group": 1401, "name_hash": -1152311088, "name": "l58_55", "mapsquare": 14903, "key": [ -1111240831, 1790345264, -366658712, -331374702 ] }, { "archive": 5, "group": 1403, "name_hash": -1152311087, "name": "l58_56", "mapsquare": 14904, "key": [ -1967472485, -192437954, 1186759929, -2078112945 ] }, { "archive": 5, "group": 1405, "name_hash": -1152281298, "name": "l59_54", "mapsquare": 15158, "key": [ -974248920, -1426940984, -551894435, 1783499939 ] }, { "archive": 5, "group": 1407, "name_hash": -1152281296, "name": "l59_56", "mapsquare": 15160, "key": [ -553466395, -1372948696, -1385390322, 710626387 ] }, { "archive": 5, "group": 1409, "name_hash": -1153264337, "name": "l47_76", "mapsquare": 12108, "key": [ 399049703, -1446144051, 1676451131, -860006347 ] }, { "archive": 5, "group": 1411, "name_hash": -1153264309, "name": "l47_83", "mapsquare": 12115, "key": [ 2146362785, 1996258091, -613445101, 539756591 ] }, { "archive": 5, "group": 1413, "name_hash": -1154336814, "name": "l32_75", "mapsquare": 8267, "key": [ -474856962, -1295973639, 789657650, -1769058462 ] }, { "archive": 5, "group": 1415, "name_hash": -1154217677, "name": "l36_69", "mapsquare": 9285, "key": [ -1376572370, -121986361, -1862348009, -378224358 ] }, { "archive": 5, "group": 1417, "name_hash": -1153264343, "name": "l47_70", "mapsquare": 12102, "key": [ -941472423, -2139246656, -735816304, -814413277 ] }, { "archive": 5, "group": 1419, "name_hash": -1154396423, "name": "l30_69", "mapsquare": 7749, "key": [ 1999726229, -1872460093, 328701683, 200119624 ] }, { "archive": 5, "group": 1421, "name_hash": -1151625926, "name": "l60_45", "mapsquare": 15405, "key": [ 1745315296, 704015968, -1726703759, 250744336 ] }, { "archive": 5, "group": 1423, "name_hash": -1151625925, "name": "l60_46", "mapsquare": 15406, "key": [ 900232781, 1037303290, 2099698517, -1839409134 ] }, { "archive": 5, "group": 1425, "name_hash": -1151625924, "name": "l60_47", "mapsquare": 15407, "key": [ -1139819812, 165771634, 462281145, -1626973480 ] }, { "archive": 5, "group": 1427, "name_hash": -1155111376, "name": "l27_79", "mapsquare": 6991, "key": [ -1448188175, 626198836, 1985989165, -1062391664 ] }, { "archive": 5, "group": 1429, "name_hash": -1155081585, "name": "l28_79", "mapsquare": 7247, "key": [ -122734679, -1029771956, 1633683499, 846700218 ] }, { "archive": 5, "group": 1431, "name_hash": -1154336850, "name": "l32_60", "mapsquare": 8252, "key": [ 983685450, -269686274, -481846473, 958878700 ] }, { "archive": 5, "group": 1433, "name_hash": -1154336849, "name": "l32_61", "mapsquare": 8253, "key": [ 369853627, -1436579511, 1227029352, -308778567 ] }, { "archive": 5, "group": 1435, "name_hash": -1154307059, "name": "l33_60", "mapsquare": 8508, "key": [ 832419339, -645747292, -1840672604, -1762366129 ] }, { "archive": 5, "group": 1437, "name_hash": -1154307058, "name": "l33_61", "mapsquare": 8509, "key": [ 1945632596, 8962682, 613374775, -649841921 ] }, { "archive": 5, "group": 1439, "name_hash": -1154277290, "name": "l34_59", "mapsquare": 8763, "key": [ -161594592, -1041269457, 871932542, -908444460 ] }, { "archive": 5, "group": 1441, "name_hash": -1421014468, "name": "l36_161", "mapsquare": 9377, "key": [ -919539010, 1637177409, -1552993082, -1871277584 ] }, { "archive": 5, "group": 1443, "name_hash": -1154336874, "name": "l32_57", "mapsquare": 8249, "key": [ -18753106, -1810101920, -1918824309, 1059212918 ] }, { "archive": 5, "group": 1445, "name_hash": -1154336873, "name": "l32_58", "mapsquare": 8250, "key": [ -282754239, 1300996433, 1312184796, 1030502655 ] }, { "archive": 5, "group": 1447, "name_hash": -1154336872, "name": "l32_59", "mapsquare": 8251, "key": [ 1142567601, 1650263019, 49659607, -899246535 ] }, { "archive": 5, "group": 1449, "name_hash": -1154307083, "name": "l33_57", "mapsquare": 8505, "key": [ -962353533, -1179227127, 370654716, -532704544 ] }, { "archive": 5, "group": 1451, "name_hash": -1154307082, "name": "l33_58", "mapsquare": 8506, "key": [ -1227747958, 1491928866, 1293676120, 2065870654 ] }, { "archive": 5, "group": 1453, "name_hash": -1154307081, "name": "l33_59", "mapsquare": 8507, "key": [ 1765022148, -80255262, -725852452, -1286377093 ] }, { "archive": 5, "group": 1455, "name_hash": -1154277292, "name": "l34_57", "mapsquare": 8761, "key": [ 1760753095, -1427813077, 266675077, -255782653 ] }, { "archive": 5, "group": 1457, "name_hash": -1154277291, "name": "l34_58", "mapsquare": 8762, "key": [ -1868090282, -1544166661, -731821081, 1736861342 ] }, { "archive": 5, "group": 1459, "name_hash": -1154277268, "name": "l34_60", "mapsquare": 8764, "key": [ 1880374454, 1213514336, -2078057600, -2021473694 ] }, { "archive": 5, "group": 1461, "name_hash": -1154277267, "name": "l34_61", "mapsquare": 8765, "key": [ 1948979312, 134066799, -78879371, -323055687 ] }, { "archive": 5, "group": 1463, "name_hash": -1154247499, "name": "l35_59", "mapsquare": 9019, "key": [ -1293726217, -1696419000, -859948399, 1059527576 ] }, { "archive": 5, "group": 1465, "name_hash": -1154247477, "name": "l35_60", "mapsquare": 9020, "key": [ -1906040194, 849367381, 2098246444, 1605043698 ] }, { "archive": 5, "group": 1467, "name_hash": -1154247476, "name": "l35_61", "mapsquare": 9021, "key": [ -665917952, -432697139, -602494723, -2097936281 ] }, { "archive": 5, "group": 1469, "name_hash": -1153472973, "name": "l40_40", "mapsquare": 10280, "key": [ -1245893544, -118079793, -1624599660, -626968532 ] }, { "archive": 5, "group": 1471, "name_hash": -1153472972, "name": "l40_41", "mapsquare": 10281, "key": [ 2129832996, -1542613289, -1612399066, -323592936 ] }, { "archive": 5, "group": 1473, "name_hash": -1153472971, "name": "l40_42", "mapsquare": 10282, "key": [ 667792020, 292003472, -998896147, 7080342 ] }, { "archive": 5, "group": 1475, "name_hash": -1153413391, "name": "l42_40", "mapsquare": 10792, "key": [ -125671574, 254943715, 1343095705, -1965171670 ] }, { "archive": 5, "group": 1477, "name_hash": -1153413328, "name": "l42_61", "mapsquare": 10813, "key": [ 78604466, -1582607088, 691011324, -1155733133 ] }, { "archive": 5, "group": 1479, "name_hash": -1153383537, "name": "l43_61", "mapsquare": 11069, "key": [ 281072607, -1791037221, -1221626554, 801634958 ] }, { "archive": 5, "group": 1481, "name_hash": -1154158071, "name": "l38_72", "mapsquare": 9800, "key": [ 1648231074, -690523856, 538076864, -489360965 ] }, { "archive": 5, "group": 1483, "name_hash": -1154307021, "name": "l33_77", "mapsquare": 8525, "key": [ 1448703859, 604202172, 1058795271, -80742538 ] }, { "archive": 5, "group": 1485, "name_hash": -1154217742, "name": "l36_46", "mapsquare": 9262, "key": [ -1327237404, -257519940, -1398355053, -970581711 ] }, { "archive": 5, "group": 1487, "name_hash": -1154187951, "name": "l37_46", "mapsquare": 9518, "key": [ -557639005, 398566091, -133936467, -850662847 ] }, { "archive": 5, "group": 1489, "name_hash": -1153472970, "name": "l40_43", "mapsquare": 10283, "key": [ -2133080221, 1327669620, 173304076, -151662318 ] }, { "archive": 5, "group": 1491, "name_hash": -1153383600, "name": "l43_40", "mapsquare": 11048, "key": [ 1543404580, -1789044122, -804862998, -191419192 ] }, { "archive": 5, "group": 1493, "name_hash": -1153353809, "name": "l44_40", "mapsquare": 11304, "key": [ -1986328953, 48921946, 1888453159, 1692551214 ] }, { "archive": 5, "group": 1495, "name_hash": -1153324018, "name": "l45_40", "mapsquare": 11560, "key": [ 1374555285, 1132619461, 1914865049, 300933991 ] }, { "archive": 5, "group": 1497, "name_hash": -1153294227, "name": "l46_40", "mapsquare": 11816, "key": [ 1299071975, 1506387508, -918208606, -872063692 ] }, { "archive": 5, "group": 1499, "name_hash": -1153294226, "name": "l46_41", "mapsquare": 11817, "key": [ 740863947, -1459447536, -1272357052, -1730407126 ] }, { "archive": 5, "group": 1501, "name_hash": -1153294224, "name": "l46_43", "mapsquare": 11819, "key": [ -1632272271, -1134214079, -1204220834, -1633494895 ] }, { "archive": 5, "group": 1503, "name_hash": -1153264436, "name": "l47_40", "mapsquare": 12072, "key": [ -124946691, 1378984061, -50235700, -1502078778 ] }, { "archive": 5, "group": 1505, "name_hash": -1153264435, "name": "l47_41", "mapsquare": 12073, "key": [ 213992849, 1682100343, -1120497450, 1116115229 ] }, { "archive": 5, "group": 1507, "name_hash": -1153264434, "name": "l47_42", "mapsquare": 12074, "key": [ 542527392, 769075547, 321487883, 1548595060 ] }, { "archive": 5, "group": 1509, "name_hash": -1153264433, "name": "l47_43", "mapsquare": 12075, "key": [ 186822346, 1877444331, -1687073745, -531331776 ] }, { "archive": 5, "group": 1511, "name_hash": -1152460042, "name": "l53_56", "mapsquare": 13624, "key": [ 2075882635, 144612382, -11140999, 1068051873 ] }, { "archive": 5, "group": 1513, "name_hash": -1152460041, "name": "l53_57", "mapsquare": 13625, "key": [ -1688194808, -751145145, -64310690, -9400824 ] }, { "archive": 5, "group": 1515, "name_hash": -1152460040, "name": "l53_58", "mapsquare": 13626, "key": [ 2091552269, -19993197, 919014338, -2088295508 ] }, { "archive": 5, "group": 1517, "name_hash": -1152460039, "name": "l53_59", "mapsquare": 13627, "key": [ 341129543, 405594157, -935930552, 1362472171 ] }, { "archive": 5, "group": 1519, "name_hash": -1152460017, "name": "l53_60", "mapsquare": 13628, "key": [ 83847925, -1748584368, -1828230097, 915424426 ] }, { "archive": 5, "group": 1521, "name_hash": -1152460016, "name": "l53_61", "mapsquare": 13629, "key": [ -1526659316, 194744161, -1617203167, 1822755797 ] }, { "archive": 5, "group": 1523, "name_hash": -1152460015, "name": "l53_62", "mapsquare": 13630, "key": [ -1632016999, -2015192780, -2137722473, 477794767 ] }, { "archive": 5, "group": 1525, "name_hash": -1154187862, "name": "l37_72", "mapsquare": 9544, "key": [ 1190108828, -1694672329, 922227461, 627710655 ] }, { "archive": 5, "group": 1527, "name_hash": -1152400464, "name": "l55_52", "mapsquare": 14132, "key": [ 1385901079, -1626256839, 2140816760, -1871930940 ] }, { "archive": 5, "group": 1529, "name_hash": -1363756198, "name": "l56_150", "mapsquare": 14486, "key": [ 801004021, 788873243, -1673638404, 152435782 ] }, { "archive": 5, "group": 1531, "name_hash": -1363756197, "name": "l56_151", "mapsquare": 14487, "key": [ 1281568133, 1646126427, 1139545719, -1051778369 ] }, { "archive": 5, "group": 1533, "name_hash": -1152370697, "name": "l56_49", "mapsquare": 14385, "key": [ -338068420, -1043269144, -515762209, 327851952 ] }, { "archive": 5, "group": 1535, "name_hash": -1152370675, "name": "l56_50", "mapsquare": 14386, "key": [ -1730596947, 85732186, -1776102738, -2096470688 ] }, { "archive": 5, "group": 1537, "name_hash": -1152370674, "name": "l56_51", "mapsquare": 14387, "key": [ -327431537, -279720677, 1848501675, 306634513 ] }, { "archive": 5, "group": 1539, "name_hash": -1152370673, "name": "l56_52", "mapsquare": 14388, "key": [ 1061380274, -429796323, -221565610, -967547362 ] }, { "archive": 5, "group": 1541, "name_hash": -1154217646, "name": "l36_79", "mapsquare": 9295, "key": [ -128031646, -1131908397, -1360861665, 1112176197 ] }, { "archive": 5, "group": 1543, "name_hash": -1154396370, "name": "l30_80", "mapsquare": 7760, "key": [ 2042192998, 743878512, -1804236758, -1160501924 ] }, { "archive": 5, "group": 1545, "name_hash": -1396079428, "name": "l42_155", "mapsquare": 10907, "key": [ 1703225431, -1175749730, 1842579622, -1861290070 ] }, { "archive": 5, "group": 1547, "name_hash": -1155051797, "name": "l29_76", "mapsquare": 7500, "key": [ -1016974560, 452375550, 1138391999, 85568702 ] }, { "archive": 5, "group": 1549, "name_hash": -1154396367, "name": "l30_83", "mapsquare": 7763, "key": [ -170495210, -1183252846, 1190019697, 650832746 ] } ] ================================================ FILE: data/saves/.gitkeep ================================================ ================================================ FILE: docker-compose.yml ================================================ services: runejs_game_server: build: context: . dockerfile: Dockerfile volumes: - ./data:/usr/src/app/data - ./cache:/usr/src/app/cache - ./config:/usr/src/app/config ports: - "43594:43594" networks: default: name: runejs_network ================================================ FILE: jest.config.ts ================================================ /* * For a detailed explanation regarding each configuration property and type check, visit: * https://jestjs.io/docs/configuration */ export default { // All imported modules in your tests should be mocked automatically // automock: false, // Stop running tests after `n` failures // bail: 0, // The directory where Jest should store its cached dependency information // cacheDirectory: "C:\\Users\\james\\AppData\\Local\\Temp\\jest", // Automatically clear mock calls, instances, contexts and results before every test clearMocks: true, // Indicates whether the coverage information should be collected while executing the test collectCoverage: true, // An array of glob patterns indicating a set of files for which coverage information should be collected // collectCoverageFrom: undefined, // The directory where Jest should output its coverage files coverageDirectory: 'coverage', // An array of regexp pattern strings used to skip coverage collection // coveragePathIgnorePatterns: [ // "\\\\node_modules\\\\" // ], roots: ['/src'], moduleNameMapper: { '@engine/(.*)': '/src/engine/$1', '@server/(.*)': '/src/server/$1', '@plugins/(.*)': '/src/plugins/$1', }, // Indicates which provider should be used to instrument code for coverage // coverageProvider: "babel", // A list of reporter names that Jest uses when writing coverage reports // coverageReporters: [ // "json", // "text", // "lcov", // "clover" // ], // An object that configures minimum threshold enforcement for coverage results // coverageThreshold: undefined, // A path to a custom dependency extractor // dependencyExtractor: undefined, // Make calling deprecated APIs throw helpful error messages // errorOnDeprecated: false, // The default configuration for fake timers // fakeTimers: { // "enableGlobally": false // }, // Force coverage collection from ignored files using an array of glob patterns // forceCoverageMatch: [], // A path to a module which exports an async function that is triggered once before all test suites // globalSetup: undefined, // A path to a module which exports an async function that is triggered once after all test suites // globalTeardown: undefined, // A set of global variables that need to be available in all test environments // globals: {}, // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. // maxWorkers: "50%", // An array of directory names to be searched recursively up from the requiring module's location // moduleDirectories: [ // "node_modules" // ], // An array of file extensions your modules use // moduleFileExtensions: [ // "js", // "mjs", // "cjs", // "jsx", // "ts", // "tsx", // "json", // "node" // ], // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module // moduleNameMapper: {}, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // modulePathIgnorePatterns: [], // Activates notifications for test results // notify: false, // An enum that specifies notification mode. Requires { notify: true } // notifyMode: "failure-change", // A preset that is used as a base for Jest's configuration preset: 'ts-jest', // Run tests from one or more projects // projects: undefined, // Use this configuration option to add custom reporters to Jest // reporters: undefined, // Automatically reset mock state before every test // resetMocks: false, // Reset the module registry before running each individual test // resetModules: false, // A path to a custom resolver // resolver: undefined, // Automatically restore mock state and implementation before every test // restoreMocks: false, // The root directory that Jest should scan for tests and modules within // rootDir: undefined, // A list of paths to directories that Jest should use to search for files in // roots: [ // "" // ], // Allows you to use a custom runner instead of Jest's default test runner // runner: "jest-runner", // The paths to modules that run some code to configure or set up the testing environment before each test // setupFiles: [], // A list of paths to modules that run some code to configure or set up the testing framework before each test // setupFilesAfterEnv: [], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, // A list of paths to snapshot serializer modules Jest should use for snapshot testing // snapshotSerializers: [], // The test environment that will be used for testing testEnvironment: 'node', // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, // Adds a location field to test results // testLocationInResults: false, // The glob patterns Jest uses to detect test files testMatch: ['**/*.test.ts'], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // testPathIgnorePatterns: [ // "\\\\node_modules\\\\" // ], // The regexp pattern or array of patterns that Jest uses to detect test files // testRegex: [], // This option allows the use of a custom results processor // testResultsProcessor: undefined, // This option allows use of a custom test runner // testRunner: "jest-circus/runner", // A map from regular expressions to paths to transformers // transform: undefined, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation // transformIgnorePatterns: [ // "\\\\node_modules\\\\", // "\\.pnp\\.[^\\\\]+$" // ], // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, // Indicates whether each individual test should be reported during the run // verbose: undefined, // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode // watchPathIgnorePatterns: [], // Whether to use watchman for file crawling // watchman: true, }; ================================================ FILE: nodemon.json ================================================ { "verbose": true, "ignore": ["src/plugins/"], "watch": ["src/"], "ext": "ts, js" } ================================================ FILE: package.json ================================================ { "name": "@runejs/server", "version": "1.0.0-alpha.3", "description": "A RuneScape game server emulator written in TypeScript.", "main": "dist/index.js", "scripts": { "start": "npm run build && concurrently \"npm run build:watch\" \"npm run start:infra\" \"npm run start:game\"", "start:game": "nodemon --delay 5000ms --max-old-space-size=2048 dist/server/runner.js -- -game", "start:game:dev": "npm run build && concurrently \"npm run build:watch\" \"npm run start:game\"", "start:login": "node --max-old-space-size=1024 dist/server/runner.js -- -login", "start:update": "node --max-old-space-size=1024 dist/server/runner.js -- -update", "start:infra": "concurrently \"npm run start:update\" \"npm run start:login\"", "start:standalone": "concurrently \"npm run start:infra\" \"npm run start:game\"", "game": "npm run start:game", "game:dev": "npm run start:game:dev", "login": "npm run start:login", "update": "npm run start:update", "infra": "npm run start:infra", "standalone": "npm run start:standalone", "build": "rimraf dist && swc ./src -d dist --strip-leading-paths", "build:watch": "swc ./src -d dist -w --strip-leading-paths", "lint": "biome lint", "lint:fin": "biome lint --write --diagnostic-level=error --reporter=summary", "lint:fix": "biome lint --write", "format": "biome format", "format:fin": "biome format --write --reporter=summary", "format:fix": "biome format --write", "fin": "npm run typecheck && npm run lint:fin && npm run format:fin && npm run test:fin", "test": "jest", "test:fin": "jest --silent --reporters=\"summary\"", "typecheck": "tsc -p ./ --noEmit" }, "repository": { "type": "git", "url": "git+ssh://git@github.com/runejs/server.git" }, "keywords": [ "runejs", "runescape", "typescript", "game server", "game engine" ], "author": "Tynarus", "license": "GPL-3.0", "bugs": { "url": "https://github.com/runejs/server/issues" }, "homepage": "https://github.com/runejs/server#readme", "dependencies": { "@runejs/common": "2.0.2-beta.3", "@runejs/filestore": "0.17.0", "@runejs/login-server": "2.1.0", "@runejs/update-server": "1.4.0", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lodash": "^4.17.21", "quadtree-lib": "^1.0.9", "rxjs": "^7.8.1", "source-map-support": "^0.5.21", "tslib": "^2.8.1", "uuid": "^11.0.5" }, "devDependencies": { "@biomejs/biome": "1.9.4", "@swc/cli": "^0.8.1", "@swc/core": "^1.10.9", "@types/jest": "^29.5.14", "@types/js-yaml": "^4.0.9", "@types/lodash": "^4.17.14", "@types/node": "^22.10.8", "@types/uuid": "^10.0.0", "chokidar": "^5.0.0", "concurrently": "^9.1.2", "jest": "^29.7.0", "nodemon": "^3.1.9", "rimraf": "^6.0.1", "ts-jest": "^29.4.9", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5.7.3" } } ================================================ FILE: src/engine/action/action-pipeline.ts ================================================ import type { ActionHook } from '@engine/action/hook/action-hook'; import { TaskExecutor } from '@engine/action/hook/task'; import type { Actor } from '@engine/world/actor/actor'; import { isPlayer } from '@engine/world/actor/util'; import { logger } from '@runejs/common'; import type { Subscription } from 'rxjs'; /** * The priority of an queueable action within the pipeline. */ export type ActionStrength = 'weak' | 'normal' | 'strong'; /** * Content action type definitions. */ export type ActionType = | 'button' | 'widget_interaction' | 'npc_init' | 'npc_interaction' | 'object_interaction' | 'item_interaction' | 'item_on_object' | 'item_on_npc' | 'item_on_player' | 'item_on_item' | 'item_on_world_item' | 'item_swap' | 'move_item' | 'spawned_item_interaction' | 'magic_on_item' | 'magic_on_player' | 'magic_on_npc' | 'player_init' | 'player_command' | 'player_interaction' | 'region_change' | 'equipment_change' | 'prayer'; export const gentleActions: ActionType[] = [ 'button', 'widget_interaction', 'player_init', 'npc_init', 'move_item', 'item_swap', 'player_command', 'region_change', ]; /** * Methods in which action hooks in progress may be cancelled. */ export type ActionCancelType = 'manual-movement' | 'pathing-movement' | 'generic' | 'keep-widgets-open' | 'button' | 'widget'; /** * The definition for the actual action pipe handler function. */ export type ActionPipeHandler = (...args: any[]) => RunnableHooks | void; /** * Basic definition of a game engine action file (.action.ts exports). */ export type ActionPipe = [ActionType, ActionPipeHandler]; /** * A list of filtered hooks for an actor to run. */ export interface RunnableHooks { // The action in progress action: T; // Matching action hooks hooks?: ActionHook[]; } /** * A specific actor's action pipeline handler. * Records action pipes and distributes content actions from the game engine down to execute plugin hooks. */ export class ActionPipeline { private static pipes = new Map(); private runningTasks: TaskExecutor[] = []; private canceling: boolean = false; private movementSubscription: Subscription; public constructor(public readonly actor: Actor) { this.movementSubscription = this.actor.walkingQueue.movementQueued$.subscribe(async () => this.cancelRunningTasks()); } public static getPipe(action: ActionType): ActionPipeHandler | null { return ActionPipeline.pipes.get(action) || null; } public static register(action: ActionType, actionPipeHandlerFn: ActionPipeHandler): void { ActionPipeline.pipes.set(action.toString(), actionPipeHandlerFn); } public shutdown(): void { this.movementSubscription.unsubscribe(); } public async call(action: ActionType, ...args: any[]): Promise { const actionHandler = ActionPipeline.pipes.get(action.toString()); if (actionHandler) { try { await this.runActionHandler(actionHandler, args); } catch (error) { if (error) { logger.error(`Error handling action ${action.toString()}`); logger.error(error); } } } } public async cancelRunningTasks(): Promise { if (this.canceling || !this.runningTasks || this.runningTasks.length === 0) { return; } this.canceling = true; for (const runningTask of this.runningTasks) { if (runningTask.running) { await runningTask.stop(); } } // Remove all tasks this.runningTasks = []; this.canceling = false; } private async runActionHandler(actionHandler: any, args: any[]): Promise { const runnableHooks: RunnableHooks | null | undefined = await actionHandler(...args); if (!runnableHooks?.hooks || runnableHooks.hooks.length === 0) { return; } for (let i = 0; i < runnableHooks.hooks.length; i++) { const hook = runnableHooks.hooks[i]; if (!hook) { continue; } // Some actions are non-cancelling if (gentleActions.indexOf(hook.type) === -1) { await this.cancelRunningTasks(); } await this.runHook(hook, runnableHooks.action); if (!hook.multi) { // If the highest priority hook does not allow other hooks // to run during this same action, then return here to break // out of the loop and complete execution. return; } } } private async runHook(actionHook: ActionHook, action: any): Promise { const { handler, task } = actionHook; if (task) { // Schedule task-based hook const taskExecutor = new TaskExecutor(this.actor, task, actionHook, action); this.runningTasks.push(taskExecutor); // Run the task until complete await taskExecutor.run(); // Cleanup and remove the task once completed const taskIdx = this.runningTasks.findIndex(task => task.taskId === taskExecutor.taskId); if (taskIdx !== -1) { this.runningTasks.splice(taskIdx, 1); } } else if (handler) { // Run basic hook await handler(action); } } public get paused(): boolean { if (isPlayer(this.actor)) { if (this.actor.interfaceState.widgetOpen()) { return true; } } return false; } } ================================================ FILE: src/engine/action/hook/action-hook.ts ================================================ import type { ActionStrength, ActionType } from '@engine/action/action-pipeline'; import type { HookTask } from '@engine/action/hook/task'; import type { QuestKey } from '@engine/config/quest-config'; import { actionHookMap } from '@engine/plugins/loader'; /** * Defines a quest requirement for an action hook. */ export interface QuestRequirement { questId: string; stage?: QuestKey; stages?: number[]; } /** * Defines a generic extensible game content action hook. */ export interface ActionHook { // The type of action to perform type: ActionType; // Whether or not this hook will allow other hooks from the same action to queue after it multi?: boolean; // The action's priority over other actions priority?: number; // The strength of the action hook strength?: ActionStrength; // [optional] Quest requirements that must be completed in order to run this hook questRequirement?: QuestRequirement; // [optional] The action function to be performed handler?: H; // [optional] The task to be performed task?: HookTask; } /** * Fetches the list of all discovered action hooks of the specified type. * @param actionType The Action Type to find the hook for. * @param filter [optional] Filter criteria to apply to the returned list. */ export const getActionHooks = (actionType: ActionType, filter?: (actionHook: T) => boolean): T[] => { const hooks = actionHookMap[actionType] as T[]; if (!hooks || hooks.length === 0) { return []; } return filter ? hooks.filter(filter) : hooks; }; /** * A sorter function that action hooks can be run through. * Action hooks will be sorted by those with quest requirements firstly, and the rest thereafter. * @param actionHooks The list of hooks to sort. */ export function sortActionHooks(actionHooks: ActionHook[]): ActionHook[] { return actionHooks.sort(actionHook => (actionHook.questRequirement !== undefined ? -1 : 1)); } ================================================ FILE: src/engine/action/hook/hook-filters.test.ts ================================================ import { advancedNumberHookFilter } from './hook-filters'; describe('action/hook hook filters', () => { describe('advancedNumberHookFilter', () => { describe('when expected is a number array', () => { const expected = [1, 2, 3]; describe('when input is in the array', () => { const input = 2; test('should return true', () => { const result = advancedNumberHookFilter(expected, input); expect(result).toEqual(true); }); }); describe('when input is not in the array', () => { const input = 999; test('should return false', () => { const result = advancedNumberHookFilter(expected, input); expect(result).toEqual(false); }); }); }); }); }); ================================================ FILE: src/engine/action/hook/hook-filters.ts ================================================ import type { ActionHook } from '@engine/action/hook/action-hook'; import type { Player } from '@engine/world/actor/player/player'; export const stringHookFilter = (expected: string | string[], input: string): boolean => { if (Array.isArray(expected)) { if (expected.indexOf(input) === -1) { return false; } } else { if (expected !== input) { return false; } } return true; }; export const numberHookFilter = (expected: number | number[], input: number): boolean => { if (Array.isArray(expected)) { if (expected.indexOf(input) === -1) { return false; } } else { if (expected !== input) { return false; } } return true; }; export const advancedNumberHookFilter = ( expected: number | number[], input: number, options?: string | string[], searchOption?: string, ): boolean => { if (expected !== undefined) { if (Array.isArray(expected)) { if (expected.indexOf(input) === -1) { return false; } } else { if (expected !== input) { return false; } } } if (options !== undefined && searchOption !== undefined) { if (Array.isArray(options)) { return options.indexOf(searchOption) !== -1; } else { return options === searchOption; } } else { return true; } }; /** * A quest requirement filter for hooks that uses the hook's `questRequirements` object. * @param player The player involved with the hook. * @param actionHook The action hook definition to filter. */ export function questHookFilter(player: Player, actionHook: ActionHook): boolean { if (!actionHook.questRequirement) { return true; } const questId = actionHook.questRequirement.questId; const playerQuest = player.quests.find(quest => quest.questId === questId); if (!playerQuest) { // @TODO quest requirements return actionHook.questRequirement.stage === 0; } if (actionHook.questRequirement.stage === 'complete') { return playerQuest.progress === 'complete'; } if (typeof playerQuest.progress === 'number') { if (actionHook.questRequirement.stage !== undefined) { if (!numberHookFilter(actionHook.questRequirement.stage, playerQuest.progress)) { return false; } } else if (actionHook.questRequirement.stages !== undefined) { if (!numberHookFilter(actionHook.questRequirement.stages, playerQuest.progress)) { return false; } } } return playerQuest.progress === actionHook.questRequirement.stage; } ================================================ FILE: src/engine/action/hook/task.ts ================================================ import type { Subscription } from 'rxjs'; import { lastValueFrom, timer } from 'rxjs'; import { v4 } from 'uuid'; import { logger } from '@runejs/common'; import type { ActionStrength } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import type { Actor } from '@engine/world/actor/actor'; import type { Npc } from '@engine/world/actor/npc'; import type { Player } from '@engine/world/actor/player/player'; import { isNpc, isPlayer } from '@engine/world/actor/util'; import { World } from '@engine/world/world'; export type TaskSessionData = { [key: string]: any }; export interface TaskDetails { actor: Actor; player: Player | undefined; npc: Npc | undefined; actionData: T; session: TaskSessionData; } export interface HookTask { canActivate?: (task: TaskExecutor, iteration?: number) => boolean | Promise; activate: (task: TaskExecutor, iteration?: number) => void | undefined | boolean | Promise; onComplete?: (task: TaskExecutor, iteration?: number) => void | Promise; delay?: number; // # of ticks before execution delayMs?: number; // # of milliseconds before execution interval?: number; // # of ticks between loop intervals (defaults to single run task) intervalMs?: number; // # of milliseconds between loop intervals (defaults to single run task) } // T = current action info (ButtonAction, MoveItemAction, etc) export class TaskExecutor { public readonly taskId = v4(); public readonly strength: ActionStrength; public running: boolean = false; public session: TaskSessionData = {}; // a session store to use for the lifetime of the task private iteration: number = 0; private intervalSubscription: Subscription; public constructor( public readonly actor: Actor, public readonly task: HookTask, public readonly hook: ActionHook, public readonly actionData: T, ) { this.strength = this.hook.strength || 'normal'; } public async run(): Promise { this.running = true; if (!!this.task.delay || !!this.task.delayMs) { await lastValueFrom(timer(this.task.delayMs !== undefined ? this.task.delayMs : this.task.delay! * World.TICK_LENGTH)); } if (!!this.task.interval || !!this.task.intervalMs) { // Looping execution task const intervalMs = this.task.intervalMs !== undefined ? this.task.intervalMs : this.task.interval! * World.TICK_LENGTH; await new Promise(resolve => { this.intervalSubscription = timer(0, intervalMs).subscribe( async () => { if (!(await this.execute())) { this.intervalSubscription?.unsubscribe(); resolve(); } }, error => { logger.error(error); resolve(); }, () => resolve(), ); }); } else { // Single execution task await this.execute(); } if (this.running) { await this.stop(); } } public async execute(): Promise { if (!this.actor) { // Actor destroyed, cancel the task return false; } if (!(await this.canActivate())) { // Unable to activate the task, cancel return false; } if (this.actor.actionPipeline.paused) { // Action paused, continue loop if applicable return true; } if (!this.running) { // Task no longer running, cancel execution return false; } try { const response = await this.task.activate(this, this.iteration++); return typeof response === 'boolean' ? response : true; } catch (error) { logger.error(`Error executing action task`); logger.error(error); return false; } } public async canActivate(): Promise { if (!this.valid) { return false; } if (!this.task.canActivate) { return true; } try { return this.task.canActivate(this, this.iteration); } catch (error) { logger.error(`Error calling action canActivate`, this.task); logger.error(error); return false; } } public async stop(): Promise { this.running = false; this.intervalSubscription?.unsubscribe(); if (this.task?.onComplete) { await this.task.onComplete(this, this.iteration); } } public getDetails(): TaskDetails { return { actor: this.actor, player: isPlayer(this.actor) ? this.actor : undefined, npc: isNpc(this.actor) ? this.actor : undefined, actionData: this.actionData, session: this.session, }; } public get valid(): boolean { return !!this.task?.activate && !!this.actionData; } } ================================================ FILE: src/engine/action/loader.ts ================================================ import { join } from 'path'; import type { ActionPipe } from '@engine/action/action-pipeline'; import { ActionPipeline } from '@engine/action/action-pipeline'; import { BUILD_DIR } from '@engine/config/directories'; import { getFiles } from '@engine/util/files'; import { logger } from '@runejs/common'; /** * Finds and loads all available action pipe files (`*.action.ts`). */ export async function loadActionFiles(): Promise { const ACTION_DIRECTORY = join(BUILD_DIR, 'action'); const PIPE_DIRECTORY = join(ACTION_DIRECTORY, 'pipe'); const blacklist = []; const loadedActions: string[] = []; for await (const path of getFiles(PIPE_DIRECTORY, blacklist)) { if (!path.endsWith('.action.ts') && !path.endsWith('.action.js')) { continue; } const location = '.' + path.substring(ACTION_DIRECTORY.length).replace('.js', ''); try { const importedAction = (require(location)?.default || null) as ActionPipe | null; if (importedAction && Array.isArray(importedAction) && importedAction[0] && importedAction[1]) { ActionPipeline.register(importedAction[0], importedAction[1]); loadedActions.push(importedAction[0]); } } catch (error) { logger.error(`Error loading action file at ${location}:`); logger.error(error); } } logger.info(`Loaded action pipes: ${loadedActions.join(', ')}.`); return Promise.resolve(); } ================================================ FILE: src/engine/action/pipe/button.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import { type ActionHook, getActionHooks } from '@engine/action/hook/action-hook'; import { advancedNumberHookFilter, questHookFilter } from '@engine/action/hook/hook-filters'; import type { Player } from '@engine/world/actor/player/player'; /** * Defines a button action hook. */ export interface ButtonActionHook extends ActionHook { // The ID of the UI widget that the button is on. widgetId?: number; // The IDs of the UI widgets that the buttons are on. widgetIds?: number[]; // The child ID or list of child IDs of the button(s) within the UI widget. buttonIds?: number | number[]; // Whether or not this item action should cancel other running or queued actions. cancelActions?: boolean; } /** * The button action hook handler function to be called when the hook's conditions are met. */ export type buttonActionHandler = (buttonAction: ButtonAction) => void | Promise; /** * Details about a button action being performed. */ export interface ButtonAction { // The player performing the action. player: Player; // The ID of the UI widget that the button is on. widgetId: number; // The child ID of the button within the UI widget. buttonId: number; } /** * The pipe that the game engine hands button actions off to. * @param player * @param widgetId * @param buttonId */ const buttonActionPipe = (player: Player, widgetId: number, buttonId: number): RunnableHooks | null => { let matchingHooks = getActionHooks('button').filter( plugin => questHookFilter(player, plugin) && ((plugin.widgetId && plugin.widgetId === widgetId) || (plugin.widgetIds && advancedNumberHookFilter(plugin.widgetIds, widgetId))) && (plugin.buttonIds === undefined || advancedNumberHookFilter(plugin.buttonIds, buttonId)), ); const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if (questActions.length !== 0) { matchingHooks = questActions; } if (matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage(`Unhandled button interaction: ${widgetId}:${buttonId}`); return null; } return { hooks: matchingHooks, action: { player, widgetId, buttonId, }, }; }; /** * Button action pipe definition. */ export default ['button', buttonActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/equipment-change.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { numberHookFilter, questHookFilter, stringHookFilter } from '@engine/action/hook/hook-filters'; import { findItem } from '@engine/config/config-handler'; import type { EquipmentSlot, ItemDetails } from '@engine/config/item-config'; import type { Player } from '@engine/world/actor/player/player'; import { logger } from '@runejs/common'; /** * Defines an equipment change action hook. */ export interface EquipmentChangeActionHook extends ActionHook { // A single game item ID or a list of item IDs that this action applies to. itemIds?: number | number[]; // A single option name or a list of option names that this action applies to. eventType?: EquipmentChangeType | EquipmentChangeType[]; } /** * The definition for an equip action function. */ export type equipmentChangeActionHandler = (equipmentChangeAction: EquipmentChangeAction) => void; /** * Equipment action types. */ export type EquipmentChangeType = 'equip' | 'unequip'; /** * Details about an item being equipped/unequipped. */ export interface EquipmentChangeAction { // The player performing the action. player: Player; // The ID of the item being equipped/unequipped. itemId: number; // Additional details about the item. itemDetails: ItemDetails; // If the item was equipped or unequipped. eventType: EquipmentChangeType; // The equipment slot. equipmentSlot: EquipmentSlot; } /** * The pipe that the game engine hands equipment actions off to. * @param player * @param itemId * @param eventType * @param slot */ const equipmentChangeActionPipe = ( player: Player, itemId: number, eventType: EquipmentChangeType, slot: EquipmentSlot, ): RunnableHooks | null => { let matchingHooks = getActionHooks('equipment_change', equipActionHook => { if (!questHookFilter(player, equipActionHook)) { return false; } if (equipActionHook.itemIds !== undefined) { if (!numberHookFilter(equipActionHook.itemIds, itemId)) { return false; } } if (equipActionHook.eventType !== undefined) { if (!stringHookFilter(equipActionHook.eventType, eventType)) { return false; } } return true; }); const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if (questActions.length !== 0) { matchingHooks = questActions; } if (!matchingHooks || matchingHooks.length === 0) { return null; } const itemDetails = findItem(itemId); if (!itemDetails) { logger.error(`Item ${itemId} not registered on the server [equipment-change action pipe]`); return null; } return { hooks: matchingHooks, action: { player, itemId, itemDetails, eventType, equipmentSlot: slot, }, }; }; /** * Equip action pipe definition. */ export default ['equipment_change', equipmentChangeActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/item-interaction.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { numberHookFilter, questHookFilter, stringHookFilter } from '@engine/action/hook/hook-filters'; import { findItem } from '@engine/config/config-handler'; import type { ItemDetails } from '@engine/config/item-config'; import type { Player } from '@engine/world/actor/player/player'; import { logger } from '@runejs/common'; /** * Defines an item action hook. */ export interface ItemInteractionActionHook extends ActionHook { // A single game item ID or a list of item IDs that this action applies to. itemIds?: number | number[]; // A single UI widget ID or a list of widget IDs that this action applies to. widgets?: { widgetId: number; containerId: number } | { widgetId: number; containerId: number }[]; // A single option name or a list of option names that this action applies to. options?: string | string[]; // Whether or not this item action should cancel other running or queued actions. cancelOtherActions?: boolean; } /** * The item action hook handler function to be called when the hook's conditions are met. */ export type itemInteractionActionHandler = (itemInteractionAction: ItemInteractionAction) => void; /** * Details about an item action being performed. */ export interface ItemInteractionAction { // The player performing the action. player: Player; // The ID of the item being interacted with. itemId: number; // The container slot that the item being interacted with is in. itemSlot: number; // The ID of the UI widget that the item is in. widgetId: number; // The ID of the UI container that the item is in. containerId: number; // Additional details about the item. itemDetails: ItemDetails; // The option that the player used (ie "equip" or "drop"). option: string; } /** * The pipe that the game engine hands item actions off to. * @param player * @param itemId * @param slot * @param widgetId * @param containerId * @param option */ const itemInteractionActionPipe = ( player: Player, itemId: number, slot: number, widgetId: number, containerId: number, option: string, ): RunnableHooks | null => { const playerWidget = Object.values(player.interfaceState.widgetSlots).find(widget => widget && widget.widgetId === widgetId); if (playerWidget && playerWidget.fakeWidget != undefined) { widgetId = playerWidget.fakeWidget; } // Find all object action plugins that reference this location object let matchingHooks = getActionHooks('item_interaction', plugin => { if (!questHookFilter(player, plugin)) { return false; } if (plugin.itemIds !== undefined) { if (!numberHookFilter(plugin.itemIds, itemId)) { return false; } } if (plugin.widgets !== undefined) { if (Array.isArray(plugin.widgets)) { let found = false; for (const widget of plugin.widgets) { if (widget.widgetId === widgetId && widget.containerId === containerId) { found = true; break; } } if (!found) { return false; } } else { if (plugin.widgets.widgetId !== widgetId || plugin.widgets.containerId !== containerId) { return false; } } } if (plugin.options !== undefined) { if (!stringHookFilter(plugin.options, option)) { return false; } } return true; }); const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if (questActions.length !== 0) { matchingHooks = questActions; } if (matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage( `Unhandled item option: ${option} ${itemId} in slot ${slot} within widget ${widgetId}:${containerId}`, ); return null; } const itemDetails = findItem(itemId); if (!itemDetails) { logger.error(`Item ${itemId} not registered on the server [item-interaction action pipe]`); return null; } return { hooks: matchingHooks, action: { player, itemId, itemSlot: slot, widgetId, containerId, itemDetails, option, }, }; }; /** * Item action pipe definition. */ export default ['item_interaction', itemInteractionActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/item-on-item.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { questHookFilter } from '@engine/action/hook/hook-filters'; import type { Player } from '@engine/world/actor/player/player'; import type { Item } from '@engine/world/items/item'; /** * Defines an item-on-item action hook. */ export interface ItemOnItemActionHook extends ActionHook { // The item pairs being used. Each item can be used on the other, so item order does not matter. items: { item1: number; item2?: number }[]; } /** * The item-on-item action hook handler function to be called when the hook's conditions are met. */ export type itemOnItemActionHandler = (itemOnItemAction: ItemOnItemAction) => void; /** * Details about an item-on-item action being performed. */ export interface ItemOnItemAction { // The player performing the action. player: Player; // The item being used. usedItem: Item; // The item that the first item is being used on. usedWithItem: Item; // The container slot that the item being used is in. usedSlot: number; // The container slot that the second item is in. usedWithSlot: number; // The ID of the UI widget that the item being used is in. usedWidgetId: number; // The ID of the UI widget that the second item is in. usedWithWidgetId: number; } /** * The pipe that the game engine hands item-on-item actions off to. * @param player * @param usedItem * @param usedSlot * @param usedWidgetId * @param usedWithItem * @param usedWithSlot * @param usedWithWidgetId */ const itemOnItemActionPipe = ( player: Player, usedItem: Item, usedSlot: number, usedWidgetId: number, usedWithItem: Item, usedWithSlot: number, usedWithWidgetId: number, ): RunnableHooks | null => { if (player.busy) { return null; } // Find all item on item action plugins that match this action let matchingHooks = getActionHooks('item_on_item', plugin => { if (questHookFilter(player, plugin)) { const used = usedItem.itemId; const usedWith = usedWithItem.itemId; return plugin.items.some(({ item1, item2 }) => { if (item2) { return (item1 === used && item2 === usedWith) || (item1 === usedWith && item2 === used); } return item1 === used || item1 === usedWith; }); } return false; }); const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if (questActions.length !== 0) { matchingHooks = questActions; } if (matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage(`Unhandled item on item interaction: ${usedItem.itemId} on ${usedWithItem.itemId}`); return null; } return { hooks: matchingHooks, action: { player, usedItem, usedWithItem, usedSlot, usedWithSlot, usedWidgetId: usedWidgetId, usedWithWidgetId: usedWithWidgetId, }, }; }; /** * Item-on-item action pipe definition. */ export default ['item_on_item', itemOnItemActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/item-on-npc.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { advancedNumberHookFilter, questHookFilter, stringHookFilter } from '@engine/action/hook/hook-filters'; import { WalkToActorPluginTask } from '@engine/action/pipe/task/walk-to-actor-plugin-task'; import type { Npc } from '@engine/world/actor/npc'; import type { Player } from '@engine/world/actor/player/player'; import type { Item } from '@engine/world/items/item'; import type { Position } from '@engine/world/position'; /** * Defines an item-on-npc action hook. */ export interface ItemOnNpcActionHook extends ActionHook { // A single npc key or a list of npc keys that this action applies to. npcs: string | string[]; // A single game item ID or a list of item IDs that this action applies to. itemIds: number | number[]; // Whether or not the player needs to walk to this NPC before performing the action. walkTo: boolean; } /** * The item-on-npc action hook handler function to be called when the hook's conditions are met. */ export type itemOnNpcActionHandler = (itemOnNpcAction: ItemOnNpcAction) => void; /** * Details about an item-on-npc action being performed. */ export interface ItemOnNpcAction { // The player performing the action. player: Player; // The NPC the action is being performed on. npc: Npc; // The position that the NPC was at when the action was initiated. position: Position; // The item being used. item: Item; // The ID of the UI widget that the item being used is in. itemWidgetId: number; // The ID of the UI container that the item being used is in. itemContainerId: number; } /** * The pipe that the game engine hands item-on-npc actions off to. * @param player * @param npc * @param position * @param item * @param itemWidgetId * @param itemContainerId */ const itemOnNpcActionPipe = ( player: Player, npc: Npc, position: Position, item: Item, itemWidgetId: number, itemContainerId: number, ): RunnableHooks | null => { const morphedNpc = player.getMorphedNpcDetails(npc); // Find all item on npc action plugins that reference this npc and item let matchingHooks = getActionHooks('item_on_npc').filter( plugin => questHookFilter(player, plugin) && stringHookFilter(plugin.npcs, morphedNpc?.key || npc.key) && advancedNumberHookFilter(plugin.itemIds, item.itemId), ); const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if (questActions.length !== 0) { matchingHooks = questActions; } if (matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage( `Unhandled item on npc interaction: ${item.itemId} on ${morphedNpc?.name || npc.name} ` + `(id-${morphedNpc?.gameId || npc.id}) @ ${position.x},${position.y},${position.level}`, ); if (morphedNpc) { player.outgoingPackets.chatboxMessage(`Note: (id-${morphedNpc.gameId}) is a morphed NPC. The parent NPC is (id-${npc.id}).`); } return null; } const walkToPlugins = matchingHooks.filter(plugin => plugin.walkTo); if (walkToPlugins.length > 0) { player.enqueueBaseTask( new WalkToActorPluginTask(walkToPlugins, player, 'npc', npc, { item, itemWidgetId, itemContainerId, }), ); return null; } return { hooks: matchingHooks, action: { player, npc, position, item, itemWidgetId, itemContainerId, }, }; }; /** * Item-on-npc action pipe definition. */ export default ['item_on_npc', itemOnNpcActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/item-on-object.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { advancedNumberHookFilter, questHookFilter } from '@engine/action/hook/hook-filters'; import { WalkToObjectPluginTask } from '@engine/action/pipe/task/walk-to-object-plugin-task'; import type { Player } from '@engine/world/actor/player/player'; import type { Item } from '@engine/world/items/item'; import type { Position } from '@engine/world/position'; import type { LandscapeObject, ObjectConfig } from '@runejs/filestore'; /** * Defines an item-on-object action hook. */ export interface ItemOnObjectActionHook extends ActionHook { // A single game object ID or a list of object IDs that this action applies to. objectIds: number | number[]; // A single game item ID or a list of item IDs that this action applies to. itemIds: number | number[]; // Whether or not the player needs to walk to this object before performing the action. walkTo: boolean; } /** * The item-on-object action hook handler function to be called when the hook's conditions are met. */ export type itemOnObjectActionHandler = (itemOnObjectAction: ItemOnObjectAction) => void; /** * Details about an item-on-object action being performed. */ export interface ItemOnObjectAction { // The player performing the action. player: Player; // The object the action is being performed on. object: LandscapeObject; // Additional details about the object that the action is being performed on. objectConfig: ObjectConfig; // The position that the game object was at when the action was initiated. position: Position; // The item being used. item: Item; // The ID of the UI widget that the item being used is in. itemWidgetId: number; // The ID of the UI container that the item being used is in. itemContainerId: number; // Whether or not this game object is an original map object or if it has been added/replaced. cacheOriginal: boolean; } /** * The pipe that the game engine hands item-on-object actions off to. * @param player * @param landscapeObject * @param objectConfig * @param position * @param item * @param itemWidgetId * @param itemContainerId * @param cacheOriginal */ const itemOnObjectActionPipe = ( player: Player, landscapeObject: LandscapeObject, objectConfig: ObjectConfig, position: Position, item: Item, itemWidgetId: number, itemContainerId: number, cacheOriginal: boolean, ): RunnableHooks | null => { // Find all item on object action plugins that reference this location object let matchingHooks = getActionHooks('item_on_object').filter( plugin => questHookFilter(player, plugin) && advancedNumberHookFilter(plugin.objectIds, landscapeObject.objectId), ); const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if (questActions.length !== 0) { matchingHooks = questActions; } // Find all item on object action plugins that reference this item if (matchingHooks.length !== 0) { matchingHooks = matchingHooks.filter(plugin => advancedNumberHookFilter(plugin.itemIds, item.itemId)); } if (matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage( `Unhandled item on object interaction: ${item.itemId} on ${objectConfig.name} ` + `(id-${landscapeObject.objectId}) @ ${position.x},${position.y},${position.level}`, ); return null; } const walkToPlugins = matchingHooks.filter(plugin => plugin.walkTo); if (walkToPlugins.length > 0) { player.enqueueBaseTask( new WalkToObjectPluginTask(walkToPlugins, player, landscapeObject, { objectConfig, item, itemWidgetId, itemContainerId, cacheOriginal, }), ); return null; } return { hooks: matchingHooks, action: { player, object: landscapeObject, objectConfig, position, item, itemWidgetId, itemContainerId, cacheOriginal, }, }; }; /** * Item-on-object action pipe definition. */ export default ['item_on_object', itemOnObjectActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/item-on-player.action.ts ================================================ import type { ActionPipe } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { advancedNumberHookFilter, questHookFilter } from '@engine/action/hook/hook-filters'; import { WalkToActorPluginTask } from '@engine/action/pipe/task/walk-to-actor-plugin-task'; import type { Player } from '@engine/world/actor/player/player'; import type { Item } from '@engine/world/items/item'; import type { Position } from '@engine/world/position'; /** * Defines an item-on-player action hook. */ export interface ItemOnPlayerActionHook extends ActionHook { // A single game item ID or a list of item IDs that this action applies to. itemIds: number | number[]; // Whether or not the player needs to walk to this Player before performing the action. walkTo: boolean; } /** * The item-on-player action hook handler function to be called when the hook's conditions are met. */ export type itemOnPlayerActionHandler = (itemOnPlayerAction: ItemOnPlayerAction) => void; /** * Details about an item-on-player action being performed. */ export interface ItemOnPlayerAction { // The player performing the action. player: Player; // The player the action is being performed on. otherPlayer: Player; // The position that the Player was at when the action was initiated. position: Position; // The item being used. item: Item; // The ID of the UI widget that the item being used is in. itemWidgetId: number; // The ID of the UI container that the item being used is in. itemContainerId: number; } // @TODO update /** * The pipe that the game engine hands item-on-player actions off to. * @param player * @param otherPlayer * @param position * @param item * @param itemWidgetId * @param itemContainerId */ const itemOnPlayerActionPipe = ( player: Player, otherPlayer: Player, position: Position, item: Item, itemWidgetId: number, itemContainerId: number, ): void => { if (player.busy) { return; } // Find all item on player action plugins that reference this item let interactionActions = getActionHooks('item_on_player').filter( plugin => questHookFilter(player, plugin) && advancedNumberHookFilter(plugin.itemIds, item.itemId), ); const questActions = interactionActions.filter(plugin => plugin.questRequirement !== undefined); if (questActions.length !== 0) { interactionActions = questActions; } if (interactionActions.length === 0) { player.outgoingPackets.chatboxMessage( `Unhandled item on player interaction: ${item.itemId} ` + `@ ${position.x},${position.y},${position.level}`, ); return; } // Separate out walk-to actions from immediate actions const walkToPlugins = interactionActions.filter(plugin => plugin.walkTo); const immediateHooks = interactionActions.filter(plugin => !plugin.walkTo); if (walkToPlugins.length > 0) { player.enqueueBaseTask( new WalkToActorPluginTask(walkToPlugins, player, 'otherPlayer', otherPlayer, { item, itemWidgetId, itemContainerId, }), ); return; } // Immediately run any non-walk-to plugins for (const actionHook of immediateHooks) { actionHook.handler({ player, otherPlayer, position, item, itemWidgetId, itemContainerId, }); } }; /** * Item-on-player action pipe definition. */ export default ['item_on_player', itemOnPlayerActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/item-on-world-item.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { questHookFilter } from '@engine/action/hook/hook-filters'; import type { Player } from '@engine/world/actor/player/player'; import type { Item } from '@engine/world/items/item'; import type { WorldItem } from '@engine/world/items/world-item'; /** * Defines an item-on-world-item action hook. * * @author jameskmonger */ export interface ItemOnWorldItemActionHook extends ActionHook { /** * The item pairs being used. Both items are optional so that you can specify a single item, a pair of items, or neither. */ items: { item?: number; worldItem?: number }[]; } /** * The item-on-world-item action hook handler function to be called when the hook's conditions are met. */ export type itemOnWorldItemActionHandler = (itemOnWorldItemAction: ItemOnWorldItemAction) => void; /** * Details about an item-on-world-item action being performed. * * @author jameskmonger */ export interface ItemOnWorldItemAction { /** * The player performing the action. */ player: Player; /** * The item being used. */ usedItem: Item; /** * The WorldItem that the first item is being used on. */ usedWithItem: WorldItem; /** * The ID of the UI widget that the item being used is in. */ usedWidgetId: number; /** * The ID of the container that the item being used is in. */ usedContainerId: number; /** * The slot within the container that the item being used is in. */ usedSlot: number; } /** * The pipe that the game engine hands item-on-world-item actions off to. * * This will call the `item_on_world_item` action hooks, if any are registered and match the action being performed. * * Both `item` and `worldItem` are optional, but if they are provided then they must match the items in use. * * @author jameskmonger */ const itemOnWorldItemActionPipe = ( player: Player, usedItem: Item, usedWithItem: WorldItem, usedWidgetId: number, usedContainerId: number, usedSlot: number, ): RunnableHooks | null => { if (player.busy) { return null; } // Find all item on item action plugins that match this action let matchingHooks = getActionHooks('item_on_world_item', plugin => { if (questHookFilter(player, plugin)) { const used = usedItem.itemId; const usedWith = usedWithItem.itemId; return plugin.items.some(({ item, worldItem }) => { const itemMatch = item === undefined || item === used; const worldItemMatch = worldItem === undefined || worldItem === usedWith; return itemMatch && worldItemMatch; }); } return false; }); const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if (questActions.length !== 0) { matchingHooks = questActions; } if (matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage(`Unhandled item on world item interaction: ${usedItem.itemId} on ${usedWithItem.itemId}`); return null; } return { hooks: matchingHooks, action: { player, usedItem, usedWithItem, usedWidgetId, usedContainerId, usedSlot, }, }; }; /** * Item-on-world-item action pipe definition. */ export default ['item_on_world_item', itemOnWorldItemActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/item-swap.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { numberHookFilter } from '@engine/action/hook/hook-filters'; import type { Player } from '@engine/world/actor/player/player'; /** * Defines a swap items action hook. */ export interface ItemSwapActionHook extends ActionHook { widgetId?: number; widgetIds?: number[]; } /** * The swap items action hook handler function to be called when the hook's conditions are met. */ export type itemSwapActionHandler = (itemSwapAction: ItemSwapAction) => void; /** * Details about a swap items action being performed. */ export interface ItemSwapAction { // The player performing the action. player: Player; // The widget id for the container. widgetId: number; // The container id within the widget. containerId: number; // The slot of the item being swapped item. fromSlot: number; // The slot of the item being swapped with. toSlot: number; } /** * The pipe that the game engine hands swap items actions off to. * @param player * @param fromSlot * @param toSlot * @param widget */ const itemSwapActionPipe = ( player: Player, fromSlot: number, toSlot: number, widget: { widgetId: number; containerId: number }, ): RunnableHooks | null => { const matchingHooks = getActionHooks('item_swap').filter( plugin => (plugin.widgetId || plugin.widgetIds) && numberHookFilter((plugin.widgetId || plugin.widgetIds)!, widget.widgetId), ); if (!matchingHooks || matchingHooks.length === 0) { player.sendMessage( `Unhandled Swap Items action: widget[${widget.widgetId}] container[${widget.containerId}] fromSlot[${fromSlot} toSlot${toSlot}`, ); return null; } return { hooks: matchingHooks, action: { player, widgetId: widget.widgetId, containerId: widget.containerId, fromSlot, toSlot, }, }; }; /** * Swap items action pipe definition. */ export default ['item_swap', itemSwapActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/magic-on-npc.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import type { Npc } from '@engine/world/actor/npc'; import type { Player } from '@engine/world/actor/player/player'; /** * Defines a button action hook. */ export interface MagicOnNPCActionHook extends ActionHook { // The npc world id that was clicked on after choosing the spell npcworldId?: number; // The IDs of the UI widgets that the buttons are on. widgetIds?: number[]; // The child ID or list of child IDs of the button(s) within the UI widget. buttonIds?: number | number[]; // Whether or not this item action should cancel other running or queued actions. cancelActions?: boolean; } /** * The button action hook handler function to be called when the hook's conditions are met. */ export type magiconnpcActionHandler = (buttonAction: MagicOnNPCAction) => void | Promise; /** * Details about a button action being performed. */ export interface MagicOnNPCAction { // The npc world id that was clicked on after choosing the spell npc: Npc; // The player performing the action. player: Player; // The ID of the UI widget that the button is on. widgetId: number; // The child ID of the button within the UI widget. buttonId: number; } /** * The pipe that the game engine hands button actions off to. * @param npc * @param player * @param widgetId * @param buttonId */ const magicOnNpcActionPipe = (npc: Npc, player: Player, widgetId: number, buttonId: number): RunnableHooks => { //console.info(`pew pew you use magic on ${npc.name}!`); // Find all object action plugins that reference this location object const matchingHooks = getActionHooks('magic_on_npc'); return { hooks: matchingHooks, action: { npc, player, widgetId, buttonId, }, }; }; /** * Button action pipe definition. */ export default ['magic_on_npc', magicOnNpcActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/move-item.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { numberHookFilter } from '@engine/action/hook/hook-filters'; import type { Player } from '@engine/world/actor/player/player'; /** * Defines a move item action hook. */ export interface MoveItemActionHook extends ActionHook { widgetId?: number; widgetIds?: number[]; } /** * The move item action hook handler function to be called when the hook's conditions are met. */ export type moveItemActionHandler = (moveItemAction: MoveItemAction) => void; /** * Details about a move item action being performed. */ export interface MoveItemAction { // The player performing the action. player: Player; // The widget id for the container. widgetId: number; // The container id within the widget. containerId: number; // The original slot of the item. fromSlot: number; // The new slot for the item. toSlot: number; } /** * The pipe that the game engine hands move item actions off to. * @param player * @param fromSlot * @param toSlot * @param widget */ const moveItemActionPipe = ( player: Player, fromSlot: number, toSlot: number, widget: { widgetId: number; containerId: number }, ): RunnableHooks | null => { const matchingHooks = getActionHooks('move_item').filter( plugin => (plugin.widgetId || plugin.widgetIds) && numberHookFilter((plugin.widgetId || plugin.widgetIds)!, widget.widgetId), ); if (!matchingHooks || matchingHooks.length === 0) { player.sendMessage( `Unhandled Move Item action: widget[${widget.widgetId}] container[${widget.containerId}] fromSlot[${fromSlot} toSlot${toSlot}`, ); return null; } return { hooks: matchingHooks, action: { player, widgetId: widget.widgetId, containerId: widget.containerId, fromSlot, toSlot, }, }; }; /** * Move item action pipe definition. */ export default ['move_item', moveItemActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/npc-init.action.ts ================================================ import type { ActionPipe } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { stringHookFilter } from '@engine/action/hook/hook-filters'; import type { Npc } from '@engine/world/actor/npc'; /** * Defines an npc init action hook. */ export interface NpcInitActionHook extends ActionHook { // A single NPC key or a list of NPC keys that this action applies to. npcs?: string | string[]; } /** * The npc init action hook handler function to be called when the hook's conditions are met. */ export type npcInitActionHandler = (npcAction: NpcInitAction) => void; /** * Details about an npc init action being performed. */ export interface NpcInitAction { // The npc that is being initialized. npc: Npc; } /** * The pipe that the game engine hands npc init actions off to. * @param npc */ const npcInitActionPipe = ({ npc }: NpcInitAction): void => { const actionHooks = getActionHooks('npc_init').filter( plugin => !plugin.npcs || stringHookFilter(plugin.npcs, npc.key), ); actionHooks.forEach(actionHook => { if (!actionHook.handler) { return; } actionHook.handler({ npc }); }); }; /** * Npc init action pipe definition. */ export default ['npc_init', npcInitActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/npc-interaction.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { questHookFilter, stringHookFilter } from '@engine/action/hook/hook-filters'; import { WalkToActorPluginTask } from '@engine/action/pipe/task/walk-to-actor-plugin-task'; import type { Npc } from '@engine/world/actor/npc'; import type { Player } from '@engine/world/actor/player/player'; import type { Position } from '@engine/world/position'; /** * Defines an npc action hook. */ export interface NpcInteractionActionHook extends ActionHook { // A single NPC key or a list of NPC keys that this action applies to. npcs?: string | string[]; // A single option name or a list of option names that this action applies to. options?: string | string[]; // Whether or not the player needs to walk to this NPC before performing the action. walkTo: boolean; } /** * The npc action hook handler function to be called when the hook's conditions are met. */ export type npcInteractionActionHandler = (npcInteractionAction: NpcInteractionAction) => void; /** * Details about an npc action being performed. */ export interface NpcInteractionAction { // The player performing the action. player: Player; // The NPC the action is being performed on. npc: Npc; // The position that the NPC was at when the action was initiated. position: Position; // The option used when interacting with the NPC option: string; } /** * The pipe that the game engine hands npc actions off to. * @param player * @param npc * @param position * @param option */ const npcInteractionActionPipe = ( player: Player, npc: Npc, position: Position, option: string, ): RunnableHooks | null => { if (player.busy) { return null; } const morphedNpc = player.getMorphedNpcDetails(npc); // Find all NPC action plugins that reference this NPC let matchingHooks = getActionHooks('npc_interaction').filter( plugin => questHookFilter(player, plugin) && (!plugin.npcs || stringHookFilter(plugin.npcs, morphedNpc?.key || npc.key)) && (!plugin.options || stringHookFilter(plugin.options, option)), ); const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if (questActions.length !== 0) { matchingHooks = questActions; } if (matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage( `Unhandled NPC interaction: ${option} ${morphedNpc?.key || npc.key} (id-${morphedNpc?.gameId || npc.id}) @ ${position.x},${position.y},${position.level}`, ); if (morphedNpc) { player.outgoingPackets.chatboxMessage(`Note: (id-${morphedNpc.gameId}) is a morphed NPC. The parent NPC is (id-${npc.id}).`); } return null; } const walkToPlugins = matchingHooks.filter(plugin => plugin.walkTo); if (walkToPlugins.length > 0) { player.enqueueBaseTask(new WalkToActorPluginTask(walkToPlugins, player, 'npc', npc, { option })); return null; } console.log(`WE ARE INTERACTING WITH NPC quests: ${questActions.length} ${matchingHooks.length}`); return { hooks: matchingHooks, action: { player, npc, position, option, }, }; }; /** * Npc action pipe definition. */ export default ['npc_interaction', npcInteractionActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/object-interaction.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { advancedNumberHookFilter, questHookFilter } from '@engine/action/hook/hook-filters'; import { WalkToObjectPluginTask } from '@engine/action/pipe/task/walk-to-object-plugin-task'; import type { Player } from '@engine/world/actor/player/player'; import type { Position } from '@engine/world/position'; import type { LandscapeObject, ObjectConfig } from '@runejs/filestore'; /** * Defines an object action hook. */ export interface ObjectInteractionActionHook extends ActionHook { // A single game object ID or a list of object IDs that this action applies to. objectIds: number | number[]; // A single option name or a list of option names that this action applies to. options: string | string[]; // Whether or not the player needs to walk to this object before performing the action. walkTo: boolean; } /** * The object action hook handler function to be called when the hook's conditions are met. */ export type objectInteractionActionHandler = (objectInteractionAction: ObjectInteractionAction) => void; /** * Details about an object action being performed. */ export interface ObjectInteractionAction { // The player performing the action. player: Player; // The object the action is being performed on. object: LandscapeObject; // Additional details about the object that the action is being performed on. objectConfig: ObjectConfig; // The position that the game object was at when the action was initiated. position: Position; // Whether or not this game object is an original map object or if it has been added/replaced. cacheOriginal: boolean; // The option that the player used (ie "cut" tree, or "smelt" furnace). option: string; } /** * The pipe that the game engine hands object actions off to. * @param player * @param landscapeObject * @param objectConfig * @param position * @param option * @param cacheOriginal */ const objectInteractionActionPipe = ( player: Player, landscapeObject: LandscapeObject, objectConfig: ObjectConfig, position: Position, option: string, cacheOriginal: boolean, ): RunnableHooks | null => { if (player.metadata.blockObjectInteractions) { return null; } // Find all object action plugins that reference this location object let matchingHooks = getActionHooks('object_interaction').filter( plugin => questHookFilter(player, plugin) && advancedNumberHookFilter(plugin.objectIds, landscapeObject.objectId, plugin.options, option), ); const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if (questActions.length !== 0) { matchingHooks = questActions; } if (matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage( `Unhandled object interaction: ${option} ${objectConfig.name} ` + `(id-${landscapeObject.objectId}) @ ${position.x},${position.y},${position.level}`, ); return null; } const walkToPlugins = matchingHooks.filter(plugin => plugin.walkTo); if (walkToPlugins.length > 0) { player.enqueueBaseTask(new WalkToObjectPluginTask(walkToPlugins, player, landscapeObject, { objectConfig, cacheOriginal, option })); return null; } return { hooks: matchingHooks, action: { player, object: landscapeObject, objectConfig, option, position, cacheOriginal, }, }; }; /** * Object action pipe definition. */ export default ['object_interaction', objectInteractionActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/player-command.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { reloadContent, reloadContentCommands } from '@engine/plugins/reload-content'; import type { Player } from '@engine/world/actor/player/player'; import { logger } from '@runejs/common'; /** * Defines a player command action hook. */ export interface PlayerCommandActionHook extends ActionHook { // The single command or list of commands that this action applies to. commands: string | string[]; // The potential arguments for this command action. args?: { name: string; type: 'number' | 'string' | 'either'; defaultValue?: number | string; }[]; } /** * The player command action hook handler function to be called when the hook's conditions are met. */ export type commandActionHandler = (playerCommandAction: PlayerCommandAction) => void; /** * Details about a player command action being performed. */ export interface PlayerCommandAction { // The player performing the action. player: Player; // The command that the player entered. command: string; // If the player used the console isConsole: boolean; // The arguments that the player entered for their command. args: { [key: string]: number | string }; } /** * The pipe that the game engine hands player command actions off to. * @param player * @param command * @param isConsole * @param inputArgs */ const playerCommandActionPipe = ( player: Player, command: string, isConsole: boolean, inputArgs: string[], ): RunnableHooks | null => { command = command.toLowerCase(); // Reload game content if (reloadContentCommands.indexOf(command) !== -1) { reloadContent(player, isConsole).catch(logger.error); return null; } const actionArgs = {}; const plugins = getActionHooks('player_command').filter(actionHook => { let valid: boolean; if (Array.isArray(actionHook.commands)) { valid = actionHook.commands.indexOf(command) !== -1; } else { valid = actionHook.commands === command; } if (!valid) { return false; } if (actionHook.args) { const args = actionHook.args; let syntaxError = `Syntax error. Try ::${command}`; args.forEach(commandArg => { syntaxError += ` ${commandArg.name}:${commandArg.type}${commandArg.defaultValue === undefined ? '' : '?'}`; }); const requiredArgLength = actionHook.args.filter(arg => arg.defaultValue === undefined).length; if (requiredArgLength > inputArgs.length) { player.sendLogMessage(syntaxError, isConsole); return; } for (let i = 0; i < actionHook.args.length; i++) { let argValue: string | number | null = inputArgs[i] || null; const pluginArg = actionHook.args[i]; if (argValue === null || argValue === undefined) { if (pluginArg.defaultValue === undefined) { player.sendLogMessage(syntaxError, isConsole); return; } else { argValue = pluginArg.defaultValue; } } else { if (pluginArg.type === 'number') { argValue = parseInt(argValue); if (isNaN(argValue)) { player.sendLogMessage(syntaxError, isConsole); return; } } else if (pluginArg.type === 'string') { if (!argValue || argValue.trim() === '') { player.sendLogMessage(syntaxError, isConsole); return; } } } actionArgs[pluginArg.name] = argValue; } } return true; }); if (plugins.length === 0) { player.sendLogMessage(`Unhandled command: ${command}`, isConsole); return null; } return { hooks: plugins, action: { player, command, isConsole, args: actionArgs, }, }; }; /** * Player command action pipe definition. */ export default ['player_command', playerCommandActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/player-init.action.ts ================================================ import type { ActionPipe } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import type { Player } from '@engine/world/actor/player/player'; /** * Defines a player init action hook. */ export type PlayerInitActionHook = ActionHook; /** * The player init action hook handler function to be called when the hook's conditions are met. */ export type playerInitActionHandler = (playerInitAction: PlayerInitAction) => void; /** * Details about a player init action being performed. */ export interface PlayerInitAction { // The player that is being initialized. player: Player; } /** * The pipe that the game engine hands player init actions off to. * @param player */ const playerInitActionPipe = ({ player }: PlayerInitAction): void => { const actionHooks = getActionHooks('player_init'); actionHooks.forEach(actionHook => { if (!actionHook.handler) { return; } actionHook.handler({ player }); }); }; /** * Player init action pipe definition. */ export default ['player_init', playerInitActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/player-interaction.action.ts ================================================ import type { RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { questHookFilter, stringHookFilter } from '@engine/action/hook/hook-filters'; import { WalkToActorPluginTask } from '@engine/action/pipe/task/walk-to-actor-plugin-task'; import type { Player } from '@engine/world/actor/player/player'; import type { Position } from '@engine/world/position'; /** * Defines a player action hook. */ export interface PlayerInteractionActionHook extends ActionHook { // A single option name or a list of option names that this action applies to. options: string | string[]; // Whether or not the player needs to walk to the other player before performing the action. walkTo: boolean; } /** * The player action hook handler function to be called when the hook's conditions are met. */ export type playerInteractionActionHandler = (playerInteractionAction: PlayerInteractionAction) => void; /** * Details about a player action being performed. */ export interface PlayerInteractionAction { // The player performing the action. player: Player; // The player that the action is being performed on. otherPlayer: Player; // The position that the other player was at when the action was initiated. position: Position; } /** * The pipe that the game engine hands player actions off to. * @param player * @param otherPlayer * @param position * @param option */ const playerInteractionActionPipe = ( player: Player, otherPlayer: Player, position: Position, option: string, ): RunnableHooks | null => { // Find all player action plugins that reference this option let matchingHooks = getActionHooks('player_interaction').filter( plugin => questHookFilter(player, plugin) && stringHookFilter(plugin.options, option), ); const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if (questActions.length !== 0) { matchingHooks = questActions; } if (matchingHooks.length === 0) { player.sendMessage(`Unhandled Player interaction: ${option} @ ${position.x},${position.y},${position.level}`); return null; } const walkToPlugins = matchingHooks.filter(plugin => plugin.walkTo); if (walkToPlugins.length > 0) { player.enqueueBaseTask(new WalkToActorPluginTask(walkToPlugins, player, 'otherPlayer', otherPlayer, {})); return null; } return { hooks: matchingHooks, action: { player, otherPlayer, position, }, }; }; /** * Player action pipe definition. */ export default ['player_interaction', playerInteractionActionPipe]; ================================================ FILE: src/engine/action/pipe/prayer.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import type { Npc } from '@engine/world/actor/npc'; import type { Player } from '@engine/world/actor/player/player'; /** * Defines a button action hook. */ export interface PrayerActionHook extends ActionHook { // The npc world id that was clicked on after choosing the spell npcworldId?: number; // The IDs of the UI widgets that the buttons are on. widgetIds?: number[]; // The child ID or list of child IDs of the button(s) within the UI widget. buttonIds?: number | number[]; // Whether or not this item action should cancel other running or queued actions. cancelActions?: boolean; } /** * The button action hook handler function to be called when the hook's conditions are met. */ export type PrayerActionHandler = (buttonAction: PrayerAction) => void | Promise; /** * Details about a button action being performed. */ export interface PrayerAction { // The npc world id that was clicked on after choosing the spell npc: Npc; // The player performing the action. player: Player; // The ID of the UI widget that the button is on. widgetId: number; // The child ID of the button within the UI widget. buttonId: number; } /** * The pipe that the game engine hands button actions off to. * @param npc * @param player * @param widgetId * @param buttonId */ const prayerActionPipe = (npc: Npc, player: Player, widgetId: number, buttonId: number): RunnableHooks => { console.info(`You used prayer`); // Find all object action plugins that reference this location object const matchingHooks = getActionHooks('button'); return { hooks: matchingHooks, action: { npc, player, widgetId, buttonId, }, }; }; /** * Button action pipe definition. */ export default ['prayer', prayerActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/region-change.action.ts ================================================ import type { ActionPipe } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import type { Player } from '@engine/world/actor/player/player'; import type { RegionType } from '@engine/world/map/region'; import type { Position } from '@engine/world/position'; import { Coords } from '@engine/world/position'; /** * Defines a player region change action hook. */ export interface RegionChangeActionHook extends ActionHook { // Optional single region type for the action hook to apply to. regionType?: RegionType; // Optional multiple region types for the action hook to apply to. regionTypes?: RegionType[]; // Optional teleporting requirement teleporting?: boolean; } /** * The player region change action hook handler function to be called when the hook's conditions are met. */ export type regionChangeActionHandler = (regionChangeAction: RegionChangeAction) => void; /** * Details about a player region change action being performed. */ export interface RegionChangeAction { // The player performing the action. player: Player; // Whether or not the player is teleporting to their new location teleporting: boolean; // The original position that the player was at before moving to the new region. originalPosition: Position; // The position that the player ended up at in the new region. currentPosition: Position; // The player's original chunk coordinates originalChunkCoords: Coords; // The player's current chunk coordinates currentChunkCoords: Coords; // The player's original map region coordinates originalMapRegionCoords: Coords; // The player's current map region coordinates currentMapRegionCoords: Coords; // The player's original map region id originalMapRegionId: number; // The player's current map region id currentMapRegionId: number; // The region types that changed for the player. regionTypes: RegionType[]; } /** * Creates a RegionChangeAction object from the given inputs. * * TODO (Jameskmonger) I changed this function's return type to `| null` to satisfy TypeScript, * not sure if this is correct or if the code was just wrong before. * * @param player The player. * @param originalPosition The player's original position. * @param currentPosition The player's current position. * @param teleporting Whether or not the player is teleporting; defaults to false. */ export const regionChangeActionFactory = ( player: Player, originalPosition: Position, currentPosition: Position, teleporting: boolean = false, ): RegionChangeAction | null => { const regionTypes: RegionType[] = []; const originalMapRegionId: number = ((originalPosition.x >> 6) << 8) + (originalPosition.y >> 6); const currentMapRegionId: number = ((currentPosition.x >> 6) << 8) + (currentPosition.y >> 6); const originalChunkCoords: Coords = { x: originalPosition.chunkX, y: originalPosition.chunkY, level: originalPosition.level, }; const currentChunkCoords: Coords = { x: currentPosition.chunkX, y: currentPosition.chunkY, level: currentPosition.level, }; if (originalMapRegionId !== currentMapRegionId) { regionTypes.push('region'); } if (!Coords.equals(originalChunkCoords, currentChunkCoords)) { regionTypes.push('chunk'); } if (regionTypes.length === 0) { return null; } return { player, regionTypes, teleporting, originalPosition, originalChunkCoords, originalMapRegionCoords: { x: originalPosition.x >> 6, y: originalPosition.y >> 6, level: originalPosition.level, }, originalMapRegionId, currentPosition: player.position, currentChunkCoords, currentMapRegionCoords: { x: currentPosition.x >> 6, y: currentPosition.y >> 6, level: currentPosition.level, }, currentMapRegionId, }; }; /** * The pipe that the game engine hands player region change actions off to. * @param actionData */ const regionChangeActionPipe = (actionData: RegionChangeAction): void => { if (!actionData) { return; } const { regionTypes } = actionData; if (!regionTypes || regionTypes.length === 0) { return; } // Find all action hooks that match the provided input const actionList = getActionHooks('region_change')?.filter(actionHook => { if (actionHook.teleporting && !actionData.teleporting) { return false; } if (actionHook.regionType) { return regionTypes.indexOf(actionHook.regionType) !== -1; } else if (actionHook.regionTypes && actionHook.regionTypes.length !== 0) { let valid = false; for (const type of actionHook.regionTypes) { if (regionTypes.indexOf(type) !== -1) { valid = true; break; } } return valid; } return false; }) || null; if (!actionList || actionList.length === 0) { // No matching actions found return; } actionList.forEach( async actionHook => new Promise(resolve => { if (actionHook && actionHook.handler) { actionHook.handler(actionData); } resolve(); }), ); }; /** * Player region change action pipe definition. */ export default ['region_change', regionChangeActionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/spawned-item-interaction.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { numberHookFilter, questHookFilter, stringHookFilter } from '@engine/action/hook/hook-filters'; import { WalkToItemPluginTask } from '@engine/action/pipe/task/walk-to-item-plugin-task'; import { findItem } from '@engine/config/config-handler'; import type { ItemDetails } from '@engine/config/item-config'; import type { Player } from '@engine/world/actor/player/player'; import type { WorldItem } from '@engine/world/items/world-item'; import { logger } from '@runejs/common'; /** * Defines a world item action hook. */ export interface SpawnedItemInteractionHook extends ActionHook { // A single game item ID or a list of item IDs that this action applies to. itemIds?: number | number[]; // A single option name or a list of option names that this action applies to. options: string | string[]; // Whether or not the player needs to walk to this world item before performing the action. walkTo: boolean; } /** * The world item action hook handler function to be called when the hook's conditions are met. */ export type spawnedItemInteractionHandler = (spawnedItemInteractionAction: SpawnedItemInteractionAction) => void; /** * Details about a world item action being performed. */ export interface SpawnedItemInteractionAction { // The player performing the action. player: Player; // The world item that the player is interacting with. worldItem: WorldItem; // Details about the item itemDetails: ItemDetails; // TODO (jkm) add "option" to the action } /** * The pipe that the game engine hands world item actions off to. * @param player * @param worldItem * @param option */ const spawnedItemInteractionPipe = ( player: Player, worldItem: WorldItem, option: string, ): RunnableHooks | null => { // Find all world item action plugins that reference this world item let matchingHooks = getActionHooks('spawned_item_interaction').filter(plugin => { if (!questHookFilter(player, plugin)) { return false; } if (plugin.itemIds !== undefined) { if (!numberHookFilter(plugin.itemIds, worldItem.itemId)) { return false; } } if (!stringHookFilter(plugin.options, option)) { return false; } return true; }); const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if (questActions.length !== 0) { matchingHooks = questActions; } if (matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage(`Unhandled world item interaction: ${option} ${worldItem.itemId}`); return null; } const itemDetails = findItem(worldItem.itemId); if (!itemDetails) { logger.error(`Item ${worldItem.itemId} not registered on the server [spawned-item-interaction action pipe]`); return null; } const walkToPlugins = matchingHooks.filter(plugin => plugin.walkTo); if (walkToPlugins.length > 0) { player.enqueueBaseTask(new WalkToItemPluginTask(walkToPlugins, player, worldItem, itemDetails)); return null; } return { hooks: matchingHooks, action: { player, worldItem, itemDetails, }, }; }; /** * World item action pipe definition. */ export default ['spawned_item_interaction', spawnedItemInteractionPipe] as ActionPipe; ================================================ FILE: src/engine/action/pipe/task/queueable-task.ts ================================================ import type { ActionHook } from '@engine/action/hook/action-hook'; import { ActorTask } from '@engine/task/impl/actor-task'; import type { Task } from '@engine/task/task'; import type { Actor } from '@engine/world/actor/actor'; import type { Player } from '@engine/world/actor/player/player'; import type { ItemOnObjectAction } from '../item-on-object.action'; import type { ObjectInteractionAction } from '../object-interaction.action'; /** * The result of running a callback function will be recorded here so that the * `QueueableTask` can make a choice to continue looping and/or to enqueue * the desired task. */ export interface QueueableTaskEval { callbackResult: boolean; shouldContinueLooping: boolean; } /** * All actions supported by this plugin task. */ type ObjectAction = ObjectInteractionAction | ItemOnObjectAction; /** * An ActionHook for a supported ObjectAction. */ type ObjectActionHook = ActionHook void>; /** * The data unique to the action being executed (i.e. excluding shared data) */ type ObjectActionData = Omit; /** * Processes the provided callback function on every single tick, and also allows any * arbitrary base task to be queued at the next tick. Useful for queuing random * movements, as well as other things. * * Can loop infinitely based on the result of the passed in `callback` function. */ export class QueueableTask extends ActorTask { /** * The plugins to execute on each tick. These will not get called if the * `callback` indicates a halting condition. */ private plugins: ObjectActionHook[]; private data: ObjectActionData | null; /** * Optionally provided base task to enqueue on each tick. Can be `null`. */ private task: Task | null; /** * This callback function will be executed on every tick. It must return a * `QueueableTaskEval` so that this loop can determine if it should * terminate or continue looping. */ private callback: () => QueueableTaskEval; constructor( plugins: ObjectActionHook[], actor: Player | Actor, callback: () => QueueableTaskEval, task: Task | null, data: ObjectActionData | null, ) { super(actor); this.plugins = plugins; this.data = data; this.task = task; this.callback = callback; } /** * Executed every tick. Depending on the callback value, this task can stop * future executions. */ public execute(): void { const ev = this.callback(); if (!ev.callbackResult) { if (!ev.shouldContinueLooping) { this.stop(); } return; } if (this.task) { // only gets executed if the callback returns true for its result this.actor.enqueueBaseTask(this.task); } // call the relevant plugins on each tick, if provided this.plugins.forEach(plugin => { if (!plugin || !plugin.handler) { return; } const action = { player: this.actor, ...this.data, } as TAction; plugin.handler(action); }); if (!ev.shouldContinueLooping) { this.stop(); return; } } } ================================================ FILE: src/engine/action/pipe/task/walk-to-actor-plugin-task.ts ================================================ import type { ActionHook } from '@engine/action/hook/action-hook'; import type { ItemOnNpcAction } from '@engine/action/pipe/item-on-npc.action'; import type { ItemOnPlayerAction } from '@engine/action/pipe/item-on-player.action'; import type { NpcInteractionAction } from '@engine/action/pipe/npc-interaction.action'; import type { PlayerInteractionAction } from '@engine/action/pipe/player-interaction.action'; import { ActorActorInteractionTask } from '@engine/task/impl/actor-actor-interaction-task'; import type { Actor } from '@engine/world/actor/actor'; import type { Player } from '@engine/world/actor/player/player'; /** * All actions supported by this plugin task. */ type ActorAction = PlayerInteractionAction | ItemOnPlayerAction | NpcInteractionAction | ItemOnNpcAction; /** * An ActionHook for a supported ObjectAction. */ type ActorActionHook = ActionHook void>; type ActorKey = 'otherPlayer' | 'npc'; /** * The data unique to the action being executed (i.e. excluding shared data) */ type ActorActionData = Omit; /** * This is a task to migrate old `walkTo` item interaction actions to the new task system. * * This is a first-pass implementation to allow for removal of the old action system. * It will be refactored in future to be more well suited to our plugin system. */ export class WalkToActorPluginTask< TAction extends ActorAction, TActorKey extends ActorKey, TOtherActor extends Actor, > extends ActorActorInteractionTask { /** * The plugins to execute when the player arrives at the object. */ private plugins: ActorActionHook[]; private data: ActorActionData; private actorKey: TActorKey; constructor( plugins: ActorActionHook[], player: Player, actorKey: TActorKey, other: TOtherActor, data: ActorActionData, ) { super(player, other); this.plugins = plugins; this.data = data; this.actorKey = actorKey; } /** * Executed every tick to check if the player has arrived yet and calls the plugins if so. */ public execute(): void { // call super to manage waiting for the movement to complete super.execute(); // check if the player has arrived yet const other = this.other; const otherPosition = this.other?.position; if (!other || !otherPosition) { return; } // call the relevant plugins this.plugins.forEach(plugin => { if (!plugin || !plugin.handler) { return; } const action = { player: this.actor, position: otherPosition, [this.actorKey]: other, ...this.data, }; // I wish I didn't have to cast here, but TypeScript is making it difficult plugin.handler(action as unknown as TAction); }); // this task only executes once, on arrival this.stop(); } } ================================================ FILE: src/engine/action/pipe/task/walk-to-item-plugin-task.ts ================================================ import type { SpawnedItemInteractionHook } from '@engine/action/pipe/spawned-item-interaction.action'; import type { ItemDetails } from '@engine/config/item-config'; import { ActorWorldItemInteractionTask } from '@engine/task/impl/actor-world-item-interaction-task'; import type { Player } from '@engine/world/actor/player/player'; import type { WorldItem } from '@engine/world/items/world-item'; /** * This is a task to migrate old `walkTo` item interaction actions to the new task system. * * This is a first-pass implementation to allow for removal of the old action system. * It will be refactored in future to be more well suited to our plugin system. */ export class WalkToItemPluginTask extends ActorWorldItemInteractionTask { /** * The plugins to execute when the player arrives at the item. */ private plugins: SpawnedItemInteractionHook[]; /** * Details about the item */ private itemDetails: ItemDetails; constructor(plugins: SpawnedItemInteractionHook[], player: Player, worldItem: WorldItem, itemDetails: ItemDetails) { super(player, worldItem); this.plugins = plugins; this.itemDetails = itemDetails; } /** * Executed every tick to check if the player has arrived yet and calls the plugins if so. */ public execute(): void { // call super to manage waiting for the movement to complete super.execute(); // check if the player has arrived yet const worldItem = this.worldItem; if (!worldItem) { return; } // call the relevant plugins this.plugins.forEach(plugin => { if (!plugin || !plugin.handler) { return; } plugin.handler({ player: this.actor, worldItem: worldItem, itemDetails: this.itemDetails, }); }); // this task only executes once, on arrival this.stop(); } } ================================================ FILE: src/engine/action/pipe/task/walk-to-object-plugin-task.ts ================================================ import type { ActionHook } from '@engine/action/hook/action-hook'; import type { ItemOnObjectAction } from '@engine/action/pipe/item-on-object.action'; import type { ObjectInteractionAction } from '@engine/action/pipe/object-interaction.action'; import { ActorLandscapeObjectInteractionTask } from '@engine/task/impl/actor-landscape-object-interaction-task'; import type { Player } from '@engine/world/actor/player/player'; import type { LandscapeObject } from '@runejs/filestore'; /** * All actions supported by this plugin task. */ type ObjectAction = ObjectInteractionAction | ItemOnObjectAction; /** * An ActionHook for a supported ObjectAction. */ type ObjectActionHook = ActionHook void>; /** * The data unique to the action being executed (i.e. excluding shared data) */ type ObjectActionData = Omit; /** * This is a task to migrate old `walkTo` item interaction actions to the new task system. * * This is a first-pass implementation to allow for removal of the old action system. * It will be refactored in future to be more well suited to our plugin system. */ export class WalkToObjectPluginTask extends ActorLandscapeObjectInteractionTask { /** * The plugins to execute when the player arrives at the object. */ private plugins: ObjectActionHook[]; private data: ObjectActionData; constructor(plugins: ObjectActionHook[], player: Player, landscapeObject: LandscapeObject, data: ObjectActionData) { super( player, landscapeObject, // TODO (jkm) handle object size // TODO (jkm) pass orientation instead of size 1, 1, ); this.plugins = plugins; this.data = data; } /** * Executed every tick to check if the player has arrived yet and calls the plugins if so. */ public execute(): void { // call super to manage waiting for the movement to complete super.execute(); // check if the player has arrived yet const landscapeObject = this.landscapeObject; const landscapeObjectPosition = this.landscapeObjectPosition; if (!landscapeObject || !landscapeObjectPosition) { return; } // call the relevant plugins this.plugins.forEach(plugin => { if (!plugin || !plugin.handler) { return; } const action = { player: this.actor, object: landscapeObject, position: landscapeObjectPosition, ...this.data, } as TAction; plugin.handler(action); }); // this task only executes once, on arrival this.stop(); } } ================================================ FILE: src/engine/action/pipe/widget-interaction.action.ts ================================================ import type { ActionPipe, RunnableHooks } from '@engine/action/action-pipeline'; import type { ActionHook } from '@engine/action/hook/action-hook'; import { getActionHooks } from '@engine/action/hook/action-hook'; import { advancedNumberHookFilter, questHookFilter } from '@engine/action/hook/hook-filters'; import type { Player } from '@engine/world/actor/player/player'; /** * Defines a widget action hook. */ export interface WidgetInteractionActionHook extends ActionHook { // A single UI widget ID or a list of widget IDs that this action applies to. widgetIds: number | number[]; // A single UI widget child ID or a list of child IDs that this action applies to. childIds?: number | number[]; // The context menu option index for this action. optionId?: number; // Whether or not this item action should cancel other running or queued actions. cancelActions?: boolean; } /** * The widget action hook handler function to be called when the hook's conditions are met. */ export type widgetInteractionActionHandler = (widgetInteractionAction: WidgetInteractionAction) => void; /** * Details about a widget action being performed. */ export interface WidgetInteractionAction { // The player performing the action. player: Player; // The ID of the UI widget that the button is on. widgetId: number; // The ID of the interacted child within the UI widget. childId: number; // The selected context menu option index. optionId: number; } /** * The pipe that the game engine hands widget actions off to. * @param player The player performing the action. * @param widgetId The ID of the widget. * @param childId The ID of the widget child being interacted with. * @param optionId The widget context option chosen by the player. */ const widgetActionPipe = ( player: Player, widgetId: number, childId: number, optionId: number, ): RunnableHooks | null => { const playerWidget = Object.values(player.interfaceState.widgetSlots).find(widget => widget && widget.widgetId === widgetId); if (playerWidget?.fakeWidget) { widgetId = playerWidget.fakeWidget; } // Find all item on item action plugins that match this action let matchingHooks = getActionHooks('widget_interaction').filter(plugin => { if (!plugin.widgetIds) { return false; } if (!questHookFilter(player, plugin)) { return false; } if (!advancedNumberHookFilter(plugin.widgetIds, widgetId)) { return false; } if (plugin.optionId !== undefined && plugin.optionId !== optionId) { return false; } if (plugin.childIds !== undefined) { return advancedNumberHookFilter(plugin.childIds, childId); } return true; }); const questActions = matchingHooks.filter(plugin => plugin.questRequirement !== undefined); if (questActions.length !== 0) { matchingHooks = questActions; } if (matchingHooks.length === 0) { player.outgoingPackets.chatboxMessage(`Unhandled widget option: ${widgetId}, ${childId}:${optionId}`); return null; } const action: WidgetInteractionAction = { player, widgetId, childId, optionId }; return { hooks: matchingHooks, action, }; }; /** * Widget action pipe definition. */ export default ['widget_interaction', widgetActionPipe] as ActionPipe; ================================================ FILE: src/engine/config/config-handler.ts ================================================ import 'json5/lib/register'; import type { ItemPresetConfiguration } from '@engine/config/item-config'; import { ItemDetails, loadItemConfigurations } from '@engine/config/item-config'; import type { ItemSpawn } from '@engine/config/item-spawn-config'; import { loadItemSpawnConfigurations } from '@engine/config/item-spawn-config'; import type { MusicTrack } from '@engine/config/music-regions-config'; import { loadMusicRegionConfigurations } from '@engine/config/music-regions-config'; import type { NpcDetails, NpcPresetConfiguration } from '@engine/config/npc-config'; import { loadNpcConfigurations, translateNpcServerConfig } from '@engine/config/npc-config'; import type { NpcSpawn } from '@engine/config/npc-spawn-config'; import { loadNpcSpawnConfigurations } from '@engine/config/npc-spawn-config'; import type { Shop } from '@engine/config/shop-config'; import { loadShopConfigurations } from '@engine/config/shop-config'; import { questMap } from '@engine/plugins/loader'; import type { Quest } from '@engine/world/actor/player/quest'; import { logger } from '@runejs/common'; import type { ObjectConfig, XteaRegion } from '@runejs/filestore'; import { loadXteaRegionFiles } from '@runejs/filestore'; import { filestore } from '@server/game/game-server'; import _ from 'lodash'; export let itemMap: { [key: string]: ItemDetails }; export let itemGroupMap: Record>; export let itemIdMap: { [key: number]: string }; export let objectMap: { [key: number]: ObjectConfig }; export let itemPresetMap: ItemPresetConfiguration; export let npcMap: { [key: string]: NpcDetails }; export let npcIdMap: { [key: number]: string }; export let npcPresetMap: NpcPresetConfiguration; export let npcSpawns: NpcSpawn[] = []; export let musicRegions: MusicTrack[] = []; export let itemSpawns: ItemSpawn[] = []; export let shopMap: { [key: string]: Shop }; export let xteaRegions: { [key: number]: XteaRegion }; export const musicRegionMap = new Map(); export const widgets: { [key: string]: any } = require('../../../data/config/widgets.json'); export async function loadCoreConfigurations(): Promise { xteaRegions = await loadXteaRegionFiles('data/config/xteas'); } export async function loadGameConfigurations(): Promise { logger.info(`Loading server configurations...`); const { items, itemIds, itemPresets, itemGroups } = await loadItemConfigurations('data/config/items/'); itemMap = items; itemGroupMap = itemGroups; itemIdMap = itemIds; itemPresetMap = itemPresets; const { npcs, npcIds, npcPresets } = await loadNpcConfigurations('data/config/npcs/'); npcMap = npcs; npcIdMap = npcIds; npcPresetMap = npcPresets; npcSpawns = await loadNpcSpawnConfigurations('data/config/npc-spawns/'); musicRegions = await loadMusicRegionConfigurations(); musicRegions.forEach(song => song.regionIds.forEach(region => musicRegionMap.set(region, song.songId))); itemSpawns = await loadItemSpawnConfigurations('data/config/item-spawns/'); shopMap = await loadShopConfigurations('data/config/shops/'); objectMap = {}; logger.info( `Loaded ${musicRegions.length} music regions, ${Object.keys(itemMap).length} items, ${itemSpawns.length} item spawns, ` + `${Object.keys(npcMap).length} npcs, ${npcSpawns.length} npc spawns, and ${Object.keys(shopMap).length} shops.`, ); } /** * find all items in all select groups * @param groupKeys array of string of which to find items connected with * @return itemsKeys array of itemkeys in all select groups */ export const findItemTagsInGroups = (groupKeys: string[]): string[] => { return Object.keys( groupKeys.reduce>((all, groupKey) => { const items = itemGroupMap[groupKey] || {}; return { ...all, ...items }; }, {}), ); }; /** * find all items which are shared by all the groups, and discard items not in all groups * @param groupKeys groups keys which to find items shared by * @return itemKeys of items shared by all groups */ export const findItemTagsInGroupFilter = (groupKeys: string[]): string[] => { if (!groupKeys || groupKeys.length === 0) { return []; } let collection: Record | undefined = undefined; groupKeys.forEach(groupKey => { if (!collection) { collection = { ...(itemGroupMap[groupKey] || {}) }; return; } const current = itemGroupMap[groupKey] || {}; Object.keys(collection).forEach(existingItemKey => { if (!(existingItemKey in current) && collection) { delete collection[existingItemKey]; } }); }); return Object.keys(collection || {}); }; export const findItem = (itemKey: number | string): ItemDetails | null => { if (!itemKey) { return null; } let gameId: number | null = null; if (typeof itemKey === 'number') { gameId = itemKey; itemKey = itemIdMap[gameId]; if (!itemKey) { logger.warn(`Item ${gameId} is not yet registered on the server.`); } } let item; if (itemKey) { item = itemMap[itemKey]; if (!item) { // Try fetching variation with suffix 0 item = itemMap[`${itemKey}:0`]; } if (item?.gameId) { gameId = item.gameId; } } if (gameId) { const cacheItem = filestore.configStore.itemStore.getItem(gameId); item = _.merge(item, cacheItem); } return item ? new ItemDetails(item) : null; }; export const findNpc = (inputKey: number | string): NpcDetails => { if (!inputKey) { throw new Error('No NPC was provided to findNpc.'); } // Pathway for finding an NPC by its game id if (typeof inputKey === 'number') { const gameId = inputKey; const npcKey = npcIdMap[gameId]; // If we can't find a config in the project for this NPC - we fallback // to the cache which is the basic info loaded by `fileserver`. if (!npcKey) { const cacheNpc = filestore.configStore.npcStore.getNpc(gameId); if (cacheNpc) { return cacheNpc; } else { logger.warn(`NPC ${gameId} is not yet configured on the server and a matching cache NPC was not found.`); throw new Error(`NPC ${gameId} is not yet configured on the server and a matching cache NPC was not found.`); } } } // Otherwise we got a string identifier for the npcs let npc = npcMap[inputKey]; if (!npc) { // Try fetching variation with suffix 0 npc = npcMap[`${npc}:0`]; } if (!npc) { logger.warn(`NPC ${inputKey} is not yet configured on the server and a matching cache NPC was not provided.`); throw new Error(`NPC ${inputKey} is not yet configured on the server and a matching cache NPC was not provided.`); } if (npc.extends) { let extensions = npc.extends; if (typeof extensions === 'string') { extensions = [extensions]; } extensions.forEach(extKey => { const extensionNpc = npcPresetMap[extKey]; if (extensionNpc) { npc = _.merge(npc, translateNpcServerConfig(undefined, extensionNpc)); } }); } return npc; }; export const findObject = (objectId: number): ObjectConfig | null => { if (!objectMap[objectId]) { const object = filestore.objectStore.getObject(objectId); if (!object) { return null; } objectMap[objectId] = object; return object; } else { return objectMap[objectId]; } }; export const findShop = (shopKey: string): Shop | null => { if (!shopKey) { return null; } return shopMap[shopKey] || null; }; export const findQuest = (questId: string): Quest | null => { const questKey = Object.keys(questMap).find(quest => quest.toLocaleLowerCase() === questId.toLocaleLowerCase()); return questKey ? questMap[questKey] : null; }; export const findMusicTrack = (trackId: number): MusicTrack | null => { return musicRegions.find(track => track.songId === trackId) || null; }; export const findMusicTrackByButtonId = (buttonId: number): MusicTrack | null => { return musicRegions.find(track => track.musicTabButtonId === buttonId) || null; }; export const findSongIdByRegionId = (regionId: number): number | null => { return musicRegionMap.get(regionId) || null; }; ================================================ FILE: src/engine/config/data-dump.ts ================================================ import { writeFileSync } from 'fs'; import { join } from 'path'; import { logger } from '@runejs/common'; import type { ItemConfig, NpcConfig, ObjectConfig, WidgetBase } from '@runejs/filestore'; import { filestore } from '@server/game/game-server'; export interface DataDumpResult { successful: boolean; filePath: string; } function dump(fileName: string, definitions: T[]): DataDumpResult { const filePath = join('data/dump', fileName); const arr: T[] = []; for (let i = 0; i < definitions.length; i++) { arr.push(definitions[i]); } try { writeFileSync(filePath, JSON.stringify(arr, null, 4)); return { successful: true, filePath, }; } catch (error) { logger.error(`Error dumping ${fileName}`); return { successful: false, filePath, }; } } export const dumpNpcs = (): DataDumpResult => { return dump('npcs.json', filestore.configStore.npcStore.decodeNpcStore()); }; export const dumpItems = (): DataDumpResult => { return dump('items.json', filestore.configStore.itemStore.decodeItemStore()); }; export const dumpObjects = (): DataDumpResult => { return dump('objects.json', filestore.configStore.objectStore.decodeObjectStore()); }; export const dumpWidgets = (): DataDumpResult => { return dump('widgets.json', filestore.widgetStore.decodeWidgetStore()); }; ================================================ FILE: src/engine/config/directories.ts ================================================ import { join } from 'path'; export const BUILD_DIR = join('.', 'dist', 'engine'); ================================================ FILE: src/engine/config/item-config.ts ================================================ import { deepMerge } from '@engine/util/objects'; import type { SkillName } from '@engine/world/actor/skills'; import { logger } from '@runejs/common'; import { loadConfigurationFiles } from '@runejs/common/fs'; export type WeaponStyle = | 'axe' | 'hammer' | 'bow' | 'claws' | 'crossbow' | 'gun' | 'slash_sword' | '2h_sword' | 'pickaxe' | 'halberd' | 'polestaff' | 'scythe' | 'spear' | 'mace' | 'dagger' | 'magical_staff' | 'darts' | 'unarmed' | 'whip'; export const weaponWidgetIds: Record = { axe: 75, hammer: 76, bow: 77, claws: 78, crossbow: 79, gun: 80, slash_sword: 81, '2h_sword': 82, pickaxe: 83, halberd: 84, polestaff: 85, scythe: 86, spear: 87, mace: 88, dagger: 89, magical_staff: 90, darts: 91, unarmed: 92, whip: 93, }; export type EquipmentSlot = | 'head' | 'back' | 'neck' | 'main_hand' | 'off_hand' | 'torso' | 'legs' | 'hands' | 'feet' | 'ring' | 'quiver' | '2h'; export const equipmentIndices = { head: 0, back: 1, neck: 2, main_hand: 3, torso: 4, off_hand: 5, legs: 7, hands: 9, feet: 10, ring: 12, quiver: 13, }; export const equipmentIndex = (equipmentSlot: EquipmentSlot): number => equipmentIndices[equipmentSlot]; export const getEquipmentSlot = (index: number): EquipmentSlot => Object.keys(equipmentIndices).find(key => equipmentIndices[key] === index) as EquipmentSlot; export type EquipmentType = 'hat' | 'helmet' | 'torso' | 'full_top' | 'one_handed' | 'two_handed'; export interface ItemRequirements { skills?: { [key: string]: number }; quests?: { [key: string]: number }; } export interface OffensiveBonuses { speed?: number; stab?: number; slash?: number; crush?: number; magic?: number; ranged?: number; } export interface DefensiveBonuses { stab?: number; slash?: number; crush?: number; magic?: number; ranged?: number; } export interface SkillBonuses { [key: string]: number; } export interface WeaponInfo { style: WeaponStyle; playerAnimations: any; } export interface ItemMetadata { [key: string]: unknown; consume_effects?: { replaced_by?: string; clock: string; // Name of timer to be used for cooldown skills?: { [key in SkillName]: number | [number, number]; }; energy?: number | [number, number]; special: boolean; }; /** * If defined, the 'bury bones' plugin will assign experience according * to this value. */ prayerBuryXp?: number; /** * If the full URL is `"https://oldschool.runescape.wiki/w/Bat_bones"` * then the value is `"Bat_bones"`. * * @example "Bat_bones" */ wikiId?: string; } export interface EquipmentData { equipmentSlot: EquipmentSlot; equipmentType?: EquipmentType; requirements?: ItemRequirements; offensiveBonuses?: OffensiveBonuses; defensiveBonuses?: DefensiveBonuses; skillBonuses?: SkillBonuses; weaponInfo?: WeaponInfo; } export interface ItemPresetConfiguration { [key: string]: ItemConfiguration; } export interface ItemConfiguration { extends?: string | string[]; game_id?: number; examine?: string; tradable?: boolean; variations?: [ { suffix: string; } & ItemConfiguration, ]; weight?: number; equippable?: boolean; consumable?: boolean; destroy?: string | boolean; groups?: string[]; equipment_data?: { equipment_slot: EquipmentSlot; equipment_type?: EquipmentType; requirements?: ItemRequirements; offensive_bonuses?: OffensiveBonuses; defensive_bonuses?: DefensiveBonuses; skill_bonuses?: SkillBonuses; weapon_info?: WeaponInfo; }; metadata?: ItemMetadata; } /** * Full server + cache details about a specific game item. */ export class ItemDetails { extends?: string | string[]; key: string; gameId: number; name: string = ''; examine: string = ''; tradable: boolean = false; equippable: boolean = false; destroy?: string | boolean; weight: number; equipmentData: EquipmentData; metadata: ItemMetadata = {}; consumable?: boolean; stackable: boolean = false; value: number = 0; groups: string[] = []; members: boolean = false; groundOptions: string[] = []; inventoryOptions: string[] = []; teamId: number; bankNoteId: number; bankNoteTemplate: number; stackableIds: number[]; stackableAmounts: number[]; public constructor(item?: ItemDetails) { if (item) { const keys = Object.keys(item); keys.forEach(key => (this[key] = item[key])); } } get lowAlchValue(): number { return Math.floor(this.value * 0.4); } get highAlchValue(): number { return Math.floor(this.lowAlchValue * 1.5); } get minimumValue(): number { return Math.floor(this.lowAlchValue * 0.25); } } export function translateItemConfig(key: string | undefined, config: ItemConfiguration): any { return { key, extends: config.extends || undefined, gameId: config.game_id, examine: config.examine, tradable: config.tradable, equippable: config.equippable, weight: config.weight, destroy: config.destroy || undefined, groups: config.groups || [], consumable: config.consumable, equipmentData: config.equipment_data ? { equipmentType: config.equipment_data?.equipment_type || undefined, equipmentSlot: config.equipment_data?.equipment_slot || undefined, requirements: config.equipment_data?.requirements || undefined, offensiveBonuses: config.equipment_data?.offensive_bonuses || undefined, defensiveBonuses: config.equipment_data?.defensive_bonuses || undefined, skillBonuses: config.equipment_data?.skill_bonuses || undefined, weaponInfo: config.equipment_data?.weapon_info || undefined, } : undefined, metadata: config.metadata ? { ...config.metadata } : {}, }; } export async function loadItemConfigurations(path: string): Promise<{ items: { [key: string]: ItemDetails }; itemIds: { [key: number]: string }; itemPresets: ItemPresetConfiguration; itemGroups: Record>; }> { const itemIds: { [key: number]: string } = {}; const items: { [key: string]: ItemDetails } = {}; const itemGroups: Record> = {}; // Record where key is group id, and value is an array of all itemstags in group let itemPresets: ItemPresetConfiguration = {}; const files = await loadConfigurationFiles(path); const itemConfigurations: Record = {}; files.forEach(itemConfigs => { const itemKeys = Object.keys(itemConfigs); itemKeys.forEach(key => { if (key === 'presets') { itemPresets = { ...itemPresets, ...itemConfigs[key] }; } else { itemConfigurations[key] = itemConfigs[key] as ItemConfiguration; } }); }); Object.entries(itemConfigurations).forEach(([key, itemConfig]) => { if (itemConfig.game_id !== undefined && !isNaN(itemConfig.game_id)) { itemIds[itemConfig.game_id] = key; let item = { ...translateItemConfig(key, itemConfig) }; if (item?.extends) { let extensions = item.extends; if (typeof extensions === 'string') { extensions = [extensions]; } extensions.forEach(extKey => { const extensionItem = itemPresets[extKey]; if (extensionItem) { const preset = translateItemConfig(undefined, extensionItem); item = deepMerge(item, preset); } }); } items[key] = item; item.groups.forEach(group => { if (!itemGroups[group]) { itemGroups[group] = {}; } itemGroups[group][key] = true; }); } if (itemConfig.variations) { for (const subItem of itemConfig.variations) { if (!subItem.game_id) { logger.warn(`Item ${key} has a variation without a game_id. Skipping.`); continue; } const subKey = subItem.suffix ? key + ':' + subItem.suffix : key; const baseItem = JSON.parse(JSON.stringify({ ...translateItemConfig(key, itemConfig) })); const subBaseItem = JSON.parse(JSON.stringify({ ...translateItemConfig(subKey, subItem) })); itemIds[subItem.game_id] = subKey; if (!items[subKey]) { let item = deepMerge(baseItem, subBaseItem); if (item?.extends) { let extensions = item.extends; if (typeof extensions === 'string') { extensions = [extensions]; } extensions.forEach(extKey => { const extensionItem = itemPresets[extKey]; if (extensionItem) { const preset = translateItemConfig(undefined, extensionItem); item = deepMerge(item, preset); } }); } items[subKey] = item; items[subKey].groups.forEach(group => { if (!itemGroups[group]) { itemGroups[group] = {}; } itemGroups[group][subKey] = true; }); } else { logger.warn(`Duplicate item key ${subKey} found - the item was not loaded.`); } } } }); return { items, itemIds, itemPresets, itemGroups }; } ================================================ FILE: src/engine/config/item-spawn-config.ts ================================================ import { Position } from '@engine/world/position'; import { loadConfigurationFiles } from '@runejs/common/fs'; export interface ItemSpawnConfiguration { item: string; amount?: number; spawn_x: number; spawn_y: number; spawn_level?: number; instance?: 'global' | 'player'; respawn?: number; metadata?: { [key: string]: unknown }; } export class ItemSpawn { public itemKey: string; public amount: number = 1; public spawnPosition: Position; public instance: 'global' | 'player' = 'global'; public respawn: number = 30; public metadata: { [key: string]: unknown } = {}; public constructor(itemKey: string, position: Position) { this.itemKey = itemKey; this.spawnPosition = position; } } export function translateItemSpawnConfig(config: ItemSpawnConfiguration): ItemSpawn { const spawn = new ItemSpawn(config.item, new Position(config.spawn_x, config.spawn_y, config.spawn_level || 0)); if (config.amount !== undefined) { spawn.amount = config.amount; } if (config.instance !== undefined) { spawn.instance = config.instance; } if (config.respawn !== undefined) { spawn.respawn = config.respawn; } if (config.metadata !== undefined) { spawn.metadata = config.metadata; } return spawn; } export async function loadItemSpawnConfigurations(path: string): Promise { const itemSpawns: ItemSpawn[] = []; const files = await loadConfigurationFiles(path); files.forEach(spawns => spawns.forEach(itemSpawn => itemSpawns.push(translateItemSpawnConfig(itemSpawn)))); return itemSpawns; } ================================================ FILE: src/engine/config/music-regions-config.ts ================================================ import * as musicRegionsFile from '../../../data/config/music/musicRegions.json'; export interface MusicRegionsConfiguration { songId: number; songName: string; musicTabButtonId: number; regionIds: number[]; } export class MusicTrack { public songId: number; public songName: string; public musicTabButtonId: number; public regionIds: number[]; public constructor(songId: number, songName: string, musicTabButtonId: number, regionIds: number[]) { this.songId = songId; this.songName = songName; this.musicTabButtonId = musicTabButtonId; this.regionIds = regionIds; } } export function translateMusicRegionsConfig(config: MusicRegionsConfiguration): MusicTrack { return new MusicTrack(config.songId, config.songName, config.musicTabButtonId, config.regionIds); } export async function loadMusicRegionConfigurations(): Promise { const regions: MusicTrack[] = []; await musicRegionsFile.musicRegions.forEach(musicRegion => regions.push(translateMusicRegionsConfig(musicRegion))); return regions; } ================================================ FILE: src/engine/config/npc-config.ts ================================================ import type { QuestRequirement } from '@engine/action/hook/action-hook'; import type { DefensiveBonuses } from '@engine/config/item-config'; import { logger } from '@runejs/common'; import { loadConfigurationFiles } from '@runejs/common/fs'; import { NpcConfig } from '@runejs/filestore'; import { filestore } from '@server/game/game-server'; import _ from 'lodash'; export interface NpcSkills { [key: string]: number; } export interface OffensiveStats { speed?: number; attack?: number; strength?: number; magic?: number; magicStrength?: number; ranged?: number; rangedStrength?: number; } export interface NpcCombatAnimations { attack?: number | number[]; defend?: number; death?: number; } export interface DropTable { itemKey: string; frequency: string; amount?: number; amountMax?: number; questRequirement?: QuestRequirement; } export interface NpcPresetConfiguration { [key: string]: NpcServerConfig; } export interface NpcServerConfig { extends?: string | string[]; game_id: number; skills?: NpcSkills; killable?: boolean; respawn_time?: number; offensive_stats?: { speed?: number; attack?: number; strength?: number; magic?: number; magic_strength?: number; ranged?: number; ranged_strength?: number; }; defensive_stats?: DefensiveBonuses; variations?: [ { suffix: string; } & NpcServerConfig, ]; animations?: NpcCombatAnimations; drop_table?: DropTable[]; metadata: { [key: string]: unknown }; } /** * Full server + cache details about a specific game NPC. */ export class NpcDetails extends NpcConfig { extends?: string | string[]; key?: string; skills?: NpcSkills; killable?: boolean; respawnTime?: number; offensiveStats?: OffensiveStats; defensiveStats?: DefensiveBonuses; combatAnimations?: NpcCombatAnimations; dropTable?: DropTable[] = []; metadata?: { [key: string]: unknown } = {}; public constructor(defaultValues: { [key: string]: any }) { super(); Object.keys(defaultValues).forEach(key => (this[key] = defaultValues[key])); } } export function translateNpcServerConfig(npcKey: string | undefined, config: NpcServerConfig): NpcDetails { return new NpcDetails({ key: npcKey, extends: config.extends || undefined, skills: config.skills || {}, killable: config.killable || false, respawnTime: config.respawn_time || 1, offensiveStats: config.offensive_stats ? { speed: config.offensive_stats.speed || undefined, attack: config.offensive_stats.attack || undefined, strength: config.offensive_stats.strength || undefined, magic: config.offensive_stats.magic || undefined, magicStrength: config.offensive_stats.magic_strength || undefined, ranged: config.offensive_stats.ranged || undefined, rangedStrength: config.offensive_stats.ranged_strength || undefined, } : undefined, defensiveStats: config.defensive_stats || undefined, combatAnimations: config.animations || {}, dropTable: config.drop_table || undefined, metadata: config.metadata || {}, }); } export async function loadNpcConfigurations(path: string): Promise<{ npcs: { [key: string]: NpcDetails }; npcIds: { [key: number]: string }; npcPresets: NpcPresetConfiguration; }> { const npcIds: { [key: number]: string } = {}; const npcs: { [key: string]: NpcDetails } = {}; let npcPresets: NpcPresetConfiguration = {}; const files = await loadConfigurationFiles<{ presets: NpcPresetConfiguration | undefined } & { [key: string]: NpcServerConfig }>(path); files.forEach(npcConfigs => { const npcKeys = Object.keys(npcConfigs); npcKeys.forEach(key => { if (key === 'presets') { npcPresets = { ...npcPresets, ...npcConfigs[key] }; } else { const npcConfig = npcConfigs[key] as NpcServerConfig; if (!isNaN(npcConfig.game_id)) { npcIds[npcConfig.game_id] = key; npcs[key] = { ...translateNpcServerConfig(key, npcConfig), ...filestore.configStore.npcStore.getNpc(npcConfig.game_id), }; } if (npcConfig.variations) { for (const variation of npcConfig.variations) { try { const subKey = key + ':' + variation.suffix; const baseItem = JSON.parse( JSON.stringify({ ...translateNpcServerConfig(key, npcConfig), ...filestore.configStore.npcStore.getNpc(npcConfig.game_id), }), ); const subBaseItem = JSON.parse( JSON.stringify({ ...translateNpcServerConfig(subKey, variation), ...filestore.configStore.npcStore.getNpc(variation.game_id), }), ); npcIds[variation.game_id] = subKey; npcs[subKey] = _.merge(baseItem, subBaseItem); } catch (error) { logger.error(`Error registering npc variant ${key}_${variation.suffix}`); logger.error(error); } } } } }); }); return { npcs, npcIds, npcPresets }; } ================================================ FILE: src/engine/config/npc-spawn-config.ts ================================================ import type { Direction } from '@engine/world/direction'; import { Position } from '@engine/world/position'; import { loadConfigurationFiles } from '@runejs/common/fs'; export interface NpcSpawnConfiguration { npc: string; spawn_x: number; spawn_y: number; spawn_level?: number; movement_radius?: number; face?: Direction; } export class NpcSpawn { public npcKey: string; public spawnPosition: Position; public movementRadius: number; public faceDirection: Direction; public constructor(npcKey: string, spawnPosition: Position, movementRadius: number = 0, faceDirection: Direction = 'WEST') { this.npcKey = npcKey; this.spawnPosition = spawnPosition; this.movementRadius = movementRadius; this.faceDirection = faceDirection; } } export function translateNpcSpawnConfig(config: NpcSpawnConfiguration): NpcSpawn { return new NpcSpawn( config.npc, new Position(config.spawn_x, config.spawn_y, config.spawn_level || 0), config.movement_radius || 0, config.face || 'WEST', ); } export async function loadNpcSpawnConfigurations(path: string): Promise { const npcSpawns: NpcSpawn[] = []; const files = await loadConfigurationFiles(path); files.forEach(spawns => spawns.forEach(npcSpawn => npcSpawns.push(translateNpcSpawnConfig(npcSpawn)))); return npcSpawns; } ================================================ FILE: src/engine/config/quest-config.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import type { Npc } from '@engine/world/actor/npc'; import type { Player } from '@engine/world/actor/player/player'; import { logger } from '@runejs/common'; export type QuestKey = number | 'complete'; export type QuestStageHandler = { [key in QuestKey]?: (player: Player) => void | Promise; }; export type QuestDialogueHandler = { [key in QuestKey]?: (player: Player, npc: Npc) => void | Promise; }; export type QuestJournalHandler = { [key in QuestKey]?: ((player: Player) => Promise) | ((player: Player) => string) | string; }; export interface QuestCompletion { questCompleteWidget: { rewardText?: string[]; modelId?: number; itemId?: number; modelRotationX?: number; modelRotationY?: number; modelZoom?: number; }; giveRewards?: ((player?: Player) => void) | ((player?: Player) => Promise); } export class PlayerQuest { public readonly questId: string; public progress: QuestKey = 0; public complete: boolean = false; public readonly metadata: { [key: string]: any } = {}; public constructor(questId: string) { this.questId = questId; } } export function questDialogueActionFactory( questId: string, npcDialogueHandler: QuestDialogueHandler, stageHandler: (player: Player) => Promise, ): npcInteractionActionHandler { return async ({ player, npc }) => { const quest = player.getQuest(questId); if (!quest) { return; } const progress = quest.progress; const dialogueHandler = npcDialogueHandler[progress]; if (dialogueHandler) { try { await dialogueHandler(player, npc); } catch (e) { logger.error(e); } await stageHandler(player); } }; } ================================================ FILE: src/engine/config/shop-config.testskip.ts ================================================ import { findItem } from '@engine/config/config-handler'; import type { Shop, ShopConfiguration } from '@engine/config/shop-config'; import { shopFactory } from '@engine/config/shop-config'; import { setupConfig } from '@server/game/game-server'; // @todo fix this by mocking the world instead of spinning it up describe('shopping', () => { const shopConfig: ShopConfiguration = { name: 'Test Shop', general_store: false, shop_sell_rate: 1.0, shop_buy_rate: 0.55, rate_modifier: 0.02, stock: [ { itemKey: 'rs:battlestaff', amount: 5, restock: 100, }, { itemKey: 'rs:staff', amount: 5, restock: 100, }, { itemKey: 'rs:magic_staff', amount: 5, restock: 200, }, { itemKey: 'rs:staff_of_air', amount: 2, restock: 1000, }, { itemKey: 'rs:staff_of_water', amount: 2, restock: 1000, }, { itemKey: 'rs:staff_of_earth', amount: 2, restock: 1000, }, { itemKey: 'rs:staff_of_fire', amount: 2, restock: 1000, }, ], }; const shopname = 'rs:test_shop'; let shop: Shop; let world; beforeAll(async () => { world = await setupConfig(); }); beforeEach(() => { shop = shopFactory(shopname, shopConfig); }); describe('shop object', () => { test('shop should exist', () => { expect(shop?.key).toEqual(shopname); }); test('shop stock should be correct', () => { expect(shopConfig.stock.reduce((all, curr) => all && shop.isItemSoldHere(curr.itemKey), true)).toBeTruthy(); expect(shop.isItemSoldHere('rs:salmon')).toBeFalsy(); }); test('player should buy item from shop at correct value', () => { // Player purchases, shop sells console.log(shop.container.items.map(a => a?.itemId)); const battleStaffItem = findItem('rs:battlestaff'); if (!battleStaffItem) { throw new Error('battleStaffItem not found in server config'); } const shopBStaffIndex = shop.container.findIndex(battleStaffItem.gameId); const shopBstaff = shop.container.items[shopBStaffIndex]; if (!shopBstaff) { throw new Error('Battle staff item not found in shop config'); } expect(shop.getBuyFromShopPrice(battleStaffItem)).toBe(Math.round(battleStaffItem.value * (shopConfig.shop_sell_rate || 1.0))); expect(shop.getBuyFromShopPrice(battleStaffItem)).toBe(7000); // BStaff standard price expect(shopBstaff.amount).toBe(5); shopBstaff.amount += 1; expect(shopBstaff.amount).toBe(6); expect(shop.getBuyFromShopPrice(battleStaffItem)).toBe(6860); shopBstaff.amount -= 3; expect(shopBstaff.amount).toBe(3); expect(shop.getBuyFromShopPrice(battleStaffItem)).toBe(7280); }); test('shop should sell at correct value', () => { // Player sells, shop pays const battleStaffItem = findItem('rs:battlestaff'); if (!battleStaffItem) { throw new Error('battleStaffItem not found in server config'); } const shopBStaffIndex = shop.container.findIndex(battleStaffItem.gameId); const shopBstaff = shop.container.items[shopBStaffIndex]; if (!shopBstaff) { throw new Error('Battle staff item not found in shop config'); } // shopSpade.amount = 2; expect(shopBstaff.amount).toBe(5); expect(shop.getSellToShopPrice(battleStaffItem)).toBe(3850); shopBstaff.amount = 4; expect(shopBstaff.amount).toBe(4); expect(shop.getSellToShopPrice(battleStaffItem)).toBe(3990); shopBstaff.amount = 1; expect(shopBstaff.amount).toBe(1); expect(shop.getSellToShopPrice(battleStaffItem)).toBe(4410); shopBstaff.amount = 16; expect(shopBstaff.amount).toBe(16); expect(shop.getSellToShopPrice(battleStaffItem)).toBe(2310); shopBstaff.amount = 3; expect(shopBstaff.amount).toBe(3); expect(shop.getSellToShopPrice(battleStaffItem)).toBe(4130); }); }); }); ================================================ FILE: src/engine/config/shop-config.ts ================================================ import { findItem, widgets } from '@engine/config/config-handler'; import type { ItemDetails } from '@engine/config/item-config'; import type { WidgetClosedEvent } from '@engine/interface/interface-state'; import type { Player } from '@engine/world/actor/player/player'; import type { ContainerUpdateEvent } from '@engine/world/items/item-container'; import { ItemContainer } from '@engine/world/items/item-container'; import { loadConfigurationFiles } from '@runejs/common/fs'; import type { Subscription } from 'rxjs'; export type ShopStock = { itemKey: string; amount: number; restock?: number }[]; export class Shop { public readonly key: string; public readonly originalStock: ShopStock; public readonly container: ItemContainer; public readonly originalSellRate: number; public readonly originalBuyRate: number; public readonly generalStore: boolean; public name: string; public sellRate: number; public buyRate: number; public rateModifier: number; private customers: Player[]; private containerSubscription: Subscription; public constructor( key: string, name: string, generalStore: boolean, stock: ShopStock, sellRate: number, buyRate: number, modifier: number, ) { this.key = key; this.name = name; this.generalStore = generalStore; this.buyRate = buyRate; this.sellRate = sellRate; this.originalBuyRate = buyRate; this.originalSellRate = sellRate; this.rateModifier = modifier; this.originalStock = stock; this.container = new ItemContainer(40); this.containerSubscription = this.container.containerUpdated.subscribe((_update: ContainerUpdateEvent) => this.updateCustomers()); this.customers = []; this.resetShopStock(); } public resetShopStock(): void { for (let i = 0; i < this.container.size; i++) { if (this.originalStock[i]) { const { itemKey, amount } = this.originalStock[i]; const itemDetails = findItem(itemKey); this.container.set(i, !itemDetails ? null : { itemId: itemDetails.gameId, amount: amount || 0 }, false); } else { this.container.set(i, null, false); } } } /** * Get the price to purchase an item from shop * @param item to purchase from shop * @return price which item is available for purchase at */ public getBuyFromShopPrice(item: ItemDetails): number { const itemKey = item.key; const itemSoldHere: boolean = this.isItemSoldHere(itemKey); let originalStockAmount: number = 0; const itemStock = this.container.amount(item.gameId); if (itemSoldHere) { const foundStock = this.originalStock.find(stock => stock && stock.itemKey === itemKey); if (foundStock) { originalStockAmount = foundStock.amount; } } else { return -1; // Cannot buy from this shop } let finalAmount: number; if (itemStock === originalStockAmount) { finalAmount = Math.round(item.value * this.sellRate); } else if (itemStock > originalStockAmount) { const overstockAmount = itemStock - originalStockAmount; let shopSellRate = this.sellRate; shopSellRate -= this.rateModifier * overstockAmount; finalAmount = item.value * shopSellRate; finalAmount = Math.round(finalAmount); } else { const understockAmount = originalStockAmount - itemStock; let shopSellRate = this.sellRate; shopSellRate += this.rateModifier * understockAmount; finalAmount = item.value * shopSellRate; finalAmount = Math.round(finalAmount); } const min = item.minimumValue || 1; return finalAmount < min ? min : finalAmount; } /** * Price which shop pays player for. * @param item */ public getSellToShopPrice(item: ItemDetails): number { const itemKey = item.key; const itemSoldHere: boolean = this.isItemSoldHere(itemKey); let originalStockAmount: number = 0; const itemStock = this.container.amount(item.gameId); if (itemSoldHere) { originalStockAmount = this.originalStock.find(stock => stock && stock.itemKey === itemKey)?.amount || 0; } else if (!this.generalStore) { return -1; // Can not sell this item to this shop (shop is not a general store!) } let finalAmount: number; if (itemStock === originalStockAmount) { finalAmount = Math.round(item.value * this.buyRate); } else if (itemStock > originalStockAmount) { const overstockAmount = itemStock - originalStockAmount; let shopBuyRate = this.buyRate; shopBuyRate -= this.rateModifier * overstockAmount; finalAmount = item.value * shopBuyRate; finalAmount = Math.round(finalAmount); } else { const understockAmount = originalStockAmount - itemStock; let shopBuyRate = this.buyRate; shopBuyRate += this.rateModifier * understockAmount; finalAmount = item.value * shopBuyRate; finalAmount = Math.round(finalAmount); } const min = item.minimumValue || 0; return finalAmount < min ? min : finalAmount; } public isItemSoldHere(itemKey: string): boolean { return this.originalStock.some(stockedItem => stockedItem.itemKey === itemKey); } public open(player: Player): void { player.metadata.lastOpenedShopKey = this.key; player.metadata.shopCloseListener = player.interfaceState.closed.subscribe((whatClosed: WidgetClosedEvent) => { if (whatClosed && whatClosed.widget && whatClosed.widget.widgetId === widgets.shop.widgetId) { this.removePlayerFromShop(player); } }); player.outgoingPackets.updateWidgetString(widgets.shop.widgetId, widgets.shop.title, this.name); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.shop, this.container); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.shopPlayerInventory, player.inventory); player.interfaceState.openWidget(widgets.shop.widgetId, { slot: 'screen', multi: true, }); player.interfaceState.openWidget(widgets.shopPlayerInventory.widgetId, { slot: 'tabarea', multi: true, }); this.customers.push(player); } private updateCustomers() { for (const player of this.customers) { if (player.metadata.lastOpenedShopKey === this.key) { player.outgoingPackets.sendUpdateAllWidgetItems(widgets.shop, this.container); } else { this.removePlayerFromShop(player); } } } private removePlayerFromShop(player: Player) { if (player.metadata.lastOpenedShopKey === this.key) { delete player.metadata.lastOpenedShopKey; player.metadata.shopCloseListener?.unsubscribe(); } this.customers = this.customers.filter(c => c !== player); } } export interface ShopConfiguration { name: string; general_store?: boolean; shop_sell_rate?: number; shop_buy_rate?: number; rate_modifier?: number; stock: ShopStock; } export function shopFactory(key: string, config: ShopConfiguration): Shop { return new Shop( key, config.name, config.general_store || false, config.stock, config.shop_sell_rate || 1.0, config.shop_buy_rate || 0.65, config.rate_modifier || 0.2, ); } export async function loadShopConfigurations(path: string): Promise<{ [key: string]: Shop }> { const shops: { [key: string]: Shop } = {}; const files = await loadConfigurationFiles<{ [key: string]: ShopConfiguration }>(path); files.forEach(shopConfigs => { const shopKeys = Object.keys(shopConfigs); shopKeys.forEach(key => { shops[key] = shopFactory(key, shopConfigs[key]); }); }); return shops; } ================================================ FILE: src/engine/interface/interface-state.ts ================================================ import type { Player } from '@engine/world/actor/player/player'; import type { ItemContainer } from '@engine/world/items/item-container'; import { logger } from '@runejs/common'; import { Subject, lastValueFrom } from 'rxjs'; import { filter, take } from 'rxjs/operators'; export type TabType = | 'combat' | 'skills' | 'quests' | 'inventory' | 'equipment' | 'prayers' | 'spells' | 'friends' | 'ignores' | 'logout' | 'emotes' | 'settings' | 'music'; export type GameInterfaceSlot = 'full' | 'screen' | 'chatbox' | 'tabarea'; export const tabIndex: { [key: string]: number } = { combat: 0, skills: 1, quests: 2, inventory: 3, equipment: 4, prayers: 5, spells: 6, friends: 8, ignores: 9, logout: 10, emotes: 11, settings: 12, music: 13, }; export interface WidgetOptions { slot: GameInterfaceSlot; multi?: boolean; queued?: boolean; containerId?: number; container?: ItemContainer; fakeWidget?: number; metadata?: { [key: string]: any }; doNotRegister?: boolean; } export class Widget { public widgetId: number; public slot: GameInterfaceSlot; public multi: boolean = false; public queued: boolean = false; public containerId: number; public container: ItemContainer | null = null; public fakeWidget?: number; public metadata: { [key: string]: any }; public constructor(interfaceId: number, options: WidgetOptions) { const { slot, multi, queued, containerId, container, fakeWidget, metadata } = options; this.widgetId = interfaceId; this.fakeWidget = fakeWidget; this.slot = slot; this.multi = multi || false; this.queued = queued || false; this.containerId = containerId || -1; this.container = container || null; this.metadata = { ...metadata }; } } export interface WidgetClosedEvent { widget: Widget; widgetId?: number; data?: number; } /** * Control's a Player's Game Interface state. */ export class InterfaceState { public readonly tabs: { [key: string]: Widget | null }; public readonly widgetSlots: { [key: string]: Widget | null }; public readonly closed: Subject = new Subject(); private readonly player: Player; private _screenOverlayWidget: number | null; private _chatOverlayWidget: number | null; public constructor(player: Player) { this.tabs = { combat: null, skills: null, quests: null, inventory: null, equipment: null, prayers: null, spells: null, friends: null, ignores: null, logout: null, emotes: null, settings: null, music: null, }; this.widgetSlots = {}; this._screenOverlayWidget = null; this._chatOverlayWidget = null; this.clearSlots(); this.player = player; } public openChatOverlayWidget(widgetId: number): void { this._chatOverlayWidget = widgetId; this.player.outgoingPackets.showChatDialogue(widgetId); } public closeChatOverlayWidget(): void { this._chatOverlayWidget = null; this.player.outgoingPackets.showChatDialogue(-1); } public openScreenOverlayWidget(widgetId: number): void { this._screenOverlayWidget = widgetId; this.player.outgoingPackets.showScreenOverlayWidget(widgetId); } public closeScreenOverlayWidget(): void { this._screenOverlayWidget = null; this.player.outgoingPackets.showScreenOverlayWidget(-1); } public async widgetClosed(slot: GameInterfaceSlot): Promise { return await lastValueFrom( this.closed .asObservable() .pipe(filter(event => event.widget.slot === slot)) .pipe(take(1)), ); } public closeWidget(slot: GameInterfaceSlot, widgetId?: number, data?: number): void { let widget: Widget | null = null; if (slot) { widget = this.widgetSlots[slot]; } else { if (!widgetId) { throw new Error('Invalid widget close request: no slot or widgetId provided'); } widget = this.findWidget(widgetId); } if (!widget) { return; } this.closed.next({ widget, widgetId, data }); this.widgetSlots[widget.slot] = null; } public openWidget(widgetId: number, options: WidgetOptions): Widget { // if(this.widgetOpen(options.slot, widgetId)) { // return; // } const widget = new Widget(widgetId, options); if (widget.queued) { // @TODO queued widgets } if (widget.slot === 'full' || !widget.multi) { this.clearSlots(); } if (!options.doNotRegister) { this.widgetSlots[widget.slot] = widget; } this.showWidget(widget); return widget; } public setTab(type: TabType, widget: Widget | number | null): void { if (widget && typeof widget === 'number') { // Create a new tab interface instance let container: ItemContainer | undefined; if (type === 'inventory') { container = this.player.inventory; } else if (type === 'equipment') { container = this.player.equipment; } widget = new Widget(widget, { slot: 'tabarea', multi: true, container, }); } widget = (widget as Widget) || null; this.tabs[type] = widget; this.player.outgoingPackets.sendTabWidget(tabIndex[type], widget === null ? -1 : widget.widgetId); } public getTab(type: TabType): Widget | null { return this.tabs[type] || null; } public findWidget(widgetId: number): Widget | null { const slots: GameInterfaceSlot[] = Object.keys(this.widgetSlots) as GameInterfaceSlot[]; let widget: Widget | null = null; slots.forEach(slot => { if (this.widgetSlots[slot]?.widgetId === widgetId) { widget = this.widgetSlots[slot]; } }); return widget || null; } public widgetOpen(slot?: GameInterfaceSlot, widgetId?: number): boolean { if (!slot) { const slots: GameInterfaceSlot[] = Object.keys(this.widgetSlots) as GameInterfaceSlot[]; return slots.some(s => this.getWidget(s) !== null); } if (widgetId === undefined) { return this.getWidget(slot) !== null; } else { return this.getWidget(slot)?.widgetId === widgetId; } } public getWidget(slot: GameInterfaceSlot): Widget | null { return this.widgetSlots[slot] || null; } public closeAllSlots(): void { const slots: GameInterfaceSlot[] = Object.keys(this.widgetSlots) as GameInterfaceSlot[]; slots.forEach(slot => this.closeWidget(slot)); this.player.outgoingPackets.closeActiveWidgets(); } private showWidget(widget: Widget): void { const { outgoingPackets: packets } = this.player; const { widgetId, containerId, slot, multi } = widget; if (slot === 'full' || !multi) { this.closeOthers(slot); } if (slot === 'full' && containerId !== undefined) { packets.showFullscreenWidget(widgetId, containerId); } else if (slot === 'screen') { const tabWidget = this.getWidget('tabarea'); if (multi && tabWidget) { packets.showScreenAndTabWidgets(widgetId, tabWidget.widgetId); } else { packets.showStandaloneScreenWidget(widgetId); } } else if (slot === 'chatbox') { if (multi) { // Dialogue Widget packets.showChatDialogue(widgetId); } else { // Chatbox Widget packets.showChatboxWidget(widgetId); } } else if (slot === 'tabarea') { const screenWidget = this.getWidget('screen'); if (!screenWidget) { logger.error(`Tried to open tab widget ${widgetId} without a screen widget open`); return; } if (multi) { packets.showScreenAndTabWidgets(screenWidget.widgetId, widgetId); } else { packets.showTabWidget(widgetId); } } } private closeOthers(openSlot: GameInterfaceSlot): void { const slots: GameInterfaceSlot[] = Object.keys(this.widgetSlots).filter(slot => slot !== openSlot) as GameInterfaceSlot[]; slots.forEach(slot => this.closeWidget(slot)); } private clearSlots(): void { this.widgetSlots.full = null; this.widgetSlots.screen = null; this.widgetSlots.chatbox = null; this.widgetSlots.tabarea = null; } public get fullScreenWidget(): Widget | null { return this.widgetSlots.full || null; } public get screenWidget(): Widget | null { return this.widgetSlots.screen || null; } public get chatboxWidget(): Widget | null { return this.widgetSlots.chatbox || null; } public get tabareaWidget(): Widget | null { return this.widgetSlots.tabarea || null; } public get screenOverlayWidget(): number | null { return this._screenOverlayWidget; } } ================================================ FILE: src/engine/net/inbound-packet-handler.ts ================================================ import { BUILD_DIR } from '@engine/config/directories'; import type { Player } from '@engine/world/actor/player/player'; import type { ByteBuffer } from '@runejs/common'; import { logger } from '@runejs/common'; import { getFiles } from '../util/files'; interface InboundPacket { opcode: number; size: number; handler: (player: Player, packet: PacketData) => void; } export interface PacketData { packetId: number; packetSize: number; buffer: ByteBuffer; } export const incomingPackets = new Map(); export const PACKET_DIRECTORY = `${BUILD_DIR}/net/inbound-packets`; export async function loadPackets(): Promise> { incomingPackets.clear(); for await (const path of getFiles(PACKET_DIRECTORY, ['.packet.js'], true)) { const location = './inbound-packets' + path.substring(PACKET_DIRECTORY.length).replace('.js', ''); const packet = require(location).default; if (Array.isArray(packet)) { packet.forEach(p => incomingPackets.set(p.opcode, p)); } else { incomingPackets.set(packet.opcode, packet); } } return incomingPackets; } export function handlePacket(player: Player, packetId: number, packetSize: number, buffer: ByteBuffer): boolean { const incomingPacket = incomingPackets.get(packetId); if (!incomingPacket) { logger.info(`Unknown packet ${packetId} with size ${packetSize} received.`); return false; } new Promise(resolve => { try { incomingPacket.handler(player, { packetId, packetSize, buffer }); } catch (error) { logger.error(`Error handling inbound packet ${packetId} with size ${packetSize}`); logger.error(error); } resolve(); }); return true; } ================================================ FILE: src/engine/net/inbound-packets/add-friend.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import { longToString } from '@engine/util/strings'; import type { Player } from '@engine/world/actor/player/player'; export default { opcode: 114, size: 8, handler: (player: Player, packet: PacketData) => player.addFriend(longToString(BigInt(packet.buffer.get('LONG')))), }; ================================================ FILE: src/engine/net/inbound-packets/add-ignore.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import { longToString } from '@engine/util/strings'; import type { Player } from '@engine/world/actor/player/player'; export default { opcode: 251, size: 8, handler: (player: Player, packet: PacketData) => player.addIgnoredPlayer(longToString(BigInt(packet.buffer.get('LONG')))), }; ================================================ FILE: src/engine/net/inbound-packets/blinking-tab-click.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; const blinkingTabClickPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; const tabIndex = buffer.get('byte'); const tabClickEventIndex = player.metadata?.tabClickEvent?.tabIndex || -1; if (tabClickEventIndex === tabIndex) { if (player.metadata.tabClickEvent) { player.metadata.tabClickEvent.event.next(true); } } }; export default { opcode: 44, size: 1, handler: blinkingTabClickPacket, }; ================================================ FILE: src/engine/net/inbound-packets/button-click.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; const buttonClickPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; const widgetId = buffer.get('short'); const buttonId = buffer.get('short'); player.actionPipeline.call('button', player, widgetId, buttonId); }; export default { opcode: 64, size: 4, handler: buttonClickPacket, }; ================================================ FILE: src/engine/net/inbound-packets/character-design.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; const characterDesignPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; // @TODO verify validity of selections const gender = buffer.get('byte'); const models = new Array(7); const colors = new Array(5); for (let i = 0; i < models.length; i++) { models[i] = buffer.get('byte'); } for (let i = 0; i < colors.length; i++) { colors[i] = buffer.get('byte'); } player.appearance = { gender, head: models[0], facialHair: models[1], torso: models[2], arms: models[3], hands: models[4], legs: models[5], feet: models[6], hairColor: colors[0], torsoColor: colors[1], legColor: colors[2], feetColor: colors[3], skinColor: colors[4], }; player.updateFlags.appearanceUpdateRequired = true; player.interfaceState.closeAllSlots(); }; export default { opcode: 231, size: 13, handler: characterDesignPacket, }; ================================================ FILE: src/engine/net/inbound-packets/chat.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; const chatPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; buffer.get('byte'); const color = buffer.get('byte'); const effects = buffer.get('byte'); const data = Buffer.from(buffer.getSlice(buffer.readerIndex, buffer.length - buffer.readerIndex)); player.updateFlags.addChatMessage({ color, effects, data }); }; export default { opcode: 75, size: -3, handler: chatPacket, }; ================================================ FILE: src/engine/net/inbound-packets/command.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import { type Player, Rights } from '@engine/world/actor/player/player'; const commandPacket = (player: Player, packet: PacketData) => { const input = packet.buffer.getString(); if (!input || input.trim().length === 0) { return; } const isConsole = packet.packetId === 246; const args = input.trim().split(' '); const command = args[0]; args.splice(0, 1); if (player.rights !== Rights.ADMIN) { player.sendLogMessage('You need to be an administrator to use commands.', isConsole); } else { player.actionPipeline.call('player_command', player, command, isConsole, args); } }; export default [ { opcode: 246, size: -3, handler: commandPacket, }, { opcode: 248, size: -1, handler: commandPacket, }, ]; ================================================ FILE: src/engine/net/inbound-packets/drop-item.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; const dropItemPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; const widgetId = buffer.get('short', 'u', 'le'); const containerId = buffer.get('short', 'u', 'le'); const slot = buffer.get('short', 'u'); const itemId = buffer.get('short', 'u', 'le'); player.actionPipeline.call('item_interaction', player, itemId, slot, widgetId, containerId, 'drop'); }; export default { opcode: 29, size: 8, handler: dropItemPacket, }; ================================================ FILE: src/engine/net/inbound-packets/examine.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; const examinePacket = (player: Player, packet: PacketData) => { const { packetId, buffer } = packet; const id = buffer.get('short', 's', 'le'); let message; if (packetId === 151) { message = activeWorld.examine.getItem(id); } else if (packetId === 148) { message = activeWorld.examine.getObject(id); } else if (packetId === 247) { message = activeWorld.examine.getNpc(id); } if (message) { player.sendMessage(message); } }; export default [ { opcode: 148, size: 2, handler: examinePacket, }, { opcode: 151, size: 2, handler: examinePacket, }, { opcode: 247, size: 2, handler: examinePacket, }, ]; ================================================ FILE: src/engine/net/inbound-packets/item-interaction.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; import { getItemOption } from '@engine/world/items/item'; const option1 = buffer => { const itemId = buffer.get('short', 'u'); const slot = buffer.get('short', 'u', 'le'); const widgetId = buffer.get('short', 's', 'le'); const containerId = buffer.get('short', 's', 'le'); return { widgetId, containerId, itemId, slot }; }; const option2 = buffer => { const itemId = buffer.get('short', 'u', 'le'); const containerId = buffer.get('short', 's', 'le'); const widgetId = buffer.get('short', 's', 'le'); const slot = buffer.get('short', 'u', 'le'); return { widgetId, containerId, itemId, slot }; }; const option3 = buffer => { const slot = buffer.get('short', 'u'); const containerId = buffer.get('short', 's', 'le'); const widgetId = buffer.get('short', 's', 'le'); const itemId = buffer.get('short', 'u'); return { widgetId, containerId, itemId, slot }; }; const option4 = buffer => { const itemId = buffer.get('short', 'u'); const slot = buffer.get('short', 'u', 'le'); const containerId = buffer.get('short', 's', 'le'); const widgetId = buffer.get('short', 's', 'le'); return { widgetId, containerId, itemId, slot }; }; const option5 = buffer => { const containerId = buffer.get('short', 's', 'le'); const widgetId = buffer.get('short', 's', 'le'); const slot = buffer.get('short', 'u', 'le'); const itemId = buffer.get('short', 'u'); return { widgetId, containerId, itemId, slot }; }; const inventoryOption1 = buffer => { const slot = buffer.get('short', 'u', 'le'); const itemId = buffer.get('short', 's', 'le'); const containerId = buffer.get('short', 's', 'le'); const widgetId = buffer.get('short', 'u'); return { widgetId, containerId, itemId, slot }; }; const inventoryOption4 = buffer => { const slot = buffer.get('short', 'u'); const widgetId = buffer.get('short', 's', 'le'); const containerId = buffer.get('short', 's', 'le'); const itemId = buffer.get('short', 'u'); return { widgetId, containerId, itemId, slot }; }; const itemInteractionPacket = (player: Player, packet: PacketData) => { const { packetId } = packet; const packets = { 38: { packetDef: option1, optionNumber: 1 }, 228: { packetDef: option2, optionNumber: 2 }, 26: { packetDef: option3, optionNumber: 3 }, 147: { packetDef: option4, optionNumber: 4 }, 102: { packetDef: option5, optionNumber: 2 }, 98: { packetDef: inventoryOption4, optionNumber: 4 }, 240: { packetDef: inventoryOption1, optionNumber: 1 }, }; const packetDetails = packets[packetId]; const { widgetId, containerId, itemId, slot } = packetDetails.packetDef(packet.buffer); const option = getItemOption(itemId, packetDetails.optionNumber, { widgetId, containerId }); player.actionPipeline.call('item_interaction', player, itemId, slot, widgetId, containerId, option); }; export default [ { opcode: 38, size: 8, handler: itemInteractionPacket, }, { opcode: 98, size: 8, handler: itemInteractionPacket, }, { opcode: 228, size: 8, handler: itemInteractionPacket, }, { opcode: 26, size: 8, handler: itemInteractionPacket, }, { opcode: 147, size: 8, handler: itemInteractionPacket, }, { opcode: 240, size: 8, handler: itemInteractionPacket, }, { opcode: 102, size: 8, handler: itemInteractionPacket, }, ]; ================================================ FILE: src/engine/net/inbound-packets/item-on-item.packet.ts ================================================ import { widgets } from '@engine/config/config-handler'; import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; import { logger } from '@runejs/common'; const itemOnItemPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; const usedWithItemId = buffer.get('short', 'u', 'le'); const usedWithSlot = buffer.get('short', 'u', 'le'); const usedWithContainerId = buffer.get('short', 's', 'le'); const usedWithWidgetId = buffer.get('short', 's', 'le'); const usedContainerId = buffer.get('short', 's', 'le'); const usedWidgetId = buffer.get('short', 's', 'le'); const usedItemId = buffer.get('short', 'u', 'le'); const usedSlot = buffer.get('short', 'u'); if ( usedWidgetId === widgets.inventory.widgetId && usedContainerId === widgets.inventory.containerId && usedWithWidgetId === widgets.inventory.widgetId && usedWithContainerId === widgets.inventory.containerId ) { if (usedSlot < 0 || usedSlot > 27 || usedWithSlot < 0 || usedWithSlot > 27) { return; } const usedItem = player.inventory.items[usedSlot]; const usedWithItem = player.inventory.items[usedWithSlot]; if (!usedItem || !usedWithItem) { return; } if (usedItem.itemId !== usedItemId || usedWithItem.itemId !== usedWithItemId) { return; } player.actionPipeline.call('item_on_item', player, usedItem, usedSlot, usedWidgetId, usedWithItem, usedWithSlot, usedWithWidgetId); } else { logger.warn( `Unhandled item on item case using widgets ${usedWidgetId}:${usedContainerId} => ${usedWithWidgetId}:${usedWithContainerId}`, ); } }; export default { opcode: 40, size: 16, handler: itemOnItemPacket, }; ================================================ FILE: src/engine/net/inbound-packets/item-on-npc.packet.ts ================================================ import { widgets } from '@engine/config/config-handler'; import type { PacketData } from '@engine/net/inbound-packet-handler'; import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; import { World } from '@engine/world/world'; import { logger } from '@runejs/common'; const itemOnNpcPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; const npcIndex = buffer.get('short', 'u'); const itemId = buffer.get('short', 'u'); const itemSlot = buffer.get('short', 'u', 'le'); const itemWidgetId = buffer.get('short'); const itemContainerId = buffer.get('short'); let usedItem; if (itemWidgetId === widgets.inventory.widgetId && itemContainerId === widgets.inventory.containerId) { if (itemSlot < 0 || itemSlot > 27) { return; } usedItem = player.inventory.items[itemSlot]; if (!usedItem) { return; } if (usedItem.itemId !== itemId) { return; } } else { logger.warn(`Unhandled item on object case using widget ${itemWidgetId}:${itemContainerId}`); } if (npcIndex < 0 || npcIndex > World.MAX_NPCS - 1) { return; } const npc = activeWorld.npcList[npcIndex]; if (!npc) { return; } const position = npc.position; const distance = Math.floor(position.distanceBetween(player.position)); // Too far away if (distance > 16) { return; } player.actionPipeline.call('item_on_npc', player, npc, position, usedItem, itemWidgetId, itemContainerId); }; export default { opcode: 208, size: 10, handler: itemOnNpcPacket, }; ================================================ FILE: src/engine/net/inbound-packets/item-on-object.packet.ts ================================================ import { widgets } from '@engine/config/config-handler'; import type { PacketData } from '@engine/net/inbound-packet-handler'; import { getVarbitMorphIndex } from '@engine/util/varbits'; import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; import { Position } from '@engine/world/position'; import { logger } from '@runejs/common'; import { filestore } from '@server/game/game-server'; const itemOnObjectPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; const objectY = buffer.get('short', 'u', 'le'); const itemId = buffer.get('short', 'u'); const objectId = buffer.get('short', 'u', 'le'); const itemSlot = buffer.get('short', 'u', 'le'); const itemWidgetId = buffer.get('short', 's', 'le'); const itemContainerId = buffer.get('short', 's', 'le'); const objectX = buffer.get('short', 'u', 'le'); let usedItem; if (itemWidgetId === widgets.inventory.widgetId && itemContainerId === widgets.inventory.containerId) { if (itemSlot < 0 || itemSlot > 27) { return; } usedItem = player.inventory.items[itemSlot]; if (!usedItem) { return; } if (usedItem.itemId !== itemId) { return; } } else { logger.warn(`Unhandled item on object case using widget ${itemWidgetId}:${itemContainerId}`); } const level = player.position.level; const objectPosition = new Position(objectX, objectY, level); const { object: locationObject, cacheOriginal } = activeWorld.findObjectAtLocation(player, objectId, objectPosition); if (!locationObject) { return; } let objectConfig = filestore.configStore.objectStore.getObject(objectId); if (!objectConfig) { logger.error(`Could not find object config for object id ${objectId}!`); } else if (objectConfig.configChangeDest) { let morphIndex = -1; if (objectConfig.varbitId === -1) { if (objectConfig.configId !== -1) { const configValue = player.metadata.configs && player.metadata.configs[objectConfig.configId] ? player.metadata.configs[objectConfig.configId] : 0; morphIndex = configValue; } } else { morphIndex = getVarbitMorphIndex(objectConfig.varbitId, player.metadata.configs); } if (morphIndex !== -1) { objectConfig = filestore.configStore.objectStore.getObject(objectConfig.configChangeDest[morphIndex]); } } player.actionPipeline.call( 'item_on_object', player, locationObject, objectConfig, objectPosition, usedItem, itemWidgetId, itemContainerId, cacheOriginal, ); }; export default { opcode: 24, size: 14, handler: itemOnObjectPacket, }; ================================================ FILE: src/engine/net/inbound-packets/item-on-player.packet.ts ================================================ import { widgets } from '@engine/config/config-handler'; import type { PacketData } from '@engine/net/inbound-packet-handler'; import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; import { World } from '@engine/world/world'; import { logger } from '@runejs/common'; const itemOnPlayerPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; const playerIndex = buffer.get('short', 'u', 'le') - 1; const itemWidgetId = buffer.get('short', 's', 'le'); const itemContainerId = buffer.get('short'); const itemId = buffer.get('short', 'u'); const itemSlot = buffer.get('short', 'u'); let usedItem; if (itemWidgetId === widgets.inventory.widgetId && itemContainerId === widgets.inventory.containerId) { if (itemSlot < 0 || itemSlot > 27) { return; } usedItem = player.inventory.items[itemSlot]; if (!usedItem) { return; } if (usedItem.itemId !== itemId) { return; } } else { logger.warn(`Unhandled item on object case using widget ${itemWidgetId}:${itemContainerId}`); } if (playerIndex < 0 || playerIndex > World.MAX_PLAYERS - 1) { return; } const otherPlayer = activeWorld.playerList[playerIndex]; if (!otherPlayer) { return; } const position = otherPlayer.position; const distance = Math.floor(position.distanceBetween(player.position)); // Too far away if (distance > 16) { return; } player.actionPipeline.call('item_on_player', player, otherPlayer, position, usedItem, itemWidgetId, itemContainerId); }; export default { opcode: 110, size: 10, handler: itemOnPlayerPacket, }; ================================================ FILE: src/engine/net/inbound-packets/item-on-world-item.packet.ts ================================================ import { widgets } from '@engine/config/config-handler'; import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; import { Position } from '@engine/world/position'; import { logger } from '@runejs/common'; /** * Parses the item on world item packet and calls the `item_on_world_item` action pipeline. * * This will check that the item being used is in the player's inventory, and that the world item exists in the correct location. * The action pipeline will not be called if either of these conditions are not met. * * @param player The player that sent the packet. * @param packet The packet to parse. * * @author jameskmonger */ const itemOnWorldItemPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; const usedWithX = buffer.get('short', 'u'); const usedSlot = buffer.get('short', 'u'); const usedWithItemId = buffer.get('short', 'u'); const usedContainerId = buffer.get('short', 's', 'be'); const usedWidgetId = buffer.get('short', 's', 'be'); const usedWithY = buffer.get('short', 'u', 'le'); const usedItemId = buffer.get('short', 'u', 'le'); const position = new Position(usedWithX, usedWithY, player.position.level); if (usedWidgetId === widgets.inventory.widgetId && usedContainerId === widgets.inventory.containerId) { // TODO (James) we should use constants for these rather than magic numbers if (usedSlot < 0 || usedSlot > 27) { return; } const usedItem = player.inventory.items[usedSlot]; const usedWithItem = player.instance.getTileModifications(position).mods.worldItems.find(p => p.itemId === usedWithItemId); if (!usedItem || !usedWithItem) { logger.warn( `Unhandled item on world item case (A) for ${usedSlot} (${usedItemId}) on ${usedWithItemId} (${usedWithX}, ${usedWithY}) by ${player.username}`, ); return; } if (usedItem.itemId !== usedItemId || usedWithItem.itemId !== usedWithItemId) { logger.warn( `Unhandled item on world item case (B) for ${usedItem.itemId}:${usedItemId} on ${usedWithItem.itemId}:${usedWithItemId} by ${player.username}`, ); return; } player.actionPipeline.call('item_on_world_item', player, usedItem, usedWithItem, usedWidgetId, usedContainerId, usedSlot); } else { logger.warn(`Unhandled item on world item case (C) using widgets ${usedWidgetId}:${usedContainerId} by ${player.username}`); } }; export default { opcode: 172, size: 14, handler: itemOnWorldItemPacket, }; ================================================ FILE: src/engine/net/inbound-packets/item-swap.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; const itemSwapPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; const swapType = buffer.get('byte'); const fromSlot = buffer.get('short', 'u'); const toSlot = buffer.get('short', 'u', 'le'); const containerId = buffer.get('short'); const widgetId = buffer.get('short'); if (toSlot < 0 || fromSlot < 0) { return; } if (swapType === 0) { player.actionPipeline.call('item_swap', player, fromSlot, toSlot, { widgetId, containerId }); } else if (swapType === 1) { player.actionPipeline.call('move_item', player, fromSlot, toSlot, { widgetId, containerId }); } }; export default { opcode: 83, size: 9, handler: itemSwapPacket, }; ================================================ FILE: src/engine/net/inbound-packets/junk.packet.ts ================================================ const handler = () => {}; export default [ { opcode: 234, size: 4, handler, }, { opcode: 160, size: 1, handler, }, { opcode: 216, size: 0, handler, }, { opcode: 13, size: 0, handler, }, { opcode: 58, // camera move size: 4, handler, }, { opcode: 121, size: 4, handler, }, { opcode: 178, size: 0, handler, }, ]; ================================================ FILE: src/engine/net/inbound-packets/magic-attack.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; const magicAttackPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; const npcWorldIndex = buffer.get('short', 'u'); // unsigned short BE const widgetId = buffer.get('short', 'u', 'le'); // unsigned short LE const widgetChildId = buffer.get('byte'); // unsigned short LE const npc = activeWorld.npcList[npcWorldIndex]; player.actionPipeline.call('magic_on_npc', npc, player, widgetId, widgetChildId); }; export default [ { opcode: 253, size: 6, handler: magicAttackPacket, }, ]; ================================================ FILE: src/engine/net/inbound-packets/npc-interaction.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; import { World } from '@engine/world/world'; import { logger } from '@runejs/common'; const npcInteractionPacket = (player: Player, packet: PacketData) => { const { buffer, packetId } = packet; const packetReaders: Record number> = { 63: () => buffer.get('short', 'u', 'le'), 116: () => buffer.get('short', 'u', 'le'), 57: () => buffer.get('short', 'u'), /*42: 'readUnsignedShortLE', 8: 'readUnsignedShortLE'*/ }; const npcIndex = packetReaders[packetId](); if (npcIndex < 0 || npcIndex > World.MAX_NPCS - 1) { return; } const npc = activeWorld.npcList[npcIndex]; if (!npc) { return; } const position = npc.position; const distance = Math.floor(position.distanceBetween(player.position)); // Too far away if (distance > 16) { return; } const actions = { 63: 0, // Usually the Talk-to option 57: 1, // Usually the Attack option 116: 2, // Usually the Pickpocket option /*42: 3, 8: 4*/ }; const morphedNpc = player.getMorphedNpcDetails(npc); const options = morphedNpc?.options || npc.options; const actionIdx = actions[packetId]; let optionName = `action-${actionIdx + 1}`; if (options && options.length >= actionIdx) { if (!options[actionIdx] || options[actionIdx].toLowerCase() === 'hidden') { // Invalid action logger.info(npc); logger.error(`1: Invalid npc ${morphedNpc?.gameId || npc.id} option ${actionIdx + 1}, options: ${JSON.stringify(options)}`); if (morphedNpc) { logger.warn(`Note: (id-${morphedNpc.gameId}) is a morphed NPC. The parent NPC is (id-${npc.id}).`); } return; } optionName = options[actionIdx]; } else { // Invalid action logger.error(`2: Invalid npc ${morphedNpc?.gameId || npc.id} option ${actionIdx + 1}, options: ${JSON.stringify(options)}`); if (morphedNpc) { logger.warn(`Note: (id-${morphedNpc.gameId}) is a morphed NPC. The parent NPC is (id-${npc.id}).`); } return; } player.actionPipeline.call('npc_interaction', player, npc, position, optionName.toLowerCase()); }; export default [ { opcode: 63, size: 2, handler: npcInteractionPacket, }, { opcode: 116, size: 2, handler: npcInteractionPacket, }, { opcode: 57, size: 2, handler: npcInteractionPacket, }, ]; ================================================ FILE: src/engine/net/inbound-packets/number-input.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; export default { opcode: 238, size: 4, handler: (player: Player, packet: PacketData) => player.numericInputEvent.next(packet.buffer.get('int', 'u')), }; ================================================ FILE: src/engine/net/inbound-packets/object-interaction.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import { getVarbitMorphIndex } from '@engine/util/varbits'; import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; import { Rights } from '@engine/world/actor/player/player'; import { Position } from '@engine/world/position'; import { logger } from '@runejs/common'; import { filestore } from '@server/game/game-server'; interface ObjectInteractionData { objectId: number; x: number; y: number; } type objectInteractionPacket = (packet: PacketData) => ObjectInteractionData; const option1: objectInteractionPacket = packet => { const { buffer } = packet; const objectId = buffer.get('short', 'u'); const y = buffer.get('short', 'u'); const x = buffer.get('short', 'u', 'le'); return { objectId, x, y }; }; const option2: objectInteractionPacket = packet => { const { buffer } = packet; const x = buffer.get('short', 'u', 'le'); const y = buffer.get('short', 'u', 'le'); const objectId = buffer.get('short', 'u', 'le'); return { objectId, x, y }; }; const option3: objectInteractionPacket = packet => { const { buffer } = packet; const y = buffer.get('short', 'u'); const objectId = buffer.get('short', 'u'); const x = buffer.get('short', 'u'); return { objectId, x, y }; }; const option4: objectInteractionPacket = packet => { const { buffer } = packet; const x = buffer.get('short', 'u', 'le'); const objectId = buffer.get('short', 'u', 'le'); const y = buffer.get('short', 'u', 'le'); return { objectId, x, y }; }; const option5: objectInteractionPacket = packet => { const { buffer } = packet; const objectId = buffer.get('short', 'u'); const y = buffer.get('short', 'u', 'le'); const x = buffer.get('short', 'u', 'le'); return { objectId, x, y }; }; const objectInteractionPackets: { [key: number]: { packetDef: objectInteractionPacket; index: number } } = { 30: { packetDef: option1, index: 0 }, 164: { packetDef: option2, index: 1 }, 183: { packetDef: option3, index: 2 }, 229: { packetDef: option4, index: 3 }, 62: { packetDef: option5, index: 4 }, }; const objectInteractionPacket = (player: Player, packet: PacketData) => { const { packetId } = packet; const { objectId, x, y } = objectInteractionPackets[packetId].packetDef(packet); const level = player.position.level; const objectPosition = new Position(x, y, level); const { object: landscapeObject, cacheOriginal } = activeWorld.findObjectAtLocation(player, objectId, objectPosition); if (!landscapeObject) { if (player.rights === Rights.ADMIN) { player.sendMessage(`Custom object ${objectId} @[${objectPosition.key}]`); } return; } let objectConfig = filestore.configStore.objectStore.getObject(objectId); if (!objectConfig) { logger.error(`[object-interaction] Could not find object config for object id ${objectId}!`); return; } if (objectConfig.configChangeDest) { let morphIndex = -1; if (objectConfig.varbitId === -1) { if (objectConfig.configId !== -1) { morphIndex = player.metadata.configs && player.metadata.configs[objectConfig.configId] ? player.metadata.configs[objectConfig.configId] : 0; } } else { morphIndex = getVarbitMorphIndex(objectConfig.varbitId, player.metadata.configs); } if (morphIndex !== -1) { objectConfig = filestore.configStore.objectStore.getObject(objectConfig.configChangeDest[morphIndex]); } } if (!objectConfig) { logger.error(`[object-interaction - after morph] Could not find object config for object id ${objectId}!`); return; } const actionIdx = objectInteractionPackets[packetId].index; let optionName = `action-${actionIdx + 1}`; if (objectConfig.options && objectConfig.options.length >= actionIdx) { if (!objectConfig.options[actionIdx]) { // Invalid action logger.error(`1: Invalid object ${objectId} option ${actionIdx + 1}, options: ${JSON.stringify(objectConfig.options)}`); return; } optionName = objectConfig.options[actionIdx]; } else { // Invalid action logger.error(`2: Invalid object ${objectId} option ${actionIdx + 1}, options: ${JSON.stringify(objectConfig.options)}`); return; } player.actionPipeline.call( 'object_interaction', player, landscapeObject, objectConfig, objectPosition, optionName.toLowerCase(), cacheOriginal, ); }; export default Object.keys(objectInteractionPackets).map(opcode => ({ opcode: parseInt(opcode, 10), size: 6, handler: objectInteractionPacket, })); ================================================ FILE: src/engine/net/inbound-packets/pickup-item.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; import { Position } from '@engine/world/position'; const pickupItemPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; const y = buffer.get('short', 'u'); const itemId = buffer.get('short', 'u'); const x = buffer.get('short', 'u', 'le'); const level = player.position.level; const worldItemPosition = new Position(x, y, level); const worldMods = player.instance.getInstancedChunk(worldItemPosition); const worldItems = worldMods?.mods?.get(worldItemPosition.key)?.worldItems || []; let worldItem = worldItems.find(i => i.itemId === itemId) || null; if (!worldItem) { const personalMods = player.personalInstance.getInstancedChunk(worldItemPosition); const personalItems = personalMods?.mods?.get(worldItemPosition.key)?.worldItems || []; worldItem = personalItems.find(i => i.itemId === itemId) || null; } if (worldItem && !worldItem.removed) { if (worldItem.owner && !worldItem.owner.equals(player)) { return; } player.actionPipeline.call('spawned_item_interaction', player, worldItem, 'pick-up'); } }; export default { opcode: 85, size: 6, handler: pickupItemPacket, }; ================================================ FILE: src/engine/net/inbound-packets/player-interaction.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import { activeWorld } from '@engine/world'; import { type Player, playerOptions } from '@engine/world/actor/player/player'; import { World } from '@engine/world/world'; import { logger } from '@runejs/common'; const playerInteractionPacket = (player: Player, packet: PacketData) => { const { buffer, packetId } = packet; const packetReaders: Record number> = { 68: () => buffer.get('short', 'u', 'le'), 211: () => buffer.get('short', 'u', 'le'), }; const playerIndex = packetReaders[packetId]() - 1; if (playerIndex < 0 || playerIndex > World.MAX_PLAYERS - 1) { return; } const otherPlayer = activeWorld.playerList[playerIndex]; if (!otherPlayer) { return; } const position = otherPlayer.position; const distance = Math.floor(position.distanceBetween(player.position)); // Too far away if (distance > 16) { return; } const actions = { 68: 0, 211: 1, }; const playerOption = playerOptions.find(playerOption => playerOption.index === actions[packetId]); if (!playerOption) { logger.error(`Invalid player option ${actions[packetId]}!`); return; } player.actionPipeline.call('player_interaction', player, otherPlayer, position, playerOption.option.toLowerCase()); }; export default [ { opcode: 68, size: 2, handler: playerInteractionPacket, }, { opcode: 211, size: 2, handler: playerInteractionPacket, }, ]; ================================================ FILE: src/engine/net/inbound-packets/private-message.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import { longToString } from '@engine/util/strings'; import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; export default { opcode: 207, size: -3, handler: (player: Player, packet: PacketData) => { const { buffer } = packet; buffer.get('byte'); // junk const nameLong = BigInt(buffer.get('long')); const username = longToString(nameLong).toLowerCase(); const messageLength = buffer.length - 9; const messageBytes = new Array(messageLength); for (let i = 0; i < messageLength; i++) { messageBytes[i] = buffer[buffer.readerIndex + i]; } const otherPlayer = activeWorld.findActivePlayerByUsername(username); if (otherPlayer) { otherPlayer.privateMessageReceived(player, messageBytes); } }, }; ================================================ FILE: src/engine/net/inbound-packets/remove-friend.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import { longToString } from '@engine/util/strings'; import type { Player } from '@engine/world/actor/player/player'; import { PrivateMessaging } from '@engine/world/actor/player/private-messaging'; export default { opcode: 255, size: 8, handler: (player: Player, packet: PacketData) => { const friendName = longToString(BigInt(packet.buffer.get('long'))); if (!friendName) { return; } player.removeFriend(friendName); PrivateMessaging.friendRemoved(player, friendName); }, }; ================================================ FILE: src/engine/net/inbound-packets/remove-ignore.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import { longToString } from '@engine/util/strings'; import type { Player } from '@engine/world/actor/player/player'; export default { opcode: 28, size: 8, handler: (player: Player, packet: PacketData) => player.removeIgnoredPlayer(longToString(BigInt(packet.buffer.get('long')))), }; ================================================ FILE: src/engine/net/inbound-packets/social-button.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; import { PrivateMessaging } from '@engine/world/actor/player/private-messaging'; export default { opcode: 32, size: 3, handler: (player: Player, packet: PacketData) => { const { buffer } = packet; const currentPrivateChatMode = player.settings.privateChatMode; player.settings.publicChatMode = buffer.get('byte'); player.settings.privateChatMode = buffer.get('byte'); player.settings.tradeMode = buffer.get('byte'); if (currentPrivateChatMode !== player.settings.privateChatMode) { PrivateMessaging.playerPrivateChatModeChanged(player); } }, }; ================================================ FILE: src/engine/net/inbound-packets/walk.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; const walkPacket = (player: Player, packet: PacketData) => { const { buffer, packetSize, packetId } = packet; let size = packetSize; if (packetId === 236) { size -= 14; } if (!player.canMove()) { return; } const totalSteps = Math.floor((size - 5) / 2); const firstY = buffer.get('short', 'u', 'le'); const runSteps = buffer.get('byte') === 1; // @TODO forced running const firstX = buffer.get('short', 'u', 'le'); const walkingQueue = player.walkingQueue; player.actionsCancelled.next('manual-movement'); walkingQueue.clear(); walkingQueue.valid = true; walkingQueue.add(firstX, firstY); for (let i = 0; i < totalSteps; i++) { const x = buffer.get('byte'); const y = buffer.get('byte'); walkingQueue.add(x + firstX, y + firstY); } }; export default [ { opcode: 73, size: -1, handler: walkPacket, }, { opcode: 236, size: -1, handler: walkPacket, }, { opcode: 89, size: -1, handler: walkPacket, }, ]; ================================================ FILE: src/engine/net/inbound-packets/widget-interaction.packet.ts ================================================ import type { PacketData } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; const widgetInteractionPacket = (player: Player, packet: PacketData) => { const { buffer } = packet; const childId = buffer.get('short'); const widgetId = buffer.get('short'); const optionId = buffer.get('short', 's', 'le'); player.actionPipeline.call('widget_interaction', player, widgetId, childId, optionId); }; export default { opcode: 132, size: 6, handler: widgetInteractionPacket, }; ================================================ FILE: src/engine/net/inbound-packets/widgets-closed.packet.ts ================================================ export default { opcode: 176, size: 0, handler: player => player.interfaceState.closeAllSlots(), }; ================================================ FILE: src/engine/net/isaac.ts ================================================ export class Isaac { private m: number[] = Array(256); // internal memory private acc = 0; // accumulator private brs = 0; // last result private cnt = 0; // counter private r: number[] = Array(256); // result array private gnt = 0; // generation counter public constructor(seed: number[]) { this.seed(seed); } public getR(): number[] { return this.r; } public add(x: number, y: number): number { const lsb = (x & 0xffff) + (y & 0xffff); const msb = (x >>> 16) + (y >>> 16) + (lsb >>> 16); return (msb << 16) | (lsb & 0xffff); } public reset(): void { this.acc = this.brs = this.cnt = 0; for (let i = 0; i < 256; ++i) this.m[i] = this.r[i] = 0; this.gnt = 0; } public seed(s: number[]): void { let a, b, c, d, e, f, g, h, i; /* seeding the seeds of love */ a = b = c = d = e = f = g = h = 0x9e3779b9; /* the golden ratio */ this.reset(); for (i = 0; i < s.length; i++) this.r[i & 0xff] += typeof s[i] === 'number' ? s[i] : 0; /* private: seed mixer */ const seed_mix = () => { a ^= b << 11; d = this.add(d, a); b = this.add(b, c); b ^= c >>> 2; e = this.add(e, b); c = this.add(c, d); c ^= d << 8; f = this.add(f, c); d = this.add(d, e); d ^= e >>> 16; g = this.add(g, d); e = this.add(e, f); e ^= f << 10; h = this.add(h, e); f = this.add(f, g); f ^= g >>> 4; a = this.add(a, f); g = this.add(g, h); g ^= h << 8; b = this.add(b, g); h = this.add(h, a); h ^= a >>> 9; c = this.add(c, h); a = this.add(a, b); }; for (i = 0; i < 4; i++ /* scramble it */) seed_mix(); for (i = 0; i < 256; i += 8) { if (s) { /* use all the information in the seed */ a = this.add(a, this.r[i + 0]); b = this.add(b, this.r[i + 1]); c = this.add(c, this.r[i + 2]); d = this.add(d, this.r[i + 3]); e = this.add(e, this.r[i + 4]); f = this.add(f, this.r[i + 5]); g = this.add(g, this.r[i + 6]); h = this.add(h, this.r[i + 7]); } seed_mix(); /* fill in m[] with messy stuff */ this.m[i + 0] = a; this.m[i + 1] = b; this.m[i + 2] = c; this.m[i + 3] = d; this.m[i + 4] = e; this.m[i + 5] = f; this.m[i + 6] = g; this.m[i + 7] = h; } if (s) { /* do a second pass to make all of the seed affect all of m[] */ for (i = 0; i < 256; i += 8) { a = this.add(a, this.m[i + 0]); b = this.add(b, this.m[i + 1]); c = this.add(c, this.m[i + 2]); d = this.add(d, this.m[i + 3]); e = this.add(e, this.m[i + 4]); f = this.add(f, this.m[i + 5]); g = this.add(g, this.m[i + 6]); h = this.add(h, this.m[i + 7]); seed_mix(); /* fill in m[] with messy stuff (again) */ this.m[i + 0] = a; this.m[i + 1] = b; this.m[i + 2] = c; this.m[i + 3] = d; this.m[i + 4] = e; this.m[i + 5] = f; this.m[i + 6] = g; this.m[i + 7] = h; } } this.prng(); /* fill in the first set of results */ this.gnt = 256; /* prepare to use the first set of results */ } public prng(n?: number): void { let i, x, y; n = n && typeof n === 'number' ? Math.abs(Math.floor(n)) : 1; while (n--) { this.cnt = this.add(this.cnt, 1); this.brs = this.add(this.brs, this.cnt); for (i = 0; i < 256; i++) { switch (i & 3) { case 0: this.acc ^= this.acc << 13; break; case 1: this.acc ^= this.acc >>> 6; break; case 2: this.acc ^= this.acc << 2; break; case 3: this.acc ^= this.acc >>> 16; break; } this.acc = this.add(this.m[(i + 128) & 0xff], this.acc); x = this.m[i]; this.m[i] = y = this.add(this.m[(x >>> 2) & 0xff], this.add(this.acc, this.brs)); this.r[i] = this.brs = this.add(this.m[(y >>> 10) & 0xff], x); } } } public rand(): number { if (!this.gnt--) { this.prng(); this.gnt = 255; } return this.r[this.gnt]; } public internals(): { a: number; b: number; c: number; m: number[]; r: number[] } { return { a: this.acc, b: this.brs, c: this.cnt, m: this.m, r: this.r }; } } ================================================ FILE: src/engine/net/outbound-packet-handler.ts ================================================ import type { Socket } from 'net'; import { xteaRegions } from '@engine/config/config-handler'; import { Packet, PacketType } from '@engine/net/packet'; import { stringToLong } from '@engine/util/strings'; import { activeWorld } from '@engine/world'; import type { Npc } from '@engine/world/actor/npc'; import type { Player, SidebarTab } from '@engine/world/actor/player/player'; import type { Item } from '@engine/world/items/item'; import { ItemContainer } from '@engine/world/items/item-container'; import type { WorldItem } from '@engine/world/items/world-item'; import type { Chunk, ChunkUpdateItem } from '@engine/world/map/chunk'; import type { ConstructedChunk, ConstructedRegion } from '@engine/world/map/region'; import { Position } from '@engine/world/position'; import { ByteBuffer, logger } from '@runejs/common'; import type { LandscapeObject } from '@runejs/filestore'; import { serverConfig } from '@server/game/game-server'; /** * A helper class for sending various network packets back to the game client. */ export class OutboundPacketHandler { private static privateMessageCounter: number = Math.floor(Math.random() * 100000000); private readonly player: Player; private readonly socket: Socket; private updatingQueue: Buffer[]; private packetQueue: Buffer[]; public constructor(player: Player) { this.updatingQueue = []; this.packetQueue = []; this.player = player; this.socket = player.socket; } public resetCamera(): void { this.queue(new Packet(7)); } public snapCameraTo(position: Position, height: number, speed: number, acceleration: number): void { const packet = new Packet(253); this.putCameraPosition(packet, position, height, speed, acceleration); this.queue(packet); } public turnCameraTowards(position: Position, height: number, speed: number, acceleration: number): void { const packet = new Packet(234); this.putCameraPosition(packet, position, height, speed, acceleration); this.queue(packet); } public updateSocialSettings(): void { const packet = new Packet(196); packet.put(this.player.settings.publicChatMode || 0); packet.put(this.player.settings.privateChatMode || 0); packet.put(this.player.settings.tradeMode || 0); this.queue(packet); } public sendPrivateMessage(chatId: number, sender: Player, message: number[]): void { const packet = new Packet(51, PacketType.DYNAMIC_SMALL); packet.put(stringToLong(sender.username.toLowerCase()), 'LONG'); packet.put(32767, 'SHORT'); packet.put(OutboundPacketHandler.privateMessageCounter++, 'INT24'); packet.put(sender.rights); packet.putBytes(Buffer.from(message)); this.queue(packet); } //packet - 129 - freezes client? //packet - 202 - directly to login screen public sendProjectile( position: Position, offsetX: number, offsetY: number, id: number, startHeight: number, endHeight: number, speed: number, lockon: number, delay: number, ) { this.updateReferencePosition(position); const packet = new Packet(1); packet.put(0); packet.put(offsetY, 'byte'); packet.put(offsetX, 'byte'); packet.put(lockon, 'SHORT', 'BIG_ENDIAN'); packet.put(id, 'SHORT', 'BIG_ENDIAN'); packet.put(startHeight); packet.put(endHeight); packet.put(delay, 'SHORT', 'BIG_ENDIAN'); packet.put(speed, 'SHORT', 'BIG_ENDIAN'); packet.put(16); packet.put(64); this.queue(packet); } public updateFriendStatus(friendName: string, worldId: number): void { const packet = new Packet(156); packet.put(stringToLong(friendName.toLowerCase()), 'LONG'); packet.put(worldId, 'SHORT'); this.queue(packet); } public sendFriendServerStatus(status: 0 | 1 | 2): void { // 0 = loading, 1 = connecting to friend server, 2 = friend list const packet = new Packet(70); packet.put(status); this.queue(packet); } public playSong(songId: number): void { const packet = new Packet(217); packet.put(songId, 'SHORT', 'LITTLE_ENDIAN'); this.queue(packet); } public playQuickSong(songId: number, previousSongId: number): void { const packet = new Packet(40); packet.put(previousSongId, 'INT24'); packet.put(songId, 'SHORT'); this.queue(packet); } public playSound(soundId: number, volume: number, delay: number = 0): void { const packet = new Packet(131); packet.put(soundId, 'SHORT'); packet.put(volume); packet.put(delay, 'SHORT'); this.queue(packet); } public playSoundAtPosition( soundId: number, soundX: number, soundY: number, volume: number, radius: number = 5, delay: number = 0, ): void { const packet = new Packet(9); const offset = 0; packet.put(offset, 'BYTE'); packet.put(soundId, 'SHORT'); packet.put((volume & 7) + (radius << 4), 'BYTE'); packet.put(delay, 'BYTE'); this.queue(packet); } public updateChunk(chunk: Chunk, chunkUpdates: ChunkUpdateItem[]): void { const { offsetX, offsetY } = this.getChunkOffset(chunk); const packet = new Packet(63, PacketType.DYNAMIC_LARGE); packet.put(offsetX); packet.put(offsetY); chunkUpdates.forEach(update => { if (update.type === 'ADD') { if (update.object && !update.object.reference) { const offset = this.getChunkPositionOffset(update.object.x, update.object.y, chunk); packet.put(241, 'BYTE'); packet.put((update.object.type << 2) + (update.object.orientation & 3)); packet.put(update.object.objectId, 'SHORT'); packet.put(offset); } else if (update.worldItem) { const offset = this.getChunkPositionOffset(update.worldItem.position.x, update.worldItem.position.y, chunk); packet.put(175, 'BYTE'); packet.put(update.worldItem.itemId, 'SHORT', 'LITTLE_ENDIAN'); packet.put(update.worldItem.amount, 'SHORT'); packet.put(offset, 'BYTE'); } } else if (update.type === 'REMOVE') { if (!update.object) { logger.warn('Tried to remove object that does not exist.'); return; } const offset = this.getChunkPositionOffset(update.object.x, update.object.y, chunk); packet.put(143, 'BYTE'); packet.put(offset); packet.put((update.object.type << 2) + (update.object.orientation & 3)); } }); this.queue(packet); } public clearChunk(chunk: Chunk): void { const { offsetX, offsetY } = this.getChunkOffset(chunk); const packet = new Packet(64); packet.put(offsetY, 'BYTE'); packet.put(offsetX); this.queue(packet); } public setWorldItem(worldItem: WorldItem, position: Position, offset: number = 0): void { this.updateReferencePosition(position); const packet = new Packet(175); packet.put(worldItem.itemId, 'SHORT', 'LITTLE_ENDIAN'); packet.put(worldItem.amount, 'SHORT'); packet.put(offset, 'BYTE'); this.queue(packet); } public removeWorldItem(worldItem: WorldItem, position: Position, offset: number = 0): void { this.updateReferencePosition(position); const packet = new Packet(74); packet.put(offset, 'BYTE'); packet.put(worldItem.itemId, 'SHORT'); this.queue(packet); } public setLocationObject(locationObject: LandscapeObject, position: Position, offset: number = 0): void { this.updateReferencePosition(position); const packet = new Packet(241); packet.put((locationObject.type << 2) + (locationObject.orientation & 3)); packet.put(locationObject.objectId, 'SHORT'); packet.put(offset); this.queue(packet); } public removeLocationObject(locationObject: LandscapeObject, position: Position, offset: number = 0): void { this.updateReferencePosition(position); const packet = new Packet(143); packet.put(offset); packet.put((locationObject.type << 2) + (locationObject.orientation & 3)); this.queue(packet); } public updateReferencePosition(position: Position): void { const offsetX = position.x - this.player.lastMapRegionUpdatePosition.chunkX * 8; const offsetY = position.y - this.player.lastMapRegionUpdatePosition.chunkY * 8; const packet = new Packet(254); packet.put(offsetY); packet.put(offsetX); this.queue(packet); } // Text dialogs = 356, 359, 363, 368, 374 // Item dialogs = 519 // Statements (no click to continue) = 210, 211, 212, 213, 214 public showChatboxWidget(widgetId: number): void { const packet = new Packet(208); packet.put(widgetId, 'SHORT'); this.queue(packet); } public setWidgetNpcHead(widgetId: number, childId: number, modelId: number): void { const packet = new Packet(160); packet.put(modelId, 'SHORT', 'LITTLE_ENDIAN'); packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN'); this.queue(packet); } public setWidgetPlayerHead(widgetId: number, childId: number): void { const packet = new Packet(210); packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN'); this.queue(packet); } public playWidgetAnimation(widgetId: number, childId: number, animationId: number): void { const packet = new Packet(24); packet.put(animationId, 'SHORT'); packet.put((widgetId << 16) | childId, 'INT'); this.queue(packet); } public showScreenAndTabWidgets(widgetId: number, tabWidgetId: number): void { const packet = new Packet(84); packet.put(tabWidgetId, 'SHORT'); packet.put(widgetId, 'SHORT', 'LITTLE_ENDIAN'); this.queue(packet); } public resetAllClientConfigs(): void { const packet = new Packet(14); this.queue(packet); } public updateClientConfig(configId: number, value: number): void { let packet: Packet; const metadata = this.player.metadata; if (!metadata.configs) { metadata.configs = []; } metadata.configs[configId] = value; if (value > 128) { packet = new Packet(2); packet.put(value, 'INT'); packet.put(configId, 'SHORT'); } else { packet = new Packet(222); packet.put(value); packet.put(configId, 'SHORT'); } this.queue(packet); } public setWidgetModelRotationAndZoom(widgetId: number, childId: number, rotationX: number, rotationY: number, zoom: number): void { const packet = new Packet(142); packet.put(rotationX, 'SHORT'); packet.put(zoom, 'SHORT', 'LITTLE_ENDIAN'); packet.put(rotationY, 'SHORT'); packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN'); this.queue(packet); } public updateWidgetModel1(widgetId: number, childId: number, modelId: number): void { const packet = new Packet(250); packet.put(modelId, 'SHORT', 'LITTLE_ENDIAN'); packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN'); this.queue(packet); } public updateWidgetItemModel(widgetId: number, itemId: number, scale?: number): void { const packet = new Packet(21); // TODO (Jameskmonger) what should the default value of `scale` be? packet.put(scale || 0, 'SHORT'); packet.put(itemId, 'SHORT', 'LITTLE_ENDIAN'); packet.put(widgetId, 'SHORT', 'LITTLE_ENDIAN'); this.queue(packet); } public updateWidgetString(widgetId: number, childId: number, value: string): void { const packet = new Packet(110, PacketType.DYNAMIC_LARGE); packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN'); packet.putString(value); this.queue(packet); } public updateWidgetColor(widgetId: number, childId: number, color: number): void { const packet = new Packet(231); packet.put(color, 'SHORT'); packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN'); this.queue(packet); } public closeActiveWidgets(): void { this.queue(new Packet(180)); } public showScreenOverlayWidget(widgetId: number): void { const packet = new Packet(56); packet.put(widgetId, 'SHORT'); this.queue(packet); } public showStandaloneScreenWidget(widgetId: number): void { const packet = new Packet(118); packet.put(widgetId, 'SHORT'); this.queue(packet); } // @TODO this can support multiple items/slots !!! public sendUpdateSingleWidgetItem(widget: { widgetId: number; containerId: number }, slot: number, item: Item | null): void { const packet = new Packet(214, PacketType.DYNAMIC_LARGE); packet.put((widget.widgetId << 16) | widget.containerId, 'INT'); packet.put(slot, 'SMART_SHORT'); if (!item) { packet.put(0, 'SHORT'); } else { packet.put(item.itemId + 1, 'SHORT'); // +1 because 0 means an empty slot if (item.amount >= 255) { packet.put(255, 'BYTE'); packet.put(item.amount, 'INT'); } else { packet.put(item.amount, 'BYTE'); } } this.queue(packet); } public update(packet: Packet, widget: { widgetId: number; containerId: number }, container: ItemContainer): void { const packed = (widget.widgetId << 16) | widget.containerId; packet.put(packed, 'INT'); const size = container.size; packet.put(size, 'SHORT'); const bound = container.items.length * 7; const payload = new Packet(-1, PacketType.FIXED, bound); //TODO: change default value of allocatedSize from 5000 to something reasonable (64 - 256 as most RS packets are quite small) for (let index = 0; index < size; index += 8) { const { bitset, buffer } = this.segment(container, index); payload.put(bitset, 'BYTE'); if (bitset == 0) { continue; } payload.putBytes(buffer); } packet.putBytes(this.strip(payload)); this.queue(packet); } public sendUpdateAllWidgetItems(widget: { widgetId: number; containerId: number }, container: ItemContainer): void { const packet = new Packet(12, PacketType.DYNAMIC_LARGE); this.update(packet, widget, container); } public sendUpdateAllWidgetItemsById(widget: { widgetId: number; containerId: number }, itemIds: number[]): void { const container = new ItemContainer(itemIds.length); const items = itemIds.map(id => (!id ? null : { itemId: id, amount: 1 })); container.setAll(items, false); this.sendUpdateAllWidgetItems(widget, container); } public setItemOnWidget(widgetId: number, childId: number, itemId: number, zoom: number): void { const packet = new Packet(120); packet.put(zoom, 'SHORT'); packet.put(itemId, 'SHORT', 'LITTLE_ENDIAN'); packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN'); this.queue(packet); } public toggleWidgetVisibility(widgetId: number, childId: number, hidden: boolean): void { const packet = new Packet(115); packet.put(hidden ? 1 : 0, 'BYTE'); packet.put((widgetId << 16) | childId, 'INT', 'LITTLE_ENDIAN'); this.queue(packet); } public moveWidgetChild(widgetId: number, childId: number, offsetX: number, offsetY: number): void { const packet = new Packet(3); packet.put((widgetId << 16) | childId, 'INT'); packet.put(offsetY, 'SHORT', 'LITTLE_ENDIAN'); packet.put(offsetX, 'SHORT', 'LITTLE_ENDIAN'); this.queue(packet); } public showTabWidget(widgetId: number): void { const packet = new Packet(237); packet.put(widgetId, 'SHORT'); this.queue(packet); } public sendTabWidget(tabIndex: SidebarTab, widgetId: number | null): void { const packet = new Packet(140); packet.put(widgetId === null || widgetId === -1 ? 65535 : widgetId, 'SHORT'); packet.put(tabIndex); this.queue(packet); } public blinkTabIcon(tabIndex: number): void { const packet = new Packet(88); packet.put(tabIndex); this.queue(packet); } public showFullscreenWidget(widgetId: number, secondaryWidgetId: number): void { const packet = new Packet(195); packet.put(secondaryWidgetId, 'SHORT'); packet.put(widgetId, 'SHORT'); this.queue(packet); } public showNumberInputDialogue(): void { const packet = new Packet(132); this.queue(packet); } public showTextInputDialogue(): void { const packet = new Packet(124); this.queue(packet); } public showChatDialogue(widgetId: number): void { const packet = new Packet(185); packet.put(widgetId, 'SHORT'); this.queue(packet); } public updateCarryWeight(weight: number): void { const packet = new Packet(171); packet.put(weight, 'SHORT'); this.queue(packet); } public showHintIcon(iconType: 2 | 3 | 4 | 5 | 6, position: Position, offset: number = 0): void { const packet = new Packet(186); packet.put(iconType, 'BYTE'); packet.put(position.x, 'SHORT'); packet.put(position.y, 'SHORT'); packet.put(offset, 'BYTE'); this.queue(packet); } public showPlayerHintIcon(player: Player): void { const packet = new Packet(186); packet.put(10, 'BYTE'); packet.put(player.worldIndex, 'SHORT'); // Packet requires a length of 6, so send some extra junk packet.put(0); packet.put(0); packet.put(0); this.queue(packet); } public showNpcHintIcon(npc: Npc): void { const packet = new Packet(186); packet.put(1, 'BYTE'); packet.put(npc.worldIndex, 'SHORT'); // Packet requires a length of 6, so send some extra junk packet.put(0); packet.put(0); packet.put(0); this.queue(packet); } public resetNpcHintIcon(): void { const packet = new Packet(186); packet.put(1, 'BYTE'); packet.put(-1, 'SHORT'); // Packet requires a length of 6, so send some extra junk packet.put(0); packet.put(0); packet.put(0); this.queue(packet); } public logout(): void { this.packetQueue = []; this.updatingQueue = []; this.socket.write(new Packet(181).toBuffer(this.player.outCipher)); } public chatboxMessage(message: string): void { const packet = new Packet(82, PacketType.DYNAMIC_SMALL); packet.putString(message); this.queue(packet); } public consoleMessage(message: string): void { const packet = new Packet(83, PacketType.DYNAMIC_SMALL); packet.putString(message); this.queue(packet); } public sendConsoleCommand(command: string, help: string): void { const packet = new Packet(85, PacketType.DYNAMIC_SMALL); packet.putString(command); packet.putString(help); this.queue(packet); } public updateSkill(skillId: number, level: number, exp: number): void { const packet = new Packet(34); packet.put(level); packet.put(skillId); packet.put(exp, 'INT', 'LITTLE_ENDIAN'); this.queue(packet); } public constructMapRegion(mapData: ConstructedRegion): void { const packet = new Packet(23, PacketType.DYNAMIC_LARGE); packet.put(this.player.position.chunkLocalY, 'short'); packet.put(this.player.position.chunkLocalX, 'short', 'le'); packet.put(this.player.position.chunkX + 6, 'short'); packet.put(this.player.position.level); packet.put(this.player.position.chunkY + 6, 'short'); packet.openBitBuffer(); const mapWorldX = mapData.renderPosition.x; const mapWorldY = mapData.renderPosition.y; const topCornerMapChunk = activeWorld.chunkManager.getChunkForWorldPosition( new Position(mapWorldX, mapWorldY, this.player.position.level), ); const playerChunk = activeWorld.chunkManager.getChunkForWorldPosition(this.player.position); const offsetX = playerChunk.position.x - (topCornerMapChunk.position.x - 2); const offsetY = playerChunk.position.y - (topCornerMapChunk.position.y - 2); mapData.drawOffsetX = offsetX - 6; // 6 === center mapData.drawOffsetY = offsetY - 6; // 6 === center for (let level = 0; level < 4; level++) { for (let x = 0; x < 13; x++) { for (let y = 0; y < 13; y++) { let mapTileOffsetX = x + mapData.drawOffsetX; let mapTileOffsetY = y + mapData.drawOffsetY; if (mapTileOffsetX < 0) { mapTileOffsetX = 0; } if (mapTileOffsetX > 12) { mapTileOffsetX = 12; } if (mapTileOffsetY < 0) { mapTileOffsetY = 0; } if (mapTileOffsetY > 12) { mapTileOffsetY = 12; } const constructedChunk: ConstructedChunk | null = mapData.chunks[level][mapTileOffsetX][mapTileOffsetY]; packet.putBits(1, constructedChunk === null ? 0 : 1); if (constructedChunk !== null) { const { templatePosition, orientation } = constructedChunk; packet.putBits(2, templatePosition?.level & 0x3); packet.putBits(10, templatePosition?.x / 8); packet.putBits(11, templatePosition?.y / 8); packet.putBits(2, orientation || 0); packet.putBits(1, 0); // unused } } } } packet.closeBitBuffer(); const encryptionEnabled = serverConfig.encryptionEnabled === undefined ? true : serverConfig.encryptionEnabled; // Put the xtea keys for the two construction room template maps // Map coords: 29,79 && 30,79 for (let mapX = 29; mapX <= 30; mapX++) { const xteaRegion = xteaRegions[`l${mapX}_79`]; for (let seeds = 0; seeds < 4; seeds++) { packet.put(encryptionEnabled ? (xteaRegion?.key[seeds] ?? 0) : 0, 'int'); } } this.queue(packet); } public updateCurrentMapChunk(): void { const packet = new Packet(166, PacketType.DYNAMIC_LARGE); packet.put(this.player.position.chunkLocalY, 'short'); packet.put(this.player.position.chunkX + 6, 'short', 'le'); packet.put(this.player.position.chunkLocalX, 'short'); packet.put(this.player.position.chunkY + 6, 'short', 'le'); packet.put(this.player.position.level); const startX = Math.floor(this.player.position.chunkX / 8); const endX = Math.floor((this.player.position.chunkX + 12) / 8); const startY = Math.floor(this.player.position.chunkY / 8); const endY = Math.floor((this.player.position.chunkY + 12) / 8); const encryptionEnabled = serverConfig.encryptionEnabled === undefined ? true : serverConfig.encryptionEnabled; for (let mapX = startX; mapX <= endX; mapX++) { for (let mapY = startY; mapY <= endY; mapY++) { const xteaRegion = xteaRegions[`l${mapX}_${mapY}`]; for (let seeds = 0; seeds < 4; seeds++) { packet.put(encryptionEnabled ? (xteaRegion?.key[seeds] ?? 0) : 0, 'int'); } } } this.queue(packet); } public updatePlayerOption(option: string, index: number = 0, placement: 'TOP' | 'BOTTOM' = 'BOTTOM'): void { const packet = new Packet(223, PacketType.DYNAMIC_SMALL); packet.putString(!option ? 'hidden' : option); packet.put(placement === 'TOP' ? 1 : 0); packet.put(index + 1); this.queue(packet); } public flushQueue(): void { if (!this.socket || this.socket.destroyed) { return; } const buffer = Buffer.concat([...this.packetQueue, ...this.updatingQueue]); if (buffer.length !== 0) { this.socket.write(buffer); } this.updatingQueue = []; this.packetQueue = []; } public queue(packet: Packet, updateTask: boolean = false): void { if (!this.socket || this.socket.destroyed) { return; } const queue = updateTask ? this.updatingQueue : this.packetQueue; const packetBuffer = packet.toBuffer(this.player.outCipher); queue.push(packetBuffer); } private putCameraPosition(packet: Packet, position: Position, height: number, speed: number, acceleration: number): void { packet.put(position.calculateChunkLocalX(this.player.lastMapRegionUpdatePosition)); packet.put(position.calculateChunkLocalY(this.player.lastMapRegionUpdatePosition)); packet.put(height, 'SHORT'); packet.put(speed); packet.put(acceleration); } private getChunkPositionOffset(x: number, y: number, chunk: Chunk): number { const offsetX = x - (chunk.position.x + 6) * 8; const offsetY = y - (chunk.position.y + 6) * 8; return offsetX * 16 + offsetY; } private getChunkOffset(chunk: Chunk): { offsetX: number; offsetY: number } { let offsetX = (chunk.position.x + 6) * 8; let offsetY = (chunk.position.y + 6) * 8; offsetX -= this.player.lastMapRegionUpdatePosition.chunkX * 8; offsetY -= this.player.lastMapRegionUpdatePosition.chunkY * 8; return { offsetX, offsetY }; } private strip(packet: Packet): Buffer { const size = packet.writerIndex; const buffer = new ByteBuffer(size); packet.copy(buffer, 0, 0, size); return Buffer.from(buffer); } private segment(container: ItemContainer, start: number): { bitset: number; buffer: Buffer } { const bound = 7 * 8; const payload = new Packet(-1, PacketType.FIXED, bound); let bitset: number = 0; for (let offset = 0; offset < 8; offset++) { const item = container.items[start + offset]; if (!item) { continue; } bitset |= 1 << offset; const large = item.amount >= 255; if (large) { payload.put(255, 'BYTE'); } payload.put(item.amount, large ? 'INT' : 'BYTE'); payload.put(item.itemId + 1, 'SHORT'); } return { bitset, buffer: this.strip(payload) }; } } ================================================ FILE: src/engine/net/packet.ts ================================================ import { ByteBuffer } from '@runejs/common'; import type { Isaac } from './isaac'; /** * The type of packet; Fixed, Dynamic Small (sized byte), or Dynamic Large (sized short) */ export enum PacketType { FIXED = 'FIXED', DYNAMIC_SMALL = 'DYNAMIC_SMALL', DYNAMIC_LARGE = 'DYNAMIC_LARGE', } /** * A single packet to be sent to the game client. */ export class Packet extends ByteBuffer { private readonly _packetId: number; private readonly _type: PacketType = PacketType.FIXED; public constructor(packetId: number, type: PacketType = PacketType.FIXED, allocatedSize: number = 5000) { super(allocatedSize); this._packetId = packetId; this._type = type; } public toBuffer(cipher: Isaac): Buffer { const packetSize = this.writerIndex; let bufferSize = packetSize + 1; // +1 for the packet id if (this.type !== PacketType.FIXED) { bufferSize += this.type === PacketType.DYNAMIC_SMALL ? 1 : 2; } const buffer = new ByteBuffer(bufferSize); buffer.put((this.packetId + (cipher !== null ? cipher.rand() : 0)) & 0xff, 'BYTE'); let copyStart = 1; if (this.type === PacketType.DYNAMIC_SMALL) { buffer.put(packetSize, 'BYTE'); copyStart = 2; } else if (this.type === PacketType.DYNAMIC_LARGE) { buffer.put(packetSize, 'SHORT'); copyStart = 3; } this.copy(buffer, copyStart, 0, packetSize); return Buffer.from(buffer); } public get packetId(): number { return this._packetId; } public get type(): PacketType { return this._type; } } ================================================ FILE: src/engine/plugins/content-plugin.ts ================================================ import { join } from 'path'; import type { ActionHook } from '@engine/action/hook/action-hook'; import type { Quest } from '@engine/world/actor/player/quest'; import { logger } from '@runejs/common'; import { getFiles } from '@runejs/common/fs'; /** * The definition of a single content plugin. */ export class ContentPlugin { public pluginId: string; public hooks?: ActionHook[]; public quests?: Quest[]; } /** * Searches for and parses all plugin files within the /plugins directory. */ export async function loadPluginFiles(): Promise { const pluginDir = join('.', 'dist', 'plugins'); const relativeDir = join('..', '..', 'plugins'); const plugins: ContentPlugin[] = []; for await (const path of getFiles(pluginDir, { type: 'whitelist', list: ['.plugin.js', 'index.js'] })) { const location = join(relativeDir, path.substring(pluginDir.length).replace('.js', '')); try { let pluginFile = require(location); if (!pluginFile) { continue; } if (pluginFile.default) { pluginFile = pluginFile.default; } const plugin = pluginFile as ContentPlugin; if (!plugin.pluginId) { logger.error(`Error loading plugin: Plugin ID not provided for .plugin file at ${path}`); continue; } if (plugins.find(loadedPlugin => loadedPlugin.pluginId === plugin.pluginId)) { logger.error(`Error loading plugin: Duplicate plugin ID ${plugin.pluginId} at ${path}`); continue; } plugins.push(plugin); } catch (error) { logger.error(`Error loading plugin file at ${location}:`); logger.error(error); } } return plugins; } ================================================ FILE: src/engine/plugins/loader.ts ================================================ import type { ActionType } from '@engine/action/action-pipeline'; import { type ActionHook, sortActionHooks } from '@engine/action/hook/action-hook'; import { loadPluginFiles } from '@engine/plugins/content-plugin'; import { Quest } from '@engine/world/actor/player/quest'; import { logger } from '@runejs/common'; /** * A type for describing the plugin action hook map. */ type PluginActionHookMap = { quest?: ActionHook[] } & { [key in ActionType]?: ActionHook[]; }; /** * A type for describing the plugin action hook map. */ interface PluginQuestMap { [key: string]: Quest; } /** * A list of action hooks imported from content plugins. */ export let actionHookMap: PluginActionHookMap = {}; /** * A list of quests imported from content plugins. */ export let questMap: PluginQuestMap = {}; /** * Searches for and loads all plugin files and their associated action hooks. */ export async function loadPlugins(): Promise { actionHookMap = {}; questMap = {}; const plugins = await loadPluginFiles(); const pluginActionHookList = plugins?.filter(plugin => !!plugin?.hooks)?.map(plugin => plugin.hooks); if (pluginActionHookList && pluginActionHookList.length !== 0) { pluginActionHookList .reduce((a, b) => (a || []).concat(b || [])) ?.forEach(action => { if (!(action instanceof Quest)) { if (!actionHookMap[action.type]) { actionHookMap[action.type] = []; } actionHookMap[action.type]!.push(action); } else { if (!actionHookMap['quest']) { actionHookMap['quest'] = []; } actionHookMap['quest'].push(action); } }); } else { logger.warn(`No action hooks detected - update plugins.`); } for (const plugin of plugins) { if (!plugin.quests) { continue; } for (const quest of plugin.quests) { questMap[quest.id] = quest; } } // @TODO implement proper sorting rules Object.keys(actionHookMap).forEach(key => (actionHookMap[key] = sortActionHooks(actionHookMap[key]))); } ================================================ FILE: src/engine/plugins/reload-content.ts ================================================ import { sep } from 'path'; import { loadGameConfigurations } from '@engine/config/config-handler'; import { loadPackets } from '@engine/net/inbound-packet-handler'; import { loadPlugins } from '@engine/plugins/loader'; import type { Player } from '@engine/world/actor/player/player'; import { logger } from '@runejs/common'; export const reloadContentCommands = ['plugins', 'reload', 'content', 'hotload', 'refresh', 'restart', 'r']; export const reloadContent = async (player: Player, isConsole: boolean = false) => { player.sendLogMessage(' ', isConsole); player.sendLogMessage('Deleting content cache...', isConsole); const includeList = ['plugins'].map(p => sep + p + sep); const ignoreList = ['node_modules', 'engine', 'server'].map(p => sep + p + sep); const pluginCache: string[] = []; const cacheKeys = Object.keys(require.cache); // Delete node cache for all the old JS plugins cacheLoop: for (const cacheKey of cacheKeys) { const cachedItem = require.cache[cacheKey]; if (!cachedItem) { continue; } const path = typeof cachedItem === 'string' ? cachedItem : cachedItem?.path; if (!path) { continue; } for (const ignoreItem of ignoreList) { if (path.indexOf(ignoreItem) !== -1) { continue cacheLoop; } } let includePath = false; for (const includeItem of includeList) { if (path.indexOf(includeItem) !== -1) { includePath = true; break; } } if (includePath) { pluginCache.push(cacheKey); } } console.log(pluginCache); for (const key of pluginCache) { delete require.cache[require.resolve(key)]; } try { player.sendLogMessage('Reloading plugins...', isConsole); await loadPlugins(); } catch (error) { player.sendLogMessage('Error reloading content.', isConsole); logger.error(error); } try { player.sendLogMessage('Reloading configurations...', isConsole); await loadGameConfigurations(); } catch (error) { player.sendLogMessage('Error reloading configurations.', isConsole); logger.error(error); } try { player.sendLogMessage('Reloading packets...', isConsole); await loadPackets(); } catch (error) { player.sendLogMessage('Error reloading packets.', isConsole); logger.error(error); } player.sendLogMessage('Reload completed.', isConsole); }; ================================================ FILE: src/engine/task/README.md ================================================ # Task system The task system allows you to write content which will be executed on the tick cycles of the server. You can configure a task to execute every `n` ticks (minimum of `1` for every tick), and you can also choose a number of other behaviours, such as whether the task should execute immediately or after a delay, as well as set to repeat indefinitely. ## Scheduling a task You can schedule a task by registering it with a `TaskScheduler`. The task in the example of below runs with an interval of `2`, i.e. it will be executed every 2 ticks. ```ts class MyTask extends Task { public constructor() { super({ interval: 2 }); } public execute(): void { console.log('2 ticks'); } } const scheduler = new TaskScheduler(); scheduler.addTask(new MyTask()); scheduler.tick(); scheduler.tick(); // '2 ticks' ``` Every two times that `scheduler.tick()` is called, it will run the `execute` function of your task. ## Task configuration You can pass a `TaskConfig` object to the `Task` constructor in order to configure various aspects of your task. ### Timing The most simple configuration option for a `Task` is the `interval` option. Your task will be executed every `interval` amount of ticks. ```ts /** * The number of ticks between each execution of the task. */ interval: number; ``` For example, with an interval of `1`, your task will run every tick. The default value is `1`. ### Immediate execution You can configure your task to execute immediately with the `immediate` option. ```ts /** * Should the task be executed on the first tick after it is added? */ immediate: boolean; ``` For example, if `immediate` is `true` and `interval` is `5`, your task will run on the 1st and 6th ticks (and so on). ### Repeating You can use the `repeat` option to tell your task to run forever. ```ts /** * Should the task be repeated indefinitely? */ repeat: boolean; ``` You can use `this.stop()` inside the task to stop it from repeating further. ### Stacking The `stackType` and `stackGroup` properties allow you to control how your task interacts with other, similar tasks. ```ts /** * How the task should be stacked with other tasks of the same stack group. */ stackType: TaskStackType; /** * The stack group for this task. */ stackGroup: string; ``` When `stackType` is set to `TaskStackType.NEVER`, other tasks with the same `stackGroup` will be stopped when your task is enqueued. A `stackType` of `TaskStackType.STACK` will allow your task to run with others of the same group. The default type is `TaskStackType.STACK` and the group is `TaskStackGroup.ACTION` (`'action'`) ## Task Subtypes Rather than extending `Task`, there are a number of subclasses you can extend which will give you some syntactic sugar around common functionality. - `ActorTask` This is the base task to be performed by an `Actor`. It will automatically listen to the actor's walking queue, and stop the task if it has a `breakType` of `ON_MOVE`. - `ActorWalkToTask` This task will make an actor walk to a `Position` or `LandscapeObject` and will expose the `atDestination` property for your extended task to query. You can then begin executing your task logic. - `ActorLandscapeObjectInteractionTask` This task extends `ActorWalkToTask` and will make an actor walk to a given `LandscapeObject`, before exposing the `landscapeObject` property for your task to use. - `ActorWorldItemInteractionTask` This task extends `ActorWalkToTask` and will make an actor walk to a given `WorldItem`, before exposing the `worldItem` property for your task to use. # Future improvements - Stalling executions for certain tasks when interface is open - should we create a `PlayerTask` to contain this behaviour? The `breakType` behaviour could be moved to this base, rather than `ActorTask` - Consider refactoring this system to use functional programming patterns. Composition should be favoured over inheritance generally, and there are some examples of future tasks which may be easier if we could compose tasks from building blocks. Consider the implementation of some task which requires both a `LandscapeObject` and a `WorldItem` - we currently would need to create some custom task which borrowed behaviour from the `ActorLandscapeObjectInteractionTask` and `ActorWorldItemInteractionTask`. TypeScript mixins could be useful here. # Content requiring conversion to task system Highest priority is to convert pieces of content which make use of the old `task` system. These are: - Magic attack - Magic teleports - Prayer - Combat - Forging (smithing) - Woodcutting The following areas will make interesting use of the task system and would serve as a good demonstration: - Health regen - NPC movement ================================================ FILE: src/engine/task/impl/actor-actor-interaction-task.ts ================================================ import type { Actor } from '@engine/world/actor/actor'; import type { Position } from '@engine/world/position'; import { ActorWalkToTask } from './actor-walk-to-task'; /** * A task for an actor to interact with another actor. * * This task extends {@link ActorWalkToTask} and will walk the actor to the other actor. * Once the actor is within range of the other actor, the task will expose the {@link other} property * * @author jameskmonger */ export abstract class ActorActorInteractionTask extends ActorWalkToTask< TActor, () => Position > { private _other: TOtherActor; /** * @param actor The actor executing this task. * @param TOtherActor The other actor to interact with. * @param walkOnStart Whether to walk to the other actor on task start. * Defaults to `false` as the client generally inits a walk on interaction. */ constructor(actor: TActor, otherActor: TOtherActor, walkOnStart = false) { super( actor, () => otherActor.position, // TODO (jkm) handle other actor size 1, walkOnStart, ); if (!otherActor) { this.stop(); return; } this._other = otherActor; } /** * Checks for the continued presence of the other actor and stops the task if it is no longer present. * * TODO (jameskmonger) unit test this */ public execute() { super.execute(); if (!this.isActive || !this.atDestination) { return; } if (!this._other) { this.stop(); return; } // TODO (jkm) check if other actor was removed from world // TODO (jkm) check if other actor has moved and repath player if so } /** * Gets the {@link TOtherActor} that this task is interacting with. * * @returns If the other actor is still present, and the actor is at the destination, the other actor. * Otherwise, `null`. * * TODO (jameskmonger) unit test this */ protected get other(): TOtherActor | null { // TODO (jameskmonger) consider if we want to do these checks rather than delegating to the child task // as currently the subclass has to store it in a subclass property if it wants to use it // without these checks if (!this.atDestination) { return null; } if (!this._other) { return null; } return this._other; } } ================================================ FILE: src/engine/task/impl/actor-landscape-object-interaction-task.ts ================================================ import { activeWorld } from '@engine/world'; import type { Actor } from '@engine/world/actor/actor'; import { Position } from '@engine/world/position'; import type { LandscapeObject } from '@runejs/filestore'; import { ActorWalkToTask } from './actor-walk-to-task'; /** * A task for an actor to interact with a {@link LandscapeObject}. * * This task extends {@link ActorWalkToTask} and will walk the actor to the object. * Once the actor is within range of the object, the task will expose the {@link landscapeObject} property * * @author jameskmonger */ export abstract class ActorLandscapeObjectInteractionTask extends ActorWalkToTask { private _landscapeObject: LandscapeObject; private _objectPosition: Position; /** * @param actor The actor executing this task. * @param landscapeObject The landscape object to interact with. * @param sizeX The size of the LandscapeObject in the X direction. * @param sizeY The size of the LandscapeObject in the Y direction. */ constructor( actor: TActor, landscapeObject: LandscapeObject, // TODO (jkm) get size/orientation automatically from the object's info sizeX: number = 1, sizeY: number = 1, ) { super( actor, landscapeObject, // TODO (jkm) atDestination must take orientation into account Math.max(sizeX, sizeY), ); if (!landscapeObject) { this.stop(); return; } // create the Position here to prevent instantiating a new Position every tick this._objectPosition = new Position(landscapeObject.x, landscapeObject.y, landscapeObject.level); this._landscapeObject = landscapeObject; } /** * Checks for the continued presence of the {@link LandscapeObject} and stops the task if it is no longer present. * * TODO (jameskmonger) unit test this */ public execute() { super.execute(); if (!this.isActive || !this.atDestination) { return; } if (!this._landscapeObject) { this.stop(); return; } const { object: worldObject } = activeWorld.findObjectAtLocation(this.actor, this._landscapeObject.objectId, this._objectPosition); if (!worldObject) { this.stop(); return; } } /** * Gets the {@link LandscapeObject} that this task is interacting with. * * @returns If the object is still present, and the actor is at the destination, the object. * Otherwise, `null`. * * TODO (jameskmonger) unit test this */ protected get landscapeObject(): LandscapeObject | null { // TODO (jameskmonger) consider if we want to do these checks rather than delegating to the child task // as currently the subclass has to store it in a subclass property if it wants to use it // without these checks if (!this.atDestination) { return null; } if (!this._landscapeObject) { return null; } return this._landscapeObject; } /** * Get the position of this task's landscape object * * @returns The position of this task's landscape object, or null if the landscape object is not present */ protected get landscapeObjectPosition(): Position | null { if (!this._landscapeObject) { return null; } return this._objectPosition; } } ================================================ FILE: src/engine/task/impl/actor-task.ts ================================================ import type { Actor } from '@engine/world/actor/actor'; import type { Subscription } from 'rxjs'; import { Task } from '../task'; import type { TaskConfig } from '../types'; import { TaskBreakType } from '../types'; /** * A task that is executed by an actor. * * If the task has a break type of ON_MOVE, the ActorTask will subscribe to the actor's * movement events and will stop executing when the actor moves. * * @author jameskmonger */ export abstract class ActorTask extends Task { /** * A function that is called when a movement event is queued on the actor. * * This will be `null` if the task does not break on movement. */ private walkingQueueSubscription: Subscription | null = null; /** * @param actor The actor executing this task. * @param config The task configuration. */ constructor( protected readonly actor: TActor, config?: TaskConfig, ) { super(config); this.listenForMovement(); } /** * Called when the task is stopped and unsubscribes from the actor's walking queue if necessary. * * TODO (jameskmonger) unit test this */ public onStop(): void { if (this.walkingQueueSubscription) { this.walkingQueueSubscription.unsubscribe(); } } /** * If required, listen to the actor's walking queue to stop the task * * This function uses `setImmediate` to ensure that the subscription to the * walking queue is not created * * TODO (jameskmonger) unit test this */ private listenForMovement(): void { if (!this.breaksOn(TaskBreakType.ON_MOVE)) { return; } setImmediate(() => { this.walkingQueueSubscription = this.actor.walkingQueue.movementQueued$.subscribe(() => { this.stop(); }); }); } } ================================================ FILE: src/engine/task/impl/actor-teleport-task.ts ================================================ import { ActorTask } from '@engine/task/impl/actor-task'; import type { Actor } from '@engine/world/actor/actor'; import type { Position } from '@engine/world/position'; /** * A task for an actor to teleport to a new position. * * @author Kat */ export class ActorTeleportTask extends ActorTask { private readonly _newPosition: Position; /** * @param actor The actor executing this task. * @param newPosition The position to teleport the actor to. */ constructor(actor: TActor, newPosition: Position) { super(actor, { repeat: false, immediate: false, }); this._newPosition = newPosition; } /** * Teleports the actor to the new position. * * TODO (Kat) unit test this */ public execute() { if (!this.newPosition) { return; } this.actor.teleport(this.newPosition); } /** * Get the position the actor will be teleported to. * * @returns The position that the actor will be teleported to. */ protected get newPosition(): Position | null { if (!this._newPosition) { return null; } return this._newPosition; } } ================================================ FILE: src/engine/task/impl/actor-walk-to-task.ts ================================================ import type { Actor } from '@engine/world/actor/actor'; import { Position } from '@engine/world/position'; import type { LandscapeObject } from '@runejs/filestore'; import { TaskBreakType, TaskStackGroup, TaskStackType } from '../types'; import { ActorTask } from './actor-task'; /** * Possible types of targets for an actor to walk to. */ type WalkToTargetType = LandscapeObject | Position; /** * The target can either be a {@link WalkToTargetType} or a function that returns a {@link WalkToTargetType}. */ type WalkToTarget = WalkToTargetType | (() => WalkToTargetType); /** * This ActorWalkToTask interface allows us to merge with the ActorWalkToTask class * and add optional methods to the class. * * There is no way to add optional methods directly to an abstract class. * * @author jameskmonger */ export interface ActorWalkToTask extends ActorTask { /** * An optional function that is called when the actor arrives at the destination. */ onArrive?(): void; } /** * An abstract task that will make an Actor walk to a specific position, * before calling the `arrive` function and continuing execution. * * The task will be stopped if the adds a new movement to their walking queue. * * @author jameskmonger */ export abstract class ActorWalkToTask extends ActorTask { private _atDestination: boolean = false; /** * @param actor The actor executing this task. * @param destination The destination position/object, or a function that returns the destination position/object. * @param distance The distance from the destination position that the actor must be within to arrive. * @param walkOnStart Whether to walk to the destination on task start. */ constructor( actor: TActor, protected readonly destination: TTarget, protected readonly distance = 1, walkOnStart = true, ) { super(actor, { interval: 1, stackType: TaskStackType.NEVER, stackGroup: TaskStackGroup.ACTION, breakTypes: [TaskBreakType.ON_MOVE], immediate: false, repeat: true, }); // TODO (jkm) should this be in constructor? or on first execute? if (walkOnStart) { this.actor.pathfinding.walkTo(this.getTargetPosition(), {}); } } /** * Every tick of the task, check if the actor has arrived at the destination. * * You can check `this.arrived` to see if the actor has arrived. * * If the actor has previously arrived at the destination, but is no longer within distance, * the task will be stopped. * * @returns `true` if the task was stopped this tick, `false` otherwise. * * TODO (jameskmonger) unit test this */ public execute() { if (!this.isActive) { return; } const destination = this.getTargetPosition(); // TODO this uses actual distances rather than tile distances // is this correct? const withinDistance = this.actor.position.withinInteractionDistance(destination, this.distance); // the WalkToTask itself is complete when the actor has arrived at the destination // execution will now continue in the extended class if (this._atDestination) { // TODO consider making this optional if (!withinDistance) { this._atDestination = false; this.stop(); } return; } if (withinDistance) { this._atDestination = true; if (this.onArrive) { this.onArrive(); } } } private getTargetPosition(): Position { const destination: WalkToTargetType = typeof this.destination === 'function' ? this.destination() : this.destination; if (destination instanceof Position) { return destination; } return new Position(destination.x, destination.y); } /** * `true` if the actor has arrived at the destination. */ protected get atDestination(): boolean { return this._atDestination; } } ================================================ FILE: src/engine/task/impl/actor-world-item-interaction-task.ts ================================================ import type { WorldItem } from '@engine/world/items/world-item'; import type { Actor } from '../../world/actor/actor'; import { ActorWalkToTask } from './actor-walk-to-task'; /** * A task for an actor to interact with a world item. * * This task extends {@link ActorWalkToTask} and will walk the actor to the world item. * Once the actor is within range of the world item, the task will expose the {@link worldItem} property * * @author jameskmonger */ export abstract class ActorWorldItemInteractionTask extends ActorWalkToTask { private _worldItem: WorldItem; /** * @param actor The actor executing this task. * @param worldItem The world item to interact with. */ constructor(actor: TActor, worldItem: WorldItem) { super(actor, worldItem.position, 1); if (!worldItem) { this.stop(); return; } this._worldItem = worldItem; } /** * Checks for the continued presence of the world item and stops the task if it is no longer present. * * TODO (jameskmonger) unit test this */ public execute() { super.execute(); if (!this.isActive || !this.atDestination) { return; } if (!this._worldItem || this._worldItem.removed) { this.stop(); return; } } /** * Gets the world item that this task is interacting with. * * @returns If the world item is still present, and the actor is at the destination, the world item. * Otherwise, `null`. * * TODO (jameskmonger) unit test this */ protected get worldItem(): WorldItem | null { // TODO (jameskmonger) consider if we want to do these checks rather than delegating to the child task // as currently the subclass has to store it in a subclass property if it wants to use it // without these checks if (!this.atDestination) { return null; } if (!this._worldItem || this._worldItem.removed) { return null; } return this._worldItem; } } ================================================ FILE: src/engine/task/task-scheduler.test.ts ================================================ import type { Task } from './task'; import { TaskScheduler } from './task-scheduler'; import { TaskStackType } from './types'; import { createMockTask } from './utils/_testing'; describe('TaskScheduler', () => { let taskScheduler: TaskScheduler; beforeEach(() => { taskScheduler = new TaskScheduler(); }); describe('when enqueueing a task', () => { let executeMock: jest.Mock; let task: Task; beforeEach(() => { ({ task, executeMock } = createMockTask()); }); it('should add the task to the running list when ticked', () => { taskScheduler.enqueue(task); taskScheduler.tick(); expect(executeMock).toHaveBeenCalled(); }); it('should not add the task to the running list until the next tick', () => { taskScheduler.enqueue(task); expect(executeMock).not.toHaveBeenCalled(); }); describe('when ticked multiple times', () => { beforeEach(() => { taskScheduler.enqueue(task); taskScheduler.tick(); taskScheduler.tick(); }); it('should tick the task twice', () => { expect(executeMock).toHaveBeenCalledTimes(2); }); }); describe('when the task is stopped', () => { beforeEach(() => { taskScheduler.enqueue(task); taskScheduler.tick(); }); it('should not tick the task after stopping', () => { task.stop(); taskScheduler.tick(); expect(executeMock).toHaveBeenCalledTimes(1); }); }); }); describe('when enqueueing a task that cannot stack', () => { const interval = 0; const stackType = TaskStackType.NEVER; const stackGroup = 'foo'; let firstExecuteMock: jest.Mock; let firstTask: Task; beforeEach(() => { ({ task: firstTask, executeMock: firstExecuteMock } = createMockTask(interval, stackType, stackGroup)); }); it('should stop any other tasks with the same stack group', () => { const { task: secondTask, executeMock: secondExecuteMock } = createMockTask(interval, stackType, stackGroup); taskScheduler.enqueue(firstTask); taskScheduler.enqueue(secondTask); taskScheduler.tick(); expect(firstExecuteMock).not.toHaveBeenCalled(); expect(secondExecuteMock).toHaveBeenCalled(); }); it('should not stop any other tasks with a different stack group', () => { const otherStackGroup = 'bar'; const { task: secondTask, executeMock: secondExecuteMock } = createMockTask(interval, stackType, otherStackGroup); taskScheduler.enqueue(firstTask); taskScheduler.enqueue(secondTask); taskScheduler.tick(); expect(firstExecuteMock).toHaveBeenCalled(); expect(secondExecuteMock).toHaveBeenCalled(); }); }); describe('when clearing the scheduler', () => { let executeMock: jest.Mock; let task: Task; beforeEach(() => { ({ task, executeMock } = createMockTask()); }); it('should stop all tasks', () => { taskScheduler.enqueue(task); taskScheduler.tick(); taskScheduler.clear(); taskScheduler.tick(); expect(executeMock).toHaveBeenCalledTimes(1); }); }); }); ================================================ FILE: src/engine/task/task-scheduler.ts ================================================ import { Queue } from '@engine/util/queue'; import type { Task } from './task'; import { TaskStackType } from './types'; /** * A class that ticks tasks in a queue, and removes them when they are no longer active. * * @author jameskmonger */ export class TaskScheduler { /** * A queue of tasks that are waiting to be added to the running list. */ private pendingTasks = new Queue(); /** * The list of tasks that are currently running. */ private runningTasks: Task[] = []; /** * Register any pending tasks, and tick any running tasks. */ public tick(): void { // Add any pending tasks to the running list while (this.pendingTasks.isNotEmpty) { const task = this.pendingTasks.dequeue(); if (!task || !task.isActive) { continue; } this.runningTasks.push(task); } // Use an iterator so that we can remove tasks from the list while iterating for (const [index, task] of this.runningTasks.entries()) { if (!task) { continue; } task.tick(); if (!task.isActive) { this.runningTasks.splice(index, 1); } } } /** * Add a task to the end of the pending queue. * * If the task has a stack type of `NEVER`, any other tasks in the scheduler * with the same stack group will be stopped. * * @param task The task to add. */ public enqueue(task: Task): void { if (!task.isActive) { return; } // if the task can't stack with others of a similar type, we need to stop them if (task.stackType === TaskStackType.NEVER) { // Use an iterator so that we can remove tasks from the list while iterating for (const [index, otherTask] of this.runningTasks.entries()) { if (!otherTask) { continue; } if (otherTask.stackGroup === task.stackGroup) { otherTask.stop(); this.runningTasks.splice(index, 1); } } for (const otherTask of this.pendingTasks.items) { if (!otherTask) { continue; } if (otherTask.stackGroup === task.stackGroup) { otherTask.stop(); } } } this.pendingTasks.enqueue(task); } /** * Clear all tasks from the scheduler. */ public clear(): void { this.pendingTasks.clear(); this.runningTasks = []; } } ================================================ FILE: src/engine/task/task.test.ts ================================================ import { Task } from './task'; import { TaskStackType } from './types'; import { createMockTask } from './utils/_testing'; describe('Task', () => { // stacking mechanics are tested in the scheduler const stackType = TaskStackType.NEVER; const stackGroup = 'foo'; const breakType = []; describe('when interval is 0', () => { const interval = 0; // no point setting this to true as the interval is 0 const immediate = false; let executeMock: jest.Mock; let task: Task; describe('and repeat is true', () => { const repeat = false; beforeEach(() => { ({ task, executeMock } = createMockTask(interval, stackType, stackGroup, immediate, breakType, repeat)); }); describe('when ticked once', () => { beforeEach(() => { task.tick(); }); it('should execute twice', () => { expect(executeMock).toHaveBeenCalled(); }); }); describe('when ticked twice', () => { beforeEach(() => { task.tick(); task.tick(); }); it('should execute twice', () => { expect(executeMock).toHaveBeenCalledTimes(1); }); }); }); describe('and repeat is false', () => { const repeat = false; beforeEach(() => { ({ task, executeMock } = createMockTask(interval, stackType, stackGroup, immediate, breakType, repeat)); }); describe('when ticked once', () => { beforeEach(() => { task.tick(); }); it('should execute once', () => { expect(executeMock).toHaveBeenCalledTimes(1); }); }); describe('when ticked twice', () => { beforeEach(() => { task.tick(); task.tick(); }); it('should execute once', () => { expect(executeMock).toHaveBeenCalledTimes(1); }); }); }); }); describe('when interval is 2', () => { const interval = 2; // not testing repeat here as it is tested above const repeat = false; let executeMock: jest.Mock; let task: Task; describe('and immediate is true', () => { const immediate = true; beforeEach(() => { ({ task, executeMock } = createMockTask(interval, stackType, stackGroup, immediate, breakType, repeat)); }); describe('when ticked once', () => { beforeEach(() => { task.tick(); }); it('should execute once', () => { expect(executeMock).toHaveBeenCalledTimes(1); }); }); describe('when ticked twice', () => { beforeEach(() => { task.tick(); task.tick(); }); // task will execute on ticks 1 and 3 it('should execute once', () => { expect(executeMock).toHaveBeenCalledTimes(1); }); }); }); describe('and immediate is false', () => { const immediate = false; beforeEach(() => { ({ task, executeMock } = createMockTask(interval, stackType, stackGroup, immediate, breakType, repeat)); }); describe('when ticked once', () => { beforeEach(() => { task.tick(); }); it('should not execute', () => { expect(executeMock).toHaveBeenCalledTimes(0); }); }); describe('when ticked twice', () => { beforeEach(() => { task.tick(); task.tick(); }); // task will execute on ticks 2 and 4 it('should execute once', () => { expect(executeMock).toHaveBeenCalledTimes(1); }); }); }); }); describe('when there is an onStop callback', () => { let task: Task; let onStopMock: jest.Mock; let executeMock: jest.Mock; beforeEach(() => { onStopMock = jest.fn(); executeMock = jest.fn(); task = new (class extends Task { constructor() { super(); } public execute(): void { executeMock(); } public onStop(): void { onStopMock(); } })(); }); describe('when the task is stopped', () => { beforeEach(() => { task.stop(); }); it('should call the onStop callback', () => { expect(onStopMock).toHaveBeenCalled(); }); it('should not call the execute callback', () => { expect(executeMock).not.toHaveBeenCalled(); }); describe('when the task is ticked', () => { beforeEach(() => { task.tick(); }); it('should not call the onStop callback', () => { expect(onStopMock).toHaveBeenCalledTimes(1); }); it('should not call the execute callback', () => { expect(executeMock).not.toHaveBeenCalled(); }); }); describe('when the task is stopped again', () => { beforeEach(() => { task.stop(); }); it('should not call the onStop callback', () => { expect(onStopMock).toHaveBeenCalledTimes(1); }); it('should not call the execute callback', () => { expect(executeMock).not.toHaveBeenCalled(); }); }); }); }); }); ================================================ FILE: src/engine/task/task.ts ================================================ import type { TaskBreakType, TaskConfig } from './types'; import { TaskStackGroup, TaskStackType } from './types'; const DEFAULT_TASK_CONFIG: Required = { interval: 1, stackType: TaskStackType.STACK, stackGroup: TaskStackGroup.ACTION, immediate: false, breakTypes: [], repeat: true, }; function readConfigValue(key: keyof TaskConfig, config?: TaskConfig): any { if (!config) { return DEFAULT_TASK_CONFIG[key]; } return config[key] !== undefined ? config[key] : DEFAULT_TASK_CONFIG[key]; } /** * This Task interface allows us to merge with the Task class * and add optional methods to the class. * * There is no way to add optional methods directly to an abstract class. * * @author jameskmonger */ export interface Task { /** * A callback that is called when the task is stopped. */ onStop?(): void; } /** * A Task which can be ticked and executes after a specified number of ticks. * * The task can be configured to execute once, or repeatedly, and can also be executed immediately. * * @author jameskmonger */ export abstract class Task { /** * How the task should be stacked with other tasks of the same stack group. */ public readonly stackType: TaskStackType; /** * The stack group for this task. */ public readonly stackGroup: string; /** * Conditions under which the task should be broken. */ public readonly breakTypes: TaskBreakType[]; /** * The number of ticks between each execution of the task. */ private interval: number; /** * The number of ticks remaining before the task is executed. */ private ticksRemaining: number; /** * Should the task be repeated indefinitely? */ private repeat: boolean; private _isActive = true; /** * @param config the configuration options for the task * * @see TaskConfig for more information on the configuration options */ public constructor(config?: TaskConfig) { this.interval = readConfigValue('interval', config); this.stackType = readConfigValue('stackType', config); this.stackGroup = readConfigValue('stackGroup', config); const immediate = readConfigValue('immediate', config); this.ticksRemaining = immediate ? 0 : this.interval; this.breakTypes = readConfigValue('breakTypes', config); this.repeat = readConfigValue('repeat', config); } /** * The task's execution logic. * * Ensure that you call `super.execute()` if you override this method! * * TODO (jameskmonger) consider some kind of workaround to enforce a super call * https://github.com/microsoft/TypeScript/issues/21388#issuecomment-360214959 */ public abstract execute(): void; /** * Whether this task breaks on the specified {@link TaskBreakType}. * * @param breakType the break type to check * * @returns true if the task breaks on the specified break type */ public breaksOn(breakType: TaskBreakType): boolean { return this.breakTypes.includes(breakType); } /** * Stop the task from executing. * * @returns true if the task was stopped, false if the task was already stopped */ public stop(): boolean { // can't stop a task that's already stopped if (!this._isActive) { return false; } this._isActive = false; if (this.onStop) { this.onStop(); } return true; } /** * Tick the task, decrementing the number of ticks remaining. * * If the number of ticks remaining reaches zero, the task is executed. * * If the task is configured to repeat, the number of ticks remaining is reset to the interval. * Otherwise, the task is stopped. */ public tick(): void { if (!this._isActive) { return; } this.ticksRemaining--; if (this.ticksRemaining <= 0) { // TODO maybe track and expose executionCount to this child function this.execute(); // TODO should we allow the repeat count to be specified? if (this.repeat) { this.ticksRemaining = this.interval; } else { // TODO should I be calling a public function rather than setting the private variable? this.stop(); } } } /** * Is the task active? */ public get isActive(): boolean { return this._isActive; } } ================================================ FILE: src/engine/task/types.ts ================================================ /** * An enum to control the different stacking modes for tasks. * * @author jameskmonger */ export enum TaskStackType { /** * This task cannot be stacked with other tasks of the same stack group. */ NEVER, /** * This task can be stacked with other tasks of the same stack group. */ STACK, } /** * An enum to control the different stack groups for tasks. * * When a task has a stack type of `NEVER`, other tasks with the same stack group will be cancelled. * * @author jameskmonger */ export enum TaskStackGroup { /** * An action task undertaken by an actor. */ ACTION = 'action', } /** * An enum to control the different breaking modes for tasks. * * @author jameskmonger */ export enum TaskBreakType { /** * This task gets stopped when the player moves */ ON_MOVE, } /** * The configuration options for a Task. * * All options are optional as they have default values. * * @author jameskmonger */ export type TaskConfig = Partial< Readonly<{ /** * How the task should be stacked with other tasks of the same stack group. */ stackType: TaskStackType; /** * The stack group for this task. */ stackGroup: string; /** * Conditions under which the task should be broken. */ breakTypes: TaskBreakType[]; /** * The number of ticks between each execution of the task. */ interval: number; /** * Should the task be executed on the first tick after it is added? */ immediate: boolean; /** * Should the task be repeated indefinitely? */ repeat: boolean; }> >; ================================================ FILE: src/engine/task/utils/_testing.ts ================================================ import { Task } from '../task'; import type { TaskBreakType } from '../types'; import { TaskStackGroup, TaskStackType } from '../types'; export function createMockTask( interval: number = 0, stackType: TaskStackType = TaskStackType.STACK, stackGroup: string = TaskStackGroup.ACTION, immediate: boolean = false, breakTypes: TaskBreakType[] = [], repeat: boolean = true, ) { const executeMock = jest.fn(); const task = new (class extends Task { constructor() { super({ interval, stackType, stackGroup, immediate, breakTypes, repeat, }); } public execute(): void { executeMock(); } })(); return { task, executeMock }; } ================================================ FILE: src/engine/util/address.ts ================================================ export const addressToInt = (address: string): number => { if (!address) { return 0; } const parts = address.split('.'); if (!parts || parts.length !== 4) { return 0; } const num = parts.map(p => parseInt(p)); return ((num[0] & 0xff) << 24) + ((num[1] & 0xff) << 16) + ((num[2] & 0xff) << 8) + (num[3] & 0xff); }; ================================================ FILE: src/engine/util/colors.ts ================================================ export function hexToRgb(hex: number): { r: number; b: number; g: number } { return { r: (hex >> 16) & 0xff, g: (hex >> 8) & 0xff, b: hex & 0xff, }; } export function hexToHexString(hex: number): string { let r = ((hex >> 16) & 0xff).toString(16); let g = ((hex >> 8) & 0xff).toString(16); let b = (hex & 0xff).toString(16); if (r === '0') { r = '00'; } if (g === '0') { g = '00'; } if (b === '0') { b = '00'; } return r + g + b; } export function rgbTo16Bit(r: number, g: number, b: number): number { return ((r & 0x1f) << 11) | ((g & 0x3f) << 5) | ((b & 0x1f) << 0); } export const colors = { green: 0x00ff00, yellow: 0xffff00, red: 0xff0000, black: 0x000000, blue: 0x01bdfe, lightred: 0xef101f, }; ================================================ FILE: src/engine/util/data.ts ================================================ export function hasValueNotNull(variable: unknown): boolean { return typeof variable !== 'undefined' && variable !== null; } ================================================ FILE: src/engine/util/error-handling.ts ================================================ import { logger } from '@runejs/common'; /* * Error handling! Feel free to add other types of errors or warnings here. :) */ export class WidgetsClosedWarning extends Error { constructor() { super(); this.name = 'WidgetsClosedWarning'; this.message = 'The active widget was closed before the action could be completed.'; } } export class ActionsCancelledWarning extends Error { constructor() { super(); this.name = 'ActionsCancelledWarning'; this.message = 'Pending and active actions were cancelled before they could be completed.'; } } const warnings = [WidgetsClosedWarning, ActionsCancelledWarning]; export function initErrorHandling(): void { process.on('unhandledRejection', (error: any, promise) => { for (const t of warnings) { if (error instanceof t) { logger.warn(`Promise cancelled with warning: ${error.name}`); return; } } logger.error(`Unhandled promise rejection from ${promise}, reason: ${error}`); if (error && error['stack']) { logger.error((error as any).stack); } }); } ================================================ FILE: src/engine/util/files.ts ================================================ import fs from 'fs'; import util from 'util'; import { watch } from 'chokidar'; import type { Observable } from 'rxjs'; import { Subject } from 'rxjs'; const readdir = util.promisify(fs.readdir); const stat = util.promisify(fs.stat); export async function getFiles(directory: string, blacklist: string[]); export async function getFiles(directory: string, whitelist: string[], useWhitelist: boolean); export async function* getFiles(directory: string, list: string[] = [], useWhitelist?: boolean): AsyncGenerator { const files = await readdir(directory); for (const file of files) { const path = directory + '/' + file; const statistics = await stat(path); if (statistics.isDirectory()) { // (Jameskmonger) I set the default value of `true` here, not sure if it is correct. for await (const child of getFiles(path, list, useWhitelist || false)) { yield child; } } else { if (!useWhitelist) { // blacklist const invalid = list.some(item => file === item); if (invalid) { continue; } } else { // whitelist const invalid = !list.some(item => file.endsWith(item)); if (invalid) { continue; } } yield path; } } } export function watchSource(dir: string): Observable { const subject = new Subject(); const watcher = watch(dir); watcher.on('ready', () => { watcher.on('all', () => { subject.next(); }); }); return subject.asObservable(); } export function watchForChanges(dir: string, regex: RegExp): void { const watcher = watch(dir); watcher.on('ready', () => { watcher.on('all', () => { Object.keys(require.cache).forEach(id => { if (regex.test(id)) { delete require.cache[id]; } }); }); }); } ================================================ FILE: src/engine/util/num.ts ================================================ export const randomBetween = (min: number, max: number): number => { return Math.floor(Math.random() * (max - min + 1) + min); }; ================================================ FILE: src/engine/util/objects.ts ================================================ /** * Merge two objects or arrays, first object takes priority in case where the values cannot be merged * @param objectA * @param objectB * @return objectC combination of objectA and objectB */ export function deepMerge(objectA: T, objectB: T): T { if (!objectA) { return objectB; } if (!objectB) { return objectA; } if (Array.isArray(objectA)) { return [...new Set([...(objectA as any), ...(objectB as any)])] as any; } if (typeof objectA === 'object') { const newObject: T = { ...objectA }; const keys = [...new Set([...Object.keys(objectA), ...Object.keys(objectB)])]; keys.forEach(key => { if (!objectA[key]) { newObject[key] = objectB[key]; return; } if (!objectB[key]) { newObject[key] = objectA[key]; } if (Array.isArray(objectA[key])) { if (!Array.isArray(objectB[key])) { newObject[key] = [...objectA[key], objectB[key]]; return; } newObject[key] = deepMerge(objectA[key], objectB[key]); return; } if (Array.isArray(objectB[key])) { if (!Array.isArray(objectA[key])) { newObject[key] = [...objectB[key], objectA[key]]; return; } console.error('Something is wrong with deepmerger', key, objectA, objectB); } if (typeof objectA[key] === 'object' || typeof objectB === 'object') { newObject[key] = deepMerge(objectA[key], objectB[key]); return; } newObject[key] = objectA[key]; }); return newObject; } return objectA; } ================================================ FILE: src/engine/util/queue.test.ts ================================================ import { Queue } from './queue'; describe('Queue', () => { let queue: Queue; beforeEach(() => { queue = new Queue(); }); describe('checking Queue length', () => { it('should be empty when created', () => { expect(queue.isEmpty).toBe(true); expect(queue.isNotEmpty).toBe(false); }); it('should be not empty when an item is added', () => { queue.enqueue(1); expect(queue.isEmpty).toBe(false); expect(queue.isNotEmpty).toBe(true); }); it('should be empty when all items are removed', () => { queue.enqueue(1); queue.dequeue(); expect(queue.isEmpty).toBe(true); expect(queue.isNotEmpty).toBe(false); }); it('should return the correct length', () => { queue.enqueue(1); queue.enqueue(2); queue.enqueue(3); expect(queue.length).toBe(3); }); }); it('should return the correct items', () => { queue.enqueue(1); queue.enqueue(2); queue.enqueue(3); expect(queue.items).toEqual([1, 2, 3]); }); describe('when peeking', () => { it('should return the correct item', () => { queue.enqueue(1); queue.enqueue(2); queue.enqueue(3); expect(queue.peek()).toBe(1); }); it('should not remove the item', () => { queue.enqueue(1); queue.enqueue(2); queue.enqueue(3); queue.peek(); expect(queue.items).toEqual([1, 2, 3]); }); it('should return undefined when the queue is empty', () => { expect(queue.peek()).toBeUndefined(); }); }); describe('when dequeuing', () => { it('should return the correct item', () => { queue.enqueue(1); queue.enqueue(2); queue.enqueue(3); expect(queue.dequeue()).toBe(1); }); it('should remove the item', () => { queue.enqueue(1); queue.enqueue(2); queue.enqueue(3); queue.dequeue(); expect(queue.items).toEqual([2, 3]); }); it('should return undefined when the queue is empty', () => { expect(queue.dequeue()).toBeUndefined(); }); }); describe('when clearing', () => { it('should remove all items', () => { queue.enqueue(1); queue.enqueue(2); queue.enqueue(3); queue.clear(); expect(queue.items).toEqual([]); }); }); describe('when iterating', () => { it('should iterate over all items', () => { queue.enqueue(1); queue.enqueue(2); queue.enqueue(3); const items: number[] = []; for (const item of queue.items) { items.push(item); } expect(items).toEqual([1, 2, 3]); }); }); }); ================================================ FILE: src/engine/util/queue.ts ================================================ /** * A first-in-first-out queue. * * @author jameskmonger */ export class Queue { private _items: TItem[] = []; /** * Add an item to the end of the queue. * @param item The item to add. */ public enqueue(item: TItem): void { this._items.push(item); } /** * Remove an item from the front of the queue. * @returns The item removed. */ public dequeue(): TItem | undefined { const item = this._items.shift(); if (!item) { return undefined; } return item; } /** * Get the item at the front of the queue without removing it. * @returns The item at the front of the queue. */ public peek(): TItem { return this._items[0]; } /** * Remove all items from the queue. */ public clear(): void { this._items = []; } /** * Get the items in the queue. */ public get items(): TItem[] { return this._items; } /** * Get the length of the queue. */ public get length(): number { return this._items.length; } /** * Is the queue empty? */ public get isEmpty(): boolean { return this._items.length === 0; } /** * Does the queue contain items? */ public get isNotEmpty(): boolean { return this._items.length > 0; } } ================================================ FILE: src/engine/util/strings.ts ================================================ import { hexToHexString } from '@engine/util/colors'; import { FontName } from '@runejs/filestore'; import { filestore } from '@server/game/game-server'; export const startsWithVowel = (str: string): boolean => { str = str.trim().toLowerCase(); const firstChar = str.charAt(0); return firstChar === 'a' || firstChar === 'e' || firstChar === 'i' || firstChar === 'o' || firstChar === 'u'; }; function getFont(font?: number | string) { if (font && typeof font === 'number') { return filestore.fontStore.getFontById(font); } else if (font && typeof font === 'string') { return filestore.fontStore.getFontByName(FontName[font]); } else { // Default font, subject to change return filestore.fontStore.getFontByName(FontName.p12_full); } } export enum TextDecoration { Color, Decoration, } function getStylingType(tag: string) { let _tag = tag; if (_tag.charAt(0) === '/') { _tag = _tag.substring(1); } if (_tag.startsWith('col')) { return TextDecoration.Color; } else { return TextDecoration.Decoration; } } // Thank you to the Apollo team for these values. :) const charWidths = [ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 7, 14, 9, 12, 12, 4, 5, 5, 10, 8, 4, 8, 4, 7, 9, 7, 9, 8, 8, 8, 9, 7, 9, 9, 4, 5, 7, 9, 7, 9, 14, 9, 8, 8, 8, 7, 7, 9, 8, 6, 8, 8, 7, 10, 9, 9, 8, 9, 8, 8, 6, 9, 8, 10, 8, 8, 8, 6, 7, 6, 9, 10, 5, 8, 8, 7, 8, 8, 7, 8, 8, 4, 7, 7, 4, 10, 8, 8, 8, 8, 6, 8, 6, 8, 8, 9, 8, 8, 8, 6, 4, 6, 12, 3, 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 8, 11, 8, 8, 4, 8, 7, 12, 6, 7, 9, 5, 12, 5, 6, 10, 6, 6, 6, 8, 8, 4, 5, 5, 6, 7, 11, 11, 11, 9, 9, 9, 9, 9, 9, 9, 13, 8, 8, 8, 8, 8, 4, 4, 5, 4, 8, 9, 9, 9, 9, 9, 9, 8, 10, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 8, 8, 8, 13, 6, 8, 8, 8, 8, 4, 4, 5, 4, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, ]; // TODO refactor a bit export function wrapText(text: string, maxWidth: number, font?: number | string): string[] { const lines: string[] = []; const selectedFont = getFont(font); const colorQueue: string[] = []; const decorationQueue: string[] = []; const remainingText = text.split('').reverse(); let currentLine = ''; let currentWidth = 0; let currentTagIndex = -1; while (remainingText.length > 0) { const char = remainingText.pop(); let hidden = false; let rendered = true; switch (char) { case '<': hidden = true; currentTagIndex = currentLine.length + 1; break; case '>': hidden = true; const currentTag = currentLine.substring(currentTagIndex, currentLine.length); currentTagIndex = -1; const isClosing = currentTag.charAt(0) === '/'; const type = getStylingType(currentTag); if (type === TextDecoration.Decoration) { if (!isClosing) { decorationQueue.push(currentTag); } else { decorationQueue.pop(); } } else { if (!isClosing) { colorQueue.push(currentTag); } else { colorQueue.pop(); } } break; case '@': break; case '\n': hidden = true; currentWidth = maxWidth; rendered = false; break; case ' ': if (currentLine[currentLine.length - 1] === ' ' || currentWidth === 0) { hidden = true; rendered = false; } break; default: break; } if (rendered) { currentLine += char; } if (!hidden && currentTagIndex == -1 && char !== undefined) { const charWidth = selectedFont.getCharWidth(char); currentWidth += charWidth; } if (currentWidth >= maxWidth) { let lastSpace = currentLine.lastIndexOf(' '); const lastTag = currentLine.lastIndexOf('<'); if (lastTag > lastSpace && char !== '\n') { lastSpace = lastTag; const type = getStylingType(currentLine.substring(lastTag + 1)); if (type === TextDecoration.Decoration) { decorationQueue.pop(); } else { colorQueue.pop(); } } let lineToPush = currentLine; let remainder = ''; if (lastSpace != -1 && char != '\n') { lineToPush = lineToPush.substring(0, lastSpace); remainder = currentLine.substring(lastSpace); } decorationQueue .slice(0) .reverse() .map(tag => (lineToPush += ``)); colorQueue .slice(0) .reverse() .map(tag => (lineToPush += ``)); lines.push(lineToPush.trim()); currentLine = ''; decorationQueue.slice(0).map(tag => (currentLine += `<${tag}>`)); colorQueue.slice(0).map(tag => (currentLine += `<${tag}>`)); remainingText.push(...remainder.split('').reverse()); currentWidth = 0; } } if (currentLine !== '\n') { lines.push(currentLine); } // logger.info('split lines: ' + lines) return lines; } const VALID_CHARS = [ '_', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '+', '=', ':', ';', '.', '>', '<', ',', '"', '[', ']', '|', '?', '/', '`', ]; export function longToString(nameLong: bigint): string { let ac: string = ''; while (nameLong !== BigInt(0)) { const l1 = nameLong; nameLong = BigInt(nameLong) / BigInt(37); ac += VALID_CHARS[parseInt(l1.toString()) - parseInt(nameLong.toString()) * 37]; } return ac.split('').reverse().join(''); } export function stringToLong(s: string): bigint { let l: bigint = BigInt(0); for (let i = 0; i < s.length && i < 12; i++) { const c = s.charAt(i); const cc = s.charCodeAt(i); l *= BigInt(37); if (c >= 'A' && c <= 'Z') l += BigInt(1 + cc - 65); else if (c >= 'a' && c <= 'z') l += BigInt(1 + cc - 97); else if (c >= '0' && c <= '9') l += BigInt(27 + cc - 48); } while (l % BigInt(37) == BigInt(0) && l != BigInt(0)) l /= BigInt(37); return l; } export const colorText = (s: string, hexColor: number): string => `${s}`; ================================================ FILE: src/engine/util/time.ts ================================================ export const rsTime = (date: Date): number => { const days = Math.round(date.getTime() / 0x5265c00); return days - 11745; }; export const daysSinceLastLogin = (lastLogin: Date): number => { if (!lastLogin) { return -1; } return Math.floor(Math.abs(new Date().valueOf() - lastLogin.valueOf()) / (1000 * 60 * 60 * 24)); }; ================================================ FILE: src/engine/util/varbits.ts ================================================ import { filestore } from '@server/game/game-server'; const varbitMasks: number[] = []; /** * Returns the index to morph actor/object into, based on set config * @param varbitId * @param playerConfig * @return index to morph into */ export function getVarbitMorphIndex(varbitId, playerConfig) { if (varbitMasks.length === 0) { let i = 2; for (let i_7_ = 0; i_7_ < 32; i_7_++) { varbitMasks[i_7_] = -1 + i; i += i; } } const varbitDefinition = filestore.configStore.varbitStore.getVarbit(varbitId); if (!varbitDefinition) { throw new Error(`Could not find varbit definition for id ${varbitId}`); } const mostSignificantBit = varbitDefinition.mostSignificantBit; const configId = varbitDefinition.index; const leastSignificantBit = varbitDefinition.leastSignificantBit; // TODO: Unknown const i_8_ = varbitMasks[mostSignificantBit - leastSignificantBit]; const configValue = playerConfig && playerConfig[configId] ? playerConfig[configId] : 0; return (configValue >> leastSignificantBit) & i_8_; } ================================================ FILE: src/engine/world/actor/actor.ts ================================================ import type { ActionCancelType } from '@engine/action/action-pipeline'; import { ActionPipeline } from '@engine/action/action-pipeline'; import { QueueableTask } from '@engine/action/pipe/task/queueable-task'; import type { DefensiveBonuses, OffensiveBonuses, SkillBonuses } from '@engine/config/item-config'; import type { Task } from '@engine/task/task'; import { TaskScheduler } from '@engine/task/task-scheduler'; import { activeWorld } from '@engine/world'; import { directionFromIndex } from '@engine/world/direction'; import type { WorldInstance } from '@engine/world/instances'; import type { Item } from '@engine/world/items/item'; import { ItemContainer } from '@engine/world/items/item-container'; import { Position } from '@engine/world/position'; import { logger } from '@runejs/common'; import { Subject } from 'rxjs'; import { filter, take } from 'rxjs/operators'; import type { ActorMetadata } from './metadata'; import { Pathfinding } from './pathfinding'; import { Skills } from './skills'; import type { Animation, Graphic } from './update-flags'; import { UpdateFlags } from './update-flags'; import { isNpc } from './util'; import { WalkingQueue } from './walking-queue'; export type ActorType = 'player' | 'npc'; /** * Handles an entity within the game world. */ export abstract class Actor { public readonly type: ActorType; public readonly updateFlags: UpdateFlags = new UpdateFlags(); public readonly skills: Skills = new Skills(this); public readonly walkingQueue: WalkingQueue = new WalkingQueue(this); public readonly inventory: ItemContainer = new ItemContainer(28); public readonly bank: ItemContainer = new ItemContainer(376); public readonly actionPipeline = new ActionPipeline(this); /** * The map of available metadata for this actor. * * You cannot guarantee that this will be populated with data, so you should always check for the existence of the * metadata you are looking for before using it. * * @author jameskmonger */ public readonly metadata: Partial = {}; /** * @deprecated - use new action system instead */ public readonly actionsCancelled: Subject = new Subject(); public pathfinding: Pathfinding = new Pathfinding(this); public lastMovementPosition: Position; protected randomMovementInterval; protected _instance: WorldInstance | null = null; /** * Is this actor currently active? If true, the actor will have its task queue processed. * * This is true for players that are currently logged in, and NPCs that are currently in the world. */ protected active: boolean; /** * @deprecated - use new action system instead */ private _busy: boolean = false; private _position: Position; private _lastMapRegionUpdatePosition: Position; private _worldIndex: number; private _walkDirection: number; private _runDirection: number; private _faceDirection: number; private _bonuses: { offensive: OffensiveBonuses; defensive: DefensiveBonuses; skill: SkillBonuses }; private readonly scheduler = new TaskScheduler(); protected constructor(actorType: ActorType) { this.type = actorType; this._walkDirection = -1; this._runDirection = -1; this._faceDirection = 6; this.clearBonuses(); } public abstract equals(actor: Actor): boolean; /** * Instantiate a task with the Actor instance and a set of arguments. * * @param taskClass The task class to instantiate. Must be a subclass of {@link Task} * @param args The arguments to pass to the task constructor * * If the task has a stack type of `NEVER`, other tasks in the same {@link TaskStackGroup} will be cancelled. */ public enqueueTask(taskClass: new (actor: Actor) => Task, ...args: never[]): void; public enqueueTask( taskClass: new (actor: Actor, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5, arg6: T6) => Task, args: [T1, T2, T3, T4, T5, T6], ): void; public enqueueTask( taskClass: new (actor: Actor, arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => Task, args: [T1, T2, T3, T4, T5], ): void; public enqueueTask( taskClass: new (actor: Actor, arg1: T1, arg2: T2, arg3: T3, arg4: T4) => Task, args: [T1, T2, T3, T4], ): void; public enqueueTask(taskClass: new (actor: Actor, arg1: T1, arg2: T2, arg3: T3) => Task, args: [T1, T2, T3]): void; public enqueueTask(taskClass: new (actor: Actor, arg1: T1, arg2: T2) => Task, args: [T1, T2]): void; public enqueueTask(taskClass: new (actor: Actor, arg1: T1) => Task, args: [T1]): void; public enqueueTask(taskClass: new (actor: Actor, ...args: T[]) => Task, args: T[]): void { if (!this.active) { logger.warn(`Attempted to instantiate task for inactive actor`); return; } if (args) { this.enqueueBaseTask(new taskClass(this, ...args)); } else { this.enqueueBaseTask(new taskClass(this)); } } /** * Adds a task to the actor's scheduler queue. These tasks will be stopped when they become inactive. * * If the task has a stack type of `NEVER`, other tasks in the same group will be cancelled. * * @param task The task to add */ public enqueueBaseTask(task: Task): void { if (!this.active) { logger.warn(`Attempted to enqueue task for inactive actor`); return; } this.scheduler.enqueue(task); } /** * Instantly teleports the actor to the specified location. * @param newPosition The actor's new position. */ public teleport(newPosition: Position): void { this.walkingQueue.clear(); this.metadata['lastPosition'] = this.position.copy(); this.position = newPosition; this.metadata.teleporting = true; } public clearBonuses(): void { this._bonuses = { offensive: { speed: 0, stab: 0, slash: 0, crush: 0, magic: 0, ranged: 0, }, defensive: { stab: 0, slash: 0, crush: 0, magic: 0, ranged: 0, }, skill: { strength: 0, prayer: 0, }, }; } public moveBehind(target: Actor): boolean { if (this.position.level !== target.position.level) { return false; } const distance = Math.floor(this.position.distanceBetween(target.position)); if (distance > 16) { this.clearFaceActor(); return false; } let ignoreDestination = true; let desiredPosition = target.position; if (target.lastMovementPosition) { desiredPosition = target.lastMovementPosition; ignoreDestination = false; } this.pathfinding.walkTo(desiredPosition, { pathingSearchRadius: distance + 2, ignoreDestination, }); return true; } public moveTo(target: Actor): boolean { if (this.position.level !== target.position.level) { return false; } const distance = Math.floor(this.position.distanceBetween(target.position)); if (distance > 16) { this.clearFaceActor(); return false; } this.pathfinding.walkTo(target.position, { pathingSearchRadius: distance + 2, ignoreDestination: true, }); return true; } public follow(target: Actor): void { this.face(target, false, false, false); this.metadata.following = target; this.moveBehind(target); const subscription = target.walkingQueue.movementEvent.subscribe(() => { if (!this.moveBehind(target)) { // (Jameskmonger) actionsCancelled is deprecated, casting this to satisfy the typecheck for now this.actionsCancelled.next(null as unknown as ActionCancelType); } }); this.actionsCancelled .pipe( filter(type => type !== 'pathing-movement'), take(1), ) .subscribe(() => { subscription.unsubscribe(); this.face(null); delete this.metadata.following; }); } public walkTo(target: Actor): boolean; public walkTo(position: Position): boolean; public walkTo(target: Actor | Position): boolean { const desiredPosition = target instanceof Position ? target : target.position; const distance = Math.floor(this.position.distanceBetween(desiredPosition)); if (distance <= 1) { return false; } if (distance > 16) { this.clearFaceActor(); this.metadata.faceActorClearedByWalking = true; return false; } this.pathfinding.walkTo(desiredPosition, { pathingSearchRadius: distance + 2, ignoreDestination: true, }); return true; } public face( face: Position | Actor | null, clearWalkingQueue: boolean = true, autoClear: boolean = true, clearedByWalking: boolean = true, ): void { if (face === null) { this.clearFaceActor(); this.updateFlags.facePosition = null; return; } if (face instanceof Position) { this.updateFlags.facePosition = face; } else if (face instanceof Actor) { this.updateFlags.faceActor = face; this.metadata.faceActor = face; this.metadata.faceActorClearedByWalking = clearedByWalking; if (autoClear) { setTimeout(() => { this.clearFaceActor(); }, 20000); } } if (clearWalkingQueue) { this.walkingQueue.clear(); this.walkingQueue.valid = false; } } public clearFaceActor(): void { if (this.metadata.faceActor) { this.updateFlags.faceActor = 'CLEAR'; this.metadata.faceActor = undefined; } } public playAnimation(animation: number | Animation | null): void { if (typeof animation === 'number') { animation = { id: animation, delay: 0 }; } this.updateFlags.animation = animation; } public stopAnimation(): void { this.updateFlags.animation = { id: -1, delay: 0 }; } public playGraphics(graphics: number | Graphic): void { if (typeof graphics === 'number') { graphics = { id: graphics, delay: 0, height: 120 }; } this.updateFlags.graphics = graphics; } public stopGraphics(): void { this.updateFlags.graphics = { id: -1, delay: 0, height: 120 }; } public removeItem(slot: number): void { this.inventory.remove(slot); } public removeBankItem(slot: number): void { this.bank.remove(slot); } public giveItem(item: number | Item): boolean { return this.inventory.add(item) !== null; } public giveBankItem(item: number | Item): boolean { return this.bank.add(item) !== null; } public hasItemInInventory(item: number | Item): boolean { return this.inventory.has(item); } public hasItemInBank(item: number | Item): boolean { return this.bank.has(item); } public hasItemOnPerson(item: number | Item): boolean { return this.hasItemInInventory(item); } public canMove(): boolean { // In the future, there will undoubtedly be various reasons for the // actor to not be able to move, but for now we are returning true. return true; } public initiateRandomMovement(): void { this.enqueueBaseTask( new QueueableTask( [], this, () => { this.moveSomewhere(); return { callbackResult: true, shouldContinueLooping: true, }; }, null, null, ), ); } public moveSomewhere(): void { if (!this.canMove()) { return; } if (isNpc(this)) { const nearbyPlayers = activeWorld.findNearbyPlayers(this.position, 24, this.instance.instanceId); if (nearbyPlayers.length === 0) { // No need for this actor to move if there are no players nearby to witness it, save some memory. :) return; } } const movementChance = Math.floor(Math.random() * 10); if (movementChance < 7) { return; } let px = this.position.x; let py = this.position.y; let movementAllowed = false; while (!movementAllowed) { px = this.position.x; py = this.position.y; const moveXChance = Math.floor(Math.random() * 10); if (moveXChance > 6) { const moveXAmount = Math.floor(Math.random() * 5); const moveXMod = Math.floor(Math.random() * 2); if (moveXMod === 0) { px -= moveXAmount; } else { px += moveXAmount; } } const moveYChance = Math.floor(Math.random() * 10); if (moveYChance > 6) { const moveYAmount = Math.floor(Math.random() * 5); const moveYMod = Math.floor(Math.random() * 2); if (moveYMod === 0) { py -= moveYAmount; } else { py += moveYAmount; } } let valid = true; if (!this.withinBounds(px, py)) { valid = false; } movementAllowed = valid; } if (px !== this.position.x || py !== this.position.y) { this.walkingQueue.clear(); this.walkingQueue.valid = true; this.walkingQueue.add(px, py); } } public forceMovement(direction: number, steps: number): void { if (!this.canMove()) { return; } let px = this.position.x; let py = this.position.y; let movementAllowed = false; while (!movementAllowed) { px = this.position.x; py = this.position.y; const movementDirection = directionFromIndex(direction); if (!movementDirection) { return; } let valid = true; for (let step = 0; step < steps; step++) { px += movementDirection.deltaX; py += movementDirection.deltaY; if (!this.withinBounds(px, py)) { valid = false; } } movementAllowed = valid; } if (px !== this.position.x || py !== this.position.y) { this.walkingQueue.clear(); this.walkingQueue.valid = true; this.walkingQueue.add(px, py); } } public withinBounds(x: number, y: number): boolean { return true; } /** * Initialise the actor. */ protected init() { this.active = true; } /** * Destroy this actor. * * This will stop the processing of its action queue. */ protected destroy() { this.active = false; this.scheduler.clear(); } protected tick() { this.scheduler.tick(); } public get position(): Position { return this._position; } public set position(value: Position) { if (!this._position) { this._lastMapRegionUpdatePosition = value; } this._position = value; } public get lastMapRegionUpdatePosition(): Position { return this._lastMapRegionUpdatePosition; } public set lastMapRegionUpdatePosition(value: Position) { this._lastMapRegionUpdatePosition = value; } public get worldIndex(): number { return this._worldIndex; } public set worldIndex(value: number) { this._worldIndex = value; } public get walkDirection(): number { return this._walkDirection; } public set walkDirection(value: number) { this._walkDirection = value; } public get runDirection(): number { return this._runDirection; } public set runDirection(value: number) { this._runDirection = value; } public get faceDirection(): number { return this._faceDirection; } public set faceDirection(value: number) { this._faceDirection = value; } public get busy(): boolean { return this._busy; } public set busy(value: boolean) { this._busy = value; } public get instance(): WorldInstance { return this._instance || activeWorld.globalInstance; } public set instance(value: WorldInstance | null) { this._instance = value; } public get bonuses(): { offensive: OffensiveBonuses; defensive: DefensiveBonuses; skill: SkillBonuses } { return this._bonuses; } } ================================================ FILE: src/engine/world/actor/combat.ts ================================================ import type { SkillName } from '@engine/world/actor/skills'; import * as combatStylesImport from '../../../../data/config/combat-styles.json'; export interface CombatStyles { [key: string]: CombatStyle[]; } export interface CombatStyle { type: 'slash' | 'stab' | 'crush'; exp: SkillName | SkillName[]; anim: string | string[]; button_id: number; } export const combatStyles: CombatStyles = combatStylesImport as CombatStyles; ================================================ FILE: src/engine/world/actor/dialogue.ts ================================================ import { findNpc } from '@engine/config/config-handler'; import { wrapText } from '@engine/util/strings'; import type { Npc } from '@engine/world/actor/npc'; import { Player } from '@engine/world/actor/player/player'; import { logger } from '@runejs/common'; import type { ParentWidget, TextWidget } from '@runejs/filestore'; import { filestore } from '@server/game/game-server'; import _ from 'lodash'; export enum Emote { POMPOUS = 'POMPOUS', UNKOWN_CREATURE = 'UNKOWN_CREATURE', VERY_SAD = 'VERY_SAD', HAPPY = 'HAPPY', SHOCKED = 'SHOCKED', WONDERING = 'WONDERING', GOBLIN = 'GOBLIN', TREE = 'TREE', GENERIC = 'GENERIC', SKEPTICAL = 'SKEPTICAL', WORRIED = 'WORRIED', DROWZY = 'DROWZY', LAUGH = 'LAUGH', SAD = 'SAD', ANGRY = 'ANGRY', EASTER_BUNNY = 'EASTER_BUNNY', BLANK_STARE = 'BLANK_STARE', SINGLE_WORD = 'SINGLE_WORD', EVIL_STARE = 'EVIL_STARE', LAUGH_EVIL = 'LAUGH_EVIL', } // A big thanks to Dust R I P for all these emotes! enum EmoteAnimation { POMPOUS_1LINE = 554, POMPOUS_2LINE = 555, POMPOUS_3LINE = 556, POMPOUS_4LINE = 557, UNKOWN_CREATURE_1LINE = 558, UNKOWN_CREATURE_2LINE = 559, UNKOWN_CREATURE_3LINE = 560, UNKOWN_CREATURE_4LINE = 561, VERY_SAD1LINE = 562, VERY_SAD2LINE = 563, VERY_SAD3LINE = 564, VERY_SAD4LINE = 565, SINGLE_WORD = 566, HAPPY_1LINE = 567, HAPPY_2LINE = 568, HAPPY_3LINE = 569, HAPPY_4LINE = 570, SHOCKED_1LINE = 571, SHOCKED_2LINE = 572, SHOCKED_3LINE = 573, SHOCKED_4LINE = 574, WONDERING_1LINE = 575, WONDERING_2LINE = 576, WONDERING_3LINE = 577, WONDERING_4LINE = 578, BLANK_STARE = 579, GOBLIN_1LINE = 580, GOBLIN_2LINE = 581, GOBLIN_3LINE = 582, GOBLIN_4LINE = 583, TREE_1LINE = 584, TREE_2LINE = 585, TREE_3LINE = 586, TREE_4LINE = 587, GENERIC_1LINE = 588, GENERIC_2LINE = 589, GENERIC_3LINE = 590, GENERIC_4LINE = 591, SKEPTICAL_1LINE = 592, SKEPTICAL_2LINE = 593, SKEPTICAL_3LINE = 594, SKEPTICAL_4LINE = 595, WORRIED_1LINE = 596, WORRIED_2LINE = 597, WORRIED_3LINE = 598, WORRIED_4LINE = 599, DROWZY_1LINE = 600, DROWZY_2LINE = 601, DROWZY_3LINE = 602, DROWZY_4LINE = 603, EVIL_STARE = 604, LAUGH_1LINE = 605, LAUGH_2LINE = 606, LAUGH_3LINE = 607, LAUGH_4LINE = 608, LAUGH_EVIL = 609, SAD_1LINE = 610, SAD_2LINE = 611, SAD_3LINE = 612, SAD_4LINE = 613, ANGRY_1LINE = 614, ANGRY_2LINE = 615, ANGRY_3LINE = 616, ANGRY_4LINE = 617, EASTER_BUNNY_1LINE = 1824, EASTER_BUNNY_2LINE = 1825, EASTER_BUNNY_3LINE = 1826, EASTER_BUNNY_4LINE = 1827, } const nonLineEmotes = [Emote.BLANK_STARE, Emote.SINGLE_WORD, Emote.EVIL_STARE, Emote.LAUGH_EVIL]; const playerWidgetIds = [64, 65, 66, 67]; const npcWidgetIds = [241, 242, 243, 244]; const optionWidgetIds = [228, 230, 232, 234, 235]; const continuableTextWidgetIds = [210, 211, 212, 213, 214]; const textWidgetIds = [215, 216, 217, 218, 219]; const titledTextWidgetId = 372; /** * Wraps dialogue text into multiple lines. * @param text - The text to wrap. * @param type - 'ACTOR' if the widget has a chat-head or an item sprite on the left, 'TEXT' if the dialogue is text only */ function wrapDialogueText(text: string, type: 'ACTOR' | 'TEXT'): string[] { let widget: TextWidget; let width = 0; switch (type) { case 'ACTOR': widget = (filestore.widgetStore.decodeWidget(playerWidgetIds[0]) as ParentWidget).children[2] as TextWidget; width = widget.width; break; case 'TEXT': widget = filestore.widgetStore.decodeWidget(textWidgetIds[0]) as TextWidget; width = widget.width; break; default: throw new Error(`Unhandled widget type: ${type}`); } return wrapText(text, width, widget.fontId); } function parseDialogueFunctionArgs(func: Function): string[] | null { const str = func.toString(); if (!str) { return null; } const argEndIndex = str.indexOf('=>'); if (argEndIndex === -1) { return null; } const arg = str .substring(0, argEndIndex) .replace(/[\\(\\) ]/g, '') .trim(); if (!arg || arg.length === 0) { return null; } return arg.split(','); } export type DialogueTree = (Function | DialogueFunction | GoToAction)[]; export interface AdditionalOptions { closeOnWalk?: boolean; permanent?: boolean; title?: string; } interface NpcParticipant { npc: Npc | number | string; key: string; } class DialogueFunction { constructor( public type: string, public execute: Function, ) {} } export const execute = (execute: Function): DialogueFunction => new DialogueFunction('execute', execute); export const goto = (to: string | Function): GoToAction => new GoToAction(to); type ParsedDialogueTree = (DialogueAction | DialogueFunction | string)[]; interface DialogueAction { tag: string; type: string; } class GoToAction implements DialogueAction { public tag: string; public type = 'GOTO'; constructor(public to: string | Function) {} } interface ActorDialogueAction extends DialogueAction { animation: number; lines: string[]; } interface NpcDialogueAction extends ActorDialogueAction { npcId: number; } interface PlayerDialogueAction extends ActorDialogueAction { player: Player; } interface TextDialogueAction extends DialogueAction { lines: string[]; canContinue: boolean; } interface TitledTextDialogueAction extends DialogueAction { title: string; lines: string[]; } interface OptionsDialogueAction extends DialogueAction { options: { [key: string]: ParsedDialogueTree }; } interface SubDialogueTreeAction extends DialogueAction { subTree: DialogueTree; npcParticipants?: NpcParticipant[]; } function parseDialogueTree(player: Player, npcParticipants: NpcParticipant[], dialogueTree: DialogueTree): ParsedDialogueTree { const parsedDialogueTree: ParsedDialogueTree = []; let carryoverDialogue: string[] = []; for (let i = 0; i < dialogueTree.length; i++) { const dialogueAction = dialogueTree[i]; if (dialogueAction instanceof DialogueFunction) { // Code execution dialogue. parsedDialogueTree.push(dialogueAction as DialogueFunction); continue; } if (dialogueAction instanceof GoToAction) { parsedDialogueTree.push(dialogueAction); continue; } let args = parseDialogueFunctionArgs(dialogueAction); if (args === null) { args = ['()']; } const dialogueType = args[0]; let tag: string | null = null; if (args.length === 2 && typeof args[1] === 'string') { if (player.metadata.dialogueIndices) { player.metadata.dialogueIndices[args[1]] = i; } else { logger.warn('Player metadata does not contain dialogueIndices'); } tag = args[1]; } if (!dialogueType) { logger.error('No arguments passed to dialogue function.'); continue; } let isOptions = false; if (dialogueType === 'options' || dialogueType === '()') { // Options or custom function dialogue. let result = dialogueAction(); if (dialogueType === '()') { const funcResult = result(); if (!Array.isArray(funcResult) || funcResult.length === 0) { logger.error('Invalid dialogue function response type.'); continue; } if (typeof funcResult[0] === 'function') { // given function returned a dialogue tree parsedDialogueTree.push(...parseDialogueTree(player, npcParticipants, funcResult)); } else { // given function returned an option list result = funcResult; isOptions = true; } } else { isOptions = true; } if (isOptions) { const options = (result as any[]).filter((option, index) => index % 2 === 0); const trees = (result as any[]).filter((option, index) => index % 2 !== 0); const optionsDialogueAction: OptionsDialogueAction = { options: {}, tag: tag || '', type: 'OPTIONS', }; if (!tag) { logger.warn('No tag provided for options dialogue.'); } for (let j = 0; j < options.length; j++) { const option = options[j]; const tree = parseDialogueTree(player, npcParticipants, trees[j]); optionsDialogueAction.options[option] = tree; } parsedDialogueTree.push(optionsDialogueAction); } } else if (dialogueType === 'text') { // Text-only dialogue (with the option to click continue). const text: string = dialogueAction(); const lines = wrapDialogueText(text, 'TEXT'); parsedDialogueTree.push({ lines, tag, type: 'TEXT', canContinue: true } as TextDialogueAction); } else if (dialogueType === 'overlay') { // Text-only dialogue (no option to continue). const text: string = dialogueAction(); const lines = wrapDialogueText(text, 'TEXT'); parsedDialogueTree.push({ lines, tag, type: 'TEXT', canContinue: false } as TextDialogueAction); } else if (dialogueType === 'titled') { // Text-only dialogue (no option to continue). const [title, text] = dialogueAction(); const lines = wrapDialogueText(text, 'TEXT'); while (lines.length < 4) { lines.push(''); } parsedDialogueTree.push({ lines, title, tag, type: 'TITLED' } as TitledTextDialogueAction); } else if (dialogueType === 'subtree') { // Dialogue sub-tree. const subTree: DialogueTree = dialogueAction(); parsedDialogueTree.push({ tag, type: 'SUBTREE', subTree, npcParticipants } as SubDialogueTreeAction); } else { // Player or Npc dialogue. let dialogueDetails: [Emote, string]; let npc: Npc | number | string = -1; if (dialogueType !== 'player') { const participant = npcParticipants.find(p => p.key === dialogueType) as NpcParticipant; if (!participant || !participant.npc) { logger.error('No matching npc found for npc dialogue action.'); continue; } npc = participant.npc; if (typeof npc !== 'number') { if (typeof npc === 'string') { npc = findNpc(npc).gameId; } else { npc = npc.id; } } dialogueDetails = dialogueAction(npc); if (npc === -1) { throw new Error('No npc found for dialogue action.'); } } else { dialogueDetails = dialogueAction(player); } const emote = dialogueDetails[0] as Emote; const text = (carryoverDialogue.join(' ') + dialogueDetails[1]) as string; carryoverDialogue = []; let lines = wrapDialogueText(text, 'ACTOR'); // logger.info('length = ' + lines.length + ' - lines equals this: ' + lines); const animation = nonLineEmotes.indexOf(emote) !== -1 ? EmoteAnimation[emote] : EmoteAnimation[`${emote}_${lines.length}LINE`]; if (!tag) { logger.warn('No tag provided for npc dialogue.'); } if (dialogueType !== 'player') { if (lines.length > 4) { while (lines.length > 4) { const copyOfLines = lines.slice(0, lines.length); lines = lines.slice(0, 4); const npcDialogueAction: NpcDialogueAction = { npcId: npc as number, animation, lines, tag: tag || '', type: 'NPC', }; parsedDialogueTree.push(npcDialogueAction); lines = copyOfLines.slice(0, copyOfLines.length); carryoverDialogue = lines.slice(4, lines.length) as string[]; lines = carryoverDialogue; if (i === dialogueTree.length - 1 && lines.length <= 4) { const npcDialogueAction: NpcDialogueAction = { npcId: npc as number, animation, lines, tag: tag || '', type: 'NPC', }; parsedDialogueTree.push(npcDialogueAction); } } } else { const npcDialogueAction: NpcDialogueAction = { npcId: npc as number, animation, lines, tag: tag || '', type: 'NPC', }; parsedDialogueTree.push(npcDialogueAction); } } else { const playerDialogueAction: PlayerDialogueAction = { player, animation, lines, tag: tag || '', type: 'PLAYER', }; parsedDialogueTree.push(playerDialogueAction); } } } return parsedDialogueTree; } async function runDialogueAction( player: Player, dialogueAction: string | DialogueFunction | DialogueAction, tag?: string | undefined | false, additionalOptions?: AdditionalOptions, ): Promise { if (dialogueAction instanceof DialogueFunction && !tag) { // Code execution dialogue. dialogueAction.execute(); return tag; } dialogueAction = dialogueAction as DialogueAction; if (dialogueAction.type === 'GOTO' && !tag) { // Goto dialogue. const goToAction = dialogueAction as GoToAction; if (typeof goToAction.to === 'function') { const goto: string = goToAction.to(); await runParsedDialogue(player, player.metadata.dialogueTree, goto, additionalOptions); } else { await runParsedDialogue(player, player.metadata.dialogueTree, goToAction.to, additionalOptions); } return tag; } let widgetId: number = -1; let isOptions = false; if (dialogueAction.type === 'OPTIONS') { // Option dialogue. const optionsAction = dialogueAction as OptionsDialogueAction; isOptions = true; const options = Object.keys(optionsAction.options); const trees = options.map(option => optionsAction.options[option]); if (tag === undefined || dialogueAction.tag === tag) { tag = undefined; widgetId = optionWidgetIds[options.length - 2]; for (let i = 0; i < options.length; i++) { player.outgoingPackets.updateWidgetString(widgetId, 1 + i, options[i]); } } else if (tag !== undefined) { for (let i = 0; i < options.length; i++) { const tree = trees[i]; const didRun = await runParsedDialogue(player, tree, tag, additionalOptions); if (didRun) { return; } } } } else if (dialogueAction.type === 'TEXT') { // Text-only dialogue. if (tag === undefined || dialogueAction.tag === tag) { tag = undefined; const textDialogueAction = dialogueAction as TextDialogueAction; const lines = textDialogueAction.lines; if (lines.length > 5) { throw new Error( `Too many lines for text dialogue! Dialogue has ${lines.length} lines but ` + `the maximum is 5: ${JSON.stringify(lines)}`, ); } widgetId = (textDialogueAction.canContinue ? continuableTextWidgetIds : textWidgetIds)[lines.length - 1]; for (let i = 0; i < lines.length; i++) { player.outgoingPackets.updateWidgetString(widgetId, i, lines[i]); } } } else if (dialogueAction.type === 'TITLED') { // Text-only dialogue. if (tag === undefined || dialogueAction.tag === tag) { tag = undefined; const titledDialogueAction = dialogueAction as TitledTextDialogueAction; const { title, lines } = titledDialogueAction; if (lines.length > 4) { throw new Error( `Too many lines for titled dialogue! Dialogue has ${lines.length} lines but ` + `the maximum is 4: ${JSON.stringify(lines)}`, ); } widgetId = titledTextWidgetId; player.outgoingPackets.updateWidgetString(widgetId, 0, title); for (let i = 0; i < lines.length; i++) { player.outgoingPackets.updateWidgetString(widgetId, i + 1, lines[i]); } } } else if (dialogueAction.type === 'SUBTREE') { // Dialogue sub-tree. const action = dialogueAction as SubDialogueTreeAction; if (!action.npcParticipants) { // (Jameskmonger) I added this log because the TypeScript types allow for this to be undefined, but // parseDialogueTree requires it. I'm not sure if it can be undefined in practice. logger.warn('No NPC participants for dialogue action'); } // (Jameskmonger) default value added here const npcParticipants = action.npcParticipants || []; if (dialogueAction.tag === tag) { const originalIndices = _.cloneDeep(player.metadata.dialogueIndices || {}); const originalTree = _.cloneDeep(player.metadata.dialogueTree || []); player.metadata.dialogueIndices = {}; const parsedSubTree = parseDialogueTree(player, npcParticipants, action.subTree); player.metadata.dialogueTree = parsedSubTree; await runParsedDialogue(player, parsedSubTree, undefined, additionalOptions); player.metadata.dialogueIndices = originalIndices; player.metadata.dialogueTree = originalTree; } else if (tag && dialogueAction.tag !== tag) { const originalIndices = _.cloneDeep(player.metadata.dialogueIndices || {}); const originalTree = _.cloneDeep(player.metadata.dialogueTree || []); player.metadata.dialogueIndices = {}; const parsedSubTree = parseDialogueTree(player, npcParticipants, action.subTree); player.metadata.dialogueTree = parsedSubTree; await runParsedDialogue(player, parsedSubTree, tag, additionalOptions); player.metadata.dialogueIndices = originalIndices; player.metadata.dialogueTree = originalTree; } } else { // Player or Npc dialogue. if (tag === undefined || dialogueAction.tag === tag) { tag = undefined; let npcId: number = -1; if (dialogueAction.type === 'NPC') { npcId = (dialogueAction as NpcDialogueAction).npcId; if (npcId === -1) { throw new Error('No npc found for dialogue action.'); } } const actorDialogueAction = dialogueAction as ActorDialogueAction; const lines = actorDialogueAction.lines; if (lines.length > 4) { throw new Error( `Too many lines for actor dialogue! Dialogue has ${lines.length} lines but ` + `the maximum is 4: ${JSON.stringify(lines)}`, ); } const animation = actorDialogueAction.animation; if (dialogueAction.type === 'NPC') { widgetId = npcWidgetIds[lines.length - 1]; player.outgoingPackets.setWidgetNpcHead(widgetId, 0, npcId); const npcDetails = filestore.configStore.npcStore.getNpc(npcId); if (npcDetails && npcDetails.name) { player.outgoingPackets.updateWidgetString(widgetId, 1, npcDetails.name); } } else { widgetId = playerWidgetIds[lines.length - 1]; player.outgoingPackets.setWidgetPlayerHead(widgetId, 0); player.outgoingPackets.updateWidgetString(widgetId, 1, player.username); } player.outgoingPackets.playWidgetAnimation(widgetId, 0, animation); for (let i = 0; i < lines.length; i++) { player.outgoingPackets.updateWidgetString(widgetId, 2 + i, lines[i]); } } } if (tag === undefined && widgetId !== -1) { const permanent = additionalOptions?.permanent || false; if (permanent) { player.interfaceState.openChatOverlayWidget(widgetId); } else { player.interfaceState.openWidget(widgetId, { slot: 'chatbox', multi: false, }); const widgetClosedEvent = await player.interfaceState.widgetClosed('chatbox'); if (widgetClosedEvent.data !== undefined) { if (isOptions && typeof widgetClosedEvent.data === 'number') { const optionsAction = dialogueAction as OptionsDialogueAction; const options = Object.keys(optionsAction.options); const trees = options.map(option => optionsAction.options[option]); const tree: ParsedDialogueTree = trees[widgetClosedEvent.data - 1]; if (tree && tree.length !== 0) { await runParsedDialogue(player, tree, tag, additionalOptions); } } } else { return false; } } } return tag; } async function runParsedDialogue( player: Player, dialogueTree: ParsedDialogueTree, tag?: string | undefined | false, additionalOptions?: AdditionalOptions, ): Promise { for (let i = 0; i < dialogueTree.length; i++) { tag = await runDialogueAction(player, dialogueTree[i], tag, additionalOptions); if (tag === false) { break; } } return tag === undefined; } export async function dialogue( participants: (Player | NpcParticipant)[], dialogueTree: DialogueTree, additionalOptions?: AdditionalOptions, ): Promise { const player: Player | undefined = participants.find(p => p instanceof Player); if (!player) { throw new Error('Player instance not provided to dialogue action.'); } let npcParticipants = participants.filter(p => !(p instanceof Player)) as NpcParticipant[]; if (!npcParticipants) { npcParticipants = []; } player.metadata.dialogueIndices = {}; const parsedDialogueTree = parseDialogueTree(player, npcParticipants, dialogueTree); player.metadata.dialogueTree = parsedDialogueTree; try { await runParsedDialogue(player, parsedDialogueTree, undefined, additionalOptions); player.interfaceState.closeAllSlots(); return true; } catch (error) { player.interfaceState.closeAllSlots(); logger.warn(error); return false; } } const itemSelectionDialogueAmounts = [1, 5, 'X', 'All']; const itemSelectionDialogues = { // 303-306 - what would you like to make? 303: { items: [2, 3], text: [7, 11], options: [ [7, 6, 5, 4], [11, 10, 9, 8], ], }, 304: { items: [2, 3, 4], text: [8, 12, 16], options: [ [8, 7, 6, 5], [12, 11, 10, 9], [16, 15, 14, 13], ], }, 305: { items: [2, 3, 4, 5], text: [9, 13, 17, 21], options: [ [9, 8, 7, 6], [13, 12, 11, 10], [17, 16, 15, 14], [21, 20, 19, 18], ], }, 306: { items: [2, 3, 4, 5, 6], text: [10, 14, 18, 22, 26], options: [ [10, 9, 8, 7], [14, 13, 12, 11], [18, 17, 16, 15], [22, 21, 20, 19], [26, 25, 24, 23], ], }, 307: { // 307 - how many would you like to cook? items: [2], text: [6], options: [[6, 5, 4, 3]], }, 309: { // 309 - how many would you like to make? items: [2], text: [6], options: [[6, 5, 4, 3]], }, }; export interface SelectableItem { itemId: number; itemName: string; offset?: number; zoom?: number; } export interface ItemSelection { itemId: number; amount: number; } export async function itemSelectionDialogue(player: Player, type: 'COOKING' | 'MAKING', items: SelectableItem[]): Promise { let widgetId = 307; if (type === 'MAKING') { if (items.length === 1) { widgetId = 309; } else { if (items.length > 5) { throw new Error(`Too many items provided to the item selection action!`); } widgetId = 301 + items.length; } } const childIds = itemSelectionDialogues[widgetId].items; childIds.forEach((childId, index) => { const itemInfo = items[index]; if (itemInfo.offset === undefined) { itemInfo.offset = -12; } if (itemInfo.zoom === undefined) { itemInfo.zoom = 180; } player.outgoingPackets.setItemOnWidget(widgetId, childId, itemInfo.itemId, itemInfo.zoom); player.outgoingPackets.moveWidgetChild(widgetId, childId, 0, itemInfo.offset); player.outgoingPackets.updateWidgetString( widgetId, itemSelectionDialogues[widgetId].text[index], '\\n\\n\\n\\n' + itemInfo.itemName, ); }); return new Promise((resolve, reject) => { player.interfaceState.openWidget(widgetId, { slot: 'chatbox', multi: true, }); let actionsSub = player.actionsCancelled.subscribe(() => { actionsSub.unsubscribe(); reject('Pending Actions Cancelled'); }); const interactionSub = player.dialogueInteractionEvent.subscribe(childId => { if (!player.interfaceState.widgetOpen('chatbox', widgetId)) { interactionSub.unsubscribe(); actionsSub.unsubscribe(); reject('Active Widget Mismatch'); return; } const options = itemSelectionDialogues[widgetId].options; const choiceIndex = options.findIndex(arr => arr.indexOf(childId) !== -1); if (choiceIndex === -1) { interactionSub.unsubscribe(); actionsSub.unsubscribe(); reject('Choice Index Not Found'); return; } const optionIndex = options[choiceIndex].indexOf(childId); if (optionIndex === -1) { interactionSub.unsubscribe(); actionsSub.unsubscribe(); reject('Option Index Not Found'); return; } const itemId = items[choiceIndex].itemId; let amount = itemSelectionDialogueAmounts[optionIndex]; if (amount === 'X') { actionsSub.unsubscribe(); player.outgoingPackets.showNumberInputDialogue(); actionsSub = player.actionsCancelled.subscribe(() => { actionsSub.unsubscribe(); reject('Pending Actions Cancelled'); }); const inputSub = player.numericInputEvent.subscribe(input => { inputSub.unsubscribe(); actionsSub.unsubscribe(); interactionSub.unsubscribe(); if (input < 1 || input > 2147483647) { player.interfaceState.closeWidget('chatbox'); reject('Invalid User Amount Input'); } else { player.interfaceState.closeWidget('chatbox'); resolve({ itemId, amount: input } as ItemSelection); } }); } else { if (amount === 'All') { amount = player.inventory.findAll(itemId).length; } actionsSub.unsubscribe(); interactionSub.unsubscribe(); player.interfaceState.closeWidget('chatbox'); resolve({ itemId, amount } as ItemSelection); } }); }); } ================================================ FILE: src/engine/world/actor/magic.ts ================================================ export interface Magic { Name: string; ButtonID: number; CoolDown: number; BaseDamage: number; EffectID: number; DamageCalculation(): number; } export abstract class Magic {} ================================================ FILE: src/engine/world/actor/metadata.ts ================================================ import type { ConstructedRegion } from '@engine/world/map/region'; import type { Position } from '../position'; import type { Actor } from './actor'; /** * The definition of the metadata available on an {@link Actor}. * * You cannot guarantee that all of these properties will be present on an actor, * so you should always check for their existence before using them. * * @author jameskmonger */ export type ActorMetadata = { /** * The custom constructed map region for this actor. * * TODO (jameskmonger) Should this live on Actor rather than on {@link Player}? I don't think NPCs can have a custom map. */ customMap: ConstructedRegion; /** * The actor currently being followed by this actor. */ following: Actor; /** * The actor which the local actor is facing towards. */ faceActor: Actor; /** * Whether a walk action has cleared the actor which the local actor is facing towards. * * TODO (jameskmonger) does this belong on this metadata? */ faceActorClearedByWalking: boolean; /** * The actor's last position before teleporting. */ lastPosition: Position; /** * Set to true if the actor is currently teleporting. */ teleporting: boolean; }; ================================================ FILE: src/engine/world/actor/npc.ts ================================================ import EventEmitter from 'events'; import { findItem, findNpc } from '@engine/config/config-handler'; import type { NpcCombatAnimations, NpcDetails } from '@engine/config/npc-config'; import type { NpcSpawn } from '@engine/config/npc-spawn-config'; import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; import { isPlayer } from '@engine/world/actor/util'; import { animationIds } from '@engine/world/config/animation-ids'; import { soundIds } from '@engine/world/config/sound-ids'; import { directionData } from '@engine/world/direction'; import type { WorldInstance } from '@engine/world/instances'; import type { Position } from '@engine/world/position'; import type { QuadtreeKey } from '@engine/world/world'; import { logger } from '@runejs/common'; import { filestore } from '@server/game/game-server'; import { v4 } from 'uuid'; import { Actor } from './actor'; import type { SkillName } from './skills'; /** * Represents a non-player character within the game world. */ export class Npc extends Actor { public readonly uuid: string; public readonly options: string[]; public readonly initialPosition: Position; public readonly key: string; public readonly varbitId: number = -1; public readonly settingId: number = -1; public readonly childrenIds?: number[]; public parent?: Npc; public id: number; public animations: NpcCombatAnimations & { walk?: number; turnAround?: number; turnLeft?: number; turnRight?: number; stand?: number; }; //ToDo: this should either be calculated by the level or from a config public experienceValue: number = 10; public npcEvents: EventEmitter = new EventEmitter(); private _name: string; private _combatLevel: number; private _movementRadius: number = 0; private quadtreeKey: QuadtreeKey | null = null; private _exists: boolean = true; private npcSpawn: NpcSpawn; private _initialized: boolean = false; public constructor(npcDetails: NpcDetails | number, npcSpawn: NpcSpawn, instance: WorldInstance | null = null) { super('npc'); this.key = npcSpawn.npcKey; this.uuid = v4(); this.position = npcSpawn.spawnPosition.clone(); this.initialPosition = this.position.clone(); this.npcSpawn = npcSpawn; if (instance) { this.instance = instance; } if (npcSpawn.movementRadius) { this._movementRadius = npcSpawn.movementRadius; } if (npcSpawn.faceDirection) { this.faceDirection = directionData[npcSpawn.faceDirection].index; } if (typeof npcDetails === 'number') { this.id = npcDetails; } else { this.id = npcDetails.gameId; this._combatLevel = npcDetails.combatLevel; this.animations = npcDetails.combatAnimations || {}; this.options = npcDetails.options || []; if (npcDetails.skills) { const skillNames = Object.keys(npcDetails.skills); skillNames.forEach(skillName => this.skills.setLevel(skillName as SkillName, npcDetails.skills?.[skillName] ?? 1)); } } const cacheDetails = filestore.configStore.npcStore.getNpc(this.id); if (cacheDetails) { // NPC not registered on the server, but exists in the game cache - use that for our info and assume it's // Not a combatant NPC since we have no useful combat information for it. this._name = cacheDetails.name || ''; this._combatLevel = cacheDetails.combatLevel; this.options = cacheDetails.options || []; this.varbitId = cacheDetails.varbitId; this.settingId = cacheDetails.settingId; this.childrenIds = cacheDetails.childrenIds; this.animations = { walk: cacheDetails.animations?.walk || undefined, turnAround: cacheDetails.animations?.turnAround || undefined, turnLeft: cacheDetails.animations?.turnLeft || undefined, turnRight: cacheDetails.animations?.turnRight || undefined, stand: cacheDetails.animations?.stand || undefined, }; } else { this._name = 'Unknown'; } this.npcEvents.on('death', this.processDeath); } public async init(): Promise { super.init(); activeWorld.chunkManager.getChunkForWorldPosition(this.position).addNpc(this); if (this.movementRadius > 0) { this.initiateRandomMovement(); } await this.actionPipeline.call('npc_init', { npc: this }); this._initialized = true; } //This is useful so that we can tie into things like "spell casts" or events, or traps, etc to finish quests or whatever public async processDeath(assailant: Actor, defender: Actor): Promise { return new Promise(resolve => { const deathPosition = defender.position; let deathAnim: number = animationIds.death; deathAnim = findNpc((defender as Npc).id).combatAnimations?.death || animationIds.death; defender.playAnimation(deathAnim); activeWorld.playLocationSound(deathPosition, defender.instance.instanceId, soundIds.npc.human.maleDeath, 5); const npcDetails = findNpc((defender as Npc).id); if (!npcDetails.dropTable) { return; } if (isPlayer(assailant)) { const itemDrops = calculateNpcDrops(assailant, npcDetails); itemDrops.forEach(drop => { const droppedItem = findItem(drop.itemKey); if (!droppedItem) { logger.error(`Unable to find item with key: ${drop.itemKey}`); return; } if (!drop.amount) { logger.error(`Unable to drop item with key: ${drop.itemKey} - no amount specified`); return; } activeWorld.globalInstance.spawnWorldItem({ itemId: droppedItem.gameId, amount: drop.amount }, deathPosition, { owner: assailant, expires: 300, }); }); } }); } public withinBounds(x: number, y: number): boolean { return !( x > this.initialPosition.x + this.movementRadius || x < this.initialPosition.x - this.movementRadius || y > this.initialPosition.y + this.movementRadius || y < this.initialPosition.y - this.movementRadius ); } public kill(respawn: boolean = true): void { this.destroy(); activeWorld.chunkManager.getChunkForWorldPosition(this.position).removeNpc(this); clearInterval(this.randomMovementInterval); activeWorld.deregisterNpc(this); if (respawn) { const npcDetails = findNpc(this.id); activeWorld.scheduleNpcRespawn(new Npc(npcDetails, this.npcSpawn)); } } public async tick(): Promise { super.tick(); return new Promise(resolve => { this.walkingQueue.process(); resolve(); }); } public async reset(): Promise { return new Promise(resolve => { this.updateFlags.reset(); resolve(); }); } /** * Forces the Npc to speak the given message to the open world. * @param message The message for the Npc to say. */ public say(message: string): void { this.updateFlags.addChatMessage({ message }); } /** * Whether or not the Npc can currently move. */ public canMove(): boolean { if (this.metadata.following) { return false; } return this.updateFlags.faceActor === null && this.updateFlags.animation === null; } /** * Plays a sound at the Npc's location for all nearby players. * @param soundId The ID of the sound effect. * @param volume The volume to play the sound at. */ public playSound(soundId: number, volume: number): void { activeWorld.playLocationSound(this.position, this.instance.instanceId, soundId, volume); } /** * Transforms the Npc visually into a different Npc. * @param npcKey The unique string key of the Npc to transform into. */ public transformInto(npcKey: string): void { const npcDetails = findNpc(npcKey); this.id = npcDetails.gameId; this.updateFlags.appearanceUpdateRequired = true; } /** * Transforms the Npc visually into a different Npc. * @param id The id of the Npc to transform into. */ public setNewId(id: number): void { this.id = id; this.updateFlags.appearanceUpdateRequired = true; } public equals(other: Npc): boolean { if (!other) { return false; } return other.id === this.id && other.uuid === this.uuid; } public set position(position: Position) { super.position = position; if (this.quadtreeKey !== null) { activeWorld.npcTree.remove(this.quadtreeKey); } this.quadtreeKey = { x: position.x, y: position.y, actor: this }; activeWorld.npcTree.push(this.quadtreeKey); } public get position(): Position { return super.position; } public get name(): string { return this._name; } public get combatLevel(): number { return this._combatLevel; } public get movementRadius(): number { return this._movementRadius; } public get exists(): boolean { return this._exists; } public set exists(value: boolean) { this._exists = value; } public get initialized(): boolean { return this._initialized; } public get instanceId(): string | null { return this.instance?.instanceId ?? null; } } /** * A basic attempt at handling the odds of receiving an item from an NPCs DropTable. * * This method gets the odds defined in the DropTable, and rolls a random number to see if the odds are met. * Also checks whether or not the drop has a quest requirement, and accounts for that. * * @param player The player receiving the drop. * @param npcDetails The NpcDetails of the NPC that contains the DropTable data. */ export function calculateNpcDrops(player: Player, npcDetails: NpcDetails): { itemKey: string; amount?: number }[] { const itemDrops: { itemKey: string; amount?: number }[] = []; const npcDropTable = npcDetails.dropTable; if (!npcDropTable) { return itemDrops; } npcDropTable.forEach(drop => { let meetsQuestRequirements = true; if (drop.questRequirement) { meetsQuestRequirements = player.getQuest(drop.questRequirement.questId).progress === drop.questRequirement.stage; } drop.amount = drop.amount || 1; drop.amountMax = drop.amountMax || 1; let odds: { numerator: number; denominator: number }; if (drop.frequency === 'always') { odds = { numerator: 1, denominator: 1 }; } else { const dividedFrequency = drop.frequency.split('/'); odds = { numerator: Number(dividedFrequency[0]), denominator: Number(dividedFrequency[1]) }; } const randomNumber = getRandomInt(odds.denominator); if (randomNumber === 1 && meetsQuestRequirements) { const randomNumberOfItems = getRandomInt(drop.amountMax, drop.amount); itemDrops.push({ itemKey: drop.itemKey, amount: randomNumberOfItems }); } }); return itemDrops; } /** * Generates a random integer between a maximum and minimum value. * @param max The largest value to generate to. * @param min The smallest value to generate from. */ function getRandomInt(max, min = 1): number { return Math.floor(Math.random() * max) + min; } ================================================ FILE: src/engine/world/actor/pathfinding.ts ================================================ import { activeWorld } from '@engine/world'; import type { Actor } from '@engine/world/actor/actor'; import { isPlayer } from '@engine/world/actor/util'; import type { WorldInstance } from '@engine/world/instances'; import type { Chunk } from '@engine/world/map/chunk'; import type { Tile } from '@engine/world/map/chunk-manager'; import { logger } from '@runejs/common'; import { Position } from '../position'; class Point { private _parent: Point | null = null; private _cost: number = 0; public constructor( private readonly _x: number, private readonly _y: number, ) {} public equals(point: Point | null): boolean { if (point === null) { return false; } if (this._cost === point._cost) { if (this._parent === null && point._parent !== null) { return false; } else if (this._parent !== null && !this._parent.equals(point._parent)) { return false; } return this._x === point._x && this._y === point._y; } return false; } public get x(): number { return this._x; } public get y(): number { return this._y; } public get parent(): Point | null { return this._parent; } public set parent(value: Point | null) { this._parent = value; } public get cost(): number { return this._cost; } public set cost(value: number) { this._cost = value; } } export interface PathingOptions { pathingSearchRadius?: number; ignoreDestination?: boolean; } export class Pathfinding { public stopped = false; private currentPoint: Point; private points: Point[][]; private closedPoints = new Set(); private openPoints = new Set(); public constructor(private actor: Actor) {} public walkTo(position: Position, options: PathingOptions): void { if (!options.pathingSearchRadius) { options.pathingSearchRadius = 16; } try { const path = this.pathTo(position.x, position.y, options.pathingSearchRadius); if (!path) { return; } const walkingQueue = this.actor.walkingQueue; walkingQueue.clear(); walkingQueue.valid = true; if (options.ignoreDestination) { path.splice(path.length - 1, 1); } for (const point of path) { walkingQueue.add(point.x, point.y); } } catch (error) { logger.error(error); } } public createTileMap(searchRadius: number = 8): { [key: string]: Tile } { const position = this.actor.position; const lowestX = position.x - searchRadius; const lowestY = position.y - searchRadius; const highestX = position.x + searchRadius; const highestY = position.y + searchRadius; const tiles: Tile[] = []; for (let x = lowestX; x < highestX; x++) { for (let y = lowestY; y < highestY; y++) { tiles.push(activeWorld.chunkManager.getTile(new Position(x, y, this.actor.position.level))); } } return Object.fromEntries(tiles.map(tile => [`${tile.x},${tile.y}`, tile])); } public pathTo(destinationX: number, destinationY: number, searchRadius: number = 16): Point[] | null { const position = this.actor.position; const lowestX = position.x - searchRadius; const lowestY = position.y - searchRadius; const highestX = position.x + searchRadius; const highestY = position.y + searchRadius; if (destinationX < lowestX || destinationX > highestX || destinationY < lowestY || destinationY > highestY) { throw new Error(`Out of range.`); } const destinationIndexX = destinationX - position.x + searchRadius; const destinationIndexY = destinationY - position.y + searchRadius; const startingIndexX = searchRadius; const startingIndexY = searchRadius; const pointLen = searchRadius * 2; if (pointLen <= 0) { throw new Error(`Why is your search radius zero?`); } this.points = [...Array(pointLen)].map(e => Array(pointLen)); for (let x = 0; x < pointLen; x++) { for (let y = 0; y < pointLen; y++) { this.points[x][y] = new Point(lowestX + x, lowestY + y); } } // Starting point this.openPoints = new Set(); this.closedPoints = new Set(); this.openPoints.add(this.points[startingIndexX][startingIndexY]); while (this.openPoints.size > 0) { if (this.stopped) { return null; } const bestPoint = this.calculateBestPoint(); if (!bestPoint || bestPoint.equals(this.points[destinationIndexX][destinationIndexY])) { break; } this.currentPoint = bestPoint; this.openPoints.delete(this.currentPoint); this.closedPoints.add(this.currentPoint); const level = this.actor.position.level; const { x, y } = this.currentPoint; const indexX = x - lowestX; const indexY = y - lowestY; // North-West if (indexX > 0 && this.points[indexX - 1] && indexY < this.points[indexX - 1].length - 1) { if (this.canPathDiagonally(x, y, new Position(x - 1, y + 1, level), -1, 1, 0x1280138, 0x1280108, 0x1280120)) { this.calculateCost(this.points[indexX - 1][indexY + 1]); } } // North-East if (indexX < this.points.length - 1 && this.points[indexX + 1] && indexY < this.points[indexX + 1].length - 1) { if (this.canPathDiagonally(x, y, new Position(x + 1, y + 1, level), 1, 1, 0x12801e0, 0x1280180, 0x1280120)) { this.calculateCost(this.points[indexX + 1][indexY + 1]); } } // South-West if (indexX > 0 && indexY > 0 && this.points[indexX - 1]) { if (this.canPathDiagonally(x, y, new Position(x - 1, y - 1, level), -1, -1, 0x128010e, 0x1280108, 0x1280102)) { this.calculateCost(this.points[indexX - 1][indexY - 1]); } } // South-East if (indexX < this.points.length - 1 && indexY > 0 && this.points[indexX + 1]) { if (this.canPathDiagonally(x, y, new Position(x + 1, y - 1, level), 1, -1, 0x1280183, 0x1280180, 0x1280102)) { this.calculateCost(this.points[indexX + 1][indexY - 1]); } } // West if (indexX > 0 && this.canPathNSEW(new Position(x - 1, y, level), 0x1280108)) { this.calculateCost(this.points[indexX - 1][indexY]); } // East if (indexX < this.points.length - 1 && this.canPathNSEW(new Position(x + 1, y, level), 0x1280180)) { this.calculateCost(this.points[indexX + 1][indexY]); } // South if (indexY > 0 && this.canPathNSEW(new Position(x, y - 1, level), 0x1280102)) { this.calculateCost(this.points[indexX][indexY - 1]); } // North if ( this.points[indexX] && indexY < this.points[indexX].length - 1 && this.canPathNSEW(new Position(x, y + 1, level), 0x1280120) ) { this.calculateCost(this.points[indexX][indexY + 1]); } } const destinationPoint = this.points[destinationIndexX][destinationIndexY]; if (!destinationPoint || !destinationPoint.parent) { // throw new Error(`Unable to find destination point.`); return null; } // build path const path: Point[] = []; let point: Point | null = destinationPoint; let iterations = 0; do { if (this.stopped) { return null; } path.push(new Point(point.x, point.y)); point = point.parent; iterations++; if (iterations > 1000) { throw new Error(`Path iteration overflow, path can not be found.`); } if (point === null) { break; } } while (!point.equals(this.points[startingIndexX][startingIndexY])); return path.reverse(); } public canMoveTo(origin: Position, destination: Position): boolean { const destinationChunk: Chunk = activeWorld.chunkManager.getChunkForWorldPosition(destination); const tile: Tile = activeWorld.chunkManager.getTile(destination); if (tile?.blocked) { return false; } const initialX: number = origin.x; const initialY: number = origin.y; const destinationLocalX: number = destination.x - destinationChunk.collisionMap.insetX; const destinationLocalY: number = destination.y - destinationChunk.collisionMap.insetY; // West if (destination.x < initialX && destination.y == initialY) { if (!this.movementPermitted(this.instance, destinationChunk, destinationLocalX, destinationLocalY, 0x1280108)) { return false; } } // East if (destination.x > initialX && destination.y == initialY) { if (!this.movementPermitted(this.instance, destinationChunk, destinationLocalX, destinationLocalY, 0x1280180)) { return false; } } // South if (destination.y < initialY && destination.x == initialX) { if (!this.movementPermitted(this.instance, destinationChunk, destinationLocalX, destinationLocalY, 0x1280102)) { return false; } } // North if (destination.y > initialY && destination.x == initialX) { if (!this.movementPermitted(this.instance, destinationChunk, destinationLocalX, destinationLocalY, 0x1280120)) { return false; } } // South-West if (destination.x < initialX && destination.y < initialY) { if ( !this.diagonalMovementPermitted( this.instance, origin, destinationChunk, destinationLocalX, destinationLocalY, initialX, initialY, -1, -1, 0x128010e, 0x1280108, 0x1280102, ) ) { return false; } } // South-East if (destination.x > initialX && destination.y < initialY) { if ( !this.diagonalMovementPermitted( this.instance, origin, destinationChunk, destinationLocalX, destinationLocalY, initialX, initialY, 1, -1, 0x1280183, 0x1280180, 0x1280102, ) ) { return false; } } // North-West if (destination.x < initialX && destination.y > initialY) { if ( !this.diagonalMovementPermitted( this.instance, origin, destinationChunk, destinationLocalX, destinationLocalY, initialX, initialY, -1, 1, 0x1280138, 0x1280108, 0x1280120, ) ) { return false; } } // North-East if (destination.x > initialX && destination.y > initialY) { if ( !this.diagonalMovementPermitted( this.instance, origin, destinationChunk, destinationLocalX, destinationLocalY, initialX, initialY, 1, 1, 0x12801e0, 0x1280180, 0x1280120, ) ) { return false; } } return true; } public movementPermitted( instance: WorldInstance, globalChunk: Chunk, destinationLocalX: number, destinationLocalY: number, i: number, ): boolean { const instancedAdjacency = instance.getInstancedChunk(globalChunk.position.x, globalChunk.position.y, globalChunk.position.level) .collisionMap.adjacency; const globalAdjacency = globalChunk.collisionMap.adjacency; try { const instancedAdjacencyForTile = instancedAdjacency[destinationLocalX][destinationLocalY]; const instancedTileFlags = instancedAdjacencyForTile === null ? null : instancedAdjacencyForTile & i; const globalAdjacencyForTile = globalAdjacency[destinationLocalX][destinationLocalY]; const globalTileFlags = globalAdjacencyForTile === null ? null : globalAdjacencyForTile & i; return instancedTileFlags === null ? globalTileFlags === 0 : instancedTileFlags === 0; } catch (error) { logger.error(`Unable to calculate movement permission for local coordinates ${destinationLocalX},${destinationLocalY}.`); return false; } } public diagonalMovementPermitted( instance: WorldInstance, origin: Position, destinationGlobalChunk: Chunk, destinationLocalX: number, destinationLocalY: number, initialX: number, initialY: number, offsetX: number, offsetY: number, destMask: number, cornerMask1: number, cornerMask2: number, ): boolean { const corner1 = this.findLocalCornerChunk(initialX + offsetX, initialY, origin); const corner2 = this.findLocalCornerChunk(initialX, initialY + offsetY, origin); return ( this.movementPermitted(instance, destinationGlobalChunk, destinationLocalX, destinationLocalY, destMask) && this.movementPermitted(instance, corner1.chunk, corner1.localX, corner1.localY, cornerMask1) && this.movementPermitted(instance, corner2.chunk, corner2.localX, corner2.localY, cornerMask2) ); } public findLocalCornerChunk(cornerX: number, cornerY: number, origin: Position): { localX: number; localY: number; chunk: Chunk } { const cornerPosition: Position = new Position(cornerX, cornerY, origin.level + 1); let cornerChunk: Chunk = activeWorld.chunkManager.getChunkForWorldPosition(cornerPosition); const tileAbove: Tile = activeWorld.chunkManager.getTile(cornerPosition); if (!tileAbove?.bridge) { cornerPosition.level = cornerPosition.level - 1; cornerChunk = activeWorld.chunkManager.getChunkForWorldPosition(cornerPosition); } const localX: number = cornerX - cornerChunk.collisionMap.insetX; const localY: number = cornerY - cornerChunk.collisionMap.insetY; return { localX, localY, chunk: cornerChunk }; } private calculateCost(point: Point): void { if (!this.currentPoint || !point) { return; } const nextStepCost = this.currentPoint.cost + this.calculateCostBetween(this.currentPoint, point); if (nextStepCost < point.cost) { this.openPoints.delete(point); this.closedPoints.delete(point); } if (!this.openPoints.has(point) && !this.closedPoints.has(point)) { point.parent = this.currentPoint; point.cost = nextStepCost; this.openPoints.add(point); } } private calculateCostBetween(current: Point, destination: Point): number { const deltaX = current.x - destination.x; const deltaY = current.y - destination.y; return (Math.abs(deltaX) + Math.abs(deltaY)) * 10; } private calculateBestPoint(): Point | null { let bestPoint: Point | null = null; this.openPoints.forEach(point => { if (!bestPoint) { bestPoint = point; } else if (point.cost < bestPoint.cost) { bestPoint = point; } }); return bestPoint; } private canPathNSEW(position: Position, i: number): boolean { const chunk = activeWorld.chunkManager.getChunkForWorldPosition(position); const destinationLocalX: number = position.x - chunk.collisionMap.insetX; const destinationLocalY: number = position.y - chunk.collisionMap.insetY; return this.movementPermitted(this.instance, chunk, destinationLocalX, destinationLocalY, i); } private canPathDiagonally( originX: number, originY: number, position: Position, offsetX: number, offsetY: number, destMask: number, cornerMask1: number, cornerMask2: number, ): boolean { const chunk = activeWorld.chunkManager.getChunkForWorldPosition(position); const destinationLocalX: number = position.x - chunk.collisionMap.insetX; const destinationLocalY: number = position.y - chunk.collisionMap.insetY; return this.diagonalMovementPermitted( this.instance, position, chunk, destinationLocalX, destinationLocalY, originX, originY, offsetX, offsetY, destMask, cornerMask1, cornerMask2, ); } private get instance(): WorldInstance { return isPlayer(this.actor) ? this.actor.instance : activeWorld.globalInstance; } } ================================================ FILE: src/engine/world/actor/player/achievements.ts ================================================ import type { Player } from '@engine/world/actor/player/player'; import { gfxIds } from '@engine/world/config/gfx-ids'; import { serverConfig } from '@server/game/game-server'; export const achievementSeries = { lumbridge: { name: 'Lumbridge', }, varrock: { name: 'Varrock', }, }; export enum AchievementSeries { LUMBRIDGE = 'lumbridge', VARROCK = 'varrock', } export interface Achievement { id: string; name: string; description: string; longDescription: string; series: AchievementSeries; } export const Achievements: { [key: string]: Achievement } = { WELCOME: { id: 'lumbridge-hans-welcome', name: 'Welcome!', description: 'Talk to Hans.', longDescription: `Speak with Hans in the Lumbridge Castle's courtyard.`, series: AchievementSeries.LUMBRIDGE, }, BURY_BONES: { id: 'bury-bones', name: 'Grave Digger', description: 'Bury the bones of the dead.', longDescription: `Bury the remains of a deceased enemy.`, series: AchievementSeries.LUMBRIDGE, }, }; export function giveAchievement(achievement: Achievement, player: Player): boolean { if (!serverConfig.giveAchievements) { return false; } if (hasAchievement(achievement, player)) { return false; } player.achievements.push(achievement.id); player.playGraphics({ id: gfxIds.levelUpFireworks, delay: 0, height: 125 }); player.sendMessage( `You've completed an Achievement in the ` + `${achievementSeries[achievement.series].name} series!`, ); player.sendMessage(`${achievement.name} - ${achievement.description}`); return true; } export function hasAchievement(achievement: Achievement, player: Player): boolean { if (!player.achievements || player.achievements.length === 0) { return false; } return player.achievements.indexOf(achievement.id) !== -1; } ================================================ FILE: src/engine/world/actor/player/attack.ts ================================================ //This will be used to pass information required to calulate attack and defense like weapon damage etc export class Attack { damageType: AttackDamageType; attackRoll: number = 0; defenseRoll: number = 0; hitChance: number = 0; damage: number = 0; maximumHit: number; } export enum AttackDamageType { Stab, Slash, Crush, Magic, Range, } ================================================ FILE: src/engine/world/actor/player/cutscenes.ts ================================================ import type { Player } from '@engine/world/actor/player/player'; import { Position } from '@engine/world/position'; /** * Various camera options for cutscenes. */ export interface CameraOptions { cameraX?: number; cameraY?: number; cameraHeight?: number; cameraMovementSpeed?: number; cameraAcceleration?: number; lookX?: number; lookY?: number; lookHeight?: number; lookMovementSpeed?: number; lookAcceleration?: number; } /** * Controls a game cutscene for a specific player. */ export class Cutscene { public readonly player: Player; private _cameraX: number; private _cameraY: number; private _cameraHeight: number; private _cameraMovementSpeed: number; private _cameraAcceleration: number; private _lookX: number; private _lookY: number; private _lookHeight: number; private _lookMovementSpeed: number; private _lookAcceleration: number; public constructor(player: Player, options?: CameraOptions) { this.player = player; if (options) { this.setCamera(options); } } /** * Sets the cutscene camera to the specified options. * @param options The camera options to use. */ public setCamera(options: CameraOptions): void { const { cameraX, cameraY, cameraHeight, cameraMovementSpeed, cameraAcceleration, lookX, lookY, lookHeight, lookMovementSpeed, lookAcceleration, } = options; if (cameraX && cameraY) { this.snapCameraTo(cameraX, cameraY, cameraHeight || 400, cameraMovementSpeed || 0, cameraAcceleration || 100); } if (lookX && lookY) { this.lookAt(lookX, lookY, lookHeight || 400, lookMovementSpeed || 0, lookAcceleration || 100); } } /** * Snaps the cutscene to a specific location. * @param cameraX The world X coordinate to snap the camera to. * @param cameraY The world Y coordinate to snap the camera to. * @param height The height of the camera relative to the ground. Defaults to 400. * @param movementSpeed The general speed of the camera movement. Defaults to 0 (immediate). * @param acceleration The acceleration speed of the camera movement. Defaults to 100 (instantaneous). */ public snapCameraTo( cameraX: number, cameraY: number, height: number = 400, movementSpeed: number = 0, acceleration: number = 100, ): void { this._cameraX = cameraX; this._cameraY = cameraY; this._cameraHeight = height; this._cameraMovementSpeed = movementSpeed; this._cameraAcceleration = acceleration; this.player.outgoingPackets.snapCameraTo(new Position(cameraX, cameraY), height, movementSpeed, acceleration); } /** * Makes the camera look at a specific location from it's current snap point. * @param lookX The world X coordinate to look towards. * @param lookY The world Y coordinate to look towards. * @param height The height that the camera should be looking at, relative to the ground. Defaults to 400. * @param movementSpeed The general speed of the camera movement. Defaults to 0 (immediate). * @param acceleration The acceleration speed of the camera movement. Defaults to 100 (instantaneous). */ public lookAt(lookX: number, lookY: number, height: number = 400, movementSpeed: number = 0, acceleration: number = 100): void { this._lookX = lookX; this._lookY = lookY; this._lookHeight = height; this._lookMovementSpeed = movementSpeed; this._lookAcceleration = acceleration; this.player.outgoingPackets.turnCameraTowards(new Position(lookX, lookY), height, movementSpeed, acceleration); } /** * Ends the current cutscene and snaps the camera back to the player character. */ public endCutscene(): void { this.player.outgoingPackets.resetCamera(); this.player.cutscene = null; } public get cameraX(): number { return this._cameraX; } public get cameraY(): number { return this._cameraY; } public get cameraHeight(): number { return this._cameraHeight; } public get cameraMovementSpeed(): number { return this._cameraMovementSpeed; } public get cameraAcceleration(): number { return this._cameraAcceleration; } public get lookX(): number { return this._lookX; } public get lookY(): number { return this._lookY; } public get lookHeight(): number { return this._lookHeight; } public get lookMovementSpeed(): number { return this._lookMovementSpeed; } public get lookAcceleration(): number { return this._lookAcceleration; } } ================================================ FILE: src/engine/world/actor/player/dialogue-action.ts ================================================ import type { Npc } from '@engine/world/actor/npc'; import type { Player } from '@engine/world/actor/player/player'; import { filestore } from '@server/game/game-server'; export const dialogueWidgetIds = { PLAYER: [64, 65, 66, 67], NPC: [241, 242, 243, 244], OPTIONS: [228, 230, 232, 234], TEXT: [210, 211, 212, 213, 214], }; type LineConstraint = [number, number]; /** * Min -> max lines for a specific dialogue type. */ const lineConstraints: { [key: string]: LineConstraint } = { PLAYER: [1, 4], NPC: [1, 4], OPTIONS: [2, 5], TEXT: [1, 5], }; export enum DialogueEmote { JOYFUL = 588, CALM_TALK_1 = 589, CALM_TALK_2 = 590, DEFAULT = 591, EVIL_1 = 592, EVIL_2 = 593, EVIL_3 = 594, ANNOYED = 595, DISTRESSED_1 = 596, DISTRESSED_2 = 597, BOWS_HEAD_SAD = 598, DRUNK_LEFT = 600, DRUNK_RIGHT = 601, NOT_INTERESTED = 602, SLEEPY = 603, DEVILISH = 604, LAUGH_1 = 605, LAUGH_2 = 606, LAUGH_3 = 607, LAUGH_4 = 608, EVIL_LAUGH = 609, SAD_1 = 610, SAD_2 = 611, SAD_3 = 598, SAD_4 = 613, CONSIDERING = 612, ANGRY_1 = 614, ANGRY_2 = 615, ANGRY_3 = 616, ANGRY_4 = 617, } export type DialogueType = 'PLAYER' | 'NPC' | 'OPTIONS' | 'TEXT'; export interface DialogueOptions { type: DialogueType; npc?: number; emote?: DialogueEmote; title?: string; skillId?: number; lines: string[]; } // @DEPRECATED export class DialogueAction { private _action: number | null = null; public constructor(private readonly p: Player) {} public async player(emote: DialogueEmote, lines: string[]): Promise { return this.dialogue({ emote, lines, type: 'PLAYER' }); } public async npc(npc: Npc | number, emote: DialogueEmote, lines: string[]): Promise { return this.dialogue({ emote, lines, type: 'NPC', npc: typeof npc === 'number' ? npc : npc.id }); } public async options(title: string, options: string[]): Promise { return this.dialogue({ type: 'OPTIONS', title, lines: options }); } public async dialogue(options: DialogueOptions): Promise { if (options.lines.length < lineConstraints[options.type][0] || options.lines.length > lineConstraints[options.type][1]) { throw new Error('Invalid line length.'); } if (options.type === 'NPC' && options.npc === undefined) { throw new Error('NPC not supplied.'); } this._action = null; let widgetIndex = options.lines.length - 1; if (options.type === 'OPTIONS') { widgetIndex--; } const widgetId = dialogueWidgetIds[options.type][widgetIndex]; if (widgetId === undefined || widgetId === null || widgetId === -1) { return Promise.resolve(this); } let textOffset = 0; if (options.type === 'PLAYER' || options.type === 'NPC') { if (!options.emote) { options.emote = DialogueEmote.DEFAULT; } if (options.type === 'NPC') { const npc = options.npc; if (npc === undefined) { // TODO (Jameskmonger) how can this error be handled in a better way? throw new Error('NPC not supplied.'); } const cacheNpc = filestore.configStore.npcStore.getNpc(npc); if (!cacheNpc) { throw new Error(`NPC ${npc} not found in cache.`); } this.p.outgoingPackets.setWidgetNpcHead(widgetId, 0, npc); this.p.outgoingPackets.updateWidgetString(widgetId, 1, cacheNpc.name || 'Unknown'); } else if (options.type === 'PLAYER') { this.p.outgoingPackets.setWidgetPlayerHead(widgetId, 0); this.p.outgoingPackets.updateWidgetString(widgetId, 1, this.p.username); } this.p.outgoingPackets.playWidgetAnimation(widgetId, 0, options.emote); textOffset = 2; } else if (options.type === 'OPTIONS') { this.p.outgoingPackets.updateWidgetString(widgetId, 0, options.title || 'No Title'); textOffset = 1; } else if (options.type === 'TEXT') { textOffset = 0; } for (let i = 0; i < options.lines.length; i++) { this.p.outgoingPackets.updateWidgetString(widgetId, textOffset + i, options.lines[i]); } return new Promise((resolve, reject) => { this.p.interfaceState.openWidget(widgetId, { slot: 'chatbox', }); const sub = this.p.interfaceState.closed.subscribe(action => { sub.unsubscribe(); this._action = action?.data ?? null; resolve(this); }); }); } public close(): void { this.p.outgoingPackets.closeActiveWidgets(); } public get action(): number | null { return this._action; } public set action(value: number | null) { this._action = value; } } export const dialogueAction = async (player: Player, options?: DialogueOptions): Promise => { if (options) { return new DialogueAction(player).dialogue(options); } else { return Promise.resolve(new DialogueAction(player)); } }; ================================================ FILE: src/engine/world/actor/player/metadata.ts ================================================ import type { Chunk } from '@engine/world/map/chunk'; import type { Position } from '@engine/world/position'; import type { LandscapeObject } from '@runejs/filestore'; import type { Subject, Subscription } from 'rxjs'; /** * The definition of the metadata directly available on a {@link Player}. * * This is a subset of the metadata available on an {@link Actor}. See {@link ActorMetadata} for more information. * * You cannot guarantee that all of these properties will be present on an actor, * so you should always check for their existence before using them. * * @author jameskmonger */ export type PlayerMetadata = { /** * The player's client configuration options (varps). */ configs: number[]; /** * The player's current and previous chunks. */ updateChunk: { oldChunk: Chunk; newChunk: Chunk; }; /** * The player's last position before teleporting. */ lastPosition: Position; /** * Used to prevent the `object_interaction` pipe from running. * * TODO (jameskmonger) We should probably deprecate this, it seems like it's already been * replaced by the `busy` property which is itself deprecated. This is only * used in Goblin Diplomacy. */ blockObjectInteractions: boolean; /** * The player's currently open shop. */ lastOpenedShopKey: string; /** * A subscription to the player's "widget closed" events. * * Used to remove a player from a shop when they close the shop's widget. */ shopCloseListener: Subscription; /** * Allows listening to a player clicking on the tab at the specified index. * * The `event` property is a `Subject` that will emit a `boolean` value when the player clicks on the tab. * * TODO (jameskmonger) This is only used in Goblin Diplomacy. It is only present when the player is taking part * in Goblin Displomacy. */ tabClickEvent: { tabIndex: number; event: Subject; }; /** * Used to process dialogue trees. */ dialogueIndices: Record; /** * The player's current dialogue tree. * * This is a `ParsedDialogueTree` type, but that type is not exported. */ dialogueTree: any; /** * A list of custom landscape objects that have been spawned by the player. * * Initialised by, and used by, the `spawn-scenery` command. */ spawnedScenery: LandscapeObject[]; /** * The last custom landscape object that was spawned by the player. * * Used to provide `undo` functionality to the `spawn-scenery` command. */ lastSpawnedScenery: LandscapeObject; /** * The timestamp of the last time the player lit a fire. * * Used to prevent the player from lighting fires too quickly. * * TODO (jameskmonger) this should not be using Dates for timing and will be converted in the new task system */ lastFire: number; /** * 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; }; ================================================ FILE: src/engine/world/actor/player/model.ts ================================================ /** * Options for sending chat messages to a player. */ export interface SendMessageOptions { dialogue?: boolean; console?: boolean; } ================================================ FILE: src/engine/world/actor/player/player-data.ts ================================================ import { existsSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import type { PlayerQuest } from '@engine/config/quest-config'; import { hasValueNotNull } from '@engine/util/data'; import type { SkillValue } from '@engine/world/actor/skills'; import type { Item } from '@engine/world/items/item'; import { MusicPlayerLoopMode, MusicPlayerMode } from '@engine/world/sound/music'; import { logger } from '@runejs/common'; import type { Player } from './player'; export interface Appearance { gender: number; head: number; torso: number; arms: number; legs: number; hands: number; feet: number; facialHair: number; hairColor: number; torsoColor: number; legColor: number; feetColor: number; skinColor: number; } export class PlayerSettings { musicVolume: number = 0; musicPlayerMode: number = MusicPlayerMode.AUTO; musicPlayerLoopMode: number = MusicPlayerLoopMode.ENABLED; soundEffectVolume: number = 0; areaEffectVolume: number = 0; splitPrivateChatEnabled: boolean = false; twoMouseButtonsEnabled: boolean = true; screenBrightness: number = 2; chatEffectsEnabled: boolean = true; acceptAidEnabled: boolean = true; runEnabled: boolean = false; autoRetaliateEnabled: boolean = true; attackStyle: number = 0; bankInsertMode: number = 0; bankWithdrawNoteMode: number = 0; publicChatMode: number = 0; privateChatMode: number = 0; tradeMode: number = 0; } export interface PlayerSave { username: string; passwordHash: string; rights: number; position: { x: number; y: number; level: number; }; lastLogin: { date: Date; address: string; }; appearance: Appearance; inventory: (Item | null)[]; bank: (Item | null)[]; equipment: (Item | null)[]; skills: SkillValue[]; settings: PlayerSettings; savedMetadata: { [key: string]: any }; questList: PlayerQuest[]; musicTracks: Array; achievements: string[]; friendsList: string[]; ignoreList: string[]; } export const defaultAppearance = (): Appearance => { return { gender: 0, head: 0, torso: 18, arms: 26, legs: 36, hands: 33, feet: 42, facialHair: 10, hairColor: 0, torsoColor: 0, legColor: 0, feetColor: 0, skinColor: 0, } as Appearance; }; export const defaultSettings = (): PlayerSettings => { return new PlayerSettings(); }; export const validateSettings = (player: Player): void => { const existingKeys = Object.keys(player.settings); const newSettings = new PlayerSettings(); const newKeys = Object.keys(newSettings); if (newKeys.length === existingKeys.length) { return; } const missingKeys = newKeys.filter(key => existingKeys.indexOf(key) === -1); for (const key of missingKeys) { player.settings[key] = newSettings[key]; } }; export function savePlayerData(player: Player): boolean { const fileName = player.username.toLowerCase() + '.json'; const filePath = join('data/saves', fileName); const playerSave: PlayerSave = { username: player.username, passwordHash: player.passwordHash, position: { x: player.position.x, y: player.position.y, level: player.position.level > 3 ? 0 : player.position.level, }, lastLogin: { date: player.loginDate, address: player.lastAddress, }, rights: player.rights.valueOf(), appearance: player.appearance, inventory: player.inventory.items, bank: player.bank.items.filter(item => { return hasValueNotNull(item); }), equipment: player.equipment.items, skills: player.skills.values, settings: player.settings, savedMetadata: player.savedMetadata, questList: player.quests, musicTracks: player.musicTracks, achievements: player.achievements, friendsList: player.friendsList, ignoreList: player.ignoreList, }; try { writeFileSync(filePath, JSON.stringify(playerSave, null, 4)); return true; } catch (error) { logger.error(`Error saving player data for ${player.username}.`); return false; } } export function playerExists(username: string): boolean { const fileName = username.toLowerCase() + '.json'; const filePath = join('data/saves', fileName); return existsSync(filePath); } export function loadPlayerSave(username: string): PlayerSave | null { const fileName = username.toLowerCase() + '.json'; const filePath = join('data/saves', fileName); if (!existsSync(filePath)) { return null; } const fileData = readFileSync(filePath, 'utf8'); if (!fileData) { return null; } try { const playerSave = JSON.parse(fileData) as PlayerSave; if (playerSave?.position?.level > 3) { playerSave.position.level = 0; } return playerSave; } catch (error) { logger.error(`Malformed player save data for ${username}.`); return null; } } ================================================ FILE: src/engine/world/actor/player/player.ts ================================================ import EventEmitter from 'events'; import type { AddressInfo, Socket } from 'net'; import type { PlayerCommandActionHook } from '@engine/action/pipe/player-command.action'; import { regionChangeActionFactory } from '@engine/action/pipe/region-change.action'; import { findItem, findMusicTrack, findNpc, findQuest, findSongIdByRegionId, itemMap, musicRegions, npcIdMap, widgets, } from '@engine/config/config-handler'; import type { EquipmentSlot, ItemDetails } from '@engine/config/item-config'; import { equipmentIndex, getEquipmentSlot } from '@engine/config/item-config'; import type { NpcDetails } from '@engine/config/npc-config'; import type { QuestKey } from '@engine/config/quest-config'; import { PlayerQuest } from '@engine/config/quest-config'; import { InterfaceState } from '@engine/interface/interface-state'; import type { Isaac } from '@engine/net/isaac'; import { OutboundPacketHandler } from '@engine/net/outbound-packet-handler'; import { actionHookMap, questMap } from '@engine/plugins/loader'; import { colors, hexToRgb, rgbTo16Bit } from '@engine/util/colors'; import { daysSinceLastLogin } from '@engine/util/time'; import { getVarbitMorphIndex } from '@engine/util/varbits'; import { activeWorld } from '@engine/world'; import type { Appearance, PlayerSettings } from '@engine/world/actor/player/player-data'; import { defaultAppearance, defaultSettings, loadPlayerSave, playerExists, savePlayerData } from '@engine/world/actor/player/player-data'; import { itemIds } from '@engine/world/config/item-ids'; import type { PlayerWidget } from '@engine/world/config/widget'; import { widgetScripts } from '@engine/world/config/widget'; import type { TileModifications } from '@engine/world/instances'; import { WorldInstance } from '@engine/world/instances'; import type { Item } from '@engine/world/items/item'; import type { ContainerUpdateEvent } from '@engine/world/items/item-container'; import { ItemContainer, getItemFromContainer } from '@engine/world/items/item-container'; import type { Chunk, ChunkUpdateItem } from '@engine/world/map/chunk'; import { Position } from '@engine/world/position'; import { MusicPlayerMode } from '@engine/world/sound/music'; import type { QuadtreeKey } from '@engine/world/world'; import { logger } from '@runejs/common'; import { filestore, serverConfig } from '@server/game/game-server'; import { Subject } from 'rxjs'; import { v4 } from 'uuid'; import { Actor } from '../actor'; import { dialogue } from '../dialogue'; import type { Npc } from '../npc'; import type { SkillName } from '../skills'; import type { Cutscene } from './cutscenes'; import type { PlayerMetadata } from './metadata'; import type { SendMessageOptions } from './model'; import { NpcSyncTask } from './sync/npc-sync-task'; import { PlayerSyncTask } from './sync/player-sync-task'; export const playerOptions: { option: string; index: number; placement: 'TOP' | 'BOTTOM' }[] = [ { option: 'Yeet', index: 1, placement: 'TOP', }, { option: 'Follow', index: 0, placement: 'BOTTOM', }, ]; export const defaultPlayerTabWidgets = () => [ -1, widgets.skillsTab, widgets.questTab, widgets.inventory.widgetId, widgets.equipment.widgetId, widgets.prayerTab, widgets.standardSpellbookTab, null, widgets.friendsList, widgets.ignoreList, widgets.logoutTab, widgets.settingsTab, widgets.emotesTab, widgets.musicPlayerTab, ]; export enum SidebarTab { COMBAT, SKILL, QUEST, INVENTORY, EQUIMENT, PRAYER, MAGIC, FRIENDS, IGNORE, LOGOUT, SETTINGS, EMOTES, MUSIC, } export enum Rights { ADMIN = 2, MOD = 1, USER = 0, } /** * A player character within the game world. */ export class Player extends Actor { public readonly clientUuid: number; public readonly username: string; public readonly passwordHash: string; public readonly playerUpdateTask: PlayerSyncTask; public readonly npcUpdateTask: NpcSyncTask; public readonly numericInputEvent: Subject; public readonly dialogueInteractionEvent: Subject; public readonly personalInstance = new WorldInstance(v4()); public readonly interfaceState = new InterfaceState(this); public isLowDetail: boolean; public trackedPlayers: Player[]; public trackedNpcs: Npc[]; public savedMetadata: { [key: string]: any } = {}; public sessionMetadata: { [key: string]: any } = {}; public quests: PlayerQuest[] = []; public musicTracks: Array = [0, 400, 547, 321]; public achievements: string[] = []; public friendsList: string[] = []; public ignoreList: string[] = []; public cutscene: Cutscene | null = null; public playerEvents: EventEmitter = new EventEmitter(); /** * Override the Actor's `metadata` property to provide a more specific type. * * You cannot guarantee that this will be populated with data, so you should always check for the existence of the * metadata you are looking for before using it. * * @author jameskmonger */ public readonly metadata: Actor['metadata'] & Partial = {}; private readonly _socket: Socket; private readonly _inCipher: Isaac; private readonly _outCipher: Isaac; private readonly _outgoingPackets: OutboundPacketHandler; private readonly _equipment: ItemContainer; private _rights: Rights; private _loginDate: Date; private _lastAddress: string; private firstTimePlayer: boolean; private _appearance: Appearance; private queuedWidgets: PlayerWidget[]; private _carryWeight: number; private _settings: PlayerSettings; private _nearbyChunks: Chunk[]; private quadtreeKey: QuadtreeKey | null = null; private privateMessageIndex: number = 1; public constructor( socket: Socket, inCipher: Isaac, outCipher: Isaac, clientUuid: number, username: string, password: string, isLowDetail: boolean, ) { super('player'); this._socket = socket; this._inCipher = inCipher; this._outCipher = outCipher; this.clientUuid = clientUuid; this.username = username; this.passwordHash = password; this._rights = Rights.ADMIN; this.isLowDetail = isLowDetail; this._outgoingPackets = new OutboundPacketHandler(this); this.playerUpdateTask = new PlayerSyncTask(this); this.npcUpdateTask = new NpcSyncTask(this); this.trackedPlayers = []; this.trackedNpcs = []; this.queuedWidgets = []; this._carryWeight = 0; this._equipment = new ItemContainer(14); this.dialogueInteractionEvent = new Subject(); this.numericInputEvent = new Subject(); this._nearbyChunks = []; this.friendsList = []; this.ignoreList = []; this.loadSaveData(); } public async init(): Promise { super.init(); this.updateFlags.mapRegionUpdateRequired = true; this.updateFlags.appearanceUpdateRequired = true; const playerChunk = activeWorld.chunkManager.getChunkForWorldPosition(this.position); playerChunk.addPlayer(this); this.outgoingPackets.updateCurrentMapChunk(); this.outgoingPackets.chatboxMessage('Welcome to RuneJS.'); this.skills.values.forEach((skill, index) => this.outgoingPackets.updateSkill(index, this.skills.getLevel(index), skill.exp)); this.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, this.inventory); this.outgoingPackets.sendUpdateAllWidgetItems(widgets.equipment, this.equipment); for (const item of this.equipment.items) { if (item) { await this.actionPipeline.call('equipment_change', this, item.itemId, 'EQUIP'); } } if (this.firstTimePlayer) { if (!serverConfig.tutorialEnabled) { this.interfaceState.openWidget(widgets.characterDesign, { slot: 'screen', multi: false, }); } } else if (serverConfig.showWelcome && (!serverConfig.tutorialEnabled || this.savedMetadata.tutorialComplete)) { const daysSinceLogin = daysSinceLastLogin(this.loginDate); let loginDaysStr = ''; if (daysSinceLogin <= 0) { loginDaysStr = 'earlier today'; } else if (daysSinceLogin === 1) { loginDaysStr = 'yesterday'; } else { loginDaysStr = daysSinceLogin + ' days ago'; } this.outgoingPackets.updateWidgetString( widgets.welcomeScreenChildren.question, 1, `Want to help RuneJS improve?\\nSend us a pull request over on Github!`, ); this.outgoingPackets.updateWidgetString( widgets.welcomeScreen, 13, `You last logged in @red@${loginDaysStr}@bla@ from: @red@${this.lastAddress}`, ); this.outgoingPackets.updateWidgetString(widgets.welcomeScreen, 16, `You have @yel@0 unread messages\\nin your message centre.`); this.outgoingPackets.updateWidgetString( widgets.welcomeScreen, 14, `\\nYou have not yet set any recovery questions.\\nIt is @lre@strongly@yel@ recommended that you do so.\\n\\nIf you don't you will be @lre@unable to recover your\\n@lre@password@yel@ if you forget it, or it is stolen.`, ); this.outgoingPackets.updateWidgetString( widgets.welcomeScreen, 22, `To change your recovery questions:\\n1) Logout and return to the frontpage of this website.\\n2) Choose 'Set new recovery questions'.`, ); this.outgoingPackets.updateWidgetString( widgets.welcomeScreen, 17, `\\nYou do not have a Bank PIN.\\nPlease visit a bank if you would like one.`, ); this.outgoingPackets.updateWidgetString( widgets.welcomeScreen, 21, `To start a subscripton:\\n1) Logout and return to the frontpage of this website.\\n2) Choose 'Start a new subscription'`, ); this.outgoingPackets.updateWidgetString( widgets.welcomeScreen, 19, `You are not a member.\\n\\nChoose to subscribe and\\nyou'll get loads of extra\\nbenefits and features.`, ); this.interfaceState.openWidget(widgets.welcomeScreen, { slot: 'full', containerId: widgets.welcomeScreenChildren.question, // The welcome screen's main button does not trigger the button packet // @todo read this from elsewhere to mark the welcome screen as closed doNotRegister: true, }); } for (const playerOption of playerOptions) { this.outgoingPackets.updatePlayerOption(playerOption.option, playerOption.index, playerOption.placement); } this.updateBonuses(); this.updateCarryWeight(true); this.updateQuestTab(); this.updateMusicTab(); this.inventory.containerUpdated.subscribe(event => this.inventoryUpdated(event)); this.playerEvents.on('exp', amt => { logger.info(`Player should have been awarded ${amt} exp if this was hooked up.`); }); this.actionsCancelled.subscribe(type => { let closeWidget: boolean; if (type === 'manual-movement' || type === 'pathing-movement') { closeWidget = true; } else if (type === 'keep-widgets-open' || type === 'button' || type === 'widget') { closeWidget = false; } else { closeWidget = true; } if (closeWidget) { this.interfaceState.closeAllSlots(); } }); this._loginDate = new Date(); this._lastAddress = (this._socket?.address() as AddressInfo)?.address || '127.0.0.1'; if (this.rights === Rights.ADMIN) { this.sendCommandList(actionHookMap.player_command as PlayerCommandActionHook[]); } this.outgoingPackets.resetAllClientConfigs(); await this.actionPipeline.call('player_init', { player: this }); activeWorld.spawnWorldItems(this); if (!this.metadata.customMap) { this.chunkChanged(playerChunk); } this.outgoingPackets.flushQueue(); logger.info(`${this.username}:${this.worldIndex} has logged in.`); } public logout(): void { if (!this.active) { return; } if (this.position.level > 3) { this.position.level = 0; } if (this.quadtreeKey) { activeWorld.playerTree.remove(this.quadtreeKey); } else { // TODO (Jameskmonger) remove this log if it isn't a problem state logger.warn(`Player ${this.username} has no quadtree key on logout.`); } this.save(); this.destroy(); this.actionsCancelled.complete(); this.walkingQueue.movementEvent.complete(); this.walkingQueue.movementQueued.complete(); this.actionPipeline.shutdown(); this.outgoingPackets.logout(); this.instance = null; activeWorld.chunkManager.getChunkForWorldPosition(this.position).removePlayer(this); activeWorld.deregisterPlayer(this); logger.info(`${this.username} has logged out.`); } public save(): void { savePlayerData(this); } public privateMessageReceived(fromPlayer: Player, messageBytes: number[]): void { this.outgoingPackets.sendPrivateMessage(this.privateMessageIndex++, fromPlayer, messageBytes); } public addFriend(friendName: string): boolean { if (!playerExists(friendName)) { return false; } friendName = friendName.toLowerCase(); this.friendsList.push(friendName); return true; } public removeFriend(friendName: string): boolean { friendName = friendName.toLowerCase(); const index = this.friendsList.findIndex(friend => friend === friendName); if (index === -1) { return false; } this.friendsList.splice(index, 1); return true; } public addIgnoredPlayer(playerName: string): boolean { if (!playerExists(playerName)) { return false; } playerName = playerName.toLowerCase(); const index = this.ignoreList.findIndex(ignoredPlayer => ignoredPlayer === playerName); if (index !== -1) { return false; } // @TODO emit event to friend service watcher this.ignoreList.push(playerName); return true; } public removeIgnoredPlayer(playerName: string): boolean { playerName = playerName.toLowerCase(); const index = this.ignoreList.findIndex(ignoredPlayer => ignoredPlayer === playerName); if (index === -1) { return false; } // @TODO emit event to friend service watcher this.ignoreList.splice(index, 1); return true; } public onNpcKill(npc: Npc) { console.log('killed npc'); } /** * Should be fired whenever the player's chunk changes. This will fire off chunk updates for all chunks not * already tracked by the player - all the new chunks that are coming into view. * @param chunk The player's new active map chunk. */ public chunkChanged(chunk: Chunk): void { const nearbyChunks = activeWorld.chunkManager.getSurroundingChunks(chunk); if (this._nearbyChunks.length === 0) { this.sendChunkUpdates(nearbyChunks); } else { const newChunks = nearbyChunks.filter(c1 => this._nearbyChunks.findIndex(c2 => c1.equals(c2)) === -1); this.sendChunkUpdates(newChunks); } this._nearbyChunks = nearbyChunks; } public async tick(): Promise { super.tick(); return new Promise(resolve => { this.walkingQueue.process(); if (this.updateFlags.mapRegionUpdateRequired) { if (this.position.x >= 6400) { // Custom map drawing area is anywhere x >= 6400 on the map if (this.metadata.customMap) { this.outgoingPackets.constructMapRegion(this.metadata.customMap); } else { logger.warn(`Player ${this.username} is in custom map area but has no custom map set.`); } } else { this.outgoingPackets.updateCurrentMapChunk(); } } resolve(); }); } public async update(): Promise { await Promise.all([this.playerUpdateTask.execute(), this.npcUpdateTask.execute()]); } public async reset(): Promise { return new Promise(resolve => { this.updateFlags.reset(); this.outgoingPackets.flushQueue(); if (this.metadata.updateChunk) { const { newChunk, oldChunk } = this.metadata.updateChunk; oldChunk.removePlayer(this); newChunk.addPlayer(this); this.chunkChanged(newChunk); this.metadata.updateChunk = undefined; } if (this.metadata.teleporting) { this.metadata.teleporting = undefined; } resolve(); }); } /** * Fetches the player's number of quest points based off of their completed quests. */ public getQuestPoints(): number { let questPoints = 0; if (this.quests && this.quests.length !== 0) { this.quests.filter(quest => quest.complete).forEach(quest => (questPoints += questMap[quest.questId]?.points || 0)); } return questPoints; } /** * Fetches a player's quest progression details. * @param questId The ID of the quest to find the player's status on. */ public getQuest(questId: string): PlayerQuest { let playerQuest = this.quests.find(quest => quest.questId === questId); if (!playerQuest) { playerQuest = new PlayerQuest(questId); this.quests.push(playerQuest); } return playerQuest; } /** * Checks if the player has unlocked the required stage of a quest * @param questId The ID of the quest to find the player's status on. * @param minimumStage The minimum quest stage required, defaults to completed * @return boolean if the player has reached the required stage, if the quest does not exist it defaults to true */ public hasQuestRequirement(questId: string, minimumStage: QuestKey = 'complete'): boolean { if (!questMap[questId]) { logger.warn(`Quest data not found for ${questId}`); return true; } let playerQuest = this.quests.find(quest => quest.questId === questId); if (!playerQuest) { playerQuest = new PlayerQuest(questId); this.quests.push(playerQuest); } return playerQuest.progress === minimumStage || playerQuest.progress >= minimumStage; } /** * Sets a player's quest progress to the specified value. * @param questId The ID of the quest to set the progress of. * @param progress The progress to set the quest to. */ public setQuestProgress(questId: string, progress: QuestKey): void { const questData = findQuest(questId); if (!questData) { logger.warn(`Quest data not found for ${questId}`); return; } let playerQuest = this.quests.find(quest => quest.questId === questId); if (!playerQuest) { playerQuest = new PlayerQuest(questId); this.quests.push(playerQuest); } if (playerQuest.progress === 0 && !playerQuest.complete) { playerQuest.progress = progress; this.modifyWidget(widgets.questTab, { childId: questData.questTabId, textColor: colors.yellow }); } else if (!playerQuest.complete && progress === 'complete') { playerQuest.complete = true; playerQuest.progress = 'complete'; this.outgoingPackets.updateClientConfig(widgetScripts.questPoints, questData.points + this.getQuestPoints()); this.modifyWidget(widgets.questReward, { childId: 2, text: `You have completed ${questData.name}!` }); this.modifyWidget(widgets.questReward, { childId: 8, text: `${questData.points} Quest Point${questData.points > 1 ? 's' : ''}`, }); for (let i = 0; i < 5; i++) { if (i >= questData.onComplete.questCompleteWidget.rewardText.length) { this.modifyWidget(widgets.questReward, { childId: 9 + i, text: '' }); } else { this.modifyWidget(widgets.questReward, { childId: 9 + i, text: questData.onComplete.questCompleteWidget.rewardText[i], }); } } if (questData.onComplete.questCompleteWidget.itemId) { const cacheItemData = filestore.configStore.itemStore.getItem(questData.onComplete.questCompleteWidget.itemId); if (cacheItemData && cacheItemData.model2d.widgetModel) { this.outgoingPackets.updateWidgetModel1(widgets.questReward, 3, cacheItemData.model2d.widgetModel); } } else if (questData.onComplete.questCompleteWidget.modelId) { this.outgoingPackets.updateWidgetModel1(widgets.questReward, 3, questData.onComplete.questCompleteWidget.modelId); } this.outgoingPackets.setWidgetModelRotationAndZoom( widgets.questReward, 3, questData.onComplete.questCompleteWidget.modelRotationX || 0, questData.onComplete.questCompleteWidget.modelRotationY || 0, questData.onComplete.questCompleteWidget.modelZoom || 0, ); this.interfaceState.openWidget(widgets.questReward, { slot: 'screen', multi: false, }); this.modifyWidget(widgets.questTab, { childId: questData.questTabId, textColor: colors.green }); if (questData.onComplete.giveRewards) { questData.onComplete.giveRewards(this); } } else { playerQuest.progress = progress; } } /** * Modifies the specified widget using the provided options. * @param widgetId The widget id of the widget to modify. * @param options The options with which to modify the widget. */ public modifyWidget(widgetId: number, options: { childId?: number; text?: string; hidden?: boolean; textColor?: number }): void { const { childId, text, hidden, textColor } = options; if (childId !== undefined) { if (text !== undefined) { this.outgoingPackets.updateWidgetString(widgetId, childId, text); } if (hidden !== undefined) { this.outgoingPackets.toggleWidgetVisibility(widgetId, childId, hidden); } if (textColor !== undefined) { const { r, g, b } = hexToRgb(textColor); this.outgoingPackets.updateWidgetColor(widgetId, childId, rgbTo16Bit(r, g, b)); } } } /** * Sets the player's specified sidebar widget to the given widget id. * @param sidebarId The sidebar to change. * @param widgetId The widget to insert into the sidebar. */ public setSidebarWidget(sidebarId: SidebarTab, widgetId: number | null): void { this.outgoingPackets.sendTabWidget(sidebarId, widgetId || null); } /** * Plays the given song for the player. * @param songId The id of the song to play. */ public playSong(songId: number): void { const musicTrack = findMusicTrack(songId); if (!musicTrack) { logger.warn(`Music track not found for id ${songId}`); return; } this.modifyWidget(widgets.musicPlayerTab, { childId: 177, text: musicTrack.songName, textColor: colors.green, }); this.savedMetadata['currentSongIdPlaying'] = songId; this.outgoingPackets.playSong(songId); } /** * Plays a sound for this specific player. * @param soundId The id of the sound effect. * @param volume The volume to play the sound at; defaults to 10 (max). * @param delay The delay after which to play the sound; defaults to 0 (no delay). */ public playSound(soundId: number, volume: number = 10, delay: number = 0): void { this.outgoingPackets.playSound(soundId, volume, delay); } /** * Sends a message to the player via the chat-box. * @param messages The single message or array of lines to send to the player. * @param showDialogue Whether or not to show the message in a "Click to continue" dialogue. * @returns A Promise that resolves when the player has clicked the "click to continue" button or * after their chat messages have been sent. */ public async sendMessage(messages: string | string[], showDialogue?: boolean): Promise; /** * Sends a message to the player via the chat-box (and the debug console if specified). * @param messages The single message or array of lines to send to the player. * @param options A list of options to provide for sending the message - includes values for `dialogue` and `console` * to enable sending the message as a dialogue message and/or adding the message to the debug console. * @returns A Promise that resolves when the player has clicked the "click to continue" button or * after their chat messages have been sent. */ public async sendMessage(messages: string | string[], options?: SendMessageOptions): Promise; public async sendMessage(messages: string | string[], options?: boolean | SendMessageOptions): Promise { if (!Array.isArray(messages)) { messages = [messages]; } let showDialogue = false; let showInConsole = false; if (options) { if (typeof options === 'boolean') { showDialogue = true; } else { showDialogue = options.dialogue || false; showInConsole = options.console || false; } } if (!showDialogue) { messages.forEach(message => this.outgoingPackets.chatboxMessage(message)); } else { for (let i = 0; i < messages.length; i++) { messages[i] = messages[i]?.trim() || ''; } return await dialogue([this], [text => (messages as string[]).join(' ')]); } if (showInConsole) { messages.forEach(message => this.outgoingPackets.consoleMessage(message)); } return true; } /** * Instantly teleports the player to the specified location. * @param newPosition The player's new position. * @param updateRegion Whether or not to sync the player's map region with their client. Defaults to true. */ public teleport(newPosition: Position, updateRegion: boolean = true): void { this.walkingQueue.clear(); const originalPosition = this.position.copy(); this.metadata.lastPosition = originalPosition; this.position = newPosition; this.metadata.teleporting = true; this.updateFlags.mapRegionUpdateRequired = updateRegion; this.lastMapRegionUpdatePosition = newPosition; const oldChunk = activeWorld.chunkManager.getChunkForWorldPosition(originalPosition); const newChunk = activeWorld.chunkManager.getChunkForWorldPosition(newPosition); if (!oldChunk.equals(newChunk)) { oldChunk.removePlayer(this); newChunk.addPlayer(this); this.metadata.updateChunk = { newChunk, oldChunk }; if (updateRegion) { this.actionPipeline.call('region_change', regionChangeActionFactory(this, originalPosition, newPosition, true)); } } } public canMove(): boolean { if (this.metadata?.castingStationarySpell) { return false; } return true; } public removeFirstItem(item: number | Item): number { const slot = this.inventory.removeFirst(item); if (slot === -1) { return -1; } this.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, slot, null); return slot; } public hasCoins(amount: number): number { return this.inventory.items.findIndex(item => item !== null && item.itemId === itemIds.coins && item.amount >= amount); } public removeItem(slot: number): void { this.inventory.remove(slot); this.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, slot, null); } public giveItem(item: number | Item | string): boolean { const addedItem = this.inventory.add(item); if (addedItem === null) { return false; } this.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, addedItem.slot, addedItem.item); return true; } public hasItemOnPerson(item: number | Item): boolean { return this.hasItemInInventory(item) || this.isItemEquipped(item); } /** * Updates the player's carry weight based off of their held items (inventory + equipment). * @param force Whether or not to force send an updated carry weight to the game client. */ public updateCarryWeight(force: boolean = false): void { const oldWeight = this._carryWeight; this._carryWeight = Math.round(this.inventory.weight() + this.equipment.weight()); if (oldWeight !== this._carryWeight || force) { this.outgoingPackets.updateCarryWeight(this._carryWeight); } } /** * Updates a player's client settings based off of which setting button they've clicked. * @param buttonId The ID of the setting button. * @TODO refactor to better match the 400+ widget system */ public settingChanged(buttonId: number): void { const settingsMappings = { 0: { setting: 'runEnabled', value: !this.settings['runEnabled'] }, 1: { setting: 'chatEffectsEnabled', value: !this.settings['chatEffectsEnabled'] }, 2: { setting: 'splitPrivateChatEnabled', value: !this.settings['splitPrivateChatEnabled'] }, 3: { setting: 'twoMouseButtonsEnabled', value: !this.settings['twoMouseButtonsEnabled'] }, 4: { setting: 'acceptAidEnabled', value: !this.settings['acceptAidEnabled'] }, // 5 is house options // 6 is unknown, might not even exist 7: { setting: 'screenBrightness', value: 1 }, 8: { setting: 'screenBrightness', value: 2 }, 9: { setting: 'screenBrightness', value: 3 }, 10: { setting: 'screenBrightness', value: 4 }, 11: { setting: 'musicVolume', value: 4 }, 12: { setting: 'musicVolume', value: 3 }, 13: { setting: 'musicVolume', value: 2 }, 14: { setting: 'musicVolume', value: 1 }, 15: { setting: 'musicVolume', value: 0 }, 16: { setting: 'soundEffectVolume', value: 4 }, 17: { setting: 'soundEffectVolume', value: 3 }, 18: { setting: 'soundEffectVolume', value: 2 }, 19: { setting: 'soundEffectVolume', value: 1 }, 20: { setting: 'soundEffectVolume', value: 0 }, 29: { setting: 'areaEffectVolume', value: 4 }, 30: { setting: 'areaEffectVolume', value: 3 }, 31: { setting: 'areaEffectVolume', value: 2 }, 32: { setting: 'areaEffectVolume', value: 1 }, 33: { setting: 'areaEffectVolume', value: 0 }, // 150: {setting: 'autoRetaliateEnabled', value: true}, // 151: {setting: 'autoRetaliateEnabled', value: false} }; if (!settingsMappings[buttonId]) { return; } const config = settingsMappings[buttonId]; this.settings[config.setting] = config.value; } /** * Updates the player's combat bonuses based off of their equipped items. */ public updateBonuses(): void { this.clearBonuses(); for (const item of this._equipment.items) { if (item === null) { continue; } this.addBonuses(item); } } public sendLogMessage(message: string, isConsole: boolean): void { if (isConsole) { this.outgoingPackets.consoleMessage(message); } else { this.outgoingPackets.chatboxMessage(message); } } public sendCommandList(commands: PlayerCommandActionHook[]): void { if (!commands || commands.length === 0) { return; } for (const command of commands) { let strCmd: string; if (Array.isArray(command.commands)) { strCmd = command.commands.join('|'); } else { strCmd = command.commands; } let strHelp: string = ''; if (command.args) { for (const arg of command.args) { if (arg.defaultValue !== undefined) { strHelp = `${strHelp} \\<${arg.name} = ${arg.defaultValue}>`; } else { strHelp = `${strHelp} \\<${arg.name}>`; } } } this.outgoingPackets.sendConsoleCommand(strCmd, strHelp); } } public isItemEquipped(item: number | Item | string): boolean { if (typeof item === 'string') { item = findItem(item)?.gameId || 0; if (!item) { return false; } } return this._equipment.has(item); } public getEquippedItem(equipmentSlot: EquipmentSlot): Item | null { return this.equipment.items[equipmentIndex(equipmentSlot)] || null; } /** * Check if a player can equip an item * @param item either an ItemDetails instance or the string id of the item to be checked * @return {equipable: boolean, missingRequirements: string[]} equipable is false if for any reason the item can not * be equipped, if it can not be equipped, a list of reasons are attached as the missingRequirements array * * defaults to equipable=true if the item string id does not exist */ public canEquipItem(item: ItemDetails | string): { equipable: boolean; missingRequirements?: string[] } { if (typeof item === 'string') { item = itemMap[item]; if (!item) { return { equipable: true }; } } const missingRequirements: string[] = []; const requirements = item.equipmentData?.requirements; if (!requirements) return { equipable: true }; missingRequirements.push( ...Object.entries(requirements.skills || {}) .filter(([skill, level]) => !this.skills.hasLevel(skill as SkillName, level)) .map(([skill, level]) => `You need to be at least level ${level} ${skill} to equip this item.`), ...Object.entries(requirements.quests || {}) .filter(([quest, stage]) => this.hasQuestRequirement(quest, stage)) .map( ([quest]) => `You must progress further in the ${quest.replace(/^([a-z]+:)/gm, '').replace(/_/g, ' ')} quest to equip this item.`, ), ); return { equipable: missingRequirements.length === 0, missingRequirements: missingRequirements }; } public equipItem(itemId: number, itemSlot: number, slot: EquipmentSlot | number): boolean { const itemToEquip = getItemFromContainer(itemId, itemSlot, this.inventory); if (!itemToEquip) { // The specified item was not found in the specified slot. return false; } let slotIndex: number; if (typeof slot === 'number') { slotIndex = slot; slot = getEquipmentSlot(slotIndex); } else { slotIndex = equipmentIndex(slot); } const itemToUnequip = this.equipment.items[slotIndex]; let shouldUnequipOffHand = false; let shouldUnequipMainHand = false; const itemDetails = findItem(itemId); if (!itemDetails || !itemDetails.equipmentData || !itemDetails.equipmentData.equipmentSlot) { this.sendMessage(`Unable to equip item ${itemId}: Missing equipment data.`); return false; } const equippable = this.canEquipItem(itemDetails); if (!equippable.equipable) { if (equippable.missingRequirements) { equippable.missingRequirements.forEach(async s => this.sendMessage(s)); } return false; } if (itemDetails && itemDetails.equipmentData) { if (itemDetails.equipmentData.equipmentType === 'two_handed') { shouldUnequipOffHand = true; } const mainHandEquipped = this.getEquippedItem('main_hand'); if (slot === 'off_hand' && mainHandEquipped) { const mainHandItemData = findItem(mainHandEquipped.itemId); if (mainHandItemData && mainHandItemData.equipmentData && mainHandItemData.equipmentData.equipmentType === 'two_handed') { shouldUnequipMainHand = true; } } } if (itemToUnequip) { if (shouldUnequipOffHand && !this.unequipItem('off_hand', false)) { return false; } if (shouldUnequipMainHand && !this.unequipItem('main_hand', false)) { return false; } this.actionPipeline.call('equipment_change', this, itemToUnequip.itemId, 'UNEQUIP', slot); this.equipment.remove(slotIndex, false); this.inventory.remove(itemSlot, false); this.equipment.set(slotIndex, itemToEquip); this.inventory.set(itemSlot, itemToUnequip); } else { this.equipment.set(slotIndex, itemToEquip); this.inventory.remove(itemSlot); if (shouldUnequipOffHand) { this.unequipItem('off_hand'); } if (shouldUnequipMainHand) { this.unequipItem('main_hand'); } } this.actionPipeline.call('equipment_change', this, itemId, 'equip', slot); this.equipmentChanged(); return true; } public equipmentChanged(): void { this.updateBonuses(); // @TODO change packets to only update modified container slots this.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, this.inventory); this.outgoingPackets.sendUpdateAllWidgetItems(widgets.equipment, this.equipment); if (this.interfaceState.widgetOpen('screen', widgets.equipmentStats.widgetId)) { this.outgoingPackets.sendUpdateAllWidgetItems(widgets.equipmentStats, this.equipment); this.syncBonuses(); } this.updateFlags.appearanceUpdateRequired = true; } public syncBonuses(): void { [ { id: 108, text: 'Stab', value: this.bonuses.offensive.stab }, { id: 109, text: 'Slash', value: this.bonuses.offensive.slash }, { id: 110, text: 'Crush', value: this.bonuses.offensive.crush }, { id: 111, text: 'Magic', value: this.bonuses.offensive.magic }, { id: 112, text: 'Range', value: this.bonuses.offensive.ranged }, { id: 113, text: 'Stab', value: this.bonuses.defensive.stab }, { id: 114, text: 'Slash', value: this.bonuses.defensive.slash }, { id: 115, text: 'Crush', value: this.bonuses.defensive.crush }, { id: 116, text: 'Magic', value: this.bonuses.defensive.magic }, { id: 117, text: 'Range', value: this.bonuses.defensive.ranged }, { id: 119, text: 'Strength', value: this.bonuses.skill.strength }, { id: 120, text: 'Prayer', value: this.bonuses.skill.prayer }, ].forEach(bonus => this.modifyWidget(widgets.equipmentStats.widgetId, { childId: bonus.id, text: `${bonus.text}: ${(bonus.value || 0) > 0 ? `+${bonus.value}` : bonus.value}`, }), ); } public unequipItem(slot: EquipmentSlot | number, updateRequired: boolean = true): boolean { const inventorySlot = this.inventory.getFirstOpenSlot(); if (inventorySlot === -1) { this.sendMessage(`You don't have enough free space to do that.`); return false; } let slotIndex: number; if (typeof slot === 'number') { slotIndex = slot; slot = getEquipmentSlot(slotIndex); } else { slotIndex = equipmentIndex(slot); } const itemInSlot = this.equipment.items[slotIndex]; if (!itemInSlot) { return true; } this.actionPipeline.call('equipment_change', this, itemInSlot.itemId, 'unequip', slot); this.equipment.remove(slotIndex); this.inventory.set(inventorySlot, itemInSlot); if (updateRequired) { this.equipmentChanged(); } return true; } /** * Transform's the player's appearance into the specified NPC. * @param npc The NPC to copy the appearance of, or null to reset. */ public transformInto(npc: NpcDetails | string | number | null): void { if (!npc) { delete this.savedMetadata.npcTransformation; this.updateFlags.appearanceUpdateRequired = true; return; } if (typeof npc !== 'number') { if (typeof npc === 'string') { if (npc.indexOf(':') !== -1) { npc = npcIdMap[npc]; } else { npc = parseInt(npc, 10); } } else { npc = npc.gameId; } } if (!npc) { logger.error(`NPC not found.`); return; } this.savedMetadata.npcTransformation = npc; this.updateFlags.appearanceUpdateRequired = true; } /** * Returns the morphed NPC details for a specific player based on his client settings * @param originalNpc */ public getMorphedNpcDetails(originalNpc: Npc) { if (!originalNpc.childrenIds) { return null; } let morphIndex: number; if (originalNpc.varbitId !== -1) { morphIndex = getVarbitMorphIndex(originalNpc.varbitId, this.metadata.configs); } else if (originalNpc.settingId !== -1) { morphIndex = this.metadata.configs && this.metadata.configs[originalNpc.settingId] ? this.metadata.configs[originalNpc.settingId] : 0; } else { logger.warn( `Tried to fetch a child NPC index, but but no varbitId or settingId were found in the NPC details. NPC: ${originalNpc.id}, childrenIDs: ${originalNpc.childrenIds}`, ); return null; } return findNpc(originalNpc.childrenIds[morphIndex]); } public equals(player: Player): boolean { return this.worldIndex === player.worldIndex && this.username === player.username && this.clientUuid === player.clientUuid; } private inventoryUpdated(event: ContainerUpdateEvent): void { if (event.type === 'CLEAR_ALL') { this.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, this.inventory); } else if (event.type === 'ADD') { if (event.slot !== undefined && event.item !== undefined) { this.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, event.slot, event.item); } else { logger.error(`Inventory update event was missing slot or item.`, event); } } this.updateCarryWeight(); } /** * Sends chunk updates to notify the client of added & removed location objects * @param chunks The chunks to update. */ private sendChunkUpdates(chunks: Chunk[]): void { const instance = this.instance; if (!instance) { logger.error(`Player ${this.username} tried to send chunk updates without an instance.`); return; } chunks.forEach(chunk => { this.outgoingPackets.clearChunk(chunk); const chunkUpdateItems: ChunkUpdateItem[] = []; const chunkModifications = instance.getInstancedChunk(chunk.position.x, chunk.position.y, chunk.position.level) || null; const personalChunkModifications = this.personalInstance?.getInstancedChunk(chunk.position.x, chunk.position.y, chunk.position.level) || null; this.findChunkUpdates(chunkModifications?.mods, chunkUpdateItems); this.findChunkUpdates(personalChunkModifications?.mods, chunkUpdateItems); if (chunkUpdateItems.length !== 0) { this.outgoingPackets.updateChunk(chunk, chunkUpdateItems); } }); } private findChunkUpdates(chunkMods: Map, chunkUpdateItems: ChunkUpdateItem[]): void { if (!chunkMods) { return; } Array.from(chunkMods.values()).forEach(worldMods => { worldMods.hiddenObjects?.forEach(object => chunkUpdateItems.push({ object, type: 'REMOVE' })); worldMods.spawnedObjects?.forEach(object => chunkUpdateItems.push({ object, type: 'ADD' })); worldMods.worldItems?.forEach(worldItem => { if (!worldItem.owner || worldItem.owner.equals(this)) { chunkUpdateItems.push({ worldItem, type: 'ADD' }); } }); }); } /** * Updates the player's quest tab progress. */ private updateQuestTab(): void { this.outgoingPackets.updateClientConfig(widgetScripts.questPoints, this.getQuestPoints()); if (!questMap) { return; } Object.keys(questMap).forEach(questKey => { const questData = questMap[questKey]; const playerQuest = this.quests.find(quest => quest.questId === questData.id); let color: number; if (playerQuest?.complete || playerQuest?.progress === 'complete') { // Quest complete, regardless of progress color = colors.green; } else if ((playerQuest?.progress || 0) > 0) { // Quest in progress, not yet complete but progress is greater than 0 color = colors.yellow; } else { // Everything else failed, so quest hasn't been started yet color = colors.red; } this.modifyWidget(widgets.questTab, { childId: questData.questTabId, textColor: color }); }); } /** * Updates the player's music tab progress. */ private updateMusicTab(): void { if (!this.savedMetadata['currentSongIdPlaying']) { this.savedMetadata['currentSongIdPlaying'] = findSongIdByRegionId( activeWorld.chunkManager.getRegionIdForWorldPosition(this.position), ); } if (this.settings.musicPlayerMode === MusicPlayerMode.MANUAL) { this.playSong(this.savedMetadata['currentSongIdPlaying']); } Object.keys(musicRegions).forEach(key => { const musicData = musicRegions[key]; let color = colors.red; if (this.musicTracks.includes(musicData.songId)) { color = colors.green; } this.modifyWidget(widgets.musicPlayerTab, { childId: musicData.musicTabButtonId, textColor: color }); }); } private addBonuses(item: Item): void { const itemData = findItem(item.itemId); if (!itemData || !itemData.equipmentData) { return; } const offensiveBonuses = itemData.equipmentData.offensiveBonuses; const defensiveBonuses = itemData.equipmentData.defensiveBonuses; const skillBonuses = itemData.equipmentData.skillBonuses; if (offensiveBonuses) { ['speed', 'stab', 'slash', 'crush', 'magic', 'ranged'].forEach( bonus => (this.bonuses.offensive[bonus] += !offensiveBonuses[bonus] ? 0 : offensiveBonuses[bonus]), ); } if (defensiveBonuses) { ['stab', 'slash', 'crush', 'magic', 'ranged'].forEach( bonus => (this.bonuses.defensive[bonus] += !defensiveBonuses[bonus] ? 0 : defensiveBonuses[bonus]), ); } if (skillBonuses) { ['strength', 'prayer'].forEach(bonus => (this.bonuses.skill[bonus] += !skillBonuses[bonus] ? 0 : skillBonuses[bonus])); } } private loadSaveData(): void { const playerSave = loadPlayerSave(this.username); const firstTimePlayer = playerSave === null; this.firstTimePlayer = firstTimePlayer; if (!firstTimePlayer) { if (playerSave.savedMetadata) { this.savedMetadata = playerSave.savedMetadata; } // Existing player logging in this.position = new Position(playerSave.position.x, playerSave.position.y, playerSave.position.level); if (playerSave.inventory && playerSave.inventory.length !== 0) { this.inventory.setAll(playerSave.inventory); } if (playerSave.bank && playerSave.bank.length !== 0) { this.bank.setAll(playerSave.bank); } if (playerSave.equipment && playerSave.equipment.length !== 0) { this.equipment.setAll(playerSave.equipment); } if (playerSave.skills && playerSave.skills.length !== 0) { this.skills.values = playerSave.skills; } this._appearance = playerSave.appearance; this._settings = playerSave.settings; this._rights = playerSave.rights || Rights.USER; const lastLogin = playerSave.lastLogin?.date; if (!lastLogin) { this._loginDate = new Date(); } else { this._loginDate = new Date(lastLogin); } if (playerSave.questList) { this.quests = playerSave.questList; } if (playerSave.musicTracks) { this.musicTracks = playerSave.musicTracks; } if (playerSave.achievements) { this.achievements = playerSave.achievements; } if (playerSave.friendsList) { this.friendsList = playerSave.friendsList; } if (playerSave.ignoreList) { this.ignoreList = playerSave.ignoreList; } this._lastAddress = playerSave.lastLogin?.address || (this._socket?.address() as AddressInfo)?.address || '127.0.0.1'; } else { // Brand new player logging in this.position = new Position(3231, 3239); this._appearance = defaultAppearance(); this._rights = Rights.USER; this.savedMetadata = { tutorialProgress: 0, tutorialComplete: false, }; } if (!this._settings) { this._settings = defaultSettings(); } } public set position(position: Position) { super.position = position; if (this.quadtreeKey !== null) { activeWorld.playerTree.remove(this.quadtreeKey); } this.quadtreeKey = { x: position.x, y: position.y, actor: this }; activeWorld.playerTree.push(this.quadtreeKey); } public get position(): Position { return super.position; } public get socket(): Socket { return this._socket; } public get inCipher(): Isaac { return this._inCipher; } public get outCipher(): Isaac { return this._outCipher; } public get outgoingPackets(): OutboundPacketHandler { return this._outgoingPackets; } public get loginDate(): Date { return this._loginDate; } public get lastAddress(): string { return this._lastAddress; } public get rights(): Rights { return this._rights; } public get appearance(): Appearance { return this._appearance; } public set appearance(value: Appearance) { this._appearance = value; } public get equipment(): ItemContainer { return this._equipment; } public get carryWeight(): number { return this._carryWeight; } public get settings(): PlayerSettings { return this._settings; } public get nearbyChunks(): Chunk[] { return this._nearbyChunks; } public get instance(): WorldInstance { return super.instance; } public set instance(value: WorldInstance | null) { if (this.instance?.instanceId) { this.instance.removePlayer(this); } if (value) { value.addPlayer(this); } this._instance = value; } } ================================================ FILE: src/engine/world/actor/player/private-messaging.ts ================================================ import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; export enum PrivateChatMode { PUBLIC = 0, FRIENDS = 1, OFF = 2, } export class PrivateMessaging { public static friendAdded(player: Player, friendName: string): void { friendName = friendName.toLowerCase(); const friend = activeWorld?.findPlayer(friendName); } public static friendRemoved(player: Player, friendName: string): void { friendName = friendName.toLowerCase(); const playerPrivateChatMode = player.settings.privateChatMode; const playerUsername = player.username.toLowerCase(); if (playerPrivateChatMode !== PrivateChatMode.PUBLIC) { const friend = activeWorld?.findPlayer(friendName); if (friend && friend.friendsList.indexOf(playerUsername) !== -1) { // Friend being removed is currently online - update their friends list if they have this player added friend.outgoingPackets.updateFriendStatus(player.username, 0); } } } /** * Updates a specific player's entire friends list. * @param player The player to update. */ public static updateFriendsList(player: Player): void { const friends = player.friendsList; if (friends && friends.length !== 0) { const onlineFriends = activeWorld.playerList.filter(p => p && friends.indexOf(p.username.toLowerCase()) !== -1); friends.forEach(friendName => { const friend = onlineFriends.find(p => p.username.toLowerCase() === friendName); if (!friend || friend.settings.privateChatMode === PrivateChatMode.OFF) { player.outgoingPackets.updateFriendStatus(friendName, 0); } else { if (friend.settings.privateChatMode === PrivateChatMode.PUBLIC) { player.outgoingPackets.updateFriendStatus(friendName, 1); } else { const otherPlayerFriendsList = friend.friendsList; player.outgoingPackets.updateFriendStatus( friendName, otherPlayerFriendsList.indexOf(player.username.toLowerCase()) !== -1 ? 1 : 0, ); } } }); } } /** * Called when the provided player logs in or changes their private chat mode. * @param player The player logging in. * @param updating If the friends list status is being updated or set initially. */ public static playerPrivateChatModeChanged(player: Player, updating: boolean = true): void { const playerName = player.username.toLowerCase(); const playerPrivateChatMode: PrivateChatMode = player.settings.privateChatMode; const playerFriendsList = player.friendsList || []; if (playerPrivateChatMode !== PrivateChatMode.OFF || updating) { const otherPlayers = activeWorld.playerList.filter(p => p && p.friendsList.indexOf(playerName) !== -1); if (otherPlayers && otherPlayers.length !== 0) { otherPlayers.forEach(otherPlayer => { let worldId = playerPrivateChatMode === PrivateChatMode.OFF ? 0 : 1; if (playerPrivateChatMode === PrivateChatMode.FRIENDS) { if (playerFriendsList.findIndex(playerName => playerName === otherPlayer.username.toLowerCase()) === -1) { worldId = 0; } } otherPlayer.outgoingPackets.updateFriendStatus(player.username, worldId); }); } } } public static playerLoggedIn(player: Player): void { PrivateMessaging.playerPrivateChatModeChanged(player, false); PrivateMessaging.updateFriendsList(player); } } ================================================ FILE: src/engine/world/actor/player/quest.ts ================================================ import type { QuestCompletion, QuestJournalHandler } from '@engine/config/quest-config'; export class Quest { public id: string; public questTabId: number; public name: string; public points: number; public journalHandler: QuestJournalHandler; public onComplete; public constructor(options: { id: string; questTabId: number; name: string; points: number; journalHandler: QuestJournalHandler; onComplete: QuestCompletion; }) { this.id = options.id; this.questTabId = options.questTabId; this.name = options.name; this.points = options.points; this.journalHandler = options.journalHandler; this.onComplete = options.onComplete; } } ================================================ FILE: src/engine/world/actor/player/sync/actor-sync.ts ================================================ import type { ByteBuffer } from '@runejs/common'; import type { Packet } from '@engine/net/packet'; import { activeWorld } from '@engine/world'; import type { Npc } from '@engine/world/actor/npc'; import type { Position } from '@engine/world/position'; import type { QuadtreeKey } from '@engine/world/world'; import type { Actor } from '../../actor'; import { isNpc, isPlayer } from '../../util'; import type { Player } from '../player'; /** * Handles the registration of nearby NPCs or Players for the specified player. */ export function registerNewActors( packet: Packet, player: Player, trackedActors: Actor[], nearbyActors: QuadtreeKey[], registerActor: (actor: Actor) => void, ): void { if (trackedActors.length >= 255) { return; } // We only want to send about 20 new actors at a time, to help save some memory and computing time // Any remaining players or npcs will be automatically picked up by subsequent updates let newActors: QuadtreeKey[] = nearbyActors.filter(m1 => !trackedActors.includes(m1.actor)); if (newActors.length > 50) { // We also sort the list of players or npcs here by how close they are to the current player if there are more than 80, so we can render the nearest first newActors = newActors .sort((a, b) => player.position.distanceBetween(a.actor.position) - player.position.distanceBetween(b.actor.position)) .slice(0, 50); } for (const newActor of newActors) { const nearbyActor = newActor.actor; if (isPlayer(nearbyActor)) { if (player.equals(nearbyActor)) { // Other player is actually this player! continue; } if (!activeWorld.playerOnline(nearbyActor)) { // Other player is no longer in the game world continue; } } else if (isNpc(nearbyActor)) { if (!activeWorld.npcExists(nearbyActor)) { // Npc is no longer in the game world continue; } } if (trackedActors.findIndex(m => m.equals(nearbyActor)) !== -1) { // Npc or other player is already tracked by this player continue; } if (!nearbyActor.position.withinViewDistance(player.position)) { // Player or npc is still too far away to be worth rendering // Also - values greater than 15 and less than -15 are too large, or too small, to be sent via 5 bits (max length of 32) continue; } // Only 255 players or npcs are able to be rendered at a time // To help performance, we limit it to 200 here if (trackedActors.length >= 255) { return; } registerActor(nearbyActor); } } /** * Handles synchronization of nearby NPCs or Players for the specified player. */ export function syncTrackedActors( packet: Packet, playerPosition: Position, appendUpdateMaskData: (actor: Actor) => void, trackedActors: Actor[], nearbyActors: QuadtreeKey[], ): Actor[] { packet.putBits(8, trackedActors.length); // Tracked actor count if (trackedActors.length === 0) { return []; } const existingTrackedActors: Actor[] = []; for (let i = 0; i < trackedActors.length; i++) { const trackedActor: Actor = trackedActors[i]; let exists = true; if (isPlayer(trackedActor)) { if (!activeWorld.playerOnline(trackedActor)) { exists = false; } } else { if (!activeWorld.npcExists(trackedActor as Npc)) { exists = false; } } if ( exists && nearbyActors.findIndex(m => m.actor.equals(trackedActor)) !== -1 && trackedActor.position.withinViewDistance(playerPosition) && !trackedActor.metadata.teleporting ) { appendMovement(trackedActor, packet); appendUpdateMaskData(trackedActor); existingTrackedActors.push(trackedActor); } else { // De-register the actor if they are no longer nearby packet.putBits(1, 1); packet.putBits(2, 3); } } return existingTrackedActors; } /** * Appends movement data of a player or NPC to the specified synchronization packet. */ export function appendMovement(actor: Actor, packet: ByteBuffer): void { if (actor.walkDirection !== -1) { // Actor is walking/running packet.putBits(1, 1); // Update required if (actor.runDirection === -1) { // Actor is walking packet.putBits(2, 1); // Actor walking packet.putBits(3, actor.walkDirection); } else { // Actor is running packet.putBits(2, 2); // Actor running packet.putBits(3, actor.walkDirection); packet.putBits(3, actor.runDirection); } packet.putBits(1, actor.updateFlags.updateBlockRequired ? 1 : 0); // Whether or not an update flag block follows } else { // Did not move if (actor.updateFlags.updateBlockRequired) { packet.putBits(1, 1); // Update required packet.putBits(2, 0); // Signify the player did not move } else { packet.putBits(1, 0); // No update required } } } export abstract class SyncTask { public abstract execute(): Promise; } ================================================ FILE: src/engine/world/actor/player/sync/npc-sync-task.ts ================================================ import { ByteBuffer } from '@runejs/common'; import { Packet, PacketType } from '@engine/net/packet'; import { activeWorld } from '@engine/world'; import type { Npc } from '@engine/world/actor/npc'; import { isPlayer } from '@engine/world/actor/util'; import type { Player } from '../player'; import { SyncTask, registerNewActors, syncTrackedActors } from './actor-sync'; /** * Handles the chonky npc synchronization packet for a specific player. */ export class NpcSyncTask extends SyncTask { private readonly player: Player; public constructor(player: Player) { super(); this.player = player; } public async execute(): Promise { return new Promise(resolve => { const npcUpdatePacket: Packet = new Packet(128, PacketType.DYNAMIC_LARGE); npcUpdatePacket.openBitBuffer(); const updateMaskData = new ByteBuffer(5000); const nearbyNpcs = activeWorld.npcTree .colliding({ x: this.player.position.x - 15, y: this.player.position.y - 15, width: 32, height: 32, }) .filter(collision => { const npc = (collision?.actor as Npc) || null; return npc && npc.initialized && npc.instanceId === this.player.instance.instanceId; }); this.player.trackedNpcs = syncTrackedActors( npcUpdatePacket, this.player.position, actor => this.appendUpdateMaskData(actor as Npc, updateMaskData), this.player.trackedNpcs, nearbyNpcs, ) as Npc[]; registerNewActors(npcUpdatePacket, this.player, this.player.trackedNpcs, nearbyNpcs, actor => { const newNpc = actor as Npc; const positionOffsetX = newNpc.position.x - this.player.position.x; const positionOffsetY = newNpc.position.y - this.player.position.y; // Add npc to this player's list of tracked npcs this.player.trackedNpcs.push(newNpc); // Notify the client of the new npc and their worldIndex npcUpdatePacket.putBits(15, newNpc.worldIndex); npcUpdatePacket.putBits(3, newNpc.faceDirection); npcUpdatePacket.putBits(5, positionOffsetX); // World Position X axis offset relative to the player npcUpdatePacket.putBits(5, positionOffsetY); // World Position Y axis offset relative to the player npcUpdatePacket.putBits(1, newNpc.updateFlags.updateBlockRequired ? 1 : 0); // Update is required npcUpdatePacket.putBits(1, 1); // Discard client walking queues npcUpdatePacket.putBits(13, newNpc.id); this.appendUpdateMaskData(newNpc, updateMaskData); }); if (updateMaskData.writerIndex !== 0) { npcUpdatePacket.putBits(15, 32767); npcUpdatePacket.closeBitBuffer(); npcUpdatePacket.putBytes(updateMaskData.flipWriter()); } else { // No npc updates were appended, so just end the packet here npcUpdatePacket.closeBitBuffer(); } this.player.outgoingPackets.queue(npcUpdatePacket, true); resolve(); }); } /** * As of 2024-09-03 this has been modified to include an extra `short` if * any updates are required. This extra `short` includes the `worldIndex` * for this NPC, which helps the client figure out which NPC to apply the * updates to. * * For the sake of efficiency, this `short` will only be added once, and it * will always be added after the first updated value's data is processed. * * Make sure to keep your client updated so that it can handle it. */ private appendUpdateMaskData(npc: Npc, updateMaskData: ByteBuffer): void { const updateFlags = npc.updateFlags; if (!updateFlags.updateBlockRequired) { return; } let mask = 0; if (updateFlags.damage !== null) { mask |= 0x1; } if (updateFlags.appearanceUpdateRequired) { mask |= 0x80; } if (updateFlags.faceActor !== null) { mask |= 0x4; } if (updateFlags.chatMessages.length !== 0) { mask |= 0x40; } if (updateFlags.facePosition !== null) { mask |= 0x8; } if (updateFlags.animation) { mask |= 0x10; } if (updateFlags.graphics) { mask |= 0x20; } updateMaskData.put(mask, 'BYTE'); let alreadyPutWorldIndex = false; const putWorldIndex = () => { if (alreadyPutWorldIndex) { return; } updateMaskData.put(npc.worldIndex, 'SHORT'); alreadyPutWorldIndex = true; }; if (updateFlags.damage !== null) { const damage = updateFlags.damage; updateMaskData.put(damage.damageDealt); updateMaskData.put(damage.damageType.valueOf()); updateMaskData.put(damage.remainingHitpoints); updateMaskData.put(damage.maxHitpoints); putWorldIndex(); } if (updateFlags.faceActor !== null) { const actor = updateFlags.faceActor; if (actor === 'CLEAR') { // Reset faced actor updateMaskData.put(65535, 'SHORT'); } else { let worldIndex = actor.worldIndex; if (isPlayer(actor)) { // Client checks if index is less than 32768. // If it is, it looks for an NPC. // If it isn't, it looks for a player (subtracting 32768 to find the index). worldIndex += 32768 + 1; } updateMaskData.put(worldIndex, 'SHORT'); } putWorldIndex(); } if (updateFlags.chatMessages.length !== 0) { const message = updateFlags.chatMessages[0]; if (message.message) { updateMaskData.putString(message.message); } else { updateMaskData.putString('Undefined Message'); } putWorldIndex(); } if (updateFlags.appearanceUpdateRequired) { updateMaskData.put(npc.id, 'SHORT'); putWorldIndex(); } if (updateFlags.facePosition) { const position = updateFlags.facePosition; updateMaskData.put(position.x * 2 + 1, 'SHORT'); updateMaskData.put(position.y * 2 + 1, 'SHORT', 'LITTLE_ENDIAN'); putWorldIndex(); } if (updateFlags.animation) { const animation = updateFlags.animation; if (animation === null || animation.id === -1) { // Reset animation updateMaskData.put(65535, 'SHORT'); updateMaskData.put(0); } else { const delay = updateFlags.animation.delay || 0; updateMaskData.put(animation.id, 'SHORT'); updateMaskData.put(delay); } putWorldIndex(); } if (updateFlags.graphics) { const { id, delay = 0, height } = updateFlags.graphics; updateMaskData.put(id, 'SHORT', 'LITTLE_ENDIAN'); updateMaskData.put((height << 16) | (delay & 0xffff), 'INT'); putWorldIndex(); } } } ================================================ FILE: src/engine/world/actor/player/sync/player-sync-task.ts ================================================ import { ByteBuffer } from '@runejs/common'; import { findItem, findNpc } from '@engine/config/config-handler'; import type { EquipmentSlot, EquipmentType, ItemDetails } from '@engine/config/item-config'; import { Packet, PacketType } from '@engine/net/packet'; import { stringToLong } from '@engine/util/strings'; import { activeWorld } from '@engine/world'; import type { UpdateFlags } from '@engine/world/actor/update-flags'; import { isPlayer } from '@engine/world/actor/util'; import type { Player } from '../player'; import { SyncTask, appendMovement, registerNewActors, syncTrackedActors } from './actor-sync'; /** * Handles the chonky player synchronization packet. */ export class PlayerSyncTask extends SyncTask { private readonly player: Player; public constructor(player: Player) { super(); this.player = player; } public async execute(): Promise { return new Promise(resolve => { const updateFlags: UpdateFlags = this.player.updateFlags; const playerUpdatePacket: Packet = new Packet(92, PacketType.DYNAMIC_LARGE); playerUpdatePacket.openBitBuffer(); const updateMaskData = new ByteBuffer(5000); if (updateFlags.mapRegionUpdateRequired || this.player.metadata.teleporting) { playerUpdatePacket.putBits(1, 1); // Update Required playerUpdatePacket.putBits(2, 3); // Map Region changed (movement type - 0=nomove, 1=walk, 2=run, 3=mapchange playerUpdatePacket.putBits(1, this.player.metadata.teleporting ? 1 : 0); // Whether or not the client should discard the current walking queue (1 if teleporting, 0 if not) playerUpdatePacket.putBits(2, this.player.position.level); // Player Height playerUpdatePacket.putBits(1, updateFlags.updateBlockRequired ? 1 : 0); // Whether or not an update flag block follows playerUpdatePacket.putBits(7, this.player.position.chunkLocalX); // Player Local Chunk X playerUpdatePacket.putBits(7, this.player.position.chunkLocalY); // Player Local Chunk Y } else { appendMovement(this.player, playerUpdatePacket); } this.appendUpdateMaskData(this.player, updateMaskData, false); let nearbyPlayers = activeWorld.playerTree .colliding({ x: this.player.position.x - 15, y: this.player.position.y - 15, width: 32, height: 32, }) .filter(collision => collision?.actor && collision.actor.instance === this.player.instance); if (nearbyPlayers.length > 200) { nearbyPlayers = activeWorld.playerTree.colliding({ x: this.player.position.x - 7, y: this.player.position.y - 7, width: 16, height: 16, }); } this.player.trackedPlayers = syncTrackedActors( playerUpdatePacket, this.player.position, actor => this.appendUpdateMaskData(actor as Player, updateMaskData), this.player.trackedPlayers, nearbyPlayers, ) as Player[]; registerNewActors(playerUpdatePacket, this.player, this.player.trackedPlayers, nearbyPlayers, actor => { const newPlayer = actor as Player; const positionOffsetX = newPlayer.position.x - this.player.position.x; const positionOffsetY = newPlayer.position.y - this.player.position.y; // Add other player to this player's list of tracked players this.player.trackedPlayers.push(newPlayer); // Notify the client of the new player and their worldIndex playerUpdatePacket.putBits(11, newPlayer.worldIndex + 1); playerUpdatePacket.putBits(5, positionOffsetX); // World Position X axis offset relative to the main player playerUpdatePacket.putBits(5, positionOffsetY); // World Position Y axis offset relative to the main player playerUpdatePacket.putBits(3, newPlayer.faceDirection); playerUpdatePacket.putBits(1, 1); // Update is required playerUpdatePacket.putBits(1, 1); // Discard client walking queues this.appendUpdateMaskData(newPlayer, updateMaskData, true); }); if (updateMaskData.writerIndex !== 0) { playerUpdatePacket.putBits(11, 2047); playerUpdatePacket.closeBitBuffer(); playerUpdatePacket.putBytes(updateMaskData.flipWriter()); } else { // No player updates were appended, so just end the packet here playerUpdatePacket.closeBitBuffer(); } this.player.outgoingPackets.queue(playerUpdatePacket, true); resolve(); }); } private appendUpdateMaskData(player: Player, updateMaskData: ByteBuffer, forceUpdate?: boolean): void { const updateFlags = player.updateFlags; if (!updateFlags.updateBlockRequired && !forceUpdate) { return; } let mask: number = 0; if (updateFlags.damage !== null) { mask |= 0x100; } if (updateFlags.appearanceUpdateRequired || forceUpdate) { mask |= 0x20; } if (updateFlags.chatMessages.length !== 0) { mask |= 0x8; } if (updateFlags.faceActor !== null) { mask |= 0x4; } if (updateFlags.facePosition) { mask |= 0x10; } if (updateFlags.graphics) { mask |= 0x200; } if (updateFlags.animation !== undefined && updateFlags.animation !== null) { mask |= 0x1; } if (mask >= 0x100) { mask |= 0x2; updateMaskData.put(mask & 0xff); updateMaskData.put(mask >> 8); } else { updateMaskData.put(mask); } if (updateFlags.damage !== null) { const damage = updateFlags.damage; updateMaskData.put(damage.damageDealt); updateMaskData.put(damage.damageType.valueOf()); updateMaskData.put(damage.remainingHitpoints); updateMaskData.put(damage.maxHitpoints); } if (updateFlags.facePosition) { const position = updateFlags.facePosition; updateMaskData.put(position.x * 2 + 1, 'SHORT'); updateMaskData.put(position.y * 2 + 1, 'SHORT', 'LITTLE_ENDIAN'); } if (updateFlags.animation !== undefined && updateFlags.animation !== null) { const animation = updateFlags.animation; if (animation === null || animation.id === -1) { // Reset animation updateMaskData.put(-1, 'SHORT', 'LITTLE_ENDIAN'); updateMaskData.put(0, 'BYTE'); } else { const delay = animation.delay || 0; updateMaskData.put(animation.id, 'SHORT', 'LITTLE_ENDIAN'); updateMaskData.put(delay, 'BYTE'); } } if (updateFlags.faceActor !== null) { if (updateFlags.faceActor === 'CLEAR') { // Reset faced actor updateMaskData.put(65535, 'SHORT'); } else { const actor = updateFlags.faceActor; let worldIndex = actor.worldIndex; if (isPlayer(actor)) { // Client checks if index is less than 32768. // If it is, it looks for an NPC. // If it isn't, it looks for a player (subtracting 32768 to find the index). worldIndex += 32768 + 1; } updateMaskData.put(worldIndex, 'SHORT'); } } if (updateFlags.chatMessages.length !== 0) { const message = updateFlags.chatMessages[0]; if (!message.data) { throw new Error('Chat message data is undefined'); } updateMaskData.put(((message.color || 0 & 0xff) << 8) + (message.effects || 0 & 0xff), 'SHORT'); updateMaskData.put(player.rights.valueOf(), 'BYTE'); updateMaskData.put(message.data.length, 'BYTE'); for (let i = 0; i < message.data.length; i++) { updateMaskData.put(message.data.readInt8(i), 'BYTE'); } } if (updateFlags.appearanceUpdateRequired || forceUpdate) { const equipment = player.equipment; const appearanceData = new ByteBuffer(500); appearanceData.put(player.appearance.gender); // Gender appearanceData.put(-1); // Skull Icon appearanceData.put(-1); // Prayer Icon if (player.savedMetadata.npcTransformation) { appearanceData.put(65535, 'SHORT'); appearanceData.put(player.savedMetadata.npcTransformation, 'SHORT'); } else { for (let i = 0; i < 4; i++) { const item = equipment.items[i]; if (item) { appearanceData.put(0x200 + item.itemId, 'SHORT'); } else { appearanceData.put(0); } } const torsoItem = player.getEquippedItem('torso'); let torsoItemData: ItemDetails | null = null; if (torsoItem) { torsoItemData = findItem(torsoItem.itemId); appearanceData.put(0x200 + torsoItem.itemId, 'SHORT'); } else { appearanceData.put(0x100 + player.appearance.torso, 'SHORT'); } const offHandItem = player.getEquippedItem('off_hand'); if (offHandItem) { appearanceData.put(0x200 + offHandItem.itemId, 'SHORT'); } else { appearanceData.put(0); } if ( torsoItemData && torsoItemData.equipmentData && torsoItemData.equipmentData.equipmentType && torsoItemData.equipmentData.equipmentType === 'full_top' ) { appearanceData.put(0); } else { appearanceData.put(0x100 + player.appearance.arms, 'SHORT'); } this.appendBasicAppearanceItem(appearanceData, player, player.appearance.legs, 'legs'); const headItem = player.getEquippedItem('head'); let helmetType: EquipmentType | null = null; let fullHelmet = false; if (headItem) { const headItemData = findItem(headItem.itemId); if (headItemData && headItemData.equipmentData && headItemData.equipmentData.equipmentType) { helmetType = headItemData.equipmentData.equipmentType; if (helmetType === 'helmet') { fullHelmet = true; } } } if (!headItem || helmetType === 'hat') { appearanceData.put(0x100 + player.appearance.head, 'SHORT'); } else { appearanceData.put(0); } this.appendBasicAppearanceItem(appearanceData, player, player.appearance.hands, 'hands'); this.appendBasicAppearanceItem(appearanceData, player, player.appearance.feet, 'feet'); if (player.appearance.gender === 1 || fullHelmet) { appearanceData.put(0); } else { appearanceData.put(0x100 + player.appearance.facialHair, 'SHORT'); } } [ player.appearance.hairColor, player.appearance.torsoColor, player.appearance.legColor, player.appearance.feetColor, player.appearance.skinColor, ].forEach(color => appearanceData.put(color)); let animations = [ 0x328, // stand 0x337, // stand turn 0x333, // walk 0x334, // turn 180 0x335, // turn 90 0x336, // turn 90 reverse 0x338, // run ]; if (player.savedMetadata.npcTransformation) { const npc = findNpc(player.savedMetadata.npcTransformation); animations = [ npc.animations?.stand || 0x328, // stand npc.animations?.turnAround || 0x337, // stand turn npc.animations?.walk || 0x333, // walk npc.animations?.turnAround || 0x334, // turn 180 npc.animations?.turnRight || 0x335, // turn 90 npc.animations?.turnLeft || 0x336, // turn 90 reverse npc.animations?.walk || 0x338, // run ]; } animations.forEach(animationId => appearanceData.put(animationId, 'SHORT')); appearanceData.put(stringToLong(player.username), 'LONG'); // Username appearanceData.put(player.skills.getCombatLevel()); // Combat Level appearanceData.put(player.skills.getTotalLevel(), 'SHORT'); // Skill Level (Total Level) const appearanceDataSize = appearanceData.writerIndex; updateMaskData.put(appearanceDataSize); updateMaskData.putBytes(appearanceData.flipWriter()); } if (updateFlags.graphics) { const delay = updateFlags.graphics.delay || 0; updateMaskData.put(updateFlags.graphics.id, 'SHORT', 'LITTLE_ENDIAN'); updateMaskData.put((updateFlags.graphics.height << 16) | (delay & 0xffff), 'INT'); } } private appendBasicAppearanceItem(buffer: ByteBuffer, player: Player, appearanceInfo: number, equipmentSlot: EquipmentSlot): void { const item = player.getEquippedItem(equipmentSlot); if (item) { buffer.put(0x200 + item.itemId, 'SHORT'); } else { buffer.put(0x100 + appearanceInfo, 'SHORT'); } } } ================================================ FILE: src/engine/world/actor/prayer.ts ================================================ export class Prayer { AnimationId: number; SoundId: number; ButtonId: number; } ================================================ FILE: src/engine/world/actor/skills.ts ================================================ import { QueueableTask } from '@engine/action/pipe/task/queueable-task'; import { startsWithVowel } from '@engine/util/strings'; import type { Player } from '@engine/world/actor/player/player'; import { gfxIds } from '@engine/world/config/gfx-ids'; import { serverConfig } from '@server/game/game-server'; import type { Actor } from './actor'; import { isPlayer } from './util'; export enum Skill { ATTACK, DEFENCE, STRENGTH, HITPOINTS, RANGED, PRAYER, MAGIC, COOKING, WOODCUTTING, FLETCHING, FISHING, FIREMAKING, CRAFTING, SMITHING, MINING, HERBLORE, AGILITY, THIEVING, SLAYER, FARMING, RUNECRAFTING, CONSTRUCTION = 22, } export type SkillName = | 'attack' | 'defence' | 'strength' | 'hitpoints' | 'ranged' | 'prayer' | 'magic' | 'cooking' | 'woodcutting' | 'fletching' | 'fishing' | 'firemaking' | 'crafting' | 'smithing' | 'mining' | 'herblore' | 'agility' | 'thieving' | 'slayer' | 'farming' | 'runecrafting' | 'construction'; export interface SkillDetail { readonly name: string; readonly advancementWidgetId?: number; } export const skillDetails: SkillDetail[] = [ { name: 'Attack', advancementWidgetId: 158 }, { name: 'Defence', advancementWidgetId: 161 }, { name: 'Strength', advancementWidgetId: 175 }, { name: 'Hitpoints', advancementWidgetId: 167 }, { name: 'Ranged', advancementWidgetId: 171 }, { name: 'Prayer', advancementWidgetId: 170 }, { name: 'Magic', advancementWidgetId: 168 }, { name: 'Cooking', advancementWidgetId: 159 }, { name: 'Woodcutting', advancementWidgetId: 177 }, { name: 'Fletching', advancementWidgetId: 165 }, { name: 'Fishing', advancementWidgetId: 164 }, { name: 'Firemaking', advancementWidgetId: 163 }, { name: 'Crafting', advancementWidgetId: 160 }, { name: 'Smithing', advancementWidgetId: 174 }, { name: 'Mining', advancementWidgetId: 169 }, { name: 'Herblore', advancementWidgetId: 166 }, { name: 'Agility', advancementWidgetId: 157 }, { name: 'Thieving', advancementWidgetId: 176 }, { name: 'Slayer', advancementWidgetId: 173 }, { name: 'Farming', advancementWidgetId: 162 }, { name: 'Runecrafting', advancementWidgetId: 172 }, null as unknown as SkillDetail, // (Jameskmonger) this is a placeholder { name: 'Construction' }, ]; export interface SkillValue { exp: number; level: number; modifiedLevel?: number; } export class SkillShortcut { public constructor( private skills: Skills, private skillName: SkillName, ) {} public addExp(exp: number): void { this.skills.addExp(this.skillName, exp); } public set level(value: number) { this.skills.setLevel(this.skillName, value); } public get level(): number { return this.skills.getLevel(this.skillName); } public set exp(value: number) { this.skills.setExp(this.skillName, value); } public get exp(): number { return this.skills.get(this.skillName).exp; } public get levelForExp(): number { return this.skills.getLevelForExp(this.exp); } } type SkillShortcutMap = { [skillName in SkillName]: SkillShortcut; }; class SkillShortcuts implements SkillShortcutMap { agility: SkillShortcut; attack: SkillShortcut; construction: SkillShortcut; cooking: SkillShortcut; crafting: SkillShortcut; defence: SkillShortcut; farming: SkillShortcut; firemaking: SkillShortcut; fishing: SkillShortcut; fletching: SkillShortcut; herblore: SkillShortcut; hitpoints: SkillShortcut; magic: SkillShortcut; mining: SkillShortcut; prayer: SkillShortcut; ranged: SkillShortcut; runecrafting: SkillShortcut; slayer: SkillShortcut; smithing: SkillShortcut; strength: SkillShortcut; thieving: SkillShortcut; woodcutting: SkillShortcut; } export class Skills extends SkillShortcuts { private static EXPERIENCE_LOOKUP_TABLE: number[] = [ 0, 83, 174, 276, 388, 512, 650, 801, 969, 1154, 1358, 1584, 1833, 2107, 2411, 2746, 3115, 3523, 3973, 4470, 5018, 5624, 6291, 7028, 7842, 8740, 9730, 10824, 12031, 13363, 14833, 16456, 18247, 20224, 22406, 24815, 27473, 30408, 33648, 37224, 41171, 45529, 50339, 55649, 61512, 67983, 75127, 83014, 91721, 101333, 111945, 123660, 136594, 150872, 166636, 184040, 203254, 224466, 247886, 273742, 302288, 333804, 368599, 407015, 449428, 496254, 547953, 605032, 668051, 737627, 814445, 899257, 992895, 1096278, 1210421, 1336443, 1475581, 1629200, 1798808, 1986068, 2192818, 2421087, 2673114, 2951373, 3258594, 3597792, 3972294, 4385776, 4842295, 5346332, 5902831, 6517253, 7195629, 7944614, 8771558, 9684577, 10692629, 11805606, 13034431, ]; private static MAXIMUM_EXPERIENCE: number = 200000000; private static MINIMUM_LEVEL: number = 0; private static MAXIMUM_LEVEL: number = 99; private static MAXIMUM_INDEX: number = Skills.EXPERIENCE_LOOKUP_TABLE.length - 1; private _values: SkillValue[]; public constructor( private actor: Actor, values?: SkillValue[], ) { super(); Object.keys(Skill) .map(skillName => skillName.toLowerCase()) .forEach(skillName => (this[skillName] = new SkillShortcut(this, skillName as SkillName))); if (values) { this._values = values; } else { this._values = this.defaultValues(); } } private static confine(value: number, min: number, max: number): number { return Math.max(min, Math.min(value, max)); } public setHitpoints(hitpoints: number): void { this.setLevel(Skill.HITPOINTS, hitpoints); } public getTotalLevel(): number { return this._values.map(skillValue => skillValue.level).reduce((accumulator, currentValue) => accumulator + currentValue); } public getCombatLevel(): number { const combatLevel = (this.defence.level + this.hitpoints.level + Math.floor(this.prayer.level / 2)) * 0.25; const melee = (this.attack.level + this.strength.level) * 0.325; const ranger = this.ranged.level * 0.4875; const mage = this.magic.level * 0.4875; return combatLevel + Math.max(melee, Math.max(ranger, mage)); } public getLevel(skill: number | SkillName, ignoreLevelModifications: boolean = false): number { const s = this.get(skill); return s.modifiedLevel !== undefined && !ignoreLevelModifications ? s.modifiedLevel : s.level; } public hasLevel(skill: number | SkillName, level: number, ignoreLevelModifications: boolean = false): boolean { return this.getLevel(skill, ignoreLevelModifications) >= level; } public getLevelForExp(exp: number, index: number | undefined = undefined): number { const start = Skills.confine(index || Skills.MAXIMUM_INDEX, Skills.MINIMUM_LEVEL, Skills.MAXIMUM_INDEX); for (let level = start; level >= 1; level--) { const requirement = Skills.EXPERIENCE_LOOKUP_TABLE[level]; if (exp >= requirement) { return level + 1; } } return 1; } public getExpForLevel(level: number): number { const index = Skills.confine(level - 1, Skills.MINIMUM_LEVEL, Skills.MAXIMUM_INDEX); return Skills.EXPERIENCE_LOOKUP_TABLE[index]; } public addExp(skill: number | SkillName, exp: number): void { const currentExp = this.get(skill).exp; const currentLevel = this.getLevelForExp(currentExp); let finalExp = currentExp + exp * serverConfig.expRate; if (finalExp > Skills.MAXIMUM_EXPERIENCE) { finalExp = Skills.MAXIMUM_EXPERIENCE; } const finalLevel = this.getLevelForExp(finalExp); this.setExp(skill, finalExp); if (isPlayer(this.actor)) { this.actor.outgoingPackets.updateSkill(this.getSkillId(skill), finalLevel, finalExp); } if (currentLevel !== finalLevel) { this.setLevel(skill, finalLevel); if (isPlayer(this.actor)) { const achievementDetails = skillDetails[this.getSkillId(skill)]; if (!achievementDetails) { return; } /** * 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, ), ); } } } public showLevelUpDialogue(skill: number | SkillName, level: number): void { if (!isPlayer(this.actor)) { return; } const player = this.actor; const achievementDetails = skillDetails[this.getSkillId(skill)]; const widgetId = achievementDetails.advancementWidgetId; if (!widgetId) { return; } const skillName = achievementDetails.name.toLowerCase(); player.modifyWidget(widgetId, { childId: 0, text: `Congratulations, you just advanced ${startsWithVowel(skillName) ? 'an' : 'a'} ` + `${skillName} level.`, }); player.modifyWidget(widgetId, { childId: 1, text: `Your ${skillName} level is now ${level}.`, }); player.interfaceState.openWidget(widgetId, { slot: 'chatbox', multi: true, }); player.playGraphics({ id: gfxIds.levelUpFireworks, delay: 0, height: 125 }); // @TODO sounds } public getSkillId(skill: number | SkillName): number { if (typeof skill === 'number') { return skill; } else { const skillName = skill.toString().toUpperCase(); return Skill[skillName].valueOf(); } } public get(skill: number | SkillName): SkillValue { if (typeof skill === 'number') { return this._values[skill]; } else { const skillName = skill.toString().toUpperCase(); return this._values[Skill[skillName].valueOf()]; } } public setExp(skill: number | SkillName, exp: number): void { const skillId = this.getSkillId(skill); this._values[skillId].exp = exp; } public setLevel(skill: number | SkillName, level: number): void { const skillId = this.getSkillId(skill); this._values[skillId].level = level; } private defaultValues(): SkillValue[] { const values: SkillValue[] = []; skillDetails.forEach(() => values.push({ exp: 0, level: 1 })); values[Skill.HITPOINTS] = { exp: 1154, level: 10 }; return values; } public get values(): SkillValue[] { return this._values; } public set values(value: SkillValue[]) { this._values = value; } } ================================================ FILE: src/engine/world/actor/update-flags.ts ================================================ import type { Position } from '../position'; import type { Actor } from './actor'; /** * A specific chat message. */ export interface ChatMessage { color?: number; effects?: number; data?: Buffer; message?: string; } /** * A graphic. */ export interface Graphic { id: number; height: number; delay?: number; } /** * An animation. */ export interface Animation { id: number; delay?: number; } export enum DamageType { NO_DAMAGE = 0, DAMAGE = 1, POISON = 2, } /** * An instance of damage. */ export interface Damage { damageDealt: number; damageType: DamageType; remainingHitpoints: number; maxHitpoints: number; } /** * Various actor updating flags. */ export class UpdateFlags { private _mapRegionUpdateRequired: boolean; private _appearanceUpdateRequired: boolean; private _chatMessages: ChatMessage[]; private _facePosition: Position | null; private _faceActor: Actor | 'CLEAR' | null; private _graphics: Graphic | null; private _animation: Animation | null; private _damage: Damage | null; public constructor() { this._chatMessages = []; this.reset(); } public reset(): void { this._mapRegionUpdateRequired = false; this._appearanceUpdateRequired = false; this._facePosition = null; this._faceActor = null; this._graphics = null; this._animation = null; this._damage = null; if (this._chatMessages.length !== 0) { this._chatMessages.shift(); } } public addDamage(amount: number, type: DamageType, remainingHitpoints: number, maxHitpoints: number): void { this.damage = { damageDealt: amount, damageType: type, remainingHitpoints, maxHitpoints, }; } public addChatMessage(chatMessage: ChatMessage): void { if (this._chatMessages.length > 4) { return; } this._chatMessages.push(chatMessage); } /** * Determines if any update is required. When false will short circuit * the entire update process. */ public get updateBlockRequired(): boolean { return ( this._appearanceUpdateRequired || this._chatMessages.length !== 0 || this._facePosition !== null || this._graphics !== null || this._animation !== null || this._faceActor !== null || this._damage !== null ); } public get mapRegionUpdateRequired(): boolean { return this._mapRegionUpdateRequired; } public set mapRegionUpdateRequired(value: boolean) { this._mapRegionUpdateRequired = value; } public get appearanceUpdateRequired(): boolean { return this._appearanceUpdateRequired; } public set appearanceUpdateRequired(value: boolean) { this._appearanceUpdateRequired = value; } public get chatMessages(): ChatMessage[] { return this._chatMessages; } public set chatMessages(value: ChatMessage[]) { this._chatMessages = value; } public get facePosition(): Position | null { return this._facePosition; } public set facePosition(value: Position | null) { this._facePosition = value; } public get faceActor(): Actor | 'CLEAR' | null { return this._faceActor; } public set faceActor(value: Actor | 'CLEAR' | null) { this._faceActor = value; } public get graphics(): Graphic | null { return this._graphics; } public set graphics(value: Graphic | null) { this._graphics = value; } public get animation(): Animation | null { return this._animation; } public set animation(value: Animation | null) { this._animation = value; } public get damage(): Damage | null { return this._damage; } public set damage(value: Damage | null) { this._damage = value; } } ================================================ FILE: src/engine/world/actor/util.ts ================================================ import type { Actor } from './actor'; import type { Npc } from './npc'; import type { Player } from './player/player'; export const isPlayer = (actor: Actor): actor is Player => actor.type === 'player'; export const isNpc = (actor: Actor): actor is Npc => actor.type === 'npc'; ================================================ FILE: src/engine/world/actor/walking-queue.ts ================================================ import { regionChangeActionFactory } from '@engine/action/pipe/region-change.action'; import { activeWorld } from '@engine/world'; import { isNpc, isPlayer } from '@engine/world/actor/util'; import { Subject } from 'rxjs'; import { Position } from '../position'; import type { Actor } from './actor'; /** * Controls an actor's movement. */ export class WalkingQueue { public readonly movementQueued = new Subject(); public readonly movementEvent = new Subject(); public readonly movementQueued$ = this.movementQueued.asObservable(); public readonly movementEvent$ = this.movementEvent.asObservable(); private queue: Position[]; private _valid: boolean; public constructor(private readonly actor: Actor) { this.queue = []; this._valid = false; } public moving(): boolean { return this.queue.length !== 0; } public clear(): void { this.queue = []; } public getLastPosition(): Position { if (this.queue.length === 0) { return this.actor.position; } else { return this.queue[this.queue.length - 1]; } } public add(x: number, y: number, positionMetadata?: { [key: string]: any }): void { let lastPosition = this.getLastPosition(); let lastX = lastPosition.x; let lastY = lastPosition.y; let diffX = x - lastX; let diffY = y - lastY; const stepsBetween = Math.max(Math.abs(diffX), Math.abs(diffY)); for (let i = 0; i < stepsBetween; i++) { if (diffX !== 0) { diffX += diffX < 0 ? 1 : -1; } if (diffY !== 0) { diffY += diffY < 0 ? 1 : -1; } lastX = x - diffX; lastY = y - diffY; const newPosition = new Position(lastX, lastY, this.actor.position.level); if (this.actor.pathfinding.canMoveTo(lastPosition, newPosition)) { lastPosition = newPosition; newPosition.metadata = { ...newPosition.metadata, ...positionMetadata, }; this.queue.push(newPosition); this.movementQueued.next(newPosition); } else { this.valid = false; break; } } if (lastX !== x || (lastY !== y && this.valid)) { const newPosition = new Position(x, y, this.actor.position.level); if (this.actor.pathfinding.canMoveTo(lastPosition, newPosition)) { newPosition.metadata = { ...newPosition.metadata, ...positionMetadata, }; this.queue.push(newPosition); this.movementQueued.next(newPosition); } else { this.valid = false; } } } public moveIfAble(xDiff: number, yDiff: number): boolean { const position = this.actor.position; const newPosition = new Position(position.x + xDiff, position.y + yDiff, position.level); if (this.actor.pathfinding.canMoveTo(position, newPosition)) { this.clear(); this.valid = true; this.add(newPosition.x, newPosition.y, { ignoreWidgets: true }); return true; } return false; } public resetDirections(): void { this.actor.walkDirection = -1; this.actor.runDirection = -1; } public calculateDirection(diffX: number, diffY: number): number { if (diffX < 0) { if (diffY < 0) { return 5; } else if (diffY > 0) { return 0; } else { return 3; } } else if (diffX > 0) { if (diffY < 0) { return 7; } else if (diffY > 0) { return 2; } else { return 4; } } else { if (diffY < 0) { return 6; } else if (diffY > 0) { return 1; } else { return -1; } } } public process(): void { if (this.actor.busy || this.queue.length === 0 || !this.valid) { this.resetDirections(); return; } const walkPosition = this.queue.shift(); if (!walkPosition) { return; } if (this.actor.metadata.faceActorClearedByWalking === undefined || this.actor.metadata.faceActorClearedByWalking) { this.actor.clearFaceActor(); } const originalPosition = this.actor.position; if (this.actor.pathfinding.canMoveTo(originalPosition, walkPosition)) { const oldChunk = activeWorld.chunkManager.getChunkForWorldPosition(originalPosition); const lastMapRegionUpdatePosition = this.actor.lastMapRegionUpdatePosition; const walkDiffX = walkPosition.x - originalPosition.x; const walkDiffY = walkPosition.y - originalPosition.y; const walkDir = this.calculateDirection(walkDiffX, walkDiffY); if (walkDir === -1) { this.resetDirections(); return; } this.actor.lastMovementPosition = this.actor.position; this.actor.position = walkPosition; let runDir = -1; // @TODO npc running if (isPlayer(this.actor)) { if (this.actor.settings.runEnabled && this.queue.length !== 0) { const runPosition = this.queue.shift(); if (!runPosition) { return; } if (this.actor.pathfinding.canMoveTo(walkPosition, runPosition)) { const runDiffX = runPosition.x - walkPosition.x; const runDiffY = runPosition.y - walkPosition.y; runDir = this.calculateDirection(runDiffX, runDiffY); if (runDir != -1) { this.actor.lastMovementPosition = this.actor.position; this.actor.position = runPosition; } } else { this.resetDirections(); this.clear(); } } } this.actor.walkDirection = walkDir; this.actor.runDirection = runDir; if (runDir !== -1) { this.actor.faceDirection = runDir; } else { this.actor.faceDirection = walkDir; } const newChunk = activeWorld.chunkManager.getChunkForWorldPosition(this.actor.position); this.movementEvent.next(this.actor.position); if (isPlayer(this.actor)) { const mapDiffX = this.actor.position.x - lastMapRegionUpdatePosition.chunkX * 8; const mapDiffY = this.actor.position.y - lastMapRegionUpdatePosition.chunkY * 8; if (mapDiffX < 16 || mapDiffX > 87 || mapDiffY < 16 || mapDiffY > 87) { this.actor.updateFlags.mapRegionUpdateRequired = true; this.actor.lastMapRegionUpdatePosition = this.actor.position; } } if (!oldChunk.equals(newChunk)) { if (isPlayer(this.actor)) { this.actor.metadata.updateChunk = { newChunk, oldChunk }; this.actor.actionPipeline.call( 'region_change', regionChangeActionFactory(this.actor, originalPosition, this.actor.position), ); } else if (isNpc(this.actor)) { oldChunk.removeNpc(this.actor); newChunk.addNpc(this.actor); } } } else { this.resetDirections(); this.clear(); } } get valid(): boolean { return this._valid; } set valid(value: boolean) { this._valid = value; } } ================================================ FILE: src/engine/world/config/animation-ids.ts ================================================ export const animationIds = { reset: -1, walk: 819, idle: 808, run: 824, milkCow: 2305, lightingFire: 733, homeTeleportDraw: 4847, homeTeleportSit: 4850, homeTeleportPullOutAndReadBook: 4853, homeTeleportReadBookAndGlowCircle: 4855, homeTeleport: 4857, fillContainerWithWater: 832, shearSheep: 893, spinSpinningWheel: 894, cry: 860, climbLadder: 828, smelting: 899, death: 2304, eat: 829, buryBones: 827, herblore: { make_potion: 363, pestle_and_mortar: 364, }, combat: { punch: 422, kick: 423, stab: 412, slash: 451, armBlock: 424, }, fadeOut: 3541, fadeIn: 2115, transparent: 15, teleport: 714, }; ================================================ FILE: src/engine/world/config/examine-data.ts ================================================ import { readFileSync } from 'fs'; import { logger } from '@runejs/common'; import { JSON_SCHEMA, load } from 'js-yaml'; interface Examine { id: number; examine: string; } export class ExamineCache { private readonly items: Map; private readonly npcs: Map; private readonly objects: Map; public constructor() { logger.info('Parsing examine data...'); this.items = parseData('data/config/examine-item-data.yaml'); this.npcs = new Map(); this.objects = new Map(); } public getItem(id: number): string | null { const examine = this.items.get(id); return examine ? examine.examine : null; } public getNpc(id: number): string | null { const examine = this.npcs.get(id); return examine ? examine.examine : null; } public getObject(id: number): string | null { const examine = this.objects.get(id); return examine ? examine.examine : null; } } function parseData(fileName: string): Map { const examineMap: Map = new Map(); try { const examineItems = load(readFileSync(fileName, 'utf8'), { schema: JSON_SCHEMA }) as Examine[]; if (!examineItems || examineItems.length === 0) { throw new Error('Unable to read examine data.'); } for (const item of examineItems) { examineMap.set(item.id, item); } } catch (error) { logger.error('Error parsing examine data: ' + error); } return examineMap; } ================================================ FILE: src/engine/world/config/gfx-ids.ts ================================================ export const gfxIds = { homeTeleportDraw: 800, homeTeleportFullDrawnCircle: 801, homeTeleportPullOutBook: 802, homeTeleportCircleGlow: 803, homeTeleport: 804, levelUpFireworks: 199, teleport: 111, }; ================================================ FILE: src/engine/world/config/harvest-tool.ts ================================================ import type { Player } from '@engine/world/actor/player/player'; import { Skill } from '@engine/world/actor/skills'; export interface HarvestTool { itemId: number; level: number; animation: number; } export enum Pickaxe { BRONZE, IRON, STEEL, MITHRIL, ADAMANT, RUNE, } export enum Axe { BRONZE, IRON, STEEL, MITHRIL, ADAMANT, RUNE, DRAGON, } const Pickaxes: HarvestTool[] = [ { itemId: 1265, level: 1, animation: 625 }, { itemId: 1267, level: 1, animation: 626 }, { itemId: 1269, level: 6, animation: 627 }, { itemId: 1273, level: 21, animation: 629 }, { itemId: 1271, level: 31, animation: 628 }, { itemId: 1275, level: 41, animation: 624 }, ]; const Axes: HarvestTool[] = [ { itemId: 1351, level: 1, animation: 879 }, { itemId: 1349, level: 1, animation: 877 }, { itemId: 1353, level: 6, animation: 875 }, { itemId: 1355, level: 21, animation: 871 }, { itemId: 1357, level: 31, animation: 869 }, { itemId: 1359, level: 41, animation: 867 }, { itemId: 6739, level: 61, animation: 2846 }, ]; /** * Checks the players inventory and equipment for pickaxe * @param player * @return the highest level pickage the player can use, or null if theres none found */ export function getBestPickaxe(player: Player): HarvestTool | null { for (let i = Pickaxes.length - 1; i >= 0; i--) { if (player.skills.hasLevel(Skill.MINING, Pickaxes[i].level)) { if (player.hasItemOnPerson(Pickaxes[i].itemId)) { return Pickaxes[i]; } } } return null; } /** * Checks the players inventory and equipment for axe * @param player * @return the highest level axe the player can use, or null if theres none found */ export function getBestAxe(player: Player): HarvestTool | null { for (let i = Axes.length - 1; i >= 0; i--) { if (player.skills.hasLevel(Skill.WOODCUTTING, Axes[i].level)) { if (player.hasItemOnPerson(Axes[i].itemId)) { return Axes[i]; } } } return null; } export function getPickaxe(pickaxe: Pickaxe): HarvestTool { return Pickaxes[pickaxe]; } export function getAxe(axe: Axe): HarvestTool { return Axes[axe]; } ================================================ FILE: src/engine/world/config/harvestable-object.ts ================================================ import { randomBetween } from '@engine/util/num'; import { objectIds } from '@engine/world/config/object-ids'; interface WeightedItem { itemConfigId: string; weight: number; } export interface IHarvestable { objects: Map; items: string | WeightedItem[]; level: number; experience: number; respawnLow: number; respawnHigh: number; baseChance: number; break: number; } // Object maps work with key is mineable object, value is empty ore const CLAY_OBJECTS: Map = new Map([...objectIds.default.clay.map(tree => [tree.default, tree.empty])] as [ number, number, ][]); const COPPER_OBJECTS: Map = new Map([ ...objectIds.default.copper.map(tree => [tree.default, tree.empty]), ] as [number, number][]); const TIN_OBJECTS: Map = new Map([...objectIds.default.tin.map(tree => [tree.default, tree.empty])] as [ number, number, ][]); const IRON_OBJECTS: Map = new Map([...objectIds.default.iron.map(tree => [tree.default, tree.empty])] as [ number, number, ][]); const COAL_OBJECTS: Map = new Map([...objectIds.default.coal.map(tree => [tree.default, tree.empty])] as [ number, number, ][]); const SILVER_OBJECTS: Map = new Map([ ...objectIds.default.silver.map(tree => [tree.default, tree.empty]), ] as [number, number][]); const GOLD_OBJECTS: Map = new Map([...objectIds.default.gold.map(tree => [tree.default, tree.empty])] as [ number, number, ][]); const MITHRIL_OBJECTS: Map = new Map([ ...objectIds.default.mithril.map(tree => [tree.default, tree.empty]), ] as [number, number][]); const ADAMANT_OBJECTS: Map = new Map([ ...objectIds.default.adamant.map(tree => [tree.default, tree.empty]), ] as [number, number][]); const RUNITE_OBJECTS: Map = new Map([ ...objectIds.default.runite.map(tree => [tree.default, tree.empty]), ] as [number, number][]); const NORMAL_OBJECTS: Map = new Map([ ...objectIds.tree.normal.map(tree => [tree.default, tree.stump]), ...objectIds.tree.dead.map(tree => [tree.default, tree.stump]), ] as [number, number][]); const ACHEY_OBJECTS: Map = new Map([...objectIds.tree.archey.map(tree => [tree.default, tree.stump])] as [ number, number, ][]); const OAK_OBJECTS: Map = new Map([...objectIds.tree.oak.map(tree => [tree.default, tree.stump])] as [ number, number, ][]); const WILLOW_OBJECTS: Map = new Map([...objectIds.tree.willow.map(tree => [tree.default, tree.stump])] as [ number, number, ][]); const TEAK_OBJECTS: Map = new Map([...objectIds.tree.teak.map(tree => [tree.default, tree.stump])] as [ number, number, ][]); const DRAMEN_OBJECTS: Map = new Map([...objectIds.tree.dramen.map(tree => [tree.default, tree.stump])] as [ number, number, ][]); const MAPLE_OBJECTS: Map = new Map([...objectIds.tree.maple.map(tree => [tree.default, tree.stump])] as [ number, number, ][]); const HOLLOW_OBJECTS: Map = new Map([...objectIds.tree.hollow.map(tree => [tree.default, tree.stump])] as [ number, number, ][]); const MAHOGANY_OBJECTS: Map = new Map([ ...objectIds.tree.mahogany.map(tree => [tree.default, tree.stump]), ] as [number, number][]); const YEW_OBJECTS: Map = new Map([...objectIds.tree.yew.map(tree => [tree.default, tree.stump])] as [ number, number, ][]); const MAGIC_OBJECTS: Map = new Map([...objectIds.tree.magic.map(tree => [tree.default, tree.stump])] as [ number, number, ][]); export enum Ore { CLAY, COPPER, TIN, IRON, COAL, SILVER, GOLD, MITHIL, ADAMANT, RUNITE, RUNE_ESS, GEM, } export enum Tree { NORMAL, ACHEY, OAK, WILLOW, TEAK, MAPLE, MAHOGANY, YEW, MAGIC, HOLLOW, DRAMEN, } export function selectWeightedItem(items: WeightedItem[]): string { const totalWeight = items.reduce((sum, item) => sum + item.weight, 0); let random = randomBetween(1, totalWeight); for (const item of items) { random -= item.weight; if (random <= 0) { return item.itemConfigId; } } return items[0].itemConfigId; // Fallback to first item } const Ores: IHarvestable[] = [ { objects: CLAY_OBJECTS, items: 'rs:clay', level: 1, experience: 5.0, respawnLow: 5, respawnHigh: 10, baseChance: 70, break: 100, }, { objects: COPPER_OBJECTS, items: 'rs:copper_ore', level: 1, experience: 17.5, respawnLow: 10, respawnHigh: 20, baseChance: 70, break: 100, }, { objects: TIN_OBJECTS, items: 'rs:tin_ore', level: 1, experience: 17.5, respawnLow: 10, respawnHigh: 20, baseChance: 70, break: 100, }, { objects: IRON_OBJECTS, items: 'rs:iron_ore', level: 15, experience: 35.0, respawnLow: 9, respawnHigh: 9, baseChance: 0.0085, break: 100, }, { objects: COAL_OBJECTS, items: 'rs:coal', level: 30, experience: 50.0, respawnLow: 20, respawnHigh: 30, baseChance: 50, break: 100, }, { objects: SILVER_OBJECTS, items: 'rs:silver_ore', level: 20, experience: 40.0, respawnLow: 30, respawnHigh: 40, baseChance: 40, break: 100, }, { objects: GOLD_OBJECTS, items: 'rs:gold_ore', level: 40, experience: 65.0, respawnLow: 50, respawnHigh: 70, baseChance: 30, break: 100, }, { objects: MITHRIL_OBJECTS, items: 'rs:mithril_ore', level: 55, experience: 65.0, respawnLow: 90, respawnHigh: 120, baseChance: 20, break: 100, }, { objects: ADAMANT_OBJECTS, items: 'rs:adamantite_ore', level: 70, experience: 95.0, respawnLow: 200, respawnHigh: 400, baseChance: 0, break: 100, }, { objects: RUNITE_OBJECTS, items: 'rs:runite_ore', level: 85, experience: 125.0, respawnLow: 1200, respawnHigh: 1200, baseChance: -10, break: 100, }, { objects: new Map([[2111, 450]]), items: [ { itemConfigId: 'rs:uncut_opal', weight: 60 }, // 60/128 { itemConfigId: 'rs:uncut_jade', weight: 30 }, // 30/128 { itemConfigId: 'rs:uncut_red_topaz', weight: 15 }, // 15/128 { itemConfigId: 'rs:uncut_sapphire', weight: 9 }, // 9/128 { itemConfigId: 'rs:uncut_emerald', weight: 5 }, // 5/128 { itemConfigId: 'rs:uncut_ruby', weight: 5 }, // 5/128 { itemConfigId: 'rs:uncut_diamond', weight: 4 }, // 4/128 ], level: 40, experience: 65.0, respawnLow: 200, respawnHigh: 400, baseChance: 28, // Base success chance at level 40 break: 100, // Always depletes after successful mining }, ]; const Trees: IHarvestable[] = [ { objects: NORMAL_OBJECTS, items: 'rs:logs', level: 1, experience: 25, respawnLow: 59, respawnHigh: 98, baseChance: 70, break: 100, }, { objects: ACHEY_OBJECTS, items: 'rs:achey_logs', level: 1, experience: 25, respawnLow: 59, respawnHigh: 98, baseChance: 70, break: 100, }, { objects: OAK_OBJECTS, items: 'rs:oak_logs', level: 15, experience: 37.5, respawnLow: 14, respawnHigh: 14, baseChance: 50, break: 100 / 8, }, { objects: WILLOW_OBJECTS, items: 'rs:willow_logs', level: 30, experience: 67.5, respawnLow: 14, respawnHigh: 14, baseChance: 30, break: 100 / 8, }, { objects: TEAK_OBJECTS, items: 'rs:teak_logs', level: 35, experience: 85, respawnLow: 15, respawnHigh: 15, baseChance: 0, break: 100 / 8, }, { objects: DRAMEN_OBJECTS, items: 'rs:dramen_branch', // You'll need to add this to logs.json level: 36, experience: 0, respawnLow: 0, respawnHigh: 0, baseChance: 100, break: 0, }, { objects: MAPLE_OBJECTS, items: 'rs:maple_logs', level: 45, experience: 100, respawnLow: 59, respawnHigh: 59, baseChance: 0, break: 100 / 8, }, { objects: HOLLOW_OBJECTS, items: 'rs:bark', // You'll need to add this to logs.json level: 45, experience: 82.5, respawnLow: 43, respawnHigh: 44, baseChance: 0, break: 100 / 8, }, { objects: MAHOGANY_OBJECTS, items: 'rs:mahogany_logs', level: 50, experience: 125, respawnLow: 14, respawnHigh: 14, baseChance: -5, break: 100 / 8, }, { objects: YEW_OBJECTS, items: 'rs:yew_logs', level: 60, experience: 175, respawnLow: 99, respawnHigh: 99, baseChance: -15, break: 100 / 8, }, { objects: MAGIC_OBJECTS, items: 'rs:magic_logs', level: 75, experience: 250, respawnLow: 199, respawnHigh: 199, baseChance: -25, break: 100 / 8, }, { objects: DRAMEN_OBJECTS, items: 'rs:dramen_branch', level: 36, experience: 0, respawnLow: 0, respawnHigh: 0, baseChance: 100, break: 0, }, { objects: HOLLOW_OBJECTS, items: 'rs:bark', level: 45, experience: 82.5, respawnLow: 43, respawnHigh: 44, baseChance: 0, break: 100 / 8, }, ]; export function getOre(ore: Ore): IHarvestable { return Ores[ore]; } export function getOreFromRock(id: number): IHarvestable { return Ores.find(ore => ore.objects.has(id)) as IHarvestable; } export function getTreeFromHealthy(id: number): IHarvestable { return Trees.find(tree => tree.objects.has(id)) as IHarvestable; } export function getOreFromDepletedRock(id: number): IHarvestable { return Ores.find(ore => { for (const [rock, expired] of ore.objects) { if (expired === id) { return true; } } return false; }) as IHarvestable; } export function getAllOreIds(): number[] { const oreIds: number[] = []; for (const ore of Ores) { for (const [rock, expired] of ore.objects) { oreIds.push(rock); oreIds.push(expired); } } return oreIds; } export function getTreeIds(): number[] { const treeIds: number[] = []; for (const tree of Trees) { for (const [healthy, expired] of tree.objects) { treeIds.push(healthy); } } return treeIds; } ================================================ FILE: src/engine/world/config/item-ids.ts ================================================ export const itemIds = { witchesPotion: { ratsTail: 300, burntMeat: 2146, eyeOfNewt: 221, onion: 1957, }, coins: 995, bucket: 1925, bucketOfMilk: 1927, bucketOfWater: 1929, banana: 1963, ashes: 592, tinderbox: 590, jug: 1935, jugOfWater: 1937, pot: 1931, potOfFlour: 1933, egg: 1944, grain: 1947, wool: 1737, ballOfWool: 1759, cabbage: 1965, flax: 1779, bowstring: 1777, potato: 1942, onion: 1957, shears: 1735, magicString: 6038, crossbowString: 9438, sinew: 9436, knife: 946, feather: 314, recruitmentDrive: { shears: 5603, }, kebab: 1971, beer: 1917, hammer: 2347, arrows: { shaft: 52, headless: 53, broken: 687, bronze: 882, bronze_p: 883, bronze_pp: 5616, bronze_s: 5622, bronze_fire_u: 598, bronze_fire_l: 942, iron: 884, iron_p: 885, iron_pp: 5617, iron_s: 5623, iron_fire_u: 2532, iron_fire_l: 2533, steel: 886, steel_p: 887, steel_pp: 5618, steel_s: 5624, steel_fire_u: 2534, steel_fire_l: 2535, mithril: 888, mithril_p: 889, mithril_pp: 5619, mithril_s: 5625, mithril_fire_u: 2536, mithril_fire_l: 2537, adamant: 890, adamant_p: 891, adamant_pp: 5620, adamant_s: 5626, adamant_fire_u: 2538, adamant_fire_l: 2539, rune: 892, rune_p: 893, rune_pp: 5621, rune_s: 5627, rune_fire_u: 2540, rune_fire_l: 2541, }, essence: { pure: 7936, rune: 1436, }, talismans: { air: 1438, earth: 1440, fire: 1442, water: 1444, body: 1446, mind: 1448, blood: 1450, chaos: 1452, cosmic: 1454, death: 1456, law: 1458, soul: 1460, nature: 1462, elemental: 5516, }, tiaras: { blank: 5525, air: 5527, mind: 5529, water: 5531, body: 5533, earth: 5535, fire: 5537, cosmic: 5539, nature: 5541, chaos: 5543, law: 5545, death: 5547, blood: 5549, soul: 5551, }, runes: { air: 556, mind: 558, water: 555, earth: 557, fire: 554, body: 559, cosmic: 564, chaos: 562, nature: 561, law: 563, death: 560, }, bars: { bronze: 2349, blurite: 9467, iron: 2351, silver: 2355, steel: 2353, gold: 2357, mithril: 2359, adamantite: 2361, runite: 2363, }, ores: { clay: 434, coal: 453, tin: 438, copper: 436, blurite: 668, iron: 440, silver: 442, gold: 444, mithril: 447, adamantite: 449, runite: 451, }, daggers: { bronze: 1205, iron: 1203, steel: 1207, mithril: 1209, adamant: 1211, rune: 1213, }, swords: { bronze: 1277, iron: 1279, steel: 1281, mithril: 1285, adamantite: 1287, runite: 1289, }, scimitars: { bronze: 1321, iron: 1323, steel: 1325, mithril: 1329, adamantite: 1331, runite: 1333, }, spears: { bronze: 1237, iron: 1239, steel: 1241, mithril: 1243, adamantite: 1245, runite: 1247, }, longswords: { bronze: 1291, iron: 1293, steel: 1295, mithril: 1299, adamantite: 1301, runite: 1303, }, twoHandSwords: { bronze: 1307, iron: 1309, steel: 1311, mithril: 1315, adamantite: 1317, runite: 1319, }, axes: { bronze: 1351, iron: 1363, steel: 1365, mithril: 1355, adamantite: 1357, runite: 1359, }, maces: { bronze: 1422, iron: 1420, steel: 1424, mithril: 1428, adamantite: 1430, runite: 1432, }, warhammers: { bronze: 1337, iron: 1335, steel: 1339, mithril: 1343, adamantite: 1345, runite: 1347, }, battleAxes: { bronze: 1375, iron: 1363, steel: 1365, mithril: 1369, adamantite: 1371, runite: 1373, }, claws: { bronze: 3095, iron: 3096, steel: 3097, mithril: 3099, adamantite: 3100, runite: 3101, }, chainbodies: { bronze: 1103, iron: 1101, steel: 1105, mithril: 1109, adamantite: 1111, runite: 1113, }, platelegs: { bronze: 1075, iron: 1067, steel: 1069, mithril: 1071, adamantite: 1073, runite: 1079, }, plateskirts: { bronze: 1087, iron: 1081, steel: 1083, mithril: 1085, adamantite: 1091, runite: 1093, }, platebodys: { bronze: 1117, iron: 1115, steel: 1119, mithril: 1121, adamantite: 1123, runite: 1127, }, mediumHelmets: { bronze: 1139, iron: 1137, steel: 1141, mithril: 1143, adamantite: 1145, runite: 1147, }, fullHelmets: { bronze: 1155, iron: 1153, steel: 1157, mithril: 1159, adamantite: 1161, runite: 1163, }, squareShields: { bronze: 1173, iron: 1175, steel: 1177, mithril: 1181, adamantite: 1183, runite: 1185, }, kiteshields: { bronze: 1189, iron: 1191, steel: 1193, mithril: 1197, adamantite: 1199, runite: 1201, }, nails: { bronze: 4819, iron: 4820, steel: 1539, mithril: 4822, adamantite: 4823, runite: 4824, }, dartTips: { bronze: 819, iron: 820, steel: 821, mithril: 822, adamantite: 823, runite: 824, }, arrowTips: { bronze: 39, iron: 40, steel: 41, mithril: 42, adamantite: 43, runite: 44, }, throwingKnives: { bronze: 864, iron: 863, steel: 865, mithril: 866, adamantite: 867, runite: 868, }, bolts: { bronze: 877, iron: 9140, steel: 9141, mithril: 9142, adamantite: 9143, runite: 9144, }, limbs: { bronze: 9420, iron: 9423, steel: 9425, mithril: 9427, adamantite: 9429, runite: 9431, }, oilLanternFrames: { steel: 4540, }, studs: { steel: 2370, }, grappleTips: { mithril: 9415, }, roots: { oak: 6043, willow: 6047, maple: 6047, yew: 6049, magic: 6051, }, logs: { normal: 1511, achey: 2862, oak: 1521, willow: 1519, teak: 6333, dramenbranch: 771, maple: 1517, bark: 3239, mahogany: 6332, yew: 1515, magic: 1513, }, bowunstrung: { woodshort: 50, woodlong: 48, oakshort: 54, oaklong: 56, compogre: 4825, willowshort: 60, willowlong: 58, mapleshort: 64, maplelong: 62, yewshort: 68, yewlong: 66, magicshort: 72, magiclong: 70, }, bowstrung: { woodshort: 841, woodlong: 839, oakshort: 843, oaklong: 845, compogre: 4827, willowshort: 849, willowlong: 847, mapleshort: 853, maplelong: 851, yewshort: 857, yewlong: 855, magicshort: 861, magiclong: 859, }, skillCapes: { attack: { untrimmed: 9747, trimmed: 9748, }, strength: { untrimmed: 9750, trimmed: 9751, }, defence: { untrimmed: 9753, trimmed: 9754, }, ranged: { untrimmed: 9756, trimmed: 9757, }, prayer: { untrimmed: 9759, trimmed: 9760, }, magic: { untrimmed: 9762, trimmed: 9763, }, runecrafting: { untrimmed: 9765, trimmed: 9766, }, constitution: { untrimmed: 9768, trimmed: 9769, }, agility: { untrimmed: 9771, trimmed: 9772, }, herblore: { untrimmed: 9774, trimmed: 9775, }, thieving: { untrimmed: 9777, trimmed: 9775, }, crafting: { untrimmed: 9780, trimmed: 9781, }, fletching: { untrimmed: 9783, trimmed: 9784, }, slayer: { untrimmed: 9786, trimmed: 9786, }, construction: { untrimmed: 9789, trimmed: 9790, }, mining: { untrimmed: 9792, trimmed: 9793, }, smithing: { untrimmed: 9795, trimmed: 9796, }, fishing: { untrimmed: 9798, trimmed: 9799, }, cooking: { untrimmed: 9801, trimmed: 9802, }, firemaking: { untrimmed: 9804, trimmed: 9805, }, woodcutting: { untrimmed: 9807, trimmed: 9808, }, farming: { untrimmed: 9810, trimmed: 9811, }, questpoint: { untrimmed: 9813, }, }, staffs: { air: 1381, fire: 1387, water: 1383, earth: 1385, }, }; ================================================ FILE: src/engine/world/config/object-ids.ts ================================================ export const objectIds = { furnace: 2781, milkableCow: 8689, fire: 2732, spinningWheel: 2644, bankBooth: [2213,18491], depositBox: 9398, shortCuts: { stile: 12982, fenceNearKharidCows: 9300, }, ladders: { taverlyDungeonOverworld: 1759, taverlyDungeonUnderground: 1755, }, brokenCart: 306, brokenCartWheel: 327, burntChest: 6420, barricade: 4421, crushedSkullBarricade: 6881, fancyBarrel: 10293, brokenDoor: 5439, spearWall: 849, crate: 357, skeletonLayingFlat: 5358, skeletonLayingAgainstWall: 5359, lumbridgeAxeInLogs: 5581, tree: { normal: [ { default: 1276, stump: 1342 }, { default: 1277, stump: 1343 }, { default: 1278, stump: 1342 }, { default: 1279, stump: 1345 }, { default: 1280, stump: 1343 }, { default: 1315, stump: 1342 }, { default: 1316, stump: 1355 }, { default: 1318, stump: 1355 }, { default: 1319, stump: 1355 }, { default: 1330, stump: 1357 }, { default: 1331, stump: 1357 }, { default: 1332, stump: 1357 }, { default: 3033, stump: 1345 }, { default: 3034, stump: 1345 }, { default: 3879, stump: 3880 }, { default: 3881, stump: 3880 }, { default: 3882, stump: 3880 }, // { default: 3883, stump: 3884 }, // sigex: no matching stump { default: 14308, stump: 1342 }, // Sigex questionable object, high value { default: 14309, stump: 1342 }, // Sigex questionable object, high value ], dead: [ // Sigex: OSRS has an extra root on the stump too { default: 1282, stump: 1347 }, { default: 1283, stump: 1347 }, { default: 1284, stump: 1348 }, { default: 1285, stump: 1349 }, { default: 1286, stump: 1351 }, { default: 1289, stump: 1353 }, { default: 1290, stump: 1354 }, { default: 1291, stump: 1351 }, { default: 1365, stump: 1352 }, { default: 1383, stump: 1358 }, { default: 1384, stump: 1359 }, { default: 3035, stump: 1347 }, // { default: 3036, stump: 1351 },// Sigex: no suitable stump offset looks wrong { default: 5902, stump: 1347 }, { default: 5903, stump: 1351 }, { default: 5904, stump: 1351 }, ], archey: [{ default: 2023, stump: 3371 }], oak: [ { default: 1281, stump: 1356 }, // { default: 3037, stump: 1342 }, // Sigex: dark Oak tutorial island no stump { default: 8467, stump: 0 }, // Farming ], willow: [ { default: 1308, stump: 7399 }, { default: 5551, stump: 5554 }, { default: 5552, stump: 5554 }, // Sigex: offset is wrong { default: 5553, stump: 5554 }, // Sigex: offset is wrong { default: 8487, stump: 1324 }, // Farming // { default: 8488, stump: 1324 }, // Farming ], teak: [ { default: 9036, stump: 9037 }, { default: 15062, stump: 9037 }, // Sigex: questionable object, high value ], dramen: [{ default: 1292, stump: -1 }], maple: [ { default: 1307, stump: 7400 }, { default: 4674, stump: 7400 }, // { default: 8444, stump: 0 }, // Farming ], hollow: [ { default: 2289, stump: 2310 }, { default: 4060, stump: 4061 }, ], mahogany: [{ default: 9034, stump: 9035 }], yew: [ { default: 1309, stump: 7402 }, // { default: 8513, stump: 0 }, // Farming ], magic: [ { default: 1306, stump: 7401 }, // { default: 8409, stump: 0 }, // Farming ], }, default: { clay: [ { default: 2108, empty: 450 }, { default: 2109, empty: 451 }, { default: 14904, empty: 14896 }, { default: 14905, empty: 14897 }, ], copper: [ { default: 11960, empty: 11555 }, { default: 11961, empty: 11556 }, { default: 11962, empty: 11557 }, { default: 11936, empty: 11552 }, { default: 11937, empty: 11553 }, { default: 11938, empty: 11554 }, { default: 2090, empty: 450 }, { default: 2091, empty: 451 }, { default: 14906, empty: 14898 }, { default: 14907, empty: 14899 }, { default: 14856, empty: 14832 }, { default: 14857, empty: 14833 }, { default: 14858, empty: 14834 }, ], tin: [ { default: 11597, empty: 11555 }, { default: 11958, empty: 11556 }, { default: 11959, empty: 11557 }, { default: 11933, empty: 11552 }, { default: 11934, empty: 11553 }, { default: 11935, empty: 11554 }, { default: 2094, empty: 450 }, { default: 2095, empty: 451 }, { default: 14092, empty: 14894 }, { default: 14903, empty: 14895 }, ], iron: [ { default: 11954, empty: 11555 }, { default: 11955, empty: 11556 }, { default: 11956, empty: 11557 }, { default: 2092, empty: 450 }, { default: 2093, empty: 451 }, { default: 14900, empty: 14892 }, { default: 14901, empty: 14893 }, { default: 14913, empty: 14915 }, { default: 14914, empty: 14916 }, ], coal: [ { default: 11963, empty: 11555 }, { default: 11964, empty: 11556 }, { default: 11965, empty: 11557 }, { default: 11930, empty: 11552 }, { default: 11931, empty: 11553 }, { default: 11932, empty: 11554 }, { default: 2096, empty: 450 }, { default: 2097, empty: 451 }, { default: 14850, empty: 14832 }, { default: 14851, empty: 14833 }, { default: 14852, empty: 14834 }, ], silver: [ { default: 11948, empty: 11555 }, { default: 11949, empty: 11556 }, { default: 11950, empty: 11557 }, { default: 2100, empty: 450 }, { default: 2101, empty: 451 }, ], gold: [ { default: 11951, empty: 11555 }, { default: 11952, empty: 11556 }, { default: 11953, empty: 11557 }, { default: 2098, empty: 450 }, { default: 2099, empty: 451 }, ], mithril: [ { default: 11945, empty: 11555 }, { default: 11946, empty: 11556 }, { default: 11947, empty: 11557 }, { default: 11942, empty: 11552 }, { default: 11943, empty: 11553 }, { default: 11944, empty: 11554 }, { default: 2102, empty: 450 }, { default: 2103, empty: 451 }, { default: 14853, empty: 14832 }, { default: 14854, empty: 14833 }, { default: 14855, empty: 14834 }, ], adamant: [ { default: 11939, empty: 11552 }, { default: 11940, empty: 11553 }, { default: 11941, empty: 11554 }, { default: 2104, empty: 450 }, { default: 2105, empty: 451 }, { default: 14862, empty: 14832 }, { default: 14863, empty: 14833 }, { default: 14864, empty: 14834 }, ], runite: [ { default: 2106, empty: 450 }, { default: 2107, empty: 451 }, { default: 14859, empty: 14832 }, { default: 14860, empty: 14833 }, { default: 14861, empty: 14834 }, ], }, }; ================================================ FILE: src/engine/world/config/scenery-spawns.ts ================================================ import { readFileSync } from 'fs'; import { logger } from '@runejs/common'; import type { LandscapeObject } from '@runejs/filestore'; import { JSON_SCHEMA, load } from 'js-yaml'; export function parseScenerySpawns(): LandscapeObject[] { try { logger.info('Parsing scenery spawns...'); const scenerySpawns = load(readFileSync('data/config/scenery-spawns.yaml', 'utf8'), { schema: JSON_SCHEMA }) as LandscapeObject[]; if (!scenerySpawns || scenerySpawns.length === 0) { throw new Error('Unable to read scenery spawns.'); } logger.info(`${scenerySpawns.length} scenery spawns found.`); return scenerySpawns; } catch (error) { logger.error('Error parsing scenery spawns: ' + error); return []; } } ================================================ FILE: src/engine/world/config/songs.ts ================================================ export const songs = new Map([ ['scape main', 0], ['iban', 1], ['autumn voyage', 2], ['unknown land', 3], ['hells bells', 4], ['sad meadow', 5], ['jollyr', 6], ['overture', 7], ['wildwood', 8], ['kingdom', 9], ['moody', 10], ['spooky2', 11], ['long way home', 12], ['mage arena', 13], ['witching', 14], ['workshop', 15], ['scape main default', 16], ['escape', 17], ['horizon', 18], ['arabique', 19], ['lullaby', 20], ['monarch waltz', 21], ['gnome king', 22], ['gnome', 23], ['attack1', 24], ['attack2', 25], ['attack3', 26], ['attack4', 27], ['attack5', 28], ['attack6', 29], ['voodoo cult', 30], ['voyage', 32], ['gnome village', 33], ['wonder', 34], ['sea shanty2', 35], ['arabian', 36], ['deep wildy', 37], ['trawler', 38], ['church music 1', 39], ['church music 2', 40], ['expecting', 41], ['wilderness2', 42], ['wilderness3', 43], ['right on track', 44], ['venture2', 45], ['harmony2', 46], ['duel arena', 47], ['morytania', 48], ['wander', 49], ['al kharid', 50], ['trawler minor', 51], ['serene', 52], ['royale', 53], ['scape soft', 54], ['high seas', 55], ['doorways', 56], ['rune essence', 57], ['nomad', 58], ['cursed', 59], ['lasting', 60], ['village', 61], ['newbie melody', 62], ['chain of command', 63], ['book of spells', 64], ['miracle dance', 65], ['legion', 66], ['close quarters', 67], ['cavern', 68], ['egypt', 69], ['upcoming', 70], ['chompy hunt', 71], ['fanfare', 72], ['alls fairy in love n war', 73], ['lightwalk', 74], ['venture', 75], ['harmony', 76], ['splendour', 77], ['reggae', 78], ['the desert', 79], ['soundscape', 80], ['wonderous', 81], ['waterfall', 82], ['big chords', 83], ['dead quiet', 84], ['vision', 85], ['dimension x', 86], ['ice melody', 87], ['twilight', 88], ['reggae2', 89], ['ambient jungle', 90], ['riverside', 91], ['sea shanty', 92], ['parade', 93], ['tribal2', 94], ['intrepid', 95], ['inspiration', 96], ['hermit', 97], ['forever', 98], ['baroque', 99], ['beyond', 100], ['gnome village2', 101], ['alone', 102], ['oriental', 103], ['camelot', 104], ['tomorrow', 105], ['expanse', 106], ['miles away', 107], ['starlight', 108], ['theme', 109], ['serenade', 110], ['still night', 111], ['gnomeball', 112], ['lightness', 113], ['jungly1', 114], ['jungly2', 115], ['greatness', 116], ['jungly3', 117], ['faerie', 118], ['fishing', 119], ['shining', 120], ['forbidden', 121], ['shine', 122], ['arabian2', 123], ['arabian3', 124], ['garden', 125], ['we are the fairies', 126], ['nightfall', 127], ['grumpy', 128], ['spookyjungle', 129], ['tree spirits', 130], ['understanding', 131], ['breeze', 132], ['the tower', 133], ['la mort', 134], ['emperor', 138], ['talking forest', 140], ['barbarianism', 141], ['complication', 142], ['down to earth', 143], ['scape cave', 144], ['yesteryear', 145], ['zealot', 146], ['silence', 147], ['emotion', 148], ['principality', 149], ['gnome theme', 150], ['start', 151], ['ballad of enchantment', 152], ['expedition', 153], ['bone dance', 154], ['neverland', 155], ['mausoleum', 156], ['medieval', 157], ['quest', 158], ['gaol', 159], ['army of darkness', 160], ['long ago', 161], ['tribal background', 162], ['flute salad', 163], ['landlubber', 164], ['tribal', 165], ['fanfare2', 166], ['fanfare3', 167], ['lonesome', 168], ['crystal sword', 169], ['the shadow', 170], ['null', 171], ['jungle island', 172], ['dunjun', 173], ['desert voyage', 174], ['spirit', 175], ['undercurrent', 176], ['adventure', 177], ['courage', 178], ['underground', 179], ['attention', 180], ['crystal cave', 181], ['dangerous', 182], ['troubled', 183], ['magical journey', 184], ['magic dance', 185], ['arrival', 186], ['tremble', 187], ['in the manor', 188], ['wolf mountain', 189], ['heart and mind', 190], ['knightly', 191], ['trinity', 192], ['mellow', 193], ["brimstail's scales", 194], ['delrith summoning', 195], ['wally cutscene', 196], ['lament of meiyerditch', 197], ['dagannoth dawn', 198], ['the mollusc menace', 200], ['slug a bug ball', 201], ['prime time', 202], ['my arms journey', 203], ['roc and roll', 204], ['high spirits', 205], ['stagnant', 241], ['time out', 242], ['stratosphere', 243], ['waterlogged', 244], ['natural', 245], ['grotto', 246], ['artistry', 247], ['aztec', 248], ['forest', 251], ['elven mist', 252], ['lost soul', 253], ['meridian', 254], ['woodland', 255], ['overpass', 256], ['sojourn', 257], ['contest', 258], ['crystal castle', 259], ['insect queen', 260], ['marzipan', 261], ['righteousness', 262], ['bandit camp', 263], ['mad eadgar', 264], ['superstition', 265], ['bone dry', 266], ['sunburn', 267], ['everywhere', 268], ['competition', 269], ['exposed', 270], ['well of voyage', 271], ['haunted mine', 277], ['deep down', 278], ['chamber', 282], ['miscellania', 284], ['etcetera', 285], ['shadowland', 286], ['lair', 287], ['deadlands', 288], ['rellekka', 289], ['saga', 290], ['borderland', 291], ['stranded', 292], ['legend', 293], ['frostbite', 294], ['warrior', 295], ['technology', 296], ['etcetera theme', 297], ['monkey madness', 303], ['marooned', 304], ['anywhere', 305], ['island life', 306], ['temple', 307], ['suspicious', 308], ['showdown', 311], ['find my way', 312], ['castlewars', 314], ['the navigator', 316], ['melodrama', 317], ['ready for battle', 318], ['stillness', 319], ['lighthouse', 320], ['scape scared', 321], ['out of the deep', 322], ['upass', 323], ['background', 324], ['cave background', 325], ['dark', 326], ['dream', 327], ['march', 328], ['regal', 329], ['cellar song', 330], ['scape sad', 331], ['scape wild', 332], ['spooky', 333], ['pirates of peril', 334], ['romancing the crone', 335], ['dangerous road', 336], ['faithless', 337], ['tiptoe', 338], ['the terrible tower', 339], ['masquerade', 340], ['the slayer', 341], ['body parts', 342], ['monster melee', 343], ["fenkenstrain's refrain", 344], ['barking mad', 345], ['goblin game', 346], ['fruits de mer', 347], ["narnode's theme", 348], ['dynasty', 351], ['scarab', 352], ['shipwrecked', 353], ['phasmatys', 354], ['the other side', 355], ['settlement', 356], ['cave of beasts', 357], ['dragontooth island', 358], ['sarcophagus', 359], ['down below', 361], ['karamja jam', 362], ['7th realm', 363], ['pathways', 364], ['eagle peak', 366], ['time to mine', 369], ['in between', 370], ['far away', 372], ['claustrophobia', 373], ['fight or flight', 375], ['temple of light', 376], ['the golem', 377], ['forgotten', 378], ['throne of the demon', 379], ['dance of the undead', 380], ['dangerous way', 381], ['city of the dead', 383], ['hypnotized', 384], ['sphinx', 387], ['mirage', 388], ['cave of the goblins', 389], ['romper chomper', 390], ['zogre dance', 392], ['path of peril', 393], ['wayward', 394], ['tale of keldagrim', 395], ['land of the dwarves', 396], ['tears of guthix', 397], ['the power of tears', 398], ['scape original', 400], ['the adventurer', 401], ['the rogues den', 402], ['the far side', 403], ['sailing journey2', 406], ['the lost melody', 407], ['frogland', 409], ['lost tribe cutscene', 410], ['evil bobs island', 411], ['into the abyss', 412], ['the quizmaster', 413], ['athletes foot', 415], ['corporal punishment', 418], ['pheasant peasant', 419], ['the lost tribe', 420], ['the chosen', 425], ['have a blast', 434], ['wilderness', 435], ['forgettable melody', 436], ['drunken dwarf', 438], ['over to nardah', 447], ['the monsters below', 448], ['jungle hunt', 453], ['home sweet home', 454], ['joy of the hunt', 460], ['the desolate isle', 461], ['spirits of elid', 462], ['fire and brimstone', 463], ['the genie', 464], ['desert heat', 465], ['ground scape', 466], ['in the pits', 469], ['strange place', 470], ['brew hoo hoo', 471], ['tzhaar', 473], ['wild side', 475], ['dead can dance', 476], ['the cellar dwellers', 478], ['jungle troubles', 479], ['mined out', 480], ['catch me if you can', 481], ['rat a tat tat', 482], ['the noble rodent', 485], ['bubble and squeak', 489], ["sarim's vermin", 490], ['rat hunt', 491], ['the trade parade', 496], ['aye car rum ba', 497], ['blistering barnacles', 498], ['distant land', 501], ['fangs for the memory', 504], ["pharoah's tomb", 505], ['land down under', 506], ['meddling kids', 508], ['corridors of power', 509], ['slither and thither', 510], ['in the clink', 511], ['mudskipper melody', 515], ['subterranea', 517], ['incantation', 519], ['grip of the talon', 520], ['xenophobe', 524], ['title fight', 525], ['victory is mine', 528], ['woe of the wyvern', 529], ['in the brine', 530], ["diango's little helpers", 532], ['roll the bones', 533], ['mind over matter', 534], ['golden touch', 535], ['dogs of war', 537], ['autumn in bridgelum', 538], ['the enchanter', 541], ['lament', 542], ['making waves', 544], ['cabin fever', 545], ['last stand', 546], ['scape santa', 547], ['poles apart', 548], ['food for thought', 558], ['malady', 559], ['dance of death', 560], ['wrath and ruin', 565], ['storm brew', 568], ['the mad mole', 573], ['fairy dragon cutscene', 574], ['chickened out', 575], ['davy jones locker', 576], ['mastermindless', 577], ['too many cooks', 582], ['chef surprize', 583], ['everlasting fire', 586], ['null and void', 587], ['pest control', 588], ['tomb raider', 591], ['no way out', 594], ['method of madness', 600], ['fear and loathing', 602], ['funny bunnies', 603], ['assault and battery', 604], ['the depths', 606], ['distillery hilarity', 610], ['trouble brewing', 611], ['head to head', 612], ['pinball wizard', 614], ['beetle juice', 615], ['back to life', 616], ['spy games', 619], ['where eagles lair', 620], ['homescape', 621], ['waking dream', 622], ['dreamstate', 623], ['the lunar isle', 625], ['way of the enchanter', 626], ['isle of everywhere', 627], ['the galleon', 630], ["life's a beach!", 631], ['little cave of horrors', 632], ['on the wing', 633], ['warriors guild', 634], ['ham fisted', 638], ['sigmunds showdown', 640], ['the last shanty', 643], ['night of the vampyre', 646], ]); ================================================ FILE: src/engine/world/config/sound-ids.ts ================================================ export const soundIds = { dropItem: 2739, pickupItem: 2582, milkCow: 372, lightingFire: 2599, fireLit: 2594, openDoor: 62, closeDoor: 60, openGate: 67, closeGate: 66, homeTeleportDraw: 193, homeTeleportSit: 196, homeTeleportPullOutBook: 194, homeTeleportCircleGlowAndTeleport: 195, teleport: 200, emptyBucket: 2401, pinBeep: 1041, potContentModified: 2584, fillContainerWithWater: 2609, sheepBaa: 2053, shearSheep: 761, spinWool: 2590, inventoryFull: 2277, buryBones: 2738, oreDepeleted: 3600, pickaxeSwing: 3220, axeSwing: [88, 89, 90], oreEmpty: 2661, smelting: 2725, eat: 2393, prayer: { deactivated: 2663, thick_skin: 2690, burst_of_strength: 2688, clarity_of_thought: 2664, sharp_eye: 2685, mystic_will: 2670, rock_skin: 2684, superhuman_strength: 2689, improved_reflexes: 2662, rapid_restore: 2679, rapid_heal: 2678, prot_item: 1982, hawk_eye: 2666, mystic_lore: 2668, steel_skin: 2687, ultimate_strength: 2691, incred_reflexes: 2667, prot_from_mage: 2675, prot_from_ranged: 2677, prot_from_melee: 2676, eagle_eye: 2665, mystic_might: 2669, retribution: 2682, redemption: 2680, smite: 2686, preserve: 2679, chivalry: 3826, piety: 3825, }, herblore: { clean_herb: 3920, make_potion: 2608, // both unfinished and finished }, npc: { human: { maleDeath: 512, maleDefence: 513, femaleDeath: 505, femaleDefence: 506, playerDefence: 516, noArmorHitPlayer: 519, noArmorHit: 511, }, }, }; ================================================ FILE: src/engine/world/config/travel-locations.ts ================================================ import { readFileSync } from 'fs'; import { Position } from '@engine/world/position'; import { JSON_SCHEMA, load } from 'js-yaml'; interface RawTravelLocation { name: string; x: number; y: number; } export interface TravelLocation { name: string; key: string; position: Position; } const readLocations = (): TravelLocation[] => { const locationData = load(readFileSync('data/config/travel-locations-data.yaml', 'utf8'), { schema: JSON_SCHEMA, }) as RawTravelLocation[]; return locationData.map(location => { return { name: location.name, key: location.name.toLowerCase(), position: new Position(location.x, location.y, 0), }; }) as TravelLocation[]; }; export class TravelLocations { public readonly locations: TravelLocation[]; public constructor() { this.locations = readLocations(); } public find(search: string): TravelLocation | null { search = search.toLowerCase().trim().replace(/_/g, ' '); for (const location of this.locations) { if (location.key.toLowerCase() === search) { return location; } } return null; } } ================================================ FILE: src/engine/world/config/widget.ts ================================================ import type { Subject } from 'rxjs'; export const widgetScripts = { musicPlayerAutoManual: 18, musicPlayerLoop: 19, attackStyle: 43, brightness: 166, unknown: 167, // ???? musicVolume: 168, soundEffectVolume: 169, mouseButtons: 170, chatEffects: 171, autoRetaliate: 172, runMode: 173, splitPrivateChat: 287, bankInsertMode: 304, bankWithdrawNoteMode: 115, acceptAid: 427, areaEffectVolume: 872, questPoints: 101, }; export interface PlayerWidget { widgetId: number; secondaryWidgetId?: number; type: 'SCREEN' | 'CHAT' | 'FULLSCREEN' | 'SCREEN_AND_TAB'; disablePlayerMovement?: boolean; closeOnWalk?: boolean; permanent?: boolean; forceClosed?: () => void; beforeOpened?: () => void; afterOpened?: () => void; closed?: Subject; } ================================================ FILE: src/engine/world/direction.ts ================================================ export interface DirectionData { index: number; deltaX: number; deltaY: number; rotation: number; } /** * A direction within the world. */ export type Direction = 'NORTH' | 'SOUTH' | 'EAST' | 'WEST' | 'NORTHEAST' | 'NORTHWEST' | 'SOUTHEAST' | 'SOUTHWEST'; export const directionData: { [key: string]: DirectionData } = { NORTH: { index: 1, deltaX: 0, deltaY: 1, rotation: 1, }, SOUTH: { index: 6, deltaX: 0, deltaY: -1, rotation: 3, }, EAST: { index: 4, deltaX: 1, deltaY: 0, rotation: 2, }, WEST: { index: 3, deltaX: -1, deltaY: 0, rotation: 0, }, NORTHEAST: { index: 2, deltaX: 1, deltaY: 1, rotation: 1, }, NORTHWEST: { index: 0, deltaX: -1, deltaY: 1, rotation: 0, }, SOUTHEAST: { index: 7, deltaX: 1, deltaY: -1, rotation: 2, }, SOUTHWEST: { index: 5, deltaX: -1, deltaY: -1, rotation: 3, }, }; export const WNES: Direction[] = ['WEST', 'NORTH', 'EAST', 'SOUTH']; export const directionFromIndex = (index: number): DirectionData | null => { const keys = Object.keys(directionData); for (const key of keys) { if (directionData[key].index === index) { return directionData[key]; } } return null; }; export const oppositeDirectionIndex = (index: number): number => { return 7 - index; }; ================================================ FILE: src/engine/world/index.ts ================================================ import { World } from '@engine/world/world'; /** * The singleton instance of this game world. */ export let activeWorld: World; /** * Creates a new instance of the game world and assigns it to the singleton world variable. */ export const activateGameWorld = async (): Promise => { activeWorld = new World(); await activeWorld.startup(); return activeWorld; }; ================================================ FILE: src/engine/world/instances.ts ================================================ import type { Player } from '@engine/world/actor/player/player'; import { activeWorld } from '@engine/world/index'; import type { Item } from '@engine/world/items/item'; import type { WorldItem } from '@engine/world/items/world-item'; import { CollisionMap } from '@engine/world/map/collision-map'; import { Position } from '@engine/world/position'; import { schedule } from '@engine/world/task'; import { World } from '@engine/world/world'; import { logger } from '@runejs/common'; import type { LandscapeObject } from '@runejs/filestore'; /** * Additional configuration info for an item being spawned in an instance. */ interface ItemSpawnConfig { /** * optional] The original owner of the spawned item. */ owner?: Player; /** * optional] When the spawned item should expire and de-spawn. */ expires?: number; /** * [optional] When the item should re-spawn after being picked up. */ respawns?: number; } /** * A game world chunk that is tied to a specific instance. */ export interface InstancedChunk { /** * A specific instanced game chunk's collision map. */ collisionMap: CollisionMap; /** * Tile modifications made to this instanced chunk. */ mods: Map; } /** * Modifications made to a single game tile within an instance. */ export class TileModifications { /** * New game objects that have been introduced to an instance. */ public readonly spawnedObjects: LandscapeObject[] = []; /** * Cache/standard game objects that have been hidden from an instance. */ public readonly hiddenObjects: LandscapeObject[] = []; /** * World items spawned onto this tile within an instance. */ public readonly worldItems: WorldItem[] = []; /** * Checks if this tile is devoid of any modifications. */ public get empty(): boolean { return this.spawnedObjects.length === 0 && this.hiddenObjects.length === 0 && this.worldItems.length === 0; } } /** * A player or group instance within the world. */ export class WorldInstance { /** * A list of game world chunks that have modifications made to them in this instance. */ public readonly chunkModifications = new Map(); /** * A list of players currently in this instance. */ public readonly players: Map = new Map(); /** * Creates a new game world instance. * @param instanceId The instanceId to apply to this new world instance. */ public constructor(public readonly instanceId: string) {} /** * Spawns a new world item in this instance. * @param item The item to spawn into the game world. * @param position The position to spawn the item at. * @param config Additional item spawn config. * If not provided, the item will stay within the instance indefinitely. */ public spawnWorldItem(item: Item | number, position: Position, config?: ItemSpawnConfig): WorldItem { const { owner, respawns, expires } = config || {}; if (typeof item === 'number') { item = { itemId: item, amount: 1 }; } const worldItem: WorldItem = { itemId: item.itemId, amount: item.amount, position, owner, expires, respawns, instance: this, }; const { chunk: instancedChunk, mods } = this.getTileModifications(position); if (owner) { // If this world item is only visible to one player initially, we setup a timeout to spawn it for all other // players after 100 game cycles. try { owner.outgoingPackets.setWorldItem(worldItem, worldItem.position); } catch (error) { logger.error(`Error spawning world item ${worldItem?.itemId} at ${worldItem?.position?.key}`, error); throw error; } } mods.worldItems.push(worldItem); instancedChunk.mods.set(position.key, mods); if (owner) { setTimeout(() => { if (worldItem.removed) { return; } this.worldItemAdded(worldItem, owner); worldItem.owner = undefined; }, 100 * World.TICK_LENGTH); } else { this.worldItemAdded(worldItem); } if (expires) { // If the world item is set to expire, set up a timeout to remove it from the game world after the // specified number of game cycles. setTimeout(() => { if (worldItem.removed) { return; } this.despawnWorldItem(worldItem); }, expires * World.TICK_LENGTH); } return worldItem; } /** * De-spawns a world item from this instance. * @param worldItem The world item to de-spawn. */ public despawnWorldItem(worldItem: WorldItem): void { const chunkMap = this.getInstancedChunk(worldItem.position); const chunkMod = chunkMap.mods.get(worldItem.position.key); if (!chunkMod) { // Object no longer exists return; } if (chunkMod.worldItems && chunkMod.worldItems.length !== 0) { const idx = chunkMod.worldItems.findIndex(i => i.itemId === worldItem.itemId && i.amount === worldItem.amount); if (idx !== -1) { chunkMod.worldItems.splice(idx, 1); } } if (chunkMod.worldItems.length === 0) { this.clearTileIfEmpty(worldItem.position); } worldItem.removed = true; this.worldItemRemoved(worldItem); if (worldItem.respawns !== undefined) { this.respawnItem(worldItem); } } /** * Re-spawns a previously de-spawned world item after a specified amount of time. * @param worldItem The item to re-spawn. */ public async respawnItem(worldItem: WorldItem): Promise { if (worldItem.respawns === undefined) { logger.warn(`Attempting to respawn item ${worldItem.itemId} at ${worldItem.position.key} that does not have a respawn time.`); return; } await schedule(worldItem.respawns); this.spawnWorldItem( { itemId: worldItem.itemId, amount: worldItem.amount, }, worldItem.position, { respawns: worldItem.respawns, owner: worldItem.owner, expires: worldItem.expires, }, ); } /** * Adds a world item to the view of any nearby players in this instance. * @param worldItem The world item that was added. * @param excludePlayer [optional] A specific player to not show this world item update to. * Usually this is used when a player drops the item and it should appear for them immediately, but have a delay * before being shown to other players in the instance. */ public worldItemAdded(worldItem: WorldItem, excludePlayer?: Player): void { const nearbyPlayers = activeWorld.findNearbyPlayers(worldItem.position, 16, this.instanceId) || []; nearbyPlayers.forEach(player => { if (excludePlayer && excludePlayer.equals(player)) { return; } player.outgoingPackets.setWorldItem(worldItem, worldItem.position); }); } /** * Removes a world item from the view of any nearby players in this instance. * @param worldItem The world item that was removed. */ public worldItemRemoved(worldItem: WorldItem): void { const nearbyPlayers = activeWorld.findNearbyPlayers(worldItem.position, 16, this.instanceId) || []; nearbyPlayers.forEach(player => player.outgoingPackets.removeWorldItem(worldItem, worldItem.position)); } /** * Temporarily hides a game object from the game world. * @param object The game object to temporarily hide from view. * @param hideTicks The number of game cycles/ticks before the object will be shown again. */ public async hideGameObjectTemporarily(object: LandscapeObject, hideTicks: number): Promise { this.hideGameObject(object); await schedule(hideTicks); this.showGameObject(object); } /** * Spawns a temporary game object within the game world. * @param object The game object to spawn. * @param position The position to spawn the object at. * @param despawnTicks The number of game cycles/ticks before the object will de-spawn. */ public async spawnTemporaryGameObject(object: LandscapeObject, position: Position, despawnTicks: number): Promise { this.spawnGameObject(object); await schedule(despawnTicks); this.despawnGameObject(object); } /** * Removes one game object and adds another to the game world. The new object may be completely different from * the one being removed, and in different positions. NOT to be confused with `replaceObject`, which will replace * and existing object with another object of the same type, orientation, and position. * @param newObject The game object being spawned. * @param oldObject The game object being removed. * @param newObjectInCache Whether or not the object being added is the original game-cache object. */ public toggleGameObjects(newObject: LandscapeObject, oldObject: LandscapeObject, newObjectInCache: boolean): void { if (newObjectInCache) { this.showGameObject(newObject); this.despawnGameObject(oldObject); } else { this.hideGameObject(oldObject); this.spawnGameObject(newObject); } } /** * Replaces a game object within the instance with a different object of the same object type, orientation, and position. * NOT to be confused with `toggleGameObjects`, which removes one object and adds a different one that may have a differing * type, orientation, or position (such as a door being opened). * @param newObject The new game object to spawn, or the id of the location object to spawn. * @param oldObject The game object being replaced. Usually a game-cache-stored object. * @param respawnTicks [optional] How many ticks it will take before the original location object respawns. * If not provided, the original game object will never re-spawn and the new location object will forever * remain in it's place (in this instance). */ public async replaceGameObject(newObject: LandscapeObject | number, oldObject: LandscapeObject, respawnTicks?: number): Promise { if (typeof newObject === 'number') { newObject = { objectId: newObject, x: oldObject.x, y: oldObject.y, level: oldObject.level, type: oldObject.type, orientation: oldObject.orientation, } as LandscapeObject; } this.hideGameObject(oldObject); this.spawnGameObject(newObject); if (respawnTicks !== undefined) { await schedule(respawnTicks); this.despawnGameObject(newObject as LandscapeObject); this.showGameObject(oldObject); } } /** * Spawn a new game object into the instance. * @param object The game object to spawn. * @param reference Whether or not the object being spawned is a reference to an existing object or if it should * be sent to the game client for forced rendering. Defaults to false for forced rendering. */ public spawnGameObject(object: LandscapeObject, reference: boolean = false): void { const position = new Position(object.x, object.y, object.level); const { chunk: instancedChunk, mods } = this.getTileModifications(position); if (mods.spawnedObjects.find(o => o.x === object.x && o.y === object.y && o.level === object.level && o.type === object.type)) { return; } mods.spawnedObjects.push(object); instancedChunk.mods.set(position.key, mods); instancedChunk.collisionMap.markGameObject(object, true); const nearbyPlayers = activeWorld.findNearbyPlayers(position, 16, this.instanceId) || []; nearbyPlayers.forEach(player => player.outgoingPackets.setLocationObject(object, position)); } /** * Remove a previously spawned game object from the instance. * @param object The game object to de-spawn. */ public despawnGameObject(object: LandscapeObject): void { const position = new Position(object.x, object.y, object.level); const instancedChunk = this.getInstancedChunk(position); const tileModifications = instancedChunk.mods.get(position.key); if (!tileModifications) { // Object no longer exists return; } if (tileModifications.spawnedObjects && tileModifications.spawnedObjects.length !== 0) { const idx = tileModifications.spawnedObjects.findIndex( o => o.objectId === object.objectId && o.type === object.type && o.orientation === object.orientation, ); if (idx !== -1) { tileModifications.spawnedObjects.splice(idx, 1); } } if (tileModifications.spawnedObjects.length === 0) { this.clearTileIfEmpty(position); } instancedChunk.collisionMap.markGameObject(object, false); const nearbyPlayers = activeWorld.findNearbyPlayers(position, 16, this.instanceId) || []; nearbyPlayers.forEach(player => player.outgoingPackets.removeLocationObject(object, position)); } /** * Hides a static game object from an instance. * @param object The cache game object to hide from the instance. */ public hideGameObject(object: LandscapeObject): void { const position = new Position(object.x, object.y, object.level); const { chunk: instancedChunk, mods } = this.getTileModifications(position); mods.hiddenObjects.push(object); instancedChunk.mods.set(position.key, mods); instancedChunk.collisionMap.markGameObject(object, false); const nearbyPlayers = activeWorld.findNearbyPlayers(position, 16, this.instanceId) || []; nearbyPlayers.forEach(player => player.outgoingPackets.removeLocationObject(object, position)); } /** * Shows a previously hidden static game object. * @param object The cache game object to stop hiding from view. */ public showGameObject(object: LandscapeObject): void { const position = new Position(object.x, object.y, object.level); const instancedChunk = this.getInstancedChunk(position); const tileModifications = instancedChunk.mods.get(position.key); if (!tileModifications) { // Object no longer exists return; } if (tileModifications.hiddenObjects && tileModifications.hiddenObjects.length !== 0) { const idx = tileModifications.hiddenObjects.findIndex( o => o.objectId === object.objectId && o.type === object.type && o.orientation === object.orientation, ); if (idx !== -1) { tileModifications.hiddenObjects.splice(idx, 1); } } if (tileModifications.hiddenObjects.length === 0) { this.clearTileIfEmpty(position); } instancedChunk.collisionMap.markGameObject(object, true); const nearbyPlayers = activeWorld.findNearbyPlayers(position, 16, this.instanceId) || []; nearbyPlayers.forEach(player => player.outgoingPackets.setLocationObject(object, position)); } /** * Fetch a list of world modifications from this instance. * @param worldPosition The game world position to find the chunk for. */ public getInstancedChunk(worldPosition: Position): InstancedChunk; /** * Fetch a list of world modifications from this instance. * @param x The X coordinate to find the chunk of. * @param y The Y coordinate to find the chunk of. * @param level The height level of the chunk. */ public getInstancedChunk(x: number, y: number, level: number): InstancedChunk; public getInstancedChunk(worldPositionOrX: Position | number, y?: number, level?: number): InstancedChunk { let chunkPosition: Position | null = null; if (typeof worldPositionOrX === 'number') { const chunk = activeWorld.chunkManager.getChunk({ x: worldPositionOrX, // using ! here because we know that if the first parameter is a number, the other two will be too y: y!, level: level!, }); if (chunk) { chunkPosition = chunk.position; } } else { chunkPosition = activeWorld.chunkManager.getChunkForWorldPosition(worldPositionOrX)?.position || null; } if (!chunkPosition) { // Chunk not found - fail gracefully logger.error('Failed to find chunk for world position', worldPositionOrX, y, level); logger.error('Something has likely gone horribly wrong!'); return { collisionMap: new CollisionMap(0, 0, 0, { instance: this }), mods: new Map(), }; } if (!this.chunkModifications.has(chunkPosition.key)) { this.chunkModifications.set(chunkPosition.key, { collisionMap: new CollisionMap(chunkPosition.x, chunkPosition.y, chunkPosition.level, { instance: this }), mods: new Map(), }); } // using ! here because we know the chunk exists, as we've just created it if it didn't return this.chunkModifications.get(chunkPosition.key)!; } /** * Fetches the list of tile modifications for a specific game tile in this instance. * @param worldPosition The world position to find the modifications for. */ public getTileModifications(worldPosition: Position): { chunk: InstancedChunk; mods: TileModifications } { const instancedChunk = this.getInstancedChunk(worldPosition); if (instancedChunk.mods.has(worldPosition.key)) { return { chunk: instancedChunk, // using ! here because we know it exists mods: instancedChunk.mods.get(worldPosition.key)!, }; } return { chunk: instancedChunk, mods: new TileModifications() }; } /** * Adds a new player to this instance. * @param player The player to allow in. */ public addPlayer(player: Player): void { this.players.set(player.username, player); } /** * Removes a player from this instance. * If the instance is not the global instance and * there are no more players within it, then this will gracefully close the instance. * @param player The player remove from the instance. */ public removePlayer(player: Player): void { this.players.delete(player.username); if (this.instanceId !== activeWorld.globalInstance.instanceId && this.players.size === 0) { this.chunkModifications.clear(); const instancedNpcs = activeWorld.findNpcsByInstance(this.instanceId); instancedNpcs?.forEach(npc => activeWorld.deregisterNpc(npc)); } } /** * Checks to see if the specified world tile is devoid of modifications for this instance. * If it is, this method will delete the `TileModifications` entry to free up memory. * @param worldPosition The position of the game world tile to check. * @private */ private clearTileIfEmpty(worldPosition: Position): void { const instancedChunk = this.getInstancedChunk(worldPosition); const mods = instancedChunk.mods.get(worldPosition.key); if (!mods) { return; } if (mods.empty) { instancedChunk.mods.delete(worldPosition.key); } } } ================================================ FILE: src/engine/world/items/item-container.ts ================================================ import { findItem } from '@engine/config/config-handler'; import { hasValueNotNull } from '@engine/util/data'; import { fromNote } from '@engine/world/items/item'; import { logger } from '@runejs/common'; import { filestore } from '@server/game/game-server'; import { Subject } from 'rxjs'; import type { Item } from './item'; export interface ContainerUpdateEvent { slot?: number; item?: Item | null; type: 'ADD' | 'REMOVE' | 'SWAP' | 'SET' | 'SET_ALL' | 'UPDATE_AMOUNT' | 'CLEAR_ALL'; } export const getItemFromContainer = (itemId: number, slot: number, container: ItemContainer): Item | null => { if (slot < 0 || slot > container.items.length - 1) { return null; } const item = container.items[slot]; if (!item || item.itemId !== itemId) { return null; } return item; }; /** * This class represents a container of items. * * TODO (jameskmonger) We should use a Map instead of an array for the items. */ type InventoryMapType = (Item | null)[]; export class ItemContainer { private readonly _size: number; private readonly _items: InventoryMapType; private readonly _containerUpdated: Subject; public constructor(size: number) { this._size = size; this._items = new Array(size); this._containerUpdated = new Subject(); for (let i = 0; i < size; i++) { this._items[i] = null; } } public clear(fireEvent: boolean = true): void { this._items.forEach((item, index) => (this._items[index] = null)); if (fireEvent) { this._containerUpdated.next({ type: 'CLEAR_ALL' }); } } public has(item: number | Item): boolean { return this.findIndex(item) !== -1; } public amount(item: number | Item): number { const itemId = typeof item === 'number' ? item : item.itemId; return this._items .map(item => (item && item.itemId === itemId ? item.amount || 0 : 0)) .reduce((accumulator, currentValue) => accumulator + currentValue); } /** * Finds all slots within the container that contain the specified items. * @param search The item id or Item object to search for. * @returns An array of slot numbers. */ public findAll(search: number | Item): number[] { if (typeof search !== 'number') { search = search.itemId; } const searchItem = findItem(search); if (!searchItem) { logger.error(`Could not find item '${search}' when searching for items in container.`); return []; } const stackable = searchItem.stackable; if (stackable) { const index = this.findIndex(search); if (!hasValueNotNull(index) || index === -1) { return []; } else { return [index]; } } else { const slots: number[] = []; for (let i = 0; i < this.size; i++) { const item = this.items[i]; if (item?.itemId === search) { slots.push(i); } } return slots; } } public findIndex(item: number | Item): number { const itemId = typeof item === 'number' ? item : item.itemId; return this._items.findIndex(i => i?.itemId === itemId); } public setAll(items: (Item | null)[], fireEvent: boolean = true): void { for (let i = 0; i < this._size; i++) { this._items[i] = items[i]; } if (fireEvent) { this._containerUpdated.next({ type: 'SET_ALL' }); } } public set(slot: number, item: Item | null, fireEvent: boolean = true): void { this._items[slot] = item; if (fireEvent) { this._containerUpdated.next({ type: 'SET', slot, item }); } } public findItemIndex(item: Item): number { for (let i = 0; i < this._size; i++) { const inventoryItem = this._items[i]; if (inventoryItem === null) { continue; } if (inventoryItem.itemId === item.itemId && inventoryItem.amount >= item.amount) { return i; } } return -1; } public add(item: number | string | Item, fireEvent: boolean = true): { item: Item; slot: number } | null { if (typeof item === 'number') { item = { itemId: item, amount: 1 } as Item; } else if (typeof item === 'string') { const itemDetails = findItem(item); if (!itemDetails) { logger.warn(`Item ${item} not configured on the server.`); return null; } item = { itemId: itemDetails.gameId, amount: 1 }; } const existingItemIndex = this.findItemIndex({ itemId: item.itemId, amount: 1 }); const cacheItem = findItem(item.itemId); if (!cacheItem) { logger.error(`Could not find item '${item.itemId}' in cache when adding item to container.`); return null; } if (existingItemIndex !== -1 && (cacheItem.stackable || cacheItem.bankNoteId != null)) { const newItem = { itemId: item.itemId, // using ! here because we know the item exists in the inventory amount: (this._items[existingItemIndex]!.amount += item.amount), } as Item; this.set(existingItemIndex, newItem, false); if (fireEvent) { this._containerUpdated.next({ type: 'UPDATE_AMOUNT', slot: existingItemIndex, item }); } // Item already in inventory and is stackable return { item: newItem, slot: existingItemIndex }; } else { const newItemIndex = this.getFirstOpenSlot(); if (newItemIndex === -1 || item.amount === 0) { // Not enough container space, or the amount of item being added is 0. return null; } this._items[newItemIndex] = item; if (fireEvent) { this._containerUpdated.next({ type: 'ADD', slot: newItemIndex, item }); } // Item added to inventory return { item, slot: newItemIndex }; } } public addStacking(item: number | Item, fireEvent: boolean = true): { item: Item; slot: number } | null { if (typeof item === 'number') { item = { itemId: item, amount: 1 } as Item; } const existingItemIndex = this.findItemIndex({ itemId: item.itemId, amount: 1 }); if (existingItemIndex !== -1) { const newItem = { itemId: item.itemId, // using ! here because we know the item exists in the inventory amount: (this._items[existingItemIndex]!.amount += item.amount), } as Item; this.set(existingItemIndex, newItem, false); if (fireEvent) { this._containerUpdated.next({ type: 'UPDATE_AMOUNT', slot: existingItemIndex, item }); } // Item already in inventory and is stackable return { item: newItem, slot: existingItemIndex }; } else { const newItemIndex = this.getFirstOpenSlot(); if (newItemIndex === -1) { // Not enough container space return null; } this._items[newItemIndex] = item; if (fireEvent) { this._containerUpdated.next({ type: 'ADD', slot: newItemIndex, item }); } // Item added to inventory return { item, slot: newItemIndex }; } } public amountInStack(slot: number): number { return this._items[slot]?.amount || 0; } public removeFirst(item: number | Item, fireEvent: boolean = true): number { const slot = this.findIndex(item); if (slot === -1) { return -1; } this._items[slot] = null; if (fireEvent) { this._containerUpdated.next({ type: 'REMOVE', slot }); } return slot; } public remove(slot: number, fireEvent: boolean = true): Item | null { const item = this._items[slot]; this._items[slot] = null; if (fireEvent) { this._containerUpdated.next({ type: 'REMOVE', slot }); } return item; } public getFirstOpenSlot(): number { return this._items.findIndex(item => !hasValueNotNull(item)); } public hasSpace(): boolean { return this.getFirstOpenSlot() !== -1; } public getOpenSlotCount(): number { let count = 0; for (let i = 0; i < this._size; i++) { if (!hasValueNotNull(this._items[i])) { count++; } } return count; } public getOpenSlots(): number[] { const slots: number[] = []; for (let i = 0; i < this._size; i++) { if (!hasValueNotNull(this._items[i])) { slots.push(i); } } return slots; } public swap(fromSlot: number, toSlot: number): void { const fromItem = this._items[fromSlot]; const toItem = this._items[toSlot]; this._items[toSlot] = fromItem; this._items[fromSlot] = toItem; } public weight(): number { let weight = 0; for (const item of this._items) { if (!item) { continue; } const itemData = findItem(item.itemId); if (!itemData?.weight) { continue; } weight += itemData.weight; } return weight; } public canFit(item: Item, everythingStacks: boolean = false): boolean { const itemDefinition = filestore.configStore.itemStore.getItem(item.itemId); if (!itemDefinition) { throw new Error(`Item ID ${item.itemId} not found!`); } if (itemDefinition.stackable || everythingStacks || fromNote(item) > -1) { if (this.has(item.itemId)) { // using ! here because we know that we have the item in the inventory const invItem = this.items[this.findIndex(item.itemId)]!; return invItem.amount + item.amount <= 2147483647; } return this.hasSpace(); } else { return this.getOpenSlotCount() >= item.amount; } } public get size(): number { return this._size; } public get items(): (Item | null)[] { return this._items; } public get containerUpdated(): Subject { return this._containerUpdated; } } ================================================ FILE: src/engine/world/items/item.ts ================================================ import { findItem, itemMap, widgets } from '@engine/config/config-handler'; import type { ParentWidget, StaticItemWidget, WidgetBase } from '@runejs/filestore'; import { filestore } from '@server/game/game-server'; export interface Item { itemId: number; amount: number; } function itemInventoryOptions(itemId: number): string[] { const itemDefinition = filestore.configStore.itemStore.getItem(itemId); if (!itemDefinition) { return []; } return itemDefinition.widgetOptions || []; } // TODO: Move to the filestore function IsParentWidget(widget: WidgetBase): widget is ParentWidget { return 'children' in widget; } // TODO: Move to the filestore function IsStaticItemWidget(widget: WidgetBase): widget is StaticItemWidget { return 'items' in widget; } export const getItemOptions = (itemId: number, widget: { widgetId: number; containerId: number }): string[] => { const widgetDefinition = filestore.widgetStore.decodeWidget(widget.widgetId) as WidgetBase; if (widget.widgetId === widgets.inventory.widgetId) { return itemInventoryOptions(itemId); } let optionsWidget: StaticItemWidget | null = null; if (IsStaticItemWidget(widgetDefinition) && widgetDefinition.options && !widget.containerId) { optionsWidget = widgetDefinition; } if (IsParentWidget(widgetDefinition)) { const widgetChild = widgetDefinition.children[widget.containerId]; if (IsStaticItemWidget(widgetChild)) { optionsWidget = widgetChild; } } if (!optionsWidget || !optionsWidget.items || !optionsWidget.options) { return itemInventoryOptions(itemId); } return optionsWidget.options; }; export const getItemOption = (itemId: number, optionNumber: number, widget: { widgetId: number; containerId: number }): string => { const optionIndex = optionNumber - 1; const options = getItemOptions(itemId, widget); let option = 'option-' + optionNumber; if (options && options.length >= optionNumber) { if (options[optionIndex] !== null && options[optionIndex].toLowerCase() !== 'hidden') { option = options[optionIndex].toLowerCase(); } } option = option.replace(/ /g, '-'); if (['wield', 'wear', 'equip'].find(s => s === option)) { option = 'equip'; } return option; }; export function parseItemId(item: number | Item): number { return typeof item !== 'number' ? item.itemId : item; } export function toNote(item: number | Item): number { item = parseItemId(item); let notedItem = Object.values(itemMap).find(i => i.bankNoteId === item); if (!notedItem) { const fallbackNote = findItem(item + 1); if (fallbackNote?.bankNoteId === item) { notedItem = fallbackNote; } } return !notedItem ? -1 : notedItem.gameId; } export function fromNote(item: number | Item): number { item = parseItemId(item); const notedItem = findItem(item); return !notedItem ? -1 : notedItem.bankNoteId; } ================================================ FILE: src/engine/world/items/world-item.ts ================================================ import type { Player } from '@engine/world/actor/player/player'; import type { WorldInstance } from '@engine/world/instances'; import type { Position } from '@engine/world/position'; export type WorldItem = { itemId: number; amount: number; position: Position; owner?: Player; expires?: number; respawns?: number; removed?: boolean; instance: WorldInstance; }; ================================================ FILE: src/engine/world/map/chunk-manager.ts ================================================ import { logger } from '@runejs/common'; import type { LandscapeFile, LandscapeObject, MapFile } from '@runejs/filestore'; import { filestore } from '@server/game/game-server'; import { Position } from '../position'; import { Chunk } from './chunk'; export class Tile { public settings: number = 0; public blocked: boolean = false; public bridge: boolean = false; public constructor( public x: number, public y: number, public level: number, settings?: number, ) { if (settings) { this.setSettings(settings); } } public setSettings(settings: number): void { this.settings = settings; this.blocked = (this.settings & 0x1) === 1; this.bridge = (this.settings & 0x2) === 2; } } export interface MapRegion { objects: LandscapeObject[]; mapFile: MapFile; } /** * Controls all of the game world's map chunks. */ export class ChunkManager { public readonly regionMap: Map = new Map(); private readonly chunkMap: Map; public constructor() { this.chunkMap = new Map(); } public getTile(position: Position): Tile { const chunkX = position.chunkX + 6; const chunkY = position.chunkY + 6; const mapRegionX = Math.floor(chunkX / 8); const mapRegionY = Math.floor(chunkY / 8); const mapWorldPositionX = (mapRegionX & 0xff) * 64; const mapWorldPositionY = mapRegionY * 64; const regionSettings = this.regionMap.get(`${mapRegionX},${mapRegionY}`)?.mapFile?.tileSettings; this.registerMapRegion(mapRegionX, mapRegionY); if (!regionSettings) { return new Tile(position.x, position.y, position.level); } const tileX = position.x - mapWorldPositionX; const tileY = position.y - mapWorldPositionY; const tileLevel = position.level; let tileSettings = regionSettings[tileLevel][tileX][tileY]; if (tileLevel < 3) { // Check for a bridge tile above the active tile const tileAboveSettings = regionSettings[tileLevel + 1][tileX][tileY]; if ((tileAboveSettings & 0x2) === 2) { // Set this tile as walkable if the tile above is a bridge - // This is because the maps are stored with bridges being one level // above where their collision maps need to be tileSettings = 0; } } return new Tile(position.x, position.y, tileLevel, tileSettings); } public registerMapRegion(mapRegionX: number, mapRegionY: number): void { const key = `${mapRegionX},${mapRegionY}`; if (this.regionMap.has(key)) { // Map region already registered return; } this.regionMap.delete(key); let mapFile: MapFile | null = null; let landscapeFile: LandscapeFile | null = null; try { mapFile = filestore.regionStore.getMapFile(mapRegionX, mapRegionY); } catch (error) { logger.error(`Error decoding map file ${mapRegionX},${mapRegionY}`); } try { landscapeFile = filestore.regionStore.getLandscapeFile(mapRegionX, mapRegionY); } catch (error) { logger.error(`Error decoding landscape file ${mapRegionX},${mapRegionY}`); } if (!mapFile) { logger.error(`No decoded map file ${mapRegionX},${mapRegionY}`); return; } if (!landscapeFile) { logger.error(`No decoded landscape file ${mapRegionX},${mapRegionY}`); return; } const region: MapRegion = { mapFile, objects: landscapeFile?.landscapeObjects || [] }; this.regionMap.set(key, region); this.registerObjects(region.objects, mapFile); } public registerObjects(objects: LandscapeObject[], mapFile: MapFile): void { if (!objects || objects.length === 0) { return; } const mapWorldPositionX = (mapFile.regionX & 0xff) * 64; const mapWorldPositionY = mapFile.regionY * 64; for (const object of objects) { const position = new Position(object.x, object.y, object.level); const localX = object.x - mapWorldPositionX; const localY = object.y - mapWorldPositionY; for (let level = 3; level >= 0; level--) { if ((mapFile.tileSettings[level][localX][localY] & 0x2) === 2) { // Object is on or underneath a bridge tile and needs to move down one level position.move(object.x, object.y, object.level - 1); } } this.getChunkForWorldPosition(position).setFilestoreLandscapeObject(object); } } public getSurroundingChunks(chunk: Chunk): Chunk[] { const chunks: Chunk[] = []; const mainX = chunk.position.x; const mainY = chunk.position.y; const level = chunk.position.level; for (let x = mainX - 2; x <= mainX + 2; x++) { for (let y = mainY - 2; y <= mainY + 2; y++) { chunks.push(this.getChunk({ x, y, level })); } } return chunks; } /** * Given a world Position, return the region ID that it is contained within. * @param position The position to use */ public getRegionIdForWorldPosition(position: Position): number { return ((position.x >> 6) << 8) + (position.y >> 6); } public getChunkForWorldPosition(position: Position): Chunk { return this.getChunk({ x: position.chunkX, y: position.chunkY, level: position.level }); } public getChunk(position: Position | { x: number; y: number; level: number }): Chunk { if (!(position instanceof Position)) { position = new Position(position.x, position.y, position.level); } const pos = position as Position; if (this.chunkMap.has(pos.key)) { // using ! here because we know it exists return this.chunkMap.get(pos.key)!; } else { const chunk = new Chunk(pos); this.chunkMap.set(pos.key, chunk); chunk.registerMapRegion(); return chunk; } } } ================================================ FILE: src/engine/world/map/chunk.ts ================================================ import { activeWorld } from '@engine/world'; import type { WorldItem } from '@engine/world/items/world-item'; import type { LandscapeObject } from '@runejs/filestore'; import type { Npc } from '../actor/npc'; import type { Player } from '../actor/player/player'; import type { Position } from '../position'; import { CollisionMap } from './collision-map'; interface CustomLandscapeObject { reference?: boolean; } export interface ChunkUpdateItem { object?: LandscapeObject & CustomLandscapeObject; worldItem?: WorldItem; type: 'ADD' | 'REMOVE'; } /** * A single map chunk within the game world that keeps track of the entities within it. */ export class Chunk { private readonly _position: Position; private readonly _players: Player[]; private readonly _npcs: Npc[]; private readonly _collisionMap: CollisionMap; private readonly _filestoreLandscapeObjects: Map; public constructor(position: Position) { this._position = position; this._players = []; this._npcs = []; this._collisionMap = new CollisionMap(position.x, position.y, position.level, { chunk: this }); this._filestoreLandscapeObjects = new Map(); } public registerMapRegion(): void { const mapRegionX = Math.floor((this.position.x + 6) / 8); const mapRegionY = Math.floor((this.position.y + 6) / 8); activeWorld.chunkManager.registerMapRegion(mapRegionX, mapRegionY); } public setFilestoreLandscapeObject(landscapeObject: LandscapeObject): void { this._filestoreLandscapeObjects.set(`${landscapeObject.x},${landscapeObject.y},${landscapeObject.objectId}`, landscapeObject); this._collisionMap.markGameObject(landscapeObject, true); } public addPlayer(player: Player): void { if (this._players.findIndex(p => p.equals(player)) === -1) { this._players.push(player); } } public removePlayer(player: Player): void { const index = this._players.findIndex(p => p.equals(player)); if (index !== -1) { this._players.splice(index, 1); } } public addNpc(npc: Npc): void { if (this._npcs.findIndex(n => n.equals(npc)) === -1) { this._npcs.push(npc); } } public removeNpc(npc: Npc): void { const index = this._npcs.findIndex(n => n.equals(npc)); if (index !== -1) { this._npcs.splice(index, 1); } } public getFilestoreLandscapeObject(objectId: number, position: Position): LandscapeObject | null { return this.filestoreLandscapeObjects.get(`${position.x},${position.y},${objectId}`) || null; } public equals(chunk: Chunk): boolean { return this.position.x === chunk.position.x && this.position.y === chunk.position.y && this.position.level === chunk.position.level; } public get position(): Position { return this._position; } public get players(): Player[] { return this._players; } public get npcs(): Npc[] { return this._npcs; } public get collisionMap(): CollisionMap { return this._collisionMap; } public get filestoreLandscapeObjects(): Map { return this._filestoreLandscapeObjects; } } ================================================ FILE: src/engine/world/map/collision-map.ts ================================================ import { activeWorld } from '@engine/world'; import type { WorldInstance } from '@engine/world/instances'; import { logger } from '@runejs/common'; import type { LandscapeObject } from '@runejs/filestore'; import { filestore } from '@server/game/game-server'; import type { Chunk } from './chunk'; /** * A map of collision masks for a chunk within the game world. */ export class CollisionMap { private heightLevel: number; private x: number; private y: number; private sizeX: number; private sizeY: number; private _insetX: number; private _insetY: number; private _adjacency: (number | null)[][]; private chunk: Chunk | undefined; private instance: WorldInstance | undefined; public constructor(x: number, y: number, heightLevel: number, options?: { chunk?: Chunk; instance?: WorldInstance }) { this.heightLevel = heightLevel; this.x = x; this.y = y; this.sizeX = 8; this.sizeY = 8; this._insetX = (x + 6) * 8; this._insetY = (y + 6) * 8; this.chunk = options?.chunk; this.instance = options?.instance; this._adjacency = new Array(this.sizeX); for (let i = 0; i < this.sizeX; i++) { this._adjacency[i] = new Array(this.sizeY); } this.reset(); } public markGameObject(landscapeObject: LandscapeObject, mark: boolean): void { const x: number = landscapeObject.x; const y: number = landscapeObject.y; const objectType = landscapeObject.type; const objectOrientation = landscapeObject.orientation; const objectDetails = filestore.configStore.objectStore.getObject(landscapeObject.objectId); if (!objectDetails) { logger.error(`Could not find object details for object id: ${landscapeObject.objectId} when marking collision map.`); return; } if (objectDetails.solid) { if (objectType === 22) { if (objectDetails.hasOptions) { this.markBlocked(x, y, mark); } } else if (objectType >= 9) { this.markSolidOccupant( x, y, objectDetails.rendering.sizeX, objectDetails.rendering.sizeY, objectOrientation, objectDetails.nonWalkable, mark, ); } else if (objectType >= 0 && objectType <= 3) { if (mark) { this.markWall(x, y, objectType, objectOrientation, objectDetails.nonWalkable); } else { this.unmarkWall(x, y, objectType, objectOrientation, objectDetails.nonWalkable); } } } } public reset(): void { for (let x = 0; x < this.sizeX; x++) { for (let y = 0; y < this.sizeY; y++) { this._adjacency[x][y] = this.chunk ? 0 : null; } } } public markWall(x: number, y: number, type: number, rotation: number, walkable: boolean): void { x -= this._insetX; y -= this._insetY; if (type == 0) { if (rotation == 0) { this.set(x, y, 128); this.set(x - 1, y, 8); } if (rotation == 1) { this.set(x, y, 2); this.set(x, y + 1, 32); } if (rotation == 2) { this.set(x, y, 8); this.set(x + 1, y, 128); } if (rotation == 3) { this.set(x, y, 32); this.set(x, y - 1, 2); } } if (type == 1 || type == 3) { if (rotation == 0) { this.set(x, y, 1); this.set(x - 1, y + 1, 16); } if (rotation == 1) { this.set(x, y, 4); this.set(x + 1, y + 1, 64); } if (rotation == 2) { this.set(x, y, 16); this.set(x + 1, y - 1, 1); } if (rotation == 3) { this.set(x, y, 64); this.set(x - 1, y - 1, 4); } } if (type == 2) { if (rotation == 0) { this.set(x, y, 130); this.set(x - 1, y, 8); this.set(x, y + 1, 32); } if (rotation == 1) { this.set(x, y, 10); this.set(x, y + 1, 32); this.set(x + 1, y, 128); } if (rotation == 2) { this.set(x, y, 40); this.set(x + 1, y, 128); this.set(x, y - 1, 2); } if (rotation == 3) { this.set(x, y, 160); this.set(x, y - 1, 2); this.set(x - 1, y, 8); } } if (walkable) { if (type == 0) { if (rotation == 0) { this.set(x, y, 0x10000); this.set(x - 1, y, 4096); } if (rotation == 1) { this.set(x, y, 1024); this.set(x, y + 1, 16384); } if (rotation == 2) { this.set(x, y, 4096); this.set(x + 1, y, 0x10000); } if (rotation == 3) { this.set(x, y, 16384); this.set(x, y - 1, 1024); } } if (type == 1 || type == 3) { if (rotation == 0) { this.set(x, y, 512); this.set(x - 1, y + 1, 8192); } if (rotation == 1) { this.set(x, y, 2048); this.set(x + 1, y + 1, 32768); } if (rotation == 2) { this.set(x, y, 8192); this.set(x + 1, y - 1, 512); } if (rotation == 3) { this.set(x, y, 32768); this.set(x - 1, y - 1, 2048); } } if (type == 2) { if (rotation == 0) { this.set(x, y, 0x10400); this.set(x - 1, y, 4096); this.set(x, y + 1, 16384); } if (rotation == 1) { this.set(x, y, 5120); this.set(x, y + 1, 16384); this.set(x + 1, y, 0x10000); } if (rotation == 2) { this.set(x, y, 20480); this.set(x + 1, y, 0x10000); this.set(x, y - 1, 1024); } if (rotation == 3) { this.set(x, y, 0x14000); this.set(x, y - 1, 1024); this.set(x - 1, y, 4096); } } } } public unmarkWall(x: number, y: number, position: number, rotation: number, impenetrable: boolean): void { x -= this._insetX; y -= this._insetY; if (position == 0) { if (rotation == 0) { this.unset(x, y, 128); this.unset(x - 1, y, 8); } if (rotation == 1) { this.unset(x, y, 2); this.unset(x, y + 1, 32); } if (rotation == 2) { this.unset(x, y, 8); this.unset(x + 1, y, 128); } if (rotation == 3) { this.unset(x, y, 32); this.unset(x, y - 1, 2); } } if (position == 1 || position == 3) { if (rotation == 0) { this.unset(x, y, 1); this.unset(x - 1, y + 1, 16); } if (rotation == 1) { this.unset(x, y, 4); this.unset(x + 1, y + 1, 64); } if (rotation == 2) { this.unset(x, y, 16); this.unset(x + 1, y - 1, 1); } if (rotation == 3) { this.unset(x, y, 64); this.unset(x - 1, y - 1, 4); } } if (position == 2) { if (rotation == 0) { this.unset(x, y, 130); this.unset(x - 1, y, 8); this.unset(x, y + 1, 32); } if (rotation == 1) { this.unset(x, y, 10); this.unset(x, y + 1, 32); this.unset(x + 1, y, 128); } if (rotation == 2) { this.unset(x, y, 40); this.unset(x + 1, y, 128); this.unset(x, y - 1, 2); } if (rotation == 3) { this.unset(x, y, 160); this.unset(x, y - 1, 2); this.unset(x - 1, y, 8); } } if (impenetrable) { if (position == 0) { if (rotation == 0) { this.unset(x, y, 0x10000); this.unset(x - 1, y, 4096); } if (rotation == 1) { this.unset(x, y, 1024); this.unset(x, y + 1, 16384); } if (rotation == 2) { this.unset(x, y, 4096); this.unset(x + 1, y, 0x10000); } if (rotation == 3) { this.unset(x, y, 16384); this.unset(x, y - 1, 1024); } } if (position == 1 || position == 3) { if (rotation == 0) { this.unset(x, y, 512); this.unset(x - 1, y + 1, 8192); } if (rotation == 1) { this.unset(x, y, 2048); this.unset(x + 1, y + 1, 32768); } if (rotation == 2) { this.unset(x, y, 8192); this.unset(x + 1, y - 1, 512); } if (rotation == 3) { this.unset(x, y, 32768); this.unset(x - 1, y - 1, 2048); } } if (position == 2) { if (rotation == 0) { this.unset(x, y, 0x10400); this.unset(x - 1, y, 4096); this.unset(x, y + 1, 16384); } if (rotation == 1) { this.unset(x, y, 5120); this.unset(x, y + 1, 16384); this.unset(x + 1, y, 0x10000); } if (rotation == 2) { this.unset(x, y, 20480); this.unset(x + 1, y, 0x10000); this.unset(x, y - 1, 1024); } if (rotation == 3) { this.unset(x, y, 0x14000); this.unset(x, y - 1, 1024); this.unset(x - 1, y, 4096); } } } } public markSolidOccupant( occupantX: number, occupantY: number, width: number, height: number, rotation: number, walkable: boolean, mark: boolean, ): void { let occupied = 256; if (walkable) { occupied += 0x20000; } occupantX -= this._insetX; occupantY -= this._insetY; if (rotation === 1 || rotation === 3) { const off = width; width = height; height = off; } for (let x = occupantX; x < occupantX + width; x++) { for (let y = occupantY; y < occupantY + height; y++) { if (mark) { this.set(x, y, occupied); } else { this.unset(x, y, occupied); } } } } public markBlocked(x: number, y: number, mark: boolean): void { x -= this._insetX; y -= this._insetY; if (this._adjacency[x][y] === null) { this._adjacency[x][y] = 0; } if (mark) { // @ts-ignore this._adjacency[x][y] |= 0x200000; } else { // @ts-ignore this._adjacency[x][y] &= 0xdfffff; } } public set(x: number, y: number, flag: number): void { let outOfBounds = false; let offsetX = 0; let offsetY = 0; if (x < 0) { offsetX = -1; x = 8 + x; } else if (x > 7) { offsetX = 1; x = x - 8; } if (y < 0) { offsetY = -1; y = 8 + y; } else if (y > 7) { offsetY = 1; y = y - 8; } if (offsetX != 0 || offsetY != 0) { this.getSiblingCollisionMap(offsetX, offsetY)?.set(x, y, flag); outOfBounds = true; } if (!outOfBounds) { if (this._adjacency[x][y] === null) { this._adjacency[x][y] = 0; } // @ts-ignore this._adjacency[x][y] |= flag; } } public unset(x: number, y: number, flag: number): void { let outOfBounds = false; if (x < 0) { this.getSiblingCollisionMap(-1, 0)?.unset(7, y, flag); outOfBounds = true; } else if (x > 7) { this.getSiblingCollisionMap(1, 0)?.unset(0, y, flag); outOfBounds = true; } if (y < 0) { this.getSiblingCollisionMap(0, -1)?.unset(x, 7, flag); outOfBounds = true; } else if (y > 7) { this.getSiblingCollisionMap(0, 1)?.unset(x, 0, flag); outOfBounds = true; } if (!outOfBounds) { if (this._adjacency[x][y] === null) { this._adjacency[x][y] = 0; } // @ts-ignore this._adjacency[x][y] &= 0xffffff - flag; } } public getSiblingCollisionMap(offsetX: number, offsetY: number): CollisionMap | null { if (this.chunk) { const offsetChunk: Chunk = activeWorld.chunkManager.getChunk({ x: this.chunk.position.x + offsetX, y: this.chunk.position.y + offsetY, level: this.heightLevel, }); return offsetChunk.collisionMap; } else if (this.instance) { const instanceChunk = this.instance.getInstancedChunk(this.x + offsetX, this.y + offsetY, this.heightLevel); return instanceChunk.collisionMap; } return null; } public get insetX(): number { return this._insetX; } public get insetY(): number { return this._insetY; } public get adjacency(): (number | null)[][] { return this._adjacency; } } ================================================ FILE: src/engine/world/map/landscape-object.ts ================================================ import type { LandscapeObject } from '@runejs/filestore'; export interface ModifiedLandscapeObject extends LandscapeObject { metadata?: { [key: string]: any }; } export const objectKey = (object: LandscapeObject, level: boolean = false): string => { return `${object.x},${object.y}${level ? `,${object.level}` : ''},${object.objectId}`; }; ================================================ FILE: src/engine/world/map/region.ts ================================================ /** * Different types of world regions. * map: 64x64 tile (13x13 tile chunks) full map region file. * chunk: 8x8 tile chunk within a map. */ import type { Position } from '@engine/world/position'; export type RegionType = 'mapfile' | 'region' | 'chunk'; /** * A type for defining region tile sizes. */ export type RegionSizeMap = { [key in RegionType]: number; }; /** * A map of region types to tile sizes. */ export const regionSizes: RegionSizeMap = { mapfile: 104, region: 64, chunk: 8, }; export abstract class ConstructedChunk { public orientation: number; protected constructor(rotation: number = 0) { this.orientation = rotation; } public abstract getTemplatePosition(): Position; public get templatePosition(): Position { return this.getTemplatePosition(); } } export interface ConstructedRegion { renderPosition: Position; chunks: ConstructedChunk[][][]; drawOffsetX?: number; drawOffsetY?: number; } export const getTemplateRotatedX = (orientation: number, localX: number, localY: number, sizeX: number = 1, sizeY: number = 1): number => { if (orientation === 1 || orientation === 3) { const i = sizeX; sizeX = sizeY; sizeY = i; } if (orientation === 0) { return localX; } if (orientation === 1) { return 7 - (localY - sizeY + 1); } if (orientation === 2) { return 7 - (localX + sizeX + 1); } return localY; }; export const getTemplateRotatedY = (orientation: number, localX: number, localY: number, sizeX: number = 1, sizeY: number = 1): number => { if (orientation === 1 || orientation === 3) { const i = sizeX; sizeX = sizeY; sizeY = i; } if (orientation === 0) { return localY; } if (orientation === 1) { return localX; } if (orientation === 2) { return 7 - (localY + sizeY + 1); } return 7 - (localX - sizeX + 1); }; export const getTemplateLocalX = (orientation: number, localX: number, localY: number, sizeX: number = 1, sizeY: number = 1): number => { if (orientation === 2) { const i = sizeX; sizeX = sizeY; sizeY = i; } if (orientation === 0) { return localX; } else if (orientation === 1) { return 7 - (localY + sizeY) + 1; } else if (orientation === 2) { return 7 - (localX + sizeX) + 1; } else { // 3 return localY; } }; export const getTemplateLocalY = (orientation: number, localX: number, localY: number, sizeX: number = 1, sizeY: number = 1): number => { if (orientation === 2) { const i = sizeX; sizeX = sizeY; sizeY = i; } if (orientation === 0) { return localY; } else if (orientation === 1) { return localX; } else if (orientation === 2) { return 7 - (localY + sizeY) + 1; } else { // 3 return 7 - (localX + sizeX) + 1; } }; ================================================ FILE: src/engine/world/position.ts ================================================ import type { Direction } from '@engine/world/direction'; import { directionData } from '@engine/world/direction'; import { logger } from '@runejs/common'; import type { LandscapeObject } from '@runejs/filestore'; import { filestore } from '@server/game/game-server'; const directionDeltaX = [-1, 0, 1, -1, 1, -1, 0, 1]; const directionDeltaY = [1, 1, 1, 0, 0, -1, -1, -1]; /** * A simplified x/y/level coordinate class. */ export class Coords { x: number; y: number; level: number; static equals(a: Coords, b: Coords): boolean { return a.x === b.x && a.y === b.y && a.level === b.level; } } /** * Represents a single position, or coordinate, within the game world. */ export class Position { public metadata: { [key: string]: any } = {}; private _x: number; private _y: number; private _level: number; public constructor(position: Position); public constructor(coords: Coords); public constructor(x: number, y: number, level?: number); public constructor(arg0: number | Coords | Position, y?: number, level?: number) { if (typeof arg0 === 'number') { // using ! here, because we know that if arg0 is a number, then y and level are numbers this.move(arg0, y!, level); } else { this.move(arg0.x, arg0.y, arg0.level); } } public clone(): Position { return new Position(this.x, this.y, this.level); } public withinInteractionDistance(gameObject: LandscapeObject, minimumDistance?: number): boolean; public withinInteractionDistance(position: Position, minimumDistance?: number): boolean; public withinInteractionDistance(target: LandscapeObject | Position, minimumDistance?: number): boolean; public withinInteractionDistance(target: LandscapeObject | Position, minimumDistance: number = 1): boolean { if (target instanceof Position) { return this.distanceBetween(target) <= minimumDistance; } else { const definition = filestore.configStore.objectStore.getObject(target.objectId); if (!definition) { logger.warn(`Object with id ${target.objectId} does not exist in the object store.`); } const occupantX = target.x; const occupantY = target.y; let width = definition?.rendering?.sizeX || 1; let height = definition?.rendering?.sizeY || 1; if (width === undefined || width === null || width < 1) { width = 1; } if (height === undefined || height === null || height < 1) { height = 1; } if (width === 1 && height === 1) { return this.distanceBetween(new Position(occupantX, occupantY, target.level)) <= minimumDistance; } else { if (target.orientation === 1 || target.orientation === 3) { const off = width; width = height; height = off; } for (let x = occupantX; x < occupantX + width; x++) { for (let y = occupantY; y < occupantY + height; y++) { if (this.distanceBetween(new Position(x, y, target.level)) <= minimumDistance) { return true; } } } } } return false; } /** * Whether or not the specified position is within the game's view distance of this position. * @param position The game world position to check the distance of. */ public withinViewDistance(position: Position): boolean { if (position.level !== this.level) { return false; } const offsetX = this.x - position.x; const offsetY = this.y - position.y; return offsetX < 16 && offsetY < 16 && offsetX > -16 && offsetY > -16; } /** * Checks to see if this position is within the two given boundaries of min and max. * @param min The minimum coordinate to check within. * @param max The maximum coordinate to check within. * @param checkPlane Whether or not to check if the position is within the same plane. Defaults to true. */ public within(min: Position, max: Position, checkPlane: boolean = true): boolean { if (checkPlane && (min.level !== max.level || max.level !== this.level)) { return false; } return this.x >= min.x && this.x <= max.x && this.y >= min.y && this.y <= max.y; } public move(x: number, y: number, level?: number): Position { this._x = x; this._y = y; if (level === undefined) { this._level = 0; } else { this._level = level; } return this; } public equalsIgnoreLevel(position: Position | { x: number; y: number }): boolean { if (!(position instanceof Position)) { position = new Position(position.x, position.y); } return this._x === position.x && this._y === position.y; } public distanceBetween(other: Position): number { return Math.abs(Math.sqrt((this.x - other.x) * (this.x - other.x) + (this.y - other.y) * (this.y - other.y))); } public fromDirection(direction: number): Position { return new Position(this.x + directionDeltaX[direction], this.y + directionDeltaY[direction], this.level); } public step(steps: number, direction: Direction): Position { return new Position(this.x + steps * directionData[direction].deltaX, this.y + steps * directionData[direction].deltaY, this.level); } public copy(): Position { return new Position(this._x, this._y, this._level); } public equals(position: Position | { x: number; y: number; level: number }): boolean { if (!(position instanceof Position)) { position = new Position(position.x, position.y, position.level); } return this._x === position.x && this._y === position.y && this._level === position.level; } public calculateChunkLocalX(position: Position): number { return this._x - 8 * position.chunkX; } public calculateChunkLocalY(position: Position): number { return this._y - 8 * position.chunkY; } /** * Sets the value of X and returns the current Position instance for chaining. * @param x The new value to set the current Position's X coordinate to. */ public setX(x: number): Position { this._x = x; return this; } /** * Sets the value of Y and returns the current Position instance for chaining. * @param y The new value to set the current Position's Y coordinate to. */ public setY(y: number): Position { this._y = y; return this; } /** * Sets the value of Level and returns the current Position instance for chaining. * @param plane The new value to set the current Position's plane to. */ public setLevel(plane: number): Position { this._level = plane; return this; } /** * Converts this Position into a simple Coords object. */ public get coords(): Coords { return { x: this._x, y: this._y, level: this._level, }; } public get chunkX(): number { return (this._x >> 3) - 6; } public get chunkY(): number { return (this._y >> 3) - 6; } public get chunkLocalX(): number { return this._x - 8 * this.chunkX; } public get chunkLocalY(): number { return this._y - 8 * this.chunkY; } public get localX(): number { return this._x - 8 * (this.chunkX + 6); } public get localY(): number { return this._y - 8 * (this.chunkY + 6); } public get x(): number { return this._x; } public set x(value: number) { this._x = value; } public get y(): number { return this._y; } public set y(value: number) { this._y = value; } public get level(): number { return this._level; } public set level(value: number) { this._level = value; } public get key(): string { return `${this.x},${this.y},${this.level}`; } } ================================================ FILE: src/engine/world/skill-util/glory-boost.ts ================================================ import { findItem } from '@engine/config/config-handler'; import { equipmentIndices } from '@engine/config/item-config'; import type { Player } from '@engine/world/actor/player/player'; export function checkForGemBoost(player: Player): number { // Check if any charged glory is equipped const neckSlotIndex = equipmentIndices['neck']; const neckItem = player.equipment.items[neckSlotIndex]; if (!neckItem) { return 256; } const itemConfig = findItem(neckItem.itemId); if (!itemConfig || !itemConfig.key.startsWith('rs:amulet_of_glory:charged_')) { return 256; } return 86; } ================================================ FILE: src/engine/world/skill-util/harvest-roll.ts ================================================ // Note if adding hunter, Strung rabbit foot makes this out of 94 instead of 99 import { findItem } from '@engine/config/config-handler'; import { randomBetween } from '@engine/util/num'; import type { Item } from '@engine/world/items/item'; export function rollBirdsNestType(): Item { const roll = randomBetween(0, 99); let itemConfigId; if (roll > 3) { // Bird egg if (roll === 0) { itemConfigId = 'rs:birds_egg_red'; } else if (roll === 1) { itemConfigId = 'rs:birds_egg_green'; } else { itemConfigId = 'rs:birds_egg_blue'; } } else if (roll > 34) { itemConfigId = 'rs:birds_nest_ring'; } else { itemConfigId = 'rs:birds_nest_seed'; } const item = findItem(itemConfigId); if (!item) { throw new Error(`Could not find item config for ${itemConfigId}`); } return { itemId: item.gameId, amount: 1 }; } export function rollGemType(): Item { const roll = randomBetween(0, 3); let itemConfigId; if (roll === 0) { itemConfigId = 'rs:uncut_diamond'; } else if (roll === 1) { itemConfigId = 'rs:uncut_ruby'; } else if (roll === 2) { itemConfigId = 'rs:uncut_emerald'; } else { itemConfigId = 'rs:uncut_sapphire'; } const item = findItem(itemConfigId); if (!item) { throw new Error(`Could not find item config for ${itemConfigId}`); } return { itemId: item.gameId, amount: 1 }; } ================================================ FILE: src/engine/world/skill-util/harvest-skill.ts ================================================ import { findItem } from '@engine/config/config-handler'; import type { Player } from '@engine/world/actor/player/player'; import { Skill } from '@engine/world/actor/skills'; import type { HarvestTool } from '@engine/world/config/harvest-tool'; import { getBestAxe } from '@engine/world/config/harvest-tool'; import type { IHarvestable } from '@engine/world/config/harvestable-object'; import { soundIds } from '@engine/world/config/sound-ids'; import { logger } from '@runejs/common'; /** * Check if a player can harvest a given {@link IHarvestable} * * @returns a {@link HarvestTool} if the player can harvest the object, or undefined if they cannot. */ export function canInitiateHarvest(player: Player, target: IHarvestable, skill: Skill): undefined | HarvestTool { const itemConfigId = typeof target.items === 'string' ? target.items : target.items[0].itemConfigId; const item = findItem(itemConfigId); if (!item) { logger.error(`Could not find item with config id ${itemConfigId} for harvestable object.`); player.sendMessage('Sorry, there was an error. Please contact a developer.'); return; } let targetName = item.name.toLowerCase(); switch (skill) { case Skill.MINING: targetName = targetName.replace(' ore', ''); break; } // Rest of the function remains the same... if (!player.skills.hasLevel(skill, target.level)) { switch (skill) { case Skill.WOODCUTTING: player.sendMessage(`You need a Woodcutting level of ${target.level} to chop down this tree.`, true); break; } return; } let tool; switch (skill) { case Skill.WOODCUTTING: tool = getBestAxe(player); break; } if (tool == null) { switch (skill) { case Skill.WOODCUTTING: player.sendMessage('You do not have an axe for which you have the level to use.'); break; } return; } if (!player.inventory.hasSpace()) { player.sendMessage(`Your inventory is too full to hold any more ${targetName}.`, true); player.playSound(soundIds.inventoryFull); return; } return tool; } ================================================ FILE: src/engine/world/sound/music.ts ================================================ export enum MusicPlayerMode { MANUAL = 0, AUTO = 1, } export enum MusicPlayerLoopMode { ENABLED = 0, DISABLED = 1, } export enum MusicTabButtonIds { AUTO_BUTTON_ID = 180, MANUAL_BUTTON_ID = 181, LOOP_BUTTON_ID = 251, } ================================================ FILE: src/engine/world/task.ts ================================================ import { World } from '@engine/world/world'; import { lastValueFrom, timer } from 'rxjs'; import { take } from 'rxjs/operators'; export const schedule = async (ticks: number): Promise => { return lastValueFrom(timer(ticks * World.TICK_LENGTH).pipe(take(1))); }; export const wait = async (waitLength: number): Promise => { return lastValueFrom(timer(waitLength).pipe(take(1))); }; ================================================ FILE: src/engine/world/world.ts ================================================ import Quadtree from 'quadtree-lib'; import { Subject, lastValueFrom } from 'rxjs'; import { take } from 'rxjs/operators'; import { v4 } from 'uuid'; import { logger } from '@runejs/common'; import type { LandscapeObject } from '@runejs/filestore'; import { loadActionFiles } from '@engine/action/loader'; import { findItem, findNpc, findObject, itemSpawns, npcSpawns } from '@engine/config/config-handler'; import { NpcSpawn } from '@engine/config/npc-spawn-config'; import { loadPlugins } from '@engine/plugins/loader'; import type { Task } from '@engine/task/task'; import { TaskScheduler } from '@engine/task/task-scheduler'; import { activeWorld } from '@engine/world'; import type { Actor } from '@engine/world/actor/actor'; import { Npc } from '@engine/world/actor/npc'; import { Player } from '@engine/world/actor/player/player'; import { ExamineCache } from '@engine/world/config/examine-data'; import { parseScenerySpawns } from '@engine/world/config/scenery-spawns'; import { TravelLocations } from '@engine/world/config/travel-locations'; import type { Direction } from '@engine/world/direction'; import { WorldInstance } from '@engine/world/instances'; import { ChunkManager } from '@engine/world/map/chunk-manager'; import type { ConstructedRegion } from '@engine/world/map/region'; import { getTemplateLocalX, getTemplateLocalY } from '@engine/world/map/region'; import { Position } from '@engine/world/position'; import { schedule } from '@engine/world/task'; import { isPlayer } from './actor/util'; export interface QuadtreeKey { x: number; y: number; actor: Actor; } /** * Controls the game world and all entities within it. */ export class World { public static readonly MAX_PLAYERS = 1600; public static readonly MAX_NPCS = 30000; public static readonly TICK_LENGTH = 600; public readonly playerList: Player[] = new Array(World.MAX_PLAYERS).fill(null); public readonly npcList: Npc[] = new Array(World.MAX_NPCS).fill(null); public readonly chunkManager: ChunkManager = new ChunkManager(); public readonly examine: ExamineCache = new ExamineCache(); public readonly scenerySpawns: LandscapeObject[]; public readonly travelLocations: TravelLocations = new TravelLocations(); public readonly playerTree: Quadtree; public readonly npcTree: Quadtree; public readonly globalInstance = new WorldInstance(v4()); public readonly tickComplete: Subject = new Subject(); private readonly scheduler = new TaskScheduler(); private readonly debugCycleDuration: boolean = process.argv.indexOf('-tickTime') !== -1; public constructor() { this.scenerySpawns = parseScenerySpawns(); this.playerTree = new Quadtree({ width: 10000, height: 10000, }); this.npcTree = new Quadtree({ width: 10000, height: 10000, }); this.setupWorldTick(); } public async startup(): Promise { await loadPlugins(); await loadActionFiles(); this.spawnGlobalNpcs(); this.spawnWorldItems(); this.spawnScenery(); } public shutdown(): void { this.kickAllPlayers(); logger.info(`Shutting down world...`); } /** * Adds a task to the world scheduler queue. These tasks will run forever until they are cancelled. * * @warning Did you mean to add a world task, rather than an Actor task? * * If the task has a stack type of `NEVER`, other tasks in the same group will be cancelled. * * @param task The task to add */ public enqueueTask(task: Task): void { this.scheduler.enqueue(task); } /** * Searched for an object by ID at the given position in any of the player's active instances. * @param actor The actor to find the object for. * @param objectId The game ID of the object. * @param objectPosition The game world position that the object is expected at. */ public findObjectAtLocation( actor: Actor, objectId: number, objectPosition: Position, ): { object: LandscapeObject | null; cacheOriginal: boolean } { const x = objectPosition.x; const y = objectPosition.y; const objectChunk = this.chunkManager.getChunkForWorldPosition(objectPosition); let customMap = false; if (isPlayer(actor) && actor.metadata.customMap) { customMap = true; const templateMapObject = this.findCustomMapObject(actor, objectId, objectPosition); if (templateMapObject) { return { object: templateMapObject, cacheOriginal: true }; } } let cacheOriginal = true; let tileModifications; let personalTileModifications; if (isPlayer(actor)) { const instance = actor.instance; if (!instance) { throw new Error(`Player ${actor.username} has no instance.`); } tileModifications = instance.getTileModifications(objectPosition); personalTileModifications = actor.personalInstance.getTileModifications(objectPosition); } else { tileModifications = this.globalInstance.getTileModifications(objectPosition); } let landscapeObject = customMap ? null : objectChunk.getFilestoreLandscapeObject(objectId, objectPosition); if (!landscapeObject) { const tileObjects = [...tileModifications.mods.spawnedObjects]; if (isPlayer(actor)) { tileObjects.push(...personalTileModifications.mods.spawnedObjects); } landscapeObject = tileObjects.find(spawnedObject => spawnedObject.objectId === objectId && spawnedObject.x === x && spawnedObject.y === y) || null; cacheOriginal = false; if (!landscapeObject) { return { object: null, cacheOriginal: false }; } } const hiddenTileObjects = [...tileModifications.mods.hiddenObjects]; if (isPlayer(actor)) { hiddenTileObjects.push(...personalTileModifications.mods.hiddenObjects); } if ( hiddenTileObjects.findIndex( spawnedObject => spawnedObject.objectId === objectId && spawnedObject.x === x && spawnedObject.y === y, ) !== -1 ) { return { object: null, cacheOriginal: false }; } return { object: landscapeObject, cacheOriginal, }; } /** * Locates a map template object from the actor's active custom map (if applicable). * @param actor The actor to find the object for. * @param objectId The ID of the object to find. * @param objectPosition The position of the copied object to find the template of. */ public findCustomMapObject(actor: Actor, objectId: number, objectPosition: Position): LandscapeObject | null { const map = (actor?.metadata?.customMap as ConstructedRegion) || null; if (!map) { return null; } const objectConfig = findObject(objectId); if (!objectConfig) { return null; } const objectChunk = this.chunkManager.getChunkForWorldPosition(objectPosition); const mapChunk = activeWorld.chunkManager.getChunkForWorldPosition(map.renderPosition); const chunkIndexX = objectChunk.position.x - (mapChunk.position.x - 2); const chunkIndexY = objectChunk.position.y - (mapChunk.position.y - 2); const objectTile = map.chunks[actor.position.level][chunkIndexX][chunkIndexY]; const tileX = objectTile.templatePosition.x; const tileY = objectTile.templatePosition.y; const tileOrientation = objectTile.orientation; const objectLocalX = objectPosition.x - (objectChunk.position.x + 6) * 8; const objectLocalY = objectPosition.y - (objectChunk.position.y + 6) * 8; const mapTemplateWorldX = tileX; const mapTemplateWorldY = tileY; const mapTemplateChunk = activeWorld.chunkManager.getChunkForWorldPosition( new Position(mapTemplateWorldX, mapTemplateWorldY, objectPosition.level), ); const templateLocalX = getTemplateLocalX( tileOrientation, objectLocalX, objectLocalY, objectConfig?.rendering?.sizeX || 1, objectConfig?.rendering?.sizeY || 1, ); const templateLocalY = getTemplateLocalY( tileOrientation, objectLocalX, objectLocalY, objectConfig?.rendering?.sizeX || 1, objectConfig?.rendering?.sizeY || 1, ); const templateObjectPosition = new Position( mapTemplateWorldX + templateLocalX, mapTemplateWorldY + templateLocalY, objectPosition.level, ); const realObject = mapTemplateChunk.getFilestoreLandscapeObject(objectId, templateObjectPosition); if (!realObject) { return null; } realObject.x = objectPosition.x; realObject.y = objectPosition.y; realObject.level = objectPosition.level; let rotation = realObject.orientation + objectTile.orientation; if (rotation > 3) { rotation -= 4; } realObject.orientation = rotation; return realObject || null; } /** * Saves player data for every active player within the game world before logging * them out for a gentle game server shutdown. */ public kickAllPlayers(): void { if (!this.playerList) { return; } logger.info(`Kicking all players...`); this.playerList.filter(player => player !== null).forEach(player => player.logout()); logger.info(`Player data save complete, world is now empty.`); } /** * Saves player data for every active player within the game world. */ public saveOnlinePlayers(): void { if (!this.playerList) { return; } logger.info(`Saving player data...`); this.playerList.filter(player => player !== null).forEach(player => player.save()); logger.info(`Player data saved.`); } /** * Players a sound at a specific position for all players within range of that position. * @param position The position to play the sound at. * @param soundId The ID of the sound effect. * @param volume The volume the sound should play at. * @param distance The distance which the sound should reach. */ public playLocationSound(position: Position, instanceId: string, soundId: number, volume: number, distance: number = 10): void { this.findNearbyPlayers(position, distance, instanceId).forEach(player => { player.outgoingPackets.updateReferencePosition(position); player.outgoingPackets.playSoundAtPosition(soundId, position.x, position.y, volume); }); } /** * Finds all NPCs within the given distance from the given position that have the specified Npc ID. * @param position The center position to search from. * @param npcId The ID of the NPCs to find. * @param distance The maximum distance to search for NPCs. * @param instanceId The NPC's active instance. */ public findNearbyNpcsById( position: Position, npcId: number, distance: number, instanceId: string = activeWorld.globalInstance.instanceId, ): Npc[] { return this.npcTree .colliding({ x: position.x - distance / 2, y: position.y - distance / 2, width: distance, height: distance, }) .map(quadree => quadree.actor as Npc) .filter(npc => npc.id === npcId && npc.instanceId === instanceId); } /** * Finds all NPCs within the game world that have the specified Npc Key. * @param npcKey The Key of the NPCs to find. * @param instanceId The NPC's active instance. */ public findNpcsByKey(npcKey: string, instanceId: string = activeWorld.globalInstance.instanceId): Npc[] { return this.npcList.filter(npc => npc && npc.key === npcKey && npc.instanceId === instanceId); } /** * Finds all NPCs within the game world that have the specified Npc ID. * @param npcId The ID of the NPCs to find. * @param instanceId The NPC's active instance. */ public findNpcsById(npcId: number, instanceId: string = activeWorld.globalInstance.instanceId): Npc[] { return this.npcList.filter(npc => npc && npc.id === npcId && npc.instanceId === instanceId); } /** * Finds all NPCs within the specified instance. * @param instanceId The NPC's active instance. */ public findNpcsByInstance(instanceId: string): Npc[] { return this.npcList.filter(npc => npc && npc.instanceId === instanceId); } /** * Finds all NPCs within the given distance from the given position. * @param position The center position to search from. * @param distance The maximum distance to search for NPCs. * @param instanceId The NPC's active instance. */ public findNearbyNpcs(position: Position, distance: number, instanceId: string = activeWorld.globalInstance.instanceId): Npc[] { return this.npcTree .colliding({ x: position.x - distance / 2, y: position.y - distance / 2, width: distance, height: distance, }) .map(quadree => quadree.actor as Npc) .filter(npc => npc.instanceId === instanceId); } /** * Finds all Players within the given distance from the given position. * @param position The center position to search from. * @param distance The maximum distance to search for Players. * @param instanceId The player's active instance. */ public findNearbyPlayers(position: Position, distance: number, instanceId: string): Player[] { return this.playerTree .colliding({ x: position.x - distance / 2, y: position.y - distance / 2, width: distance, height: distance, }) .map(quadree => quadree.actor as Player) .filter(player => player.personalInstance.instanceId === instanceId || player.instance?.instanceId === instanceId); } /** * Finds a logged in player via their username. * @param username The player's username. */ public findActivePlayerByUsername(username: string): Player | null { username = username.toLowerCase(); return this.playerList.find(p => p && p.username.toLowerCase() === username) || null; } /** * Spawns the list of pre-configured items into either the global instance or a player's personal instance. * @param player [optional] The player to load the instanced items for. Uses the global world instance if not provided. */ public spawnWorldItems(player?: Player): void { const instance = player ? player.personalInstance : this.globalInstance; itemSpawns .filter(spawn => (player ? spawn.instance === 'player' : spawn.instance === 'global')) .forEach(itemSpawn => { const itemDetails = findItem(itemSpawn.itemKey); if (itemDetails && itemDetails.gameId !== undefined) { instance.spawnWorldItem({ itemId: itemDetails.gameId, amount: itemSpawn.amount }, itemSpawn.spawnPosition, { respawns: itemSpawn.respawn, owner: player || undefined, }); } else { logger.error(`Item ${itemSpawn.itemKey} can not be spawned; it has not yet been registered on the server.`); } }); } public spawnGlobalNpcs(): void { npcSpawns.forEach(npcSpawn => { const npcDetails = findNpc(npcSpawn.npcKey); this.registerNpc(new Npc(npcDetails, npcSpawn)); }); } public async spawnNpc( npcKey: string | number, position: Position, face: Direction, movementRadius: number = 0, instanceId: string = activeWorld.globalInstance.instanceId, ): Promise { if (!npcKey) { throw new Error('NPC key must be provided.'); } const npcData = findNpc(npcKey); const npc = new Npc(npcData, new NpcSpawn(npcData.key ? npcData.key : `unknown_${npcData}`, position, movementRadius, face)); // TODO (jkm) this function doesn't use the passed in `instanceId`! await this.registerNpc(npc); return npc; } public spawnScenery(): void { this.scenerySpawns.forEach(locationObject => this.globalInstance.spawnGameObject(locationObject)); } public async setupWorldTick(): Promise { await schedule(1); this.worldTick(); } public generateFakePlayers(): void { const x: number = 3222; const y: number = 3222; let xOffset: number = 0; let yOffset: number = 0; const spawnChunk = this.chunkManager.getChunkForWorldPosition(new Position(x, y, 0)); for (let i = 0; i < 1000; i++) { // TODO (Jameskmonger) we should be able to create a player without a connection, and without passing nulls in const player = new Player(null as any, null as any, null as any, i, `test${i}`, 'abs', true); this.registerPlayer(player); player.interfaceState.closeAllSlots(); xOffset++; if (xOffset > 20) { xOffset = 0; yOffset--; } player.position = new Position(x + xOffset, y + yOffset, 0); const newChunk = this.chunkManager.getChunkForWorldPosition(player.position); if (!spawnChunk.equals(newChunk)) { spawnChunk.removePlayer(player); newChunk.addPlayer(player); } player.initiateRandomMovement(); } } public async worldTick(): Promise { const hrStart = Date.now(); this.scheduler.tick(); const activePlayers: Player[] = this.playerList.filter(player => player !== null); if (activePlayers.length === 0) { return Promise.resolve().then(() => { setTimeout(async () => this.worldTick(), World.TICK_LENGTH); //TODO: subtract processing time }); } const activeNpcs: Npc[] = this.npcList.filter(npc => npc !== null); await Promise.all([...activePlayers.map(async player => player.tick()), ...activeNpcs.map(async npc => npc.tick())]); await Promise.all(activePlayers.map(async player => player.update())); await Promise.all([...activePlayers.map(async player => player.reset()), ...activeNpcs.map(async npc => npc.reset())]); const hrEnd = Date.now(); const duration = hrEnd - hrStart; const delay = Math.max(World.TICK_LENGTH - duration, 0); if (this.debugCycleDuration) { logger.info(`World tick completed in ${duration} ms, next tick in ${delay} ms.`); } setTimeout(async () => this.worldTick(), delay); this.tickComplete.next(); return Promise.resolve(); } public async nextTick(): Promise { await lastValueFrom(this.tickComplete.asObservable().pipe(take(1))); } public async ticks(count: number): Promise { await lastValueFrom(this.tickComplete.asObservable().pipe(take(count))); } public async scheduleNpcRespawn(npc: Npc): Promise { await schedule(10); return await this.registerNpc(npc); } /** * Returns the number of remaining open player slots before this world reaches maximum capacity. */ public playerSlotsRemaining(): number { return this.playerList.filter(player => !player).length; } public findPlayer(playerUsername: string): Player | null { playerUsername = playerUsername.toLowerCase(); return this.playerList?.find(p => Boolean(p) && p.username.toLowerCase() === playerUsername) || null; } public playerOnline(player: Player | string): boolean { if (typeof player === 'string') { player = player.toLowerCase(); return this.playerList.findIndex(p => Boolean(p) && p.username.toLowerCase() === player) !== -1; } else { const foundPlayer = this.playerList[player.worldIndex]; if (!foundPlayer) { return false; } return foundPlayer.equals(player); } } /** * Registers a new player to the game world. * Returns false if the world is full, otherwise returns true when the player has been registered. * @param player The player to register. */ public registerPlayer(player: Player): boolean { if (!player) { return false; } const index = this.playerList.findIndex(p => p === null); if (index === -1) { logger.warn('World full!'); return false; } player.worldIndex = index; this.playerList[index] = player; return true; } /** * Clears the given player's game world slot, signalling that they have disconnected fully. * @param player The player to remove from the world list. */ public deregisterPlayer(player: Player): void { delete this.playerList[player.worldIndex]; } public npcExists(npc: Npc): boolean { const foundNpc = this.npcList[npc.worldIndex]; if (!foundNpc || !foundNpc.exists) { return false; } return foundNpc.equals(npc); } public async registerNpc(npc: Npc): Promise { if (!npc) { return false; } const index = this.npcList.findIndex(n => n === null); if (index === -1) { logger.warn('NPC list full!'); return false; } npc.worldIndex = index; this.npcList[index] = npc; await npc.init(); return true; } public deregisterNpc(npc: Npc): void { npc.exists = false; delete this.npcList[npc.worldIndex]; } } ================================================ FILE: src/plugins/buttons/logout-button.plugin.ts ================================================ import type { buttonActionHandler } from '@engine/action/pipe/button.action'; import { widgets } from '@engine/config/config-handler'; import { activeWorld } from '@engine/world'; export const handler: buttonActionHandler = details => { const { player } = details; const playerName = player.username.toLowerCase(); player.logout(); // Update online players friends lists that have this player as a friend const otherPlayers = activeWorld.playerList.filter(p => p && p.friendsList.indexOf(playerName) !== -1); if (otherPlayers && otherPlayers.length !== 0) { otherPlayers.forEach(otherPlayer => otherPlayer.outgoingPackets.updateFriendStatus(playerName, 0)); } }; export default { pluginId: 'rs:logout_button', hooks: [ { type: 'button', widgetId: widgets.logoutTab, buttonIds: 6, handler, }, ], }; ================================================ FILE: src/plugins/buttons/magic-attack.plugin.ts ================================================ import type { TaskExecutor } from '@engine/action/hook/task'; import type { MagicOnNPCAction, MagicOnNPCActionHook } from '@engine/action/pipe/magic-on-npc.action'; import type { Player } from '@engine/world/actor/player/player'; import { logger } from '@runejs/common'; const buttonIds: number[] = [ 0, // Home Teleport ]; function attack_target(player: Player, elapsedTicks: number): boolean { logger.info('attacking?'); return true; } const spells = ['Wind Strike', 'Confuse', 'Water Strike', 'unknown?', 'Earth Strike']; export const activate = (task: TaskExecutor, elapsedTicks: number = 0) => { const { npc, player, widgetId, buttonId } = task.actionData; const attackerX = player.position.x; const attackerY = player.position.y; const victimX = npc.position.x; const victimY = npc.position.y; const offsetX = victimY - attackerY; const offsetY = victimX - attackerX; player.walkingQueue.clear(); //npc world index would be -1 for players player.outgoingPackets.sendProjectile(player.position, offsetX, offsetY, 250, 40, 36, 100, npc.worldIndex + 1, 1); console.info(`${player.username} smites ${npc.name} with ${spells[buttonId]}`); }; export default { pluginId: 'rs:magic', hooks: { type: 'magic_on_npc', widgetId: 192, buttonIds: buttonIds, task: { activate, interval: 0, }, } as MagicOnNPCActionHook, }; ================================================ FILE: src/plugins/buttons/magic-teleports.plugin.ts ================================================ import type { TaskExecutor } from '@engine/action/hook/task'; import type { ButtonAction, ButtonActionHook } from '@engine/action/pipe/button.action'; import { QueueableTask } from '@engine/action/pipe/task/queueable-task'; import { widgets } from '@engine/config/config-handler'; import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; import { Skill } from '@engine/world/actor/skills'; import { animationIds } from '@engine/world/config/animation-ids'; import { gfxIds } from '@engine/world/config/gfx-ids'; import { itemIds } from '@engine/world/config/item-ids'; import { soundIds } from '@engine/world/config/sound-ids'; import type { TravelLocation } from '@engine/world/config/travel-locations'; import type { Item } from '@engine/world/items/item'; import { Position } from '@engine/world/position'; import { openHouse } from '@plugins/skills/construction/house'; import { serverConfig } from '@server/game/game-server'; enum Teleports { Home = 591, House = 581, Varrock = 12, Lumbridge = 15, Falador = 18, Camelot = 22, Ardougne = 388, Watchtower = 389, Trollheim = 492, 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 = { [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 = { [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 = { [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[] = [ 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); player.playGraphics({ id: gfxIds.homeTeleportDraw, delay: 0, height: 0 }); player.outgoingPackets.playSound(soundIds.homeTeleportDraw, 10); } else if (elapsedTicks === 7) { player.playAnimation(animationIds.homeTeleportSit); player.playGraphics({ id: gfxIds.homeTeleportFullDrawnCircle, delay: 0, height: 0 }); player.outgoingPackets.playSound(soundIds.homeTeleportSit, 10); } else if (elapsedTicks === 12) { player.playAnimation(animationIds.homeTeleportPullOutAndReadBook); player.playGraphics({ id: gfxIds.homeTeleportPullOutBook, delay: 0, height: 0 }); player.outgoingPackets.playSound(soundIds.homeTeleportPullOutBook, 10); } else if (elapsedTicks === 16) { player.playAnimation(animationIds.homeTeleportReadBookAndGlowCircle); player.playGraphics({ id: gfxIds.homeTeleportCircleGlow, delay: 0, height: 0 }); player.outgoingPackets.playSound(soundIds.homeTeleportCircleGlowAndTeleport, 10); } else if (elapsedTicks === 20) { player.playAnimation(animationIds.homeTeleport); player.playGraphics({ id: gfxIds.homeTeleport, delay: 0, height: 0 }); } else if (elapsedTicks === 22) { queueTeleport(player, TeleportLocations[Teleports.Home]); return true; } return false; } type MagicCost = Record; /** * 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 = []; 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; } const insufficient = 'You do not have enough runes to cast this spell.'; const activate = (task: TaskExecutor, elapsedTicks: number = 0) => { const { player, buttonId } = task.actionData; let completed: boolean = false; switch (buttonId) { 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 (!serverConfig.bypassTeleportRequirements && !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) { player.metadata.castingStationarySpell = false; task.stop(); } }; export default { pluginId: 'rs:magic_teleports', hooks: [ { type: 'button', widgetId: 192, buttonIds: buttonIds, task: { activate, interval: 1, }, } as ButtonActionHook, ], }; ================================================ FILE: src/plugins/buttons/player-emotes.plugin.ts ================================================ import type { buttonActionHandler } from '@engine/action/pipe/button.action'; import { widgets } from '@engine/config/config-handler'; import type { Player } from '@engine/world/actor/player/player'; import { itemIds } from '@engine/world/config/item-ids'; interface Emote { animationId: number; name: string; unlockable?: boolean; graphicId?: number; } interface SkillcapeEmote extends Emote { itemIds: Array; } const { skillCapes } = itemIds; export const skillCapeEmotes: SkillcapeEmote[] = [ { animationId: 4959, name: 'Attack', itemIds: [skillCapes.attack.untrimmed, skillCapes.attack.trimmed], graphicId: 823 }, { animationId: 4981, name: 'Strength', itemIds: [skillCapes.strength.untrimmed, skillCapes.strength.trimmed], graphicId: 828 }, { animationId: 4961, name: 'Defence', itemIds: [skillCapes.defence.untrimmed, skillCapes.defence.trimmed], graphicId: 824 }, { animationId: 4973, name: 'Ranged', itemIds: [skillCapes.ranged.untrimmed, skillCapes.ranged.trimmed], graphicId: 832 }, { animationId: 4979, name: 'Prayer', itemIds: [skillCapes.prayer.untrimmed, skillCapes.prayer.trimmed], graphicId: 829 }, { animationId: 4939, name: 'Magic', itemIds: [skillCapes.magic.untrimmed, skillCapes.magic.trimmed], graphicId: 813 }, { animationId: 4947, name: 'Runecrafting', itemIds: [skillCapes.runecrafting.untrimmed, skillCapes.runecrafting.trimmed], graphicId: 817, }, { animationId: 4971, name: 'Constitution', itemIds: [skillCapes.constitution.untrimmed, skillCapes.constitution.trimmed], graphicId: 833, }, { animationId: 4977, name: 'Agility', itemIds: [skillCapes.agility.untrimmed, skillCapes.agility.trimmed], graphicId: 830 }, { animationId: 4969, name: 'Herblore', itemIds: [skillCapes.herblore.untrimmed, skillCapes.herblore.trimmed], graphicId: 835 }, { animationId: 4965, name: 'Thieving', itemIds: [skillCapes.thieving.untrimmed, skillCapes.thieving.trimmed], graphicId: 826 }, { animationId: 4949, name: 'Crafting', itemIds: [skillCapes.crafting.untrimmed, skillCapes.crafting.trimmed], graphicId: 818 }, { animationId: 4937, name: 'Fletching', itemIds: [skillCapes.fletching.untrimmed, skillCapes.fletching.trimmed], graphicId: 812 }, { animationId: 4967, name: 'Slayer', itemIds: [skillCapes.slayer.untrimmed, skillCapes.slayer.trimmed], graphicId: 827 }, { animationId: 4953, name: 'Construction', itemIds: [skillCapes.construction.untrimmed, skillCapes.construction.trimmed], graphicId: 820, }, { animationId: 4941, name: 'Mining', itemIds: [skillCapes.mining.untrimmed, skillCapes.mining.trimmed], graphicId: 814 }, { animationId: 4943, name: 'Smithing', itemIds: [skillCapes.smithing.untrimmed, skillCapes.smithing.trimmed], graphicId: 815 }, { animationId: 4951, name: 'Fishing', itemIds: [skillCapes.fishing.untrimmed, skillCapes.fishing.trimmed], graphicId: 819 }, { animationId: 4955, name: 'Cooking', itemIds: [skillCapes.cooking.untrimmed, skillCapes.cooking.trimmed], graphicId: 821 }, { animationId: 4975, name: 'Firemaking', itemIds: [skillCapes.firemaking.untrimmed, skillCapes.firemaking.trimmed], graphicId: 831 }, { animationId: 4957, name: 'Woodcutting', itemIds: [skillCapes.woodcutting.untrimmed, skillCapes.woodcutting.trimmed], graphicId: 822 }, { animationId: 4963, name: 'Farming', itemIds: [skillCapes.farming.untrimmed, skillCapes.farming.trimmed], graphicId: 825 }, { animationId: 4945, name: 'Quest point', itemIds: [skillCapes.questpoint.untrimmed], graphicId: 816 }, ]; export const emotes: { [key: number]: Emote } = { 1: { animationId: 855, name: 'YES' }, 2: { animationId: 856, name: 'NO' }, 3: { animationId: 858, name: 'BOW' }, 4: { animationId: 859, name: 'ANGRY' }, 5: { animationId: 857, name: 'THINKING' }, 6: { animationId: 863, name: 'WAVE' }, 7: { animationId: 2113, name: 'SHRUG' }, 8: { animationId: 862, name: 'CHEER' }, 9: { animationId: 864, name: 'BECKON' }, 10: { animationId: 861, name: 'LAUGH' }, 11: { animationId: 2109, name: 'JUMP FOR JOY' }, 12: { animationId: 2111, name: 'YAWN' }, 13: { animationId: 866, name: 'DANCE' }, 14: { animationId: 2106, name: 'JIG' }, 15: { animationId: 2107, name: 'SPIN' }, 16: { animationId: 2108, name: 'HEADBANG' }, 17: { animationId: 860, name: 'CRY' }, 18: { animationId: 1368, name: 'BLOW KISS' }, 19: { animationId: 2105, name: 'PANIC' }, 20: { animationId: 2110, name: 'RASPBERRY' }, 21: { animationId: 865, name: 'CLAP' }, 22: { animationId: 2112, name: 'SALUTE' }, 23: { animationId: 2127, name: 'GOBLIN BOW', unlockable: true }, 24: { animationId: 2128, name: 'GOBLIN SALUTE', unlockable: true }, 25: { animationId: 1131, name: 'GLASS BOX', unlockable: true }, 26: { animationId: 1130, name: 'CLIMB ROPE', unlockable: true }, 27: { animationId: 1129, name: 'LEAN', unlockable: true }, 28: { animationId: 1128, name: 'GLASS WALL', unlockable: true }, 32: { animationId: 4276, name: 'IDEA', unlockable: true, graphicId: 712 }, 30: { animationId: 4278, name: 'STAMP', unlockable: true }, 31: { animationId: 4280, name: 'FLAP', unlockable: true }, 29: { animationId: 4275, name: 'FACEPALM', unlockable: true }, 33: { animationId: 3544, name: 'ZOMBIE WALK', unlockable: true }, 34: { animationId: 3543, name: 'ZOMBIE DANCE', unlockable: true }, 35: { animationId: 2836, name: 'SCARED', unlockable: true }, 36: { animationId: 6111, name: 'RABBIT HOP', unlockable: true }, // @TODO missing in 435 cache??? 37: { animationId: -1, name: 'SKILLCAPE' }, // @TODO skillcape emotes }; export function unlockEmote(player: Player, emoteName: string): void { const unlockedEmotes: string[] = player.savedMetadata.unlockedEmotes || []; if (unlockedEmotes.indexOf(emoteName) === -1) { unlockedEmotes.push(emoteName); player.savedMetadata.unlockedEmotes = unlockedEmotes; } unlockEmotes(player); } export function lockEmote(player: Player, emoteName: string): void { const unlockedEmotes: string[] = player.savedMetadata.unlockedEmotes || []; const index = unlockedEmotes.indexOf(emoteName); if (index !== -1) { unlockedEmotes.splice(index, 1); player.savedMetadata.unlockedEmotes = unlockedEmotes; } unlockEmotes(player); } export function unlockEmotes(player: Player): void { let sosConfig = 0; let eventConfig = 0; let goblinConfig = 0; const unlockedEmotes: string[] = player.savedMetadata.unlockedEmotes || []; for (const name of unlockedEmotes) { if ((name === 'GOBLIN BOW' || name === 'GOBLIN SALUTE') && goblinConfig === 0) goblinConfig += 7; if (name === 'FLAP') sosConfig += 1; if (name === 'FACEPALM') sosConfig += 2; if (name === 'IDEA') sosConfig += 4; if (name === 'STAMP') sosConfig += 8; if (name === 'GLASS WALL') eventConfig += 1; if (name === 'GLASS BOX') eventConfig += 2; if (name === 'CLIMB ROPE') eventConfig += 4; if (name === 'LEAN') eventConfig += 8; if (name === 'SCARED') eventConfig += 16; if (name === 'ZOMBIE DANCE') eventConfig += 32; if (name === 'ZOMBIE WALK') eventConfig += 64; if (name === 'RABBIT HOP') eventConfig += 128; if (name === 'SKILLCAPE') eventConfig += 256; } player.outgoingPackets.updateClientConfig(465, goblinConfig); player.outgoingPackets.updateClientConfig(802, sosConfig); player.outgoingPackets.updateClientConfig(313, eventConfig); } const buttonIds = Object.keys(emotes).map(v => parseInt(v)); export const handler: buttonActionHandler = details => { const { player, buttonId } = details; const emote = emotes[buttonId]; if (emote.name === 'SKILLCAPE') { const equippedBackItem = player.getEquippedItem('back'); if (equippedBackItem) { if (skillCapeEmotes.some(item => item.itemIds.includes(equippedBackItem.itemId))) { const skillcapeEmote = skillCapeEmotes.filter(item => item.itemIds.includes(equippedBackItem.itemId)); player.playAnimation(skillcapeEmote[0].animationId); if (skillcapeEmote[0].graphicId) { player.playGraphics({ id: skillcapeEmote[0].graphicId, delay: 0, height: 0 }); } } } else { player.sendMessage(`You need to be wearing a skillcape in order to perform that emote.`, true); } } else { if (emote.unlockable) { const unlockedEmotes: string[] = player.savedMetadata.unlockedEmotes || []; if (unlockedEmotes.indexOf(emote.name) === -1) { player.sendMessage(`You have not unlocked this emote.`, true); return; } } player.playAnimation(emote.animationId); if (emote.graphicId !== undefined) { player.playGraphics({ id: emote.graphicId, height: 0 }); } } }; export default { pluginId: 'rs:player_emotes', hooks: [{ type: 'button', widgetId: widgets.emotesTab, buttonIds, handler }], }; ================================================ FILE: src/plugins/buttons/player-setting-button.plugin.ts ================================================ import type { buttonActionHandler } from '@engine/action/pipe/button.action'; import { widgets } from '@engine/config/config-handler'; const buttonIds: number[] = [ 0, // walk/run 11, 12, 13, 14, 15, // music volume 16, 17, 18, 19, 20, // sound effect volume 29, 30, 31, 32, 33, // area effect volume 2, // split private chat 3, // mouse buttons 7, 8, 9, 10, // screen brightness 1, // chat effects 4, // accept aid 5, // house options ]; export const handler: buttonActionHandler = details => { const { player, buttonId } = details; player.settingChanged(buttonId); }; export default { pluginId: 'rs:player_setting_button', hooks: [{ type: 'button', widgetId: widgets.settingsTab, buttonIds: buttonIds, handler }], }; ================================================ FILE: src/plugins/combat/combat-styles.plugin.ts ================================================ import type { buttonActionHandler } from '@engine/action/pipe/button.action'; import type { EquipmentChangeAction, equipmentChangeActionHandler } from '@engine/action/pipe/equipment-change.action'; import type { playerInitActionHandler } from '@engine/action/pipe/player-init.action'; import { findItem, widgets } from '@engine/config/config-handler'; import type { ItemDetails, WeaponStyle } from '@engine/config/item-config'; import { weaponWidgetIds } from '@engine/config/item-config'; import { combatStyles } from '@engine/world/actor/combat'; import type { Player } from '@engine/world/actor/player/player'; import { SidebarTab } from '@engine/world/actor/player/player'; import { widgetScripts } from '@engine/world/config/widget'; import { serverConfig } from '@server/game/game-server'; export function updateCombatStyle(player: Player, weaponStyle: WeaponStyle, styleIndex: number): void { player.savedMetadata.combatStyle = [weaponStyle, styleIndex]; player.settings.attackStyle = styleIndex; const buttonId = combatStyles[weaponStyle]?.[styleIndex]?.button_id; if (buttonId !== undefined) { player.outgoingPackets.updateClientConfig(widgetScripts.attackStyle, buttonId); } } export function showUnarmed(player: Player): void { player.modifyWidget(widgets.defaultCombatStyle, { childId: 0, text: 'Unarmed' }); player.setSidebarWidget(SidebarTab.COMBAT, widgets.defaultCombatStyle); let style = 0; if (player.savedMetadata.combatStyle) { style = player.savedMetadata.combatStyle[1] || null; if (style && style > 2) { style = 2; } } updateCombatStyle(player, 'unarmed', style); } export function setWeaponWidget(player: Player, weaponStyle: WeaponStyle, itemDetails: ItemDetails | null): void { player.modifyWidget(weaponWidgetIds[weaponStyle], { childId: 0, text: itemDetails?.name || 'Unknown' }); player.setSidebarWidget(SidebarTab.COMBAT, weaponWidgetIds[weaponStyle]); if (player.savedMetadata.combatStyle) { updateCombatStyle(player, weaponStyle, player.savedMetadata.combatStyle[1] || 0); } } export function updateCombatStyleWidget(player: Player): void { const equippedItem = player.getEquippedItem('main_hand'); if (equippedItem) { const itemDetails = findItem(equippedItem.itemId); const weaponStyle = itemDetails?.equipmentData?.weaponInfo?.style || null; if (weaponStyle) { setWeaponWidget(player, weaponStyle, itemDetails); } else { showUnarmed(player); } } else { showUnarmed(player); } } const equip: equipmentChangeActionHandler = ({ player, itemDetails, equipmentSlot }) => { if (equipmentSlot === 'main_hand') { const weaponStyle = itemDetails?.equipmentData?.weaponInfo?.style || null; if (!weaponStyle) { showUnarmed(player); return; } setWeaponWidget(player, weaponStyle, itemDetails); } }; const initAction: playerInitActionHandler = ({ player }) => { if (!serverConfig.tutorialEnabled || player.savedMetadata.tutorialComplete) { updateCombatStyleWidget(player); } }; const combatStyleSelection: buttonActionHandler = ({ player, buttonId }) => { const equippedItem = player.getEquippedItem('main_hand'); let weaponStyle: string | null = 'unarmed'; if (equippedItem) { weaponStyle = findItem(equippedItem.itemId)?.equipmentData?.weaponInfo?.style || null; if (!weaponStyle || !combatStyles[weaponStyle]) { weaponStyle = 'unarmed'; } } const combatStyle = combatStyles[weaponStyle].findIndex(combatStyle => combatStyle.button_id === buttonId); if (combatStyle !== -1) { player.savedMetadata.combatStyle = [weaponStyle, combatStyle]; } }; export default { pluginId: 'rs:combat_styles', hooks: [ { type: 'equipment_change', eventType: 'equip', handler: equip, }, { type: 'equipment_change', eventType: 'unequip', handler: (details: EquipmentChangeAction): void => { if (details.equipmentSlot === 'main_hand') { showUnarmed(details.player); } }, }, { type: 'player_init', handler: initAction, }, { type: 'button', widgetIds: Object.values(weaponWidgetIds), handler: combatStyleSelection, }, ], }; ================================================ FILE: src/plugins/commands/bank-command.plugin.ts ================================================ import { getActionHooks } from '@engine/action/hook/action-hook'; import { advancedNumberHookFilter } from '@engine/action/hook/hook-filters'; import type { ObjectInteractionActionHook } from '@engine/action/pipe/object-interaction.action'; import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; import { objectIds } from '@engine/world/config/object-ids'; const action: commandActionHandler = details => { const interactionActions = getActionHooks('object_interaction').filter(plugin => advancedNumberHookFilter(plugin.objectIds, objectIds.bankBooth[0], plugin.options, 'use-quickly'), ); interactionActions.forEach(plugin => { if (!plugin.handler) { return; } plugin.handler({ player: details.player, object: { objectId: objectIds.bankBooth[0], level: details.player.position.level, x: details.player.position.x, y: details.player.position.y, orientation: 0, type: 0, }, option: 'use-quickly', position: details.player.position, objectConfig: undefined as any, cacheOriginal: undefined as any, }); }); }; export default { pluginId: 'rs:bank_command', hooks: [ { type: 'player_command', commands: ['bank'], handler: action, }, ], }; ================================================ FILE: src/plugins/commands/camera-commands.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; import { Position } from '@engine/world/position'; const moveCameraAction: commandActionHandler = ({ player, args }) => { const { x, y, height, speed, acceleration } = args; player.outgoingPackets.snapCameraTo( new Position(x as number, y as number, player.position.level), height as number, speed as number, acceleration as number, ); }; const turnCameraAction: commandActionHandler = ({ player, args }) => { const { x, y, height, speed, acceleration } = args; player.outgoingPackets.turnCameraTowards( new Position(x as number, y as number, player.position.level), height as number, speed as number, acceleration as number, ); }; const lookCameraAction: commandActionHandler = ({ player, args }) => { const { cameraX, cameraY, cameraHeight, lookX, lookY, lookHeight, speed, acceleration } = args; player.outgoingPackets.snapCameraTo( new Position(cameraX as number, cameraY as number, player.position.level), cameraHeight as number, speed as number, acceleration as number, ); player.outgoingPackets.turnCameraTowards( new Position(lookX as number, lookY as number, player.position.level), lookHeight as number, speed as number, acceleration as number, ); }; const lookTestAction: commandActionHandler = ({ player }) => { const cameraX = 3219; const cameraY = 3238; const cameraHeight = 500; const lookX = 3219; const lookY = 3250; const lookHeight = 500; const speed = 3; const acceleration = 50; player.outgoingPackets.snapCameraTo(new Position(cameraX, cameraY), cameraHeight, speed, acceleration); player.outgoingPackets.turnCameraTowards(new Position(lookX, lookY), lookHeight, speed, acceleration); }; export default { pluginId: 'rs:camera_commands', hooks: [ { type: 'player_command', commands: ['looktest', 'lt'], handler: lookTestAction, }, { type: 'player_command', commands: ['cameralook'], args: [ { name: 'cameraX', type: 'number', }, { name: 'cameraY', type: 'number', }, { name: 'cameraHeight', type: 'number', }, { name: 'lookX', type: 'number', }, { name: 'lookY', type: 'number', }, { name: 'lookHeight', type: 'number', }, { name: 'speed', type: 'number', defaultValue: 0, }, { name: 'acceleration', type: 'number', defaultValue: 100, }, ], handler: lookCameraAction, }, { type: 'player_command', commands: ['mcam', 'movecamera', 'move_camera', 'setcam', 'setcamera', 'set_camera'], args: [ { name: 'x', type: 'number', }, { name: 'y', type: 'number', }, { name: 'height', type: 'number', }, { name: 'speed', type: 'number', defaultValue: 0, }, { name: 'acceleration', type: 'number', defaultValue: 100, }, ], handler: moveCameraAction, }, { type: 'player_command', commands: ['tcam', 'turncamera', 'turn_camera'], args: [ { name: 'x', type: 'number', }, { name: 'y', type: 'number', }, { name: 'height', type: 'number', }, { name: 'speed', type: 'number', defaultValue: 0, }, { name: 'acceleration', type: 'number', defaultValue: 100, }, ], handler: turnCameraAction, }, ], }; ================================================ FILE: src/plugins/commands/clear-inventory-command.plugin.ts ================================================ import type { PlayerCommandAction } from '@engine/action/pipe/player-command.action'; export default { pluginId: 'rs:clear_inventory_command', hooks: [ { type: 'player_command', commands: ['clear'], handler: (details: PlayerCommandAction): void => details.player.inventory.clear(), }, ], }; ================================================ FILE: src/plugins/commands/client-config-command.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; const action: commandActionHandler = details => { const { player, args } = details; const configId = args.configId as number; const configValue = args.configValue as number; player.outgoingPackets.updateClientConfig(configId, configValue); }; export default { pluginId: 'rs:client_config_command', hooks: [ { type: 'player_command', commands: ['config', 'conf'], args: [ { name: 'configId', type: 'number', }, { name: 'configValue', type: 'number', }, ], handler: action, }, ], }; ================================================ FILE: src/plugins/commands/current-position-command.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; const action: commandActionHandler = details => { const { player } = details; player.sendLogMessage(`@[ ${player.position.x}, ${player.position.y}, ${player.position.level} ]`, details.isConsole); }; export default { pluginId: 'rs:current_position_command', hooks: [ { type: 'player_command', commands: ['pos', 'loc', 'position', 'location', 'coords', 'coordinates', 'mypos', 'myloc'], handler: action, }, ], }; ================================================ FILE: src/plugins/commands/data-dump-command.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; import type { DataDumpResult } from '@engine/config/data-dump'; import { dumpItems, dumpNpcs, dumpObjects, dumpWidgets } from '@engine/config/data-dump'; const action: commandActionHandler = ({ player, args, isConsole }) => { const dataType = args.dataType as string; const functionMap: { [key: string]: () => DataDumpResult } = { npcs: dumpNpcs, items: dumpItems, objects: dumpObjects, widgets: dumpWidgets, }; const types = Object.keys(functionMap); if (types.indexOf(dataType) === -1) { player.sendLogMessage(`Invalid data type, please use one of the following:`, isConsole); player.sendLogMessage(`[ ${types.join(', ')} ]`, isConsole); return; } let dataName = dataType; if (dataType.endsWith('s')) { dataName = dataType.substring(0, dataType.length - 2); } player.sendLogMessage(`Dumping ${dataName} data...`, isConsole); const result = functionMap[dataType](); if (result.successful) { player.sendLogMessage(`Saved ${dataName} data to ${result.filePath}.`, isConsole); } else { player.sendLogMessage(`Error dumping ${dataName} data.`, isConsole); } }; export default { pluginId: 'rs:data_dump_command', hooks: [ { type: 'player_command', commands: ['dump', 'data', 'datadump', 'dd'], args: [ { name: 'dataType', type: 'string', }, ], handler: action, }, ], }; ================================================ FILE: src/plugins/commands/dump-metadata-command.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; const action: commandActionHandler = details => { const { player } = details; const metadata = { ...player.metadata }; for (const metadataKey of Object.keys(metadata)) { if (typeof (metadata as any)[metadataKey] === 'function') { (metadata as any)[metadataKey] = typeof (metadata as any)[metadataKey]; } if (Array.isArray((metadata as any)[metadataKey]) && (metadata as any)[metadataKey].length > 30) { (metadata as any)[metadataKey] = `Array (${(metadata as any)[metadataKey].length} entries)`; } if ( typeof (metadata as any)[metadataKey] === 'object' && (metadata as any)[metadataKey] !== null && 'unsubscribe' in (metadata as any)[metadataKey] ) { (metadata as any)[metadataKey] = `Observable { closed: ${(metadata as any)[metadataKey].closed} }`; } } console.log(metadata); const stringified = JSON.stringify(metadata, null, 4); stringified.split('\n').forEach(split => { player.sendLogMessage(split, details.isConsole); console.log(split); }); }; export default { pluginId: 'rs:dump_metadata_command', hooks: [ { type: 'player_command', commands: ['dump_metadata'], handler: action, }, ], }; ================================================ FILE: src/plugins/commands/give-item-command.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; import { findItem } from '@engine/config/config-handler'; import { itemIds } from '@engine/world/config/item-ids'; const action: commandActionHandler = details => { const { player, args } = details; const inventorySlot = player.inventory.getFirstOpenSlot(); if (inventorySlot === -1) { player.sendLogMessage(`You don't have enough free space to do that.`, details.isConsole); return; } const itemSearch: string = args.itemSearch as string; let itemId: number | null = null; if (itemSearch.match(/^[0-9]+$/)) { itemId = parseInt(itemSearch, 10); } else { if (itemSearch.indexOf(':') !== -1) { itemId = findItem(itemSearch)?.gameId || null; } else { // @TODO nested item ids itemId = itemIds[itemSearch]; } } if (!itemId || isNaN(itemId)) { throw new Error(`Item name not found.`); } let amount: number = args.amount as number; if (amount > 2000000000) { throw new Error(`Unable to give more than 2,000,000,000.`); } const itemDefinition = findItem(itemId); if (!itemDefinition) { throw new Error(`Item ID ${itemId} not found!`); } let actualAmount = 0; if (itemDefinition.stackable) { const item = { itemId, amount }; player.giveItem(item); actualAmount = amount; } else { if (amount > 28) { amount = 28; } for (let i = 0; i < amount; i++) { if (player.giveItem({ itemId, amount: 1 })) { actualAmount++; } else { break; } } } player.sendLogMessage(`Added ${actualAmount}x ${itemDefinition.name} to inventory.`, details.isConsole); }; export default { pluginId: 'rs:give_item_command', hooks: [ { type: 'player_command', commands: ['give', 'item', 'spawn'], args: [ { name: 'itemSearch', type: 'string', }, { name: 'amount', type: 'number', defaultValue: 1, }, ], handler: action, }, ], }; ================================================ FILE: src/plugins/commands/groups-debug.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; import { findItemTagsInGroupFilter, findItemTagsInGroups } from '@engine/config/config-handler'; const selectGroups: commandActionHandler = ({ player, args, isConsole }) => { const groups: string | number = args.groupkeys; if (!groups || typeof groups !== 'string') { player.sendLogMessage('invalid input', isConsole); return; } player.sendLogMessage('results:', isConsole); findItemTagsInGroups(groups.split(',')).forEach(itemName => { player.sendLogMessage(itemName, isConsole); }); return; }; const filterGroups: commandActionHandler = ({ player, args, isConsole }) => { const groups: string | number = args.groupkeys; if (!groups || typeof groups !== 'string') { player.sendLogMessage('invalid input', isConsole); return; } player.sendLogMessage('results:', isConsole); findItemTagsInGroupFilter(groups.split(',')).forEach(itemName => { player.sendLogMessage(itemName, isConsole); }); return; }; export default { pluginId: 'promises:groups-debug', hooks: [ { type: 'player_command', commands: ['selectgroups'], args: [ { name: 'groupkeys', type: 'string', }, ], handler: selectGroups, }, { type: 'player_command', commands: ['filtergroups'], args: [ { name: 'groupkeys', type: 'string', }, ], handler: filterGroups, }, ], }; ================================================ FILE: src/plugins/commands/pathing-commands.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; import { Position } from '@engine/world/position'; const action: commandActionHandler = details => { const { player, args } = details; const x: number = args.x as number; const y: number = args.y as number; const pathingDiameter: number = args.diameter as number; player.pathfinding.walkTo(new Position(x, y, player.position.level), { pathingSearchRadius: pathingDiameter }); }; export default { pluginId: 'rs:pathing_commands', hooks: [ { type: 'player_command', commands: ['path'], args: [ { name: 'x', type: 'number', }, { name: 'y', type: 'number', }, { name: 'diameter', type: 'number', defaultValue: 64, }, ], handler: action, }, ], }; ================================================ FILE: src/plugins/commands/player-animation-command.plugin.ts ================================================ import type { PlayerCommandActionHook, commandActionHandler } from '@engine/action/pipe/player-command.action'; const action: commandActionHandler = (details): void => { const { player, args } = details; const animationId: number = args.animationId as number; player.playAnimation(animationId); }; export default { pluginId: 'rs:player_animation_command', hooks: [ { type: 'player_command', commands: ['anim', 'animation', 'playanim'], args: [ { name: 'animationId', type: 'number', }, ], handler: action, } as PlayerCommandActionHook, ], }; ================================================ FILE: src/plugins/commands/player-graphics-command.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; const action: commandActionHandler = details => { const { player, args } = details; const graphicsId: number = args.graphicsId as number; const height: number = args.height as number; player.playGraphics({ id: graphicsId, delay: 0, height: height }); }; export default { pluginId: 'rs:player_graphics_command', hooks: [ { type: 'player_command', commands: ['gfx', 'graphics'], args: [ { name: 'graphicsId', type: 'number', }, { name: 'height', type: 'number', defaultValue: 120, }, ], handler: action, }, ], }; ================================================ FILE: src/plugins/commands/quest-list-command.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; import { questMap } from '@engine/plugins/loader'; const action: commandActionHandler = details => { for (const quest of Object.values(questMap)) { details.player.sendLogMessage(quest.id, details.isConsole); } }; export default { pluginId: 'promises:quest-list-command', hooks: [ { type: 'player_command', commands: ['quest-list', 'quests'], handler: action, }, ], }; ================================================ FILE: src/plugins/commands/region-debug-commands.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; import { Position } from '@engine/world/position'; import { logger } from '@runejs/common'; const debugMapRegion = ( player: Player, mapRegionX: number, mapRegionY: number, worldX: number, worldY: number, level: number = -1, ): void => { const key = `${mapRegionX},${mapRegionY}`; player.sendMessage(`Region ${key} - ${activeWorld.chunkManager.getRegionIdForWorldPosition(player.position)}`); if (!activeWorld.chunkManager.regionMap.has(key)) { player.sendMessage(`Map region not loaded.`); return; } if (level === -1) { level = player.position.level; } const region = activeWorld.chunkManager.regionMap.get(key); if (!region) { player.sendMessage(`Map region not loaded.`); logger.error(`Map region not loaded. ${key}`); return; } let debug: string = `\nRegion ${key},${level}\n\n`; for (let y = 63; y >= 0; y--) { const line = new Array(64).fill('?'); for (let x = 0; x < 64; x++) { const tileWorldX = worldX + x; const tileWorldY = worldY + y; if (tileWorldX === player.position.x && tileWorldY === player.position.y) { line[x] = '@'; } else if (region.mapFile?.tileSettings) { const tileSettings = activeWorld.chunkManager.getTile(new Position(tileWorldX, tileWorldY, level)).settings; if (!tileSettings) { line[x] = '.'; } else if (tileSettings > 9) { line[x] = 'x'; } else { line[x] = tileSettings + ''; } } } debug += `|${line.join('')}|\n`; } logger.info(debug); }; const regionDebugHandler: commandActionHandler = ({ player, args }) => { const chunkX = player.position.chunkX + 6; const chunkY = player.position.chunkY + 6; const mapRegionX = Math.floor(chunkX / 8); const mapRegionY = Math.floor(chunkY / 8); const worldX = (mapRegionX & 0xff) * 64; const worldY = mapRegionY * 64; debugMapRegion(player, mapRegionX, mapRegionY, worldX, worldY, (args?.level as number) || -1); }; const tileDebugHandler: commandActionHandler = ({ player }) => { const tile = activeWorld.chunkManager.getTile(player.position); const tile0 = activeWorld.chunkManager.getTile(player.position.copy().setLevel(0)); const tile1 = activeWorld.chunkManager.getTile(player.position.copy().setLevel(1)); const tile2 = activeWorld.chunkManager.getTile(player.position.copy().setLevel(2)); const tile3 = activeWorld.chunkManager.getTile(player.position.copy().setLevel(3)); const chunkX = player.position.chunkX + 6; const chunkY = player.position.chunkY + 6; const mapRegionX = Math.floor(chunkX / 8); const mapRegionY = Math.floor(chunkY / 8); const worldX = (mapRegionX & 0xff) * 64; const worldY = mapRegionY * 64; player.sendMessage( [ `Tile ${player.position.key} settings: ${tile.settings}`, `Local Pos: ${player.position.x - worldX},${player.position.y - worldY}`, `Tile@0=(${tile0.settings}), Tile@1=(${tile1.settings}), Tile@2=(${tile2.settings}), Tile@3=(${tile3.settings})`, ], { console: true }, ); }; export default { pluginId: 'rs:region_debug_commands', hooks: [ { type: 'player_command', commands: ['regioninfo', 'region', 'myregion', 'regiondebug', 'region_info', 'my_region', 'region_debug'], args: [ { name: 'level', type: 'number', defaultValue: -1, }, ], handler: regionDebugHandler, }, { type: 'player_command', commands: ['tileinfo', 'tile', 'mytile', 'tiledebug', 'tile_info', 'my_tile', 'tile_debug'], handler: tileDebugHandler, }, ], }; ================================================ FILE: src/plugins/commands/reset-camera-command.plugin.ts ================================================ import type { PlayerCommandAction } from '@engine/action/pipe/player-command.action'; export default { pluginId: 'rs:reset_camera_command', hooks: [ { type: 'player_command', commands: ['reset_camera', 'resetcamera'], handler: ({ player }: PlayerCommandAction): void => player.outgoingPackets.resetCamera(), }, ], }; ================================================ FILE: src/plugins/commands/sound-song-commands.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; const songAction: commandActionHandler = details => { const { player, args } = details; player.outgoingPackets.playSong(args.songId as number); }; const soundAction: commandActionHandler = details => { const { player, args } = details; player.playSound(args.soundId as number, args.volume as number); }; const quickSongAction: commandActionHandler = details => { const { player, args } = details; player.outgoingPackets.playQuickSong(args.songId as number, args.prevSongId as number); }; export default { pluginId: 'rs:sound_commands', hooks: [ { type: 'player_command', commands: 'song', args: [ { name: 'songId', type: 'number', }, ], handler: songAction, }, { type: 'player_command', commands: ['sound', 'so'], args: [ { name: 'soundId', type: 'number', }, { name: 'volume', type: 'number', defaultValue: 10, }, ], handler: soundAction, }, { type: 'player_command', commands: 'quicksong', args: [ { name: 'songId', type: 'number', }, { name: 'prevSongId', type: 'number', }, ], handler: quickSongAction, }, ], }; ================================================ FILE: src/plugins/commands/spawn-npc-command.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; import { findNpc } from '@engine/config/config-handler'; import type { NpcDetails } from '@engine/config/npc-config'; import { NpcSpawn } from '@engine/config/npc-spawn-config'; import { activeWorld } from '@engine/world'; import { Npc } from '@engine/world/actor/npc'; const action: commandActionHandler = ({ player, args }) => { let npcKey: string | number = args.npcKey; let npcDetails: NpcDetails | null = null; if (typeof npcKey === 'string' && npcKey.match(/^[0-9]+$/)) { npcKey = parseInt(npcKey, 10); } if (typeof npcKey === 'string') { npcDetails = findNpc(npcKey); npcKey = npcDetails.gameId; } const movementRadius: number = args.movementRadius as number; const npc = new Npc( npcDetails ? npcDetails : npcKey, new NpcSpawn(npcDetails?.key ? npcDetails.key : `unknown-${npcKey}`, player.position.clone(), movementRadius, 'WEST'), player.instance, ); activeWorld.registerNpc(npc); }; export default { pluginId: 'rs:spawn_npc_command', hooks: [ { type: 'player_command', commands: ['npc', 'spawnnpc', 'spawn_npc'], args: [ { name: 'npcKey', type: 'either', }, { name: 'movementRadius', type: 'number', defaultValue: 0, }, ], handler: action, }, ], }; ================================================ FILE: src/plugins/commands/spawn-scenery-command.plugin.ts ================================================ import { writeFileSync } from 'fs'; import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; import { objectIds } from '@engine/world/config/object-ids'; import { logger } from '@runejs/common'; import type { LandscapeObject } from '@runejs/filestore'; import { dump } from 'js-yaml'; const spawnSceneryAction: commandActionHandler = ({ player, args }) => { const locationObjectSearch: string = (args.locationObjectSearch as string).trim(); let locationObjectId: number; if (locationObjectSearch.match(/^[0-9]+$/)) { locationObjectId = parseInt(locationObjectSearch, 10); } else { // @TODO nested object ids locationObjectId = objectIds[locationObjectSearch]; } if (isNaN(locationObjectId)) { throw new Error(`Location object name not found.`); } const objectType = args.objectType as number; const objectOrientation = args.objectOrientation as number; const position = player.position.copy(); const locationObject: LandscapeObject = { objectId: locationObjectId, x: position.x, y: position.y, level: position.level, type: objectType, orientation: objectOrientation, }; player.metadata.lastSpawnedScenery = locationObject; if (!player.metadata.spawnedScenery) { player.metadata.spawnedScenery = []; } player.metadata.spawnedScenery.push(locationObject); player.instance.spawnGameObject(locationObject); }; const undoSceneryAction: commandActionHandler = details => { const { player } = details; const o = player.metadata.lastSpawnedScenery; if (!o) { return; } player.instance.despawnGameObject(o); delete player.metadata.lastSpawnedScenery; if (player.metadata.spawnedScenery) { player.metadata.spawnedScenery.pop(); } }; const dumpSceneryAction: commandActionHandler = details => { const { player } = details; const path = `data/dump/scene-${new Date().getTime()}.yml`; writeFileSync(path, dump(player.metadata.spawnedScenery)); logger.info(path); player.metadata.spawnedScenery = []; }; export default { pluginId: 'rs:spawn_scenery_command', hooks: [ { type: 'player_command', commands: ['scene', 'sc'], args: [ { name: 'locationObjectSearch', type: 'string', }, { name: 'objectOrientation', type: 'number', defaultValue: 0, }, { name: 'objectType', type: 'number', defaultValue: 10, }, ], handler: spawnSceneryAction, }, { type: 'player_command', commands: ['undoscene', 'undosc'], handler: undoSceneryAction, }, { type: 'player_command', commands: ['dumpscene', 'dumpsc'], handler: dumpSceneryAction, }, ], }; ================================================ FILE: src/plugins/commands/spawn-test-players-command.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; import { activeWorld } from '@engine/world'; import { Player } from '@engine/world/actor/player/player'; import { Position } from '@engine/world/position'; import { World } from '@engine/world/world'; const handler: commandActionHandler = ({ player, args }) => { const playerCount = args.playerCount as number; if (playerCount > World.MAX_PLAYERS - 1) { player.sendMessage(`Error: Max player count is ${World.MAX_PLAYERS - 1}.`); return; } const x: number = player.position.x; const y: number = player.position.y; let xOffset: number = 0; let yOffset: number = 0; const spawnChunk = activeWorld.chunkManager.getChunkForWorldPosition(new Position(x, y, 0)); const worldSlotsRemaining = activeWorld.playerSlotsRemaining() - 1; if (worldSlotsRemaining <= 0) { player.sendMessage(`Error: The game world is full.`); return; } const playerSpawnCount = playerCount > worldSlotsRemaining ? worldSlotsRemaining : playerCount; if (playerSpawnCount < playerCount) { player.sendMessage(`Warning: There was only room for ${playerSpawnCount}/${playerCount} player spawns.`); } // TODO (JameskmongeR) what's the difference between this and `generateFakePlayers` for (let i = 0; i < playerSpawnCount; i++) { // TODO (Jameskmonger) we should be able to create a player without a connection, and without passing nulls in const testPlayer = new Player(null as any, null as any, null as any, i, `test${i}`, 'abs', true); activeWorld.registerPlayer(testPlayer); testPlayer.interfaceState.closeAllSlots(); xOffset++; if (xOffset > 20) { xOffset = 0; yOffset--; } testPlayer.position = new Position(x + xOffset, y + yOffset, 0); const newChunk = activeWorld.chunkManager.getChunkForWorldPosition(testPlayer.position); if (!spawnChunk.equals(newChunk)) { spawnChunk.removePlayer(testPlayer); newChunk.addPlayer(testPlayer); } testPlayer.initiateRandomMovement(); } }; export default { pluginId: 'rs:spawn_test_players_command', hooks: [ { type: 'player_command', commands: ['spawn_players', 'spawnplayers'], args: [ { name: 'playerCount', type: 'number', }, ], handler, }, ], }; ================================================ FILE: src/plugins/commands/stat-commands.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; const setLevelAction: commandActionHandler = ({ player, args }) => { const skillId = args?.skillId || null; const level: number | null = (args?.level as number) || null; if (!skillId || !level) { player.sendMessage(`Invalid syntax: Use ::setlevel skill_id skill_level`); return; } const skill = player.skills[skillId]; if (!skill) { player.sendMessage(`Skill ${skillId} not found.`); return; } const exp = player.skills.getExpForLevel(level); skill.exp = exp; skill.level = level; player.outgoingPackets.updateSkill(player.skills.getSkillId(skillId as any), level, exp); }; export default { pluginId: 'rs:stat_commands', hooks: [ { type: 'player_command', commands: ['setlevel', 'setlvl'], args: [ { name: 'skillId', type: 'string', }, { name: 'level', type: 'number', }, ], handler: setLevelAction, }, ], }; ================================================ FILE: src/plugins/commands/teleport-command.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; import { activeWorld } from '@engine/world'; import { Position } from '@engine/world/position'; const action: commandActionHandler = details => { const { player, args } = details; const x = args.XorPlayerName; if (typeof x === 'string') { const playerWithName = activeWorld.findPlayer(x); if (playerWithName) { player.teleport(playerWithName.position); return; } } const xCoord: number = typeof x === 'string' ? parseInt(x, 10) : x; if (isNaN(xCoord)) { return; } const y: number = args.y as number; const level: number = args.level as number; player.teleport(new Position(xCoord, y, level)); }; const goUpAction: commandActionHandler = details => { const { player } = details; player.teleport(new Position(player.position.x, player.position.y, player.position.level + 1)); }; const goDownAction: commandActionHandler = details => { const { player } = details; if (player.position.level > 0) { player.teleport(new Position(player.position.x, player.position.y, player.position.level - 1)); } }; const setLevelCommand: commandActionHandler = details => { const { player, args } = details; const level: number = args.level as number; if (!isNaN(level) && level >= 0 && level <= 255) { player.teleport(new Position(player.position.x, player.position.y, level)); } } export default { pluginId: 'rs:teleport_command_plugin', hooks: [ { type: 'player_command', commands: [ 'move', 'goto', 'teleport', 'tele', 'moveto', 'setpos' ], args: [ { name: 'XorPlayerName', type: 'string', }, { name: 'y', type: 'number', defaultValue: 3222, }, { name: 'level', type: 'number', defaultValue: 0, }, ], handler: action, }, { type: 'player_command', commands: [ 'up', 'goup' ], handler: goUpAction, }, { type: 'player_command', commands: [ 'down', 'godown' ], handler: goDownAction, }, { type: 'player_command', commands: [ 'setheightlevel', 'heightlevel', 'hl' ], args: [ { name: 'level', type: 'number', }, ], handler: setLevelCommand, } ], }; ================================================ FILE: src/plugins/commands/transform-command.plugin.ts ================================================ import type { PlayerCommandActionHook, commandActionHandler } from '@engine/action/pipe/player-command.action'; const action: commandActionHandler = details => { const { player, args } = details; player.transformInto(details && details.args ? details.args['npcKey'] : null); }; export default { pluginId: 'rs:transform_command', hooks: [ { type: 'player_command', commands: ['transform'], args: [ { name: 'npcKey', type: 'either', defaultValue: undefined, }, ], handler: action, } as PlayerCommandActionHook, ], }; ================================================ FILE: src/plugins/commands/travel-back-command.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; const action: commandActionHandler = details => { const { player } = details; if (player.metadata.lastPosition) { player.teleport(player.metadata.lastPosition); } }; export default { pluginId: 'rs:travel_back_command', hooks: [ { type: 'player_command', commands: ['back'], handler: action, }, ], }; ================================================ FILE: src/plugins/commands/travel-command.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; import { activeWorld } from '@engine/world'; import type { TravelLocation } from '@engine/world/config/travel-locations'; const action: commandActionHandler = details => { const { player, args } = details; const search: string = args.search as string; const location = activeWorld.travelLocations.find(search) as TravelLocation; if (location) { player.teleport(location.position); player.sendLogMessage(`Welcome to ${location.name}`, details.isConsole); } else { player.sendLogMessage(`Unknown location ${search}`, details.isConsole); } }; export default { pluginId: 'rs:travel_command', hooks: [ { type: 'player_command', commands: ['travel'], args: [ { name: 'search', type: 'string', }, ], handler: action, }, ], }; ================================================ FILE: src/plugins/commands/widget-commands.plugin.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; const action: commandActionHandler = details => { const { player, args } = details; const widgetId: number = args.widgetId as number; const secondaryWidgetId: number = args.secondaryWidgetId as number; if (secondaryWidgetId === 1) { player.interfaceState.openWidget(widgetId, { slot: 'screen', }); } else { player.interfaceState.openWidget(widgetId, { slot: 'screen', multi: true, }); player.interfaceState.openWidget(secondaryWidgetId, { slot: 'tabarea', multi: true, }); } }; export default { pluginId: 'rs:widget_commands', hooks: [ { type: 'player_command', commands: ['widget'], args: [ { name: 'widgetId', type: 'number', }, { name: 'secondaryWidgetId', type: 'number', defaultValue: 1, }, ], handler: action, }, ], }; ================================================ FILE: src/plugins/dialogue/dialogue-option.plugin.ts ================================================ import type { widgetInteractionActionHandler } from '@engine/action/pipe/widget-interaction.action'; const dialogueIds = [64, 65, 66, 67, 241, 242, 243, 244, 228, 230, 232, 234, 210, 211, 212, 213, 214]; /** * Handles a basic NPC/Player/Option/Text dialogue choice/action. */ export const action: widgetInteractionActionHandler = details => { const { player, widgetId, childId } = details; player.interfaceState.closeWidget('chatbox', widgetId, childId); }; export default { pluginId: 'rs:dialog_choice', hooks: [ { type: 'widget_interaction', widgetIds: dialogueIds, handler: action, cancelActions: true, }, ], }; ================================================ FILE: src/plugins/dialogue/item-selection.plugin.ts ================================================ import type { widgetInteractionActionHandler } from '@engine/action/pipe/widget-interaction.action'; /** * Handles an item selection dialogue choice. */ export const action: widgetInteractionActionHandler = details => { const { player, widgetId, childId } = details; player.interfaceState.closeWidget('chatbox', widgetId, childId); }; export default { pluginId: 'rs:item_selection_choice', hooks: [ { type: 'widget_interaction', widgetIds: [303, 304, 305, 306, 307, 309], handler: action, cancelActions: false, }, ], }; ================================================ FILE: src/plugins/items/buckets/empty-container.plugin.ts ================================================ import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import { widgets } from '@engine/config/config-handler'; import { itemIds } from '@engine/world/config/item-ids'; import { soundIds } from '@engine/world/config/sound-ids'; import { getItemFromContainer } from '@engine/world/items/item-container'; export const handler: itemInteractionActionHandler = details => { const { player, itemId, itemSlot } = details; const inventory = player.inventory; const item = getItemFromContainer(itemId, itemSlot, inventory); if (!item) { // The specified item was not found in the specified slot. return; } inventory.remove(itemSlot); player.playSound(soundIds.emptyBucket, 5); switch (itemId) { case itemIds.jugOfWater: player.giveItem(itemIds.jug); break; default: player.giveItem(itemIds.bucket); break; } // @TODO only update necessary slots player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, inventory); }; export default { pluginId: 'rs:empty_container', hooks: [ { type: 'item_interaction', widgets: widgets.inventory, options: 'empty', itemIds: [itemIds.bucketOfMilk, itemIds.bucketOfWater, itemIds.jugOfWater], handler, cancelOtherActions: false, }, ], }; ================================================ FILE: src/plugins/items/buckets/fill-container.plugin.ts ================================================ import type { itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action'; import { findItem } from '@engine/config/config-handler'; import { animationIds } from '@engine/world/config/animation-ids'; import { itemIds } from '@engine/world/config/item-ids'; import { soundIds } from '@engine/world/config/sound-ids'; import { logger } from '@runejs/common'; const FountainIds: number[] = [879]; const SinkIds: number[] = [14878, 873]; const WellIds: number[] = [878]; export const handler: itemOnObjectActionHandler = details => { const { player, objectConfig, item } = details; const itemDef = findItem(item.itemId); if (!itemDef) { logger.error(`No item found for fill container plugin: ${item.itemId}`); return; } if (item.itemId !== itemIds.bucket && WellIds.indexOf(objectConfig.gameId) > -1) { player.sendMessage(`If I drop my ${itemDef.name.toLowerCase()} down there, I don't think I'm likely to get it back.`); return; } player.playAnimation(animationIds.fillContainerWithWater); player.playSound(soundIds.fillContainerWithWater, 7); player.removeFirstItem(item.itemId); switch (item.itemId) { case itemIds.bucket: player.giveItem(itemIds.bucketOfWater); break; case itemIds.jug: player.giveItem(itemIds.jugOfWater); break; } const objectName = details.objectConfig.name || ''; if (!objectName) { logger.warn(`Fill container object ${details.object.objectId} has no name.`); } player.sendMessage(`You fill the ${itemDef.name.toLowerCase()} from the ${objectName.toLowerCase()}.`); }; export default { pluginId: 'rs:fill_container', hooks: [ { type: 'item_on_object', objectIds: [...FountainIds, ...WellIds, ...SinkIds], itemIds: [itemIds.bucket, itemIds.jug], walkTo: true, handler, }, ], }; ================================================ FILE: src/plugins/items/capes/skillcape-emotes.plugin.ts ================================================ import type { equipmentChangeActionHandler } from '@engine/action/pipe/equipment-change.action'; import { itemIds } from '@engine/world/config/item-ids'; import { lockEmote, unlockEmote } from '@plugins/buttons/player-emotes.plugin'; export const skillcapeIds: Array = Object.keys(itemIds.skillCapes).flatMap(skill => [ itemIds.skillCapes[skill].untrimmed, itemIds.skillCapes[skill].trimmed, ]); export const equip: equipmentChangeActionHandler = details => { const { player } = details; unlockEmote(player, 'SKILLCAPE'); }; export const unequip: equipmentChangeActionHandler = details => { const { player } = details; lockEmote(player, 'SKILLCAPE'); player.stopAnimation(); player.stopGraphics(); }; export default { pluginId: 'rs:skillcape_emotes', hooks: [ { type: 'equipment_change', eventType: 'equip', handler: equip, itemIds: skillcapeIds, }, { type: 'equipment_change', eventType: 'unequip', handler: unequip, itemIds: skillcapeIds, }, ], }; ================================================ FILE: src/plugins/items/consumables/eating.plugin.ts ================================================ import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import { findItem, widgets } from '@engine/config/config-handler'; import { randomBetween } from '@engine/util/num'; import type { SkillName } from '@engine/world/actor/skills'; import { animationIds } from '@engine/world/config/animation-ids'; import { soundIds } from '@engine/world/config/sound-ids'; export const action: itemInteractionActionHandler = details => { const { player, itemId, itemSlot, itemDetails } = details; if (!itemDetails.consumable) { player.sendMessage('Item is not registered as consumable!'); return; } if (!itemDetails.metadata.consume_effects) { player.sendMessage('Item is missing consume effects!'); return; } if (!itemDetails.metadata.consume_effects.clock) { player.sendMessage('Item is missing clock!'); return; } if (itemDetails.metadata.consume_effects.special) { player.sendMessage('Cannot handle special foods yet!'); return; } const clock = 'clock_' + itemDetails.metadata.consume_effects.clock; // Check if player recently ate if (player.metadata[clock]) { return; } const inventoryItem = player.inventory.items[itemSlot]; if (!inventoryItem || inventoryItem.itemId !== itemId) { return; } const replacementItemDetails = itemDetails.metadata.consume_effects.replaced_by ? findItem(itemDetails.metadata.consume_effects.replaced_by) : null; if (replacementItemDetails) { player.inventory.items[itemSlot] = { itemId: replacementItemDetails.gameId, amount: 1 }; } else { player.inventory.items[itemSlot] = null; } player.playSound(soundIds.eat); player.playAnimation(animationIds.eat); details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, details.player.inventory); // this used to use `setTimeout` but will need rewriting to be synced with ticks // see https://github.com/runejs/server/issues/417 details.player.sendMessage('[debug] see issue #417'); // Set a timeout so player cant spam eat // player.metadata[clock] = true; // setTimeout(() => { // player.metadata[clock] = false; // }, World.TICK_LENGTH * 3); if (itemDetails.metadata.consume_effects.energy) { // TODO: Give player run energy } if (itemDetails.metadata.consume_effects.skills) { const skillModifiers = itemDetails.metadata.consume_effects.skills; for (const sk in skillModifiers) { const skill: SkillName = sk as SkillName; let value; if (Array.isArray(skillModifiers[skill])) { value = randomBetween(skillModifiers[skill][0], skillModifiers[skill][1]); } else { value = skillModifiers[skill]; } const playerSkill = player.skills[skill]; const maxLevel = playerSkill.levelForExp; const currentLevel = playerSkill.level || playerSkill.levelForExp; if (skill === 'hitpoints') { let newHealth: number = currentLevel + value; if (newHealth > maxLevel) { newHealth = maxLevel; } playerSkill.level = newHealth; player.sendMessage(`You eat the ${itemDetails.name}, and it restores ${newHealth - currentLevel} health.`); } else { let newLevel: number = currentLevel + value; if (newLevel > maxLevel + value) { newLevel = maxLevel + value; } playerSkill.level = newLevel; } player.outgoingPackets.updateSkill(player.skills.getSkillId(skill), playerSkill.level, playerSkill.exp); } } }; export default { pluginId: 'rs:eating', hooks: [ { type: 'item_interaction', widgets: widgets.inventory, options: 'eat', handler: action, cancelOtherActions: true, }, ], }; ================================================ FILE: src/plugins/items/drop-item.plugin.ts ================================================ import type { ActionCancelType } from '@engine/action/action-pipeline'; import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import { widgets } from '@engine/config/config-handler'; import { dialogue, execute } from '@engine/world/actor/dialogue'; import { Rights } from '@engine/world/actor/player/player'; import { soundIds } from '@engine/world/config/sound-ids'; import { getItemFromContainer } from '@engine/world/items/item-container'; import { serverConfig } from '@server/game/game-server'; export const handler: itemInteractionActionHandler = ({ player, itemId, itemSlot }) => { const inventory = player.inventory; const item = getItemFromContainer(itemId, itemSlot, inventory); if (!item) { // The specified item was not found in the specified slot. return; } if (!serverConfig.adminDropsEnabled && player.rights === Rights.ADMIN) { dialogue( [player], [ text => 'Administrators are not allowed to drop items.', options => [ `Destroy the item!`, [ execute(() => { inventory.remove(itemSlot); player.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, itemSlot, null); }), ], `Bank the item!`, [ execute(() => { inventory.remove(itemSlot); player.bank.add(item); player.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, itemSlot, null); }), ], ], ], ); return; } inventory.remove(itemSlot); player.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, itemSlot, null); player.playSound(soundIds.dropItem, 5); player.instance.spawnWorldItem(item, player.position, { owner: player, expires: 300 }); // (Jameskmonger) actionsCancelled is deprecated, casting this to satisfy the typecheck for now player.actionsCancelled.next(null as unknown as ActionCancelType); }; export default { pluginId: 'rs:drop_item', hooks: [ { type: 'item_interaction', widgets: widgets.inventory, options: 'drop', handler, cancelOtherActions: false, }, ], }; ================================================ FILE: src/plugins/items/equipment/equip-item.plugin.ts ================================================ import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import { widgets } from '@engine/config/config-handler'; export const handler: itemInteractionActionHandler = details => { const { player, itemId, itemSlot, itemDetails } = details; if (!itemDetails) { // The item is not yet configured on the server. player.sendMessage(`Item ${itemId} is not yet configured on the server.`); return; } player.equipItem(itemId, itemSlot, itemDetails.equipmentData?.equipmentSlot); }; export default { pluginId: 'rs:equip_item', hooks: [ { type: 'item_interaction', widgets: widgets.inventory, options: 'equip', handler, cancelOtherActions: false, }, ], }; ================================================ FILE: src/plugins/items/equipment/equipment-stats.plugin.ts ================================================ import type { buttonActionHandler } from '@engine/action/pipe/button.action'; import { widgets } from '@engine/config/config-handler'; export const handler: buttonActionHandler = details => { const { player } = details; player.updateBonuses(); player.syncBonuses(); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.equipmentStats, player.equipment); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory); player.interfaceState.openWidget(widgets.equipmentStats.widgetId, { multi: true, slot: 'screen', }); player.interfaceState.openWidget(widgets.inventory.widgetId, { multi: true, slot: 'tabarea', }); }; export default { pluginId: 'rs:equipment_stat_view', hooks: [{ type: 'button', widgetId: widgets.equipment.widgetId, buttonIds: 24, handler }], }; ================================================ FILE: src/plugins/items/equipment/unequip-item.plugin.ts ================================================ import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import { widgets } from '@engine/config/config-handler'; import { getItemFromContainer } from '@engine/world/items/item-container'; export const handler: itemInteractionActionHandler = details => { const { player, itemId, itemSlot, itemDetails } = details; const equipment = player.equipment; const item = getItemFromContainer(itemId, itemSlot, equipment); if (!item) { // The specified item was not found in the specified slot. return; } if (!itemDetails) { // The item is not yet configured on the server. player.sendMessage(`Item ${itemId} is not yet configured on the server.`); return; } player.unequipItem(itemDetails.equipmentData?.equipmentSlot); }; export default { pluginId: 'rs:unequip_item', hooks: [ { type: 'item_interaction', widgets: [widgets.equipment, widgets.equipmentStats], options: 'remove', handler, cancelOtherActions: false, }, ], }; ================================================ FILE: src/plugins/items/herblore/clean-herb.ts ================================================ import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import { findItem, widgets } from '@engine/config/config-handler'; import type { ItemDetails } from '@engine/config/item-config'; import { soundIds } from '@engine/world/config/sound-ids'; import { logger } from '@runejs/common'; interface IGrimyHerb { grimy: ItemDetails; clean: ItemDetails; level: number; experience: number; } /** * A list of all the herbs that can be cleaned. * * (Jameskmonger) I have put ! after findItem() because we know the items exist. */ const herbs: IGrimyHerb[] = [ { grimy: findItem('rs:grimy_guam')!, clean: findItem('rs:herb_guam')!, level: 3, experience: 2.5, }, { grimy: findItem('rs:grimy_marrentill')!, clean: findItem('rs:herb_marrentill')!, level: 5, experience: 3.8, }, { grimy: findItem('rs:grimy_tarromin')!, clean: findItem('rs:herb_tarromin')!, level: 11, experience: 5, }, { grimy: findItem('rs:grimy_harralander')!, clean: findItem('rs:herb_harralander')!, level: 20, experience: 6.3, }, { grimy: findItem('rs:grimy_ranarr')!, clean: findItem('rs:herb_ranarr')!, level: 25, experience: 7.5, }, { grimy: findItem('rs:grimy_toadflax')!, clean: findItem('rs:herb_toadflax')!, level: 30, experience: 8, }, { grimy: findItem('rs:grimy_irit')!, clean: findItem('rs:herb_irit')!, level: 40, experience: 8.8, }, { grimy: findItem('rs:grimy_avantoe')!, clean: findItem('rs:herb_avantoe')!, level: 48, experience: 10, }, { grimy: findItem('rs:grimy_kwuarm')!, clean: findItem('rs:herb_kwuarm')!, level: 54, experience: 11.3, }, { grimy: findItem('rs:grimy_snapdragon')!, clean: findItem('rs:herb_snapdragon')!, level: 59, experience: 11.8, }, { grimy: findItem('rs:grimy_cadantine')!, clean: findItem('rs:herb_cadantine')!, level: 65, experience: 12.5, }, { grimy: findItem('rs:grimy_lantadyme')!, clean: findItem('rs:herb_lantadyme')!, level: 67, experience: 13.1, }, { grimy: findItem('rs:grimy_dwarf_weed')!, clean: findItem('rs:herb_dwarf_weed')!, level: 70, experience: 13.8, }, { grimy: findItem('rs:grimy_torstol')!, clean: findItem('rs:herb_torstol')!, level: 75, experience: 15, }, ]; export const action: itemInteractionActionHandler = details => { const { player, itemId, itemSlot } = details; const herb = herbs.find(herb => herb.grimy.gameId === itemId); if (!herb) { return; } if (!player.skills.hasLevel('herblore', herb.level)) { player.sendMessage(`You need a Herblore level of ${herb.level} to identify this herb.`, true); return; } const inventoryItem = player.inventory.items[itemSlot]; // Always check for cheaters if (!inventoryItem) { logger.warn(`[herblore] Player ${player.username} tried to clean herb without having it in their inventory.`); return; } if (inventoryItem.itemId !== herb.grimy.gameId) { logger.warn(`[herblore] Player ${player.username} tried to clean herb but itemId did not match.`); return; } player.skills.addExp('herblore', herb.experience); player.inventory.set(itemSlot, { itemId: herb.clean.gameId, amount: 1 }); details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, details.player.inventory); player.playSound(soundIds.herblore.clean_herb); }; export default { type: 'item_action', widgets: widgets.inventory, options: 'identify', itemIds: herbs.map(herb => herb.grimy.gameId), action, cancelOtherActions: true, }; ================================================ FILE: src/plugins/items/move-item.plugin.ts ================================================ import type { itemSwapActionHandler } from '@engine/action/pipe/item-swap.action'; import { widgets } from '@engine/config/config-handler'; import type { Player } from '@engine/world/actor/player/player'; import type { ItemContainer } from '@engine/world/items/item-container'; type WidgetDetail = [number, number, (player: Player) => ItemContainer]; const movableWidgets: WidgetDetail[] = [ // Player Bank Screen [widgets.bank.screenWidget.widgetId, widgets.bank.screenWidget.containerId, player => player.bank], ]; function moveItem( player: Player, container: ItemContainer, widget: { widgetId: number; containerId: number }, fromSlot: number, toSlot: number, ): void { if (toSlot > container.size - 1 || fromSlot > container.size - 1) { return; } if (fromSlot < toSlot) { let slot = toSlot; let current = container.remove(fromSlot); while (slot >= fromSlot) { const temp = container.remove(slot); container.set(slot, current); current = temp; slot--; } } else { let slot = toSlot; let current = container.remove(fromSlot); while (slot <= fromSlot) { const temp = container.remove(slot); container.set(slot, current); current = temp; slot++; } } player.outgoingPackets.sendUpdateAllWidgetItems(widget, container); } export const action: itemSwapActionHandler = details => { const { player, widgetId, containerId, fromSlot, toSlot } = details; const widgetDetails = movableWidgets.filter(widgetDetail => widgetDetail[0] === widgetId && widgetDetail[1] === containerId); if (widgetDetails && widgetDetails[0]) { const itemContainer: ItemContainer = widgetDetails[0][2](player); moveItem(player, itemContainer, { widgetId, containerId }, fromSlot, toSlot); } }; export default { pluginId: 'rs:move_item', hooks: [ { type: 'move_item', widgetIds: movableWidgets.map(widgetDetails => widgetDetails[0]), handler: action, }, ], }; ================================================ FILE: src/plugins/items/pickup-item.plugin.ts ================================================ import type { ActionCancelType } from '@engine/action/action-pipeline'; import type { spawnedItemInteractionHandler } from '@engine/action/pipe/spawned-item-interaction.action'; import { widgets } from '@engine/config/config-handler'; import { soundIds } from '@engine/world/config/sound-ids'; import type { Item } from '@engine/world/items/item'; import { logger } from '@runejs/common'; export const handler: spawnedItemInteractionHandler = ({ player, worldItem, itemDetails }) => { const inventory = player.inventory; const amount = worldItem.amount; let slot = -1; if (itemDetails.stackable) { const existingItemIndex = inventory.findIndex(worldItem.itemId); if (existingItemIndex !== -1) { const existingItem = inventory.items[existingItemIndex]; if (existingItem && existingItem.amount + worldItem.amount >= 2147483647) { // @TODO create new item stack return; } else { slot = existingItemIndex; } } } if (slot === -1) { slot = inventory.getFirstOpenSlot(); } if (slot === -1) { player.sendMessage(`You don't have enough free space to do that.`); return; } if (!worldItem.instance) { logger.error(`World item ${worldItem.itemId} has no instance`); return; } worldItem.instance.despawnWorldItem(worldItem); const item: Item = { itemId: worldItem.itemId, amount, }; const addedItem = inventory.add(item); if (!addedItem) { logger.error(`Failed to add item ${item.itemId} to inventory for player ${player.username}`); return; } player.outgoingPackets.sendUpdateSingleWidgetItem(widgets.inventory, addedItem.slot, addedItem.item); player.playSound(soundIds.pickupItem, 3); // (Jameskmonger) actionsCancelled is deprecated, casting this to satisfy the typecheck for now player.actionsCancelled.next(null as unknown as ActionCancelType); }; export default { pluginId: 'rs:pickup_item', hooks: [ { type: 'spawned_item_interaction', options: 'pick-up', handler, walkTo: true, }, ], }; ================================================ FILE: src/plugins/items/pots/empty-pot.plugin.ts ================================================ import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import { widgets } from '@engine/config/config-handler'; import { itemIds } from '@engine/world/config/item-ids'; import { soundIds } from '@engine/world/config/sound-ids'; import { getItemFromContainer } from '@engine/world/items/item-container'; export const action: itemInteractionActionHandler = details => { const { player, itemId, itemSlot } = details; const inventory = player.inventory; const item = getItemFromContainer(itemId, itemSlot, inventory); if (!item) { // The specified item was not found in the specified slot. return; } inventory.remove(itemSlot); player.playSound(soundIds.potContentModified, 5); player.giveItem(itemIds.pot); }; export default { pluginId: 'rs:empty_pot', hooks: [ { type: 'item_interaction', widgets: widgets.inventory, options: 'empty', itemIds: [itemIds.potOfFlour], handler: action, cancelOtherActions: false, }, ], }; ================================================ FILE: src/plugins/items/rotten-potato/helpers/rotten-potato-helpers.ts ================================================ import type { ItemOnItemAction } from '@engine/action/pipe/item-on-item.action'; import type { WidgetInteractionAction } from '@engine/action/pipe/widget-interaction.action'; import { findItem, widgets } from '@engine/config/config-handler'; import type { ItemDetails } from '@engine/config/item-config'; import { Rights } from '@engine/world/actor/player/player'; /** * The rotten potato item. * * (Jameskmonger) I have put ! after findItem() because we know the item exists. */ export const RottenPotatoItem: ItemDetails = findItem('rs:rotten_potato')!; export const ExecuteIfAdmin = (details: ItemOnItemAction | WidgetInteractionAction, callback) => { if (details.player.rights === Rights.ADMIN) { callback(details); return; } while (details.player.inventory.has(RottenPotatoItem.gameId)) { details.player.inventory.removeFirst(RottenPotatoItem.gameId, false); } details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, details.player.inventory); }; ================================================ FILE: src/plugins/items/rotten-potato/helpers/rotten-potato-travel.ts ================================================ import type { widgetInteractionActionHandler } from '@engine/action/pipe/widget-interaction.action'; import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; const INTRO_PAGE_COUNT = 1; const ITEMS_PER_PAGE = 15; const pageCount = INTRO_PAGE_COUNT + Math.round(activeWorld.travelLocations.locations.length / ITEMS_PER_PAGE); export function openTravel(player: Player, page: number) { const widget = player.interfaceState.openWidget(27, { slot: 'screen', fakeWidget: 3100002, metadata: { page: page, }, }); // Prev page button player.modifyWidget(widget.widgetId, { childId: 95, hidden: widget.metadata.page === 1, // hide prev page button if we are on the first page }); const isLastPage = p => pageCount === page * 2; // 2 "pages" per page // Next page buttton player.modifyWidget(widget.widgetId, { childId: 97, hidden: isLastPage(page), }); // prev page label player.modifyWidget(widget.widgetId, { childId: 98, text: widget.metadata.page * 2 - 1 === 1 ? `` : `Page ${widget.metadata.page * 2 - 1} `, }); // next page label player.modifyWidget(widget.widgetId, { childId: 99, text: `Page ${widget.metadata.page * 2} `, }); // clear default lines of both open pages for (let i = 0; i < ITEMS_PER_PAGE * 2; i++) { player.modifyWidget(widget.widgetId, { childId: 33 + i, text: '', hidden: true, }); } let currentLocation = ITEMS_PER_PAGE * 2 * (page - INTRO_PAGE_COUNT - 1) + INTRO_PAGE_COUNT * ITEMS_PER_PAGE; const historyPage = [ 'Last locations', '', '', ...(player.savedMetadata.lastTravel || new Array(10)).map((location: number | undefined) => location === undefined ? '' : activeWorld.travelLocations.locations[location]?.name, ), ]; if (widget.metadata.page * 2 - 1 === 1) { for (let i = 0; i < ITEMS_PER_PAGE * 2; i += 2) { player.modifyWidget(widget.widgetId, { childId: 101 + i, text: historyPage[currentLocation + ITEMS_PER_PAGE] || '', hidden: false, }); currentLocation++; } } else { for (let i = 0; i < ITEMS_PER_PAGE * 2; i += 2) { player.modifyWidget(widget.widgetId, { childId: 101 + i, text: activeWorld.travelLocations.locations[currentLocation]?.name || '', hidden: !activeWorld.travelLocations.locations[currentLocation]?.name, }); currentLocation++; } } for (let i = 0; i < 30; i += 2) { player.modifyWidget(widget.widgetId, { childId: 131 + i - 1, hidden: !activeWorld.travelLocations.locations[currentLocation]?.name, }); player.modifyWidget(widget.widgetId, { childId: 131 + i, text: activeWorld.travelLocations.locations[currentLocation]?.name || '', hidden: !activeWorld.travelLocations.locations[currentLocation]?.name, }); currentLocation++; } } export const travelMenuInteract: widgetInteractionActionHandler = details => { const playerWidget = details.player.interfaceState.findWidget(27); if (!playerWidget || !playerWidget.metadata.page) { return; } switch (details.childId) { case 160: openTravel(details.player, 1); return; case 94: openTravel(details.player, playerWidget.metadata.page - 1); return; case 96: openTravel(details.player, playerWidget.metadata.page + 1); return; } let selectedIndex: number | undefined = undefined; if (details.childId >= 101 && details.childId <= 129) { selectedIndex = (details.childId - 99) / 2 - 1; } if (details.childId >= 131 && details.childId <= 159) { selectedIndex = (details.childId - 129) / 2 - 1 + 15; } if (selectedIndex != undefined) { let teleportIndex = selectedIndex + 30 * (playerWidget.metadata.page - 1); if (!details.player.savedMetadata.lastTravel) { details.player.savedMetadata.lastTravel = new Array(10); } if (teleportIndex < INTRO_PAGE_COUNT * ITEMS_PER_PAGE) { if (teleportIndex < 3 && teleportIndex > 12) { openTravel(details.player, playerWidget.metadata.page); } teleportIndex = details.player.savedMetadata.lastTravel[teleportIndex - 3]; if (!teleportIndex) { openTravel(details.player, playerWidget.metadata.page); } } else { teleportIndex = teleportIndex - INTRO_PAGE_COUNT * ITEMS_PER_PAGE; } const newTravelLog = [teleportIndex, ...details.player.savedMetadata.lastTravel.slice(0, 9)]; for (let index = 1; newTravelLog.length; index++) { const element = newTravelLog[index]; if (element === teleportIndex) { newTravelLog[index] = newTravelLog[index + 1]; } if (element === undefined) { if (newTravelLog[index + 1] === undefined) { break; } newTravelLog[index] = newTravelLog[index + 1]; } } details.player.savedMetadata.lastTravel = newTravelLog; details.player.teleport(activeWorld.travelLocations.locations[teleportIndex].position); details.player.interfaceState.closeAllSlots(); } else { openTravel(details.player, playerWidget.metadata.page); } }; ================================================ FILE: src/plugins/items/rotten-potato/hooks/rotten-potato-command-hook.ts ================================================ import type { commandActionHandler } from '@engine/action/pipe/player-command.action'; const spawnPotato: commandActionHandler = details => { details.player.giveItem('rs:rotten_potato'); }; export default spawnPotato; ================================================ FILE: src/plugins/items/rotten-potato/hooks/rotten-potato-eat.ts ================================================ import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import { dialogue, execute } from '@engine/world/actor/dialogue'; enum DialogueOption { SET_ALL_STATS, WIPE_INVENTORY, SETUP_POH, TELEPORT_TO_PLAYER, SPAWN_AGGRESSIVE_NPC, } const eatPotato: itemInteractionActionHandler = async details => { let chosenOption: DialogueOption; await dialogue( [details.player], [ options => [ `Set all stats`, [execute(() => (chosenOption = DialogueOption.SET_ALL_STATS))], `Wipe inventory`, [execute(() => (chosenOption = DialogueOption.WIPE_INVENTORY))], `Setup POH`, [execute(() => (chosenOption = DialogueOption.SETUP_POH))], `Teleport to player`, [execute(() => (chosenOption = DialogueOption.TELEPORT_TO_PLAYER))], `Spawn aggressive NPC`, [execute(() => (chosenOption = DialogueOption.SPAWN_AGGRESSIVE_NPC))], ], ], ); switch (chosenOption!) { case DialogueOption.SET_ALL_STATS: break; case DialogueOption.TELEPORT_TO_PLAYER: break; default: break; } }; export default eatPotato; ================================================ FILE: src/plugins/items/rotten-potato/hooks/rotten-potato-item-on-item.ts ================================================ import type { itemOnItemActionHandler } from '@engine/action/pipe/item-on-item.action'; import { findItem } from '@engine/config/config-handler'; import { RottenPotatoItem } from '@plugins/items/rotten-potato/helpers/rotten-potato-helpers'; const itemOnPotato: itemOnItemActionHandler = details => { const slotToDelete = details.usedItem.itemId === RottenPotatoItem.gameId ? details.usedWithSlot : details.usedSlot; const inventoryItem = details.player.inventory.items[slotToDelete]; if (!inventoryItem) { details.player.sendMessage(`You don't have that item in your inventory.`); return; } const item = inventoryItem.itemId; const itemDetails = findItem(item); details.player.removeItem(slotToDelete); details.player.sendLogMessage(`Whee... ${itemDetails?.name || 'Unknown item'} All gone!`, false); }; export default itemOnPotato; ================================================ FILE: src/plugins/items/rotten-potato/hooks/rotten-potato-item-on-player.ts ================================================ import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import type { itemOnPlayerActionHandler } from '@engine/action/pipe/item-on-player.action'; import { widgets } from '@engine/config/config-handler'; import type { Player } from '@engine/world/actor/player/player'; import type { Item } from '@engine/world/items/item'; import { logger } from '@runejs/common'; export const potatoOnPlayer: itemOnPlayerActionHandler = details => { const widget = details.player.interfaceState.openWidget(widgets.bank.depositBoxWidget.widgetId, { slot: 'screen', fakeWidget: 3100001, }); widget.metadata['player'] = details.otherPlayer; details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.depositBoxWidget, details.otherPlayer.inventory); details.player.modifyWidget(widgets.bank.depositBoxWidget.widgetId, { childId: widgets.bank.depositBoxWidget.titleText, text: `${details.otherPlayer.username}'s Inventory`, }); }; export const potatoManipulatePlayerInventory: itemInteractionActionHandler = details => { const playerWidget = details.player.interfaceState.findWidget(widgets.bank.depositBoxWidget.widgetId); if (!playerWidget) { return; } const otherPlayer: Player = playerWidget.metadata['player']; if (!otherPlayer) { return; } // If the item is a noted item, we need to de-note it const itemIdToAdd: number = details.itemId; let countToRemove: number; if (details.option.endsWith('all')) { countToRemove = -1; } else { countToRemove = +details.option.replace('deposit-', ''); } const slotsWithItem = otherPlayer.inventory.findAll(details.itemId); let itemAmount = 0; slotsWithItem.forEach(slot => { const item = otherPlayer.inventory.items[slot]; if (!item) { throw new Error(`Container item was not present, for item id ${details.itemId} in inventory, while trying to deposit`); } if (item.itemId !== details.itemId) { throw new Error(`Container item id mismatch, for item id ${details.itemId} in inventory, while trying to deposit`); } itemAmount += item.amount; }); if (countToRemove == -1 || countToRemove > itemAmount) { countToRemove = itemAmount; } if (!details.player.inventory.canFit({ itemId: itemIdToAdd, amount: countToRemove })) { details.player.sendMessage('Your inventory is full.'); return; } const itemToAdd: Item = { itemId: itemIdToAdd, amount: 0 }; while (countToRemove > 0 && otherPlayer.inventory.has(details.itemId)) { const invIndex = otherPlayer.inventory.findIndex(details.itemId); const invItem = otherPlayer.inventory.items[invIndex]; if (!invItem) { logger.error(`Could not find item ${details.itemId} in inventory at slot ${invIndex} in rotten potato`); return; } if (countToRemove >= invItem.amount) { itemToAdd.amount += invItem.amount; countToRemove -= invItem.amount; otherPlayer.inventory.remove(invIndex); } else { itemToAdd.amount += countToRemove; invItem.amount -= countToRemove; countToRemove = 0; } } details.player.inventory.addStacking(itemToAdd); details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.depositBoxWidget, otherPlayer.inventory); details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, details.player.inventory); otherPlayer.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, otherPlayer.inventory); }; ================================================ FILE: src/plugins/items/rotten-potato/hooks/rotten-potato-peel.ts ================================================ import { getActionHooks } from '@engine/action/hook/action-hook'; import { advancedNumberHookFilter } from '@engine/action/hook/hook-filters'; import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import type { ObjectInteractionActionHook } from '@engine/action/pipe/object-interaction.action'; import { dialogue, execute } from '@engine/world/actor/dialogue'; import type { Player } from '@engine/world/actor/player/player'; import { objectIds } from '@engine/world/config/object-ids'; import { openTravel } from '@plugins/items/rotten-potato/helpers/rotten-potato-travel'; function openBank(player: Player) { const interactionActions = getActionHooks('object_interaction').filter(plugin => advancedNumberHookFilter(plugin.objectIds, objectIds.bankBooth[0], plugin.options, 'use-quickly'), ); interactionActions.forEach(plugin => { if (!plugin.handler) { return; } plugin.handler({ player: player, object: { objectId: objectIds.bankBooth[0], level: player.position.level, x: player.position.x, y: player.position.y, orientation: 0, type: 0, }, option: 'use-quickly', position: player.position, objectConfig: undefined as any, cacheOriginal: undefined as any, }); }); } enum DialogueOption { BANK, TELEPORT_MENU, TELEPORT_TO_RARE_DROP, FORCE_RARE_DROP, } const peelPotato: itemInteractionActionHandler = async details => { let chosenOption: DialogueOption; // console.log(world.travelLocations.locations) await dialogue( [details.player], [ options => [ `Bank menu`, [execute(() => (chosenOption = DialogueOption.BANK))], `Travel Far!`, [execute(() => (chosenOption = DialogueOption.TELEPORT_MENU))], // `Teleport to RARE!`, [ // execute(() => chosenOption = DialogueOption.TELEPORT_TO_RARE_DROP) // ], // `Spawn RARE!`, [ // execute(() => chosenOption = DialogueOption.FORCE_RARE_DROP) // ], ], ], ); // using ! here because we have just set it in the dialogue switch (chosenOption!) { case DialogueOption.BANK: openBank(details.player); break; case DialogueOption.TELEPORT_MENU: openTravel(details.player, 1); break; default: break; } }; export default peelPotato; ================================================ FILE: src/plugins/items/rotten-potato/rotten-potato.plugin.ts ================================================ import type { WidgetInteractionActionHook } from '@engine/action/pipe/widget-interaction.action'; import { widgets } from '@engine/config/config-handler'; import { ExecuteIfAdmin, RottenPotatoItem } from '@plugins/items/rotten-potato/helpers/rotten-potato-helpers'; import { travelMenuInteract } from '@plugins/items/rotten-potato/helpers/rotten-potato-travel'; import spawnPotato from '@plugins/items/rotten-potato/hooks/rotten-potato-command-hook'; import eatPotato from '@plugins/items/rotten-potato/hooks/rotten-potato-eat'; import itemOnPotato from '@plugins/items/rotten-potato/hooks/rotten-potato-item-on-item'; import { potatoManipulatePlayerInventory, potatoOnPlayer } from '@plugins/items/rotten-potato/hooks/rotten-potato-item-on-player'; import peelPotato from '@plugins/items/rotten-potato/hooks/rotten-potato-peel'; export default { pluginId: 'promises:rotten-potato', hooks: [ { type: 'item_interaction', widgets: widgets.inventory, itemIds: RottenPotatoItem.gameId, options: 'peel', handler: details => ExecuteIfAdmin(details, peelPotato), cancelOtherActions: true, }, { type: 'item_interaction', widgets: widgets.inventory, itemIds: RottenPotatoItem.gameId, options: 'eat', handler: details => ExecuteIfAdmin(details, eatPotato), cancelOtherActions: true, }, { type: 'item_on_player', itemIds: RottenPotatoItem.gameId, handler: details => ExecuteIfAdmin(details, potatoOnPlayer), walkTo: false, }, { type: 'player_command', commands: ['potato'], handler: details => ExecuteIfAdmin(details, spawnPotato), cancelOtherActions: true, }, { type: 'item_on_item', items: [{ item1: RottenPotatoItem.gameId }], handler: details => ExecuteIfAdmin(details, itemOnPotato), cancelOtherActions: true, }, { type: 'item_interaction', widgets: { ...widgets.bank.depositBoxWidget, widgetId: 3100001 }, options: ['deposit-1', 'deposit-5', 'deposit-10', 'deposit-all'], handler: details => ExecuteIfAdmin(details, potatoManipulatePlayerInventory), }, { type: 'widget_interaction', widgetIds: 3100002, handler: details => ExecuteIfAdmin(details, travelMenuInteract), multi: true, } as WidgetInteractionActionHook, ], }; ================================================ FILE: src/plugins/items/runecrafting/tiaras.plugin.ts ================================================ import type { equipmentChangeActionHandler } from '@engine/action/pipe/equipment-change.action'; export const equip: equipmentChangeActionHandler = details => { const { player } = details; player.outgoingPackets.updateClientConfig(491, 1); }; export const unequip: equipmentChangeActionHandler = details => { const { player } = details; player.outgoingPackets.updateClientConfig(491, 0); }; export default { pluginId: 'rs:tiaras', hooks: [ { type: 'equipment_change', eventType: 'equip', handler: equip, itemIds: 5527, }, { type: 'equipment_change', eventType: 'unequip', handler: unequip, itemIds: 5527, }, ], }; ================================================ FILE: src/plugins/items/shopping/buy-from-shop.plugin.ts ================================================ import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import { findItem, findShop, widgets } from '@engine/config/config-handler'; import { itemIds } from '@engine/world/config/item-ids'; import type { Item } from '@engine/world/items/item'; import type { ItemContainer } from '@engine/world/items/item-container'; import { getItemFromContainer } from '@engine/world/items/item-container'; import { logger } from '@runejs/common'; function removeCoins(inventory: ItemContainer, coinsIndex: number, cost: number): void { const coins = inventory.items[coinsIndex]; if (!coins) { logger.error(`Could not find coins in inventory at slot ${coinsIndex} while trying to remove coins`); return; } const amountAfterPurchase = coins.amount - cost; inventory.set(coinsIndex, { itemId: itemIds.coins, amount: amountAfterPurchase }); } export const handler: itemInteractionActionHandler = details => { const { player, itemId, itemSlot, widgetId, option } = details; if (!player.interfaceState.findWidget(widgetId)) { return; } const openedShopKey = player.metadata.lastOpenedShopKey; if (!openedShopKey) { return; } const shop = findShop(openedShopKey); if (!shop) { return; } const shopContainer = shop.container; const shopItem = getItemFromContainer(itemId, itemSlot, shopContainer); if (!shopItem) { // The specified item was not found in the specified slot. return; } if (shopItem.amount <= 0) { // Out of stock return; } const buyAmounts = { 'buy-1': 1, 'buy-5': 5, 'buy-10': 10, }; let buyAmount = buyAmounts[option]; if (shopItem.amount < buyAmount) { buyAmount = shopItem.amount; } const buyItem = findItem(itemId); if (!buyItem) { logger.error(`Could not find cache item for item id ${itemId} in shop ${openedShopKey}`); return; } const buyItemValue = shop.getBuyFromShopPrice(buyItem); player.sendMessage(`${buyItem.key} : ${buyItemValue}, ${buyItem.value}`); let buyCost = buyAmount * buyItemValue; const coinsIndex = player.hasCoins(buyCost); if (coinsIndex === -1) { player.sendMessage(`You don't have enough coins.`); return; } const inventory = player.inventory; if (buyItem.stackable) { const inventoryStackSlot = inventory.items.findIndex(item => itemId === itemId); if (inventoryStackSlot === -1) { if (inventory.getFirstOpenSlot() === -1) { player.sendMessage(`You don't have enough space in your inventory.`); return; } } else { const inventoryItem = inventory.items[inventoryStackSlot]; if (!inventoryItem) { logger.error( `Coult not find inventory item at slot ${inventoryStackSlot} for player ${player.username} while trying to stack`, ); return; } if (inventoryItem.amount + buyAmount >= 2147483647) { player.sendMessage(`You don't have enough space in your inventory.`); return; } shopContainer.set(itemSlot, { itemId, amount: shopItem.amount - buyAmount }); removeCoins(inventory, coinsIndex, buyCost); const item: Item = { itemId, amount: inventoryItem.amount + buyAmount, }; inventory.set(inventoryStackSlot, item); } } else { let bought = 0; for (let i = 0; i < buyAmount; i++) { if (inventory.add({ itemId, amount: 1 }) !== null) { bought++; } else { break; } } if (bought !== buyAmount) { player.sendMessage(`You don't have enough space in your inventory.`); } shopContainer.set(itemSlot, { itemId, amount: shopItem.amount - bought }); buyCost = bought * buyItemValue; removeCoins(inventory, coinsIndex, buyCost); } player.outgoingPackets.sendUpdateSingleWidgetItem(widgets.shop, itemSlot, shopContainer.items[itemSlot]); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.shopPlayerInventory, inventory); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, inventory); }; export default { pluginId: 'rs:shop_buy', hooks: [ { type: 'item_interaction', widgets: widgets.shop, options: ['buy-1', 'buy-5', 'buy-10'], handler, cancelOtherActions: false, }, ], }; ================================================ FILE: src/plugins/items/shopping/item-value.plugin.ts ================================================ import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import { findItem, findShop, widgets } from '@engine/config/config-handler'; import { getItemFromContainer } from '@engine/world/items/item-container'; export const shopSellValueHandler: itemInteractionActionHandler = details => { const { player, itemId, itemSlot, widgetId, option } = details; if (!player.interfaceState.findWidget(widgetId)) { return; } const openedShopKey = player.metadata.lastOpenedShopKey; if (!openedShopKey) { return; } const shop = findShop(openedShopKey); if (!shop) { return; } const shopContainer = shop.container; const shopItem = getItemFromContainer(itemId, itemSlot, shopContainer); console.log(itemId, itemSlot, openedShopKey, shopContainer); if (!shopItem) { // The specified item was not found in the specified slot. player.sendMessage(`ERROR item not in shopslot.`); return; } if (shopItem.amount <= 0) { player.sendMessage(`The shop has ran out of stock.`); // Out of stock return; } const buyItem = findItem(itemId); if (!buyItem) { // The specified item was not found in the specified slot. player.sendMessage(`Error item does not exist. [id:${itemId}]`); return; } player.sendMessage(`${buyItem.name}: currently costs ${shop.getBuyFromShopPrice(buyItem)} coins.`); }; export const shopPurchaseValueHandler: itemInteractionActionHandler = ({ player, itemDetails }) => { const openedShopKey = player.metadata.lastOpenedShopKey; if (!openedShopKey) { return; } const shop = findShop(openedShopKey); if (!shop) { return; } const shopBuyPrice = shop.getBuyFromShopPrice(itemDetails); if (shopBuyPrice === -1) { player.sendMessage(`You can't sell this item to this shop.`); } else { player.sendMessage(`${itemDetails.name}: shop will buy for ${shopBuyPrice} coins.`); } }; export default { pluginId: 'rs:shop_item_value', hooks: [ { type: 'item_interaction', widgets: widgets.shop, options: 'value', handler: shopSellValueHandler, cancelOtherActions: false, }, { type: 'item_interaction', widgets: widgets.shopPlayerInventory, options: 'value', handler: shopPurchaseValueHandler, cancelOtherActions: false, }, ], }; ================================================ FILE: src/plugins/items/shopping/sell-to-shop.plugin.ts ================================================ import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import { findShop, widgets } from '@engine/config/config-handler'; import { itemIds } from '@engine/world/config/item-ids'; import { getItemFromContainer } from '@engine/world/items/item-container'; export const handler: itemInteractionActionHandler = details => { const { player, itemId, itemSlot, option, itemDetails } = details; if (!player.interfaceState.findWidget(widgets.shop.widgetId)) { return; } const openedShopKey = player.metadata.lastOpenedShopKey; if (!openedShopKey) { return; } const shop = findShop(openedShopKey); if (!shop) { return; } const inventory = player.inventory; const inventoryItem = getItemFromContainer(itemId, itemSlot, inventory); if (!inventoryItem) { // The specified item was not found in the specified slot. return; } const sellAmounts = { 'sell-1': 1, 'sell-5': 5, 'sell-10': 10, }; let sellAmount = sellAmounts[option]; const shopContainer = shop.container; const shopSpaces = shopContainer.items.filter(item => item === null); const shopItemIndex = shopContainer.items.findIndex(item => item !== null && item.itemId === itemId); if (shopItemIndex === -1 && shopSpaces.length === 0) { player.sendMessage(`There isn't enough space in the shop.`); return; } const shopItem = shopContainer.items[shopItemIndex]; if (itemDetails.stackable) { if (inventoryItem.amount < sellAmount) { inventory.remove(itemSlot); sellAmount = inventoryItem.amount; } else { inventory.set(itemSlot, { itemId, amount: inventoryItem.amount - sellAmount }); } } else { const foundItems = inventory.items.map((item, i) => (item !== null && item.itemId === itemId ? i : null)).filter(i => i !== null); if (foundItems.length < sellAmount) { sellAmount = foundItems.length; } for (let i = 0; i < sellAmount; i++) { const item = foundItems[i]; if (!item) { throw new Error(`Inventory item was not present, for item id ${itemId} in inventory, while trying to sell`); } inventory.remove(item); } } const itemValue = shop.getSellToShopPrice(itemDetails); // @TODO scale price per item, not per sale if (!shopItem) { shopContainer.set(shopContainer.getFirstOpenSlot(), { itemId, amount: sellAmount }); } else { shopItem.amount += sellAmount; } const sellPrice = sellAmount * itemValue; // @TODO scale price per item, not per sale if (sellPrice > 0) { let coinsIndex = player.hasCoins(1); if (coinsIndex === -1) { coinsIndex = inventory.getFirstOpenSlot(); inventory.set(coinsIndex, { itemId: itemIds.coins, amount: sellPrice }); } else { // TODO (Jameskmonger) consider being explicit to prevent dupes inventory.items[coinsIndex]!.amount += sellPrice; } } player.outgoingPackets.sendUpdateAllWidgetItems(widgets.shop, shopContainer); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.shopPlayerInventory, inventory); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, inventory); }; export default { pluginId: 'rs:shop_sell', hooks: [ { type: 'item_interaction', widgets: widgets.shopPlayerInventory, options: ['sell-1', 'sell-5', 'sell-10'], handler, cancelOtherActions: false, }, ], }; ================================================ FILE: src/plugins/items/swap-items.plugin.ts ================================================ import type { itemSwapActionHandler } from '@engine/action/pipe/item-swap.action'; import { widgets } from '@engine/config/config-handler'; import type { Player } from '@engine/world/actor/player/player'; import type { ItemContainer } from '@engine/world/items/item-container'; type WidgetDetail = [number, number, (player: Player) => ItemContainer]; const swappableWidgets: WidgetDetail[] = [ // Player Inventory [widgets.inventory.widgetId, widgets.inventory.containerId, player => player.inventory], // Player Bank Screen [widgets.bank.screenWidget.widgetId, widgets.bank.screenWidget.containerId, player => player.bank], ]; function swapItems(container: ItemContainer, fromSlot: number, toSlot: number): void { if (toSlot > container.size - 1 || fromSlot > container.size - 1) { return; } container.swap(fromSlot, toSlot); } export const action: itemSwapActionHandler = details => { const { player, widgetId, containerId, fromSlot, toSlot } = details; const widgetDetails = swappableWidgets.filter(widgetDetail => widgetDetail[0] === widgetId && widgetDetail[1] === containerId); if (widgetDetails && widgetDetails[0]) { const itemContainer: ItemContainer = widgetDetails[0][2](player); swapItems(itemContainer, fromSlot, toSlot); } }; export default { pluginId: 'rs:swap_items', hooks: [ { type: 'item_swap', widgetIds: swappableWidgets.map(widgetDetails => widgetDetails[0]), handler: action, }, ], }; ================================================ FILE: src/plugins/music/music-regions.plugin.ts ================================================ import type { playerInitActionHandler } from '@engine/action/pipe/player-init.action'; import { findMusicTrack, findSongIdByRegionId, musicRegionMap, musicRegions, widgets } from '@engine/config/config-handler'; import { colors } from '@engine/util/colors'; import { MusicPlayerMode } from '@engine/world/sound/music'; musicRegions.forEach(song => song.regionIds.forEach(region => musicRegionMap.set(region, song.songId))); function getByValue(map, searchValue) { for (const [key, value] of map.entries()) { if (value === searchValue) return key; } } const regionChangedHandler = ({ player, currentMapRegionId }): void => { const songId = findSongIdByRegionId(currentMapRegionId); if (songId == null) { return; } const musicTrack = findMusicTrack(songId); if (!musicTrack) { return; } const songName = musicTrack.songName; // player.sendMessage(`Playing ${songId}:${getByValue(songs, songId)} at region ${currentMapRegionId}`); if (!player.musicTracks.includes(songId)) { player.musicTracks.push(songId); player.sendMessage('You have unlocked a new music track: ' + songName + '.'); player.modifyWidget(widgets.musicPlayerTab, { childId: musicTrack.musicTabButtonId, textColor: colors.green }); } if (player.settings.musicPlayerMode === MusicPlayerMode.AUTO) { player.playSong(songId); } }; const playerInitHandler: playerInitActionHandler = ({ player }): void => { // Plays the appropriate location's song on player init regionChangedHandler({ player, currentMapRegionId: ((player.position.x >> 6) << 8) + (player.position.y >> 6) }); }; export default { pluginId: 'rs:music_regions', hooks: [ { type: 'region_change', regionType: 'region', handler: regionChangedHandler, }, { type: 'player_init', handler: playerInitHandler, }, ], }; ================================================ FILE: src/plugins/music/music-tab.plugin.ts ================================================ import type { buttonActionHandler } from '@engine/action/pipe/button.action'; import { findMusicTrackByButtonId, findSongIdByRegionId, widgets } from '@engine/config/config-handler'; import { activeWorld } from '@engine/world'; import { widgetScripts } from '@engine/world/config/widget'; import { MusicPlayerMode, MusicTabButtonIds } from '@engine/world/sound/music'; import { logger } from '@runejs/common'; export const handler: buttonActionHandler = details => { const { player, buttonId } = details; if (buttonId === MusicTabButtonIds.AUTO_BUTTON_ID) { player.settings.musicPlayerMode = MusicPlayerMode.AUTO; const songIdForCurrentRegion = findSongIdByRegionId(activeWorld.chunkManager.getRegionIdForWorldPosition(player.position)); if (!songIdForCurrentRegion) { logger.warn(`No song found for current region`); return; } if (player.savedMetadata['currentSongIdPlaying'] !== songIdForCurrentRegion) { player.playSong(songIdForCurrentRegion); } } else if (buttonId === MusicTabButtonIds.MANUAL_BUTTON_ID) { player.settings.musicPlayerMode = MusicPlayerMode.MANUAL; } else if (buttonId === MusicTabButtonIds.LOOP_BUTTON_ID) { player.settings.musicPlayerLoopMode ^= 1; } const musicTrack = findMusicTrackByButtonId(buttonId); if (musicTrack === null) { return; } else if (player.musicTracks.includes(musicTrack.songId)) { player.playSong(musicTrack.songId); player.settings.musicPlayerMode = MusicPlayerMode.MANUAL; player.outgoingPackets.updateClientConfig(widgetScripts.musicPlayerAutoManual, 0); } else { player.sendMessage("You haven't unlocked this piece of music yet!"); } }; export default { pluginId: 'rs:music_tab', hooks: [{ type: 'button', widgetId: widgets.musicPlayerTab, handler }], }; ================================================ FILE: src/plugins/npcs/al-kharid/dommik-crafting-shop.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import { findShop } from '@engine/config/config-handler'; import { DialogueEmote, dialogueAction } from '@engine/world/actor/player/dialogue-action'; const tradeAction: npcInteractionActionHandler = ({ player }) => findShop('rs:dommiks_crafting_store')?.open(player); const talkToAction: npcInteractionActionHandler = details => { const { player, npc } = details; dialogueAction(player) .then(async d => d.npc(npc, DialogueEmote.CALM_TALK_1, ['Would you like to buy some crafting equipment?'])) .then(async d => d.options('Would you like to buy some crafting equipment?', [ "No thanks. I've got all the Crafting equipment I need.", "Let's see what you've got, then.", ]), ) .then(async d => { switch (d.action) { case 1: return d .player(DialogueEmote.JOYFUL, ["No thanks; I've got all the Crafting equipment I need."]) .then(async d => d.npc(npc, DialogueEmote.CALM_TALK_2, ['Okay. Fare well on your travels.'])) .then(d => { d.close(); return d; }); case 2: return d.player(DialogueEmote.CALM_TALK_1, ['No, thank you.']).then(d => { tradeAction(details); return d; }); } }); }; export default { pluginId: 'rs:dommik_crafting_shop', hooks: [ { type: 'npc_interaction', npcs: 'rs:alkharid_dommik', options: 'trade', walkTo: true, handler: tradeAction }, { type: 'npc_interaction', npcs: 'rs:alkharid_dommik', options: 'talk-to', walkTo: true, handler: talkToAction }, ], }; ================================================ FILE: src/plugins/npcs/al-kharid/gem-trader.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import { findShop } from '@engine/config/config-handler'; import { DialogueEmote, dialogueAction } from '@engine/world/actor/player/dialogue-action'; const tradeAction: npcInteractionActionHandler = ({ player }) => findShop('rs:alkharid_gem_trader')?.open(player); const talkToAction: npcInteractionActionHandler = details => { const { player, npc } = details; dialogueAction(player) .then(async d => d.npc(npc, DialogueEmote.CALM_TALK_1, ['Good day to you, traveller.', 'Would you be interested in buying some gems?']), ) .then(async d => d.options('Would you be interested in buying some gems?', ['Yes, please.', 'No, thank you.'])) .then(async d => { switch (d.action) { case 1: return d.player(DialogueEmote.JOYFUL, ['Yes, please!']).then(d => { tradeAction(details); return d; }); case 2: return d .player(DialogueEmote.CALM_TALK_1, ['No, thank you.']) .then(async d => d.npc(npc, DialogueEmote.ANNOYED, ['Eh, suit yourself.'])) .then(d => { d.close(); return d; }); } }); }; export default { pluginId: 'rs:gem_trader', hooks: [ { type: 'npc_interaction', npcs: 'rs:alkharid_gem_trader', options: 'trade', walkTo: true, handler: tradeAction }, { type: 'npc_interaction', npcs: 'rs:alkharid_gem_trader', options: 'talk-to', walkTo: true, handler: talkToAction }, ], }; ================================================ FILE: src/plugins/npcs/al-kharid/karim.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import { widgets } from '@engine/config/config-handler'; import { Emote, dialogue, execute } from '@engine/world/actor/dialogue'; import { itemIds } from '@engine/world/config/item-ids'; import { logger } from '@runejs/common'; const talkToAction: npcInteractionActionHandler = details => { const { player, npc } = details; dialogue( [player, { npc, key: 'karim' }], [ karim => [Emote.HAPPY, `Would you like to buy a nice kebab? Only one gold.`], options => [ `I think i'll give it a miss.`, [player => [Emote.DROWZY, `I think i'll give it a miss.`]], `Yes please.`, [ player => [Emote.HAPPY, `Yes please.`], execute(() => { const inventory = player.inventory; if (inventory.has(itemIds.coins)) { const index = inventory.findIndex(itemIds.coins); const item = inventory.items[index]; if (!inventory.hasSpace()) { player.sendMessage(`You don't have enough space in your inventory.`); return; } if (!item) { logger.warn(`Could not find item with id ${itemIds.coins} in player inventory. [Karim plugin]`); return; } inventory.remove(index); if (item.amount !== 1) { inventory.add({ itemId: itemIds.coins, amount: item.amount - 1 }); } inventory.add({ itemId: itemIds.kebab, amount: 1 }); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, inventory); return; } if (!inventory.has(itemIds.coins)) { dialogue( [player, { npc, key: 'karim' }], [ player => [Emote.ANGRY, `Oops, I forgot to bring any money with me.`], karim => [Emote.GENERIC, `Come back when you have some.`], ], ); } }), ], ], ], ); }; export default { pluginId: 'rs:karim', hooks: [{ type: 'npc_interaction', npcs: 'rs:alkharid_karim', options: 'talk-to', walkTo: true, handler: talkToAction }], }; ================================================ FILE: src/plugins/npcs/al-kharid/louie-armoured-legs.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import { findShop } from '@engine/config/config-handler'; const tradeAction: npcInteractionActionHandler = ({ player }) => findShop('rs:louies_armored_legs')?.open(player); export default { pluginId: 'rs:louie_armored_legs', hooks: [ { type: 'npc_interaction', npcs: 'rs:alkharid_louie', options: 'trade', walkTo: true, handler: tradeAction, }, ], }; ================================================ FILE: src/plugins/npcs/al-kharid/ranael-super-skirt.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import { findShop } from '@engine/config/config-handler'; const tradeAction: npcInteractionActionHandler = ({ player }) => findShop('rs:ranaels_skirt_store')?.open(player); export default { pluginId: 'rs:ranael_super_skirt', hooks: [ { type: 'npc_interaction', npcs: 'rs:alkharid_ranael', walkTo: true, options: 'trade', handler: tradeAction, }, ], }; ================================================ FILE: src/plugins/npcs/falador/custom-guards.plugin.ts ================================================ import type { npcInitActionHandler } from '@engine/action/pipe/npc-init.action'; import { findNpc } from '@engine/config/config-handler'; import { randomBetween } from '@engine/util/num'; import { activeWorld } from '@engine/world'; import type { Npc } from '@engine/world/actor/npc'; import { Position } from '@engine/world/position'; import { World } from '@engine/world/world'; const npcs = ['rs:guard:0', 'rs:guard:1']; const npcObjects = npcs.map(sNpc => findNpc(sNpc)); interface DialogueNpcTree { a?: string; b?: string; a_anim?: number; b_anim?: number; } const dialogueTrees: DialogueNpcTree[][] = [ [ { a: 'Hello sir!', a_anim: 863, }, { b: 'Hello solider.', b_anim: 863, }, { a: 'What is my purpose?', }, { b: 'You get pickpocketed and murdered for scrolls with clues.', }, { a_anim: 837, a: 'Oh my god.', }, { b: 'Yeah welcome to the club pal.', b_anim: 2113, }, ], [ { a: 'Um... So, Sir', }, { a_anim: 2836, a: 'That enhancment treatment we discussed...?', }, { b: `Well... It's kind of weird to be saying this, given the nature of my work,`, }, { b: `but I really think you should think twice about this enhancement therapy,`, }, { b: `It's all still pretty experimental.`, }, { a: `I've made up my mind about it, Sir. I gotta go big. Real big.`, }, { b: `You're young, and I have been around the block,`, }, { b: `so let me give a bit of advice for free: Size isn't everything.`, }, { a: `In my world it is. All the guys are getting it done.`, }, { b: `Really? I didn't know that. And they are not having any problems?`, }, { b: `I mean... performing their duties so to speak.`, }, { b_anim: 14, a: `On the contrary, Sir. They are real beasts, all of them.`, }, { a: `Always at it, day and night. Really competing to see who's the biggest.`, }, { b_anim: 404, b: `Wow. I mean... I don't judge, of course.`, }, { b: `I mean, this one time in military school. I mean, I was pretty drunk, of course...`, }, { a: `You're losing me, Sir. But can you get me those steroids or not?`, }, { a: `I don't wanna be the smallest guy in the room.`, }, { b_anim: 404, b: `Steroids? Oh, steroids! I thought you were talking about something else...`, }, { a: `Losing me again, Sir. What did you think I was talking about?`, }, { b: `Oh, nothing... I'll get you the damn steroids, don't worry.`, }, { a: `I mean, I thought I was pretty clear but... `, }, { a: `Well you seem pretty fixated on guys, though. But you know,`, }, { a: `that's just fine. This is a medieval world we live in, and as far as I'm concerned,`, }, { a: `who you love is entirely up to you.`, }, { b_anim: 856, b: `Shut up!`, }, ], ]; function startIdleDialogueTree(npc: Npc, closeNpc: Npc, dialogueTree: DialogueNpcTree[]) { if (npc.busy || closeNpc.busy) { return; } npc.busy = true; closeNpc.busy = true; npc.face(closeNpc); closeNpc.face(npc); setTimeout(() => doDialogue(npc, closeNpc, 0, dialogueTree), 3 * World.TICK_LENGTH); } function doDialogue(a: Npc, b: Npc, dialogueIndex: number, dialogueTree: DialogueNpcTree[]) { a.stopAnimation(); b.stopAnimation(); if (dialogueIndex > dialogueTree.length - 1 || !a.exists || !b.exists) { a.busy = false; b.busy = false; a.clearFaceActor(); b.clearFaceActor(); return; } const currentDialogue = dialogueTree[dialogueIndex]; if (currentDialogue.a) { a.say(currentDialogue.a); } if (currentDialogue.b) { b.say(currentDialogue.b); } if (currentDialogue.a_anim) { a.playAnimation(currentDialogue.a_anim); } if (currentDialogue.b_anim) { b.playAnimation(currentDialogue.b_anim); } setTimeout(() => doDialogue(a, b, dialogueIndex + 1, dialogueTree), 5 * World.TICK_LENGTH); } const npcIdleAction = (npc: Npc) => { if (Math.random() >= 0.14) { const currentLocation = new Position(npc.position); const closeNpcs = activeWorld.findNearbyNpcs(currentLocation, 4); for (const closeNpc of closeNpcs) { if (closeNpc === npc) { continue; } if (npcObjects.find(oNpc => oNpc.gameId === closeNpc.id)) { if (closeNpc.busy) { continue; } startIdleDialogueTree(npc, closeNpc, dialogueTrees[randomBetween(0, dialogueTrees.length - 1)]); return; } } } }; const guardInitAction: npcInitActionHandler = ({ npc }) => { // this used to use `setInterval` but will need rewriting to be synced with ticks // see https://github.com/runejs/server/issues/417 // setInterval(() => npcIdleAction(npc), (Math.floor(Math.random() * 20) + 10) * World.TICK_LENGTH); }; export default { pluginId: 'promises:custom_guards', hooks: [ { type: 'npc_init', npcs: npcs, handler: guardInitAction, }, ], }; ================================================ FILE: src/plugins/npcs/lumbridge/bob.plugin.ts ================================================ import type { NpcInteractionActionHook } from '@engine/action/pipe/npc-interaction.action'; import { findShop } from '@engine/config/config-handler'; import type { ContentPlugin } from '@engine/plugins/content-plugin'; const bobHook: NpcInteractionActionHook = { type: 'npc_interaction', npcs: 'rs:lumbridge_bob', options: 'trade', walkTo: true, handler: ({ player }) => findShop('rs:lumbridge_bobs_axes')?.open(player), }; const bobPlugin: ContentPlugin = { pluginId: 'rs:bob', hooks: [bobHook], }; export default bobPlugin; ================================================ FILE: src/plugins/npcs/lumbridge/hans.plugin.ts ================================================ import type { NpcInteractionActionHook } from '@engine/action/pipe/npc-interaction.action'; import { Emote, dialogue, execute, goto } from '@engine/world/actor/dialogue'; import { Achievements, giveAchievement } from '@engine/world/actor/player/achievements'; import { animationIds } from '@engine/world/config/animation-ids'; const handler = async ({ player, npc }) => { let sadEnding = false; const dialogueParticipants = [player, { npc, key: 'hans' }]; const dialogueTree = [ hans => [Emote.GENERIC, `Welcome to RuneJS!`], (hans, tag_Hans_Question) => [ Emote.HAPPY, `How do you feel about RuneJS so far?\n` + `Please take a moment to let us know what you think!`, ], options => [ `Love it!`, [ player => [Emote.HAPPY, `Loving it so far, thanks for asking!`], hans => [Emote.HAPPY, `You're very welcome! Glad to hear it.`], ], `Kind of cool.`, [ player => [Emote.GENERIC, `It's kind of cool, I guess. Bit of a weird gimmick.`], hans => [Emote.HAPPY, `Please let us know if you have any suggestions.`], ], `Not my cup of tea, honestly.`, [player => [Emote.SKEPTICAL, `Not really my cup of tea, but keep at it.`], hans => [Emote.GENERIC, `Thanks for the support!`]], `It's literally the worst.`, [ player => [Emote.ANGRY, `Literally the worst thing I've ever seen. You disgust me on a personal level.`], hans => [Emote.SAD, `I-is that so?... Well I'm... I'm sorry to hear that.`], execute(() => (sadEnding = true)), ], `What?`, [player => [Emote.DROWZY, `What?...`], goto('tag_Hans_Question')], ], ]; const dialogueSuccessful = await dialogue(dialogueParticipants, dialogueTree); npc.clearFaceActor(); player.clearFaceActor(); if (dialogueSuccessful) { if (sadEnding) { // @todo These appear to be broken, debug // npc.playAnimation(animationIds.cry); // npc.say(`Jerk!`); player.sendMessage(`Hans wanders off rather dejectedly.`); } else { player.sendMessage(`Hans wanders off aimlessly through the courtyard.`); } giveAchievement(Achievements.WELCOME, player); } }; export default { pluginId: 'rs:hans', hooks: [ { type: 'npc_interaction', npcs: 'rs:hans', options: 'talk-to', walkTo: true, handler, } as NpcInteractionActionHook, ], }; ================================================ FILE: src/plugins/npcs/lumbridge/lumbridge-farm-helpers.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import { Emote, dialogue, goto } from '@engine/world/actor/dialogue'; const millieDialogue: npcInteractionActionHandler = async details => dialogue( [details.player, { npc: details.npc, key: 'millie' }], [ millie => [Emote.GENERIC, `Hello Adventurer. Welcome to Mill Lane Mill. Can I help you?`], options => [ `Who are you?`, [ player => [Emote.WONDERING, `Who are you?`], millie => [ Emote.HAPPY, `I'm Miss Millicent Miller the Miller of Mill Lane Mill. ` + `Our family have been milling flour for generations.`, ], player => [Emote.GENERIC, `It's a good business to be in. People will always need flour.`], goto('tag_Mill_Flour'), ], `What is this place?`, [ player => [Emote.WONDERING, `What is this place?`], millie => [ Emote.HAPPY, `This is Mill Lane Mill. Millers of the finest flour in Gielinor, ` + `and home to the Miller family for many generations.`, ], millie => [Emote.GENERIC, `We take grain from the field nearby and mill into flour.`], goto('tag_Mill_Flour'), ], `How do I mill flour?`, [ (player, tag_Mill_Flour) => [Emote.WONDERING, `How do I mill flour?`], millie => [ Emote.GENERIC, `Making flour is pretty easy. First of all you need to get some grain. ` + `You can pick some from wheat fields. There is one just outside the Mill, but there are ` + `many others scattered across Gielinor.`, ], millie => [ Emote.GENERIC, `Feel free to pick wheat from our field! There always seems to be plenty ` + `of wheat there.`, ], player => [Emote.WONDERING, `Then I bring my wheat here?`], millie => [ Emote.GENERIC, `Yes, or one of the other mills in Gielinor. They all work the same way. ` + `Just take your grain to the top floor of the mill (up two ladders, there are three floors ` + `including this one) and then place some`, ], millie => [ Emote.GENERIC, `grain into the hopper. Then you need to start the grinding process by ` + `pulling the hopper lever. You can add more grain, but each time you add grain you have to ` + `pull the hopper lever again.`, ], player => [Emote.WONDERING, `So where does the flour go then?`], millie => [ Emote.GENERIC, `The flour appears in this room here, you'll need a pot to put the flour ` + `into. One pot will hold the flour made by one load of grain`, ], millie => [ Emote.GENERIC, `And that's it! You now have some pots of finely ground flour of the ` + `highest quality. Ideal for making tasty cakes or delicous bread. I'm not a cook so you'll ` + `have to ask a cook to find`, ], millie => [Emote.GENERIC, `out how to bake things.`], player => [Emote.HAPPY, `Great! Thanks for your help.`], ], `I'm fine, thanks.`, [player => [Emote.GENERIC, `I'm fine, thanks.`]], ], ], ); const gillieDialogue: npcInteractionActionHandler = async details => dialogue( [details.player, { npc: details.npc, key: 'gillie' }], [ gillie => [Emote.HAPPY, `Hello, I'm Gillie the Milkmaid. What can I do for you?`], options => [ `Who are you?`, [ player => [Emote.WONDERING, `Who are you?`], gillie => [Emote.GENERIC, `My name is Gillie Groats. My father is a farmer and I milk the cows for him.`], player => [Emote.WONDERING, `Do you have nay buckets of milk spare?`], gillie => [ Emote.GENERIC, `I'm afraid not. We need all of our milk to sell to market, ` + `but you can milk the cow yourself if you need the milk.`, ], player => [Emote.GENERIC, `Thanks.`], ], `So how do you milk a cow then?`, [ player => [Emote.WONDERING, `So how do you milk a cow then?`], gillie => [Emote.HAPPY, `It's very easy. First you need an empty bucket to hold the milk.`], gillie => [Emote.HAPPY, `Then find a dairy cow to milk - you can't milk just any cow.`], player => [Emote.SKEPTICAL, `How do I find a dairy cow?`], gillie => [ Emote.GENERIC, `They are easy to spot - they are dark brown and white, unlike ` + `beef cows, which are light brown and white. We also tether them to a post to stop them ` + `wandering around all over the place.`, ], gillie => [Emote.GENERIC, `There are a couple very near, in this field.`], gillie => [Emote.GENERIC, `Then just milk the cow and your bucket will fill with tasty, untritious milk.`], ], `I'm fine, thanks.`, [player => [Emote.GENERIC, `I'm fine, thanks.`]], ], ], ); export default { pluginId: 'rs:lumbridge_farm_helpers', hooks: [ { type: 'npc_interaction', npcs: 'rs:gillie_groats', options: 'talk-to', walkTo: true, handler: gillieDialogue, }, { type: 'npc_interaction', npcs: 'rs:millie_miller', options: 'talk-to', walkTo: true, handler: millieDialogue, }, ], }; ================================================ FILE: src/plugins/npcs/lumbridge/shopkeeper.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import { findShop } from '@engine/config/config-handler'; const action: npcInteractionActionHandler = ({ player }) => { findShop('rs:lumbridge_general_store')?.open(player); }; export default { pluginId: 'rs:lumbridge_general_store', hooks: [ { type: 'npc_interaction', npcs: 'rs:lumbridge_shop_keeper', options: 'trade', walkTo: true, handler: action, }, ], }; ================================================ FILE: src/plugins/npcs/port-sarim/betty.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import { findShop } from '@engine/config/config-handler'; import { Emote, dialogue, execute } from '@engine/world/actor/dialogue'; const shopAction: npcInteractionActionHandler = details => findShop('rs:bettys_magic_emporium')?.open(details.player); const dialogueAction: npcInteractionActionHandler = details => { const { player, npc } = details; let openShop = false; dialogue( [details.player, { npc: details.npc, key: 'betty' }], [ betty => [Emote.HAPPY, `Welcome to the magic emporium.`], options => [ `Can I see your wares?`, [ player => [Emote.HAPPY, `Can I see your wares?`], execute(() => { openShop = true; }), ], `Sorry I'm not into magic.`, [ player => [Emote.GENERIC, `Sorry I'm not into magic.`], betty => [Emote.HAPPY, `Well, if you see anyone who is into magic, please send them my way.`], ], ], ], ); if (openShop) { shopAction(details); } }; export default { pluginId: 'rs:betty_shop', hooks: [ { type: 'npc_interaction', npcs: 'rs:betty', options: 'trade', walkTo: true, handler: shopAction, }, { type: 'npc_interaction', npcs: 'rs:betty', options: 'talk-to', walkTo: true, handler: dialogueAction, }, ], }; ================================================ FILE: src/plugins/npcs/varrock/blue-moon-inn.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import { widgets } from '@engine/config/config-handler'; import { Emote, dialogue, execute } from '@engine/world/actor/dialogue'; import { itemIds } from '@engine/world/config/item-ids'; const talkToBartender: npcInteractionActionHandler = details => { const { player, npc } = details; dialogue( [player, { npc, key: 'bartender' }], [ bartender => [Emote.HAPPY, 'What can I do yer for?'], options => [ `A glass of your finest ale please.`, [ player => [Emote.HAPPY, `A glass of your finest ale please.`], bartender => [Emote.HAPPY, `No problemo. That'll be 2 coins.`], execute(() => { const index = player.inventory.findIndex(itemIds.coins); const hasCoins = player.inventory.has(itemIds.coins); if (!hasCoins) { dialogue( [player, { npc, key: 'bartender' }], [player => [Emote.VERY_SAD, `Oh dear. I don't seem to have enough money.`]], ); } if (hasCoins && player.inventory.amountInStack(index) >= 2) { const amount = player.inventory.amountInStack(index); // Check inventory. if (!player.inventory.hasSpace()) { player.sendMessage(`You don't have enough space in your inventory.`); return; } // Take the coins player.inventory.remove(index); if (amount - 2 !== 0) { player.inventory.add({ itemId: itemIds.coins, amount: amount - 2, }); } // Give the beer. player.inventory.add(itemIds.beer); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory); } }), ], `Can you recommend where an adventurer might make his fortune?`, [ player => [Emote.WONDERING, `Can you recommend where an adventurer might make his fortune?`], bartender => [Emote.LAUGH, `Ooh I don't know if I should be giving away information, makes the game too easy.`], options => [ `Oh ah well...`, [player => [Emote.WORRIED, `Oh ah well...`]], `Game? What are you talking about?`, [ player => [Emote.WORRIED, `Game? What are you talking about?`], bartender => [Emote.GENERIC, `This world around us... is an online game... called Old School RuneScape.`], player => [Emote.GENERIC, `Nope, still don't understand what you are talking about. What does 'online' mean?`], bartender => [ Emote.GENERIC, `It's a sort of connection between magic boxes across the world, big boxes on people's desktops and little ones people can carry. They can talk to each other to play games.`, ], player => [Emote.GENERIC, `I give up. You're obviously completely mad!`], ], `Just a small clue?`, [ player => [Emote.WONDERING, `Just a small clue?`], bartender => [ Emote.VERY_SAD, `Go and talk to the bartender at the Jolly Boar Inn, he doesn't seem to mind giving away clues.`, ], ], ], ], `Do you know where I can get some good equipment?`, [ player => [Emote.WONDERING, `Do you know where I can get some good equipment?`], bartender => [ Emote.HAPPY, `Well, there's the sword shop across the road, or there's also all sorts of shops up around the market.`, ], ], ], ], ); }; const talkToCook: npcInteractionActionHandler = details => { const { npc, player } = details; dialogue( [player, { npc, key: 'cook' }], [ cook => [Emote.ANGRY, `What do you want? I'm busy!`], options => [ `Can you sell me any food?`, [ player => [Emote.WONDERING, `Can you sell me any food?`], cook => [ Emote.GENERIC, `I suppose I could sell you some cabbage, if you're willing to pay for it. Cabbage is good for you.`, ], execute(() => { const hasCoins = player.inventory.has(itemIds.coins); const index = player.inventory.findIndex(itemIds.coins); // The player doesn't have any coins. if (!hasCoins) { dialogue( [player, { npc, key: 'cook' }], [ player => [Emote.VERY_SAD, `Oh, I haven't got any money.`], cook => [Emote.ANGRY, `Why are you asking me to sell you food if you haven't got any money? Go away!`], ], ); } // The player has enough coins if (hasCoins && player.inventory.amountInStack(index) >= 2) { const amount = player.inventory.amountInStack(index); dialogue( [player, { npc, key: 'cook' }], [ options => [ `Alright I'll buy a cabbage.`, [ player => [Emote.HAPPY, `Alright I'll buy a cabbage.`], execute(() => { // Check inventory. if (!player.inventory.hasSpace()) { player.sendMessage(`You don't have enough space in your inventory.`); return; } // Take the coins player.inventory.remove(index); if (amount - 2 !== 0) { player.inventory.add({ itemId: itemIds.coins, amount: amount - 2, }); } // Give the cabbage. player.inventory.add(itemIds.cabbage); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory); }), cook => [ Emote.HAPPY, `It's a deal. Now, make sure you eat it all up. Cabbage is good for you.`, ], ], `No thanks, I don't like cabbage.`, [ player => [Emote.GENERIC, `No thanks, I don't like cabbage.`], cook => [Emote.SAD, `Bah! People these days only appreciate junk food.`], ], ], ], ); } }), ], `Can you give any free food?`, [ // PLAYER: Can you give any free food? player => [Emote.GENERIC, `Can you give any free food?`], cook => [Emote.GENERIC, `Can you give my any free money?`], player => [Emote.GENERIC, `Why should I give you free money?`], cook => [Emote.GENERIC, `Why should I give you free food?`], player => [Emote.GENERIC, `Oh, forget it.`], ], `I don't want anything from this horrible kitchen.`, [ player => [Emote.SHOCKED, `I don't want anything from this horrible kitchen.`], cook => [Emote.ANGRY, `How dare you? I put a lot of effort into cleaning this kitchen.`], cook => [Emote.ANGRY, `My daily sweat and elbow-grease keep this kitchen clean!`], player => [Emote.GENERIC, `Ewww!`], cook => [Emote.SAD, `Oh, just leave me alone.`], ], ], ], ); }; export default { pluginId: 'rs:blue_moon_inn_dialogue', hooks: [ { type: 'npc_interaction', npcs: 'rs:blue_moon_innk_bartender', handler: talkToBartender, }, { type: 'npc_interaction', npcs: 'rs:blue_moon_inn_cook', handler: talkToCook, }, ], }; ================================================ FILE: src/plugins/npcs/varrock/master-smithing-tutor.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import { widgets } from '@engine/config/config-handler'; import { Emote, dialogue, execute, goto } from '@engine/world/actor/dialogue'; import { itemIds } from '@engine/world/config/item-ids'; const talkTo: npcInteractionActionHandler = details => { const { player, npc } = details; dialogue( [player, { npc, key: 'tutor' }], [ player => [Emote.GENERIC, `Hello.`], tutor => [Emote.GENERIC, `Well met! Are you interested in hearing about the art of smithing?`], options => [ `How can I train my smithing?`, [ (player, tag_how_to_train) => [Emote.WONDERING, `How can I train my smithing?`], tutor => [Emote.GENERIC, `To be able to smith anything, you're going to need one of these beauties.`], execute(() => { player.inventory.add(itemIds.hammer); player.sendMessage('The Master Smithing Tutor gives you a hammer.', true); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory); }), tutor => [Emote.GENERIC, `You're going to get your hand on some metal bars.`], tutor => [Emote.GENERIC, `You could do this by mining your own ores and smelting them at a furnace.`], tutor => [Emote.GENERIC, `There is a furnace in Lumbridge, just north of the castle opposite the general store.`], tutor => [Emote.GENERIC, `If you are looking for some ore, there is a mine east of Varrock.`], tutor => [Emote.GENERIC, `There you can find some copper and tin ore. Don't forget to bring a pickaxe.`], tutor => [Emote.GENERIC, `When you have your bars, bring them to an anvil to open the smithing interface.`], tutor => [Emote.GENERIC, `If the item name is in black, this means you do not have the level to smith the item.`], tutor => [Emote.GENERIC, `If the name is in white, this means you have the level to smith the item.`], tutor => [Emote.GENERIC, `You will see the bars required to smith the item underneath the name of the item.`], tutor => [ Emote.GENERIC, `If the number of bars is in orange, this means that you do not have enough bars to smith the item.`, ], tutor => [Emote.GENERIC, `If it is in green, this means you have enough bars for the item.`], player => [Emote.GENERIC, `Thanks for the advice.`], options => [ `What kinds of things can I smith?`, [goto('tag_what_kinds')], `Not right now, thank you.`, [goto('tag_no_thanks')], ], ], `What kinds of things can I smith?`, [ (player, tag_what_kinds) => [Emote.WONDERING, `What kinds of things can I smith?`], tutor => [Emote.GENERIC, `There are many things you can make, from weapons to your good old fashioned armour.`], tutor => [ Emote.GENERIC, `Weapons are the cheapest things to smith. They range from a measly one bar, all the way to three bars.`, ], tutor => [ Emote.GENERIC, `Armour can be the costliest item to smith, the cost of each item ranges from a measly one bar all the way up to a whopping five bars.`, ], tutor => [ Emote.GENERIC, `Some weapons and armours, such as darts, will require you to have gained knowledge on how to smith them.`, ], tutor => [Emote.GENERIC, `This is due to the complex nature of the weapon.`], tutor => [Emote.GENERIC, `You might find other items don't require conventional bars you would gather.`], tutor => [ Emote.GENERIC, `Some may require you to piece back together or even infuse a crystal into a piece of armour.`, ], tutor => [Emote.GENERIC, `Is there anything else you want to know?`], options => [`How can i train my smithing?`, [goto('tag_how_to_train')], `No, thank you.`, [goto('tag_no_thanks')]], ], `Not right now, thank you.`, [ (player, tag_no_thanks) => [Emote.GENERIC, `Not right now, thank you.`], tutor => [Emote.GENERIC, `Well, just come back any time you want to know anything!`], ], ], ], ); }; export default { pluginId: 'rs:master_smithing_tutor', hooks: [ { type: 'npc_interaction', npcs: 'rs:master_smithing_tutor', options: ['talk-to'], walkTo: true, handler: talkTo, }, ], }; ================================================ FILE: src/plugins/npcs/varrock/wilough.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import { findNpc } from '@engine/config/config-handler'; import { Emote, dialogue } from '@engine/world/actor/dialogue'; const talkTo: npcInteractionActionHandler = details => { const { player, npc } = details; const shilop = findNpc('rs:varrock_shilop'); dialogue( [player, { npc, key: 'wilough' }, { npc: shilop.gameId, key: 'shilop' }], [ player => [Emote.GENERIC, `Hello again.`], wilough => [Emote.GENERIC, `You think you're tough do you?`], player => [Emote.GENERIC, `Pardon?`], wilough => [Emote.ANGRY, `I can beat anyone up!`], shilop => [Emote.BLANK_STARE, `He can you know!`], player => [Emote.BLANK_STARE, `Really?`], ], ); }; export default { pluginId: 'rs:varrock_wilough_dialogue', hooks: [ { npcs: 'rs:varrock_wilough', type: 'npc_interaction', options: ['talk-to'], walkTo: true, handler: talkTo, }, ], }; ================================================ FILE: src/plugins/npcs/varrock/zaff-superior-staffs.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import { findShop } from '@engine/config/config-handler'; import { Emote, dialogue, execute } from '@engine/world/actor/dialogue'; const tradeAction: npcInteractionActionHandler = ({ player }) => findShop('rs:zaffs_superior_staffs')?.open(player); const talkToAction: npcInteractionActionHandler = details => { const { player, npc } = details; dialogue( [player, { npc, key: 'zaff' }], [ zaff => [Emote.GENERIC, `Would you like to buy or sell some staffs?`], options => [ `Yes, please!`, [ execute(() => { tradeAction(details); }), ], 'Have you any extra stock of battlestaffs I can buy?', [ player => [Emote.GENERIC, 'Have you any extra stock of battlestaffs I can buy?'], zaff => [Emote.WONDERING, "No, I'm afraid I can't help you."], execute(() => { player.sendMessage( "You must complete the Varrock Achievement Diary before you can access Zaff's extra battlestaff stock.", ); }), ], 'No, thank you.', [ player => [Emote.GENERIC, 'No, thank you.'], zaff => [Emote.GENERIC, "Well 'stick' your head in if you change your mind."], player => [Emote.GENERIC, "Huh, terrible pun! You just can't get the 'staff' these days!"], ], ], ], ); }; export default { pluginId: 'rs:zaffs_staffs', hooks: [ { type: 'npc_interaction', npcs: 'rs:varrock_zaff', options: 'trade', walkTo: true, handler: tradeAction, }, { type: 'npc_interaction', npcs: 'rs:varrock_zaff', options: 'talk-to', walkTo: true, handler: talkToAction, }, ], }; ================================================ FILE: src/plugins/objects/bank/bank.plugin.ts ================================================ import type { buttonActionHandler } from '@engine/action/pipe/button.action'; import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { widgets } from '@engine/config/config-handler'; import { Emote, dialogue, execute } from '@engine/world/actor/dialogue'; import type { Player } from '@engine/world/actor/player/player'; import { objectIds } from '@engine/world/config/object-ids'; import { widgetScripts } from '@engine/world/config/widget'; import type { Item } from '@engine/world/items/item'; import { fromNote, toNote } from '@engine/world/items/item'; import type { ItemContainer } from '@engine/world/items/item-container'; import { logger } from '@runejs/common'; const buttonIds: number[] = [ 92, // as note 93, // as item 98, // swap 99, // insert ]; export const openBankInterface: objectInteractionActionHandler = ({ player }) => { player.interfaceState.openWidget(widgets.bank.screenWidget.widgetId, { slot: 'screen', multi: true, }); player.interfaceState.openWidget(widgets.bank.tabWidget.widgetId, { slot: 'tabarea', multi: true, }); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.tabWidget, player.inventory); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.screenWidget, player.bank); player.outgoingPackets.updateClientConfig(widgetScripts.bankInsertMode, player.settings.bankInsertMode); player.outgoingPackets.updateClientConfig(widgetScripts.bankWithdrawNoteMode, player.settings.bankWithdrawNoteMode); }; export const openPinSettings: objectInteractionActionHandler = ({ player }) => { player.interfaceState.openWidget(widgets.bank.pinSettingsWidget.widgetId, { slot: 'screen', }); }; export const depositItem: itemInteractionActionHandler = details => { // Check if player might be spawning widget client-side if (!details.player.interfaceState.findWidget(widgets.bank.screenWidget.widgetId)) { return; } // Check if the player has the item if (!details.player.hasItemInInventory(details.itemId)) { return; } let itemIdToAdd: number = details.itemId; const fromNoteId: number = fromNote(details.itemId); if (fromNoteId > -1) { itemIdToAdd = fromNoteId; } let countToRemove: number; switch (details.option) { case 'deposit-1': // Deposit 1 countToRemove = 1; break; case 'deposit-5': // Deposit 5 countToRemove = 5; break; case 'deposit-10': // Deposit 10 countToRemove = 10; break; case 'deposit-all': // Deposit all countToRemove = -1; break; default: // Should never happen throw new Error('Unhandled option in banking plugin: ' + details.option); } const playerInventory = details.player.inventory; const playerBank = details.player.bank; const slotsWithItem = playerInventory.findAll(details.itemId); let itemAmount = 0; slotsWithItem.forEach(slot => { const item = playerInventory.items[slot]; if (!item) { throw new Error(`Container item was not present, for item id ${details.itemId} in inventory, while trying to deposit`); } if (item.itemId !== details.itemId) { throw new Error(`Container item id mismatch, for item id ${details.itemId} in inventory, while trying to deposit`); } itemAmount += item.amount; }); if (countToRemove === -1 || countToRemove > itemAmount) { countToRemove = itemAmount; } if (!playerBank.canFit({ itemId: itemIdToAdd, amount: countToRemove }, true)) { details.player.sendMessage('Your bank is full.'); return; } const itemToAdd: Item = { itemId: itemIdToAdd, amount: removeFromContainer(playerInventory, details.itemId, countToRemove), }; playerBank.addStacking(itemToAdd); updateBankingInterface(details.player); }; export const withdrawItem: itemInteractionActionHandler = details => { // Check if player might be spawning widget client-side if (!details.player.interfaceState.findWidget(widgets.bank.screenWidget.widgetId)) { return; } // Check if the player has the item if (!details.player.hasItemInBank(details.itemId)) { return; } let itemIdToAdd: number = details.itemId; let stackable: boolean = details.itemDetails.stackable; if (details.player.settings.bankWithdrawNoteMode) { const toNoteId: number = toNote(details.itemId); if (toNoteId > -1) { itemIdToAdd = toNoteId; stackable = true; } else { details.player.sendMessage('This item can not be withdrawn as a note.'); } } let countToRemove: number; switch (details.option) { case 'withdraw-1': // Withdraw 1 countToRemove = 1; break; case 'withdraw-5': // Withdraw 5 countToRemove = 5; break; case 'withdraw-10': // Withdraw 10 countToRemove = 10; break; case 'withdraw-all': // Withdraw all countToRemove = -1; break; default: // Should never happen throw new Error('Unhandled option in banking plugin: ' + details.option); } const playerBank = details.player.bank; const playerInventory = details.player.inventory; const slotWithItem = playerBank.findIndex(details.itemId); const itemInBank = playerBank.items[slotWithItem]; if (!itemInBank) { logger.error(`Container item was not present, for item id ${details.itemId} in bank, while trying to withdraw`); return; } const itemAmount = itemInBank.amount; if (countToRemove === -1 || countToRemove > itemAmount) { countToRemove = itemAmount; } if (!stackable) { const slots = playerInventory.getOpenSlotCount(); if (slots < countToRemove) { countToRemove = slots; } } if (!playerInventory.canFit({ itemId: itemIdToAdd, amount: countToRemove }) || countToRemove === 0) { details.player.sendMessage('Your inventory is full.'); return; } const itemToAdd: Item = { itemId: itemIdToAdd, amount: removeFromContainer(playerBank, details.itemId, countToRemove), }; if (stackable) { playerInventory.add({ itemId: itemToAdd.itemId, amount: itemToAdd.amount }); } else { for (let count = 0; count < itemToAdd.amount; count++) { playerInventory.add({ itemId: itemToAdd.itemId, amount: 1 }); } } updateBankingInterface(details.player); }; export const updateBankingInterface = (player: Player) => { player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.tabWidget, player.inventory); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.screenWidget, player.bank); }; /** * Removes an item from a container (e.g. bank or inventory) and returns the amount of items it removed. * @param from - The container to remove from * @param itemId - The item to remove * @param amount - The amount to remove * @returns The amount of items it removed */ export const removeFromContainer = (from: ItemContainer, itemId: number, amount: number) => { let resultingAmount = 0; let removeAmount = amount; while (removeAmount > 0 && from.has(itemId)) { const containerIndex = from.findIndex(itemId); const containerItem = from.items[containerIndex]; if (!containerItem) { throw new Error(`Container item was not present, for item id ${itemId} in bank, while trying to remove`); } if (removeAmount >= containerItem.amount) { resultingAmount += containerItem.amount; removeAmount -= containerItem.amount; from.remove(containerIndex); } else { resultingAmount += removeAmount; containerItem.amount -= removeAmount; removeAmount = 0; } } return resultingAmount; }; export const btnAction: buttonActionHandler = details => { const { player, buttonId } = details; player.settingChanged(buttonId); const settingsMappings = { 92: { setting: 'bankWithdrawNoteMode', value: 1 }, 93: { setting: 'bankWithdrawNoteMode', value: 0 }, 98: { setting: 'bankInsertMode', value: 0 }, 99: { setting: 'bankInsertMode', value: 1 }, }; if (!settingsMappings[buttonId]) { return; } const config = settingsMappings[buttonId]; player.settings[config.setting] = config.value; }; const useBankBoothAction: objectInteractionActionHandler = async details => { const { player } = details; let openBank = false; let openPin = false; await dialogue( [player, { npc: 'rs:generic_banker', key: 'banker' }], [ banker => [Emote.HAPPY, `Good day, how can I help you?`], options => [ `I'd Like to access my bank account, please.`, [ execute(() => { openBank = true; }), ], `I'd like to check my PIN settings.`, [ execute(() => { openPin = true; }), ], `What is this place?`, [ player => [Emote.WONDERING, `What is this place?`], banker => [Emote.HAPPY, `This is a branch of the Bank of Gielinor. We have branches in many towns.`], player => [Emote.WONDERING, `And what do you do?`], banker => [Emote.GENERIC, `We will look after your items and money for you.`], banker => [Emote.GENERIC, `Leave your valuables with us if you want to keep them safe.`], ], ], ], ); if (openBank) { openBankInterface(details as any); } else if (openPin) { openPinSettings(details); } }; export default { pluginId: 'rs:banking', hooks: [ { type: 'object_interaction', objectIds: objectIds.bankBooth, options: ['use'], walkTo: true, handler: useBankBoothAction, }, { type: 'object_interaction', objectIds: objectIds.bankBooth, options: ['use-quickly'], walkTo: true, handler: openBankInterface, }, { type: 'item_interaction', widgets: widgets.bank.tabWidget, options: ['deposit-1', 'deposit-5', 'deposit-10', 'deposit-all'], handler: depositItem, }, { type: 'item_interaction', widgets: widgets.bank.screenWidget, options: ['withdraw-1', 'withdraw-5', 'withdraw-10', 'withdraw-all'], handler: withdrawItem, }, { type: 'button', widgetId: widgets.bank.screenWidget.widgetId, buttonIds: buttonIds, handler: btnAction, }, ], }; ================================================ FILE: src/plugins/objects/bank/deposit-box.plugin.ts ================================================ import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { widgets } from '@engine/config/config-handler'; import { objectIds } from '@engine/world/config/object-ids'; import type { Item } from '@engine/world/items/item'; import { fromNote } from '@engine/world/items/item'; export const openDepositBoxInterface: objectInteractionActionHandler = ({ player }) => { player.interfaceState.openWidget(widgets.bank.depositBoxWidget.widgetId, { slot: 'screen', multi: true, }); player.interfaceState.openWidget(widgets.disabledTab, { slot: 'tabarea', multi: true, }); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.depositBoxWidget, player.inventory); }; export const depositItem: itemInteractionActionHandler = details => { // Check if player might be spawning widget clientside if (!details.player.interfaceState.findWidget(widgets.bank.depositBoxWidget.widgetId)) { return; } // Check if the player has the item if (!details.player.hasItemInInventory(details.itemId)) { return; } // If the item is a noted item, we need to de-note it let itemIdToAdd: number = details.itemId; const fromNoteId: number = fromNote(details.itemId); if (fromNoteId > -1) { itemIdToAdd = fromNoteId; } let countToRemove: number; if (details.option.endsWith('all')) { countToRemove = -1; } else { countToRemove = +details.option.replace('deposit-', ''); } const playerInventory = details.player.inventory; const playerBank = details.player.bank; const slotsWithItem = playerInventory.findAll(details.itemId); let itemAmount: number = 0; slotsWithItem.forEach(slot => { const item = playerInventory.items[slot]; if (!item) { throw new Error(`Container item was not present, for item id ${details.itemId} in inventory, while trying to deposit`); } if (item.itemId !== details.itemId) { throw new Error(`Container item id mismatch, for item id ${details.itemId} in inventory, while trying to deposit`); } itemAmount += item.amount; }); if (countToRemove == -1 || countToRemove > itemAmount) { countToRemove = itemAmount; } if (!playerBank.canFit({ itemId: itemIdToAdd, amount: countToRemove }, true)) { details.player.sendMessage('Your bank is full.'); return; } const itemToAdd: Item = { itemId: itemIdToAdd, amount: 0 }; while (countToRemove > 0 && playerInventory.has(details.itemId)) { const invIndex = playerInventory.findIndex(details.itemId); const invItem = playerInventory.items[invIndex]; if (!invItem) { throw new Error(`Inventory item was not present, for item id ${details.itemId} in bank, while trying to deposit`); } if (countToRemove >= invItem.amount) { itemToAdd.amount += invItem.amount; countToRemove -= invItem.amount; playerInventory.remove(invIndex); } else { itemToAdd.amount += countToRemove; invItem.amount -= countToRemove; countToRemove = 0; } } playerBank.addStacking(itemToAdd); details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, details.player.inventory); details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.bank.depositBoxWidget, details.player.inventory); }; export default { pluginId: 'rs:bank_deposit_box', hooks: [ { type: 'object_interaction', objectIds: objectIds.depositBox, options: ['deposit'], walkTo: true, handler: openDepositBoxInterface, }, { type: 'item_interaction', widgets: widgets.bank.depositBoxWidget, options: ['deposit-1', 'deposit-5', 'deposit-10', 'deposit-all'], handler: depositItem, }, ], }; ================================================ FILE: src/plugins/objects/cows/cow.plugin.ts ================================================ import type { itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action'; import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { findItem, findNpc } from '@engine/config/config-handler'; import { DialogueEmote, dialogueAction } from '@engine/world/actor/player/dialogue-action'; import type { Player } from '@engine/world/actor/player/player'; import { animationIds } from '@engine/world/config/animation-ids'; import { itemIds } from '@engine/world/config/item-ids'; import { objectIds } from '@engine/world/config/object-ids'; import { soundIds } from '@engine/world/config/sound-ids'; import type { ObjectConfig } from '@runejs/filestore'; function milkCow(details: { objectConfig: ObjectConfig; player: Player }): void { const { player, objectConfig } = details; const emptyBucketItem = findItem(itemIds.bucket); // TODO: `findItem` should probably throw this error internally. if (emptyBucketItem === null) { throw new Error('Failed to milk cow as no item matching bucket was found.'); } if (player.hasItemInInventory(itemIds.bucket)) { player.playAnimation(animationIds.milkCow); player.playSound(soundIds.milkCow, 7); player.removeFirstItem(itemIds.bucket); player.giveItem(itemIds.bucketOfMilk); player.sendMessage(`You milk the ${objectConfig.name} and receive some milk.`); } else { const gilleGroats = findNpc('rs:gillie_groats'); const gillieId = gilleGroats.gameId; dialogueAction(player) .then(async d => d.npc(gillieId, DialogueEmote.LAUGH_1, [`Tee hee! You've never milked a cow before, have you?`])) .then(async d => d.player(DialogueEmote.CALM_TALK_1, ['Erm... No. How could you tell?'])) .then(async d => d.npc(gillieId, DialogueEmote.LAUGH_2, [ `Because you're spilling milk all over the floor. What a`, 'waste! You need something to hold the milk.', ]), ) .then(async d => d.player(DialogueEmote.CONSIDERING, [`Ah yes, I really should have guessed that one, shouldn't`, 'I?'])) .then(async d => d.npc(gillieId, DialogueEmote.LAUGH_2, [ `You're from the city aren't you... Try it again with a`, `${emptyBucketItem.name.toLowerCase()}.`, ]), ) .then(async d => d.player(DialogueEmote.CALM_TALK_2, [`Right, I'll do that.`])) .then(d => { d.close(); }); } } export const actionItem: itemOnObjectActionHandler = details => milkCow(details); export const actionInteract: objectInteractionActionHandler = details => milkCow(details); export default { pluginId: 'rs:cow_milking', hooks: [ { type: 'object_interaction', objectIds: objectIds.milkableCow, options: 'milk', walkTo: true, handler: actionInteract, }, { type: 'item_on_object', objectIds: objectIds.milkableCow, itemIds: itemIds.bucket, walkTo: true, handler: actionItem, }, ], }; ================================================ FILE: src/plugins/objects/crates/crates.plugin.ts ================================================ import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { findItem, widgets } from '@engine/config/config-handler'; import { itemIds } from '@engine/world/config/item-ids'; export const action: objectInteractionActionHandler = details => { const veggies = [itemIds.onion, itemIds.grain, itemIds.cabbage]; details.player.busy = true; details.player.playAnimation(827); const random = Math.floor(Math.random() * veggies.length); const pickedItem = findItem(veggies[random])!; details.player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, details.player.inventory); // this used to use `setInterval` but will need rewriting to be synced with ticks // see https://github.com/runejs/server/issues/417 details.player.sendMessage('[debug] see issue #417'); // setTimeout(() => { // details.player.sendMessage(`You found a ${pickedItem.name.toLowerCase()} chest!.`); // details.player.playSound(2581, 7); // details.player.instance.hideGameObjectTemporarily(details.object, 60); // details.player.giveItem(pickedItem.gameId); // details.player.busy = false; // }, World.TICK_LENGTH); }; export default { pluginId: 'rs:crates', hooks: [ { type: 'object_interaction', objectIds: [366, 357, 355], options: ['loot', 'search', 'examine'], walkTo: true, handler: action, }, ], }; ================================================ FILE: src/plugins/objects/doors/door.plugin.ts ================================================ import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { soundIds } from '@engine/world/config/sound-ids'; import { WNES, directionData } from '@engine/world/direction'; import type { LandscapeObject } from '@runejs/filestore'; // @TODO move to yaml config const doors = [ { closed: 1530, open: 1531, hinge: 'RIGHT', }, { closed: 11707, open: 11708, hinge: 'RIGHT', }, { closed: 1533, open: 1534, hinge: 'RIGHT', }, { closed: 1516, open: 1517, hinge: 'LEFT', }, { closed: 1519, open: 1520, hinge: 'RIGHT', }, { closed: 1536, open: 1537, hinge: 'LEFT', }, { closed: 11993, open: 11994, hinge: 'RIGHT', }, { closed: 13001, open: 13002, hinge: 'RIGHT', }, ]; const leftHingeDir: { [key: string]: string } = { NORTH: 'WEST', SOUTH: 'EAST', WEST: 'SOUTH', EAST: 'NORTH', }; const rightHingeDir: { [key: string]: string } = { NORTH: 'EAST', SOUTH: 'WEST', WEST: 'NORTH', EAST: 'SOUTH', }; export const action: objectInteractionActionHandler = ({ player, object: door, position, cacheOriginal }): void => { let opening = true; let doorConfig = doors.find(d => d.closed === door.objectId); let hingeConfig; let replacementDoorId: number; if (!doorConfig) { doorConfig = doors.find(d => d.open === door.objectId); if (!doorConfig) { return; } opening = false; hingeConfig = doorConfig.hinge === 'LEFT' ? rightHingeDir : leftHingeDir; replacementDoorId = doorConfig.closed; } else { hingeConfig = doorConfig.hinge === 'LEFT' ? leftHingeDir : rightHingeDir; replacementDoorId = doorConfig.open; } const startDir = WNES[door.orientation]; const endDir = hingeConfig[startDir]; const endPosition = position.step(opening ? 1 : -1, opening ? startDir : endDir); const replacementDoor: LandscapeObject = { objectId: replacementDoorId, x: endPosition.x, y: endPosition.y, level: position.level, type: door.type, orientation: directionData[endDir].rotation, }; player.instance.toggleGameObjects(replacementDoor, door, !cacheOriginal); // 70 = close gate, 71 = open gate, 62 = open door, 60 = close door player.playSound(opening ? soundIds.openDoor : soundIds.closeDoor, 7); }; export default { pluginId: 'rs:standard_doors', hooks: [ { type: 'object_interaction', objectIds: [1530, 4465, 4467, 3014, 3017, 3018, 3019, 1536, 1537, 1533, 1531, 1534, 12348, 11993, 11994, 13001, 13002], options: ['open', 'close'], walkTo: true, handler: action, }, ], }; ================================================ FILE: src/plugins/objects/doors/double-door.plugin.ts ================================================ import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { activeWorld } from '@engine/world'; import { WNES } from '@engine/world/direction'; import { Position } from '@engine/world/position'; import { action as doorAction } from '@plugins/objects/doors/door.plugin'; import { logger } from '@runejs/common'; const doubleDoors = [ { closed: [1516, 1519], open: [1517, 1520], }, ]; const closingDelta = { WEST: { x: 1, y: 0 }, EAST: { x: -1, y: 0 }, NORTH: { x: 0, y: -1 }, SOUTH: { x: 0, y: 1 }, }; const openingDelta = { LEFT: { WEST: { x: 0, y: 1 }, EAST: { x: 0, y: -1 }, NORTH: { x: 1, y: 0 }, SOUTH: { x: -1, y: 0 }, }, RIGHT: { WEST: { x: 0, y: -1 }, EAST: { x: 0, y: 1 }, NORTH: { x: -1, y: 0 }, SOUTH: { x: 1, y: 0 }, }, }; const action: objectInteractionActionHandler = details => { const { player, object: door, position, cacheOriginal } = details; let doorConfig = doubleDoors.find(d => d.closed.indexOf(door.objectId) !== -1); let doorIds: number[]; let opening = true; if (!doorConfig) { doorConfig = doubleDoors.find(d => d.open.indexOf(door.objectId) !== -1); if (!doorConfig) { return; } opening = false; doorIds = doorConfig.open; } else { doorIds = doorConfig.closed; } const leftDoorId = doorIds[0]; const rightDoorId = doorIds[1]; const hinge = leftDoorId === door.objectId ? 'LEFT' : 'RIGHT'; const direction = WNES[door.orientation]; let deltaX = 0; let deltaY = 0; const otherDoorId = hinge === 'LEFT' ? rightDoorId : leftDoorId; if (!opening) { deltaX += closingDelta[direction].x; deltaY += closingDelta[direction].y; } else { deltaX += openingDelta[hinge][direction].x; deltaY += openingDelta[hinge][direction].y; } if (!otherDoorId || (deltaX === 0 && deltaY === 0)) { logger.error('Improperly handled double door at ' + door.x + ',' + door.y + ',' + door.level); return; } const otherDoorPosition = new Position(door.x + deltaX, door.y + deltaY, door.level); const { object: otherDoor } = activeWorld.findObjectAtLocation(player, otherDoorId, otherDoorPosition); if (!otherDoor) { return; } // TODO (Jameskmonger) fix the 'as any' here, I used it to satisfy TypeScript strict checks doorAction({ player, object: door, objectConfig: null as any, position, cacheOriginal, option: opening ? 'open' : 'close', }); doorAction({ player, object: otherDoor, objectConfig: null as any, position: otherDoorPosition, cacheOriginal, option: opening ? 'open' : 'close', }); }; export default { pluginId: 'rs:double_doors', hooks: [ { type: 'object_interaction', objectIds: [1519, 1516, 1517, 1520], options: ['open', 'close'], walkTo: true, handler: action, }, ], }; ================================================ FILE: src/plugins/objects/doors/gate.plugin.ts ================================================ import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { activeWorld } from '@engine/world'; import { soundIds } from '@engine/world/config/sound-ids'; import { WNES, directionData } from '@engine/world/direction'; import type { Chunk } from '@engine/world/map/chunk'; import type { ModifiedLandscapeObject } from '@engine/world/map/landscape-object'; import { Position } from '@engine/world/position'; import { logger } from '@runejs/common'; const gates = [ { main: 1551, mainOpen: 1552, hinge: 'LEFT', secondary: 1553, secondaryOpen: 1556, }, { main: 12986, mainOpen: 12988, hinge: 'LEFT', secondary: 12987, secondaryOpen: 12989, }, ]; // @TODO clean up this disgusting code const action: objectInteractionActionHandler = details => { const { player, cacheOriginal } = details; let { object: gate, position } = details; if ((gate as ModifiedLandscapeObject).metadata) { const metadata = (gate as ModifiedLandscapeObject).metadata; if (!metadata) { logger.error(`Could not find metadata for gate with id ${gate.objectId}`); player.sendMessage('Oops, something went wrong. Please report this to a developer.'); return; } player.instance.toggleGameObjects(metadata.originalMain, metadata.main, true); player.instance.toggleGameObjects(metadata.originalSecond, metadata.second, true); player.playSound(soundIds.closeGate, 7); } else { let details = gates.find(g => g.main === gate.objectId); let clickedSecondary = false; let secondGate; let hinge; let direction = WNES[gate.orientation]; let hingeChunk: Chunk; let gateSecondPosition: Position | null = null; if (!details) { details = gates.find(g => g.secondary === gate.objectId); if (!details) { logger.error(`Could not find gate details for gate with id ${gate.objectId} on second pass.`); player.sendMessage('Oops, something went wrong. Please report this to a developer.'); return; } secondGate = gate; gateSecondPosition = position; clickedSecondary = true; hinge = details.hinge; let deltaX = 0; let deltaY = 0; if (hinge === 'LEFT') { switch (direction) { case 'WEST': deltaY--; break; case 'EAST': deltaY++; break; case 'NORTH': deltaX--; break; case 'SOUTH': deltaX++; break; } } else if (hinge === 'RIGHT') { switch (direction) { case 'WEST': deltaY++; break; case 'EAST': deltaY--; break; case 'NORTH': deltaX++; break; case 'SOUTH': deltaX--; break; } } const pos = new Position(gate.x + deltaX, gate.y + deltaY, gate.level); hingeChunk = activeWorld.chunkManager.getChunkForWorldPosition(pos); const mainGate = hingeChunk.getFilestoreLandscapeObject(details.main, pos); if (mainGate) { gate = mainGate; direction = WNES[gate.orientation]; position = pos; } else { logger.error('Could not find main gate for secondary gate at ' + gate.x + ',' + gate.y + ',' + gate.level); player.sendMessage('Oops, something went wrong. Please report this to a developer.'); } } else { hinge = details.hinge; } let deltaX = 0; let deltaY = 0; let newX = 0; let newY = 0; if (hinge === 'LEFT') { switch (direction) { case 'WEST': deltaY++; newX--; break; case 'EAST': deltaY--; newX++; break; case 'NORTH': deltaX++; newY++; break; case 'SOUTH': deltaX--; newY--; break; } } else if (hinge === 'RIGHT') { switch (direction) { case 'WEST': deltaY--; newX++; break; case 'EAST': deltaY++; newX--; break; case 'NORTH': deltaX--; newY--; break; case 'SOUTH': deltaX++; newY++; break; } } const leftHingeDirections: { [key: string]: string } = { NORTH: 'WEST', SOUTH: 'EAST', WEST: 'SOUTH', EAST: 'NORTH', }; const rightHingeDirections: { [key: string]: string } = { NORTH: 'EAST', SOUTH: 'WEST', WEST: 'NORTH', EAST: 'SOUTH', }; if (deltaX === 0 && deltaY === 0) { logger.error('Improperly handled gate at ' + gate.x + ',' + gate.y + ',' + gate.level); return; } const newDirection = hinge === 'LEFT' ? leftHingeDirections[direction] : rightHingeDirections[direction]; if (!clickedSecondary) { gateSecondPosition = new Position(gate.x + deltaX, gate.y + deltaY, gate.level); } if (!gateSecondPosition) { logger.error('Improperly handled gate at ' + gate.x + ',' + gate.y + ',' + gate.level); player.sendMessage('Oops, something went wrong. Please report this to a developer.'); return; } const gateSecondChunk = activeWorld.chunkManager.getChunkForWorldPosition(gateSecondPosition); if (!clickedSecondary) { secondGate = gateSecondChunk.getFilestoreLandscapeObject(details.secondary, gateSecondPosition); } const newPosition = position.step(1, direction); const newSecondPosition = new Position(newPosition.x + newX, newPosition.y + newY, gate.level); const newHinge = { objectId: details.mainOpen, x: newPosition.x, y: newPosition.y, level: newPosition.level, type: gate.type, orientation: directionData[newDirection].rotation, } as ModifiedLandscapeObject; const newSecond = { objectId: details.secondaryOpen, x: newSecondPosition.x, y: newSecondPosition.y, level: newSecondPosition.level, type: gate.type, orientation: directionData[newDirection].rotation, } as ModifiedLandscapeObject; const metadata = { second: JSON.parse(JSON.stringify(newSecond)), originalSecond: secondGate, main: JSON.parse(JSON.stringify(newHinge)), originalMain: gate, }; newHinge.metadata = metadata; newSecond.metadata = metadata; player.instance.toggleGameObjects(newHinge, gate, !cacheOriginal); player.instance.toggleGameObjects(newSecond, secondGate, !cacheOriginal); player.playSound(soundIds.openGate, 7); } }; export default { pluginId: 'rs:gates', hooks: [ { type: 'object_interaction', objectIds: [1551, 1552, 1553, 1556, 12986, 12987, 12988, 12989], options: ['open', 'close'], walkTo: true, handler: action, }, ], }; ================================================ FILE: src/plugins/objects/dungeon-entrances/taverly-dungeon-ladder.plugin.ts ================================================ import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { animationIds } from '@engine/world/config/animation-ids'; import { objectIds } from '@engine/world/config/object-ids'; export const enterDungeon: objectInteractionActionHandler = details => { const loc = details.player.position.clone(); loc.y += 6400; details.player.playAnimation(animationIds.climbLadder); // this used to use `setInterval` but will need rewriting to be synced with ticks // see https://github.com/runejs/server/issues/417 details.player.sendMessage('[debug] see issue #417'); // setTimeout(() => { // details.player.teleport(loc); // }, World.TICK_LENGTH); }; export const exitDungeon: objectInteractionActionHandler = details => { const loc = details.player.position.clone(); loc.y -= 6400; details.player.playAnimation(animationIds.climbLadder); // this used to use `setInterval` but will need rewriting to be synced with ticks // see https://github.com/runejs/server/issues/417 details.player.sendMessage('[debug] see issue #417'); // setTimeout(() => { // details.player.teleport(loc); // }, World.TICK_LENGTH); }; export default { pluginId: 'rs:taverly_dungeon_ladder', hooks: [ { type: 'object_interaction', objectIds: objectIds.ladders.taverlyDungeonOverworld, options: ['climb-down'], walkTo: true, handler: enterDungeon, }, { type: 'object_interaction', objectIds: objectIds.ladders.taverlyDungeonUnderground, options: ['climb-up'], walkTo: true, handler: exitDungeon, }, ], }; ================================================ FILE: src/plugins/objects/item-spawns/take-axe.plugin.ts ================================================ import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { itemIds } from '@engine/world/config/item-ids'; import { objectIds } from '@engine/world/config/object-ids'; import { logger } from '@runejs/common'; const itemMappings: Record = { [objectIds.lumbridgeAxeInLogs]: itemIds.axes.bronze, }; export const action: objectInteractionActionHandler = details => { const { player, option } = details; const name = details.objectConfig.name || ''; if (!name) { logger.warn(`Object ${details.object.objectId} has no name.`); } switch (option) { case 'take-axe': player.playAnimation(827); player.sendMessage(`You take the axe.`); player.playSound(2581, 7); player.giveItem(itemMappings[details.object.objectId]); return; default: player.sendMessage(`This has not been implemented.`); return; } }; export default { pluginId: 'rs:take_axe', hooks: [ { type: 'object_interaction', objectIds: [objectIds.lumbridgeAxeInLogs], options: ['take-axe'], walkTo: true, handler: action, }, ], }; ================================================ FILE: src/plugins/objects/ladders/ladder.plugin.ts ================================================ import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { ActorTask } from '@engine/task/impl/actor-task'; import { ActorTeleportTask } from '@engine/task/impl/actor-teleport-task'; import type { Actor } from '@engine/world/actor/actor'; import { dialogueAction } from '@engine/world/actor/player/dialogue-action'; import { Position } from '@engine/world/position'; import { logger } from '@runejs/common'; const planes = { min: 0, max: 3 }; const validate: (level: number) => boolean = level => { return planes.min <= level && level <= planes.max; }; //TODO: prevent no-clipping. export const action: objectInteractionActionHandler = details => { const { player, option } = details; const ladderObjectName = details.objectConfig.name || ''; if (!ladderObjectName) { logger.warn(`Ladder object ${details.object.objectId} has no name.`); } if (option === 'climb') { dialogueAction(player) .then(async d => d.options(`Climb up or down the ${ladderObjectName.toLowerCase()}?`, [ `Climb up the ${ladderObjectName.toLowerCase()}.`, `Climb down the ${ladderObjectName.toLowerCase()}.`, ]), ) .then(d => { d.close(); switch (d.action) { case 1: case 2: player.enqueueTask( class LadderTask extends ActorTask { constructor(actor: Actor) { super(actor, { repeat: false, immediate: false }); } execute() { action({ ...details, option: `climb-${d.action === 1 ? 'up' : 'down'}` }); } }, ); return; } }); return; } const up = option === 'climb-up'; const { position } = player; const newPosition = new Position(position.x, position.y, position.level); newPosition.level = position.level + (up ? 1 : -1); if (position.level === 0) { if (newPosition.level === 1 && position.y >= 6400) { newPosition.level = 0; newPosition.y -= 6414; newPosition.x++; } else if (newPosition.level === -1) { newPosition.level = 0; newPosition.y += 6414; newPosition.x--; } } if (!validate(newPosition.level)) return; if (!ladderObjectName.startsWith('Stair')) { player.playAnimation(up ? 828 : 827); } player.sendMessage(`You climb ${option.slice(6)} the ${ladderObjectName.toLowerCase()}.`); player.enqueueTask(ActorTeleportTask, [newPosition]); }; export default { pluginId: 'rs:ladders', hooks: [ { type: 'object_interaction', objectIds: [1738, 1739, 1740, 1746, 1747, 1748, 2147, 2148, 12964, 12965, 12966], options: ['climb', 'climb-up', 'climb-down'], walkTo: true, handler: action, }, ], }; ================================================ FILE: src/plugins/objects/mill/flour-bin.plugin.ts ================================================ import type { itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action'; import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import type { playerInitActionHandler } from '@engine/action/pipe/player-init.action'; import type { Player } from '@engine/world/actor/player/player'; import { itemIds } from '@engine/world/config/item-ids'; import { soundIds } from '@engine/world/config/sound-ids'; import type { ObjectConfig } from '@runejs/filestore'; function flourBin(details: { objectConfig: ObjectConfig; player: Player }): void { const { player, objectConfig } = details; if (!details.player.savedMetadata['mill-flour']) { player.sendMessage( `The ${(objectConfig.name || '').toLowerCase()} is already empty. You need to place wheat in the hopper upstairs `, ); player.sendMessage(`first.`); } else { if (player.hasItemInInventory(itemIds.pot)) { player.playSound(soundIds.potContentModified, 7); player.removeFirstItem(itemIds.pot); player.giveItem(itemIds.potOfFlour); details.player.savedMetadata['mill-flour'] -= 1; } else { player.sendMessage(`You need a pot to hold the flour in.`); } } updateBin(details); } export const updateBin: playerInitActionHandler = details => { const count = (details.player.savedMetadata['mill-flour'] || 0) === 0 ? 0 : 1; details.player.outgoingPackets.updateClientConfig(695, count); }; const actionInteract: objectInteractionActionHandler = details => { flourBin(details); }; const actionItem: itemOnObjectActionHandler = details => { flourBin(details); }; export default { pluginId: 'rs:flour_bin', hooks: [ { type: 'item_on_object', objectIds: [1781, 5792, 1782], itemIds: [itemIds.pot], walkTo: true, handler: actionItem, }, { type: 'player_init', handler: updateBin, }, { type: 'object_interaction', objectIds: [1781, 1782], options: ['empty'], walkTo: true, handler: actionInteract, }, ], }; ================================================ FILE: src/plugins/objects/mill/hopper-controls.plugin.ts ================================================ import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; export const action: objectInteractionActionHandler = details => { details.player.busy = true; details.player.playAnimation(3571); details.player.playSound(2400, 5); details.player.personalInstance.replaceGameObject(2722, details.object, 1); // this used to use `setInterval` but will need rewriting to be synced with ticks // see https://github.com/runejs/server/issues/417 details.player.sendMessage('[debug] see issue #417'); // setTimeout(() => { // if (details.player.savedMetadata['mill-grain'] && details.player.savedMetadata['mill-grain'] >= 1) { // details.player.sendMessage(`You operate the hopper. The grain slide down the chute.`); // if (!details.player.savedMetadata['mill-flour']) { // details.player.savedMetadata['mill-flour'] = 0; // } // details.player.savedMetadata['mill-flour'] += details.player.savedMetadata['mill-grain']; // details.player.savedMetadata['mill-grain'] = 0; // details.player.outgoingPackets.updateClientConfig(695, 1); // } else { // details.player.sendMessage(`You operate the hopper. Nothing interesting happens.`); // } // details.player.busy = false; // }, World.TICK_LENGTH); }; export default { pluginId: 'rs:grain_hopper_controls', hooks: [ { type: 'object_interaction', objectIds: [2718, 2721], options: ['operate'], walkTo: true, handler: action, }, ], }; ================================================ FILE: src/plugins/objects/mill/hopper.plugin.ts ================================================ import type { itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action'; import { itemIds } from '@engine/world/config/item-ids'; export const action: itemOnObjectActionHandler = details => { if (details.player.savedMetadata['mill-grain'] && details.player.savedMetadata['mill-grain'] === 1) { details.player.sendMessage(`There is already grain in the hopper.`); return; } details.player.busy = true; details.player.playAnimation(3572); details.player.playSound(2576, 5); // this used to use `setInterval` but will need rewriting to be synced with ticks // see https://github.com/runejs/server/issues/417 details.player.sendMessage('[debug] see issue #417'); // setTimeout(() => { // details.player.removeFirstItem(itemIds.grain); // details.player.sendMessage(`You put the grain in the hopper. You should now pull the lever nearby to operate`); // details.player.sendMessage(`the hopper.`); // details.player.savedMetadata['mill-grain'] = 1; // details.player.busy = false; // }, World.TICK_LENGTH); }; export default { pluginId: 'rs:grain_hopper', hooks: [ { type: 'item_on_object', objectIds: [2714, 2717], itemIds: [itemIds.grain], walkTo: true, handler: action, }, ], }; ================================================ FILE: src/plugins/objects/pickables/pickables.plugin.ts ================================================ import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { findItem } from '@engine/config/config-handler'; import { itemIds } from '@engine/world/config/item-ids'; import { logger } from '@runejs/common'; export const action: objectInteractionActionHandler = details => { details.player.busy = true; details.player.playAnimation(827); let itemId: number; let prefix = 'some'; switch (details.objectConfig.name) { case 'Wheat': itemId = itemIds.grain; break; case 'Onion': itemId = itemIds.onion; prefix = 'an'; break; case 'Potato': prefix = 'a'; itemId = itemIds.potato; break; case 'Flax': itemId = itemIds.flax; break; case 'Cabbage': default: itemId = itemIds.cabbage; break; } const pickedItem = findItem(itemId); if (!pickedItem) { logger.warn(`Could not find item for pickable with id ${itemId}`); details.player.busy = false; return; } // this used to use `setInterval` but will need rewriting to be synced with ticks // see https://github.com/runejs/server/issues/417 details.player.sendMessage('[debug] see issue #417'); // setTimeout(() => { // details.player.sendMessage(`You ${details.option} the ${(details.objectConfig.name || '').toLowerCase()} and receive ${prefix} ${pickedItem.name.toLowerCase()}.`); // details.player.playSound(2581, 7); // if (details.objectConfig.name !== 'Flax' || Math.floor(Math.random() * 10) === 1) { // details.player.instance.hideGameObjectTemporarily(details.object, 30); // } // details.player.giveItem(pickedItem.gameId); // details.player.busy = false; // }, World.TICK_LENGTH); }; export default { pluginId: 'rs:pickables', hooks: [ { type: 'object_interaction', objectIds: [313, 5583, 5584, 5585, 1161, 3366, 312, 2646], options: ['pick'], walkTo: true, handler: action, }, ], }; ================================================ FILE: src/plugins/player/follow-player.plugin.js ================================================ module.exports = { pluginId: 'rs:follow_player', hooks: [ { type: 'player_interaction', options: 'follow', handler: details => details.player.follow(details.otherPlayer), }, ], }; ================================================ FILE: src/plugins/player/login-unlock-emotes.plugin.ts ================================================ import type { playerInitActionHandler } from '@engine/action/pipe/player-init.action'; import { unlockEmotes } from '@plugins/buttons/player-emotes.plugin'; export const handler: playerInitActionHandler = ({ player }) => unlockEmotes(player); export default { pluginId: 'rs:unlock_player_emotes', hooks: [{ type: 'player_init', handler }], }; ================================================ FILE: src/plugins/player/login-update-settings.plugin.ts ================================================ import type { playerInitActionHandler } from '@engine/action/pipe/player-init.action'; import { validateSettings } from '@engine/world/actor/player/player-data'; import { widgetScripts } from '@engine/world/config/widget'; export const handler: playerInitActionHandler = ({ player }) => { validateSettings(player); const settings = player.settings; player.outgoingPackets.updateClientConfig(widgetScripts.brightness, settings.screenBrightness); player.outgoingPackets.updateClientConfig(widgetScripts.mouseButtons, settings.twoMouseButtonsEnabled ? 0 : 1); player.outgoingPackets.updateClientConfig(widgetScripts.splitPrivateChat, settings.splitPrivateChatEnabled ? 1 : 0); player.outgoingPackets.updateClientConfig(widgetScripts.chatEffects, settings.chatEffectsEnabled ? 0 : 1); player.outgoingPackets.updateClientConfig(widgetScripts.acceptAid, settings.acceptAidEnabled ? 1 : 0); player.outgoingPackets.updateClientConfig(widgetScripts.musicVolume, settings.musicVolume); player.outgoingPackets.updateClientConfig(widgetScripts.soundEffectVolume, settings.soundEffectVolume); player.outgoingPackets.updateClientConfig(widgetScripts.areaEffectVolume, settings.areaEffectVolume); player.outgoingPackets.updateClientConfig(widgetScripts.runMode, settings.runEnabled ? 1 : 0); player.outgoingPackets.updateClientConfig(widgetScripts.autoRetaliate, settings.autoRetaliateEnabled ? 0 : 1); player.outgoingPackets.updateClientConfig(widgetScripts.attackStyle, settings.attackStyle); player.outgoingPackets.updateClientConfig(widgetScripts.bankInsertMode, settings.bankInsertMode); player.outgoingPackets.updateClientConfig(widgetScripts.bankWithdrawNoteMode, settings.bankWithdrawNoteMode); player.outgoingPackets.updateSocialSettings(); }; export default { pluginId: 'rs:update_player_settings', hooks: [{ type: 'player_init', handler }], }; ================================================ FILE: src/plugins/player/update-friends-list.plugin.ts ================================================ import type { playerInitActionHandler } from '@engine/action/pipe/player-init.action'; import { PrivateMessaging } from '@engine/world/actor/player/private-messaging'; export const handler: playerInitActionHandler = ({ player }) => { PrivateMessaging.playerLoggedIn(player); player.outgoingPackets.sendFriendServerStatus(2); }; export default { pluginId: 'rs:update_friends_list', hooks: [{ type: 'player_init', handler }], }; ================================================ FILE: src/plugins/quests/cooks-assistant-quest.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import type { PlayerQuest, QuestJournalHandler } from '@engine/config/quest-config'; import type { DialogueTree } from '@engine/world/actor/dialogue'; import { Emote, dialogue, execute, goto } from '@engine/world/actor/dialogue'; import type { Player } from '@engine/world/actor/player/player'; import { Quest } from '@engine/world/actor/player/quest'; import { itemIds } from '@engine/world/config/item-ids'; const journalHandler: QuestJournalHandler = { 0: `I can start this quest by speaking to the Cook in the Kitchen on the ground floor of Lumbridge Castle.`, 50: player => { let questLog = `It's the Duke of Lumbridge's birthday and I have to help ` + `his Cook make him a birthday cake. To do this I need to ` + `bring him the following ingredients:\n`; const quest = player.getQuest('rs:cooks_assistant'); if (player.hasItemInInventory(itemIds.bucketOfMilk) || quest.metadata.givenMilk) { questLog += `I have found a bucket of milk to give to the cook.\n`; } else { questLog += `I need to find a bucket of milk. There's a cattle field east ` + `of Lumbridge, I should make sure I take an empty bucket with me.\n`; } if (player.hasItemInInventory(itemIds.potOfFlour) || quest.metadata.givenFlour) { questLog += `I have found a pot of flour to give to the cook.\n`; } else { questLog += `I need to find a pot of flour. There's a mill found north-` + `west of Lumbridge, I should take an empty pot with me.\n`; } if (player.hasItemInInventory(itemIds.egg) || quest.metadata.givenEgg) { questLog += `I have found an egg to give to the cook.\n`; } else { questLog += `I need to find an egg. The cook normally gets his eggs from ` + `the Groats' farm, found just to the west of the cattle field.`; } return questLog; }, complete: `It was the Duke of Lumbridge's birthday, but his cook had ` + `forgotten to buy the ingredients he needed to make him a ` + `cake. I brought the cook an egg, some flour and some milk ` + `and the cook made a delicious looking cake with them.\n\n` + `As a reward he now lets me use his high quality range ` + `which lets me burn things less whenever I wish to cook ` + `there.\n\n` + `QUEST COMPLETE!`, }; function dialogueIngredientQuestions(): Function { return (options, tag_INGREDIENT_QUESTIONS) => [ `Where do I find some flour?`, [ player => [Emote.GENERIC, `Where do I find some flour?`], cook => [ Emote.GENERIC, `There is a Mill fairly close, go North and then West. Mill Lane Mill ` + `is just off the road to Draynor. I usually get my flour from there.`, ], cook => [Emote.HAPPY, `Talk to Millie, she'll help, she's a lovely girl and a fine Miller.`], goto('tag_INGREDIENT_QUESTIONS'), ], `How about milk?`, [ player => [Emote.GENERIC, `How about milk?`], cook => [Emote.GENERIC, `There is a cattle field on the other side of the river, just across ` + `the road from Groats' Farm.`], cook => [ Emote.HAPPY, `Talk to Gillie Groats, she look after the Dairy Cows - ` + `she'll tell you everything you need to know about milking cows!`, ], goto('tag_INGREDIENT_QUESTIONS'), ], `And eggs? Where are they found?`, [ player => [Emote.GENERIC, `And eggs? Where are they found?`], cook => [Emote.GENERIC, `I normally get my eggs from the Groats' farm, on the other side of ` + `the river.`], cook => [Emote.GENERIC, `But any chicken should lay eggs.`], goto('tag_INGREDIENT_QUESTIONS'), ], `Actually, I know where to find this stuff.`, [player => [Emote.GENERIC, `I've got all the information I need. Thanks.`]], ]; } const startQuestAction: npcInteractionActionHandler = details => { const { player, npc } = details; dialogue( [player, { npc, key: 'cook' }], [ cook => [Emote.WORRIED, `What am I to do?`], options => [ `What's wrong?`, [], `Can you make me a cake?`, [ player => [Emote.HAPPY, `You're a cook, why don't you bake me a cake?`], cook => [Emote.SAD, `*sniff* Don't talk to me about cakes...`], ], `You don't look very happy.`, [ player => [Emote.WORRIED, `You don't look very happy.`], cook => [ Emote.SAD, `No, I'm not. The world is caving in around me - I am overcome by dark feelings ` + `of impending doom.`, ], options => [ `What's wrong?`, [], `I'd take the rest of the day off if I were you.`, [ player => [Emote.GENERIC, `I'd take the rest of the day off if I were you.`], cook => [Emote.WORRIED, `No, that's the worst thing I could do. I'd get in terrible trouble.`], player => [Emote.SKEPTICAL, `Well maybe you need to take a holiday...`], cook => [Emote.SAD, `That would be nice, but the Duke doesn't allow holidays for core staff.`], player => [Emote.LAUGH, `Hmm, why not run away to the sea and start a new life as a Pirate?`], cook => [ Emote.SKEPTICAL, `My wife gets sea sick, and I have an irrational fear of eyepatches. ` + `I don't see it working myself.`, ], player => [Emote.WORRIED, `I'm afraid I've run out of ideas.`], cook => [Emote.SAD, `I know I'm doomed.`], ], ], ], `Nice hat!`, [ player => [Emote.HAPPY, `Nice hat!`], cook => [Emote.SKEPTICAL, `Err thank you. It's a pretty ordinary cooks hat really.`], player => [Emote.HAPPY, `Still, suits you. The trousers are pretty special too.`], cook => [Emote.SKEPTICAL, `It's all standard cook's issue uniform...`], player => [ Emote.POMPOUS, `The whole hat, apron, striped trousers ensemble - it works. It makes you ` + `look like a real cook.`, ], cook => [ Emote.ANGRY, `I am a real cook! I haven't got time to be chatting about Culinary Fashion. ` + `I am in desperate need of help!`, ], ], ], player => [Emote.HAPPY, `What's wrong?`], cook => [ Emote.WORRIED, `Oh dear, oh dear, oh dear, I'm in a terrible terrible ` + ` mess! It's the Duke's birthday today, and I should be making him a lovely big birthday cake.`, ], cook => [ Emote.WORRIED, `I've forgotten to buy the ingredients. I'll never get ` + `them in time now. He'll sack me! What will I do? I have four children and a goat to ` + `look after. Would you help me? Please?`, ], options => [ `I'm always happy to help a cook in distress.`, [ execute(() => { player.setQuestProgress('rs:cooks_assistant', 50); }), player => [Emote.GENERIC, `Yes, I'll help you.`], cook => [ Emote.HAPPY, `Oh thank you, thank you. I need milk, an egg and flour. I'd be very grateful ` + `if you can get them for me.`, ], player => [Emote.GENERIC, `So where do I find these ingredients then?`], dialogueIngredientQuestions(), ], `I can't right now, maybe later.`, [ player => [Emote.GENERIC, `No, I don't feel like it. Maybe later.`], cook => [Emote.ANGRY, `Fine. I always knew you Adventurer types were callous beasts. ` + `Go on your merry way!`], ], ], ], ); }; function youStillNeed(quest: PlayerQuest): DialogueTree { return [ text => `You still need to get\n` + `${!quest.metadata.givenMilk ? `A bucket of milk. ` : ``}${!quest.metadata.givenFlour ? `A pot of flour. ` : ``}${!quest.metadata.givenEgg ? `An egg.` : ``}`, options => [ `I'll get right on it.`, [player => [Emote.GENERIC, `I'll get right on it.`]], `Can you remind me how to find these things again?`, [player => [Emote.GENERIC, `So where do I find these ingredients then?`], dialogueIngredientQuestions()], ], ]; } const handInIngredientsAction: npcInteractionActionHandler = async details => { const { player, npc } = details; const dialogueTree: DialogueTree = [cook => [Emote.GENERIC, `How are you getting on with finding the ingredients?`]]; const quest = player.getQuest('rs:cooks_assistant'); const ingredients = [ { itemId: itemIds.bucketOfMilk, text: `Here's a bucket of milk.`, attr: 'givenMilk' }, { itemId: itemIds.potOfFlour, text: `Here's a pot of flour.`, attr: 'givenFlour' }, { itemId: itemIds.egg, text: `Here's a fresh egg.`, attr: 'givenEgg' }, ]; for (const ingredient of ingredients) { if (quest.metadata[ingredient.attr]) { quest.metadata.ingredientCount++; continue; } if (!player.hasItemInInventory(ingredient.itemId)) { continue; } dialogueTree.push( player => [Emote.GENERIC, ingredient.text], execute(() => { const quest = player.getQuest('rs:cooks_assistant'); if (player.removeFirstItem(ingredient.itemId) !== -1) { quest.metadata[ingredient.attr] = true; } }), ); } let questComplete: boolean = false; dialogueTree.push( goto(() => { const count = [quest.metadata.givenMilk, quest.metadata.givenFlour, quest.metadata.givenEgg].filter( value => value === true, ).length; if (count === 3) { return 'tag_ALL_INGREDIENTS'; } else if (count === 0) { return 'tag_NO_INGREDIENTS'; } else { return 'tag_SOME_INGREDIENTS'; } }), (subtree, tag_ALL_INGREDIENTS) => [ cook => [Emote.HAPPY, `You've brought me everything I need! I am saved! Thank you!`], player => [Emote.WONDERING, `So do I get to go to the Duke's Party?`], cook => [Emote.SAD, `I'm afraid not, only the big cheeses get to dine with the Duke.`], player => [Emote.GENERIC, `Well, maybe one day I'll be important enough to sit on the Duke's table.`], cook => [Emote.SKEPTICAL, `Maybe, but I won't be holding my breath.`], execute(() => { questComplete = true; }), ], (subtree, tag_NO_INGREDIENTS) => [ player => [Emote.GENERIC, `I haven't got any of them yet, I'm still looking.`], cook => [ Emote.SAD, `Please get the ingredients quickly. I'm running out of time! ` + `The Duke will throw me into the streets!`, ], ...youStillNeed(quest), ], (subtree, tag_SOME_INGREDIENTS) => [ cook => [ Emote.SAD, `Thanks for the ingredients you have got so far, please get the rest quickly. ` + `I'm running out of time! The Duke will throw me into the streets!`, ], ...youStillNeed(quest), ], ); await dialogue([player, { npc, key: 'cook' }], dialogueTree); if (questComplete) { player.setQuestProgress('rs:cooks_assistant', 'complete'); } }; export default { pluginId: 'rs:cooks_assistant_quest', quests: [ new Quest({ id: 'rs:cooks_assistant', questTabId: 27, name: `Cook's Assistant`, points: 1, journalHandler, onComplete: { questCompleteWidget: { rewardText: ['300 Cooking XP'], itemId: 1891, modelZoom: 240, modelRotationX: 180, modelRotationY: 180, }, giveRewards: (player: Player): void => player.skills.cooking.addExp(300), }, }), ], hooks: [ { type: 'npc_interaction', questRequirement: { questId: 'rs:cooks_assistant', stage: 0, }, npcs: 'rs:lumbridge_castle_cook', options: 'talk-to', walkTo: true, handler: startQuestAction, }, { type: 'npc_interaction', questRequirement: { questId: 'rs:cooks_assistant', stage: 50, }, npcs: 'rs:lumbridge_castle_cook', options: 'talk-to', walkTo: true, handler: handInIngredientsAction, }, ], }; ================================================ FILE: src/plugins/quests/goblin-diplomacy-tutorial/goblin-diplomacy-quest.plugin.ts ================================================ import type { buttonActionHandler } from '@engine/action/pipe/button.action'; import type { equipmentChangeActionHandler } from '@engine/action/pipe/equipment-change.action'; import type { playerInitActionHandler } from '@engine/action/pipe/player-init.action'; import { findNpc, widgets } from '@engine/config/config-handler'; import type { QuestJournalHandler } from '@engine/config/quest-config'; import { questDialogueActionFactory } from '@engine/config/quest-config'; import { tabIndex } from '@engine/interface/interface-state'; import { activeWorld } from '@engine/world'; import { dialogue } from '@engine/world/actor/dialogue'; import type { Player } from '@engine/world/actor/player/player'; import { defaultPlayerTabWidgets } from '@engine/world/actor/player/player'; import { Quest } from '@engine/world/actor/player/quest'; import { WorldInstance } from '@engine/world/instances'; import { Position } from '@engine/world/position'; import { updateCombatStyleWidget } from '@plugins/combat/combat-styles.plugin'; import { serverConfig } from '@server/game/game-server'; import { Subject } from 'rxjs'; import { take } from 'rxjs/operators'; import { v4 } from 'uuid'; import { harlanDialogueHandler } from './melee-tutor-dialogue'; import { runescapeGuideDialogueHandler } from './runescape-guide-dialogue'; import { goblinDiplomacyStageHandler } from './stage-handler'; export const tutorialTabWidgetOrder = [ [tabIndex['settings'], widgets.settingsTab], [tabIndex['friends'], widgets.friendsList], [tabIndex['ignores'], widgets.ignoreList], [tabIndex['emotes'], widgets.emotesTab], [tabIndex['music'], widgets.musicPlayerTab], [tabIndex['inventory'], widgets.inventory.widgetId], [tabIndex['skills'], widgets.skillsTab], [tabIndex['equipment'], widgets.equipment.widgetId], [tabIndex['combat'], -1], // @TODO prayer, magic, ]; export function showTabWidgetHint( player: Player, tabIndex: number, availableTabs: number, finalProgress: number, helpTitle: string, helpText: string, ): void { const tabClickEvent = { tabIndex, event: new Subject(), }; player.metadata.tabClickEvent = tabClickEvent; dialogue([player], [titled => [helpTitle, helpText]], { permanent: true, }); unlockAvailableTabs(player, availableTabs); player.outgoingPackets.blinkTabIcon(tabIndex); tabClickEvent.event.pipe(take(1)).subscribe(async () => { player.setQuestProgress('tyn:goblin_diplomacy', finalProgress); tabClickEvent.event.complete(); delete player.metadata.tabClickEvent; await tutorialHandler(player); }); } export function unlockAvailableTabs(player: Player, availableTabs?: number): void { let doCombatStyleTab = false; if (availableTabs === undefined) { availableTabs = tutorialTabWidgetOrder.length; } for (let i = 0; i < availableTabs; i++) { if (tutorialTabWidgetOrder[i][1] === -1) { doCombatStyleTab = true; } player.setSidebarWidget(tutorialTabWidgetOrder[i][0], tutorialTabWidgetOrder[i][1]); } if (doCombatStyleTab) { updateCombatStyleWidget(player); } } export function npcHint(player: Player, npcKey: string | number): void { if (typeof npcKey === 'string') { const npc = findNpc(npcKey); npcKey = npc.gameId; } const npc = activeWorld.findNpcsById(npcKey, player.instance.instanceId)[0] || null; if (npc) { player.outgoingPackets.showNpcHintIcon(npc); } } export const startTutorial = async (player: Player): Promise => { player.setQuestProgress('tyn:goblin_diplomacy', 0); defaultPlayerTabWidgets().forEach((widgetId: number, tabIndex: number) => { if (widgetId !== -1) { player.outgoingPackets.sendTabWidget(tabIndex, widgetId === widgets.logoutTab ? widgetId : null); } }); player.inventory.add('rs:pot'); player.inventory.add('rs:logs'); player.inventory.add('rs:bones'); player.inventory.add('rs:coins'); player.inventory.add('rs:coins'); player.inventory.add('rs:coins'); player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory); await dialogue([player], [titled => [`Getting Started`, `\nCreate your character!`]], { permanent: true, }); player.interfaceState.openWidget(widgets.characterDesign, { slot: 'screen', }); await player.interfaceState.widgetClosed('screen'); }; export async function spawnGoblinBoi(player: Player, spawnPoint: 'beginning' | 'end'): Promise { const nearbyGoblins = activeWorld.findNpcsByKey('rs:goblin', player.instance.instanceId); if (nearbyGoblins && nearbyGoblins.length > 0) { // Goblin is already spawned, do nothing return; } // Spawn the goblin where it needs to be :) if (spawnPoint === 'beginning') { //const goblin = await world.spawnNpc('rs:goblin', new Position(3219, 3246), 'SOUTH', // 0, player.instance.instanceId); const goblin = await activeWorld.spawnNpc('rs:goblin', new Position(3221, 3257), 'SOUTH', 0, player.instance.instanceId); goblin.pathfinding.walkTo(new Position(3219, 3246), { pathingSearchRadius: 16, ignoreDestination: false, }); } else { await activeWorld.spawnNpc('rs:goblin', new Position(3219, 3246), 'SOUTH', 0, player.instance.instanceId); } } export async function tutorialHandler(player: Player): Promise { const progress = player.getQuest('tyn:goblin_diplomacy').progress; const handler = goblinDiplomacyStageHandler[progress]; defaultPlayerTabWidgets().forEach((widgetId: number, tabIndex: number) => { if (widgetId !== -1) { player.setSidebarWidget(tabIndex, widgetId === widgets.logoutTab ? widgetId : null); } }); if (handler) { player.outgoingPackets.resetNpcHintIcon(); await handler(player); } } function spawnQuestNpcs(player: Player): void { activeWorld.spawnNpc('rs:runescape_guide', new Position(3230, 3238), 'SOUTH', 2, player.instance.instanceId); activeWorld.spawnNpc('rs:melee_combat_tutor', new Position(3219, 3238), 'EAST', 1, player.instance.instanceId); } const tutorialInitAction: playerInitActionHandler = async ({ player }) => { if (serverConfig.tutorialEnabled && !player.savedMetadata.tutorialComplete) { player.instance = new WorldInstance(v4()); player.metadata.blockObjectInteractions = true; spawnQuestNpcs(player); await tutorialHandler(player); } else { defaultPlayerTabWidgets().forEach((widgetId: number, tabIndex: number) => { if (widgetId !== -1) { player.setSidebarWidget(tabIndex, widgetId); } }); } }; const trainingSwordEquipAction: equipmentChangeActionHandler = async ({ player, itemDetails }) => { const progress = player.getQuest('tyn:goblin_diplomacy').progress; if (progress === 85) { const swordEquipped = player.isItemEquipped('rs:training_sword'); const shieldEquipped = player.isItemEquipped('rs:training_shield'); if ((itemDetails.key === 'rs:training_sword' && shieldEquipped) || (itemDetails.key === 'rs:training_shield' && swordEquipped)) { player.setQuestProgress('tyn:goblin_diplomacy', 90); await tutorialHandler(player); } } }; const createCharacterAction: buttonActionHandler = ({ player }): void => { player.interfaceState.closeAllSlots(); }; const journalHandler: QuestJournalHandler = { 0: `stinkyu hoomsn HAHA\n\n\nf1nglewuRt`, }; const QUEST_ID = 'tyn:goblin_diplomacy'; const QUEST = new Quest({ id: QUEST_ID, questTabId: 28, name: `Goblin Diplomacy`, points: 1, journalHandler, onComplete: { questCompleteWidget: { rewardText: ['A training sword & shield'], itemId: 9703, modelZoom: 200, modelRotationX: 0, modelRotationY: 180, }, }, }); /** * Custom Goblin Diplomacy tutorial quest! */ export default { pluginId: 'tyn:goblin_diplomacy_quest', quests: [QUEST], hooks: [ { type: 'player_init', handler: tutorialInitAction, }, { type: 'npc_interaction', handler: questDialogueActionFactory(QUEST_ID, runescapeGuideDialogueHandler, tutorialHandler), npcs: 'rs:runescape_guide', options: 'talk-to', walkTo: true, }, { type: 'npc_interaction', handler: questDialogueActionFactory(QUEST_ID, harlanDialogueHandler, tutorialHandler), npcs: 'rs:melee_combat_tutor', options: 'talk-to', walkTo: true, }, { type: 'equipment_change', eventType: 'equip', handler: trainingSwordEquipAction, itemIds: [9703, 9704], }, { type: 'button', widgetId: widgets.characterDesign, handler: createCharacterAction, }, ], }; ================================================ FILE: src/plugins/quests/goblin-diplomacy-tutorial/melee-tutor-dialogue.ts ================================================ import type { QuestDialogueHandler } from '@engine/config/quest-config'; import { Emote, dialogue, execute } from '@engine/world/actor/dialogue'; export const harlanDialogueHandler: QuestDialogueHandler = { 50: async (player, npc) => { await dialogue( [player, { npc, key: 'harlan' }], [ harlan => [Emote.GENERIC, `Greetings, adventurer. How can I assist you?`], player => [Emote.WONDERING, `The guide in there said you could unlock my inventory and stuff.`], harlan => [Emote.LAUGH, `I suppose I could, yes. But where's the fun in that?`], player => [Emote.SAD, `I just want to fight something.`], harlan => [ Emote.GENERIC, `I'm sure you'll get the chance, what with all the recent goblin attacks on this side of the River Lum.`, ], harlan => [Emote.GENERIC, `To that end, let me show you your inventory.`], execute(() => { player.setQuestProgress('tyn:goblin_diplomacy', 55); }), ], ); }, 55: async (player, npc) => { await dialogue([player, { npc, key: 'harlan' }], [harlan => [Emote.GENERIC, `Speak with me once you've opened your inventory.`]]); }, 60: async (player, npc) => { await dialogue( [player, { npc, key: 'harlan' }], [ player => [Emote.SAD, `Doesn't look like I had much on me...`], harlan => [Emote.GENERIC, `I would say the goblins likely ran through your pockets before the Guard hauled you in.`], harlan => [Emote.GENERIC, `Lets check out your hitpoints and make sure you're in proper shape after that.`], execute(() => { player.setQuestProgress('tyn:goblin_diplomacy', 65); }), ], ); }, 65: async (player, npc) => { await dialogue( [player, { npc, key: 'harlan' }], [ harlan => [ Emote.GENERIC, `Click on your skills tab to view your hitpoints stat. It should be blinking over near your inventory.`, ], ], ); }, 70: async (player, npc) => { await dialogue( [player, { npc, key: 'harlan' }], [ harlan => [ Emote.GENERIC, `You appear to be in good shape. Though with a backpack like that, ` + `I don't think you'll make much headway against those goblins...`, ], harlan => [Emote.GENERIC, `I'll provide you with some starter equipment - but from there, you're on your own.`], harlan => [Emote.GENERIC, `But before I can do that, you'll need to open your Equipment tab.`], execute(() => { player.setQuestProgress('tyn:goblin_diplomacy', 75); }), ], ); }, 75: async (player, npc) => { await dialogue([player, { npc, key: 'harlan' }], [harlan => [Emote.GENERIC, `Speak with me once you've opened your equipment.`]]); }, 80: async (player, npc) => { await dialogue( [player, { npc, key: 'harlan' }], [ harlan => [Emote.GENERIC, `Have this training equipment and try it on.`], execute(() => { player.inventory.add('rs:training_sword'); player.inventory.add('rs:training_shield'); player.setQuestProgress('tyn:goblin_diplomacy', 85); }), text => `Harlan hands you a Training sword and shield.`, ], ); }, 85: async (player, npc) => { await dialogue( [player, { npc, key: 'harlan' }], [harlan => [Emote.GENERIC, `Try on the Training sword and shield and we can continue.`]], ); }, }; ================================================ FILE: src/plugins/quests/goblin-diplomacy-tutorial/runescape-guide-dialogue.ts ================================================ import type { QuestDialogueHandler } from '@engine/config/quest-config'; import { Emote, dialogue, execute } from '@engine/world/actor/dialogue'; import type { Player } from '@engine/world/actor/player/player'; import { defaultPlayerTabWidgets } from '@engine/world/actor/player/player'; import { updateCombatStyleWidget } from '@plugins/combat/combat-styles.plugin'; export const runescapeGuideDialogueHandler: QuestDialogueHandler = { 5: async (player: Player, npc) => { await dialogue( [player, { npc, key: 'guide' }], [ guide => [Emote.GENERIC, `Greetings adventurer, welcome to RuneScape.`], player => [Emote.SKEPTICAL, `How did I get here?...`], guide => [Emote.GENERIC, `Seems like a goblin smacked you over the head on your way in. Nasty little things.`], player => [Emote.DROWZY, `I guess that explains the headache.`], guide => [Emote.GENERIC, `I would imagine so. Now, it's my job here is to show new players around.`], options => [ `Go on.`, [ player => [Emote.GENERIC, `Carry on then.`], guide => [ Emote.GENERIC, `We'll start with the Options menu. Click on the blinking spanner icon at the bottom right of your game screen.`, ], execute(() => { player.setQuestProgress('tyn:goblin_diplomacy', 10); }), ], `I know how the game works already.`, [ player => [Emote.HAPPY, `This isn't my first time here, I'm good.`], guide => [Emote.HAPPY, `Oh good, I won't tell you what you already know then.`], execute(() => { player.savedMetadata.tutorialComplete = true; player.setQuestProgress('tyn:goblin_diplomacy', 'complete'); player.instance = null; defaultPlayerTabWidgets().forEach((widgetId: number, tabIndex: number) => { if (widgetId !== -1) { player.setSidebarWidget(tabIndex, widgetId); } }); updateCombatStyleWidget(player); player.metadata.blockObjectInteractions = false; }), ], ], ], ); }, 10: async (player, npc) => { await dialogue( [player, { npc, key: 'guide' }], [guide => [Emote.GENERIC, 'Please click on the blinking spanner icon, then we can continue.']], ); }, 15: async (player, npc) => { await dialogue( [player, { npc, key: 'guide' }], [ guide => [ Emote.HAPPY, `Next we'll move on to the more social side of things. Click on the blinking icon to learn about the Friends List.`, ], execute(() => { player.setQuestProgress('tyn:goblin_diplomacy', 20); }), ], ); }, 20: async (player, npc) => { await dialogue( [player, { npc, key: 'guide' }], [guide => [Emote.GENERIC, `Please return to me once you've gone through all three social tabs.`]], ); }, 25: async (player, npc) => { await dialogue( [player, { npc, key: 'guide' }], [guide => [Emote.GENERIC, `Please return to me once you've gone through all three social tabs.`]], ); }, 30: async (player, npc) => { await dialogue( [player, { npc, key: 'guide' }], [guide => [Emote.GENERIC, `Please return to me once you've gone through all three social tabs.`]], ); }, 35: async (player, npc) => { await dialogue( [player, { npc, key: 'guide' }], [ player => [Emote.HAPPY, `I've gone through the Friends List and everything. When do I get to kill things?`], guide => [Emote.LAUGH, `All in good time, ${player.username}. We've a little more to discuss yet - like music!`], player => [Emote.SKEPTICAL, `Music? Doesn't everyone turn that off?`], guide => [Emote.SAD, `Some people find it nostalgic.`], player => [Emote.SAD, `Sorry, go on...`], guide => [Emote.GENERIC, `Yes... As I was saying... Music can be accessed from the music tab.`], execute(() => { player.setQuestProgress('tyn:goblin_diplomacy', 40); }), ], ); }, 45: async (player, npc) => { await dialogue( [player, { npc, key: 'guide' }], [ player => [Emote.HAPPY, `That music tab is pretty nostalgic, I'll give ya that.`], guide => [Emote.LAUGH, `Isn't it? Sometimes I can hear Harmony in my sleep.`], player => [Emote.WONDERING, `So what's next?`], guide => [Emote.GENERIC, `Next you'll be moving on to my friend Harlan, to learn about your inventory and skills.`], player => [Emote.HAPPY, `So I finally get to kill something?`], guide => [Emote.SAD, `That's all you adventurers ever want to do...`], guide => [ Emote.GENERIC, `Oh well. Head on over to Harlan, the melee combat tutor, and I'm sure he'll show you how to kill something.`, ], execute(() => { player.setQuestProgress('tyn:goblin_diplomacy', 50); }), ], ); }, 50: async (player, npc) => { await dialogue( [player, { npc, key: 'guide' }], [guide => [Emote.GENERIC, `Please speak to my friend Harlan, the melee combat tutor, to continue.`]], ); }, }; ================================================ FILE: src/plugins/quests/goblin-diplomacy-tutorial/stage-handler.ts ================================================ import { findNpc } from '@engine/config/config-handler'; import type { QuestStageHandler } from '@engine/config/quest-config'; import { tabIndex } from '@engine/interface/interface-state'; import { activeWorld } from '@engine/world'; import { dialogue } from '@engine/world/actor/dialogue'; import { Cutscene } from '@engine/world/actor/player/cutscenes'; import { soundIds } from '@engine/world/config/sound-ids'; import { schedule } from '@engine/world/task'; import { npcHint, showTabWidgetHint, spawnGoblinBoi, startTutorial, tutorialHandler, unlockAvailableTabs, } from '@plugins/quests/goblin-diplomacy-tutorial/goblin-diplomacy-quest.plugin'; export const goblinDiplomacyStageHandler: QuestStageHandler = { 0: async player => { await startTutorial(player); player.setQuestProgress('tyn:goblin_diplomacy', 5); await tutorialHandler(player); }, 5: async player => { npcHint(player, 'rs:runescape_guide'); await dialogue([player], [titled => [`Getting Started`, `\nWelcome to RuneScape!\nSpeak with the Guide to begin your journey.`]], { permanent: true, }); }, 10: player => { showTabWidgetHint( player, tabIndex['settings'], 1, 15, `Game Options`, `The Options menu can be used to modify various game settings.\n` + `Click the blinking icon to open the Options menu.\n\n` + `When you're finished, speak with the Guide to continue.`, ); }, 15: player => { npcHint(player, 'rs:runescape_guide'); unlockAvailableTabs(player, 1); dialogue([player], [titled => [`Getting Started`, `\nSpeak with the Guide to continue.`]], { permanent: true, }); }, 20: player => { showTabWidgetHint(player, tabIndex['friends'], 2, 25, `Friends List`, `\nKeep track of your friends via the Friends List.`); }, 25: player => { showTabWidgetHint( player, tabIndex['ignores'], 3, 30, `Ignore List`, `\nThe Ignore List allows you to block messages from other users.\n` + `Check it out by clicking the blinking icon at the bottom right.`, ); }, 30: player => { showTabWidgetHint( player, tabIndex['emotes'], 4, 35, `Emotes`, `Perform emotes for other players via the Emotes tab.\n\n` + `Click on the blinking Emotes tab to see the list of emotes you can perform, then speak with the Guide to continue.`, ); }, 35: player => { npcHint(player, 'rs:runescape_guide'); unlockAvailableTabs(player, 4); dialogue([player], [titled => [`Continue`, `\nSpeak with the Guide to continue.`]], { permanent: true, }); }, 40: player => { showTabWidgetHint( player, tabIndex['music'], 5, 45, `Music`, `Check out the music tab to view and play all of your favorite RuneScape tracks!\n` + `Once you've unlocked them, of course.`, ); }, 45: player => { npcHint(player, 'rs:runescape_guide'); unlockAvailableTabs(player, 5); dialogue([player], [titled => [`Continue`, `\nSpeak with the Guide to continue.`]], { permanent: true, }); }, 50: player => { player.metadata.blockObjectInteractions = false; npcHint(player, 'rs:melee_combat_tutor'); unlockAvailableTabs(player, 5); dialogue([player], [titled => [`Continue`, `\nSpeak with the Melee Combat Tutor to continue.`]], { permanent: true, }); }, 55: player => { showTabWidgetHint( player, tabIndex['inventory'], 6, 60, `Inventory`, `Your inventory contains any items held on your person that aren't equipped. ` + `Click the blinking backpack icon to open your inventory.`, ); }, 60: player => { npcHint(player, 'rs:melee_combat_tutor'); unlockAvailableTabs(player, 6); dialogue([player], [titled => [`Continue`, `\nTalk-to the Melee Combat Tutor to continue.`]], { permanent: true, }); }, 65: player => { showTabWidgetHint( player, tabIndex['skills'], 7, 70, `Skills`, `You can see your character's skill levels on the Skills tab, including your current number of hitpoints. ` + `If your hitpoints ever reach zero, you'll die - so be careful!`, ); }, 70: player => { npcHint(player, 'rs:melee_combat_tutor'); unlockAvailableTabs(player, 7); dialogue([player], [titled => [`Continue`, `\nTalk-to the Melee Combat Tutor to continue.`]], { permanent: true, }); }, 75: player => { showTabWidgetHint( player, tabIndex['equipment'], 8, 80, `Equipment`, `The equipment tab contains details on everything you have equipped, as well as any stat ` + `bonuses received from your equipment.`, ); }, 80: player => { npcHint(player, 'rs:melee_combat_tutor'); unlockAvailableTabs(player, 8); dialogue([player], [titled => [`Continue`, `\nTalk-to the Melee Combat Tutor to continue.`]], { permanent: true, }); }, 85: player => { unlockAvailableTabs(player, 8); dialogue([player], [titled => [`Continue`, `\nEquip the Training sword and shield.`]], { permanent: true, }); }, 90: async player => { npcHint(player, 'rs:melee_combat_tutor'); unlockAvailableTabs(player, 8); dialogue([player], [titled => [`Continue`, `\nTalk-to the Melee Combat Tutor to continue.`]], { permanent: true, }); // @TODO vvv this is all placeholder code for the cutscene that will be needed later :) await spawnGoblinBoi(player, 'beginning'); await schedule(10); const cameraX = 3219; const cameraY = 3240; const cameraHeight = 320; const lookX = 3219; const lookY = 3246; const lookHeight = 300; const speed = 0; const acceleration = 64; player.cutscene = new Cutscene(player); player.cutscene.snapCameraTo(cameraX, cameraY, cameraHeight, speed, acceleration); player.cutscene.lookAt(lookX, lookY, lookHeight, speed, acceleration); await schedule(3); function getAnim() { const goblinDetails = findNpc('rs:goblin'); const anims = goblinDetails.combatAnimations; if (!anims) { return null; } if (!anims.attack) { return null; } if (Array.isArray(anims.attack)) { return anims.attack[0]; } return anims.attack; } const goblinAnim = getAnim(); if (goblinAnim !== null) { activeWorld.findNpcsByKey('rs:goblin', player.instance.instanceId)[0].playAnimation(goblinAnim); } player.playSound(soundIds.npc.human.maleDefence, 5); }, }; ================================================ FILE: src/plugins/quests/quest-journal.plugin.ts ================================================ import type { buttonActionHandler } from '@engine/action/pipe/button.action'; import { widgets } from '@engine/config/config-handler'; import type { QuestKey } from '@engine/config/quest-config'; import { questMap } from '@engine/plugins/loader'; import { wrapText } from '@engine/util/strings'; import type { Quest } from '@engine/world/actor/player/quest'; export const handler: buttonActionHandler = async ({ player, buttonId }) => { const quest = Object.values(questMap).find(quest => quest.questTabId === buttonId); if (!quest) { return; } const [playerQuest] = player.quests.filter(playerQuest => playerQuest.questId === quest.id); let playerStage: QuestKey = 0; if (playerQuest && playerQuest.progress !== undefined) { playerStage = playerQuest.progress; } const journalHandler = quest.journalHandler[playerStage]; if (journalHandler === undefined) { const questJournalStages = Object.keys(quest.journalHandler); let journalEntry; for (const stage of questJournalStages) { const stageNum = parseInt(stage, 10); if (isNaN(stageNum) || playerStage === 'complete') { continue; } if (stageNum <= playerStage) { journalEntry = stage; } else { break; } } } const color = 128; let text: string = ''; if (typeof journalHandler === 'function') { text = await Promise.resolve(journalHandler(player)); } else if (typeof journalHandler === 'string') { text = journalHandler; } let lines: string[]; if (text) { lines = wrapText(text as string, 395); } else { lines = ['Invalid Quest Stage']; } player.modifyWidget(widgets.questJournal, { childId: 2, text: '@dre@' + quest.name }); for (let i = 0; i <= 100; i++) { if (i === 0) { player.modifyWidget(widgets.questJournal, { childId: 3, text: `${lines[0]}` }); continue; } if (lines.length > i) { player.modifyWidget(widgets.questJournal, { childId: i + 4, text: `${lines[i]}` }); } else { player.modifyWidget(widgets.questJournal, { childId: i + 4, text: '' }); } } player.interfaceState.openWidget(widgets.questJournal, { slot: 'screen', multi: false, }); }; export default { pluginId: 'rs:quest_journal', hooks: [{ type: 'button', widgetId: widgets.questTab, handler }], }; ================================================ FILE: src/plugins/quests/witchs-potion-quest.plugin.ts ================================================ import type { npcInteractionActionHandler } from '@engine/action/pipe/npc-interaction.action'; import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import type { QuestJournalHandler } from '@engine/config/quest-config'; import { Emote, dialogue, execute, goto } from '@engine/world/actor/dialogue'; import type { Player } from '@engine/world/actor/player/player'; import { Quest } from '@engine/world/actor/player/quest'; import { itemIds } from '@engine/world/config/item-ids'; import { Position } from '@engine/world/position'; const journalHandler: QuestJournalHandler = { 0: `I can start this quest by speaking to Hetty in her house in Rimmington, West of Port Sarim`, 50: player => { let questLog = `I spoke to Hetty in her house at Rimmington. Hetty told me she could increase my magic power if I can bring her certain ingredients for a potion. Hetty needs me to bring her the following:`; const questLogIngredientData = [ { itemId: itemIds.witchesPotion.ratsTail, haveText: `I have a rat's tail with me,`, dontHaveText: `A rat's tail, I could get one from a small rat.`, }, { itemId: itemIds.witchesPotion.burntMeat, haveText: `I have a piece of burnt meat with me,`, dontHaveText: `A piece of burnt meat, I could thoroughly cook a piece of raw beef.`, }, { itemId: itemIds.witchesPotion.onion, haveText: `I have an onion with me,`, dontHaveText: `An onion, I could probably find one at a farm.`, }, { itemId: itemIds.witchesPotion.eyeOfNewt, haveText: `I have an eye of newt with me,`, dontHaveText: `An eye of newt, maybe the Magic shop in Port Sarim would sell me this?`, }, ]; let ingredientsObtained = 0; for (const ingredient of questLogIngredientData) { questLog += `\n`; if (player.hasItemInInventory(ingredient.itemId)) { questLog += ingredient.haveText; ingredientsObtained++; } else { questLog += ingredient.dontHaveText; } } if (ingredientsObtained === 4) { questLog += `\nI should bring these ingredients to Hetty.`; } return questLog; }, 75: `I brought her an onion, a rat's tail, a piece of burnt meat and an eye of newt which she used to make a potion. I should drink from the cauldron and improve my magic!`, complete: `I brought her an onion, a rat's tail, a piece of burnt meat and an eye of newt which she used to make a potion.\n I drank from the cauldron and my magic power increased!\n QUEST COMPLETE!`, }; const drinkThePotionDialogue: npcInteractionActionHandler = details => { const { player, npc } = details; player.face(npc); dialogue( [player, { npc, key: 'hetty' }], [hetty => [Emote.ANGRY, `Well are you going to drink the potion or not?`], player => [Emote.GENERIC, `Yes, I will.`]], ); }; const drinkFromCauldron: objectInteractionActionHandler = async details => { const { player, object } = details; let questComplete = false; player.face(new Position(object.x, object.y)); await dialogue( [player], [ text => `You drink from the cauldron. It tastes horrible!\nYou feel yourself imbued with power.`, execute(() => { questComplete = true; }), ], ); if (questComplete) { player.setQuestProgress('rs:witchs_potion', `complete`); } }; const attemptToDrinkBeforeAllowed: objectInteractionActionHandler = async details => { const { player, object } = details; player.face(new Position(object.x, object.y)); await dialogue([player], [player => [Emote.GENERIC, `As nice as that looks I think I'll give it a miss for now.`]]); }; const dialogueIngredientQuestions: npcInteractionActionHandler = details => { const { player, npc } = details; const hasRatsTail = player.hasItemInInventory(itemIds.witchesPotion.ratsTail); const hasBurntMeat = player.hasItemInInventory(itemIds.witchesPotion.burntMeat); const hasOnion = player.hasItemInInventory(itemIds.witchesPotion.onion); const hasEyeOfNewt = player.hasItemInInventory(itemIds.witchesPotion.eyeOfNewt); const ingredients = [ { itemId: itemIds.witchesPotion.ratsTail, haveText: `I have the rat's tail (ewww), `, dontHaveText: `I don't have a rat's tail, `, }, { itemId: itemIds.witchesPotion.burntMeat, haveText: `I have the burnt meat, `, dontHaveText: `I don't have any burnt meat, `, }, { itemId: itemIds.witchesPotion.onion, haveText: `I have an onion, and `, dontHaveText: `I don't have an onion, and `, }, { itemId: itemIds.witchesPotion.eyeOfNewt, haveText: `I have the eye of newt, yum!`, dontHaveText: `I don't have an eye of newt.`, }, ]; let requirementsDialogue = ``; for (const ingredient of ingredients) { if (player.hasItemInInventory(ingredient.itemId)) { requirementsDialogue += ingredient.haveText; } else { requirementsDialogue += ingredient.dontHaveText; } } dialogue( [player, { npc, key: 'hetty' }], [ goto(() => { const count = [hasRatsTail, hasEyeOfNewt, hasOnion, hasBurntMeat].filter(value => value === true).length; if (count === 4) { return 'tag_ALL_INGREDIENTS'; } else if (count === 0) { return 'tag_NO_INGREDIENTS'; } else { return 'tag_SOME_INGREDIENTS'; } }), (subtree, tag_ALL_INGREDIENTS) => [ hetty => [Emote.HAPPY, `So have you found the things for the potion?`], player => [Emote.HAPPY, `Yes I have everything!`], hetty => [Emote.HAPPY, `Excellent, can I have them then?`], (text, tag_has_ingredients) => `You pass the ingredients to Hetty and she puts them all into her cauldron. Hetty closes her eyes and begins to chant. The cauldron bubbles mysteriously.`, player => [Emote.GENERIC, `Well, is it ready?`], execute(() => { player.removeFirstItem(itemIds.witchesPotion.ratsTail); player.removeFirstItem(itemIds.witchesPotion.onion); player.removeFirstItem(itemIds.witchesPotion.burntMeat); player.removeFirstItem(itemIds.witchesPotion.eyeOfNewt); player.setQuestProgress(`rs:witchs_potion`, 75); }), hetty => [Emote.HAPPY, `Ok, now drink from the cauldron.`], ], (subtree, tag_NO_INGREDIENTS) => [ player => [Emote.HAPPY, `I've been looking for those ingredients.`], hetty => [Emote.HAPPY, `So what have you found so far?`], player => [Emote.GENERIC, `I'm afraid I don't have any of them yet.`], hetty => [ Emote.SAD, `Well I can't make the potion without them! Remember... You need an eye of newt, a rat's tail, an onion, and a piece of burnt meat. Off you go dear!`, ], ], (subtree, tag_SOME_INGREDIENTS) => [ player => [Emote.HAPPY, `I've been looking for those ingredients.`], hetty => [Emote.HAPPY, `So what have you found so far?`], player => [Emote.GENERIC, requirementsDialogue], hetty => [Emote.GENERIC, `Great, but I'll need the other ingredients as well.`], ], ], ); }; const afterQuestDialogue: npcInteractionActionHandler = details => { const { player, npc } = details; player.face(npc); dialogue( [player, { npc, key: 'hetty' }], [ hetty => [Emote.HAPPY, `How's your magic coming along?`], player => [Emote.HAPPY, `I'm practicing and slowly getting better.`], hetty => [Emote.HAPPY, `Good, good.`], ], ); }; const startQuestAction: npcInteractionActionHandler = async details => { const { player, npc } = details; player.face(npc); npc.face(player); let beginQuest = false; await dialogue( [player, { npc, key: 'hetty' }], [ hetty => [Emote.WONDERING, 'What could you want with an old woman like me?'], options => [ `I am in search of a quest.`, [ (player, tag_search_of_quest) => [Emote.GENERIC, `I am in search of a quest.`], hetty => [Emote.HAPPY, `Hmmm... Maybe I can think of something for you.`], hetty => [Emote.HAPPY, `Would you like to become more proficient in the dark arts?`], options => [ `Yes, help me become one with my darker side.`, [player => [Emote.HAPPY, `Yes, help me become one with my darker side.`], goto(`tag_darker_side`)], `No, I have my principles and honour.`, [ player => [Emote.GENERIC, `No, I have my principles and honour.`], hetty => [Emote.SAD, `Suit yourself, but you're missing out.`], ], `What, you mean improve my magic?`, [ player => [Emote.SAD, 'What, you mean improve my magic?'], text => `The witch sighs.`, hetty => [Emote.GENERIC, 'Yes, improve your magic...'], hetty => [Emote.SAD, 'Do you have no sense of drama?'], options => [ `Yes, I'd like to improve my magic.`, [ player => [Emote.GENERIC, `Yes, I'd like to improve my magic.`], (hetty, tag_darker_side) => [ Emote.HAPPY, `Okay, I'm going to make a potion to help bring out your darker self.`, ], hetty => [Emote.GENERIC, `You will need certain ingredients.`], player => [Emote.GENERIC, `What do I need?`], execute(() => { beginQuest = true; }), hetty => [ Emote.WONDERING, `You need an eye of newt, a rat's tail, an onion... Oh and a piece of burnt meat.`, ], player => [Emote.HAPPY, `Great, I'll go and get them.`], ], `No, I'm not interested.`, [ player => [Emote.SAD, `No, I'm not interested.`], hetty => [Emote.SAD, `Many aren't to start off with.`], text => `The witch smiles mysteriously.`, hetty => [Emote.GENERIC, `But I think you'll be drawn back to this place.`], ], `Show me the mysteries of the dark arts...`, [player => [Emote.GENERIC, `Show me the mysteries of the dark arts...`], goto(`tag_darker_side`)], ], ], ], ], `I've heard that you are a witch.`, [ player => [Emote.HAPPY, `I've heard that you are a witch.`], hetty => [Emote.HAPPY, `Yes it does seem to be getting fairly common knowledge.`], hetty => [Emote.WORRIED, `I fear I may get a visit from the witch hunters of Falador before long.`], options => [ `I am in search of a quest.`, [goto('tag_search_of_quest')], `Goodbye.`, [player => [Emote.VERY_SAD, `Goodbye.`]], ], ], ], ], ); if (beginQuest) { player.setQuestProgress('rs:witchs_potion', 50); } }; export default { pluginId: 'rs:witchs_potion_quest', quests: [ new Quest({ id: 'rs:witchs_potion', questTabId: 42, name: `Witch's Potion`, points: 1, journalHandler, onComplete: { questCompleteWidget: { rewardText: ['325 Magic XP'], itemId: 221, modelZoom: 240, modelRotationX: 180, modelRotationY: 180, }, giveRewards: (player: Player): void => player.skills.magic.addExp(325), }, }), ], hooks: [ { type: 'npc_interaction', npcs: 'rs:hetty', options: 'talk-to', walkTo: true, handler: startQuestAction, }, { type: 'npc_interaction', questRequirement: { questId: 'rs:witchs_potion', stage: 50, }, npcs: 'rs:hetty', options: 'talk-to', walkTo: true, handler: dialogueIngredientQuestions, }, { type: 'npc_interaction', questRequirement: { questId: 'rs:witchs_potion', stage: 75, }, npcs: 'rs:hetty', options: 'talk-to', walkTo: true, handler: drinkThePotionDialogue, }, { type: 'npc_interaction', questRequirement: { questId: 'rs:witchs_potion', stage: 'complete', }, npcs: 'rs:hetty', options: 'talk-to', walkTo: true, handler: afterQuestDialogue, }, { type: 'object_interaction', objectIds: 2024, questRequirement: { questId: 'rs:witchs_potion', stage: 75, }, options: 'drink from', walkTo: true, handler: drinkFromCauldron, }, { type: 'object_interaction', objectIds: 2024, options: 'drink from', walkTo: true, handler: attemptToDrinkBeforeAllowed, }, ], }; ================================================ FILE: src/plugins/skills/construction/con-constants.ts ================================================ import { Position } from '@engine/world/position'; export const MAP_SIZE = 13; export type RoomType = | 'empty' | 'empty_grass' | 'garden' | 'formal_garden' | 'parlor' | 'kitchen' | 'dining_room' | 'bedroom' | 'skill_hall' | 'quest_hall' | 'portal_chamber' | 'combat_room' | 'games_room' | 'treasure_room' | 'chapel' | 'study' | 'throne_room' | 'workshop' | 'oubliette' | 'costume_room'; export const RoomStyle = { basic_wood: 0, basic_stone: 1, whitewashed_stone: 2, fremennik_wood: 3, tropical_wood: 4, fancy_stone: 5, }; /** * A map of room types to their respective world map template positions within the game. */ export const roomTemplates: { [key in RoomType]: Position } = { empty: new Position(1856, 5056), empty_grass: new Position(1864, 5056), garden: new Position(1856, 5064), formal_garden: new Position(1872, 5064), parlor: new Position(1856, 5112), kitchen: new Position(1872, 5112), dining_room: new Position(1888, 5112), bedroom: new Position(1904, 5112), skill_hall: new Position(1864, 5104), quest_hall: new Position(1912, 5104), portal_chamber: new Position(1864, 5088), combat_room: new Position(1880, 5088), games_room: new Position(1896, 5088), treasure_room: new Position(1912, 5088), chapel: new Position(1872, 5096), study: new Position(1888, 5096), throne_room: new Position(1904, 5096), workshop: new Position(1856, 5096), oubliette: new Position(1904, 5080), costume_room: new Position(1904, 5064, 0), }; /** * A map of room builder widget button ids to their respective room types. */ export const roomBuilderButtonMap: { [key: number]: RoomType } = { 160: 'parlor', 161: 'garden', 162: 'kitchen', 163: 'dining_room', 164: 'workshop', 165: 'bedroom', 166: 'skill_hall', 167: 'games_room', 168: 'combat_room', 169: 'quest_hall', 170: 'study', 171: 'costume_room', 172: 'chapel', 173: 'portal_chamber', 174: 'formal_garden', 175: 'throne_room', 176: 'oubliette', 177: 'treasure_room', // @TODO dungeon corridor 178: 'treasure_room', // @TODO dungeon junction 179: 'treasure_room', // @TODO dungeon stair 180: 'treasure_room', }; export const instance1 = new Position(6400, 6400); export const instance1PohSpawn = new Position(6400 + 36, 6400 + 36); export const instance1Max = new Position(6400 + 64, 6400 + 64); export const instance2 = new Position(6400, 6464); export const instance2PohSpawn = new Position(6400 + 36, 6464 + 36); // for reference export const instance2Max = new Position(6400 + 64, 6464 + 64); // Standard home outer door ids: closed[13100, 13101], open[13102, 13103] ================================================ FILE: src/plugins/skills/construction/home-saver.ts ================================================ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import type { Player } from '@engine/world/actor/player/player'; import type { ConstructedRegion } from '@engine/world/map/region'; import type { Room } from '@plugins/skills/construction/house'; import { House } from '@plugins/skills/construction/house'; import { logger } from '@runejs/common'; import JSON5 from 'json5'; /** * Gets the PoH save file name for the given player. * @param player */ const getSaveFileName = (player: Player): string => `${player.username.toLowerCase().replace(/ /g, '_')}.json5`; /** * Loads and returns the given player's PoH as a ConstructedRegion object. * Returns null if the player does not have a house. * @param player */ export const loadHouse = (player: Player): House | null => { const houseSaveDir = join('data', 'houses'); if (!existsSync(houseSaveDir)) { mkdirSync(houseSaveDir); return null; } const filePath = join(houseSaveDir, getSaveFileName(player)); try { const customMapFile = readFileSync(filePath, 'utf-8'); if (!customMapFile) { return null; } const customMap = JSON5.parse(customMapFile); if (!customMap) { return null; } const loadedHouse = customMap as House; const house = new House(); house.copyRooms(loadedHouse.rooms); return house; } catch (error) { logger.error(`Error loading player house for ${player.username}.`); logger.error(error); return null; } }; /** * Saves the given player's house as a JSON5 file. * @param player */ export const saveHouse = (player: Player): void => { const customMap = player.metadata.customMap as ConstructedRegion; if (!customMap) { return; } const houseSaveDir = join('data', 'houses'); if (!existsSync(houseSaveDir)) { mkdirSync(houseSaveDir); } const filePath = join(houseSaveDir, getSaveFileName(player)); const house = new House(); house.rooms = customMap.chunks as Room[][][]; try { writeFileSync(filePath, JSON5.stringify(house, null, 4)); } catch (error) { logger.error(`Error saving player house for ${player.username}.`); logger.error(error); } }; ================================================ FILE: src/plugins/skills/construction/house.ts ================================================ import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; import type { ConstructedRegion } from '@engine/world/map/region'; import { ConstructedChunk } from '@engine/world/map/region'; import type { Position } from '@engine/world/position'; import type { RoomType } from '@plugins/skills/construction/con-constants'; import { MAP_SIZE, instance1, instance1Max, instance1PohSpawn, instance2, instance2Max, roomTemplates, } from '@plugins/skills/construction/con-constants'; import { loadHouse } from '@plugins/skills/construction/home-saver'; export const openHouse = (player: Player): void => { let pohPosition: Position = instance1; let playerSpawn: Position = instance1PohSpawn; if (player.position.within(instance1, instance1Max, false)) { playerSpawn = player.position.copy().setY(player.position.y + 64); pohPosition = instance2; } else if (player.position.within(instance2, instance2Max, false)) { playerSpawn = player.position.copy().setY(player.position.y - 64); } const playerHouse = loadHouse(player); if (playerHouse) { player.metadata.customMap = { renderPosition: pohPosition, chunks: playerHouse.rooms, } as ConstructedRegion; } player.teleport(playerSpawn); if (!player.metadata.customMap) { const house = new House(); house.rooms[0][6][6] = new Room('garden'); player.metadata.customMap = { renderPosition: pohPosition, chunks: house.rooms, } as ConstructedRegion; } else { player.metadata.customMap.renderPosition = pohPosition; } for (let plane = 0; plane < 3; plane++) { for (let chunkX = 0; chunkX < 13; chunkX++) { for (let chunkY = 0; chunkY < 13; chunkY++) { const room = player.metadata.customMap.chunks[plane][chunkX][chunkY]; if (!room) { continue; } const templatePosition = room.templatePosition; // load all the PoH template maps into memory so that their collision maps are generated activeWorld.chunkManager.getChunk(templatePosition); } } } player.sendMessage(`Welcome home.`); }; export class House { public rooms: (Room | null)[][][]; public constructor() { this.rooms = new Array(4); for (let level = 0; level < 4; level++) { this.rooms[level] = new Array(MAP_SIZE); for (let x = 0; x < MAP_SIZE; x++) { this.rooms[level][x] = new Array(MAP_SIZE).fill(null); if (level === 0) { for (let y = 0; y < MAP_SIZE; y++) { this.rooms[level][x][y] = new Room('empty_grass'); } } } } } public copyRooms(rooms: (Room | null)[][][]): void { for (let level = 0; level < 4; level++) { for (let x = 0; x < MAP_SIZE; x++) { for (let y = 0; y < MAP_SIZE; y++) { const existingRoom = rooms[level][x][y] ?? null; this.rooms[level][x][y] = existingRoom ? new Room(existingRoom.type, existingRoom.orientation) : null; } } } } } export class Room extends ConstructedChunk { public readonly type: RoomType; public constructor(type: RoomType, orientation: number = 0) { super(orientation); this.type = type; } public getTemplatePosition(): Position { return roomTemplates[this.type]; } } ================================================ FILE: src/plugins/skills/construction/index.ts ================================================ import type { PlayerCommandAction } from '@engine/action/pipe/player-command.action'; import type { PlayerInitAction } from '@engine/action/pipe/player-init.action'; import { saveHouse } from '@plugins/skills/construction/home-saver'; import { openHouse } from '@plugins/skills/construction/house'; import { doorHotspotHandler, roomBuilderWidgetHandler } from '@plugins/skills/construction/room-builder'; import { instance1, instance1Max, instance2, instance2Max, roomBuilderButtonMap } from './con-constants'; export default { pluginId: 'rs:construction', hooks: [ { type: 'button', widgetIds: 402, buttonIds: Object.keys(roomBuilderButtonMap).map(key => parseInt(key, 10)), handler: roomBuilderWidgetHandler, }, { type: 'object_interaction', objectIds: [15313, 15314], options: 'build', walkTo: true, handler: doorHotspotHandler, }, { type: 'player_command', commands: ['con', 'poh', 'house'], handler: ({ player }: PlayerCommandAction): void => openHouse(player), }, { type: 'player_command', commands: ['savepoh', 'savehouse'], handler: ({ player }: PlayerCommandAction): void => { player.sendMessage(`Saving house data...`); saveHouse(player); }, }, { type: 'player_init', handler: ({ player }: PlayerInitAction): void => { if (player.position.within(instance1, instance1Max, false) || player.position.within(instance2, instance2Max, false)) { openHouse(player); } }, }, ], }; ================================================ FILE: src/plugins/skills/construction/room-builder.ts ================================================ import type { buttonActionHandler } from '@engine/action/pipe/button.action'; import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { dialogue, execute, goto } from '@engine/world/actor/dialogue'; import type { Player } from '@engine/world/actor/player/player'; import type { Coords } from '@engine/world/position'; import { MAP_SIZE, roomBuilderButtonMap } from '@plugins/skills/construction/con-constants'; import { Room, openHouse } from '@plugins/skills/construction/house'; import { getCurrentRoom } from '@plugins/skills/construction/util'; import { logger } from '@runejs/common'; const newRoomOriention = (player: Player): number => { const currentRoom = getCurrentRoom(player); if (!currentRoom) { return 0; } const playerLocalX = player.position.localX; const playerLocalY = player.position.localY; let deltaX = 0; let deltaY = 0; let orientation = 0; if (playerLocalX === 7) { // build east deltaX = 1; orientation = 1; } else if (playerLocalX === 0) { // build west deltaX = -1; orientation = 3; } else if (playerLocalY === 7) { // build north deltaY = 1; orientation = 0; } else if (playerLocalY === 0) { // build south deltaY = -1; orientation = 2; } return orientation; }; export const canBuildNewRoom = (player: Player): Coords | null => { const currentRoom = getCurrentRoom(player); if (!currentRoom) { return null; } const playerLocalX = player.position.localX; const playerLocalY = player.position.localY; let buildX = currentRoom.x; let buildY = currentRoom.y; if (playerLocalX === 7) { // build east if (currentRoom.x < MAP_SIZE - 3) { buildX = currentRoom.x + 1; } } else if (playerLocalX === 0) { // build west if (currentRoom.x > 2) { buildX = currentRoom.x - 1; } } else if (playerLocalY === 7) { // build north if (currentRoom.y < MAP_SIZE - 3) { buildY = currentRoom.y + 1; } } else if (playerLocalY === 0) { // build south if (currentRoom.y > 2) { buildY = currentRoom.y - 1; } } if (buildX === currentRoom.x && buildY === currentRoom.y) { player.sendMessage(`You can not build there.`); return null; } const playerCustomMap = player.metadata.customMap; if (!playerCustomMap) { logger.error(`Player ${player.username} does not have a custom map.`); return null; } const rooms = playerCustomMap.chunks as Room[][][]; const existingRoom = rooms[player.position.level][buildX][buildY]; if (existingRoom && existingRoom.type !== 'empty_grass' && existingRoom.type !== 'empty') { player.sendMessage(`${existingRoom.type} already exists there`); // @TODO return null; } return { x: buildX, y: buildY, level: player.position.level, }; }; export const roomBuilderWidgetHandler: buttonActionHandler = async ({ player, buttonId }) => { const newRoomCoords = canBuildNewRoom(player); if (!newRoomCoords) { return; } const chosenRoomType = roomBuilderButtonMap[buttonId]; if (!chosenRoomType) { return; } const playerCustomMap = player.metadata.customMap; if (!playerCustomMap) { logger.error(`Player ${player.username} does not have a custom map.`); return; } const createdRoom = new Room(chosenRoomType, newRoomOriention(player)); playerCustomMap.chunks[newRoomCoords.level][newRoomCoords.x][newRoomCoords.y] = createdRoom; player.interfaceState.closeAllSlots(); openHouse(player); await dialogue( [player], [ (options, tag_Home) => [ 'Rotate Counter-Clockwise', [ execute(() => { createdRoom.orientation = createdRoom.orientation > 0 ? createdRoom.orientation - 1 : 3; openHouse(player); }), goto('tag_Home'), ], 'Rotate Clockwise', [ execute(() => { createdRoom.orientation = createdRoom.orientation < 3 ? createdRoom.orientation + 1 : 0; openHouse(player); }), goto('tag_Home'), ], 'Accept', [execute(() => {})], ], ], ); }; export const doorHotspotHandler: objectInteractionActionHandler = ({ player }) => { if (!canBuildNewRoom(player)) { return; } player.interfaceState.openWidget(402, { slot: 'screen' }); }; ================================================ FILE: src/plugins/skills/construction/util.ts ================================================ import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; import type { Coords } from '@engine/world/position'; import { Position } from '@engine/world/position'; /** * Finds the local coordinates of the room that the player is currently in within their PoH. * Returns null if the player is not currently in a custom map region. * @param player The player to find the room for. */ export const getCurrentRoom = (player: Player): Coords | null => { const customMap = player.metadata?.customMap; if (!customMap) { return null; } const mapWorldX = customMap.renderPosition.x; const mapWorldY = customMap.renderPosition.y; const topCornerMapChunk = activeWorld.chunkManager.getChunkForWorldPosition(new Position(mapWorldX, mapWorldY, player.position.level)); const playerChunk = activeWorld.chunkManager.getChunkForWorldPosition(player.position); const currentRoomX = playerChunk.position.x - (topCornerMapChunk.position.x - 2); const currentRoomY = playerChunk.position.y - (topCornerMapChunk.position.y - 2); return { x: currentRoomX, y: currentRoomY, level: player.position.level, }; }; ================================================ FILE: src/plugins/skills/crafting/sheep-plugin.plugin.ts ================================================ import type { itemOnNpcActionHandler } from '@engine/action/pipe/item-on-npc.action'; import type { npcInitActionHandler } from '@engine/action/pipe/npc-init.action'; import { animationIds } from '@engine/world/config/animation-ids'; import { itemIds } from '@engine/world/config/item-ids'; import { soundIds } from '@engine/world/config/sound-ids'; const initAction: npcInitActionHandler = ({ npc }) => { // this used to use `setInterval` but will need rewriting to be synced with ticks // see https://github.com/runejs/server/issues/417 // setInterval(() => { // if(Math.random() >= 0.66) { // npc.say(`Baa!`); // npc.playSound(soundIds.sheepBaa, 4); // } // }, (Math.floor(Math.random() * 20) + 10) * World.TICK_LENGTH); }; export const shearAction: itemOnNpcActionHandler = ({ player, npc }) => { player.busy = true; player.playAnimation(animationIds.shearSheep); player.playSound(soundIds.shearSheep, 5); // set to face position, so it does not look weird when the player walk away npc.face(player.position); // this used to use `setInterval` but will need rewriting to be synced with ticks // see https://github.com/runejs/server/issues/417 player.sendMessage('[debug] see issue #417'); // setTimeout(() => { // if(Math.random() >= 0.66) { // player.sendMessage('The sheep manages to get away from you!'); // npc.forceMovement(player.faceDirection, 5); // } else { // player.sendMessage('You get some wool.'); // player.giveItem(itemIds.wool); // npc.say('Baa!'); // npc.playSound(soundIds.sheepBaa, 4); // npc.transformInto('rs:naked_sheep'); // setTimeout(() => { // npc.transformInto('rs:sheep'); // }, (Math.floor(Math.random() * 20) + 10) * World.TICK_LENGTH); // } // player.busy = false; // }, World.TICK_LENGTH); }; export default { pluginId: 'rs:sheep_shearing', hooks: [ { type: 'npc_init', npcs: 'rs:sheep', handler: initAction, }, { type: 'item_on_npc', npcs: 'rs:sheep', itemIds: [itemIds.shears, itemIds.recruitmentDrive.shears], walkTo: true, handler: shearAction, }, ], }; ================================================ FILE: src/plugins/skills/crafting/spinning-wheel.plugin.ts ================================================ import type { ButtonAction, buttonActionHandler } from '@engine/action/pipe/button.action'; import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { findItem, widgets } from '@engine/config/config-handler'; import { ActorTask } from '@engine/task/impl/actor-task'; import type { Player } from '@engine/world/actor/player/player'; import { Skill } from '@engine/world/actor/skills'; import { animationIds } from '@engine/world/config/animation-ids'; import { itemIds } from '@engine/world/config/item-ids'; import { objectIds } from '@engine/world/config/object-ids'; import { soundIds } from '@engine/world/config/sound-ids'; import { logger } from '@runejs/common'; interface Spinnable { input: number | number[]; output: number; experience: number; requiredLevel: number; } interface SpinnableButton { shouldTakeInput: boolean; count: number; spinnable: Spinnable; } const ballOfWool: Spinnable = { input: itemIds.wool, output: itemIds.ballOfWool, experience: 2.5, requiredLevel: 1 }; const bowString: Spinnable = { input: itemIds.flax, output: itemIds.bowstring, experience: 15, requiredLevel: 10 }; const rootsCbowString: Spinnable = { input: [itemIds.roots.oak, itemIds.roots.willow, itemIds.roots.maple, itemIds.roots.yew], output: itemIds.crossbowString, experience: 15, requiredLevel: 10, }; const sinewCbowString: Spinnable = { input: itemIds.sinew, output: itemIds.crossbowString, experience: 15, requiredLevel: 10, }; const magicAmuletString: Spinnable = { input: itemIds.roots.magic, output: itemIds.magicString, experience: 30, requiredLevel: 19, }; const widgetButtonIds: Map = new Map([ [100, { shouldTakeInput: false, count: 1, spinnable: ballOfWool }], [99, { shouldTakeInput: false, count: 5, spinnable: ballOfWool }], [98, { shouldTakeInput: false, count: 10, spinnable: ballOfWool }], [97, { shouldTakeInput: true, count: 0, spinnable: ballOfWool }], [95, { shouldTakeInput: false, count: 1, spinnable: bowString }], [94, { shouldTakeInput: false, count: 5, spinnable: bowString }], [93, { shouldTakeInput: false, count: 10, spinnable: bowString }], [91, { shouldTakeInput: true, count: 0, spinnable: bowString }], [107, { shouldTakeInput: false, count: 1, spinnable: magicAmuletString }], [106, { shouldTakeInput: false, count: 5, spinnable: magicAmuletString }], [105, { shouldTakeInput: false, count: 10, spinnable: magicAmuletString }], [104, { shouldTakeInput: true, count: 0, spinnable: magicAmuletString }], [121, { shouldTakeInput: false, count: 1, spinnable: rootsCbowString }], [120, { shouldTakeInput: false, count: 5, spinnable: rootsCbowString }], [119, { shouldTakeInput: false, count: 10, spinnable: rootsCbowString }], [118, { shouldTakeInput: true, count: 0, spinnable: rootsCbowString }], [114, { shouldTakeInput: false, count: 1, spinnable: sinewCbowString }], [113, { shouldTakeInput: false, count: 5, spinnable: sinewCbowString }], [112, { shouldTakeInput: false, count: 10, spinnable: sinewCbowString }], [111, { shouldTakeInput: true, count: 0, spinnable: sinewCbowString }], ]); export const openSpinningInterface: objectInteractionActionHandler = details => { details.player.interfaceState.openWidget(widgets.whatWouldYouLikeToSpin, { slot: 'screen', }); }; /** * A task to (repeatedly if needed) spin a product from a spinnable. */ class SpinProductTask extends ActorTask { /** * The number of ticks that `execute` has been called inside this task. */ private elapsedTicks = 0; /** * The number of items that should be spun. */ private count: number; /** * The number of items that have been spun. */ private created = 0; /** * The spinnable that is being used. */ private spinnable: Spinnable; /** * The currently being spun input. */ private currentItem: number; /** * The index of the current input being spun. */ private currentItemIndex = 0; constructor(player: Player, spinnable: Spinnable, count: number) { super(player); this.spinnable = spinnable; this.count = count; } public execute(): void { if (this.created === this.count) { this.stop(); return; } // As an multiple items can be used for one of the recipes, check if its an array let isArray = false; if (Array.isArray(this.spinnable.input)) { isArray = true; this.currentItem = this.spinnable.input[0]; } else { this.currentItem = this.spinnable.input; } // Check if out of input material if (!this.actor.hasItemInInventory(this.currentItem)) { let cancel = false; if (isArray) { if (this.currentItemIndex < (this.spinnable.input).length) { this.currentItemIndex++; this.currentItem = (this.spinnable.input)[this.currentItemIndex]; } else { cancel = true; } } else { cancel = true; } if (cancel) { const itemName = findItem(this.currentItem)?.name || ''; this.actor.sendMessage(`You don't have any ${itemName.toLowerCase()}.`); this.stop(); return; } } // Spinning takes 3 ticks for each item if (this.elapsedTicks % 3 === 0) { this.actor.removeFirstItem(this.currentItem); this.actor.giveItem(this.spinnable.output); this.actor.skills.addExp(Skill.CRAFTING, this.spinnable.experience); this.created++; } // animation plays once every two items if (this.elapsedTicks % 6 === 0) { this.actor.playAnimation(animationIds.spinSpinningWheel); this.actor.outgoingPackets.playSound(soundIds.spinWool, 5); } this.elapsedTicks++; } } const spinProduct: any = (details: ButtonAction, spinnable: Spinnable, count: number) => { details.player.enqueueTask(SpinProductTask, [spinnable, count]); }; export const buttonClicked: buttonActionHandler = details => { // Check if player might be spawning widget clientside if (!details.player.interfaceState.findWidget(459)) { return; } const product = widgetButtonIds.get(details.buttonId); if (!product) { logger.error(`Unhandled button id ${details.buttonId} for buttonClicked in spinning wheel.`); return; } // Close the widget as it is no longer needed details.player.interfaceState.closeAllSlots(); if (!details.player.skills.hasLevel(Skill.CRAFTING, product.spinnable.requiredLevel)) { const outputName = findItem(product.spinnable.output)?.name || ''; details.player.sendMessage( `You need a crafting level of ${product.spinnable.requiredLevel} to craft ${outputName.toLowerCase()}.`, true, ); return; } if (!product.shouldTakeInput) { // If the player has not chosen make X, we dont need to get input and can just start the crafting spinProduct(details, product.spinnable, product.count); } else { // We should prepare for a number to be sent from the client const numericInputSpinSub = details.player.numericInputEvent.subscribe(number => { actionCancelledSpinSub?.unsubscribe(); numericInputSpinSub?.unsubscribe(); // When a number is recieved we can start crafting the product spinProduct(details, product.spinnable, number); }); // If the player moves or cancels the number input, we do not want to wait for input, as they could be depositing // items into their bank. const actionCancelledSpinSub = details.player.actionsCancelled.subscribe(() => { actionCancelledSpinSub?.unsubscribe(); numericInputSpinSub?.unsubscribe(); }); // Ask the player to enter how many they want to create details.player.outgoingPackets.showNumberInputDialogue(); } }; export default { pluginId: 'rs:spinning_wheel', hooks: [ { type: 'object_interaction', objectIds: objectIds.spinningWheel, options: ['spin'], walkTo: true, handler: openSpinningInterface, }, { type: 'button', widgetId: widgets.whatWouldYouLikeToSpin, buttonIds: Array.from(widgetButtonIds.keys()), handler: buttonClicked, }, ], }; ================================================ FILE: src/plugins/skills/firemaking/chance.ts ================================================ /** * Roll a chance to light a fire. * * TODO (jameskmonger) this was ported from the old codebase and needs to be documented. * * @param logLevel The firemaking level required to light the log. * @param playerLevel The player's current firemaking level. * @returns `true` if the player successfully lights the fire, `false` otherwise. */ export const canLight = (logLevel: number, playerLevel: number): boolean => { if (playerLevel < logLevel) { return false; } playerLevel++; const hostRatio = Math.random() * logLevel; const clientRatio = Math.random() * ((playerLevel - logLevel) * (1 + logLevel * 0.01)); return hostRatio < clientRatio; }; /** * Roll a chance to 'chain' a fire. * * TODO (jameskmonger) this was ported from the old codebase and needs to be documented. * what is "chain"? * * @param logLevel The firemaking level required to light the log. * @param playerLevel The player's current firemaking level. * @returns `true` if the player successfully lights the fire, `false` otherwise. */ export const canChain = (logLevel: number, playerLevel: number): boolean => { if (playerLevel < logLevel) { return false; } playerLevel++; const hostRatio = Math.random() * logLevel; const clientRatio = Math.random() * ((playerLevel - logLevel) * (1 + logLevel * 0.01)); return clientRatio - hostRatio < 3.5; }; ================================================ FILE: src/plugins/skills/firemaking/data.ts ================================================ import { findItem } from '@engine/config/config-handler'; import type { Burnable } from './types'; // using ! here because we know the items exist export const FIREMAKING_LOGS: Burnable[] = [ { logItem: findItem('rs:logs')!, requiredLevel: 1, experienceGained: 40, }, { logItem: findItem('rs:oak_logs')!, requiredLevel: 15, experienceGained: 60, }, { logItem: findItem('rs:willow_logs')!, requiredLevel: 30, experienceGained: 90, }, { logItem: findItem('rs:teak_logs')!, requiredLevel: 35, experienceGained: 105, }, { logItem: findItem('rs:maple_logs')!, requiredLevel: 45, experienceGained: 135, }, { logItem: findItem('rs:mahogany_logs')!, requiredLevel: 50, experienceGained: 157.5, }, { logItem: findItem('rs:yew_logs')!, requiredLevel: 60, experienceGained: 202.5, }, { logItem: findItem('rs:magic_logs')!, requiredLevel: 75, experienceGained: 303.8, }, ]; ================================================ FILE: src/plugins/skills/firemaking/firemaking-task.ts ================================================ import { ActorWorldItemInteractionTask } from '@engine/task/impl/actor-world-item-interaction-task'; import type { Player } from '@engine/world/actor/player/player'; import { animationIds } from '@engine/world/config/animation-ids'; import { soundIds } from '@engine/world/config/sound-ids'; import type { WorldItem } from '@engine/world/items/world-item'; import { canLight } from './chance'; import { FIREMAKING_LOGS } from './data'; import { lightFire } from './light-fire'; import type { Burnable } from './types'; /** * A firemaking task on a {@link WorldItem} log. * * This task extends {@link ActorWorldItemInteractionTask} which is a task that * handles the interaction between an {@link Actor} and a {@link WorldItem}. * * The {@link ActorWalkToTask} (which our parent class extends) automatically * sets a {@link TaskBreakType} of {@link TaskBreakType.ON_MOVE} which will * cancel this task if the player clicks to move. * * By default, the {@link ActorWalkToTask} also sets a {@link TaskStackType} of * {@link TaskStackType.NEVER} and a {@link TaskStackGroup} of {@link TaskStackGroup.ACTION} * which means that this other actions will cancel the firemaking attempts. * * @author jameskmonger */ class FiremakingTask extends ActorWorldItemInteractionTask { /** * The log being lit. */ private logInfo: Burnable; /** * The number of ticks that `execute` has been called inside this task. */ private elapsedTicks = 0; private canLightFire = false; /** * Create a new firemaking task. * * @param player The player that is attempting to light the fire. * @param logWorldItem The world item that represents the log. */ constructor(player: Player, logWorldItem: WorldItem) { super(player, logWorldItem); const logInfo = FIREMAKING_LOGS.find(l => l.logItem.gameId === logWorldItem.itemId); if (!logInfo) { throw new Error(`Invalid firemaking log item id: ${logWorldItem.itemId}`); } this.logInfo = logInfo; } /** * Execute the main firemaking task loop. This method is called every game tick until the task is completed. * * As this task extends {@link ActorWorldItemInteractionTask}, it's important that the * {@link super.execute} method is called at the start of this method. * * The base `execute` performs a number of checks that allow this task to function healthily. */ public execute() { super.execute(); /** * As this task extends {@link ActorWorldItemInteractionTask}, the base classes {@link ActorWorldItemInteractionTask["worldItem"]} * property will be null if the item isn't valid anymore or the player isn't in the right position. * * Therefore if `worldItem` is null, we can return early here, as our player is likely walking to the task. */ if (!this.worldItem) { return; } // store the tick count before incrementing so we don't need to keep track of it in all the separate branches const tickCount = this.elapsedTicks++; if (this.canLightFire) { if (tickCount === 2) { lightFire(this.actor, this.actor.position, this.worldItem, this.logInfo.experienceGained); this.stop(); } // the rest of the function is for *attempting* to light the fire // so we can return early here return; } // play animation every 12 ticks if (tickCount % 12 === 0) { this.actor.playAnimation(animationIds.lightingFire); } // TODO (jameskmonger) reconsider this, is there a minimum tick count? // OSRS wiki implies that there isn't // https://oldschool.runescape.wiki/w/Firemaking#Success_chance const passedMinimumThreshold = tickCount > 10; this.canLightFire = passedMinimumThreshold && canLight(this.logInfo.requiredLevel, this.actor.skills.firemaking.level); // if we can now light the fire, reset the timer so that on the next tick we can begin lighting the fire if (this.canLightFire) { this.elapsedTicks = 0; this.actor.busy = true; this.actor.playSound(soundIds.fireLit, 7); return; } // play lighting sound every 4th tick if (tickCount % 4 === 0) { this.actor.playSound(soundIds.lightingFire, 10, 0); } } } /** * Run the firemaking task for a player. * * @param player The player that is attempting to light the fire. * @param worldItemLog The WorldItem that represents the log. */ export function runFiremakingTask(player: Player, worldItemLog: WorldItem) { player.enqueueTask(FiremakingTask, [worldItemLog]); } ================================================ FILE: src/plugins/skills/firemaking/index.ts ================================================ import type { ItemOnItemActionHook, itemOnItemActionHandler } from '@engine/action/pipe/item-on-item.action'; import type { ItemOnWorldItemActionHook } from '@engine/action/pipe/item-on-world-item.action'; import { itemIds } from '@engine/world/config/item-ids'; import { canChain } from './chance'; import { FIREMAKING_LOGS } from './data'; import { runFiremakingTask } from './firemaking-task'; import { canLightFireAtCurrentPosition, lightFire } from './light-fire'; /** * Action hook for lighting a log with a tinderbox in the player's inventory. */ const tinderboxOnLogHandler: itemOnItemActionHandler = details => { const { player, usedItem, usedWithItem, usedSlot, usedWithSlot } = details; if (player.metadata.lastFire && Date.now() - player.metadata.lastFire < 600) { return; } const log = usedItem.itemId !== itemIds.tinderbox ? usedItem : usedWithItem; const removeFromSlot = usedItem.itemId !== itemIds.tinderbox ? usedSlot : usedWithSlot; const skillInfo = FIREMAKING_LOGS.find(l => l.logItem.gameId === log.itemId); if (!skillInfo) { player.sendMessage(`Mishandled firemaking log ${log.itemId}.`); return; } if (player.skills.firemaking.level < skillInfo.requiredLevel) { player.sendMessage(`You need a Firemaking level of ${skillInfo.requiredLevel} to light this log.`); return; } if (!canLightFireAtCurrentPosition(player)) { player.sendMessage('You cannot light a fire here.'); return; } player.removeItem(removeFromSlot); const worldItemLog = player.instance.spawnWorldItem(log, player.position, { owner: player, expires: 300 }); // TODO (jameskmonger) chaining functionality needs documentation, I can't find anything about it online if ( player.metadata.lastFire && Date.now() - player.metadata.lastFire < 1200 && canChain(skillInfo.requiredLevel, player.skills.firemaking.level) ) { lightFire(player, player.position, worldItemLog, skillInfo.experienceGained); } else { player.sendMessage('You attempt to light the logs.'); runFiremakingTask(player, worldItemLog); } }; /** * Firemaking plugin * * TODO: * - Document/remove `canChain` functionality - this is not documented anywhere online (RS wiki etc) */ export default { pluginId: 'rs:firemaking', hooks: [ { type: 'item_on_item', items: FIREMAKING_LOGS.map(log => ({ item1: itemIds.tinderbox, item2: log.logItem.gameId })), handler: tinderboxOnLogHandler, } as ItemOnItemActionHook, { type: 'item_on_world_item', items: FIREMAKING_LOGS.map(log => ({ item: itemIds.tinderbox, worldItem: log.logItem.gameId })), handler: ({ player, usedWithItem }) => { runFiremakingTask(player, usedWithItem); }, } as ItemOnWorldItemActionHook, ], }; ================================================ FILE: src/plugins/skills/firemaking/light-fire.ts ================================================ import { randomBetween } from '@engine/util/num'; import type { Player } from '@engine/world/actor/player/player'; import { itemIds } from '@engine/world/config/item-ids'; import { objectIds } from '@engine/world/config/object-ids'; import type { WorldItem } from '@engine/world/items/world-item'; import type { Position } from '@engine/world/position'; import type { LandscapeObject } from '@runejs/filestore'; const fireDurationTicks = (): number => { return randomBetween(100, 200); // 1-2 minutes }; /** * Whether or not a fire can be lit at the player's position. * * This is `true` if there are no spawned objects at the specified position of type 10. * * Probably should be moved to a more generic location (maybe on WorldInstance) * * @param player The player attempting to light the fire. * @returns `true` if a fire can be lit at the specified position, `false` otherwise. * * @author jameskmonger */ export const canLightFireAtCurrentPosition = (player: Player): boolean => { const existingFire = player.instance .getTileModifications(player.position) .mods.spawnedObjects.find( o => o.x === player.position.x && o.y === player.position.y && o.level === player.position.level && o.type === 10, ); return existingFire === undefined; }; /** * Light a fire at the specified position. * * @param player The player lighting the fire. * @param position The position to light the fire at. * @param worldItemLog The world item representing the log. * @param burnExp The experience gained for lighting the fire. */ export const lightFire = (player: Player, position: Position, worldItemLog: WorldItem, burnExp: number): void => { if (!canLightFireAtCurrentPosition(player)) { player.sendMessage('You cannot light a fire here.'); return; } player.instance.despawnWorldItem(worldItemLog); const fireObject: LandscapeObject = { objectId: objectIds.fire, x: position.x, y: position.y, level: position.level, type: 10, orientation: 0, }; player.playAnimation(null); player.sendMessage('The fire catches and the logs begin to burn.'); player.skills.firemaking.addExp(burnExp); if (!player.walkingQueue.moveIfAble(-1, 0)) { if (!player.walkingQueue.moveIfAble(1, 0)) { if (!player.walkingQueue.moveIfAble(0, -1)) { player.walkingQueue.moveIfAble(0, 1); } } } player.instance.spawnTemporaryGameObject(fireObject, position, fireDurationTicks()).then(() => { player.instance.spawnWorldItem({ itemId: itemIds.ashes, amount: 1 }, position, { expires: 300 }); }); player.face(position, false); player.metadata.lastFire = Date.now(); player.busy = false; }; ================================================ FILE: src/plugins/skills/firemaking/types.ts ================================================ import type { ItemDetails } from '@engine/config/item-config'; /** * The definition for a burnable log. */ export type Burnable = { /** * The item details for the log. */ logItem: ItemDetails; /** * The firemaking level required to light the log. */ requiredLevel: number; /** * The experience gained for lighting the log. */ experienceGained: number; }; ================================================ FILE: src/plugins/skills/fletching/fletching-constants.ts ================================================ import { itemIds } from '@engine/world/config/item-ids'; import type { Fletchable } from '@plugins/skills/fletching/fletching-types'; export const knifeId: number = itemIds.knife; export const fletchables: Map> = new Map>([ [ 'bow(u)', new Map([ [ 'wood short', { level: 1, experience: 5, item: { itemId: itemIds.bowunstrung.woodshort, amount: 1 }, ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }], }, ], [ 'wood long', { level: 10, experience: 10, item: { itemId: itemIds.bowunstrung.woodlong, amount: 1 }, ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }], }, ], [ 'oak short', { level: 20, experience: 16.5, item: { itemId: itemIds.bowunstrung.oakshort, amount: 1 }, ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }], }, ], [ 'oak long', { level: 25, experience: 25, item: { itemId: itemIds.bowunstrung.oaklong, amount: 1 }, ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }], }, ], [ 'comp ogre', { level: 30, experience: 45, item: { itemId: itemIds.bowunstrung.compogre, amount: 1 }, ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }], }, ], [ 'willow short', { level: 35, experience: 33.3, item: { itemId: itemIds.bowunstrung.willowshort, amount: 1 }, ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }], }, ], [ 'willow long', { level: 40, experience: 41.5, item: { itemId: itemIds.bowunstrung.willowlong, amount: 1 }, ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }], }, ], [ 'maple short', { level: 50, experience: 50, item: { itemId: itemIds.bowunstrung.mapleshort, amount: 1 }, ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }], }, ], [ 'maple long', { level: 55, experience: 58.3, item: { itemId: itemIds.bowunstrung.maplelong, amount: 1 }, ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }], }, ], [ 'yew short', { level: 65, experience: 67.5, item: { itemId: itemIds.bowunstrung.yewshort, amount: 1 }, ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }], }, ], [ 'yew long', { level: 70, experience: 75, item: { itemId: itemIds.bowunstrung.yewlong, amount: 1 }, ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }], }, ], [ 'magic short', { level: 80, experience: 83.3, item: { itemId: itemIds.bowunstrung.magicshort, amount: 1 }, ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }], }, ], [ 'magic long', { level: 85, experience: 91.5, item: { itemId: itemIds.bowunstrung.magiclong, amount: 1 }, ingredient: [{ itemId: itemIds.logs.normal, amount: 1 }], }, ], ]), ], [ 'bow', new Map([ [ 'wood short', { level: 1, experience: 5, item: { itemId: itemIds.bowstrung.woodshort, amount: 1 }, ingredient: [ { itemId: itemIds.bowunstrung.woodshort, amount: 1 }, { itemId: itemIds.bowstring, amount: 1 }, ], }, ], [ 'wood long', { level: 10, experience: 10, item: { itemId: itemIds.bowstrung.woodlong, amount: 1 }, ingredient: [ { itemId: itemIds.bowunstrung.woodlong, amount: 1 }, { itemId: itemIds.bowstring, amount: 1 }, ], }, ], [ 'oak short', { level: 20, experience: 16.5, item: { itemId: itemIds.bowunstrung.oakshort, amount: 1 }, ingredient: [ { itemId: itemIds.bowunstrung.oakshort, amount: 1 }, { itemId: itemIds.bowstring, amount: 1 }, ], }, ], [ 'oak long', { level: 25, experience: 25, item: { itemId: itemIds.bowstrung.oaklong, amount: 1 }, ingredient: [ { itemId: itemIds.bowunstrung.oaklong, amount: 1 }, { itemId: itemIds.bowstring, amount: 1 }, ], }, ], [ 'comp ogre', { level: 30, experience: 45, item: { itemId: itemIds.bowstrung.compogre, amount: 1 }, ingredient: [ { itemId: itemIds.bowunstrung.compogre, amount: 1 }, { itemId: itemIds.bowstring, amount: 1 }, ], }, ], [ 'willow short', { level: 35, experience: 33.3, item: { itemId: itemIds.bowstrung.willowshort, amount: 1 }, ingredient: [ { itemId: itemIds.bowunstrung.willowshort, amount: 1 }, { itemId: itemIds.bowstring, amount: 1 }, ], }, ], [ 'willow long', { level: 40, experience: 41.5, item: { itemId: itemIds.bowstrung.willowlong, amount: 1 }, ingredient: [ { itemId: itemIds.bowunstrung.willowlong, amount: 1 }, { itemId: itemIds.bowstring, amount: 1 }, ], }, ], [ 'maple short', { level: 50, experience: 50, item: { itemId: itemIds.bowstrung.mapleshort, amount: 1 }, ingredient: [ { itemId: itemIds.bowunstrung.mapleshort, amount: 1 }, { itemId: itemIds.bowstring, amount: 1 }, ], }, ], [ 'maple long', { level: 55, experience: 58.3, item: { itemId: itemIds.bowstrung.maplelong, amount: 1 }, ingredient: [ { itemId: itemIds.bowunstrung.maplelong, amount: 1 }, { itemId: itemIds.bowstring, amount: 1 }, ], }, ], [ 'yew short', { level: 65, experience: 67.5, item: { itemId: itemIds.bowstrung.yewshort, amount: 1 }, ingredient: [ { itemId: itemIds.bowunstrung.yewshort, amount: 1 }, { itemId: itemIds.bowstring, amount: 1 }, ], }, ], [ 'yew long', { level: 70, experience: 75, item: { itemId: itemIds.bowstrung.yewlong, amount: 1 }, ingredient: [ { itemId: itemIds.bowunstrung.yewlong, amount: 1 }, { itemId: itemIds.bowstring, amount: 1 }, ], }, ], [ 'magic short', { level: 80, experience: 83.3, item: { itemId: itemIds.bowstrung.magicshort, amount: 1 }, ingredient: [ { itemId: itemIds.bowunstrung.magicshort, amount: 1 }, { itemId: itemIds.bowstring, amount: 1 }, ], }, ], [ 'magic long', { level: 85, experience: 91.5, item: { itemId: itemIds.bowstrung.magiclong, amount: 1 }, ingredient: [ { itemId: itemIds.bowunstrung.magiclong, amount: 1 }, { itemId: itemIds.bowstring, amount: 1 }, ], }, ], ]), ], ]); ================================================ FILE: src/plugins/skills/fletching/fletching-types.ts ================================================ import type { Item } from '@engine/world/items/item'; export interface Fletchable { item: Item; level: number; experience: number; ingredient: Item[]; } ================================================ FILE: src/plugins/skills/fletching/fletching.plugin.ts ================================================ //fletching stuff goes below this! lets do it! export default { pluginId: 'rs:fletching', }; ================================================ FILE: src/plugins/skills/level-up-dialogue.plugin.ts ================================================ import type { widgetInteractionActionHandler } from '@engine/action/pipe/widget-interaction.action'; const widgetIds = [158, 161, 175, 167, 171, 170, 168, 159, 177, 165, 164, 163, 160, 174, 169, 166, 157, 176, 173, 162, 172]; /** * Handles a level-up dialogue action. */ export const handler: widgetInteractionActionHandler = ({ player }) => player.interfaceState.closeChatOverlayWidget(); export default { pluginId: 'rs:close_level_up_message', hooks: [{ type: 'widget_interaction', widgetIds, handler, cancelActions: false }], }; ================================================ FILE: src/plugins/skills/mining/chance.ts ================================================ import { randomBetween } from '@engine/util/num'; import type { IHarvestable } from '@engine/world/config/harvestable-object'; /** * Roll a random number between 0 and 255 and compare it to the percent needed to mine the ore. * * @param ore The ore to mine * @param toolLevel The level of the pickaxe being used * @param miningLevel The player's mining level * * @returns True if the tree was successfully cut, false otherwise */ export const canMine = (ore: IHarvestable, toolLevel: number, miningLevel: number): boolean => { const successChance = randomBetween(0, 255); const percentNeeded = ore.baseChance + toolLevel + miningLevel; return successChance <= percentNeeded; }; ================================================ FILE: src/plugins/skills/mining/mining-task.ts ================================================ import { findItem } from '@engine/config/config-handler'; import { equipmentIndices } from '@engine/config/item-config'; import { ActorLandscapeObjectInteractionTask } from '@engine/task/impl/actor-landscape-object-interaction-task'; import { colors } from '@engine/util/colors'; import { randomBetween } from '@engine/util/num'; import { colorText } from '@engine/util/strings'; import type { Player } from '@engine/world/actor/player/player'; import { Skill } from '@engine/world/actor/skills'; import type { HarvestTool } from '@engine/world/config/harvest-tool'; import type { IHarvestable } from '@engine/world/config/harvestable-object'; import { selectWeightedItem } from '@engine/world/config/harvestable-object'; import { soundIds } from '@engine/world/config/sound-ids'; import { checkForGemBoost } from '@engine/world/skill-util/glory-boost'; import { rollGemType } from '@engine/world/skill-util/harvest-roll'; import type { LandscapeObject } from '@runejs/filestore'; import { canMine } from './chance'; /** * A task that handles mining. It is a subclass of ActorLandscapeObjectInteractionTask, which means that it will * walk to the object, and then execute the task when it is in range. * * The mining task will repeat until the player's inventory is full, or the rock is depleted, or the task is otherwise * stopped. * * @author jameskmonger */ export class MiningTask extends ActorLandscapeObjectInteractionTask { /** * The number of ticks that have elapsed since this task was started. * * We use this to determine when to mine the next ore, or play the next animation. */ private elapsedTicks = 0; /** * The name of the item that we are mining. */ private targetItemName: string; constructor( player: Player, landscapeObject: LandscapeObject, private readonly ore: IHarvestable, private readonly tool: HarvestTool, ) { super(player, landscapeObject); const itemConfigId = typeof ore.items === 'string' ? ore.items : selectWeightedItem(ore.items); const item = findItem(itemConfigId); if (!item) { throw new Error(`Could not find item with ID ${itemConfigId}`); } this.targetItemName = item.name.toLowerCase().replace(' ore', ''); } private isGemRock(): boolean { return this.landscapeObject?.objectId === 2111; } private hasChargedGlory(): boolean { const neckSlotIndex = equipmentIndices['neck']; // This is 2 const neckItem = this.actor.equipment.items[neckSlotIndex]; if (!neckItem) { return false; } const itemConfig = findItem(neckItem.itemId); if (!itemConfig) { return false; } return itemConfig.key.startsWith('rs:amulet_of_glory:charged_'); } public execute(): void { const taskIteration = this.elapsedTicks++; // This will be null if the player is not in range of the object. if (!this.landscapeObject) { return; } if (!this.hasLevel()) { this.actor.sendMessage(`You need a Mining level of ${this.ore.level} to mine this rock.`, true); return; } if (!this.hasMaterials()) { this.actor.sendMessage('You do not have a pickaxe for which you have the level to use.'); return; } // Check if the players inventory is full, and notify them if its full. if (!this.actor.inventory.hasSpace()) { this.actor.sendMessage(`Your inventory is too full to hold any more ${this.targetItemName}.`, true); this.actor.playSound(soundIds.inventoryFull); return; } // mining in original plugin took 3 ticks to mine a rock, so we'll do the same for now if (taskIteration % 3 !== 0) { return; } this.actor.playSound(soundIds.pickaxeSwing, 7, 0); this.actor.playAnimation(this.tool.animation); // Get tool level, and set it to 2 if the tool is an iron hatchet or iron pickaxe // TODO why is this set to 2? Was ported from the old code let toolLevel = this.tool.level - 1; if (this.tool.itemId === 1349 || this.tool.itemId === 1267) { toolLevel = 2; } // roll for success const succeeds = canMine({ ...this.ore, baseChance: this.getGemMiningChance() }, toolLevel, this.actor.skills.mining.level); if (!succeeds) { return; } const findsRareGem = randomBetween(1, checkForGemBoost(this.actor)) === 1; if (findsRareGem) { this.actor.sendMessage(colorText('You found a rare gem.', colors.red)); this.actor.giveItem(rollGemType()); } else { this.actor.sendMessage(`You manage to mine some ${this.targetItemName}.`); const itemToGive = typeof this.ore.items === 'string' ? this.ore.items : selectWeightedItem(this.ore.items); this.actor.giveItem(itemToGive); // TODO (Jameskmonger) handle Gem rocks and Pure essence rocks // if (itemToAdd === 1436 && details.player.skills.hasLevel(Skill.MINING, 30)) { // itemToAdd = 7936; // } // if (details.object.objectId === 2111 && details.player.skills.hasLevel(Skill.MINING, 30)) { // itemToAdd = rollGemRockResult().itemId; // } } this.actor.skills.addExp(Skill.MINING, this.ore.experience); // check if the rock is depleted if (randomBetween(0, 100) <= this.ore.break) { this.actor.playSound(soundIds.oreDepeleted); this.actor.playAnimation(null); const replacementObject = this.ore.objects.get(this.landscapeObject.objectId); if (replacementObject) { const respawnTime = randomBetween(this.ore.respawnLow, this.ore.respawnHigh); this.actor.instance.replaceGameObject(replacementObject, this.landscapeObject, respawnTime); } this.stop(); return; } } /** * Checks if the player has the pickaxe they started with. * * @returns true if the player has the pickaxe, false otherwise */ private hasMaterials() { return this.actor.inventory.has(this.tool.itemId); } private getGemMiningChance(): number { if (!this.isGemRock()) { return this.ore.baseChance; } // Base chance scaling from 28 to 70 based on level let chance = this.ore.baseChance + ((this.actor.skills.mining.level - this.ore.level) * (70 - 28)) / (99 - 40); // Glory multiplies chance by 3 (from 28-70 to 84-210) if (this.hasChargedGlory()) { chance *= 3; } return chance; } /** * Check that the player still has the level to mine the ore. * * @returns true if the player has the level, false otherwise */ private hasLevel() { return this.actor.skills.hasLevel(Skill.MINING, this.ore.level); } } ================================================ FILE: src/plugins/skills/mining/mining.plugin.ts ================================================ import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { Skill } from '@engine/world/actor/skills'; import { getBestPickaxe } from '@engine/world/config/harvest-tool'; import { getAllOreIds, getOreFromRock } from '@engine/world/config/harvestable-object'; import { soundIds } from '@engine/world/config/sound-ids'; import { MiningTask } from './mining-task'; const action: objectInteractionActionHandler = details => { // Get the mining details for the target rock const ore = getOreFromRock(details.object.objectId); if (!ore) { details.player.sendMessage('There is current no ore available in this rock.'); details.player.playSound(soundIds.oreEmpty, 7, 0); return; } if (!details.player.skills.hasLevel(Skill.MINING, ore.level)) { details.player.sendMessage(`You need a Mining level of ${ore.level} to mine this rock.`, true); return; } const tool = getBestPickaxe(details.player); if (!tool) { details.player.sendMessage('You do not have a pickaxe for which you have the level to use.'); return; } if (!tool) { return; } details.player.sendMessage('You swing your pick at the rock.'); details.player.face(details.position); details.player.playAnimation(tool.animation); //handleHarvesting(details, tool, ore, Skill.MINING); details.player.enqueueTask(MiningTask, [details.object, ore, tool]); }; export default { pluginId: 'rs:mining', hooks: [ { type: 'object_interaction', options: ['mine'], objectIds: getAllOreIds(), walkTo: true, handler: action, }, ], }; ================================================ FILE: src/plugins/skills/mining/prospecting.plugin.ts ================================================ import type { objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { findItem } from '@engine/config/config-handler'; import { getAllOreIds, getOreFromRock } from '@engine/world/config/harvestable-object'; import { soundIds } from '@engine/world/config/sound-ids'; const action: objectInteractionActionHandler = details => { details.player.sendMessage('You examine the rock for ores.'); details.player.face(details.position); const ore = getOreFromRock(details.object.objectId); details.player.playSound(soundIds.oreEmpty, 7, 0); const itemConfigId = typeof ore.items === 'string' ? ore.items : ore.items[0].itemConfigId; const oreItem = findItem(itemConfigId); if (!oreItem) { details.player.sendMessage('Sorry, something went wrong. Please report this to a developer.'); return; } // this used to use `setInterval` but will need rewriting to be synced with ticks // see https://github.com/runejs/server/issues/417 details.player.sendMessage('[debug] see issue #417'); // setTimeout(() => { // if (!ore) { // details.player.sendMessage('There is current no ore available in this rock.'); // return; // } // const oreName = oreItem.name.toLowerCase().replace(' ore', ''); // details.player.sendMessage(`This rock contains ${oreName}.`); // }, World.TICK_LENGTH * 3); }; export default { pluginId: 'rs:prospecting', hooks: [ { type: 'object_interaction', options: ['prospect'], objectIds: getAllOreIds(), walkTo: true, handler: action, }, ], }; ================================================ FILE: src/plugins/skills/prayer/bury-bones.plugin.ts ================================================ import type { itemInteractionActionHandler } from '@engine/action/pipe/item-interaction.action'; import { findItem, widgets } from '@engine/config/config-handler'; import { Achievements, giveAchievement } from '@engine/world/actor/player/achievements'; import { Skill } from '@engine/world/actor/skills'; import { animationIds } from '@engine/world/config/animation-ids'; import { soundIds } from '@engine/world/config/sound-ids'; const action: itemInteractionActionHandler = details => { const { player, option } = details; if (option !== 'bury') return; if (!player.canMove()) return; // bones can be buried only if prayerBuryXp is defined, but they can also // grant zero xp - this checks for that edge case if (!details.itemDetails.metadata.prayerBuryXp && details.itemDetails.metadata.prayerBuryXp !== 0) { return; } player.sendMessage(`You bury the ${details.itemDetails.name}.`); player.playAnimation(animationIds.buryBones); player.removeItem(details.itemSlot); player.playSound(soundIds.buryBones); player.skills.addExp(Skill.PRAYER, details.itemDetails.metadata.prayerBuryXp); giveAchievement(Achievements.BURY_BONES, player); }; const allBones: number[] = [ findItem('rs:bones')?.gameId, findItem('rs:bones_burnt')?.gameId, findItem('rs:bones_wolf')?.gameId, findItem('rs:bones_bat')?.gameId, findItem('rs:bones_big')?.gameId, findItem('rs:bones_dagannoth')?.gameId, findItem('rs:bones_babydragon')?.gameId, findItem('rs:bones_dragon')?.gameId, findItem('rs:bones_wyvern')?.gameId, findItem('rs:bones_monkey_normal')?.gameId, findItem('rs:bones_monkey_small_zombie')?.gameId, findItem('rs:bones_monkey_large_zombie')?.gameId, findItem('rs:bones_monkey_gorilla')?.gameId, findItem('rs:bones_monkey_bearded_gorilla')?.gameId, findItem('rs:bones_monkey_small_ninja')?.gameId, findItem('rs:bones_monkey_medium_ninja')?.gameId, findItem('rs:bones_monkey_skeleton_gorilla')?.gameId, findItem('rs:bones_jogre')?.gameId, findItem('rs:bones_zogre')?.gameId, findItem('rs:bones_fayrg')?.gameId, findItem('rs:bones_raurg')?.gameId, findItem('rs:bones_ourg')?.gameId, ].filter(id => typeof id === 'number') as number[]; export default { pluginId: 'rs:prayer_bury_bones', hooks: [ { type: 'item_interaction', widgets: widgets.inventory, options: 'bury', itemIds: allBones, handler: action, cancelOtherActions: true, }, ], }; ================================================ FILE: src/plugins/skills/runecrafting/runecrafting-altar.plugin.ts ================================================ import type { ItemOnObjectAction, itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action'; import type { ObjectInteractionAction, objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { findItem } from '@engine/config/config-handler'; import type { Player } from '@engine/world/actor/player/player'; import { itemIds } from '@engine/world/config/item-ids'; import type { Item } from '@engine/world/items/item'; /** * @Author NickNick */ import { altars, getEntityByAttr, getEntityIds, runes, talismans } from '@plugins/skills/runecrafting/runecrafting-constants'; import type { RunecraftingAltar } from '@plugins/skills/runecrafting/runecrafting-types'; import { logger } from '@runejs/common'; const enterAltar: itemOnObjectActionHandler = (details: ItemOnObjectAction) => { const { player, object, item } = details; const altar = getEntityByAttr(altars, 'entranceId', object.objectId); const rune = getEntityByAttr(runes, 'altar.entranceId', object.objectId); if (!altar) { logger.error(`No altar [entrance] found for runecrafting altar plugin: ${object.objectId}`); return; } if (!rune) { logger.error(`No rune found for runecrafting altar plugin: ${object.objectId}`); return; } if (item.itemId === itemIds.talismans.elemental) { if ( rune.talisman.id === itemIds.talismans.air || rune.talisman.id === itemIds.talismans.water || rune.talisman.id === itemIds.talismans.earth || rune.talisman.id === itemIds.talismans.fire ) { finishEnterAltar(player, item, altar); return; } } // Wrong talisman. if (item.itemId !== rune.talisman.id) { player.sendMessage('Nothing interesting happens.'); return; } // Correct talisman. if (item.itemId === rune.talisman.id) { finishEnterAltar(player, item, altar); } }; function finishEnterAltar(player: Player, item: Item, altar: RunecraftingAltar): void { const talisman = findItem(item.itemId); if (!talisman) { logger.error(`No talisman found for runecrafting altar plugin: ${item.itemId}`); return; } player.sendMessage(`You hold the ${talisman.name} towards the mysterious ruins.`); player.sendMessage(`You feel a powerful force take hold of you..`); player.teleport(altar.entrance); } const exitAltar: objectInteractionActionHandler = (details: ObjectInteractionAction) => { const { player, object } = details; const altar = getEntityByAttr(altars, 'portalId', object.objectId); if (!altar) { logger.error(`No altar [exit] found for runecrafting altar plugin: ${object.objectId}`); return; } player.teleport(altar.exit); }; export default { pluginId: 'rs:runecrafting_altars', hooks: [ { type: 'item_on_object', itemIds: getEntityIds(talismans, 'id'), objectIds: getEntityIds(altars, 'entranceId'), walkTo: true, handler: enterAltar, }, { type: 'object_interaction', objectIds: getEntityIds(altars, 'portalId'), walkTo: true, handler: exitAltar, }, ], }; ================================================ FILE: src/plugins/skills/runecrafting/runecrafting-constants.ts ================================================ /** * @Author NickNick */ /* RUNECRAFTING Tiara Configs Air - config 491 1 Mind - config 491 2 Water - config 491 4 Earth - config 491 8 Fire - config 491 16 Body - config 491 32 Cosmic - config 491 64 Chaos - config 491 128 Nature - config 491 256 Law - config 491 512 Death - config 491 1024 */ import { itemIds } from '@engine/world/config/item-ids'; import { Position } from '@engine/world/position'; import type { RunecraftingAltar, RunecraftingCombinationRune, RunecraftingRune, RunecraftingTalisman, RunecraftingTiara, } from '@plugins/skills/runecrafting/runecrafting-types'; export const tiaras: Map = new Map([ [ 'air', { id: itemIds.tiaras.air, config: 1, level: 1, xp: 25.0, recipe: { ingredients: [itemIds.talismans.air, itemIds.tiaras.blank] }, }, ], [ 'mind', { id: itemIds.tiaras.mind, config: 2, level: 1, xp: 27.5, recipe: { ingredients: [itemIds.talismans.mind, itemIds.tiaras.blank] }, }, ], [ 'water', { id: itemIds.tiaras.water, config: 4, level: 1, xp: 30, recipe: { ingredients: [itemIds.talismans.water, itemIds.tiaras.blank] }, }, ], [ 'body', { id: itemIds.tiaras.body, config: 32, level: 1, xp: 37.5, recipe: { ingredients: [itemIds.talismans.body, itemIds.tiaras.blank] }, }, ], [ 'earth', { id: itemIds.tiaras.earth, config: 8, level: 1, xp: 32.5, recipe: { ingredients: [itemIds.talismans.earth, itemIds.tiaras.blank] }, }, ], [ 'fire', { id: itemIds.tiaras.fire, config: 16, level: 1, xp: 35, recipe: { ingredients: [itemIds.talismans.fire, itemIds.tiaras.blank] }, }, ], [ 'cosmic', { id: itemIds.tiaras.cosmic, config: 64, level: 1, xp: 40, recipe: { ingredients: [itemIds.talismans.cosmic, itemIds.tiaras.blank] }, }, ], [ 'nature', { id: itemIds.tiaras.nature, config: 256, level: 1, xp: 45, recipe: { ingredients: [itemIds.talismans.nature, itemIds.tiaras.blank] }, }, ], [ 'chaos', { id: itemIds.tiaras.chaos, config: 128, level: 1, xp: 42.5, recipe: { ingredients: [itemIds.talismans.chaos, itemIds.tiaras.blank] }, }, ], [ 'law', { id: itemIds.tiaras.law, config: 512, level: 1, xp: 47.5, recipe: { ingredients: [itemIds.talismans.law, itemIds.tiaras.blank] }, }, ], [ 'death', { id: itemIds.tiaras.death, config: 1024, level: 1, xp: 50, recipe: { ingredients: [itemIds.talismans.death, itemIds.tiaras.blank] }, }, ], ]); export const talismans: Map = new Map([ ['air', { id: itemIds.talismans.air }], ['mind', { id: itemIds.talismans.mind }], ['water', { id: itemIds.talismans.water }], ['body', { id: itemIds.talismans.body }], ['earth', { id: itemIds.talismans.earth }], ['fire', { id: itemIds.talismans.fire }], ['cosmic', { id: itemIds.talismans.cosmic }], ['nature', { id: itemIds.talismans.nature }], ['chaos', { id: itemIds.talismans.chaos }], ['law', { id: itemIds.talismans.law }], ['death', { id: itemIds.talismans.death }], ['elemental', { id: itemIds.talismans.elemental }], ]); export const altars: Map = new Map([ [ 'air', { entranceId: 2452, craftingId: 2478, portalId: 2465, entrance: new Position(2841, 4829, 0), exit: new Position(2983, 3292, 0), }, ], [ 'mind', { entranceId: 2453, craftingId: 2479, portalId: 2466, entrance: new Position(2793, 4828, 0), exit: new Position(2980, 3514, 0), }, ], [ 'water', { entranceId: 2454, craftingId: 2480, portalId: 2467, entrance: new Position(2726, 4832, 0), exit: new Position(3187, 3166, 0), }, ], [ 'earth', { entranceId: 2455, craftingId: 2481, portalId: 2468, entrance: new Position(2655, 4830, 0), exit: new Position(3304, 3474, 0), }, ], [ 'fire', { entranceId: 2456, craftingId: 2482, portalId: 2469, entrance: new Position(2574, 4849, 0), exit: new Position(3311, 3256, 0), }, ], [ 'body', { entranceId: 2457, craftingId: 2483, portalId: 2470, entrance: new Position(2524, 4825, 0), exit: new Position(3051, 3445, 0), }, ], [ 'cosmic', { entranceId: 2458, craftingId: 2484, portalId: 2471, entrance: new Position(2142, 4813, 0), exit: new Position(2408, 4379, 0), }, ], [ 'law', { entranceId: 2459, craftingId: 2485, portalId: 2472, entrance: new Position(2464, 4818, 0), exit: new Position(2858, 3379, 0), }, ], [ 'nature', { entranceId: 2460, craftingId: 2486, portalId: 2473, entrance: new Position(2400, 4835, 0), exit: new Position(2867, 3019, 0), }, ], [ 'chaos', { entranceId: 2461, craftingId: 2487, portalId: 2474, entrance: new Position(2268, 4842, 0), exit: new Position(3058, 3591, 0), }, ], [ 'death', { entranceId: 2462, craftingId: 2488, portalId: 2475, entrance: new Position(2208, 4830, 0), exit: new Position(3222, 3222, 0), }, ], ]); export const runes: Map = new Map([ [ 'air', { id: 556, xp: 5.0, level: 1, essence: [itemIds.essence.pure, itemIds.essence.rune], altar: altars.get('air'), talisman: talismans.get('air'), tiara: tiaras.get('air'), } as RunecraftingRune, ], [ 'mind', { id: 558, xp: 5.5, level: 1, essence: [itemIds.essence.pure, itemIds.essence.rune], altar: altars.get('mind'), talisman: talismans.get('mind'), tiara: tiaras.get('mind'), } as RunecraftingRune, ], [ 'water', { id: 555, xp: 6, level: 5, essence: [itemIds.essence.pure, itemIds.essence.rune], altar: altars.get('water'), talisman: talismans.get('water'), tiara: tiaras.get('water'), } as RunecraftingRune, ], [ 'earth', { id: 557, xp: 6.5, level: 9, essence: [itemIds.essence.pure, itemIds.essence.rune], altar: altars.get('earth'), talisman: talismans.get('earth'), tiara: tiaras.get('earth'), } as RunecraftingRune, ], [ 'fire', { id: 554, xp: 7.0, level: 14, essence: [itemIds.essence.pure, itemIds.essence.rune], altar: altars.get('fire'), talisman: talismans.get('fire'), tiara: tiaras.get('fire'), } as RunecraftingRune, ], [ 'body', { id: 559, xp: 7.5, level: 20, essence: [itemIds.essence.pure, itemIds.essence.rune], altar: altars.get('body'), talisman: talismans.get('body'), tiara: tiaras.get('body'), } as RunecraftingRune, ], [ 'cosmic', { id: 564, xp: 8.0, level: 27, essence: [itemIds.essence.pure], altar: altars.get('cosmic'), talisman: talismans.get('cosmic'), tiara: tiaras.get('cosmic'), } as RunecraftingRune, ], [ 'chaos', { id: 562, xp: 8.5, level: 35, essence: [itemIds.essence.pure], altar: altars.get('chaos'), talisman: talismans.get('chaos'), tiara: tiaras.get('chaos'), } as RunecraftingRune, ], [ 'nature', { id: 561, xp: 9.0, level: 44, essence: [itemIds.essence.pure], altar: altars.get('nature'), talisman: talismans.get('nature'), tiara: tiaras.get('nature'), } as RunecraftingRune, ], [ 'law', { id: 563, xp: 9.5, level: 54, essence: [itemIds.essence.pure], altar: altars.get('law'), talisman: talismans.get('law'), tiara: tiaras.get('law'), } as RunecraftingRune, ], [ 'death', { id: 560, xp: 10.0, level: 65, essence: [itemIds.essence.pure], altar: altars.get('death'), talisman: talismans.get('death'), tiara: tiaras.get('death'), } as RunecraftingRune, ], ]); export const combinationRunes: Map = new Map([ [ 'mist', { altar: [altars.get('air'), altars.get('water')], id: 4695, level: 7, talisman: [talismans.get('air'), talismans.get('water')], tiara: [tiaras.get('air'), tiaras.get('water')], runes: [runes.get('air'), runes.get('water')], xp: [8.0, 8.5], } as RunecraftingCombinationRune, ], [ 'dust', { id: 4696, level: 10, xp: [8.3, 9.0], altar: [altars.get('air'), altars.get('earth')], talisman: [talismans.get('air'), talismans.get('earth')], runes: [runes.get('air'), runes.get('earth')], tiara: [tiaras.get('air'), tiaras.get('earth')], } as RunecraftingCombinationRune, ], [ 'mud', { id: 4698, level: 13, xp: [9.3, 9.5], altar: [altars.get('water'), altars.get('earth')], talisman: [talismans.get('water'), talismans.get('earth')], runes: [runes.get('water'), runes.get('earth')], tiara: [tiaras.get('water'), tiaras.get('earth')], } as RunecraftingCombinationRune, ], [ 'smoke', { id: 4697, level: 15, xp: [8.5, 9.5], altar: [altars.get('air'), altars.get('fire')], talisman: [talismans.get('air'), talismans.get('fire')], runes: [runes.get('air'), runes.get('fire')], tiara: [tiaras.get('air'), tiaras.get('fire')], } as RunecraftingCombinationRune, ], [ 'steam', { id: 4694, level: 19, xp: [9.5, 10.0], altar: [altars.get('water'), altars.get('fire')], talisman: [talismans.get('water'), talismans.get('fire')], runes: [runes.get('water'), runes.get('fire')], tiara: [tiaras.get('water'), tiaras.get('fire')], } as RunecraftingCombinationRune, ], [ 'lava', { id: 4699, level: 23, xp: [10.0, 10.5], altar: [altars.get('earth'), altars.get('fire')], talisman: [talismans.get('earth'), talismans.get('fire')], runes: [runes.get('earth'), runes.get('fire')], tiara: [tiaras.get('earth'), tiaras.get('fire')], } as RunecraftingCombinationRune, ], ]); export function getEntityByAttr(entities: Map, attr: string, value: unknown): T | null { let entity: T | null = null; const splits = attr.split('.'); // Handles dot seperated attribute names. if (splits.length === 2) { entities.forEach(e => { if (e[splits[0]][splits[1]] === value) { entity = e; } }); } // Handles single attribute name. if (splits.length === 1) { entities.forEach(e => { if (e[attr] === value) { entity = e; } }); } return entity; } export function getEntityIds(entities: Map, property: keyof T): number[] { const entityIds: number[] = []; entities.forEach((entity: T) => { if (entity[property] && typeof entity[property] === 'number') { const tempnum: number = entity[property] as any; entityIds.push(tempnum); } }); return entityIds; } export function runeMultiplier(runeId: number, level: number): number { switch (runeId) { case 556: return Math.floor(level / 11.0) + 1; case 558: return Math.floor(level / 14.0) + 1; case 555: return Math.floor(level / 19.0) + 1; case 557: return Math.floor(level / 26.0) + 1; case 554: return Math.floor(level / 35.0) + 1; case 559: return Math.floor(level / 46.0) + 1; case 564: return Math.floor(level / 59.0) + 1; case 562: return Math.floor(level / 74.0) + 1; case 561: return Math.floor(level / 91.0) + 1; case 563: return 1.0; case 560: return 1.0; } return 1.0; } ================================================ FILE: src/plugins/skills/runecrafting/runecrafting-crafting.plugin.ts ================================================ import type { ItemOnObjectAction, itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action'; import type { ObjectInteractionAction, objectInteractionActionHandler } from '@engine/action/pipe/object-interaction.action'; import { findItem, widgets } from '@engine/config/config-handler'; import { randomBetween } from '@engine/util/num'; /** * @Author NickNick */ import { Skill } from '@engine/world/actor/skills'; import { itemIds } from '@engine/world/config/item-ids'; import { altars, combinationRunes, getEntityByAttr, getEntityIds, runeMultiplier, runes, } from '@plugins/skills/runecrafting/runecrafting-constants'; import type { RunecraftingCombinationRune } from '@plugins/skills/runecrafting/runecrafting-types'; import { logger } from '@runejs/common'; const craftRune: objectInteractionActionHandler = (details: ObjectInteractionAction) => { const { player, object } = details; const rune = getEntityByAttr(runes, 'altar.craftingId', object.objectId); if (!rune) { logger.error(`No rune [crafting] found for runecrafting plugin: ${object.objectId}`); return; } const runeDetails = findItem(rune.id); if (!runeDetails) { logger.warn(`Could not find rune details for rune id ${rune.id}`); return; } const level = player.skills.get(Skill.RUNECRAFTING).level; if (level < rune.level) { player.sendMessage(`You need a runecrafting level of ${rune.level} to craft ${runeDetails.name}.`); return; } let essenceAvailable = 0; rune.essence.forEach(essenceId => { essenceAvailable += player.inventory.findAll(essenceId).length; }); if (essenceAvailable > 0) { // Remove essence from inventory. rune.essence.forEach(essenceId => { player.inventory.findAll(essenceId).forEach(index => { player.inventory.remove(index); }); }); // Add crafted runes to inventory. player.inventory.add({ itemId: rune.id, amount: runeMultiplier(rune.id, level) * essenceAvailable }); // Add experience player.skills.addExp(Skill.RUNECRAFTING, rune.xp * essenceAvailable); // Update widget items. player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory); return; } player.sendMessage(`You do not have any rune essence to bind.`); }; function getCombinationRuneByAltar(itemId: number, objectId: number): RunecraftingCombinationRune | undefined { for (const combinationRune of combinationRunes.values()) { const altarIndex = combinationRune.altar.findIndex(altar => altar.craftingId === objectId); if (altarIndex > -1 && combinationRune.talisman[altarIndex ^ 1].id === itemId) { return combinationRune; } } return undefined; } const craftCombinationRune: itemOnObjectActionHandler = (details: ItemOnObjectAction) => { const { player, object, item } = details; const rune = getCombinationRuneByAltar(item.itemId, object.objectId); if (!rune) { player.sendMessage('Nothing interesting happens.'); return; } const altarIndex = rune.altar.findIndex(altar => object.objectId === altar.craftingId); const shouldBreakTalisman = randomBetween(0, 1) === 1; const requiredRunesIndex = player.inventory.findIndex(rune.runes[altarIndex ^ 1].id); if (requiredRunesIndex < 0) { player.sendMessage(`You don't have any runes to bind.`); return; } const runeDetails = findItem(rune.id); if (!runeDetails) { logger.warn(`Could not find rune details for rune id ${rune.id}`); return; } const level = player.skills.get(Skill.RUNECRAFTING).level; if (level < rune.level) { player.sendMessage(`You need a runecrafting level of ${rune.level} to craft ${runeDetails.name}.`); return; } const essenceAvailable = player.inventory.findAll(itemIds.essence.pure).length; const requiredRunesAvailable = player.inventory.amountInStack(requiredRunesIndex); if (essenceAvailable > 0 && requiredRunesIndex > 0) { const amountToCraft = Math.min(essenceAvailable, requiredRunesAvailable); // Remove runes from inventory if (amountToCraft === requiredRunesAvailable) { player.inventory.remove(requiredRunesIndex, false); } else { player.inventory.set(requiredRunesIndex, { itemId: rune.runes[altarIndex ^ 1].id, amount: requiredRunesAvailable - amountToCraft, }); } // Remove essence from inventory. for (let i = 0; i < amountToCraft; i++) { player.inventory.removeFirst(itemIds.essence.pure); } // Add crafted runes to inventory. player.inventory.add({ itemId: rune.id, amount: amountToCraft }); // Add experience player.skills.addExp(Skill.RUNECRAFTING, rune.xp[altarIndex] * essenceAvailable); if (shouldBreakTalisman) { player.inventory.removeFirst(item.itemId); } // Update widget items. player.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, player.inventory); player.sendMessage(`You craft some ${runeDetails.name}.`); return; } // player.sendMessage(`You do not have any pure essence to bind.`); }; export default { pluginId: 'rs:runecrafting', hooks: [ { type: 'object_interaction', objectIds: getEntityIds(altars, 'craftingId'), walkTo: true, handler: craftRune, }, { type: 'item_on_object', objectIds: getEntityIds(altars, 'craftingId'), walkTo: true, handler: craftCombinationRune, }, ], }; ================================================ FILE: src/plugins/skills/runecrafting/runecrafting-tiara.plugin.ts ================================================ /** * @Author NickNick */ import type { equipmentChangeActionHandler } from '@engine/action/pipe/equipment-change.action'; import { getEntityByAttr, getEntityIds, tiaras } from '@plugins/skills/runecrafting/runecrafting-constants'; import { logger } from '@runejs/common'; const unequipTiara: equipmentChangeActionHandler = details => { const { player } = details; player.outgoingPackets.updateClientConfig(491, 0); }; const equipTiara: equipmentChangeActionHandler = details => { const { player, itemId } = details; const tiara = getEntityByAttr(tiaras, 'id', itemId); if (!tiara) { logger.error(`No tiara [equipping] found for runecrafting plugin: ${itemId}`); return; } player.outgoingPackets.updateClientConfig(491, tiara.config); }; export default { pluginId: 'rs:runecrafting_tiaras', hooks: [ { type: 'equipment_change', eventType: 'equip', itemIds: getEntityIds(tiaras, 'id'), handler: equipTiara, }, { type: 'equipment_change', eventType: 'unequip', itemIds: getEntityIds(tiaras, 'id'), handler: unequipTiara, }, ], }; ================================================ FILE: src/plugins/skills/runecrafting/runecrafting-types.ts ================================================ /** * @Author NickNick */ import type { Item } from '@engine/world/items/item'; import type { Position } from '@engine/world/position'; export interface RunecraftingRecipe { ingredients: Item[] | number[]; } export interface RunecraftingTiara { id: number; config: number; recipe: RunecraftingRecipe; level: number; xp: number; } export interface RunecraftingTalisman { id: number; } export interface RunecraftingAltar { entranceId: number; craftingId: number; portalId: number; entrance: Position; exit: Position; } export interface RunecraftingRune { id: number; xp: number; level: number; essence: number[]; altar: RunecraftingAltar; tiara: RunecraftingTiara; talisman: RunecraftingTalisman; } export interface RunecraftingCombinationRune { id: number; xp: [number, number]; level: number; altar: [RunecraftingAltar, RunecraftingAltar]; tiara: [RunecraftingTiara, RunecraftingTiara]; talisman: [RunecraftingTalisman, RunecraftingTalisman]; runes: [RunecraftingRune, RunecraftingRune]; } ================================================ FILE: src/plugins/skills/skill-guides/Strength.json ================================================ { "id": 121, "name": "Strength", "members": false, "sub_guides": [ { "name": "Weapons", "lines": [ { "item": "rs:black_halberd", "text": "Black halberd\\n(10 Attack is needed)", "level": 5 }, { "item": "rs:white_halberd", "text": "White halberd\\n(10 Attack is needed)", "level": 5 }, { "item": "rs:mithril_halberd", "text": "Mithril halberd\\n(20 Attack is needed)", "level": 10 }, { "item": "rs:adamant_halberd", "text": "Adamant halberd\\n(30 Attack is needed)", "level": 15 }, { "item": "rs:rune_halberd", "text": "Rune halberd\\n(40 Attack is needed)", "level": 20 }, { "item": "rs:dragon_halberd", "text": "Dragon halberd\\n(60 Attack is needed)", "level": 30 }, { "item": "rs:granite_maul", "text": "Granite maul\\n(50 Attack is needed)", "level": 50 }, { "item": "rs:tzhaar-ket-om", "text": "TzHaar-Ket-Om\\nobsidian maul", "level": 60 }, { "item": "rs:dharoks_greataxe", "text": "Dharok's greataxe\\n(70 Attack is needed)", "level": 70 }, { "item": "rs:torags_hammers", "text": "Torag's hammers\\n(70 Attack is needed)", "level": 70 } ] }, { "name": "Armour", "lines": [ { "item": "rs:granite", "text": "Granite Armour", "level": 50 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/agility.json ================================================ { "id": 122, "name": "Agility", "members": true, "sub_guides": [ { "name": "Courses", "lines": [ { "item": "rs:gnome_stronghold_agility_course", "text": "Gnome Stronghold Agility Course", "level": 1 }, { "item": "rs:gnomeball_game", "text": "Gnomeball Game", "level": 1 }, { "item": "rs:werewolf_skullball_game", "text": "Werewolf Skullball game", "level": 25 }, { "item": "rs:agility_pyramid", "text": "Agility Pyramid", "level": 30 }, { "item": "rs:barbarian_outpost_agility_course", "text": "Barbarian Outpost Agility Course", "level": 35 }, { "item": "rs:ape_atoll_agility_course", "text": "Ape Atoll Agility Course", "level": 48 }, { "item": "rs:wilderness_course", "text": "Wilderness Course", "level": 52 }, { "item": "rs:slayer_staff", "text": "Werewolf Agility Course", "level": 60 } ] }, { "name": "Areas", "lines": [ { "item": "rs:agility_jump_green", "text": "Rope-swing to Moss Giant Island", "level": 10 }, { "item": "rs:agility_jump_green", "text": "Stepping stones in Karamja Dungeon", "level": 12 }, { "item": "rs:agility_jump_green", "text": "Monkey bars under Edgeville", "level": 15 }, { "item": "rs:agility_contortion_green", "text": "Pipe contortion in Karamja Dungeon", "level": 22 }, { "item": "rs:agility_jump_green", "text": "Stepping stones in south-eastern Karamja", "level": 30 }, { "item": "rs:agility_contortion_green", "text": "Pipe contortion in Karamja Dungeon", "level": 34 }, { "item": "rs:agility_balance_green", "text": "Elf area log balance", "level": 45 }, { "item": "rs:agility_contortion_green", "text": "Contortion in Yanille Dungeon small room", "level": 49 }, { "item": "rs:agility_contortion_green", "text": "Access the God Wars Dungeon area via the \\nAgility route", "level": 60 }, { "item": "rs:agility_climb", "text": "Yanille Dungeon's rubble climb", "level": 67 }, { "item": "rs:agility_climb", "text": "Enter the Saradomins area of the \\nGod Wars Dungeon", "level": 70 } ] }, { "name": "Shortcuts", "lines": [ { "item": "rs:agility_climb", "text": "Falador Agility shortcut", "level": 5 }, { "item": "rs:agility_jump_yellow", "text": "Jump fence south of Varrock", "level": 13 }, { "item": "rs:agility_contortion_yellow", "text": "Yanille Agility shortcut", "level": 16 }, { "item": "rs:agility_balance_yellow", "text": "Coal Truck log balance", "level": 20 }, { "item": "rs:agility_contortion_yellow", "text": "Falador Agility shortcut", "level": 26 }, { "item": "rs:agility_balance_yellow", "text": "Draynor Manor stones to Champions' Guild", "level": 31 }, { "item": "rs:agility_balance_yellow", "text": "Ardougne log balance shortcut", "level": 33 }, { "item": "rs:agility_climb", "text": "Gnome Stronghold shortcut", "level": 37 }, { "item": "rs:agility_climb", "text": "Al Kahrid Mining pit cliffside scramble", "level": 38 }, { "item": "rs:agility_climb", "text": "Trollheim easy cliffside scramble", "level": 41 }, { "item": "rs:agility_contortion_yellow", "text": "Dwarven Mine narrow crevice", "level": 42 }, { "item": "rs:agility_climb", "text": "Trollheim medium cliffside scramble", "level": 43 }, { "item": "rs:agility_climb", "text": "Trollheim advanced cliffside scramble", "level": 44 }, { "item": "rs:agility_contortion_yellow", "text": "Cosmic Temple - narrow walkway", "level": 46 }, { "item": "rs:agility_contortion_yellow", "text": "Deep Wilderness - narrow tunnel", "level": 46 }, { "item": "rs:agility_climb", "text": "Trollheim hard cliffside scramble", "level": 47 }, { "item": "rs:agility_contortion_yellow", "text": "Pipe from Edgeville dungeon to Varrock Sewers", "level": 51 }, { "item": "rs:agility_climb", "text": "Port Phasmatys ectopool shortcut", "level": 58 }, { "item": "rs:agility_jump_yellow", "text": "Elven overpass easy cliffside scamble", "level": 59 }, { "item": "rs:agility_climb", "text": "Slayer Tower medium spiked chain climb", "level": 61 }, { "item": "rs:agility_climb", "text": "Taverley dungeon lesser demon fence shortcut", "level": 63 }, { "item": "rs:agility_climb", "text": "Trollheim Wilderness route", "level": 64 }, { "item": "rs:agility_climb", "text": "Temple on the Salve to Morytania shortcut", "level": 65 }, { "item": "rs:agility_climb", "text": "Elven overpass medium cliffside scramble", "level": 68 }, { "item": "rs:agility_contortion_yellow", "text": "Taverley Dungeon short-cuts to blue dragons", "level": 70 }, { "item": "rs:agility_climb", "text": "Slayer Tower advanced spiked chain", "level": 71 }, { "item": "rs:agility_jump_yellow", "text": "Taverley Dungeon spiked blades jump", "level": 80 }, { "item": "rs:agility_jump_yellow", "text": "Fremennik Slayer Dungeon spiked blades jump", "level": 81 }, { "item": "rs:agility_jump_yellow", "text": "Brimhaven Dungeon eastern stepping stones", "level": 83 }, { "item": "rs:agility_contortion_yellow", "text": "Iorwerth southern shortcut", "level": 84 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/attack.json ================================================ { "id": 118, "name": "Attack", "members": false, "sub_guides": [ { "name": "Weapons", "lines": [ { "item": "rs:bronze_dagger", "text": "Bronze", "level": 1 }, { "item": "rs:iron_dagger", "text": "Iron", "level": 1 }, { "item": "rs:steel_dagger", "text": "Steel", "level": 5 }, { "item": "rs:black_dagger", "text": "Black", "level": 10 }, { "item": "rs:white_dagger", "text": "Members: White", "level": 10 }, { "item": "rs:mithril_dagger", "text": "Mithril", "level": 20 }, { "item": "rs:adamant_dagger", "text": "Adamant", "level": 30 }, { "item": "rs:battlestaff", "text": "Members: Battlestaves (with 30 Magic)", "level": 30 }, { "item": "rs:rune_dagger", "text": "Rune", "level": 40 }, { "item": "rs:granite_maul", "text": "Members: Granite Maul", "level": 50 }, { "item": "rs:dragon_dagger", "text": "Members: Dragon", "level": 60 }, { "item": "rs:toktz_xil_ak", "text": "Members: Obsidian weapons", "level": 60 }, { "item": "rs:abyssal_whip", "text": "Members: Abyssal Whip", "level": 70 }, { "item": "rs:dharoks_greataxe", "text": "Members: Dharok's greataxe", "level": 70 }, { "item": "rs:torags_hammers", "text": "Members: Torag's hammers", "level": 70 }, { "item": "rs:veracs_flail", "text": "Members: Verac's flail", "level": 70 }, { "item": "rs:guthans_warspear", "text": "Members: Guthan's warspear", "level": 70 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/construction.json ================================================ { "id": 143, "name": "Construction", "members": true, "sub_guides": [ { "name": "Rooms", "lines": [ { "item": "rs:garden", "text": "Garden", "level": 1 }, { "item": "rs:parlour", "text": "Parlour", "level": 1 }, { "item": "rs:kitchen", "text": "Kitchen", "level": 5 }, { "item": "rs:dining_room", "text": "Dining room", "level": 10 }, { "item": "rs:workshop", "text": "Workshop", "level": 15 }, { "item": "rs:bedroom", "text": "Bedroom", "level": 20 }, { "item": "rs:hall_skill_trophies", "text": "Hall (skill trophies)", "level": 25 }, { "item": "rs:league_hall", "text": "Leauge hall", "level": 27 }, { "item": "rs:games_room", "text": "Games room", "level": 30 }, { "item": "rs:combat_room", "text": "Combat room", "level": 32 }, { "item": "rs:hall_quest_trophies", "text": "Hall (quest trophies)", "level": 35 }, { "item": "rs:menagerie", "text": "Menagerie", "level": 37 }, { "item": "rs:study", "text": "Study", "level": 40 }, { "item": "rs:costume_room", "text": "Costume room", "level": 42 }, { "item": "rs:chapel", "text": "Chapel", "level": 45 }, { "item": "rs:portal_chamber", "text": "Portal chamber", "level": 50 }, { "item": "rs:formal_garden", "text": "Formal garden", "level": 55 }, { "item": "rs:throne_room", "text": "Throne room", "level": 60 }, { "item": "rs:oubliette", "text": "Oubliette", "level": 65 }, { "item": "rs:superior_garden", "text": "Superior garden", "level": 65 }, { "item": "rs:dungeon", "text": "Dungeon", "level": 70 }, { "item": "rs:treasure_room", "text": "Treasure room", "level": 75 }, { "item": "rs:achievement_gallery", "text": "Achievement Gallery", "level": 80 }, { "item": "rs:league_hall", "text": "Garden", "level": 1 } ] }, { "name": "Skills", "lines": [ { "item": "rs:clay_fireplace", "text": "Clay fireplace", "level": 3 }, { "item": "rs:firepit", "text": "Firepit", "level": 5 }, { "item": "rs:pump_and_drain", "text": "Pump and drain", "level": 7 }, { "item": "rs:firepit_with_hook", "text": "Firepit with hook", "level": 11 }, { "item": "rs:repair_bench", "text": "Repair bench", "level": 15 }, { "item": "rs:pluming_stand", "text": "Pluming stand", "level": 16 }, { "item": "rs:crafting_table_1", "text": "Crafting table 1", "level": 16 }, { "item": "rs:firepit_with_pot", "text": "Firepit with pot", "level": 17 }, { "item": "rs:wooden_workbench", "text": "Wooden workbench", "level": 17 }, { "item": "rs:small_oven", "text": "Small oven", "level": 24 }, { "item": "rs:crafting_table_2", "text": "Crafting table 2", "level": 25 }, { "item": "rs:pump_and_tub", "text": "Pump and tub", "level": 27 }, { "item": "rs:large_oven", "text": "Large oven", "level": 29 }, { "item": "rs:oak_workbench", "text": "Oak workbench", "level": 32 }, { "item": "rs:stone_fireplace", "text": "Stone fireplace", "level": 33 }, { "item": "rs:crafting_table_3", "text": "Crafting table 3", "level": 34 }, { "item": "rs:steel_range", "text": "Steel range", "level": 34 }, { "item": "rs:whetstone", "text": "Whetstone", "level": 35 }, { "item": "rs:shield_easel", "text": "Shield easel", "level": 41 }, { "item": "rs:fancy_range", "text": "Fancy range", "level": 42 }, { "item": "rs:crafting_table_4", "text": "Crafting table 4", "level": 42 }, { "item": "rs:steel_framed_workbench", "text": "Steel framed workbench", "level": 46 }, { "item": "rs:sink", "text": "Sink", "level": 47 }, { "item": "rs:armour_stand", "text": "Armour stand", "level": 55 }, { "item": "rs:workbench_with_vice", "text": "Workbench with vice", "level": 62 }, { "item": "rs:marble_fireplace", "text": "Marble fireplace", "level": 63 }, { "item": "rs:banner_easel", "text": "Banner easel", "level": 66 }, { "item": "rs:workbench_with_lathe", "text": "Workbench with lathe", "level": 77 }, { "item": "rs:ancient_altar", "text": "Ancient altar", "level": 80 }, { "item": "rs:lunar_altar", "text": "Lunar altar", "level": 80 }, { "item": "rs:dark_altar", "text": "Dark altar", "level": 80 } ] }, { "name": "Surfaces", "lines": [ { "item": "rs:crude_wooden_chair", "text": "Crude wooden chair", "level": 1 }, { "item": "rs:wooden_chair", "text": "Wooden chair", "level": 8 }, { "item": "rs:wooden_dining_table", "text": "Wooden dining table", "level": 10 }, { "item": "rs:wooden_kitchen_table", "text": "Wooden kitchen table", "level": 12 }, { "item": "rs:rocking_chair", "text": "Rocking chair", "level": 14 }, { "item": "rs:oak_chair", "text": "Oak chair", "level": 19 }, { "item": "rs:wooden_bed", "text": "Wooden bed", "level": 20 }, { "item": "rs:oak_dining_table", "text": "Oak dining table", "level": 22 }, { "item": "rs:oak_dining_bench", "text": "Oak dining bench", "level": 22 }, { "item": "rs:oak_armchair", "text": "Oak armchair", "level": 26 }, { "item": "rs:oak_bed", "text": "Oak bed", "level": 30 }, { "item": "rs:carved_oak_dining_table", "text": "Carved oak dining table", "level": 31 }, { "item": "rs:carved_oak_dining_bench", "text": "Carved oak dining bench", "level": 31 }, { "item": "rs:oak_kitchen_table", "text": "Oak kitchen table", "level": 32 }, { "item": "rs:large_oak_bed", "text": "Large oak bed", "level": 34 }, { "item": "rs:teak_armchair", "text": "Teak armchair", "level": 35 }, { "item": "rs:teak_dining_table", "text": "Teak dining table", "level": 38 }, { "item": "rs:teak_dining_bench", "text": "Teak dining bench", "level": 38 }, { "item": "rs:teak_bed", "text": "Teak bed", "level": 40 }, { "item": "rs:carved_teak_dining_bench", "text": "Carved teak dining bench", "level": 44 }, { "item": "rs:carved_teak_dining_table", "text": "Carved teak dining table", "level": 45 }, { "item": "rs:large_teak_bed", "text": "Large teak bed", "level": 45 }, { "item": "rs:mahogany_armchair", "text": "Mahogany armchair", "level": 50 }, { "item": "rs:mahogany_dining_table", "text": "Mahogany dining table", "level": 52 }, { "item": "rs:mahogany_dining_bench", "text": "Mahogany dining bench", "level": 52 }, { "item": "rs:teak_kitchen_table", "text": "Teak kitchen table", "level": 52 }, { "item": "rs:mahogany_four_poster_bed", "text": "Mahogany four-poster bed", "level": 53 }, { "item": "rs:oak_throne", "text": "Oak throne", "level": 60 }, { "item": "rs:gilded_mahogany_four_poster_bed", "text": "Gilded mahogany four-poster bed", "level": 60 }, { "item": "rs:gilded_mahogany_dining_bench", "text": "Gilded mahogany dining bench", "level": 61 }, { "item": "rs:teak_throne", "text": "Teak throne", "level": 67 }, { "item": "rs:gilded_mahogany_and_marble_table", "text": "Gilded mahogany and marble table", "level": 72 }, { "item": "rs:mahogany_throne", "text": "Mahogany throne", "level": 74 }, { "item": "rs:gilded_mahogany_throne", "text": "Gilded mahogany throne", "level": 81 }, { "item": "rs:teak_kitchen_table", "text": "Teak kitchen table", "level": 52 }, { "item": "rs:teak_kitchen_table", "text": "Teak kitchen table", "level": 52 }, { "item": "rs:teak_kitchen_table", "text": "Teak kitchen table", "level": 52 }, { "item": "rs:skeleton_throne", "text": "Skeleton throne", "level": 88 }, { "item": "rs:crystal_throne", "text": "Crystal throne", "level": 95 }, { "item": "rs:demonic_throne", "text": "Demonic throne", "level": 99 } ] }, { "name": "Storage", "lines": [ { "item": "rs:wooden_bookcase", "text": "Wooden bookcase", "level": 4 }, { "item": "rs:wooden_shelves_1", "text": "Wooden shelves 1", "level": 6 }, { "item": "rs:beer_barrel", "text": "Beer barrel", "level": 7 }, { "item": "rs:wooden_larder", "text": "Wooden larder", "level": 9 }, { "item": "rs:cider_barrel", "text": "Cider barrel", "level": 12 }, { "item": "rs:wooden_shelves_2", "text": "Wooden shelves 2", "level": 12 }, { "item": "rs:tool_store_1", "text": "Tool store 1", "level": 15 }, { "item": "rs:asgarnian_ale_barrel", "text": "Asgarnian ale barrel", "level": 18 }, { "item": "rs:shoe_box", "text": "Shoe box", "level": 20 }, { "item": "rs:wooden_shaving_stand", "text": "Wooden shaving stand", "level": 21 }, { "item": "rs:wooden_shelves_3", "text": "Wooden shelves 3", "level": 23 }, { "item": "rs:tool_store_2", "text": "Tool store 2", "level": 25 }, { "item": "rs:greenmans_ale_barrel", "text": "Greenman's Ale barrel", "level": 26 }, { "item": "rs:oak_chest_of_drawers", "text": "oak chest of drawers", "level": 27 }, { "item": "rs:oak_bookcase", "text": "Oak bookcase", "level": 29 }, { "item": "rs:oak_shaving_stand", "text": "Oak shaving stand", "level": 29 }, { "item": "rs:oak_larder", "text": "Oak larder", "level": 33 }, { "item": "rs:oak_shelves_1", "text": "Oak shelves 1", "level": 34 }, { "item": "rs:tool_store_3", "text": "Tool store 3", "level": 35 }, { "item": "rs:dragon_bitter_barrel", "text": "Dragon Bitter barrel", "level": 36 }, { "item": "rs:oak_dresser", "text": "Oak dresser", "level": 37 }, { "item": "rs:oak_Wardrobe_bedroom", "text": "Oak wardrobe (bedroom)", "level": 39 }, { "item": "rs:mahogany_bookcase", "text": "Mahogany bookcase", "level": 40 }, { "item": "rs:oak_wardrobe_costume_room", "text": "Oak wardrobe (costtume room)", "level": 15 }, { "item": "rs:teak_larder", "text": "Teak larder", "level": 43 }, { "item": "rs:oak_fancy_dress_box", "text": "Oak fancy dress box", "level": 44 }, { "item": "rs:tool_store_4", "text": "Tool store 4", "level": 44 }, { "item": "rs:oak_shelves_2", "text": "Oak shelves 2", "level": 45 }, { "item": "rs:oak_armour_case", "text": "Oak armour case", "level": 46 }, { "item": "rs:teak_dresser", "text": "Teak dresser", "level": 46 }, { "item": "rs:chefs_delight_barrel", "text": "Chef's Delight barrel", "level": 48 }, { "item": "rs:oak_treasure_chest", "text": "Oak treasure chest", "level": 48 }, { "item": "rs:oak_toy_box", "text": "Oak toy box", "level": 50 }, { "item": "rs:carved_oak_wardrobe_costume_room", "text": "Carved oak wardrobe (costume room)", "level": 51 }, { "item": "rs:teak_chest_of_drawers", "text": "Teak chest of drawers", "level": 51 }, { "item": "rs:oak_cape_rack", "text": "Oak cape rack", "level": 54 }, { "item": "rs:tool_store_5", "text": "Tool store 5", "level": 55 }, { "item": "rs:teak_shelves_1", "text": "Teak shelves 1", "level": 56 }, { "item": "rs:fancy_teak_dresser", "text": "Fancy teak dresser", "level": 56 }, { "item": "rs:servants_moneybag", "text": "Servant's moneybag", "level": 58 }, { "item": "rs:teak_wardrobe_costume_room", "text": "Teak wardrobe (costume room)", "level": 60 }, { "item": "rs:spice_rack", "text": "Spice Rack", "level": 60 }, { "item": "rs:teak_fancy_dress_box", "text": "Teak fancy dress box", "level": 62 }, { "item": "rs:teak_cape_rack", "text": "Teak cape rack", "level": 63 }, { "item": "rs:teak_wardrobe_bedroom", "text": "Teak wardrobe (bedroom)", "level": 63 }, { "item": "rs:teak_armour_case", "text": "Teak armour case", "level": 64 }, { "item": "rs:mahogany_dresser", "text": "Mahogany dresser", "level": 64 }, { "item": "rs:teak_treasure_chest", "text": "Teak treasure chest", "level": 66 }, { "item": "rs:teak_toy_box", "text": "Teak toyo boy", "level": 68 }, { "item": "rs:carved_teak_wardrobe_costume_room", "text": "Carved teak wardrobe (costume room)", "level": 69 }, { "item": "rs:mahogany_cape_rack", "text": "Mahogany cape rack", "level": 72 }, { "item": "rs:gilded_mahogany_dresser", "text": "Gilded mahogany dresser", "level": 74 }, { "item": "rs:mahogany_wardrobe_bedroom", "text": "Mahogany wardrobe (bedroom)", "level": 75 }, { "item": "rs:mahogany_wardrobe_costume_room", "text": "Mahogany wardrobe (costume room)", "level": 78 }, { "item": "rs:mahogany_fancy_dress_box", "text": "Mahogany fancy dress box", "level": 80 }, { "item": "rs:basic_jewellery_box", "text": "Basic jewellery box", "level": 81 }, { "item": "rs:gilded_mahogany_cape_rack", "text": "Gilded mahogany cape rack", "level": 81 }, { "item": "rs:mahogany_armour_case", "text": "Mahogany armour case", "level": 82 }, { "item": "rs:mahogany_treasure_chest", "text": "Mahogany treasure chest", "level": 84 }, { "item": "rs:fancy_jewellery_box", "text": "Fancy jewellery box", "level": 86 }, { "item": "rs:mahogany_toy_box", "text": "Mahogany toy box", "level": 86 }, { "item": "rs:gilded_mahogany_wardrobe_costume_room", "text": "Gilded mahogany wardrobe (costume room)", "level": 87 }, { "item": "rs:gilded_mahogany_wardrobe_bedroom", "text": "Gilded mahogany wardrobe (Bedroom)", "level": 87 }, { "item": "rs:marble_cape_rack", "text": "Marble cape rack", "level": 90 }, { "item": "rs:marble_wardrobe_costume_room", "text": "Marble wardrobe (costume room)", "level": 96 }, { "item": "rs:magic_stone_cape_rack", "text": "Magic stone cape rack", "level": 99 } ] }, { "name": "Decorative", "lines": [ { "item": "rs:redberry_bushes", "text": "Redberry bushes\\nPayment: Cabbages(10) x4", "level": 10 }, { "item": "rs:brown_rug", "text": "Brown rug", "level": 2 }, { "item": "rs:torn_curtains", "text": "Torn curtains", "level": 2 }, { "item": "rs:rug", "text": "Rug", "level": 13 }, { "item": "rs:curtains", "text": "Curtains", "level": 18 }, { "item": "rs:oak_clock", "text": "Oak clock", "level": 25 }, { "item": "rs:oak_lectern", "text": "Oak lectern", "level": 40 }, { "item": "rs:opulent_curtains", "text": "Opulent curtains", "level": 40 }, { "item": "rs:globe", "text": "globe", "level": 41 }, { "item": "rs:alchemical_chart", "text": "Alchemical chart", "level": 43 }, { "item": "rs:wooden_telescope", "text": "Wooden telescope", "level": 44 }, { "item": "rs:oak_eagle_lectern", "text": "Oak eagle lectern", "level": 47 }, { "item": "rs:oak_demon_lectern", "text": "Oak demon lectern", "level": 47 }, { "item": "rs:ornamental_globe", "text": "Ornamental globe", "level": 50 }, { "item": "rs:teak_clock", "text": "Teak clock", "level": 55 }, { "item": "rs:teak_eagle_lectern", "text": "Teak eagle lectern", "level": 57 }, { "item": "rs:teak_demon_lectern", "text": "Teak demon lectern", "level": 57 }, { "item": "rs:lunar_globe", "text": "Lunar globe", "level": 59 }, { "item": "rs:astronomical_chart", "text": "Astronomical chart", "level": 63 }, { "item": "rs:teak_telescope", "text": "Teak telescope", "level": 64 }, { "item": "rs:opulent_rug", "text": "Opulent rug", "level": 65 }, { "item": "rs:mahogany_eagle_lectern", "text": "Mahogany eagle lectern", "level": 67 }, { "item": "rs:mahogany_demon_lectern", "text": "Mahogany demon lectern", "level": 67 }, { "item": "rs:celestial_globe", "text": "Celestial globe", "level": 68 }, { "item": "rs:dungeon_candles", "text": "Dungeon candles", "level": 72 }, { "item": "rs:decorative_dungeon_bloodstain", "text": "Decorative dungeon bloodstain", "level": 72 }, { "item": "rs:armillary_sphere", "text": "Armillary sphere", "level": 77 }, { "item": "rs:marble_lectern", "text": "Marble lectern", "level": 77 }, { "item": "rs:infernal chart", "text": "Infernal chart", "level": 83 }, { "item": "rs:decorative_dungeon_pipe", "text": "Decorative dungeon pipe", "level": 83 }, { "item": "rs:dungeon_torches", "text": "Dungeon torches", "level": 84 }, { "item": "rs:mahoganys_scope", "text": "Mahogany's scope", "level": 84 }, { "item": "rs:gilded_mahogany_clock", "text": "Gilded mahogany clock", "level": 85 }, { "item": "rs:small_orrery", "text": "Small orrery", "level": 86 }, { "item": "rs:hanging_dungeon_skeleton", "text": "Hanging dungeon skeleton", "level": 94 }, { "item": "rs:dungeon_skull_torches", "text": "Dungeon skull torches", "level": 94 }, { "item": "rs:large_orrery", "text": "Large orrery", "level": 95 } ] }, { "name": "Trophies", "lines": [ { "item": "rs:oak_wall_decoration", "text": "Oak wall decoration", "level": 16 }, { "item": "rs:trophy_pedestal", "text": "Trophy pedestal", "level": 27 }, { "item": "rug_league_hall", "text": "Rug (league hall)", "level": 28 }, { "item": "rs:trailblazer_rug", "text": "Trailblazer rug", "level": 28 }, { "item": "rs:suit_of_armour", "text": "Suit of armour", "level": 28 }, { "item": "rs:banner_stand", "text": "Banner stand", "level": 30 }, { "item": "rs:league_statue", "text": "League statue", "level": 32 }, { "item": "rs:trailblazer_globe", "text": "Trailblazer globe", "level": 32 }, { "item": "rs:oak_outfit_stand", "text": "Oak outfit stand", "level": 34 }, { "item": "rs:small_potrait", "text": "Small potrait", "level": 35 }, { "item": "rs:oak_trophy_case", "text": "Oak trophy case", "level": 36 }, { "item": "rs:mounted_bass", "text": "mounted_bass", "level": 36 }, { "item": "rs:teak_wall_decoration", "text": "Teak wall decoration", "level": 36 }, { "item": "rs:minor_slayer_monster_head", "text": "Minor Slayer monster head", "level": 38 }, { "item": "rs:small_map", "text": "Small map", "level": 38 }, { "item": "rs:rune_display_cases", "text": "Rune display cases", "level": 41 }, { "item": "rs:mounted_sword", "text": "Mounted sword", "level": 42 }, { "item": "rs:small_landscape", "text": "Small landscape", "level": 44 }, { "item": "rs:mounted_anti_dragon_shield", "text": "Mounted Anti-Dragon Shield", "level": 47 }, { "item": "rs:mounted_amulet_of_glory", "text": "Mounted Amulet of Glory", "level": 47 }, { "item": "rs:mounted_cape_of_legends", "text": "Mounted Cape of Legends", "level": 47 }, { "item": "rs:mounted_mythical_cape", "text": "Mounted Mythical Cape", "level": 47 }, { "item": "rs:leagues_accomplishment_scroll", "text": "Leaugues accomomplishment scroll", "level": 48 }, { "item": "rs:large_potrait", "text": "Large potrait", "level": 55 }, { "item": "rs:gilded_mahogany_wall_decoration", "text": "Gilded mahogany wall decoration", "level": 56 }, { "item": "rs:mounted_swordfish", "text": "Mounted swordfish", "level": 56 }, { "item": "rs:medium_map", "text": "Medium map", "level": 58 }, { "item": "rs:medium_slayer_monster_head", "text": "Medium Slayer monster head", "level": 58 }, { "item": "rs:opulent_rug_league_hall", "text": "Opulent rug league hall", "level": 65 }, { "item": "rs:large_landscape", "text": "Large landscape", "level": 65 }, { "item": "rs:round_wall_mounted_shield", "text": "Round wall mounted shield", "level": 66 }, { "item": "rs:mahogany_outfit_stand", "text": "Mahogany outfit stand", "level": 74 }, { "item": "rs:mounted_shark", "text": "Mounted shark", "level": 76 }, { "item": "rs:square_wall_mounted_shield", "text": "Square wall-mounted shield", "level": 76 }, { "item": "rs:mahogany_trophy_case", "text": "Mahogany trophy case", "level": 78 }, { "item": "rs:major_slayer_monster_head", "text": "Major Slayer monster head", "level": 78 }, { "item": "rs:large_map", "text": "Large map", "level": 78 }, { "item": "rs:quest_list", "text": "Quest list", "level": 80 }, { "item": "rs:mounted_emblem", "text": "Mounted emblem", "level": 80 }, { "item": "rs:mounted_coins", "text": "Mounted coins", "level": 80 }, { "item": "rs:cape_hanger", "text": "Cape hanger", "level": 80 }, { "item": "rs:mounted_digsite_pendant", "text": "Mounted Digsite Pendant", "level": 82 } ] }, { "name": "Games", "lines": [ { "item": "rs:hoop_and_stick_game", "text": "Hoop-and-stick game", "level": 30 }, { "item": "rs:boxing_ring", "text": "Boxing ring", "level": 32 }, { "item": "boxing_glove_rack", "text": "Boxing glove rack", "level": 34 }, { "item": "rs:oak_prize_chest", "text": "Oak pricechest", "level": 34 }, { "item": "rs:lesser_magical_balance", "text": "Lesser magical balance", "level": 37 }, { "item": "rs:jester_game", "text": "Jester game", "level": 39 }, { "item": "rs:clay_attack_stone", "text": "Clay attack stone", "level": 39 }, { "item": "rs:fencing_ring", "text": "Fencing ring", "level": 41 }, { "item": "rs:weapons_rack", "text": "Weapons rack", "level": 44 }, { "item": "rs:teak_prize_chest", "text": "Teak_prize_chest", "level": 44 }, { "item": "rs:combat_dummy", "text": "Combat dummy", "level": 48 }, { "item": "rs:treasure_hunt_fairy_house", "text": "Treasure hunt fairy house", "level": 49 }, { "item": "rs:combat_ring", "text": "Combat ring", "level": 51 }, { "item": "rs:combat_dummy_undead_and_slayer", "text": "Combat dummy (Undead and slayer)", "level": 53 }, { "item": "rs:dartboard", "text": "Dartboard)", "level": 54 }, { "item": "rs:extra_weapons_rack", "text": "Extra weapons rack", "level": 54 }, { "item": "rs:mahogany_prize_chest", "text": "Mahogany prize chest", "level": 54 }, { "item": "rs:medium_balance", "text": "Medium balance", "level": 57 }, { "item": "rs:limestone_attack_stone", "text": "Limestone attack stone", "level": 59 }, { "item": "rs:hangman_game", "text": "Hangman game", "level": 59 }, { "item": "rs:simple_pet_arena", "text": "Simple pet arena", "level": 63 }, { "item": "rs:ranging pedestals", "text": "Ranging pedestals", "level": 71 }, { "item": "rs:greater_magical_Balance", "text": "Greater magical balance", "level": 77 }, { "item": "rs:marble_attack_stone", "text": "Marble attack stone", "level": 79 }, { "item": "rs:archery_target", "text": "Archergy target", "level": 81 }, { "item": "rs:balance_beam", "text": "Balance beam", "level": 81 } ] }, { "name": "Garden", "lines": [ { "item": "rs:exit_portal", "text": "Exit portal", "level": 1 }, { "item": "rs:low_level_plants", "text": "Low-level plants", "level": 1 }, { "item": "rs:decorative_rock", "text": "Decorative_rock", "level": 5 }, { "item": "rs:con_tree", "text": "Tree", "level": 5 }, { "item": "rs:mid_level_plants", "text": "Mid-level plants", "level": 6 }, { "item": "rs:pond", "text": "Pond", "level": 10 }, { "item": "rs:nice_tree", "text": "Nice tree", "level": 10 }, { "item": "rs:high_level_plants", "text": "High-level plants", "level": 12 }, { "item": "rs:imp_statue", "text": "Imp statue", "level": 15 }, { "item": "rs:oak_tree", "text": "Oak tree", "level": 15 }, { "item": "rs:willow_tree", "text": "Willow tree", "level": 30 }, { "item": "rs:tip_jar", "text": "Tip jar", "level": 40 }, { "item": "rs:maple_tree", "text": "Maple tree", "level": 45 }, { "item": "rs:boundary_stones", "text": "Boundary stones", "level": 55 }, { "item": "rs:thorny_hedge", "text": "Thorny hedge", "level": 56 }, { "item": "rs:desert_pet_habitat", "text": "Desert_pet_habitat", "level": 57 }, { "item": "rs:wooden_fence", "text": "Wooden fence", "level": 59 }, { "item": "rs:nice_hedge", "text": "Nice hedge", "level": 60 }, { "item": "rs:yew_tree", "text": "Yew tree", "level": 60 }, { "item": "rs:stone_wall", "text": "Stone wall", "level": 63 }, { "item": "rs:small_box_hedge", "text": "Small box hedge", "level": 64 }, { "item": "rs:gazebo", "text": "Gazebo", "level": 65 }, { "item": "rs:zen_theme", "text": "Zen theme", "level": 65 }, { "item": "rs:topiary_bush", "text": "Topiary bush", "level": 65 }, { "item": "rs:sunflower", "text": "Sunflower", "level": 66 }, { "item": "rs:rosemary", "text": "Rosemary", "level": 66 }, { "item": "rs:teak_garden_bench", "text": "Teak garden bench", "level": 66 }, { "item": "rs:iron_railings", "text": "Iron railings", "level": 67 }, { "item": "rs:topiary_hedge", "text": "Topiary hedge", "level": 68 }, { "item": "rs:dungeon_entrance", "text": "Dungeon entrance", "level": 70 }, { "item": "rs:marigolds", "text": "marigolds", "level": 71 }, { "item": "rs:daffodils", "text": "Daffodils", "level": 71 }, { "item": "rs:picket_fence", "text": "Picket fence", "level": 71 }, { "item": "rs:small_fountain", "text": "Small fountain", "level": 71 }, { "item": "rs:fancy_hedge", "text": "Fancy hedge", "level": 72 }, { "item": "rs:magic_tree", "text": "Magic tree", "level": 75 }, { "item": "rs:otherwordly_theme", "text": "Otherworldly_theme", "level": 75 }, { "item": "rs:spirit_tree", "text": "Spirit tree", "level": 75 }, { "item": "rs:large_fountain", "text": "Large fountain", "level": 75 }, { "item": "rs:garden_fence", "text": "Garden_fence", "level": 75 }, { "item": "rs:tall_fancy_hedge", "text": "Tall fancy hedge", "level": 76 }, { "item": "rs:topiary_bush", "text": "Topiary bush", "level": 65 }, { "item": "rs:topiary_bush", "text": "Topiary bush", "level": 65 }, { "item": "rs:roses", "text": "Roses", "level": 76 }, { "item": "rs:bluebells", "text": "Bluebells", "level": 76 }, { "item": "rs:gnome_bench", "text": "Gnome bench", "level": 77 }, { "item": "rs:marble_wall", "text": "Marble wakk", "level": 79 }, { "item": "rs:obelisk", "text": "Obelisk", "level": 80 }, { "item": "rs:tall_box_hedge", "text": "Tall box hedge", "level": 80 }, { "item": "rs:posh_fountain", "text": "Posh fountain", "level": 81 }, { "item": "rs:fairy_ring", "text": "Fairy ring", "level": 85 }, { "item": "rs:marble_garden_bench", "text": "Marble garden bench (decoration only)", "level": 88 } ] }, { "name": "Dungeon", "lines": [ { "item": "rs:throne_room_floor_decoration", "text": "2x Throne room floor decoration", "level": 61 }, { "item": "rs:oak_cage", "text": "Oak cage", "level": 65 }, { "item": "rs:oubliette_spikes", "text": "Oubliette spikes", "level": 65 }, { "item": "rs:steel_cage", "text": "Steel cage", "level": 68 }, { "item": "rs:oak_and_steel_cage", "text": "Oak and steel cage", "level": 70 }, { "item": "rs:skeleton_guard", "text": "Skeleton guard", "level": 70 }, { "item": "rs:tentacle_pool", "text": "Tentacle pool", "level": 71 }, { "item": "rs:spike_trap", "text": "Spike trap", "level": 72 }, { "item": "rs:large_trapdoor", "text": "Large trapdoor", "level": 74 }, { "item": "rs:oak_dungeon_door", "text": "Oak dungeon door", "level": 74 }, { "item": "rs:guard_dog", "text": "Guard dog", "level": 74 }, { "item": "rs:wooden_dungeon_treasure_crate", "text": "Wooden dungeon treasure crate", "level": 75 }, { "item": "rs:demon", "text": "Demon", "level": 75 }, { "item": "rs:steel_cage", "text": "Steel cage", "level": 75 }, { "item": "rs:man_trap", "text": "Man trap", "level": 76 }, { "item": "rs:oubliette_flame_pit", "text": "Oubliette flame pit", "level": 77 }, { "item": "rs:hobgoblin_guard", "text": "Hobgoblin guard", "level": 78 }, { "item": "rs:oak_dungeon_treasure_chest", "text": "Oak dungeon treasure chest", "level": 79 }, { "item": "rs:spiked_cage", "text": "Spiked cage", "level": 80 }, { "item": "rs:tangle_vine", "text": "Tangle vine", "level": 80 }, { "item": "rs:Kalphite soldier", "text": "Kalphite soldier", "level": 80 }, { "item": "rs:lesser_magic_cage", "text": "Lesser magic cage", "level": 82 }, { "item": "rs:baby_red_dragon", "text": "Baby red dragon", "level": 82 }, { "item": "rs:teak_dungeon_treasure_chest", "text": "Teak dungeon treasure chest", "level": 83 }, { "item": "rs:rocnar", "text": "Rocnar", "level": 83 }, { "item": "rs:steel_plated_oak_door", "text": "Steel plated oak door", "level": 84 }, { "item": "rs:marble_trap", "text": "Marble trap", "level": 84 }, { "item": "rs:tok-xil", "text": "Tok-Xil", "level": 85 }, { "item": "rs:bone_cage", "text": "Bone cage", "level": 85 }, { "item": "rs:huge_spider", "text": "Huge spider", "level": 86 }, { "item": "rs:mahogany_dungeon_treasure_chest", "text": "Mahogany dungeon treasure chest", "level": 87 }, { "item": "rs:teleport_trap", "text": "Teleport trap", "level": 88 }, { "item": "rs:greater_magic_cage", "text": "Greater magic cage", "level": 89 }, { "item": "rs:troll_guard", "text": "Troll guard", "level": 90 }, { "item": "rs:dagannoth", "text": "Dagannoth", "level": 90 }, { "item": "rs:magic_dungeon_treasure_chest", "text": "Magic dungeon treasure chest", "level": 91 }, { "item": "rs:marble_dungeon_door", "text": "Marble dungeon door", "level": 94 }, { "item": "rs:hellhound", "text": "Hellhound", "level": 94 }, { "item": "rs:steel_dragon", "text": "steel_dragon", "level": 95 } ] }, { "name": "Chapel", "lines": [ { "item": "rs:oak_altar", "text": "Oak altar", "level": 45 }, { "item": "rs:steel_torches_chapel", "text": "Steel torches chapel", "level": 45 }, { "item": "rs:icon_of_gnome_child", "text": "Icon of Gnome Child", "level": 45 }, { "item": "rs:symbol_of_saradomin", "text": "Symbol of Saradomin", "level": 48 }, { "item": "rs:symbol_of_guthix", "text": "Symbol of Guthix", "level": 48 }, { "item": "rs:symbol_of_zamorak", "text": "Symbol of Zamorak", "level": 48 }, { "item": "rs:wooden_torches_chapel", "text": "Wooden torches (chapel)", "level": 48 }, { "item": "rs:chapel_windchimes", "text": "Chapel windchimes", "level": 49 }, { "item": "rs:small_chapel_statue", "text": "Small chapel statue", "level": 49 }, { "item": "rs:shuttered_chapel_window", "text": "Shuttered chapel window", "level": 49 }, { "item": "rs:teak_altar", "text": "Teak altar", "level": 50 }, { "item": "rs:steel_candlesticks", "text": "Steel candlesticks", "level": 53 }, { "item": "rs:cloth_covered_teak_altar", "text": "Cloth covered teak altar", "level": 56 }, { "item": "rs:gold_candlesticks", "text": "Gold candlesticks", "level": 57 }, { "item": "rs:chapel_bells", "text": "Chapel bells", "level": 58 }, { "item": "rs:icon_of_saradomin", "text": "Icon of Saradomin", "level": 59 }, { "item": "rs:Icon of Guthix", "text": "Icon of Guthix", "level": 59 }, { "item": "rs:icon_of_zamorak", "text": "Icon of Zamorak", "level": 59 }, { "item": "rs:cloth_covered_mahogany_altar", "text": "Cloth-covered mahogany altar", "level": 60 }, { "item": "rs:oak_incense_burners", "text": "Oak incense burners", "level": 61 }, { "item": "rs:limestone_altar", "text": "Limestone altar", "level": 64 }, { "item": "rs:mahogany_incense_burners", "text": "Mahogany incense burners", "level": 65 }, { "item": "rs:medium_chapel_statue", "text": "Medium_chapel_statue", "level": 69 }, { "item": "rs:chapel_organ", "text": "Chapel organ", "level": 69 }, { "item": "rs:decorative_chapel_window", "text": "Decorative chapel window", "level": 69 }, { "item": "rs:marble_incense_burner", "text": "Marble incense burner", "level": 69 }, { "item": "rs:marble_altar", "text": "Marble altar", "level": 70 }, { "item": "rs:icon_of_bob_the_cat", "text": "Icon of bob the Cat", "level": 71 }, { "item": "rs:gilded_marble_altar", "text": "Gilded marble altar", "level": 75 }, { "item": "rs:large_chapel_statue", "text": "Large chapel statue", "level": 89 }, { "item": "rs:stained_glass_chapel_window", "text": "Stained-glass chapel window", "level": 89 } ] }, { "name": "Other", "lines": [ { "item": "rs:cup_of_tea", "text": "Mahogany Homes (beginner)", "level": 1 }, { "item": "rs:cat_blanket", "text": "Cat blanket", "level": 5 }, { "item": "rs:stash_units", "text": "STASH units (beginner)", "level": 12 }, { "item": "rs:cat_basket", "text": "Cat basket", "level": 19 }, { "item": "rs:mahogany_homes", "text": "Mahogany Homes(novice)", "level": 20 }, { "item": "rs:rope_bell_pull", "text": "Rope bell pull", "level": 26 }, { "item": "rs:oak_staircase", "text": "Oak staircase", "level": 27 }, { "item": "rs:stash_units_easy", "text": "STASH units (easy)", "level": 27 }, { "item": "rs:cushioned_cat_basket", "text": "Cushioned cat basket", "level": 33 }, { "item": "rs:teak_bell_pull", "text": "Teak bell pull", "level": 37 }, { "item": "rs:crystal_ball", "text": "Crystal ball", "level": 42 }, { "item": "rs:stash_units_medium", "text": "STASH units (medium)", "level": 42 }, { "item": "rs:teak_staircase", "text": "Teak staircase", "level": 48 }, { "item": "rs:teak_portal_frame", "text": "Teak portal frame", "level": 50 }, { "item": "rs:cup_of_tea", "text": "Mahogany Homes (adept)", "level": 50 }, { "item": "rs:elemental_sphere", "text": "Elemental sphere", "level": 54 }, { "item": "rs:stash_units_hard", "text": "STASH units (hard)", "level": 50 }, { "item": "rs:hallowed_sepulchre", "text": "Hallowed Sepulchre - Repair bridge", "level": 56 }, { "item": "rs:gilded_teak_bell_pull", "text": "Gilded teak bell pull", "level": 60 }, { "item": "rs:greater_teleport_focus", "text": "Greater teleport focus", "level": 65 }, { "item": "rs:mahogany_portal_frame", "text": "Mahogany portal frame", "level": 65 }, { "item": "rs:crystal_of_power", "text": "Crystal of power", "level": 66 }, { "item": "rs:limestone_spiral_staircase", "text": "Limestone spiral staircase", "level": 67 }, { "item": "rs:oak_oubliette_ladrer", "text": "Oak oubliette ladder", "level": 68 }, { "item": "rs:oak_lever", "text": "Oak lever", "level": 68 }, { "item": "rs:stash_units_elite", "text": "STASH units (Elite)", "level": 77 }, { "item": "rs:teak_oubliette_ladder", "text": "Teak oubliette ladder", "level": 78 }, { "item": "rs:teak_lever", "text": "Teak lever", "level": 78 }, { "item": "rs:marble_portal_frame", "text": "Marble portal frame", "level": 80 }, { "item": "rs:scrying_pool", "text": "Scrying pool", "level": 80 }, { "item": "rs:marble_staircase", "text": "Marble staircase", "level": 82 }, { "item": "rs:mahogany_oubliette_ladder", "text": "Mahogany oubliette ladder", "level": 88 }, { "item": "rs:mahogany_lever", "text": "Mahogany lever", "level": 88 }, { "item": "rs:stash_units_master", "text": "STASH units (master)", "level": 88 }, { "item": "rs:marble_spiral", "text": "Marble spiral", "level": 97 } ] }, { "name": "Servants", "lines": [ { "item": "rs:servant_rick", "text": "Rick", "level": 20 }, { "item": "rs:servant_maid", "text": "Maid", "level": 25 }, { "item": "rs:servant_cook", "text": "Cook", "level": 30 }, { "item": "rs:servant_butler", "text": "Butler", "level": 39 }, { "item": "rs:servant_demon_butler", "text": "Demon Butler", "level": 50 } ] }, { "name": "House Size", "lines": [ { "item": "rs:hammer", "text": "Maximum rooms: 24", "level": 1 }, { "item": "rs:saw", "text": "Maximum dimensions: 3x3", "level": 1 }, { "item": "rs:saw", "text": "Maximum dimensions: 4x4", "level": 15 }, { "item": "rs:saw", "text": "Maximum dimensions: 5x5", "level": 30 }, { "item": "rs:saw", "text": "Maximum dimensions: 6x6", "level": 45 }, { "item": "rs:hammer", "text": "Maximum rooms: 25", "level": 50 }, { "item": "rs:hammer", "text": "Maximum rooms: 26", "level": 56 }, { "item": "rs:saw", "text": "Maximum dimensions: 7x7", "level": 60 }, { "item": "rs:hammer", "text": "Maximum rooms: 27", "level": 62 }, { "item": "rs:hammer", "text": "Maximum rooms: 28", "level": 68 }, { "item": "rs:hammer", "text": "Maximum rooms: 29", "level": 74 }, { "item": "rs:hammer", "text": "Maximum rooms: 30", "level": 80 }, { "item": "rs:hammer", "text": "Maximum rooms: 31", "level": 86 }, { "item": "rs:hammer", "text": "Maximum rooms: 32", "level": 92 }, { "item": "rs:hammer", "text": "Maximum rooms: 33", "level": 96 }, { "item": "rs:hammer", "text": "Maximum rooms: 34", "level": 99 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/cooking.json ================================================ { "id": 129, "name": "Cooking", "members": false, "sub_guides": [ { "name": "Meats", "lines": [ { "item": "rs:meat", "text": "Meat", "level": 1 }, { "item": "rs:shrimps", "text": "Shrimp", "level": 1 }, { "item": "rs:chicken", "text": "Chicken", "level": 1 }, { "item": "rs:anchovies", "text": "Anchovies", "level": 1 }, { "item": "rs:sardine", "text": "Sardine", "level": 1 }, { "item": "rs:karambwan", "text": "Members: Karambwan", "level": 1 }, { "item": "rs:ugthanki_kebab", "text": "Ugthanki kebab", "level": 1 }, { "item": "rs:herring", "text": "Herring", "level": 5 }, { "item": "rs:mackerel", "text": "Mackerel", "level": 10 }, { "item": "rs:thin_snail_meat", "text": "Members: Thin snail", "level": 12 }, { "item": "rs:trout", "text": "Trout", "level": 15 }, { "item": "rs:lean_snail_meat", "text": "Members: Lean snail", "level": 17 }, { "item": "rs:cod", "text": "Members: Cod", "level": 18 }, { "item": "rs:pike", "text": "Pike", "level": 20 }, { "item": "rs:salmon", "text": "Salmon", "level": 25 }, { "item": "rs:tuna", "text": "Tuna", "level": 30 }, { "item": "rs:cooked_chompy", "text": "Members: Cooked chompy", "level": 30 }, { "item": "rs:fish_cake", "text": "Members: Fishcakes", "level": 31 }, { "item": "rs:cave_eel", "text": "Members: Cave eel", "level": 38 }, { "item": "rs:lobster", "text": "Lobster", "level": 40 }, { "item": "rs:jubbly", "text": "Members: Jubbly", "level": 41 }, { "item": "rs:bass", "text": "Members: Bass", "level": 43 }, { "item": "rs:swordfish", "text": "Swordfish", "level": 45 }, { "item": "rs:lava_eel", "text": "Members: Lava eel", "level": 53 }, { "item": "rs:shark", "text": "Members: Shark", "level": 80 }, { "item": "rs:sea_turtle", "text": "Members: Sea turtle", "level": 82 }, { "item": "rs:manta_ray", "text": "Members: Manta ray", "level": 90 } ] }, { "name": "Bread", "lines": [ { "item": "rs:bread", "text": "Bread", "level": 1 }, { "item": "rs:pitta_bread", "text": "Members: Pitta bread", "level": 58 } ] }, { "name": "Pies", "lines": [ { "item": "rs:redberry_pie", "text": "Redberry pie", "level": 10 }, { "item": "rs:meat_pie", "text": "Meat pie", "level": 20 }, { "item": "rs:mud_pie", "text": "Members: Mud pie", "level": 29 }, { "item": "rs:apple_pie", "text": "Apple pie", "level": 30 }, { "item": "rs:garden_pie", "text": "Members: Garden pie", "level": 34 }, { "item": "rs:fish_pie", "text": "Members: Fish pie", "level": 47 }, { "item": "rs:admiral_pie", "text": "Members: Admiral pie", "level": 70 }, { "item": "rs:wild_pie", "text": "Members: Wild pie", "level": 85 }, { "item": "rs:summer_pie", "text": "Members: Summer pie", "level": 95 } ] }, { "name": "Stews", "lines": [ { "item": "rs:stew", "text": "Stew", "level": 25 }, { "item": "rs:banana_stew", "text": "Banana stew", "level": 25 }, { "item": "rs:spicy_stew", "text": "Spicy stew", "level": 25 } ] }, { "name": "Pizzas", "lines": [ { "item": "rs:plain_pizza", "text": "Plain pizza", "level": 35 }, { "item": "rs:meat_pizza", "text": "Meat pizza", "level": 45 }, { "item": "rs:anchovy_pizza", "text": "Anchovy pizza", "level": 55 }, { "item": "rs:pineapple_pizza", "text": "Members: Pineapple pizza", "level": 65 } ] }, { "name": "Cakes", "lines": [ { "item": "rs:cake", "text": "Cake", "level": 40 }, { "item": "rs:chocolate_cake", "text": "Chocolate cake", "level": 50 } ] }, { "name": "Wine", "lines": [ { "item": "rs:wine", "text": "Wine", "level": 35 }, { "item": "rs:wine_of_zamorak", "text": "Members: Wine of Zamorak", "level": 65 } ] }, { "name": "Hot drinks", "lines": [ { "item": "rs:nettle_tea", "text": "Nettle tea", "level": 20 } ] }, { "name": "Brewing", "lines": [ { "item": "rs:cider", "text": "Cider (4 Apple mush)", "level": 14 }, { "item": "rs:dwarven_stout", "text": "Dwarven Stout (4 Hammerstone hops)", "level": 19 }, { "item": "rs:asgarnian_ale", "text": "Asgarnian Ale (4 Asgarnian hops)", "level": 24 }, { "item": "rs:greenmans_ale", "text": "Greenman's Ale (4 Harralander leaves)", "level": 29 }, { "item": "rs:wizards_mind_bomb", "text": "Wizard's Mind Bomb (4 Yinillian hops)", "level": 34 }, { "item": "rs:dragon_bitter", "text": "Dragon Bitter (4 Krandorian hops)", "level": 39 }, { "item": "rs:moonlight_mead", "text": "Moonlight Mead (4 Bittercap mushrooms)", "level": 44 }, { "item": "rs:axemans_folly", "text": "Axeman's Folly (1 Oak root)", "level": 49 }, { "item": "rs:chefs_delight", "text": "Chef's Delight (4 Portions of chocolate dust)", "level": 54 }, { "item": "rs:slayers_respite", "text": "Slayer's Respite (4 Wildblood hops)", "level": 54 } ] }, { "name": "Vegetable", "lines": [ { "item": "rs:baked_potato", "text": "Baked potato", "level": 7 }, { "item": "rs:spicy_sauce", "text": "Spicy sauce (topping ingredient)", "level": 9 }, { "item": "rs:scrambled_egg", "text": "Scrambled egg (topping ingredient)", "level": 13 }, { "item": "rs:scrambled_egg_and_tomato", "text": "Scrambled egg and tomato (topping)", "level": 23 }, { "item": "rs:sweetcorn", "text": "Sweetcorn", "level": 28 }, { "item": "rs:baked_potato_with_butter", "text": "Baked potato with butter", "level": 39 }, { "item": "rs:fried_onion", "text": "Fried onion (topping ingredient)", "level": 42 }, { "item": "rs:fried_mushroom", "text": "Fried mushroom (topping ingredient)", "level": 46 }, { "item": "rs:baked_potato_with_butter_and_cheese", "text": "Baked potato with butter and cheese", "level": 47 }, { "item": "rs:baked_potato_with_egg_and_tomato", "text": "Baked potato with egg and tomato", "level": 51 }, { "item": "rs:fried_mushroom_and_onion", "text": "Fried mushroom and onion (topping)", "level": 57 }, { "item": "rs:baked_potato_with_mushroom_and_onion", "text": "Baked potato with mushroom and onion", "level": 64 }, { "item": "rs:tuna_and_sweetcorn", "text": "Tuna and sweetcorn (topping)", "level": 67 }, { "item": "rs:baked_potato_with_tuna_and_sweetcorn", "text": "Baked potato with tuna and sweetcorn", "level": 68 } ] }, { "name": "Diary", "lines": [ { "item": "rs:butter", "text": "Butter", "level": 38 }, { "item": "rs:cheese", "text": "Cheese", "level": 48 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/crafting.json ================================================ { "id": 131, "name": "Crafting", "members": false, "sub_guides": [ { "name": "Weaving", "lines": [ { "item": "rs:vegetable_sack", "text": "Vegetable sack", "level": 21 }, { "item": "rs:drift_net", "text": "Drift net", "level": 26 }, { "item": "rs:fruit_basket", "text": "Fruit basket", "level": 36 } ] }, { "name": "Armour", "lines": [ { "item": "rs:leather_gloves", "text": "Leather gloves", "level": 1 }, { "item": "rs:leather_boots", "text": "Leather boots", "level": 7 }, { "item": "rs:leather_cowl", "text": "Leather cowl", "level": 5 }, { "item": "rs:leather_vambraces", "text": "Leather vambraces", "level": 11 }, { "item": "rs:leather_body", "text": "Leather body", "level": 14 }, { "item": "rs:snelmet", "text": "Members: Snail helmet", "level": 15 }, { "item": "rs:crab_shell_armour", "text": "Crab shell armour", "level": 15 }, { "item": "rs:leather_chaps", "text": "Leather chaps", "level": 18 }, { "item": "rs:hard_leather_body", "text": "Hard leather body", "level": 28 }, { "item": "rs:broodoo_shield", "text": "Members: Broodoo shield", "level": 35 }, { "item": "rs:coif", "text": "Members: Coif", "level": 38 }, { "item": "rs:studded_body", "text": "Members: Studded body", "level": 41 }, { "item": "rs:studded_chaps", "text": "Members: Studded chaps", "level": 44 }, { "item": "rs:snakeskin_boots", "text": "Members: Snakeskin boots", "level": 45 }, { "item": "rs:snakeskin_vambraces", "text": "Members: Snakeskin vambraces", "level": 47 }, { "item": "rs:snakeskin_bandana", "text": "Members: Snakeskin Bandana", "level": 48 }, { "item": "rs:snakeskin_chaps", "text": "Members: Snakeskin chaps", "level": 51 }, { "item": "rs:snakeskin_body", "text": "Members: Snakeskin body", "level": 53 }, { "item": "rs:green_dragonhide_vambraces", "text": "Members: Green Dragonhide vambraces", "level": 57 }, { "item": "rs:green_dragonhide_chaps", "text": "Members: Green dragonhide chaps", "level": 60 }, { "item": "rs:splitbark_gauntlets", "text": "Members: Splitbark gauntlets", "level": 60 }, { "item": "rs:splitbark_boots", "text": "Members: Splitbark boots", "level": 60 }, { "item": "rs:splitbark_helm", "text": "Members: Splitbark helm", "level": 61 }, { "item": "rs:splitbark_body", "text": "Members: Splitbark body", "level": 62 }, { "item": "rs:splitbark_legs", "text": "Members: Splitbark legs", "level": 62 }, { "item": "rs:green_dragonhide_chaps", "text": "Members: Green dragonhide chaps", "level": 63 }, { "item": "rs:blue_dragonhide_chaps", "text": "Members: Blue dragonhide chaps", "level": 66 }, { "item": "rs:blue_dragonhide_chaps", "text": "Members: Blue dragonhide chaps", "level": 68 }, { "item": "rs:blue_dragonhide_body", "text": "Members: Blue dragonhide body", "level": 71 }, { "item": "rs:red_dragonhide_vambraces", "text": "Members: Red dragonhide vambraces", "level": 73 }, { "item": "rs:red_dragonhide_chaps", "text": "Members: Red dragonhide chaps", "level": 75 }, { "item": "rs:red_dragonhide_body", "text": "Members: Red dragonhide body", "level": 77 }, { "item": "rs:black_dragonhide_vambraces", "text": "Members: Snakeskin chaps", "level": 79 }, { "item": "rs:black_dragonhide_chaps", "text": "Members: Snakeskin chaps", "level": 82 }, { "item": "rs:black_dragonhide_body", "text": "Members: Black dragonhide body", "level": 84 } ] }, { "name": "Spinning", "lines": [ { "item": "rs:wool", "text": "Wool", "level": 1 }, { "item": "rs:bow_strings", "text": "Members: Flax into bow strings", "level": 10 }, { "item": "rs:sinew_strings", "text": "Members: Sinew into crossbow strings", "level": 10 }, { "item": "rs:magic_strings", "text": "Members: Magic tree roots into magic strings", "level": 19 }, { "item": "rs:yak_hair", "text": "Members: Yak hair into rope", "level": 30 } ] }, { "name": "Pottery", "lines": [ { "item": "rs:pot", "text": "Pot", "level": 1 }, { "item": "rs:pie_dish", "text": "Pie dish", "level": 7 }, { "item": "rs:bowl", "text": "Bowl", "level": 8 }, { "item": "rs:plant_pot", "text": "Members: Plant pot", "level": 19 }, { "item": "rs:pot_lid", "text": "Members: Pot lid", "level": 25 } ] }, { "name": "Glass", "lines": [ { "item": "rs:beer_glass", "text": "Beer Glass", "level": 1 }, { "item": "rs:candle_lantern", "text": "Candle lantern", "level": 4 }, { "item": "rs:oil_lamp", "text": "Oil lamp", "level": 12 }, { "item": "rs:oil_lantern", "text": "Oil lantern", "level": 26 }, { "item": "rs:vial", "text": "Vial", "level": 33 }, { "item": "rs:fishbowl", "text": "Fishbowl", "level": 42 }, { "item": "rs:glass_orb", "text": "Glass orb", "level": 46 }, { "item": "rs:bullseye_lantern_lens", "text": "Bullsye lantern lens", "level": 49 } ] }, { "name": "Jewellery", "lines": [ { "item": "rs:cut_opal", "text": "Cut opal", "level": 1 }, { "item": "rs:opal_ring", "text": "Opal ring", "level": 1 }, { "item": "rs:polished_buttons", "text": "Polished buttons", "level": 3 }, { "item": "rs:gold_ring", "text": "Gold ring", "level": 5 }, { "item": "rs:gold_necklace", "text": "Gold necklace", "level": 6 }, { "item": "rs:gold_amulet", "text": "Gold amulet", "level": 8 }, { "item": "rs:cut_jade", "text": "Cut jade", "level": 13 }, { "item": "rs:jade_ring", "text": "Jade ring", "level": 13 }, { "item": "rs:holy_symbol", "text": "Holy symbol", "level": 16 }, { "item": "rs:cut_red_topaz", "text": "Members: Cut red topaz", "level": 16 }, { "item": "rs:cut_sapphire", "text": "Cut sapphire", "level": 20 }, { "item": "rs:sapphire_ring", "text": "Sapphire ring", "level": 20 }, { "item": "rs:sapphire_necklace", "text": "Sapphire necklace", "level": 22 }, { "item": "rs:tiara", "text": "Tiara", "level": 23 }, { "item": "rs:sapphire_amulet", "text": "Sapphire amulet", "level": 24 }, { "item": "rs:cut_emerald", "text": "Cut emerald", "level": 27 }, { "item": "rs:emerald_ring", "text": "Emerald ring", "level": 27 }, { "item": "rs:emerald_necklace", "text": "Emerald necklace", "level": 29 }, { "item": "rs:emerald_amulet", "text": "Emerald amulet", "level": 31 }, { "item": "rs:cut_ruby", "text": "Cut ruby", "level": 34 }, { "item": "rs:ruby_ring", "text": "Ruby ring", "level": 34 }, { "item": "rs:ruby_necklace", "text": "Ruby necklace", "level": 40 }, { "item": "rs:cut_diamond", "text": "Cut diamond", "level": 43 }, { "item": "rs:diamond_ring", "text": "Diamond ring", "level": 43 }, { "item": "rs:ruby_amulet", "text": "Ruby amulet", "level": 50 }, { "item": "rs:cut_dragonstone", "text": "Members: Cut Dragonstone", "level": 55 }, { "item": "rs:dragonstone_ring", "text": "Members: Dragonstone ring", "level": 55 }, { "item": "rs:diamond_necklace", "text": "Diamond necklace", "level": 56 }, { "item": "rs:cut_onyx", "text": "Members: Cut onyx", "level": 67 }, { "item": "rs:onyx_ring", "text": "Onyx Ring", "level": 67 }, { "item": "rs:diamond_amulet", "text": "Diamond amulet", "level": 70 }, { "item": "rs:dragonstone_amulet", "text": "Members: Dragonstone amulet", "level": 80 }, { "item": "rs:onyx_necklace", "text": "Members Onyx necklace", "level": 82 }, { "item": "rs:onyx_amulet", "text": "Members: Onyx amulet", "level": 90 } ] }, { "name": "Weaponry", "lines": [ { "item": "rs:silver_sickle", "text": "Silver sickle", "level": 18 }, { "item": "rs:silver_crossbow_bolts", "text": "Silver crossbow bolts", "level": 21 }, { "item": "rs:water_battlestaff", "text": "Water battlestaf", "level": 54 }, { "item": "rs:earth_battlestaff", "text": "Earth battlestaf", "level": 58 }, { "item": "rs:fire_battlestaff", "text": "Fire battlestaf", "level": 62 }, { "item": "rs:air_battlestaff", "text": "Air battlestaf", "level": 66 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/defence.json ================================================ { "id": 124, "name": "Defence", "members": false, "sub_guides": [ { "name": "Armour", "lines": [ { "item": "rs:bronze_medium_helm", "text": "Bronze armour\\nwield any bronze equipment", "level": 1 }, { "item": "rs:iron_medium_helm", "text": "Iron armour\\nwield any iron equipment", "level": 1 }, { "item": "rs:steel_medium_helm", "text": "Steel armour\\nwield any steel equipment", "level": 5 }, { "item": "rs:black_medium_helm", "text": "Black armour\\nwield any black equipment", "level": 10 }, { "item": "rs:white_medium_helm", "text": "Members: white armour\\nwield any white equipment", "level": 10 }, { "item": "rs:initiate_medium_helm", "text": "Members: Initiate armour\\n(after Recruitment Drive, with 10 Prayer)", "level": 20 }, { "item": "rs:mithril_medium_helm", "text": "Mithril armour\\nwield any mithril equipment", "level": 20 }, { "item": "rs:adamant_medium_helm", "text": "Adamant armour\\nwield any adamant equipment", "level": 30 }, { "item": "rs:proselyte_medium_helm", "text": "Members: Proselyte armour\\n(after Slug Menance, with 20 Prayer)", "level": 30 }, { "item": "rs:rune_medium_helm", "text": "Rune armour\\nwield any rune equipment", "level": 40 }, { "item": "rs:rock_shell_helm", "text": "Members: Rock-shell armour\\n(after completing Fremennik Trials)", "level": 40 }, { "item": "rs:berserker_helm", "text": "Members: Fremennik helmets\\n(after completing Fremennik Trials)", "level": 45 }, { "item": "rs:granite_shield", "text": "Granite armour\\nwield any granite equipment", "level": 50 }, { "item": "rs:helm_of_neitiznot", "text": "Members: Helm of neitiznot\\n(after Fremennik Isles)", "level": 55 }, { "item": "rs:Members: Dragon armour", "text": "Dragon armour\\nwield any dragon equipment", "level": 60 }, { "item": "rs:3rd_age_full_helmet", "text": "3rd Age armour\\nwield any 3rd Age equipment", "level": 65 }, { "item": "rs:torags_helmet", "text": "Barrows armour\\nwield any Barrows equipment", "level": 70 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/farming.json ================================================ { "id": 142, "name": "Farming", "members": true, "sub_guides": [ { "name": "Allotments", "lines": [ { "item": "rs:potatoes", "text": "Potatoes\\nPayment: Compost x2", "level": 1 }, { "item": "rs:onions", "text": "Onions\\nPayment: Potatoes(10)", "level": 5 }, { "item": "rs:cabbages", "text": "Cabbages\\nPayment: Onions(10)", "level": 7 }, { "item": "rs:tomatoes", "text": "Tomatoes\\nPayment: Cabbages(10)", "level": 12 }, { "item": "rs:sweetcorn", "text": "Sweetcorn\\nPayment: Jute fibre x10", "level": 20 }, { "item": "rs:strawberries", "text": "Strawberries\\nPayment: Apples(5)", "level": 31 }, { "item": "rs:watermelons", "text": "Watermelons\\nPayment: Curry leaf x10", "level": 47 }, { "item": "rs:snape_grass", "text": "Snape grass\\nPayment: Jangerberries x5", "level": 61 } ] }, { "name": "Hops", "lines": [ { "item": "rs:barley", "text": "Barley\\nPayment: Compost x3", "level": 3 }, { "item": "rs:hammerstone_hops", "text": "Hammerstone hops\\nPayment: Marigolds", "level": 4 }, { "item": "rs:asgarnian_hops", "text": "Asgarnian hops\\nPayment: Onions(10)", "level": 8 }, { "item": "rs:jute_plants", "text": "Jute plants\\nPayment: Barley malt x6", "level": 13 }, { "item": "rs:yanillian_hops", "text": "Yanillian hops\\nPayment: Tomatoes(5)", "level": 16 }, { "item": "rs:krandorian_hops", "text": "Krandorian hops\\nPayment: Cabbages(10) x3", "level": 21 }, { "item": "rs:wildblood_hops", "text": "Wildblood hops\\nPayment: Nasturtiums", "level": 28 } ] }, { "name": "Trees", "lines": [ { "item": "rs:oak_trees", "text": "Oak trees\\nPayment: Tomatoes(5)", "level": 15 }, { "item": "rs:willow_trees", "text": "Willow trees\\nPayment: Apples(5)", "level": 30 }, { "item": "rs:maple_trees", "text": "Mapple trees\\nPayment: Oranges(5)", "level": 45 }, { "item": "rs:yew_trees", "text": "Yew trees\\nPayment: Cactus spine x10", "level": 60 }, { "item": "rs:magic_trees", "text": "Magic trees\\nPayment: Coconut x25", "level": 75 } ] }, { "name": "Fruit Trees", "lines": [ { "item": "rs:apple_trees", "text": "Apple trees\\nPayment: Sweetcorn x9", "level": 27 }, { "item": "rs:banana_trees", "text": "Banana trees\\nPayment: Apples(5) x4", "level": 33 }, { "item": "rs:orange_trees", "text": "Orange trees\\nPayment: Strawberries(5) x3", "level": 39 }, { "item": "rs:curry_trees", "text": "Curry trees\\nPayment: Bananas(5) x3", "level": 42 }, { "item": "rs:pineapple_trees", "text": "Pineapple trees\\nPayment: Watermelon x10", "level": 51 }, { "item": "rs:papaya_trees", "text": "Papaya trees\\nPayment: Pineapple x10", "level": 57 }, { "item": "rs:palm_trees", "text": "Palm trees\\nPayment: Papaya fruit x15", "level": 68 } ] }, { "name": "Bushes", "lines": [ { "item": "rs:redberry_bushes", "text": "Redberry bushes\\nPayment: Cabbages(10) x4", "level": 10 }, { "item": "rs:cadavaberry_bushes", "text": "Cadavanerry bushes\\nPayment: Tomatoes(5) x3", "level": 22 }, { "item": "rs:dwellberry_bushes", "text": "Dwellberry bushes\\nPayment: Strawberries(5) x3", "level": 36 }, { "item": "rs:jangerberry_bushes", "text": "Jangerberry bushes\\nPayment: Watermelon x6", "level": 48 }, { "item": "rs:white_berry_bushes", "text": "White berry bushes\\nPayment: Mushroom x8", "level": 59 }, { "item": "rs:poison_ivy_bushes", "text": "Poison ivy bushes", "level": 70 } ] }, { "name": "Flowers", "lines": [ { "item": "rs:marigolds", "text": "Marigolds\\nProtects onions, tomatoes and potatoes", "level": 2 }, { "item": "rs:rosemary", "text": "Rosemary\\nProtects cabbages from disease", "level": 22 }, { "item": "scarecrow", "text": "Make and place a scarecrow\\nProtects sweetcorn from birds", "level": 23 }, { "item": "rs:nasturtiums", "text": "Nasturtiums\\nProtects watermelons from disease", "level": 24 }, { "item": "rs:woad", "text": "Woad", "level": 25 }, { "item": "rs:limpwurt_root", "text": "Limpwurt plants", "level": 26 }, { "item": "rs:white_lily", "text": "White lily\\nProtects all allotment crops", "level": 58 } ] }, { "name": "Herbs", "lines": [ { "item": "rs:guam_leaf", "text": "Guam leaf", "level": 9 }, { "item": "rs:marrentill", "text": "Marrentill", "level": 14 }, { "item": "tarromin", "text": "Tarromin", "level": 19 }, { "item": "rs:harralander", "text": "Harralander", "level": 26 }, { "item": "rs:goutweed", "text": "Goutweed\\nMust have completed Eadgar's Ruse", "level": 29 }, { "item": "rs:ranarr_weed", "text": "Ranarr weed", "level": 32 }, { "item": "rs:toadflax", "text": "Toadflax", "level": 38 }, { "item": "rs:avantoe", "text": "Avantoe", "level": 50 }, { "item": "rs:kwuarm", "text": "Kwuarm", "level": 56 }, { "item": "rs:snapdragon", "text": "Snapdragon", "level": 62 }, { "item": "rs:cadantine", "text": "Cadantine", "level": 67 }, { "item": "rs:lantadyme", "text": "Lantadyme", "level": 73 }, { "item": "rs:dwarf_weed", "text": "Dwarf weed", "level": 79 }, { "item": "rs:torstol", "text": "Torstol", "level": 85 } ] }, { "name": "special", "lines": [ { "item": "rs:teak_trees", "text": "Teak trees\\nPayment: Limpwurt root x15", "level": 35 }, { "item": "rs:grapes", "text": "Grapes", "level": 36 }, { "item": "rs:bittercap_mushrooms", "text": "Bittercap mushrooms", "level": 53 }, { "item": "rs:mahogany_trees", "text": "Mahogany trees\\nPayment: yanillian hops x25", "level": 55 }, { "item": "rs:cacti", "text": "Cacti\\nPayment: Cadava berries x6", "level": 55 }, { "item": "rs:belladonna", "text": "Belladonna", "level": 63 }, { "item": "rs:potato_cacti", "text": "Potato cacti", "level": 64 }, { "item": "rs:hespori", "text": "Hespori", "level": 65 }, { "item": "rs:calquat_trees", "text": "Calquat trees\\nPayment: Poison ivy Berries x8", "level": 72 } ] }, { "name": "Multiple roots", "lines": [ { "item": "rs:oak_roots", "text": "2x oak roots", "level": 23 }, { "item": "rs:oak_roots", "text": "3x oak roots", "level": 31 }, { "item": "rs:willow_roots", "text": "2x willow roots", "level": 38 }, { "item": "rs:oak_roots", "text": "4x oak roots", "level": 39 }, { "item": "rs:willow_roots", "text": "3x willow roots", "level": 46 }, { "item": "rs:mapple_roots", "text": "2x mapple roots", "level": 53 }, { "item": "rs:willow_roots", "text": "4x willow roots", "level": 54 }, { "item": "rs:maple_roots", "text": "3x mapple roots", "level": 61 }, { "item": "rs:yew_roots", "text": "2x yew roots", "level": 68 }, { "item": "rs:maple_roots", "text": "4x mapple roots", "level": 69 }, { "item": "rs:yew_roots", "text": "3x yew roots", "level": 76 }, { "item": "rs:magic_roots", "text": "2x magic roots", "level": 83 }, { "item": "rs:yew_roots", "text": "4x yew roots", "level": 84 }, { "item": "rs:magic_roots", "text": "3x magic roots", "level": 91 }, { "item": "rs:magic_roots", "text": "4x magic roots", "level": 99 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/firemaking.json ================================================ { "id": 132, "name": "Firemaking", "members": false, "sub_guides": [ { "name": "Burning", "lines": [ { "item": "rs:logs", "text": "Normal logs", "level": 1 }, { "item": "rs:achey_logs", "text": "Members: Achey logs", "level": 1 }, { "item": "rs:pyre_logs", "text": "Members: Pyre logs", "level": 5 }, { "item": "rs:oak_logs", "text": "Oak logs", "level": 15 }, { "item": "rs:oak_pyre_logs", "text": "Members: Oak pyre logs", "level": 20 }, { "item": "rs:willow_logs", "text": "Willow logs", "level": 30 }, { "item": "rs:teak_logs", "text": "Members: Teak logs", "level": 35 }, { "item": "rs:willow_pyre_logs", "text": "Members: Willow pyre logs", "level": 35 }, { "item": "rs:teak_pyre_logs", "text": "Members: Teak pyre logs", "level": 40 }, { "item": "rs:maple_logs", "text": "Maple logs", "level": 45 }, { "item": "rs:mahogany_logs", "text": "Members: Mahogany logs", "level": 50 }, { "item": "rs:maple_pyre_logs", "text": "Members: Maple pyre logs", "level": 50 }, { "item": "rs:mahogany_pyre_logs", "text": "Members: Mahogany pyre logs", "level": 55 }, { "item": "rs:yew_logs", "text": "Yew logs", "level": 60 }, { "item": "rs:yew_pyre_logs", "text": "Members: Yew pyre logs", "level": 65 }, { "item": "rs:magic_logs", "text": "Members: Magic logs", "level": 75 }, { "item": "rs:magic_pyre_logs", "text": "Members: Magic pyre logs", "level": 80 } ] }, { "name": "Barbarian", "lines": [ { "item": "rs:normal_pyre_ships", "text": "Normal Pyre Ships (with 11 Crafting)", "level": 11 }, { "item": "rs:normal_logs", "text": "Normal Logs", "level": 21 }, { "item": "rs:achey_logs", "text": "Achey Logs", "level": 21 }, { "item": "rs:oak_pyre_ships", "text": "Oak Pyre Ships (with 25 Crafting)", "level": 25 }, { "item": "rs:oak_logs", "text": "Oak Logs", "level": 35 }, { "item": "rs:willow_pyre_ships", "text": "Willow Pyre Ships (with 40 Crafting)", "level": 40 }, { "item": "rs:teak_pyre_ships", "text": "Teak Pyre Ships (with 45 Crafting)", "level": 45 }, { "item": "rs:willow_logs", "text": "Willow Logs", "level": 50 }, { "item": "rs:teak_logs", "text": "Teak Logs", "level": 55 }, { "item": "rs:maple_pyre_ships", "text": "Maple Pyre Ships (with 55 Crafting)", "level": 55 }, { "item": "rs:mahogany_pyre_ships", "text": "Mahogany Pyre Ships (with 60 Crafting)", "level": 60 }, { "item": "rs:maple_logs", "text": "Maple Logs", "level": 65 }, { "item": "rs:yew_pyre_ships", "text": "Yew Pyre Ships (with 70 Crafting)", "level": 70 }, { "item": "rs:mahogany_logs", "text": "Mahogany Logs", "level": 70 }, { "item": "rs:yew_logs", "text": "Yew Logs", "level": 80 }, { "item": "rs:magic_pyre_ships", "text": "Magic Pyre Ships", "level": 85 }, { "item": "rs:magic_logs", "text": "Magic Logs", "level": 95 } ] }, { "name": "Equipment", "lines": [ { "item": "rs:candle", "text": "Candle", "level": 1 }, { "item": "rs:candle_lanterns", "text": "Candle lanterns", "level": 4 }, { "item": "rs:oil_lamps", "text": "Oil lamps", "level": 12 }, { "item": "rs:iron_spits", "text": "Members: Iron spits", "level": 20 }, { "item": "rs:oil_lanterns", "text": "Oil lanterns", "level": 26 }, { "item": "rs:harpie_bug_lanterns", "text": "Harpie bug lanterns (not a light source)", "level": 33 }, { "item": "rs:bullseye_lanterns", "text": "Bullseye lanterns", "level": 49 }, { "item": "rs:sapphire_lanterns", "text": "Sapphire lanterns", "level": 49 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/fishing.json ================================================ { "id": 126, "name": "Fishing", "members": false, "sub_guides": [ { "name": "Catching", "lines": [ { "item": "rs:raw_shrimp", "text": "Raw Shrimp", "level": 1 }, { "item": "rs:raw_sardine", "text": "Raw Sardine", "level": 5 }, { "item": "rs:raw_karambwanji", "text": "Members: Raw Karambwanji", "level": 5 }, { "item": "rs:raw_herring", "text": "Raw Herring", "level": 10 }, { "item": "rs:raw_anchovies", "text": "Raw Anchovies", "level": 15 }, { "item": "rs:raw_mackerel", "text": "Members: Raw Mackerel", "level": 16 }, { "item": "rs:raw_trout", "text": "Raw Trout", "level": 20 }, { "item": "rs:raw_cod", "text": "Members: Raw Cod", "level": 23 }, { "item": "rs:raw_pike", "text": "Raw Pike", "level": 25 }, { "item": "rs:raw_slimy_eel", "text": "Members: Raw Slimy Eel", "level": 28 }, { "item": "rs:raw_salmon", "text": "Raw Salmon", "level": 30 }, { "item": "rs:raw_tuna", "text": "Raw Tuna", "level": 35 }, { "item": "rs:raw_cave_eel", "text": "Members: Raw Cave Eel", "level": 38 }, { "item": "rs:raw_lobster", "text": "Raw Lobster", "level": 40 }, { "item": "rs:raw_bass", "text": "Members: Raw Bass", "level": 46 }, { "item": "rs:raw_swordfish", "text": "Raw Swordfish", "level": 50 }, { "item": "rs:raw_lava_eel", "text": "Members: Raw Lava Eel", "level": 53 }, { "item": "rs:raw_monkfish", "text": "Members: Raw Monkfish", "level": 62 }, { "item": "rs:raw_karambwan", "text": "Members: Raw Karambwan", "level": 65 }, { "item": "rs:raw_shark", "text": "Raw Shark", "level": 76 }, { "item": "rs:raw_sea_turtle", "text": "Raw Sea Turtle", "level": 79 }, { "item": "rs:raw_manta_ray", "text": "Raw Manta Ray", "level": 81 } ] }, { "name": "Equipment", "lines": [ { "item": "rs:small_fishing_net", "text": "Small Fishing Net", "level": 1 }, { "item": "rs:fishing_rod", "text": "Fishing Rod", "level": 5 }, { "item": "rs:big_fishing_net", "text": "Big Fishing Net", "level": 16 }, { "item": "rs:fly_fishing_rod", "text": "Members: Fly Fishing Rod", "level": 20 }, { "item": "rs:harpoon", "text": "harpoon", "level": 35 }, { "item": "rs:lobster_pot", "text": "Lobster_Pot", "level": 40 }, { "item": "rs:oily_fishing_rod", "text": "Oily Fishing Rod", "level": 53 }, { "item": "rs:karambwan_vessel", "text": "Karambwan Vessel", "level": 65 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/fletching.json ================================================ { "id": 138, "name": "Fletching", "members": true, "sub_guides": [ { "name": "Arrows", "lines": [ { "item": "rs:arrow_shaft", "text": "15 Arrow shafts (logs)", "level": 1 }, { "item": "rs:bronze_arrow", "text": "Bronze arrows", "level": 1 }, { "item": "rs:arrow_shaft", "text": "30 Arrow shafts (Oak logs)", "level": 15 }, { "item": "rs:iron_arrow", "text": "Iron arrows", "level": 15 }, { "item": "rs:arrow_shaft", "text": "45 Arrow shafts (Willow logs)", "level": 30 }, { "item": "rs:steel_arrow", "text": "Steel arrows", "level": 30 }, { "item": "rs:arrow_shaft", "text": "60 Arrow shafts (Maple logs)", "level": 45 }, { "item": "rs:mithril_arrow", "text": "Mithril Arrows", "level": 45 }, { "item": "rs:broad_arrow", "text": "Broad arrows", "level": 52 }, { "item": "rs:arrow_shaft", "text": "75 Arrow shafts (Yew logs)", "level": 52 }, { "item": "rs:adamant_arrow", "text": "Adamant arrows", "level": 60 }, { "item": "rs:arrow_shaft", "text": "90 Arrow shafts (Magic logs)", "level": 75 }, { "item": "rs:rune_arrow", "text": "Rune arrows", "level": 75 } ] }, { "name": "Bows", "lines": [ { "item": "rs:shortbow", "text": "Shortbows", "level": 5 }, { "item": "rs:longbow", "text": "Longbows", "level": 10 }, { "item": "rs:oak_shortbow", "text": "Oak shortbows", "level": 20 }, { "item": "rs:oak_longbow", "text": "Oak longbows", "level": 25 }, { "item": "rs:willow_shortbow", "text": "Willow shortbows", "level": 35 }, { "item": "rs:willow_longbow", "text": "Willow longbows", "level": 40 }, { "item": "rs:maple_shortbow", "text": "Maple shortbows", "level": 50 }, { "item": "rs:maple_longbow", "text": "Maple longbows", "level": 55 }, { "item": "rs:yew_shortbow", "text": "Yew shortbows", "level": 65 }, { "item": "rs:yew_longbow", "text": "Yew longbows", "level": 70 }, { "item": "rs:magic_shortbow", "text": "Magic shortbows", "level": 80 }, { "item": "rs:magic_longbow", "text": "Magic longbows", "level": 85 } ] }, { "name": "Bolts", "lines": [ { "item": "rs:bronze_bolt", "text": "Bronze bolts", "level": 9 }, { "item": "rs:opal_bolt", "text": "Opal-tipped bronze bolts", "level": 11 }, { "item": "rs:blurite_bolt", "text": "Blurite bolts (After Knight´s Sword quest)", "level": 24 }, { "item": "rs:jade_bolt", "text": "Jade-tipped blurite bolts", "level": 26 }, { "item": "rs:iron_bolt", "text": "Iron bolts", "level": 39 }, { "item": "rs:silver_bolt", "text": "Silver bolts", "level": 43 }, { "item": "rs:steel_bolt", "text": "Steel bolts", "level": 46 }, { "item": "rs:mithril_bolt", "text": "Mithril bolts", "level": 54 }, { "item": "rs:sapphire_bolt", "text": "Sapphire-tipped mithril bolts", "level": 56 }, { "item": "rs:emerald_bolt", "text": "Emerald-tipped mithril bolts", "level": 56 }, { "item": "rs:adamant_bolt", "text": "Adamant bolts", "level": 61 }, { "item": "rs:runite_bolt", "text": "Runite bolts", "level": 69 }, { "item": "rs:onyx_bolt", "text": "Onyx bolts", "level": 69 } ] }, { "name": "Darts", "lines": [ { "item": "rs:bronze_dart", "text": "Bronze darts", "level": 10 }, { "item": "rs:iron_dart", "text": "Iron darts", "level": 22 }, { "item": "rs:steel_dart", "text": "Steel darts", "level": 37 }, { "item": "rs:mithril_dart", "text": "Mithril darts", "level": 52 }, { "item": "rs:adamant_dart", "text": "Adamant darts", "level": 67 }, { "item": "rs:rune_dart", "text": "Rune darts", "level": 81 } ] }, { "name": "Javelins", "lines": [ { "item": "rs:bronze_javelin", "text": "Bronze javelins", "level": 3 }, { "item": "rs:iron_javelin", "text": "Iron javelins", "level": 17 }, { "item": "rs:steel_javelin", "text": "Steel javelins", "level": 32 }, { "item": "rs:mithril_javelin", "text": "Mithril javelins", "level": 47 }, { "item": "rs:adamant_javelin", "text": "Adamant javelins", "level": 62 }, { "item": "rs:rune_javelin", "text": "Rune javelins", "level": 77 } ] }, { "name": "Other", "lines": [ { "item": "rs:battlestaff", "text": "Battlestaves", "level": 40 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/herblore.json ================================================ { "id": 125, "name": "Herblore", "members": true, "sub_guides": [ { "name": "Potions", "lines": [ { "item": "rs:herblore_attack_potion", "text": "Attack potion\\nGuam leaf & eye of newt", "level": 3 }, { "item": "rs:herblore_anti_poison_potion", "text": "Anti-poison potion\\nMarrentil & ground unicorn horn", "level": 5 }, { "item": "rs:herblore_relicyms_balm", "text": "Relicym's balm", "level": 8 }, { "item": "rs:herblore_strength_potion", "text": "Strength potion\\nTarromin & limpwurt root", "level": 12 }, { "item": "rs:herblore_serum_207", "text": "Serum 207\\nTarromin & ashes", "level": 15 }, { "item": "rs:herblore_stat_restore_potion", "text": "Stat restore potion\\nHarralander & red spiders' eggs", "level": 22 }, { "item": "rs:guthix_balance_potion", "text": "Guthix balance potion", "level": 22 }, { "item": "rs:herblore_energy_potion", "text": "Energy potion\\nHarralander & chocolate dust", "level": 26 }, { "item": "rs:herblore_defence_potion", "text": "Defence potion\\nRanarr weed & white berries", "level": 30 }, { "item": "rs:herblore_combat_potion", "text": "Combat potion\\nHarralander & ground desert goat horn", "level": 36 }, { "item": "rs:herblore_prayer_restore_potion", "text": "Prayer restore potion\\nRanarr weed & snape grass", "level": 38 }, { "item": "rs:herblore_super_attack_potion", "text": "Super attack potion\\nIrit leaf & eye of newt", "level": 45 }, { "item": "rs:herblore_super_anti_poison_potion", "text": "Super anti-poison potion\\nIrit leaf & ground unicorn horn", "level": 48 }, { "item": "rs:herblore_fishing_potion", "text": "Fishing potion\\nAvantoe & snape grass", "level": 50 }, { "item": "rs:herblore_super_energy_potion", "text": "Super energy potion\\nAvantoe & Mort Myre fungi", "level": 52 }, { "item": "rs:herblore_hunting_potion", "text": "Hunting potion - Avantoe & ground\\nsabre-toothed kebbit teeth", "level": 53 }, { "item": "rs:herblore_super_strength_potion", "text": "Super strength potion\\nKwuarm & limpwurt root", "level": 55 }, { "item": "rs:herblore_magic_essence_potion", "text": "Magic essence potion\\nStar flower & ground gorak's claw", "level": 57 }, { "item": "rs:herblore_weapon_poison", "text": "Weapon Poison\\nKwuarm & ground blue dragon scale", "level": 60 }, { "item": "rs:herblore_super_defence_potion", "text": "Super defence potion\\nCadantine & white berries", "level": 66 }, { "item": "rs:herblore_antidote_plus", "text": "Antidote+\\nCoconut milk, toadflax & yew roots", "level": 68 }, { "item": "rs:herblore_anti_firebreath_potion", "text": "Anti-firebreath potion\\nLantadyme & ground blue dragon scale", "level": 69 }, { "item": "rs:herblore_ranging_potion", "text": "Ranging potion\\nDwarf weed & wine of Zamorak", "level": 72 }, { "item": "rs:herblore_weapon_poison_plus", "text": "Weapon poison(+)\\nCoconut milk, cactus spine & red spiders' eggs", "level": 73 }, { "item": "rs:herblore_magic_potion", "text": "Magic potion\\nLantadyme & potato cactus", "level": 76 }, { "item": "rs:herblore_zamorak_brew", "text": "Zamorak brew\\nTorstol & jangerberries", "level": 78 }, { "item": "rs:herblore_antidote_plus_plus", "text": "Antidote++\\nCoconut milk, irit leaf & magic tree roots", "level": 79 }, { "item": "rs:herblore_saradomin_brew", "text": "Saradomin brew\\nToadflax & crushed birdnest", "level": 81 }, { "item": "rs:herblore_weapon_poison_plus_plus", "text": "Weapon poison(++)\\nCoconut milk, nightshade & poison ivy berries", "level": 82 } ] }, { "name": "Herbs", "lines": [ { "item": "rs:guam_leaf", "text": "Guam leaf", "level": 3 }, { "item": "rs:rogues_purse", "text": "Rogue's purse", "level": 3 }, { "item": "rs:snake_weed", "text": "Snake weed", "level": 3 }, { "item": "rs:marrentill", "text": "Marrentill", "level": 5 }, { "item": "rs:tarromin", "text": "Tarromin", "level": 11 }, { "item": "rs:harralander", "text": "Harralander", "level": 20 }, { "item": "rs:ranarr_weed", "text": "Ranarr weed", "level": 25 }, { "item": "rs:toadflax", "text": "Toadflax", "level": 30 }, { "item": "rs:irit_leaf", "text": "Irit leaf", "level": 40 }, { "item": "rs:avantoe", "text": "Avantoe", "level": 48 }, { "item": "rs:kwuarm", "text": "Kwuarm", "level": 54 }, { "item": "rs:snapdragon", "text": "Snapdragon", "level": 59 }, { "item": "rs:cadantine", "text": "Cadantine", "level": 65 }, { "item": "rs:lantadyme", "text": "Lantadyme", "level": 67 }, { "item": "rs:dwarf_weed", "text": "Dwarf weed", "level": 70 }, { "item": "rs:torstol", "text": "Torstol", "level": 75 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/hitpoint.json ================================================ { "id": 119, "name": "Hitpoints", "members": false, "sub_guides": [ { "name": "Hitpoints", "lines": [ { "text": "Hitpoints are used to tell you how healthy\\n your character is. A character who reaches\\n0 Hitpoints has died, but will reappear in\\ntheir chosen respawn location(normally Lumbridge).\\n\\nIf you see any red 'hit splash' during combat\\n, the number shown corresponds to the number\\n of Hitpoints lost as a result of that strike.\\n\\nBlue hit splash means no damage has been dealt.\\n\\nGreen hit splash are poison damage.", "level": "" } ] }, { "name": "Healing", "lines": [ { "item": "rs:purple_sweets", "text": "Purple Sweets: Restores 1-3 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:anchovies", "text": "Anchovies: Restores 1 Hitpoint", "level": "" }, { "item": "rs:shrimp", "text": "Shrimp: Restores 3 Hitpoints", "level": "" }, { "item": "rs:cooked_chicken", "text": "Cooked chicken: Restores 3 Hitpoints", "level": "" }, { "item": "rs:sardine", "text": "Sardine: Restores 3 Hitpoints", "level": "" }, { "item": "rs:cooked_meat", "text": "Cooked meat: Restores 3 Hitpoints", "level": "" }, { "item": "rs:bread", "text": "Bread: Restores 5 Hitpoints", "level": "" }, { "item": "rs:bread", "text": "Bread: Restores 5 Hitpoints", "level": "" }, { "item": "rs:herring", "text": "Herring: Restores 5 Hitpoints", "level": "" }, { "item": "rs:cooked_rabbit", "text": "Cooked Rabbit: Restores 5 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:mackerel", "text": "Mackerel: Restores 6 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:botanical_pie", "text": "Botanical Pie: Restores 6 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:slimy_eel", "text": "Slimy Eel: Restores 6-10 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:trout", "text": "Trout: Restores 7 Hitpoints", "level": "" }, { "item": "rs:cod", "text": "Cod: Restores 7 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:roast_rabbit", "text": "Roast Rabbit: Restores 7 Hitpoints", "level": "" }, { "item": "rs:cave_eel", "text": "Cave Eel: Restores 7-11 Hitpoints", "level": "" }, { "item": "rs:pike", "text": "Pike: Restores 8 Hitpoints", "level": "" }, { "item": "rs:salmon", "text": "Salmon: Restores 9 Hitpoints", "level": "" }, { "item": "rs:redberry_pie", "text": "Redberry Pie: Restores 9 Hitpoints", "level": "" }, { "item": "rs:tuna", "text": "Tuna: Restores 10 Hitpoints", "level": "" }, { "item": "rs:crab_meat", "text": "Crab meat: Restores 10 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:fishcake", "text": "Cooked Fishcake: Restores 11 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:jug_of_wine", "text": "Jug of wine: Restores 11 Hitpoints", "level": "" }, { "item": "rs:meat_pie", "text": "Meat pie: Restores 11 Hitpoints", "level": "" }, { "item": "rs:lava_eel", "text": "Lava Eel: Restores 11 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:garden_pie", "text": "Garden pie: Restores 12 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:fish_pie", "text": "Fish pie: Restores 12 Hitpoints", "level": "" }, { "item": "rs:cake", "text": "Cake: Restores 12 Hitpoints", "level": "" }, { "item": "rs:lobster", "text": "Lobster: Restores 12 Hitpoints", "level": "" }, { "item": "rs:bass", "text": "Bass: Restores 13 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:swordfish", "text": "Swordfish: Restores 14 Hitpoints", "level": "" }, { "item": "rs:plain_pizza", "text": "Plain pizza: Restores 14 Hitpoints", "level": "" }, { "item": "rs:apple_pie", "text": "Apple pie: Restores 14 Hitpoints", "level": "" }, { "item": "rs:potato_with_butter", "text": "Potato with butter: Restores 14 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:chilli_potato", "text": "Chilli Potato: Restores 14 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:chocolate_cake", "text": "Chocolate Cake: Restores 15 Hitpoints", "level": "" }, { "item": "rs:monkfish", "text": "Monkfish: Restores 16 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:admiral_pie", "text": "Admiral pie: Restores 16 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:meat_pizza", "text": "Meat pizza: Restores 16 Hitpoints", "level": "" }, { "item": "rs:potato_with_cheese", "text": "Potato with cheese: Restores 16 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:egg_potato", "text": "Egg Potato: Restores 16 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:cooked_karambwan", "text": "Cooked karambwan: Restores 18 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:anchovy_pizza", "text": "Anchovy pizza: Restores 18 Hitpoints", "level": "" }, { "item": "rs:ugthanki_kebab", "text": "Ugthanki kebab: Restores 19 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:jug_of_wine", "text": "Jug of wine: Restores 11 Hitpoints", "level": "" }, { "item": "rs:shark", "text": "Shark: Restores 20 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:mushroom_potato", "text": "Mushroom Potato: Restores 20 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:sea_turtle", "text": "Sea Turtle: Restores 21 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:manta_ray", "text": "Manta Ray: Restores 22 Hitpoints", "level": "" }, { "item": "rs:tuna_potato", "text": "Tuna Potato: Restores 22 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:wild_pie", "text": "Wild pie: Restores 22 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:summer_pie", "text": "Summer pie: Restores 22 Hitpoints\\n(Members only)", "level": "" }, { "item": "rs:pineapple_pizza", "text": "Pineapple pizza: Restores 22 Hitpoints", "level": "" }, { "item": "rs:saradomin_brew", "text": "Saradomin brew: Restores 15% of your\\nHitpoints level plus 2 - can boost beyond\\nyour level (members only)", "level": "" } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/magic.json ================================================ { "id": 137, "name": "Magic", "members": false, "sub_guides": [ { "name": "Normal Spells", "lines": [ { "item": "rs:battle_staff", "text": "Click a spell icon below to see the\\nrequired runes to cast each spell.\\n\\nBy opening the spellbook icon on the side\\n interface, you can see what each spell does\\nby moving your mouse over its icon.", "level": "" }, { "item": "rs:spell_home_teleport", "text": "Click a spell icon below to see the\\nrequired runes to cast each spell.\\n\\nBy opening the spellbook icon on the side\\n interface, you can see what each spell does\\nby moving your mouse over its icon.", "level": "" }, { "item": "rs:spell_wind_strike", "text": "Wind Strike", "level": 1 }, { "item": "rs:spell_confuse", "text": "Confuse", "level": 3 }, { "item": "rs:spell_water_strike", "text": "Water Strike", "level": 5 }, { "item": "rs:spell_level_1_enchant", "text": "Level 1 enchant", "level": 7 }, { "item": "rs:spell_earth_strike", "text": "Earth Strike", "level": 9 }, { "item": "rs:spell_weaken", "text": "Weaken", "level": 11 }, { "item": "rs:spell_fire_strike", "text": "Fire Strike", "level": 13 }, { "item": "rs:spell_bones_to_bananas", "text": "Bones to bananas", "level": 15 }, { "item": "rs:spell_wind_bolt", "text": "Wind Bolt", "level": 17 }, { "item": "rs:spell_curse", "text": "Curse", "level": 19 }, { "item": "rs:spell_bind", "text": "Bind", "level": 20 }, { "item": "rs:spell_low_level_alchemy", "text": "Low level alchemy", "level": 21 }, { "item": "rs:spell_water_bolt", "text": "Water Bolt", "level": 23 }, { "item": "rs:spell_varrock_teleport", "text": "Varrock teleport", "level": 25 }, { "item": "rs:spell_level_2_enchant", "text": "Level 2 enchant", "level": 27 }, { "item": "rs:spell_earth_bolt", "text": "Earth Bolt", "level": 29 }, { "item": "rs:spell_lumbridge_teleport", "text": "Lumbridge teleport", "level": 31 }, { "item": "rs:spell_telekinetic_grab", "text": "Telekinetic grab", "level": 33 }, { "item": "rs:spell_fire_bolt", "text": "Fire Bolt", "level": 35 }, { "item": "rs:spell_falador_teleport", "text": "Falador teleport", "level": 37 }, { "item": "rs:spell_crumble_undead", "text": "Crumble undead", "level": 39 }, { "item": "rs:spell_teleport_to_house", "text": "Teleport to house", "level": 40 }, { "item": "rs:spell_wind_blast", "text": "Wind Blast", "level": 41 }, { "item": "rs:spell_superheat_item", "text": "Superheat item", "level": 43 }, { "item": "rs:spell_camelot_teleport", "text": "Members: Camelot teleport", "level": 45 }, { "item": "rs:spell_water_blast", "text": "Water Blast", "level": 47 }, { "item": "rs:spell_level_3_enchant", "text": "Level 3 enchant", "level": 49 }, { "item": "rs:spell_iban_blast", "text": "Members: Iban blast\\n(after Underground Pass)", "level": 50 }, { "item": "rs:spell_snare", "text": "Snare", "level": 50 }, { "item": "rs:spell_magic_dart", "text": "Members: Magic dart\\n(with 55 Slayer)", "level": 50 }, { "item": "rs:spell_ardougne_teleport", "text": "Members: Ardougne teleport\\n(after Plague City)", "level": 51 }, { "item": "rs:spell_earth_blast", "text": "Earth Blast", "level": 53 }, { "item": "rs:spell_high_level_alchemy", "text": "High level alchemy", "level": 55 }, { "item": "rs:spell_charge_water_orb", "text": "Members: Charge water orb", "level": 56 }, { "item": "rs:spell_level_4_enchant", "text": "Level 4 enchant", "level": 57 }, { "item": "rs:spell_watchtower_teleport", "text": "Members: Watchtower teleport\\n(after Watchtower)", "level": 58 }, { "item": "rs:spell_fire_blast", "text": "Fire Blast", "level": 59 }, { "item": "rs:spell_charge_earth_orb", "text": "Members: Charge earth orb", "level": 60 }, { "item": "rs:spell_bones_to_peaches", "text": "Members: Bones to peaches", "level": 60 }, { "item": "rs:spell_claws_of_guthix", "text": "Members: Claws of Guthix\\n(after Mage Arena)", "level": 60 }, { "item": "rs:spell_flames_of_zamorak", "text": "Members: Flames of Zamorak\\n(after Mage Arena", "level": 60 }, { "item": "rs:spell_saradomin_strike", "text": "Members: Saradomin Strike\\n(after Mage Arena)", "level": 60 }, { "item": "rs:spell_trollheim_teleport", "text": "Members: Trollheim teleport\\n(after Eadgar's Ruse)", "level": 61 }, { "item": "rs:spell_wind_wave", "text": "Members: Wind Wave", "level": 62 }, { "item": "rs:spell_charge_fire_orb", "text": "Members: Charge fire orb", "level": 63 }, { "item": "rs:spell_ape_atoll_teleport", "text": "Members: Ape atoll teleport\\n(after Recipe for Disaster)", "level": 64 }, { "item": "rs:spell_water_wave", "text": "Members: Water wave", "level": 65 }, { "item": "rs:spell_charge_air_orb", "text": "Members: Charge air orb", "level": 66 }, { "item": "rs:spell_vulnerability", "text": "Members: Vulnerability", "level": 66 }, { "item": "rs:spell_level_5_enchant", "text": "Members: Level 5 enchant", "level": 68 }, { "item": "rs:spell_earth_wave", "text": "Members: Earth Wave", "level": 70 }, { "item": "rs:spell_enfeeble", "text": "Members: Enfeeble", "level": 73 }, { "item": "rs:spell_teleother_lumbridge", "text": "Members: Teleother Lumbridge", "level": 74 }, { "item": "rs:spell_fire_wave", "text": "Members: Fire Wave", "level": 75 }, { "item": "rs:spell_entangle", "text": "Members: Entangle", "level": 79 }, { "item": "rs:spell_stun", "text": "Members: Stun", "level": 80 }, { "item": "rs:spell_charge", "text": "Members: Charge\\n(after Mage Arena)", "level": 80 }, { "item": "rs:spell_teleother_falador", "text": "Members: Teleother to Falador", "level": 82 }, { "item": "rs:spell_tele_block", "text": "Tele block", "level": 85 }, { "item": "rs:spell_level_6_enchant", "text": "Level 6 enchant", "level": 87 }, { "item": "rs:spell_teleother_camelot", "text": "Members: Teleother to Camelot", "level": 90 } ] }, { "name": "Ancient Magicks", "members": true, "lines": [ { "item": "rs:ancient_magick_guide", "text": "Ancient magicks are available after\\ncompleting the Desert Treasure quest.", "level": "" }, { "item": "rs:spell_home_teleport_ancient", "text": "Home teleport (Edgeville)", "level": "" }, { "item": "rs:spell_smoke_rush", "text": "Smoke rush", "level": 50 }, { "item": "rs:spell_shadow_rush", "text": "Shadow rush", "level": 52 }, { "item": "rs:spell_paddewwa_teleport", "text": "Paddewwa teleport", "level": 54 }, { "item": "rs:spell_blood_rush", "text": "Blood rush", "level": 56 }, { "item": "rs:spell_ice_rush", "text": "Mithril", "level": 58 }, { "item": "rs:spell_senntisten_teleport", "text": "Senntisten teleport", "level": 60 }, { "item": "rs:spell_smoke_burst", "text": "Smoke burst", "level": 62 }, { "item": "rs:spell_shadow_burst", "text": "Shadow burst", "level": 64 }, { "item": "rs:Kharyll teleport", "text": "Kharyll teleport", "level": 66 }, { "item": "rs:spell_blood_burst", "text": "Blood burst", "level": 68 }, { "item": "rs:spell_ice_burst", "text": "Ice burst", "level": 70 }, { "item": "rs:spell_lassar_teleport", "text": "Lassar teleport", "level": 72 }, { "item": "rs:spell_smoke_blitz", "text": "Smoke Blitz", "level": 74 }, { "item": "rs:spell_shadow_blitz", "text": "Shadow blitz", "level": 76 }, { "item": "rs:spell_dareeyak_teleport", "text": "Dareeyak teleport", "level": 78 }, { "item": "rs:spell_blood_blitz", "text": "Blood Blitz", "level": 80 }, { "item": "rs:spell_ice_blitz", "text": "Ice blitz", "level": 82 }, { "item": "rs:guthans_warspear", "text": "Members: Guthan's warspear", "level": 84 }, { "item": "rs:spell_carrallangar_teleport", "text": "Carrallangar teleport", "level": 85 }, { "item": "rs:spell_smoke_barrage", "text": "Smoke barrage", "level": 86 }, { "item": "rs:spell_shadow_barrage", "text": "Shadow barrage", "level": 88 }, { "item": "rs:spell_annakarl teleport", "text": "Annakarl teleport", "level": 90 }, { "item": "rs:spell_blood_barrage", "text": "Blood barrage", "level": 92 }, { "item": "rs:spell_ice_barrage", "text": "Ice barrage", "level": 94 }, { "item": "rs:spell_ghorrock_teleport", "text": "Ghorrock teleport", "level": 96 } ] }, { "name": "Lunar Spells", "members": true, "lines": [ { "item": "rs:lunar_spells_guide", "text": "Lunar spells are available after completing\\nthe Lunar Diplomacy quest.", "level": "" }, { "item": "rs:spell_home_teleport_lunar", "text": "Home teleport (Lunar Isle)", "level": "" }, { "item": "rs:spell_bake_pie", "text": "Bake pie", "level": 65 }, { "item": "rs:spell_geomancy", "text": "Geomancy", "level": 65 }, { "item": "rs:spell_cure_plant", "text": "Cure plant", "level": 66 }, { "item": "rs:spell_npc_contact", "text": "NPC Contact", "level": 67 }, { "item": "rs:spell_cure_other", "text": "Cure other", "level": 68 }, { "item": "rs:spell_moonclan_teleport", "text": "Moonclan teleport", "level": 69 }, { "item": "rs:spell_telegroup_moonclan", "text": "Tele group Moonclan", "level": 70 }, { "item": "rs:spell_cure_me", "text": "Cure me", "level": 71 }, { "item": "rs:spell_ourania_teleport", "text": "Ourania teleport", "level": 71 }, { "item": "rs:spell_waterbirth_teleport", "text": "Waterbirth teleport", "level": 72 }, { "item": "rs:spell_telegroup_waterbirth", "text": "Tele group Waterbirth", "level": 73 }, { "item": "rs:spell_barbarian_teleport", "text": "Barbarian teleport", "level": 72 }, { "item": "rs:spell_telegroup_barbarian", "text": "Tele group Barbarian", "level": 74 }, { "item": "rs:spell_spin_flax", "text": "Spin Flax", "level": 76 }, { "item": "rs:spell_superglass_make", "text": "Superglass make", "level": 77 }, { "item": "rs:spell_khazard_teleport", "text": "Khazard teleport", "level": 78 }, { "item": "rs:spell_telegroup_khazard", "text": "Tele group Khazard", "level": 79 }, { "item": "rs:spell_string_jewellery", "text": "String jewellery", "level": 80 }, { "item": "rs:spell_stat_restore_pot_share", "text": "Stat restore pot share", "level": 81 }, { "item": "rs:spell_magic_imbue", "text": "Magic imbue", "level": 82 }, { "item": "rs:spell_fertile_soil", "text": "Fertile soil", "level": 83 }, { "item": "rs:spell_boost_potion_share", "text": "Boost potion share", "level": 84 }, { "item": "rs:spell_fishing_guild_teleport", "text": "Fishing guild teleport", "level": 85 }, { "item": "rs:spell_telegroup_fishing_guild", "text": "Tele group fishing guild", "level": 86 }, { "item": "rs:spell_catherby_teleport", "text": "Catherby teleport", "level": 87 }, { "item": "rs:spell_telegroup_catherby", "text": "Tele group Catherby", "level": 88 }, { "item": "rs:spell_recharge_dragonstone", "text": "Recharge Dragonstone", "level": 89 }, { "item": "rs:spell_ice_plateau_teleport", "text": "Ice plateau teleport", "level": 89 }, { "item": "rs:spell_telegroup_ice_plateau", "text": "Tele group Ice plateau", "level": 90 }, { "item": "rs:spell_energy_transfer", "text": "Energy transfer", "level": 91 }, { "item": "rs:spell_heal_other", "text": "Heal other", "level": 92 }, { "item": "rs:spell_vengeance_other", "text": "Vengeance other", "level": 93 }, { "item": "rs:spell_vengeancer", "text": "Vengeance", "level": 94 }, { "item": "rs:spell_heal_group", "text": "Heal group", "level": 95 } ] }, { "name": "Armour", "members": true, "lines": [ { "item": "rs:wizard_boots", "text": "Wizard boots", "level": 20 }, { "item": "rsmystic_robes", "text": "Mystic robes\\n(20 Defence required)", "level": 40 }, { "item": "rs:enchanted_robes", "text": "Enchanted robes\\n(20 Defence required)", "level": 40 }, { "item": "rs:splitbark_armour", "text": "Splitbark armour\\n(40 Defence required)", "level": 40 }, { "item": "rs:skeletal_armour", "text": "Skeletal_armour\\n(after The Fremmenik Trials, 40 Defence required", "level": 40 }, { "item": "rs:infinity_robes", "text": "Infinity robes\\n(25 Defence required)", "level": 50 }, { "item": "rs:god_capes", "text": "God capes\\n(after Mage Arena)", "level": 60 }, { "item": "rs:lunar_armour", "text": "Lunar armour\\n(after Lunar Diplomacy, 40 Defence required)", "level": 65 }, { "item": "rs:ahrims_robes", "text": "Ahrims robes\\n(with 70 Defence)", "level": 70 } ] }, { "name": "Bolts", "members": true, "lines": [ { "item": "rs:spell_enchant_opal_tipped_crossbow_bolts", "text": "Opal-tipped crossbow bolts\\n1 cosmic + 2 air runes", "level": 4 }, { "item": "rs:spell_enchant_sapphire_tipped_crossbow_bolts", "text": "Sapphire-tipped crossbow bolts\\n1 cosmic + 1 water + 1 mind rune", "level": 7 }, { "item": "rs:spell_enchant_jade_tipped_crossbow_bolts", "text": "Jade-tipped crossbow bolts\\n1 cosmic + 2 earth runes", "level": 14 }, { "item": "rs:spell_enchant_pearl_tipped_crossbow_bolts", "text": "Pearl-tipped crossbow bolts\\n1 cosmic + 2 water runes", "level": 24 }, { "item": "rs:spell_enchant_emerald_tipped_crossbow_bolts", "text": "Emerald-tipped crossbow bolts\\n1 cosmic + 3 air runes + 1 nature rune", "level": 27 }, { "item": "rs:spell_enchant_red_topaz_tipped_crossbow_bolts", "text": "Red topaz-tipped crossbow bolts\\n1 cosmic + 2 blood runes", "level": 29 }, { "item": "rs:spell_enchant_ruby_tipped_crossbow_bolts", "text": "Opal-tipped crossbow bolts\\n1 cosmic + 5 fire + 1 blood runes", "level": 49 }, { "item": "rs:spell_enchant_diamond_tipped_crossbow_bolts", "text": "Diamond-tipped crossbow bolts\\n1 cosmic + 10 earth + 2 law runes", "level": 57 }, { "item": "rs:spell_enchant_dragonstone_tipped_crossbow_bolts", "text": "Dragonstone-tipped crossbow bolts\\n1 cosmic + 15 earth + 1 soul runes", "level": 68 }, { "item": "rs:spell_enchant_onyx_tipped_crossbow_bolts", "text": "Onyx-tipped crossbow bolts\\n1 cosmic + 20 fire + 1 death runes", "level": 87 } ] }, { "name": "Weapons", "members": true, "lines": [ { "item": "rs:battlestaves", "text": "Battlestaves\\n(30 Attack required)", "level": 20 }, { "item": "rs:mysticstaves", "text": "Mystic staves\\n(40 Attack required)", "level": 40 }, { "item": "rs:beginner_wand", "text": "Beginner wand", "level": 45 }, { "item": "rs:apprentice_wand", "text": "Apprentice wand", "level": 50 }, { "item": "rs:ancient_staff", "text": "Ancient staff\\n(after Desert Treasure, 50 Attack required", "level": 50 }, { "item": "rs:slayer_staff", "text": "Slayer staff\\n(with 55 Slayer)", "level": 50 }, { "item": "rs:teacher_wand", "text": "Teacher wand", "level": 55 }, { "item": "rs:master_wand", "text": "Master wand", "level": 60 }, { "item": "rs:god_staves", "text": "God staves\\n(after Mage Arena)", "level": 60 }, { "item": "rs:toktz-mej-tal", "text": "TokTz-Mej-Tal\\n(60 Attack required)", "level": 60 }, { "item": "rs:ahrims_robes", "text": "Ahrim's staff\\n(70 Attack required)", "level": 70 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/mining.json ================================================ { "id": 120, "name": "Mining", "members": false, "sub_guides": [ { "name": "Rocks", "lines": [ { "item": "rs:rune_essence", "text": "Rune essence (after RuneMysteries quest)", "level": 1 }, { "item": "rs:clay", "text": "Clay", "level": 1 }, { "item": "rs:copper_ore", "text": "Copper ore", "level": 1 }, { "item": "rs:tin_ore", "text": "Tin ore", "level": 1 }, { "item": "rs:blurite_ore", "text": "Blurite ore", "level": 10 }, { "item": "rs:limestone", "text": "Members: Limestone", "level": 10 }, { "item": "rs:iron_ore", "text": "Iron ore", "level": 15 }, { "item": "rs:silver_ore", "text": "Silver ore", "level": 20 }, { "item": "rs:coal", "text": "Coal", "level": 30 }, { "item": "rs:gold_ore", "text": "Gold", "level": 40 }, { "item": "rs:granite", "text": "Members: Granite", "level": 45 }, { "item": "rs:mithril_ore", "text": "Mithril ore", "level": 55 }, { "item": "rs:adamantite_ore", "text": "Adamantite ore", "level": 70 }, { "item": "rs:soft_clay", "text": "Members: Soft clay", "level": 70 }, { "item": "rs:runite_ore", "text": "Runite ore", "level": 85 } ] }, { "name": "Equipment", "lines": [ { "item": "rs:bronze_pickaxe", "text": "Bronze pickaxe", "level": 1 }, { "item": "rs:iron_pickaxe", "text": "Iron pickaxe", "level": 1 }, { "item": "rs:steel_pickaxe", "text": "Steel pickaxe", "level": 6 }, { "item": "rs:mithril_pickaxe", "text": "Mithril pickaxe", "level": 21 }, { "item": "rs:adamant_pickaxe", "text": "Adamant pickaxe", "level": 31 }, { "item": "rs:rune_pickaxe", "text": "Rune pickaxe", "level": 41 } ] }, { "name": "Areas", "lines": [ { "item": "rs:mithril_ore", "text": "Mining guild", "level": 60 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/prayer.json ================================================ { "id": 130, "name": "Prayer", "members": false, "sub_guides": [ { "name": "Prayers", "lines": [ { "item": "rs:prayer_guide", "text": "While any of your prayers are active, your prayer\\n points will drain. Once you have run out of prayer\\n points, you will no longer be able to use prayers.\\n Visit an altar to recharge your prayer points.", "level": "" }, { "item": "rs:thick_skin", "text": "Thick Skin", "level": 1 }, { "item": "rs:burst_of_strength", "text": "Burst of Strength", "level": 4 }, { "item": "rs:clarity_of_thought", "text": "Clariy of Thought", "level": 7 }, { "item": "rs:sharp_eye", "text": "Sharp Eye", "level": 8 }, { "item": "rs:mystic_will", "text": "Mystic Will", "level": 9 }, { "item": "rs:rock_skin", "text": "Rock Skin", "level": 10 }, { "item": "rs:superhuman_strength", "text": "Superhuman Strength", "level": 13 }, { "item": "rs:improved_reflexes", "text": "Improved Reflexes", "level": 16 }, { "item": "rs:rapid_restore", "text": "Rapid Restore", "level": 19 }, { "item": "rs:rapid_heal", "text": "Rapid Heal", "level": 22 }, { "item": "rs:protect_item", "text": "Protect Item", "level": 25 }, { "item": "rs:hawk_eye", "text": "Hawk Eye", "level": 26 }, { "item": "rs:mystic_lore", "text": "Mystical Lore", "level": 27 }, { "item": "rs:steel_skin", "text": "Steel Skin", "level": 28 }, { "item": "rs:ultimate_strength", "text": "Ultimate Strength", "level": 31 }, { "item": "rs:incredible_reflexes", "text": "Incredible Reflexes", "level": 34 }, { "item": "rs:protect_from_magic", "text": "Protect from Magic", "level": 37 }, { "item": "rs:protect_from_missiles", "text": "Protect from Missiles", "level": 40 }, { "item": "rs:protect_from_melee", "text": "Protect from Melee", "level": 43 }, { "item": "rs:eagle_eye", "text": "Eagle Eye", "level": 44 }, { "item": "rs:mystic_might", "text": "Mystic Might", "level": 45 }, { "item": "rs:retribution", "text": "Members: Retribution", "level": 46 }, { "item": "rs:redemption", "text": "Members: Redemption", "level": 49 }, { "item": "rs:smite", "text": "Members: Smite", "level": 52 } ] }, { "name": "Equipment", "lines": [ { "item": "rs:initate_full_helmet", "text": "Initiate armour\\n(after Recruitment Drive, with\\nlevel 20 defence.", "level": "10" }, { "item": "rs:proselyte_full_helmet", "text": "Proselyte armour\\n(after Slug Menace, with\\nlevel 30 defence.", "level": "20" } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/ranged.json ================================================ { "id": 127, "name": "Ranged", "members": false, "sub_guides": [ { "name": "Bows", "lines": [ { "item": "rs:standard_bow", "text": "Standard bows\\n/Ammo: Arrows up to iron", "level": 1 }, { "item": "rs:oak_bow", "text": "Oak bows\\nAmmo: Arrows up to steel", "level": 5 }, { "item": "rs:willow_bow", "text": "Willow bows\\nAmmo: Arrows up to mithril", "level": 20 }, { "item": "rs:maple_bow", "text": "Maple bows\\nAmmo: Arrows up to adamant", "level": 30 }, { "item": "rs:ogre_composite_bow", "text": "Members: Ogre, composite bows\\nAmmo:'brutal' arrows up to rune", "level": 30 }, { "item": "rs:yew_bow", "text": "Yew bows\\nAmmo: Arrows up to rune", "level": 40 }, { "item": "rs:magic_bow", "text": "Magic bows\\nAmmo: Arrows up to rune", "level": 50 }, { "item": "rs:seercull_bow", "text": "Members: Seercull bows\\nAmmo: Arrows up to rune", "level": 50 }, { "item": "rs:crystal_bow", "text": "Members: Crystal bows (with 50 Agility)\\nAmmo: None", "level": 70 } ] }, { "name": "Thrown", "lines": [ { "item": "rs:bronze_knife", "text": "Bronze knifes\\nthrowing knifes", "level": 1 }, { "item": "rs:iron_knife", "text": "Iron knifes\\nthrowing knifes", "level": 1 }, { "item": "rs:steel_knife", "text": "Steel knifes\\nthrowing knifes", "level": 5 }, { "item": "rs:black_knife", "text": "Black knifes\\nthrowing knifes", "level": 10 }, { "item": "rs:mithril_knife", "text": "Mithril knifes\\nthrowing knifes", "level": 20 }, { "item": "rs:adamant_knife", "text": "Adamant knifes\\nthrowing knifes)", "level": 30 }, { "item": "rs:rune_knife", "text": "Rune knifes\\nthrowing knifes", "level": 40 }, { "item": "rs:toktz-xil-ul", "text": "Toktz xil ul\\nobsidian throwing rings", "level": 60 } ] }, { "name": "Crossbows", "lines": [ { "item": "rs:crossbow", "text": "Crossbow\\nAmmo: Bronze crossbow bolts", "level": 1 }, { "item": "rs:phoenix_crossbow", "text": "Phoenix crossbow\\nAmmo: Bronze crossbow bolts", "level": 5 }, { "item": "rs:bronze_crossbow", "text": "Members: Crossbow\\nAmmo: Bronze crossbow bolts", "level": 1 }, { "item": "rs:blurite_crossbow", "text": "Members: Blurite Crossbow\\nAmmo: bolts up to blurite", "level": 16 }, { "item": "rs:iron_crossbow", "text": "Members: Crossbow\\nAmmo: Bolts up to iron", "level": 26 }, { "item": "rs:dorgeshuun_crossbow", "text": "Members: Crossbow\\nAmmo: Bolts up to iron", "level": 28 }, { "item": "rs:steel_crossbow", "text": "Members: Steel crossbow\\nAmmo: Bolts up to steel", "level": 31 }, { "item": "rs:mithril_crossbow", "text": "Members: mithril crossbow\\nAmmo: Bolts up to mithril", "level": 36 }, { "item": "rs:adamant_crossbow", "text": "Members: Adamant crossbow\\nAmmo: Bolts up to adamant", "level": 46 }, { "item": "rs:rune_crossbow", "text": "Members: Rune crossbow\\nAmmo: Bolts up to rune", "level": 61 }, { "item": "rs:karils_crossbow", "text": "Members: Karil's crossbow\\nAmmo: Bolt rack only", "level": 70 } ] }, { "name": "Armour", "lines": [ { "item": "rs:plain_leather_items", "text": "Plain leather items\\nwield any bronze equipment", "level": 1 }, { "item": "rs:hard_leather_body", "text": "Hard leather body\\n(10 Defence required)", "level": 5 }, { "item": "rs:studded_leather_body", "text": "Studded leather chaps\\n(20 Defence required)", "level": 20 }, { "item": "rs:studded_leather_chaps", "text": "Studded leather chaps", "level": 20 }, { "item": "rs:coif", "text": "Coif", "level": 20 }, { "item": "rs:snake_skin_armour", "text": "Members: Snakeskin armour\\n(30 Defence required)", "level": 30 }, { "item": "rs:ranger_boots", "text": "Ranger boots", "level": 40 }, { "item": "rs:robin_hood_hat", "text": "Robin Hood hat", "level": 40 }, { "item": "rs:green_dragonhide_vambraces", "text": "Members: Green dragonhide vambraces)", "level": 40 }, { "item": "rs:green_dragonhide_chaps", "text": "Members: Proselyte armour)", "level": 40 }, { "item": "rs:green_dragonhide_body", "text": "Green dragonhide body\\n(40 Defence required)", "level": 30 }, { "item": "rs:blue_dragonhide_vambraces", "text": "Members: Blue dragonhide vambraces)", "level": 50 }, { "item": "rs:blue_dragonhide_chaps", "text": "Members: Blue dragonhide chaps", "level": 50 }, { "item": "rs:blue_dragonhide_body", "text": "Members: Blue dragonhide body\\n(40 Defence required)", "level": 50 }, { "item": "rs:red_dragonhide_vambraces", "text": "Members: Red dragonhide vambraces)", "level": 60 }, { "item": "rs:red_dragonhide_chaps", "text": "Members: Red dragonhide chaps)", "level": 60 }, { "item": "rs:red_dragonhide_body", "text": "Members: Red dragonhide body\\n(40 Defence required)", "level": 60 }, { "item": "rs:black_dragonhide_vambraces", "text": "Members: Black dragonhide vambraces)", "level": 70 }, { "item": "rs:black_dragonhide_chaps", "text": "Members: Black dragonhide chaps)", "level": 70 }, { "item": "rs:black_dragonhide_body", "text": "Members: Black dragonhide body\\n(40 Defence required)", "level": 70 }, { "item": "rs:karils_leathertop", "text": "Members: Karil's leather armour\\(70 Defence required)", "level": 70 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/runecrafting.json ================================================ { "id": 140, "name": "Runecrafting", "members": false, "sub_guides": [ { "name": "Runes", "lines": [ { "item": "rs:air_rune", "text": "Air runes", "level": 1 }, { "item": "rs:mind_rune", "text": "Mind runes", "level": 2 }, { "item": "rs:water_rune", "text": "Water runes", "level": 5 }, { "item": "rs:mist_rune", "text": "Members: Mist runes", "level": 6 }, { "item": "rs:earth_rune", "text": "Earth runes", "level": 9 }, { "item": "rs:dust_rune", "text": "Members: Dust runes", "level": 10 }, { "item": "rs:mud_rune", "text": "Members: Mud runes", "level": 13 }, { "item": "rs:fire_rune", "text": "Fire runes", "level": 14 }, { "item": "rs:smoke_rune", "text": "Members: Smoke runes", "level": 15 }, { "item": "rs:steam_rune", "text": "Members: Steam runes", "level": 19 }, { "item": "rs:body_rune", "text": "Body runes", "level": 20 }, { "item": "rs:lava_rune", "text": "Members: Lava runes", "level": 23 }, { "item": "rs:cosmic_rune", "text": "Members: Cosmic runes", "level": 27 }, { "item": "rs:chaos_rune", "text": "Members: Chaos runes", "level": 35 }, { "item": "rs:astral_rune", "text": "Members: Astral runes", "level": 40 }, { "item": "rs:nature_rune", "text": "Members: Nature runes", "level": 44 }, { "item": "rs:law_rune", "text": "Members: Law runes", "level": 54 }, { "item": "rs:death_rune", "text": "Members: Death runes", "level": 65 }, { "item": "rs:blood_rune", "text": "Members: Blood runes", "level": 77 }, { "item": "rs:soul_rune", "text": "Members: Soul runes", "level": 90 } ] }, { "name": "Multiple", "lines": [ { "item": "rs:air_rune", "text": "2 Air runes per essence", "level": 11 }, { "item": "rs:mind_rune", "text": "2 Mind runes per essence", "level": 14 }, { "item": "rs:water_rune", "text": "2 Water runes per essence", "level": 19 }, { "item": "rs:air_rune", "text": "3 Air runes per essence", "level": 22 }, { "item": "rs:earth_rune", "text": "2 Earth runes per essence", "level": 26 }, { "item": "rs:mind_rune", "text": "3 Mind runes per essence", "level": 28 }, { "item": "rs:air_rune", "text": "4 Air runes per essence", "level": 33 }, { "item": "rs:fire_rune", "text": "2 Fire runes per essence", "level": 35 }, { "item": "rs:water_rune", "text": "3 Water runes per essence", "level": 38 }, { "item": "rs:mind_rune", "text": "4 Mind runes per essence", "level": 42 }, { "item": "rs:air_rune", "text": "5 Air runes per essence", "level": 44 }, { "item": "rs:body_rune", "text": "2 Body runes per essence", "level": 46 }, { "item": "rs:earth_rune", "text": "3 Earth runes per essence", "level": 52 }, { "item": "rs:air_rune", "text": "6 Air runes per essence", "level": 55 }, { "item": "rs:mind_rune", "text": "5 Mind runes per essence", "level": 56 }, { "item": "rs:water_rune", "text": "4 Water runes per essence", "level": 57 }, { "item": "rs:cosmic_rune", "text": "Members: 2 Cosmic runes per essence", "level": 59 }, { "item": "rs:air_rune", "text": "7 Air runes per essence", "level": 66 }, { "item": "rs:mind_rune", "text": "6 Mind runes per essence", "level": 70 }, { "item": "rs:fire_rune", "text": "3 Fire runes per essence", "level": 70 }, { "item": "rs:chaos_rune", "text": "Members: 2 Chaos runes per essence", "level": 74 }, { "item": "rs:water_rune", "text": "5 Water runes per essence", "level": 76 }, { "item": "rs:air_rune", "text": "8 Air runes per essence", "level": 77 }, { "item": "rs:earth_rune", "text": "4 Earth runes per essence", "level": 78 }, { "item": "rs:astral_rune", "text": "Members: 2 Astral runes per essence", "level": 82 }, { "item": "rs:mind_rune", "text": "7 Mind runes per essence", "level": 84 }, { "item": "rs:air_rune", "text": "9 Air runes per essence", "level": 88 }, { "item": "rs:nature_rune", "text": "Members: 2 Nature runes per essence", "level": 91 }, { "item": "rs:body_rune", "text": "3 Body runes per essence", "level": 92 }, { "item_id": "rs:water_rune", "text": "6 Water runes per essence", "level": 95 }, { "item": "rs:law_rune", "text": "Members: 2 Law runes per essence", "level": 95 }, { "item": "rs:mind_rune", "text": "8 Mind runes per essence", "level": 98 }, { "item": "rs:air_rune", "text": "10 Air runes per essence", "level": 99 }, { "item": "rs:death_rune", "text": "Members: 2 Death runes per essence", "level": 99 } ] }, { "name": "Pouches", "lines": [ { "item": "rs:small_pouch", "text": "Small pouch: Holds 3 extra essence", "level": 1 }, { "item": "rs:medium_pouch", "text": "Medium pouch: Holds 6 extra essence", "level": 25 }, { "item": "rs:large_pouch", "text": "Large pouch: Holds 9 extra essence", "level": 50 }, { "item": "rs:giant_pouch", "text": "Giant pouch: Holds 12 extra essence", "level": 75 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/skill-guide-config.ts ================================================ import { itemMap } from '@engine/config/config-handler'; import type { ItemDetails } from '@engine/config/item-config'; import { loadConfigurationFiles } from '@runejs/common/fs'; interface SkillGuideConfiguration { id: number; name: string; members: boolean; sub_guides: { name: string; lines: { item: string; text: string; level: number; }[]; }[]; } export interface SkillGuide { id: number; name: string; members: boolean; sub_guides: SkillSubGuide[]; } export interface SkillSubGuide { name: string; lines: { item: ItemDetails | undefined; text: string; level: number; }[]; } /** * Loads the skill guides from the new json format. * @param path * @return SkillGuideConfiguration[] */ export async function loadSkillGuideConfigurations(path: string): Promise { const skillGuides: SkillGuide[] = []; const files = await loadConfigurationFiles(path); files.forEach(skillGuide => { if (!skillGuide?.sub_guides) { return; } const subGuides: SkillSubGuide[] = []; skillGuide.sub_guides.forEach(subGuide => { const subGuideLines: SkillSubGuide['lines'] = []; subGuide.lines.forEach(line => { subGuideLines.push({ item: itemMap[line.item], text: line.text, level: line.level, }); }); subGuides.push({ name: subGuide.name, lines: subGuideLines, }); }); skillGuides.push({ id: skillGuide.id, name: skillGuide.name, members: skillGuide.members, sub_guides: subGuides, }); }); return skillGuides; } ================================================ FILE: src/plugins/skills/skill-guides/skill-guides.plugin.ts ================================================ import type { ButtonActionHook, buttonActionHandler } from '@engine/action/pipe/button.action'; import type { WidgetInteractionActionHook, widgetInteractionActionHandler } from '@engine/action/pipe/widget-interaction.action'; import { widgets } from '@engine/config/config-handler'; import type { Player } from '@engine/world/actor/player/player'; import { logger } from '@runejs/common'; import type { SkillGuide, SkillSubGuide } from './skill-guide-config'; import { loadSkillGuideConfigurations } from './skill-guide-config'; const skillGuidePath = __dirname.replace(/dist/, 'src'); const sidebarTextIds = [131, 108, 109, 112, 122, 125, 128, 143, 146, 149, 159, 162, 165]; const sidebarIds = [129, 98, -1, 110, 113, 123, 126, 134, 144, 147, 150, 160, 163]; let guides: SkillGuide[] = []; function loadGuide(player: Player, guideId: number, subGuideId: number = 0, refreshSidebar: boolean = true): void { const guide = guides.find(g => g.id === guideId); if (!guide) { logger.error(`Could not find skill guide with id ${guideId}`); return; } if (refreshSidebar) { player.modifyWidget(widgets.skillGuide, { childId: 133, text: guide.members ? 'Members only skill' : '' }); for (let i = 0; i < sidebarTextIds.length; i++) { const sidebarId = sidebarIds[i]; let hidden: boolean = true; if (i >= guide.sub_guides.length) { player.modifyWidget(widgets.skillGuide, { childId: sidebarTextIds[i], text: '' }); hidden = true; } else { player.modifyWidget(widgets.skillGuide, { childId: sidebarTextIds[i], text: guide.sub_guides[i].name }); hidden = false; } if (sidebarId !== -1) { // Apparently you can never have only TWO subguides... // Because childId 98 deletes both options 2 AND 3. So, good thing there are no guides with only 2 sections, I guess?... // Verified this in an interface editor, and they are indeed grouped in a single layer for some reason... player.modifyWidget(widgets.skillGuide, { childId: sidebarIds[i], hidden }); } } } const subGuide: SkillSubGuide = guide.sub_guides[subGuideId]; player.modifyWidget(widgets.skillGuide, { childId: 1, text: guide.name + ' - ' + subGuide.name }); const itemIds: number[] = subGuide.lines.map(g => g.item?.gameId || 0).concat(new Array(30 - subGuide.lines.length).fill(null)); player.outgoingPackets.sendUpdateAllWidgetItemsById({ widgetId: widgets.skillGuide, containerId: 132 }, itemIds); for (let i = 0; i < 30; i++) { if (subGuide.lines.length <= i) { player.modifyWidget(widgets.skillGuide, { childId: 5 + i, text: '' }); player.modifyWidget(widgets.skillGuide, { childId: 45 + i, text: '' }); } else { player.modifyWidget(widgets.skillGuide, { childId: 5 + i, text: subGuide.lines[i].level.toString() }); player.modifyWidget(widgets.skillGuide, { childId: 45 + i, text: subGuide.lines[i].text }); } } player.interfaceState.openWidget(widgets.skillGuide, { slot: 'screen', multi: false, }); player.metadata.activeSkillGuide = guideId; } export const guideHandler: buttonActionHandler = async details => { const { player, buttonId } = details; if (!guides.length) { guides = await loadSkillGuideConfigurations(skillGuidePath); } loadGuide(player, buttonId); }; export const subGuideHandler: widgetInteractionActionHandler = async details => { const { player, childId } = details; if (!guides.length) { guides = await loadSkillGuideConfigurations(skillGuidePath); } const activeSkillGuide = player.metadata.activeSkillGuide; if (!activeSkillGuide) { return; } const guide = guides.find(g => g.id === activeSkillGuide); if (!guide) { logger.error(`Could not find skill guide with id ${activeSkillGuide}`); return; } const subGuideId = sidebarTextIds.indexOf(childId); if (subGuideId >= guide.sub_guides.length) { return; } loadGuide(player, guide.id, subGuideId, true); }; export default { pluginId: 'rs:skill_guides', hooks: [ { type: 'button', widgetId: widgets.skillsTab, handler: guideHandler, } as ButtonActionHook, { type: 'widget_interaction', widgetIds: widgets.skillGuide, optionId: 0, handler: subGuideHandler, } as WidgetInteractionActionHook, ], }; ================================================ FILE: src/plugins/skills/skill-guides/slayer.json ================================================ { "id": 141, "name": "Slayer", "members": false, "sub_guides": [ { "name": "Monsters", "lines": [ { "item": "rs:crawling_hands", "text": "Crawling hands", "level": 5 }, { "item": "rs:cave_bugs", "text": "Cave bugs", "level": 7 }, { "item": "rs:cave_crawlers", "text": "Cave crawlers", "level": 10 }, { "item": "rs:Banshees", "text": "banshees", "level": 15 }, { "item": "rs:cave_slime", "text": "Cave slime", "level": 17 }, { "item": "rs:rockslugs", "text": "Rockslugs", "level": 20 }, { "item": "rs:desert_lizards", "text": "Desert lizards", "level": 22 }, { "item": "rs:cockatrice", "text": "Cockatrice", "level": 25 }, { "item": "rs:pyrefiends", "text": "Pyrefiends", "level": 30 }, { "item": "rs:mogres", "text": "Mogres", "level": 32 }, { "item": "rs:harpie_bug_swarms", "text": "Harpie bug swarms", "level": 33 }, { "item": "rs:wall_beasts", "text": "Wall beasts", "level": 35 }, { "item": "rs:killerwatts", "text": "Killerwatts", "level": 37 }, { "item": "rs:basilisks", "text": "Basilisks", "level": 40 }, { "item": "rs:fever_spiders", "text": "Fever spiders", "level": 42 }, { "item": "rs:infernal_mages", "text": "Infernal mages", "level": 45 }, { "item": "rs:brine_rats", "text": "Brine rats", "level": 47 }, { "item": "rs:bloodvelds", "text": "Bloodvelds", "level": 50 }, { "item": "rs:jellies", "text": "Jellies", "level": 52 }, { "item": "rs:turoth", "text": "Turoth", "level": 55 }, { "item": "rs:cave_horrors", "text": "Cave horrors", "level": 58 }, { "item": "rs:aberrant_spectres", "text": "Aberrant spectres", "level": 60 }, { "item": "rs:dust_devils", "text": "Dust devils", "level": 65 }, { "item": "rs:kurask", "text": "Kurask", "level": 70 }, { "item": "rs:skeletal_wyverns", "text": "Skeletal wyverns", "level": 72 }, { "item": "rs:gargoyles", "text": "Gargoyles", "level": 75 }, { "item": "rs:nechryael", "text": "Nechryael", "level": 80 }, { "item": "rs:abyssal demons", "text": "Abyssal demons", "level": 85 }, { "item": "rs:dark_beasts", "text": "Dark beasts", "level": 58 } ] }, { "name": "Equipment", "lines": [ { "item": "rs:spiny_helmet", "text": "Spiney helmet\\n(5 Defence required)", "level": 1 }, { "item": "rs:rock_hammer", "text": "Rock hammer & Rock Thrownhammer", "level": 1 }, { "item": "rs:facemask", "text": "Facemask", "level": 10 }, { "item": "rs:earmuffs", "text": "Earmuffs", "level": 15 }, { "item": "rs:mirror_shield", "text": "Mirror shield\\n(20 Defence required)", "level": 25 }, { "item": "rs:harpie_bug_lantern", "text": "Harpie bug lantern\\n(not a light source)", "level": 33 }, { "item": "rs:witchwood_icon", "text": "Witchwood icon", "level": 35 }, { "item": "rs:insulated_boots", "text": "Insultated boots", "level": 37 }, { "item": "rs:slayer_bell", "text": "Slayer bell", "level": 39 }, { "item": "rs:slayer_gloves", "text": "Slayer gloves", "level": 42 }, { "item": "rs:boots_of_stone", "text": "Boots of Stone", "level": 44 }, { "item": "rs:leaf_bladed_spear", "text": "Leafbladed spear\\n(50 Attack required)", "level": 55 }, { "item": "rs:broad_arrows", "text": "Broad arrows\\n(50 Ranged required)", "level": 55 }, { "item": "rs:slayer_staff", "text": "Slayer's staff\\n(50 Magic required)", "level": 55 }, { "item": "rs:fungicide_spray", "text": "Fungicide spray", "level": 57 }, { "item": "rs:nose_peg", "text": "Nose peg", "level": 60 } ] }, { "name": "Slayer Masters", "lines": [ { "item": "rs:enchanted_gem", "text": "Burthorpe master\\n(Level 3 combat required)", "level": 1 }, { "item": "rs:enchanted_gem", "text": "Canifis master\\n(Level 20 combat required)", "level": 1 }, { "item": "rs:enchanted_gem", "text": "Edgeville Dungeon master\\n(Level 40 combat required)", "level": 1 }, { "item": "rs:enchanted_gem", "text": "Zanaris master\\n(Level 70 combat required)", "level": 1 }, { "item": "rs:enchanted_gem", "text": "Shilo Village master\\n(Level 100 combat required)", "level": 50 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/smithing.json ================================================ { "id": 123, "name": "Smithing", "members": true, "sub_guides": [ { "name": "Smelting", "lines": [ { "item": "rs:bronze_bar", "text": "Bronze bar\\n1 tin ore & 1 copper ore", "level": 1 }, { "item": "rs:blurite_bar", "text": "Members: Blurite\\n(after Knight's Sword quest)", "level": 8 }, { "item": "rs:iron_bar", "text": "Iron bar\\n50% chance of success", "level": 15 }, { "item": "rs:silver_bar", "text": "Silver bar\\nSilver ore only", "level": 20 }, { "item": "rs:steel_bar", "text": "Steel bar\\n2 coal & iron ore", "level": 30 }, { "item": "rs:gold_bar", "text": "Gold bar\\nGold ore only", "level": 40 }, { "item": "rs:mithril_bar", "text": "Mithril bar\\n4 coal * 1 mithril ore", "level": 50 }, { "item": "rs:adamant_bar", "text": "Adamant bar\\n6 coal & 1 adamantite ore", "level": 70 }, { "item": "rs:runite_bar", "text": "Runite bar\\n8 coal & 1 runite ore", "level": 85 } ] }, { "name": "Bronze", "lines": [ { "item": "rs:bronze_dagger", "text": "Bronze daggers\\nrequires 1 bar", "level": 1 }, { "item": "rs:bronze_axes", "text": "Bronze axes \\nrequires 1 bar", "level": 1 }, { "item": "rs:bronze_mace", "text": "Bronze maces\\nrequires 1 bar", "level": 2 }, { "item": "rs:bronze_medium_helm", "text": "Bronze medium helms\\nrequires 1 bar", "level": 3 }, { "item": "rs:bronze_crossbow_bolts", "text": "Members: Bronze crossbow bolts\\nrequires 1 bar to make 10", "level": 3 }, { "item": "rs:bronze_sword", "text": "Bronze swords\\nrequires 1 bar", "level": 4 }, { "item": "rs:bronze_dart_tips", "text": "Members: Bronze dart tips\\nrequires 1 bar to make 10", "level": 4 }, { "item": "rs:bronze_wire", "text": "Bronze wire\\nrequires 1 bar", "level": 4 }, { "item": "rs:bronze_nails", "text": "Bronze nails\\nrequires 1 bar to make 10", "level": 4 }, { "item": "rs:bronze_scimitars", "text": "Bronze scimitars\\nrequires 2 bars", "level": 5 }, { "item": "rs:bronze_spears", "text": "Bronze spears\\nrequires 1 bar", "level": 5 }, { "item": "rs:bronze_hastae", "text": "Bronze hastae\\nrequires 1 bar & 1 log", "level": 5 }, { "item": "rs:bronze_arrowheads", "text": "Bronze arrowheads\\nrequires 1 bar to make 15", "level": 5 }, { "item": "rs:bronze_crossbow_limb", "text": "Bronze crossbow limbs\\nrequires 1 bar", "level": 6 }, { "item": "rs:bronze_longsword", "text": "Bronze longswords \\nrequires 2 bars", "level": 6 }, { "item": "rs:bronze_javelin_heads", "text": "Members: Bronze javelin heads\\nrequires 1 bar to make 5", "level": 6 }, { "item": "rs:bronze_full_helm", "text": "Bronze full helms\\nrequires 2 bars", "level": 7 }, { "item": "rs:bronze_throwing_knives", "text": "Members: Bronze throwing knives\\nrequires 1 bar to make 5", "level": 7 }, { "item": "rs:bronze_square_shield", "text": "Bronze square shields\\nrequires 2 bars", "level": 8 }, { "item": "rs:bronze_warhammer", "text": "Bronze warhammers\\nrequires 3 bars", "level": 9 }, { "item": "rs:bronze_battleaxe", "text": "Bronze battleaxes\\nrequires 3 bars", "level": 10 }, { "item": "rs:bronze_chainbody", "text": "Bronze chainbodies\\nrequires 3 bars", "level": 11 }, { "item": "rs:bronze_kiteshield", "text": "Bronze kiteshields\\nrequires 3 bars", "level": 12 }, { "item": "rs:bronze_claws", "text": "Members: Bronze claws\\nrequires 2 bars", "level": 13 }, { "item": "rs:bronze_two_handed_sword", "text": "Bronze two-handed swords\\nrequires 3 bars", "level": 14 }, { "item": "rs:bronze_platelegs", "text": "Bronze platelegs\\nrequires 3 bars", "level": 16 }, { "item": "rs:bronze_skirt", "text": "Bronze plateskirts\\nrequires 3 bars", "level": 16 }, { "item": "rs:bronze_platebody", "text": "Bronze platebodies \\nrequires 5 bars", "level": 18 } ] }, { "name": "Blurite", "lines": [ { "item": "rs:bronze_bolt", "text": "Bronze bolts", "level": 9 }, { "item": "rs:opal_bolt", "text": "Opal-tipped bronze bolts", "level": 11 }, { "item": "rs:blurite_bolt", "text": "Blurite bolts (After Knight´s Sword quest)", "level": 24 }, { "item": "rs:jade_bolt", "text": "Jade-tipped blurite bolts", "level": 26 }, { "item": "rs:iron_bolt", "text": "Iron bolts", "level": 39 }, { "item": "rs:silver_bolt", "text": "Silver bolts", "level": 43 }, { "item": "rs:steel_bolt", "text": "Steel bolts", "level": 46 }, { "item": "rs:mithril_bolt", "text": "Mithril bolts", "level": 54 }, { "item": "rs:sapphire_bolt", "text": "Sapphire-tipped mithril bolts", "level": 56 }, { "item": "rs:emerald_bolt", "text": "Emerald-tipped mithril bolts", "level": 56 }, { "item": "rs:adamant_bolt", "text": "Adamant bolts", "level": 61 }, { "item": "rs:runite_bolt", "text": "Runite bolts", "level": 69 }, { "item": "rs:onyx_bolt", "text": "Onyx bolts", "level": 69 } ] }, { "name": "Iron", "lines": [ { "item": "rs:iron_dagger", "text": "Iron daggers\\nrequires 1 bar", "level": 15 }, { "item": "rs:iron_axes", "text": "Iron axes \\nrequires 1 bar", "level": 16 }, { "item": "rs:iron_mace", "text": "Iron maces\\nrequires 1 bar", "level": 17 }, { "item": "rs:iron_medium_helm", "text": "Iron medium helms\\nrequires 1 bar", "level": 17 }, { "item": "rs:iron_crossbow_bolts", "text": "Members: Iron crossbow bolts\\nrequires 1 bar to make 10", "level": 18 }, { "item": "rs:iron_sword", "text": "Iron swords\\nrequires 1 bar", "level": 18 }, { "item": "rs:iron_dart_tips", "text": "Members: Iron dart tips\\nrequires 1 bar to make 10", "level": 19 }, { "item": "rs:iron_wire", "text": "Iron wire\\nrequires 1 bar", "level": 19 }, { "item": "rs:iron_nails", "text": "Iron nails\\nrequires 1 bar to make 10", "level": 19 }, { "item": "rs:iron_scimitars", "text": "Iron scimitars\\nrequires 2 bars", "level": 20 }, { "item": "rs:iron_spears", "text": "Iron spears\\nrequires 1 bar", "level": 20 }, { "item": "rs:iron_hastae", "text": "Iron hastae\\nrequires 1 bar & 1 log", "level": 20 }, { "item": "rs:iron_arrowheads", "text": "Iron arrowheads\\nrequires 1 bar to make 15", "level": 20 }, { "item": "rs:iron_crossbow_limb", "text": "Iron crossbow limbs\\nrequires 1 bar", "level": 20 }, { "item": "rs:iron_longsword", "text": "Iron longswords\\nrequires 2 bars", "level": 21 }, { "item": "rs:iron_javelin_heads", "text": "Members: Iron javelin heads\\nrequires 1 bar to make 5", "level": 21 }, { "item": "rs:iron_full_helm", "text": "Iron full helms\\nrequires 2 bars", "level": 22 }, { "item": "rs:iron_throwing_knives", "text": "Members: Iron throwing knives\\nrequires 1 bar to make 5", "level": 22 }, { "item": "rs:iron_square_shield", "text": "Iron square shields\\nrequires 2 bars", "level": 23 }, { "item": "rs:iron_warhammer", "text": "Iron warhammers\\nrequires 3 bars", "level": 24 }, { "item": "rs:iron_battleaxe", "text": "Iron battleaxes\\nrequires 3 bars", "level": 25 }, { "item": "rs:iron_chainbody", "text": "Iron chainbodies\\nrequires 3 bars", "level": 26 }, { "item": "rs:iron_kiteshield", "text": "Iron kiteshields\\nrequires 3 bars", "level": 27 }, { "item": "rs:iron_claws", "text": "Members: Iron claws\\nrequires 2 bars", "level": 28 }, { "item": "rs:iron_two_handed_sword", "text": "Iron two-handed swords\\nrequires 3 bars", "level": 29 }, { "item": "rs:iron_platelegs", "text": "Iron platelegs\\nrequires 3 bars", "level": 31 }, { "item": "rs:iron_skirt", "text": "Iron plateskirts\\nrequires 3 bars", "level": 31 }, { "item": "rs:iron_platebody", "text": "Iron platebodies \\nrequires 5 bars", "level": 33 } ] }, { "name": "Steel", "lines": [ { "item": "rs:Steel_dagger", "text": "Steel daggers\\nrequires 1 bar", "level": 30 }, { "item": "rs:Steel_axes", "text": "Steel axes \\nrequires 1 bar", "level": 31 }, { "item": "rs:Steel_mace", "text": "Steel maces\\nrequires 1 bar", "level": 32 }, { "item": "rs:Steel_medium_helm", "text": "Steel medium helms\\nrequires 1 bar", "level": 33 }, { "item": "rs:Steel_crossbow_bolts", "text": "Members: Steel crossbow bolts\\nrequires 1 bar to make 10", "level": 33 }, { "item": "rs:Steel_sword", "text": "Steel swords\\nrequires 1 bar", "level": 34 }, { "item": "rs:Steel_dart_tips", "text": "Members: Steel dart tips\\nrequires 1 bar to make 10", "level": 34 }, { "item": "rs:Steel_wire", "text": "Steel wire\\nrequires 1 bar", "level": 34 }, { "item": "rs:Steel_nails", "text": "Steel nails\\nrequires 1 bar to make 10", "level": 34 }, { "item": "rs:Steel_scimitars", "text": "Steel scimitars\\nrequires 2 bars", "level": 35 }, { "item": "rs:Steel_spears", "text": "Steel spears\\nrequires 1 bar", "level": 35 }, { "item": "rs:Steel_hastae", "text": "Steel hastae\\nrequires 1 bar & 1 Willow log", "level": 35 }, { "item": "rs:Steel_arrowheads", "text": "Steel arrowheads\\nrequires 1 bar to make 15", "level": 35 }, { "item": "rs:Steel_crossbow_limb", "text": "Steel crossbow limbs\\nrequires 1 bar", "level": 36 }, { "item": "rs:Steel_longsword", "text": "Steel longswords\\nrequires 2 bars", "level": 36 }, { "item": "rs:Steel_javelin_heads", "text": "Members: Steel javelin heads\\nrequires 1 bar to make 5", "level": 36 }, { "item": "rs:Steel_full_helm", "text": "Steel full helms\\nrequires 2 bars", "level": 33 }, { "item": "rs:Steel_throwing_knives", "text": "Members: Steel throwing knives\\nrequires 1 bar to make 5", "level": 33 }, { "item": "rs:Steel_square_shield", "text": "Steel square shields\\nrequires 2 bars", "level": 34 }, { "item": "rs:Steel_warhammer", "text": "Steel warhammers\\nrequires 3 bars", "level": 39 }, { "item": "rs:Steel_battleaxe", "text": "Steel battleaxes\\nrequires 3 bars", "level": 40 }, { "item": "rs:Steel_chainbody", "text": "Steel chainbodies\\nrequires 3 bars", "level": 41 }, { "item": "rs:Steel_kiteshield", "text": "Steel kiteshields\\nrequires 3 bars", "level": 42 }, { "item": "rs:Steel_claws", "text": "Members: Steel claws\\nrequires 2 bars", "level": 43 }, { "item": "rs:Steel_two_handed_sword", "text": "Steel two-handed swords\\nrequires 3 bars", "level": 44 }, { "item": "rs:Steel_platelegs", "text": "Steel platelegs\\nrequires 3 bars", "level": 46 }, { "item": "rs:Steel_skirt", "text": "Steel plateskirts\\nrequires 3 bars", "level": 46 }, { "item": "rs:Steel_platebody", "text": "Steel platebodies \\nrequires 5 bars", "level": 44 } ] }, { "name": "Mithril", "lines": [ { "item": "rs:mithril_dagger", "text": "Mithril daggers\\nrequires 1 bar", "level": 50 }, { "item": "rs:mithril_axes", "text": "Mithril axes \\nrequires 1 bar", "level": 51 }, { "item": "rs:mithril_mace", "text": "Mithril maces\\nrequires 1 bar", "level": 52 }, { "item": "rs:mithril_medium_helm", "text": "Mithril medium helms\\nrequires 1 bar", "level": 53 }, { "item": "rs:mithril_crossbow_bolts", "text": "Members: Mithril crossbow bolts\\nrequires 1 bar to make 10", "level": 53 }, { "item": "rs:mithril_sword", "text": "Mithril swords\\nrequires 1 bar", "level": 54 }, { "item": "rs:mithril_dart_tips", "text": "Members: Mithril dart tips\\nrequires 1 bar to make 10", "level": 54 }, { "item": "rs:mithril_wire", "text": "Mithril wire\\nrequires 1 bar", "level": 54 }, { "item": "rs:mithril_nails", "text": "Mithril nails\\nrequires 1 bar to make 10", "level": 54 }, { "item": "rs:mithril_scimitars", "text": "Mithril scimitars\\nrequires 2 bars", "level": 55 }, { "item": "rs:mithril_spears", "text": "Mithril spears\\nrequires 1 bar", "level": 55 }, { "item": "rs:mithril_hastae", "text": "Mithril hastae\\nrequires 1 bar & 1 maple log", "level": 55 }, { "item": "rs:mithril_arrowheads", "text": "Mithril arrowheads\\nrequires 1 bar to make 15", "level": 55 }, { "item": "rs:mithril_crossbow_limb", "text": "Mithril crossbow limbs\\nrequires 1 bar", "level": 56 }, { "item": "rs:mithril_longsword", "text": "Mithril longswords\\nrequires 2 bars", "level": 56 }, { "item": "rs:mithril_javelin_heads", "text": "Members: Mithril javelin heads\\nrequires 1 bar to make 5", "level": 56 }, { "item": "rs:mithril_full_helm", "text": "Mithril full helms\\nrequires 2 bars", "level": 57 }, { "item": "rs:mithril_throwing_knives", "text": "Members: Mithril throwing knives\\nrequires 1 bar to make 5", "level": 57 }, { "item": "rs:mithril_square_shield", "text": "Mithril square shields\\nrequires 2 bars", "level": 58 }, { "item": "rs:mithril_warhammer", "text": "Mithril warhammers\\nrequires 3 bars", "level": 59 }, { "item": "rs:mithril_battleaxe", "text": "Mithril battleaxes\\nrequires 3 bars", "level": 60 }, { "item": "rs:mithril_chainbody", "text": "Mithril chainbodies\\nrequires 3 bars", "level": 61 }, { "item": "rs:mithril_kiteshield", "text": "Mithril kiteshields\\nrequires 3 bars", "level": 62 }, { "item": "rs:mithril_claws", "text": "Members: Mithril claws\\nrequires 2 bars", "level": 63 }, { "item": "rs:mithril_two_handed_sword", "text": "Mithril two-handed swords\\nrequires 3 bars", "level": 64 }, { "item": "rs:mithril_platelegs", "text": "Mithril platelegs\\nrequires 3 bars", "level": 66 }, { "item": "rs:mithril_skirt", "text": "Mithril plateskirts\\nrequires 3 bars", "level": 66 }, { "item": "rs:mithril_platebody", "text": "Mithril platebodies \\nrequires 5 bars", "level": 68 } ] }, { "name": "Adamant", "lines": [ { "item": "rs:adamant_dagger", "text": "Adamant daggers\\nrequires 1 bar", "level": 70 }, { "item": "rs:adamant_axes", "text": "Adamant axes \\nrequires 1 bar", "level": 71 }, { "item": "rs:adamant_mace", "text": "Adamant maces\\nrequires 1 bar", "level": 72 }, { "item": "rs:adamant_medium_helm", "text": "Adamant medium helms\\nrequires 1 bar", "level": 73 }, { "item": "rs:adamant_crossbow_bolts", "text": "Members: Adamant crossbow bolts\\nrequires 1 bar to make 10", "level": 73 }, { "item": "rs:adamant_sword", "text": "Adamant swords\\nrequires 1 bar", "level": 74 }, { "item": "rs:adamant_dart_tips", "text": "Members: Adamant dart tips\\nrequires 1 bar to make 10", "level": 74 }, { "item": "rs:adamant_wire", "text": "Adamant wire\\nrequires 1 bar", "level": 74 }, { "item": "rs:adamant_nails", "text": "Adamant nails\\nrequires 1 bar to make 10", "level": 74 }, { "item": "rs:adamant_scimitars", "text": "Adamant scimitars\\nrequires 2 bars", "level": 75 }, { "item": "rs:adamant_spears", "text": "Adamant spears\\nrequires 1 bar", "level": 75 }, { "item": "rs:adamant_hastae", "text": "Adamant hastae\\nrequires 1 bar & 1 yew log", "level": 75 }, { "item": "rs:adamant_arrowheads", "text": "Adamant arrowheads\\nrequires 1 bar to make 15", "level": 75 }, { "item": "rs:adamant_crossbow_limb", "text": "Adamant crossbow limbs\\nrequires 1 bar", "level": 76 }, { "item": "rs:adamant_longsword", "text": "Adamant longswords\\nrequires 2 bars", "level": 76 }, { "item": "rs:adamant_javelin_heads", "text": "Members: Adamant javelin heads\\nrequires 1 bar to make 5", "level": 76 }, { "item": "rs:adamant_full_helm", "text": "Adamant full helms\\nrequires 2 bars", "level": 77 }, { "item": "rs:adamant_throwing_knives", "text": "Members: Adamant throwing knives\\nrequires 1 bar to make 5", "level": 77 }, { "item": "rs:adamant_square_shield", "text": "Adamant square shields\\nrequires 2 bars", "level": 78 }, { "item": "rs:adamant_warhammer", "text": "Adamant warhammers\\nrequires 3 bars", "level": 79 }, { "item": "rs:adamant_battleaxe", "text": "Adamant battleaxes\\nrequires 3 bars", "level": 80 }, { "item": "rs:adamant_chainbody", "text": "Adamant chainbodies\\nrequires 3 bars", "level": 81 }, { "item": "rs:adamant_kiteshield", "text": "Adamant kiteshields\\nrequires 3 bars", "level": 82 }, { "item": "rs:adamant_claws", "text": "Members: Adamant claws\\nrequires 2 bars", "level": 83 }, { "item": "rs:adamant_two_handed_sword", "text": "Adamant two-handed swords\\nrequires 3 bars", "level": 84 }, { "item": "rs:adamant_platelegs", "text": "Adamant platelegs\\nrequires 3 bars", "level": 86 }, { "item": "rs:adamant_skirt", "text": "Adamant plateskirts\\nrequires 3 bars", "level": 86 }, { "item": "rs:adamant_platebody", "text": "Adamant platebodies \\nrequires 5 bars", "level": 88 } ] }, { "name": "Rune", "lines": [ { "item": "rs:rune_dagger", "text": "Rune daggers\\nrequires 1 bar", "level": 85 }, { "item": "rs:rune_axes", "text": "Rune axes \\nrequires 1 bar", "level": 86 }, { "item": "rs:rune_mace", "text": "Rune maces\\nrequires 1 bar", "level": 87 }, { "item": "rs:rune_medium_helm", "text": "Rune medium helms\\nrequires 1 bar", "level": 88 }, { "item": "rs:rune_crossbow_bolts", "text": "Members: Rune crossbow bolts\\nrequires 1 bar to make 10", "level": 88 }, { "item": "rs:rune_sword", "text": "Rune swords\\nrequires 1 bar", "level": 89 }, { "item": "rs:rune_dart_tips", "text": "Members: Rune dart tips\\nrequires 1 bar to make 10", "level": 89 }, { "item": "rs:rune_wire", "text": "Rune wire\\nrequires 1 bar", "level": 89 }, { "item": "rs:rune_nails", "text": "Rune nails\\nrequires 1 bar to make 10", "level": 90 }, { "item": "rs:rune_scimitars", "text": "Rune scimitars\\nrequires 2 bars", "level": 90 }, { "item": "rs:rune_spears", "text": "Rune spears\\nrequires 1 bar", "level": 90 }, { "item": "rs:rune_hastae", "text": "Rune hastae\\nrequires 1 bar & 1 magic log", "level": 90 }, { "item": "rs:rune_arrowheads", "text": "Rune arrowheads\\nrequires 1 bar to make 15", "level": 90 }, { "item": "rs:rune_crossbow_limb", "text": "Rune crossbow limbs\\nrequires 1 bar", "level": 91 }, { "item": "rs:rune_longsword", "text": "Rune longswords\\nrequires 2 bars", "level": 91 }, { "item": "rs:rune_javelin_heads", "text": "Members: Rune javelin heads\\nrequires 1 bar to make 5", "level": 91 }, { "item": "rs:rune_full_helm", "text": "Rune full helms\\nrequires 2 bars", "level": 92 }, { "item": "rs:rune_throwing_knives", "text": "Members: Rune throwing knives\\nrequires 1 bar to make 5", "level": 92 }, { "item": "rs:rune_square_shield", "text": "Rune square shields\\nrequires 2 bars", "level": 93 }, { "item": "rs:rune_warhammer", "text": "Rune warhammers\\nrequires 3 bars", "level": 94 }, { "item": "rs:rune_battleaxe", "text": "Rune battleaxes\\nrequires 3 bars", "level": 95 }, { "item": "rs:rune_chainbody", "text": "Rune chainbodies\\nrequires 3 bars", "level": 96 }, { "item": "rs:rune_kiteshield", "text": "Rune kiteshields\\nrequires 3 bars", "level": 97 }, { "item": "rs:rune_claws", "text": "Members: Rune claws\\nrequires 2 bars", "level": 98 }, { "item": "rs:rune_two_handed_sword", "text": "Rune two-handed swords\\nrequires 3 bars", "level": 99 }, { "item": "rs:rune_platelegs", "text": "Rune platelegs\\nrequires 3 bars", "level": 99 }, { "item": "rs:rune_skirt", "text": "Rune plateskirts\\nrequires 3 bars", "level": 99 }, { "item": "rs:rune_platebody", "text": "Rune platebodies \\nrequires 5 bars", "level": 99 } ] }, { "name": "Other", "lines": [ { "item": "rs:dragon_square_shield", "text": "Dragon square shield", "level": 60 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/thieving.json ================================================ { "id": 128, "name": "Attack", "members": true, "sub_guides": [ { "name": "Pickpocket", "lines": [ { "item": "rs:citizen", "text": "Citizen", "level": 1 }, { "item": "rs:farmer", "text": "Farmer", "level": 10 }, { "item": "rs:female_ham_follower", "text": "Female H.A.M follower", "level": 15 }, { "item": "rs:male_ham_follower", "text": "Male H.A.M follower", "level": 20 }, { "item": "rs:warrior", "text": "Warrior", "level": 25 }, { "item": "rs:rogue", "text": "Rogue", "level": 32 }, { "item": "rs:cave_goblin", "text": "Cave goblin", "level": 36 }, { "item": "rs:master_farmer", "text": "Master f armer", "level": 38 }, { "item": "rs:guard", "text": "Guard", "level": 40 }, { "item": "rs:fremennik", "text": "Fremennik", "level": 45 }, { "item": "rs:bearded_pollnivnian_bandit", "text": "Bearded Pollnivnian bandit", "level": 45 }, { "item": "rs:desert_bandit", "text": "Desert bandit", "level": 53 }, { "item": "rs:knight", "text": "Knight", "level": 55 }, { "item": "rs:pollnivnian_bandit", "text": "Pollnivnian bandit", "level": 55 }, { "item": "rs:watchman", "text": "Watchman", "level": 65 }, { "item": "rs:menaphite_thug", "text": "Menaphite thug", "level": 65 }, { "item": "rs:paladin", "text": "Paladin", "level": 70 }, { "item": "rs:gnome", "text": "Gnome", "level": 75 }, { "item": "rs:hero", "text": "Hero", "level": 80 }, { "item": "rs:vyre", "text": "Vyre", "level": 82 }, { "item": "rs:elf", "text": "Elf", "level": 85 }, { "item": "rs:tzhaar", "text": "TzHaar", "level": 90 } ] }, { "name": "Stalls", "lines": [ { "item": "rs:vegetable_stall", "text": "Vegetable stall", "level": 2 }, { "item": "rs:Cake_stall", "text": "Cake stall", "level": 5 }, { "item": "rs:tea_stall", "text": "Tea stall", "level": 5 }, { "item": "rs:crafting_stall", "text": "Crafting stall", "level": 5 }, { "item": "rs:monkey_food_stall", "text": "Monkey food stall", "level": 5 }, { "item": "rs:silk_stall", "text": "Silk stall", "level": 20 }, { "item": "rs:wine_stall", "text": "Wine stall", "level": 22 }, { "item": "rs:fruit_stall", "text": "Fruit stall", "level": 25 }, { "item": "rs:seed_stall", "text": "Seed stall", "level": 27 }, { "item": "rs:fur_stall", "text": "Fur stall", "level": 35 }, { "item": "rs:fish_stall", "text": "Fish stall", "level": 42 }, { "item": "rs:crossbow_stall", "text": "Crossbow stall", "level": 49 }, { "item": "rs:silver_stall", "text": "Silver stall", "level": 50 }, { "item": "rs:magic_stall", "text": "Magic stall", "level": 65 }, { "item": "rs:scimitar_stall", "text": "Scimitar stall", "level": 65 }, { "item": "rs:spices_stall", "text": "Spices stall", "level": 65 }, { "item": "rs:gems_stall", "text": "Gems stall", "level": 75 }, { "item": "rs:ore_stall", "text": "Ore stall", "level": 82 } ] }, { "name": "Chests", "lines": [ { "item": "rs:ardougne_relekka_wilderness", "text": "Ardougne, Rellekka and the Wilderness", "level": 13 }, { "item": "rs:upstairs_ardougne_rellekka", "text": "Upstairs in Ardougne and Rellekka", "level": 28 }, { "item": "rs:the_isles_of_souls", "text": "The Isle of Souls", "level": 28 }, { "item": "rs:upstairs_ardougne", "text": "Upstairs in Ardougne", "level": 43 }, { "item": "rs:hemenster_rellekka", "text": "Hemenster and Rellekka", "level": 47 }, { "item": "rs:dorgesh_kaan_average_chest", "text": "Dorgesh-Kaan (average chests)", "level": 52 }, { "item": "rs:chaos_druid_tower_north_ardougne", "text": "Chaos druid tower north of Ardougne", "level": 59 }, { "item": "rs:lizardman_temple_molch", "text": "Lizardman Temple beneath Molch", "level": 64 }, { "item": "rs:ardougne_castle", "text": "Ardougne Castle", "level": 72 }, { "item": "rs:dorgesh_kaan_rich", "text": "Dorgesh-Kaan (rich chests)", "level": 78 }, { "item": "rs:wilderness_rogues_castle", "text": "Wilderness Rogue's Castle", "level": 84 } ] }, { "name": "Other", "lines": [ { "item": "rs:pyramid_plunder_room_1", "text": "Pyramid Plunder minigame - Room 1\\nIn the Jalsavrah Pyramid in Sophanem", "level": 21 }, { "item": "rs:pyramid_plunder_room_2", "text": "Pyramid Plunder - Room 2", "level": 31 }, { "item": "rs:pyramid_plunder_room_3", "text": "Pyramid Plunder - Room 3", "level": 41 }, { "item": "rs:uncut_sapphire", "text": "Can crack the wall safes in Rogues'Den", "level": 50 }, { "item": "rs:pyramid_plunder_room_4", "text": "Pyramid Plunder - Room 4", "level": 51 }, { "item": "rs:pyramid_plunder_room_5", "text": "Pyramid Plunder - Room 5", "level": 61 }, { "item": "rs:pyramid_plunder_room_6", "text": "Pyramid Plunder - 6", "level": 71 }, { "item": "rs:pyramid_plunder_room_7", "text": "Pyramid Plunder - 7)", "level": 81 }, { "item": "rs:pyramid_plunder_room_8", "text": "Pyramid Plunder - 8", "level": 91 } ] } ] } ================================================ FILE: src/plugins/skills/skill-guides/woodcutting.json ================================================ { "id": 139, "name": "Woodcutting", "members": false, "sub_guides": [ { "name": "Trees", "lines": [ { "item": "rs:logs", "text": "Normal trees", "level": 1 }, { "item": "rs:achey_logs", "text": "Achey trees", "level": 1 }, { "item": "rs:oak_logs", "text": "Oak trees", "level": 15 }, { "item": "rs:willow_logs", "text": "Willow trees", "level": 30 }, { "item": "rs:teak_logs", "text": "Teak trees", "level": 35 }, { "item": "rs:maple_logs", "text": "Maple trees", "level": 45 }, { "item": "rs:mahogany_logs", "text": "Mahogany trees", "level": 50 }, { "item": "rs:yew_logs", "text": "Yew trees", "level": 60 }, { "item": "rs:magic_logs", "text": "Magic trees", "level": 75 } ] }, { "name": "Axes", "lines": [ { "item": "rs:bronze_axe", "text": "Bronze axe", "level": 1 }, { "item": "rs:iron_axe", "text": "Iron axe", "level": 1 }, { "item": "rs:steel_axe", "text": "Steel axe", "level": 6 }, { "item": "rs:black_axe", "text": "Black axe", "level": 11 }, { "item": "rs:mithril_axe", "text": "Mithril axe", "level": 21 }, { "item": "rs:adamant_axe", "text": "Adamant axe", "level": 31 }, { "item": "rs:rune_axe", "text": "Rune axe", "level": 41 }, { "item": "rs:dragon_axe", "text": "Dragon axe", "level": 61 } ] }, { "name": "Other", "lines": [ { "item": "rs:dwarf_remains", "text": "Missing content", "level": 1 } ] } ] } ================================================ FILE: src/plugins/skills/smithing/forging-constants.ts ================================================ import { itemIds } from '@engine/world/config/item-ids'; import type { Smithable } from '@plugins/skills/smithing/forging-types'; export const anvilIds: number[] = [2782, 2783, 4306, 6150]; /** * Map bars and levels. */ export const bars: Map = new Map([ [itemIds.bars.bronze, 1], [itemIds.bars.iron, 15], [itemIds.bars.steel, 30], [itemIds.bars.mithril, 50], [itemIds.bars.adamantite, 70], [itemIds.bars.runite, 85], ]); export const smithables: Map> = new Map>([ [ 'dagger', new Map([ [ 'bronze', { level: 1, experience: 12.5, item: { itemId: itemIds.daggers.bronze, amount: 1 }, ingredient: { itemId: itemIds.bars.bronze, amount: 1 }, }, ], [ 'iron', { level: 15, experience: 25, item: { itemId: itemIds.daggers.iron, amount: 1 }, ingredient: { itemId: itemIds.bars.iron, amount: 1 }, }, ], [ 'steel', { level: 30, experience: 37.5, item: { itemId: itemIds.daggers.steel, amount: 1 }, ingredient: { itemId: itemIds.bars.steel, amount: 1 }, }, ], [ 'mithril', { level: 50, experience: 50, item: { itemId: itemIds.daggers.mithril, amount: 1 }, ingredient: { itemId: itemIds.bars.mithril, amount: 1 }, }, ], [ 'adamant', { level: 70, experience: 62.5, item: { itemId: itemIds.daggers.adamant, amount: 1 }, ingredient: { itemId: itemIds.bars.adamantite, amount: 1 }, }, ], [ 'rune', { level: 85, experience: 75, item: { itemId: itemIds.daggers.rune, amount: 1 }, ingredient: { itemId: itemIds.bars.runite, amount: 1 }, }, ], ]), ], [ 'axe', new Map([ [ 'bronze', { level: 1, experience: 12.5, item: { itemId: itemIds.axes.bronze, amount: 1 }, ingredient: { itemId: itemIds.bars.bronze, amount: 1 }, }, ], [ 'iron', { level: 16, experience: 25, item: { itemId: itemIds.axes.iron, amount: 1 }, ingredient: { itemId: itemIds.bars.iron, amount: 1 }, }, ], [ 'steel', { level: 31, experience: 37.5, item: { itemId: itemIds.axes.steel, amount: 1 }, ingredient: { itemId: itemIds.bars.steel, amount: 1 }, }, ], [ 'mithril', { level: 51, experience: 50, item: { itemId: itemIds.axes.mithril, amount: 1 }, ingredient: { itemId: itemIds.bars.mithril, amount: 1 }, }, ], [ 'adamant', { level: 71, experience: 62.5, item: { itemId: itemIds.axes.adamantite, amount: 1 }, ingredient: { itemId: itemIds.bars.adamantite, amount: 1 }, }, ], [ 'rune', { level: 86, experience: 75, item: { itemId: itemIds.axes.runite, amount: 1 }, ingredient: { itemId: itemIds.bars.runite, amount: 1 }, }, ], ]), ], [ 'mace', new Map([ [ 'bronze', { level: 2, experience: 12.5, item: { itemId: itemIds.maces.bronze, amount: 1 }, ingredient: { itemId: itemIds.bars.bronze, amount: 1 }, }, ], [ 'iron', { level: 17, experience: 25, item: { itemId: itemIds.maces.iron, amount: 1 }, ingredient: { itemId: itemIds.bars.iron, amount: 1 }, }, ], [ 'steel', { level: 32, experience: 37.5, item: { itemId: itemIds.maces.steel, amount: 1 }, ingredient: { itemId: itemIds.bars.steel, amount: 1 }, }, ], [ 'mithril', { level: 52, experience: 50, item: { itemId: itemIds.maces.mithril, amount: 1 }, ingredient: { itemId: itemIds.bars.mithril, amount: 1 }, }, ], [ 'adamant', { level: 72, experience: 62.5, item: { itemId: itemIds.maces.adamantite, amount: 1 }, ingredient: { itemId: itemIds.bars.adamantite, amount: 1 }, }, ], [ 'rune', { level: 87, experience: 75, item: { itemId: itemIds.maces.runite, amount: 1 }, ingredient: { itemId: itemIds.bars.runite, amount: 1 }, }, ], ]), ], [ 'mediumHelm', new Map([ [ 'bronze', { level: 3, experience: 12.5, item: { itemId: itemIds.mediumHelmets.bronze, amount: 1 }, ingredient: { itemId: itemIds.bars.bronze, amount: 1 }, }, ], [ 'iron', { level: 18, experience: 25, item: { itemId: itemIds.mediumHelmets.iron, amount: 1 }, ingredient: { itemId: itemIds.bars.iron, amount: 1 }, }, ], [ 'steel', { level: 33, experience: 37.5, item: { itemId: itemIds.mediumHelmets.steel, amount: 1 }, ingredient: { itemId: itemIds.bars.steel, amount: 1 }, }, ], [ 'mithril', { level: 53, experience: 50, item: { itemId: itemIds.mediumHelmets.mithril, amount: 1 }, ingredient: { itemId: itemIds.bars.mithril, amount: 1 }, }, ], [ 'adamant', { level: 73, experience: 62.5, item: { itemId: itemIds.mediumHelmets.adamantite, amount: 1 }, ingredient: { itemId: itemIds.bars.adamantite, amount: 1 }, }, ], [ 'rune', { level: 88, experience: 75, item: { itemId: itemIds.mediumHelmets.runite, amount: 1 }, ingredient: { itemId: itemIds.bars.runite, amount: 1 }, }, ], ]), ], [ 'bolts', new Map([ [ 'bronze', { level: 3, experience: 12.5, item: { itemId: itemIds.bolts.bronze, amount: 15 }, ingredient: { itemId: itemIds.bars.bronze, amount: 1 }, }, ], [ 'iron', { level: 18, experience: 25, item: { itemId: itemIds.bolts.iron, amount: 15 }, ingredient: { itemId: itemIds.bars.iron, amount: 1 }, }, ], [ 'steel', { level: 33, experience: 37.5, item: { itemId: itemIds.bolts.steel, amount: 15 }, ingredient: { itemId: itemIds.bars.steel, amount: 1 }, }, ], [ 'mithril', { level: 53, experience: 50, item: { itemId: itemIds.bolts.mithril, amount: 15 }, ingredient: { itemId: itemIds.bars.mithril, amount: 1 }, }, ], [ 'adamant', { level: 73, experience: 62.5, item: { itemId: itemIds.bolts.adamantite, amount: 15 }, ingredient: { itemId: itemIds.bars.adamantite, amount: 1 }, }, ], [ 'rune', { level: 88, experience: 75, item: { itemId: itemIds.bolts.runite, amount: 15 }, ingredient: { itemId: itemIds.bars.runite, amount: 1 }, }, ], ]), ], [ 'sword', new Map([ [ 'bronze', { level: 4, experience: 12.5, item: { itemId: itemIds.swords.bronze, amount: 1 }, ingredient: { itemId: itemIds.bars.bronze, amount: 1 }, }, ], [ 'iron', { level: 19, experience: 25, item: { itemId: itemIds.swords.iron, amount: 1 }, ingredient: { itemId: itemIds.bars.iron, amount: 1 }, }, ], [ 'steel', { level: 34, experience: 37.5, item: { itemId: itemIds.swords.steel, amount: 1 }, ingredient: { itemId: itemIds.bars.steel, amount: 1 }, }, ], [ 'mithril', { level: 54, experience: 50, item: { itemId: itemIds.swords.mithril, amount: 1 }, ingredient: { itemId: itemIds.bars.mithril, amount: 1 }, }, ], [ 'adamant', { level: 74, experience: 62.5, item: { itemId: itemIds.swords.adamantite, amount: 1 }, ingredient: { itemId: itemIds.bars.adamantite, amount: 1 }, }, ], [ 'rune', { level: 89, experience: 75, item: { itemId: itemIds.swords.runite, amount: 1 }, ingredient: { itemId: itemIds.bars.runite, amount: 1 }, }, ], ]), ], [ 'dartTips', new Map([ [ 'bronze', { level: 4, experience: 12.5, item: { itemId: itemIds.dartTips.bronze, amount: 10, }, ingredient: { itemId: itemIds.bars.bronze, amount: 1, }, }, ], [ 'iron', { level: 19, experience: 25, item: { itemId: itemIds.dartTips.iron, amount: 10, }, ingredient: { itemId: itemIds.bars.iron, amount: 1, }, }, ], [ 'steel', { level: 34, experience: 37.5, item: { itemId: itemIds.dartTips.steel, amount: 10, }, ingredient: { itemId: itemIds.bars.steel, amount: 1, }, }, ], [ 'mithril', { level: 54, experience: 50, item: { itemId: itemIds.dartTips.mithril, amount: 10, }, ingredient: { itemId: itemIds.bars.mithril, amount: 1, }, }, ], [ 'adamant', { level: 74, experience: 62.5, item: { itemId: itemIds.dartTips.adamantite, amount: 10, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 1, }, }, ], [ 'rune', { level: 89, experience: 75, item: { itemId: itemIds.dartTips.runite, amount: 10, }, ingredient: { itemId: itemIds.bars.runite, amount: 1, }, }, ], ]), ], [ 'nails', new Map([ [ 'bronze', { level: 4, experience: 12.5, item: { itemId: itemIds.nails.bronze, amount: 10, }, ingredient: { itemId: itemIds.bars.bronze, amount: 1, }, }, ], [ 'iron', { level: 19, experience: 25, item: { itemId: itemIds.nails.iron, amount: 10, }, ingredient: { itemId: itemIds.bars.iron, amount: 1, }, }, ], [ 'steel', { level: 34, experience: 37.5, item: { itemId: itemIds.nails.steel, amount: 10, }, ingredient: { itemId: itemIds.bars.steel, amount: 1, }, }, ], [ 'mithril', { level: 54, experience: 50, item: { itemId: itemIds.nails.mithril, amount: 10, }, ingredient: { itemId: itemIds.bars.mithril, amount: 1, }, }, ], [ 'adamant', { level: 74, experience: 62.5, item: { itemId: itemIds.nails.adamantite, amount: 10, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 1, }, }, ], [ 'rune', { level: 89, experience: 75, item: { itemId: itemIds.nails.runite, amount: 10, }, ingredient: { itemId: itemIds.bars.runite, amount: 1, }, }, ], ]), ], [ 'scimitar', new Map([ [ 'bronze', { level: 5, experience: 25, item: { itemId: itemIds.scimitars.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 2, }, }, ], [ 'iron', { level: 20, experience: 50, item: { itemId: itemIds.scimitars.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 2, }, }, ], [ 'steel', { level: 35, experience: 75, item: { itemId: itemIds.scimitars.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 2, }, }, ], [ 'mithril', { level: 55, experience: 100, item: { itemId: itemIds.scimitars.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 2, }, }, ], [ 'adamant', { level: 75, experience: 125, item: { itemId: itemIds.scimitars.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 2, }, }, ], [ 'rune', { level: 90, experience: 150, item: { itemId: itemIds.scimitars.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 2, }, }, ], ]), ], [ 'spear', new Map([ [ 'bronze', { level: 5, experience: 25, item: { itemId: itemIds.spears.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 1, }, }, ], [ 'iron', { level: 20, experience: 25, item: { itemId: itemIds.spears.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 1, }, }, ], [ 'steel', { level: 35, experience: 37.5, item: { itemId: itemIds.spears.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 1, }, }, ], [ 'mithril', { level: 55, experience: 50, item: { itemId: itemIds.spears.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 1, }, }, ], [ 'adamant', { level: 75, experience: 62.5, item: { itemId: itemIds.spears.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 1, }, }, ], [ 'rune', { level: 90, experience: 75, item: { itemId: itemIds.spears.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 1, }, }, ], ]), ], [ 'arrowTips', new Map([ [ 'bronze', { level: 5, experience: 12.5, item: { itemId: itemIds.arrowTips.bronze, amount: 15, }, ingredient: { itemId: itemIds.bars.bronze, amount: 1, }, }, ], [ 'iron', { level: 20, experience: 25, item: { itemId: itemIds.arrowTips.iron, amount: 15, }, ingredient: { itemId: itemIds.bars.iron, amount: 1, }, }, ], [ 'steel', { level: 35, experience: 37.5, item: { itemId: itemIds.arrowTips.steel, amount: 15, }, ingredient: { itemId: itemIds.bars.steel, amount: 1, }, }, ], [ 'mithril', { level: 55, experience: 50, item: { itemId: itemIds.arrowTips.mithril, amount: 15, }, ingredient: { itemId: itemIds.bars.mithril, amount: 1, }, }, ], [ 'adamant', { level: 75, experience: 62.5, item: { itemId: itemIds.arrowTips.adamantite, amount: 15, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 1, }, }, ], [ 'rune', { level: 90, experience: 75, item: { itemId: itemIds.arrowTips.runite, amount: 15, }, ingredient: { itemId: itemIds.bars.runite, amount: 1, }, }, ], ]), ], [ 'limbs', new Map([ [ 'bronze', { level: 6, experience: 12.5, item: { itemId: itemIds.limbs.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 1, }, }, ], [ 'iron', { level: 23, experience: 25, item: { itemId: itemIds.limbs.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 1, }, }, ], [ 'steel', { level: 36, experience: 37.5, item: { itemId: itemIds.limbs.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 1, }, }, ], [ 'mithril', { level: 56, experience: 50, item: { itemId: itemIds.limbs.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 1, }, }, ], [ 'adamant', { level: 76, experience: 62.5, item: { itemId: itemIds.limbs.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 1, }, }, ], [ 'rune', { level: 91, experience: 75, item: { itemId: itemIds.limbs.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 1, }, }, ], ]), ], [ 'longsword', new Map([ [ 'bronze', { level: 6, experience: 25, item: { itemId: itemIds.longswords.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 2, }, }, ], [ 'iron', { level: 21, experience: 50, item: { itemId: itemIds.longswords.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 2, }, }, ], [ 'steel', { level: 36, experience: 75, item: { itemId: itemIds.longswords.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 2, }, }, ], [ 'mithril', { level: 56, experience: 100, item: { itemId: itemIds.longswords.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 2, }, }, ], [ 'adamant', { level: 76, experience: 125, item: { itemId: itemIds.longswords.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 2, }, }, ], [ 'rune', { level: 91, experience: 150, item: { itemId: itemIds.longswords.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 2, }, }, ], ]), ], [ 'fullHelm', new Map([ [ 'bronze', { level: 7, experience: 25, item: { itemId: itemIds.fullHelmets.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 2, }, }, ], [ 'iron', { level: 22, experience: 50, item: { itemId: itemIds.fullHelmets.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 2, }, }, ], [ 'steel', { level: 37, experience: 75, item: { itemId: itemIds.fullHelmets.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 2, }, }, ], [ 'mithril', { level: 57, experience: 100, item: { itemId: itemIds.fullHelmets.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 2, }, }, ], [ 'adamant', { level: 77, experience: 125, item: { itemId: itemIds.fullHelmets.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 2, }, }, ], [ 'rune', { level: 92, experience: 150, item: { itemId: itemIds.fullHelmets.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 2, }, }, ], ]), ], [ 'knife', new Map([ [ 'steel', { level: 37, experience: 37.5, item: { itemId: itemIds.throwingKnives.steel, amount: 5, }, ingredient: { itemId: itemIds.bars.steel, amount: 1, }, }, ], [ 'mithril', { level: 57, experience: 50, item: { itemId: itemIds.throwingKnives.mithril, amount: 5, }, ingredient: { itemId: itemIds.bars.mithril, amount: 1, }, }, ], [ 'adamant', { level: 77, experience: 62.5, item: { itemId: itemIds.throwingKnives.adamantite, amount: 5, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 1, }, }, ], [ 'rune', { level: 92, experience: 75, item: { itemId: itemIds.throwingKnives.runite, amount: 5, }, ingredient: { itemId: itemIds.bars.runite, amount: 1, }, }, ], ]), ], [ 'squareShield', new Map([ [ 'bronze', { level: 8, experience: 25, item: { itemId: itemIds.squareShields.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 2, }, }, ], [ 'iron', { level: 23, experience: 50, item: { itemId: itemIds.squareShields.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 2, }, }, ], [ 'steel', { level: 38, experience: 75, item: { itemId: itemIds.squareShields.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 2, }, }, ], [ 'mithril', { level: 58, experience: 100, item: { itemId: itemIds.squareShields.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 2, }, }, ], [ 'adamant', { level: 78, experience: 125, item: { itemId: itemIds.squareShields.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 2, }, }, ], [ 'rune', { level: 93, experience: 150, item: { itemId: itemIds.squareShields.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 2, }, }, ], ]), ], [ 'warhammer', new Map([ [ 'bronze', { level: 9, experience: 37.5, item: { itemId: itemIds.warhammers.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 3, }, }, ], [ 'iron', { level: 24, experience: 75, item: { itemId: itemIds.warhammers.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 3, }, }, ], [ 'steel', { level: 39, experience: 112.5, item: { itemId: itemIds.warhammers.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 3, }, }, ], [ 'mithril', { level: 59, experience: 150, item: { itemId: itemIds.warhammers.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 3, }, }, ], [ 'adamant', { level: 79, experience: 187.5, item: { itemId: itemIds.warhammers.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 3, }, }, ], [ 'rune', { level: 94, experience: 225, item: { itemId: itemIds.warhammers.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 3, }, }, ], ]), ], [ 'battleaxe', new Map([ [ 'bronze', { level: 9, experience: 37.5, item: { itemId: itemIds.battleAxes.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 3, }, }, ], [ 'iron', { level: 24, experience: 75, item: { itemId: itemIds.battleAxes.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 3, }, }, ], [ 'steel', { level: 39, experience: 112.5, item: { itemId: itemIds.battleAxes.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 3, }, }, ], [ 'mithril', { level: 59, experience: 150, item: { itemId: itemIds.battleAxes.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 3, }, }, ], [ 'adamant', { level: 79, experience: 187.5, item: { itemId: itemIds.battleAxes.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 3, }, }, ], [ 'rune', { level: 94, experience: 225, item: { itemId: itemIds.battleAxes.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 3, }, }, ], ]), ], [ 'chainbody', new Map([ [ 'bronze', { level: 11, experience: 37.5, item: { itemId: itemIds.chainbodies.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 3, }, }, ], [ 'iron', { level: 26, experience: 75, item: { itemId: itemIds.chainbodies.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 3, }, }, ], [ 'steel', { level: 41, experience: 112.5, item: { itemId: itemIds.chainbodies.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 3, }, }, ], [ 'mithril', { level: 61, experience: 150, item: { itemId: itemIds.chainbodies.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 3, }, }, ], [ 'adamant', { level: 81, experience: 187.5, item: { itemId: itemIds.chainbodies.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 3, }, }, ], [ 'rune', { level: 96, experience: 225, item: { itemId: itemIds.chainbodies.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 3, }, }, ], ]), ], [ 'kiteshield', new Map([ [ 'bronze', { level: 12, experience: 37.5, item: { itemId: itemIds.kiteshields.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 3, }, }, ], [ 'iron', { level: 27, experience: 75, item: { itemId: itemIds.kiteshields.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 3, }, }, ], [ 'steel', { level: 42, experience: 112.5, item: { itemId: itemIds.kiteshields.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 3, }, }, ], [ 'mithril', { level: 62, experience: 150, item: { itemId: itemIds.kiteshields.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 3, }, }, ], [ 'adamant', { level: 82, experience: 187.5, item: { itemId: itemIds.kiteshields.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 3, }, }, ], [ 'rune', { level: 97, experience: 225, item: { itemId: itemIds.kiteshields.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 3, }, }, ], ]), ], [ 'claws', new Map([ [ 'bronze', { level: 13, experience: 25, item: { itemId: itemIds.claws.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 2, }, }, ], [ 'iron', { level: 28, experience: 50, item: { itemId: itemIds.claws.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 2, }, }, ], [ 'steel', { level: 43, experience: 75, item: { itemId: itemIds.claws.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 2, }, }, ], [ 'mithril', { level: 63, experience: 100, item: { itemId: itemIds.claws.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 2, }, }, ], [ 'adamant', { level: 83, experience: 125, item: { itemId: itemIds.claws.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 2, }, }, ], [ 'rune', { level: 98, experience: 150, item: { itemId: itemIds.claws.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 2, }, }, ], ]), ], [ 'twoHandedSword', new Map([ [ 'bronze', { level: 14, experience: 37.5, item: { itemId: itemIds.twoHandSwords.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 3, }, }, ], [ 'iron', { level: 29, experience: 75, item: { itemId: itemIds.twoHandSwords.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 3, }, }, ], [ 'steel', { level: 44, experience: 112.5, item: { itemId: itemIds.twoHandSwords.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 3, }, }, ], [ 'mithril', { level: 64, experience: 150, item: { itemId: itemIds.twoHandSwords.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 3, }, }, ], [ 'adamant', { level: 84, experience: 187.5, item: { itemId: itemIds.twoHandSwords.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 3, }, }, ], [ 'rune', { level: 99, experience: 225, item: { itemId: itemIds.twoHandSwords.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 3, }, }, ], ]), ], [ 'platelegs', new Map([ [ 'bronze', { level: 16, experience: 37.5, item: { itemId: itemIds.platelegs.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 3, }, }, ], [ 'iron', { level: 31, experience: 75, item: { itemId: itemIds.platelegs.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 3, }, }, ], [ 'steel', { level: 46, experience: 112.5, item: { itemId: itemIds.platelegs.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 3, }, }, ], [ 'mithril', { level: 66, experience: 150, item: { itemId: itemIds.platelegs.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 3, }, }, ], [ 'adamant', { level: 86, experience: 187.5, item: { itemId: itemIds.platelegs.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 3, }, }, ], [ 'rune', { level: 99, experience: 225, item: { itemId: itemIds.platelegs.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 3, }, }, ], ]), ], [ 'plateskirt', new Map([ [ 'bronze', { level: 16, experience: 37.5, item: { itemId: itemIds.plateskirts.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 3, }, }, ], [ 'iron', { level: 31, experience: 75, item: { itemId: itemIds.plateskirts.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 3, }, }, ], [ 'steel', { level: 46, experience: 112.5, item: { itemId: itemIds.plateskirts.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 3, }, }, ], [ 'mithril', { level: 66, experience: 150, item: { itemId: itemIds.plateskirts.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 3, }, }, ], [ 'adamant', { level: 86, experience: 187.5, item: { itemId: itemIds.plateskirts.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 3, }, }, ], [ 'rune', { level: 99, experience: 225, item: { itemId: itemIds.plateskirts.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 3, }, }, ], ]), ], [ 'platebody', new Map([ [ 'bronze', { level: 16, experience: 37.5, item: { itemId: itemIds.platebodys.bronze, amount: 1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 5, }, }, ], [ 'iron', { level: 31, experience: 75, item: { itemId: itemIds.platebodys.iron, amount: 1, }, ingredient: { itemId: itemIds.bars.iron, amount: 5, }, }, ], [ 'steel', { level: 46, experience: 112.5, item: { itemId: itemIds.platebodys.steel, amount: 1, }, ingredient: { itemId: itemIds.bars.steel, amount: 5, }, }, ], [ 'mithril', { level: 66, experience: 150, item: { itemId: itemIds.platebodys.mithril, amount: 1, }, ingredient: { itemId: itemIds.bars.mithril, amount: 5, }, }, ], [ 'adamant', { level: 86, experience: 187.5, item: { itemId: itemIds.platebodys.adamantite, amount: 1, }, ingredient: { itemId: itemIds.bars.adamantite, amount: 5, }, }, ], [ 'rune', { level: 99, experience: 225, item: { itemId: itemIds.platebodys.runite, amount: 1, }, ingredient: { itemId: itemIds.bars.runite, amount: 5, }, }, ], ]), ], [ 'unknown', new Map([ [ 'any', { level: 1, experience: 0, item: { itemId: -1, amount: -1, }, ingredient: { itemId: itemIds.bars.bronze, amount: 1, }, }, ], ]), ], ]); /** * TODO (Jameskmonger) I ran a find-and-replace over this to stop TypeScript errors, recommend refactoring */ export const widgetItems: Map> = new Map>([ [ itemIds.bars.bronze, new Map([ [ 146, [ // Dagger, Sword, Scimitar, Longsword, 2h sword smithables.get('dagger')?.get('bronze') as Smithable, smithables.get('sword')?.get('bronze') as Smithable, smithables.get('scimitar')?.get('bronze') as Smithable, smithables.get('longsword')?.get('bronze') as Smithable, smithables.get('twoHandedSword')?.get('bronze') as Smithable, ], ], [ 147, [ // Axe, Mace, Warhammer, Battleaxe, Claws smithables.get('axe')?.get('bronze') as Smithable, smithables.get('mace')?.get('bronze') as Smithable, smithables.get('warhammer')?.get('bronze') as Smithable, smithables.get('battleaxe')?.get('bronze') as Smithable, smithables.get('claws')?.get('bronze') as Smithable, ], ], [ 148, [ // Chainbody, Platelegs, Plateskirt, Platebody, *Lantern* smithables.get('chainbody')?.get('bronze') as Smithable, smithables.get('platelegs')?.get('bronze') as Smithable, smithables.get('plateskirt')?.get('bronze') as Smithable, smithables.get('platebody')?.get('bronze') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], [ 149, [ // Medium helm, Full helm, Sq shield, kite shield, Nails smithables.get('mediumHelm')?.get('bronze') as Smithable, smithables.get('fullHelm')?.get('bronze') as Smithable, smithables.get('squareShield')?.get('bronze') as Smithable, smithables.get('kiteshield')?.get('bronze') as Smithable, smithables.get('nails')?.get('bronze') as Smithable, ], ], [ 150, [ // Dart tip, Arrowtips, Throwing knives, *Other*, *Studs* smithables.get('dartTips')?.get('bronze') as Smithable, smithables.get('arrowTips')?.get('bronze') as Smithable, smithables.get('unknown')?.get('any') as Smithable, smithables.get('unknown')?.get('any') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], [ 151, [ // Bolts, Limbs, Grapple tips smithables.get('bolts')?.get('bronze') as Smithable, smithables.get('limbs')?.get('bronze') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], ]), ], [ itemIds.bars.iron, new Map([ [ 146, [ // Dagger, Sword, Scimitar, Longsword, 2h sword smithables.get('dagger')?.get('iron') as Smithable, smithables.get('sword')?.get('iron') as Smithable, smithables.get('scimitar')?.get('iron') as Smithable, smithables.get('longsword')?.get('iron') as Smithable, smithables.get('twoHandedSword')?.get('iron') as Smithable, ], ], [ 147, [ // Axe, Mace, Warhammer, Battleaxe, Claws smithables.get('axe')?.get('iron') as Smithable, smithables.get('mace')?.get('iron') as Smithable, smithables.get('warhammer')?.get('iron') as Smithable, smithables.get('battleaxe')?.get('iron') as Smithable, smithables.get('claws')?.get('iron') as Smithable, ], ], [ 148, [ // Chainbody, Platelegs, Plateskirt, Platebody, *Lantern* smithables.get('chainbody')?.get('iron') as Smithable, smithables.get('platelegs')?.get('iron') as Smithable, smithables.get('plateskirt')?.get('iron') as Smithable, smithables.get('platebody')?.get('iron') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], [ 149, [ // Medium helm, Full helm, Sq shield, kite shield, Nails smithables.get('mediumHelm')?.get('iron') as Smithable, smithables.get('fullHelm')?.get('iron') as Smithable, smithables.get('squareShield')?.get('iron') as Smithable, smithables.get('kiteshield')?.get('iron') as Smithable, smithables.get('nails')?.get('iron') as Smithable, ], ], [ 150, [ // Dart tip, Arrowtips, Throwing knives, *Other*, *Studs* smithables.get('dartTips')?.get('iron') as Smithable, smithables.get('arrowTips')?.get('iron') as Smithable, smithables.get('unknown')?.get('any') as Smithable, smithables.get('unknown')?.get('any') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], [ 151, [ // Bolts, Limbs, Grapple tips smithables.get('bolts')?.get('iron') as Smithable, smithables.get('limbs')?.get('iron') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], ]), ], [ itemIds.bars.steel, new Map([ [ 146, [ // Dagger, Sword, Scimitar, Longsword, 2h sword smithables.get('dagger')?.get('steel') as Smithable, smithables.get('sword')?.get('steel') as Smithable, smithables.get('scimitar')?.get('steel') as Smithable, smithables.get('longsword')?.get('steel') as Smithable, smithables.get('twoHandedSword')?.get('steel') as Smithable, ], ], [ 147, [ // Axe, Mace, Warhammer, Battleaxe, Claws smithables.get('axe')?.get('steel') as Smithable, smithables.get('mace')?.get('steel') as Smithable, smithables.get('warhammer')?.get('steel') as Smithable, smithables.get('battleaxe')?.get('steel') as Smithable, smithables.get('claws')?.get('steel') as Smithable, ], ], [ 148, [ // Chainbody, Platelegs, Plateskirt, Platebody, *Lantern* smithables.get('chainbody')?.get('steel') as Smithable, smithables.get('platelegs')?.get('steel') as Smithable, smithables.get('plateskirt')?.get('steel') as Smithable, smithables.get('platebody')?.get('steel') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], [ 149, [ // Medium helm, Full helm, Sq shield, kite shield, Nails smithables.get('mediumHelm')?.get('steel') as Smithable, smithables.get('fullHelm')?.get('steel') as Smithable, smithables.get('squareShield')?.get('steel') as Smithable, smithables.get('kiteshield')?.get('steel') as Smithable, smithables.get('nails')?.get('steel') as Smithable, ], ], [ 150, [ // Dart tip, Arrowtips, Throwing knives, *Other*, *Studs* smithables.get('dartTips')?.get('steel') as Smithable, smithables.get('arrowTips')?.get('steel') as Smithable, smithables.get('knife')?.get('steel') as Smithable, smithables.get('unknown')?.get('any') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], [ 151, [ // Bolts, Limbs, Grapple tips smithables.get('bolts')?.get('steel') as Smithable, smithables.get('limbs')?.get('steel') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], ]), ], [ itemIds.bars.mithril, new Map([ [ 146, [ // Dagger, Sword, Scimitar, Longsword, 2h sword smithables.get('dagger')?.get('mithril') as Smithable, smithables.get('sword')?.get('mithril') as Smithable, smithables.get('scimitar')?.get('mithril') as Smithable, smithables.get('longsword')?.get('mithril') as Smithable, smithables.get('twoHandedSword')?.get('mithril') as Smithable, ], ], [ 147, [ // Axe, Mace, Warhammer, Battleaxe, Claws smithables.get('axe')?.get('mithril') as Smithable, smithables.get('mace')?.get('mithril') as Smithable, smithables.get('warhammer')?.get('mithril') as Smithable, smithables.get('battleaxe')?.get('mithril') as Smithable, smithables.get('claws')?.get('mithril') as Smithable, ], ], [ 148, [ // Chainbody, Platelegs, Plateskirt, Platebody, *Lantern* smithables.get('chainbody')?.get('mithril') as Smithable, smithables.get('platelegs')?.get('mithril') as Smithable, smithables.get('plateskirt')?.get('mithril') as Smithable, smithables.get('platebody')?.get('mithril') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], [ 149, [ // Medium helm, Full helm, Sq shield, kite shield, Nails smithables.get('mediumHelm')?.get('mithril') as Smithable, smithables.get('fullHelm')?.get('mithril') as Smithable, smithables.get('squareShield')?.get('mithril') as Smithable, smithables.get('kiteshield')?.get('mithril') as Smithable, smithables.get('nails')?.get('mithril') as Smithable, ], ], [ 150, [ // Dart tip, Arrowtips, Throwing knives, *Other*, *Studs* smithables.get('dartTips')?.get('mithril') as Smithable, smithables.get('arrowTips')?.get('mithril') as Smithable, smithables.get('knife')?.get('mithril') as Smithable, smithables.get('unknown')?.get('any') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], [ 151, [ // Bolts, Limbs, Grapple tips smithables.get('bolts')?.get('mithril') as Smithable, smithables.get('limbs')?.get('mithril') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], ]), ], [ itemIds.bars.adamantite, new Map([ [ 146, [ // Dagger, Sword, Scimitar, Longsword, 2h sword smithables.get('dagger')?.get('adamant') as Smithable, smithables.get('sword')?.get('adamant') as Smithable, smithables.get('scimitar')?.get('adamant') as Smithable, smithables.get('longsword')?.get('adamant') as Smithable, smithables.get('twoHandedSword')?.get('adamant') as Smithable, ], ], [ 147, [ // Axe, Mace, Warhammer, Battleaxe, Claws smithables.get('axe')?.get('adamant') as Smithable, smithables.get('mace')?.get('adamant') as Smithable, smithables.get('warhammer')?.get('adamant') as Smithable, smithables.get('battleaxe')?.get('adamant') as Smithable, smithables.get('claws')?.get('adamant') as Smithable, ], ], [ 148, [ // Chainbody, Platelegs, Plateskirt, Platebody, *Lantern* smithables.get('chainbody')?.get('adamant') as Smithable, smithables.get('platelegs')?.get('adamant') as Smithable, smithables.get('plateskirt')?.get('adamant') as Smithable, smithables.get('platebody')?.get('adamant') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], [ 149, [ // Medium helm, Full helm, Sq shield, kite shield, Nails smithables.get('mediumHelm')?.get('adamant') as Smithable, smithables.get('fullHelm')?.get('adamant') as Smithable, smithables.get('squareShield')?.get('adamant') as Smithable, smithables.get('kiteshield')?.get('adamant') as Smithable, smithables.get('nails')?.get('adamant') as Smithable, ], ], [ 150, [ // Dart tip, Arrowtips, Throwing knives, *Other*, *Studs* smithables.get('dartTips')?.get('adamant') as Smithable, smithables.get('arrowTips')?.get('adamant') as Smithable, smithables.get('knife')?.get('adamant') as Smithable, smithables.get('unknown')?.get('any') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], [ 151, [ // Bolts, Limbs, Grapple tips smithables.get('bolts')?.get('adamant') as Smithable, smithables.get('limbs')?.get('adamant') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], ]), ], [ itemIds.bars.runite, new Map([ [ 146, [ // Dagger, Sword, Scimitar, Longsword, 2h sword smithables.get('dagger')?.get('rune') as Smithable, smithables.get('sword')?.get('rune') as Smithable, smithables.get('scimitar')?.get('rune') as Smithable, smithables.get('longsword')?.get('rune') as Smithable, smithables.get('twoHandedSword')?.get('rune') as Smithable, ], ], [ 147, [ // Axe, Mace, Warhammer, Battleaxe, Claws smithables.get('axe')?.get('rune') as Smithable, smithables.get('mace')?.get('rune') as Smithable, smithables.get('warhammer')?.get('rune') as Smithable, smithables.get('battleaxe')?.get('rune') as Smithable, smithables.get('claws')?.get('rune') as Smithable, ], ], [ 148, [ // Chainbody, Platelegs, Plateskirt, Platebody, *Lantern* smithables.get('chainbody')?.get('rune') as Smithable, smithables.get('platelegs')?.get('rune') as Smithable, smithables.get('plateskirt')?.get('rune') as Smithable, smithables.get('platebody')?.get('rune') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], [ 149, [ // Medium helm, Full helm, Sq shield, kite shield, Nails smithables.get('mediumHelm')?.get('rune') as Smithable, smithables.get('fullHelm')?.get('rune') as Smithable, smithables.get('squareShield')?.get('rune') as Smithable, smithables.get('kiteshield')?.get('rune') as Smithable, smithables.get('nails')?.get('rune') as Smithable, ], ], [ 150, [ // Dart tip, Arrowtips, Throwing knives, *Other*, *Studs* smithables.get('dartTips')?.get('rune') as Smithable, smithables.get('arrowTips')?.get('rune') as Smithable, smithables.get('knife')?.get('rune') as Smithable, smithables.get('unknown')?.get('any') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], [ 151, [ // Bolts, Limbs, Grapple tips smithables.get('bolts')?.get('rune') as Smithable, smithables.get('limbs')?.get('rune') as Smithable, smithables.get('unknown')?.get('any') as Smithable, ], ], ]), ], ]); ================================================ FILE: src/plugins/skills/smithing/forging-task.ts ================================================ import { widgets } from '@engine/config/config-handler'; import { ActorTask } from '@engine/task/impl/actor-task'; import type { Player } from '@engine/world/actor/player/player'; import { Skill } from '@engine/world/actor/skills'; import type { Smithable } from './forging-types'; /** * A task that handles the forging of an item. * * Operates repeatedly every 4 ticks, and stops when the player has forged the amount they wanted. * * @author jameskmonger */ export class ForgingTask extends ActorTask { private elapsedTicks = 0; private amountForged = 0; constructor( player: Player, private readonly smithable: Smithable, private readonly amount: number, ) { super(player); } public execute(): void { const taskIteration = this.elapsedTicks++; // completed the task if we've forged the amount we wanted if (this.amountForged >= this.amount) { this.stop(); return; } // TODO (Jameskmonger) remove magic number this.actor.playAnimation(898); // only do something every 4 ticks if (taskIteration % 4 !== 0) { return; } // can't continue if (!this.hasMaterials()) { this.stop(); // TODO (Jameskmonger) send message return; } // Remove ingredients for (let i = 0; i < this.smithable.ingredient.amount; i++) { this.actor.inventory.removeFirst(this.smithable.ingredient.itemId); } // Add item to inventory this.actor.inventory.add({ itemId: this.smithable.item.itemId, amount: this.smithable.item.amount, }); this.actor.outgoingPackets.sendUpdateAllWidgetItems(widgets.inventory, this.actor.inventory); this.actor.skills.addExp(Skill.SMITHING, this.smithable.experience); this.amountForged++; } /** * Whether the player has the required materials to forge the item. * @returns {boolean} True if the player has the required materials, false otherwise. */ private hasMaterials() { return this.smithable.ingredient.amount <= this.actor.inventory.findAll(this.smithable.ingredient.itemId).length; } } ================================================ FILE: src/plugins/skills/smithing/forging-types.ts ================================================ import type { Item } from '@engine/world/items/item'; export interface Smithable { item: Item; level: number; experience: number; ingredient: Item; } ================================================ FILE: src/plugins/skills/smithing/forging.plugin.ts ================================================ import type { ItemInteractionActionHook } from '@engine/action/pipe/item-interaction.action'; import type { ItemOnObjectActionHook, itemOnObjectActionHandler } from '@engine/action/pipe/item-on-object.action'; import { widgets } from '@engine/config/config-handler'; import { findItem } from '@engine/config/config-handler'; import type { Player } from '@engine/world/actor/player/player'; import { Skill } from '@engine/world/actor/skills'; import { itemIds } from '@engine/world/config/item-ids'; import { Position } from '@engine/world/position'; import { anvilIds, bars, smithables, widgetItems } from '@plugins/skills/smithing/forging-constants'; import type { Smithable } from '@plugins/skills/smithing/forging-types'; import { logger } from '@runejs/common'; import { ForgingTask } from './forging-task'; /** * Get the item ids of all the smithable items, as a flat array. * * @param input A two-dimensional map of smithables, keyed by type and then by item id. * e.g. smithables.get('dagger').get('bronze') * @returns A flat array of item ids, e.g. [ bronze_dagger_id, iron_dagger_id, ...] * @remarks This is used to check if the player has the correct item in their inventory. */ const mapSmithableItemIdsToFlatArray = (input: Map>) => { const result: number[] = []; input.forEach(type => { type.forEach(smithable => { result.push(smithable.item.itemId); }); }); return result; }; /** * Flatten a two-dimensional map of Smithables into an array. * * TODO (Jameskmonger): this should not be done at runtime! At startup would be one thing, * but this happens in the `canActivate` method. * * @param input A two-dimensional map of smithables, keyed by type and then by item id. * e.g. smithables.get('dagger').get('bronze') * @returns A flat array of item ids, e.g. [ bronze_dagger_id, iron_dagger_id, ...] * @remarks This is used to check if the player has the correct item in their inventory. */ const mapSmithablesToFlatArray = (input: Map>) => { const results: Smithable[] = []; input.forEach(values => { values.forEach(value => { results.push(value); }); }); return results; }; /** * Lookup a smithable from just an item id. * @param itemId */ const findSmithableByItemId = (itemId: number): Smithable | null => { return ( mapSmithablesToFlatArray(smithables).find(smithable => { return smithable.item.itemId === itemId; }) || null ); }; /** * Check if the player is able to forge an item. */ const canForge = (player: Player, smithable: Smithable): boolean => { // In case the smithable doesn't exist. if (!smithable) { return false; } // Check if the player has the level required. if (smithable.level > player.skills.getLevel(Skill.SMITHING)) { const item = findItem(smithable.item.itemId); if (!item) { logger.error(`Could not find smithable item with id ${smithable.item.itemId}`); return false; } player.sendMessage(`You have to be at least level ${smithable.level} to smith ${item.name}s.`, true); return false; } // Check if the player has sufficient materials. if (!hasMaterials(player, smithable)) { const ingredient = findItem(smithable.ingredient.itemId); if (!ingredient) { logger.error(`Could not find smithable ingredient with id ${smithable.ingredient.itemId}`); return false; } player.sendMessage(`You don't have enough ${ingredient.name}s.`, true); return false; } player.interfaceState.closeAllSlots(); return true; }; /** * Checks if the player has enough materials * @param player * @param smithable */ const hasMaterials = (player: Player, smithable: Smithable) => { return smithable.ingredient.amount <= player.inventory.findAll(smithable.ingredient.itemId).length; }; /** * Opens the forging interface, and loads the items. * @param details */ const openForgingInterface: itemOnObjectActionHandler = details => { const { player, item, object } = details; const amountInInventory = player.inventory.findAll(item).length; player.face(new Position(object.x, object.y)); // The player does not have a hammer. if (!player.inventory.has(itemIds.hammer)) { player.sendMessage(`You need a hammer to work the metal with.`, true); return; } const barLevel = bars.get(item.itemId); if (barLevel === undefined) { logger.warn(`Could not find bar level for item id ${item.itemId}`); return; } const bar = findItem(item.itemId); if (barLevel === undefined || !bar) { logger.error(`Could not find bar with id ${item.itemId}`); return; } if (barLevel > player.skills.getLevel(Skill.SMITHING)) { player.sendMessage(`You have to be at least level ${barLevel} to smith ${bar.name}s.`, true); return; } player.outgoingPackets.updateClientConfig(210, amountInInventory); player.outgoingPackets.updateClientConfig(211, player.skills.getLevel(Skill.SMITHING)); details.player.interfaceState.openWidget(widgets.anvil.widgetId, { slot: 'screen', }); const barWidgetItems = widgetItems.get(item.itemId); if (barWidgetItems === undefined) { logger.warn(`Could not find bar widget items for item id ${item.itemId}`); return; } barWidgetItems.forEach((items, containerId) => { items.forEach((smithable, index) => { player.outgoingPackets.sendUpdateSingleWidgetItem( { widgetId: widgets.anvil.widgetId, containerId: containerId, }, index, smithable.item, ); }); }); }; export default { pluginId: 'rs:forging', hooks: [ { type: 'item_on_object', itemIds: [...bars.keys()], objectIds: anvilIds, walkTo: true, cancelOtherActions: true, handler: openForgingInterface, } as ItemOnObjectActionHook, { type: 'item_interaction', itemIds: [...mapSmithableItemIdsToFlatArray(smithables)], options: ['make', 'make-5', 'make-10'], cancelOtherActions: true, handler: ({ player, itemId, option }) => { const smithable = findSmithableByItemId(itemId); if (!smithable) { logger.error(`Could not find smithable with item id ${itemId}`); player.sendMessage('Could not find smithable, please tell a dev.'); return; } let wantedAmount = 0; switch (option) { case 'make': wantedAmount = 1; break; case 'make-5': wantedAmount = 5; break; case 'make-10': wantedAmount = 10; break; } if (!canForge(player, smithable)) { return; } player.enqueueTask(ForgingTask, [smithable, wantedAmount]); }, } as ItemInteractionActionHook, ], }; ================================================ FILE: src/plugins/skills/smithing/smelting-constants.ts ================================================ import { widgets } from '@engine/config/config-handler'; import { itemIds } from '@engine/world/config/item-ids'; import type { Bar, Smeltable } from '@plugins/skills/smithing/smelting-types'; const BRONZE: Bar = { barId: itemIds.bars.bronze, requiredLevel: 1, experience: 6.2, ingredients: [ { itemId: itemIds.ores.copper, amount: 1 }, { itemId: itemIds.ores.tin, amount: 1 }, ], }; const BLURITE: Bar = { barId: itemIds.bars.blurite, quest: 'theKnightsSword', requiredLevel: 8, experience: 8, ingredients: [{ itemId: itemIds.ores.blurite, amount: 1 }], }; const IRON: Bar = { barId: itemIds.bars.iron, requiredLevel: 15, experience: 12.5, ingredients: [{ itemId: itemIds.ores.iron, amount: 1 }], }; const SILVER: Bar = { barId: itemIds.bars.silver, requiredLevel: 20, experience: 13.6, ingredients: [{ itemId: itemIds.ores.silver, amount: 1 }], }; const STEEL: Bar = { barId: itemIds.bars.steel, requiredLevel: 30, experience: 17.5, ingredients: [ { itemId: itemIds.ores.iron, amount: 1 }, { itemId: itemIds.ores.coal, amount: 2 }, ], }; const GOLD: Bar = { barId: itemIds.bars.gold, requiredLevel: 40, experience: 22.5, ingredients: [{ itemId: itemIds.ores.gold, amount: 1 }], }; const MITHRIL: Bar = { barId: itemIds.bars.mithril, requiredLevel: 50, experience: 30, ingredients: [ { itemId: itemIds.ores.mithril, amount: 1 }, { itemId: itemIds.ores.coal, amount: 4 }, ], }; const ADAMANTITE: Bar = { barId: itemIds.bars.adamantite, requiredLevel: 70, experience: 37.5, ingredients: [ { itemId: itemIds.ores.adamantite, amount: 1 }, { itemId: itemIds.ores.coal, amount: 6 }, ], }; const RUNEITE: Bar = { barId: itemIds.bars.runite, requiredLevel: 85, experience: 50, ingredients: [ { itemId: itemIds.ores.runite, amount: 1 }, { itemId: itemIds.ores.coal, amount: 8 }, ], }; export const widgetItems = [ { slot: widgets.furnace.slots.slot1, bar: BLURITE }, { slot: widgets.furnace.slots.slot2, bar: IRON }, { slot: widgets.furnace.slots.slot3, bar: SILVER }, { slot: widgets.furnace.slots.slot4, bar: STEEL }, { slot: widgets.furnace.slots.slot5, bar: GOLD }, { slot: widgets.furnace.slots.slot6, bar: MITHRIL }, { slot: widgets.furnace.slots.slot7, bar: ADAMANTITE }, { slot: widgets.furnace.slots.slot8, bar: RUNEITE }, ]; /** * Defines the widget button ids. */ export const widgetButtonIds: Map = new Map([ [16, { takesInput: false, count: 1, bar: BRONZE }], [15, { takesInput: false, count: 5, bar: BRONZE }], [14, { takesInput: false, count: 10, bar: BRONZE }], [13, { takesInput: true, count: 0, bar: BRONZE }], [20, { takesInput: false, count: 1, bar: BLURITE }], [19, { takesInput: false, count: 5, bar: BLURITE }], [18, { takesInput: false, count: 10, bar: BLURITE }], [17, { takesInput: true, count: 0, bar: BLURITE }], [24, { takesInput: false, count: 1, bar: IRON }], [23, { takesInput: false, count: 5, bar: IRON }], [22, { takesInput: false, count: 10, bar: IRON }], [21, { takesInput: true, count: 0, bar: IRON }], [28, { takesInput: false, count: 1, bar: SILVER }], [27, { takesInput: false, count: 5, bar: SILVER }], [26, { takesInput: false, count: 10, bar: SILVER }], [25, { takesInput: true, count: 0, bar: SILVER }], [32, { takesInput: false, count: 1, bar: STEEL }], [31, { takesInput: false, count: 5, bar: STEEL }], [30, { takesInput: false, count: 10, bar: STEEL }], [29, { takesInput: true, count: 0, bar: STEEL }], [36, { takesInput: false, count: 1, bar: GOLD }], [35, { takesInput: false, count: 5, bar: GOLD }], [34, { takesInput: false, count: 10, bar: GOLD }], [33, { takesInput: true, count: 0, bar: GOLD }], [40, { takesInput: false, count: 1, bar: MITHRIL }], [39, { takesInput: false, count: 5, bar: MITHRIL }], [38, { takesInput: false, count: 10, bar: MITHRIL }], [37, { takesInput: true, count: 0, bar: MITHRIL }], [44, { takesInput: false, count: 1, bar: ADAMANTITE }], [43, { takesInput: false, count: 5, bar: ADAMANTITE }], [42, { takesInput: false, count: 10, bar: ADAMANTITE }], [41, { takesInput: true, count: 0, bar: ADAMANTITE }], [48, { takesInput: false, count: 1, bar: RUNEITE }], [47, { takesInput: false, count: 5, bar: RUNEITE }], [46, { takesInput: false, count: 10, bar: RUNEITE }], [45, { takesInput: true, count: 0, bar: RUNEITE }], ]); ================================================ FILE: src/plugins/skills/smithing/smelting-task.ts ================================================ import { findItem } from '@engine/config/config-handler'; import { ActorTask } from '@engine/task/impl/actor-task'; import type { Player } from '@engine/world/actor/player/player'; import { Skill } from '@engine/world/actor/skills'; import { animationIds } from '@engine/world/config/animation-ids'; import { soundIds } from '@engine/world/config/sound-ids'; import type { Smeltable } from './smelting-types'; /** * A task that handles the smelting of an item. * * Operates repeatedly every 3 ticks, and stops when the player has smelted the amount they wanted. * * @author jameskmonger */ export class SmeltingTask extends ActorTask { private elapsedTicks = 0; private amountSmelted = 0; constructor( player: Player, private readonly smeltable: Smeltable, private readonly amount: number, ) { super(player); } public execute(): void { const taskIteration = this.elapsedTicks++; // completed the task if we've smelted the amount we wanted if (this.amountSmelted >= this.amount) { this.stop(); return; } const bar = this.smeltable.bar; const barItem = findItem(bar.barId); if (!barItem) { this.actor.sendMessage(`Could not find item with id ${bar.barId}. Please tell a dev.`); this.stop(); return; } if (!this.hasMaterials()) { this.actor.sendMessage(`You don't have enough ${barItem.name.toLowerCase()}.`, true); this.stop(); return; } if (!this.hasLevel()) { this.actor.sendMessage(`You need a smithing level of ${bar.requiredLevel} to smelt ${barItem.name.toLowerCase()}s.`, true); return; } // Smelting takes 3 ticks for each item if (taskIteration % 3 !== 0) { return; } bar.ingredients.forEach(item => { for (let i = 0; i < item.amount; i++) { this.actor.removeFirstItem(item.itemId); } }); this.actor.giveItem(bar.barId); this.actor.skills.addExp(Skill.SMITHING, bar.experience); this.amountSmelted++; this.actor.playAnimation(animationIds.smelting); this.actor.outgoingPackets.playSound(soundIds.smelting, 5); } /** * Whether the player has the required materials to smelt the item. * @returns {boolean} True if the player has the required materials, false otherwise. */ private hasMaterials() { return this.smeltable.bar.ingredients.every(item => { const itemIndex = this.actor.inventory.findIndex(item); if (itemIndex === -1 || this.actor.inventory.amountInStack(itemIndex) < item.amount) { return false; } return true; }); } /** * Whether the player has the required level to smelt the item. * @returns {boolean} True if the player has the required level, false otherwise. */ private hasLevel() { return this.actor.skills.hasLevel(Skill.SMITHING, this.smeltable.bar.requiredLevel); } } ================================================ FILE: src/plugins/skills/smithing/smelting-types.ts ================================================ import type { Item } from '@engine/world/items/item'; export interface Bar { barId: number; quest?: string; requiredLevel: number; ingredients: Item[]; experience: number; } export interface Smeltable { takesInput: boolean; count: number; bar: Bar; } ================================================ FILE: src/plugins/skills/smithing/smelting.plugin.ts ================================================ import type { ButtonActionHook, buttonActionHandler } from '@engine/action/pipe/button.action'; import type { ObjectInteractionAction, ObjectInteractionActionHook, objectInteractionActionHandler, } from '@engine/action/pipe/object-interaction.action'; import { widgets } from '@engine/config/config-handler'; import { colors } from '@engine/util/colors'; import { Skill } from '@engine/world/actor/skills'; import { objectIds } from '@engine/world/config/object-ids'; import { widgetButtonIds, widgetItems } from '@plugins/skills/smithing/smelting-constants'; import { SmeltingTask } from './smelting-task'; export const openSmeltingInterface: objectInteractionActionHandler = details => { details.player.interfaceState.openWidget(widgets.furnace.widgetId, { slot: 'chatbox', }); loadSmeltingInterface(details); }; // We need to tell the widget what the bars actually look like. const loadSmeltingInterface = (details: ObjectInteractionAction) => { const theKnightsSwordQuest = details.player.quests.find(quest => quest.questId === 'theKnightsSword'); // Send the items to the widget. widgetItems.forEach(item => { details.player.outgoingPackets.setItemOnWidget(widgets.furnace.widgetId, item.slot.modelId, item.bar.barId, 125); if (!details.player.skills.hasLevel(Skill.SMITHING, item.bar.requiredLevel)) { details.player.modifyWidget(widgets.furnace.widgetId, { childId: item.slot.titleId, textColor: colors.red }); } else { details.player.modifyWidget(widgets.furnace.widgetId, { childId: item.slot.titleId, textColor: colors.black }); } // TODO (Jameskmonger) I don't think that this logic is correct.. it targets all items, not just those related to the quest. // Check if the player has completed 'The Knight's Sword' quest, even if the level is okay. if (Boolean(item.bar.quest) && (!theKnightsSwordQuest || theKnightsSwordQuest.complete)) { details.player.modifyWidget(widgets.furnace.widgetId, { childId: item.slot.titleId, textColor: colors.red }); } }); }; export const buttonClicked: buttonActionHandler = details => { // Check if player might be spawning widget clientside // TODO - this should be handled by the engine if (!details.player.interfaceState.findWidget(widgets.furnace.widgetId)) { return; } const smeltable = widgetButtonIds.get(details.buttonId); // TODO (Jameskmonger) check for quest-specific items, e.g. the knights sword // const theKnightsSwordQuest: PlayerQuest = details.player.quests.find(quest => quest.questId === 'theKnightsSword'); // if (bar.quest !== undefined && (theKnightsSwordQuest == undefined || theKnightsSwordQuest.complete)) { // details.player.sendMessage(`You need to complete The Knight's Sword quest first.`, true); // return; // } if (!smeltable) { details.player.sendMessage(`Could not find smeltable for button id ${details.buttonId}. Please tell a dev.`); return; } details.player.interfaceState.closeAllSlots(); if (!smeltable.takesInput) { details.player.enqueueTask(SmeltingTask, [smeltable, smeltable.count]); return; } const numericInputSpinSubscription = details.player.numericInputEvent.subscribe(number => { actionCancelledSpinSubscription?.unsubscribe(); numericInputSpinSubscription?.unsubscribe(); details.player.enqueueTask(SmeltingTask, [smeltable, number]); }); const actionCancelledSpinSubscription = details.player.actionsCancelled.subscribe(() => { actionCancelledSpinSubscription?.unsubscribe(); numericInputSpinSubscription?.unsubscribe(); }); details.player.outgoingPackets.showNumberInputDialogue(); }; export default { pluginId: 'rs:smelting', hooks: [ { type: 'object_interaction', objectIds: [objectIds.furnace, 11666], options: ['smelt'], walkTo: true, handler: openSmeltingInterface, } as ObjectInteractionActionHook, { type: 'button', widgetId: widgets.furnace.widgetId, buttonIds: Array.from(widgetButtonIds.keys()), handler: buttonClicked, } as ButtonActionHook, ], }; ================================================ FILE: src/plugins/skills/woodcutting/chance.ts ================================================ import { randomBetween } from '@engine/util/num'; import type { IHarvestable } from '@engine/world/config/harvestable-object'; /** * Roll a random number between 0 and 255 and compare it to the percent needed to cut the tree. * * @param tree The tree to cut * @param toolLevel The level of the axe being used * @param woodcuttingLevel The player's woodcutting level * * @returns True if the tree was successfully cut, false otherwise */ export const canCut = (tree: IHarvestable, toolLevel: number, woodcuttingLevel: number): boolean => { const successChance = randomBetween(0, 255); const percentNeeded = tree.baseChance + toolLevel + woodcuttingLevel; return successChance <= percentNeeded; }; ================================================ FILE: src/plugins/skills/woodcutting/index.ts ================================================ import type { ObjectInteractionActionHook } from '@engine/action/pipe/object-interaction.action'; import { getTreeIds } from '@engine/world/config/harvestable-object'; import { runWoodcuttingTask } from './woodcutting-task'; /** * Woodcutting plugin * * This uses the task system to schedule actions. */ export default { pluginId: 'rs:woodcutting', hooks: [ /** * "Chop down" / "chop" object interaction hook. */ { type: 'object_interaction', options: ['chop down', 'chop'], objectIds: getTreeIds(), handler: ({ player, object }) => { runWoodcuttingTask(player, object); }, } as ObjectInteractionActionHook, ], }; ================================================ FILE: src/plugins/skills/woodcutting/woodcutting-task.ts ================================================ import { findItem, findObject } from '@engine/config/config-handler'; import { ActorLandscapeObjectInteractionTask } from '@engine/task/impl/actor-landscape-object-interaction-task'; import { colors } from '@engine/util/colors'; import { randomBetween } from '@engine/util/num'; import { colorText } from '@engine/util/strings'; import { activeWorld } from '@engine/world'; import type { Player } from '@engine/world/actor/player/player'; import { Skill } from '@engine/world/actor/skills'; import type { IHarvestable } from '@engine/world/config/harvestable-object'; import { getTreeFromHealthy } from '@engine/world/config/harvestable-object'; import { soundIds } from '@engine/world/config/sound-ids'; import { rollBirdsNestType } from '@engine/world/skill-util/harvest-roll'; import { canInitiateHarvest } from '@engine/world/skill-util/harvest-skill'; import { logger } from '@runejs/common'; import type { LandscapeObject } from '@runejs/filestore'; import { canCut } from './chance'; class WoodcuttingTask extends ActorLandscapeObjectInteractionTask { /** * The tree being cut down. */ private treeInfo: IHarvestable; /** * The number of ticks that `execute` has been called inside this task. */ private elapsedTicks = 0; /** * Create a new woodcutting task. * * @param player The player that is attempting to cut down the tree. * @param landscapeObject The object that represents the tree. * @param sizeX The size of the tree in x axis. * @param sizeY The size of the tree in y axis. */ constructor(player: Player, landscapeObject: LandscapeObject, sizeX: number, sizeY: number) { super(player, landscapeObject, sizeX, sizeY); if (!landscapeObject) { this.stop(); return; } this.treeInfo = getTreeFromHealthy(landscapeObject.objectId); if (!this.treeInfo) { this.stop(); return; } } private getItemToAdd(): string | null { this.actor.sendMessage(`Looking for item ${this.treeInfo.items}`); if (typeof this.treeInfo.items === 'string') { return this.treeInfo.items; } // Handle weighted items const totalWeight = this.treeInfo.items.reduce((sum, item) => sum + item.weight, 0); let random = randomBetween(1, totalWeight); for (const item of this.treeInfo.items) { random -= item.weight; if (random <= 0) { return item.itemConfigId; } } return null; } /** * Execute the main woodcutting task loop. This method is called every game tick until the task is completed. * * As this task extends {@link ActorLandscapeObjectInteractionTask}, it's important that the * `super.execute` method is called at the start of this method. * * The base `execute` performs a number of checks that allow this task to function healthily. */ public execute(): void { super.execute(); if (!this.isActive || !this.landscapeObject) { return; } // store the tick count before incrementing so we don't need to keep track of it in all the separate branches const taskIteration = this.elapsedTicks++; const tool = canInitiateHarvest(this.actor, this.treeInfo, Skill.WOODCUTTING); if (!tool) { this.stop(); return; } if (taskIteration === 0) { this.actor.sendMessage('You swing your axe at the tree.'); this.actor.face(this.landscapeObjectPosition); this.actor.playAnimation(tool.animation); // First tick / iteration should never proceed beyond this point. return; } // play a random axe sound at the correct time if (taskIteration % 3 !== 0) { const randomSoundIdx = Math.floor(Math.random() * soundIds.axeSwing.length); this.actor.playSound(soundIds.axeSwing[randomSoundIdx], 7, 0); } // roll for success const succeeds = canCut(this.treeInfo, tool.level, this.actor.skills.woodcutting.level); if (!succeeds) { this.actor.playAnimation(tool.animation); // Keep chopping. return; } const itemConfigId = this.getItemToAdd(); if (!itemConfigId) { logger.error('Could not determine item to add from tree'); this.actor.sendMessage('Sorry, an error occurred. Please report this to a developer.'); this.stop(); return; } const logItem = findItem(itemConfigId); if (!logItem) { logger.error(`Could not find log item with id ${itemConfigId}`); this.actor.sendMessage('Sorry, an error occurred. Please report this to a developer.'); this.stop(); return; } const targetName = (logItem.name || '').toLowerCase(); // if player doesn't have space in inventory, stop the task if (!this.actor.inventory.hasSpace()) { this.actor.sendMessage(`Your inventory is too full to hold any more ${targetName}.`, true); this.actor.playSound(soundIds.inventoryFull); this.stop(); return; } const roll = randomBetween(1, 256); // roll for bird nest chance if (roll === 1) { this.actor.sendMessage(colorText(`A bird's nest falls out of the tree.`, colors.red)); activeWorld.globalInstance.spawnWorldItem(rollBirdsNestType(), this.actor.position, { owner: this.actor || null, expires: 300, }); } else { // Standard log chopper this.actor.sendMessage(`You manage to chop some ${targetName}.`); this.actor.giveItem(itemConfigId); } this.actor.skills.woodcutting.addExp(this.treeInfo.experience); // check if the tree should be broken if (randomBetween(0, 100) <= this.treeInfo.break) { // TODO (Jameskmonger) is this the correct sound? this.actor.playSound(soundIds.oreDepeleted); const brokenTreeId = this.treeInfo.objects.get(this.landscapeObject.objectId); if (brokenTreeId !== undefined) { this.actor.instance.replaceGameObject( brokenTreeId, this.landscapeObject, randomBetween(this.treeInfo.respawnLow, this.treeInfo.respawnHigh), ); } else { logger.error(`Could not find broken tree id for tree id ${this.landscapeObject.objectId}`); } this.stop(); } } /** * This method is called when the task stops. */ public onStop(): void { super.onStop(); this.actor.stopAnimation(); } } export function runWoodcuttingTask(player: Player, landscapeObject: LandscapeObject): void { const objectConfig = findObject(landscapeObject.objectId); if (!objectConfig) { logger.warn(`Player ${player.username} attempted to run a woodcutting task on an invalid object (id: ${landscapeObject.objectId})`); return; } const sizeX = objectConfig.rendering.sizeX; const sizeY = objectConfig.rendering.sizeY; player.enqueueTask(WoodcuttingTask, [landscapeObject, sizeX, sizeY]); } ================================================ FILE: src/server/game/game-server-config.ts ================================================ export interface GameServerConfig { rsaMod: string; rsaExp: string; host: string; port: number; encryptionEnabled: boolean; loginServerHost: string; loginServerPort: number; updateServerHost: string; updateServerPort: number; showWelcome: boolean; expRate: number; giveAchievements: boolean; checkCredentials: boolean; tutorialEnabled: boolean; adminDropsEnabled: boolean; bypassTeleportRequirements?: boolean; } ================================================ FILE: src/server/game/game-server-connection.ts ================================================ import type { Socket } from 'net'; import { handlePacket, incomingPackets } from '@engine/net/inbound-packet-handler'; import type { Player } from '@engine/world/actor/player/player'; import { logger } from '@runejs/common'; import { ByteBuffer } from '@runejs/common'; export class GameServerConnection { private activePacketId: number | null = null; private activePacketSize: number | null = null; private activeBuffer: ByteBuffer | null; public constructor( private readonly clientSocket: Socket, private readonly player: Player, ) {} public decodeMessage(buffer?: ByteBuffer): void | Promise { if (!this.activeBuffer) { if (!buffer) { logger.error(`No buffer provided to decodeMessage.`); return; } else { this.activeBuffer = buffer; } } else if (buffer) { const readable = this.activeBuffer.readable; const newBuffer = new ByteBuffer(readable + buffer.length); this.activeBuffer.copy(newBuffer, 0, this.activeBuffer.readerIndex); buffer.copy(newBuffer, readable, 0); this.activeBuffer = newBuffer; } if (this.activePacketId === null) { this.activePacketId = -1; } if (this.activePacketSize === null) { this.activePacketSize = -1; } const inCipher = this.player.inCipher; if (this.activePacketId === -1) { if (this.activeBuffer.readable < 1) { return; } this.activePacketId = this.activeBuffer.get('byte', 'u'); this.activePacketId = (this.activePacketId - inCipher.rand()) & 0xff; const incomingPacket = incomingPackets.get(this.activePacketId); if (incomingPacket) { this.activePacketSize = incomingPacket.size; } else { this.activePacketSize = -3; } } // Packet will provide the size if (this.activePacketSize === -1) { if (this.activeBuffer.readable < 1) { return; } this.activePacketSize = this.activeBuffer.get('byte', 'u'); } // Packet has no set size let clearBuffer = false; if (this.activePacketSize === -3) { if (this.activeBuffer.readable < 1) { return; } this.activePacketSize = this.activeBuffer.readable; clearBuffer = true; } if (this.activeBuffer.readable < this.activePacketSize) { return; } // read packet data let packetData: ByteBuffer | null = null; if (this.activePacketSize !== 0) { packetData = new ByteBuffer(this.activePacketSize); this.activeBuffer.copy(packetData, 0, this.activeBuffer.readerIndex, this.activeBuffer.readerIndex + this.activePacketSize); this.activeBuffer.readerIndex += this.activePacketSize; } if (packetData && !handlePacket(this.player, this.activePacketId, this.activePacketSize, packetData)) { logger.error( `Player packets out of sync for ${this.player.username}, resetting packet buffer...`, `If you're seeing this, there's a packet that needs fixing. :)`, ); clearBuffer = true; } if (clearBuffer) { this.activeBuffer = null; } this.activePacketId = null; this.activePacketSize = null; if (this.activeBuffer !== null && this.activeBuffer.readable > 0) { this.decodeMessage(); } } public connectionDestroyed(): void { logger.info(`Connection destroyed.`); this.player?.logout(); } public closeSocket(): void { this.clientSocket.destroy(); } } ================================================ FILE: src/server/game/game-server.ts ================================================ import { logger } from '@runejs/common'; import { SocketServer, parseServerConfig } from '@runejs/common/net'; import { Filestore } from '@runejs/filestore'; import { loadCoreConfigurations, loadGameConfigurations, xteaRegions } from '@engine/config/config-handler'; import { loadPackets } from '@engine/net/inbound-packet-handler'; import { watchForChanges, watchSource } from '@engine/util/files'; import { activateGameWorld } from '@engine/world'; import type { GameServerConfig } from '@server/game/game-server-config'; import { GatewayServer } from '@server/gateway/gateway-server'; /** * The singleton instance containing the server's active configuration settings. */ export let serverConfig: GameServerConfig; /** * The singleton instance referencing the game's asset file store. */ export let filestore: Filestore; export const openGatewayServer = (host: string, port: number): void => { SocketServer.launch('Game Gateway Server', host, port, socket => new GatewayServer(socket)); }; export async function setupConfig(): Promise { serverConfig = parseServerConfig(); if (!serverConfig) { logger.error('Unable to start server due to missing or invalid server configuration.'); return false; } await loadCoreConfigurations(); filestore = new Filestore('cache', { xteas: xteaRegions }); await loadGameConfigurations(); return true; } /** * Configures the game server, parses the asset file store, initializes the game world, * and finally spins up the game server itself. */ export async function launchGameServer(): Promise { const config = await setupConfig(); if (!config) { return; } await loadPackets(); const world = await activateGameWorld(); if (process.argv.indexOf('-fakePlayers') !== -1) { world.generateFakePlayers(); } openGatewayServer(serverConfig.host, serverConfig.port); watchSource('src/').subscribe(() => world.saveOnlinePlayers()); watchForChanges('dist/plugins/', /[/\\]plugins[/\\]/); } ================================================ FILE: src/server/gateway/gateway-server.ts ================================================ import type { Socket } from 'net'; import { createConnection } from 'net'; import { logger } from '@runejs/common'; import { ByteBuffer } from '@runejs/common'; import { SocketServer, parseServerConfig } from '@runejs/common/net'; import { LoginResponseCode } from '@runejs/login-server'; import { Isaac } from '@engine/net/isaac'; import { activeWorld } from '@engine/world'; import { Player } from '@engine/world/actor/player/player'; import type { GameServerConfig } from '@server/game/game-server-config'; import { GameServerConnection } from '@server/game/game-server-connection'; const serverConfig = parseServerConfig(); export type ServerType = 'game_server' | 'login_server' | 'update_server'; export class GatewayServer extends SocketServer { private serverType: ServerType; private gameServerConnection: GameServerConnection; private loginServerSocket: Socket; private updateServerSocket: Socket; private serverKey: bigint; public constructor(private readonly clientSocket: Socket) { super(clientSocket); } public initialHandshake(buffer: ByteBuffer): boolean { if (this.serverType) { this.decodeMessage(buffer); return true; } // First communication from the game client to the server gateway // Here we find out what kind of connection the client is making - game, or update server? // If game - they'll need to pass through the login server to authenticate first! const packetId = buffer.get('byte', 'u'); if (packetId === 15) { this.serverType = 'update_server'; this.updateServerSocket = createConnection({ host: serverConfig.updateServerHost, port: serverConfig.updateServerPort, }); this.updateServerSocket.on('data', data => this.clientSocket.write(data)); this.updateServerSocket.on('end', () => { logger.info(`Update server connection closed.`); }); this.updateServerSocket.on('error', () => { logger.error(`Update server error.`); }); this.updateServerSocket.setNoDelay(true); this.updateServerSocket.setKeepAlive(true); this.updateServerSocket.setTimeout(30000); } else if (packetId === 14) { this.serverType = 'login_server'; this.loginServerSocket = createConnection({ host: serverConfig.loginServerHost, port: serverConfig.loginServerPort, }); this.loginServerSocket.on('data', data => { this.parseLoginServerResponse(new ByteBuffer(data)); }); this.loginServerSocket.on('end', () => { logger.error(`Login server error.`); }); this.loginServerSocket.setNoDelay(true); this.loginServerSocket.setKeepAlive(true); this.loginServerSocket.setTimeout(30000); } else { logger.error(`Invalid initial client handshake packet id.`); return false; } const data = buffer.getSlice(1, buffer.length); const socket = this.serverType === 'login_server' ? this.loginServerSocket : this.updateServerSocket; socket.write(data); return true; } public decodeMessage(buffer: ByteBuffer): void | Promise { if (this.serverType === 'login_server') { this.loginServerSocket.write(buffer); } else if (this.serverType === 'update_server') { this.updateServerSocket.write(buffer); } else { this.gameServerConnection?.decodeMessage(buffer); } } public connectionDestroyed(): void { this.loginServerSocket?.destroy(); this.updateServerSocket?.destroy(); this.gameServerConnection?.connectionDestroyed(); } private async parseLoginServerResponse(buffer: ByteBuffer): Promise { if (!this.serverKey) { // Login handshake response const handshakeResponseCode = buffer.get('byte'); if (handshakeResponseCode === 0) { this.serverKey = BigInt(buffer.get('long')); } } else { // Login response const loginResponseCode = buffer.get('byte'); if (loginResponseCode === LoginResponseCode.SUCCESS) { try { const clientKey1 = buffer.get('int'); const clientKey2 = buffer.get('int'); const gameClientId = buffer.get('int'); const username = buffer.getString(); const passwordHash = buffer.getString(); const lowDetail = buffer.get('byte') === 1; if (activeWorld.playerOnline(username)) { // Player is already logged in! // @TODO move to login server buffer = new ByteBuffer(1); buffer.put(LoginResponseCode.ALREADY_LOGGED_IN); } else { this.serverType = 'game_server'; await this.createPlayer([clientKey1, clientKey2], gameClientId, username, passwordHash, lowDetail ? 'low' : 'high'); return; } } catch (e) { this.gameServerConnection?.closeSocket(); logger.error(e); } } } // Write the login server response back to the game client this.clientSocket.write(buffer); } private async createPlayer( clientKeys: [number, number], gameClientId: number, username: string, passwordHash: string, detail: 'high' | 'low', ): Promise { const sessionKey: number[] = [ Number(clientKeys[0]), Number(clientKeys[1]), Number(this.serverKey >> BigInt(32)), Number(this.serverKey), ]; const inCipher = new Isaac(sessionKey); for (let i = 0; i < 4; i++) { sessionKey[i] += 50; } const outCipher = new Isaac(sessionKey); const player = new Player(this.clientSocket, inCipher, outCipher, gameClientId, username, passwordHash, detail === 'low'); this.gameServerConnection = new GameServerConnection(this.clientSocket, player); activeWorld.registerPlayer(player); const outputBuffer = new ByteBuffer(6); outputBuffer.put(LoginResponseCode.SUCCESS, 'byte'); outputBuffer.put(player.rights.valueOf(), 'byte'); outputBuffer.put(0, 'byte'); // account flagged outputBuffer.put(player.worldIndex + 1, 'short'); outputBuffer.put(0, 'byte'); // membership status (for friends list count) this.clientSocket.write(outputBuffer); await player.init(); } } ================================================ FILE: src/server/runner.ts ================================================ import 'source-map-support/register'; import { initErrorHandling } from '@engine/util/error-handling'; import { activeWorld } from '@engine/world'; import { logger } from '@runejs/common'; import { launchLoginServer } from '@runejs/login-server'; import { launchUpdateServer } from '@runejs/update-server'; import { launchGameServer } from '@server/game/game-server'; const shutdownEvents = [ 'SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT', 'SIGBUS', 'SIGFPE', 'SIGUSR1', 'SIGSEGV', 'SIGUSR2', 'SIGTERM', ]; let running: boolean = true; let type: 'game' | 'login' | 'update' = 'game'; if (process.argv.indexOf('-login') !== -1) { type = 'login'; } else if (process.argv.indexOf('-update') !== -1) { type = 'update'; } shutdownEvents.forEach(signal => process.on(signal as any, () => { if (!running) { return; } running = false; logger.warn(`${signal} received.`); if (type === 'game') { activeWorld?.shutdown(); } logger.info(`${type.charAt(0).toUpperCase()}${type.substring(1)} Server shutting down...`); process.exit(0); }), ); initErrorHandling(); if (type === 'game') { launchGameServer(); } else if (type === 'login') { launchLoginServer(); } else if (type === 'update') { launchUpdateServer(); } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "noEmit": true, "module": "commonjs", "target": "ESNext", "jsx": "preserve", "importHelpers": true, "moduleResolution": "node", "experimentalDecorators": true, "esModuleInterop": true, "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "sourceMap": true, "strict": false, "strictBindCallApply": true, "strictFunctionTypes": false, "strictNullChecks": true, "strictPropertyInitialization": false, "baseUrl": ".", "allowJs": true, "outDir": "./dist", "paths": { "@engine/*": ["src/engine/*"], "@server/*": ["src/server/*"], "@plugins/*": ["src/plugins/*"] }, "types": ["jest", "node"], "lib": ["esnext", "dom", "dom.iterable", "scripthost"] }, "include": ["src/**/*.js", "src/**/*.ts", "tests/**/*.js", "tests/**/*.ts"], "exclude": ["node_modules"] }