crimsonland 0.1.0.dev4__py3-none-any.whl → 0.1.0.dev6__py3-none-any.whl

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.
crimson/projectiles.py CHANGED
@@ -112,7 +112,7 @@ class SecondaryProjectile:
112
112
  vel_y: float = 0.0
113
113
  type_id: int = 0
114
114
  owner_id: int = -100
115
- lifetime: float = 0.0
115
+ trail_timer: float = 0.0
116
116
  target_id: int = -1
117
117
 
118
118
 
@@ -220,6 +220,7 @@ class ProjectilePool:
220
220
  damage_scale_by_type: dict[int, float] | None = None,
221
221
  damage_scale_default: float = 1.0,
222
222
  ion_aoe_scale: float = 1.0,
223
+ detail_preset: int = 5,
223
224
  rng: Callable[[], int] | None = None,
224
225
  runtime_state: object | None = None,
225
226
  players: list[PlayerDamageable] | None = None,
@@ -280,6 +281,12 @@ class ProjectilePool:
280
281
  if rng is None:
281
282
  rng = _rng_zero
282
283
 
284
+ effects = None
285
+ sfx_queue = None
286
+ if runtime_state is not None:
287
+ effects = getattr(runtime_state, "effects", None)
288
+ sfx_queue = getattr(runtime_state, "sfx_queue", None)
289
+
283
290
  hits: list[tuple[int, float, float, float, float, float, float]] = []
284
291
  margin = 64.0
285
292
 
@@ -292,6 +299,170 @@ class ProjectilePool:
292
299
  def _damage_type_for() -> int:
293
300
  return 1
294
301
 
