hytopia 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/docs/server.playerentitycontroller.md +1 -1
  2. package/docs/server.playerentitycontroller.tickwithplayerinput.md +1 -1
  3. package/examples/hygrounds/assets/icons/auto-sniper.png +0 -0
  4. package/examples/hygrounds/assets/icons/gravity-potion.png +0 -0
  5. package/examples/hygrounds/assets/icons/revolver.png +0 -0
  6. package/examples/hygrounds/assets/icons/submachine-gun.png +0 -0
  7. package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper-named-nodes.glb +0 -0
  8. package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper.glb +0 -0
  9. package/examples/hygrounds/assets/models/items/.optimized/auto-sniper/auto-sniper.glb.md5 +1 -0
  10. package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion-named-nodes.glb +0 -0
  11. package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion.glb +0 -0
  12. package/examples/hygrounds/assets/models/items/.optimized/gravity-potion/gravity-potion.glb.md5 +1 -0
  13. package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver-named-nodes.glb +0 -0
  14. package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver.glb +0 -0
  15. package/examples/hygrounds/assets/models/items/.optimized/revolver/revolver.glb.md5 +1 -0
  16. package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun-named-nodes.glb +0 -0
  17. package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun.glb +0 -0
  18. package/examples/hygrounds/assets/models/items/.optimized/submachine-gun/submachine-gun.glb.md5 +1 -0
  19. package/examples/hygrounds/assets/models/items/auto-sniper.glb +0 -0
  20. package/examples/hygrounds/assets/models/items/gravity-potion.glb +0 -0
  21. package/examples/hygrounds/assets/models/items/revolver.glb +0 -0
  22. package/examples/hygrounds/assets/models/items/submachine-gun.glb +0 -0
  23. package/examples/hygrounds/assets/ui/index.html +51 -1
  24. package/examples/hygrounds/classes/GameManager.ts +39 -1
  25. package/examples/hygrounds/classes/GamePlayerEntity.ts +44 -13
  26. package/examples/hygrounds/classes/GunEntity.ts +1 -5
  27. package/examples/hygrounds/classes/ItemFactory.ts +12 -0
  28. package/examples/hygrounds/classes/items/GravityPotionEntity.ts +48 -0
  29. package/examples/hygrounds/classes/items/ShieldPotionEntity.ts +1 -1
  30. package/examples/hygrounds/classes/weapons/AK47Entity.ts +1 -1
  31. package/examples/hygrounds/classes/weapons/AutoShotgunEntity.ts +2 -2
  32. package/examples/hygrounds/classes/weapons/AutoSniperEntity.ts +43 -0
  33. package/examples/hygrounds/classes/weapons/LightMachineGunEntity.ts +3 -3
  34. package/examples/hygrounds/classes/weapons/RevolverEntity.ts +46 -0
  35. package/examples/hygrounds/classes/weapons/RocketLauncherEntity.ts +11 -2
  36. package/examples/hygrounds/classes/weapons/ShotgunEntity.ts +1 -1
  37. package/examples/hygrounds/classes/weapons/SubmachineGunEntity.ts +43 -0
  38. package/examples/hygrounds/gameConfig.ts +38 -6
  39. package/examples/hygrounds/index.ts +2 -1
  40. package/examples/player-persistence/README.md +3 -0
  41. package/examples/player-persistence/assets/map.json +2623 -0
  42. package/examples/player-persistence/dev/persistence/player-player-1.json +6 -0
  43. package/examples/player-persistence/dev/persistence/test.json +3 -0
  44. package/examples/player-persistence/index.ts +126 -0
  45. package/examples/player-persistence/package.json +16 -0
  46. package/package.json +1 -1
  47. package/server.api.json +1 -1
  48. package/server.d.ts +3 -1
  49. package/server.js +1 -1
  50. /package/examples/hygrounds/assets/audio/sfx/{shield-potion-consume.mp3 → potion-consume.mp3} +0 -0
