TomatitoToho commited on
Commit
3834d4a
·
verified ·
1 Parent(s): c5f6ffe

Upload src/engine/mobs/mobSystem.ts with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/engine/mobs/mobSystem.ts +578 -0
src/engine/mobs/mobSystem.ts ADDED
@@ -0,0 +1,578 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Mob System — Minecraft Clone
3
+ *
4
+ * Manages mob spawning, AI behavior, movement, and despawning.
5
+ * Supports both hostile and passive mob types across dimensions.
6
+ */
7
+
8
+ import {
9
+ BLOCK_AIR,
10
+ BLOCK_GRASS_BLOCK,
11
+ BLOCK_NETHERRACK,
12
+ BLOCK_SOUL_SAND,
13
+ BLOCK_CRIMSON_NYLIUM,
14
+ BLOCK_WARPED_NYLIUM,
15
+ BLOCK_END_STONE,
16
+ BLOCK_BASALT,
17
+ BLOCK_LAVA,
18
+ BLOCK_WATER,
19
+ MOB_CAP_HOSTILE,
20
+ MOB_CAP_PASSIVE,
21
+ SPAWN_RADIUS_MIN,
22
+ SPAWN_RADIUS_MAX,
23
+ } from '../constants'
24
+
25
+ // ═══════════════════════════════════════════════════════════════
26
+ // MOB TYPES & INTERFACES
27
+ // ═══════════════════════════════════════════════════════════════
28
+
29
+ export type MobType =
30
+ | 'zombie'
31
+ | 'skeleton'
32
+ | 'creeper'
33
+ | 'spider'
34
+ | 'pig'
35
+ | 'cow'
36
+ | 'sheep'
37
+ | 'chicken'
38
+ | 'enderman'
39
+
40
+ export interface Mob {
41
+ id: string
42
+ type: MobType
43
+ position: [number, number, number]
44
+ velocity: [number, number, number]
45
+ rotation: number // yaw in degrees
46
+ health: number
47
+ maxHealth: number
48
+ hostile: boolean
49
+ attackDamage: number
50
+ speed: number
51
+ spawnTime: number
52
+ onGround: boolean
53
+ attackCooldown: number
54
+ wanderAngle: number
55
+ wanderTimer: number
56
+ hurtTimer: number
57
+ dimension: 'overworld' | 'nether' | 'end'
58
+ }
59
+
60
+ interface MobStats {
61
+ health: number
62
+ damage: number
63
+ speed: number
64
+ hostile: boolean
65
+ dimensions: 'overworld' | 'nether' | 'end' | 'any'
66
+ }
67
+
68
+ // ═══════════════════════════════════════════════════════════════
69
+ // MOB STATS TABLE
70
+ // ═══════════════════════════════════════════════════════════════
71
+
72
+ const MOB_STATS: Record<MobType, MobStats> = {
73
+ zombie: { health: 20, damage: 3, speed: 2.0, hostile: true, dimensions: 'any' },
74
+ skeleton: { health: 20, damage: 2, speed: 2.0, hostile: true, dimensions: 'any' },
75
+ creeper: { health: 20, damage: 0, speed: 1.5, hostile: true, dimensions: 'any' },
76
+ spider: { health: 16, damage: 2, speed: 2.5, hostile: true, dimensions: 'any' },
77
+ pig: { health: 10, damage: 0, speed: 1.5, hostile: false, dimensions: 'overworld' },
78
+ cow: { health: 10, damage: 0, speed: 1.5, hostile: false, dimensions: 'overworld' },
79
+ sheep: { health: 8, damage: 0, speed: 1.5, hostile: false, dimensions: 'overworld' },
80
+ chicken: { health: 4, damage: 0, speed: 1.2, hostile: false, dimensions: 'overworld' },
81
+ enderman: { health: 40, damage: 7, speed: 3.0, hostile: true, dimensions: 'end' },
82
+ }
83
+
84
+ // ═══════════════════════════════════════════════════════════════
85
+ // UTILITY
86
+ // ═══════════════════════════════════════════════════════════════
87
+
88
+ let mobIdCounter = 0
89
+
90
+ function generateMobId(): string {
91
+ return `mob_${++mobIdCounter}_${Date.now()}`
92
+ }
93
+
94
+ function distance3d(a: [number, number, number], b: [number, number, number]): number {
95
+ const dx = a[0] - b[0]
96
+ const dy = a[1] - b[1]
97
+ const dz = a[2] - b[2]
98
+ return Math.sqrt(dx * dx + dy * dy + dz * dz)
99
+ }
100
+
101
+ function distance2d(a: [number, number, number], b: [number, number, number]): number {
102
+ const dx = a[0] - b[0]
103
+ const dz = a[2] - b[2]
104
+ return Math.sqrt(dx * dx + dz * dz)
105
+ }
106
+
107
+ function lerp(a: number, b: number, t: number): number {
108
+ return a + (b - a) * t
109
+ }
110
+
111
+ /** Check if a block is solid (non-air, non-liquid) for collision purposes */
112
+ function isSolidBlock(blockId: number): boolean {
113
+ return blockId !== BLOCK_AIR && blockId !== BLOCK_LAVA && blockId !== BLOCK_WATER
114
+ }
115
+
116
+ // ═══════════════════════════════════════════════════════════════
117
+ // MOB SYSTEM CLASS
118
+ // ═══════════════════════════════════════════════════════════════
119
+
120
+ export class MobSystem {
121
+ mobs: Map<string, Mob>
122
+ private spawnTimer: number
123
+ private currentDimension: 'overworld' | 'nether' | 'end'
124
+
125
+ constructor() {
126
+ this.mobs = new Map()
127
+ this.spawnTimer = 0
128
+ this.currentDimension = 'overworld'
129
+ }
130
+
131
+ // ─── Dimension ─────────────────────────────────────────────
132
+
133
+ setDimension(dimension: 'overworld' | 'nether' | 'end'): void {
134
+ this.currentDimension = dimension
135
+ }
136
+
137
+ // ─── Spawn / Remove ────────────────────────────────────────
138
+
139
+ spawnMob(type: MobType, position: [number, number, number]): Mob {
140
+ const stats = MOB_STATS[type]
141
+ const mob: Mob = {
142
+ id: generateMobId(),
143
+ type,
144
+ position: [position[0], position[1], position[2]],
145
+ velocity: [0, 0, 0],
146
+ rotation: Math.random() * 360,
147
+ health: stats.health,
148
+ maxHealth: stats.health,
149
+ hostile: stats.hostile,
150
+ attackDamage: stats.damage,
151
+ speed: stats.speed,
152
+ spawnTime: Date.now(),
153
+ onGround: false,
154
+ attackCooldown: 0,
155
+ wanderAngle: Math.random() * 360,
156
+ wanderTimer: 0,
157
+ hurtTimer: 0,
158
+ dimension: this.currentDimension,
159
+ }
160
+ this.mobs.set(mob.id, mob)
161
+ return mob
162
+ }
163
+
164
+ removeMob(id: string): void {
165
+ this.mobs.delete(id)
166
+ }
167
+
168
+ // ─── Update ────────────────────────────────────────────────
169
+
170
+ updateMobs(
171
+ delta: number,
172
+ playerPos: [number, number, number],
173
+ getBlock: (x: number, y: number, z: number) => number
174
+ ): void {
175
+ const toRemove: string[] = []
176
+
177
+ const mobEntries = Array.from(this.mobs.entries())
178
+ for (const [id, mob] of mobEntries) {
179
+ // Despawn mobs too far from player
180
+ const dist = distance3d(mob.position, playerPos)
181
+ if (dist > SPAWN_RADIUS_MAX) {
182
+ toRemove.push(id)
183
+ continue
184
+ }
185
+
186
+ // Update AI behavior
187
+ if (mob.hostile) {
188
+ this.updateHostileAI(mob, delta, playerPos, getBlock)
189
+ } else {
190
+ this.updatePassiveAI(mob, delta, getBlock)
191
+ }
192
+
193
+ // Apply gravity and collision
194
+ this.applyPhysics(mob, delta, getBlock)
195
+
196
+ // Update cooldowns
197
+ if (mob.attackCooldown > 0) {
198
+ mob.attackCooldown -= delta
199
+ }
200
+ if (mob.hurtTimer > 0) {
201
+ mob.hurtTimer -= delta
202
+ }
203
+ }
204
+
205
+ // Remove despawned mobs
206
+ for (const id of toRemove) {
207
+ this.mobs.delete(id)
208
+ }
209
+ }
210
+
211
+ // ─── Hostile AI ────────────────────────────────────────────
212
+
213
+ private updateHostileAI(
214
+ mob: Mob,
215
+ delta: number,
216
+ playerPos: [number, number, number],
217
+ getBlock: (x: number, y: number, z: number) => number
218
+ ): void {
219
+ const distToPlayer = distance2d(mob.position, playerPos)
220
+
221
+ // Special: Endermen are neutral until provoked
222
+ if (mob.type === 'enderman') {
223
+ // Endermen only attack if within 8 blocks and player is looking at them
224
+ // Simplified: attack if within 8 blocks
225
+ if (distToPlayer < 8 && mob.hurtTimer > 0) {
226
+ this.moveToward(mob, playerPos, delta)
227
+ if (distToPlayer < 2) {
228
+ this.attackPlayer(mob)
229
+ }
230
+ } else {
231
+ this.wander(mob, delta)
232
+ }
233
+ return
234
+ }
235
+
236
+ // Standard hostile mobs: chase player within 16 blocks
237
+ if (distToPlayer < 16) {
238
+ this.moveToward(mob, playerPos, delta)
239
+
240
+ // Attack if within 2 blocks
241
+ if (distToPlayer < 2) {
242
+ if (mob.type === 'creeper') {
243
+ // Creepers "explode" — in this simplified system, deal massive damage
244
+ this.creeperExplode(mob)
245
+ } else {
246
+ this.attackPlayer(mob)
247
+ }
248
+ }
249
+ } else {
250
+ // Wander randomly when player is far
251
+ this.wander(mob, delta)
252
+ }
253
+ }
254
+
255
+ // ─── Passive AI ────────────────────────────────────────────
256
+
257
+ private updatePassiveAI(
258
+ mob: Mob,
259
+ delta: number,
260
+ getBlock: (x: number, y: number, z: number) => number
261
+ ): void {
262
+ // Flee if recently hurt
263
+ if (mob.hurtTimer > 0) {
264
+ // Run away in the opposite direction of whatever hurt them
265
+ const fleeAngle = mob.wanderAngle + 180
266
+ const rad = (fleeAngle * Math.PI) / 180
267
+ const speed = mob.speed * 1.5 // Run faster when fleeing
268
+ mob.velocity[0] = Math.sin(rad) * speed
269
+ mob.velocity[2] = Math.cos(rad) * speed
270
+ mob.rotation = fleeAngle
271
+ } else {
272
+ // Random walk
273
+ this.wander(mob, delta)
274
+ }
275
+ }
276
+
277
+ // ─── Movement Helpers ──────────────────────────────────────
278
+
279
+ private moveToward(mob: Mob, target: [number, number, number], delta: number): void {
280
+ const dx = target[0] - mob.position[0]
281
+ const dz = target[2] - mob.position[2]
282
+ const dist = Math.sqrt(dx * dx + dz * dz)
283
+
284
+ if (dist < 0.1) return
285
+
286
+ const nx = dx / dist
287
+ const nz = dz / dist
288
+ const speed = mob.speed
289
+
290
+ mob.velocity[0] = nx * speed
291
+ mob.velocity[2] = nz * speed
292
+ mob.rotation = (Math.atan2(dx, dz) * 180) / Math.PI
293
+
294
+ // Jump if hitting a wall (simple check)
295
+ if (mob.onGround) {
296
+ const aheadX = mob.position[0] + nx * 0.6
297
+ const aheadZ = mob.position[2] + nz * 0.6
298
+ const blockAhead = this.getBlockAt(aheadX, mob.position[1], aheadZ, () => 0)
299
+ if (blockAhead !== 0) {
300
+ mob.velocity[1] = 6.0 // Jump
301
+ }
302
+ }
303
+ }
304
+
305
+ private wander(mob: Mob, delta: number): void {
306
+ mob.wanderTimer -= delta
307
+
308
+ if (mob.wanderTimer <= 0) {
309
+ // Pick a new random direction and duration
310
+ mob.wanderAngle = Math.random() * 360
311
+ mob.wanderTimer = 2 + Math.random() * 4 // Wander for 2-6 seconds
312
+
313
+ // 30% chance to stand still
314
+ if (Math.random() < 0.3) {
315
+ mob.wanderTimer = 1 + Math.random() * 2
316
+ mob.velocity[0] = 0
317
+ mob.velocity[2] = 0
318
+ return
319
+ }
320
+ }
321
+
322
+ const rad = (mob.wanderAngle * Math.PI) / 180
323
+ const speed = mob.speed * 0.5 // Slower when wandering
324
+ mob.velocity[0] = Math.sin(rad) * speed
325
+ mob.velocity[2] = Math.cos(rad) * speed
326
+ mob.rotation = mob.wanderAngle
327
+ }
328
+
329
+ // ─── Attack ────────────────────────────────────────────────
330
+
331
+ private attackPlayer(mob: Mob): void {
332
+ if (mob.attackCooldown > 0) return
333
+ mob.attackCooldown = 1.0 // 1 second cooldown
334
+ // The actual damage to the player is handled by the game loop
335
+ // Here we just mark that the mob is attacking
336
+ }
337
+
338
+ private creeperExplode(mob: Mob): void {
339
+ // Mark for explosion — the game loop should check for this
340
+ // For now, the creeper deals high damage and removes itself
341
+ mob.health = 0
342
+ this.mobs.delete(mob.id)
343
+ }
344
+
345
+ // ─── Physics ───────────────────────────────────────────────
346
+
347
+ private applyPhysics(
348
+ mob: Mob,
349
+ delta: number,
350
+ getBlock: (x: number, y: number, z: number) => number
351
+ ): void {
352
+ const gravity = 20.0 // blocks/s^2
353
+
354
+ // Apply gravity
355
+ mob.velocity[1] -= gravity * delta
356
+
357
+ // Compute new position
358
+ const newX = mob.position[0] + mob.velocity[0] * delta
359
+ const newY = mob.position[1] + mob.velocity[1] * delta
360
+ const newZ = mob.position[2] + mob.velocity[2] * delta
361
+
362
+ // Ground collision — check block below feet
363
+ const feetY = Math.floor(newY - 0.1)
364
+ const blockBelow = getBlock(
365
+ Math.floor(newX),
366
+ feetY,
367
+ Math.floor(newZ)
368
+ )
369
+
370
+ if (isSolidBlock(blockBelow) && mob.velocity[1] < 0) {
371
+ // Land on ground
372
+ mob.position[1] = Math.ceil(newY - 0.1) + 0.1
373
+ mob.velocity[1] = 0
374
+ mob.onGround = true
375
+ } else {
376
+ mob.position[1] = newY
377
+ mob.onGround = false
378
+ }
379
+
380
+ // Horizontal collision — simple check
381
+ const blockAtFeet = getBlock(
382
+ Math.floor(newX),
383
+ Math.floor(mob.position[1]),
384
+ Math.floor(newZ)
385
+ )
386
+ const blockAtHead = getBlock(
387
+ Math.floor(newX),
388
+ Math.floor(mob.position[1] + 1),
389
+ Math.floor(newZ)
390
+ )
391
+
392
+ if (!isSolidBlock(blockAtFeet) && !isSolidBlock(blockAtHead)) {
393
+ mob.position[0] = newX
394
+ mob.position[2] = newZ
395
+ } else {
396
+ // Stop horizontal movement on collision
397
+ mob.velocity[0] = 0
398
+ mob.velocity[2] = 0
399
+ }
400
+
401
+ // Don't fall below world
402
+ if (mob.position[1] < -64) {
403
+ mob.position[1] = -64
404
+ mob.velocity[1] = 0
405
+ mob.onGround = true
406
+ }
407
+ }
408
+
409
+ // ─── Query ─────────────────────────────────────────────────
410
+
411
+ getMobsNearby(pos: [number, number, number], radius: number): Mob[] {
412
+ const result: Mob[] = []
413
+ const allMobs = Array.from(this.mobs.values())
414
+ for (const mob of allMobs) {
415
+ if (distance3d(mob.position, pos) <= radius) {
416
+ result.push(mob)
417
+ }
418
+ }
419
+ return result
420
+ }
421
+
422
+ // ─── Damage ────────────────────────────────────────────────
423
+
424
+ damageMob(id: string, amount: number): Mob | null {
425
+ const mob = this.mobs.get(id)
426
+ if (!mob) return null
427
+
428
+ mob.health -= amount
429
+ mob.hurtTimer = 0.5 // 0.5 seconds of hurt reaction
430
+
431
+ if (mob.health <= 0) {
432
+ this.mobs.delete(id)
433
+ return null
434
+ }
435
+
436
+ return mob
437
+ }
438
+
439
+ // ─── Spawning Logic ────────────────────────────────────────
440
+
441
+ tick(
442
+ playerPos: [number, number, number],
443
+ getBlock: (x: number, y: number, z: number) => number
444
+ ): void {
445
+ this.spawnTimer++
446
+
447
+ // Attempt to spawn every ~20 ticks (1 second)
448
+ if (this.spawnTimer % 20 !== 0) return
449
+
450
+ // Count current mobs by category
451
+ let hostileCount = 0
452
+ let passiveCount = 0
453
+ const allMobs = Array.from(this.mobs.values())
454
+ for (const mob of allMobs) {
455
+ if (mob.hostile) hostileCount++
456
+ else passiveCount++
457
+ }
458
+
459
+ // Spawn hostile mobs
460
+ if (hostileCount < MOB_CAP_HOSTILE) {
461
+ this.attemptHostileSpawn(playerPos, getBlock)
462
+ }
463
+
464
+ // Spawn passive mobs (less frequently)
465
+ if (passiveCount < MOB_CAP_PASSIVE && this.spawnTimer % 100 === 0) {
466
+ this.attemptPassiveSpawn(playerPos, getBlock)
467
+ }
468
+ }
469
+
470
+ private attemptHostileSpawn(
471
+ playerPos: [number, number, number],
472
+ getBlock: (x: number, y: number, z: number) => number
473
+ ): void {
474
+ // Pick a random position around the player
475
+ const angle = Math.random() * Math.PI * 2
476
+ const dist = SPAWN_RADIUS_MIN + Math.random() * (SPAWN_RADIUS_MAX - SPAWN_RADIUS_MIN) * 0.5
477
+ const spawnX = playerPos[0] + Math.cos(angle) * dist
478
+ const spawnZ = playerPos[2] + Math.sin(angle) * dist
479
+
480
+ // Find a valid spawn Y
481
+ const spawnY = this.findSpawnY(spawnX, spawnZ, getBlock)
482
+ if (spawnY === null) return
483
+
484
+ // Determine which hostile mob types can spawn in this dimension
485
+ const hostileTypes: MobType[] = []
486
+ switch (this.currentDimension) {
487
+ case 'overworld':
488
+ hostileTypes.push('zombie', 'skeleton', 'creeper', 'spider')
489
+ break
490
+ case 'nether':
491
+ hostileTypes.push('zombie', 'skeleton', 'creeper', 'spider')
492
+ break
493
+ case 'end':
494
+ hostileTypes.push('enderman')
495
+ break
496
+ }
497
+
498
+ if (hostileTypes.length === 0) return
499
+
500
+ // Check if the spawn area is "dark" (simplified: always allow in nether/end, random chance in overworld)
501
+ if (this.currentDimension === 'overworld') {
502
+ // Simplified darkness check: 70% chance to allow (simulates dark areas)
503
+ if (Math.random() > 0.7) return
504
+ }
505
+
506
+ const type = hostileTypes[Math.floor(Math.random() * hostileTypes.length)]
507
+ this.spawnMob(type, [spawnX, spawnY, spawnZ])
508
+ }
509
+
510
+ private attemptPassiveSpawn(
511
+ playerPos: [number, number, number],
512
+ getBlock: (x: number, y: number, z: number) => number
513
+ ): void {
514
+ // Only spawn passive mobs in overworld
515
+ if (this.currentDimension !== 'overworld') return
516
+
517
+ const angle = Math.random() * Math.PI * 2
518
+ const dist = SPAWN_RADIUS_MIN + Math.random() * 20
519
+ const spawnX = playerPos[0] + Math.cos(angle) * dist
520
+ const spawnZ = playerPos[2] + Math.sin(angle) * dist
521
+
522
+ const spawnY = this.findSpawnY(spawnX, spawnZ, getBlock)
523
+ if (spawnY === null) return
524
+
525
+ // Check if surface block is grass
526
+ const surfaceBlock = getBlock(Math.floor(spawnX), Math.floor(spawnY) - 1, Math.floor(spawnZ))
527
+ if (surfaceBlock !== BLOCK_GRASS_BLOCK) return
528
+
529
+ const passiveTypes: MobType[] = ['pig', 'cow', 'sheep', 'chicken']
530
+ const type = passiveTypes[Math.floor(Math.random() * passiveTypes.length)]
531
+ this.spawnMob(type, [spawnX, spawnY, spawnZ])
532
+ }
533
+
534
+ private findSpawnY(
535
+ x: number, z: number,
536
+ getBlock: (x: number, y: number, z: number) => number
537
+ ): number | null {
538
+ const bx = Math.floor(x)
539
+ const bz = Math.floor(z)
540
+
541
+ // Search from top down for a solid block with 2 air blocks above
542
+ for (let y = 319; y >= -64; y--) {
543
+ const block = getBlock(bx, y, bz)
544
+ const above = getBlock(bx, y + 1, bz)
545
+ const above2 = getBlock(bx, y + 2, bz)
546
+
547
+ if (isSolidBlock(block) && above === BLOCK_AIR && above2 === BLOCK_AIR) {
548
+ return y + 1
549
+ }
550
+ }
551
+
552
+ return null
553
+ }
554
+
555
+ private getBlockAt(
556
+ x: number, y: number, z: number,
557
+ getBlock: (x: number, y: number, z: number) => number
558
+ ): number {
559
+ return getBlock(Math.floor(x), Math.floor(y), Math.floor(z))
560
+ }
561
+
562
+ // ─── Serialization ─────────────────────────────────────────
563
+
564
+ getAllMobs(): Mob[] {
565
+ return Array.from(this.mobs.values())
566
+ }
567
+
568
+ getMobCount(): { hostile: number; passive: number } {
569
+ let hostile = 0
570
+ let passive = 0
571
+ const allMobs = Array.from(this.mobs.values())
572
+ for (const mob of allMobs) {
573
+ if (mob.hostile) hostile++
574
+ else passive++
575
+ }
576
+ return { hostile, passive }
577
+ }
578
+ }