Skip to content

Commit de8159d

Browse files
committed
feat: add NPC/Entity bindings and example commands
- Add NPCPlugin, NPCEntity, Role, FlockPlugin and related type definitions - Add Java bindings for NPC system classes in ScriptBindings.java - Add example commands: /npcspawn, /npcroles, /npcdespawn - Add NPCInteractHandler event example - Add null checks for getDefaultWorld() across all commands - Fix scoreboard HUD to handle missing default world
1 parent c243d1a commit de8159d

19 files changed

Lines changed: 1171 additions & 35 deletions

File tree

docs/types/types.d.ts

Lines changed: 395 additions & 15 deletions
Large diffs are not rendered by default.

docs/types/types.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/kitchensink/src/commands/breakblock.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ export function registerBreakBlockCommand(): void {
2424
}
2525

2626
const world = Universe.get().getDefaultWorld();
27+
if (!world) {
28+
ctx.sendMessage("No default world configured");
29+
return;
30+
}
2731
const success = world.breakBlock(x, y, z, harvestLevel);
2832

2933
if (success) {

examples/kitchensink/src/commands/getblock.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export function registerGetBlockCommand(): void {
1818
}
1919

2020
const world = Universe.get().getDefaultWorld();
21+
if (!world) {
22+
ctx.sendMessage("No default world configured");
23+
return;
24+
}
2125
const blockType = world.getBlockType(x, y, z);
2226

2327
if (blockType) {

examples/kitchensink/src/commands/giveitem.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export function registerGiveItemCommand(): void {
1818

1919
const senderName = ctx.getSenderName();
2020
const world = Universe.get().getDefaultWorld();
21+
if (!world) {
22+
ctx.sendMessage("No default world configured");
23+
return;
24+
}
2125
const players = world.getPlayers();
2226

2327
let foundPlayer = null;

examples/kitchensink/src/commands/hand.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ export function registerHandCommand(): void {
22
commands.register("hand", "Show info about the item in your hand", (ctx) => {
33
const senderName = ctx.getSenderName();
44
const world = Universe.get().getDefaultWorld();
5+
if (!world) {
6+
ctx.sendMessage("No default world configured");
7+
return;
8+
}
59
const players = world.getPlayers();
610

711
let foundPlayer = null;

examples/kitchensink/src/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export { registerWallCommand } from "./wall";
1919
export { registerSquareCommand } from "./square";
2020
export { registerLongTaskCommand } from "./longtask";
2121
export { registerGuiCommand, registerUpdatingGuiCommand, registerGuiAssets } from "./gui";
22+
export { registerNpcSpawnCommand, registerNpcRolesCommand, registerNpcDespawnCommand } from "./npcspawn";
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
export function registerNpcSpawnCommand(): void {
2+
commands.registerWorld("npcspawn", "Spawn an NPC by role name", (ctx) => {
3+
const input = ctx.getInput();
4+
const parts = input.split(" ");
5+
6+
if (parts.length < 2) {
7+
ctx.sendMessage("Usage: /npcspawn <role_name> [scale]");
8+
ctx.sendMessage("Example: /npcspawn trork_warrior");
9+
return;
10+
}
11+
12+
const roleName = parts[1];
13+
const scale = parts.length >= 3 ? parseFloat(parts[2]) : 1.0;
14+
15+
if (isNaN(scale) || scale <= 0) {
16+
ctx.sendMessage("Invalid scale value");
17+
return;
18+
}
19+
20+
const senderName = ctx.getSenderName();
21+
const universe = Universe.get();
22+
const players = universe.getPlayers();
23+
24+
let foundPlayerRef = null;
25+
for (let i = 0; i < players.length; i++) {
26+
if (players[i].getUsername() === senderName) {
27+
foundPlayerRef = players[i];
28+
break;
29+
}
30+
}
31+
32+
if (!foundPlayerRef) {
33+
ctx.sendMessage("Could not find player");
34+
return;
35+
}
36+
37+
const transform = foundPlayerRef.getTransform();
38+
const playerPos = transform.getPosition();
39+
const playerRot = transform.getRotation();
40+
41+
const spawnPos = new Vector3d(playerPos.getX() + 2, playerPos.getY(), playerPos.getZ());
42+
const spawnRot = new Vector3f(0, playerRot.getYaw(), 0);
43+
44+
const npcPlugin = NPCPlugin.get();
45+
46+
if (!npcPlugin.hasRoleName(roleName)) {
47+
ctx.sendMessage("Unknown NPC role: " + roleName);
48+
ctx.sendMessage("Use /npcroles to see available roles");
49+
return;
50+
}
51+
52+
const worldUuid = foundPlayerRef.getWorldUuid();
53+
if (!worldUuid) {
54+
ctx.sendMessage("Player is not in a world");
55+
return;
56+
}
57+
58+
const world = universe.getWorld(worldUuid);
59+
if (!world) {
60+
ctx.sendMessage("Could not find player's world");
61+
return;
62+
}
63+
64+
const store = world.getEntityStore().getStore();
65+
66+
const result = npcPlugin.spawnNPC(store, roleName, null, spawnPos, spawnRot);
67+
68+
if (result) {
69+
ctx.sendMessage("Spawned NPC: " + roleName);
70+
} else {
71+
ctx.sendMessage("Failed to spawn NPC: " + roleName);
72+
}
73+
});
74+
}
75+
76+
export function registerNpcRolesCommand(): void {
77+
commands.register("npcroles", "List available NPC roles", (ctx) => {
78+
const npcPlugin = NPCPlugin.get();
79+
const roles = npcPlugin.getRoleTemplateNames(true);
80+
81+
ctx.sendMessage("Available NPC roles (" + roles.length + "):");
82+
83+
const maxDisplay = 20;
84+
for (let i = 0; i < Math.min(roles.length, maxDisplay); i++) {
85+
ctx.sendMessage(" - " + roles[i]);
86+
}
87+
88+
if (roles.length > maxDisplay) {
89+
ctx.sendMessage(" ... and " + (roles.length - maxDisplay) + " more");
90+
}
91+
});
92+
}
93+
94+
export function registerNpcDespawnCommand(): void {
95+
commands.registerWorld("npcdespawn", "Despawn nearby NPCs", (ctx) => {
96+
const input = ctx.getInput();
97+
const parts = input.split(" ");
98+
99+
const radius = parts.length >= 2 ? parseFloat(parts[1]) : 10.0;
100+
101+
if (isNaN(radius) || radius <= 0) {
102+
ctx.sendMessage("Invalid radius");
103+
return;
104+
}
105+
106+
const senderName = ctx.getSenderName();
107+
const universe = Universe.get();
108+
const players = universe.getPlayers();
109+
110+
let foundPlayerRef = null;
111+
for (let i = 0; i < players.length; i++) {
112+
if (players[i].getUsername() === senderName) {
113+
foundPlayerRef = players[i];
114+
break;
115+
}
116+
}
117+
118+
if (!foundPlayerRef) {
119+
ctx.sendMessage("Could not find player");
120+
return;
121+
}
122+
123+
const transform = foundPlayerRef.getTransform();
124+
const playerPos = transform.getPosition();
125+
126+
const worldUuid = foundPlayerRef.getWorldUuid();
127+
if (!worldUuid) {
128+
ctx.sendMessage("Player is not in a world");
129+
return;
130+
}
131+
132+
const world = universe.getWorld(worldUuid);
133+
if (!world) {
134+
ctx.sendMessage("Could not find player's world");
135+
return;
136+
}
137+
138+
const store = world.getEntityStore().getStore();
139+
140+
const npcComponentType = NPCEntity.getComponentType();
141+
if (!npcComponentType) {
142+
ctx.sendMessage("NPC system not available");
143+
return;
144+
}
145+
146+
let despawnCount = 0;
147+
const radiusSq = radius * radius;
148+
149+
store.forEachChunk(npcComponentType, (archetypeChunk, commandBuffer) => {
150+
for (let index = 0; index < archetypeChunk.size(); index++) {
151+
const npcEntity = archetypeChunk.getComponent(index, npcComponentType);
152+
if (npcEntity) {
153+
const npcRef = archetypeChunk.getReferenceTo(index);
154+
const transformComponent = commandBuffer.getComponent(npcRef, TransformComponent.getComponentType());
155+
if (transformComponent) {
156+
const npcPos = transformComponent.getPosition();
157+
const dx = npcPos.getX() - playerPos.getX();
158+
const dy = npcPos.getY() - playerPos.getY();
159+
const dz = npcPos.getZ() - playerPos.getZ();
160+
const distSq = dx * dx + dy * dy + dz * dz;
161+
162+
if (distSq <= radiusSq) {
163+
npcEntity.setToDespawn();
164+
npcEntity.setDespawnTime(0);
165+
despawnCount++;
166+
}
167+
}
168+
}
169+
}
170+
});
171+
172+
ctx.sendMessage("Marked " + despawnCount + " NPCs for despawn within " + radius + " blocks");
173+
});
174+
}

examples/kitchensink/src/commands/pyramid.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export function registerPyramidCommand(): void {
4040
const baseZ = Math.floor(pos.getZ());
4141

4242
const world = universe.getDefaultWorld();
43+
if (!world) {
44+
ctx.sendMessage("No default world configured");
45+
return;
46+
}
4347
let count = 0;
4448

4549
for (let layer = 0; layer < size; layer++) {

examples/kitchensink/src/commands/serverinfo.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export function registerServerInfoCommand(): void {
22
commands.register("serverinfo", "Display server name, player count, and default world", (ctx) => {
33
ctx.sendMessage("Server: " + HytaleServer.get().getServerName());
44
ctx.sendMessage("Players: " + Universe.get().getPlayerCount());
5-
ctx.sendMessage("Default world: " + Universe.get().getDefaultWorld().getName());
5+
const defaultWorld = Universe.get().getDefaultWorld();
6+
ctx.sendMessage("Default world: " + (defaultWorld ? defaultWorld.getName() : "None"));
67
});
78
}

0 commit comments

Comments
 (0)