diff --git a/data/config/npc-spawns/lumbridge/lumbridge-fishing.json b/data/config/npc-spawns/lumbridge/lumbridge-fishing.json new file mode 100644 index 000000000..60ddefb7a --- /dev/null +++ b/data/config/npc-spawns/lumbridge/lumbridge-fishing.json @@ -0,0 +1,18 @@ +[ + { + "npc": "rs:fishing_spot_bait_a", + "spawn_x": 3239, + "spawn_y": 3241, + "spawn_level": 0, + "movement_radius": 0, + "face": "WEST" + }, + { + "npc": "rs:fishing_spot_bait_a", + "spawn_x": 3239, + "spawn_y": 3244, + "spawn_level": 0, + "movement_radius": 0, + "face": "WEST" + } +] diff --git a/data/config/npcs/fishing.json b/data/config/npcs/fishing.json new file mode 100644 index 000000000..0c8ce9645 --- /dev/null +++ b/data/config/npcs/fishing.json @@ -0,0 +1,5 @@ +{ + "rs:fishing_spot_bait_a": { + "game_id": 233 + } +} diff --git a/src/engine/task/impl/actor-actor-interaction-task.ts b/src/engine/task/impl/actor-actor-interaction-task.ts new file mode 100644 index 000000000..209993558 --- /dev/null +++ b/src/engine/task/impl/actor-actor-interaction-task.ts @@ -0,0 +1,100 @@ +import { LandscapeObject } from '@runejs/filestore'; +import { activeWorld, Position } from '@engine/world'; +import { Actor } from '@engine/world/actor'; +import { ActorWalkToTask } from './actor-walk-to-task'; + +/** + * A task for an {@link Actor} to interact with another {@link Actor}. + * + * This task extends {@link ActorWalkToTask} and will walk the actor to the object. + * Once the actor is within range of the target Actor, the task will expose the `otherActor` property + * + * @author jameshallam + */ +export abstract class ActorActorInteractionTask extends ActorWalkToTask { + /* + * TODO (jameskmonger) consider exposing this, currently people must always access it through `otherActor` + * or through their own constructor + */ + private _targetActor: TOtherActor; + + /** + * Gets the {@link TOtherActor} that this task is interacting with. + * + * @returns The target actor, if is still present, and if the actor is at the destination. + * Otherwise, `null`. + * + * TODO (jameskmonger) unit test this + */ + protected get otherActor(): TOtherActor | null { + if (!this.atDestination) { + return null; + } + + if (!this._targetActor) { + return null; + } + + return this._targetActor; + } + + /** + * Get the position of this task's target npc + * + * @returns The position of this task's target npc, or `null` if the npc is not present + */ + protected get otherActorPosition(): Position { + if (!this._targetActor) { + return null; + } + return this._targetActor.position + } + + /** + * @param actor The actor executing this task. + * @param targetActor The `TOtherActor` to interact with. + * @param sizeX The size of the target TOtherActor in the X direction. + * @param sizeY The size of the target TOtherActor in the Y direction. + */ + constructor ( + actor: TActor, + targetActor: TOtherActor, + sizeX: number = 1, + sizeY: number = 1 + ) { + super( + actor, + // TODO (jameskmonger) this doesn't currently account for a moving NPC target + targetActor.position, + Math.max(sizeX, sizeY) + ); + + if (!targetActor) { + this.stop(); + return; + } + + this._targetActor = targetActor; + } + + /** + * Checks for the continued presence of the target {@link 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; + } + + // stop the task if the actor no longer exists + if (!this._targetActor) { + this.stop(); + return; + } + + // TODO: check npc still exists + } +} diff --git a/src/engine/task/impl/actor-landscape-object-interaction-task.ts b/src/engine/task/impl/actor-landscape-object-interaction-task.ts index 33455274f..935a82a9e 100644 --- a/src/engine/task/impl/actor-landscape-object-interaction-task.ts +++ b/src/engine/task/impl/actor-landscape-object-interaction-task.ts @@ -12,6 +12,10 @@ import { ActorWalkToTask } from './actor-walk-to-task'; * @author jameskmonger */ export abstract class ActorLandscapeObjectInteractionTask extends ActorWalkToTask { + /* + * TODO (jameskmonger) consider exposing this, currently people must always access it through `otherActor` + * or through their own constructor + */ private _landscapeObject: LandscapeObject; private _objectPosition: Position; @@ -24,9 +28,6 @@ export abstract class ActorLandscapeObjectInteractionTask= 0; i--) { + if (player.skills.hasLevel(Skill.FISHING, FishingRods[i].level)) { + if (player.hasItemOnPerson(FishingRods[i].itemId)) { + return FishingRods[i]; + } + } + } + return null; +} + + /** * Checks the players inventory and equipment for pickaxe * @param player @@ -63,6 +84,7 @@ export function getBestPickaxe(player: Player): HarvestTool | null { } return null; } + /** * Checks the players inventory and equipment for axe * @param player diff --git a/src/engine/world/skill-util/harvest-skill.ts b/src/engine/world/skill-util/harvest-skill.ts index bae772b29..7cc87a68e 100644 --- a/src/engine/world/skill-util/harvest-skill.ts +++ b/src/engine/world/skill-util/harvest-skill.ts @@ -2,7 +2,7 @@ import { Player } from '@engine/world/actor/player/player'; import { IHarvestable } from '@engine/world/config/harvestable-object'; import { soundIds } from '@engine/world/config/sound-ids'; import { Skill } from '@engine/world/actor/skills'; -import { getBestAxe, getBestPickaxe, HarvestTool } from '@engine/world/config/harvest-tool'; +import { getBestAxe, getBestPickaxe, HarvestTool, getFishingRod } from '@engine/world/config/harvest-tool'; import { randomBetween } from '@engine/util/num'; import { ObjectInteractionAction } from '@engine/action'; import { colors } from '@engine/util/colors'; @@ -18,19 +18,20 @@ import { loopingEvent } from '@engine/plugins'; * * @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 { +export function canInitiateHarvest(player: Player, target: Pick, skill: Skill): undefined | HarvestTool { if (!target) { switch (skill) { case Skill.MINING: player.sendMessage('There is current no ore available in this rock.'); + player.playSound(soundIds.oreEmpty, 7, 0); + break; + case Skill.FISHING: + player.sendMessage('There are no fish in that spot.'); break; default: player.sendMessage(colorText('HARVEST SKILL ERROR, PLEASE CONTACT DEVELOPERS', colors.red)); break; - - } - player.playSound(soundIds.oreEmpty, 7, 0); return; } @@ -39,6 +40,9 @@ export function canInitiateHarvest(player: Player, target: IHarvestable, skill: case Skill.MINING: targetName = targetName.replace(' ore', ''); break; + case Skill.FISHING: + targetName = 'fish'; + break; } @@ -48,27 +52,41 @@ export function canInitiateHarvest(player: Player, target: IHarvestable, skill: case Skill.MINING: player.sendMessage(`You need a Mining level of ${target.level} to mine this rock.`, true); break; + case Skill.FISHING: + player.sendMessage(`You need a Fishing level of ${target.level} to fish at this spot.`); + break; case Skill.WOODCUTTING: player.sendMessage(`You need a Woodcutting level of ${target.level} to chop down this tree.`, true); break; } return; } + // Check the players equipment and inventory for a tool let tool; switch (skill) { case Skill.MINING: tool = getBestPickaxe(player); break; + case Skill.FISHING: + // TODO (jameshallam93): different spots need different equipment + tool = getFishingRod(player); + break; case Skill.WOODCUTTING: tool = getBestAxe(player); break; } + + // TODO (jameshallam93): some activities need more than one tool, e.g. bait + if (tool == null) { switch (skill) { case Skill.MINING: player.sendMessage('You do not have a pickaxe for which you have the level to use.'); break; + case Skill.FISHING: + player.sendMessage('You do not have a fishing rod for which you have the level to use.'); + break; case Skill.WOODCUTTING: player.sendMessage('You do not have an axe for which you have the level to use.'); break; @@ -88,9 +106,11 @@ export function canInitiateHarvest(player: Player, target: IHarvestable, skill: export function handleHarvesting(details: ObjectInteractionAction, tool: HarvestTool, target: IHarvestable, skill: Skill): void { let itemToAdd = target.itemId; + // This is rune essence to pure essence if (itemToAdd === 1436 && details.player.skills.hasLevel(Skill.MINING, 30)) { itemToAdd = 7936; } + // This is to deal with gem rocks if (details.object.objectId === 2111 && details.player.skills.hasLevel(Skill.MINING, 30)) { itemToAdd = rollGemRockResult().itemId; } @@ -106,6 +126,9 @@ export function handleHarvesting(details: ObjectInteractionAction, tool: Harvest case Skill.MINING: details.player.sendMessage('You swing your pick at the rock.'); break; + case Skill.FISHING: + details.player.sendMessage('You cast your line out.'); + break; case Skill.WOODCUTTING: details.player.sendMessage('You swing your axe at the tree.'); break; diff --git a/src/plugins/skills/fishing/fishing-task.ts b/src/plugins/skills/fishing/fishing-task.ts new file mode 100644 index 000000000..678b300f8 --- /dev/null +++ b/src/plugins/skills/fishing/fishing-task.ts @@ -0,0 +1,93 @@ +import { Skill } from '@engine/world/actor/skills'; +import { Player, Npc } from '@engine/world/actor'; +import { soundIds } from '@engine/world/config/sound-ids'; +import { findItem, findNpc, findObject } from '@engine/config/config-handler'; +import { activeWorld } from '@engine/world'; +import { ActorActorInteractionTask } from '@engine/task/impl'; +import { LandscapeObject } from '@runejs/filestore'; +import { logger } from '@runejs/common'; +import { IHarvestable } from '@engine/world/config'; +import { canInitiateHarvest } from '@engine/world/skill-util/harvest-skill'; +import { randomBetween } from '@engine/util'; + + +class FishingTask extends ActorActorInteractionTask{ + + private elapsedTicks = 0; + + constructor( + player: Player, + fishingSpot: Npc, + ) { + super( + player, + fishingSpot, + 1, + 1 + ); + + + if (!fishingSpot) { + this.stop(); + return; + } + + } + public execute(): void { + super.execute() + + if (!this.isActive || !this.otherActor) { + 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++; + + // TODO wire this up into fishing spot config + const fishingSpotInfo: Pick = { + itemId: 335, + + // TODO change this too + level: 1 + } + + const tool = canInitiateHarvest(this.actor, fishingSpotInfo, Skill.FISHING); + + if (!tool) { + this.stop(); + return; + } + if(taskIteration === 0) { + this.actor.sendMessage('You swing your axe at the tree.'); + this.actor.face(this.otherActor); + this.actor.playAnimation(tool.animation); + return; + } + const roll = randomBetween(1, 256) + if(roll > 200){ + this.actor.giveItem(335) + } + + this.actor.sendMessage('Doing a fish with a lovely long ' + findItem(tool.itemId).name) + this.actor.playAnimation(tool.animation) + } + public onStop(): void { + super.onStop(); + + this.actor.sendMessage('=====Stopped fishing=======') + } +} + + +export function runFishingTask(player: Player, npc: Npc): void { + // const npcConfig = findNpc(npc.id); + + // if (!npcConfig) { + // logger.warn(`Player ${player.username} attempted to run a fishing task on an invalid object (id: ${npc.id})`); + // return; + // } + + const sizeX = 1 + const sizeY = 1 + player.enqueueTask(FishingTask, [ npc, sizeX, sizeY ]); +} diff --git a/src/plugins/skills/fishing/index.ts b/src/plugins/skills/fishing/index.ts new file mode 100644 index 000000000..2e60e6162 --- /dev/null +++ b/src/plugins/skills/fishing/index.ts @@ -0,0 +1,26 @@ +import { + NpcInteractionActionHook, +} from '@engine/action'; +import { runFishingTask } from './fishing-task'; + +/** + * Fishing plugin + * + * This uses the task system to schedule actions. + */ +export default { + pluginId: 'rs:fishing', + hooks: [ + /** + * "Chop down" / "chop" object interaction hook. + */ + { + type: 'npc_interaction', + options: 'bait' , + npcs: 'rs:fishing_spot_bait_a', + handler: ({ player, npc }) => { + runFishingTask(player, npc); + } + } as NpcInteractionActionHook + ] +};