Repository navigation
Expand file tree
/
Copy pathexplode.zig
More file actions
2017 lines (1827 loc) · 93.5 KB
/
Copy pathexplode.zig
File metadata and controls
2017 lines (1827 loc) · 93.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! `C:\lancer\game\explode.cpp`: an object's end, as it is seen and heard. The Explode order
//! ([`aiexplode.zig`](aiexplode.zig)) runs the ship down to it; here are the blast that ends it
//! and what the explosions leave for the frames after.
//!
//! The final blasts' sound, their bursts of flame and sparkle ([`particles.zig`](particles.zig)),
//! their fireballs, burning bits and shockwaves ([`shockwave.zig`](shockwave.zig)), the break-up
//! that cuts a ship's parts into pieces that fly apart
//! ([`explode/breakup.zig`](explode/breakup.zig)), the capital ships' splits
//! ([`explode/split.zig`](explode/split.zig)), the Ulysses' end
//! ([`explode/ulysses.zig`](explode/ulysses.zig)), the chunks of rock
//! ([`explode/chunks.zig`](explode/chunks.zig)), the Uber Explode
//! ([`explode/uber.zig`](explode/uber.zig)), the burning wrecks, and the point the camera watches a
//! break-up from.
const std = @import("std");
const Allocator = std.mem.Allocator;
const math = @import("../surrender/math.zig");
const Vector = math.Vector;
const srapiext = @import("../surrender/surrenderlib/srapiext.zig");
const srcore = @import("../surrender/surrenderlib/srcore.zig");
const srlight = @import("../surrender/surrenderlib/srlight.zig");
const srtexture = @import("../surrender/surrenderlib/srtexture.zig");
const create = @import("create.zig");
const gameobj = @import("gameobj.zig");
const Random = @import("../random.zig").Random;
const matmanager = @import("matmanager.zig");
const objects = @import("objects.zig");
const shield = @import("shield.zig");
const particles = @import("particles.zig");
pub const breakup = @import("explode/breakup.zig");
pub const rocks = @import("explode/chunks.zig");
pub const extras = @import("explode/extras.zig");
pub const split = @import("explode/split.zig");
pub const uber = @import("explode/uber.zig");
pub const ulysses = @import("explode/ulysses.zig");
const shockwave = @import("shockwave.zig");
const sound3d = @import("sound3d.zig");
const table = @import("table.zig");
const xtrabits = @import("xtrabits.zig");
const erayfx = @import("erayfx.zig");
const shp = @import("../../formats/shp.zig");
const Clock = @import("main.zig").Clock;
const ai = @import("ai.zig");
const aigeneric = @import("aigeneric.zig");
const cloak = @import("cloak.zig");
const deathmatch = @import("deathmatch.zig");
const radio = @import("radio.zig");
test {
std.testing.refAllDecls(@This());
}
/// What the explosions leave for the frames after them.
pub const Explosions = struct {
images: Images,
settings: Settings = .{},
/// The pieces the bits are made of, which a mission loads as it starts.
debris: Debris = .{},
/// The bits flying (`explosion_bits`), as many as the detail holds: the next thrown
/// (`explosion_bit_next`) takes the place of the oldest. When they last moved on
/// (`explosion_bits_moved_at`). The game also counts them (`explosion_bit_count`), which
/// nothing reads.
bits: *Bits,
moved_at: i32 = 0,
/// The pieces the ships' break-ups send flying (`0x0055AE88`).
pieces: breakup.Pieces,
/// The capital ships splitting in two (`0x0055335C`).
splits: split.Splits,
/// The chunks of rock flying (`rock_chunks`).
chunks: *rocks.Chunks,
/// The Uber Explode's hemisphere and sphere, and its blast going off.
uber: *uber.Uber,
/// The fireballs going off (`explosion_fireballs`, `0x00553398`), in as many slots as the
/// settings give them.
fireballs: [Fireballs.fuller.slots()]?Fireball = @splat(null),
/// The point view `0x1B` watches (`0x0055AD0C`), which the player's ship's break-up leaves
/// where the ship blew up; null until it has.
marker: ?Marker = null,
/// The burning wrecks' red lights (`0x0055AD24`) and their smoke (`0x0055AD60`).
burn_lights: [max_burn_lights]?BurnLight = @splat(null),
streams: [max_streams]?Stream = @splat(null),
pub const max_bits = BitPool.lasting.room(.high);
const Bits = table.Ring(Bit, max_bits);
pub const Marker = struct {
position: Vector,
/// How far it drifts a tick (`0x0055AD10`): a quarter of the ship's velocity, which is a
/// step's.
drift: Vector,
};
/// The textures an explosion shows (`explosions_init`, `0x0046B240`).
pub const Images = struct {
/// The bang's frames, `explosion\bang_00000` to `bang_00015` (`0x00558734`).
bang: [16]*srtexture.Image,
/// Nine frames in a three by three sheet, `explosion\explosion sheet` (`0x00558730`).
sheet: *srtexture.Image,
/// The flak's nine frames, three by three, which a special fireball plays (`flak04`,
/// `0x00562CCC`).
flak: *srtexture.Image,
uber: uber.Images,
/// The names `explosions_init` makes of `explosion\bang_000%02d`.
const bang_names = names: {
var names: [16][]const u8 = undefined;
for (&names, 0..) |*name, n| name.* = std.fmt.comptimePrint("explosion\\bang_000{d:0>2}", .{n});
break :names names;
};
pub fn load(textures: *srtexture.Table) (Allocator.Error || matmanager.Error)!Images {
var images: Images = undefined;
for (&images.bang, bang_names) |*image, name| image.* = try matmanager.textureRequire(textures, name);
images.sheet = try matmanager.textureRequire(textures, "explosion\\explosion sheet");
images.flak = try matmanager.textureRequire(textures, "flak04");
images.uber = try .load(textures);
return images;
}
};
pub fn init(gpa: Allocator, images: Images) Allocator.Error!Explosions {
const bits = try gpa.create(Bits);
errdefer gpa.destroy(bits);
bits.* = .{};
const chunks = try gpa.create(rocks.Chunks);
errdefer gpa.destroy(chunks);
chunks.* = .{};
const huge: *uber.Uber = try .create(gpa, images.uber);
errdefer huge.destroy(gpa);
return .{ .images = images, .bits = bits, .chunks = chunks, .uber = huge, .pieces = try .create(gpa), .splits = .init(gpa) };
}
pub fn deinit(explosions: *Explosions) void {
explosions.pieces.gpa.destroy(explosions.bits);
explosions.pieces.gpa.destroy(explosions.chunks);
explosions.uber.destroy(explosions.pieces.gpa);
explosions.pieces.deinit();
explosions.splits.deinit();
}
/// As a mission starts again: nothing flying or going off, and no marker, with the same
/// settings; the debris is loaded again (`Debris.load`).
pub fn reset(explosions: *Explosions) void {
explosions.pieces.reset();
explosions.splits.reset();
explosions.bits.* = .{};
explosions.chunks.* = .{};
explosions.uber.blast = null;
explosions.* = .{ .images = explosions.images, .settings = explosions.settings, .bits = explosions.bits, .chunks = explosions.chunks, .uber = explosions.uber, .pieces = explosions.pieces, .splits = explosions.splits };
}
/// `explosions_update` (`0x0046E480`), once a frame: the Uber Explode goes on
/// (`uber.Uber.frame`), the marker drifts on, the bits fly on, the wrecks burn on
/// (`burnFrame`), the pieces fly on (`breakup.Pieces.frame`), the splits go on
/// (`split.Splits.frame`), each fireball plays on (`Fireball.frame`) until it is done, and the
/// chunks of rock fly on (`rocks.Chunks.frame`).
///
/// **Improvement:** the marker drifts by `drift` a tick, where the game adds it once a frame,
/// which comes to the same at a frame a tick.
pub fn frame(explosions: *Explosions, world: gameobj.World) void {
explosions.uber.frame(world);
const clock = world.clock;
if (explosions.marker) |*marker| marker.position += marker.drift * @as(Vector, @splat(@floatFromInt(clock.frameTicks())));
const ticks: f32 = @floatFromInt(clock.frame_start - explosions.moved_at);
const seconds = ticks * Bit.per_tick;
for (&explosions.bits.slots) |*slot| {
const bit = &(slot.* orelse continue);
if (bit.life) |life| if (bit.born + life < clock.frame_start) {
slot.* = null;
continue;
};
bit.fly(seconds);
}
explosions.burnFrame(world, ticks);
explosions.moved_at = clock.frame_start;
explosions.pieces.frame(world);
explosions.splits.frame(world);
for (&explosions.fireballs) |*slot| {
const fireball = &(slot.* orelse continue);
if (!fireball.frame(clock)) slot.* = null;
}
explosions.chunks.frame(clock);
}
/// The rest of `explosions_update`: the Uber Explode's objects, the bits and the pieces go into
/// the world's layer, the burning wrecks' lights among the lights, and each fireball showing,
/// with its light among the lights, `ahead` of a tick past the frame's tick.
pub fn draw(explosions: *Explosions, gpa: Allocator, scene: *srcore.Scene, ahead: f32) Allocator.Error!void {
try explosions.uber.draw(gpa, scene);
for (&explosions.bits.slots) |*slot| {
const bit = &(slot.* orelse continue);
bit.object.position = bit.at + bit.velocity * @as(Vector, @splat(ahead * Bit.per_tick));
try xtrabits.sceneAdd(gpa, scene, .{ .mesh = &bit.object }, .world);
}
try explosions.pieces.draw(gpa, scene, ahead);
try explosions.splits.draw(gpa, scene);
try explosions.chunks.draw(gpa, scene, ahead);
for (&explosions.burn_lights) |*slot| {
const burning = &(slot.* orelse continue);
try xtrabits.sceneAdd(gpa, scene, .{ .light = &burning.light }, .world);
}
for (&explosions.fireballs) |*slot| {
const fireball = &(slot.* orelse continue);
if (!fireball.showing) continue;
fireball.show(explosions.images, ahead);
try xtrabits.sceneAdd(gpa, scene, .{ .sprites = &fireball.set }, .world);
if (fireball.light) |*light| try xtrabits.sceneAdd(gpa, scene, .{ .light = light }, .background);
}
}
/// `explosion_bit` (`0x004717D0`): throws a piece of debris, or a body by `how`'s chance
/// (`Debris.pickFor`), out of `at` along `direction`, in the place of the oldest bit. It leaves
/// at 1500 to 4500 a second times `how`'s speed, a little off its direction, turns a random
/// way each frame, and flies for 17.5 to 22.5 seconds.
///
/// The game can also throw a chunk of rock, which no caller asks for. A piece the game has no
/// model for is not thrown.
pub fn throwBit(explosions: *Explosions, at: Vector, direction: Vector, how: Bit.Throw, clock: *const Clock, random: *Random) void {
const piece = explosions.debris.pickFor(how, random) orelse return;
const leaving = direction * @as(Vector, @splat((random.fraction() + 0.5) * Bit.speed));
const stray = random.centredVector(@splat(Bit.stray));
const velocity = math.transform(math.fromAngleVector(stray), leaving) * @as(Vector, @splat(how.speed));
const life: ?i32 = switch (explosions.settings.bit_pool) {
.lasting => null,
.original => Bit.flight.draw(random),
};
explosions.addBit(piece, at, velocity, life, clock, random);
}
/// `part_burn_lights` (`0x00471470`): a red light, `Burn light`, at the first point of each of
/// the part's lists of them, hanging from the part, in the first free slots while there are.
fn burnLights(explosions: *Explosions, on: objects.PartOf, data: shp.PartData) void {
for (data.point_lists) |list| {
if (list.kind != .light or list.points.len == 0) continue;
const slot = table.firstFree(BurnLight, &explosions.burn_lights) orelse return;
slot.* = .{
.on = on,
.at = list.points[0].position.vector(),
.light = .{ .mask = 0, .intensity = burn_intensity, .colour = burn_colour, .kind = .{ .point = .{ .position = @splat(0), .range = burn_reach } } },
};
}
}
/// `part_streams` (`0x004715D0`): a stream of `template`'s smoke from each point of each of
/// the part's lists of them, hanging from the part and leaving along the normal of the vertex
/// the point stands on, for good or for `burn_life` ticks, in the first free slots while there
/// are.
fn smoke(explosions: *Explosions, world: gameobj.World, on: objects.PartOf, data: shp.PartData, forever: bool, template: *const particles.Template) void {
const levels = on.part.part().object.levels;
const mesh = if (levels.len > 0) levels[0].mesh else null;
for (data.point_lists) |list| {
if (list.kind != .streams) continue;
for (list.points) |point| {
const slot = table.firstFree(Stream, &explosions.streams) orelse return;
const normal: Vector = if (mesh) |from| if (point.vertex < from.normals.len) from.normals[point.vertex] else @splat(0) else @splat(0);
slot.* = .{ .on = .{ .part = on }, .emitter = .{
.life = if (forever) forever_life else burn_life,
.born = world.clock.frame_start,
.place = .{ .position = point.position.vector() },
.direction = normal,
.spread = stream_spread,
.speed = stream_speed,
.speed_range = stream_speed_range,
.template = template,
} };
}
}
}
/// Streams `emitter`, hanging from or standing as `on` says, among the burning wrecks' smoke
/// (`burn_streams`), as the extras a few types carry do (`create.extra`): in the first free
/// slot, or in the first where all are taken, in place of what streams there.
///
/// **Fix:** the game always puts it in the first slot, where a burning wreck's smoke may be
/// streaming already, which then stops.
pub fn hangStream(explosions: *Explosions, on: Stream.On, emitter: particles.Emitter) void {
const slot = table.firstFree(Stream, &explosions.streams) orelse &explosions.streams[0];
slot.* = .{ .on = on, .emitter = emitter };
}
/// Lets go of the stream of `template` that hangs from the object in slot `index`, where there
/// is one (`particle_emitter_free` on the extra's emitter, which the game finds among
/// `burn_streams`). The particles it sent live on.
pub fn dropStream(explosions: *Explosions, index: u16, template: *const particles.Template) void {
for (&explosions.streams) |*slot| {
const stream = &(slot.* orelse continue);
if (stream.on.object() == index and stream.emitter.template == template) slot.* = null;
}
}
/// The burning wrecks' part of `explosions_update`, `ticks` since the bits last moved on: each
/// stream sends its smoke or sparks out (`particles.Pool.stream`), from where its part stands or
/// where it stands in the world, and goes once its life is over, and each light fades and goes
/// once it is spent, flickering meanwhile.
///
/// **Fix:** the game keeps a light or a stream hanging from its part's frame after the wreck is
/// gone; OpenReliant lets it go with the wreck.
fn burnFrame(explosions: *Explosions, world: gameobj.World, ticks: f32) void {
const all = world.objects;
for (&explosions.streams) |*slot| {
const stream = &(slot.* orelse continue);
const parent: ?math.Place = switch (stream.on) {
.part => |on| (on.live(all) orelse {
slot.* = null;
continue;
}).drawn(),
.world => null,
};
if (!streamWithin(world, &stream.emitter, parent)) slot.* = null;
}
for (&explosions.burn_lights) |*slot| {
const burning = &(slot.* orelse continue);
burning.left -= ticks * burn_fade_per_tick;
const part = burning.on.live(all) orelse {
slot.* = null;
continue;
};
const place = part.drawn();
if (!(burning.left > 0)) {
slot.* = null;
continue;
}
burning.light.intensity = 1 - world.random.fraction() * burn_flicker;
burning.light.kind.point.position = place.point(burning.at);
}
}
/// `0x00471B20`, a stream's spark (`particles.Emitter.spark`): a small piece of debris thrown
/// out of `at` at `velocity` a second, in the place of the oldest bit, turning a random way
/// each frame, for -1 to 3 seconds. One whose flight is over before it starts is let go before
/// it is drawn, having still taken the oldest bit's place. A piece the game has no model for is
/// not thrown.
pub fn throwSpark(explosions: *Explosions, at: Vector, velocity: Vector, clock: *const Clock, random: *Random) void {
const piece = explosions.debris.pick(Bit.spark_size, random) orelse return;
explosions.addBit(piece, at, velocity, Bit.spark_flight.draw(random), clock, random);
}
/// Puts `piece` in the place of the oldest bit, lit, at `at`, flying at `velocity` a second,
/// turning a random way each frame, for `life` ticks from now, or until its place is taken.
fn addBit(explosions: *Explosions, piece: Debris.Picked, at: Vector, velocity: Vector, life: ?i32, clock: *const Clock, random: *Random) void {
const spin = random.centredVector(@splat(Bit.tumble));
const settings = explosions.settings;
explosions.bits.take(settings.bit_pool.room(settings.detail)).* = .{
.born = clock.frame_start,
.life = life,
.object = .{
.flags = .{ .lit = true },
.light_mask = explosions.settings.debris_lights.loose(),
.position = at,
.scale = piece.scale,
.radius = piece.levels[0].mesh.radius,
.levels = piece.levels,
},
.at = at,
.velocity = velocity,
.spin = spin,
};
}
/// `explosion_fireball` (`0x0046BD00`): sets a fireball off at `at`, into the first free of
/// the slots the settings give, and not at all where there is none.
pub fn setOff(explosions: *Explosions, at: Vector, spec: Fireball.Spec, clock: *const Clock, random: *Random) void {
const style = explosions.settings.fireballs;
const slot = table.firstFree(Fireball, explosions.fireballs[0..style.slots()]) orelse return;
slot.* = .init(explosions.images, at, spec, style, clock, random);
}
};
/// How the explosions are shown, which a mission's restart keeps.
pub const Settings = struct {
/// The settings' graphic detail (`graphic_detail`, `0x005D54E0`).
detail: Detail = .high,
debris_lights: DebrisLights = .like_ships,
fireballs: Fireballs = .fuller,
bit_pool: BitPool = .lasting,
uber: uber.Style = .fuller,
};
/// How many burning bits the explosions keep flying, and for how long.
///
/// **Improvement:** `lasting` keeps room for 4000, where the game keeps 100, 300 or 500 by the
/// detail, and a bit an explosion throws flies on until its place is needed, where the game's go
/// after 17.5 to 22.5 seconds; a capital ship's split throws hundreds, which the game's pool lets
/// go of within seconds. A stream's spark still goes after its few seconds. `--original` restores
/// the game's.
pub const BitPool = enum {
lasting,
original,
/// Room for how many, at `detail`.
pub fn room(pool: BitPool, detail: Detail) usize {
return switch (pool) {
.lasting => 4000,
.original => detail.bits(),
};
}
};
/// How many fireballs go off at once, and how they light what is around them.
///
/// **Improvement:** `fuller` gives them room for 128, where the game keeps 30 and a burst takes 18,
/// so a second burst close after a first loses fireballs. A fireball's light moves with it as it
/// drifts, where the game leaves the light where the fireball went off, and starts 50% brighter, at
/// 15 where the game's starts at 10, so it also reaches 50% farther. Each frame of its animation
/// fades into the next (`Fireball.show`). `--original` restores the game's.
pub const Fireballs = enum {
original,
fuller,
pub fn slots(style: Fireballs) usize {
return switch (style) {
.original => 30,
.fuller => 128,
};
}
/// A fireball's light as it goes off (the game's is `0x004DC520`).
fn peak(style: Fireballs) f32 {
return switch (style) {
.original => 10,
.fuller => 15,
};
}
/// How long before the end of its `life` a fireball starts to fade out, still playing: over
/// the last fifth of it, where the game's shows in full to the end and is gone at once.
fn fadeOut(style: Fireballs, life: i32) i32 {
return switch (style) {
.original => 0,
.fuller => @divTrunc(life, 5),
};
}
};
/// Which of the backdrop's lights reach an explosion's debris: its bits and a ship's pieces.
///
/// **Improvement:** debris takes the lights a ship's part takes, one of each pair. The game makes
/// it with a light mask of 0, so both key lights and both fill lights reach it, and it shows
/// washed out; `--original` restores that.
pub const DebrisLights = enum {
like_ships,
every_light,
/// The mask for debris of a part whose own mask is `ship`.
pub fn mask(lights: DebrisLights, ship: u32) u32 {
return switch (lights) {
.like_ships => ship,
.every_light => 0,
};
}
/// The mask for a burning bit or a chunk of rock: a part's of a model that lists no
/// components.
pub fn loose(lights: DebrisLights) u32 {
return lights.mask(objects.lightMask(false));
}
};
/// The settings' graphic detail (`Gdetail`, `graphic_detail`, `0x005D54E0`; `winmain.Details`),
/// which sets how many bits the explosions keep flying, and the detail of the shields, the gates,
/// the Ice Field, the loadout's ships and the levels of detail (`main.detailDivisor`).
pub const Detail = enum(u2) {
low = 0,
medium = 1,
high = 2,
/// `explosions_init`'s count of them.
pub fn bits(detail: Detail) usize {
return switch (detail) {
.low => 100,
.medium => 300,
.high => 500,
};
}
};
/// A model's levels of detail, as a bit draws them: its first part's, their distances stretched so
/// that the bit keeps its detail further off. The game stretches the model's own.
pub const Levels = struct {
levels: [max]srapiext.Level = undefined,
count: u8 = 0,
/// The levels a bit keeps: the finest, where a model has more.
const max = 8;
fn of(from: []const srapiext.Level, stretch: f32) Levels {
var made: Levels = .{};
for (from[0..@min(from.len, max)]) |level| {
made.levels[made.count] = .{ .mesh = level.mesh, .until = level.until * stretch };
made.count += 1;
}
return made;
}
pub fn slice(levels: *const Levels) []const srapiext.Level {
return levels.levels[0..levels.count];
}
};
/// The pieces of debris the bits are made of (`explosions_init`): the ten debris types' models,
/// their distances stretched by half again (`0x0055AE60`, `0x004DC4E0`); and the bodies, the four
/// crewmen's, stretched by two and a half times (`0x00553388`, `0x004DC59C`).
pub const Debris = struct {
pieces: [count]Levels = @splat(.{}),
bodies: [body_count]Levels = @splat(.{}),
/// The chunks of rock's five models, as they are (`0x0055871C`).
rock_chunks: [rock_chunk_count]Levels = @splat(.{}),
const count = 10;
const stretch: f32 = 1.5;
const body_count = 4;
const body_stretch: f32 = 2.5;
const rock_chunk_count = 5;
/// A body is drawn 2.5 times as large, or three quarters as large for a bit no larger than a
/// tenth (`0x004DC420`).
const body_scale: f32 = 2.5;
const small_body_scale: f32 = 0.75;
const small_body: f32 = 0.1;
/// Each type's model, counted as used so that it stays loaded (`ship_type_first_levels`).
pub fn load(all: *create.Objects, types: create.Types) Debris {
var debris: Debris = .{};
loadEach(&debris.pieces, all, types, gameobj.Type.of(.debris), stretch);
loadEach(&debris.bodies, all, types, gameobj.Type.of(.crewman), body_stretch);
loadEach(&debris.rock_chunks, all, types, gameobj.Type.of(.rock_chunk), 1);
return debris;
}
/// Each of `into` from the types from `first` on, stretched by `by`.
fn loadEach(into: []Levels, all: *create.Objects, types: create.Types, first: gameobj.Type, by: f32) void {
for (into, 0..) |*levels, n| {
const found = xtrabits.firstLevels(all, types, @fromBackingInt(first.number() + @as(u32, @intCast(n)))) orelse continue;
levels.* = .of(found, by);
}
}
/// A piece for a bit thrown `how` (`explosion_bit`): a body by its chance, one of the first
/// three crewmen at random, drawn at `body_scale` or, for a small bit, `small_body_scale`;
/// otherwise a piece of debris (`pick`). A chance of one is a body without a draw. The game
/// picks the crewman as a draw times -3 back from the first, which comes to the same; a draw of
/// exactly one picks the fourth.
fn pickFor(debris: *const Debris, how: Bit.Throw, random: *Random) ?Picked {
if (!(how.bodies == 1 or random.fraction() < how.bodies)) return debris.pick(how.size, random);
const which: usize = @intFromFloat(random.fraction() * (body_count - 1));
const levels = debris.bodies[which].slice();
if (levels.len == 0) return null;
return .{ .levels = levels, .scale = if (how.size > small_body) body_scale else small_body_scale };
}
/// A piece picked for a bit, and how large it is drawn.
const Picked = struct {
levels: []const srapiext.Level,
scale: f32,
};
/// A piece for a bit, by a draw (`piece`), drawn at half to one and a half times `size` by
/// another; null, drawing no more, where the game has no model for it.
fn pick(debris: *const Debris, size: f32, random: *Random) ?Picked {
const levels = debris.piece(random.fraction()).slice();
if (levels.len == 0) return null;
return .{ .levels = levels, .scale = (random.fraction() + 0.5) * size };
}
/// The piece for a draw of `r`: the first below a quarter, the last below a half, and above,
/// one of the rest from the second on, by how far past a half it is times `piece_spread`.
fn piece(debris: *const Debris, r: f32) *const Levels {
if (r < 0.25) return &debris.pieces[0];
if (r < 0.5) return &debris.pieces[count - 1];
return &debris.pieces[1 + @as(usize, @intFromFloat((r - 0.5) * piece_spread))];
}
/// How far a draw past a half steps through the pieces from the second (`0x004DC840`, which
/// the game holds negative and subtracts).
const piece_spread: f32 = 16;
};
/// A bit an explosion throws out (0x28 bytes): a piece of debris, lit, flying off and tumbling for
/// a time.
pub const Bit = struct {
/// When it was thrown and how long it flies, in ticks (`+0x00`, `+0x04`); null for as long as
/// its place is not taken (`BitPool.lasting`).
born: i32,
life: ?i32,
object: srapiext.MeshObject,
/// Where it is at the frame's tick, which the game keeps in its object; the object is drawn
/// from here (`Explosions.draw`).
at: Vector,
/// How far it flies a second (`+0x10`), and its turn a frame, as angles (`+0x1C`).
velocity: Vector,
spin: Vector,
/// How a bit is thrown: its size, its speed, and the chance it is a body.
pub const Throw = struct {
size: f32,
speed: f32,
bodies: f32 = 0,
};
/// How fast it leaves, a second, before its throw's speed: half this to half again
/// (`0x004DC508`); and how far off its direction it may stray about each axis, and turn each
/// frame, in radians, half of it either way (`0x004DC408`, `0x004DC420`).
const speed: f32 = 3000;
const stray: f32 = 0.5;
const tumble: f32 = 0.1;
/// How long it flies (`0x004DC83C`); and a spark's size, and how long it flies
/// (`0x004DC844`).
const flight: Flight = .{ .ticks = 2000, .spread = 500 };
const spark_size: f32 = 0.1;
const spark_flight: Flight = .{ .ticks = 100, .spread = 400 };
/// How long a bit flies, in ticks, and how much more or less, half of `spread` either way.
const Flight = struct {
ticks: i32,
spread: f32,
fn draw(how_long: Flight, random: *Random) i32 {
return how_long.ticks + @as(i32, @intFromFloat(random.centred() * how_long.spread));
}
};
/// Its velocity is a second's, which is 100 ticks (`0x004DC518`).
const per_tick: f32 = 0.01;
/// Moves it on by `seconds` of its velocity, and turns it by its spin, once whatever the time.
fn fly(bit: *Bit, seconds: f32) void {
bit.at += bit.velocity * @as(Vector, @splat(seconds));
bit.object.orientation = math.product(bit.object.orientation, math.fromAngleVector(bit.spin));
}
};
/// A fireball going off (0x2C bytes): a sprite that plays an animation of fire where it was set
/// off, drifting, with a light that fades as it plays where it has one.
pub const Fireball = struct {
/// Its sprite, and the set that draws it (`ExplodeParticle BMO`); its light
/// (`ExplodeParticle Light`). A second sprite shows the next frame of its animation, which the
/// first fades into, over the bang's next texture (`fading`).
sprite: [2]srapiext.Sprite,
set: srapiext.SpriteSet,
fading: srapiext.Surface,
light: ?srlight.Light,
/// Where it is at the frame's tick, which the game keeps in its sprite, and how far it drifts
/// a tick (`+0x08`). How long it had played at the frame's tick.
at: Vector,
velocity: Vector,
age: i32 = 0,
/// When it was set off, how long it plays and how long it waits first, in ticks (`+0x14`,
/// `+0x18`, `+0x20`).
born: i32,
life: i32,
delay: i32,
look: Look,
/// Whether it is coloured by how far it has played, from black to white (`+0x24`).
lit: bool,
/// Whether it is the flak's (`+0x28`, `Spec.special`).
special: bool,
/// Whether it showed at the last frame: past its wait, and not yet done.
showing: bool = false,
/// How it plays, and how its light burns and moves.
style: Fireballs,
/// How it plays, and whether it is mirrored (`+0x1C`): a word the game builds from its kind
/// and two random bits.
pub const Look = packed struct(u32) {
/// The sheet's frames mirrored left for right, and top for bottom.
mirror_u: bool,
mirror_v: bool,
/// The bang's sixteen frames, which it plays unmirrored; otherwise the sheet's nine.
bang: bool,
_: u29 = 0,
/// The look of a fireball of `kind`, mirrored by the two lowest bits of `drawn`, a draw
/// of `rand` (`explosion_fireball`).
fn of(kind: Kind, drawn: u15) Look {
var look: Look = @bitCast(@as(u32, @as(u2, @truncate(drawn))));
look.bang = kind == .bang;
return look;
}
/// The sheet's cell `at` as a sprite's span of it, mirrored as the look says.
fn cell(look: Look, at: u32) [4]f32 {
const span = gridCell(at, sheet_step, sheet_cell);
const across: [2]f32 = if (look.mirror_u) .{ span[0][1], span[0][0] } else span[0];
const down: [2]f32 = if (look.mirror_v) .{ span[1][1], span[1][0] } else span[1];
return .{ across[0], across[1], down[0], down[1] };
}
};
/// What `explosion_fireball` is given.
pub const Spec = struct {
kind: Kind = .bang,
/// How far it reaches either side of its centre.
size: f32,
life: i32 = 150,
/// Whether it lights what is around it.
light: bool = false,
delay: i32 = 0,
lit: bool = false,
velocity: Vector = @splat(0),
/// The flak's: the flak's frames, added to what is behind, a bang playing their nine
/// cells `flak_step` apart, unmirrored.
special: bool = false,
};
pub const Kind = enum { bang, sheet };
/// The flak's frames: each a third of the image across and down from the last, and as wide
/// (`0x004DC614`).
const flak_step: f32 = 0.33;
/// The flak's cell `at` as a sprite's span of it.
fn flakCell(at: u32) [4]f32 {
const span = gridCell(at, flak_step, flak_step);
return .{ span[0][0], span[0][1], span[1][0], span[1][1] };
}
/// How many cells the sheet and the flak have across, and down.
const grid_side = 3;
/// Cell `at` of a grid of `grid_side` by `grid_side`, from the top left along each row, each
/// `step` from the last and `width` across and down: its span across, and down.
fn gridCell(at: u32, step: f32, width: f32) [2][2]f32 {
const u = @as(f32, @floatFromInt(at % grid_side)) * step;
const v = @as(f32, @floatFromInt(at / grid_side)) * step;
return .{ .{ u, u + width }, .{ v, v + width } };
}
/// The light's colour, and how far it reaches: its intensity times the square root of its size
/// times `light_reach` (`0x004DC48C`). Its intensity fades from the style's peak to nothing as
/// it plays.
const light_colour = [3]f32{ 1, 0.5, 0.1 };
const light_reach: f32 = 50;
/// The sheet's frames: each `sheet_step` across and down from the last, `sheet_cell` across
/// (`0x004DC82C`, `0x004DC828`: 82 and 81 of its 256 texels).
const sheet_step: f32 = 82.0 / 256.0;
const sheet_cell: f32 = 81.0 / 256.0;
fn init(images: Explosions.Images, at: Vector, spec: Spec, style: Fireballs, clock: *const Clock, random: *Random) Fireball {
var set: srapiext.SpriteSet = .{ .sprites = &.{} };
set.surface.material.lit[0] = spec.lit;
set.surface.material.blend[0] = if (spec.special) .add else .premultiplied;
const image = if (spec.special) images.flak else switch (spec.kind) {
.bang => images.bang[0],
.sheet => images.sheet,
};
set.surface.textures = .{ .{ .image = image }, .none };
const look: Look = .of(spec.kind, random.rand());
const sprite: srapiext.Sprite = .{ .offset = at, .half_size = .{ spec.size, spec.size }, .bias = -spec.size };
return .{
.sprite = .{ sprite, sprite },
.set = set,
.fading = set.surface,
.light = if (spec.light) .{
.mask = 0,
.intensity = style.peak(),
.colour = light_colour,
.kind = .{ .point = .{ .position = at, .range = @sqrt(spec.size) * light_reach } },
} else null,
.at = at,
.velocity = spec.velocity,
.born = clock.frame_start,
.life = spec.life,
.delay = spec.delay,
.look = look,
.lit = spec.lit,
.special = spec.special,
.style = style,
};
}
/// Plays on for the frame, once its wait is over: its age and its drift. Whether it is still
/// going.
fn frame(fireball: *Fireball, clock: *const Clock) bool {
const age = clock.frame_start - fireball.delay - fireball.born;
fireball.showing = false;
if (age < 0) return true;
if (age >= fireball.life) return false;
fireball.age = age;
fireball.at += fireball.velocity * @as(Vector, @splat(@floatFromInt(clock.frame_duration)));
fireball.showing = true;
return true;
}
/// How it shows, `ahead` of a tick past the frame's tick: the frame of its animation this far
/// through its life, where it has drifted to, its colour where it is lit, and its light's fade.
///
/// **Improvement:** with the `fuller` style, each frame of its animation fades into the next,
/// where the game flips from one to the next: sixteen or nine over a second and a half. It
/// fades out as its last frames play, where the game's vanishes after the last. It is drawn
/// further along between the ticks as well (`particles.Pool.draw`).
fn show(fireball: *Fireball, images: Explosions.Images, ahead: f32) void {
const bang_images = fireball.look.bang and !fireball.special;
const frames: u32 = if (bang_images) images.bang.len else grid_side * grid_side;
const life: f32 = @floatFromInt(fireball.life);
const played = @min((@as(f32, @floatFromInt(fireball.age)) + ahead) / life, 1);
const step = @min((@as(f32, @floatFromInt(fireball.age * @as(i32, @intCast(frames)))) + ahead * @as(f32, @floatFromInt(frames))) / life, @as(f32, @floatFromInt(frames - 1)));
const first: u32 = @intFromFloat(step);
const next = @min(first + 1, frames - 1);
const fade: f32 = if (fireball.style == .fuller and next > first) step - @as(f32, @floatFromInt(first)) else 0;
const kept = fireball.left(ahead);
const offset = fireball.at + fireball.velocity * @as(Vector, @splat(ahead));
for (&fireball.sprite, [2]u32{ first, next }, [2]f32{ 1 - fade, fade }) |*sprite, cell, share| {
sprite.offset = offset;
sprite.fade = share * kept;
if (fireball.lit) sprite.colour = @splat(played);
if (!fireball.look.bang) {
sprite.uv = fireball.look.cell(cell);
} else if (fireball.special) {
sprite.uv = flakCell(cell);
}
}
if (bang_images) {
fireball.set.surface.textures[0].image = images.bang[first];
fireball.fading = fireball.set.surface;
fireball.fading.textures[0].image = images.bang[next];
fireball.sprite[1].surface = &fireball.fading;
}
fireball.set.sprites = fireball.sprite[0..if (fade > 0) 2 else 1];
if (fireball.light) |*light| {
light.intensity = fireball.style.peak() * (1 - played);
if (fireball.style == .fuller) light.kind.point.position = offset;
}
}
/// How much of it shows `ahead` of a tick past the frame's tick: all of it until its style
/// fades it out, then less and less, to nothing as its life ends.
fn left(fireball: Fireball, ahead: f32) f32 {
const out = fireball.style.fadeOut(fireball.life);
if (out == 0) return 1;
const to_go = @as(f32, @floatFromInt(fireball.life - fireball.age)) - ahead;
return std.math.clamp(to_go / @as(f32, @floatFromInt(out)), 0, 1);
}
};
/// Within this far of the camera an explosion is sure of a voice (`0x004DC4BC`, its square).
const close: f32 = 20000;
/// The voice class an explosion at `at` is heard on: sure of a voice close to the camera, one of
/// the explosions' own further off.
pub fn soundClass(world: gameobj.World, at: Vector) ?sound3d.Class {
const hearing = world.hearing orelse return null;
const offset = at - hearing.camera.position;
return if (math.dot(offset, offset) < close * close) .guaranteed else .explosions;
}
/// Plays the first explosion's sound at `at`, on `class`.
pub fn sound(world: gameobj.World, at: Vector, class: sound3d.Class) void {
sound3d.playIn(world, at, null, null, .explosion01, 1, class);
}
/// The flame a blast bursts into (`0x00553348`): five to six seconds of it, growing from nothing
/// to a half-size of 400 as it goes from yellowish white through orange to nothing. It thins
/// slowly with distance.
pub const flame: particles.Template = .{
.life = 500,
.life_spread = 100,
.size = .through(0, 100, 400),
.colour = .{ .through(1, 0.75, 0), .through(1, 0.25, 0), .through(0, 0, 0) },
.distance = 0.05,
};
/// The sparkle a blast leaves (`0x0055334C`): specks of white, 25 across either way, that last one
/// to six seconds and fade out.
pub const sparkle: particles.Template = .{
.life = 100,
.life_spread = 500,
.size = .through(0, 25, 25),
.colour = .{ .through(1, 1, 0), .through(1, 1, 0), .through(1, 1, 0) },
};
/// How much of a ship's velocity, a step's, the particles of its blast carry on with, a tick's.
const Carried = union(enum) {
/// This share of it.
share: f32,
/// This share and up to as much again, at random.
share_or_more: f32,
fn of(carried: Carried, random: *Random) f32 {
return switch (carried) {
.share => |share| share,
.share_or_more => |share| (random.fraction() + 1) * share,
};
}
};
/// How a blast's flame leaves it: how fast, a tick, how much of the ship's velocity it carries, and
/// how many.
const Flames = struct {
speed: f32,
speed_range: f32,
carried: Carried,
count: i32,
};
/// Sends `count` particles out of `emitter` at once, as the camera sees them.
pub fn burstFrom(world: gameobj.World, emitter: *particles.Emitter, count: i32) void {
burstWithin(world, emitter, null, count);
}
/// Sends `count` particles out of `emitter` at once, where `parent` puts it, or where it stands
/// where null, as the camera sees them (`particles.Pool.burst`), where the world has particles.
pub fn burstWithin(world: gameobj.World, emitter: *particles.Emitter, parent: ?math.Place, count: i32) void {
const pool = world.particles orelse return;
pool.burst(emitter, parent, count, world.sending() orelse return);
}
/// Sends out what `emitter` streams over the frame, where `parent` puts it, as the camera sees it
/// (`particles.Pool.stream`): whether the emitter still lives. Where the world has no particles or
/// no camera, nothing goes out and it lives on.
pub fn streamWithin(world: gameobj.World, emitter: *particles.Emitter, parent: ?math.Place) bool {
const pool = world.particles orelse return true;
return pool.stream(emitter, parent, world.sending() orelse return true);
}
/// A burst of `flame` from a ship at `at`, moving at `velocity`, spreading out mostly across the
/// view: the emitter stands turned 60 degrees back and a random way about the camera's forward
/// axis, from the camera's orientation. Returns the emitter, whose place and carried velocity a
/// blast's shockwave takes; none without a camera.
fn flames(world: gameobj.World, at: Vector, velocity: Vector, how: Flames) ?particles.Emitter {
const view = (world.camera orelse return null).place;
const turn = math.fromAngles(-std.math.pi / 3.0, 0, world.random.fraction() * std.math.tau);
var emitter: particles.Emitter = .{
.born = world.clock.frame_start,
.template = &flame,
.place = .{ .position = at, .orientation = math.product(turn, view.orientation) },
.spread = .{ 1, 1, 0.2 },
.speed = how.speed,
.speed_range = how.speed_range,
.inherited = velocity * @as(Vector, @splat(how.carried.of(world.random))),
};
burstFrom(world, &emitter, how.count);
return emitter;
}
/// How a burst of sparkle leaves: of which template, how fast, a tick, and how many.
const Sparkles = struct {
template: *const particles.Template = &sparkle,
speed_range: f32 = 7,
count: i32 = 150,
};
/// The sparkle a missile leaves (`missile_explode`), `sparkle`'s for one to three seconds: 50 of
/// it, slower than a ship's.
const missile_sparkle: particles.Template = sparkled: {
var template = sparkle;
template.life_spread = 200;
break :sparkled template;
};
const missile_sparkles: Sparkles = .{ .template = &missile_sparkle, .speed_range = 4, .count = 50 };
/// A burst of sparkle from something at `at`, moving at `velocity`, carrying `carried` of it,
/// drifting every way.
fn sparkles(world: gameobj.World, at: Vector, velocity: Vector, carried: f32, how: Sparkles) void {
var emitter: particles.Emitter = .{
.born = world.clock.frame_start,
.template = how.template,
.place = .{ .position = at },
.spread = .{ 1, 1, 1 },
.speed_range = how.speed_range,
.inherited = velocity * @as(Vector, @splat(carried)),
};
burstFrom(world, &emitter, how.count);
}
/// Sets a fireball off at `at`, where the world has explosions.
pub fn fireballAt(world: gameobj.World, at: Vector, spec: Fireball.Spec) void {
const explosions = world.explosions orelse return;
explosions.setOff(at, spec, world.clock, world.random);
}
/// Sets the Uber Explode off for `owner` (`uber.Uber.start`), in the style the settings give,
/// where the world has explosions.
pub fn uberExplode(world: gameobj.World, owner: u16, place: math.Place, size: f32, duration: i32) void {
const explosions = world.explosions orelse return;
explosions.uber.start(world, owner, place, size, duration, explosions.settings.uber);
}
/// Throws a chunk of rock from `at` along `direction` (`rocks.throw`), where the world has
/// explosions.
pub fn throwChunk(world: gameobj.World, at: Vector, direction: Vector, how: rocks.Throw) void {
const explosions = world.explosions orelse return;
rocks.throw(explosions, at, direction, how, world.clock, world.random);
}
/// A throw of bits every way: how many, and how.
const Scatter = struct {
count: usize,
throw: Bit.Throw,
};
/// Throws `how`'s bits out of `at` every way, where the world has explosions.
fn scatter(world: gameobj.World, at: Vector, how: Scatter) void {
const explosions = world.explosions orelse return;
for (0..how.count) |_| {
const direction = math.normalize(world.random.centredVector(@splat(1)));
explosions.throwBit(at, direction, how.throw, world.clock, world.random);
}
}