Skip to content

Commit

Permalink
feat: added EntityNpcTrait
Browse files Browse the repository at this point in the history
  • Loading branch information
PMK744 committed Jan 22, 2025
1 parent f3d270b commit 632bf07
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 17 deletions.
1 change: 1 addition & 0 deletions packages/core/src/entity/traits/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from "./physics";
export * from "./effects";
export * from "./visibility";
export * from "./equipment";
export * from "./npc";
162 changes: 162 additions & 0 deletions packages/core/src/entity/traits/npc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import {
ActorDataId,
ActorDataType,
DataItem,
Gamemode
} from "@serenityjs/protocol";

import { EntityIdentifier, EntityInteractMethod } from "../../enums";
import { JSONLikeObject } from "../../types";
import { Player } from "../player";
import { DialogueForm } from "../../ui";

import { EntityTrait } from "./trait";

interface EntityNpcDialogueComponent extends JSONLikeObject {
/**
* The title of the npc dialogue.
*/
title: string;

/**
* The dialogue content of the npc.
*/
dialogue: string;

/**
* The buttons of the npc dialogue. [ButtonName, Command]
*/
buttons: Array<[string, string]>;
}

class EntityNpcTrait extends EntityTrait {
public static readonly identifier = "npc";
public static readonly types = [EntityIdentifier.Npc];

/**
* The component used to store the npc dialogue form data.
*/
public get component(): EntityNpcDialogueComponent {
return this.entity.getComponent("npc") as EntityNpcDialogueComponent;
}

/**
* The title of the npc dialogue form.
*/
public get title(): string {
return this.component.title;
}

/**
* The title of the npc dialogue form.
*/
public set title(value: string) {
this.component.title = value;
}

/**
* The dialogue content of the npc dialogue form.
*/
public get dialogue(): string {
return this.component.dialogue;
}

/**
* The dialogue content of the npc dialogue form.
*/
public set dialogue(value: string) {
this.component.dialogue = value;
}

/**
* The buttons of the npc dialogue form.
*/
public get buttons(): Array<[string, string]> {
return this.component.buttons;
}

/**
* The buttons of the npc dialogue form.
*/
public set buttons(value: Array<[string, string]>) {
this.component.buttons = value;
}

/**
* Adds a button to the npc dialogue form.
* @param text The text of the button.
* @returns The index of the button.
*/
public addButton(text: string, command = ""): number {
// Add the button to the npc dialogue form
this.component.buttons.push([text, command]);

// Return the index of the button
return this.component.buttons.length - 1;
}

public onAdd(): void {
// Check if the entity has a npc component
if (this.entity.hasComponent(this.identifier)) return;

// Add the npc component to the entity
this.entity.addComponent<EntityNpcDialogueComponent>(this.identifier, {
title: "NPC",
dialogue: "",
buttons: []
});

// Create a new metadata item for the npc component
const metadata = new DataItem(ActorDataId.HasNpc, ActorDataType.Byte, 1);

// Add the metadata item to the entity
this.entity.metadata.set(ActorDataId.HasNpc, metadata);
}

public onRemove(): void {
// Remove the npc component from the entity
this.entity.removeComponent(this.identifier);

// Remove the metadata item from the entity
this.entity.metadata.delete(ActorDataId.HasNpc);
}

public onInteract(player: Player, method: EntityInteractMethod): void {
// Check if the player is in creative mode and attacking the entity
if (
method === EntityInteractMethod.Attack &&
player.gamemode === Gamemode.Creative
) {
// Remove the entity from the dimension
this.entity.despawn();
}

// Check if the entity is not being interacted with (right-click)
if (method !== EntityInteractMethod.Interact) return;

// Create a new dialogue form for the entity, and indicate that it is from a trait
const form = new DialogueForm(this.entity, true);
form.title = this.title;
form.content = this.dialogue;

// Add the buttons to the dialogue form
for (const [text] of this.buttons) form.button(text);

// Show the form to the player
form.show(player, (index, error) => {
// Check if the index is null or an error occurred
if (index === null || error) return;

// Get the command from the button index
const command = this.buttons[index] ? this.buttons[index][1] : "";

// Execute the command if it is not empty
if (command.length > 0) player.executeCommand(command);

// Close the form for the player
return form.close(player);
});
}
}

export { EntityNpcTrait };
87 changes: 73 additions & 14 deletions packages/core/src/handlers/npc-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { NpcRequestPacket, NpcRequestType, Packet } from "@serenityjs/protocol";
import { Connection } from "@serenityjs/raknet";

import { NetworkHandler } from "../network";
import { DialogueForm, DialogueFormProperties } from "../ui";
import { EntityNpcTrait } from "../entity";

