diff --git a/src/world/mob/player/action/dialogue/dialogue-action.ts b/src/world/mob/player/action/dialogue/dialogue-action.ts index 32ca4eee3..b3c33d16c 100644 --- a/src/world/mob/player/action/dialogue/dialogue-action.ts +++ b/src/world/mob/player/action/dialogue/dialogue-action.ts @@ -1,9 +1,17 @@ import { Player } from '@server/world/mob/player/player'; import { gameCache } from '@server/game-server'; +import { Npc } from '@server/world/mob/npc/npc'; const interfaceIds = { PLAYER: [ 968, 973, 979, 986 ], - NPC: [ 4882, 4887, 4893, 4900 ] + NPC: [ 4882, 4887, 4893, 4900 ], + OPTIONS: [ 2459, 2469, 2480, 2492 ] +}; + +const lineConstraints = { + PLAYER: [ 1, 4 ], + NPC: [ 1, 4 ], + OPTIONS: [ 2, 5 ] }; export enum DialogueEmote { @@ -39,57 +47,105 @@ export enum DialogueEmote { ANGRY_4 = 617 } -export type DialogueType = 'PLAYER' | 'NPC'; +export type DialogueType = 'PLAYER' | 'NPC' | 'OPTIONS'; export interface DialogueOptions { type: DialogueType; npc?: number; emote?: DialogueEmote; + title?: string; lines: string[]; } -export const closeDialogue = (player: Player): void => { - player.packetSender.closeActiveInterfaces(); -}; +export class DialogueAction { + + private _action: number = null; + + public constructor(private readonly p: Player) { + } -export const dialogueAction = (player: Player, options: DialogueOptions): Promise => { - if(options.lines.length === 0 || options.lines.length > 4) { - throw 'Invalid line length.'; + public player(emote: DialogueEmote, lines: string[]): Promise { + return this.dialogue({ emote, lines, type: 'PLAYER' }); } - if(options.type === 'NPC' && options.npc === undefined) { - throw 'NPC not supplied.'; + public npc(npc: Npc, emote: DialogueEmote, lines: string[]): Promise { + return this.dialogue({ emote, lines, type: 'NPC', npc: npc.id }); } - const interfaceId = interfaceIds[options.type][options.lines.length - 1]; - let textOffset = 1; + public options(title: string, options: string[]): Promise { + return this.dialogue({ type: 'OPTIONS', title, lines: options }); + } + + public dialogue(options: DialogueOptions): Promise { + if(options.lines.length < lineConstraints[options.type][0] || options.lines.length > lineConstraints[options.type][1]) { + throw 'Invalid line length.'; + } - if(options.type === 'PLAYER' || options.type === 'NPC') { - if(!options.emote) { - options.emote = DialogueEmote.DEFAULT; + if(options.type === 'NPC' && options.npc === undefined) { + throw 'NPC not supplied.'; } - if(options.type === 'NPC') { - player.packetSender.setInterfaceModel2(interfaceId + 1, options.npc); - player.packetSender.updateInterfaceString(interfaceId + 2, gameCache.npcDefinitions.get(options.npc).name); - } else if(options.type === 'PLAYER') { - // @TODO - player.packetSender.updateInterfaceString(interfaceId + 2, player.username); + this._action = null; + + let interfaceIndex = options.lines.length - 1; + if(options.type === 'OPTIONS') { + interfaceIndex--; } - player.packetSender.playInterfaceAnimation(interfaceId + 1, options.emote); - textOffset += 2; + const interfaceId = interfaceIds[options.type][interfaceIndex]; + let textOffset = 1; + + if(options.type === 'PLAYER' || options.type === 'NPC') { + if(!options.emote) { + options.emote = DialogueEmote.DEFAULT; + } + + if(options.type === 'NPC') { + this.p.packetSender.setInterfaceModel2(interfaceId + 1, options.npc); + this.p.packetSender.updateInterfaceString(interfaceId + 2, gameCache.npcDefinitions.get(options.npc).name); + } else if(options.type === 'PLAYER') { + this.p.packetSender.setInterfacePlayerHead(interfaceId + 1); + this.p.packetSender.updateInterfaceString(interfaceId + 2, this.p.username); + } + + this.p.packetSender.playInterfaceAnimation(interfaceId + 1, options.emote); + textOffset += 2; + } else if(options.type === 'OPTIONS') { + this.p.packetSender.updateInterfaceString(interfaceId + 1, options.title); + textOffset += 1; + } + + for(let i = 0; i < options.lines.length; i++) { + this.p.packetSender.updateInterfaceString(interfaceId + textOffset + i, options.lines[i]); + } + + this.p.packetSender.showChatboxInterface(interfaceId); + + return new Promise(resolve => { + this.p.dialogueInteractionEvent.subscribe(action => { + this._action = action; + resolve(this); + }) + }); } - for(let i = 0; i < options.lines.length; i++) { - player.packetSender.updateInterfaceString(interfaceId + textOffset + i, options.lines[i]); + public close(): void { + this.p.packetSender.closeActiveInterfaces(); } - player.packetSender.showChatboxInterface(interfaceId); + public get action(): number { + return this._action; + } - return new Promise(resolve => { - player.dialogueInteractionEvent.subscribe(value => { - resolve(value); - }) - }); + public set action(value: number) { + this._action = value; + } +} + +export const dialogueAction = (player: Player, options?: DialogueOptions): Promise => { + if(options) { + return new DialogueAction(player).dialogue(options); + } else { + return Promise.resolve(new DialogueAction(player)); + } }; diff --git a/src/world/mob/player/action/input-command-action.ts b/src/world/mob/player/action/input-command-action.ts index dc6406dee..bd1a7ab8e 100644 --- a/src/world/mob/player/action/input-command-action.ts +++ b/src/world/mob/player/action/input-command-action.ts @@ -2,7 +2,7 @@ import { Player } from '../player'; import { logger } from '@runejs/logger/dist/logger'; import { world } from '@server/game-server'; import { interfaceIds } from '../game-interface'; -import { closeDialogue, dialogueAction, DialogueEmote } from '@server/world/mob/player/action/dialogue/dialogue-action'; +import { npcAction } from '@server/world/mob/player/action/npc-action'; type commandHandler = (player: Player, args?: string[]) => void; @@ -69,9 +69,21 @@ const commands: { [key: string]: commandHandler } = { }, chat: (player: Player) => { - dialogueAction(player, { type: 'NPC', emote: DialogueEmote.CALM_TALK_1, npc: 0, lines: [ 'Welcome to RuneScape!' ] }) - .then(() => dialogueAction(player, { type: 'NPC', emote: DialogueEmote.CALM_TALK_2, npc: 0, lines: [ 'How do you feel about Rune.JS so far?', 'Let us know what you think!' ] })) - .then(() => closeDialogue(player)); + npcAction(player, world.npcList[0]); + }, + + chati: (player: Player, args: string[]) => { + if(args.length !== 1) { + throw `chati interfaceId`; + } + + const interfaceId: number = parseInt(args[0]); + + if(isNaN(interfaceId)) { + throw `chati interfaceId`; + } + + player.packetSender.showChatboxInterface(interfaceId); } }; diff --git a/src/world/mob/player/action/npc-action.ts b/src/world/mob/player/action/npc-action.ts new file mode 100644 index 000000000..cd4d7984f --- /dev/null +++ b/src/world/mob/player/action/npc-action.ts @@ -0,0 +1,44 @@ +import { Player } from '@server/world/mob/player/player'; +import { Npc } from '@server/world/mob/npc/npc'; +import { dialogueAction, DialogueEmote } from '@server/world/mob/player/action/dialogue/dialogue-action'; + +export const npcAction = (player: Player, npc: Npc): void => { + if(npc.id === 0) { + dialogueAction(player, { type: 'NPC', emote: DialogueEmote.CALM_TALK_1, npc: 0, lines: [ 'Welcome to RuneScape!' ] }) + .then(d => d.npc(npc, DialogueEmote.CALM_TALK_2, [ 'How do you feel about Rune.JS so far?', 'Please take a moment to let us know what you think!' ])) + .then(d => d.options('Thoughts?', [ 'Love it!', 'Kind of cool.', `Eh, I don't know...`, `Not my cup of tea, honestly.`, `It's literally the worst.` ])) + .then(d => { + switch(d.action) { + case 1: + return d.player(DialogueEmote.JOYFUL, [ 'Loving it so far, thanks for asking!' ]) + .then(d => d.npc(npc, DialogueEmote.JOYFUL, [ `You're very welcome! Glad to hear it.` ])); + case 2: + return d.player(DialogueEmote.DEFAULT, [ `It's kind of cool, I guess.`, 'Bit of a weird gimmick.' ]) + .then(d => d.npc(npc, DialogueEmote.DEFAULT, [ `Please let us know if you have any suggestions.` ])); + case 3: + return d.player(DialogueEmote.NOT_INTERESTED, [ `Ehhh... I don't know...` ]) + .then(d => d.npc(npc, DialogueEmote.CALM_TALK_1, [ `We're always open to feedback or`, `Pull Requests anytime you like.` ])) + .then(d => d.player(DialogueEmote.CALM_TALK_1, [ `I'll keep that in mind, thanks.` ])); + case 4: + return d.player(DialogueEmote.CALM_TALK_2, [ `Not really my cup of tea, but keep at it.` ]) + .then(d => d.npc(npc, DialogueEmote.JOYFUL, [ `Thanks for the support!` ])); + case 5: + return d.player(DialogueEmote.ANGRY_1, [ `Literally the worst thing I've ever seen.`, 'You disgust me on a personal level.' ]) + .then(d => d.npc(npc, DialogueEmote.SAD_3, [ `I-is that so?...`, `Well I'm... I'm sorry to hear that.` ])) + .then(d => { + d.action = 1; + return d; + }); + } + }) + .then(d => { + d.close(); + + if(d.action === 1) { + player.packetSender.chatboxMessage('Hans wanders off rather dejectedly.'); + } else { + player.packetSender.chatboxMessage('Hans wanders off aimlessly through the courtyard.'); + } + }); + } +}; diff --git a/src/world/mob/player/packet/impl/button-click-packet.ts b/src/world/mob/player/packet/impl/button-click-packet.ts index 9eb72a9fe..5eeea3a01 100644 --- a/src/world/mob/player/packet/impl/button-click-packet.ts +++ b/src/world/mob/player/packet/impl/button-click-packet.ts @@ -18,6 +18,13 @@ const settingButtons: number[] = [ 150, 151, // auto retaliate ]; +const dialogueActions: { [key: number]: number } = { + 2494: 1, 2495: 2, 2496: 3, 2497: 4, 2498: 5, + 2482: 1, 2483: 2, 2484: 3, 2485: 4, + 2471: 1, 2472: 2, 2473: 3, + 2461: 1, 2462: 2 +}; + export const buttonClickPacket: incomingPacket = (player: Player, packetId: number, packetSize: number, packet: RsBuffer): void => { const buttonId = packet.readShortBE(); @@ -25,6 +32,8 @@ export const buttonClickPacket: incomingPacket = (player: Player, packetId: numb player.logout(); } else if(settingButtons.indexOf(buttonId) !== -1) { player.settingChanged(buttonId); + } else if(dialogueActions.hasOwnProperty(buttonId)) { + player.dialogueInteractionEvent.next(dialogueActions[buttonId]); } else if(ignoreButtons.indexOf(buttonId) === -1) { console.log(`Unhandled button ${buttonId} clicked.`); } diff --git a/src/world/mob/player/packet/impl/npc-interaction-packet.ts b/src/world/mob/player/packet/impl/npc-interaction-packet.ts new file mode 100644 index 000000000..cbbfa283a --- /dev/null +++ b/src/world/mob/player/packet/impl/npc-interaction-packet.ts @@ -0,0 +1,32 @@ +import { incomingPacket } from '../incoming-packet'; +import { Player } from '../../player'; +import { RsBuffer } from '@server/net/rs-buffer'; +import { world } from '@server/game-server'; +import { World } from '@server/world/world'; +import { npcAction } from '@server/world/mob/player/action/npc-action'; + +export const npcInteractionPacket: incomingPacket = (player: Player, packetId: number, packetSize: number, packet: RsBuffer): void => { + const npcIndex = packet.readUnsignedShortLE(); + + if(npcIndex < 0 || npcIndex > World.MAX_NPCS - 1) { + return; + } + + const npc = world.npcList[npcIndex]; + if(!npc) { + return; + } + + const distance = Math.floor(npc.position.distanceBetween(player.position)); + + if(distance > 16) { + return; + } + + if(distance === 1) { + npcAction(player, npc); + } else { + // @TODO wait for the player to finish walking to their target + npcAction(player, npc); + } +}; diff --git a/src/world/mob/player/packet/incoming-packet-directory.ts b/src/world/mob/player/packet/incoming-packet-directory.ts index 590511e56..97fe931b1 100644 --- a/src/world/mob/player/packet/incoming-packet-directory.ts +++ b/src/world/mob/player/packet/incoming-packet-directory.ts @@ -13,6 +13,7 @@ import { itemOption1Packet } from './impl/item-option-1-packet'; import { commandPacket } from './impl/command-packet'; import { itemSwapPacket } from './impl/item-swap-packet'; import { dialogueInteractionPacket } from '@server/world/mob/player/packet/impl/dialogue-interaction-packet'; +import { npcInteractionPacket } from '@server/world/mob/player/packet/impl/npc-interaction-packet'; const packets: { [key: number]: incomingPacket } = { 19: interfaceClickPacket, @@ -21,6 +22,8 @@ const packets: { [key: number]: incomingPacket } = { 79: buttonClickPacket, 226: dialogueInteractionPacket, + 112: npcInteractionPacket, + 28: walkPacket, 213: walkPacket, 247: walkPacket, diff --git a/src/world/mob/player/packet/packet-sender.ts b/src/world/mob/player/packet/packet-sender.ts index 002c294ee..9155a318b 100644 --- a/src/world/mob/player/packet/packet-sender.ts +++ b/src/world/mob/player/packet/packet-sender.ts @@ -123,6 +123,13 @@ export class PacketSender { this.send(packet); } + public setInterfacePlayerHead(interfaceId: number): void { + const packet = new Packet(255); + packet.writeNegativeOffsetShortLE(interfaceId); + + this.send(packet); + } + /** * Clears the player's current map chunk of all ground items and spawned/modified landscape objects. */ diff --git a/src/world/mob/player/task/updating/npc-update-task.ts b/src/world/mob/player/task/updating/npc-update-task.ts index fa702dfc9..7e4a300c5 100644 --- a/src/world/mob/player/task/updating/npc-update-task.ts +++ b/src/world/mob/player/task/updating/npc-update-task.ts @@ -40,7 +40,7 @@ export class NpcUpdateTask extends Task { this.player.trackedNpcs.push(newNpc); // Notify the client of the new npc and their worldIndex - npcUpdatePacket.writeBits(14, newNpc.worldIndex + 1); + npcUpdatePacket.writeBits(14, newNpc.worldIndex); npcUpdatePacket.writeBits(1, newNpc.updateFlags.updateBlockRequired ? 1 : 0); // Update is required npcUpdatePacket.writeBits(5, positionOffsetY); // World Position Y axis offset relative to the player npcUpdatePacket.writeBits(5, positionOffsetX); // World Position X axis offset relative to the player