@@ -457,7 +457,7 @@ Called when the controlled entity is spawned. In PlayerEntityController, this fu
457
457
 
458
458
  </td><td>
459
459
 
460
- Ticks the player movement for the entity controller, overriding the default implementation.
460
+ Ticks the player movement for the entity controller, overriding the default implementation. If the entity to tick is a child entity, only the event will be emitted but the default movement logic will not be applied.
461
461
 
462
462
 
463
463
  </td></tr>
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## PlayerEntityController.tickWithPlayerInput() method
6
6
 
7
- Ticks the player movement for the entity controller, overriding the default implementation.
7
+ Ticks the player movement for the entity controller, overriding the default implementation. If the entity to tick is a child entity, only the event will be emitted but the default movement logic will not be applied.
8
8
 
9
9
  **Signature:**
10
10
 
@@ -0,0 +1 @@
1
+ eb57c9bd2558eab8508e66cebc58793ce1b660c3bc089533623b8ec937e6c33c
@@ -0,0 +1 @@
1
+ 64e7588c50e1ca56989a396252683fb9e16cb8c5814237b46562983a3aa35fe4
@@ -0,0 +1 @@
1
+ e4d9e2e03c083b63c01b59dab34218c6aae28bbd88fcda7db44e2dcb2075a080
@@ -0,0 +1 @@
1
+ 85850ccf234d28a1d2a306cc3b9dd7ec8196b759be5011e38a96ded3605e9201
@@ -20,6 +20,9 @@
20
20
  <!-- Game Start Announcement -->
21
21
  <div class="game-start-announcement">DEATHMATCH!</div>
22
22
 
23
+ <!-- Winner Announcement -->
24
+ <div class="winner-announcement"></div>
25
+
23
26
  <!-- Leaderboard -->
24
27
  <div class="leaderboard">
25
28
  <div class="leaderboard-title">HyGrounds Deathmatch</div>
@@ -224,6 +227,17 @@
224
227
  }, 3000);
225
228
  }
226
229
 