class NpcRequestHandler extends NetworkHandler {
public static readonly packet = Packet.NpcRequest;
Expand All @@ -11,27 +13,84 @@ class NpcRequestHandler extends NetworkHandler {
const player = this.serenity.getPlayerByConnection(connection);
if (!player) return connection.disconnect();

// Get the form id from the packet.
const formId = JSON.parse(packet.scene).formId;
// Check if the packet has properties
const hasProperties = packet.scene.length > 0;

// Get the form properties from the packet scene.
const { formId, fromTrait } = hasProperties
? (JSON.parse(packet.scene) as DialogueFormProperties)
: { formId: -1, fromTrait: null }; // Provide default values.

// Check if the form id is -1 and the from trait is null.
// If so, we can indicate that the form that request is to modify the contents of the dialogue.
// But, we need to check if the targeted entity has a npc trait. If not, then we should close the form.
if (formId === -1 && fromTrait === null) {
// Get the entity from the packet unique entity id.
const entity = player.dimension.getEntity(packet.runtimeActorId, true);

// Check if the entity exists.
if (!entity)
throw new Error(
`Entity with runtime id ${packet.runtimeActorId} not found.`
);

// Check if the entity has a npc trait.
if (!entity.hasTrait(EntityNpcTrait))
return DialogueForm.closeForm(player, entity.uniqueId);

// Get the npc trait from the entity.
const npc = entity.getTrait(EntityNpcTrait);

// Switch the packet type to handle the npc request.
switch (packet.type) {
case NpcRequestType.SetName: {
// Update the entity name tag with the packet actions.
entity.nameTag = packet.actions;
break;
}

case NpcRequestType.SetInteractText: {
// Update the npc dialogue with the packet actions.
npc.dialogue = packet.actions;
break;
}
}

return;
}

// Get the form from the player's pending forms.
// And check if the form exists, if not delete the form.
const form = player.pendingForms.get(formId);
if (!form) return;
if (!form) return void player.pendingForms.delete(formId);

switch (packet.type) {
// Return if the packet type is not an execute action.
default:
return;
// Get the form instance from the form.
const instance = form.instance as DialogueForm;
// Check if the form response was derived from a trait.
if (fromTrait) {
switch (packet.type) {
case NpcRequestType.ExecuteAction: {
// Get the button index from the packet.
const index = packet.index as number;

case NpcRequestType.ExecuteAction: {
// Call the form result with the packet index.
form.result(packet.index as unknown as null, null);
break;
// Call the form result with the index.
return form.result(index as unknown as null, null);
}
}
}
} else {
// Check if the action was to click a button.
// If not, return the form result with a null value.
if (packet.type !== NpcRequestType.ExecuteAction) return;

// Close the form for the player.
form.instance.close(player);
// Call the form result with the packet index.
form.result(packet.index as unknown as null, null);

// Close the form for the player.
instance.close(player);

// Delete the form from the pending forms map.
player.pendingForms.delete(formId);
}
}
}

Expand Down
47 changes: 44 additions & 3 deletions packages/core/src/ui/dialogue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ interface DialogueFormButton {
text: string;
}

interface DialogueFormProperties {
/**
* The form id of the dialogue form.
*/
formId: number;

/**
* Whether the dialogue form is from a trait.
*/
fromTrait: boolean;
}

class DialogueForm extends Form<number> {
public readonly type = ModalFormType.Dialogue;

Expand All @@ -36,6 +48,11 @@ class DialogueForm extends Form<number> {
*/
public content: string = "";

/**
* Whether the dialogue form is from a trait.
*/
public fromTrait: boolean;

/**
* The buttons of the dialogue form.
*/
Expand All @@ -44,12 +61,14 @@ class DialogueForm extends Form<number> {
/**
* Creates a new dialogue form.
* @param target The target entity that the dialogue is focused on.
* @param fromTrait Whether the dialogue form is from a trait.
*/
public constructor(target: Entity) {
public constructor(target: Entity, fromTrait = false) {
super();

// Set the properties of the dialogue form.
this.target = target;
this.fromTrait = fromTrait;
}

/**
Expand Down Expand Up @@ -91,7 +110,15 @@ class DialogueForm extends Form<number> {
packet.uniqueEntityId = this.target.uniqueId;
packet.action = NpcDialogueAction.Open;
packet.dialogue = this.content;
packet.scene = JSON.stringify({ formId: this.formId });

// Create the dialogue form properties
const properties: DialogueFormProperties = {
formId: this.formId,
fromTrait: this.fromTrait
};

// Set the scene to the form id and from trait.
packet.scene = JSON.stringify(properties);
packet.name = this.title;
packet.json = JSON.stringify(buttons);

Expand All @@ -118,6 +145,20 @@ class DialogueForm extends Form<number> {
// Send the packet to the player.
player.send(packet);
}

public static closeForm(player: Player, id: bigint): void {
// Create a new NpcDialoguePacket.
const packet = new NpcDialoguePacket();
packet.uniqueEntityId = id;
packet.action = NpcDialogueAction.Close;
packet.dialogue = String();
packet.scene = JSON.stringify({ formId: -1 });
packet.name = String();
packet.json = String();

// Send the packet to the player.
player.send(packet);
}
}

export { DialogueForm };
export { DialogueForm, DialogueFormButton, DialogueFormProperties };

0 comments on commit 632bf07

Please sign in to comment.