302
+ def _spawn_ion_hit_effects(type_id: int, pos_x: float, pos_y: float) -> None:
303
+ if effects is None or not hasattr(effects, "spawn"):
304
+ return
305
+
306
+ ring_scale = 0.0
307
+ ring_strength = 0.0
308
+ burst_scale = 0.0
309
+ if type_id == int(ProjectileTypeId.ION_MINIGUN):
310
+ ring_scale = 1.5
311
+ ring_strength = 0.1
312
+ burst_scale = 0.8
313
+ elif type_id == int(ProjectileTypeId.ION_RIFLE):
314
+ ring_scale = 1.2
315
+ ring_strength = 0.4
316
+ burst_scale = 1.2
317
+ elif type_id == int(ProjectileTypeId.ION_CANNON):
318
+ ring_scale = 1.0
319
+ ring_strength = 1.0
320
+ burst_scale = 2.2
321
+ if isinstance(sfx_queue, list):
322
+ sfx_queue.append("sfx_shockwave")
323
+ else:
324
+ return
325
+
326
+ detail = int(detail_preset)
327
+
328
+ # Port of `FUN_0042f270(pos, ring_scale, ring_strength)`: ring burst (effect_id=1).
329
+ effects.spawn(
330
+ effect_id=1,
331
+ pos_x=float(pos_x),
332
+ pos_y=float(pos_y),
333
+ vel_x=0.0,
334
+ vel_y=0.0,
335
+ rotation=0.0,
336
+ scale=1.0,
337
+ half_width=4.0,
338
+ half_height=4.0,
339
+ age=0.0,
340
+ lifetime=float(ring_strength) * 0.8,
341
+ flags=0x19,
342
+ color_r=0.6,
343
+ color_g=0.6,
344
+ color_b=0.9,
345
+ color_a=1.0,
346
+ rotation_step=0.0,
347
+ scale_step=float(ring_scale) * 45.0,
348
+ detail_preset=detail,
349
+ )
350
+
351
+ # Port of `FUN_0042f540(pos, burst_scale)`: burst cloud (effect_id=0).
352
+ burst = float(burst_scale) * 0.8
353
+ lifetime = min(burst * 0.7, 1.1)
354
+ half = burst * 32.0
355
+ count = int(half)
356
+ if detail < 3:
357
+ count //= 2
358
+
359
+ for _ in range(max(0, count)):
360
+ rotation = float(int(rng()) & 0x7F) * 0.049087387
361
+ vel_x = float((int(rng()) & 0x7F) - 0x40) * burst * 1.4
362
+ vel_y = float((int(rng()) & 0x7F) - 0x40) * burst * 1.4
363
+ scale_step = (float(int(rng()) % 100) * 0.01 + 0.1) * burst
364
+ effects.spawn(
365
+ effect_id=0,
366
+ pos_x=float(pos_x),
367
+ pos_y=float(pos_y),
368
+ vel_x=vel_x,
369
+ vel_y=vel_y,
370
+ rotation=rotation,
371
+ scale=1.0,
372
+ half_width=half,
373
+ half_height=half,
374
+ age=0.0,
375
+ lifetime=float(lifetime),
376
+ flags=0x1D,
377
+ color_r=0.4,
378
+ color_g=0.5,
379
+ color_b=1.0,
380
+ color_a=0.5,
381
+ rotation_step=0.0,
382
+ scale_step=scale_step,
383
+ detail_preset=detail,
384
+ )
385
+
386
+ def _spawn_plasma_cannon_hit_effects(pos_x: float, pos_y: float) -> None:
387
+ """Port of `projectile_update` Plasma Cannon hit extras.
388
+
389
+ Native does:
390
+ - `sfx_play_panned(sfx_explosion_medium)`
391
+ - `sfx_play_panned(sfx_shockwave)`
392
+ - `FUN_0042f330(pos, 1.5, 1.0)`
393
+ - `FUN_0042f330(pos, 1.0, 1.0)`
394
+ """
395
+
396
+ if effects is None or not hasattr(effects, "spawn"):
397
+ return
398
+
399
+ if isinstance(sfx_queue, list):
400
+ sfx_queue.append("sfx_explosion_medium")
401
+ sfx_queue.append("sfx_shockwave")
402
+
403
+ detail = int(detail_preset)
404
+
405
+ def _spawn_ring(*, scale: float) -> None:
406
+ effects.spawn(
407
+ effect_id=1,
408
+ pos_x=float(pos_x),
409
+ pos_y=float(pos_y),
410
+ vel_x=0.0,
411
+ vel_y=0.0,
412
+ rotation=0.0,
413
+ scale=1.0,
414
+ half_width=4.0,
415
+ half_height=4.0,
416
+ age=0.1,
417
+ lifetime=1.0,
418
+ flags=0x19,
419
+ color_r=0.9,
420
+ color_g=0.6,
421
+ color_b=0.3,
422
+ color_a=1.0,
423
+ rotation_step=0.0,
424
+ scale_step=float(scale) * 45.0,
425
+ detail_preset=detail,
426
+ )
427
+
428
+ _spawn_ring(scale=1.5)
429
+ _spawn_ring(scale=1.0)
430
+
431
+ def _spawn_splitter_hit_effects(pos_x: float, pos_y: float) -> None:
432
+ """Port of `FUN_0042f3f0(pos, 26.0, 3)` from the Splitter Gun hit branch."""
433
+
434
+ if effects is None or not hasattr(effects, "spawn"):
435
+ return
436
+
437
+ detail = int(detail_preset)
438
+ for _ in range(3):
439
+ angle = float(int(rng()) & 0x1FF) * (math.tau / 512.0)
440
+ radius = float(int(rng()) % 26)
441
+ jitter_age = -float(int(rng()) & 0xFF) * 0.0012
442
+ lifetime = 0.1 - jitter_age
443
+
444
+ effects.spawn(
445
+ effect_id=0,
446
+ pos_x=float(pos_x) + math.cos(angle) * radius,
447
+ pos_y=float(pos_y) + math.sin(angle) * radius,
448
+ vel_x=0.0,
449
+ vel_y=0.0,
450
+ rotation=0.0,
451
+ scale=1.0,
452
+ half_width=4.0,
453
+ half_height=4.0,
454
+ age=jitter_age,
455
+ lifetime=lifetime,
456
+ flags=0x19,
457
+ color_r=1.0,
458
+ color_g=0.9,
459
+ color_b=0.1,
460
+ color_a=1.0,
461
+ rotation_step=0.0,
462
+ scale_step=55.0,
463
+ detail_preset=detail,
464
+ )
465
+
295
466
  def _apply_damage_to_creature(
296
467
  creature_index: int,
297
468
  damage: float,
@@ -476,54 +647,6 @@ class ProjectilePool:
476
647
  acc_x = 0.0
477
648
  acc_y = 0.0
478
649
 
479
- if proj.hits_players:
480
- hit_player_idx = None
481
- if players is not None:
482
- for idx, player in enumerate(players):
483
- if float(player.health) <= 0.0:
484
- continue
485
- player_radius = _hit_radius_for(player)
486
- hit_r = proj.hit_radius + player_radius
487
- if _distance_sq(proj.pos_x, proj.pos_y, player.pos_x, player.pos_y) <= hit_r * hit_r:
488
- hit_player_idx = idx
489
- break
490
-
491
- if hit_player_idx is None:
492
- step += 3
493
- continue
494
-
495
- type_id = proj.type_id
496
- hit_x = float(proj.pos_x)
497
- hit_y = float(proj.pos_y)
498
- player = players[int(hit_player_idx)] if players is not None else None
499
- target_x = float(getattr(player, "pos_x", hit_x) if player is not None else hit_x)
500
- target_y = float(getattr(player, "pos_y", hit_y) if player is not None else hit_y)
501
- hits.append((type_id, proj.origin_x, proj.origin_y, hit_x, hit_y, target_x, target_y))
502
-
503
- if proj.life_timer != 0.25 and type_id not in (
504
- ProjectileTypeId.FIRE_BULLETS,
505
- ProjectileTypeId.GAUSS_GUN,
506
- ProjectileTypeId.BLADE_GUN,
507
- ):
508
- proj.life_timer = 0.25
509
- jitter = rng() & 3
510
- proj.pos_x += dir_x * float(jitter)
511
- proj.pos_y += dir_y * float(jitter)
512
-
513
- dist = math.hypot(proj.origin_x - proj.pos_x, proj.origin_y - proj.pos_y)
514
- if dist < 50.0:
515
- dist = 50.0
516
-
517
- damage_scale = _damage_scale(type_id)
518
- damage_amount = ((100.0 / dist) * damage_scale * 30.0 + 10.0) * 0.95
519
- if damage_amount > 0.0:
520
- if apply_player_damage is not None:
521
- apply_player_damage(int(hit_player_idx), float(damage_amount))
522
- elif players is not None:
523
- players[int(hit_player_idx)].health -= float(damage_amount)
524
-
525
- break
526
-
527
650
  hit_idx = None
528
651
  for idx, creature in enumerate(creatures):
529
652
  if creature.hp <= 0.0:
@@ -537,6 +660,61 @@ class ProjectilePool:
537
660
  break
538
661
 
539
662
  if hit_idx is None:
663
+ if proj.hits_players:
664
+ hit_player_idx = None
665
+ owner_id = int(proj.owner_id)
666
+ owner_player_index = -1 - owner_id if owner_id < 0 and owner_id != -100 else None
667
+ if players is not None:
668
+ for idx, player in enumerate(players):
669
+ if owner_player_index is not None and idx == owner_player_index:
670
+ continue
671
+ if float(player.health) <= 0.0:
672
+ continue
673
+ player_radius = _hit_radius_for(player)
674
+ hit_r = proj.hit_radius + player_radius
675
+ if (
676
+ _distance_sq(proj.pos_x, proj.pos_y, player.pos_x, player.pos_y)
677
+ <= hit_r * hit_r
678
+ ):
679
+ hit_player_idx = idx
680
+ break
681
+
682
+ if hit_player_idx is None:
683
+ step += 3
684
+ continue
685
+
686
+ type_id = proj.type_id
687
+ hit_x = float(proj.pos_x)
688
+ hit_y = float(proj.pos_y)
689
+ player = players[int(hit_player_idx)] if players is not None else None
690
+ target_x = float(getattr(player, "pos_x", hit_x) if player is not None else hit_x)
691
+ target_y = float(getattr(player, "pos_y", hit_y) if player is not None else hit_y)
692
+ hits.append((type_id, proj.origin_x, proj.origin_y, hit_x, hit_y, target_x, target_y))
693
+
694
+ if proj.life_timer != 0.25 and type_id not in (
695
+ ProjectileTypeId.FIRE_BULLETS,
696
+ ProjectileTypeId.GAUSS_GUN,
697
+ ProjectileTypeId.BLADE_GUN,
698
+ ):
699
+ proj.life_timer = 0.25
700
+ jitter = rng() & 3
701
+ proj.pos_x += dir_x * float(jitter)
702
+ proj.pos_y += dir_y * float(jitter)
703
+
704
+ dist = math.hypot(proj.origin_x - proj.pos_x, proj.origin_y - proj.pos_y)
705
+ if dist < 50.0:
706
+ dist = 50.0
707
+
708
+ damage_scale = _damage_scale(type_id)
709
+ damage_amount = ((100.0 / dist) * damage_scale * 30.0 + 10.0) * 0.95
710
+ if damage_amount > 0.0:
711
+ if apply_player_damage is not None:
712
+ apply_player_damage(int(hit_player_idx), float(damage_amount))
713
+ elif players is not None:
714
+ players[int(hit_player_idx)].health -= float(damage_amount)
715
+
716
+ break
717
+
540
718
  step += 3
541
719
  continue
542
720
 
@@ -548,6 +726,7 @@ class ProjectilePool:
548
726
  creature.flags |= CreatureFlags.SELF_DAMAGE_TICK
549
727
 
550
728
  if type_id == ProjectileTypeId.SPLITTER_GUN:
729
+ _spawn_splitter_hit_effects(proj.pos_x, proj.pos_y)
551
730
  self.spawn(
552
731
  pos_x=proj.pos_x,
553
732
  pos_y=proj.pos_y,
@@ -598,6 +777,8 @@ class ProjectilePool:
598
777
  if type_id == ProjectileTypeId.ION_RIFLE:
599
778
  if runtime_state is not None and getattr(runtime_state, "shock_chain_projectile_id", -1) == proj_index:
600
779
  proj.reserved = float(int(hit_idx) + 1)
780
+ if type_id in (ProjectileTypeId.ION_MINIGUN, ProjectileTypeId.ION_RIFLE, ProjectileTypeId.ION_CANNON):
781
+ _spawn_ion_hit_effects(int(type_id), target_x, target_y)
601
782
  elif type_id == ProjectileTypeId.PLASMA_CANNON:
602
783
  size = float(getattr(creature, "size", 50.0) or 50.0)
603
784
  ring_radius = size * 0.5 + 1.0
@@ -617,6 +798,7 @@ class ProjectilePool:
617
798
  owner_id=-100,
618
799
  base_damage=plasma_meta,
619
800
  )
801
+ _spawn_plasma_cannon_hit_effects(proj.pos_x, proj.pos_y)
620
802
  elif type_id == ProjectileTypeId.SHRINKIFIER:
621
803
  if hasattr(creature, "size"):
622
804
  new_size = float(getattr(creature, "size", 50.0) or 50.0) * 0.65
@@ -821,12 +1003,13 @@ class SecondaryProjectilePool:
821
1003
  entry.pos_y = float(pos_y)
822
1004
  entry.owner_id = int(owner_id)
823
1005
  entry.target_id = -1
1006
+ entry.trail_timer = 0.0
824
1007
 
825
1008
  if entry.type_id == 3:
1009
+ # Detonation state: `vel_x` becomes the expansion timer and `vel_y` the scale.
826
1010
  entry.vel_x = 0.0
827
- entry.vel_y = 0.0
1011
+ entry.vel_y = float(time_to_live)
828
1012
  entry.speed = float(time_to_live)
829
- entry.lifetime = 0.0
830
1013
  return index
831
1014
 
832
1015
  # Effects.md: vel = cos/sin(angle - PI/2) * 90 (190 for type 2).
@@ -838,7 +1021,6 @@ class SecondaryProjectilePool:
838
1021
  entry.vel_x = vx
839
1022
  entry.vel_y = vy
840
1023
  entry.speed = float(time_to_live)
841
- entry.lifetime = 0.0
842
1024
  return index
843
1025
 
844
1026
  def iter_active(self) -> list[SecondaryProjectile]:
@@ -873,6 +1055,7 @@ class SecondaryProjectilePool:
873
1055
  rand = _rng_zero
874
1056
  freeze_active = False
875
1057
  effects = None
1058
+ sprite_effects = None
876
1059
  sfx_queue = None
877
1060
  if runtime_state is not None:
878
1061
  rng = getattr(runtime_state, "rng", None)
@@ -884,6 +1067,7 @@ class SecondaryProjectilePool:
884
1067
  freeze_active = True
885
1068
 
886
1069
  effects = getattr(runtime_state, "effects", None)
1070
+ sprite_effects = getattr(runtime_state, "sprite_effects", None)
887
1071
  sfx_queue = getattr(runtime_state, "sfx_queue", None)
888
1072
 
889
1073
  for entry in self._entries:
@@ -891,9 +1075,10 @@ class SecondaryProjectilePool:
891
1075
  continue
892
1076
 
893
1077
  if entry.type_id == 3:
894
- entry.lifetime += dt * 3.0
895
- t = entry.lifetime
896
- scale = entry.speed
1078
+ # Detonation: `vel_x` becomes the expansion timer (0..1) and `vel_y` the scale.
1079
+ entry.vel_x += dt * 3.0
1080
+ t = float(entry.vel_x)
1081
+ scale = float(entry.vel_y)
897
1082
  if t > 1.0:
898
1083
  if fx_queue is not None:
899
1084
  fx_queue.add(
@@ -973,6 +1158,23 @@ class SecondaryProjectilePool:
973
1158
 
974
1159
  entry.speed -= dt * 0.5
975
1160
 
1161
+ # Rocket smoke trail (`trail_timer` in crimsonland.exe).
1162
+ entry.trail_timer -= (abs(float(entry.vel_x)) + abs(float(entry.vel_y))) * dt * 0.01
1163
+ if entry.trail_timer < 0.0:
1164
+ dir_x = math.cos(float(entry.angle) - math.pi / 2.0)
1165
+ dir_y = math.sin(float(entry.angle) - math.pi / 2.0)
1166
+ spawn_x = float(entry.pos_x) - dir_x * 9.0
1167
+ spawn_y = float(entry.pos_y) - dir_y * 9.0
1168
+ vel_x = math.cos(float(entry.angle) + math.pi / 2.0) * 90.0
1169
+ vel_y = math.sin(float(entry.angle) + math.pi / 2.0) * 90.0
1170
+ if sprite_effects is not None and hasattr(sprite_effects, "spawn"):
1171
+ sprite_id = sprite_effects.spawn(pos_x=spawn_x, pos_y=spawn_y, vel_x=vel_x, vel_y=vel_y, scale=14.0)
1172
+ try:
1173
+ sprite_effects.entries[int(sprite_id)].color_a = 0.25
1174
+ except Exception:
1175
+ pass
1176
+ entry.trail_timer = 0.06
1177
+
976
1178
  # projectile_update uses creature_find_in_radius(..., 8.0, ...)
977
1179
  hit_idx: int | None = None
978
1180
  for idx, creature in enumerate(creatures):
@@ -1025,16 +1227,70 @@ class SecondaryProjectilePool:
1025
1227
  rand=rand,
1026
1228
  )
1027
1229
 
1230
+ if entry.type_id == 1 and effects is not None and hasattr(effects, "spawn_explosion_burst") and int(detail_preset) > 2:
1231
+ effects.spawn_explosion_burst(
1232
+ pos_x=float(entry.pos_x),
1233
+ pos_y=float(entry.pos_y),
1234
+ scale=0.4,
1235
+ rand=rand,
1236
+ detail_preset=int(detail_preset),
1237
+ )
1238
+
1028
1239
  entry.type_id = 3
1029
1240
  entry.vel_x = 0.0
1030
- entry.vel_y = 0.0
1031
- entry.speed = det_scale
1032
- entry.lifetime = 0.0
1241
+ entry.vel_y = float(det_scale)
1242
+ entry.trail_timer = 0.0
1243
+
1244
+ # Extra debris and scorch decals on detonation.
1245
+ if not freeze_active:
1246
+ extra_decals = 0
1247
+ extra_radius = 0.0
1248
+ if entry.type_id == 3:
1249
+ # NOTE: entry.type_id is already 3 here; use det_scale based on prior type.
1250
+ if det_scale == 1.0:
1251
+ extra_decals = 0x14
1252
+ extra_radius = 90.0
1253
+ elif det_scale == 0.35:
1254
+ extra_decals = 10
1255
+ extra_radius = 63.0
1256
+ elif det_scale == 0.25:
1257
+ extra_decals = 3
1258
+ extra_radius = 44.0
1259
+ if fx_queue is not None and extra_decals > 0:
1260
+ cx = float(creatures[hit_idx].x)
1261
+ cy = float(creatures[hit_idx].y)
1262
+ for _ in range(int(extra_decals)):
1263
+ angle = float(int(rand()) % 0x274) * 0.01
1264
+ radius = float(int(rand()) % max(1, int(extra_radius)))
1265
+ fx_queue.add_random(
1266
+ pos_x=cx + math.cos(angle) * radius,
1267
+ pos_y=cy + math.sin(angle) * radius,
1268
+ rand=rand,
1269
+ )
1270
+
1271
+ if sprite_effects is not None and hasattr(sprite_effects, "spawn"):
1272
+ step = math.tau / 10.0
1273
+ for idx in range(10):
1274
+ mag = float(int(rand()) % 800) * 0.1
1275
+ ang = float(idx) * step
1276
+ vel_x = math.cos(ang) * mag
1277
+ vel_y = math.sin(ang) * mag
1278
+ sprite_id = sprite_effects.spawn(
1279
+ pos_x=float(entry.pos_x),
1280
+ pos_y=float(entry.pos_y),
1281
+ vel_x=vel_x,
1282
+ vel_y=vel_y,
1283
+ scale=14.0,
1284
+ )
1285
+ try:
1286
+ sprite_effects.entries[int(sprite_id)].color_a = 0.37
1287
+ except Exception:
1288
+ pass
1289
+
1033
1290
  continue
1034
1291
 
1035
1292
  if entry.speed <= 0.0:
1036
1293
  entry.type_id = 3
1037
1294
  entry.vel_x = 0.0
1038
- entry.vel_y = 0.0
1039
- entry.speed = 0.5
1040
- entry.lifetime = 0.0
1295
+ entry.vel_y = 0.5
1296
+ entry.trail_timer = 0.0