@adatechnology/game-weapons-three 0.7.2 → 0.8.0
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.
- package/README.md +35 -0
- package/assets/weapon-svg/generated/propane_p13.svg +1 -1
- package/dist/index.d.ts +390 -5
- package/dist/index.global.js +12899 -1052
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +2598 -73
- package/dist/index.js.map +1 -1
- package/package.json +11 -10
package/dist/index.js
CHANGED
|
@@ -219,7 +219,40 @@ var ACCESSORY_TEXT = {
|
|
|
219
219
|
metrics: { mass: "Weapon mass", aim: "Time to the sights", fitted: "Accessories on the weapon", suppressed: "Suppressed" },
|
|
220
220
|
yes: "yes",
|
|
221
221
|
no: "no",
|
|
222
|
-
attachments: {
|
|
222
|
+
attachments: {
|
|
223
|
+
suppressor: "Suppressor",
|
|
224
|
+
oil_filter_suppressor: "Improvised oil filter suppressor",
|
|
225
|
+
integral_suppressor: "Integral suppressor",
|
|
226
|
+
muzzle_brake: "Muzzle brake",
|
|
227
|
+
muzzle_brake_heavy: "Heavy muzzle brake",
|
|
228
|
+
compensator: "Compensator",
|
|
229
|
+
red_dot: "Red dot sight",
|
|
230
|
+
holographic_sight: "Holographic sight",
|
|
231
|
+
acog_4x: "ACOG 4x",
|
|
232
|
+
scope_3_9x: "Rifle scope 3-9x",
|
|
233
|
+
scope_6_24x: "Rifle scope 6-24x",
|
|
234
|
+
night_vision_scope: "Night vision scope",
|
|
235
|
+
thermal_scope: "Thermal scope",
|
|
236
|
+
integral_optic_1_5x: "Integral 1.5x optic of the AUG",
|
|
237
|
+
integral_optic_3x: "Integral 3x optic of the G36",
|
|
238
|
+
pso_1: "PSO-1 scope",
|
|
239
|
+
fixed_scope_10x: "Fixed 10x scope of the M24",
|
|
240
|
+
vertical_grip: "Vertical foregrip",
|
|
241
|
+
bipod: "Bipod",
|
|
242
|
+
m203: "M203 grenade launcher",
|
|
243
|
+
gp_25: "GP-25 grenade launcher",
|
|
244
|
+
ag36: "AG36 grenade launcher",
|
|
245
|
+
weapon_light: "Weapon light",
|
|
246
|
+
laser_sight: "Laser sight",
|
|
247
|
+
extended_magazine: "Extended magazine",
|
|
248
|
+
drum_magazine: "Drum magazine",
|
|
249
|
+
dual_coupler: "Dual magazine coupler",
|
|
250
|
+
sawn_off_barrel: "Sawn-off barrel",
|
|
251
|
+
long_barrel: "Long barrel",
|
|
252
|
+
sling: "Sling",
|
|
253
|
+
ammo_backpack: "Ammunition backpack",
|
|
254
|
+
tripod: "Tripod mount"
|
|
255
|
+
},
|
|
223
256
|
fitRefused: "Could not change {mount} of {weapon}: {reason}",
|
|
224
257
|
fitting: "Fitting {attachment} on {weapon} ({seconds} s)",
|
|
225
258
|
removing: "Taking {attachment} off {weapon} ({seconds} s)",
|
|
@@ -598,6 +631,731 @@ function weaponRecipes(curve) {
|
|
|
598
631
|
];
|
|
599
632
|
}
|
|
600
633
|
|
|
634
|
+
// src/data/reload-sounds.json
|
|
635
|
+
var reload_sounds_default = {
|
|
636
|
+
source: "Authored in the kit. The art scene delivered sfx for the magazine, the bolt and the shell (cena L1339-L1343, used here for mag_seat, charging_handle_release and shell_insert); the gallery draws no sound, so the other cues combine the same primitives (metal click, thud, filtered noise, tones) of game-audio. Each cue is played at the second the reload clip says, so it follows the animation and scales with the weapon's reload time.",
|
|
637
|
+
profiles: {
|
|
638
|
+
weight: {
|
|
639
|
+
light: {
|
|
640
|
+
pitch: 1.3,
|
|
641
|
+
gain: 0.85
|
|
642
|
+
},
|
|
643
|
+
medium: {
|
|
644
|
+
pitch: 1,
|
|
645
|
+
gain: 1
|
|
646
|
+
},
|
|
647
|
+
heavy: {
|
|
648
|
+
pitch: 0.78,
|
|
649
|
+
gain: 1.2
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
material: {
|
|
653
|
+
steel: {
|
|
654
|
+
pitch: 1,
|
|
655
|
+
gain: 1
|
|
656
|
+
},
|
|
657
|
+
polymer: {
|
|
658
|
+
pitch: 0.82,
|
|
659
|
+
gain: 0.8
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
comment: "Light: pistols and revolvers. Medium: rifles, SMGs, bolt guns. Heavy: shotguns, belt-fed guns, launchers. Steel is the default; the polymer frames are listed in `polymer`.",
|
|
664
|
+
weights: {
|
|
665
|
+
pistol: "light",
|
|
666
|
+
revolver: "light",
|
|
667
|
+
rifle: "medium",
|
|
668
|
+
semiint: "medium",
|
|
669
|
+
semi12: "heavy",
|
|
670
|
+
p90: "medium",
|
|
671
|
+
belt: "heavy",
|
|
672
|
+
pump: "heavy",
|
|
673
|
+
lever: "medium",
|
|
674
|
+
bolt: "medium",
|
|
675
|
+
boltmag: "medium",
|
|
676
|
+
break: "heavy",
|
|
677
|
+
rocket: "heavy",
|
|
678
|
+
flame: "heavy"
|
|
679
|
+
},
|
|
680
|
+
polymer: [
|
|
681
|
+
"glock_17",
|
|
682
|
+
"glock_18",
|
|
683
|
+
"glock_19",
|
|
684
|
+
"taurus_g2c",
|
|
685
|
+
"taurus_ts9",
|
|
686
|
+
"fn_five_seven",
|
|
687
|
+
"pistol_9mm",
|
|
688
|
+
"fn_p90",
|
|
689
|
+
"hk_ump_45"
|
|
690
|
+
],
|
|
691
|
+
cues: {
|
|
692
|
+
mag_release: [
|
|
693
|
+
{
|
|
694
|
+
layer: "scene_tone",
|
|
695
|
+
at: 0,
|
|
696
|
+
wave: "square",
|
|
697
|
+
f: 2400,
|
|
698
|
+
f2: 1680,
|
|
699
|
+
d: 0.02,
|
|
700
|
+
vol: 0.3
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
layer: "scene_noise",
|
|
704
|
+
at: 0,
|
|
705
|
+
type: "highpass",
|
|
706
|
+
f: 3e3,
|
|
707
|
+
q: 0.7,
|
|
708
|
+
d: 0.03,
|
|
709
|
+
vol: 0.24
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
layer: "thud",
|
|
713
|
+
at: 0.02,
|
|
714
|
+
gain: 0.12,
|
|
715
|
+
frequency: 700
|
|
716
|
+
}
|
|
717
|
+
],
|
|
718
|
+
mag_out: [
|
|
719
|
+
{
|
|
720
|
+
layer: "scene_noise",
|
|
721
|
+
at: 0,
|
|
722
|
+
type: "bandpass",
|
|
723
|
+
f: 900,
|
|
724
|
+
q: 2,
|
|
725
|
+
d: 0.1,
|
|
726
|
+
vol: 0.3,
|
|
727
|
+
f2: 600
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
layer: "metal",
|
|
731
|
+
at: 0.08,
|
|
732
|
+
frequency: 1200,
|
|
733
|
+
gain: 0.2
|
|
734
|
+
}
|
|
735
|
+
],
|
|
736
|
+
mag_insert: [
|
|
737
|
+
{
|
|
738
|
+
layer: "scene_noise",
|
|
739
|
+
at: 0,
|
|
740
|
+
type: "bandpass",
|
|
741
|
+
f: 700,
|
|
742
|
+
q: 2,
|
|
743
|
+
d: 0.12,
|
|
744
|
+
vol: 0.3,
|
|
745
|
+
f2: 1100
|
|
746
|
+
}
|
|
747
|
+
],
|
|
748
|
+
mag_seat: [
|
|
749
|
+
{
|
|
750
|
+
layer: "scene_tone",
|
|
751
|
+
at: 0,
|
|
752
|
+
wave: "square",
|
|
753
|
+
f: 1100,
|
|
754
|
+
f2: 770,
|
|
755
|
+
d: 0.02,
|
|
756
|
+
vol: 0.5
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
layer: "scene_noise",
|
|
760
|
+
at: 0,
|
|
761
|
+
type: "highpass",
|
|
762
|
+
f: 3e3,
|
|
763
|
+
q: 0.7,
|
|
764
|
+
d: 0.03,
|
|
765
|
+
vol: 0.4
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
layer: "scene_tone",
|
|
769
|
+
at: 0,
|
|
770
|
+
wave: "sine",
|
|
771
|
+
f: 320,
|
|
772
|
+
f2: 200,
|
|
773
|
+
d: 0.05,
|
|
774
|
+
vol: 0.35
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
layer: "thud",
|
|
778
|
+
at: 0.01,
|
|
779
|
+
gain: 0.18,
|
|
780
|
+
frequency: 600
|
|
781
|
+
}
|
|
782
|
+
],
|
|
783
|
+
slide_release: [
|
|
784
|
+
{
|
|
785
|
+
layer: "metal",
|
|
786
|
+
at: 0,
|
|
787
|
+
frequency: 900,
|
|
788
|
+
gain: 0.35
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
layer: "scene_tone",
|
|
792
|
+
at: 0.02,
|
|
793
|
+
wave: "square",
|
|
794
|
+
f: 2e3,
|
|
795
|
+
f2: 1400,
|
|
796
|
+
d: 0.02,
|
|
797
|
+
vol: 0.4
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
layer: "scene_noise",
|
|
801
|
+
at: 0.02,
|
|
802
|
+
type: "highpass",
|
|
803
|
+
f: 3e3,
|
|
804
|
+
q: 0.7,
|
|
805
|
+
d: 0.03,
|
|
806
|
+
vol: 0.32
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
layer: "thud",
|
|
810
|
+
at: 0.03,
|
|
811
|
+
gain: 0.25,
|
|
812
|
+
frequency: 500
|
|
813
|
+
}
|
|
814
|
+
],
|
|
815
|
+
charging_handle_pull: [
|
|
816
|
+
{
|
|
817
|
+
layer: "scene_noise",
|
|
818
|
+
at: 0,
|
|
819
|
+
type: "bandpass",
|
|
820
|
+
f: 1500,
|
|
821
|
+
q: 3,
|
|
822
|
+
d: 0.09,
|
|
823
|
+
vol: 0.3,
|
|
824
|
+
f2: 900
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
layer: "metal",
|
|
828
|
+
at: 0.06,
|
|
829
|
+
frequency: 1300,
|
|
830
|
+
gain: 0.25
|
|
831
|
+
}
|
|
832
|
+
],
|
|
833
|
+
charging_handle_release: [
|
|
834
|
+
{
|
|
835
|
+
layer: "scene_tone",
|
|
836
|
+
at: 0,
|
|
837
|
+
wave: "square",
|
|
838
|
+
f: 2200,
|
|
839
|
+
f2: 1540,
|
|
840
|
+
d: 0.02,
|
|
841
|
+
vol: 0.4
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
layer: "scene_noise",
|
|
845
|
+
at: 0,
|
|
846
|
+
type: "highpass",
|
|
847
|
+
f: 3e3,
|
|
848
|
+
q: 0.7,
|
|
849
|
+
d: 0.03,
|
|
850
|
+
vol: 0.32
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
layer: "scene_tone",
|
|
854
|
+
at: 0.02,
|
|
855
|
+
wave: "square",
|
|
856
|
+
f: 2700,
|
|
857
|
+
f2: 1890,
|
|
858
|
+
d: 0.02,
|
|
859
|
+
vol: 0.4
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
layer: "scene_noise",
|
|
863
|
+
at: 0.02,
|
|
864
|
+
type: "highpass",
|
|
865
|
+
f: 3e3,
|
|
866
|
+
q: 0.7,
|
|
867
|
+
d: 0.03,
|
|
868
|
+
vol: 0.32
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
layer: "thud",
|
|
872
|
+
at: 0.03,
|
|
873
|
+
gain: 0.2,
|
|
874
|
+
frequency: 600
|
|
875
|
+
}
|
|
876
|
+
],
|
|
877
|
+
bolt_lift: [
|
|
878
|
+
{
|
|
879
|
+
layer: "metal",
|
|
880
|
+
at: 0,
|
|
881
|
+
frequency: 1500,
|
|
882
|
+
gain: 0.3
|
|
883
|
+
},
|
|
884
|
+
{
|
|
885
|
+
layer: "scene_noise",
|
|
886
|
+
at: 0,
|
|
887
|
+
type: "bandpass",
|
|
888
|
+
f: 2e3,
|
|
889
|
+
q: 3,
|
|
890
|
+
d: 0.04,
|
|
891
|
+
vol: 0.15,
|
|
892
|
+
f2: 1500
|
|
893
|
+
}
|
|
894
|
+
],
|
|
895
|
+
bolt_pull: [
|
|
896
|
+
{
|
|
897
|
+
layer: "scene_noise",
|
|
898
|
+
at: 0,
|
|
899
|
+
type: "bandpass",
|
|
900
|
+
f: 1400,
|
|
901
|
+
q: 3,
|
|
902
|
+
d: 0.12,
|
|
903
|
+
vol: 0.3,
|
|
904
|
+
f2: 800
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
layer: "metal",
|
|
908
|
+
at: 0.1,
|
|
909
|
+
frequency: 900,
|
|
910
|
+
gain: 0.3
|
|
911
|
+
}
|
|
912
|
+
],
|
|
913
|
+
bolt_push: [
|
|
914
|
+
{
|
|
915
|
+
layer: "scene_noise",
|
|
916
|
+
at: 0,
|
|
917
|
+
type: "bandpass",
|
|
918
|
+
f: 800,
|
|
919
|
+
q: 3,
|
|
920
|
+
d: 0.1,
|
|
921
|
+
vol: 0.3,
|
|
922
|
+
f2: 1400
|
|
923
|
+
},
|
|
924
|
+
{
|
|
925
|
+
layer: "metal",
|
|
926
|
+
at: 0.08,
|
|
927
|
+
frequency: 1100,
|
|
928
|
+
gain: 0.3
|
|
929
|
+
}
|
|
930
|
+
],
|
|
931
|
+
bolt_lock: [
|
|
932
|
+
{
|
|
933
|
+
layer: "metal",
|
|
934
|
+
at: 0,
|
|
935
|
+
frequency: 1800,
|
|
936
|
+
gain: 0.4
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
layer: "thud",
|
|
940
|
+
at: 0.02,
|
|
941
|
+
gain: 0.2,
|
|
942
|
+
frequency: 700
|
|
943
|
+
}
|
|
944
|
+
],
|
|
945
|
+
shell_insert: [
|
|
946
|
+
{
|
|
947
|
+
layer: "scene_tone",
|
|
948
|
+
at: 0,
|
|
949
|
+
wave: "square",
|
|
950
|
+
f: 1400,
|
|
951
|
+
f2: 980,
|
|
952
|
+
d: 0.02,
|
|
953
|
+
vol: 0.35
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
layer: "scene_noise",
|
|
957
|
+
at: 0,
|
|
958
|
+
type: "highpass",
|
|
959
|
+
f: 3e3,
|
|
960
|
+
q: 0.7,
|
|
961
|
+
d: 0.03,
|
|
962
|
+
vol: 0.28
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
layer: "scene_noise",
|
|
966
|
+
at: 0.02,
|
|
967
|
+
type: "bandpass",
|
|
968
|
+
f: 1200,
|
|
969
|
+
q: 3,
|
|
970
|
+
d: 0.05,
|
|
971
|
+
vol: 0.25
|
|
972
|
+
}
|
|
973
|
+
],
|
|
974
|
+
pump_back: [
|
|
975
|
+
{
|
|
976
|
+
layer: "scene_noise",
|
|
977
|
+
at: 0,
|
|
978
|
+
type: "bandpass",
|
|
979
|
+
f: 1e3,
|
|
980
|
+
q: 2,
|
|
981
|
+
d: 0.12,
|
|
982
|
+
vol: 0.3,
|
|
983
|
+
f2: 700
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
layer: "metal",
|
|
987
|
+
at: 0.1,
|
|
988
|
+
frequency: 900,
|
|
989
|
+
gain: 0.35
|
|
990
|
+
}
|
|
991
|
+
],
|
|
992
|
+
pump_forward: [
|
|
993
|
+
{
|
|
994
|
+
layer: "scene_noise",
|
|
995
|
+
at: 0,
|
|
996
|
+
type: "bandpass",
|
|
997
|
+
f: 700,
|
|
998
|
+
q: 2,
|
|
999
|
+
d: 0.1,
|
|
1000
|
+
vol: 0.3,
|
|
1001
|
+
f2: 1e3
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
layer: "metal",
|
|
1005
|
+
at: 0.08,
|
|
1006
|
+
frequency: 1300,
|
|
1007
|
+
gain: 0.4
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
layer: "thud",
|
|
1011
|
+
at: 0.1,
|
|
1012
|
+
gain: 0.25,
|
|
1013
|
+
frequency: 500
|
|
1014
|
+
}
|
|
1015
|
+
],
|
|
1016
|
+
lever_open: [
|
|
1017
|
+
{
|
|
1018
|
+
layer: "metal",
|
|
1019
|
+
at: 0,
|
|
1020
|
+
frequency: 1e3,
|
|
1021
|
+
gain: 0.35
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
layer: "scene_noise",
|
|
1025
|
+
at: 0,
|
|
1026
|
+
type: "bandpass",
|
|
1027
|
+
f: 1200,
|
|
1028
|
+
q: 3,
|
|
1029
|
+
d: 0.08,
|
|
1030
|
+
vol: 0.2,
|
|
1031
|
+
f2: 800
|
|
1032
|
+
}
|
|
1033
|
+
],
|
|
1034
|
+
lever_close: [
|
|
1035
|
+
{
|
|
1036
|
+
layer: "metal",
|
|
1037
|
+
at: 0,
|
|
1038
|
+
frequency: 1500,
|
|
1039
|
+
gain: 0.4
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
layer: "thud",
|
|
1043
|
+
at: 0.02,
|
|
1044
|
+
gain: 0.2,
|
|
1045
|
+
frequency: 550
|
|
1046
|
+
}
|
|
1047
|
+
],
|
|
1048
|
+
cylinder_open: [
|
|
1049
|
+
{
|
|
1050
|
+
layer: "metal",
|
|
1051
|
+
at: 0,
|
|
1052
|
+
frequency: 1500,
|
|
1053
|
+
gain: 0.3
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
layer: "scene_noise",
|
|
1057
|
+
at: 0.02,
|
|
1058
|
+
type: "bandpass",
|
|
1059
|
+
f: 1800,
|
|
1060
|
+
q: 3,
|
|
1061
|
+
d: 0.08,
|
|
1062
|
+
vol: 0.2,
|
|
1063
|
+
f2: 1200
|
|
1064
|
+
}
|
|
1065
|
+
],
|
|
1066
|
+
cylinder_eject: [
|
|
1067
|
+
{
|
|
1068
|
+
layer: "metal",
|
|
1069
|
+
at: 0,
|
|
1070
|
+
frequency: 2400,
|
|
1071
|
+
gain: 0.25
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
layer: "metal",
|
|
1075
|
+
at: 0.05,
|
|
1076
|
+
frequency: 2800,
|
|
1077
|
+
gain: 0.2
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
layer: "metal",
|
|
1081
|
+
at: 0.11,
|
|
1082
|
+
frequency: 3200,
|
|
1083
|
+
gain: 0.15
|
|
1084
|
+
}
|
|
1085
|
+
],
|
|
1086
|
+
speedloader: [
|
|
1087
|
+
{
|
|
1088
|
+
layer: "scene_noise",
|
|
1089
|
+
at: 0,
|
|
1090
|
+
type: "bandpass",
|
|
1091
|
+
f: 1500,
|
|
1092
|
+
q: 3,
|
|
1093
|
+
d: 0.08,
|
|
1094
|
+
vol: 0.25,
|
|
1095
|
+
f2: 2e3
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
layer: "metal",
|
|
1099
|
+
at: 0,
|
|
1100
|
+
frequency: 2e3,
|
|
1101
|
+
gain: 0.25
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
layer: "metal",
|
|
1105
|
+
at: 0.05,
|
|
1106
|
+
frequency: 2400,
|
|
1107
|
+
gain: 0.2
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
layer: "thud",
|
|
1111
|
+
at: 0.07,
|
|
1112
|
+
gain: 0.15,
|
|
1113
|
+
frequency: 800
|
|
1114
|
+
}
|
|
1115
|
+
],
|
|
1116
|
+
cylinder_close: [
|
|
1117
|
+
{
|
|
1118
|
+
layer: "metal",
|
|
1119
|
+
at: 0,
|
|
1120
|
+
frequency: 1100,
|
|
1121
|
+
gain: 0.45
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
layer: "thud",
|
|
1125
|
+
at: 0.01,
|
|
1126
|
+
gain: 0.2,
|
|
1127
|
+
frequency: 500
|
|
1128
|
+
}
|
|
1129
|
+
],
|
|
1130
|
+
barrels_open: [
|
|
1131
|
+
{
|
|
1132
|
+
layer: "metal",
|
|
1133
|
+
at: 0,
|
|
1134
|
+
frequency: 1e3,
|
|
1135
|
+
gain: 0.4
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
layer: "thud",
|
|
1139
|
+
at: 0.03,
|
|
1140
|
+
gain: 0.2,
|
|
1141
|
+
frequency: 450
|
|
1142
|
+
}
|
|
1143
|
+
],
|
|
1144
|
+
casings_eject: [
|
|
1145
|
+
{
|
|
1146
|
+
layer: "metal",
|
|
1147
|
+
at: 0,
|
|
1148
|
+
frequency: 2600,
|
|
1149
|
+
gain: 0.25
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
layer: "metal",
|
|
1153
|
+
at: 0.08,
|
|
1154
|
+
frequency: 3e3,
|
|
1155
|
+
gain: 0.2
|
|
1156
|
+
}
|
|
1157
|
+
],
|
|
1158
|
+
barrels_close: [
|
|
1159
|
+
{
|
|
1160
|
+
layer: "metal",
|
|
1161
|
+
at: 0,
|
|
1162
|
+
frequency: 1100,
|
|
1163
|
+
gain: 0.5
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
layer: "thud",
|
|
1167
|
+
at: 0.02,
|
|
1168
|
+
gain: 0.3,
|
|
1169
|
+
frequency: 450
|
|
1170
|
+
}
|
|
1171
|
+
],
|
|
1172
|
+
belt_cover_open: [
|
|
1173
|
+
{
|
|
1174
|
+
layer: "metal",
|
|
1175
|
+
at: 0,
|
|
1176
|
+
frequency: 800,
|
|
1177
|
+
gain: 0.4
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
layer: "thud",
|
|
1181
|
+
at: 0.03,
|
|
1182
|
+
gain: 0.2,
|
|
1183
|
+
frequency: 500
|
|
1184
|
+
}
|
|
1185
|
+
],
|
|
1186
|
+
belt_insert: [
|
|
1187
|
+
{
|
|
1188
|
+
layer: "metal",
|
|
1189
|
+
at: 0,
|
|
1190
|
+
frequency: 1400,
|
|
1191
|
+
gain: 0.3
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
layer: "metal",
|
|
1195
|
+
at: 0.08,
|
|
1196
|
+
frequency: 1800,
|
|
1197
|
+
gain: 0.25
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
layer: "thud",
|
|
1201
|
+
at: 0.1,
|
|
1202
|
+
gain: 0.25,
|
|
1203
|
+
frequency: 450
|
|
1204
|
+
}
|
|
1205
|
+
],
|
|
1206
|
+
belt_cover_close: [
|
|
1207
|
+
{
|
|
1208
|
+
layer: "metal",
|
|
1209
|
+
at: 0,
|
|
1210
|
+
frequency: 1e3,
|
|
1211
|
+
gain: 0.45
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
layer: "thud",
|
|
1215
|
+
at: 0.02,
|
|
1216
|
+
gain: 0.3,
|
|
1217
|
+
frequency: 400
|
|
1218
|
+
}
|
|
1219
|
+
],
|
|
1220
|
+
rocket_load: [
|
|
1221
|
+
{
|
|
1222
|
+
layer: "scene_noise",
|
|
1223
|
+
at: 0,
|
|
1224
|
+
type: "bandpass",
|
|
1225
|
+
f: 600,
|
|
1226
|
+
q: 1.5,
|
|
1227
|
+
d: 0.25,
|
|
1228
|
+
vol: 0.3,
|
|
1229
|
+
f2: 400
|
|
1230
|
+
},
|
|
1231
|
+
{
|
|
1232
|
+
layer: "thud",
|
|
1233
|
+
at: 0.2,
|
|
1234
|
+
gain: 0.2,
|
|
1235
|
+
frequency: 450
|
|
1236
|
+
}
|
|
1237
|
+
],
|
|
1238
|
+
rocket_seat: [
|
|
1239
|
+
{
|
|
1240
|
+
layer: "metal",
|
|
1241
|
+
at: 0,
|
|
1242
|
+
frequency: 900,
|
|
1243
|
+
gain: 0.45
|
|
1244
|
+
},
|
|
1245
|
+
{
|
|
1246
|
+
layer: "thud",
|
|
1247
|
+
at: 0.01,
|
|
1248
|
+
gain: 0.35,
|
|
1249
|
+
frequency: 350
|
|
1250
|
+
}
|
|
1251
|
+
],
|
|
1252
|
+
grenade_load: [
|
|
1253
|
+
{
|
|
1254
|
+
layer: "metal",
|
|
1255
|
+
at: 0,
|
|
1256
|
+
frequency: 1100,
|
|
1257
|
+
gain: 0.35
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
layer: "scene_noise",
|
|
1261
|
+
at: 0,
|
|
1262
|
+
type: "bandpass",
|
|
1263
|
+
f: 1e3,
|
|
1264
|
+
q: 2,
|
|
1265
|
+
d: 0.1,
|
|
1266
|
+
vol: 0.2,
|
|
1267
|
+
f2: 700
|
|
1268
|
+
}
|
|
1269
|
+
],
|
|
1270
|
+
breech_open: [
|
|
1271
|
+
{
|
|
1272
|
+
layer: "metal",
|
|
1273
|
+
at: 0,
|
|
1274
|
+
frequency: 1e3,
|
|
1275
|
+
gain: 0.35
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
layer: "thud",
|
|
1279
|
+
at: 0.03,
|
|
1280
|
+
gain: 0.15,
|
|
1281
|
+
frequency: 500
|
|
1282
|
+
}
|
|
1283
|
+
],
|
|
1284
|
+
breech_close: [
|
|
1285
|
+
{
|
|
1286
|
+
layer: "metal",
|
|
1287
|
+
at: 0,
|
|
1288
|
+
frequency: 1100,
|
|
1289
|
+
gain: 0.45
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
layer: "thud",
|
|
1293
|
+
at: 0.02,
|
|
1294
|
+
gain: 0.25,
|
|
1295
|
+
frequency: 450
|
|
1296
|
+
}
|
|
1297
|
+
],
|
|
1298
|
+
tube_drop: [
|
|
1299
|
+
{
|
|
1300
|
+
layer: "thud",
|
|
1301
|
+
at: 0,
|
|
1302
|
+
gain: 0.35,
|
|
1303
|
+
frequency: 380
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
layer: "metal",
|
|
1307
|
+
at: 0.05,
|
|
1308
|
+
frequency: 700,
|
|
1309
|
+
gain: 0.2
|
|
1310
|
+
}
|
|
1311
|
+
],
|
|
1312
|
+
tube_seat: [
|
|
1313
|
+
{
|
|
1314
|
+
layer: "metal",
|
|
1315
|
+
at: 0,
|
|
1316
|
+
frequency: 900,
|
|
1317
|
+
gain: 0.45
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
layer: "thud",
|
|
1321
|
+
at: 0.02,
|
|
1322
|
+
gain: 0.3,
|
|
1323
|
+
frequency: 380
|
|
1324
|
+
}
|
|
1325
|
+
]
|
|
1326
|
+
}
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
// src/audio/reload-sounds.ts
|
|
1330
|
+
var RELOAD_CUE_IDS = Object.keys(reload_sounds_default.cues);
|
|
1331
|
+
var DEFAULT_SOUND_PROFILE = { weight: "medium", material: "steel" };
|
|
1332
|
+
var RELOAD_CUES = reload_sounds_default.cues;
|
|
1333
|
+
var polymer = new Set(reload_sounds_default.polymer);
|
|
1334
|
+
var weights = reload_sounds_default.weights;
|
|
1335
|
+
var soundProfileOf = (weaponId, modelFamily) => ({ weight: weights[modelFamily] ?? "medium", material: polymer.has(weaponId) ? "polymer" : "steel" });
|
|
1336
|
+
var reloadCueSteps = (id, profile = DEFAULT_SOUND_PROFILE, table = RELOAD_CUES) => {
|
|
1337
|
+
const steps = table[id];
|
|
1338
|
+
if (!steps) return [];
|
|
1339
|
+
const weight = reload_sounds_default.profiles.weight[profile.weight];
|
|
1340
|
+
const material = reload_sounds_default.profiles.material[profile.material];
|
|
1341
|
+
const pitch = weight.pitch * material.pitch;
|
|
1342
|
+
const gain = weight.gain * material.gain;
|
|
1343
|
+
return steps.map((entry) => {
|
|
1344
|
+
switch (entry.layer) {
|
|
1345
|
+
case "metal":
|
|
1346
|
+
return { ...entry, frequency: Math.round(entry.frequency * pitch), gain: entry.gain * gain };
|
|
1347
|
+
case "thud":
|
|
1348
|
+
return { ...entry, frequency: Math.round(entry.frequency * pitch), gain: entry.gain * gain };
|
|
1349
|
+
case "scene_tone":
|
|
1350
|
+
return { ...entry, f: Math.round(entry.f * pitch), ...entry.f2 === void 0 ? {} : { f2: Math.round(entry.f2 * pitch) }, vol: entry.vol * gain };
|
|
1351
|
+
case "scene_noise":
|
|
1352
|
+
return { ...entry, f: Math.round(entry.f * pitch), ...entry.f2 === void 0 ? {} : { f2: Math.round(entry.f2 * pitch) }, vol: entry.vol * gain };
|
|
1353
|
+
default:
|
|
1354
|
+
return entry;
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
};
|
|
1358
|
+
|
|
601
1359
|
// src/audio.ts
|
|
602
1360
|
var NOISE_SECONDS = 1.5;
|
|
603
1361
|
var DEFAULT_SEED = 1;
|
|
@@ -606,6 +1364,7 @@ function createWeaponAudio(volume = 0.6, options = {}) {
|
|
|
606
1364
|
const classes = { ...GAME_SOUND_CLASSES, ...options.classes };
|
|
607
1365
|
const distanceCurve = { ...GAME_DISTANCE_CURVE, ...options.distance };
|
|
608
1366
|
const mechanicalTable = { ...MECHANICAL_SOUNDS, ...options.mechanical };
|
|
1367
|
+
const cueTable = { ...RELOAD_CUES, ...options.reloadCues };
|
|
609
1368
|
const shared = options.engine;
|
|
610
1369
|
const engine = shared ?? createAudioEngine({
|
|
611
1370
|
seed,
|
|
@@ -658,17 +1417,20 @@ function createWeaponAudio(volume = 0.6, options = {}) {
|
|
|
658
1417
|
const shotSound = (input) => computeShotSound(input, options);
|
|
659
1418
|
return {
|
|
660
1419
|
shot(energyJ, category, extras = {}) {
|
|
661
|
-
const
|
|
662
|
-
if (!
|
|
663
|
-
classBurstAt(0,
|
|
664
|
-
if (
|
|
665
|
-
tone({ seconds:
|
|
1420
|
+
const sound3 = shotSound({ ...extras, energyJ, category });
|
|
1421
|
+
if (!sound3.audible) return;
|
|
1422
|
+
classBurstAt(0, sound3.cutoff, sound3.gain, sound3.duration);
|
|
1423
|
+
if (sound3.crack.gain > 0) burst({ seconds: sound3.crack.duration, gain: sound3.crack.gain, filter: "highpass", frequency: sound3.crack.cutoff });
|
|
1424
|
+
tone({ seconds: sound3.body.duration, gain: sound3.body.gain, frequency: sound3.body.frequency, to: sound3.body.to });
|
|
666
1425
|
},
|
|
667
1426
|
shotSound,
|
|
668
1427
|
click() {
|
|
669
1428
|
mechanical("dry_fire");
|
|
670
1429
|
},
|
|
671
1430
|
mechanical,
|
|
1431
|
+
cue(id, profile) {
|
|
1432
|
+
for (const entry of reloadCueSteps(id, profile, cueTable)) step(entry);
|
|
1433
|
+
},
|
|
672
1434
|
event(event) {
|
|
673
1435
|
const id = mechanicalSoundOf(event);
|
|
674
1436
|
if (id) mechanical(id);
|
|
@@ -717,7 +1479,7 @@ var VIEWMODEL_NODE_NAME = "weapon-viewmodel";
|
|
|
717
1479
|
var IN_HAND_NODE_NAME = "weapon-in-hand";
|
|
718
1480
|
var LASER_RAYCAST_HZ = 30;
|
|
719
1481
|
var isHeld = (object) => {
|
|
720
|
-
for (let node = object; node; node = node.parent) if (node.name === VIEWMODEL_NODE_NAME || node.name === IN_HAND_NODE_NAME) return true;
|
|
1482
|
+
for (let node = object; node; node = node.parent) if (node.name === VIEWMODEL_NODE_NAME || node.name === IN_HAND_NODE_NAME || node.userData?.viewmodel === true) return true;
|
|
721
1483
|
return false;
|
|
722
1484
|
};
|
|
723
1485
|
var isVisibleInScene = (object) => {
|
|
@@ -747,8 +1509,144 @@ var createLaserRaycast = (THREE, scene, camera, options = {}) => {
|
|
|
747
1509
|
};
|
|
748
1510
|
};
|
|
749
1511
|
|
|
1512
|
+
// src/fx/hand-hold.ts
|
|
1513
|
+
var HAND_HOLD = {
|
|
1514
|
+
/** Half the width of a pistol grip, the palm sits this far (plus the palm) off its middle. */
|
|
1515
|
+
gripHalfWidth: 0.0145,
|
|
1516
|
+
/** Half the thickness of a palm. */
|
|
1517
|
+
palm: 0.011,
|
|
1518
|
+
/** How far up the grip the web of the hand sits from its middle. */
|
|
1519
|
+
gripUp: 6e-3,
|
|
1520
|
+
/** How far down the grip the supporting hand of a pistol sits from the middle. */
|
|
1521
|
+
supportDown: 0.036,
|
|
1522
|
+
/** Where along the handguard (0 the receiver, 1 the end of the forend) the supporting hand closes. */
|
|
1523
|
+
forendAt: 0.6,
|
|
1524
|
+
/** How far under the handguard the palm sits (half a handguard plus the palm). */
|
|
1525
|
+
forendUnder: 0.034,
|
|
1526
|
+
/** The grip of a long gun that has no drawn pistol grip: how far under the receiver the hand sits. */
|
|
1527
|
+
strapUnder: 0.04,
|
|
1528
|
+
/** How far lower than touching the palm hangs under a handguard, so the fingers close under it and not over it. */
|
|
1529
|
+
forendDrop: 0.03,
|
|
1530
|
+
/** How far (radians) the fingers of a support hand under a handguard rise from level. */
|
|
1531
|
+
forendTilt: 0.5,
|
|
1532
|
+
/** How far a handgun's support hand dips for the new magazine, along the grip. */
|
|
1533
|
+
magDip: 0.045,
|
|
1534
|
+
/** Closure of the fingers on a grip and on a forend (0 open, 1 closed). */
|
|
1535
|
+
closed: 0.9,
|
|
1536
|
+
forend: 0.6,
|
|
1537
|
+
/** How fast the supporting hand goes to the magazine and back, per second. */
|
|
1538
|
+
magRate: 7,
|
|
1539
|
+
/** The hands shrink with the tool: a tool drawn at scale `s` takes hands at `s ** handScaleExponent`. */
|
|
1540
|
+
handScaleExponent: 0.55
|
|
1541
|
+
};
|
|
1542
|
+
var handFrame = (THREE, x, y) => {
|
|
1543
|
+
const ax = x.clone().normalize();
|
|
1544
|
+
const az = ax.clone().cross(y).normalize();
|
|
1545
|
+
const ay = az.clone().cross(ax);
|
|
1546
|
+
return new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(ax, ay, az));
|
|
1547
|
+
};
|
|
1548
|
+
var poseOf = (anchor, rootQuaternion) => {
|
|
1549
|
+
const position = anchor.part.localToWorld(anchor.offset.clone());
|
|
1550
|
+
const q = rootQuaternion.clone().multiply(anchor.quaternion);
|
|
1551
|
+
return { position: [position.x, position.y, position.z], quaternion: [q.x, q.y, q.z, q.w] };
|
|
1552
|
+
};
|
|
1553
|
+
var blendPose = (THREE, a, b, k) => {
|
|
1554
|
+
const q = new THREE.Quaternion(...a.quaternion).slerp(new THREE.Quaternion(...b.quaternion), k);
|
|
1555
|
+
return {
|
|
1556
|
+
position: [a.position[0] + (b.position[0] - a.position[0]) * k, a.position[1] + (b.position[1] - a.position[1]) * k, a.position[2] + (b.position[2] - a.position[2]) * k],
|
|
1557
|
+
quaternion: [q.x, q.y, q.z, q.w]
|
|
1558
|
+
};
|
|
1559
|
+
};
|
|
1560
|
+
var targetsOf = (THREE, hold, rootQuaternion, magazine, weights2) => {
|
|
1561
|
+
let right = hold.right ? poseOf(hold.right, rootQuaternion) : null;
|
|
1562
|
+
let left = null;
|
|
1563
|
+
if (hold.left) {
|
|
1564
|
+
left = poseOf(hold.left, rootQuaternion);
|
|
1565
|
+
if (hold.leftMagazine && magazine > 0) left = blendPose(THREE, left, poseOf(hold.leftMagazine, rootQuaternion), magazine);
|
|
1566
|
+
}
|
|
1567
|
+
hold.works?.forEach((work, index) => {
|
|
1568
|
+
const k = weights2?.[index] ?? 0;
|
|
1569
|
+
if (k <= 0) return;
|
|
1570
|
+
const pose = poseOf(work.anchor, rootQuaternion);
|
|
1571
|
+
if (work.hand === "R") right = right ? blendPose(THREE, right, pose, k) : pose;
|
|
1572
|
+
else left = left ? blendPose(THREE, left, pose, k) : pose;
|
|
1573
|
+
});
|
|
1574
|
+
return { R: right, L: left, grip: hold.grip, gripL: hold.gripLeft, scale: hold.scale };
|
|
1575
|
+
};
|
|
1576
|
+
var V = (THREE, x, y, z) => new THREE.Vector3(x, y, z);
|
|
1577
|
+
var gripFrame = (THREE, up, side) => {
|
|
1578
|
+
const forward = V(THREE, 1, 0, 0);
|
|
1579
|
+
const toSide = V(THREE, 0, 0, 1);
|
|
1580
|
+
const tilt = new THREE.Quaternion().setFromUnitVectors(V(THREE, 0, 1, 0), up);
|
|
1581
|
+
return handFrame(THREE, forward.clone().applyQuaternion(tilt), toSide.clone().applyQuaternion(tilt).multiplyScalar(side));
|
|
1582
|
+
};
|
|
1583
|
+
var forendFrame = (THREE, tilt = 0) => handFrame(THREE, V(THREE, 0, Math.sin(tilt), Math.cos(tilt)), V(THREE, 0, -Math.cos(tilt), Math.sin(tilt)));
|
|
1584
|
+
function holdOfModel(THREE, model, holderScale) {
|
|
1585
|
+
const { root, inner, parts } = model;
|
|
1586
|
+
const frames = parts.frames;
|
|
1587
|
+
const toRoot = (x, y, z) => V(THREE, inner.position.x + x * inner.scale.x, y, z);
|
|
1588
|
+
const handgun = parts.family === "pistol" || parts.family === "revolver";
|
|
1589
|
+
root.updateWorldMatrix(true, true);
|
|
1590
|
+
let gripAt;
|
|
1591
|
+
let up;
|
|
1592
|
+
if (parts.grip) {
|
|
1593
|
+
gripAt = toRoot(parts.grip.position.x, parts.grip.position.y, parts.grip.position.z);
|
|
1594
|
+
up = V(THREE, 0, 1, 0).applyQuaternion(parts.grip.quaternion);
|
|
1595
|
+
} else {
|
|
1596
|
+
const receiverHeight = frames?.receiverHeight ?? 0.06;
|
|
1597
|
+
gripAt = toRoot(frames?.gripX ?? 0.07, -receiverHeight / 2 - HAND_HOLD.strapUnder, 0);
|
|
1598
|
+
up = V(THREE, Math.sin(0.3), Math.cos(0.3), 0);
|
|
1599
|
+
}
|
|
1600
|
+
const side = HAND_HOLD.gripHalfWidth + HAND_HOLD.palm;
|
|
1601
|
+
const right = {
|
|
1602
|
+
part: root,
|
|
1603
|
+
offset: gripAt.clone().addScaledVector(up, HAND_HOLD.gripUp).add(V(THREE, 0, 0, side)),
|
|
1604
|
+
quaternion: gripFrame(THREE, up, 1)
|
|
1605
|
+
};
|
|
1606
|
+
let left = null;
|
|
1607
|
+
let leftMagazine = null;
|
|
1608
|
+
if (handgun) {
|
|
1609
|
+
left = { part: root, offset: gripAt.clone().addScaledVector(up, -HAND_HOLD.supportDown).add(V(THREE, 0, 0, -side)), quaternion: gripFrame(THREE, up, -1) };
|
|
1610
|
+
} else if (parts.pump) {
|
|
1611
|
+
const box2 = new THREE.Box3().setFromObject(parts.pump);
|
|
1612
|
+
const center = box2.getCenter(new THREE.Vector3());
|
|
1613
|
+
const under = parts.pump.worldToLocal(V(THREE, center.x, box2.min.y - HAND_HOLD.palm, center.z));
|
|
1614
|
+
left = { part: parts.pump, offset: under, quaternion: forendFrame(THREE) };
|
|
1615
|
+
} else if (frames) {
|
|
1616
|
+
const handguardHeight = frames.receiverHeight * 0.8;
|
|
1617
|
+
const x = frames.receiverEnd + (frames.handguardEnd - frames.receiverEnd) * HAND_HOLD.forendAt;
|
|
1618
|
+
left = { part: root, offset: toRoot(x, frames.barrelY - handguardHeight / 2 - HAND_HOLD.palm - HAND_HOLD.forendDrop, 0), quaternion: forendFrame(THREE, HAND_HOLD.forendTilt) };
|
|
1619
|
+
}
|
|
1620
|
+
if (left && parts.mag) {
|
|
1621
|
+
const box2 = new THREE.Box3().setFromObject(parts.mag);
|
|
1622
|
+
const center = box2.getCenter(new THREE.Vector3());
|
|
1623
|
+
const local = parts.mag.worldToLocal(V(THREE, center.x, box2.min.y - HAND_HOLD.palm, center.z));
|
|
1624
|
+
leftMagazine = handgun && left ? { part: root, offset: left.offset.clone().addScaledVector(up, -HAND_HOLD.magDip), quaternion: left.quaternion } : { part: parts.mag, offset: local, quaternion: forendFrame(THREE) };
|
|
1625
|
+
}
|
|
1626
|
+
return {
|
|
1627
|
+
right,
|
|
1628
|
+
left,
|
|
1629
|
+
leftMagazine,
|
|
1630
|
+
scale: holderScale ** HAND_HOLD.handScaleExponent,
|
|
1631
|
+
grip: HAND_HOLD.closed,
|
|
1632
|
+
gripLeft: HAND_HOLD.forend
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
function holdOfProp(THREE, group, category, scale) {
|
|
1636
|
+
const handle = group.getObjectByName("handle") ?? group.getObjectByName("body") ?? group;
|
|
1637
|
+
const head = group.getObjectByName("head");
|
|
1638
|
+
const size = (object) => new THREE.Box3().setFromObject(object).getSize(new THREE.Vector3());
|
|
1639
|
+
const wrap = handFrame(THREE, V(THREE, 0, 1, 0), V(THREE, 0, 0, 1));
|
|
1640
|
+
const handleSize = size(handle);
|
|
1641
|
+
const right = { part: group, offset: handle.position.clone().add(V(THREE, 0, handleSize.y / 2 + 2e-3, 0)), quaternion: wrap };
|
|
1642
|
+
const long = head && ["smg", "carbine", "rifle", "sniper", "shotgun", "crossbow", "flame"].includes(category);
|
|
1643
|
+
const headSize = head ? size(head) : null;
|
|
1644
|
+
const left = long && head && headSize ? { part: group, offset: head.position.clone().add(V(THREE, -headSize.x * 0.15, -headSize.y / 2 - HAND_HOLD.palm, 0)), quaternion: forendFrame(THREE) } : null;
|
|
1645
|
+
return { right, left, leftMagazine: null, scale: scale ** HAND_HOLD.handScaleExponent, grip: HAND_HOLD.closed, gripLeft: HAND_HOLD.forend };
|
|
1646
|
+
}
|
|
1647
|
+
|
|
750
1648
|
// src/props.ts
|
|
751
|
-
import { mergeByMaterial } from "@adatechnology/game-
|
|
1649
|
+
import { mergeByMaterial, shapeGeometries as shapeGeometries2 } from "@adatechnology/game-shapes-three";
|
|
752
1650
|
|
|
753
1651
|
// src/models/index.ts
|
|
754
1652
|
import { WEAPONS } from "@adatechnology/game-weapons";
|
|
@@ -2306,6 +3204,9 @@ var models_default = {
|
|
|
2306
3204
|
}
|
|
2307
3205
|
};
|
|
2308
3206
|
|
|
3207
|
+
// src/models/kit.ts
|
|
3208
|
+
import { shapeGeometries } from "@adatechnology/game-shapes-three";
|
|
3209
|
+
|
|
2309
3210
|
// src/models/rounded-box.ts
|
|
2310
3211
|
var uvAlong = (THREE, faceDirection, normal, uvAxis, projectionAxis, radius, sideLength) => {
|
|
2311
3212
|
const arcLength = 2 * Math.PI * radius / 4;
|
|
@@ -2332,11 +3233,11 @@ function createRoundedBoxGeometry(THREE, width, height, depth, segments, radius)
|
|
|
2332
3233
|
const count = segments * 2 + 1;
|
|
2333
3234
|
const rounding = Math.min(width / 2, height / 2, depth / 2, radius);
|
|
2334
3235
|
const geometry = new THREE.BoxGeometry(1, 1, 1, count, count, count);
|
|
2335
|
-
const
|
|
3236
|
+
const flat2 = geometry.toNonIndexed();
|
|
2336
3237
|
geometry.setIndex(null);
|
|
2337
|
-
geometry.setAttribute("position",
|
|
2338
|
-
geometry.setAttribute("normal",
|
|
2339
|
-
geometry.setAttribute("uv",
|
|
3238
|
+
geometry.setAttribute("position", flat2.attributes.position);
|
|
3239
|
+
geometry.setAttribute("normal", flat2.attributes.normal);
|
|
3240
|
+
geometry.setAttribute("uv", flat2.attributes.uv);
|
|
2340
3241
|
const position = new THREE.Vector3();
|
|
2341
3242
|
const normal = new THREE.Vector3();
|
|
2342
3243
|
const box2 = new THREE.Vector3(width, height, depth).divideScalar(2).subScalar(rounding);
|
|
@@ -2417,9 +3318,10 @@ var createMaterials = (THREE) => {
|
|
|
2417
3318
|
};
|
|
2418
3319
|
};
|
|
2419
3320
|
var kits = /* @__PURE__ */ new WeakMap();
|
|
2420
|
-
function modelKit(
|
|
2421
|
-
const cached = kits.get(
|
|
3321
|
+
function modelKit(host) {
|
|
3322
|
+
const cached = kits.get(host);
|
|
2422
3323
|
if (cached) return cached;
|
|
3324
|
+
const THREE = shapeGeometries(host);
|
|
2423
3325
|
const geometries = /* @__PURE__ */ new Map();
|
|
2424
3326
|
const shadowed = (mesh) => {
|
|
2425
3327
|
mesh.castShadow = true;
|
|
@@ -2478,7 +3380,7 @@ function modelKit(THREE) {
|
|
|
2478
3380
|
return group;
|
|
2479
3381
|
}
|
|
2480
3382
|
};
|
|
2481
|
-
kits.set(
|
|
3383
|
+
kits.set(host, kit);
|
|
2482
3384
|
return kit;
|
|
2483
3385
|
}
|
|
2484
3386
|
function heatify(glow, mesh) {
|
|
@@ -2661,6 +3563,7 @@ function buildBullpup(kit, options) {
|
|
|
2661
3563
|
const gx = bodyLength * 0.8;
|
|
2662
3564
|
const grip = kit.pivot(group, gx, -0.085, 0);
|
|
2663
3565
|
grip.rotation.z = -0.25;
|
|
3566
|
+
parts.grip = grip;
|
|
2664
3567
|
box2(grip, 0.03, 0.1, 0.03, M.poly, 0, 0, 0);
|
|
2665
3568
|
box2(group, 0.1, 6e-3, 0.03, body, gx + 0.01, -0.06, 0);
|
|
2666
3569
|
parts.trigger = box2(group, 4e-3, 0.016, 4e-3, M.dark, gx + 0.02, -0.05, 0, 0.15);
|
|
@@ -2880,6 +3783,7 @@ function buildAt4(kit, group, parts) {
|
|
|
2880
3783
|
box2(group, 0.012, 0.05, 0.02, M.dark, 0.71, 0.08, -0.035);
|
|
2881
3784
|
const grip = kit.pivot(group, 0.68, -0.05, 0);
|
|
2882
3785
|
grip.rotation.z = -0.15;
|
|
3786
|
+
parts.grip = grip;
|
|
2883
3787
|
box2(grip, 0.035, 0.11, 0.032, M.dark, 0, -0.05, 0);
|
|
2884
3788
|
box2(group, 0.14, 0.035, 0.06, M.rubber, 0.32, -0.06, 0);
|
|
2885
3789
|
box2(group, 0.3, 0.012, 0.055, M.dark, 0.5, -0.05, 0);
|
|
@@ -3209,6 +4113,7 @@ function buildFrame(kit, group, parts, options, style, size, materials) {
|
|
|
3209
4113
|
const gy = -h / 2 - gripLength / 2 + 6e-3;
|
|
3210
4114
|
const grip = kit.pivot(group, gx, gy, 0);
|
|
3211
4115
|
grip.rotation.z = style.ga;
|
|
4116
|
+
parts.grip = grip;
|
|
3212
4117
|
const gripWidth = style.grip === "chunky" ? w * 1.18 : w * 1.06;
|
|
3213
4118
|
const gripDepth = style.grip === "chunky" ? 0.036 : 0.032;
|
|
3214
4119
|
kit.roundness = 0.3;
|
|
@@ -3332,6 +4237,7 @@ function buildRevolver(kit, options) {
|
|
|
3332
4237
|
parts.hammer = hammer;
|
|
3333
4238
|
const grip = kit.pivot(group, 0.024, -0.062, 0);
|
|
3334
4239
|
grip.rotation.z = -0.38;
|
|
4240
|
+
parts.grip = grip;
|
|
3335
4241
|
box2(grip, 0.034, 0.1, 0.031, options.wood ? M.wood : M.rubber, 0, 0, 0);
|
|
3336
4242
|
if (!options.wood) for (let i = 0; i < 4; i++) box2(grip, 6e-3, 0.07, 0.033, M.dark, -0.012 + i * 8e-3, 0, 0);
|
|
3337
4243
|
box2(group, 0.038, 4e-3, 8e-3, metal2, 0.062, -0.036, 0);
|
|
@@ -3520,6 +4426,7 @@ function buildGrip(context) {
|
|
|
3520
4426
|
if (!context.isTraditional && o.grip !== "none") {
|
|
3521
4427
|
const grip = kit.pivot(group, gx - 0.012, -hR / 2 - 0.046, 0);
|
|
3522
4428
|
grip.rotation.z = -0.3;
|
|
4429
|
+
parts.grip = grip;
|
|
3523
4430
|
box2(grip, 0.032, 0.105, 0.028, furn, 0, 0, 0);
|
|
3524
4431
|
for (let i = 0; i < 3; i++) box2(grip, 6e-3, 0.012, 0.026, furn, 0.017, 0.022 - i * 0.02, 0);
|
|
3525
4432
|
box2(grip, 0.034, 8e-3, 0.03, furn, 0, -0.052, 0);
|
|
@@ -3899,7 +4806,7 @@ function buildProp(THREE, prop) {
|
|
|
3899
4806
|
if (part.shape.kind !== "box") continue;
|
|
3900
4807
|
const [w, h, d] = part.shape.size;
|
|
3901
4808
|
const material = new THREE.MeshStandardMaterial({ color: prop.colors[part.id] ?? 7829367, roughness: 0.6, metalness: part.material === "steel" ? 0.6 : 0.05 });
|
|
3902
|
-
const mesh = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), material);
|
|
4809
|
+
const mesh = new THREE.Mesh(new (shapeGeometries2(THREE)).BoxGeometry(w, h, d), material);
|
|
3903
4810
|
mesh.name = part.id;
|
|
3904
4811
|
mesh.position.set(part.position[0], part.position[1], part.position[2]);
|
|
3905
4812
|
mesh.castShadow = true;
|
|
@@ -3970,11 +4877,13 @@ function createBoxFx(options) {
|
|
|
3970
4877
|
let model = null;
|
|
3971
4878
|
let inHand = null;
|
|
3972
4879
|
let def = null;
|
|
4880
|
+
let hold = null;
|
|
3973
4881
|
let kick = 0;
|
|
3974
4882
|
let dip = 0;
|
|
3975
4883
|
let reloading = false;
|
|
3976
4884
|
const v = new THREE.Vector3();
|
|
3977
4885
|
const q = new THREE.Quaternion();
|
|
4886
|
+
const q2 = new THREE.Quaternion();
|
|
3978
4887
|
const pointForward = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
|
|
3979
4888
|
const equip = (weapon) => {
|
|
3980
4889
|
if (model) hand.remove(model);
|
|
@@ -3988,6 +4897,7 @@ function createBoxFx(options) {
|
|
|
3988
4897
|
model.quaternion.copy(pointForward);
|
|
3989
4898
|
hand.add(model);
|
|
3990
4899
|
inHand = def.id;
|
|
4900
|
+
hold = holdOfProp(THREE, model, def.category, model.scale.x);
|
|
3991
4901
|
};
|
|
3992
4902
|
const handPoint = (x, y, z, out) => out.set(HAND_OFFSET[0] + x, HAND_OFFSET[1] + y, HAND_OFFSET[2] + z).applyQuaternion(camera.quaternion).add(camera.position);
|
|
3993
4903
|
const throwBody = (kind, mesh, from, velocity, radius) => {
|
|
@@ -4079,9 +4989,15 @@ function createBoxFx(options) {
|
|
|
4079
4989
|
if (model) hand.remove(model);
|
|
4080
4990
|
model = null;
|
|
4081
4991
|
inHand = null;
|
|
4992
|
+
hold = null;
|
|
4993
|
+
},
|
|
4994
|
+
handTargets() {
|
|
4995
|
+
if (!model || !hold) return null;
|
|
4996
|
+
hand.updateMatrixWorld(true);
|
|
4997
|
+
return targetsOf(THREE, hold, model.getWorldQuaternion(q2), 0);
|
|
4082
4998
|
},
|
|
4083
4999
|
handle(event, tool) {
|
|
4084
|
-
if (event.type === "explosion") return;
|
|
5000
|
+
if (event.type === "explosion" || event.type === "player") return;
|
|
4085
5001
|
const weapon = arsenal.weapons.get(event.weapon);
|
|
4086
5002
|
if (event.type === "tool") equip(event.weapon);
|
|
4087
5003
|
if (!weapon) return;
|
|
@@ -4136,6 +5052,7 @@ var ease = (progress) => progress < 0.5 ? 2 * progress * progress : 1 - (-2 * pr
|
|
|
4136
5052
|
var move = (part, axis, start, duration, from, to) => ({ part, channel: "position", axis, start, duration, from, to });
|
|
4137
5053
|
var turn = (part, axis, start, duration, from, to) => ({ part, channel: "rotation", axis, start, duration, from, to });
|
|
4138
5054
|
var cue = (t, type, data) => ({ t, type, ...data ? { data } : {} });
|
|
5055
|
+
var sound2 = (t, id) => ({ t, type: "sound", data: { id } });
|
|
4139
5056
|
var stage = (id, t) => ({ t, type: STAGE_EVENT, data: { stage: id } });
|
|
4140
5057
|
var valueAt = (tweens, t, includeInstants) => {
|
|
4141
5058
|
let value = 0;
|
|
@@ -4233,7 +5150,7 @@ function magazineSwap(parts, start) {
|
|
|
4233
5150
|
move("mag", "y", start + 0.16, 0, 0, direction * drop * 2.2),
|
|
4234
5151
|
move("mag", "y", start + MAGAZINE_IN_AT, 0.3, direction * drop * 2.2, 0)
|
|
4235
5152
|
],
|
|
4236
|
-
events: [cue(start + 0.16, "drop_magazine", { up: direction > 0 })],
|
|
5153
|
+
events: [sound2(start, "mag_release"), cue(start + 0.16, "drop_magazine", { up: direction > 0 }), sound2(start + 0.16, "mag_out"), sound2(start + MAGAZINE_IN_AT, "mag_insert"), sound2(start + MAGAZINE_IN_AT + 0.3, "mag_seat")],
|
|
4237
5154
|
end: start + MAGAZINE_SWAP_SECONDS
|
|
4238
5155
|
};
|
|
4239
5156
|
}
|
|
@@ -4306,43 +5223,52 @@ function clearClips(context) {
|
|
|
4306
5223
|
// src/animation/reload-clips.ts
|
|
4307
5224
|
import { RELOAD_STAGES } from "@adatechnology/game-weapons";
|
|
4308
5225
|
var KINDS = ["tactical", "empty", "one_by_one"];
|
|
5226
|
+
var chargeSounds = (start) => [sound2(start, "charging_handle_pull"), sound2(start + 0.2, "charging_handle_release")];
|
|
4309
5227
|
var charge = (context, start) => {
|
|
4310
5228
|
const travel = strokeOf(context.parts);
|
|
4311
5229
|
return [move("bolt", "x", start, 0.12, 0, travel), move("bolt", "x", start + 0.18, 0.06, travel, 0)];
|
|
4312
5230
|
};
|
|
5231
|
+
var boltSounds = (start) => [sound2(start, "bolt_lift"), sound2(start + 0.1, "bolt_pull"), sound2(start + 0.26, "bolt_push"), sound2(start + 0.45, "bolt_lock")];
|
|
4313
5232
|
var cycleWithoutEject = (start) => ({ tweens: boltStroke(start), events: [], end: start + 0.55 });
|
|
5233
|
+
var RACK_BACK = 0.14;
|
|
5234
|
+
var RACK_HOLD = 0.04;
|
|
5235
|
+
var RACK_FORWARD = 0.06;
|
|
4314
5236
|
function magazineReload(context, kind) {
|
|
4315
5237
|
const { parts, family } = context;
|
|
4316
5238
|
const swap = magazineSwap(parts, 0);
|
|
4317
5239
|
const draft = { tweens: [...swap.tweens], events: [...swap.events], end: swap.end };
|
|
4318
5240
|
if (family === "pistol") {
|
|
4319
5241
|
if (kind === "empty" && parts.slide) {
|
|
4320
|
-
|
|
4321
|
-
draft.end
|
|
5242
|
+
const travel = strokeOf(parts);
|
|
5243
|
+
draft.tweens.push(move("slide", "x", swap.end + 0.04, RACK_BACK, 0, travel), move("slide", "x", swap.end + 0.04 + RACK_BACK + RACK_HOLD, RACK_FORWARD, travel, 0));
|
|
5244
|
+
draft.events.push(sound2(swap.end + 0.04 + RACK_BACK + RACK_HOLD + 0.02, "slide_release"));
|
|
5245
|
+
draft.end += 0.04 + RACK_BACK + RACK_HOLD + RACK_FORWARD + 0.08;
|
|
4322
5246
|
}
|
|
4323
5247
|
} else if (family === "boltmag") {
|
|
4324
5248
|
const stroke = cycleWithoutEject(swap.end);
|
|
4325
5249
|
draft.tweens.push(...stroke.tweens);
|
|
5250
|
+
draft.events.push(...boltSounds(swap.end));
|
|
4326
5251
|
draft.end = stroke.end;
|
|
4327
5252
|
} else if (kind === "empty" && parts.bolt) {
|
|
4328
5253
|
draft.tweens.push(...charge(context, swap.end));
|
|
5254
|
+
draft.events.push(...chargeSounds(swap.end));
|
|
4329
5255
|
draft.end += 0.35;
|
|
4330
5256
|
}
|
|
4331
5257
|
return draft;
|
|
4332
5258
|
}
|
|
4333
5259
|
function revolverReload(kind) {
|
|
4334
|
-
const events = [cue(0.32, "eject_casing", { from: "chambers", down: false })];
|
|
4335
|
-
if (kind !== "one_by_one") events.push(cue(0.7, "move_in", { from: "chambers" }));
|
|
5260
|
+
const events = [sound2(0, "cylinder_open"), cue(0.32, "eject_casing", { from: "chambers", down: false }), sound2(0.32, "cylinder_eject"), sound2(1.2, "cylinder_close")];
|
|
5261
|
+
if (kind !== "one_by_one") events.push(cue(0.7, "move_in", { from: "chambers" }), sound2(0.7, "speedloader"));
|
|
4336
5262
|
return { tweens: [turn("crane", "x", 0, 0.22, 0, -1.25), turn("crane", "x", 1.05, 0.2, -1.25, 0)], events, end: 1.3 };
|
|
4337
5263
|
}
|
|
4338
5264
|
var breakReload = () => ({
|
|
4339
5265
|
tweens: [turn("barrels", "z", 0, 0.2, 0, -0.5), turn("barrels", "z", 1, 0.15, -0.5, 0)],
|
|
4340
|
-
events: [cue(0.25, "eject_casing", { from: "breech", down: false }), cue(0.65, "move_in", { from: "breech" })],
|
|
5266
|
+
events: [sound2(0, "barrels_open"), cue(0.25, "eject_casing", { from: "breech", down: false }), sound2(0.25, "casings_eject"), cue(0.65, "move_in", { from: "breech" }), sound2(0.65, "shell_insert"), sound2(0.8, "shell_insert"), sound2(1.13, "barrels_close")],
|
|
4341
5267
|
end: 1.2
|
|
4342
5268
|
});
|
|
4343
5269
|
var beltReload = (context) => ({
|
|
4344
5270
|
tweens: [turn("cover", "z", 0, 0.2, 0, 0.9), move("belt", "z", 0.2, 0.25, 0, -0.3), move("belt", "z", 0.55, 0.3, -0.3, 0), turn("cover", "z", 0.9, 0.18, 0.9, 0), ...charge(context, 1.15)],
|
|
4345
|
-
events: [],
|
|
5271
|
+
events: [sound2(0, "belt_cover_open"), sound2(0.8, "belt_insert"), sound2(1.05, "belt_cover_close"), ...chargeSounds(1.15)],
|
|
4346
5272
|
end: 1.55
|
|
4347
5273
|
});
|
|
4348
5274
|
var UNIT = 1;
|
|
@@ -4350,9 +5276,16 @@ function tubeReload(context, kind) {
|
|
|
4350
5276
|
const { parts, family } = context;
|
|
4351
5277
|
const draft = { tweens: [], events: [], end: UNIT };
|
|
4352
5278
|
if (kind !== "empty") return draft;
|
|
4353
|
-
if (family === "pump" && parts.pump)
|
|
4354
|
-
|
|
4355
|
-
|
|
5279
|
+
if (family === "pump" && parts.pump) {
|
|
5280
|
+
draft.tweens.push(move("pump", "x", 0.88, 0.05, 0, PUMP_TRAVEL), move("pump", "x", 0.94, 0.05, PUMP_TRAVEL, 0));
|
|
5281
|
+
draft.events.push(sound2(0.88, "pump_back"), sound2(0.94, "pump_forward"));
|
|
5282
|
+
} else if (family === "lever" && parts.lever) {
|
|
5283
|
+
draft.tweens.push(turn("lever", "z", 0.87, 0.06, 0, LEVER_TURN), turn("lever", "z", 0.94, 0.06, LEVER_TURN, 0));
|
|
5284
|
+
draft.events.push(sound2(0.87, "lever_open"), sound2(0.94, "lever_close"));
|
|
5285
|
+
} else if (parts.bolt) {
|
|
5286
|
+
draft.tweens.push(move("bolt", "x", 0.88, 0.07, 0, strokeOf(parts)), move("bolt", "x", 0.95, 0.04, strokeOf(parts), 0));
|
|
5287
|
+
draft.events.push(sound2(0.88, "charging_handle_pull"), sound2(0.95, "charging_handle_release"));
|
|
5288
|
+
}
|
|
4356
5289
|
return draft;
|
|
4357
5290
|
}
|
|
4358
5291
|
function boltReload(context) {
|
|
@@ -4366,13 +5299,13 @@ function boltReload(context) {
|
|
|
4366
5299
|
move("bolt", "x", 0.86, 0.07, BOLT_TRAVEL, 0),
|
|
4367
5300
|
turn("boltHandle", "x", 0.93, 0.05, BOLT_TURN, 0)
|
|
4368
5301
|
],
|
|
4369
|
-
events: [],
|
|
5302
|
+
events: [sound2(0, "bolt_lift"), sound2(0.05, "bolt_pull"), sound2(0.86, "bolt_push"), sound2(0.95, "bolt_lock")],
|
|
4370
5303
|
end: UNIT
|
|
4371
5304
|
};
|
|
4372
5305
|
}
|
|
4373
5306
|
function internalMagazineReload(context) {
|
|
4374
5307
|
const travel = strokeOf(context.parts);
|
|
4375
|
-
return { tweens: context.parts.bolt ? [move("bolt", "x", 0, 0.04, 0, travel), move("bolt", "x", 0.89, 0.04, travel, 0)] : [], events: [], end: UNIT };
|
|
5308
|
+
return { tweens: context.parts.bolt ? [move("bolt", "x", 0, 0.04, 0, travel), move("bolt", "x", 0.89, 0.04, travel, 0)] : [], events: context.parts.bolt ? [sound2(0.04, "charging_handle_pull"), sound2(0.89, "charging_handle_release")] : [], end: UNIT };
|
|
4376
5309
|
}
|
|
4377
5310
|
function heavyReload(context) {
|
|
4378
5311
|
const { parts, family } = context;
|
|
@@ -4380,20 +5313,20 @@ function heavyReload(context) {
|
|
|
4380
5313
|
if (parts.disposable) {
|
|
4381
5314
|
return {
|
|
4382
5315
|
tweens: [move(ROOT, "y", 0.6, 0, 0, -0.6), move(ROOT, "y", 0.6, 0.5, -0.6, 0)],
|
|
4383
|
-
events: [cue(0, "drop_tube"), cue(0.6, "seat_tube")],
|
|
5316
|
+
events: [cue(0, "drop_tube"), sound2(0, "tube_drop"), cue(0.6, "seat_tube"), sound2(1.08, "tube_seat")],
|
|
4384
5317
|
end: 1.15
|
|
4385
5318
|
};
|
|
4386
5319
|
}
|
|
4387
5320
|
if (parts.round) {
|
|
4388
5321
|
return {
|
|
4389
5322
|
tweens: [move("round", "x", 0, 0.55, 0.45, 0), move("round", "y", 0, 0.55, 0.12, 0)],
|
|
4390
|
-
events: [cue(0, "show_round")],
|
|
5323
|
+
events: [cue(0, "show_round"), sound2(0, "rocket_load"), sound2(0.55, "rocket_seat")],
|
|
4391
5324
|
end: 0.6
|
|
4392
5325
|
};
|
|
4393
5326
|
}
|
|
4394
5327
|
return {
|
|
4395
5328
|
tweens: [turn("breech_0", "y", 0, 0.25, 0, -1.6), turn("breech_0", "y", 0.76, 0.22, -1.6, 0)],
|
|
4396
|
-
events: [cue(0.3, "breech_round")],
|
|
5329
|
+
events: [sound2(0, "breech_open"), cue(0.3, "breech_round"), sound2(0.3, "grenade_load"), sound2(0.96, "breech_close")],
|
|
4397
5330
|
end: 1.05
|
|
4398
5331
|
};
|
|
4399
5332
|
}
|
|
@@ -4424,11 +5357,12 @@ function reloadClips(context) {
|
|
|
4424
5357
|
}
|
|
4425
5358
|
return clips.length > 0 ? clips : first ? [renamed(first, "reload")] : [];
|
|
4426
5359
|
}
|
|
5360
|
+
var FEED_SECONDS = 0.4;
|
|
4427
5361
|
function feedClip(context) {
|
|
4428
5362
|
const { family } = context;
|
|
4429
|
-
if (family === "revolver") return composeClip("feed", [], [cue(0, "move_in", { from: "chambers", next: true })], 0.24);
|
|
4430
|
-
if (family === "bolt" || family === "semiint") return composeClip("feed", [], [cue(0, "move_in", { from: "topLoad" })
|
|
4431
|
-
if (isTube(context)) return composeClip("feed", [], [cue(0, "move_in", { from: "loadPort" })
|
|
5363
|
+
if (family === "revolver") return composeClip("feed", [], [cue(0, "move_in", { from: "chambers", next: true }), sound2(0.1, "shell_insert")], 0.24);
|
|
5364
|
+
if (family === "bolt" || family === "semiint") return composeClip("feed", [], [cue(0, "move_in", { from: "topLoad" }), sound2(0.2, "shell_insert")], FEED_SECONDS);
|
|
5365
|
+
if (isTube(context)) return composeClip("feed", [], [cue(0, "move_in", { from: "loadPort" }), sound2(0.2, "shell_insert")], FEED_SECONDS);
|
|
4432
5366
|
return void 0;
|
|
4433
5367
|
}
|
|
4434
5368
|
|
|
@@ -5131,6 +6065,7 @@ function buildRound(kit, dims, shape) {
|
|
|
5131
6065
|
import { createVfxRenderer } from "@adatechnology/game-vfx-three";
|
|
5132
6066
|
|
|
5133
6067
|
// src/fx/effects.ts
|
|
6068
|
+
import { MUZZLE_SMOKE } from "@adatechnology/game-weapons";
|
|
5134
6069
|
var FLASH_BY_CASE = { shell: 1.6, big: 2.6, tiny: 0.5, pistol: 0.8, rifle: 1.2 };
|
|
5135
6070
|
var VIEWMODEL_EFFECT_IDS = { flash: "viewmodel_muzzle_flash", smoke: "weapon_smoke_puff", backblast: "launcher_backblast", flameJet: "flame_jet" };
|
|
5136
6071
|
var vec = (vector) => [vector.x, vector.y, vector.z];
|
|
@@ -5139,8 +6074,9 @@ function createEffects(THREE, sink, random) {
|
|
|
5139
6074
|
const seed = () => 1 + Math.floor(random() * 2147483646);
|
|
5140
6075
|
const barrel = (orientation) => vec(new THREE.Vector3(1, 0, 0).applyQuaternion(orientation));
|
|
5141
6076
|
return {
|
|
5142
|
-
muzzleFlash(position, orientation, size) {
|
|
5143
|
-
|
|
6077
|
+
muzzleFlash(position, orientation, size, profile = MUZZLE_SMOKE.smokeless) {
|
|
6078
|
+
if (profile.puffs <= 0 && profile.flash <= 0) return;
|
|
6079
|
+
sink?.play({ effect: VIEWMODEL_EFFECT_IDS.flash, point: vec(position), direction: barrel(orientation), orientation: quaternion(orientation), seed: seed(), values: { size, ...profile } });
|
|
5144
6080
|
},
|
|
5145
6081
|
smoke(position, velocity, size, life, opacity) {
|
|
5146
6082
|
sink?.play({ effect: VIEWMODEL_EFFECT_IDS.smoke, point: vec(position), seed: seed(), values: { vx: velocity.x, vy: velocity.y, vz: velocity.z, radius: size, life, opacity } });
|
|
@@ -5165,13 +6101,17 @@ var flameMaterials = (kit) => {
|
|
|
5165
6101
|
new THREE.MeshBasicMaterial({ color: 16773296, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false })
|
|
5166
6102
|
];
|
|
5167
6103
|
};
|
|
5168
|
-
function launchRocket(context) {
|
|
6104
|
+
function launchRocket(context, flyRound = true) {
|
|
5169
6105
|
const { kit, scene, parts, random } = context;
|
|
5170
6106
|
const { THREE } = kit;
|
|
5171
6107
|
const forward = context.forward();
|
|
5172
6108
|
const muzzle = context.positionOf(parts.muzzle);
|
|
5173
6109
|
const rear = context.positionOf(parts.rear);
|
|
5174
6110
|
context.effects.backblast(rear, context.orientation());
|
|
6111
|
+
if (!flyRound) {
|
|
6112
|
+
if (parts.round) parts.round.visible = false;
|
|
6113
|
+
return;
|
|
6114
|
+
}
|
|
5175
6115
|
const rocket = parts.makeRound ? parts.makeRound() : buildFlyingRocket(kit, parts.launcherType);
|
|
5176
6116
|
if (parts.round) parts.round.visible = false;
|
|
5177
6117
|
rocket.quaternion.copy(context.orientation());
|
|
@@ -5366,6 +6306,7 @@ var toWorld = (session, vector, scaled = false) => {
|
|
|
5366
6306
|
};
|
|
5367
6307
|
|
|
5368
6308
|
// src/fx/viewmodel-events.ts
|
|
6309
|
+
import { muzzleSmokeOf } from "@adatechnology/game-weapons";
|
|
5369
6310
|
var PI13 = Math.PI;
|
|
5370
6311
|
var UGL_FLASH = 1.6;
|
|
5371
6312
|
var restFlat = (object) => {
|
|
@@ -5471,7 +6412,11 @@ function createClipEventHandler(world, session) {
|
|
|
5471
6412
|
case "muzzle_flash":
|
|
5472
6413
|
if (data.at === "ugl") {
|
|
5473
6414
|
if (parts.uglMuzzle && world.layers.muzzleFlash) effects.muzzleFlash(positionOf(parts.uglMuzzle), root.getWorldQuaternion(new THREE.Quaternion()), UGL_FLASH);
|
|
5474
|
-
} else if (parts.muzzle && world.layers.muzzleFlash
|
|
6415
|
+
} else if (parts.muzzle && world.layers.muzzleFlash && (session.def.action.kind === "firearm" || session.def.action.kind === "launcher")) {
|
|
6416
|
+
const profile = muzzleSmokeOf({ caliber: session.caliber, weapon: session.def, suppressed: session.suppressed });
|
|
6417
|
+
const size = session.def.action.kind === "launcher" ? 2.2 : FLASH_BY_CASE[session.caseType];
|
|
6418
|
+
effects.muzzleFlash(positionOf(parts.muzzle), root.getWorldQuaternion(new THREE.Quaternion()), size, profile);
|
|
6419
|
+
}
|
|
5475
6420
|
break;
|
|
5476
6421
|
case "eject_casing":
|
|
5477
6422
|
if (data.from === "chambers") ejectAll(parts.chambers, () => ({ velocity: vector(-0.5 - random() * 0.3, -0.1 + random() * 0.3, (random() - 0.5) * 0.25), spin: vector(10, 20, 5), tilt: 0 }));
|
|
@@ -5502,6 +6447,9 @@ function createClipEventHandler(world, session) {
|
|
|
5502
6447
|
case "breech_round":
|
|
5503
6448
|
breechRound();
|
|
5504
6449
|
break;
|
|
6450
|
+
case "sound":
|
|
6451
|
+
world.sound(session.id, String(data.id), session.soundProfile);
|
|
6452
|
+
break;
|
|
5505
6453
|
default:
|
|
5506
6454
|
break;
|
|
5507
6455
|
}
|
|
@@ -5525,10 +6473,321 @@ function createClipEventHandler(world, session) {
|
|
|
5525
6473
|
}
|
|
5526
6474
|
}
|
|
5527
6475
|
|
|
6476
|
+
// src/fx/hand-work.ts
|
|
6477
|
+
import { sampleClip } from "@adatechnology/game-contracts";
|
|
6478
|
+
|
|
6479
|
+
// src/data/hand-work.json
|
|
6480
|
+
var hand_work_default = {
|
|
6481
|
+
source: "Authored in the kit: the gallery's clips move the parts (slide, bolt, bolt handle, crane, cover) but the Galeria 3D do Arsenal draws no hands, so which hand works which part is written here. Times are not here: a hand takes a part while the clip of the weapon moves it, so they follow the clip and the seconds of the rule's reload.",
|
|
6482
|
+
timing: {
|
|
6483
|
+
lead: 0.08,
|
|
6484
|
+
tail: 0.06,
|
|
6485
|
+
comment: "fractions of the clip's duration: the hand arrives this long before the part starts to move and lets go this long after it stops"
|
|
6486
|
+
},
|
|
6487
|
+
pistolRelease: {
|
|
6488
|
+
comment: "Pistols whose slide stop is worked with the thumb of the shooting hand after the magazine is in. Empty by default: every pistol is racked by the support hand taking the rear of the slide (the reading shot of the move). Put an id here only for a pistol that is meant to be released by the thumb.",
|
|
6489
|
+
weapons: []
|
|
6490
|
+
},
|
|
6491
|
+
akCharge: {
|
|
6492
|
+
comment: "Guns charged by a handle on the right of the receiver, reached over the top by the support hand; the rest of the magazine guns are charged at the rear top (AR style).",
|
|
6493
|
+
weapons: [
|
|
6494
|
+
"ak_47",
|
|
6495
|
+
"akm",
|
|
6496
|
+
"ak_74",
|
|
6497
|
+
"ak_12",
|
|
6498
|
+
"rpk",
|
|
6499
|
+
"saiga_12",
|
|
6500
|
+
"sks",
|
|
6501
|
+
"m14_ebr",
|
|
6502
|
+
"fn_fal",
|
|
6503
|
+
"fn_parafal",
|
|
6504
|
+
"hk_g3a3",
|
|
6505
|
+
"hk_psg_1",
|
|
6506
|
+
"svd_dragunov"
|
|
6507
|
+
]
|
|
6508
|
+
},
|
|
6509
|
+
works: [
|
|
6510
|
+
{
|
|
6511
|
+
family: "pistol",
|
|
6512
|
+
clips: [
|
|
6513
|
+
"reload_empty"
|
|
6514
|
+
],
|
|
6515
|
+
part: "slide",
|
|
6516
|
+
hand: "L",
|
|
6517
|
+
anchor: "slide_rear",
|
|
6518
|
+
style: "rack"
|
|
6519
|
+
},
|
|
6520
|
+
{
|
|
6521
|
+
family: "pistol",
|
|
6522
|
+
clips: [
|
|
6523
|
+
"reload_empty"
|
|
6524
|
+
],
|
|
6525
|
+
part: "slide",
|
|
6526
|
+
hand: "R",
|
|
6527
|
+
anchor: "thumb_release",
|
|
6528
|
+
style: "release"
|
|
6529
|
+
},
|
|
6530
|
+
{
|
|
6531
|
+
family: "rifle",
|
|
6532
|
+
clips: [
|
|
6533
|
+
"reload_empty"
|
|
6534
|
+
],
|
|
6535
|
+
part: "bolt",
|
|
6536
|
+
hand: "L",
|
|
6537
|
+
anchor: "rear_top",
|
|
6538
|
+
style: "ar"
|
|
6539
|
+
},
|
|
6540
|
+
{
|
|
6541
|
+
family: "rifle",
|
|
6542
|
+
clips: [
|
|
6543
|
+
"reload_empty"
|
|
6544
|
+
],
|
|
6545
|
+
part: "bolt",
|
|
6546
|
+
hand: "L",
|
|
6547
|
+
anchor: "side_right",
|
|
6548
|
+
style: "ak"
|
|
6549
|
+
},
|
|
6550
|
+
{
|
|
6551
|
+
family: "p90",
|
|
6552
|
+
clips: [
|
|
6553
|
+
"reload_empty"
|
|
6554
|
+
],
|
|
6555
|
+
part: "bolt",
|
|
6556
|
+
hand: "L",
|
|
6557
|
+
anchor: "rear_top",
|
|
6558
|
+
style: "ar"
|
|
6559
|
+
},
|
|
6560
|
+
{
|
|
6561
|
+
family: "belt",
|
|
6562
|
+
clips: [
|
|
6563
|
+
"reload"
|
|
6564
|
+
],
|
|
6565
|
+
part: "cover",
|
|
6566
|
+
hand: "L",
|
|
6567
|
+
anchor: "cover_top",
|
|
6568
|
+
merge: 1
|
|
6569
|
+
},
|
|
6570
|
+
{
|
|
6571
|
+
family: "boltmag",
|
|
6572
|
+
clips: [
|
|
6573
|
+
"reload",
|
|
6574
|
+
"reload_empty",
|
|
6575
|
+
"reload_one_by_one",
|
|
6576
|
+
"cycle"
|
|
6577
|
+
],
|
|
6578
|
+
part: "boltHandle",
|
|
6579
|
+
hand: "R",
|
|
6580
|
+
anchor: "top_handle"
|
|
6581
|
+
},
|
|
6582
|
+
{
|
|
6583
|
+
family: "bolt",
|
|
6584
|
+
clips: [
|
|
6585
|
+
"reload",
|
|
6586
|
+
"reload_empty",
|
|
6587
|
+
"reload_one_by_one",
|
|
6588
|
+
"feed"
|
|
6589
|
+
],
|
|
6590
|
+
hand: "R",
|
|
6591
|
+
anchor: "loading_port",
|
|
6592
|
+
whole: true,
|
|
6593
|
+
lead: 0.3,
|
|
6594
|
+
tail: 0.3
|
|
6595
|
+
},
|
|
6596
|
+
{
|
|
6597
|
+
family: "bolt",
|
|
6598
|
+
clips: [
|
|
6599
|
+
"reload",
|
|
6600
|
+
"reload_empty",
|
|
6601
|
+
"reload_one_by_one",
|
|
6602
|
+
"cycle"
|
|
6603
|
+
],
|
|
6604
|
+
part: "boltHandle",
|
|
6605
|
+
hand: "R",
|
|
6606
|
+
anchor: "top_handle"
|
|
6607
|
+
},
|
|
6608
|
+
{
|
|
6609
|
+
family: "pump",
|
|
6610
|
+
clips: [
|
|
6611
|
+
"feed"
|
|
6612
|
+
],
|
|
6613
|
+
hand: "R",
|
|
6614
|
+
anchor: "loading_port",
|
|
6615
|
+
whole: true,
|
|
6616
|
+
lead: 0.3,
|
|
6617
|
+
tail: 0.3
|
|
6618
|
+
},
|
|
6619
|
+
{
|
|
6620
|
+
family: "semi12",
|
|
6621
|
+
clips: [
|
|
6622
|
+
"feed"
|
|
6623
|
+
],
|
|
6624
|
+
hand: "R",
|
|
6625
|
+
anchor: "loading_port",
|
|
6626
|
+
whole: true,
|
|
6627
|
+
lead: 0.3,
|
|
6628
|
+
tail: 0.3
|
|
6629
|
+
},
|
|
6630
|
+
{
|
|
6631
|
+
family: "semiint",
|
|
6632
|
+
clips: [
|
|
6633
|
+
"reload",
|
|
6634
|
+
"reload_empty",
|
|
6635
|
+
"reload_one_by_one",
|
|
6636
|
+
"feed"
|
|
6637
|
+
],
|
|
6638
|
+
hand: "R",
|
|
6639
|
+
anchor: "loading_port",
|
|
6640
|
+
whole: true,
|
|
6641
|
+
lead: 0.3,
|
|
6642
|
+
tail: 0.3
|
|
6643
|
+
},
|
|
6644
|
+
{
|
|
6645
|
+
family: "lever",
|
|
6646
|
+
clips: [
|
|
6647
|
+
"feed"
|
|
6648
|
+
],
|
|
6649
|
+
hand: "R",
|
|
6650
|
+
anchor: "loading_port",
|
|
6651
|
+
whole: true,
|
|
6652
|
+
lead: 0.3,
|
|
6653
|
+
tail: 0.3
|
|
6654
|
+
},
|
|
6655
|
+
{
|
|
6656
|
+
family: "revolver",
|
|
6657
|
+
clips: [
|
|
6658
|
+
"reload",
|
|
6659
|
+
"reload_one_by_one"
|
|
6660
|
+
],
|
|
6661
|
+
part: "crane",
|
|
6662
|
+
hand: "L",
|
|
6663
|
+
anchor: "cylinder_side",
|
|
6664
|
+
merge: 1
|
|
6665
|
+
}
|
|
6666
|
+
]
|
|
6667
|
+
};
|
|
6668
|
+
|
|
6669
|
+
// src/fx/hand-work.ts
|
|
6670
|
+
var V2 = (THREE, x, y, z) => new THREE.Vector3(x, y, z);
|
|
6671
|
+
var boxIn = (THREE, root, part) => {
|
|
6672
|
+
const world = new THREE.Box3().setFromObject(part);
|
|
6673
|
+
const box2 = new THREE.Box3();
|
|
6674
|
+
for (const x of [world.min.x, world.max.x]) for (const y of [world.min.y, world.max.y]) for (const z of [world.min.z, world.max.z]) box2.expandByPoint(root.worldToLocal(V2(THREE, x, y, z)));
|
|
6675
|
+
return box2;
|
|
6676
|
+
};
|
|
6677
|
+
var anchorsOf = (THREE, model, right) => {
|
|
6678
|
+
const { root, parts } = model;
|
|
6679
|
+
root.updateWorldMatrix(true, true);
|
|
6680
|
+
const palm = HAND_HOLD.palm;
|
|
6681
|
+
const anchorAt = (part, at, quaternion2) => ({ part, offset: part.worldToLocal(root.localToWorld(at.clone())), quaternion: quaternion2 });
|
|
6682
|
+
return (kind, partId) => {
|
|
6683
|
+
const part = partId ? parts[partId] ?? null : null;
|
|
6684
|
+
if (kind === "loading_port") {
|
|
6685
|
+
const port = parts.loadPort;
|
|
6686
|
+
if (!port) return null;
|
|
6687
|
+
const at = root.worldToLocal(port.getWorldPosition(V2(THREE, 0, 0, 0))).add(V2(THREE, 0, -palm - 4e-3, 0));
|
|
6688
|
+
return anchorAt(port, at, handFrame(THREE, V2(THREE, 0, 0, -1), V2(THREE, 0, -1, 0)));
|
|
6689
|
+
}
|
|
6690
|
+
if (kind === "thumb_release") {
|
|
6691
|
+
if (!right || !parts.slide) return null;
|
|
6692
|
+
return { part: right.part, offset: right.offset.clone().add(V2(THREE, 0, 0.012, 0)), quaternion: right.quaternion };
|
|
6693
|
+
}
|
|
6694
|
+
if (!part) return null;
|
|
6695
|
+
const b = boxIn(THREE, root, part);
|
|
6696
|
+
const c = b.getCenter(V2(THREE, 0, 0, 0));
|
|
6697
|
+
switch (kind) {
|
|
6698
|
+
case "slide_rear":
|
|
6699
|
+
return anchorAt(part, V2(THREE, b.min.x + 0.014, b.max.y - 6e-3, b.min.z - palm), handFrame(THREE, V2(THREE, 1, 0, 0), V2(THREE, 0, 0, -1)));
|
|
6700
|
+
case "rear_top":
|
|
6701
|
+
return anchorAt(part, V2(THREE, b.min.x + 0.016, b.max.y + palm, c.z), handFrame(THREE, V2(THREE, 1, 0, 0), V2(THREE, 0, 1, 0)));
|
|
6702
|
+
case "side_right":
|
|
6703
|
+
return anchorAt(part, V2(THREE, c.x, c.y, b.max.z + palm), handFrame(THREE, V2(THREE, 1, 0, 0), V2(THREE, 0, 0, 1)));
|
|
6704
|
+
case "top_handle":
|
|
6705
|
+
return anchorAt(part, V2(THREE, c.x, c.y, b.max.z + palm), handFrame(THREE, V2(THREE, 1, 0, 0), V2(THREE, 0, 0, 1)));
|
|
6706
|
+
case "cover_top":
|
|
6707
|
+
return anchorAt(part, V2(THREE, c.x, b.max.y + palm, c.z), handFrame(THREE, V2(THREE, 1, 0, 0), V2(THREE, 0, 1, 0)));
|
|
6708
|
+
case "cylinder_side":
|
|
6709
|
+
return anchorAt(part, V2(THREE, c.x, c.y, b.min.z - palm), handFrame(THREE, V2(THREE, 1, 0, 0), V2(THREE, 0, 0, -1)));
|
|
6710
|
+
default:
|
|
6711
|
+
return null;
|
|
6712
|
+
}
|
|
6713
|
+
};
|
|
6714
|
+
};
|
|
6715
|
+
var releasePistols = new Set(hand_work_default.pistolRelease.weapons);
|
|
6716
|
+
var akCharge = new Set(hand_work_default.akCharge.weapons);
|
|
6717
|
+
function handWorkOf(THREE, model, weaponId, right) {
|
|
6718
|
+
const family = model.parts.family;
|
|
6719
|
+
const anchors = anchorsOf(THREE, model, right);
|
|
6720
|
+
const style = family === "pistol" ? releasePistols.has(weaponId) ? "release" : "rack" : akCharge.has(weaponId) ? "ak" : "ar";
|
|
6721
|
+
const out = [];
|
|
6722
|
+
for (const entry of hand_work_default.works) {
|
|
6723
|
+
if (entry.family !== family || entry.style && entry.style !== style) continue;
|
|
6724
|
+
const anchor = anchors(entry.anchor, entry.part ?? null);
|
|
6725
|
+
if (!anchor) continue;
|
|
6726
|
+
out.push({ hand: entry.hand, anchor, part: entry.whole ? null : entry.part ?? null, clips: entry.clips, merge: entry.merge ?? 0, id: entry.anchor, ...entry.lead === void 0 ? {} : { lead: entry.lead }, ...entry.tail === void 0 ? {} : { tail: entry.tail } });
|
|
6727
|
+
}
|
|
6728
|
+
return out;
|
|
6729
|
+
}
|
|
6730
|
+
var segmentsCache = /* @__PURE__ */ new WeakMap();
|
|
6731
|
+
var motionOf = (clip, part, merge) => {
|
|
6732
|
+
const cache = segmentsCache.get(clip) ?? /* @__PURE__ */ new Map();
|
|
6733
|
+
segmentsCache.set(clip, cache);
|
|
6734
|
+
const key = `${part}|${merge}`;
|
|
6735
|
+
const found = cache.get(key);
|
|
6736
|
+
if (found) return found;
|
|
6737
|
+
const steps = 240;
|
|
6738
|
+
const dt = clip.duration / steps;
|
|
6739
|
+
const spans = [];
|
|
6740
|
+
let start = null;
|
|
6741
|
+
for (let i = 1; i <= steps; i++) {
|
|
6742
|
+
const a = sampleClip(clip, part, (i - 1) * dt);
|
|
6743
|
+
const b = sampleClip(clip, part, i * dt);
|
|
6744
|
+
const moving = [0, 1, 2].some((axis) => Math.abs(b.position[axis] - a.position[axis]) > 1e-6 || Math.abs(b.rotation[axis] - a.rotation[axis]) > 1e-6);
|
|
6745
|
+
if (moving && start === null) start = (i - 1) * dt;
|
|
6746
|
+
if (!moving && start !== null) {
|
|
6747
|
+
spans.push([start, (i - 1) * dt]);
|
|
6748
|
+
start = null;
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
if (start !== null) spans.push([start, clip.duration]);
|
|
6752
|
+
const joined = [];
|
|
6753
|
+
for (const span of spans) {
|
|
6754
|
+
const last = joined[joined.length - 1];
|
|
6755
|
+
if (last && span[0] - last[1] <= merge * clip.duration) last[1] = span[1];
|
|
6756
|
+
else joined.push([span[0], span[1]]);
|
|
6757
|
+
}
|
|
6758
|
+
cache.set(key, joined);
|
|
6759
|
+
return joined;
|
|
6760
|
+
};
|
|
6761
|
+
var smooth = (x) => {
|
|
6762
|
+
const k = Math.min(1, Math.max(0, x));
|
|
6763
|
+
return k * k * (3 - 2 * k);
|
|
6764
|
+
};
|
|
6765
|
+
function workWeights(works, running) {
|
|
6766
|
+
return works.map((work) => {
|
|
6767
|
+
const lead = work.lead ?? hand_work_default.timing.lead;
|
|
6768
|
+
const tail = work.tail ?? hand_work_default.timing.tail;
|
|
6769
|
+
let best = 0;
|
|
6770
|
+
for (const { name, time, clip } of running) {
|
|
6771
|
+
if (!work.clips.includes(name)) continue;
|
|
6772
|
+
const spans = work.part ? motionOf(clip, work.part, work.merge) : [[0, clip.duration]];
|
|
6773
|
+
for (const [from, to] of spans) {
|
|
6774
|
+
const before = lead * clip.duration;
|
|
6775
|
+
const after = tail * clip.duration;
|
|
6776
|
+
const w = work.part ? smooth((time - (from - before)) / before) * smooth((to + after - time) / after) : smooth(time / before) * smooth((clip.duration - time) / after);
|
|
6777
|
+
best = Math.max(best, w);
|
|
6778
|
+
}
|
|
6779
|
+
}
|
|
6780
|
+
return best;
|
|
6781
|
+
});
|
|
6782
|
+
}
|
|
6783
|
+
|
|
5528
6784
|
// src/fx/viewmodel.ts
|
|
5529
6785
|
var PI14 = Math.PI;
|
|
5530
6786
|
var HAND_OFFSET2 = [0.2, -0.2, -0.42];
|
|
6787
|
+
var HANDGUN_OFFSET = [0.1, -0.12, -0.44];
|
|
5531
6788
|
var HEAT_DISPLAY_MAX_FALLBACK = 120;
|
|
6789
|
+
var WALL_PULL = { margin: 0.03, max: 0.7, rate: 14, probe: 3 };
|
|
6790
|
+
var HAND_IN_VIEW = { floor: -0.88, max: 0.18, rate: 10 };
|
|
5532
6791
|
var LAUNCH_VOLUME = 0.6;
|
|
5533
6792
|
var fittedOf2 = (parts) => parts.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } }));
|
|
5534
6793
|
var collapsedStockPose = (move2) => {
|
|
@@ -5537,6 +6796,14 @@ var collapsedStockPose = (move2) => {
|
|
|
5537
6796
|
};
|
|
5538
6797
|
var underbarrelStep = (step, tool) => tool.underbarrelActive && step.type === "play" && (step.clip === "shot" || step.clip === "reload") ? { ...step, variant: "ugl" } : step;
|
|
5539
6798
|
var caliberOf = (arsenal, def) => def.action.kind === "firearm" ? arsenal.calibers.get(def.action.caliber) : void 0;
|
|
6799
|
+
var TRIGGER_FINGER = { safe: 0, ready: 0.5, pulled: 1, readySeconds: 1.5, burstSeconds: 0.14, tremor: 0.03 };
|
|
6800
|
+
var TRIGGER_TURN = 0.35;
|
|
6801
|
+
var indexCurl = (turn2, sinceShot) => {
|
|
6802
|
+
const ready = sinceShot < TRIGGER_FINGER.readySeconds ? TRIGGER_FINGER.ready : TRIGGER_FINGER.safe;
|
|
6803
|
+
const pull = Math.min(1, Math.max(0, turn2 / TRIGGER_TURN));
|
|
6804
|
+
const held = sinceShot < TRIGGER_FINGER.burstSeconds ? TRIGGER_FINGER.pulled - TRIGGER_FINGER.tremor * (0.5 + 0.5 * Math.sin(sinceShot * 220)) : 0;
|
|
6805
|
+
return Math.max(ready + (TRIGGER_FINGER.pulled - ready) * pull, held);
|
|
6806
|
+
};
|
|
5540
6807
|
function createViewmodel(options) {
|
|
5541
6808
|
const { THREE, scene, camera, arsenal, groundAt, audio } = options;
|
|
5542
6809
|
const kit = modelKit(THREE);
|
|
@@ -5559,6 +6826,15 @@ function createViewmodel(options) {
|
|
|
5559
6826
|
let condition = options.condition ?? "new";
|
|
5560
6827
|
let lastAmmo = 0;
|
|
5561
6828
|
let smokeDebt = 0;
|
|
6829
|
+
let secondsSinceShot = Number.POSITIVE_INFINITY;
|
|
6830
|
+
let hold = null;
|
|
6831
|
+
let reloading = false;
|
|
6832
|
+
let magazineHand = 0;
|
|
6833
|
+
let clock = 0;
|
|
6834
|
+
let lastShot = -Infinity;
|
|
6835
|
+
let triggerRest = 0;
|
|
6836
|
+
let feedCued = false;
|
|
6837
|
+
const rootQuaternion = new THREE.Quaternion();
|
|
5562
6838
|
const world = {
|
|
5563
6839
|
kit,
|
|
5564
6840
|
scene,
|
|
@@ -5568,9 +6844,15 @@ function createViewmodel(options) {
|
|
|
5568
6844
|
audio,
|
|
5569
6845
|
layers,
|
|
5570
6846
|
groundAt,
|
|
6847
|
+
sound: (weapon, cue2, profile) => {
|
|
6848
|
+
audio?.cue(cue2, profile);
|
|
6849
|
+
options.onSound?.({ weapon, cue: cue2, profile });
|
|
6850
|
+
},
|
|
5571
6851
|
schedule: (delay, seconds, step, done) => void tasks.push({ delay, seconds, elapsed: 0, step: (progress) => step(ease(progress)), done })
|
|
5572
6852
|
};
|
|
5573
6853
|
const heatMax = arsenal.rules.heat.displayMax || HEAT_DISPLAY_MAX_FALLBACK;
|
|
6854
|
+
const visual = arsenal.rules.heat.visual;
|
|
6855
|
+
const hasSuppressor = (parts) => parts.some((part) => arsenal.attachments.get(part.id)?.kind === "suppressor");
|
|
5574
6856
|
const release = () => {
|
|
5575
6857
|
if (!session) return;
|
|
5576
6858
|
session.attachments?.dispose();
|
|
@@ -5578,6 +6860,7 @@ function createViewmodel(options) {
|
|
|
5578
6860
|
player = null;
|
|
5579
6861
|
session = null;
|
|
5580
6862
|
tasks = [];
|
|
6863
|
+
hold = null;
|
|
5581
6864
|
};
|
|
5582
6865
|
const heavy = (current) => ({
|
|
5583
6866
|
kit,
|
|
@@ -5597,6 +6880,14 @@ function createViewmodel(options) {
|
|
|
5597
6880
|
const model = def ? buildWeaponModelById(THREE, weapon) : void 0;
|
|
5598
6881
|
if (!def || !model) return false;
|
|
5599
6882
|
release();
|
|
6883
|
+
hold = null;
|
|
6884
|
+
reloading = false;
|
|
6885
|
+
magazineHand = 0;
|
|
6886
|
+
reach = 0;
|
|
6887
|
+
pull = 0;
|
|
6888
|
+
lift = 0;
|
|
6889
|
+
handHeight = 0;
|
|
6890
|
+
handDepth = 0;
|
|
5600
6891
|
rigStock(THREE, model, def);
|
|
5601
6892
|
rigUnderbarrel(THREE, model, def);
|
|
5602
6893
|
const length = new THREE.Box3().setFromObject(model.root).getSize(new THREE.Vector3()).x;
|
|
@@ -5606,31 +6897,36 @@ function createViewmodel(options) {
|
|
|
5606
6897
|
holder.add(model.root);
|
|
5607
6898
|
const caliber = caliberOf(arsenal, def);
|
|
5608
6899
|
const dims = caliber?.cartridge;
|
|
6900
|
+
const hostTool = options.tool?.();
|
|
6901
|
+
const onTool = hostTool?.weapon === weapon ? hostTool : void 0;
|
|
6902
|
+
const fitted = onTool?.attachments ? fittedOf2(onTool.attachments) : [...def.fitted ?? []];
|
|
5609
6903
|
session = {
|
|
5610
6904
|
id: weapon,
|
|
5611
6905
|
def,
|
|
6906
|
+
caliber,
|
|
5612
6907
|
model,
|
|
5613
6908
|
holderScale: scale,
|
|
5614
6909
|
lengthMetres: length,
|
|
5615
6910
|
caseType: caliber?.caseType ?? "rifle",
|
|
6911
|
+
soundProfile: soundProfileOf(weapon, model.parts.family),
|
|
5616
6912
|
kit,
|
|
5617
6913
|
cartridge: caliber && dims ? { dims, shape: cartridgeShape(caliber, dims) } : null,
|
|
5618
6914
|
stuckCase: null,
|
|
5619
6915
|
nextChamber: 0,
|
|
5620
6916
|
attachments: null,
|
|
5621
|
-
attachmentsSignature: ""
|
|
6917
|
+
attachmentsSignature: "",
|
|
6918
|
+
suppressed: onTool?.suppressed ?? hasSuppressor(fitted)
|
|
5622
6919
|
};
|
|
5623
|
-
const hostTool = options.tool?.();
|
|
5624
|
-
const onTool = hostTool?.weapon === weapon ? hostTool : void 0;
|
|
5625
|
-
const fitted = onTool?.attachments ? fittedOf2(onTool.attachments) : [...def.fitted ?? []];
|
|
5626
6920
|
session.attachments = fitAttachmentModels(model, fitted, kit, { catalogue: arsenal.attachments, isDrawn: () => layers.attachments, areBeamsOn: () => layers.beams, ...options.attachmentModels ? { overrides: options.attachmentModels } : {}, ...options.raycast ? { raycast: options.raycast } : {} });
|
|
5627
6921
|
session.attachmentsSignature = attachmentsKey(onTool?.attachments);
|
|
5628
6922
|
const action = def.action;
|
|
5629
6923
|
const reloadFamily = action.kind === "firearm" || action.kind === "flame" || action.kind === "launcher" ? action.family : void 0;
|
|
5630
6924
|
const rpm = action.kind === "firearm" ? action.rpm : 0;
|
|
5631
6925
|
const rig = createModelRig(model);
|
|
6926
|
+
const clips = buildToolClips({ parts: model.parts, family: model.parts.family, caseType: session.caseType, reloadFamily });
|
|
6927
|
+
feedCued = clips.feed?.events?.some((entry) => entry.type === "sound") ?? false;
|
|
5632
6928
|
player = createToolClipPlayer({
|
|
5633
|
-
clips
|
|
6929
|
+
clips,
|
|
5634
6930
|
rig,
|
|
5635
6931
|
onEvent: createClipEventHandler(world, session),
|
|
5636
6932
|
repeatInterval: () => rpm > 0 ? 60 / rpm : 0.1
|
|
@@ -5638,25 +6934,67 @@ function createViewmodel(options) {
|
|
|
5638
6934
|
if (model.parts.stockMove && onTool?.stock === "collapsed") rig.setPartPose("stock", collapsedStockPose(model.parts.stockMove));
|
|
5639
6935
|
applyCondition(kit, model, condition);
|
|
5640
6936
|
lastAmmo = options.tool?.().ammo ?? 0;
|
|
6937
|
+
hand.updateMatrixWorld(true);
|
|
6938
|
+
hold = holdOfModel(THREE, model, scale);
|
|
6939
|
+
hold = { ...hold, works: handWorkOf(THREE, model, weapon, hold.right) };
|
|
6940
|
+
triggerRest = model.parts.trigger?.rotation.z ?? 0;
|
|
6941
|
+
lastShot = -Infinity;
|
|
5641
6942
|
return true;
|
|
5642
6943
|
};
|
|
5643
6944
|
const syncAttachments = (current, tool) => {
|
|
5644
|
-
if (!current.attachments || tool?.weapon !== current.id
|
|
6945
|
+
if (!current.attachments || tool?.weapon !== current.id) return;
|
|
6946
|
+
if (tool.suppressed !== void 0) current.suppressed = tool.suppressed;
|
|
6947
|
+
if (!tool.attachments) return;
|
|
5645
6948
|
const signature = attachmentsKey(tool.attachments);
|
|
5646
6949
|
if (signature === current.attachmentsSignature) return;
|
|
5647
6950
|
current.attachmentsSignature = signature;
|
|
5648
|
-
|
|
6951
|
+
const fitted = fittedOf2(tool.attachments);
|
|
6952
|
+
if (tool.suppressed === void 0) current.suppressed = hasSuppressor(fitted);
|
|
6953
|
+
current.attachments.sync(fitted);
|
|
6954
|
+
};
|
|
6955
|
+
let reach = 0;
|
|
6956
|
+
let pull = 0;
|
|
6957
|
+
let lift = 0;
|
|
6958
|
+
let handHeight = 0;
|
|
6959
|
+
let handDepth = 0;
|
|
6960
|
+
const ahead = new THREE.Vector3();
|
|
6961
|
+
const up = new THREE.Vector3();
|
|
6962
|
+
const seen = new THREE.Vector3();
|
|
6963
|
+
const eyeTurn = new THREE.Quaternion();
|
|
6964
|
+
const corner = new THREE.Vector3();
|
|
6965
|
+
const bounds = new THREE.Box3();
|
|
6966
|
+
const measureReach = (current) => {
|
|
6967
|
+
bounds.setFromObject(current.model.root);
|
|
6968
|
+
let farthest = 0;
|
|
6969
|
+
for (const x of [bounds.min.x, bounds.max.x]) for (const y of [bounds.min.y, bounds.max.y]) for (const z of [bounds.min.z, bounds.max.z]) farthest = Math.max(farthest, corner.set(x, y, z).sub(camera.position).dot(ahead));
|
|
6970
|
+
return farthest;
|
|
5649
6971
|
};
|
|
5650
|
-
const place = () => {
|
|
5651
|
-
const
|
|
6972
|
+
const place = (dt = 0) => {
|
|
6973
|
+
const family = session?.model.parts.family;
|
|
6974
|
+
const [x, y, z] = family === "pistol" || family === "revolver" ? HANDGUN_OFFSET : HAND_OFFSET2;
|
|
5652
6975
|
hand.position.set(x, y, z).applyQuaternion(camera.quaternion).add(camera.position);
|
|
5653
6976
|
hand.quaternion.copy(camera.quaternion);
|
|
6977
|
+
if (session && handDepth > 0.05 && "fov" in camera) {
|
|
6978
|
+
const tanHalf = Math.tan((camera.fov ?? 70) * PI14 / 360);
|
|
6979
|
+
const want2 = Math.max(0, Math.min(HAND_IN_VIEW.max, lift + (HAND_IN_VIEW.floor - handHeight) * tanHalf * handDepth));
|
|
6980
|
+
lift += (want2 - lift) * Math.min(1, dt * HAND_IN_VIEW.rate);
|
|
6981
|
+
if (lift > 1e-4) hand.position.addScaledVector(up.set(0, 1, 0).applyQuaternion(camera.quaternion), lift);
|
|
6982
|
+
}
|
|
6983
|
+
hand.updateMatrixWorld(true);
|
|
6984
|
+
if (!options.contact || !session) return;
|
|
6985
|
+
ahead.set(0, 0, -1).applyQuaternion(camera.quaternion);
|
|
6986
|
+
if (reach === 0) reach = Math.max(0.01, measureReach(session));
|
|
6987
|
+
const free = options.contact(camera.position, ahead, WALL_PULL.probe);
|
|
6988
|
+
const want = free === null ? 0 : Math.max(0, Math.min(WALL_PULL.max, reach + WALL_PULL.margin - free));
|
|
6989
|
+
pull += (want - pull) * Math.min(1, dt * WALL_PULL.rate);
|
|
6990
|
+
if (pull < 1e-4) return;
|
|
6991
|
+
hand.position.addScaledVector(ahead, -pull);
|
|
5654
6992
|
hand.updateMatrixWorld(true);
|
|
5655
6993
|
};
|
|
5656
6994
|
const glow = (tool) => {
|
|
5657
6995
|
if (!session) return;
|
|
5658
6996
|
const heat = (tool?.heat ?? 0) * heatMax;
|
|
5659
|
-
const level = Math.max(0, Math.min(1, (heat -
|
|
6997
|
+
const level = Math.max(0, Math.min(1, (heat - visual.glowFrom) / visual.glowSpan));
|
|
5660
6998
|
for (const mesh of session.model.parts.glow) {
|
|
5661
6999
|
const material = mesh.material;
|
|
5662
7000
|
material.emissive.setRGB(level, 0.2 * level * level, 0.02 * level * level);
|
|
@@ -5664,56 +7002,73 @@ function createViewmodel(options) {
|
|
|
5664
7002
|
}
|
|
5665
7003
|
};
|
|
5666
7004
|
const handleShot = (current, event, tool) => {
|
|
7005
|
+
secondsSinceShot = 0;
|
|
7006
|
+
if (event.suppressed !== void 0) current.suppressed = event.suppressed;
|
|
7007
|
+
else if (tool.suppressed !== void 0) current.suppressed = tool.suppressed;
|
|
7008
|
+
else current.suppressed = hasSuppressor(tool.attachments ? fittedOf2(tool.attachments) : current.def.fitted ?? []);
|
|
5667
7009
|
if (tool.underbarrelActive && current.model.parts.uglMuzzle) {
|
|
5668
7010
|
audio?.play("explosion", LAUNCH_VOLUME, distance());
|
|
5669
7011
|
return;
|
|
5670
7012
|
}
|
|
5671
7013
|
const action = current.def.action;
|
|
5672
7014
|
if (action.kind === "launcher") {
|
|
5673
|
-
launchRocket(heavy(current));
|
|
7015
|
+
launchRocket(heavy(current), options.flyRocket !== false);
|
|
5674
7016
|
audio?.play("explosion", LAUNCH_VOLUME, distance());
|
|
5675
7017
|
}
|
|
5676
7018
|
if (action.kind !== "firearm") return;
|
|
5677
7019
|
const caliber = caliberOf(arsenal, current.def);
|
|
5678
7020
|
if (!caliber) return;
|
|
5679
|
-
audio?.shot(muzzleEnergy3(caliber) * (caliber.pellets ?? 1), current.def.category, { caliber, distance: distance(), ...
|
|
7021
|
+
audio?.shot(muzzleEnergy3(caliber) * (caliber.pellets ?? 1), current.def.category, { caliber, distance: distance(), ...current.suppressed ? { suppressed: true } : {} });
|
|
5680
7022
|
};
|
|
5681
7023
|
return {
|
|
5682
7024
|
equip,
|
|
5683
7025
|
unequip: release,
|
|
5684
7026
|
handle(event, tool) {
|
|
5685
|
-
if (event.type === "explosion") return;
|
|
7027
|
+
if (event.type === "explosion" || event.type === "player") return;
|
|
5686
7028
|
if (event.type === "tool") equip(event.weapon);
|
|
5687
7029
|
if (!session || !player || event.weapon !== session.id) return;
|
|
5688
7030
|
const current = session;
|
|
5689
|
-
if (event.type === "shot")
|
|
5690
|
-
|
|
7031
|
+
if (event.type === "shot") {
|
|
7032
|
+
lastShot = clock;
|
|
7033
|
+
handleShot(current, event, tool);
|
|
7034
|
+
} else if (event.type === "reload" && event.phase === "start") {
|
|
7035
|
+
current.nextChamber = 0;
|
|
7036
|
+
reloading = true;
|
|
7037
|
+
} else if (event.type === "reload" && event.phase === "end") reloading = false;
|
|
5691
7038
|
else if (event.type === "clear" && event.phase === "end") current.stuckCase = null;
|
|
5692
7039
|
const mechanical = weaponEventOf(event);
|
|
5693
|
-
|
|
7040
|
+
const cued = mechanical?.type === "feed" ? feedCued : mechanical?.type === "reload_stage" && (player.running.some(({ clip }) => clip.events?.some((entry) => entry.type === "sound")) ?? false);
|
|
7041
|
+
if (mechanical && !cued) audio?.event(mechanical);
|
|
5694
7042
|
const step = toolAnimationStep(event);
|
|
5695
7043
|
if (!step) return;
|
|
5696
7044
|
const last = step.type === "play" && step.clip === "shot" && current.model.parts.family === "pistol" && tool.ammo === 0;
|
|
5697
7045
|
player.apply(last ? { ...step, variant: "last" } : underbarrelStep(step, tool));
|
|
5698
7046
|
},
|
|
5699
7047
|
update(dt) {
|
|
7048
|
+
clock += dt;
|
|
5700
7049
|
if (session) {
|
|
5701
|
-
place();
|
|
7050
|
+
place(dt);
|
|
5702
7051
|
player?.update(dt);
|
|
7052
|
+
const wantsMagazine = reloading || options.tool?.().reloading === true;
|
|
7053
|
+
magazineHand += ((wantsMagazine ? 1 : 0) - magazineHand) * Math.min(1, dt * HAND_HOLD.magRate);
|
|
5703
7054
|
const tool = options.tool?.();
|
|
5704
7055
|
syncAttachments(session, tool);
|
|
5705
7056
|
session.attachments?.update();
|
|
5706
7057
|
glow(tool);
|
|
5707
7058
|
if (tool && session.def.action.kind === "flame") {
|
|
5708
|
-
if (tool.ammo < lastAmmo)
|
|
7059
|
+
if (tool.ammo < lastAmmo) {
|
|
7060
|
+
flameJet(heavy(session), dt, session.model.parts.launcherType === "flame" ? 1 : 0.6);
|
|
7061
|
+
lastShot = clock;
|
|
7062
|
+
}
|
|
5709
7063
|
lastAmmo = tool.ammo;
|
|
5710
7064
|
}
|
|
5711
|
-
|
|
5712
|
-
|
|
7065
|
+
secondsSinceShot += dt;
|
|
7066
|
+
if (tool && tool.heat !== void 0 && tool.heat * heatMax > visual.smokeFrom && secondsSinceShot < visual.smokeSeconds && session.model.parts.muzzle) {
|
|
7067
|
+
smokeDebt += dt * (tool.heat * heatMax - visual.smokeFrom) * 0.1;
|
|
5713
7068
|
while (smokeDebt > 1) {
|
|
5714
7069
|
smokeDebt -= 1;
|
|
5715
|
-
const k = 0.5 + tool.heat * heatMax / 60;
|
|
5716
|
-
effects.smoke(worldPositionOf(session, session.model.parts.muzzle, new THREE.Vector3()), new THREE.Vector3((random() - 0.5) * 0.
|
|
7070
|
+
const k = 0.5 + (tool.heat * heatMax - visual.smokeFrom) / 60;
|
|
7071
|
+
effects.smoke(worldPositionOf(session, session.model.parts.muzzle, new THREE.Vector3()), new THREE.Vector3((random() - 0.5) * 0.015, 0.07 + random() * 0.08, (random() - 0.5) * 0.015), 4e-3 * k, 1.2 + random() * 0.6, Math.min(0.18, 0.06 + k * 0.04));
|
|
5717
7072
|
}
|
|
5718
7073
|
}
|
|
5719
7074
|
}
|
|
@@ -5734,9 +7089,25 @@ function createViewmodel(options) {
|
|
|
5734
7089
|
condition = next;
|
|
5735
7090
|
if (session) applyCondition(kit, session.model, condition);
|
|
5736
7091
|
},
|
|
7092
|
+
handTargets() {
|
|
7093
|
+
if (!session || !hold) return null;
|
|
7094
|
+
hand.updateMatrixWorld(true);
|
|
7095
|
+
const targets = targetsOf(THREE, hold, session.model.root.getWorldQuaternion(rootQuaternion), magazineHand, hold.works ? workWeights(hold.works, player?.running ?? []) : void 0);
|
|
7096
|
+
const trigger = session.model.parts.trigger;
|
|
7097
|
+
if (trigger) targets.fingers = { index: indexCurl(trigger.rotation.z - triggerRest, clock - lastShot) };
|
|
7098
|
+
if (targets.R && "fov" in camera) {
|
|
7099
|
+
seen.set(...targets.R.position).sub(camera.position).applyQuaternion(eyeTurn.copy(camera.quaternion).invert());
|
|
7100
|
+
handDepth = -seen.z;
|
|
7101
|
+
handHeight = handDepth > 0.05 ? seen.y / handDepth / Math.tan((camera.fov ?? 70) * PI14 / 360) : 0;
|
|
7102
|
+
}
|
|
7103
|
+
return targets;
|
|
7104
|
+
},
|
|
5737
7105
|
get inHand() {
|
|
5738
7106
|
return session?.id ?? null;
|
|
5739
7107
|
},
|
|
7108
|
+
get clips() {
|
|
7109
|
+
return (player?.running ?? []).map(({ name, clip }) => ({ name, seconds: clip.duration }));
|
|
7110
|
+
},
|
|
5740
7111
|
get casings() {
|
|
5741
7112
|
return particles.count("casing");
|
|
5742
7113
|
},
|
|
@@ -5759,6 +7130,7 @@ function createWeaponFx(options) {
|
|
|
5759
7130
|
const model = createViewmodel({
|
|
5760
7131
|
layers,
|
|
5761
7132
|
...options.soundDistance ? { soundDistance: options.soundDistance } : {},
|
|
7133
|
+
...options.onSound ? { onSound: options.onSound } : {},
|
|
5762
7134
|
THREE: options.THREE,
|
|
5763
7135
|
scene: options.scene,
|
|
5764
7136
|
camera: options.camera,
|
|
@@ -5770,11 +7142,13 @@ function createWeaponFx(options) {
|
|
|
5770
7142
|
...options.condition ? { condition: options.condition } : {},
|
|
5771
7143
|
...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
|
|
5772
7144
|
...options.raycast ? { raycast: options.raycast } : {},
|
|
5773
|
-
...options.
|
|
7145
|
+
...options.contact ? { contact: options.contact } : {},
|
|
7146
|
+
...options.effects ? { effects: options.effects } : {},
|
|
7147
|
+
...options.flyRocket === void 0 ? {} : { flyRocket: options.flyRocket }
|
|
5774
7148
|
});
|
|
5775
7149
|
return {
|
|
5776
7150
|
handle(event, tool) {
|
|
5777
|
-
if (event.type === "explosion") return;
|
|
7151
|
+
if (event.type === "explosion" || event.type === "player") return;
|
|
5778
7152
|
if (event.type === "tool") {
|
|
5779
7153
|
if (hasWeaponModel(event.weapon)) box2.unequip();
|
|
5780
7154
|
else model.unequip();
|
|
@@ -5787,10 +7161,14 @@ function createWeaponFx(options) {
|
|
|
5787
7161
|
model.update(dt);
|
|
5788
7162
|
},
|
|
5789
7163
|
setCondition: (condition) => model.setCondition(condition),
|
|
7164
|
+
handTargets: () => model.handTargets() ?? box2.handTargets(),
|
|
5790
7165
|
layers,
|
|
5791
7166
|
get inHand() {
|
|
5792
7167
|
return model.inHand ?? box2.inHand;
|
|
5793
7168
|
},
|
|
7169
|
+
get clips() {
|
|
7170
|
+
return model.clips;
|
|
7171
|
+
},
|
|
5794
7172
|
get casings() {
|
|
5795
7173
|
return box2.casings + model.casings;
|
|
5796
7174
|
},
|
|
@@ -6536,21 +7914,34 @@ function weaponThumbnailUrl(id, base = DEFAULT_THUMBNAIL_BASE) {
|
|
|
6536
7914
|
}
|
|
6537
7915
|
|
|
6538
7916
|
// src/world-explosives.ts
|
|
7917
|
+
import { buildItem } from "@adatechnology/game-items-three";
|
|
6539
7918
|
var WORLD_EXPLOSIVE_PREFIX = "world_";
|
|
7919
|
+
var P13_VALVE_MESH = "valve";
|
|
7920
|
+
var P13_VALVE_COLOR = 12095802;
|
|
7921
|
+
var P13_WALL = 25e-4;
|
|
6540
7922
|
var SHAPES2 = {
|
|
6541
7923
|
fuel_barrel: { shape: { kind: "cylinder", radius: 0.3, height: 0.9 }, position: [0, 0.45, 0], color: 3104666 },
|
|
6542
|
-
propane_p13: {
|
|
7924
|
+
propane_p13: {
|
|
7925
|
+
shape: { kind: "cylinder", radius: 0.176, height: 0.49 },
|
|
7926
|
+
position: [0, 0.245, 0],
|
|
7927
|
+
color: 3497862,
|
|
7928
|
+
model: { id: "lpg_cylinder_13kg", category: "fuel", size: [0.35299, 0.49, 0.35844], model: "lpg_cylinder_13kg" },
|
|
7929
|
+
wall: P13_WALL
|
|
7930
|
+
},
|
|
6543
7931
|
car_fuel_tank: { shape: { kind: "box", size: [0.9, 0.25, 0.5] }, position: [0, 0.4, 0], color: 4935509 }
|
|
6544
7932
|
};
|
|
6545
7933
|
var WORLD_EXPLOSIVE_IDS = Object.keys(SHAPES2);
|
|
6546
7934
|
var SHELL = { thickness: 1e-3, fill: 0.03 };
|
|
6547
|
-
var explosivePart = (def, shape) => ({
|
|
7935
|
+
var explosivePart = (def, shape, scale) => ({
|
|
6548
7936
|
id: "tank",
|
|
6549
7937
|
material: "steel",
|
|
6550
7938
|
shape: shape.shape,
|
|
6551
7939
|
position: shape.position,
|
|
6552
|
-
...SHELL,
|
|
6553
|
-
|
|
7940
|
+
...shape.wall === void 0 ? SHELL : { thickness: shape.wall },
|
|
7941
|
+
// a real mass (the vessel and what it holds) replaces the hollow-sheet density
|
|
7942
|
+
...def.mass !== void 0 && shape.wall !== void 0 ? { mass: def.mass } : {},
|
|
7943
|
+
...def.leak ? { contains: { fluid: def.leak.fluid, liters: def.leak.liters, pressurized: def.leak.pressurized } } : {},
|
|
7944
|
+
explodes: { energy: def.energy * scale, radius: def.injuryRadius * scale, lethalRadius: def.lethalRadius * scale, effect: def.id, ...def.chain ? { chain: { ...def.chain } } : {} }
|
|
6554
7945
|
});
|
|
6555
7946
|
var meshOf = (THREE, shape) => {
|
|
6556
7947
|
const geometry = shape.shape.kind === "cylinder" ? new THREE.CylinderGeometry(shape.shape.radius, shape.shape.radius, shape.shape.height, 16) : shape.shape.kind === "box" ? new THREE.BoxGeometry(...shape.shape.size) : new THREE.SphereGeometry(0.2);
|
|
@@ -6562,16 +7953,55 @@ var meshOf = (THREE, shape) => {
|
|
|
6562
7953
|
mesh.receiveShadow = true;
|
|
6563
7954
|
return mesh;
|
|
6564
7955
|
};
|
|
6565
|
-
var
|
|
7956
|
+
var modelOf = (THREE, shape, wear = 0) => {
|
|
7957
|
+
if (!shape.model) return void 0;
|
|
7958
|
+
const { root, kind } = buildItem(THREE, shape.model, { wear });
|
|
7959
|
+
if (kind !== "model") return void 0;
|
|
7960
|
+
root.traverse((node) => {
|
|
7961
|
+
const mesh = node;
|
|
7962
|
+
if (!mesh.isMesh) return;
|
|
7963
|
+
mesh.userData.destructionPart = "tank";
|
|
7964
|
+
mesh.castShadow = true;
|
|
7965
|
+
mesh.receiveShadow = true;
|
|
7966
|
+
const material = mesh.material;
|
|
7967
|
+
if (material.color?.getHex() === P13_VALVE_COLOR) mesh.name = P13_VALVE_MESH;
|
|
7968
|
+
});
|
|
7969
|
+
return root;
|
|
7970
|
+
};
|
|
7971
|
+
var worldExplosiveValve = (THREE, root) => {
|
|
7972
|
+
const valve = root.getObjectByName(P13_VALVE_MESH);
|
|
7973
|
+
if (!valve) return void 0;
|
|
7974
|
+
root.updateMatrixWorld(true);
|
|
7975
|
+
const centre = new THREE.Box3().setFromObject(valve).getCenter(new THREE.Vector3());
|
|
7976
|
+
return [centre.x, centre.y, centre.z];
|
|
7977
|
+
};
|
|
7978
|
+
var worldExplosiveShell = (THREE, defId) => {
|
|
7979
|
+
const shape = SHAPES2[defId];
|
|
7980
|
+
const model = shape ? modelOf(THREE, shape, 2) : void 0;
|
|
7981
|
+
if (!model) return void 0;
|
|
7982
|
+
model.traverse((node) => {
|
|
7983
|
+
const mesh = node;
|
|
7984
|
+
if (!mesh.isMesh) return;
|
|
7985
|
+
delete mesh.userData.destructionPart;
|
|
7986
|
+
mesh.material.color.multiplyScalar(0.35);
|
|
7987
|
+
});
|
|
7988
|
+
model.name = `${WORLD_EXPLOSIVE_PREFIX}${defId}_shell`;
|
|
7989
|
+
return model;
|
|
7990
|
+
};
|
|
7991
|
+
var worldExplosiveItem = (def, name, options = {}) => {
|
|
6566
7992
|
const shape = SHAPES2[def.id];
|
|
6567
7993
|
if (!shape) return void 0;
|
|
6568
|
-
const
|
|
7994
|
+
const scale = () => options.scale?.() ?? 1;
|
|
7995
|
+
const spec = () => ({ name, parts: [explosivePart(def, shape, scale())], joints: [], tags: ["explosive"] });
|
|
6569
7996
|
return {
|
|
6570
7997
|
id: `${WORLD_EXPLOSIVE_PREFIX}${def.id}`,
|
|
6571
7998
|
name,
|
|
6572
7999
|
build: (THREE) => {
|
|
6573
|
-
const
|
|
6574
|
-
|
|
8000
|
+
const T = THREE;
|
|
8001
|
+
const model = modelOf(T, shape);
|
|
8002
|
+
if (model) return model;
|
|
8003
|
+
const group = new T.Group();
|
|
8004
|
+
group.add(meshOf(T, shape));
|
|
6575
8005
|
return group;
|
|
6576
8006
|
},
|
|
6577
8007
|
spec
|
|
@@ -6623,6 +8053,100 @@ function createWorldEffects(options) {
|
|
|
6623
8053
|
};
|
|
6624
8054
|
}
|
|
6625
8055
|
|
|
8056
|
+
// src/scenarios.ts
|
|
8057
|
+
var TARGET_Z = -4;
|
|
8058
|
+
var SHOOTER_Z = -1.5;
|
|
8059
|
+
function weaponsScenarios(locale) {
|
|
8060
|
+
const pt = locale.toLowerCase().startsWith("pt");
|
|
8061
|
+
const T = (p, e) => pt ? p : e;
|
|
8062
|
+
const faceY = (id) => id === "target_steel_plate" ? 1.35 : id === "target_plywood" ? 1.5 : 1.2;
|
|
8063
|
+
const aimAt = (room, id) => room.look([0, faceY(id) + 0.05, SHOOTER_Z], [0, faceY(id), TARGET_Z]);
|
|
8064
|
+
return [
|
|
8065
|
+
{
|
|
8066
|
+
id: "target_steel_heavy_through_hole",
|
|
8067
|
+
name: T("Alvo de a\xE7o: um .50 BMG do Barrett M82A1 atravessa a chapa e o 9 mm s\xF3 amassa", "Steel target: the .50 BMG of the Barrett M82A1 goes through the plate and the 9 mm only dents it"),
|
|
8068
|
+
run: ({ room, step, expect }) => {
|
|
8069
|
+
const id = room.spawn("target_steel_plate", [0, 0, TARGET_Z]).objectId;
|
|
8070
|
+
room.setTool("pistol_9mm");
|
|
8071
|
+
room.waitUntilReady();
|
|
8072
|
+
aimAt(room, "target_steel_plate");
|
|
8073
|
+
room.use();
|
|
8074
|
+
step(0.3);
|
|
8075
|
+
const pistol = room.world.part(id, "face");
|
|
8076
|
+
expect((pistol?.dents.length ?? 0) > 0 && !pistol?.holes.some((h) => h.through), T(`9 mm na chapa de 10 mm: amassados ${pistol?.dents.length ?? 0}, furos que atravessam ${pistol?.holes.filter((h) => h.through).length ?? 0} (esperado: amassa e n\xE3o atravessa)`, `9 mm on the 10 mm plate: dents ${pistol?.dents.length ?? 0}, through holes ${pistol?.holes.filter((h) => h.through).length ?? 0} (expected: dents it, no through hole)`));
|
|
8077
|
+
room.setTool("barrett_m82a1");
|
|
8078
|
+
room.waitUntilReady();
|
|
8079
|
+
aimAt(room, "target_steel_plate");
|
|
8080
|
+
room.use();
|
|
8081
|
+
step(0.3);
|
|
8082
|
+
const rifle = room.world.part(id, "face");
|
|
8083
|
+
const hole = rifle?.holes.find((h) => h.through);
|
|
8084
|
+
expect(!!hole, T(`.50 BMG na chapa: furo que atravessa ${hole ? "sim" : "n\xE3o"} (furos: ${rifle?.holes.length ?? 0})`, `.50 BMG on the plate: through hole ${hole ? "yes" : "no"} (holes: ${rifle?.holes.length ?? 0})`), { holes: rifle?.holes.length });
|
|
8085
|
+
}
|
|
8086
|
+
},
|
|
8087
|
+
{
|
|
8088
|
+
id: "target_concrete_stops_bullet",
|
|
8089
|
+
name: T("Alvo de concreto: um 9 mm n\xE3o atravessa o bloco", "Concrete target: a 9 mm does not go through the block"),
|
|
8090
|
+
run: ({ room, step, expect }) => {
|
|
8091
|
+
const id = room.spawn("target_concrete", [0, 0, TARGET_Z]).objectId;
|
|
8092
|
+
room.setTool("pistol_9mm");
|
|
8093
|
+
room.waitUntilReady();
|
|
8094
|
+
aimAt(room, "target_concrete");
|
|
8095
|
+
room.use();
|
|
8096
|
+
step(0.2);
|
|
8097
|
+
const face = room.world.part(id, "face");
|
|
8098
|
+
const through = face?.holes.some((h) => h.through) ?? true;
|
|
8099
|
+
const marks = (face?.holes.length ?? 0) + (face?.dents.length ?? 0) + (face?.cracks.length ?? 0);
|
|
8100
|
+
expect(!through && marks > 0, T(`Bloco de concreto: atravessou ${through ? "sim" : "n\xE3o"}, marcas ${marks} (esperado: n\xE3o atravessar e marcar)`, `Concrete block: went through ${through ? "yes" : "no"}, marks ${marks} (expected: stopped and marked)`), { through, marks });
|
|
8101
|
+
}
|
|
8102
|
+
},
|
|
8103
|
+
{
|
|
8104
|
+
id: "target_plywood_shotgun_holes",
|
|
8105
|
+
name: T("Alvo de compensado: a escopeta de ferrolho crava v\xE1rios chumbos na madeira", "Plywood target: the pump shotgun drives several pellets through the wood"),
|
|
8106
|
+
run: ({ room, step, expect }) => {
|
|
8107
|
+
const id = room.spawn("target_plywood", [0, 0, TARGET_Z]).objectId;
|
|
8108
|
+
room.setTool("shotgun_pump_12");
|
|
8109
|
+
room.waitUntilReady();
|
|
8110
|
+
aimAt(room, "target_plywood");
|
|
8111
|
+
room.use();
|
|
8112
|
+
step(1.1);
|
|
8113
|
+
const holes = room.world.part(id, "face")?.holes.length ?? 0;
|
|
8114
|
+
expect(holes >= 3, T(`Chumbos na madeira num tiro: ${holes} furos (esperado: 3 ou mais)`, `Pellets in the wood from one shot: ${holes} holes (expected: 3 or more)`), { holes });
|
|
8115
|
+
}
|
|
8116
|
+
},
|
|
8117
|
+
{
|
|
8118
|
+
id: "target_steel_hammer_dents",
|
|
8119
|
+
name: T("Alvo de a\xE7o: a marreta amassa a chapa sem romper", "Steel target: the hammer dents the plate without breaking it"),
|
|
8120
|
+
run: ({ room, step, expect }) => {
|
|
8121
|
+
const id = room.spawn("target_steel_plate", [0, 0, TARGET_Z]).objectId;
|
|
8122
|
+
room.setTool("hammer");
|
|
8123
|
+
room.waitUntilReady();
|
|
8124
|
+
room.look([0, 1.4, TARGET_Z + 0.65], [0, 1.35, TARGET_Z]);
|
|
8125
|
+
let swings = 0;
|
|
8126
|
+
for (; swings < 8 && (room.world.part(id, "face")?.dents.length ?? 0) === 0; swings++) {
|
|
8127
|
+
room.use();
|
|
8128
|
+
step(1);
|
|
8129
|
+
}
|
|
8130
|
+
const face = room.world.part(id, "face");
|
|
8131
|
+
expect((face?.dents.length ?? 0) > 0 && face?.status !== "broken", T(`Amassados na chapa depois de ${swings} golpe(s): ${face?.dents.length ?? 0}`, `Dents in the plate after ${swings} swing(s): ${face?.dents.length ?? 0}`), { swings });
|
|
8132
|
+
}
|
|
8133
|
+
},
|
|
8134
|
+
{
|
|
8135
|
+
id: "target_steel_torch_melts",
|
|
8136
|
+
name: T("Alvo de a\xE7o: o ma\xE7arico de corte derrete a chapa", "Steel target: the cutting torch melts the plate"),
|
|
8137
|
+
run: ({ room, expect }) => {
|
|
8138
|
+
const id = room.spawn("target_steel_plate", [0, 0, TARGET_Z]).objectId;
|
|
8139
|
+
room.setTool("cutting_torch");
|
|
8140
|
+
room.waitUntilReady();
|
|
8141
|
+
room.look([0, faceY("target_steel_plate") + 0.05, TARGET_Z + 0.2], [0, faceY("target_steel_plate"), TARGET_Z]);
|
|
8142
|
+
room.use(20);
|
|
8143
|
+
const face = room.world.part(id, "face");
|
|
8144
|
+
expect((face?.melt ?? 0) > 0.02 || face?.status === "melted" || (face?.holes.length ?? 0) > 0, T(`Chapa depois do ma\xE7arico: ${Math.round(face?.temperature ?? 0)} \xB0C, derretido ${Math.round((face?.melt ?? 0) * 100)}%`, `Plate after the torch: ${Math.round(face?.temperature ?? 0)} \xB0C, ${Math.round((face?.melt ?? 0) * 100)}% melted`), { temperature: face?.temperature, melt: face?.melt });
|
|
8145
|
+
}
|
|
8146
|
+
}
|
|
8147
|
+
];
|
|
8148
|
+
}
|
|
8149
|
+
|
|
6626
8150
|
// src/area.ts
|
|
6627
8151
|
var WEAPON_ITEM_PREFIX = "weapon_";
|
|
6628
8152
|
var COUNTER_ITEM_ID = "counter_weapons";
|
|
@@ -6658,6 +8182,10 @@ var TEXT = {
|
|
|
6658
8182
|
counter: "Weapon counter",
|
|
6659
8183
|
targets: { target_steel_plate: "Steel plate target", target_plywood: "Plywood target", target_concrete: "Concrete block target" },
|
|
6660
8184
|
reloadStart: "Reload started: {weapon} ({ammo}/{capacity})",
|
|
8185
|
+
reloadClip: "Reload of {weapon}: {kind}, clip {clip} ({seconds} s)",
|
|
8186
|
+
soundCue: "Sound: {cue} ({weapon})",
|
|
8187
|
+
soundCues: { mag_release: "magazine release button", mag_out: "magazine slides out", mag_insert: "magazine slides in", mag_seat: "magazine seats (click)", slide_release: "slide snaps forward", charging_handle_pull: "charging handle pulled", charging_handle_release: "charging handle released", bolt_lift: "bolt handle lifted", bolt_pull: "bolt pulled back", bolt_push: "bolt pushed forward", bolt_lock: "bolt handle locked down", shell_insert: "round inserted", pump_back: "pump back", pump_forward: "pump forward", lever_open: "lever down", lever_close: "lever up", cylinder_open: "cylinder opens", cylinder_eject: "cases fall out", speedloader: "speed loader empties", cylinder_close: "cylinder closes", barrels_open: "barrels open", casings_eject: "cases pop out", barrels_close: "barrels close", belt_cover_open: "cover opens", belt_insert: "belt seats", belt_cover_close: "cover closes", rocket_load: "rocket slides in", rocket_seat: "rocket seats", grenade_load: "grenade goes in", breech_open: "breech opens", breech_close: "breech closes", tube_drop: "empty tube dropped", tube_seat: "new tube seats" },
|
|
8188
|
+
reloadKinds: { tactical: "tactical (round chambered, no pull)", empty: "empty chamber (works the action)", one_by_one: "one by one", full: "speed loader" },
|
|
6661
8189
|
reloadEnd: "Reload done: {weapon} ({ammo}/{capacity})",
|
|
6662
8190
|
worldExplosives: { fuel_barrel: "Fuel barrel", propane_p13: "Propane cylinder (P13)", car_fuel_tank: "Car fuel tank" },
|
|
6663
8191
|
mountedM134: "M134 Minigun on its tripod",
|
|
@@ -6680,6 +8208,10 @@ var TEXT = {
|
|
|
6680
8208
|
counter: "Balc\xE3o de armas",
|
|
6681
8209
|
targets: { target_steel_plate: "Alvo: chapa de a\xE7o", target_plywood: "Alvo: compensado", target_concrete: "Alvo: bloco de concreto" },
|
|
6682
8210
|
reloadStart: "Recarga iniciada: {weapon} ({ammo}/{capacity})",
|
|
8211
|
+
reloadClip: "Recarga de {weapon}: {kind}, clipe {clip} ({seconds} s)",
|
|
8212
|
+
soundCue: "Som: {cue} ({weapon})",
|
|
8213
|
+
soundCues: { mag_release: "bot\xE3o do carregador", mag_out: "carregador desliza para fora", mag_insert: "carregador desliza para dentro", mag_seat: "carregador encaixa (clique)", slide_release: "ferrolho volta com estalo", charging_handle_pull: "alavanca de armar puxada", charging_handle_release: "alavanca de armar solta", bolt_lift: "alavanca do ferrolho levantada", bolt_pull: "ferrolho puxado", bolt_push: "ferrolho empurrado", bolt_lock: "alavanca do ferrolho travada", shell_insert: "cartucho entra", pump_back: "bomba para tr\xE1s", pump_forward: "bomba para frente", lever_open: "alavanca para baixo", lever_close: "alavanca para cima", cylinder_open: "tambor abre", cylinder_eject: "c\xE1psulas caem", speedloader: "speedloader esvazia", cylinder_close: "tambor fecha", barrels_open: "canos abrem", casings_eject: "estojos saltam", barrels_close: "canos fecham", belt_cover_open: "tampa abre", belt_insert: "cinta encaixa", belt_cover_close: "tampa fecha", rocket_load: "foguete desliza para dentro", rocket_seat: "foguete encaixa", grenade_load: "granada entra", breech_open: "culatra abre", breech_close: "culatra fecha", tube_drop: "tubo vazio cai", tube_seat: "tubo novo encaixa" },
|
|
8214
|
+
reloadKinds: { tactical: "t\xE1tica (com bala na c\xE2mara, sem puxar)", empty: "vazia (puxa o ferrolho ou a trava)", one_by_one: "uma a uma", full: "carregador r\xE1pido" },
|
|
6683
8215
|
reloadEnd: "Recarga completa: {weapon} ({ammo}/{capacity})",
|
|
6684
8216
|
worldExplosives: { fuel_barrel: "Barril de combust\xEDvel", propane_p13: "Botij\xE3o de g\xE1s (P13)", car_fuel_tank: "Tanque de combust\xEDvel do carro" },
|
|
6685
8217
|
mountedM134: "M134 Minigun no trip\xE9",
|
|
@@ -6832,6 +8364,7 @@ function createWeaponsTestModule(options = {}) {
|
|
|
6832
8364
|
...worldExplosives.map((item) => withThumbnail(item, item.id.slice(WORLD_EXPLOSIVE_PREFIX.length))),
|
|
6833
8365
|
...mountedM134.map((item) => withThumbnail(item, MOUNTED_M134.weaponId))
|
|
6834
8366
|
],
|
|
8367
|
+
scenarios: (scenarioLocale) => weaponsScenarios(scenarioLocale ?? locale),
|
|
6835
8368
|
area: () => ({
|
|
6836
8369
|
placements: [
|
|
6837
8370
|
...weaponsLayout(weaponIds, pitchOf).filter((placement) => !placement.item.startsWith(WORLD_EXPLOSIVE_PREFIX) || worldExplosiveIds.has(placement.item)),
|
|
@@ -6874,7 +8407,10 @@ function createWeaponsTestModule(options = {}) {
|
|
|
6874
8407
|
audio.dispose();
|
|
6875
8408
|
audio = createWeaponAudio(void 0, { ...seedOption, engine: lentEngine });
|
|
6876
8409
|
}
|
|
8410
|
+
let simulatedMs = 0;
|
|
8411
|
+
const offClock = lent.onFrame((dt) => simulatedMs += dt * 1e3);
|
|
6877
8412
|
const created = createWeaponFx({
|
|
8413
|
+
onSound: ({ weapon, cue: cue2, profile }) => lent.log(fill(text.soundCue, { weapon: arsenal.name(weapon), cue: text.soundCues[cue2] ?? cue2 }), { weapon, cue: cue2, weight: profile.weight, material: profile.material }, "sound"),
|
|
6878
8414
|
THREE: lent.THREE,
|
|
6879
8415
|
scene: lent.scene,
|
|
6880
8416
|
camera: lent.camera,
|
|
@@ -6884,10 +8420,13 @@ function createWeaponsTestModule(options = {}) {
|
|
|
6884
8420
|
tool: lent.tool,
|
|
6885
8421
|
soundDistance: () => soundDistance,
|
|
6886
8422
|
raycast: createLaserRaycast(lent.THREE, lent.scene, lent.camera),
|
|
8423
|
+
// the weapon against a wall asks as often as the laser, on the clock of the simulation (so a fast-forwarded room measures as often per frame)
|
|
8424
|
+
contact: createLaserRaycast(lent.THREE, lent.scene, lent.camera, { now: () => simulatedMs }),
|
|
6887
8425
|
...lent.effects ? { effects: lent.effects } : {},
|
|
6888
8426
|
...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
|
|
6889
8427
|
...options.condition ? { condition: options.condition } : {},
|
|
6890
|
-
...options.seed === void 0 ? {} : { seed: options.seed }
|
|
8428
|
+
...options.seed === void 0 ? {} : { seed: options.seed },
|
|
8429
|
+
...options.flyRocket === void 0 ? {} : { flyRocket: options.flyRocket }
|
|
6891
8430
|
});
|
|
6892
8431
|
fx = created;
|
|
6893
8432
|
const camera = lent.camera;
|
|
@@ -6918,15 +8457,25 @@ function createWeaponsTestModule(options = {}) {
|
|
|
6918
8457
|
}
|
|
6919
8458
|
const tool = lent.tool();
|
|
6920
8459
|
created.handle(event, tool);
|
|
8460
|
+
if (event.type === "reload" && event.phase === "start") {
|
|
8461
|
+
const clip = created.clips.find((entry) => entry.name.startsWith("reload"));
|
|
8462
|
+
if (clip) {
|
|
8463
|
+
const kind = event.kind ?? "tactical";
|
|
8464
|
+
lent.log(fill(text.reloadClip, { weapon: arsenal.name(event.weapon), kind: text.reloadKinds[kind] ?? kind, clip: clip.name, seconds: clip.seconds.toFixed(2) }), { weapon: event.weapon, kind, clip: clip.name, seconds: clip.seconds });
|
|
8465
|
+
}
|
|
8466
|
+
}
|
|
6921
8467
|
if (event.type === "reload") lent.log(fill(event.phase === "start" ? text.reloadStart : text.reloadEnd, { weapon: arsenal.name(event.weapon), ammo: tool.ammo, capacity: tool.capacity }), { weapon: event.weapon, phase: event.phase });
|
|
6922
8468
|
});
|
|
6923
8469
|
const offFrame = lent.onFrame((dt) => created.update(dt));
|
|
8470
|
+
const offHands = lent.setHandTargets?.(() => created.handTargets());
|
|
6924
8471
|
created.handle({ type: "tool", weapon: lent.tool().weapon }, lent.tool());
|
|
6925
8472
|
return () => {
|
|
6926
8473
|
accessories.dispose();
|
|
6927
8474
|
offBench?.();
|
|
6928
8475
|
offEvents();
|
|
6929
8476
|
offFrame();
|
|
8477
|
+
offClock();
|
|
8478
|
+
offHands?.();
|
|
6930
8479
|
created.dispose();
|
|
6931
8480
|
audio?.dispose();
|
|
6932
8481
|
fx = null;
|
|
@@ -6935,6 +8484,973 @@ function createWeaponsTestModule(options = {}) {
|
|
|
6935
8484
|
}
|
|
6936
8485
|
};
|
|
6937
8486
|
}
|
|
8487
|
+
|
|
8488
|
+
// src/explosives-area.ts
|
|
8489
|
+
import { EXPLOSIONS } from "@adatechnology/game-vfx";
|
|
8490
|
+
import { createArsenal as createArsenal2 } from "@adatechnology/game-weapons";
|
|
8491
|
+
|
|
8492
|
+
// src/explosives-text.ts
|
|
8493
|
+
var EXPLOSIVES_TEXT = {
|
|
8494
|
+
en: {
|
|
8495
|
+
group: "Explosives",
|
|
8496
|
+
name: "Explosives area",
|
|
8497
|
+
cylinder: "Propane cylinder (P13)",
|
|
8498
|
+
actions: {
|
|
8499
|
+
shoot: { name: "Shoot the cylinder", hint: "Equips the 9 mm pistol, aims at the cylinder and fires: a hole in the shell, a white gas jet, a torch, the blast" },
|
|
8500
|
+
valve: { name: "Open the valve", hint: "Opens (or closes) the valve: the gas leaks with no fire until a flame reaches it" },
|
|
8501
|
+
light: { name: "Light it (lighter)", hint: "Holds a lighter flame at the valve: a gas leak catches fire and the cylinder goes off" },
|
|
8502
|
+
fire: { name: "Throw it in the fire", hint: "Lights a fire next to the cylinder (and the table): the shell heats up until the fire chain sets it off" },
|
|
8503
|
+
reset: { name: "Restart", hint: "Lays the scene again: a new cylinder, wall, furniture and people" }
|
|
8504
|
+
},
|
|
8505
|
+
blastScale: { name: "Blast strength (game multiplier)", hint: "Multiplies the energy and the lethal and injury radii of the P13 for the next one laid (restarts the scene). 1 is the arsenal; the arsenal itself is changed with ArsenalOptions.patch.explosives.propane_p13" },
|
|
8506
|
+
layers: { rings: "Lethal and injury rings" },
|
|
8507
|
+
metrics: { gas: "Gas left in the cylinder", clock: "Seconds since the first leak", phase: "Phase" },
|
|
8508
|
+
phases: { rest: "At rest", leaking: "Leaking gas", torch: "Burning jet", primed: "About to go off", exploded: "Exploded" },
|
|
8509
|
+
rings: { lethal: "lethal", injury: "injury" },
|
|
8510
|
+
triggers: { shot: "a bullet", fire: "fire", explosion: "another blast" },
|
|
8511
|
+
things: { zombie: "Zombie", survivor: "Survivor", fridge: "Fridge", kitchen_cabinet_base: "Kitchen cabinet", stove: "Stove", camp_table: "Table", can: "Can of beans", bottle: "Water bottle", chair: "Chair" },
|
|
8512
|
+
verdicts: { wall_pieces: "broke loose into {pieces} pieces and cracked ({n} marks)", lost_parts: "lost {n} parts", dead: "dead", hurt: "hurt (vitality {vitality}%)", unhurt: "unhurt", wall_broken: "cracked and lost {n} brick panels", wall_cracked: "cracked ({n} marks)", wall_intact: "intact", burning: "burning", standing: "standing" },
|
|
8513
|
+
log: {
|
|
8514
|
+
leak_shot: "Hole of {diameter} mm in the shell: gas leaking at {rate} l/s ({liters} l left in the cylinder)",
|
|
8515
|
+
leak_valve: "Valve open: gas leaking at {rate} l/s with no fire ({liters} l left in the cylinder)",
|
|
8516
|
+
valve_closed: "Valve closed: the leak stopped ({liters} l left)",
|
|
8517
|
+
valve_closed_late: "Too late to close the valve: the cylinder is already going off",
|
|
8518
|
+
primed: "The cylinder was set off by {trigger}: it goes off in {delay} s",
|
|
8519
|
+
jet_lit: "The jet caught fire (torch) {seconds} s after the hole; the cylinder goes off in {rest} s",
|
|
8520
|
+
jet_lit_by: "The leaking gas caught fire (torch) from {by}; the cylinder goes off in {rest} s",
|
|
8521
|
+
lighter_none: "Lighter flame at the valve: no gas is leaking, nothing happens",
|
|
8522
|
+
lighter_lit: "Lighter flame at the leaking valve: the gas lights",
|
|
8523
|
+
fire_set: "Fire lit next to the cylinder ({temperature} \xB0C, radius {radius} m): the shell heats up",
|
|
8524
|
+
heating: "Shell at {temperature} \xB0C (the fire chain starts at {limit} \xB0C)",
|
|
8525
|
+
explode: "Cylinder exploded: fireball of {fireball} m, lethal up to {lethal} m, injuries up to {injury} m (energy {energy} kJ, strength x{scale})",
|
|
8526
|
+
shell: "The torn shell flew up at {speed} m/s trailing fire for {trail} s",
|
|
8527
|
+
shell_landed: "The shell came down {distance} m away after {seconds} s",
|
|
8528
|
+
residual: "Residual fire of {radius} m for {seconds} s",
|
|
8529
|
+
aftermath_person: "{who} at {distance} m: {verdict}",
|
|
8530
|
+
aftermath_wall: "Wall behind the cylinder: {verdict}",
|
|
8531
|
+
aftermath_object: "{what} at {distance} m: {verdict}",
|
|
8532
|
+
reset: "Scene laid again with a blast strength of x{scale}",
|
|
8533
|
+
scale: "Blast strength x{scale}: energy {energy} kJ, lethal {lethal} m, injury {injury} m (valid for the next cylinder; the scene restarts)",
|
|
8534
|
+
no_room: "The explosives area was loaded without the room: this action needs it",
|
|
8535
|
+
shoot: "9 mm shot at the cylinder from {distance} m",
|
|
8536
|
+
no_vessel: "There is no cylinder in the scene (restart it)"
|
|
8537
|
+
},
|
|
8538
|
+
sheet: {
|
|
8539
|
+
kicker: "P13 cooking gas cylinder",
|
|
8540
|
+
title: "What happens to a P13 under a bullet, a flame or a blast",
|
|
8541
|
+
body: "Every kitchen has one. It goes off from a bullet or from fire, and it chains. The sequence is the Explosives Lab one (SC.p13): the gas jet hisses, catches fire as a torch, and the shell goes off.",
|
|
8542
|
+
facts: { energy: "Energy", lethal: "Lethal radius", injury: "Injury radius", gas: "Gas", mass: "Mass", torch: "Torch after the hole", shot_chain: "Blast after a bullet", fire_chain: "Blast after fire" },
|
|
8543
|
+
steps: [
|
|
8544
|
+
{ at: "0 s", text: "The bullet goes through the 2.5 mm steel shell: a white gas jet leaves the hole, hissing and shoving the cylinder" },
|
|
8545
|
+
{ at: "0.8 s", text: "The jet catches fire and burns as a torch at the hole" },
|
|
8546
|
+
{ at: "1.4 s", text: "The fuel fireball (light 70), the mushroom rises 7 m over 8 s, the shell flies up at 13 m/s trailing fire" },
|
|
8547
|
+
{ at: "1.4 s - 8.4 s", text: "Residual fire of 1.4 m for 7 s; walls, furniture and people inside the rings take the blast" }
|
|
8548
|
+
],
|
|
8549
|
+
segments: { jet: "White gas jet", torch: "Torch", blast: "Blast" },
|
|
8550
|
+
note: "Opening the valve leaks the gas with no fire; a flame near it lights the leak and the cylinder goes off 1.4 s later. The numbers come from the arsenal (propane_p13); the strength multiplier is a game value."
|
|
8551
|
+
},
|
|
8552
|
+
scenarios: {
|
|
8553
|
+
shot: "P13 cylinder: a 9 mm through the shell leaks gas, the jet lights and the cylinder goes off with its chain delay",
|
|
8554
|
+
shot_leak: "The shot made a gas leak with a white pressurized jet",
|
|
8555
|
+
shot_ignite: "The jet caught fire (torch) between 0.7 and 1.0 s after the hole",
|
|
8556
|
+
shot_explode: "The cylinder exploded within 1.6 s of the shot",
|
|
8557
|
+
shot_zombie: "The zombie 3 m from the cylinder did not survive the blast",
|
|
8558
|
+
shot_survivor: "The survivor 8 m away, past the lethal ring, is hurt but alive",
|
|
8559
|
+
shot_wall: "The brick wall behind it was marked by the blast",
|
|
8560
|
+
valve: "P13 cylinder: the open valve leaks gas without fire and the cylinder goes off only when a flame lights it",
|
|
8561
|
+
valve_leak: "The open valve leaks gas",
|
|
8562
|
+
valve_quiet: "Four seconds of leaking gas with no flame: no fire and no blast",
|
|
8563
|
+
valve_lit: "The lighter flame lit the leak",
|
|
8564
|
+
valve_explode: "The cylinder went off within 1.6 s of the flame",
|
|
8565
|
+
fire: "P13 cylinder in a fire: the heat sets it off by the fire chain",
|
|
8566
|
+
fire_explode: "The cylinder in the fire exploded",
|
|
8567
|
+
chain: "P13 cylinder: another blast next to it sets it off by the explosion chain",
|
|
8568
|
+
chain_explode: "The second cylinder exploded after the first one",
|
|
8569
|
+
push_p13: "P13 blast: the loose items, the furniture and the people around it are thrown away from it, less the farther they stand",
|
|
8570
|
+
push_grenade: "Thrown M67 grenade: the loose items and the people around it are thrown away from it, less the farther they stand",
|
|
8571
|
+
push_told: "The explosion reached the host as an event",
|
|
8572
|
+
push_found: "{n} bodies were laid around the blast to be measured ({kinds})",
|
|
8573
|
+
push_outward: "Every body inside the reach moved away from the blast ({moved} of {n}, {least} m at the least)",
|
|
8574
|
+
push_falloff: "{what}: the nearer one moved more than the farther one ({near} m at {nearDistance} m, {far} m at {farDistance} m)",
|
|
8575
|
+
push_person: "{who} at {distance} m was pushed {moved} m away",
|
|
8576
|
+
push_heavy: "{what} (too heavy to grab, not fixed) was moved {moved} m by the blast"
|
|
8577
|
+
}
|
|
8578
|
+
},
|
|
8579
|
+
"pt-BR": {
|
|
8580
|
+
group: "Explosivos",
|
|
8581
|
+
name: "\xC1rea de explosivos",
|
|
8582
|
+
cylinder: "Botij\xE3o de g\xE1s (P13)",
|
|
8583
|
+
actions: {
|
|
8584
|
+
shoot: { name: "Atirar no botij\xE3o", hint: "Equipa a pistola 9 mm, mira no botij\xE3o e atira: furo no casco, jato branco de g\xE1s, ma\xE7arico, explos\xE3o" },
|
|
8585
|
+
valve: { name: "Abrir o registro", hint: "Abre (ou fecha) o registro: o g\xE1s vaza sem fogo at\xE9 uma chama encostar" },
|
|
8586
|
+
light: { name: "Acender (isqueiro)", hint: "P\xF5e a chama de um isqueiro no registro: g\xE1s vazando pega fogo e o botij\xE3o explode" },
|
|
8587
|
+
fire: { name: "Jogar no fogo", hint: "Acende um fogo ao lado do botij\xE3o (e da mesa): o casco esquenta at\xE9 a cadeia de fogo detonar" },
|
|
8588
|
+
reset: { name: "Reiniciar", hint: "Monta a cena de novo: botij\xE3o novo, parede, m\xF3veis e pessoas" }
|
|
8589
|
+
},
|
|
8590
|
+
blastScale: { name: "For\xE7a da explos\xE3o (multiplicador de jogo)", hint: "Multiplica a energia e os raios letal e de ferimento do P13 para o pr\xF3ximo botij\xE3o montado (reinicia a cena). 1 \xE9 o arsenal; o arsenal em si muda com ArsenalOptions.patch.explosives.propane_p13" },
|
|
8591
|
+
layers: { rings: "An\xE9is letal e de ferimento" },
|
|
8592
|
+
metrics: { gas: "G\xE1s no botij\xE3o", clock: "Segundos desde o primeiro vazamento", phase: "Fase" },
|
|
8593
|
+
phases: { rest: "Em repouso", leaking: "Vazando g\xE1s", torch: "Jato em chamas", primed: "Prestes a explodir", exploded: "Explodiu" },
|
|
8594
|
+
rings: { lethal: "letal", injury: "ferimento" },
|
|
8595
|
+
triggers: { shot: "um tiro", fire: "fogo", explosion: "outra explos\xE3o" },
|
|
8596
|
+
things: { zombie: "Zumbi", survivor: "Sobrevivente", fridge: "Geladeira", kitchen_cabinet_base: "Arm\xE1rio de cozinha", stove: "Fog\xE3o", camp_table: "Mesa", can: "Lata de feij\xE3o", bottle: "Garrafa de \xE1gua", chair: "Cadeira" },
|
|
8597
|
+
verdicts: { wall_pieces: "se soltou em {pieces} peda\xE7os e trincou ({n} marcas)", lost_parts: "perdeu {n} pe\xE7as", dead: "morto", hurt: "ferido (vitalidade {vitality}%)", unhurt: "ileso", wall_broken: "trincou e perdeu {n} pain\xE9is de tijolo", wall_cracked: "trincou ({n} marcas)", wall_intact: "intacta", burning: "pegando fogo", standing: "de p\xE9" },
|
|
8598
|
+
log: {
|
|
8599
|
+
leak_shot: "Furo de {diameter} mm no casco: g\xE1s vazando a {rate} l/s ({liters} l no botij\xE3o)",
|
|
8600
|
+
leak_valve: "Registro aberto: g\xE1s vazando a {rate} l/s, sem fogo ({liters} l no botij\xE3o)",
|
|
8601
|
+
valve_closed: "Registro fechado: o vazamento parou ({liters} l restantes)",
|
|
8602
|
+
valve_closed_late: "Tarde demais para fechar o registro: o botij\xE3o j\xE1 vai explodir",
|
|
8603
|
+
primed: "O botij\xE3o foi detonado por {trigger}: explode em {delay} s",
|
|
8604
|
+
jet_lit: "O jato pegou fogo (ma\xE7arico) {seconds} s depois do furo; o botij\xE3o explode em {rest} s",
|
|
8605
|
+
jet_lit_by: "O g\xE1s vazando pegou fogo (ma\xE7arico) por {by}; o botij\xE3o explode em {rest} s",
|
|
8606
|
+
lighter_none: "Chama do isqueiro no registro: n\xE3o h\xE1 g\xE1s vazando, nada acontece",
|
|
8607
|
+
lighter_lit: "Chama do isqueiro no registro que vaza: o g\xE1s acendeu",
|
|
8608
|
+
fire_set: "Fogo aceso ao lado do botij\xE3o ({temperature} \xB0C, raio {radius} m): o casco esquenta",
|
|
8609
|
+
heating: "Casco a {temperature} \xB0C (a cadeia de fogo come\xE7a em {limit} \xB0C)",
|
|
8610
|
+
explode: "Botij\xE3o explodiu: bola de fogo de {fireball} m, letal at\xE9 {lethal} m, ferimentos at\xE9 {injury} m (energia {energy} kJ, for\xE7a x{scale})",
|
|
8611
|
+
shell: "O casco rasgado voou para cima a {speed} m/s com rastro de fogo por {trail} s",
|
|
8612
|
+
shell_landed: "O casco caiu a {distance} m depois de {seconds} s",
|
|
8613
|
+
residual: "Fogo residual de {radius} m por {seconds} s",
|
|
8614
|
+
aftermath_person: "{who} a {distance} m: {verdict}",
|
|
8615
|
+
aftermath_wall: "Parede atr\xE1s do botij\xE3o: {verdict}",
|
|
8616
|
+
aftermath_object: "{what} a {distance} m: {verdict}",
|
|
8617
|
+
reset: "Cena montada de novo com for\xE7a de explos\xE3o x{scale}",
|
|
8618
|
+
scale: "For\xE7a da explos\xE3o x{scale}: energia {energy} kJ, letal {lethal} m, ferimento {injury} m (vale para o pr\xF3ximo botij\xE3o; a cena reinicia)",
|
|
8619
|
+
no_room: "A \xE1rea de explosivos foi carregada sem a sala: esta a\xE7\xE3o precisa dela",
|
|
8620
|
+
shoot: "Tiro de 9 mm no botij\xE3o a {distance} m",
|
|
8621
|
+
no_vessel: "N\xE3o h\xE1 botij\xE3o na cena (reinicie)"
|
|
8622
|
+
},
|
|
8623
|
+
sheet: {
|
|
8624
|
+
kicker: "Botij\xE3o de g\xE1s de cozinha P13",
|
|
8625
|
+
title: "O que acontece com um P13 sob um tiro, uma chama ou uma explos\xE3o",
|
|
8626
|
+
body: "Toda cozinha tem um. Explode com tiro ou fogo, e entra em cadeia. A sequ\xEAncia \xE9 a do Laborat\xF3rio de Explos\xF5es (SC.p13): o jato de g\xE1s assobia, pega fogo como ma\xE7arico e o casco explode.",
|
|
8627
|
+
facts: { energy: "Energia", lethal: "Raio letal", injury: "Raio de ferimento", gas: "G\xE1s", mass: "Massa", torch: "Ma\xE7arico ap\xF3s o furo", shot_chain: "Explos\xE3o ap\xF3s um tiro", fire_chain: "Explos\xE3o ap\xF3s fogo" },
|
|
8628
|
+
steps: [
|
|
8629
|
+
{ at: "0 s", text: "A bala atravessa o casco de a\xE7o de 2,5 mm: um jato branco de g\xE1s sai do furo, assobiando e sacudindo o botij\xE3o" },
|
|
8630
|
+
{ at: "0,8 s", text: "O jato pega fogo e queima como ma\xE7arico no furo" },
|
|
8631
|
+
{ at: "1,4 s", text: "A bola de fogo de combust\xEDvel (luz 70), o cogumelo sobe 7 m em 8 s, o casco voa a 13 m/s com rastro de fogo" },
|
|
8632
|
+
{ at: "1,4 s - 8,4 s", text: "Fogo residual de 1,4 m por 7 s; paredes, m\xF3veis e pessoas dentro dos an\xE9is levam a explos\xE3o" }
|
|
8633
|
+
],
|
|
8634
|
+
segments: { jet: "Jato branco de g\xE1s", torch: "Ma\xE7arico", blast: "Explos\xE3o" },
|
|
8635
|
+
note: "Abrir o registro vaza o g\xE1s sem fogo; uma chama perto acende o vazamento e o botij\xE3o explode 1,4 s depois. Os n\xFAmeros v\xEAm do arsenal (propane_p13); o multiplicador de for\xE7a \xE9 um valor de jogo."
|
|
8636
|
+
},
|
|
8637
|
+
scenarios: {
|
|
8638
|
+
shot: "Botij\xE3o P13: um 9 mm no casco vaza g\xE1s, o jato acende e o botij\xE3o explode com o atraso da cadeia",
|
|
8639
|
+
shot_leak: "O tiro fez um vazamento de g\xE1s com jato branco pressurizado",
|
|
8640
|
+
shot_ignite: "O jato pegou fogo (ma\xE7arico) entre 0,7 e 1,0 s depois do furo",
|
|
8641
|
+
shot_explode: "O botij\xE3o explodiu at\xE9 1,6 s depois do tiro",
|
|
8642
|
+
shot_zombie: "O zumbi a 3 m do botij\xE3o n\xE3o sobreviveu \xE0 explos\xE3o",
|
|
8643
|
+
shot_survivor: "O sobrevivente a 8 m, depois do anel letal, fica ferido mas vivo",
|
|
8644
|
+
shot_wall: "A parede de tijolo atr\xE1s dele ficou marcada pela explos\xE3o",
|
|
8645
|
+
valve: "Botij\xE3o P13: o registro aberto vaza g\xE1s sem fogo e o botij\xE3o s\xF3 explode quando uma chama acende",
|
|
8646
|
+
valve_leak: "O registro aberto vaza g\xE1s",
|
|
8647
|
+
valve_quiet: "Quatro segundos de g\xE1s vazando sem chama: sem fogo e sem explos\xE3o",
|
|
8648
|
+
valve_lit: "A chama do isqueiro acendeu o vazamento",
|
|
8649
|
+
valve_explode: "O botij\xE3o explodiu at\xE9 1,6 s depois da chama",
|
|
8650
|
+
fire: "Botij\xE3o P13 no fogo: o calor detona pela cadeia de fogo",
|
|
8651
|
+
fire_explode: "O botij\xE3o no fogo explodiu",
|
|
8652
|
+
chain: "Botij\xE3o P13: outra explos\xE3o ao lado detona pela cadeia de explos\xE3o",
|
|
8653
|
+
chain_explode: "O segundo botij\xE3o explodiu depois do primeiro",
|
|
8654
|
+
push_p13: "Explos\xE3o do P13: os itens soltos, os m\xF3veis e as pessoas em volta s\xE3o jogados para longe, menos quanto mais longe est\xE3o",
|
|
8655
|
+
push_grenade: "Granada M67 arremessada: os itens soltos e as pessoas em volta s\xE3o jogados para longe, menos quanto mais longe est\xE3o",
|
|
8656
|
+
push_told: "A explos\xE3o chegou ao host como evento",
|
|
8657
|
+
push_found: "{n} corpos foram postos em volta da explos\xE3o para serem medidos ({kinds})",
|
|
8658
|
+
push_outward: "Todo corpo dentro do alcance se afastou da explos\xE3o ({moved} de {n}, no m\xEDnimo {least} m)",
|
|
8659
|
+
push_falloff: "{what}: o mais perto andou mais que o mais longe ({near} m a {nearDistance} m, {far} m a {farDistance} m)",
|
|
8660
|
+
push_person: "{who} a {distance} m foi empurrado {moved} m para longe",
|
|
8661
|
+
push_heavy: "{what} (pesado demais para pegar, mas solto) foi movido {moved} m pela explos\xE3o"
|
|
8662
|
+
}
|
|
8663
|
+
}
|
|
8664
|
+
};
|
|
8665
|
+
var explosivesTextOf = (locale) => EXPLOSIVES_TEXT[locale] ?? EXPLOSIVES_TEXT.en;
|
|
8666
|
+
var fillText2 = (template, vars) => template.replace(/\{(\w+)\}/g, (_, key) => String(vars[key] ?? ""));
|
|
8667
|
+
|
|
8668
|
+
// src/explosives-bench.ts
|
|
8669
|
+
var EXPLOSIVES_AREA_ID = "explosives_area";
|
|
8670
|
+
var FLAME_C = 1100;
|
|
8671
|
+
var FLAME_RADIUS = 0.2;
|
|
8672
|
+
var FLAME_SECONDS = 0.3;
|
|
8673
|
+
var FIRE_CHAIN_C = 300;
|
|
8674
|
+
var FIRE_OFFSET = [0.4, 0, 0.15];
|
|
8675
|
+
var FIRE_VISUAL_RADIUS = 0.6;
|
|
8676
|
+
var HISS_SHOVE = 0.35;
|
|
8677
|
+
var AFTERMATH_DELAY = 2;
|
|
8678
|
+
var SHOOTER = "pistol_9mm";
|
|
8679
|
+
var AIM_HEIGHT = 0.2;
|
|
8680
|
+
var GRAVITY2 = 9.81;
|
|
8681
|
+
var REPORTED_OBJECTS = ["fridge", "kitchen_cabinet_base", "stove", "camp_table"];
|
|
8682
|
+
var RING_SEGMENTS = 128;
|
|
8683
|
+
var WALL_ID = "target_wall";
|
|
8684
|
+
var WALL_Z = -11;
|
|
8685
|
+
var dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
8686
|
+
var num = (value, digits = 1) => value.toFixed(digits);
|
|
8687
|
+
function createExplosivesBench(options) {
|
|
8688
|
+
const { text, def, room, profile } = options;
|
|
8689
|
+
const log = text.log;
|
|
8690
|
+
let host = null;
|
|
8691
|
+
let strength = options.blastScale;
|
|
8692
|
+
let inArea = false;
|
|
8693
|
+
let lastBlast;
|
|
8694
|
+
let ringsOn = true;
|
|
8695
|
+
let initial = true;
|
|
8696
|
+
let aftermathDue = [];
|
|
8697
|
+
const vessels = /* @__PURE__ */ new Map();
|
|
8698
|
+
const wallParts = /* @__PURE__ */ new Set();
|
|
8699
|
+
const shells = [];
|
|
8700
|
+
const T = () => host?.THREE;
|
|
8701
|
+
const now = () => room?.world.time ?? 0;
|
|
8702
|
+
const say = (message, data = {}) => host?.log(message, { source: EXPLOSIVES_AREA_ID, ...data });
|
|
8703
|
+
const leakConfig = def.leak;
|
|
8704
|
+
const igniteDelay = leakConfig?.igniteDelay ?? 0.8;
|
|
8705
|
+
const hissSeconds = leakConfig?.hissSeconds ?? igniteDelay;
|
|
8706
|
+
const chainFire = (() => {
|
|
8707
|
+
const fire2 = def.chain?.fire;
|
|
8708
|
+
return fire2 === void 0 ? 1.4 : typeof fire2 === "number" ? fire2 : fire2[0];
|
|
8709
|
+
})();
|
|
8710
|
+
const liters = (vessel) => room?.world.part(vessel.objectId, "tank")?.liters ?? 0;
|
|
8711
|
+
const throughHoles = (vessel) => room?.world.part(vessel.objectId, "tank")?.holes.filter((hole) => hole.through).length ?? 0;
|
|
8712
|
+
const leaking = (vessel) => !vessel.exploded && liters(vessel) > 0 && throughHoles(vessel) > 0;
|
|
8713
|
+
const centreOf = (vessel) => {
|
|
8714
|
+
const p = vessel.root.getWorldPosition(new (T()).Vector3());
|
|
8715
|
+
return [p.x, p.y + AIM_HEIGHT, p.z];
|
|
8716
|
+
};
|
|
8717
|
+
const valveOf = (vessel) => worldExplosiveValve(T(), vessel.root) ?? centreOf(vessel);
|
|
8718
|
+
const target = () => [...vessels.values()].find((vessel) => !vessel.exploded) ?? [...vessels.values()][0];
|
|
8719
|
+
const lethalOf = (vessel) => def.lethalRadius * vessel.scale;
|
|
8720
|
+
const injuryOf = (vessel) => def.injuryRadius * vessel.scale;
|
|
8721
|
+
const labelSprite = (words, colour, at) => {
|
|
8722
|
+
try {
|
|
8723
|
+
const canvas = document.createElement("canvas");
|
|
8724
|
+
canvas.width = 256;
|
|
8725
|
+
canvas.height = 64;
|
|
8726
|
+
const context = canvas.getContext("2d");
|
|
8727
|
+
if (!context) return null;
|
|
8728
|
+
context.fillStyle = colour;
|
|
8729
|
+
context.font = "bold 40px sans-serif";
|
|
8730
|
+
context.textAlign = "center";
|
|
8731
|
+
context.fillText(words, 128, 46);
|
|
8732
|
+
const sprite = new (T()).Sprite(new (T()).SpriteMaterial({ map: new (T()).CanvasTexture(canvas), transparent: true, depthWrite: false }));
|
|
8733
|
+
sprite.scale.set(2.4, 0.6, 1);
|
|
8734
|
+
sprite.position.set(...at);
|
|
8735
|
+
return sprite;
|
|
8736
|
+
} catch {
|
|
8737
|
+
return null;
|
|
8738
|
+
}
|
|
8739
|
+
};
|
|
8740
|
+
const ringLine = (radius, colour) => {
|
|
8741
|
+
const points = [];
|
|
8742
|
+
for (let i = 0; i < RING_SEGMENTS; i++) {
|
|
8743
|
+
const angle = i / RING_SEGMENTS * Math.PI * 2;
|
|
8744
|
+
points.push(Math.cos(angle) * radius, 0, Math.sin(angle) * radius);
|
|
8745
|
+
}
|
|
8746
|
+
const geometry = new (T()).BufferGeometry();
|
|
8747
|
+
geometry.setAttribute("position", new (T()).Float32BufferAttribute(points, 3));
|
|
8748
|
+
return new (T()).LineLoop(geometry, new (T()).LineBasicMaterial({ color: colour }));
|
|
8749
|
+
};
|
|
8750
|
+
const drawRings = (vessel) => {
|
|
8751
|
+
const group = new (T()).Group();
|
|
8752
|
+
group.name = "blast_rings";
|
|
8753
|
+
const lethal = lethalOf(vessel);
|
|
8754
|
+
const injury = injuryOf(vessel);
|
|
8755
|
+
group.add(ringLine(lethal, 16726832), ringLine(injury, 16756768));
|
|
8756
|
+
const lethalLabel = labelSprite(`${num(lethal, 0)} m ${text.rings.lethal}`, "#ff6a5e", [0, 0.35, lethal]);
|
|
8757
|
+
const injuryLabel = labelSprite(`${num(injury, 0)} m ${text.rings.injury}`, "#ffc44d", [0, 0.35, injury]);
|
|
8758
|
+
for (const label of [lethalLabel, injuryLabel]) if (label) group.add(label);
|
|
8759
|
+
const p = vessel.root.getWorldPosition(new (T()).Vector3());
|
|
8760
|
+
group.position.set(p.x, 0.03, p.z);
|
|
8761
|
+
group.visible = ringsOn;
|
|
8762
|
+
(host?.scene).add(group);
|
|
8763
|
+
vessel.rings = group;
|
|
8764
|
+
};
|
|
8765
|
+
const clearRings = (vessel) => {
|
|
8766
|
+
vessel.rings?.removeFromParent();
|
|
8767
|
+
vessel.rings = void 0;
|
|
8768
|
+
};
|
|
8769
|
+
const phaseOf = (vessel) => {
|
|
8770
|
+
if (!vessel) return "rest";
|
|
8771
|
+
if (vessel.exploded) return "exploded";
|
|
8772
|
+
if (vessel.explodeAt !== void 0) return "primed";
|
|
8773
|
+
if (vessel.lit) return "torch";
|
|
8774
|
+
return vessel.leakAt === void 0 ? "rest" : "leaking";
|
|
8775
|
+
};
|
|
8776
|
+
const ignite = (vessel, by) => {
|
|
8777
|
+
if (vessel.lit || vessel.exploded || !room) return;
|
|
8778
|
+
const leak = vessel.leaks[vessel.leaks.length - 1];
|
|
8779
|
+
const point = leak?.point ?? valveOf(vessel);
|
|
8780
|
+
room.world.hit(vessel.objectId, { type: "heat", point, temperature: FLAME_C, radius: FLAME_RADIUS, duration: FLAME_SECONDS, source: "p13_flame" });
|
|
8781
|
+
vessel.lit = true;
|
|
8782
|
+
vessel.litAt = now();
|
|
8783
|
+
const rest = vessel.explodeAt === void 0 ? chainFire : Math.max(0, vessel.explodeAt - now());
|
|
8784
|
+
if (by === "shot") say(fillText2(log.jet_lit, { seconds: num(now() - (vessel.leakAt ?? now()), 1), rest: num(rest, 1) }), { phase: "torch", objectId: vessel.objectId, seconds: now() - (vessel.leakAt ?? now()), rest });
|
|
8785
|
+
else say(fillText2(log.lighter_lit, {}), { phase: "torch", objectId: vessel.objectId, by });
|
|
8786
|
+
};
|
|
8787
|
+
const shove = (vessel) => {
|
|
8788
|
+
const leak = vessel.leaks[vessel.leaks.length - 1];
|
|
8789
|
+
if (!room || !leak) return;
|
|
8790
|
+
const [dx, dy, dz] = leak.direction;
|
|
8791
|
+
const length = Math.hypot(dx, dy, dz) || 1;
|
|
8792
|
+
room.renderer.impulse(vessel.objectId, [-dx / length * HISS_SHOVE, -dy / length * HISS_SHOVE * 0.2, -dz / length * HISS_SHOVE], leak.point);
|
|
8793
|
+
};
|
|
8794
|
+
const torchSprites = (vessel) => {
|
|
8795
|
+
const leak = vessel.leaks[vessel.leaks.length - 1];
|
|
8796
|
+
if (!room || !leak) return;
|
|
8797
|
+
const [dx, dy, dz] = leak.direction;
|
|
8798
|
+
const length = Math.hypot(dx, dy, dz) || 1;
|
|
8799
|
+
const flame = () => 3 + Math.random() * 2.5;
|
|
8800
|
+
for (let i = 0; i < 3; i++) room.vfx.spawn("fire", leak.point, [dx / length * flame() + (Math.random() - 0.5) * 0.7, dy / length * flame() + 0.6 + (Math.random() - 0.5) * 0.7, dz / length * flame() + (Math.random() - 0.5) * 0.7], { size: 0.45 + Math.random() * 0.35, life: 0.7 });
|
|
8801
|
+
if (vessel.torchFrame++ % 3 === 0) room.vfx.spawn("smoke", leak.point, [dx / length * 2, 1.5, dz / length * 2], { size: 0.7, life: 1 });
|
|
8802
|
+
};
|
|
8803
|
+
const launchShell = (vessel, point) => {
|
|
8804
|
+
const config = def.shell;
|
|
8805
|
+
if (!config || !host) return;
|
|
8806
|
+
const mesh = worldExplosiveShell(T(), def.id);
|
|
8807
|
+
if (!mesh) return;
|
|
8808
|
+
mesh.position.set(point[0], Math.max(point[1], 0.3), point[2]);
|
|
8809
|
+
host.scene.add(mesh);
|
|
8810
|
+
shells.push({
|
|
8811
|
+
mesh,
|
|
8812
|
+
position: [point[0], Math.max(point[1], 0.3), point[2]],
|
|
8813
|
+
velocity: [(Math.random() - 0.5) * 3, config.launchSpeed, (Math.random() - 0.5) * 3],
|
|
8814
|
+
spin: [(Math.random() - 0.5) * 12, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 12],
|
|
8815
|
+
age: 0,
|
|
8816
|
+
trail: config.trailSeconds,
|
|
8817
|
+
landed: false,
|
|
8818
|
+
start: point
|
|
8819
|
+
});
|
|
8820
|
+
say(fillText2(log.shell, { speed: num(config.launchSpeed, 0), trail: num(config.trailSeconds, 1) }), { phase: "shell", speed: config.launchSpeed, trailSeconds: config.trailSeconds });
|
|
8821
|
+
void vessel;
|
|
8822
|
+
};
|
|
8823
|
+
const stepShells = (dt) => {
|
|
8824
|
+
if (!host) return;
|
|
8825
|
+
for (const shell of shells) {
|
|
8826
|
+
if (shell.landed) continue;
|
|
8827
|
+
shell.age += dt;
|
|
8828
|
+
shell.velocity[1] -= GRAVITY2 * dt;
|
|
8829
|
+
for (let axis = 0; axis < 3; axis++) shell.position[axis] = shell.position[axis] + shell.velocity[axis] * dt;
|
|
8830
|
+
const ground = host.groundAt(shell.position[0], shell.position[2]) + 0.18;
|
|
8831
|
+
if (shell.position[1] <= ground && shell.velocity[1] < 0) {
|
|
8832
|
+
shell.position[1] = ground;
|
|
8833
|
+
if (Math.abs(shell.velocity[1]) > 1.5) {
|
|
8834
|
+
shell.velocity = [shell.velocity[0] * 0.5, -shell.velocity[1] * 0.35, shell.velocity[2] * 0.5];
|
|
8835
|
+
} else {
|
|
8836
|
+
shell.landed = true;
|
|
8837
|
+
shell.mesh.rotation.set(Math.PI / 2, shell.mesh.rotation.y, 0);
|
|
8838
|
+
const away = dist(shell.start, [shell.position[0], shell.position[1], shell.position[2]]);
|
|
8839
|
+
say(fillText2(log.shell_landed, { distance: num(away, 1), seconds: num(shell.age, 1) }), { phase: "shell_landed", distance: away, seconds: shell.age });
|
|
8840
|
+
}
|
|
8841
|
+
} else {
|
|
8842
|
+
shell.mesh.rotation.x += shell.spin[0] * dt;
|
|
8843
|
+
shell.mesh.rotation.y += shell.spin[1] * dt;
|
|
8844
|
+
shell.mesh.rotation.z += shell.spin[2] * dt;
|
|
8845
|
+
}
|
|
8846
|
+
shell.mesh.position.set(shell.position[0], shell.position[1], shell.position[2]);
|
|
8847
|
+
if (room && shell.age < shell.trail) {
|
|
8848
|
+
const at = [shell.position[0], shell.position[1], shell.position[2]];
|
|
8849
|
+
room.vfx.spawn("fire", at, [(Math.random() - 0.5) * 0.6, -0.3 + Math.random() * 0.4, (Math.random() - 0.5) * 0.6], { size: 0.6, life: 0.6 });
|
|
8850
|
+
room.vfx.spawn("smoke", at, [(Math.random() - 0.5) * 0.4, 0.2, (Math.random() - 0.5) * 0.4], { size: 0.7, life: 1.2 });
|
|
8851
|
+
}
|
|
8852
|
+
}
|
|
8853
|
+
};
|
|
8854
|
+
const clearShells = () => {
|
|
8855
|
+
for (const shell of shells) shell.mesh.removeFromParent();
|
|
8856
|
+
shells.length = 0;
|
|
8857
|
+
};
|
|
8858
|
+
const fullVitality = /* @__PURE__ */ new Map();
|
|
8859
|
+
const witnesses = () => {
|
|
8860
|
+
if (!room) return [];
|
|
8861
|
+
const origin = [...vessels.values()][0]?.centre ?? options.scene.cylinder;
|
|
8862
|
+
const found = [];
|
|
8863
|
+
for (const spawned of room.spawned()) {
|
|
8864
|
+
const position = spawned.root.getWorldPosition(new (T()).Vector3());
|
|
8865
|
+
const at = [position.x, position.y, position.z];
|
|
8866
|
+
const distance = dist([at[0], 0, at[2]], [origin[0], 0, origin[2]]);
|
|
8867
|
+
if (spawned.itemId.startsWith("zombie_") || spawned.itemId.startsWith("person_")) {
|
|
8868
|
+
const state = room.world.state(spawned.objectId);
|
|
8869
|
+
const alive = state?.body?.alive ?? true;
|
|
8870
|
+
const raw = state?.body?.vitality;
|
|
8871
|
+
if (raw !== void 0) fullVitality.set(spawned.objectId, Math.max(fullVitality.get(spawned.objectId) ?? 0, raw));
|
|
8872
|
+
const vitality = raw === void 0 ? void 0 : Math.max(0, raw) / Math.max(1e-9, fullVitality.get(spawned.objectId) ?? raw);
|
|
8873
|
+
found.push({ kind: "person", id: spawned.objectId, itemId: spawned.itemId, distance, alive, ...vitality === void 0 ? {} : { vitality } });
|
|
8874
|
+
} else if (REPORTED_OBJECTS.includes(spawned.itemId)) {
|
|
8875
|
+
const parts = room.world.state(spawned.objectId)?.parts ?? [];
|
|
8876
|
+
found.push({ kind: "object", id: spawned.objectId, itemId: spawned.itemId, distance, broken: parts.filter((part) => part.status === "broken").length, marks: parts.filter((part) => part.burning).length });
|
|
8877
|
+
}
|
|
8878
|
+
}
|
|
8879
|
+
if (wallParts.size > 0) {
|
|
8880
|
+
let broken = 0;
|
|
8881
|
+
let marks = 0;
|
|
8882
|
+
let pieces = 0;
|
|
8883
|
+
for (const id of room.world.ids()) {
|
|
8884
|
+
const mine = (room.world.state(id)?.parts ?? []).filter((part) => wallParts.has(part.id));
|
|
8885
|
+
if (mine.length === 0) continue;
|
|
8886
|
+
pieces++;
|
|
8887
|
+
broken += mine.filter((part) => part.status === "broken").length;
|
|
8888
|
+
marks += mine.reduce((n, part) => n + part.cracks.length + part.holes.length, 0);
|
|
8889
|
+
}
|
|
8890
|
+
found.push({ kind: "wall", id: "target_wall", itemId: "target_wall", distance: Math.abs(WALL_Z - origin[2]), broken, marks, pieces });
|
|
8891
|
+
}
|
|
8892
|
+
return found;
|
|
8893
|
+
};
|
|
8894
|
+
const whoIs = (itemId) => itemId.startsWith("zombie_") ? text.things.zombie : text.things.survivor;
|
|
8895
|
+
const aftermath = (point) => {
|
|
8896
|
+
for (const w of witnesses()) {
|
|
8897
|
+
const distance = num(w.distance, 1);
|
|
8898
|
+
if (w.kind === "person") {
|
|
8899
|
+
const verdict = w.alive === false ? text.verdicts.dead : w.vitality !== void 0 && w.vitality < 0.999 ? fillText2(text.verdicts.hurt, { vitality: num(w.vitality * 100, 0) }) : text.verdicts.unhurt;
|
|
8900
|
+
say(fillText2(log.aftermath_person, { who: whoIs(w.itemId), distance, verdict }), { phase: "aftermath", kind: w.kind, objectId: w.id, distance: w.distance, alive: w.alive, vitality: w.vitality, point });
|
|
8901
|
+
} else if (w.kind === "wall") {
|
|
8902
|
+
const verdict = (w.pieces ?? 1) > 1 ? fillText2(text.verdicts.wall_pieces, { pieces: w.pieces ?? 0, n: w.marks ?? 0 }) : (w.broken ?? 0) > 0 ? fillText2(text.verdicts.wall_broken, { n: w.broken ?? 0 }) : (w.marks ?? 0) > 0 ? fillText2(text.verdicts.wall_cracked, { n: w.marks ?? 0 }) : text.verdicts.wall_intact;
|
|
8903
|
+
say(fillText2(log.aftermath_wall, { verdict }), { phase: "aftermath", kind: w.kind, objectId: w.id, broken: w.broken, marks: w.marks, pieces: w.pieces });
|
|
8904
|
+
} else {
|
|
8905
|
+
const verdict = (w.broken ?? 0) > 0 ? fillText2(text.verdicts.lost_parts, { n: w.broken ?? 0 }) : (w.marks ?? 0) > 0 ? text.verdicts.burning : text.verdicts.standing;
|
|
8906
|
+
say(fillText2(log.aftermath_object, { what: text.things[w.itemId] ?? w.itemId, distance, verdict }), { phase: "aftermath", kind: w.kind, objectId: w.id, distance: w.distance, broken: w.broken });
|
|
8907
|
+
}
|
|
8908
|
+
}
|
|
8909
|
+
};
|
|
8910
|
+
const onEvent = (event) => {
|
|
8911
|
+
const vessel = vessels.get(event.object);
|
|
8912
|
+
if (!vessel || !("part" in event) || event.part !== "tank") return;
|
|
8913
|
+
if (event.type === "leak") {
|
|
8914
|
+
witnesses();
|
|
8915
|
+
if (vessel.leakAt === void 0) {
|
|
8916
|
+
vessel.leakAt = event.time;
|
|
8917
|
+
vessel.cause = vessel.valveRequested ? "valve" : "shot";
|
|
8918
|
+
}
|
|
8919
|
+
vessel.valveRequested = false;
|
|
8920
|
+
vessel.leaks.push({ point: event.point, direction: event.direction, rate: event.rate });
|
|
8921
|
+
if (vessel.leaks.length > 8) vessel.leaks.shift();
|
|
8922
|
+
const holes = room?.world.part(vessel.objectId, "tank")?.holes ?? [];
|
|
8923
|
+
const diameter = (holes[holes.length - 1]?.diameter ?? 9e-3) * 1e3;
|
|
8924
|
+
const vars = { diameter: num(diameter, 0), rate: num(event.rate, 2), liters: num(liters(vessel), 1) };
|
|
8925
|
+
say(fillText2(vessel.cause === "valve" && vessel.leaks.length === 1 ? log.leak_valve : log.leak_shot, vars), { phase: "leak", objectId: vessel.objectId, cause: vessel.cause, ratePerS: event.rate, diameterMm: diameter, liters: liters(vessel), pressurized: event.pressurized });
|
|
8926
|
+
} else if (event.type === "primed") {
|
|
8927
|
+
vessel.explodeAt = event.time + event.delay;
|
|
8928
|
+
say(fillText2(log.primed, { trigger: text.triggers[event.trigger], delay: num(event.delay, 1) }), { phase: "primed", objectId: vessel.objectId, trigger: event.trigger, delaySeconds: event.delay });
|
|
8929
|
+
} else if (event.type === "explode") {
|
|
8930
|
+
vessel.exploded = true;
|
|
8931
|
+
vessel.explodedAt = event.time;
|
|
8932
|
+
vessel.centre = event.point;
|
|
8933
|
+
const radius = profile?.visualRadius;
|
|
8934
|
+
const fireball = (radius ?? 2) * 2;
|
|
8935
|
+
const lethal = lethalOf(vessel);
|
|
8936
|
+
const injury = injuryOf(vessel);
|
|
8937
|
+
say(fillText2(log.explode, { fireball: num(fireball, 1), lethal: num(lethal, 1), injury: num(injury, 1), energy: num(def.energy * vessel.scale / 1e3, 0), scale: num(vessel.scale, 2) }), {
|
|
8938
|
+
phase: "explode",
|
|
8939
|
+
objectId: vessel.objectId,
|
|
8940
|
+
fireballMetres: fireball,
|
|
8941
|
+
lethalRadius: lethal,
|
|
8942
|
+
injuryRadius: injury,
|
|
8943
|
+
energyJ: def.energy * vessel.scale,
|
|
8944
|
+
scale: vessel.scale,
|
|
8945
|
+
point: event.point,
|
|
8946
|
+
sinceLeakSeconds: vessel.leakAt === void 0 ? void 0 : event.time - vessel.leakAt
|
|
8947
|
+
});
|
|
8948
|
+
const residual = profile?.residualFires?.[0];
|
|
8949
|
+
if (residual) say(fillText2(log.residual, { radius: num(residual.radius, 1), seconds: num(residual.life, 0) }), { phase: "residual", radius: residual.radius, seconds: residual.life });
|
|
8950
|
+
if (vessel.fireHandle !== void 0) room?.vfx.stop(vessel.fireHandle);
|
|
8951
|
+
launchShell(vessel, event.point);
|
|
8952
|
+
aftermathDue.push({ at: now() + AFTERMATH_DELAY, vessel, point: event.point });
|
|
8953
|
+
}
|
|
8954
|
+
};
|
|
8955
|
+
const frame = (dt) => {
|
|
8956
|
+
const t = now();
|
|
8957
|
+
for (const vessel of vessels.values()) {
|
|
8958
|
+
if (vessel.exploded) {
|
|
8959
|
+
vessel.lit = false;
|
|
8960
|
+
continue;
|
|
8961
|
+
}
|
|
8962
|
+
if (vessel.leakAt !== void 0) {
|
|
8963
|
+
if (t - vessel.leakAt <= hissSeconds && leaking(vessel)) shove(vessel);
|
|
8964
|
+
if (vessel.cause === "shot" && !vessel.lit && t - vessel.leakAt >= igniteDelay && leaking(vessel)) ignite(vessel, "shot");
|
|
8965
|
+
}
|
|
8966
|
+
if (vessel.lit) {
|
|
8967
|
+
if (leaking(vessel)) torchSprites(vessel);
|
|
8968
|
+
else vessel.lit = false;
|
|
8969
|
+
}
|
|
8970
|
+
if (vessel.fireUntil !== void 0 && t < vessel.fireUntil && room && vessel.firePoint) {
|
|
8971
|
+
const fire2 = def.fire;
|
|
8972
|
+
room.world.hit(vessel.objectId, { type: "heat", point: vessel.firePoint, temperature: fire2?.temperature ?? 700, radius: fire2?.radius ?? 1.4, duration: dt, source: "p13_fire" });
|
|
8973
|
+
if (vessel.explodeAt === void 0 && t >= vessel.nextHeatLog) {
|
|
8974
|
+
vessel.nextHeatLog = t + 1;
|
|
8975
|
+
say(fillText2(log.heating, { temperature: num(room.world.part(vessel.objectId, "tank")?.temperature ?? 0, 0), limit: num(FIRE_CHAIN_C, 0) }), { phase: "heating", objectId: vessel.objectId });
|
|
8976
|
+
}
|
|
8977
|
+
}
|
|
8978
|
+
}
|
|
8979
|
+
stepShells(dt);
|
|
8980
|
+
if (aftermathDue.length > 0) {
|
|
8981
|
+
const ready = aftermathDue.filter((entry) => entry.at <= t);
|
|
8982
|
+
aftermathDue = aftermathDue.filter((entry) => entry.at > t);
|
|
8983
|
+
for (const entry of ready) aftermath(entry.point);
|
|
8984
|
+
}
|
|
8985
|
+
};
|
|
8986
|
+
const needRoom = () => {
|
|
8987
|
+
if (room) return true;
|
|
8988
|
+
say(log.no_room);
|
|
8989
|
+
return false;
|
|
8990
|
+
};
|
|
8991
|
+
const withVessel = () => {
|
|
8992
|
+
const vessel = target();
|
|
8993
|
+
if (!vessel || vessel.exploded) say(log.no_vessel);
|
|
8994
|
+
return vessel && !vessel.exploded ? vessel : void 0;
|
|
8995
|
+
};
|
|
8996
|
+
const shoot = () => {
|
|
8997
|
+
if (!needRoom() || !room || !host) return false;
|
|
8998
|
+
const vessel = withVessel();
|
|
8999
|
+
if (!vessel) return false;
|
|
9000
|
+
if (host.tool().weapon !== SHOOTER) room.setTool(SHOOTER);
|
|
9001
|
+
room.waitUntilReady();
|
|
9002
|
+
const eye = host.camera.position;
|
|
9003
|
+
const aim = centreOf(vessel);
|
|
9004
|
+
room.look([eye.x, eye.y, eye.z], aim);
|
|
9005
|
+
say(fillText2(log.shoot, { distance: num(dist([eye.x, eye.y, eye.z], aim), 1) }), { phase: "shoot", objectId: vessel.objectId, weapon: SHOOTER });
|
|
9006
|
+
room.use();
|
|
9007
|
+
return true;
|
|
9008
|
+
};
|
|
9009
|
+
const valve = () => {
|
|
9010
|
+
if (!needRoom() || !room) return false;
|
|
9011
|
+
const vessel = withVessel();
|
|
9012
|
+
if (!vessel) return false;
|
|
9013
|
+
if (vessel.valveOpen) {
|
|
9014
|
+
if (vessel.explodeAt !== void 0) {
|
|
9015
|
+
say(log.valve_closed_late, { phase: "valve", objectId: vessel.objectId });
|
|
9016
|
+
return false;
|
|
9017
|
+
}
|
|
9018
|
+
room.world.seal(vessel.objectId, "tank");
|
|
9019
|
+
vessel.valveOpen = false;
|
|
9020
|
+
if (throughHoles(vessel) === 0) {
|
|
9021
|
+
vessel.leakAt = void 0;
|
|
9022
|
+
vessel.cause = void 0;
|
|
9023
|
+
vessel.leaks = [];
|
|
9024
|
+
vessel.lit = false;
|
|
9025
|
+
}
|
|
9026
|
+
say(fillText2(log.valve_closed, { liters: num(liters(vessel), 1) }), { phase: "valve_closed", objectId: vessel.objectId, liters: liters(vessel) });
|
|
9027
|
+
return true;
|
|
9028
|
+
}
|
|
9029
|
+
vessel.valveRequested = true;
|
|
9030
|
+
const opened = room.world.vent(vessel.objectId, "tank", { point: valveOf(vessel), normal: [0, 1, 0] });
|
|
9031
|
+
vessel.valveRequested = false;
|
|
9032
|
+
vessel.valveOpen = opened;
|
|
9033
|
+
return opened;
|
|
9034
|
+
};
|
|
9035
|
+
const light = () => {
|
|
9036
|
+
if (!needRoom() || !room) return false;
|
|
9037
|
+
const vessel = withVessel();
|
|
9038
|
+
if (!vessel) return false;
|
|
9039
|
+
const at = vessel.leaks[vessel.leaks.length - 1]?.point ?? valveOf(vessel);
|
|
9040
|
+
room.vfx.burst("fire", at, [0, 1, 0], 6, 0.8);
|
|
9041
|
+
if (!leaking(vessel)) {
|
|
9042
|
+
say(log.lighter_none, { phase: "lighter", objectId: vessel.objectId, leaking: false });
|
|
9043
|
+
return false;
|
|
9044
|
+
}
|
|
9045
|
+
ignite(vessel, "lighter");
|
|
9046
|
+
return true;
|
|
9047
|
+
};
|
|
9048
|
+
const fire = () => {
|
|
9049
|
+
if (!needRoom() || !room) return false;
|
|
9050
|
+
const vessel = withVessel();
|
|
9051
|
+
if (!vessel) return false;
|
|
9052
|
+
const base = vessel.root.getWorldPosition(new (T()).Vector3());
|
|
9053
|
+
const point = [base.x + FIRE_OFFSET[0], base.y + FIRE_OFFSET[1], base.z + FIRE_OFFSET[2]];
|
|
9054
|
+
const config = def.fire ?? { temperature: 700, radius: 1.4, seconds: 7 };
|
|
9055
|
+
vessel.firePoint = point;
|
|
9056
|
+
vessel.fireUntil = now() + config.seconds + 3;
|
|
9057
|
+
vessel.nextHeatLog = now();
|
|
9058
|
+
vessel.fireHandle = room.vfx.fire({ point, radius: FIRE_VISUAL_RADIUS, life: config.seconds + 3 });
|
|
9059
|
+
for (const spawned of room.spawned()) {
|
|
9060
|
+
if (spawned.itemId !== "camp_table") continue;
|
|
9061
|
+
const top = spawned.root.getWorldPosition(new (T()).Vector3());
|
|
9062
|
+
room.world.hit(spawned.objectId, { type: "heat", point: [top.x, top.y + 0.7, top.z], temperature: config.temperature + 200, radius: 0.9, duration: 3, source: "p13_fire" });
|
|
9063
|
+
}
|
|
9064
|
+
say(fillText2(log.fire_set, { temperature: num(config.temperature, 0), radius: num(config.radius, 1) }), { phase: "fire", objectId: vessel.objectId, temperatureC: config.temperature, radius: config.radius });
|
|
9065
|
+
return true;
|
|
9066
|
+
};
|
|
9067
|
+
const reset = () => {
|
|
9068
|
+
if (!needRoom() || !room) return false;
|
|
9069
|
+
room.openArea(EXPLOSIVES_AREA_ID);
|
|
9070
|
+
say(fillText2(log.reset, { scale: num(strength, 2) }), { phase: "reset", scale: strength });
|
|
9071
|
+
return true;
|
|
9072
|
+
};
|
|
9073
|
+
const actionIds = ["shoot", "valve", "light", "fire", "reset"];
|
|
9074
|
+
const actions = { shoot, valve, light, fire, reset };
|
|
9075
|
+
const sheet = () => {
|
|
9076
|
+
const energy = def.energy * strength / 1e3;
|
|
9077
|
+
return {
|
|
9078
|
+
kicker: text.sheet.kicker,
|
|
9079
|
+
title: text.sheet.title,
|
|
9080
|
+
body: text.sheet.body,
|
|
9081
|
+
facts: [
|
|
9082
|
+
{ label: text.sheet.facts.energy, value: `${num(energy, 0)} kJ (x${num(strength, 2)})` },
|
|
9083
|
+
{ label: text.sheet.facts.lethal, value: `${num(def.lethalRadius * strength, 1)} m` },
|
|
9084
|
+
{ label: text.sheet.facts.injury, value: `${num(def.injuryRadius * strength, 1)} m` },
|
|
9085
|
+
{ label: text.sheet.facts.gas, value: `${num(leakConfig?.liters ?? 0, 0)} l` },
|
|
9086
|
+
{ label: text.sheet.facts.mass, value: `${num(def.mass ?? 0, 0)} kg` },
|
|
9087
|
+
{ label: text.sheet.facts.torch, value: `${num(igniteDelay, 1)} s` },
|
|
9088
|
+
{ label: text.sheet.facts.shot_chain, value: `${num(typeof def.chain?.shot === "number" ? def.chain.shot : def.chain?.shot?.[0] ?? 0, 1)} s` },
|
|
9089
|
+
{ label: text.sheet.facts.fire_chain, value: `${num(chainFire, 1)} s` }
|
|
9090
|
+
],
|
|
9091
|
+
steps: text.sheet.steps,
|
|
9092
|
+
note: text.sheet.note,
|
|
9093
|
+
timeline: {
|
|
9094
|
+
segments: [
|
|
9095
|
+
{ name: text.sheet.segments.jet, start: 0, end: igniteDelay, color: "#e8eef2" },
|
|
9096
|
+
{ name: text.sheet.segments.torch, start: igniteDelay, end: chainFire, color: "#ff9a2e" },
|
|
9097
|
+
{ name: text.sheet.segments.blast, start: chainFire, end: chainFire + 1, color: "#ff4d2e" }
|
|
9098
|
+
],
|
|
9099
|
+
ticks: [0, igniteDelay, chainFire],
|
|
9100
|
+
metric: "clock"
|
|
9101
|
+
}
|
|
9102
|
+
};
|
|
9103
|
+
};
|
|
9104
|
+
const adopt = (data) => {
|
|
9105
|
+
const spawned = data;
|
|
9106
|
+
if (spawned.itemId !== `${WORLD_EXPLOSIVE_PREFIX}${def.id}`) return;
|
|
9107
|
+
const vessel = { objectId: spawned.objectId, root: spawned.root, scale: strength, leaks: [], valveRequested: false, valveOpen: false, lit: false, exploded: false, nextHeatLog: 0, torchFrame: 0 };
|
|
9108
|
+
vessels.set(vessel.objectId, vessel);
|
|
9109
|
+
if (wallParts.size === 0) for (const part of room?.world.state(WALL_ID)?.parts ?? []) wallParts.add(part.id);
|
|
9110
|
+
if (inArea && host) drawRings(vessel);
|
|
9111
|
+
};
|
|
9112
|
+
const change = (change2) => {
|
|
9113
|
+
if (change2.kind === "action") {
|
|
9114
|
+
if (actionIds.includes(change2.id)) actions[change2.id]();
|
|
9115
|
+
} else if (change2.kind === "layer" && change2.id === "rings") {
|
|
9116
|
+
ringsOn = change2.on;
|
|
9117
|
+
for (const vessel of vessels.values()) if (vessel.rings) vessel.rings.visible = ringsOn;
|
|
9118
|
+
} else if (change2.kind === "control" && change2.id === "blast_scale" && typeof change2.value === "number") {
|
|
9119
|
+
const first = initial;
|
|
9120
|
+
initial = false;
|
|
9121
|
+
if (change2.value === strength && !first) return;
|
|
9122
|
+
strength = change2.value;
|
|
9123
|
+
host?.bench?.sheet?.(sheet());
|
|
9124
|
+
if (first) return;
|
|
9125
|
+
say(fillText2(log.scale, { scale: num(strength, 2), energy: num(def.energy * strength / 1e3, 0), lethal: num(def.lethalRadius * strength, 1), injury: num(def.injuryRadius * strength, 1) }), { phase: "scale", scale: strength });
|
|
9126
|
+
if (inArea) reset();
|
|
9127
|
+
}
|
|
9128
|
+
};
|
|
9129
|
+
return {
|
|
9130
|
+
room,
|
|
9131
|
+
def,
|
|
9132
|
+
blastScale: () => strength,
|
|
9133
|
+
areaOpened: () => {
|
|
9134
|
+
inArea = true;
|
|
9135
|
+
},
|
|
9136
|
+
controls: () => [
|
|
9137
|
+
...actionIds.map((id) => ({ kind: "action", id, name: text.actions[id].name, hint: text.actions[id].hint })),
|
|
9138
|
+
{ kind: "number", id: "blast_scale", name: text.blastScale.name, hint: text.blastScale.hint, unit: "x", min: 0.25, max: 3, step: 0.05, default: options.blastScale }
|
|
9139
|
+
],
|
|
9140
|
+
layers: () => [{ id: "rings", name: text.layers.rings }],
|
|
9141
|
+
metrics: () => [
|
|
9142
|
+
{ id: "gas", name: text.metrics.gas, unit: "l", digits: 1, max: leakConfig?.liters ?? 24, read: () => liters(target() ?? { objectId: "" }) },
|
|
9143
|
+
{ id: "clock", name: text.metrics.clock, unit: "s", digits: 2, read: () => {
|
|
9144
|
+
const v = target();
|
|
9145
|
+
return v?.leakAt === void 0 ? 0 : (v.explodedAt ?? now()) - v.leakAt;
|
|
9146
|
+
} },
|
|
9147
|
+
{ id: "phase", name: text.metrics.phase, read: () => text.phases[phaseOf(target())] }
|
|
9148
|
+
],
|
|
9149
|
+
attach(lent) {
|
|
9150
|
+
host = lent;
|
|
9151
|
+
const offs = [];
|
|
9152
|
+
if (room) {
|
|
9153
|
+
offs.push(room.world.on("*", onEvent));
|
|
9154
|
+
offs.push(room.on("spawn", adopt));
|
|
9155
|
+
for (const spawned of room.spawned()) adopt(spawned);
|
|
9156
|
+
}
|
|
9157
|
+
offs.push(
|
|
9158
|
+
lent.on((event) => {
|
|
9159
|
+
if (event.type === "explosion") lastBlast = { point: event.point, energy: event.energy, radius: event.radius, cause: event.cause, ...event.explosive === void 0 ? {} : { explosive: event.explosive } };
|
|
9160
|
+
})
|
|
9161
|
+
);
|
|
9162
|
+
offs.push(lent.onFrame(frame));
|
|
9163
|
+
offs.push(lent.bench?.onChange(change) ?? (() => void 0));
|
|
9164
|
+
offs.push(
|
|
9165
|
+
lent.onClear?.(() => {
|
|
9166
|
+
for (const vessel of vessels.values()) clearRings(vessel);
|
|
9167
|
+
vessels.clear();
|
|
9168
|
+
clearShells();
|
|
9169
|
+
wallParts.clear();
|
|
9170
|
+
aftermathDue = [];
|
|
9171
|
+
lastBlast = void 0;
|
|
9172
|
+
inArea = false;
|
|
9173
|
+
}) ?? (() => void 0)
|
|
9174
|
+
);
|
|
9175
|
+
lent.bench?.sheet?.(sheet());
|
|
9176
|
+
return () => {
|
|
9177
|
+
for (const off of offs) off();
|
|
9178
|
+
for (const vessel of vessels.values()) clearRings(vessel);
|
|
9179
|
+
vessels.clear();
|
|
9180
|
+
clearShells();
|
|
9181
|
+
host = null;
|
|
9182
|
+
};
|
|
9183
|
+
},
|
|
9184
|
+
actions,
|
|
9185
|
+
vessel: () => {
|
|
9186
|
+
const v = target();
|
|
9187
|
+
if (!v) return void 0;
|
|
9188
|
+
return { objectId: v.objectId, leakAt: v.leakAt, litAt: v.litAt, explodedAt: v.explodedAt, cause: v.cause, valveOpen: v.valveOpen, lit: v.lit, exploded: v.exploded, point: v.leaks[v.leaks.length - 1]?.point };
|
|
9189
|
+
},
|
|
9190
|
+
lastBlast: () => lastBlast,
|
|
9191
|
+
witnesses
|
|
9192
|
+
};
|
|
9193
|
+
}
|
|
9194
|
+
|
|
9195
|
+
// src/explosives-scenarios.ts
|
|
9196
|
+
var TORCH_WINDOW = [0.7, 1];
|
|
9197
|
+
var BLAST_WITHIN = 1.6;
|
|
9198
|
+
var CYLINDER_ITEM = `${WORLD_EXPLOSIVE_PREFIX}propane_p13`;
|
|
9199
|
+
var PUSH_KINDS = [
|
|
9200
|
+
{ kind: "can", item: "item_canned_beans" },
|
|
9201
|
+
{ kind: "bottle", item: "item_water_bottle" },
|
|
9202
|
+
{ kind: "chair", item: "dining_chair" }
|
|
9203
|
+
];
|
|
9204
|
+
var PUSH_READ_AT = 0.8;
|
|
9205
|
+
var MOVED = 0.15;
|
|
9206
|
+
var HEAVY_ITEMS = ["camp_table"];
|
|
9207
|
+
var flat = (a, b) => Math.hypot(a[0] - b[0], a[2] - b[2]);
|
|
9208
|
+
var bearing = (centre, distance, degrees) => [centre[0] + Math.sin(degrees * Math.PI / 180) * distance, 0, centre[2] + Math.cos(degrees * Math.PI / 180) * distance];
|
|
9209
|
+
var PUSH_LAYOUT = {
|
|
9210
|
+
can: [[2.5, -52], [5, 12], [8, 78]],
|
|
9211
|
+
bottle: [[2.5, 26], [5, -62], [8, -78]],
|
|
9212
|
+
chair: [[2.5, -18], [5, 60], [8, -42]]
|
|
9213
|
+
};
|
|
9214
|
+
function pushScenario(all, bench, mode) {
|
|
9215
|
+
const text = all.scenarios;
|
|
9216
|
+
const things = all.things;
|
|
9217
|
+
return {
|
|
9218
|
+
id: `explosives_push_${mode}`,
|
|
9219
|
+
name: mode === "p13" ? text.push_p13 : text.push_grenade,
|
|
9220
|
+
run: ({ room, step, expect }) => {
|
|
9221
|
+
let centre;
|
|
9222
|
+
if (mode === "p13") {
|
|
9223
|
+
room.openArea(EXPLOSIVES_AREA_ID);
|
|
9224
|
+
const cylinder = room.spawned().find((spawned) => spawned.itemId === CYLINDER_ITEM);
|
|
9225
|
+
const at = cylinder?.root?.position;
|
|
9226
|
+
centre = [at?.x ?? 0, 0, at?.z ?? -10.66];
|
|
9227
|
+
} else {
|
|
9228
|
+
room.clear();
|
|
9229
|
+
centre = [0, 0, -6];
|
|
9230
|
+
}
|
|
9231
|
+
const placed = [];
|
|
9232
|
+
const lay = (kind, item, at) => {
|
|
9233
|
+
try {
|
|
9234
|
+
const spawned = room.spawn(item, at, 0);
|
|
9235
|
+
placed.push({ kind, objectId: spawned.objectId, itemId: item, from: at, root: spawned.root });
|
|
9236
|
+
} catch {
|
|
9237
|
+
}
|
|
9238
|
+
};
|
|
9239
|
+
for (const { kind, item } of PUSH_KINDS) PUSH_LAYOUT[kind].forEach(([distance, degrees]) => lay(kind, item, bearing(centre, distance, degrees)));
|
|
9240
|
+
if (mode === "grenade") {
|
|
9241
|
+
lay("zombie", "zombie_walker", bearing(centre, 3.4, -34));
|
|
9242
|
+
lay("survivor", "person_police_officer", bearing(centre, 7.5, 78));
|
|
9243
|
+
lay("camp_table", "camp_table", bearing(centre, 3, 90));
|
|
9244
|
+
}
|
|
9245
|
+
step(0.6);
|
|
9246
|
+
const start = (body) => [body.root.position.x, body.root.position.y, body.root.position.z];
|
|
9247
|
+
const origin = new Map(placed.map((body) => [body.objectId, start(body)]));
|
|
9248
|
+
const partsOf = (objectId) => room.world.state(objectId)?.parts.length ?? 0;
|
|
9249
|
+
for (const spawned of room.spawned()) {
|
|
9250
|
+
const known = placed.some((body2) => body2.objectId === spawned.objectId);
|
|
9251
|
+
const role = spawned.itemId.startsWith("zombie_") ? "zombie" : spawned.itemId.startsWith("person_") ? "survivor" : HEAVY_ITEMS.includes(spawned.itemId) ? spawned.itemId : void 0;
|
|
9252
|
+
if (known || !role || mode !== "p13") continue;
|
|
9253
|
+
const root = spawned.root;
|
|
9254
|
+
const body = { kind: role, objectId: spawned.objectId, itemId: spawned.itemId, from: [root.position.x, root.position.y, root.position.z], root };
|
|
9255
|
+
placed.push(body);
|
|
9256
|
+
origin.set(body.objectId, body.from);
|
|
9257
|
+
}
|
|
9258
|
+
const partsBefore = new Map(placed.map((body) => [body.objectId, partsOf(body.objectId)]));
|
|
9259
|
+
expect(placed.length > 0, fillText2(text.push_found, { n: placed.length, kinds: [...new Set(placed.map((body) => body.kind))].join(", ") }), { placed: placed.map((body) => body.itemId) });
|
|
9260
|
+
if (mode === "p13") bench.actions.shoot();
|
|
9261
|
+
else {
|
|
9262
|
+
room.setTool("frag_grenade");
|
|
9263
|
+
step(0.5);
|
|
9264
|
+
room.look([0, 1.6, 9.5], [0, 1, -2.5]);
|
|
9265
|
+
room.use();
|
|
9266
|
+
}
|
|
9267
|
+
let waited = 0;
|
|
9268
|
+
while (!bench.lastBlast() && waited < 9) {
|
|
9269
|
+
step(0.05);
|
|
9270
|
+
waited += 0.05;
|
|
9271
|
+
}
|
|
9272
|
+
const blast = bench.lastBlast();
|
|
9273
|
+
expect(!!blast, text.push_told, { waited });
|
|
9274
|
+
if (!blast) return;
|
|
9275
|
+
step(PUSH_READ_AT);
|
|
9276
|
+
const rows = placed.map((body) => {
|
|
9277
|
+
const from = origin.get(body.objectId) ?? body.from;
|
|
9278
|
+
const now = start(body);
|
|
9279
|
+
const distance = flat(from, blast.point);
|
|
9280
|
+
const away = Math.max(1e-6, distance);
|
|
9281
|
+
const outward = ((now[0] - from[0]) * (from[0] - blast.point[0]) + (now[2] - from[2]) * (from[2] - blast.point[2])) / away;
|
|
9282
|
+
return { ...body, distance, outward, moved: flat(now, from) };
|
|
9283
|
+
});
|
|
9284
|
+
const torn = (row) => PUSH_KINDS.some((entry) => entry.kind === row.kind) && partsOf(row.objectId) < (partsBefore.get(row.objectId) ?? 0);
|
|
9285
|
+
const reached = rows.filter((row) => row.distance < blast.radius * 0.85 && !torn(row));
|
|
9286
|
+
const movedAway = reached.filter((row) => row.outward >= MOVED);
|
|
9287
|
+
const least = reached.length === 0 ? 0 : Math.min(...reached.map((row) => row.outward));
|
|
9288
|
+
expect(reached.length > 0 && movedAway.length === reached.length, fillText2(text.push_outward, { moved: movedAway.length, n: reached.length, least: least.toFixed(2) }), { rows: reached.map((row) => ({ kind: row.kind, distance: row.distance, outward: row.outward })), torn: rows.filter(torn).map((row) => row.objectId) });
|
|
9289
|
+
for (const { kind } of PUSH_KINDS) {
|
|
9290
|
+
const ofKind = reached.filter((row) => row.kind === kind).sort((a, b) => a.distance - b.distance);
|
|
9291
|
+
if (ofKind.length < 2) continue;
|
|
9292
|
+
const near = ofKind[0];
|
|
9293
|
+
const far = ofKind[ofKind.length - 1];
|
|
9294
|
+
expect(near.outward > far.outward, fillText2(text.push_falloff, { what: things[kind] ?? kind, near: near.outward.toFixed(2), nearDistance: near.distance.toFixed(1), far: far.outward.toFixed(2), farDistance: far.distance.toFixed(1) }), { near: near.outward, far: far.outward });
|
|
9295
|
+
}
|
|
9296
|
+
for (const row of reached.filter((entry) => entry.kind === "zombie" || entry.kind === "survivor")) {
|
|
9297
|
+
expect(row.outward >= MOVED, fillText2(text.push_person, { who: things[row.kind] ?? row.kind, distance: row.distance.toFixed(1), moved: row.outward.toFixed(2) }), { row });
|
|
9298
|
+
}
|
|
9299
|
+
const table = reached.find((row) => HEAVY_ITEMS.includes(row.kind));
|
|
9300
|
+
if (table) expect(table.moved >= MOVED, fillText2(text.push_heavy, { what: things[table.itemId] ?? table.itemId, moved: table.moved.toFixed(2) }), { table });
|
|
9301
|
+
}
|
|
9302
|
+
};
|
|
9303
|
+
}
|
|
9304
|
+
function explosivesScenarios(locale, bench) {
|
|
9305
|
+
const text = explosivesTextOf(locale).scenarios;
|
|
9306
|
+
const record = (room) => {
|
|
9307
|
+
const events = [];
|
|
9308
|
+
const stop = room.world.on("*", (event) => {
|
|
9309
|
+
if (event.object.startsWith(CYLINDER_ITEM)) events.push({ type: event.type, time: event.time, object: event.object });
|
|
9310
|
+
});
|
|
9311
|
+
return { events, stop };
|
|
9312
|
+
};
|
|
9313
|
+
const first = (events, type, after = -Infinity) => events.find((event) => event.type === type && event.time >= after);
|
|
9314
|
+
return [
|
|
9315
|
+
{
|
|
9316
|
+
id: "explosives_p13_shot",
|
|
9317
|
+
name: text.shot,
|
|
9318
|
+
run: ({ room, step, expect }) => {
|
|
9319
|
+
room.openArea(EXPLOSIVES_AREA_ID);
|
|
9320
|
+
const { events, stop } = record(room);
|
|
9321
|
+
bench.actions.shoot();
|
|
9322
|
+
step(2.4);
|
|
9323
|
+
stop();
|
|
9324
|
+
const leak = first(events, "leak");
|
|
9325
|
+
const explode = first(events, "explode");
|
|
9326
|
+
const view = bench.vessel();
|
|
9327
|
+
expect(!!leak, text.shot_leak, { events: events.map((event) => event.type) });
|
|
9328
|
+
const torchAfter = view?.litAt !== void 0 && view.leakAt !== void 0 ? view.litAt - view.leakAt : -1;
|
|
9329
|
+
expect(torchAfter >= TORCH_WINDOW[0] && torchAfter <= TORCH_WINDOW[1] + 0.05, `${text.shot_ignite} (${torchAfter.toFixed(2)} s)`, { torchAfter });
|
|
9330
|
+
const blastAfter = leak && explode ? explode.time - leak.time : -1;
|
|
9331
|
+
expect(blastAfter > 0 && blastAfter <= BLAST_WITHIN, `${text.shot_explode} (${blastAfter.toFixed(2)} s)`, { blastAfter });
|
|
9332
|
+
const witnesses = bench.witnesses();
|
|
9333
|
+
const zombie = witnesses.find((w) => w.itemId.startsWith("zombie_"));
|
|
9334
|
+
expect(!!zombie && (zombie.alive === false || (zombie.vitality ?? 1) < 0.5), `${text.shot_zombie} (${zombie?.distance.toFixed(1)} m)`, { zombie });
|
|
9335
|
+
const survivor = witnesses.find((w) => w.kind === "person" && !w.itemId.startsWith("zombie_"));
|
|
9336
|
+
expect(!!survivor && survivor.alive === true && (survivor.vitality ?? 1) < 1, `${text.shot_survivor} (${survivor?.distance.toFixed(1)} m, ${survivor?.alive ? "alive" : "dead"})`, { survivor });
|
|
9337
|
+
const wall = witnesses.find((w) => w.kind === "wall");
|
|
9338
|
+
expect((wall?.marks ?? 0) + (wall?.broken ?? 0) + ((wall?.pieces ?? 1) - 1) > 0, text.shot_wall, { wall });
|
|
9339
|
+
}
|
|
9340
|
+
},
|
|
9341
|
+
{
|
|
9342
|
+
id: "explosives_p13_valve",
|
|
9343
|
+
name: text.valve,
|
|
9344
|
+
run: ({ room, step, expect }) => {
|
|
9345
|
+
room.openArea(EXPLOSIVES_AREA_ID);
|
|
9346
|
+
const { events, stop } = record(room);
|
|
9347
|
+
bench.actions.valve();
|
|
9348
|
+
step(4);
|
|
9349
|
+
expect(!!first(events, "leak"), text.valve_leak);
|
|
9350
|
+
expect(!first(events, "primed") && !first(events, "explode"), text.valve_quiet, { events: events.map((event) => event.type) });
|
|
9351
|
+
const before = room.world.time;
|
|
9352
|
+
bench.actions.light();
|
|
9353
|
+
step(2.2);
|
|
9354
|
+
stop();
|
|
9355
|
+
expect(bench.vessel()?.lit === true || bench.vessel()?.litAt !== void 0, text.valve_lit);
|
|
9356
|
+
const explode = first(events, "explode", before);
|
|
9357
|
+
expect(!!explode && explode.time - before <= BLAST_WITHIN, `${text.valve_explode} (${explode ? (explode.time - before).toFixed(2) : "no blast"} s)`, { after: explode ? explode.time - before : null });
|
|
9358
|
+
}
|
|
9359
|
+
},
|
|
9360
|
+
{
|
|
9361
|
+
id: "explosives_p13_fire",
|
|
9362
|
+
name: text.fire,
|
|
9363
|
+
run: ({ room, step, expect }) => {
|
|
9364
|
+
room.openArea(EXPLOSIVES_AREA_ID);
|
|
9365
|
+
const { events, stop } = record(room);
|
|
9366
|
+
bench.actions.fire();
|
|
9367
|
+
step(8);
|
|
9368
|
+
stop();
|
|
9369
|
+
expect(!!first(events, "explode"), text.fire_explode, { events: events.map((event) => event.type) });
|
|
9370
|
+
}
|
|
9371
|
+
},
|
|
9372
|
+
{
|
|
9373
|
+
id: "explosives_p13_chain",
|
|
9374
|
+
name: text.chain,
|
|
9375
|
+
run: ({ room, step, expect }) => {
|
|
9376
|
+
room.openArea(EXPLOSIVES_AREA_ID);
|
|
9377
|
+
const near = [1, 0, -10];
|
|
9378
|
+
const second = room.spawn(CYLINDER_ITEM, near, 0);
|
|
9379
|
+
const { events, stop } = record(room);
|
|
9380
|
+
bench.actions.shoot();
|
|
9381
|
+
step(4);
|
|
9382
|
+
stop();
|
|
9383
|
+
const blasts = events.filter((event) => event.type === "explode");
|
|
9384
|
+
const other = blasts.find((event) => event.object === second.objectId);
|
|
9385
|
+
expect(blasts.length >= 2 && !!other, `${text.chain_explode} (${blasts.length} blasts)`, { blasts: blasts.map((event) => `${event.object}@${event.time.toFixed(2)}`) });
|
|
9386
|
+
}
|
|
9387
|
+
},
|
|
9388
|
+
pushScenario(explosivesTextOf(locale), bench, "p13"),
|
|
9389
|
+
pushScenario(explosivesTextOf(locale), bench, "grenade")
|
|
9390
|
+
];
|
|
9391
|
+
}
|
|
9392
|
+
|
|
9393
|
+
// src/explosives-area.ts
|
|
9394
|
+
var DEFAULT_EXPLOSIVES_SCENE = {
|
|
9395
|
+
furniture: [
|
|
9396
|
+
{ item: "fridge", at: [-1.65, 0, -10.55] },
|
|
9397
|
+
{ item: "kitchen_cabinet_base", at: [-0.78, 0, -10.6] },
|
|
9398
|
+
{ item: "stove", at: [0.72, 0, -10.58] },
|
|
9399
|
+
{ item: "camp_table", at: [2.4, 0, -10.2] }
|
|
9400
|
+
],
|
|
9401
|
+
cylinder: [0.02, 0, -10.66],
|
|
9402
|
+
zombie: { item: "zombie_walker", at: [-1.6, 0, -8.1] },
|
|
9403
|
+
survivor: { item: "person_police_officer", at: [6, 0, -5.3] },
|
|
9404
|
+
eye: [0, 1.7, -4.7]
|
|
9405
|
+
};
|
|
9406
|
+
var EXPLOSIVE_ID = "propane_p13";
|
|
9407
|
+
var AIM_HEIGHT2 = 0.25;
|
|
9408
|
+
function createExplosivesTestModule(options = {}) {
|
|
9409
|
+
const locale = options.locale ?? "pt-BR";
|
|
9410
|
+
const arsenal = options.arsenal ?? createArsenal2({ locale });
|
|
9411
|
+
const text = explosivesTextOf(locale);
|
|
9412
|
+
const scene = { ...DEFAULT_EXPLOSIVES_SCENE, ...options.scene };
|
|
9413
|
+
const def = arsenal.explosives.get(EXPLOSIVE_ID);
|
|
9414
|
+
if (!def) throw new Error(`explosives area: the arsenal has no "${EXPLOSIVE_ID}"`);
|
|
9415
|
+
const bench = createExplosivesBench({ locale, text, def, scene, room: options.room, blastScale: options.blastScale ?? 1, profile: EXPLOSIONS[EXPLOSIVE_ID] });
|
|
9416
|
+
return {
|
|
9417
|
+
id: EXPLOSIVES_AREA_ID,
|
|
9418
|
+
name: text.name,
|
|
9419
|
+
group: text.group,
|
|
9420
|
+
bench,
|
|
9421
|
+
// the cylinder of this area is the world explosive of the weapons module with the bench's strength: the same id, the same model, the same part
|
|
9422
|
+
items: () => {
|
|
9423
|
+
const item = worldExplosiveItem(def, text.cylinder, { scale: () => bench.blastScale() });
|
|
9424
|
+
return item ? [item] : [];
|
|
9425
|
+
},
|
|
9426
|
+
area: () => {
|
|
9427
|
+
bench.areaOpened();
|
|
9428
|
+
const placements = [
|
|
9429
|
+
...scene.furniture.map((piece) => ({ item: piece.item, position: piece.at, ...piece.rotationY === void 0 ? {} : { rotationY: piece.rotationY } })),
|
|
9430
|
+
{ item: `${WORLD_EXPLOSIVE_PREFIX}${EXPLOSIVE_ID}`, position: scene.cylinder },
|
|
9431
|
+
// facing the cylinder (the front of an item looks to +z, the cylinder is at -z)
|
|
9432
|
+
{ item: scene.zombie.item, position: scene.zombie.at, rotationY: Math.PI },
|
|
9433
|
+
{ item: scene.survivor.item, position: scene.survivor.at, rotationY: Math.PI }
|
|
9434
|
+
];
|
|
9435
|
+
const centre = [scene.cylinder[0], AIM_HEIGHT2, scene.cylinder[2]];
|
|
9436
|
+
return { placements, view: { eye: scene.eye, target: centre } };
|
|
9437
|
+
},
|
|
9438
|
+
controls: () => bench.controls(),
|
|
9439
|
+
layers: () => bench.layers(),
|
|
9440
|
+
metrics: () => bench.metrics(),
|
|
9441
|
+
scenarios: (scenarioLocale) => explosivesScenarios(scenarioLocale ?? locale, bench),
|
|
9442
|
+
attach: (host) => bench.attach(host)
|
|
9443
|
+
};
|
|
9444
|
+
}
|
|
9445
|
+
|
|
9446
|
+
// src/projectile-model.ts
|
|
9447
|
+
function projectileModel(THREE, query) {
|
|
9448
|
+
if (query.kind !== "rocket") return null;
|
|
9449
|
+
const launcher = buildWeaponModelById(THREE, query.weapon);
|
|
9450
|
+
if (!launcher || launcher.parts.family !== "rocket") return null;
|
|
9451
|
+
const round2 = launcher.parts.makeRound ? launcher.parts.makeRound() : buildFlyingRocket(modelKit(THREE), launcher.parts.launcherType);
|
|
9452
|
+
return { model: round2, axis: [1, 0, 0] };
|
|
9453
|
+
}
|
|
6938
9454
|
export {
|
|
6939
9455
|
ARCHETYPE_MODELS,
|
|
6940
9456
|
ATTACHMENT_MODELS,
|
|
@@ -6942,12 +9458,16 @@ export {
|
|
|
6942
9458
|
CLASS_REFERENCE_ENERGY,
|
|
6943
9459
|
COUNTER_ITEM_ID,
|
|
6944
9460
|
DEFAULT_ENERGY_CURVE,
|
|
9461
|
+
DEFAULT_EXPLOSIVES_SCENE,
|
|
6945
9462
|
DEFAULT_THUMBNAIL_BASE,
|
|
9463
|
+
EXPLOSIVES_AREA_ID,
|
|
9464
|
+
EXPLOSIVES_TEXT,
|
|
6946
9465
|
GAME_DISTANCE_CURVE,
|
|
6947
9466
|
GAME_SOUND_CLASSES,
|
|
6948
9467
|
MECHANICAL_SOUNDS,
|
|
6949
9468
|
MODEL_ENTRIES,
|
|
6950
9469
|
MOUNTED_M134_ITEM_ID,
|
|
9470
|
+
P13_VALVE_MESH,
|
|
6951
9471
|
WEAPON_ITEM_PREFIX,
|
|
6952
9472
|
WEAPON_THUMBNAILS,
|
|
6953
9473
|
WORLD_EFFECTS,
|
|
@@ -6963,23 +9483,28 @@ export {
|
|
|
6963
9483
|
buildWeaponModelById,
|
|
6964
9484
|
cartridgeShape,
|
|
6965
9485
|
computeShotSound,
|
|
9486
|
+
createExplosivesTestModule,
|
|
6966
9487
|
createWeaponAudio,
|
|
6967
9488
|
createWeaponFx,
|
|
6968
9489
|
createWeaponsTestModule,
|
|
6969
9490
|
createWorldEffects,
|
|
6970
9491
|
deriveFrames,
|
|
9492
|
+
explosivesTextOf,
|
|
6971
9493
|
fitAttachmentModels,
|
|
6972
9494
|
hasWeaponModel,
|
|
6973
9495
|
mechanicalSoundOf,
|
|
6974
9496
|
modelKit,
|
|
6975
9497
|
mountFrame,
|
|
6976
9498
|
partRegistry,
|
|
9499
|
+
projectileModel,
|
|
6977
9500
|
rigStock,
|
|
6978
9501
|
rigUnderbarrel,
|
|
6979
9502
|
weaponProp,
|
|
6980
9503
|
weaponThumbnailCredit,
|
|
6981
9504
|
weaponThumbnailUrl,
|
|
6982
9505
|
weaponsLayout,
|
|
6983
|
-
worldExplosiveItem
|
|
9506
|
+
worldExplosiveItem,
|
|
9507
|
+
worldExplosiveShell,
|
|
9508
|
+
worldExplosiveValve
|
|
6984
9509
|
};
|
|
6985
9510
|
//# sourceMappingURL=index.js.map
|