230
+ function showWinnerAnnouncement(username) {
231
+ const announcement = document.querySelector('.winner-announcement');
232
+ announcement.textContent = `${username} wins!`;
233
+ announcement.classList.add('active');
234
+
235
+ // Remove the active class after animation completes
236
+ setTimeout(() => {
237
+ announcement.classList.remove('active');
238
+ }, 5000);
239
+ }
240
+
227
241
  hytopia.registerSceneUITemplate('item-label', (id, onState) => {
228
242
  const template = document.getElementById('item-label-template');
229
243
  const clone = template.content.cloneNode(true);
@@ -268,6 +282,11 @@
268
282
  showGameStartAnnouncement();
269
283
  }
270
284
 
285
+ if (type === 'announce-winner') {
286
+ const { username } = data;
287
+ showWinnerAnnouncement(username);
288
+ }
289
+
271
290
  if (type === 'ammo-indicator') {
272
291
  const { ammo, totalAmmo, show, reloading } = data;
273
292
 
@@ -540,7 +559,29 @@
540
559
  }
541
560
 
542
561
  .game-start-announcement.active {
543
- animation: announcementFade 3s ease-in-out forwards;
562
+ animation: announcementFade 5s ease-in-out forwards;
563
+ }
564
+
565
+ .winner-announcement {
566
+ position: fixed;
567
+ top: 50%;
568
+ left: 50%;
569
+ transform: translate(-50%, -50%);
570
+ font-family: 'Inter', sans-serif;
571
+ font-size: 80px;
572
+ font-weight: bold;
573
+ color: #ffd700;
574
+ text-shadow: 0 0 15px rgba(0, 0, 0, 0.8), 0 0 30px rgba(255, 215, 0, 0.6);
575
+ text-transform: uppercase;
576
+ letter-spacing: 3px;
577
+ opacity: 0;
578
+ z-index: 1000;
579
+ pointer-events: none;
580
+ text-align: center;
581
+ }
582
+
583
+ .winner-announcement.active {
584
+ animation: winnerFade 5s ease-in-out forwards;
544
585
  }
545
586
 
546
587
  @keyframes announcementFade {
@@ -550,6 +591,15 @@
550
591
  100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
551
592
  }
552
593
 
594
+ @keyframes winnerFade {
595
+ 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); }
596
+ 20% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); }
597
+ 40% { opacity: 1; transform: translate(-50%, -50%) scale(1.1); }
598
+ 60% { opacity: 1; transform: translate(-50%, -50%) scale(1.2); }
599
+ 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
600
+ 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.8); }
601
+ }
602
+
553
603
  .hit-damage-container {
554
604
  position: fixed;
555
605
  top: 50%;
@@ -96,6 +96,8 @@ export default class GameManager {
96
96
  this._gameActive = false;
97
97
  this.world.chatManager.sendBroadcastMessage('Game over! Starting the next round in 10 seconds...', 'FF0000');
98
98
 
99
+ this._focusWinningPlayer();
100
+
99
101
  // Clear any existing restart timer
100
102
  if (this._restartTimer) {
101
103
  clearTimeout(this._restartTimer);
@@ -205,6 +207,7 @@ export default class GameManager {
205
207
  if (playerEntity instanceof GamePlayerEntity) {
206
208
  playerEntity.setActiveInventorySlotIndex(0); // reset to pickaxe at slot 0
207
209
  playerEntity.dropAllInventoryItems();
210
+ playerEntity.resetCamera();
208
211
  playerEntity.resetMaterials();
209
212
  playerEntity.health = 100;
210
213
  playerEntity.shield = 0;
@@ -239,6 +242,41 @@ export default class GameManager {
239
242
  this.resetLeaderboard();
240
243
  }
241
244
 
245
+ public _focusWinningPlayer() {
246
+ if (!this.world) return;
247
+
248
+ // Find player with most kills
249
+ let highestKills = 0;
250
+ let winningPlayer = '';
251
+
252
+ this._killCounter.forEach((kills, player) => {
253
+ if (kills > highestKills) {
254
+ highestKills = kills;
255
+ winningPlayer = player;
256
+ }
257
+ });
258
+
259
+ // Get winning player entity
260
+ const winningPlayerEntity = this.world.entityManager
261
+ .getAllPlayerEntities()
262
+ .find(entity => entity.player.username === winningPlayer);
263
+
264
+ if (!winningPlayerEntity) return;
265
+
266
+ this.world.entityManager.getAllPlayerEntities().forEach(playerEntity => {
267
+ if (playerEntity instanceof GamePlayerEntity) {
268
+ if (playerEntity.player.username !== winningPlayer) { // don't change camera for the winner
269
+ playerEntity.focusCameraOnPlayer(winningPlayerEntity as GamePlayerEntity);
270
+ }
271
+
272
+ playerEntity.player.ui.sendData({
273
+ type: 'announce-winner',
274
+ username: winningPlayer,
275
+ });
276
+ }
277
+ });
278
+ }
279
+
242
280
  /**
243
281
  * Syncs UI for all connected players
244
282
  */
@@ -262,7 +300,7 @@ export default class GameManager {
262
300
  this.world.chatManager.sendPlayerMessage(player, '- Search for chests and weapons to survive');
263
301
  this.world.chatManager.sendPlayerMessage(player, '- Break blocks with your pickaxe to gain materials');
264
302
  this.world.chatManager.sendPlayerMessage(player, '- Right click to spend 3 materials to place a block');
265
- this.world.chatManager.sendPlayerMessage(player, '- Some weapons can zoom with "Z".');
303
+ this.world.chatManager.sendPlayerMessage(player, '- Some weapons zoom with "Z". Drop items with "Q"');
266
304
  }
267
305
 
268
306
  /**
@@ -92,6 +92,7 @@ export default class GamePlayerEntity extends PlayerEntity {
92
92
  super.spawn(world, position, rotation);
93
93
  this._setupPlayerInventory();
94
94
  this._autoHealTicker();
95
+ this._outOfWorldTicker();
95
96
  this._updatePlayerUIHealth();
96
97
  }
97
98
 
@@ -121,11 +122,7 @@ export default class GamePlayerEntity extends PlayerEntity {
121
122
 
122
123
  if (attacker) {
123
124
  GameManager.instance.addKill(attacker.player.username);
124
-
125
- // Focus on the player that killed you
126
- this.player.camera.setMode(PlayerCameraMode.THIRD_PERSON);
127
- this.player.camera.setAttachedToEntity(attacker);
128
- this.player.camera.setModelHiddenNodes([]);
125
+ this.focusCameraOnPlayer(attacker);
129
126
  }
130
127
 
131
128
  this.dropAllInventoryItems();
@@ -137,16 +134,26 @@ export default class GamePlayerEntity extends PlayerEntity {
137
134
  });
138
135
 
139
136
  this.playerController.idleLoopedAnimations = [ 'sleep' ];
140
- this.world.chatManager.sendPlayerMessage(this.player, 'You have died! Respawning in 10 seconds...', 'FF0000');
141
- this._respawnTimer = setTimeout(() => this.respawn(), 10 * 1000);
137
+ this.world.chatManager.sendPlayerMessage(this.player, 'You have died! Respawning in 5 seconds...', 'FF0000');
138
+ this._respawnTimer = setTimeout(() => this.respawn(), 5 * 1000);
142
139
 
143
140
  if (attacker) {
144
- this.world.chatManager.sendBroadcastMessage(`${attacker.player.username} killed ${this.player.username} with a ${attacker.getActiveItemName()}!`, 'FF0000');
141
+ if (this.player.username !== attacker.player.username) {
142
+ this.world.chatManager.sendBroadcastMessage(`${attacker.player.username} killed ${this.player.username} with a ${attacker.getActiveItemName()}!`, 'FF0000');
143
+ } else {
144
+ this.world.chatManager.sendBroadcastMessage(`${this.player.username} self-destructed!`, 'FF0000');
145
+ }
145
146
  }
146
147
  }
147
148
  }
148
149
  }
149
150
 
151
+ public focusCameraOnPlayer(player: GamePlayerEntity): void {
152
+ this.player.camera.setMode(PlayerCameraMode.THIRD_PERSON);
153
+ this.player.camera.setAttachedToEntity(player);
154
+ this.player.camera.setModelHiddenNodes([]);
155
+ }
156
+
150
157
  public dealtDamage(damage: number): void {
151
158
  this.player.ui.sendData({
152
159
  type: 'show-damage',
@@ -206,6 +213,11 @@ export default class GamePlayerEntity extends PlayerEntity {
206
213
  this.playerController.runLoopedAnimations = ['run_lower', 'run_upper'];
207
214
  }
208
215
 
216
+ public resetCamera(): void {
217
+ this._setupPlayerCamera();
218
+ this.player.camera.setAttachedToEntity(this);
219
+ }
220
+
209
221
  public resetMaterials(): void {
210
222
  this._materials = 0;
211
223
  this._updatePlayerUIMaterials();
@@ -238,8 +250,12 @@ export default class GamePlayerEntity extends PlayerEntity {
238
250
  this._updatePlayerUIInventoryActiveSlot();
239
251
  }
240
252
 
253
+ public setGravity(gravityScale: number): void {
254
+ this.setGravityScale(gravityScale);
255
+ }
256
+
241
257
  public takeDamage(damage: number, hitDirection: Vector3Like, attacker?: GamePlayerEntity): void {
242
- if (!this.isSpawned || !this.world || !GameManager.instance.isGameActive || this._dead) return;
258
+ if (!this.isSpawned || !this.world || !GameManager.instance.isGameActive || this._dead) return;
243
259
 
244
260
  this._playDamageAudio();
245
261
 
@@ -331,7 +347,7 @@ export default class GamePlayerEntity extends PlayerEntity {
331
347
 
332
348
  private _onTickWithPlayerInput = (payload: EventPayloads[BaseEntityControllerEvent.TICK_WITH_PLAYER_INPUT]): void => {
333
349
  const { input } = payload;
334
-
350
+
335
351
  if (this._dead) {
336
352
  return;
337
353
  }
@@ -459,7 +475,10 @@ export default class GamePlayerEntity extends PlayerEntity {
459
475
  origin,
460
476
  this.player.camera.facingDirection,
461
477
  INTERACT_RANGE,
462
- { filterExcludeRigidBody: this.rawRigidBody }
478
+ {
479
+ filterExcludeRigidBody: this.rawRigidBody,
480
+ filterFlags: 8, // Rapier exclude sensors,
481
+ }
463
482
  );
464
483
 
465
484
  const hitEntity = raycastHit?.hitEntity;
@@ -552,13 +571,25 @@ export default class GamePlayerEntity extends PlayerEntity {
552
571
 
553
572
  private _autoHealTicker(): void {
554
573
  setTimeout(() => {
555
- if (!this.isSpawned || this._dead) return;
574
+ if (!this.isSpawned) return;
556
575
 
557
- if (this.health < this._maxHealth) {
576
+ if (this.health < this._maxHealth && !this._dead) {
558
577
  this.health += 1;
559
578
  }
560
579
 
561
580
  this._autoHealTicker();
562
581
  }, 2000);
563
582
  }
583
+
584
+ private _outOfWorldTicker(): void {
585
+ setTimeout(() => {
586
+ if (!this.isSpawned) return;
587
+
588
+ if (this.position.y < -100 && !this._dead) {
589
+ this.takeDamage(MAX_HEALTH + MAX_SHIELD, { x: 0, y: 0, z: -1 });
590
+ }
591
+
592
+ this._outOfWorldTicker();
593
+ }, 3000);
594
+ }
564
595
  }
@@ -142,11 +142,7 @@ export default abstract class GunEntity extends ItemEntity {
142
142
  const direction = player.player.camera.facingDirection;
143
143
 
144
144
  return {
145
- origin: {
146
- x: x + (direction.x * 0.5),
147
- y: y + (direction.y * 0.5) + cameraYOffset,
148
- z: z + (direction.z * 0.5),
149
- },
145
+ origin: { x, y: y + cameraYOffset, z },
150
146
  direction
151
147
  };
152
148
  }
@@ -13,9 +13,15 @@ export default class ItemFactory {
13
13
  case 'auto-shotgun':
14
14
  itemModule = await import('./weapons/AutoShotgunEntity');
15
15
  break;
16
+ case 'auto-sniper':
17
+ itemModule = await import('./weapons/AutoSniperEntity');
18
+ break;
16
19
  case 'bolt-action-sniper':
17
20
  itemModule = await import('./weapons/BoltActionSniperEntity');
18
21
  break;
22
+ case 'gravity-potion':
23
+ itemModule = await import('./items/GravityPotionEntity');
24
+ break;
19
25
  case 'light-machine-gun':
20
26
  itemModule = await import('./weapons/LightMachineGunEntity');
21
27
  break;
@@ -28,9 +34,15 @@ export default class ItemFactory {
28
34
  case 'pistol':
29
35
  itemModule = await import('./weapons/PistolEntity');
30
36
  break;
37
+ case 'revolver':
38
+ itemModule = await import('./weapons/RevolverEntity');
39
+ break;
31
40
  case 'rocket-launcher':
32
41
  itemModule = await import('./weapons/RocketLauncherEntity');
33
42
  break;
43
+ case 'submachine-gun':
44
+ itemModule = await import('./weapons/SubmachineGunEntity');
45
+ break;
34
46
  case 'shotgun':
35
47
  itemModule = await import('./weapons/ShotgunEntity');
36
48
  break;
@@ -0,0 +1,48 @@
1
+ import { Quaternion } from 'hytopia';
2
+ import ItemEntity from "../ItemEntity";
3
+ import GamePlayerEntity from '../GamePlayerEntity';
4
+ import type { ItemEntityOptions } from "../ItemEntity";
5
+
6
+ const GRAVITY_SCALE = 0.3;
7
+ const GRAVITY_DURATION_MS = 15 * 1000; // 15 seconds
8
+
9
+ const DEFAULT_GRAVITY_POTION_OPTIONS: ItemEntityOptions = {
10
+ heldHand: 'right',
11
+ iconImageUri: 'icons/gravity-potion.png',
12
+ idleAnimation: 'idle_gun_right',
13
+ mlAnimation: 'shoot_gun_right',
14
+ modelUri: 'models/items/gravity-potion.glb',
15
+ modelScale: 0.4,
16
+ name: 'Gravity Potion',
17
+ consumable: true,
18
+ consumeAudioUri: 'audio/sfx/potion-consume.mp3',
19
+ consumeTimeMs: 1000,
20
+ quantity: 1,
21
+ }
22
+
23
+ export default class GravityPotionEntity extends ItemEntity {
24
+ public constructor(options: Partial<ItemEntityOptions> = {}) {
25
+ super({ ...DEFAULT_GRAVITY_POTION_OPTIONS, ...options });
26
+ }
27
+
28
+ public override consume(): void {
29
+ if (!(this.parent instanceof GamePlayerEntity)) {
30
+ return;
31
+ }
32
+
33
+ const parent = this.parent as GamePlayerEntity;
34
+
35
+ // Apply gravity potion effect
36
+ parent.setGravity(GRAVITY_SCALE);
37
+ setTimeout(() => parent.setGravity(1), GRAVITY_DURATION_MS);
38
+
39
+ super.consume();
40
+ }
41
+
42
+ public override equip(): void {
43
+ super.equip();
44
+
45
+ this.setPosition({ x: 0, y: 0.15, z: -0.2 });
46
+ this.setRotation(Quaternion.fromEuler(-90, 0, 0));
47
+ }
48
+ }
@@ -14,7 +14,7 @@ const DEFAULT_SHIELD_POTION_OPTIONS: ItemEntityOptions = {
14
14
  modelScale: 0.4,
15
15
  name: 'Shield Potion',
16
16
  consumable: true,
17
- consumeAudioUri: 'audio/sfx/shield-potion-consume.mp3',
17
+ consumeAudioUri: 'audio/sfx/potion-consume.mp3',
18
18
  consumeTimeMs: 1000,
19
19
  quantity: 1,
20
20
  }
@@ -16,7 +16,7 @@ const DEFAULT_AK47_OPTIONS: GunEntityOptions = {
16
16
  scopeZoom: 2,
17
17
  modelUri: 'models/items/ak-47.glb',
18
18
  modelScale: 1.3,
19
- range: 70,
19
+ range: 80,
20
20
  reloadAudioUri: 'audio/sfx/rifle-reload.mp3',
21
21
  reloadTimeMs: 2200,
22
22
  shootAudioUri: 'audio/sfx/rifle-shoot.mp3',
@@ -4,7 +4,7 @@ import type { GunEntityOptions } from '../GunEntity';
4
4
 
5
5
  const DEFAULT_AUTO_SHOTGUN_OPTIONS: GunEntityOptions = {
6
6
  ammo: 6,
7
- damage: 10, // Per pellet (7 pellets = 70 max damage)
7
+ damage: 11,
8
8
  fireRate: 1.5,
9
9
  heldHand: 'both',
10
10
  iconImageUri: 'icons/auto-shotgun.png',
@@ -15,7 +15,7 @@ const DEFAULT_AUTO_SHOTGUN_OPTIONS: GunEntityOptions = {
15
15
  totalAmmo: 30,
16
16
  modelUri: 'models/items/auto-shotgun.glb',
17
17
  modelScale: 1.2,
18
- range: 8,
18
+ range: 10,
19
19
  reloadAudioUri: 'audio/sfx/shotgun-reload.mp3',
20
20
  reloadTimeMs: 3500,
21
21
  shootAudioUri: 'audio/sfx/shotgun-shoot.mp3',
@@ -0,0 +1,43 @@
1
+ import { Quaternion, Vector3Like, QuaternionLike } from 'hytopia';
2
+ import GunEntity from '../GunEntity';
3
+ import type { GunEntityOptions } from '../GunEntity';
4
+
5
+ const DEFAULT_AUTO_SNIPER_OPTIONS: GunEntityOptions = {
6
+ ammo: 10,
7
+ damage: 50,
8
+ fireRate: 1.5,
9
+ heldHand: 'both',
10
+ iconImageUri: 'icons/auto-sniper.png',
11
+ idleAnimation: 'idle_gun_both',
12
+ mlAnimation: 'shoot_gun_both',
13
+ name: 'Auto Sniper',
14
+ maxAmmo: 10,
15
+ totalAmmo: 20,
16
+ scopeZoom: 5,
17
+ modelUri: 'models/items/auto-sniper.glb',
18
+ modelScale: 1.3,
19
+ range: 100,
20
+ reloadAudioUri: 'audio/sfx/sniper-reload.mp3',
21
+ reloadTimeMs: 2200,
22
+ shootAudioUri: 'audio/sfx/sniper-shoot.mp3',
23
+ };
24
+
25
+ export default class AutoSniperEntity extends GunEntity {
26
+ public constructor(options: Partial<GunEntityOptions> = {}) {
27
+ super({ ...DEFAULT_AUTO_SNIPER_OPTIONS, ...options });
28
+ }
29
+
30
+ public override shoot(): void {
31
+ if (!this.parent || !this.processShoot()) return;
32
+
33
+ super.shoot();
34
+ }
35
+
36
+ public override getMuzzleFlashPositionRotation(): { position: Vector3Like, rotation: QuaternionLike } {
37
+ return {
38
+ position: { x: 0, y: 0.01, z: -2.7 },
39
+ rotation: Quaternion.fromEuler(0, 90, 0),
40
+ };
41
+ }
42
+ }
43
+
@@ -4,7 +4,7 @@ import type { GunEntityOptions } from '../GunEntity';
4
4
 
5
5
  const DEFAULT_LIGHT_MACHINE_GUN_OPTIONS: GunEntityOptions = {
6
6
  ammo: 50,
7
- damage: 15,
7
+ damage: 9,
8
8
  fireRate: 10,
9
9
  heldHand: 'both',
10
10
  iconImageUri: 'icons/light-machine-gun.png',
@@ -13,10 +13,10 @@ const DEFAULT_LIGHT_MACHINE_GUN_OPTIONS: GunEntityOptions = {
13
13
  name: 'Light Machine Gun',
14
14
  maxAmmo: 50,
15
15
  totalAmmo: 300,
16
- scopeZoom: 1.5,
16
+ scopeZoom: 1.35,
17
17
  modelUri: 'models/items/light-machine-gun.glb',
18
18
  modelScale: 1.3,
19
- range: 50,
19
+ range: 60,
20
20
  reloadAudioUri: 'audio/sfx/machine-gun-reload.mp3',
21
21
  reloadTimeMs: 4200,
22
22
  shootAudioUri: 'audio/sfx/machine-gun-shoot.mp3',
@@ -0,0 +1,46 @@
1
+ import { Quaternion, Vector3Like, QuaternionLike } from 'hytopia';
2
+ import GunEntity from '../GunEntity';
3
+ import type { GunEntityOptions } from '../GunEntity';
4
+ import type GamePlayerEntity from '../GamePlayerEntity';
5
+
6
+ const DEFAULT_REVOLVER_OPTIONS: GunEntityOptions = {
7
+ ammo: 6,
8
+ damage: 45,
9
+ fireRate: 2,
10
+ heldHand: 'right',
11
+ iconImageUri: 'icons/revolver.png',
12
+ idleAnimation: 'idle_gun_right',
13
+ mlAnimation: 'shoot_gun_right',
14
+ name: 'Revolver',
15
+ maxAmmo: 6,
16
+ totalAmmo: 24,
17
+ modelUri: 'models/items/revolver.glb',
18
+ modelScale: 1.3,
19
+ range: 30,
20
+ reloadAudioUri: 'audio/sfx/pistol-reload.mp3',
21
+ reloadTimeMs: 2000,
22
+ shootAudioUri: 'audio/sfx/rifle-shoot.mp3',
23
+ };
24
+
25
+ export default class RevolverEntity extends GunEntity {
26
+ public constructor(options: Partial<GunEntityOptions> = {}) {
27
+ super({ ...DEFAULT_REVOLVER_OPTIONS, ...options });
28
+ }
29
+
30
+ public override shoot(): void {
31
+ if (!this.parent || !this.processShoot()) return;
32
+
33
+ super.shoot();
34
+
35
+ // Cancel input since pistol requires click-to-shoot
36
+ (this.parent as GamePlayerEntity).player.input.ml = false;
37
+ }
38
+
39
+ public override getMuzzleFlashPositionRotation(): { position: Vector3Like, rotation: QuaternionLike } {
40
+ return {
41
+ position: { x: 0.03, y: 0.18, z: -0.7 },
42
+ rotation: Quaternion.fromEuler(0, 90, 0),
43
+ };
44
+ }
45
+ }
46
+
@@ -1,4 +1,4 @@
1
- import { Audio, Entity, Quaternion, Vector3Like, QuaternionLike, RigidBodyType, EntityEvent, Vector3 } from 'hytopia';
1
+ import { Audio, CollisionGroup, Entity, Quaternion, Vector3Like, QuaternionLike, RigidBodyType, EntityEvent, Vector3, Collider } from 'hytopia';
2
2
  import GunEntity from '../GunEntity';
3
3
  import { BEDROCK_BLOCK_ID } from '../../gameConfig';
4
4
  import type { GunEntityOptions } from '../GunEntity';
@@ -64,6 +64,15 @@ export default class RocketLauncherEntity extends GunEntity {
64
64
  modelScale: 0.75,
65
65
  rigidBodyOptions: {
66
66
  type: RigidBodyType.KINEMATIC_VELOCITY,
67
+ colliders: [
68
+ {
69
+ ...Collider.optionsFromModelUri('models/items/rocket-missile.glb', 0.75),
70
+ collisionGroups: {
71
+ belongsTo: [ CollisionGroup.ENTITY ],
72
+ collidesWith: [ CollisionGroup.BLOCK ],
73
+ }
74
+ },
75
+ ],
67
76
  linearVelocity: {
68
77
  x: direction.x * 30,
69
78
  y: direction.y * 30,
@@ -71,7 +80,7 @@ export default class RocketLauncherEntity extends GunEntity {
71
80
  },
72
81
  }
73
82
  });
74
-
83
+
75
84
  // Create a despawn timer if it doesn't hit
76
85
  setTimeout(() => {
77
86
  if (rocketMissileEntity.isSpawned) {
@@ -5,7 +5,7 @@ import type GamePlayerEntity from '../GamePlayerEntity';
5
5
 
6
6
  const DEFAULT_SHOTGUN_OPTIONS: GunEntityOptions = {
7
7
  ammo: 4,
8
- damage: 12, // Per pellet (7 pellets = 84 max damage)
8
+ damage: 13,
9
9
  fireRate: 0.8,
10
10
  heldHand: 'both',
11
11
  iconImageUri: 'icons/shotgun.png',