@adatechnology/game-weapons-three 0.7.2 → 0.9.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 +37 -0
- package/assets/weapon-svg/generated/propane_p13.svg +1 -1
- package/dist/index.d.ts +410 -6
- package/dist/index.global.js +34167 -3545
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +2711 -97
- package/dist/index.js.map +1 -1
- package/package.json +12 -10
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// src/area.ts
|
|
2
|
+
import { markKitOrigin as markKitOrigin4 } from "@adatechnology/game-contracts";
|
|
2
3
|
import { createArsenal } from "@adatechnology/game-weapons";
|
|
4
|
+
import { furnitureSpec } from "@adatechnology/game-floorplan-three";
|
|
3
5
|
|
|
4
6
|
// src/attachments-key.ts
|
|
5
7
|
var attachmentsKey = (parts) => {
|
|
@@ -219,7 +221,40 @@ var ACCESSORY_TEXT = {
|
|
|
219
221
|
metrics: { mass: "Weapon mass", aim: "Time to the sights", fitted: "Accessories on the weapon", suppressed: "Suppressed" },
|
|
220
222
|
yes: "yes",
|
|
221
223
|
no: "no",
|
|
222
|
-
attachments: {
|
|
224
|
+
attachments: {
|
|
225
|
+
suppressor: "Suppressor",
|
|
226
|
+
oil_filter_suppressor: "Improvised oil filter suppressor",
|
|
227
|
+
integral_suppressor: "Integral suppressor",
|
|
228
|
+
muzzle_brake: "Muzzle brake",
|
|
229
|
+
muzzle_brake_heavy: "Heavy muzzle brake",
|
|
230
|
+
compensator: "Compensator",
|
|
231
|
+
red_dot: "Red dot sight",
|
|
232
|
+
holographic_sight: "Holographic sight",
|
|
233
|
+
acog_4x: "ACOG 4x",
|
|
234
|
+
scope_3_9x: "Rifle scope 3-9x",
|
|
235
|
+
scope_6_24x: "Rifle scope 6-24x",
|
|
236
|
+
night_vision_scope: "Night vision scope",
|
|
237
|
+
thermal_scope: "Thermal scope",
|
|
238
|
+
integral_optic_1_5x: "Integral 1.5x optic of the AUG",
|
|
239
|
+
integral_optic_3x: "Integral 3x optic of the G36",
|
|
240
|
+
pso_1: "PSO-1 scope",
|
|
241
|
+
fixed_scope_10x: "Fixed 10x scope of the M24",
|
|
242
|
+
vertical_grip: "Vertical foregrip",
|
|
243
|
+
bipod: "Bipod",
|
|
244
|
+
m203: "M203 grenade launcher",
|
|
245
|
+
gp_25: "GP-25 grenade launcher",
|
|
246
|
+
ag36: "AG36 grenade launcher",
|
|
247
|
+
weapon_light: "Weapon light",
|
|
248
|
+
laser_sight: "Laser sight",
|
|
249
|
+
extended_magazine: "Extended magazine",
|
|
250
|
+
drum_magazine: "Drum magazine",
|
|
251
|
+
dual_coupler: "Dual magazine coupler",
|
|
252
|
+
sawn_off_barrel: "Sawn-off barrel",
|
|
253
|
+
long_barrel: "Long barrel",
|
|
254
|
+
sling: "Sling",
|
|
255
|
+
ammo_backpack: "Ammunition backpack",
|
|
256
|
+
tripod: "Tripod mount"
|
|
257
|
+
},
|
|
223
258
|
fitRefused: "Could not change {mount} of {weapon}: {reason}",
|
|
224
259
|
fitting: "Fitting {attachment} on {weapon} ({seconds} s)",
|
|
225
260
|
removing: "Taking {attachment} off {weapon} ({seconds} s)",
|
|
@@ -598,6 +633,731 @@ function weaponRecipes(curve) {
|
|
|
598
633
|
];
|
|
599
634
|
}
|
|
600
635
|
|
|
636
|
+
// src/data/reload-sounds.json
|
|
637
|
+
var reload_sounds_default = {
|
|
638
|
+
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.",
|
|
639
|
+
profiles: {
|
|
640
|
+
weight: {
|
|
641
|
+
light: {
|
|
642
|
+
pitch: 1.3,
|
|
643
|
+
gain: 0.85
|
|
644
|
+
},
|
|
645
|
+
medium: {
|
|
646
|
+
pitch: 1,
|
|
647
|
+
gain: 1
|
|
648
|
+
},
|
|
649
|
+
heavy: {
|
|
650
|
+
pitch: 0.78,
|
|
651
|
+
gain: 1.2
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
material: {
|
|
655
|
+
steel: {
|
|
656
|
+
pitch: 1,
|
|
657
|
+
gain: 1
|
|
658
|
+
},
|
|
659
|
+
polymer: {
|
|
660
|
+
pitch: 0.82,
|
|
661
|
+
gain: 0.8
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
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`.",
|
|
666
|
+
weights: {
|
|
667
|
+
pistol: "light",
|
|
668
|
+
revolver: "light",
|
|
669
|
+
rifle: "medium",
|
|
670
|
+
semiint: "medium",
|
|
671
|
+
semi12: "heavy",
|
|
672
|
+
p90: "medium",
|
|
673
|
+
belt: "heavy",
|
|
674
|
+
pump: "heavy",
|
|
675
|
+
lever: "medium",
|
|
676
|
+
bolt: "medium",
|
|
677
|
+
boltmag: "medium",
|
|
678
|
+
break: "heavy",
|
|
679
|
+
rocket: "heavy",
|
|
680
|
+
flame: "heavy"
|
|
681
|
+
},
|
|
682
|
+
polymer: [
|
|
683
|
+
"glock_17",
|
|
684
|
+
"glock_18",
|
|
685
|
+
"glock_19",
|
|
686
|
+
"taurus_g2c",
|
|
687
|
+
"taurus_ts9",
|
|
688
|
+
"fn_five_seven",
|
|
689
|
+
"pistol_9mm",
|
|
690
|
+
"fn_p90",
|
|
691
|
+
"hk_ump_45"
|
|
692
|
+
],
|
|
693
|
+
cues: {
|
|
694
|
+
mag_release: [
|
|
695
|
+
{
|
|
696
|
+
layer: "scene_tone",
|
|
697
|
+
at: 0,
|
|
698
|
+
wave: "square",
|
|
699
|
+
f: 2400,
|
|
700
|
+
f2: 1680,
|
|
701
|
+
d: 0.02,
|
|
702
|
+
vol: 0.3
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
layer: "scene_noise",
|
|
706
|
+
at: 0,
|
|
707
|
+
type: "highpass",
|
|
708
|
+
f: 3e3,
|
|
709
|
+
q: 0.7,
|
|
710
|
+
d: 0.03,
|
|
711
|
+
vol: 0.24
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
layer: "thud",
|
|
715
|
+
at: 0.02,
|
|
716
|
+
gain: 0.12,
|
|
717
|
+
frequency: 700
|
|
718
|
+
}
|
|
719
|
+
],
|
|
720
|
+
mag_out: [
|
|
721
|
+
{
|
|
722
|
+
layer: "scene_noise",
|
|
723
|
+
at: 0,
|
|
724
|
+
type: "bandpass",
|
|
725
|
+
f: 900,
|
|
726
|
+
q: 2,
|
|
727
|
+
d: 0.1,
|
|
728
|
+
vol: 0.3,
|
|
729
|
+
f2: 600
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
layer: "metal",
|
|
733
|
+
at: 0.08,
|
|
734
|
+
frequency: 1200,
|
|
735
|
+
gain: 0.2
|
|
736
|
+
}
|
|
737
|
+
],
|
|
738
|
+
mag_insert: [
|
|
739
|
+
{
|
|
740
|
+
layer: "scene_noise",
|
|
741
|
+
at: 0,
|
|
742
|
+
type: "bandpass",
|
|
743
|
+
f: 700,
|
|
744
|
+
q: 2,
|
|
745
|
+
d: 0.12,
|
|
746
|
+
vol: 0.3,
|
|
747
|
+
f2: 1100
|
|
748
|
+
}
|
|
749
|
+
],
|
|
750
|
+
mag_seat: [
|
|
751
|
+
{
|
|
752
|
+
layer: "scene_tone",
|
|
753
|
+
at: 0,
|
|
754
|
+
wave: "square",
|
|
755
|
+
f: 1100,
|
|
756
|
+
f2: 770,
|
|
757
|
+
d: 0.02,
|
|
758
|
+
vol: 0.5
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
layer: "scene_noise",
|
|
762
|
+
at: 0,
|
|
763
|
+
type: "highpass",
|
|
764
|
+
f: 3e3,
|
|
765
|
+
q: 0.7,
|
|
766
|
+
d: 0.03,
|
|
767
|
+
vol: 0.4
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
layer: "scene_tone",
|
|
771
|
+
at: 0,
|
|
772
|
+
wave: "sine",
|
|
773
|
+
f: 320,
|
|
774
|
+
f2: 200,
|
|
775
|
+
d: 0.05,
|
|
776
|
+
vol: 0.35
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
layer: "thud",
|
|
780
|
+
at: 0.01,
|
|
781
|
+
gain: 0.18,
|
|
782
|
+
frequency: 600
|
|
783
|
+
}
|
|
784
|
+
],
|
|
785
|
+
slide_release: [
|
|
786
|
+
{
|
|
787
|
+
layer: "metal",
|
|
788
|
+
at: 0,
|
|
789
|
+
frequency: 900,
|
|
790
|
+
gain: 0.35
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
layer: "scene_tone",
|
|
794
|
+
at: 0.02,
|
|
795
|
+
wave: "square",
|
|
796
|
+
f: 2e3,
|
|
797
|
+
f2: 1400,
|
|
798
|
+
d: 0.02,
|
|
799
|
+
vol: 0.4
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
layer: "scene_noise",
|
|
803
|
+
at: 0.02,
|
|
804
|
+
type: "highpass",
|
|
805
|
+
f: 3e3,
|
|
806
|
+
q: 0.7,
|
|
807
|
+
d: 0.03,
|
|
808
|
+
vol: 0.32
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
layer: "thud",
|
|
812
|
+
at: 0.03,
|
|
813
|
+
gain: 0.25,
|
|
814
|
+
frequency: 500
|
|
815
|
+
}
|
|
816
|
+
],
|
|
817
|
+
charging_handle_pull: [
|
|
818
|
+
{
|
|
819
|
+
layer: "scene_noise",
|
|
820
|
+
at: 0,
|
|
821
|
+
type: "bandpass",
|
|
822
|
+
f: 1500,
|
|
823
|
+
q: 3,
|
|
824
|
+
d: 0.09,
|
|
825
|
+
vol: 0.3,
|
|
826
|
+
f2: 900
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
layer: "metal",
|
|
830
|
+
at: 0.06,
|
|
831
|
+
frequency: 1300,
|
|
832
|
+
gain: 0.25
|
|
833
|
+
}
|
|
834
|
+
],
|
|
835
|
+
charging_handle_release: [
|
|
836
|
+
{
|
|
837
|
+
layer: "scene_tone",
|
|
838
|
+
at: 0,
|
|
839
|
+
wave: "square",
|
|
840
|
+
f: 2200,
|
|
841
|
+
f2: 1540,
|
|
842
|
+
d: 0.02,
|
|
843
|
+
vol: 0.4
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
layer: "scene_noise",
|
|
847
|
+
at: 0,
|
|
848
|
+
type: "highpass",
|
|
849
|
+
f: 3e3,
|
|
850
|
+
q: 0.7,
|
|
851
|
+
d: 0.03,
|
|
852
|
+
vol: 0.32
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
layer: "scene_tone",
|
|
856
|
+
at: 0.02,
|
|
857
|
+
wave: "square",
|
|
858
|
+
f: 2700,
|
|
859
|
+
f2: 1890,
|
|
860
|
+
d: 0.02,
|
|
861
|
+
vol: 0.4
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
layer: "scene_noise",
|
|
865
|
+
at: 0.02,
|
|
866
|
+
type: "highpass",
|
|
867
|
+
f: 3e3,
|
|
868
|
+
q: 0.7,
|
|
869
|
+
d: 0.03,
|
|
870
|
+
vol: 0.32
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
layer: "thud",
|
|
874
|
+
at: 0.03,
|
|
875
|
+
gain: 0.2,
|
|
876
|
+
frequency: 600
|
|
877
|
+
}
|
|
878
|
+
],
|
|
879
|
+
bolt_lift: [
|
|
880
|
+
{
|
|
881
|
+
layer: "metal",
|
|
882
|
+
at: 0,
|
|
883
|
+
frequency: 1500,
|
|
884
|
+
gain: 0.3
|
|
885
|
+
},
|
|
886
|
+
{
|
|
887
|
+
layer: "scene_noise",
|
|
888
|
+
at: 0,
|
|
889
|
+
type: "bandpass",
|
|
890
|
+
f: 2e3,
|
|
891
|
+
q: 3,
|
|
892
|
+
d: 0.04,
|
|
893
|
+
vol: 0.15,
|
|
894
|
+
f2: 1500
|
|
895
|
+
}
|
|
896
|
+
],
|
|
897
|
+
bolt_pull: [
|
|
898
|
+
{
|
|
899
|
+
layer: "scene_noise",
|
|
900
|
+
at: 0,
|
|
901
|
+
type: "bandpass",
|
|
902
|
+
f: 1400,
|
|
903
|
+
q: 3,
|
|
904
|
+
d: 0.12,
|
|
905
|
+
vol: 0.3,
|
|
906
|
+
f2: 800
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
layer: "metal",
|
|
910
|
+
at: 0.1,
|
|
911
|
+
frequency: 900,
|
|
912
|
+
gain: 0.3
|
|
913
|
+
}
|
|
914
|
+
],
|
|
915
|
+
bolt_push: [
|
|
916
|
+
{
|
|
917
|
+
layer: "scene_noise",
|
|
918
|
+
at: 0,
|
|
919
|
+
type: "bandpass",
|
|
920
|
+
f: 800,
|
|
921
|
+
q: 3,
|
|
922
|
+
d: 0.1,
|
|
923
|
+
vol: 0.3,
|
|
924
|
+
f2: 1400
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
layer: "metal",
|
|
928
|
+
at: 0.08,
|
|
929
|
+
frequency: 1100,
|
|
930
|
+
gain: 0.3
|
|
931
|
+
}
|
|
932
|
+
],
|
|
933
|
+
bolt_lock: [
|
|
934
|
+
{
|
|
935
|
+
layer: "metal",
|
|
936
|
+
at: 0,
|
|
937
|
+
frequency: 1800,
|
|
938
|
+
gain: 0.4
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
layer: "thud",
|
|
942
|
+
at: 0.02,
|
|
943
|
+
gain: 0.2,
|
|
944
|
+
frequency: 700
|
|
945
|
+
}
|
|
946
|
+
],
|
|
947
|
+
shell_insert: [
|
|
948
|
+
{
|
|
949
|
+
layer: "scene_tone",
|
|
950
|
+
at: 0,
|
|
951
|
+
wave: "square",
|
|
952
|
+
f: 1400,
|
|
953
|
+
f2: 980,
|
|
954
|
+
d: 0.02,
|
|
955
|
+
vol: 0.35
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
layer: "scene_noise",
|
|
959
|
+
at: 0,
|
|
960
|
+
type: "highpass",
|
|
961
|
+
f: 3e3,
|
|
962
|
+
q: 0.7,
|
|
963
|
+
d: 0.03,
|
|
964
|
+
vol: 0.28
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
layer: "scene_noise",
|
|
968
|
+
at: 0.02,
|
|
969
|
+
type: "bandpass",
|
|
970
|
+
f: 1200,
|
|
971
|
+
q: 3,
|
|
972
|
+
d: 0.05,
|
|
973
|
+
vol: 0.25
|
|
974
|
+
}
|
|
975
|
+
],
|
|
976
|
+
pump_back: [
|
|
977
|
+
{
|
|
978
|
+
layer: "scene_noise",
|
|
979
|
+
at: 0,
|
|
980
|
+
type: "bandpass",
|
|
981
|
+
f: 1e3,
|
|
982
|
+
q: 2,
|
|
983
|
+
d: 0.12,
|
|
984
|
+
vol: 0.3,
|
|
985
|
+
f2: 700
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
layer: "metal",
|
|
989
|
+
at: 0.1,
|
|
990
|
+
frequency: 900,
|
|
991
|
+
gain: 0.35
|
|
992
|
+
}
|
|
993
|
+
],
|
|
994
|
+
pump_forward: [
|
|
995
|
+
{
|
|
996
|
+
layer: "scene_noise",
|
|
997
|
+
at: 0,
|
|
998
|
+
type: "bandpass",
|
|
999
|
+
f: 700,
|
|
1000
|
+
q: 2,
|
|
1001
|
+
d: 0.1,
|
|
1002
|
+
vol: 0.3,
|
|
1003
|
+
f2: 1e3
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
layer: "metal",
|
|
1007
|
+
at: 0.08,
|
|
1008
|
+
frequency: 1300,
|
|
1009
|
+
gain: 0.4
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
layer: "thud",
|
|
1013
|
+
at: 0.1,
|
|
1014
|
+
gain: 0.25,
|
|
1015
|
+
frequency: 500
|
|
1016
|
+
}
|
|
1017
|
+
],
|
|
1018
|
+
lever_open: [
|
|
1019
|
+
{
|
|
1020
|
+
layer: "metal",
|
|
1021
|
+
at: 0,
|
|
1022
|
+
frequency: 1e3,
|
|
1023
|
+
gain: 0.35
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
layer: "scene_noise",
|
|
1027
|
+
at: 0,
|
|
1028
|
+
type: "bandpass",
|
|
1029
|
+
f: 1200,
|
|
1030
|
+
q: 3,
|
|
1031
|
+
d: 0.08,
|
|
1032
|
+
vol: 0.2,
|
|
1033
|
+
f2: 800
|
|
1034
|
+
}
|
|
1035
|
+
],
|
|
1036
|
+
lever_close: [
|
|
1037
|
+
{
|
|
1038
|
+
layer: "metal",
|
|
1039
|
+
at: 0,
|
|
1040
|
+
frequency: 1500,
|
|
1041
|
+
gain: 0.4
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
layer: "thud",
|
|
1045
|
+
at: 0.02,
|
|
1046
|
+
gain: 0.2,
|
|
1047
|
+
frequency: 550
|
|
1048
|
+
}
|
|
1049
|
+
],
|
|
1050
|
+
cylinder_open: [
|
|
1051
|
+
{
|
|
1052
|
+
layer: "metal",
|
|
1053
|
+
at: 0,
|
|
1054
|
+
frequency: 1500,
|
|
1055
|
+
gain: 0.3
|
|
1056
|
+
},
|
|
1057
|
+
{
|
|
1058
|
+
layer: "scene_noise",
|
|
1059
|
+
at: 0.02,
|
|
1060
|
+
type: "bandpass",
|
|
1061
|
+
f: 1800,
|
|
1062
|
+
q: 3,
|
|
1063
|
+
d: 0.08,
|
|
1064
|
+
vol: 0.2,
|
|
1065
|
+
f2: 1200
|
|
1066
|
+
}
|
|
1067
|
+
],
|
|
1068
|
+
cylinder_eject: [
|
|
1069
|
+
{
|
|
1070
|
+
layer: "metal",
|
|
1071
|
+
at: 0,
|
|
1072
|
+
frequency: 2400,
|
|
1073
|
+
gain: 0.25
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
layer: "metal",
|
|
1077
|
+
at: 0.05,
|
|
1078
|
+
frequency: 2800,
|
|
1079
|
+
gain: 0.2
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
layer: "metal",
|
|
1083
|
+
at: 0.11,
|
|
1084
|
+
frequency: 3200,
|
|
1085
|
+
gain: 0.15
|
|
1086
|
+
}
|
|
1087
|
+
],
|
|
1088
|
+
speedloader: [
|
|
1089
|
+
{
|
|
1090
|
+
layer: "scene_noise",
|
|
1091
|
+
at: 0,
|
|
1092
|
+
type: "bandpass",
|
|
1093
|
+
f: 1500,
|
|
1094
|
+
q: 3,
|
|
1095
|
+
d: 0.08,
|
|
1096
|
+
vol: 0.25,
|
|
1097
|
+
f2: 2e3
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
layer: "metal",
|
|
1101
|
+
at: 0,
|
|
1102
|
+
frequency: 2e3,
|
|
1103
|
+
gain: 0.25
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
layer: "metal",
|
|
1107
|
+
at: 0.05,
|
|
1108
|
+
frequency: 2400,
|
|
1109
|
+
gain: 0.2
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
layer: "thud",
|
|
1113
|
+
at: 0.07,
|
|
1114
|
+
gain: 0.15,
|
|
1115
|
+
frequency: 800
|
|
1116
|
+
}
|
|
1117
|
+
],
|
|
1118
|
+
cylinder_close: [
|
|
1119
|
+
{
|
|
1120
|
+
layer: "metal",
|
|
1121
|
+
at: 0,
|
|
1122
|
+
frequency: 1100,
|
|
1123
|
+
gain: 0.45
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
layer: "thud",
|
|
1127
|
+
at: 0.01,
|
|
1128
|
+
gain: 0.2,
|
|
1129
|
+
frequency: 500
|
|
1130
|
+
}
|
|
1131
|
+
],
|
|
1132
|
+
barrels_open: [
|
|
1133
|
+
{
|
|
1134
|
+
layer: "metal",
|
|
1135
|
+
at: 0,
|
|
1136
|
+
frequency: 1e3,
|
|
1137
|
+
gain: 0.4
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
layer: "thud",
|
|
1141
|
+
at: 0.03,
|
|
1142
|
+
gain: 0.2,
|
|
1143
|
+
frequency: 450
|
|
1144
|
+
}
|
|
1145
|
+
],
|
|
1146
|
+
casings_eject: [
|
|
1147
|
+
{
|
|
1148
|
+
layer: "metal",
|
|
1149
|
+
at: 0,
|
|
1150
|
+
frequency: 2600,
|
|
1151
|
+
gain: 0.25
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
layer: "metal",
|
|
1155
|
+
at: 0.08,
|
|
1156
|
+
frequency: 3e3,
|
|
1157
|
+
gain: 0.2
|
|
1158
|
+
}
|
|
1159
|
+
],
|
|
1160
|
+
barrels_close: [
|
|
1161
|
+
{
|
|
1162
|
+
layer: "metal",
|
|
1163
|
+
at: 0,
|
|
1164
|
+
frequency: 1100,
|
|
1165
|
+
gain: 0.5
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
layer: "thud",
|
|
1169
|
+
at: 0.02,
|
|
1170
|
+
gain: 0.3,
|
|
1171
|
+
frequency: 450
|
|
1172
|
+
}
|
|
1173
|
+
],
|
|
1174
|
+
belt_cover_open: [
|
|
1175
|
+
{
|
|
1176
|
+
layer: "metal",
|
|
1177
|
+
at: 0,
|
|
1178
|
+
frequency: 800,
|
|
1179
|
+
gain: 0.4
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
layer: "thud",
|
|
1183
|
+
at: 0.03,
|
|
1184
|
+
gain: 0.2,
|
|
1185
|
+
frequency: 500
|
|
1186
|
+
}
|
|
1187
|
+
],
|
|
1188
|
+
belt_insert: [
|
|
1189
|
+
{
|
|
1190
|
+
layer: "metal",
|
|
1191
|
+
at: 0,
|
|
1192
|
+
frequency: 1400,
|
|
1193
|
+
gain: 0.3
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
layer: "metal",
|
|
1197
|
+
at: 0.08,
|
|
1198
|
+
frequency: 1800,
|
|
1199
|
+
gain: 0.25
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
layer: "thud",
|
|
1203
|
+
at: 0.1,
|
|
1204
|
+
gain: 0.25,
|
|
1205
|
+
frequency: 450
|
|
1206
|
+
}
|
|
1207
|
+
],
|
|
1208
|
+
belt_cover_close: [
|
|
1209
|
+
{
|
|
1210
|
+
layer: "metal",
|
|
1211
|
+
at: 0,
|
|
1212
|
+
frequency: 1e3,
|
|
1213
|
+
gain: 0.45
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
layer: "thud",
|
|
1217
|
+
at: 0.02,
|
|
1218
|
+
gain: 0.3,
|
|
1219
|
+
frequency: 400
|
|
1220
|
+
}
|
|
1221
|
+
],
|
|
1222
|
+
rocket_load: [
|
|
1223
|
+
{
|
|
1224
|
+
layer: "scene_noise",
|
|
1225
|
+
at: 0,
|
|
1226
|
+
type: "bandpass",
|
|
1227
|
+
f: 600,
|
|
1228
|
+
q: 1.5,
|
|
1229
|
+
d: 0.25,
|
|
1230
|
+
vol: 0.3,
|
|
1231
|
+
f2: 400
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
layer: "thud",
|
|
1235
|
+
at: 0.2,
|
|
1236
|
+
gain: 0.2,
|
|
1237
|
+
frequency: 450
|
|
1238
|
+
}
|
|
1239
|
+
],
|
|
1240
|
+
rocket_seat: [
|
|
1241
|
+
{
|
|
1242
|
+
layer: "metal",
|
|
1243
|
+
at: 0,
|
|
1244
|
+
frequency: 900,
|
|
1245
|
+
gain: 0.45
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
layer: "thud",
|
|
1249
|
+
at: 0.01,
|
|
1250
|
+
gain: 0.35,
|
|
1251
|
+
frequency: 350
|
|
1252
|
+
}
|
|
1253
|
+
],
|
|
1254
|
+
grenade_load: [
|
|
1255
|
+
{
|
|
1256
|
+
layer: "metal",
|
|
1257
|
+
at: 0,
|
|
1258
|
+
frequency: 1100,
|
|
1259
|
+
gain: 0.35
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
layer: "scene_noise",
|
|
1263
|
+
at: 0,
|
|
1264
|
+
type: "bandpass",
|
|
1265
|
+
f: 1e3,
|
|
1266
|
+
q: 2,
|
|
1267
|
+
d: 0.1,
|
|
1268
|
+
vol: 0.2,
|
|
1269
|
+
f2: 700
|
|
1270
|
+
}
|
|
1271
|
+
],
|
|
1272
|
+
breech_open: [
|
|
1273
|
+
{
|
|
1274
|
+
layer: "metal",
|
|
1275
|
+
at: 0,
|
|
1276
|
+
frequency: 1e3,
|
|
1277
|
+
gain: 0.35
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
layer: "thud",
|
|
1281
|
+
at: 0.03,
|
|
1282
|
+
gain: 0.15,
|
|
1283
|
+
frequency: 500
|
|
1284
|
+
}
|
|
1285
|
+
],
|
|
1286
|
+
breech_close: [
|
|
1287
|
+
{
|
|
1288
|
+
layer: "metal",
|
|
1289
|
+
at: 0,
|
|
1290
|
+
frequency: 1100,
|
|
1291
|
+
gain: 0.45
|
|
1292
|
+
},
|
|
1293
|
+
{
|
|
1294
|
+
layer: "thud",
|
|
1295
|
+
at: 0.02,
|
|
1296
|
+
gain: 0.25,
|
|
1297
|
+
frequency: 450
|
|
1298
|
+
}
|
|
1299
|
+
],
|
|
1300
|
+
tube_drop: [
|
|
1301
|
+
{
|
|
1302
|
+
layer: "thud",
|
|
1303
|
+
at: 0,
|
|
1304
|
+
gain: 0.35,
|
|
1305
|
+
frequency: 380
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
layer: "metal",
|
|
1309
|
+
at: 0.05,
|
|
1310
|
+
frequency: 700,
|
|
1311
|
+
gain: 0.2
|
|
1312
|
+
}
|
|
1313
|
+
],
|
|
1314
|
+
tube_seat: [
|
|
1315
|
+
{
|
|
1316
|
+
layer: "metal",
|
|
1317
|
+
at: 0,
|
|
1318
|
+
frequency: 900,
|
|
1319
|
+
gain: 0.45
|
|
1320
|
+
},
|
|
1321
|
+
{
|
|
1322
|
+
layer: "thud",
|
|
1323
|
+
at: 0.02,
|
|
1324
|
+
gain: 0.3,
|
|
1325
|
+
frequency: 380
|
|
1326
|
+
}
|
|
1327
|
+
]
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
// src/audio/reload-sounds.ts
|
|
1332
|
+
var RELOAD_CUE_IDS = Object.keys(reload_sounds_default.cues);
|
|
1333
|
+
var DEFAULT_SOUND_PROFILE = { weight: "medium", material: "steel" };
|
|
1334
|
+
var RELOAD_CUES = reload_sounds_default.cues;
|
|
1335
|
+
var polymer = new Set(reload_sounds_default.polymer);
|
|
1336
|
+
var weights = reload_sounds_default.weights;
|
|
1337
|
+
var soundProfileOf = (weaponId, modelFamily) => ({ weight: weights[modelFamily] ?? "medium", material: polymer.has(weaponId) ? "polymer" : "steel" });
|
|
1338
|
+
var reloadCueSteps = (id, profile = DEFAULT_SOUND_PROFILE, table = RELOAD_CUES) => {
|
|
1339
|
+
const steps = table[id];
|
|
1340
|
+
if (!steps) return [];
|
|
1341
|
+
const weight = reload_sounds_default.profiles.weight[profile.weight];
|
|
1342
|
+
const material = reload_sounds_default.profiles.material[profile.material];
|
|
1343
|
+
const pitch = weight.pitch * material.pitch;
|
|
1344
|
+
const gain = weight.gain * material.gain;
|
|
1345
|
+
return steps.map((entry) => {
|
|
1346
|
+
switch (entry.layer) {
|
|
1347
|
+
case "metal":
|
|
1348
|
+
return { ...entry, frequency: Math.round(entry.frequency * pitch), gain: entry.gain * gain };
|
|
1349
|
+
case "thud":
|
|
1350
|
+
return { ...entry, frequency: Math.round(entry.frequency * pitch), gain: entry.gain * gain };
|
|
1351
|
+
case "scene_tone":
|
|
1352
|
+
return { ...entry, f: Math.round(entry.f * pitch), ...entry.f2 === void 0 ? {} : { f2: Math.round(entry.f2 * pitch) }, vol: entry.vol * gain };
|
|
1353
|
+
case "scene_noise":
|
|
1354
|
+
return { ...entry, f: Math.round(entry.f * pitch), ...entry.f2 === void 0 ? {} : { f2: Math.round(entry.f2 * pitch) }, vol: entry.vol * gain };
|
|
1355
|
+
default:
|
|
1356
|
+
return entry;
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
};
|
|
1360
|
+
|
|
601
1361
|
// src/audio.ts
|
|
602
1362
|
var NOISE_SECONDS = 1.5;
|
|
603
1363
|
var DEFAULT_SEED = 1;
|
|
@@ -606,6 +1366,7 @@ function createWeaponAudio(volume = 0.6, options = {}) {
|
|
|
606
1366
|
const classes = { ...GAME_SOUND_CLASSES, ...options.classes };
|
|
607
1367
|
const distanceCurve = { ...GAME_DISTANCE_CURVE, ...options.distance };
|
|
608
1368
|
const mechanicalTable = { ...MECHANICAL_SOUNDS, ...options.mechanical };
|
|
1369
|
+
const cueTable = { ...RELOAD_CUES, ...options.reloadCues };
|
|
609
1370
|
const shared = options.engine;
|
|
610
1371
|
const engine = shared ?? createAudioEngine({
|
|
611
1372
|
seed,
|
|
@@ -658,17 +1419,20 @@ function createWeaponAudio(volume = 0.6, options = {}) {
|
|
|
658
1419
|
const shotSound = (input) => computeShotSound(input, options);
|
|
659
1420
|
return {
|
|
660
1421
|
shot(energyJ, category, extras = {}) {
|
|
661
|
-
const
|
|
662
|
-
if (!
|
|
663
|
-
classBurstAt(0,
|
|
664
|
-
if (
|
|
665
|
-
tone({ seconds:
|
|
1422
|
+
const sound3 = shotSound({ ...extras, energyJ, category });
|
|
1423
|
+
if (!sound3.audible) return;
|
|
1424
|
+
classBurstAt(0, sound3.cutoff, sound3.gain, sound3.duration);
|
|
1425
|
+
if (sound3.crack.gain > 0) burst({ seconds: sound3.crack.duration, gain: sound3.crack.gain, filter: "highpass", frequency: sound3.crack.cutoff });
|
|
1426
|
+
tone({ seconds: sound3.body.duration, gain: sound3.body.gain, frequency: sound3.body.frequency, to: sound3.body.to });
|
|
666
1427
|
},
|
|
667
1428
|
shotSound,
|
|
668
1429
|
click() {
|
|
669
1430
|
mechanical("dry_fire");
|
|
670
1431
|
},
|
|
671
1432
|
mechanical,
|
|
1433
|
+
cue(id, profile) {
|
|
1434
|
+
for (const entry of reloadCueSteps(id, profile, cueTable)) step(entry);
|
|
1435
|
+
},
|
|
672
1436
|
event(event) {
|
|
673
1437
|
const id = mechanicalSoundOf(event);
|
|
674
1438
|
if (id) mechanical(id);
|
|
@@ -717,7 +1481,7 @@ var VIEWMODEL_NODE_NAME = "weapon-viewmodel";
|
|
|
717
1481
|
var IN_HAND_NODE_NAME = "weapon-in-hand";
|
|
718
1482
|
var LASER_RAYCAST_HZ = 30;
|
|
719
1483
|
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;
|
|
1484
|
+
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
1485
|
return false;
|
|
722
1486
|
};
|
|
723
1487
|
var isVisibleInScene = (object) => {
|
|
@@ -747,10 +1511,149 @@ var createLaserRaycast = (THREE, scene, camera, options = {}) => {
|
|
|
747
1511
|
};
|
|
748
1512
|
};
|
|
749
1513
|
|
|
1514
|
+
// src/fx/hand-hold.ts
|
|
1515
|
+
var HAND_HOLD = {
|
|
1516
|
+
/** Half the width of a pistol grip, the palm sits this far (plus the palm) off its middle. */
|
|
1517
|
+
gripHalfWidth: 0.0145,
|
|
1518
|
+
/** Half the thickness of a palm. */
|
|
1519
|
+
palm: 0.011,
|
|
1520
|
+
/** How far up the grip the web of the hand sits from its middle. */
|
|
1521
|
+
gripUp: 6e-3,
|
|
1522
|
+
/** How far down the grip the supporting hand of a pistol sits from the middle. */
|
|
1523
|
+
supportDown: 0.036,
|
|
1524
|
+
/** Where along the handguard (0 the receiver, 1 the end of the forend) the supporting hand closes. */
|
|
1525
|
+
forendAt: 0.6,
|
|
1526
|
+
/** How far under the handguard the palm sits (half a handguard plus the palm). */
|
|
1527
|
+
forendUnder: 0.034,
|
|
1528
|
+
/** The grip of a long gun that has no drawn pistol grip: how far under the receiver the hand sits. */
|
|
1529
|
+
strapUnder: 0.04,
|
|
1530
|
+
/** How far lower than touching the palm hangs under a handguard, so the fingers close under it and not over it. */
|
|
1531
|
+
forendDrop: 0.03,
|
|
1532
|
+
/** How far (radians) the fingers of a support hand under a handguard rise from level. */
|
|
1533
|
+
forendTilt: 0.5,
|
|
1534
|
+
/** How far a handgun's support hand dips for the new magazine, along the grip. */
|
|
1535
|
+
magDip: 0.045,
|
|
1536
|
+
/** Closure of the fingers on a grip and on a forend (0 open, 1 closed). */
|
|
1537
|
+
closed: 0.9,
|
|
1538
|
+
forend: 0.6,
|
|
1539
|
+
/** How fast the supporting hand goes to the magazine and back, per second. */
|
|
1540
|
+
magRate: 7,
|
|
1541
|
+
/** The hands shrink with the tool: a tool drawn at scale `s` takes hands at `s ** handScaleExponent`. */
|
|
1542
|
+
handScaleExponent: 0.55
|
|
1543
|
+
};
|
|
1544
|
+
var handFrame = (THREE, x, y) => {
|
|
1545
|
+
const ax = x.clone().normalize();
|
|
1546
|
+
const az = ax.clone().cross(y).normalize();
|
|
1547
|
+
const ay = az.clone().cross(ax);
|
|
1548
|
+
return new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(ax, ay, az));
|
|
1549
|
+
};
|
|
1550
|
+
var poseOf = (anchor, rootQuaternion) => {
|
|
1551
|
+
const position = anchor.part.localToWorld(anchor.offset.clone());
|
|
1552
|
+
const q = rootQuaternion.clone().multiply(anchor.quaternion);
|
|
1553
|
+
return { position: [position.x, position.y, position.z], quaternion: [q.x, q.y, q.z, q.w] };
|
|
1554
|
+
};
|
|
1555
|
+
var blendPose = (THREE, a, b, k) => {
|
|
1556
|
+
const q = new THREE.Quaternion(...a.quaternion).slerp(new THREE.Quaternion(...b.quaternion), k);
|
|
1557
|
+
return {
|
|
1558
|
+
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],
|
|
1559
|
+
quaternion: [q.x, q.y, q.z, q.w]
|
|
1560
|
+
};
|
|
1561
|
+
};
|
|
1562
|
+
var targetsOf = (THREE, hold, rootQuaternion, magazine, weights2) => {
|
|
1563
|
+
let right = hold.right ? poseOf(hold.right, rootQuaternion) : null;
|
|
1564
|
+
let left = null;
|
|
1565
|
+
if (hold.left) {
|
|
1566
|
+
left = poseOf(hold.left, rootQuaternion);
|
|
1567
|
+
if (hold.leftMagazine && magazine > 0) left = blendPose(THREE, left, poseOf(hold.leftMagazine, rootQuaternion), magazine);
|
|
1568
|
+
}
|
|
1569
|
+
hold.works?.forEach((work, index) => {
|
|
1570
|
+
const k = weights2?.[index] ?? 0;
|
|
1571
|
+
if (k <= 0) return;
|
|
1572
|
+
const pose = poseOf(work.anchor, rootQuaternion);
|
|
1573
|
+
if (work.hand === "R") right = right ? blendPose(THREE, right, pose, k) : pose;
|
|
1574
|
+
else left = left ? blendPose(THREE, left, pose, k) : pose;
|
|
1575
|
+
});
|
|
1576
|
+
return { R: right, L: left, grip: hold.grip, gripL: hold.gripLeft, scale: hold.scale };
|
|
1577
|
+
};
|
|
1578
|
+
var V = (THREE, x, y, z) => new THREE.Vector3(x, y, z);
|
|
1579
|
+
var gripFrame = (THREE, up, side) => {
|
|
1580
|
+
const forward = V(THREE, 1, 0, 0);
|
|
1581
|
+
const toSide = V(THREE, 0, 0, 1);
|
|
1582
|
+
const tilt = new THREE.Quaternion().setFromUnitVectors(V(THREE, 0, 1, 0), up);
|
|
1583
|
+
return handFrame(THREE, forward.clone().applyQuaternion(tilt), toSide.clone().applyQuaternion(tilt).multiplyScalar(side));
|
|
1584
|
+
};
|
|
1585
|
+
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)));
|
|
1586
|
+
function holdOfModel(THREE, model, holderScale) {
|
|
1587
|
+
const { root, inner, parts } = model;
|
|
1588
|
+
const frames = parts.frames;
|
|
1589
|
+
const toRoot = (x, y, z) => V(THREE, inner.position.x + x * inner.scale.x, y, z);
|
|
1590
|
+
const handgun = parts.family === "pistol" || parts.family === "revolver";
|
|
1591
|
+
root.updateWorldMatrix(true, true);
|
|
1592
|
+
let gripAt;
|
|
1593
|
+
let up;
|
|
1594
|
+
if (parts.grip) {
|
|
1595
|
+
gripAt = toRoot(parts.grip.position.x, parts.grip.position.y, parts.grip.position.z);
|
|
1596
|
+
up = V(THREE, 0, 1, 0).applyQuaternion(parts.grip.quaternion);
|
|
1597
|
+
} else {
|
|
1598
|
+
const receiverHeight = frames?.receiverHeight ?? 0.06;
|
|
1599
|
+
gripAt = toRoot(frames?.gripX ?? 0.07, -receiverHeight / 2 - HAND_HOLD.strapUnder, 0);
|
|
1600
|
+
up = V(THREE, Math.sin(0.3), Math.cos(0.3), 0);
|
|
1601
|
+
}
|
|
1602
|
+
const side = HAND_HOLD.gripHalfWidth + HAND_HOLD.palm;
|
|
1603
|
+
const right = {
|
|
1604
|
+
part: root,
|
|
1605
|
+
offset: gripAt.clone().addScaledVector(up, HAND_HOLD.gripUp).add(V(THREE, 0, 0, side)),
|
|
1606
|
+
quaternion: gripFrame(THREE, up, 1)
|
|
1607
|
+
};
|
|
1608
|
+
let left = null;
|
|
1609
|
+
let leftMagazine = null;
|
|
1610
|
+
if (handgun) {
|
|
1611
|
+
left = { part: root, offset: gripAt.clone().addScaledVector(up, -HAND_HOLD.supportDown).add(V(THREE, 0, 0, -side)), quaternion: gripFrame(THREE, up, -1) };
|
|
1612
|
+
} else if (parts.pump) {
|
|
1613
|
+
const box2 = new THREE.Box3().setFromObject(parts.pump);
|
|
1614
|
+
const center = box2.getCenter(new THREE.Vector3());
|
|
1615
|
+
const under = parts.pump.worldToLocal(V(THREE, center.x, box2.min.y - HAND_HOLD.palm, center.z));
|
|
1616
|
+
left = { part: parts.pump, offset: under, quaternion: forendFrame(THREE) };
|
|
1617
|
+
} else if (frames) {
|
|
1618
|
+
const handguardHeight = frames.receiverHeight * 0.8;
|
|
1619
|
+
const x = frames.receiverEnd + (frames.handguardEnd - frames.receiverEnd) * HAND_HOLD.forendAt;
|
|
1620
|
+
left = { part: root, offset: toRoot(x, frames.barrelY - handguardHeight / 2 - HAND_HOLD.palm - HAND_HOLD.forendDrop, 0), quaternion: forendFrame(THREE, HAND_HOLD.forendTilt) };
|
|
1621
|
+
}
|
|
1622
|
+
if (left && parts.mag) {
|
|
1623
|
+
const box2 = new THREE.Box3().setFromObject(parts.mag);
|
|
1624
|
+
const center = box2.getCenter(new THREE.Vector3());
|
|
1625
|
+
const local = parts.mag.worldToLocal(V(THREE, center.x, box2.min.y - HAND_HOLD.palm, center.z));
|
|
1626
|
+
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) };
|
|
1627
|
+
}
|
|
1628
|
+
return {
|
|
1629
|
+
right,
|
|
1630
|
+
left,
|
|
1631
|
+
leftMagazine,
|
|
1632
|
+
scale: holderScale ** HAND_HOLD.handScaleExponent,
|
|
1633
|
+
grip: HAND_HOLD.closed,
|
|
1634
|
+
gripLeft: HAND_HOLD.forend
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
function holdOfProp(THREE, group, category, scale) {
|
|
1638
|
+
const handle = group.getObjectByName("handle") ?? group.getObjectByName("body") ?? group;
|
|
1639
|
+
const head = group.getObjectByName("head");
|
|
1640
|
+
const size = (object) => new THREE.Box3().setFromObject(object).getSize(new THREE.Vector3());
|
|
1641
|
+
const wrap = handFrame(THREE, V(THREE, 0, 1, 0), V(THREE, 0, 0, 1));
|
|
1642
|
+
const handleSize = size(handle);
|
|
1643
|
+
const right = { part: group, offset: handle.position.clone().add(V(THREE, 0, handleSize.y / 2 + 2e-3, 0)), quaternion: wrap };
|
|
1644
|
+
const long = head && ["smg", "carbine", "rifle", "sniper", "shotgun", "crossbow", "flame"].includes(category);
|
|
1645
|
+
const headSize = head ? size(head) : null;
|
|
1646
|
+
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;
|
|
1647
|
+
return { right, left, leftMagazine: null, scale: scale ** HAND_HOLD.handScaleExponent, grip: HAND_HOLD.closed, gripLeft: HAND_HOLD.forend };
|
|
1648
|
+
}
|
|
1649
|
+
|
|
750
1650
|
// src/props.ts
|
|
751
|
-
import {
|
|
1651
|
+
import { markKitOrigin as markKitOrigin2 } from "@adatechnology/game-contracts";
|
|
1652
|
+
import { buildItem, hasItemModel } from "@adatechnology/game-items-three";
|
|
1653
|
+
import { mergeByMaterial, shapeGeometries as shapeGeometries2 } from "@adatechnology/game-shapes-three";
|
|
752
1654
|
|
|
753
1655
|
// src/models/index.ts
|
|
1656
|
+
import { markKitOrigin } from "@adatechnology/game-contracts";
|
|
754
1657
|
import { WEAPONS } from "@adatechnology/game-weapons";
|
|
755
1658
|
|
|
756
1659
|
// src/data/models.json
|
|
@@ -2306,6 +3209,9 @@ var models_default = {
|
|
|
2306
3209
|
}
|
|
2307
3210
|
};
|
|
2308
3211
|
|
|
3212
|
+
// src/models/kit.ts
|
|
3213
|
+
import { shapeGeometries } from "@adatechnology/game-shapes-three";
|
|
3214
|
+
|
|
2309
3215
|
// src/models/rounded-box.ts
|
|
2310
3216
|
var uvAlong = (THREE, faceDirection, normal, uvAxis, projectionAxis, radius, sideLength) => {
|
|
2311
3217
|
const arcLength = 2 * Math.PI * radius / 4;
|
|
@@ -2332,11 +3238,11 @@ function createRoundedBoxGeometry(THREE, width, height, depth, segments, radius)
|
|
|
2332
3238
|
const count = segments * 2 + 1;
|
|
2333
3239
|
const rounding = Math.min(width / 2, height / 2, depth / 2, radius);
|
|
2334
3240
|
const geometry = new THREE.BoxGeometry(1, 1, 1, count, count, count);
|
|
2335
|
-
const
|
|
3241
|
+
const flat2 = geometry.toNonIndexed();
|
|
2336
3242
|
geometry.setIndex(null);
|
|
2337
|
-
geometry.setAttribute("position",
|
|
2338
|
-
geometry.setAttribute("normal",
|
|
2339
|
-
geometry.setAttribute("uv",
|
|
3243
|
+
geometry.setAttribute("position", flat2.attributes.position);
|
|
3244
|
+
geometry.setAttribute("normal", flat2.attributes.normal);
|
|
3245
|
+
geometry.setAttribute("uv", flat2.attributes.uv);
|
|
2340
3246
|
const position = new THREE.Vector3();
|
|
2341
3247
|
const normal = new THREE.Vector3();
|
|
2342
3248
|
const box2 = new THREE.Vector3(width, height, depth).divideScalar(2).subScalar(rounding);
|
|
@@ -2417,9 +3323,10 @@ var createMaterials = (THREE) => {
|
|
|
2417
3323
|
};
|
|
2418
3324
|
};
|
|
2419
3325
|
var kits = /* @__PURE__ */ new WeakMap();
|
|
2420
|
-
function modelKit(
|
|
2421
|
-
const cached = kits.get(
|
|
3326
|
+
function modelKit(host) {
|
|
3327
|
+
const cached = kits.get(host);
|
|
2422
3328
|
if (cached) return cached;
|
|
3329
|
+
const THREE = shapeGeometries(host);
|
|
2423
3330
|
const geometries = /* @__PURE__ */ new Map();
|
|
2424
3331
|
const shadowed = (mesh) => {
|
|
2425
3332
|
mesh.castShadow = true;
|
|
@@ -2478,7 +3385,7 @@ function modelKit(THREE) {
|
|
|
2478
3385
|
return group;
|
|
2479
3386
|
}
|
|
2480
3387
|
};
|
|
2481
|
-
kits.set(
|
|
3388
|
+
kits.set(host, kit);
|
|
2482
3389
|
return kit;
|
|
2483
3390
|
}
|
|
2484
3391
|
function heatify(glow, mesh) {
|
|
@@ -2661,6 +3568,7 @@ function buildBullpup(kit, options) {
|
|
|
2661
3568
|
const gx = bodyLength * 0.8;
|
|
2662
3569
|
const grip = kit.pivot(group, gx, -0.085, 0);
|
|
2663
3570
|
grip.rotation.z = -0.25;
|
|
3571
|
+
parts.grip = grip;
|
|
2664
3572
|
box2(grip, 0.03, 0.1, 0.03, M.poly, 0, 0, 0);
|
|
2665
3573
|
box2(group, 0.1, 6e-3, 0.03, body, gx + 0.01, -0.06, 0);
|
|
2666
3574
|
parts.trigger = box2(group, 4e-3, 0.016, 4e-3, M.dark, gx + 0.02, -0.05, 0, 0.15);
|
|
@@ -2880,6 +3788,7 @@ function buildAt4(kit, group, parts) {
|
|
|
2880
3788
|
box2(group, 0.012, 0.05, 0.02, M.dark, 0.71, 0.08, -0.035);
|
|
2881
3789
|
const grip = kit.pivot(group, 0.68, -0.05, 0);
|
|
2882
3790
|
grip.rotation.z = -0.15;
|
|
3791
|
+
parts.grip = grip;
|
|
2883
3792
|
box2(grip, 0.035, 0.11, 0.032, M.dark, 0, -0.05, 0);
|
|
2884
3793
|
box2(group, 0.14, 0.035, 0.06, M.rubber, 0.32, -0.06, 0);
|
|
2885
3794
|
box2(group, 0.3, 0.012, 0.055, M.dark, 0.5, -0.05, 0);
|
|
@@ -3209,6 +4118,7 @@ function buildFrame(kit, group, parts, options, style, size, materials) {
|
|
|
3209
4118
|
const gy = -h / 2 - gripLength / 2 + 6e-3;
|
|
3210
4119
|
const grip = kit.pivot(group, gx, gy, 0);
|
|
3211
4120
|
grip.rotation.z = style.ga;
|
|
4121
|
+
parts.grip = grip;
|
|
3212
4122
|
const gripWidth = style.grip === "chunky" ? w * 1.18 : w * 1.06;
|
|
3213
4123
|
const gripDepth = style.grip === "chunky" ? 0.036 : 0.032;
|
|
3214
4124
|
kit.roundness = 0.3;
|
|
@@ -3332,6 +4242,7 @@ function buildRevolver(kit, options) {
|
|
|
3332
4242
|
parts.hammer = hammer;
|
|
3333
4243
|
const grip = kit.pivot(group, 0.024, -0.062, 0);
|
|
3334
4244
|
grip.rotation.z = -0.38;
|
|
4245
|
+
parts.grip = grip;
|
|
3335
4246
|
box2(grip, 0.034, 0.1, 0.031, options.wood ? M.wood : M.rubber, 0, 0, 0);
|
|
3336
4247
|
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
4248
|
box2(group, 0.038, 4e-3, 8e-3, metal2, 0.062, -0.036, 0);
|
|
@@ -3520,6 +4431,7 @@ function buildGrip(context) {
|
|
|
3520
4431
|
if (!context.isTraditional && o.grip !== "none") {
|
|
3521
4432
|
const grip = kit.pivot(group, gx - 0.012, -hR / 2 - 0.046, 0);
|
|
3522
4433
|
grip.rotation.z = -0.3;
|
|
4434
|
+
parts.grip = grip;
|
|
3523
4435
|
box2(grip, 0.032, 0.105, 0.028, furn, 0, 0, 0);
|
|
3524
4436
|
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
4437
|
box2(grip, 0.034, 8e-3, 0.03, furn, 0, -0.052, 0);
|
|
@@ -3820,6 +4732,7 @@ function buildWeaponModelById(THREE, id) {
|
|
|
3820
4732
|
built.group.position.x = -new THREE.Box3().setFromObject(built.group).min.x;
|
|
3821
4733
|
} else root = fitLength(kit, built.group, entry.length / 1e3);
|
|
3822
4734
|
root.name = id;
|
|
4735
|
+
markKitOrigin(root, { owner: "game-weapons-three", kind: "weapon", id });
|
|
3823
4736
|
const model = { id, root, inner: built.group, parts: built.parts };
|
|
3824
4737
|
if (!built.parts.frames) built.parts.frames = deriveFrames(THREE, model);
|
|
3825
4738
|
return model;
|
|
@@ -3899,7 +4812,7 @@ function buildProp(THREE, prop) {
|
|
|
3899
4812
|
if (part.shape.kind !== "box") continue;
|
|
3900
4813
|
const [w, h, d] = part.shape.size;
|
|
3901
4814
|
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);
|
|
4815
|
+
const mesh = new THREE.Mesh(new (shapeGeometries2(THREE)).BoxGeometry(w, h, d), material);
|
|
3903
4816
|
mesh.name = part.id;
|
|
3904
4817
|
mesh.position.set(part.position[0], part.position[1], part.position[2]);
|
|
3905
4818
|
mesh.castShadow = true;
|
|
@@ -3908,15 +4821,90 @@ function buildProp(THREE, prop) {
|
|
|
3908
4821
|
}
|
|
3909
4822
|
return group;
|
|
3910
4823
|
}
|
|
4824
|
+
var WEAPON_ITEM_MODELS = /* @__PURE__ */ new Map([
|
|
4825
|
+
["wrench", { item: "adjustable_wrench" }],
|
|
4826
|
+
["extinguisher", { item: "fire_extinguisher", flip: true }],
|
|
4827
|
+
["blowtorch", { item: "gas_torch", flip: true }],
|
|
4828
|
+
["crossbow", { turn: 1 }],
|
|
4829
|
+
["knife", { flip: true }],
|
|
4830
|
+
["meat_cleaver", { flip: true }],
|
|
4831
|
+
["screwdriver", { flip: true }]
|
|
4832
|
+
]);
|
|
4833
|
+
function registerWeaponItemModel(weaponId, model) {
|
|
4834
|
+
WEAPON_ITEM_MODELS.set(weaponId, model ?? { item: "" });
|
|
4835
|
+
}
|
|
4836
|
+
function itemModelOfWeapon(weaponId) {
|
|
4837
|
+
const id = WEAPON_ITEM_MODELS.get(weaponId)?.item ?? weaponId;
|
|
4838
|
+
return id && hasItemModel(id) ? id : void 0;
|
|
4839
|
+
}
|
|
4840
|
+
function buildItemWeaponModel(THREE, def) {
|
|
4841
|
+
const itemId = itemModelOfWeapon(def.id);
|
|
4842
|
+
if (!itemId) return void 0;
|
|
4843
|
+
const { root } = buildItem(THREE, { id: itemId, category: "weapon", size: [0, 0, 0], model: itemId });
|
|
4844
|
+
const group = markKitOrigin2(new THREE.Group(), { owner: "game-weapons-three", kind: "weapon", id: def.id });
|
|
4845
|
+
group.name = def.id;
|
|
4846
|
+
group.add(root);
|
|
4847
|
+
const sizeOf = () => {
|
|
4848
|
+
group.updateMatrixWorld(true);
|
|
4849
|
+
return new THREE.Box3().setFromObject(root).getSize(new THREE.Vector3());
|
|
4850
|
+
};
|
|
4851
|
+
let size = sizeOf();
|
|
4852
|
+
if (size.y > size.x && size.y >= size.z) root.rotateOnWorldAxis(new THREE.Vector3(0, 0, 1), -Math.PI / 2);
|
|
4853
|
+
else if (size.z > size.x && size.z > size.y) root.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), Math.PI / 2);
|
|
4854
|
+
size = sizeOf();
|
|
4855
|
+
if (size.z < size.y) root.rotateOnWorldAxis(new THREE.Vector3(1, 0, 0), Math.PI / 2);
|
|
4856
|
+
const turn2 = WEAPON_ITEM_MODELS.get(def.id)?.turn ?? 0;
|
|
4857
|
+
if (turn2) root.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), turn2 * Math.PI / 2);
|
|
4858
|
+
const box2 = new THREE.Box3().setFromObject(root);
|
|
4859
|
+
const middle = (box2.min.x + box2.max.x) / 2;
|
|
4860
|
+
const span = [
|
|
4861
|
+
[Infinity, -Infinity],
|
|
4862
|
+
[Infinity, -Infinity]
|
|
4863
|
+
];
|
|
4864
|
+
const point = new THREE.Vector3();
|
|
4865
|
+
root.traverse((node) => {
|
|
4866
|
+
const mesh = node;
|
|
4867
|
+
const position = mesh.isMesh ? mesh.geometry.getAttribute("position") : void 0;
|
|
4868
|
+
if (!position) return;
|
|
4869
|
+
for (let i = 0; i < position.count; i++) {
|
|
4870
|
+
point.fromBufferAttribute(position, i).applyMatrix4(mesh.matrixWorld);
|
|
4871
|
+
const half = span[point.x > middle ? 1 : 0];
|
|
4872
|
+
half[0] = Math.min(half[0], point.z);
|
|
4873
|
+
half[1] = Math.max(half[1], point.z);
|
|
4874
|
+
}
|
|
4875
|
+
});
|
|
4876
|
+
const reach = span.map(([low, high]) => high - low);
|
|
4877
|
+
const flip = WEAPON_ITEM_MODELS.get(def.id)?.flip === true;
|
|
4878
|
+
if (reach[0] > reach[1] !== flip) root.rotateOnWorldAxis(new THREE.Vector3(0, 1, 0), Math.PI);
|
|
4879
|
+
group.updateMatrixWorld(true);
|
|
4880
|
+
const laid = new THREE.Box3().setFromObject(root);
|
|
4881
|
+
const centre = laid.getCenter(new THREE.Vector3());
|
|
4882
|
+
root.position.sub(new THREE.Vector3(centre.x, laid.min.y, centre.z));
|
|
4883
|
+
const length = laid.max.x - laid.min.x;
|
|
4884
|
+
const handle = new THREE.Object3D();
|
|
4885
|
+
handle.name = "handle";
|
|
4886
|
+
handle.position.set(-length * 0.35, 0, 0);
|
|
4887
|
+
group.add(handle);
|
|
4888
|
+
root.traverse((node) => {
|
|
4889
|
+
const mesh = node;
|
|
4890
|
+
if (!mesh.isMesh) return;
|
|
4891
|
+
mesh.castShadow = true;
|
|
4892
|
+
mesh.receiveShadow = true;
|
|
4893
|
+
});
|
|
4894
|
+
return group;
|
|
4895
|
+
}
|
|
3911
4896
|
function buildWeaponModel(THREE, def) {
|
|
3912
|
-
return buildProp(THREE, weaponProp(def));
|
|
4897
|
+
return buildItemWeaponModel(THREE, def) ?? buildProp(THREE, weaponProp(def));
|
|
3913
4898
|
}
|
|
3914
4899
|
var MODEL_BODY = "body";
|
|
3915
4900
|
function buildModeledProp(THREE, def, name, decorate) {
|
|
3916
|
-
const model = buildWeaponModelById(THREE, def.id)
|
|
4901
|
+
const model = buildWeaponModelById(THREE, def.id) ?? (() => {
|
|
4902
|
+
const root = buildItemWeaponModel(THREE, def);
|
|
4903
|
+
return root ? { root } : void 0;
|
|
4904
|
+
})();
|
|
3917
4905
|
if (!model) return void 0;
|
|
3918
4906
|
decorate?.(model.root);
|
|
3919
|
-
const group = new THREE.Group();
|
|
4907
|
+
const group = markKitOrigin2(new THREE.Group(), { owner: "game-weapons-three", kind: "weapon", id: def.id });
|
|
3920
4908
|
group.name = name;
|
|
3921
4909
|
const box2 = new THREE.Box3().setFromObject(model.root);
|
|
3922
4910
|
const center = box2.getCenter(new THREE.Vector3());
|
|
@@ -3970,11 +4958,13 @@ function createBoxFx(options) {
|
|
|
3970
4958
|
let model = null;
|
|
3971
4959
|
let inHand = null;
|
|
3972
4960
|
let def = null;
|
|
4961
|
+
let hold = null;
|
|
3973
4962
|
let kick = 0;
|
|
3974
4963
|
let dip = 0;
|
|
3975
4964
|
let reloading = false;
|
|
3976
4965
|
const v = new THREE.Vector3();
|
|
3977
4966
|
const q = new THREE.Quaternion();
|
|
4967
|
+
const q2 = new THREE.Quaternion();
|
|
3978
4968
|
const pointForward = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI / 2);
|
|
3979
4969
|
const equip = (weapon) => {
|
|
3980
4970
|
if (model) hand.remove(model);
|
|
@@ -3988,6 +4978,7 @@ function createBoxFx(options) {
|
|
|
3988
4978
|
model.quaternion.copy(pointForward);
|
|
3989
4979
|
hand.add(model);
|
|
3990
4980
|
inHand = def.id;
|
|
4981
|
+
hold = holdOfProp(THREE, model, def.category, model.scale.x);
|
|
3991
4982
|
};
|
|
3992
4983
|
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
4984
|
const throwBody = (kind, mesh, from, velocity, radius) => {
|
|
@@ -4079,9 +5070,15 @@ function createBoxFx(options) {
|
|
|
4079
5070
|
if (model) hand.remove(model);
|
|
4080
5071
|
model = null;
|
|
4081
5072
|
inHand = null;
|
|
5073
|
+
hold = null;
|
|
5074
|
+
},
|
|
5075
|
+
handTargets() {
|
|
5076
|
+
if (!model || !hold) return null;
|
|
5077
|
+
hand.updateMatrixWorld(true);
|
|
5078
|
+
return targetsOf(THREE, hold, model.getWorldQuaternion(q2), 0);
|
|
4082
5079
|
},
|
|
4083
5080
|
handle(event, tool) {
|
|
4084
|
-
if (event.type === "explosion") return;
|
|
5081
|
+
if (event.type === "explosion" || event.type === "player") return;
|
|
4085
5082
|
const weapon = arsenal.weapons.get(event.weapon);
|
|
4086
5083
|
if (event.type === "tool") equip(event.weapon);
|
|
4087
5084
|
if (!weapon) return;
|
|
@@ -4136,6 +5133,7 @@ var ease = (progress) => progress < 0.5 ? 2 * progress * progress : 1 - (-2 * pr
|
|
|
4136
5133
|
var move = (part, axis, start, duration, from, to) => ({ part, channel: "position", axis, start, duration, from, to });
|
|
4137
5134
|
var turn = (part, axis, start, duration, from, to) => ({ part, channel: "rotation", axis, start, duration, from, to });
|
|
4138
5135
|
var cue = (t, type, data) => ({ t, type, ...data ? { data } : {} });
|
|
5136
|
+
var sound2 = (t, id) => ({ t, type: "sound", data: { id } });
|
|
4139
5137
|
var stage = (id, t) => ({ t, type: STAGE_EVENT, data: { stage: id } });
|
|
4140
5138
|
var valueAt = (tweens, t, includeInstants) => {
|
|
4141
5139
|
let value = 0;
|
|
@@ -4233,7 +5231,7 @@ function magazineSwap(parts, start) {
|
|
|
4233
5231
|
move("mag", "y", start + 0.16, 0, 0, direction * drop * 2.2),
|
|
4234
5232
|
move("mag", "y", start + MAGAZINE_IN_AT, 0.3, direction * drop * 2.2, 0)
|
|
4235
5233
|
],
|
|
4236
|
-
events: [cue(start + 0.16, "drop_magazine", { up: direction > 0 })],
|
|
5234
|
+
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
5235
|
end: start + MAGAZINE_SWAP_SECONDS
|
|
4238
5236
|
};
|
|
4239
5237
|
}
|
|
@@ -4306,43 +5304,52 @@ function clearClips(context) {
|
|
|
4306
5304
|
// src/animation/reload-clips.ts
|
|
4307
5305
|
import { RELOAD_STAGES } from "@adatechnology/game-weapons";
|
|
4308
5306
|
var KINDS = ["tactical", "empty", "one_by_one"];
|
|
5307
|
+
var chargeSounds = (start) => [sound2(start, "charging_handle_pull"), sound2(start + 0.2, "charging_handle_release")];
|
|
4309
5308
|
var charge = (context, start) => {
|
|
4310
5309
|
const travel = strokeOf(context.parts);
|
|
4311
5310
|
return [move("bolt", "x", start, 0.12, 0, travel), move("bolt", "x", start + 0.18, 0.06, travel, 0)];
|
|
4312
5311
|
};
|
|
5312
|
+
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
5313
|
var cycleWithoutEject = (start) => ({ tweens: boltStroke(start), events: [], end: start + 0.55 });
|
|
5314
|
+
var RACK_BACK = 0.14;
|
|
5315
|
+
var RACK_HOLD = 0.04;
|
|
5316
|
+
var RACK_FORWARD = 0.06;
|
|
4314
5317
|
function magazineReload(context, kind) {
|
|
4315
5318
|
const { parts, family } = context;
|
|
4316
5319
|
const swap = magazineSwap(parts, 0);
|
|
4317
5320
|
const draft = { tweens: [...swap.tweens], events: [...swap.events], end: swap.end };
|
|
4318
5321
|
if (family === "pistol") {
|
|
4319
5322
|
if (kind === "empty" && parts.slide) {
|
|
4320
|
-
|
|
4321
|
-
draft.end
|
|
5323
|
+
const travel = strokeOf(parts);
|
|
5324
|
+
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));
|
|
5325
|
+
draft.events.push(sound2(swap.end + 0.04 + RACK_BACK + RACK_HOLD + 0.02, "slide_release"));
|
|
5326
|
+
draft.end += 0.04 + RACK_BACK + RACK_HOLD + RACK_FORWARD + 0.08;
|
|
4322
5327
|
}
|
|
4323
5328
|
} else if (family === "boltmag") {
|
|
4324
5329
|
const stroke = cycleWithoutEject(swap.end);
|
|
4325
5330
|
draft.tweens.push(...stroke.tweens);
|
|
5331
|
+
draft.events.push(...boltSounds(swap.end));
|
|
4326
5332
|
draft.end = stroke.end;
|
|
4327
5333
|
} else if (kind === "empty" && parts.bolt) {
|
|
4328
5334
|
draft.tweens.push(...charge(context, swap.end));
|
|
5335
|
+
draft.events.push(...chargeSounds(swap.end));
|
|
4329
5336
|
draft.end += 0.35;
|
|
4330
5337
|
}
|
|
4331
5338
|
return draft;
|
|
4332
5339
|
}
|
|
4333
5340
|
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" }));
|
|
5341
|
+
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")];
|
|
5342
|
+
if (kind !== "one_by_one") events.push(cue(0.7, "move_in", { from: "chambers" }), sound2(0.7, "speedloader"));
|
|
4336
5343
|
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
5344
|
}
|
|
4338
5345
|
var breakReload = () => ({
|
|
4339
5346
|
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" })],
|
|
5347
|
+
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
5348
|
end: 1.2
|
|
4342
5349
|
});
|
|
4343
5350
|
var beltReload = (context) => ({
|
|
4344
5351
|
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: [],
|
|
5352
|
+
events: [sound2(0, "belt_cover_open"), sound2(0.8, "belt_insert"), sound2(1.05, "belt_cover_close"), ...chargeSounds(1.15)],
|
|
4346
5353
|
end: 1.55
|
|
4347
5354
|
});
|
|
4348
5355
|
var UNIT = 1;
|
|
@@ -4350,9 +5357,16 @@ function tubeReload(context, kind) {
|
|
|
4350
5357
|
const { parts, family } = context;
|
|
4351
5358
|
const draft = { tweens: [], events: [], end: UNIT };
|
|
4352
5359
|
if (kind !== "empty") return draft;
|
|
4353
|
-
if (family === "pump" && parts.pump)
|
|
4354
|
-
|
|
4355
|
-
|
|
5360
|
+
if (family === "pump" && parts.pump) {
|
|
5361
|
+
draft.tweens.push(move("pump", "x", 0.88, 0.05, 0, PUMP_TRAVEL), move("pump", "x", 0.94, 0.05, PUMP_TRAVEL, 0));
|
|
5362
|
+
draft.events.push(sound2(0.88, "pump_back"), sound2(0.94, "pump_forward"));
|
|
5363
|
+
} else if (family === "lever" && parts.lever) {
|
|
5364
|
+
draft.tweens.push(turn("lever", "z", 0.87, 0.06, 0, LEVER_TURN), turn("lever", "z", 0.94, 0.06, LEVER_TURN, 0));
|
|
5365
|
+
draft.events.push(sound2(0.87, "lever_open"), sound2(0.94, "lever_close"));
|
|
5366
|
+
} else if (parts.bolt) {
|
|
5367
|
+
draft.tweens.push(move("bolt", "x", 0.88, 0.07, 0, strokeOf(parts)), move("bolt", "x", 0.95, 0.04, strokeOf(parts), 0));
|
|
5368
|
+
draft.events.push(sound2(0.88, "charging_handle_pull"), sound2(0.95, "charging_handle_release"));
|
|
5369
|
+
}
|
|
4356
5370
|
return draft;
|
|
4357
5371
|
}
|
|
4358
5372
|
function boltReload(context) {
|
|
@@ -4366,13 +5380,13 @@ function boltReload(context) {
|
|
|
4366
5380
|
move("bolt", "x", 0.86, 0.07, BOLT_TRAVEL, 0),
|
|
4367
5381
|
turn("boltHandle", "x", 0.93, 0.05, BOLT_TURN, 0)
|
|
4368
5382
|
],
|
|
4369
|
-
events: [],
|
|
5383
|
+
events: [sound2(0, "bolt_lift"), sound2(0.05, "bolt_pull"), sound2(0.86, "bolt_push"), sound2(0.95, "bolt_lock")],
|
|
4370
5384
|
end: UNIT
|
|
4371
5385
|
};
|
|
4372
5386
|
}
|
|
4373
5387
|
function internalMagazineReload(context) {
|
|
4374
5388
|
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 };
|
|
5389
|
+
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
5390
|
}
|
|
4377
5391
|
function heavyReload(context) {
|
|
4378
5392
|
const { parts, family } = context;
|
|
@@ -4380,20 +5394,20 @@ function heavyReload(context) {
|
|
|
4380
5394
|
if (parts.disposable) {
|
|
4381
5395
|
return {
|
|
4382
5396
|
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")],
|
|
5397
|
+
events: [cue(0, "drop_tube"), sound2(0, "tube_drop"), cue(0.6, "seat_tube"), sound2(1.08, "tube_seat")],
|
|
4384
5398
|
end: 1.15
|
|
4385
5399
|
};
|
|
4386
5400
|
}
|
|
4387
5401
|
if (parts.round) {
|
|
4388
5402
|
return {
|
|
4389
5403
|
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")],
|
|
5404
|
+
events: [cue(0, "show_round"), sound2(0, "rocket_load"), sound2(0.55, "rocket_seat")],
|
|
4391
5405
|
end: 0.6
|
|
4392
5406
|
};
|
|
4393
5407
|
}
|
|
4394
5408
|
return {
|
|
4395
5409
|
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")],
|
|
5410
|
+
events: [sound2(0, "breech_open"), cue(0.3, "breech_round"), sound2(0.3, "grenade_load"), sound2(0.96, "breech_close")],
|
|
4397
5411
|
end: 1.05
|
|
4398
5412
|
};
|
|
4399
5413
|
}
|
|
@@ -4424,11 +5438,12 @@ function reloadClips(context) {
|
|
|
4424
5438
|
}
|
|
4425
5439
|
return clips.length > 0 ? clips : first ? [renamed(first, "reload")] : [];
|
|
4426
5440
|
}
|
|
5441
|
+
var FEED_SECONDS = 0.4;
|
|
4427
5442
|
function feedClip(context) {
|
|
4428
5443
|
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" })
|
|
5444
|
+
if (family === "revolver") return composeClip("feed", [], [cue(0, "move_in", { from: "chambers", next: true }), sound2(0.1, "shell_insert")], 0.24);
|
|
5445
|
+
if (family === "bolt" || family === "semiint") return composeClip("feed", [], [cue(0, "move_in", { from: "topLoad" }), sound2(0.2, "shell_insert")], FEED_SECONDS);
|
|
5446
|
+
if (isTube(context)) return composeClip("feed", [], [cue(0, "move_in", { from: "loadPort" }), sound2(0.2, "shell_insert")], FEED_SECONDS);
|
|
4432
5447
|
return void 0;
|
|
4433
5448
|
}
|
|
4434
5449
|
|
|
@@ -5060,6 +6075,7 @@ function applyCondition(kit, model, condition) {
|
|
|
5060
6075
|
}
|
|
5061
6076
|
|
|
5062
6077
|
// src/models/cartridge.ts
|
|
6078
|
+
import { markKitOrigin as markKitOrigin3 } from "@adatechnology/game-contracts";
|
|
5063
6079
|
var BOTTLENECK_RATIO = 0.8;
|
|
5064
6080
|
function cartridgeShape(caliber, dims) {
|
|
5065
6081
|
if (caliber.caseType === "shell" || caliber.rim === "shell") return "shell";
|
|
@@ -5071,7 +6087,7 @@ function cartridgeShape(caliber, dims) {
|
|
|
5071
6087
|
}
|
|
5072
6088
|
function buildCartridge(kit, dims, shape) {
|
|
5073
6089
|
const { M, cylinderY } = kit;
|
|
5074
|
-
const group = kit.group();
|
|
6090
|
+
const group = markKitOrigin3(kit.group(), { owner: "game-weapons-three", kind: "ammunition", id: shape });
|
|
5075
6091
|
const radius = dims.caseDiameter / 2;
|
|
5076
6092
|
const bulletRadius = dims.projectileDiameter / 2;
|
|
5077
6093
|
const caseLength = dims.caseLength;
|
|
@@ -5103,7 +6119,7 @@ function buildCartridge(kit, dims, shape) {
|
|
|
5103
6119
|
}
|
|
5104
6120
|
function buildCasing(kit, dims, shape) {
|
|
5105
6121
|
const { M, cylinderX } = kit;
|
|
5106
|
-
const group = kit.group();
|
|
6122
|
+
const group = markKitOrigin3(kit.group(), { owner: "game-weapons-three", kind: "ammunition", id: shape });
|
|
5107
6123
|
const radius = dims.caseDiameter / 2e3;
|
|
5108
6124
|
const length = dims.caseLength / 1e3;
|
|
5109
6125
|
if (shape === "shell") {
|
|
@@ -5119,7 +6135,7 @@ function buildCasing(kit, dims, shape) {
|
|
|
5119
6135
|
return { group, radius, length };
|
|
5120
6136
|
}
|
|
5121
6137
|
function buildRound(kit, dims, shape) {
|
|
5122
|
-
const group = kit.group();
|
|
6138
|
+
const group = markKitOrigin3(kit.group(), { owner: "game-weapons-three", kind: "ammunition", id: shape });
|
|
5123
6139
|
const cartridge = buildCartridge(kit, dims, shape);
|
|
5124
6140
|
cartridge.scale.setScalar(1e-3);
|
|
5125
6141
|
cartridge.rotation.z = -Math.PI / 2;
|
|
@@ -5131,6 +6147,7 @@ function buildRound(kit, dims, shape) {
|
|
|
5131
6147
|
import { createVfxRenderer } from "@adatechnology/game-vfx-three";
|
|
5132
6148
|
|
|
5133
6149
|
// src/fx/effects.ts
|
|
6150
|
+
import { MUZZLE_SMOKE } from "@adatechnology/game-weapons";
|
|
5134
6151
|
var FLASH_BY_CASE = { shell: 1.6, big: 2.6, tiny: 0.5, pistol: 0.8, rifle: 1.2 };
|
|
5135
6152
|
var VIEWMODEL_EFFECT_IDS = { flash: "viewmodel_muzzle_flash", smoke: "weapon_smoke_puff", backblast: "launcher_backblast", flameJet: "flame_jet" };
|
|
5136
6153
|
var vec = (vector) => [vector.x, vector.y, vector.z];
|
|
@@ -5139,8 +6156,9 @@ function createEffects(THREE, sink, random) {
|
|
|
5139
6156
|
const seed = () => 1 + Math.floor(random() * 2147483646);
|
|
5140
6157
|
const barrel = (orientation) => vec(new THREE.Vector3(1, 0, 0).applyQuaternion(orientation));
|
|
5141
6158
|
return {
|
|
5142
|
-
muzzleFlash(position, orientation, size) {
|
|
5143
|
-
|
|
6159
|
+
muzzleFlash(position, orientation, size, profile = MUZZLE_SMOKE.smokeless) {
|
|
6160
|
+
if (profile.puffs <= 0 && profile.flash <= 0) return;
|
|
6161
|
+
sink?.play({ effect: VIEWMODEL_EFFECT_IDS.flash, point: vec(position), direction: barrel(orientation), orientation: quaternion(orientation), seed: seed(), values: { size, ...profile } });
|
|
5144
6162
|
},
|
|
5145
6163
|
smoke(position, velocity, size, life, opacity) {
|
|
5146
6164
|
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 +6183,17 @@ var flameMaterials = (kit) => {
|
|
|
5165
6183
|
new THREE.MeshBasicMaterial({ color: 16773296, transparent: true, opacity: 0.9, blending: THREE.AdditiveBlending, depthWrite: false })
|
|
5166
6184
|
];
|
|
5167
6185
|
};
|
|
5168
|
-
function launchRocket(context) {
|
|
6186
|
+
function launchRocket(context, flyRound = true) {
|
|
5169
6187
|
const { kit, scene, parts, random } = context;
|
|
5170
6188
|
const { THREE } = kit;
|
|
5171
6189
|
const forward = context.forward();
|
|
5172
6190
|
const muzzle = context.positionOf(parts.muzzle);
|
|
5173
6191
|
const rear = context.positionOf(parts.rear);
|
|
5174
6192
|
context.effects.backblast(rear, context.orientation());
|
|
6193
|
+
if (!flyRound) {
|
|
6194
|
+
if (parts.round) parts.round.visible = false;
|
|
6195
|
+
return;
|
|
6196
|
+
}
|
|
5175
6197
|
const rocket = parts.makeRound ? parts.makeRound() : buildFlyingRocket(kit, parts.launcherType);
|
|
5176
6198
|
if (parts.round) parts.round.visible = false;
|
|
5177
6199
|
rocket.quaternion.copy(context.orientation());
|
|
@@ -5366,6 +6388,7 @@ var toWorld = (session, vector, scaled = false) => {
|
|
|
5366
6388
|
};
|
|
5367
6389
|
|
|
5368
6390
|
// src/fx/viewmodel-events.ts
|
|
6391
|
+
import { muzzleSmokeOf } from "@adatechnology/game-weapons";
|
|
5369
6392
|
var PI13 = Math.PI;
|
|
5370
6393
|
var UGL_FLASH = 1.6;
|
|
5371
6394
|
var restFlat = (object) => {
|
|
@@ -5471,7 +6494,11 @@ function createClipEventHandler(world, session) {
|
|
|
5471
6494
|
case "muzzle_flash":
|
|
5472
6495
|
if (data.at === "ugl") {
|
|
5473
6496
|
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
|
|
6497
|
+
} else if (parts.muzzle && world.layers.muzzleFlash && (session.def.action.kind === "firearm" || session.def.action.kind === "launcher")) {
|
|
6498
|
+
const profile = muzzleSmokeOf({ caliber: session.caliber, weapon: session.def, suppressed: session.suppressed });
|
|
6499
|
+
const size = session.def.action.kind === "launcher" ? 2.2 : FLASH_BY_CASE[session.caseType];
|
|
6500
|
+
effects.muzzleFlash(positionOf(parts.muzzle), root.getWorldQuaternion(new THREE.Quaternion()), size, profile);
|
|
6501
|
+
}
|
|
5475
6502
|
break;
|
|
5476
6503
|
case "eject_casing":
|
|
5477
6504
|
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 +6529,9 @@ function createClipEventHandler(world, session) {
|
|
|
5502
6529
|
case "breech_round":
|
|
5503
6530
|
breechRound();
|
|
5504
6531
|
break;
|
|
6532
|
+
case "sound":
|
|
6533
|
+
world.sound(session.id, String(data.id), session.soundProfile);
|
|
6534
|
+
break;
|
|
5505
6535
|
default:
|
|
5506
6536
|
break;
|
|
5507
6537
|
}
|
|
@@ -5525,10 +6555,321 @@ function createClipEventHandler(world, session) {
|
|
|
5525
6555
|
}
|
|
5526
6556
|
}
|
|
5527
6557
|
|
|
6558
|
+
// src/fx/hand-work.ts
|
|
6559
|
+
import { sampleClip } from "@adatechnology/game-contracts";
|
|
6560
|
+
|
|
6561
|
+
// src/data/hand-work.json
|
|
6562
|
+
var hand_work_default = {
|
|
6563
|
+
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.",
|
|
6564
|
+
timing: {
|
|
6565
|
+
lead: 0.08,
|
|
6566
|
+
tail: 0.06,
|
|
6567
|
+
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"
|
|
6568
|
+
},
|
|
6569
|
+
pistolRelease: {
|
|
6570
|
+
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.",
|
|
6571
|
+
weapons: []
|
|
6572
|
+
},
|
|
6573
|
+
akCharge: {
|
|
6574
|
+
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).",
|
|
6575
|
+
weapons: [
|
|
6576
|
+
"ak_47",
|
|
6577
|
+
"akm",
|
|
6578
|
+
"ak_74",
|
|
6579
|
+
"ak_12",
|
|
6580
|
+
"rpk",
|
|
6581
|
+
"saiga_12",
|
|
6582
|
+
"sks",
|
|
6583
|
+
"m14_ebr",
|
|
6584
|
+
"fn_fal",
|
|
6585
|
+
"fn_parafal",
|
|
6586
|
+
"hk_g3a3",
|
|
6587
|
+
"hk_psg_1",
|
|
6588
|
+
"svd_dragunov"
|
|
6589
|
+
]
|
|
6590
|
+
},
|
|
6591
|
+
works: [
|
|
6592
|
+
{
|
|
6593
|
+
family: "pistol",
|
|
6594
|
+
clips: [
|
|
6595
|
+
"reload_empty"
|
|
6596
|
+
],
|
|
6597
|
+
part: "slide",
|
|
6598
|
+
hand: "L",
|
|
6599
|
+
anchor: "slide_rear",
|
|
6600
|
+
style: "rack"
|
|
6601
|
+
},
|
|
6602
|
+
{
|
|
6603
|
+
family: "pistol",
|
|
6604
|
+
clips: [
|
|
6605
|
+
"reload_empty"
|
|
6606
|
+
],
|
|
6607
|
+
part: "slide",
|
|
6608
|
+
hand: "R",
|
|
6609
|
+
anchor: "thumb_release",
|
|
6610
|
+
style: "release"
|
|
6611
|
+
},
|
|
6612
|
+
{
|
|
6613
|
+
family: "rifle",
|
|
6614
|
+
clips: [
|
|
6615
|
+
"reload_empty"
|
|
6616
|
+
],
|
|
6617
|
+
part: "bolt",
|
|
6618
|
+
hand: "L",
|
|
6619
|
+
anchor: "rear_top",
|
|
6620
|
+
style: "ar"
|
|
6621
|
+
},
|
|
6622
|
+
{
|
|
6623
|
+
family: "rifle",
|
|
6624
|
+
clips: [
|
|
6625
|
+
"reload_empty"
|
|
6626
|
+
],
|
|
6627
|
+
part: "bolt",
|
|
6628
|
+
hand: "L",
|
|
6629
|
+
anchor: "side_right",
|
|
6630
|
+
style: "ak"
|
|
6631
|
+
},
|
|
6632
|
+
{
|
|
6633
|
+
family: "p90",
|
|
6634
|
+
clips: [
|
|
6635
|
+
"reload_empty"
|
|
6636
|
+
],
|
|
6637
|
+
part: "bolt",
|
|
6638
|
+
hand: "L",
|
|
6639
|
+
anchor: "rear_top",
|
|
6640
|
+
style: "ar"
|
|
6641
|
+
},
|
|
6642
|
+
{
|
|
6643
|
+
family: "belt",
|
|
6644
|
+
clips: [
|
|
6645
|
+
"reload"
|
|
6646
|
+
],
|
|
6647
|
+
part: "cover",
|
|
6648
|
+
hand: "L",
|
|
6649
|
+
anchor: "cover_top",
|
|
6650
|
+
merge: 1
|
|
6651
|
+
},
|
|
6652
|
+
{
|
|
6653
|
+
family: "boltmag",
|
|
6654
|
+
clips: [
|
|
6655
|
+
"reload",
|
|
6656
|
+
"reload_empty",
|
|
6657
|
+
"reload_one_by_one",
|
|
6658
|
+
"cycle"
|
|
6659
|
+
],
|
|
6660
|
+
part: "boltHandle",
|
|
6661
|
+
hand: "R",
|
|
6662
|
+
anchor: "top_handle"
|
|
6663
|
+
},
|
|
6664
|
+
{
|
|
6665
|
+
family: "bolt",
|
|
6666
|
+
clips: [
|
|
6667
|
+
"reload",
|
|
6668
|
+
"reload_empty",
|
|
6669
|
+
"reload_one_by_one",
|
|
6670
|
+
"feed"
|
|
6671
|
+
],
|
|
6672
|
+
hand: "R",
|
|
6673
|
+
anchor: "loading_port",
|
|
6674
|
+
whole: true,
|
|
6675
|
+
lead: 0.3,
|
|
6676
|
+
tail: 0.3
|
|
6677
|
+
},
|
|
6678
|
+
{
|
|
6679
|
+
family: "bolt",
|
|
6680
|
+
clips: [
|
|
6681
|
+
"reload",
|
|
6682
|
+
"reload_empty",
|
|
6683
|
+
"reload_one_by_one",
|
|
6684
|
+
"cycle"
|
|
6685
|
+
],
|
|
6686
|
+
part: "boltHandle",
|
|
6687
|
+
hand: "R",
|
|
6688
|
+
anchor: "top_handle"
|
|
6689
|
+
},
|
|
6690
|
+
{
|
|
6691
|
+
family: "pump",
|
|
6692
|
+
clips: [
|
|
6693
|
+
"feed"
|
|
6694
|
+
],
|
|
6695
|
+
hand: "R",
|
|
6696
|
+
anchor: "loading_port",
|
|
6697
|
+
whole: true,
|
|
6698
|
+
lead: 0.3,
|
|
6699
|
+
tail: 0.3
|
|
6700
|
+
},
|
|
6701
|
+
{
|
|
6702
|
+
family: "semi12",
|
|
6703
|
+
clips: [
|
|
6704
|
+
"feed"
|
|
6705
|
+
],
|
|
6706
|
+
hand: "R",
|
|
6707
|
+
anchor: "loading_port",
|
|
6708
|
+
whole: true,
|
|
6709
|
+
lead: 0.3,
|
|
6710
|
+
tail: 0.3
|
|
6711
|
+
},
|
|
6712
|
+
{
|
|
6713
|
+
family: "semiint",
|
|
6714
|
+
clips: [
|
|
6715
|
+
"reload",
|
|
6716
|
+
"reload_empty",
|
|
6717
|
+
"reload_one_by_one",
|
|
6718
|
+
"feed"
|
|
6719
|
+
],
|
|
6720
|
+
hand: "R",
|
|
6721
|
+
anchor: "loading_port",
|
|
6722
|
+
whole: true,
|
|
6723
|
+
lead: 0.3,
|
|
6724
|
+
tail: 0.3
|
|
6725
|
+
},
|
|
6726
|
+
{
|
|
6727
|
+
family: "lever",
|
|
6728
|
+
clips: [
|
|
6729
|
+
"feed"
|
|
6730
|
+
],
|
|
6731
|
+
hand: "R",
|
|
6732
|
+
anchor: "loading_port",
|
|
6733
|
+
whole: true,
|
|
6734
|
+
lead: 0.3,
|
|
6735
|
+
tail: 0.3
|
|
6736
|
+
},
|
|
6737
|
+
{
|
|
6738
|
+
family: "revolver",
|
|
6739
|
+
clips: [
|
|
6740
|
+
"reload",
|
|
6741
|
+
"reload_one_by_one"
|
|
6742
|
+
],
|
|
6743
|
+
part: "crane",
|
|
6744
|
+
hand: "L",
|
|
6745
|
+
anchor: "cylinder_side",
|
|
6746
|
+
merge: 1
|
|
6747
|
+
}
|
|
6748
|
+
]
|
|
6749
|
+
};
|
|
6750
|
+
|
|
6751
|
+
// src/fx/hand-work.ts
|
|
6752
|
+
var V2 = (THREE, x, y, z) => new THREE.Vector3(x, y, z);
|
|
6753
|
+
var boxIn = (THREE, root, part) => {
|
|
6754
|
+
const world = new THREE.Box3().setFromObject(part);
|
|
6755
|
+
const box2 = new THREE.Box3();
|
|
6756
|
+
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)));
|
|
6757
|
+
return box2;
|
|
6758
|
+
};
|
|
6759
|
+
var anchorsOf = (THREE, model, right) => {
|
|
6760
|
+
const { root, parts } = model;
|
|
6761
|
+
root.updateWorldMatrix(true, true);
|
|
6762
|
+
const palm = HAND_HOLD.palm;
|
|
6763
|
+
const anchorAt = (part, at, quaternion2) => ({ part, offset: part.worldToLocal(root.localToWorld(at.clone())), quaternion: quaternion2 });
|
|
6764
|
+
return (kind, partId) => {
|
|
6765
|
+
const part = partId ? parts[partId] ?? null : null;
|
|
6766
|
+
if (kind === "loading_port") {
|
|
6767
|
+
const port = parts.loadPort;
|
|
6768
|
+
if (!port) return null;
|
|
6769
|
+
const at = root.worldToLocal(port.getWorldPosition(V2(THREE, 0, 0, 0))).add(V2(THREE, 0, -palm - 4e-3, 0));
|
|
6770
|
+
return anchorAt(port, at, handFrame(THREE, V2(THREE, 0, 0, -1), V2(THREE, 0, -1, 0)));
|
|
6771
|
+
}
|
|
6772
|
+
if (kind === "thumb_release") {
|
|
6773
|
+
if (!right || !parts.slide) return null;
|
|
6774
|
+
return { part: right.part, offset: right.offset.clone().add(V2(THREE, 0, 0.012, 0)), quaternion: right.quaternion };
|
|
6775
|
+
}
|
|
6776
|
+
if (!part) return null;
|
|
6777
|
+
const b = boxIn(THREE, root, part);
|
|
6778
|
+
const c = b.getCenter(V2(THREE, 0, 0, 0));
|
|
6779
|
+
switch (kind) {
|
|
6780
|
+
case "slide_rear":
|
|
6781
|
+
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)));
|
|
6782
|
+
case "rear_top":
|
|
6783
|
+
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)));
|
|
6784
|
+
case "side_right":
|
|
6785
|
+
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)));
|
|
6786
|
+
case "top_handle":
|
|
6787
|
+
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)));
|
|
6788
|
+
case "cover_top":
|
|
6789
|
+
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)));
|
|
6790
|
+
case "cylinder_side":
|
|
6791
|
+
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)));
|
|
6792
|
+
default:
|
|
6793
|
+
return null;
|
|
6794
|
+
}
|
|
6795
|
+
};
|
|
6796
|
+
};
|
|
6797
|
+
var releasePistols = new Set(hand_work_default.pistolRelease.weapons);
|
|
6798
|
+
var akCharge = new Set(hand_work_default.akCharge.weapons);
|
|
6799
|
+
function handWorkOf(THREE, model, weaponId, right) {
|
|
6800
|
+
const family = model.parts.family;
|
|
6801
|
+
const anchors = anchorsOf(THREE, model, right);
|
|
6802
|
+
const style = family === "pistol" ? releasePistols.has(weaponId) ? "release" : "rack" : akCharge.has(weaponId) ? "ak" : "ar";
|
|
6803
|
+
const out = [];
|
|
6804
|
+
for (const entry of hand_work_default.works) {
|
|
6805
|
+
if (entry.family !== family || entry.style && entry.style !== style) continue;
|
|
6806
|
+
const anchor = anchors(entry.anchor, entry.part ?? null);
|
|
6807
|
+
if (!anchor) continue;
|
|
6808
|
+
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 } });
|
|
6809
|
+
}
|
|
6810
|
+
return out;
|
|
6811
|
+
}
|
|
6812
|
+
var segmentsCache = /* @__PURE__ */ new WeakMap();
|
|
6813
|
+
var motionOf = (clip, part, merge) => {
|
|
6814
|
+
const cache = segmentsCache.get(clip) ?? /* @__PURE__ */ new Map();
|
|
6815
|
+
segmentsCache.set(clip, cache);
|
|
6816
|
+
const key = `${part}|${merge}`;
|
|
6817
|
+
const found = cache.get(key);
|
|
6818
|
+
if (found) return found;
|
|
6819
|
+
const steps = 240;
|
|
6820
|
+
const dt = clip.duration / steps;
|
|
6821
|
+
const spans = [];
|
|
6822
|
+
let start = null;
|
|
6823
|
+
for (let i = 1; i <= steps; i++) {
|
|
6824
|
+
const a = sampleClip(clip, part, (i - 1) * dt);
|
|
6825
|
+
const b = sampleClip(clip, part, i * dt);
|
|
6826
|
+
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);
|
|
6827
|
+
if (moving && start === null) start = (i - 1) * dt;
|
|
6828
|
+
if (!moving && start !== null) {
|
|
6829
|
+
spans.push([start, (i - 1) * dt]);
|
|
6830
|
+
start = null;
|
|
6831
|
+
}
|
|
6832
|
+
}
|
|
6833
|
+
if (start !== null) spans.push([start, clip.duration]);
|
|
6834
|
+
const joined = [];
|
|
6835
|
+
for (const span of spans) {
|
|
6836
|
+
const last = joined[joined.length - 1];
|
|
6837
|
+
if (last && span[0] - last[1] <= merge * clip.duration) last[1] = span[1];
|
|
6838
|
+
else joined.push([span[0], span[1]]);
|
|
6839
|
+
}
|
|
6840
|
+
cache.set(key, joined);
|
|
6841
|
+
return joined;
|
|
6842
|
+
};
|
|
6843
|
+
var smooth = (x) => {
|
|
6844
|
+
const k = Math.min(1, Math.max(0, x));
|
|
6845
|
+
return k * k * (3 - 2 * k);
|
|
6846
|
+
};
|
|
6847
|
+
function workWeights(works, running) {
|
|
6848
|
+
return works.map((work) => {
|
|
6849
|
+
const lead = work.lead ?? hand_work_default.timing.lead;
|
|
6850
|
+
const tail = work.tail ?? hand_work_default.timing.tail;
|
|
6851
|
+
let best = 0;
|
|
6852
|
+
for (const { name, time, clip } of running) {
|
|
6853
|
+
if (!work.clips.includes(name)) continue;
|
|
6854
|
+
const spans = work.part ? motionOf(clip, work.part, work.merge) : [[0, clip.duration]];
|
|
6855
|
+
for (const [from, to] of spans) {
|
|
6856
|
+
const before = lead * clip.duration;
|
|
6857
|
+
const after = tail * clip.duration;
|
|
6858
|
+
const w = work.part ? smooth((time - (from - before)) / before) * smooth((to + after - time) / after) : smooth(time / before) * smooth((clip.duration - time) / after);
|
|
6859
|
+
best = Math.max(best, w);
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
return best;
|
|
6863
|
+
});
|
|
6864
|
+
}
|
|
6865
|
+
|
|
5528
6866
|
// src/fx/viewmodel.ts
|
|
5529
6867
|
var PI14 = Math.PI;
|
|
5530
6868
|
var HAND_OFFSET2 = [0.2, -0.2, -0.42];
|
|
6869
|
+
var HANDGUN_OFFSET = [0.1, -0.12, -0.44];
|
|
5531
6870
|
var HEAT_DISPLAY_MAX_FALLBACK = 120;
|
|
6871
|
+
var WALL_PULL = { margin: 0.03, max: 0.7, rate: 14, probe: 3 };
|
|
6872
|
+
var HAND_IN_VIEW = { floor: -0.88, max: 0.18, rate: 10 };
|
|
5532
6873
|
var LAUNCH_VOLUME = 0.6;
|
|
5533
6874
|
var fittedOf2 = (parts) => parts.map((part) => ({ id: part.id, mount: part.mount, ...part.integral ? { integral: true } : {}, ...part.on === void 0 ? {} : { on: part.on } }));
|
|
5534
6875
|
var collapsedStockPose = (move2) => {
|
|
@@ -5537,6 +6878,14 @@ var collapsedStockPose = (move2) => {
|
|
|
5537
6878
|
};
|
|
5538
6879
|
var underbarrelStep = (step, tool) => tool.underbarrelActive && step.type === "play" && (step.clip === "shot" || step.clip === "reload") ? { ...step, variant: "ugl" } : step;
|
|
5539
6880
|
var caliberOf = (arsenal, def) => def.action.kind === "firearm" ? arsenal.calibers.get(def.action.caliber) : void 0;
|
|
6881
|
+
var TRIGGER_FINGER = { safe: 0, ready: 0.5, pulled: 1, readySeconds: 1.5, burstSeconds: 0.14, tremor: 0.03 };
|
|
6882
|
+
var TRIGGER_TURN = 0.35;
|
|
6883
|
+
var indexCurl = (turn2, sinceShot) => {
|
|
6884
|
+
const ready = sinceShot < TRIGGER_FINGER.readySeconds ? TRIGGER_FINGER.ready : TRIGGER_FINGER.safe;
|
|
6885
|
+
const pull = Math.min(1, Math.max(0, turn2 / TRIGGER_TURN));
|
|
6886
|
+
const held = sinceShot < TRIGGER_FINGER.burstSeconds ? TRIGGER_FINGER.pulled - TRIGGER_FINGER.tremor * (0.5 + 0.5 * Math.sin(sinceShot * 220)) : 0;
|
|
6887
|
+
return Math.max(ready + (TRIGGER_FINGER.pulled - ready) * pull, held);
|
|
6888
|
+
};
|
|
5540
6889
|
function createViewmodel(options) {
|
|
5541
6890
|
const { THREE, scene, camera, arsenal, groundAt, audio } = options;
|
|
5542
6891
|
const kit = modelKit(THREE);
|
|
@@ -5559,6 +6908,15 @@ function createViewmodel(options) {
|
|
|
5559
6908
|
let condition = options.condition ?? "new";
|
|
5560
6909
|
let lastAmmo = 0;
|
|
5561
6910
|
let smokeDebt = 0;
|
|
6911
|
+
let secondsSinceShot = Number.POSITIVE_INFINITY;
|
|
6912
|
+
let hold = null;
|
|
6913
|
+
let reloading = false;
|
|
6914
|
+
let magazineHand = 0;
|
|
6915
|
+
let clock = 0;
|
|
6916
|
+
let lastShot = -Infinity;
|
|
6917
|
+
let triggerRest = 0;
|
|
6918
|
+
let feedCued = false;
|
|
6919
|
+
const rootQuaternion = new THREE.Quaternion();
|
|
5562
6920
|
const world = {
|
|
5563
6921
|
kit,
|
|
5564
6922
|
scene,
|
|
@@ -5568,9 +6926,15 @@ function createViewmodel(options) {
|
|
|
5568
6926
|
audio,
|
|
5569
6927
|
layers,
|
|
5570
6928
|
groundAt,
|
|
6929
|
+
sound: (weapon, cue2, profile) => {
|
|
6930
|
+
audio?.cue(cue2, profile);
|
|
6931
|
+
options.onSound?.({ weapon, cue: cue2, profile });
|
|
6932
|
+
},
|
|
5571
6933
|
schedule: (delay, seconds, step, done) => void tasks.push({ delay, seconds, elapsed: 0, step: (progress) => step(ease(progress)), done })
|
|
5572
6934
|
};
|
|
5573
6935
|
const heatMax = arsenal.rules.heat.displayMax || HEAT_DISPLAY_MAX_FALLBACK;
|
|
6936
|
+
const visual = arsenal.rules.heat.visual;
|
|
6937
|
+
const hasSuppressor = (parts) => parts.some((part) => arsenal.attachments.get(part.id)?.kind === "suppressor");
|
|
5574
6938
|
const release = () => {
|
|
5575
6939
|
if (!session) return;
|
|
5576
6940
|
session.attachments?.dispose();
|
|
@@ -5578,6 +6942,7 @@ function createViewmodel(options) {
|
|
|
5578
6942
|
player = null;
|
|
5579
6943
|
session = null;
|
|
5580
6944
|
tasks = [];
|
|
6945
|
+
hold = null;
|
|
5581
6946
|
};
|
|
5582
6947
|
const heavy = (current) => ({
|
|
5583
6948
|
kit,
|
|
@@ -5597,6 +6962,14 @@ function createViewmodel(options) {
|
|
|
5597
6962
|
const model = def ? buildWeaponModelById(THREE, weapon) : void 0;
|
|
5598
6963
|
if (!def || !model) return false;
|
|
5599
6964
|
release();
|
|
6965
|
+
hold = null;
|
|
6966
|
+
reloading = false;
|
|
6967
|
+
magazineHand = 0;
|
|
6968
|
+
reach = 0;
|
|
6969
|
+
pull = 0;
|
|
6970
|
+
lift = 0;
|
|
6971
|
+
handHeight = 0;
|
|
6972
|
+
handDepth = 0;
|
|
5600
6973
|
rigStock(THREE, model, def);
|
|
5601
6974
|
rigUnderbarrel(THREE, model, def);
|
|
5602
6975
|
const length = new THREE.Box3().setFromObject(model.root).getSize(new THREE.Vector3()).x;
|
|
@@ -5606,31 +6979,36 @@ function createViewmodel(options) {
|
|
|
5606
6979
|
holder.add(model.root);
|
|
5607
6980
|
const caliber = caliberOf(arsenal, def);
|
|
5608
6981
|
const dims = caliber?.cartridge;
|
|
6982
|
+
const hostTool = options.tool?.();
|
|
6983
|
+
const onTool = hostTool?.weapon === weapon ? hostTool : void 0;
|
|
6984
|
+
const fitted = onTool?.attachments ? fittedOf2(onTool.attachments) : [...def.fitted ?? []];
|
|
5609
6985
|
session = {
|
|
5610
6986
|
id: weapon,
|
|
5611
6987
|
def,
|
|
6988
|
+
caliber,
|
|
5612
6989
|
model,
|
|
5613
6990
|
holderScale: scale,
|
|
5614
6991
|
lengthMetres: length,
|
|
5615
6992
|
caseType: caliber?.caseType ?? "rifle",
|
|
6993
|
+
soundProfile: soundProfileOf(weapon, model.parts.family),
|
|
5616
6994
|
kit,
|
|
5617
6995
|
cartridge: caliber && dims ? { dims, shape: cartridgeShape(caliber, dims) } : null,
|
|
5618
6996
|
stuckCase: null,
|
|
5619
6997
|
nextChamber: 0,
|
|
5620
6998
|
attachments: null,
|
|
5621
|
-
attachmentsSignature: ""
|
|
6999
|
+
attachmentsSignature: "",
|
|
7000
|
+
suppressed: onTool?.suppressed ?? hasSuppressor(fitted)
|
|
5622
7001
|
};
|
|
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
7002
|
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
7003
|
session.attachmentsSignature = attachmentsKey(onTool?.attachments);
|
|
5628
7004
|
const action = def.action;
|
|
5629
7005
|
const reloadFamily = action.kind === "firearm" || action.kind === "flame" || action.kind === "launcher" ? action.family : void 0;
|
|
5630
7006
|
const rpm = action.kind === "firearm" ? action.rpm : 0;
|
|
5631
7007
|
const rig = createModelRig(model);
|
|
7008
|
+
const clips = buildToolClips({ parts: model.parts, family: model.parts.family, caseType: session.caseType, reloadFamily });
|
|
7009
|
+
feedCued = clips.feed?.events?.some((entry) => entry.type === "sound") ?? false;
|
|
5632
7010
|
player = createToolClipPlayer({
|
|
5633
|
-
clips
|
|
7011
|
+
clips,
|
|
5634
7012
|
rig,
|
|
5635
7013
|
onEvent: createClipEventHandler(world, session),
|
|
5636
7014
|
repeatInterval: () => rpm > 0 ? 60 / rpm : 0.1
|
|
@@ -5638,25 +7016,67 @@ function createViewmodel(options) {
|
|
|
5638
7016
|
if (model.parts.stockMove && onTool?.stock === "collapsed") rig.setPartPose("stock", collapsedStockPose(model.parts.stockMove));
|
|
5639
7017
|
applyCondition(kit, model, condition);
|
|
5640
7018
|
lastAmmo = options.tool?.().ammo ?? 0;
|
|
7019
|
+
hand.updateMatrixWorld(true);
|
|
7020
|
+
hold = holdOfModel(THREE, model, scale);
|
|
7021
|
+
hold = { ...hold, works: handWorkOf(THREE, model, weapon, hold.right) };
|
|
7022
|
+
triggerRest = model.parts.trigger?.rotation.z ?? 0;
|
|
7023
|
+
lastShot = -Infinity;
|
|
5641
7024
|
return true;
|
|
5642
7025
|
};
|
|
5643
7026
|
const syncAttachments = (current, tool) => {
|
|
5644
|
-
if (!current.attachments || tool?.weapon !== current.id
|
|
7027
|
+
if (!current.attachments || tool?.weapon !== current.id) return;
|
|
7028
|
+
if (tool.suppressed !== void 0) current.suppressed = tool.suppressed;
|
|
7029
|
+
if (!tool.attachments) return;
|
|
5645
7030
|
const signature = attachmentsKey(tool.attachments);
|
|
5646
7031
|
if (signature === current.attachmentsSignature) return;
|
|
5647
7032
|
current.attachmentsSignature = signature;
|
|
5648
|
-
|
|
7033
|
+
const fitted = fittedOf2(tool.attachments);
|
|
7034
|
+
if (tool.suppressed === void 0) current.suppressed = hasSuppressor(fitted);
|
|
7035
|
+
current.attachments.sync(fitted);
|
|
7036
|
+
};
|
|
7037
|
+
let reach = 0;
|
|
7038
|
+
let pull = 0;
|
|
7039
|
+
let lift = 0;
|
|
7040
|
+
let handHeight = 0;
|
|
7041
|
+
let handDepth = 0;
|
|
7042
|
+
const ahead = new THREE.Vector3();
|
|
7043
|
+
const up = new THREE.Vector3();
|
|
7044
|
+
const seen = new THREE.Vector3();
|
|
7045
|
+
const eyeTurn = new THREE.Quaternion();
|
|
7046
|
+
const corner = new THREE.Vector3();
|
|
7047
|
+
const bounds = new THREE.Box3();
|
|
7048
|
+
const measureReach = (current) => {
|
|
7049
|
+
bounds.setFromObject(current.model.root);
|
|
7050
|
+
let farthest = 0;
|
|
7051
|
+
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));
|
|
7052
|
+
return farthest;
|
|
5649
7053
|
};
|
|
5650
|
-
const place = () => {
|
|
5651
|
-
const
|
|
7054
|
+
const place = (dt = 0) => {
|
|
7055
|
+
const family = session?.model.parts.family;
|
|
7056
|
+
const [x, y, z] = family === "pistol" || family === "revolver" ? HANDGUN_OFFSET : HAND_OFFSET2;
|
|
5652
7057
|
hand.position.set(x, y, z).applyQuaternion(camera.quaternion).add(camera.position);
|
|
5653
7058
|
hand.quaternion.copy(camera.quaternion);
|
|
7059
|
+
if (session && handDepth > 0.05 && "fov" in camera) {
|
|
7060
|
+
const tanHalf = Math.tan((camera.fov ?? 70) * PI14 / 360);
|
|
7061
|
+
const want2 = Math.max(0, Math.min(HAND_IN_VIEW.max, lift + (HAND_IN_VIEW.floor - handHeight) * tanHalf * handDepth));
|
|
7062
|
+
lift += (want2 - lift) * Math.min(1, dt * HAND_IN_VIEW.rate);
|
|
7063
|
+
if (lift > 1e-4) hand.position.addScaledVector(up.set(0, 1, 0).applyQuaternion(camera.quaternion), lift);
|
|
7064
|
+
}
|
|
7065
|
+
hand.updateMatrixWorld(true);
|
|
7066
|
+
if (!options.contact || !session) return;
|
|
7067
|
+
ahead.set(0, 0, -1).applyQuaternion(camera.quaternion);
|
|
7068
|
+
if (reach === 0) reach = Math.max(0.01, measureReach(session));
|
|
7069
|
+
const free = options.contact(camera.position, ahead, WALL_PULL.probe);
|
|
7070
|
+
const want = free === null ? 0 : Math.max(0, Math.min(WALL_PULL.max, reach + WALL_PULL.margin - free));
|
|
7071
|
+
pull += (want - pull) * Math.min(1, dt * WALL_PULL.rate);
|
|
7072
|
+
if (pull < 1e-4) return;
|
|
7073
|
+
hand.position.addScaledVector(ahead, -pull);
|
|
5654
7074
|
hand.updateMatrixWorld(true);
|
|
5655
7075
|
};
|
|
5656
7076
|
const glow = (tool) => {
|
|
5657
7077
|
if (!session) return;
|
|
5658
7078
|
const heat = (tool?.heat ?? 0) * heatMax;
|
|
5659
|
-
const level = Math.max(0, Math.min(1, (heat -
|
|
7079
|
+
const level = Math.max(0, Math.min(1, (heat - visual.glowFrom) / visual.glowSpan));
|
|
5660
7080
|
for (const mesh of session.model.parts.glow) {
|
|
5661
7081
|
const material = mesh.material;
|
|
5662
7082
|
material.emissive.setRGB(level, 0.2 * level * level, 0.02 * level * level);
|
|
@@ -5664,56 +7084,73 @@ function createViewmodel(options) {
|
|
|
5664
7084
|
}
|
|
5665
7085
|
};
|
|
5666
7086
|
const handleShot = (current, event, tool) => {
|
|
7087
|
+
secondsSinceShot = 0;
|
|
7088
|
+
if (event.suppressed !== void 0) current.suppressed = event.suppressed;
|
|
7089
|
+
else if (tool.suppressed !== void 0) current.suppressed = tool.suppressed;
|
|
7090
|
+
else current.suppressed = hasSuppressor(tool.attachments ? fittedOf2(tool.attachments) : current.def.fitted ?? []);
|
|
5667
7091
|
if (tool.underbarrelActive && current.model.parts.uglMuzzle) {
|
|
5668
7092
|
audio?.play("explosion", LAUNCH_VOLUME, distance());
|
|
5669
7093
|
return;
|
|
5670
7094
|
}
|
|
5671
7095
|
const action = current.def.action;
|
|
5672
7096
|
if (action.kind === "launcher") {
|
|
5673
|
-
launchRocket(heavy(current));
|
|
7097
|
+
launchRocket(heavy(current), options.flyRocket !== false);
|
|
5674
7098
|
audio?.play("explosion", LAUNCH_VOLUME, distance());
|
|
5675
7099
|
}
|
|
5676
7100
|
if (action.kind !== "firearm") return;
|
|
5677
7101
|
const caliber = caliberOf(arsenal, current.def);
|
|
5678
7102
|
if (!caliber) return;
|
|
5679
|
-
audio?.shot(muzzleEnergy3(caliber) * (caliber.pellets ?? 1), current.def.category, { caliber, distance: distance(), ...
|
|
7103
|
+
audio?.shot(muzzleEnergy3(caliber) * (caliber.pellets ?? 1), current.def.category, { caliber, distance: distance(), ...current.suppressed ? { suppressed: true } : {} });
|
|
5680
7104
|
};
|
|
5681
7105
|
return {
|
|
5682
7106
|
equip,
|
|
5683
7107
|
unequip: release,
|
|
5684
7108
|
handle(event, tool) {
|
|
5685
|
-
if (event.type === "explosion") return;
|
|
7109
|
+
if (event.type === "explosion" || event.type === "player") return;
|
|
5686
7110
|
if (event.type === "tool") equip(event.weapon);
|
|
5687
7111
|
if (!session || !player || event.weapon !== session.id) return;
|
|
5688
7112
|
const current = session;
|
|
5689
|
-
if (event.type === "shot")
|
|
5690
|
-
|
|
7113
|
+
if (event.type === "shot") {
|
|
7114
|
+
lastShot = clock;
|
|
7115
|
+
handleShot(current, event, tool);
|
|
7116
|
+
} else if (event.type === "reload" && event.phase === "start") {
|
|
7117
|
+
current.nextChamber = 0;
|
|
7118
|
+
reloading = true;
|
|
7119
|
+
} else if (event.type === "reload" && event.phase === "end") reloading = false;
|
|
5691
7120
|
else if (event.type === "clear" && event.phase === "end") current.stuckCase = null;
|
|
5692
7121
|
const mechanical = weaponEventOf(event);
|
|
5693
|
-
|
|
7122
|
+
const cued = mechanical?.type === "feed" ? feedCued : mechanical?.type === "reload_stage" && (player.running.some(({ clip }) => clip.events?.some((entry) => entry.type === "sound")) ?? false);
|
|
7123
|
+
if (mechanical && !cued) audio?.event(mechanical);
|
|
5694
7124
|
const step = toolAnimationStep(event);
|
|
5695
7125
|
if (!step) return;
|
|
5696
7126
|
const last = step.type === "play" && step.clip === "shot" && current.model.parts.family === "pistol" && tool.ammo === 0;
|
|
5697
7127
|
player.apply(last ? { ...step, variant: "last" } : underbarrelStep(step, tool));
|
|
5698
7128
|
},
|
|
5699
7129
|
update(dt) {
|
|
7130
|
+
clock += dt;
|
|
5700
7131
|
if (session) {
|
|
5701
|
-
place();
|
|
7132
|
+
place(dt);
|
|
5702
7133
|
player?.update(dt);
|
|
7134
|
+
const wantsMagazine = reloading || options.tool?.().reloading === true;
|
|
7135
|
+
magazineHand += ((wantsMagazine ? 1 : 0) - magazineHand) * Math.min(1, dt * HAND_HOLD.magRate);
|
|
5703
7136
|
const tool = options.tool?.();
|
|
5704
7137
|
syncAttachments(session, tool);
|
|
5705
7138
|
session.attachments?.update();
|
|
5706
7139
|
glow(tool);
|
|
5707
7140
|
if (tool && session.def.action.kind === "flame") {
|
|
5708
|
-
if (tool.ammo < lastAmmo)
|
|
7141
|
+
if (tool.ammo < lastAmmo) {
|
|
7142
|
+
flameJet(heavy(session), dt, session.model.parts.launcherType === "flame" ? 1 : 0.6);
|
|
7143
|
+
lastShot = clock;
|
|
7144
|
+
}
|
|
5709
7145
|
lastAmmo = tool.ammo;
|
|
5710
7146
|
}
|
|
5711
|
-
|
|
5712
|
-
|
|
7147
|
+
secondsSinceShot += dt;
|
|
7148
|
+
if (tool && tool.heat !== void 0 && tool.heat * heatMax > visual.smokeFrom && secondsSinceShot < visual.smokeSeconds && session.model.parts.muzzle) {
|
|
7149
|
+
smokeDebt += dt * (tool.heat * heatMax - visual.smokeFrom) * 0.1;
|
|
5713
7150
|
while (smokeDebt > 1) {
|
|
5714
7151
|
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.
|
|
7152
|
+
const k = 0.5 + (tool.heat * heatMax - visual.smokeFrom) / 60;
|
|
7153
|
+
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
7154
|
}
|
|
5718
7155
|
}
|
|
5719
7156
|
}
|
|
@@ -5734,9 +7171,25 @@ function createViewmodel(options) {
|
|
|
5734
7171
|
condition = next;
|
|
5735
7172
|
if (session) applyCondition(kit, session.model, condition);
|
|
5736
7173
|
},
|
|
7174
|
+
handTargets() {
|
|
7175
|
+
if (!session || !hold) return null;
|
|
7176
|
+
hand.updateMatrixWorld(true);
|
|
7177
|
+
const targets = targetsOf(THREE, hold, session.model.root.getWorldQuaternion(rootQuaternion), magazineHand, hold.works ? workWeights(hold.works, player?.running ?? []) : void 0);
|
|
7178
|
+
const trigger = session.model.parts.trigger;
|
|
7179
|
+
if (trigger) targets.fingers = { index: indexCurl(trigger.rotation.z - triggerRest, clock - lastShot) };
|
|
7180
|
+
if (targets.R && "fov" in camera) {
|
|
7181
|
+
seen.set(...targets.R.position).sub(camera.position).applyQuaternion(eyeTurn.copy(camera.quaternion).invert());
|
|
7182
|
+
handDepth = -seen.z;
|
|
7183
|
+
handHeight = handDepth > 0.05 ? seen.y / handDepth / Math.tan((camera.fov ?? 70) * PI14 / 360) : 0;
|
|
7184
|
+
}
|
|
7185
|
+
return targets;
|
|
7186
|
+
},
|
|
5737
7187
|
get inHand() {
|
|
5738
7188
|
return session?.id ?? null;
|
|
5739
7189
|
},
|
|
7190
|
+
get clips() {
|
|
7191
|
+
return (player?.running ?? []).map(({ name, clip }) => ({ name, seconds: clip.duration }));
|
|
7192
|
+
},
|
|
5740
7193
|
get casings() {
|
|
5741
7194
|
return particles.count("casing");
|
|
5742
7195
|
},
|
|
@@ -5759,6 +7212,7 @@ function createWeaponFx(options) {
|
|
|
5759
7212
|
const model = createViewmodel({
|
|
5760
7213
|
layers,
|
|
5761
7214
|
...options.soundDistance ? { soundDistance: options.soundDistance } : {},
|
|
7215
|
+
...options.onSound ? { onSound: options.onSound } : {},
|
|
5762
7216
|
THREE: options.THREE,
|
|
5763
7217
|
scene: options.scene,
|
|
5764
7218
|
camera: options.camera,
|
|
@@ -5770,11 +7224,13 @@ function createWeaponFx(options) {
|
|
|
5770
7224
|
...options.condition ? { condition: options.condition } : {},
|
|
5771
7225
|
...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
|
|
5772
7226
|
...options.raycast ? { raycast: options.raycast } : {},
|
|
5773
|
-
...options.
|
|
7227
|
+
...options.contact ? { contact: options.contact } : {},
|
|
7228
|
+
...options.effects ? { effects: options.effects } : {},
|
|
7229
|
+
...options.flyRocket === void 0 ? {} : { flyRocket: options.flyRocket }
|
|
5774
7230
|
});
|
|
5775
7231
|
return {
|
|
5776
7232
|
handle(event, tool) {
|
|
5777
|
-
if (event.type === "explosion") return;
|
|
7233
|
+
if (event.type === "explosion" || event.type === "player") return;
|
|
5778
7234
|
if (event.type === "tool") {
|
|
5779
7235
|
if (hasWeaponModel(event.weapon)) box2.unequip();
|
|
5780
7236
|
else model.unequip();
|
|
@@ -5787,10 +7243,14 @@ function createWeaponFx(options) {
|
|
|
5787
7243
|
model.update(dt);
|
|
5788
7244
|
},
|
|
5789
7245
|
setCondition: (condition) => model.setCondition(condition),
|
|
7246
|
+
handTargets: () => model.handTargets() ?? box2.handTargets(),
|
|
5790
7247
|
layers,
|
|
5791
7248
|
get inHand() {
|
|
5792
7249
|
return model.inHand ?? box2.inHand;
|
|
5793
7250
|
},
|
|
7251
|
+
get clips() {
|
|
7252
|
+
return model.clips;
|
|
7253
|
+
},
|
|
5794
7254
|
get casings() {
|
|
5795
7255
|
return box2.casings + model.casings;
|
|
5796
7256
|
},
|
|
@@ -6536,21 +7996,34 @@ function weaponThumbnailUrl(id, base = DEFAULT_THUMBNAIL_BASE) {
|
|
|
6536
7996
|
}
|
|
6537
7997
|
|
|
6538
7998
|
// src/world-explosives.ts
|
|
7999
|
+
import { buildItem as buildItem2 } from "@adatechnology/game-items-three";
|
|
6539
8000
|
var WORLD_EXPLOSIVE_PREFIX = "world_";
|
|
8001
|
+
var P13_VALVE_MESH = "valve";
|
|
8002
|
+
var P13_VALVE_COLOR = 12095802;
|
|
8003
|
+
var P13_WALL = 25e-4;
|
|
6540
8004
|
var SHAPES2 = {
|
|
6541
8005
|
fuel_barrel: { shape: { kind: "cylinder", radius: 0.3, height: 0.9 }, position: [0, 0.45, 0], color: 3104666 },
|
|
6542
|
-
propane_p13: {
|
|
8006
|
+
propane_p13: {
|
|
8007
|
+
shape: { kind: "cylinder", radius: 0.176, height: 0.49 },
|
|
8008
|
+
position: [0, 0.245, 0],
|
|
8009
|
+
color: 3497862,
|
|
8010
|
+
model: { id: "lpg_cylinder_13kg", category: "fuel", size: [0.35299, 0.49, 0.35844], model: "lpg_cylinder_13kg" },
|
|
8011
|
+
wall: P13_WALL
|
|
8012
|
+
},
|
|
6543
8013
|
car_fuel_tank: { shape: { kind: "box", size: [0.9, 0.25, 0.5] }, position: [0, 0.4, 0], color: 4935509 }
|
|
6544
8014
|
};
|
|
6545
8015
|
var WORLD_EXPLOSIVE_IDS = Object.keys(SHAPES2);
|
|
6546
8016
|
var SHELL = { thickness: 1e-3, fill: 0.03 };
|
|
6547
|
-
var explosivePart = (def, shape) => ({
|
|
8017
|
+
var explosivePart = (def, shape, scale) => ({
|
|
6548
8018
|
id: "tank",
|
|
6549
8019
|
material: "steel",
|
|
6550
8020
|
shape: shape.shape,
|
|
6551
8021
|
position: shape.position,
|
|
6552
|
-
...SHELL,
|
|
6553
|
-
|
|
8022
|
+
...shape.wall === void 0 ? SHELL : { thickness: shape.wall },
|
|
8023
|
+
// a real mass (the vessel and what it holds) replaces the hollow-sheet density
|
|
8024
|
+
...def.mass !== void 0 && shape.wall !== void 0 ? { mass: def.mass } : {},
|
|
8025
|
+
...def.leak ? { contains: { fluid: def.leak.fluid, liters: def.leak.liters, pressurized: def.leak.pressurized } } : {},
|
|
8026
|
+
explodes: { energy: def.energy * scale, radius: def.injuryRadius * scale, lethalRadius: def.lethalRadius * scale, effect: def.id, ...def.chain ? { chain: { ...def.chain } } : {} }
|
|
6554
8027
|
});
|
|
6555
8028
|
var meshOf = (THREE, shape) => {
|
|
6556
8029
|
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 +8035,55 @@ var meshOf = (THREE, shape) => {
|
|
|
6562
8035
|
mesh.receiveShadow = true;
|
|
6563
8036
|
return mesh;
|
|
6564
8037
|
};
|
|
6565
|
-
var
|
|
8038
|
+
var modelOf = (THREE, shape, wear = 0) => {
|
|
8039
|
+
if (!shape.model) return void 0;
|
|
8040
|
+
const { root, kind } = buildItem2(THREE, shape.model, { wear });
|
|
8041
|
+
if (kind !== "model") return void 0;
|
|
8042
|
+
root.traverse((node) => {
|
|
8043
|
+
const mesh = node;
|
|
8044
|
+
if (!mesh.isMesh) return;
|
|
8045
|
+
mesh.userData.destructionPart = "tank";
|
|
8046
|
+
mesh.castShadow = true;
|
|
8047
|
+
mesh.receiveShadow = true;
|
|
8048
|
+
const material = mesh.material;
|
|
8049
|
+
if (material.color?.getHex() === P13_VALVE_COLOR) mesh.name = P13_VALVE_MESH;
|
|
8050
|
+
});
|
|
8051
|
+
return root;
|
|
8052
|
+
};
|
|
8053
|
+
var worldExplosiveValve = (THREE, root) => {
|
|
8054
|
+
const valve = root.getObjectByName(P13_VALVE_MESH);
|
|
8055
|
+
if (!valve) return void 0;
|
|
8056
|
+
root.updateMatrixWorld(true);
|
|
8057
|
+
const centre = new THREE.Box3().setFromObject(valve).getCenter(new THREE.Vector3());
|
|
8058
|
+
return [centre.x, centre.y, centre.z];
|
|
8059
|
+
};
|
|
8060
|
+
var worldExplosiveShell = (THREE, defId) => {
|
|
8061
|
+
const shape = SHAPES2[defId];
|
|
8062
|
+
const model = shape ? modelOf(THREE, shape, 2) : void 0;
|
|
8063
|
+
if (!model) return void 0;
|
|
8064
|
+
model.traverse((node) => {
|
|
8065
|
+
const mesh = node;
|
|
8066
|
+
if (!mesh.isMesh) return;
|
|
8067
|
+
delete mesh.userData.destructionPart;
|
|
8068
|
+
mesh.material.color.multiplyScalar(0.35);
|
|
8069
|
+
});
|
|
8070
|
+
model.name = `${WORLD_EXPLOSIVE_PREFIX}${defId}_shell`;
|
|
8071
|
+
return model;
|
|
8072
|
+
};
|
|
8073
|
+
var worldExplosiveItem = (def, name, options = {}) => {
|
|
6566
8074
|
const shape = SHAPES2[def.id];
|
|
6567
8075
|
if (!shape) return void 0;
|
|
6568
|
-
const
|
|
8076
|
+
const scale = () => options.scale?.() ?? 1;
|
|
8077
|
+
const spec = () => ({ name, parts: [explosivePart(def, shape, scale())], joints: [], tags: ["explosive"] });
|
|
6569
8078
|
return {
|
|
6570
8079
|
id: `${WORLD_EXPLOSIVE_PREFIX}${def.id}`,
|
|
6571
8080
|
name,
|
|
6572
8081
|
build: (THREE) => {
|
|
6573
|
-
const
|
|
6574
|
-
|
|
8082
|
+
const T = THREE;
|
|
8083
|
+
const model = modelOf(T, shape);
|
|
8084
|
+
if (model) return model;
|
|
8085
|
+
const group = new T.Group();
|
|
8086
|
+
group.add(meshOf(T, shape));
|
|
6575
8087
|
return group;
|
|
6576
8088
|
},
|
|
6577
8089
|
spec
|
|
@@ -6623,6 +8135,100 @@ function createWorldEffects(options) {
|
|
|
6623
8135
|
};
|
|
6624
8136
|
}
|
|
6625
8137
|
|
|
8138
|
+
// src/scenarios.ts
|
|
8139
|
+
var TARGET_Z = -4;
|
|
8140
|
+
var SHOOTER_Z = -1.5;
|
|
8141
|
+
function weaponsScenarios(locale) {
|
|
8142
|
+
const pt = locale.toLowerCase().startsWith("pt");
|
|
8143
|
+
const T = (p, e) => pt ? p : e;
|
|
8144
|
+
const faceY = (id) => id === "target_steel_plate" ? 1.35 : id === "target_plywood" ? 1.5 : 1.2;
|
|
8145
|
+
const aimAt = (room, id) => room.look([0, faceY(id) + 0.05, SHOOTER_Z], [0, faceY(id), TARGET_Z]);
|
|
8146
|
+
return [
|
|
8147
|
+
{
|
|
8148
|
+
id: "target_steel_heavy_through_hole",
|
|
8149
|
+
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"),
|
|
8150
|
+
run: ({ room, step, expect }) => {
|
|
8151
|
+
const id = room.spawn("target_steel_plate", [0, 0, TARGET_Z]).objectId;
|
|
8152
|
+
room.setTool("pistol_9mm");
|
|
8153
|
+
room.waitUntilReady();
|
|
8154
|
+
aimAt(room, "target_steel_plate");
|
|
8155
|
+
room.use();
|
|
8156
|
+
step(0.3);
|
|
8157
|
+
const pistol = room.world.part(id, "face");
|
|
8158
|
+
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)`));
|
|
8159
|
+
room.setTool("barrett_m82a1");
|
|
8160
|
+
room.waitUntilReady();
|
|
8161
|
+
aimAt(room, "target_steel_plate");
|
|
8162
|
+
room.use();
|
|
8163
|
+
step(0.3);
|
|
8164
|
+
const rifle = room.world.part(id, "face");
|
|
8165
|
+
const hole = rifle?.holes.find((h) => h.through);
|
|
8166
|
+
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 });
|
|
8167
|
+
}
|
|
8168
|
+
},
|
|
8169
|
+
{
|
|
8170
|
+
id: "target_concrete_stops_bullet",
|
|
8171
|
+
name: T("Alvo de concreto: um 9 mm n\xE3o atravessa o bloco", "Concrete target: a 9 mm does not go through the block"),
|
|
8172
|
+
run: ({ room, step, expect }) => {
|
|
8173
|
+
const id = room.spawn("target_concrete", [0, 0, TARGET_Z]).objectId;
|
|
8174
|
+
room.setTool("pistol_9mm");
|
|
8175
|
+
room.waitUntilReady();
|
|
8176
|
+
aimAt(room, "target_concrete");
|
|
8177
|
+
room.use();
|
|
8178
|
+
step(0.2);
|
|
8179
|
+
const face = room.world.part(id, "face");
|
|
8180
|
+
const through = face?.holes.some((h) => h.through) ?? true;
|
|
8181
|
+
const marks = (face?.holes.length ?? 0) + (face?.dents.length ?? 0) + (face?.cracks.length ?? 0);
|
|
8182
|
+
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 });
|
|
8183
|
+
}
|
|
8184
|
+
},
|
|
8185
|
+
{
|
|
8186
|
+
id: "target_plywood_shotgun_holes",
|
|
8187
|
+
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"),
|
|
8188
|
+
run: ({ room, step, expect }) => {
|
|
8189
|
+
const id = room.spawn("target_plywood", [0, 0, TARGET_Z]).objectId;
|
|
8190
|
+
room.setTool("shotgun_pump_12");
|
|
8191
|
+
room.waitUntilReady();
|
|
8192
|
+
aimAt(room, "target_plywood");
|
|
8193
|
+
room.use();
|
|
8194
|
+
step(1.1);
|
|
8195
|
+
const holes = room.world.part(id, "face")?.holes.length ?? 0;
|
|
8196
|
+
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 });
|
|
8197
|
+
}
|
|
8198
|
+
},
|
|
8199
|
+
{
|
|
8200
|
+
id: "target_steel_hammer_dents",
|
|
8201
|
+
name: T("Alvo de a\xE7o: a marreta amassa a chapa sem romper", "Steel target: the hammer dents the plate without breaking it"),
|
|
8202
|
+
run: ({ room, step, expect }) => {
|
|
8203
|
+
const id = room.spawn("target_steel_plate", [0, 0, TARGET_Z]).objectId;
|
|
8204
|
+
room.setTool("hammer");
|
|
8205
|
+
room.waitUntilReady();
|
|
8206
|
+
room.look([0, 1.4, TARGET_Z + 0.65], [0, 1.35, TARGET_Z]);
|
|
8207
|
+
let swings = 0;
|
|
8208
|
+
for (; swings < 8 && (room.world.part(id, "face")?.dents.length ?? 0) === 0; swings++) {
|
|
8209
|
+
room.use();
|
|
8210
|
+
step(1);
|
|
8211
|
+
}
|
|
8212
|
+
const face = room.world.part(id, "face");
|
|
8213
|
+
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 });
|
|
8214
|
+
}
|
|
8215
|
+
},
|
|
8216
|
+
{
|
|
8217
|
+
id: "target_steel_torch_melts",
|
|
8218
|
+
name: T("Alvo de a\xE7o: o ma\xE7arico de corte derrete a chapa", "Steel target: the cutting torch melts the plate"),
|
|
8219
|
+
run: ({ room, expect }) => {
|
|
8220
|
+
const id = room.spawn("target_steel_plate", [0, 0, TARGET_Z]).objectId;
|
|
8221
|
+
room.setTool("cutting_torch");
|
|
8222
|
+
room.waitUntilReady();
|
|
8223
|
+
room.look([0, faceY("target_steel_plate") + 0.05, TARGET_Z + 0.2], [0, faceY("target_steel_plate"), TARGET_Z]);
|
|
8224
|
+
room.use(20);
|
|
8225
|
+
const face = room.world.part(id, "face");
|
|
8226
|
+
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 });
|
|
8227
|
+
}
|
|
8228
|
+
}
|
|
8229
|
+
];
|
|
8230
|
+
}
|
|
8231
|
+
|
|
6626
8232
|
// src/area.ts
|
|
6627
8233
|
var WEAPON_ITEM_PREFIX = "weapon_";
|
|
6628
8234
|
var COUNTER_ITEM_ID = "counter_weapons";
|
|
@@ -6658,6 +8264,10 @@ var TEXT = {
|
|
|
6658
8264
|
counter: "Weapon counter",
|
|
6659
8265
|
targets: { target_steel_plate: "Steel plate target", target_plywood: "Plywood target", target_concrete: "Concrete block target" },
|
|
6660
8266
|
reloadStart: "Reload started: {weapon} ({ammo}/{capacity})",
|
|
8267
|
+
reloadClip: "Reload of {weapon}: {kind}, clip {clip} ({seconds} s)",
|
|
8268
|
+
soundCue: "Sound: {cue} ({weapon})",
|
|
8269
|
+
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" },
|
|
8270
|
+
reloadKinds: { tactical: "tactical (round chambered, no pull)", empty: "empty chamber (works the action)", one_by_one: "one by one", full: "speed loader" },
|
|
6661
8271
|
reloadEnd: "Reload done: {weapon} ({ammo}/{capacity})",
|
|
6662
8272
|
worldExplosives: { fuel_barrel: "Fuel barrel", propane_p13: "Propane cylinder (P13)", car_fuel_tank: "Car fuel tank" },
|
|
6663
8273
|
mountedM134: "M134 Minigun on its tripod",
|
|
@@ -6680,6 +8290,10 @@ var TEXT = {
|
|
|
6680
8290
|
counter: "Balc\xE3o de armas",
|
|
6681
8291
|
targets: { target_steel_plate: "Alvo: chapa de a\xE7o", target_plywood: "Alvo: compensado", target_concrete: "Alvo: bloco de concreto" },
|
|
6682
8292
|
reloadStart: "Recarga iniciada: {weapon} ({ammo}/{capacity})",
|
|
8293
|
+
reloadClip: "Recarga de {weapon}: {kind}, clipe {clip} ({seconds} s)",
|
|
8294
|
+
soundCue: "Som: {cue} ({weapon})",
|
|
8295
|
+
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" },
|
|
8296
|
+
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
8297
|
reloadEnd: "Recarga completa: {weapon} ({ammo}/{capacity})",
|
|
6684
8298
|
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
8299
|
mountedM134: "M134 Minigun no trip\xE9",
|
|
@@ -6699,23 +8313,20 @@ var TEXT = {
|
|
|
6699
8313
|
};
|
|
6700
8314
|
var textOf = (locale) => TEXT[locale] ?? TEXT.en;
|
|
6701
8315
|
var fill = (template, vars) => template.replace(/\{(\w+)\}/g, (_, key) => String(vars[key] ?? ""));
|
|
6702
|
-
var
|
|
6703
|
-
|
|
8316
|
+
var COUNTER_FURNITURE = "buffet";
|
|
8317
|
+
var counterItem = (name) => {
|
|
8318
|
+
const counter = (THREE) => furnitureSpec(THREE, COUNTER_FURNITURE, { w: COUNTER.depth, d: COUNTER.length, h: COUNTER.height });
|
|
6704
8319
|
return {
|
|
6705
|
-
|
|
6706
|
-
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
joints: [
|
|
6713
|
-
{ a: "top", b: "side_a", kind: "screw" },
|
|
6714
|
-
{ a: "top", b: "side_b", kind: "screw" }
|
|
6715
|
-
],
|
|
6716
|
-
tags: ["furniture"]
|
|
8320
|
+
id: COUNTER_ITEM_ID,
|
|
8321
|
+
name,
|
|
8322
|
+
build: (THREE) => {
|
|
8323
|
+
const piece = counter(THREE);
|
|
8324
|
+
const root = piece.build(THREE);
|
|
8325
|
+
root.userData.counterSpec = { ...piece.spec, name, tags: [...piece.spec.tags ?? [], "furniture"] };
|
|
8326
|
+
return root;
|
|
6717
8327
|
},
|
|
6718
|
-
|
|
8328
|
+
spec: (root) => root.userData.counterSpec,
|
|
8329
|
+
movable: false
|
|
6719
8330
|
};
|
|
6720
8331
|
};
|
|
6721
8332
|
var targetProp = (id, name) => {
|
|
@@ -6738,7 +8349,7 @@ var targetProp = (id, name) => {
|
|
|
6738
8349
|
};
|
|
6739
8350
|
var weaponItem = (def, name) => {
|
|
6740
8351
|
const id = `${WEAPON_ITEM_PREFIX}${def.id}`;
|
|
6741
|
-
if (!hasWeaponModel(def.id)) return asItem(id, name, weaponProp(def, name), { weapon: def.id, movable: true });
|
|
8352
|
+
if (!hasWeaponModel(def.id) && !itemModelOfWeapon(def.id)) return asItem(id, name, weaponProp(def, name), { weapon: def.id, movable: true });
|
|
6742
8353
|
return {
|
|
6743
8354
|
id,
|
|
6744
8355
|
name,
|
|
@@ -6756,6 +8367,10 @@ var mountedM134Item = (def, name) => ({
|
|
|
6756
8367
|
weapon: def.id,
|
|
6757
8368
|
movable: false
|
|
6758
8369
|
});
|
|
8370
|
+
var targetItem = (id, name) => {
|
|
8371
|
+
const prop = targetProp(id, name);
|
|
8372
|
+
return { id, name, build: (THREE) => markKitOrigin4(buildProp(THREE, prop), { owner: "game-weapons-three", kind: "target", id }), spec: prop.spec, movable: false };
|
|
8373
|
+
};
|
|
6759
8374
|
var asItem = (id, name, prop, extra = {}) => ({
|
|
6760
8375
|
id,
|
|
6761
8376
|
name,
|
|
@@ -6827,11 +8442,12 @@ function createWeaponsTestModule(options = {}) {
|
|
|
6827
8442
|
},
|
|
6828
8443
|
items: () => [
|
|
6829
8444
|
...weaponDefs.map((def) => withThumbnail(weaponItem(def, arsenal.name(def.id)), def.id)),
|
|
6830
|
-
|
|
6831
|
-
...TARGETS.map((target) =>
|
|
8445
|
+
counterItem(text.counter),
|
|
8446
|
+
...TARGETS.map((target) => targetItem(target.id, text.targets[target.id] ?? target.id)),
|
|
6832
8447
|
...worldExplosives.map((item) => withThumbnail(item, item.id.slice(WORLD_EXPLOSIVE_PREFIX.length))),
|
|
6833
8448
|
...mountedM134.map((item) => withThumbnail(item, MOUNTED_M134.weaponId))
|
|
6834
8449
|
],
|
|
8450
|
+
scenarios: (scenarioLocale) => weaponsScenarios(scenarioLocale ?? locale),
|
|
6835
8451
|
area: () => ({
|
|
6836
8452
|
placements: [
|
|
6837
8453
|
...weaponsLayout(weaponIds, pitchOf).filter((placement) => !placement.item.startsWith(WORLD_EXPLOSIVE_PREFIX) || worldExplosiveIds.has(placement.item)),
|
|
@@ -6874,7 +8490,10 @@ function createWeaponsTestModule(options = {}) {
|
|
|
6874
8490
|
audio.dispose();
|
|
6875
8491
|
audio = createWeaponAudio(void 0, { ...seedOption, engine: lentEngine });
|
|
6876
8492
|
}
|
|
8493
|
+
let simulatedMs = 0;
|
|
8494
|
+
const offClock = lent.onFrame((dt) => simulatedMs += dt * 1e3);
|
|
6877
8495
|
const created = createWeaponFx({
|
|
8496
|
+
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
8497
|
THREE: lent.THREE,
|
|
6879
8498
|
scene: lent.scene,
|
|
6880
8499
|
camera: lent.camera,
|
|
@@ -6884,10 +8503,13 @@ function createWeaponsTestModule(options = {}) {
|
|
|
6884
8503
|
tool: lent.tool,
|
|
6885
8504
|
soundDistance: () => soundDistance,
|
|
6886
8505
|
raycast: createLaserRaycast(lent.THREE, lent.scene, lent.camera),
|
|
8506
|
+
// 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)
|
|
8507
|
+
contact: createLaserRaycast(lent.THREE, lent.scene, lent.camera, { now: () => simulatedMs }),
|
|
6887
8508
|
...lent.effects ? { effects: lent.effects } : {},
|
|
6888
8509
|
...options.attachmentModels ? { attachmentModels: options.attachmentModels } : {},
|
|
6889
8510
|
...options.condition ? { condition: options.condition } : {},
|
|
6890
|
-
...options.seed === void 0 ? {} : { seed: options.seed }
|
|
8511
|
+
...options.seed === void 0 ? {} : { seed: options.seed },
|
|
8512
|
+
...options.flyRocket === void 0 ? {} : { flyRocket: options.flyRocket }
|
|
6891
8513
|
});
|
|
6892
8514
|
fx = created;
|
|
6893
8515
|
const camera = lent.camera;
|
|
@@ -6918,15 +8540,25 @@ function createWeaponsTestModule(options = {}) {
|
|
|
6918
8540
|
}
|
|
6919
8541
|
const tool = lent.tool();
|
|
6920
8542
|
created.handle(event, tool);
|
|
8543
|
+
if (event.type === "reload" && event.phase === "start") {
|
|
8544
|
+
const clip = created.clips.find((entry) => entry.name.startsWith("reload"));
|
|
8545
|
+
if (clip) {
|
|
8546
|
+
const kind = event.kind ?? "tactical";
|
|
8547
|
+
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 });
|
|
8548
|
+
}
|
|
8549
|
+
}
|
|
6921
8550
|
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
8551
|
});
|
|
6923
8552
|
const offFrame = lent.onFrame((dt) => created.update(dt));
|
|
8553
|
+
const offHands = lent.setHandTargets?.(() => created.handTargets());
|
|
6924
8554
|
created.handle({ type: "tool", weapon: lent.tool().weapon }, lent.tool());
|
|
6925
8555
|
return () => {
|
|
6926
8556
|
accessories.dispose();
|
|
6927
8557
|
offBench?.();
|
|
6928
8558
|
offEvents();
|
|
6929
8559
|
offFrame();
|
|
8560
|
+
offClock();
|
|
8561
|
+
offHands?.();
|
|
6930
8562
|
created.dispose();
|
|
6931
8563
|
audio?.dispose();
|
|
6932
8564
|
fx = null;
|
|
@@ -6935,6 +8567,976 @@ function createWeaponsTestModule(options = {}) {
|
|
|
6935
8567
|
}
|
|
6936
8568
|
};
|
|
6937
8569
|
}
|
|
8570
|
+
|
|
8571
|
+
// src/explosives-area.ts
|
|
8572
|
+
import { EXPLOSIONS } from "@adatechnology/game-vfx";
|
|
8573
|
+
import { createArsenal as createArsenal2 } from "@adatechnology/game-weapons";
|
|
8574
|
+
|
|
8575
|
+
// src/explosives-bench.ts
|
|
8576
|
+
import { markKitOrigin as markKitOrigin5 } from "@adatechnology/game-contracts";
|
|
8577
|
+
|
|
8578
|
+
// src/explosives-text.ts
|
|
8579
|
+
var EXPLOSIVES_TEXT = {
|
|
8580
|
+
en: {
|
|
8581
|
+
group: "Explosives",
|
|
8582
|
+
name: "Explosives area",
|
|
8583
|
+
cylinder: "Propane cylinder (P13)",
|
|
8584
|
+
actions: {
|
|
8585
|
+
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" },
|
|
8586
|
+
valve: { name: "Open the valve", hint: "Opens (or closes) the valve: the gas leaks with no fire until a flame reaches it" },
|
|
8587
|
+
light: { name: "Light it (lighter)", hint: "Holds a lighter flame at the valve: a gas leak catches fire and the cylinder goes off" },
|
|
8588
|
+
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" },
|
|
8589
|
+
reset: { name: "Restart", hint: "Lays the scene again: a new cylinder, wall, furniture and people" }
|
|
8590
|
+
},
|
|
8591
|
+
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" },
|
|
8592
|
+
layers: { rings: "Lethal and injury rings" },
|
|
8593
|
+
metrics: { gas: "Gas left in the cylinder", clock: "Seconds since the first leak", phase: "Phase" },
|
|
8594
|
+
phases: { rest: "At rest", leaking: "Leaking gas", torch: "Burning jet", primed: "About to go off", exploded: "Exploded" },
|
|
8595
|
+
rings: { lethal: "lethal", injury: "injury" },
|
|
8596
|
+
triggers: { shot: "a bullet", fire: "fire", explosion: "another blast" },
|
|
8597
|
+
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" },
|
|
8598
|
+
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" },
|
|
8599
|
+
log: {
|
|
8600
|
+
leak_shot: "Hole of {diameter} mm in the shell: gas leaking at {rate} l/s ({liters} l left in the cylinder)",
|
|
8601
|
+
leak_valve: "Valve open: gas leaking at {rate} l/s with no fire ({liters} l left in the cylinder)",
|
|
8602
|
+
valve_closed: "Valve closed: the leak stopped ({liters} l left)",
|
|
8603
|
+
valve_closed_late: "Too late to close the valve: the cylinder is already going off",
|
|
8604
|
+
primed: "The cylinder was set off by {trigger}: it goes off in {delay} s",
|
|
8605
|
+
jet_lit: "The jet caught fire (torch) {seconds} s after the hole; the cylinder goes off in {rest} s",
|
|
8606
|
+
jet_lit_by: "The leaking gas caught fire (torch) from {by}; the cylinder goes off in {rest} s",
|
|
8607
|
+
lighter_none: "Lighter flame at the valve: no gas is leaking, nothing happens",
|
|
8608
|
+
lighter_lit: "Lighter flame at the leaking valve: the gas lights",
|
|
8609
|
+
fire_set: "Fire lit next to the cylinder ({temperature} \xB0C, radius {radius} m): the shell heats up",
|
|
8610
|
+
heating: "Shell at {temperature} \xB0C (the fire chain starts at {limit} \xB0C)",
|
|
8611
|
+
explode: "Cylinder exploded: fireball of {fireball} m, lethal up to {lethal} m, injuries up to {injury} m (energy {energy} kJ, strength x{scale})",
|
|
8612
|
+
shell: "The torn shell flew up at {speed} m/s trailing fire for {trail} s",
|
|
8613
|
+
shell_landed: "The shell came down {distance} m away after {seconds} s",
|
|
8614
|
+
residual: "Residual fire of {radius} m for {seconds} s",
|
|
8615
|
+
aftermath_person: "{who} at {distance} m: {verdict}",
|
|
8616
|
+
aftermath_wall: "Wall behind the cylinder: {verdict}",
|
|
8617
|
+
aftermath_object: "{what} at {distance} m: {verdict}",
|
|
8618
|
+
reset: "Scene laid again with a blast strength of x{scale}",
|
|
8619
|
+
scale: "Blast strength x{scale}: energy {energy} kJ, lethal {lethal} m, injury {injury} m (valid for the next cylinder; the scene restarts)",
|
|
8620
|
+
no_room: "The explosives area was loaded without the room: this action needs it",
|
|
8621
|
+
shoot: "9 mm shot at the cylinder from {distance} m",
|
|
8622
|
+
no_vessel: "There is no cylinder in the scene (restart it)"
|
|
8623
|
+
},
|
|
8624
|
+
sheet: {
|
|
8625
|
+
kicker: "P13 cooking gas cylinder",
|
|
8626
|
+
title: "What happens to a P13 under a bullet, a flame or a blast",
|
|
8627
|
+
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.",
|
|
8628
|
+
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" },
|
|
8629
|
+
steps: [
|
|
8630
|
+
{ 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" },
|
|
8631
|
+
{ at: "0.8 s", text: "The jet catches fire and burns as a torch at the hole" },
|
|
8632
|
+
{ 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" },
|
|
8633
|
+
{ 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" }
|
|
8634
|
+
],
|
|
8635
|
+
segments: { jet: "White gas jet", torch: "Torch", blast: "Blast" },
|
|
8636
|
+
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."
|
|
8637
|
+
},
|
|
8638
|
+
scenarios: {
|
|
8639
|
+
shot: "P13 cylinder: a 9 mm through the shell leaks gas, the jet lights and the cylinder goes off with its chain delay",
|
|
8640
|
+
shot_leak: "The shot made a gas leak with a white pressurized jet",
|
|
8641
|
+
shot_ignite: "The jet caught fire (torch) between 0.7 and 1.0 s after the hole",
|
|
8642
|
+
shot_explode: "The cylinder exploded within 1.6 s of the shot",
|
|
8643
|
+
shot_zombie: "The zombie 3 m from the cylinder did not survive the blast",
|
|
8644
|
+
shot_survivor: "The survivor 8 m away, past the lethal ring, is hurt but alive",
|
|
8645
|
+
shot_wall: "The brick wall behind it was marked by the blast",
|
|
8646
|
+
valve: "P13 cylinder: the open valve leaks gas without fire and the cylinder goes off only when a flame lights it",
|
|
8647
|
+
valve_leak: "The open valve leaks gas",
|
|
8648
|
+
valve_quiet: "Four seconds of leaking gas with no flame: no fire and no blast",
|
|
8649
|
+
valve_lit: "The lighter flame lit the leak",
|
|
8650
|
+
valve_explode: "The cylinder went off within 1.6 s of the flame",
|
|
8651
|
+
fire: "P13 cylinder in a fire: the heat sets it off by the fire chain",
|
|
8652
|
+
fire_explode: "The cylinder in the fire exploded",
|
|
8653
|
+
chain: "P13 cylinder: another blast next to it sets it off by the explosion chain",
|
|
8654
|
+
chain_explode: "The second cylinder exploded after the first one",
|
|
8655
|
+
push_p13: "P13 blast: the loose items, the furniture and the people around it are thrown away from it, less the farther they stand",
|
|
8656
|
+
push_grenade: "Thrown M67 grenade: the loose items and the people around it are thrown away from it, less the farther they stand",
|
|
8657
|
+
push_told: "The explosion reached the host as an event",
|
|
8658
|
+
push_found: "{n} bodies were laid around the blast to be measured ({kinds})",
|
|
8659
|
+
push_outward: "Every body inside the reach moved away from the blast ({moved} of {n}, {least} m at the least)",
|
|
8660
|
+
push_falloff: "{what}: the nearer one moved more than the farther one ({near} m at {nearDistance} m, {far} m at {farDistance} m)",
|
|
8661
|
+
push_person: "{who} at {distance} m was pushed {moved} m away",
|
|
8662
|
+
push_heavy: "{what} (too heavy to grab, not fixed) was moved {moved} m by the blast"
|
|
8663
|
+
}
|
|
8664
|
+
},
|
|
8665
|
+
"pt-BR": {
|
|
8666
|
+
group: "Explosivos",
|
|
8667
|
+
name: "\xC1rea de explosivos",
|
|
8668
|
+
cylinder: "Botij\xE3o de g\xE1s (P13)",
|
|
8669
|
+
actions: {
|
|
8670
|
+
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" },
|
|
8671
|
+
valve: { name: "Abrir o registro", hint: "Abre (ou fecha) o registro: o g\xE1s vaza sem fogo at\xE9 uma chama encostar" },
|
|
8672
|
+
light: { name: "Acender (isqueiro)", hint: "P\xF5e a chama de um isqueiro no registro: g\xE1s vazando pega fogo e o botij\xE3o explode" },
|
|
8673
|
+
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" },
|
|
8674
|
+
reset: { name: "Reiniciar", hint: "Monta a cena de novo: botij\xE3o novo, parede, m\xF3veis e pessoas" }
|
|
8675
|
+
},
|
|
8676
|
+
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" },
|
|
8677
|
+
layers: { rings: "An\xE9is letal e de ferimento" },
|
|
8678
|
+
metrics: { gas: "G\xE1s no botij\xE3o", clock: "Segundos desde o primeiro vazamento", phase: "Fase" },
|
|
8679
|
+
phases: { rest: "Em repouso", leaking: "Vazando g\xE1s", torch: "Jato em chamas", primed: "Prestes a explodir", exploded: "Explodiu" },
|
|
8680
|
+
rings: { lethal: "letal", injury: "ferimento" },
|
|
8681
|
+
triggers: { shot: "um tiro", fire: "fogo", explosion: "outra explos\xE3o" },
|
|
8682
|
+
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" },
|
|
8683
|
+
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" },
|
|
8684
|
+
log: {
|
|
8685
|
+
leak_shot: "Furo de {diameter} mm no casco: g\xE1s vazando a {rate} l/s ({liters} l no botij\xE3o)",
|
|
8686
|
+
leak_valve: "Registro aberto: g\xE1s vazando a {rate} l/s, sem fogo ({liters} l no botij\xE3o)",
|
|
8687
|
+
valve_closed: "Registro fechado: o vazamento parou ({liters} l restantes)",
|
|
8688
|
+
valve_closed_late: "Tarde demais para fechar o registro: o botij\xE3o j\xE1 vai explodir",
|
|
8689
|
+
primed: "O botij\xE3o foi detonado por {trigger}: explode em {delay} s",
|
|
8690
|
+
jet_lit: "O jato pegou fogo (ma\xE7arico) {seconds} s depois do furo; o botij\xE3o explode em {rest} s",
|
|
8691
|
+
jet_lit_by: "O g\xE1s vazando pegou fogo (ma\xE7arico) por {by}; o botij\xE3o explode em {rest} s",
|
|
8692
|
+
lighter_none: "Chama do isqueiro no registro: n\xE3o h\xE1 g\xE1s vazando, nada acontece",
|
|
8693
|
+
lighter_lit: "Chama do isqueiro no registro que vaza: o g\xE1s acendeu",
|
|
8694
|
+
fire_set: "Fogo aceso ao lado do botij\xE3o ({temperature} \xB0C, raio {radius} m): o casco esquenta",
|
|
8695
|
+
heating: "Casco a {temperature} \xB0C (a cadeia de fogo come\xE7a em {limit} \xB0C)",
|
|
8696
|
+
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})",
|
|
8697
|
+
shell: "O casco rasgado voou para cima a {speed} m/s com rastro de fogo por {trail} s",
|
|
8698
|
+
shell_landed: "O casco caiu a {distance} m depois de {seconds} s",
|
|
8699
|
+
residual: "Fogo residual de {radius} m por {seconds} s",
|
|
8700
|
+
aftermath_person: "{who} a {distance} m: {verdict}",
|
|
8701
|
+
aftermath_wall: "Parede atr\xE1s do botij\xE3o: {verdict}",
|
|
8702
|
+
aftermath_object: "{what} a {distance} m: {verdict}",
|
|
8703
|
+
reset: "Cena montada de novo com for\xE7a de explos\xE3o x{scale}",
|
|
8704
|
+
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)",
|
|
8705
|
+
no_room: "A \xE1rea de explosivos foi carregada sem a sala: esta a\xE7\xE3o precisa dela",
|
|
8706
|
+
shoot: "Tiro de 9 mm no botij\xE3o a {distance} m",
|
|
8707
|
+
no_vessel: "N\xE3o h\xE1 botij\xE3o na cena (reinicie)"
|
|
8708
|
+
},
|
|
8709
|
+
sheet: {
|
|
8710
|
+
kicker: "Botij\xE3o de g\xE1s de cozinha P13",
|
|
8711
|
+
title: "O que acontece com um P13 sob um tiro, uma chama ou uma explos\xE3o",
|
|
8712
|
+
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.",
|
|
8713
|
+
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" },
|
|
8714
|
+
steps: [
|
|
8715
|
+
{ 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" },
|
|
8716
|
+
{ at: "0,8 s", text: "O jato pega fogo e queima como ma\xE7arico no furo" },
|
|
8717
|
+
{ 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" },
|
|
8718
|
+
{ 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" }
|
|
8719
|
+
],
|
|
8720
|
+
segments: { jet: "Jato branco de g\xE1s", torch: "Ma\xE7arico", blast: "Explos\xE3o" },
|
|
8721
|
+
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."
|
|
8722
|
+
},
|
|
8723
|
+
scenarios: {
|
|
8724
|
+
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",
|
|
8725
|
+
shot_leak: "O tiro fez um vazamento de g\xE1s com jato branco pressurizado",
|
|
8726
|
+
shot_ignite: "O jato pegou fogo (ma\xE7arico) entre 0,7 e 1,0 s depois do furo",
|
|
8727
|
+
shot_explode: "O botij\xE3o explodiu at\xE9 1,6 s depois do tiro",
|
|
8728
|
+
shot_zombie: "O zumbi a 3 m do botij\xE3o n\xE3o sobreviveu \xE0 explos\xE3o",
|
|
8729
|
+
shot_survivor: "O sobrevivente a 8 m, depois do anel letal, fica ferido mas vivo",
|
|
8730
|
+
shot_wall: "A parede de tijolo atr\xE1s dele ficou marcada pela explos\xE3o",
|
|
8731
|
+
valve: "Botij\xE3o P13: o registro aberto vaza g\xE1s sem fogo e o botij\xE3o s\xF3 explode quando uma chama acende",
|
|
8732
|
+
valve_leak: "O registro aberto vaza g\xE1s",
|
|
8733
|
+
valve_quiet: "Quatro segundos de g\xE1s vazando sem chama: sem fogo e sem explos\xE3o",
|
|
8734
|
+
valve_lit: "A chama do isqueiro acendeu o vazamento",
|
|
8735
|
+
valve_explode: "O botij\xE3o explodiu at\xE9 1,6 s depois da chama",
|
|
8736
|
+
fire: "Botij\xE3o P13 no fogo: o calor detona pela cadeia de fogo",
|
|
8737
|
+
fire_explode: "O botij\xE3o no fogo explodiu",
|
|
8738
|
+
chain: "Botij\xE3o P13: outra explos\xE3o ao lado detona pela cadeia de explos\xE3o",
|
|
8739
|
+
chain_explode: "O segundo botij\xE3o explodiu depois do primeiro",
|
|
8740
|
+
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",
|
|
8741
|
+
push_grenade: "Granada M67 arremessada: os itens soltos e as pessoas em volta s\xE3o jogados para longe, menos quanto mais longe est\xE3o",
|
|
8742
|
+
push_told: "A explos\xE3o chegou ao host como evento",
|
|
8743
|
+
push_found: "{n} corpos foram postos em volta da explos\xE3o para serem medidos ({kinds})",
|
|
8744
|
+
push_outward: "Todo corpo dentro do alcance se afastou da explos\xE3o ({moved} de {n}, no m\xEDnimo {least} m)",
|
|
8745
|
+
push_falloff: "{what}: o mais perto andou mais que o mais longe ({near} m a {nearDistance} m, {far} m a {farDistance} m)",
|
|
8746
|
+
push_person: "{who} a {distance} m foi empurrado {moved} m para longe",
|
|
8747
|
+
push_heavy: "{what} (pesado demais para pegar, mas solto) foi movido {moved} m pela explos\xE3o"
|
|
8748
|
+
}
|
|
8749
|
+
}
|
|
8750
|
+
};
|
|
8751
|
+
var explosivesTextOf = (locale) => EXPLOSIVES_TEXT[locale] ?? EXPLOSIVES_TEXT.en;
|
|
8752
|
+
var fillText2 = (template, vars) => template.replace(/\{(\w+)\}/g, (_, key) => String(vars[key] ?? ""));
|
|
8753
|
+
|
|
8754
|
+
// src/explosives-bench.ts
|
|
8755
|
+
var EXPLOSIVES_AREA_ID = "explosives_area";
|
|
8756
|
+
var FLAME_C = 1100;
|
|
8757
|
+
var FLAME_RADIUS = 0.2;
|
|
8758
|
+
var FLAME_SECONDS = 0.3;
|
|
8759
|
+
var FIRE_CHAIN_C = 300;
|
|
8760
|
+
var FIRE_OFFSET = [0.4, 0, 0.15];
|
|
8761
|
+
var FIRE_VISUAL_RADIUS = 0.6;
|
|
8762
|
+
var HISS_SHOVE = 0.35;
|
|
8763
|
+
var AFTERMATH_DELAY = 2;
|
|
8764
|
+
var SHOOTER = "pistol_9mm";
|
|
8765
|
+
var AIM_HEIGHT = 0.2;
|
|
8766
|
+
var GRAVITY2 = 9.81;
|
|
8767
|
+
var REPORTED_OBJECTS = ["fridge", "kitchen_cabinet_base", "stove", "camp_table"];
|
|
8768
|
+
var RING_SEGMENTS = 128;
|
|
8769
|
+
var WALL_ID = "target_wall";
|
|
8770
|
+
var WALL_Z = -11;
|
|
8771
|
+
var dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
8772
|
+
var num = (value, digits = 1) => value.toFixed(digits);
|
|
8773
|
+
function createExplosivesBench(options) {
|
|
8774
|
+
const { text, def, room, profile } = options;
|
|
8775
|
+
const log = text.log;
|
|
8776
|
+
let host = null;
|
|
8777
|
+
let strength = options.blastScale;
|
|
8778
|
+
let inArea = false;
|
|
8779
|
+
let lastBlast;
|
|
8780
|
+
let ringsOn = true;
|
|
8781
|
+
let initial = true;
|
|
8782
|
+
let aftermathDue = [];
|
|
8783
|
+
const vessels = /* @__PURE__ */ new Map();
|
|
8784
|
+
const wallParts = /* @__PURE__ */ new Set();
|
|
8785
|
+
const shells = [];
|
|
8786
|
+
const T = () => host?.THREE;
|
|
8787
|
+
const now = () => room?.world.time ?? 0;
|
|
8788
|
+
const say = (message, data = {}) => host?.log(message, { source: EXPLOSIVES_AREA_ID, ...data });
|
|
8789
|
+
const leakConfig = def.leak;
|
|
8790
|
+
const igniteDelay = leakConfig?.igniteDelay ?? 0.8;
|
|
8791
|
+
const hissSeconds = leakConfig?.hissSeconds ?? igniteDelay;
|
|
8792
|
+
const chainFire = (() => {
|
|
8793
|
+
const fire2 = def.chain?.fire;
|
|
8794
|
+
return fire2 === void 0 ? 1.4 : typeof fire2 === "number" ? fire2 : fire2[0];
|
|
8795
|
+
})();
|
|
8796
|
+
const liters = (vessel) => room?.world.part(vessel.objectId, "tank")?.liters ?? 0;
|
|
8797
|
+
const throughHoles = (vessel) => room?.world.part(vessel.objectId, "tank")?.holes.filter((hole) => hole.through).length ?? 0;
|
|
8798
|
+
const leaking = (vessel) => !vessel.exploded && liters(vessel) > 0 && throughHoles(vessel) > 0;
|
|
8799
|
+
const centreOf = (vessel) => {
|
|
8800
|
+
const p = vessel.root.getWorldPosition(new (T()).Vector3());
|
|
8801
|
+
return [p.x, p.y + AIM_HEIGHT, p.z];
|
|
8802
|
+
};
|
|
8803
|
+
const valveOf = (vessel) => worldExplosiveValve(T(), vessel.root) ?? centreOf(vessel);
|
|
8804
|
+
const target = () => [...vessels.values()].find((vessel) => !vessel.exploded) ?? [...vessels.values()][0];
|
|
8805
|
+
const lethalOf = (vessel) => def.lethalRadius * vessel.scale;
|
|
8806
|
+
const injuryOf = (vessel) => def.injuryRadius * vessel.scale;
|
|
8807
|
+
const labelSprite = (words, colour, at) => {
|
|
8808
|
+
try {
|
|
8809
|
+
const canvas = document.createElement("canvas");
|
|
8810
|
+
canvas.width = 256;
|
|
8811
|
+
canvas.height = 64;
|
|
8812
|
+
const context = canvas.getContext("2d");
|
|
8813
|
+
if (!context) return null;
|
|
8814
|
+
context.fillStyle = colour;
|
|
8815
|
+
context.font = "bold 40px sans-serif";
|
|
8816
|
+
context.textAlign = "center";
|
|
8817
|
+
context.fillText(words, 128, 46);
|
|
8818
|
+
const sprite = new (T()).Sprite(new (T()).SpriteMaterial({ map: new (T()).CanvasTexture(canvas), transparent: true, depthWrite: false }));
|
|
8819
|
+
sprite.scale.set(2.4, 0.6, 1);
|
|
8820
|
+
sprite.position.set(...at);
|
|
8821
|
+
return sprite;
|
|
8822
|
+
} catch {
|
|
8823
|
+
return null;
|
|
8824
|
+
}
|
|
8825
|
+
};
|
|
8826
|
+
const ringLine = (radius, colour) => {
|
|
8827
|
+
const points = [];
|
|
8828
|
+
for (let i = 0; i < RING_SEGMENTS; i++) {
|
|
8829
|
+
const angle = i / RING_SEGMENTS * Math.PI * 2;
|
|
8830
|
+
points.push(Math.cos(angle) * radius, 0, Math.sin(angle) * radius);
|
|
8831
|
+
}
|
|
8832
|
+
const geometry = new (T()).BufferGeometry();
|
|
8833
|
+
geometry.setAttribute("position", new (T()).Float32BufferAttribute(points, 3));
|
|
8834
|
+
return new (T()).LineLoop(geometry, new (T()).LineBasicMaterial({ color: colour }));
|
|
8835
|
+
};
|
|
8836
|
+
const drawRings = (vessel) => {
|
|
8837
|
+
const group = markKitOrigin5(new (T()).Group(), { owner: "game-weapons-three", kind: "aid" });
|
|
8838
|
+
group.name = "blast_rings";
|
|
8839
|
+
const lethal = lethalOf(vessel);
|
|
8840
|
+
const injury = injuryOf(vessel);
|
|
8841
|
+
group.add(ringLine(lethal, 16726832), ringLine(injury, 16756768));
|
|
8842
|
+
const lethalLabel = labelSprite(`${num(lethal, 0)} m ${text.rings.lethal}`, "#ff6a5e", [0, 0.35, lethal]);
|
|
8843
|
+
const injuryLabel = labelSprite(`${num(injury, 0)} m ${text.rings.injury}`, "#ffc44d", [0, 0.35, injury]);
|
|
8844
|
+
for (const label of [lethalLabel, injuryLabel]) if (label) group.add(label);
|
|
8845
|
+
const p = vessel.root.getWorldPosition(new (T()).Vector3());
|
|
8846
|
+
group.position.set(p.x, 0.03, p.z);
|
|
8847
|
+
group.visible = ringsOn;
|
|
8848
|
+
(host?.scene).add(group);
|
|
8849
|
+
vessel.rings = group;
|
|
8850
|
+
};
|
|
8851
|
+
const clearRings = (vessel) => {
|
|
8852
|
+
vessel.rings?.removeFromParent();
|
|
8853
|
+
vessel.rings = void 0;
|
|
8854
|
+
};
|
|
8855
|
+
const phaseOf = (vessel) => {
|
|
8856
|
+
if (!vessel) return "rest";
|
|
8857
|
+
if (vessel.exploded) return "exploded";
|
|
8858
|
+
if (vessel.explodeAt !== void 0) return "primed";
|
|
8859
|
+
if (vessel.lit) return "torch";
|
|
8860
|
+
return vessel.leakAt === void 0 ? "rest" : "leaking";
|
|
8861
|
+
};
|
|
8862
|
+
const ignite = (vessel, by) => {
|
|
8863
|
+
if (vessel.lit || vessel.exploded || !room) return;
|
|
8864
|
+
const leak = vessel.leaks[vessel.leaks.length - 1];
|
|
8865
|
+
const point = leak?.point ?? valveOf(vessel);
|
|
8866
|
+
room.world.hit(vessel.objectId, { type: "heat", point, temperature: FLAME_C, radius: FLAME_RADIUS, duration: FLAME_SECONDS, source: "p13_flame" });
|
|
8867
|
+
vessel.lit = true;
|
|
8868
|
+
vessel.litAt = now();
|
|
8869
|
+
const rest = vessel.explodeAt === void 0 ? chainFire : Math.max(0, vessel.explodeAt - now());
|
|
8870
|
+
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 });
|
|
8871
|
+
else say(fillText2(log.lighter_lit, {}), { phase: "torch", objectId: vessel.objectId, by });
|
|
8872
|
+
};
|
|
8873
|
+
const shove = (vessel) => {
|
|
8874
|
+
const leak = vessel.leaks[vessel.leaks.length - 1];
|
|
8875
|
+
if (!room || !leak) return;
|
|
8876
|
+
const [dx, dy, dz] = leak.direction;
|
|
8877
|
+
const length = Math.hypot(dx, dy, dz) || 1;
|
|
8878
|
+
room.renderer.impulse(vessel.objectId, [-dx / length * HISS_SHOVE, -dy / length * HISS_SHOVE * 0.2, -dz / length * HISS_SHOVE], leak.point);
|
|
8879
|
+
};
|
|
8880
|
+
const torchSprites = (vessel) => {
|
|
8881
|
+
const leak = vessel.leaks[vessel.leaks.length - 1];
|
|
8882
|
+
if (!room || !leak) return;
|
|
8883
|
+
const [dx, dy, dz] = leak.direction;
|
|
8884
|
+
const length = Math.hypot(dx, dy, dz) || 1;
|
|
8885
|
+
const flame = () => 3 + Math.random() * 2.5;
|
|
8886
|
+
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 });
|
|
8887
|
+
if (vessel.torchFrame++ % 3 === 0) room.vfx.spawn("smoke", leak.point, [dx / length * 2, 1.5, dz / length * 2], { size: 0.7, life: 1 });
|
|
8888
|
+
};
|
|
8889
|
+
const launchShell = (vessel, point) => {
|
|
8890
|
+
const config = def.shell;
|
|
8891
|
+
if (!config || !host) return;
|
|
8892
|
+
const mesh = worldExplosiveShell(T(), def.id);
|
|
8893
|
+
if (!mesh) return;
|
|
8894
|
+
mesh.position.set(point[0], Math.max(point[1], 0.3), point[2]);
|
|
8895
|
+
host.scene.add(mesh);
|
|
8896
|
+
shells.push({
|
|
8897
|
+
mesh,
|
|
8898
|
+
position: [point[0], Math.max(point[1], 0.3), point[2]],
|
|
8899
|
+
velocity: [(Math.random() - 0.5) * 3, config.launchSpeed, (Math.random() - 0.5) * 3],
|
|
8900
|
+
spin: [(Math.random() - 0.5) * 12, (Math.random() - 0.5) * 6, (Math.random() - 0.5) * 12],
|
|
8901
|
+
age: 0,
|
|
8902
|
+
trail: config.trailSeconds,
|
|
8903
|
+
landed: false,
|
|
8904
|
+
start: point
|
|
8905
|
+
});
|
|
8906
|
+
say(fillText2(log.shell, { speed: num(config.launchSpeed, 0), trail: num(config.trailSeconds, 1) }), { phase: "shell", speed: config.launchSpeed, trailSeconds: config.trailSeconds });
|
|
8907
|
+
void vessel;
|
|
8908
|
+
};
|
|
8909
|
+
const stepShells = (dt) => {
|
|
8910
|
+
if (!host) return;
|
|
8911
|
+
for (const shell of shells) {
|
|
8912
|
+
if (shell.landed) continue;
|
|
8913
|
+
shell.age += dt;
|
|
8914
|
+
shell.velocity[1] -= GRAVITY2 * dt;
|
|
8915
|
+
for (let axis = 0; axis < 3; axis++) shell.position[axis] = shell.position[axis] + shell.velocity[axis] * dt;
|
|
8916
|
+
const ground = host.groundAt(shell.position[0], shell.position[2]) + 0.18;
|
|
8917
|
+
if (shell.position[1] <= ground && shell.velocity[1] < 0) {
|
|
8918
|
+
shell.position[1] = ground;
|
|
8919
|
+
if (Math.abs(shell.velocity[1]) > 1.5) {
|
|
8920
|
+
shell.velocity = [shell.velocity[0] * 0.5, -shell.velocity[1] * 0.35, shell.velocity[2] * 0.5];
|
|
8921
|
+
} else {
|
|
8922
|
+
shell.landed = true;
|
|
8923
|
+
shell.mesh.rotation.set(Math.PI / 2, shell.mesh.rotation.y, 0);
|
|
8924
|
+
const away = dist(shell.start, [shell.position[0], shell.position[1], shell.position[2]]);
|
|
8925
|
+
say(fillText2(log.shell_landed, { distance: num(away, 1), seconds: num(shell.age, 1) }), { phase: "shell_landed", distance: away, seconds: shell.age });
|
|
8926
|
+
}
|
|
8927
|
+
} else {
|
|
8928
|
+
shell.mesh.rotation.x += shell.spin[0] * dt;
|
|
8929
|
+
shell.mesh.rotation.y += shell.spin[1] * dt;
|
|
8930
|
+
shell.mesh.rotation.z += shell.spin[2] * dt;
|
|
8931
|
+
}
|
|
8932
|
+
shell.mesh.position.set(shell.position[0], shell.position[1], shell.position[2]);
|
|
8933
|
+
if (room && shell.age < shell.trail) {
|
|
8934
|
+
const at = [shell.position[0], shell.position[1], shell.position[2]];
|
|
8935
|
+
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 });
|
|
8936
|
+
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 });
|
|
8937
|
+
}
|
|
8938
|
+
}
|
|
8939
|
+
};
|
|
8940
|
+
const clearShells = () => {
|
|
8941
|
+
for (const shell of shells) shell.mesh.removeFromParent();
|
|
8942
|
+
shells.length = 0;
|
|
8943
|
+
};
|
|
8944
|
+
const fullVitality = /* @__PURE__ */ new Map();
|
|
8945
|
+
const witnesses = () => {
|
|
8946
|
+
if (!room) return [];
|
|
8947
|
+
const origin = [...vessels.values()][0]?.centre ?? options.scene.cylinder;
|
|
8948
|
+
const found = [];
|
|
8949
|
+
for (const spawned of room.spawned()) {
|
|
8950
|
+
const position = spawned.root.getWorldPosition(new (T()).Vector3());
|
|
8951
|
+
const at = [position.x, position.y, position.z];
|
|
8952
|
+
const distance = dist([at[0], 0, at[2]], [origin[0], 0, origin[2]]);
|
|
8953
|
+
if (spawned.itemId.startsWith("zombie_") || spawned.itemId.startsWith("person_")) {
|
|
8954
|
+
const state = room.world.state(spawned.objectId);
|
|
8955
|
+
const alive = state?.body?.alive ?? true;
|
|
8956
|
+
const raw = state?.body?.vitality;
|
|
8957
|
+
if (raw !== void 0) fullVitality.set(spawned.objectId, Math.max(fullVitality.get(spawned.objectId) ?? 0, raw));
|
|
8958
|
+
const vitality = raw === void 0 ? void 0 : Math.max(0, raw) / Math.max(1e-9, fullVitality.get(spawned.objectId) ?? raw);
|
|
8959
|
+
found.push({ kind: "person", id: spawned.objectId, itemId: spawned.itemId, distance, alive, ...vitality === void 0 ? {} : { vitality } });
|
|
8960
|
+
} else if (REPORTED_OBJECTS.includes(spawned.itemId)) {
|
|
8961
|
+
const parts = room.world.state(spawned.objectId)?.parts ?? [];
|
|
8962
|
+
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 });
|
|
8963
|
+
}
|
|
8964
|
+
}
|
|
8965
|
+
if (wallParts.size > 0) {
|
|
8966
|
+
let broken = 0;
|
|
8967
|
+
let marks = 0;
|
|
8968
|
+
let pieces = 0;
|
|
8969
|
+
for (const id of room.world.ids()) {
|
|
8970
|
+
const mine = (room.world.state(id)?.parts ?? []).filter((part) => wallParts.has(part.id));
|
|
8971
|
+
if (mine.length === 0) continue;
|
|
8972
|
+
pieces++;
|
|
8973
|
+
broken += mine.filter((part) => part.status === "broken").length;
|
|
8974
|
+
marks += mine.reduce((n, part) => n + part.cracks.length + part.holes.length, 0);
|
|
8975
|
+
}
|
|
8976
|
+
found.push({ kind: "wall", id: "target_wall", itemId: "target_wall", distance: Math.abs(WALL_Z - origin[2]), broken, marks, pieces });
|
|
8977
|
+
}
|
|
8978
|
+
return found;
|
|
8979
|
+
};
|
|
8980
|
+
const whoIs = (itemId) => itemId.startsWith("zombie_") ? text.things.zombie : text.things.survivor;
|
|
8981
|
+
const aftermath = (point) => {
|
|
8982
|
+
for (const w of witnesses()) {
|
|
8983
|
+
const distance = num(w.distance, 1);
|
|
8984
|
+
if (w.kind === "person") {
|
|
8985
|
+
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;
|
|
8986
|
+
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 });
|
|
8987
|
+
} else if (w.kind === "wall") {
|
|
8988
|
+
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;
|
|
8989
|
+
say(fillText2(log.aftermath_wall, { verdict }), { phase: "aftermath", kind: w.kind, objectId: w.id, broken: w.broken, marks: w.marks, pieces: w.pieces });
|
|
8990
|
+
} else {
|
|
8991
|
+
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;
|
|
8992
|
+
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 });
|
|
8993
|
+
}
|
|
8994
|
+
}
|
|
8995
|
+
};
|
|
8996
|
+
const onEvent = (event) => {
|
|
8997
|
+
const vessel = vessels.get(event.object);
|
|
8998
|
+
if (!vessel || !("part" in event) || event.part !== "tank") return;
|
|
8999
|
+
if (event.type === "leak") {
|
|
9000
|
+
witnesses();
|
|
9001
|
+
if (vessel.leakAt === void 0) {
|
|
9002
|
+
vessel.leakAt = event.time;
|
|
9003
|
+
vessel.cause = vessel.valveRequested ? "valve" : "shot";
|
|
9004
|
+
}
|
|
9005
|
+
vessel.valveRequested = false;
|
|
9006
|
+
vessel.leaks.push({ point: event.point, direction: event.direction, rate: event.rate });
|
|
9007
|
+
if (vessel.leaks.length > 8) vessel.leaks.shift();
|
|
9008
|
+
const holes = room?.world.part(vessel.objectId, "tank")?.holes ?? [];
|
|
9009
|
+
const diameter = (holes[holes.length - 1]?.diameter ?? 9e-3) * 1e3;
|
|
9010
|
+
const vars = { diameter: num(diameter, 0), rate: num(event.rate, 2), liters: num(liters(vessel), 1) };
|
|
9011
|
+
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 });
|
|
9012
|
+
} else if (event.type === "primed") {
|
|
9013
|
+
vessel.explodeAt = event.time + event.delay;
|
|
9014
|
+
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 });
|
|
9015
|
+
} else if (event.type === "explode") {
|
|
9016
|
+
vessel.exploded = true;
|
|
9017
|
+
vessel.explodedAt = event.time;
|
|
9018
|
+
vessel.centre = event.point;
|
|
9019
|
+
const radius = profile?.visualRadius;
|
|
9020
|
+
const fireball = (radius ?? 2) * 2;
|
|
9021
|
+
const lethal = lethalOf(vessel);
|
|
9022
|
+
const injury = injuryOf(vessel);
|
|
9023
|
+
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) }), {
|
|
9024
|
+
phase: "explode",
|
|
9025
|
+
objectId: vessel.objectId,
|
|
9026
|
+
fireballMetres: fireball,
|
|
9027
|
+
lethalRadius: lethal,
|
|
9028
|
+
injuryRadius: injury,
|
|
9029
|
+
energyJ: def.energy * vessel.scale,
|
|
9030
|
+
scale: vessel.scale,
|
|
9031
|
+
point: event.point,
|
|
9032
|
+
sinceLeakSeconds: vessel.leakAt === void 0 ? void 0 : event.time - vessel.leakAt
|
|
9033
|
+
});
|
|
9034
|
+
const residual = profile?.residualFires?.[0];
|
|
9035
|
+
if (residual) say(fillText2(log.residual, { radius: num(residual.radius, 1), seconds: num(residual.life, 0) }), { phase: "residual", radius: residual.radius, seconds: residual.life });
|
|
9036
|
+
if (vessel.fireHandle !== void 0) room?.vfx.stop(vessel.fireHandle);
|
|
9037
|
+
launchShell(vessel, event.point);
|
|
9038
|
+
aftermathDue.push({ at: now() + AFTERMATH_DELAY, vessel, point: event.point });
|
|
9039
|
+
}
|
|
9040
|
+
};
|
|
9041
|
+
const frame = (dt) => {
|
|
9042
|
+
const t = now();
|
|
9043
|
+
for (const vessel of vessels.values()) {
|
|
9044
|
+
if (vessel.exploded) {
|
|
9045
|
+
vessel.lit = false;
|
|
9046
|
+
continue;
|
|
9047
|
+
}
|
|
9048
|
+
if (vessel.leakAt !== void 0) {
|
|
9049
|
+
if (t - vessel.leakAt <= hissSeconds && leaking(vessel)) shove(vessel);
|
|
9050
|
+
if (vessel.cause === "shot" && !vessel.lit && t - vessel.leakAt >= igniteDelay && leaking(vessel)) ignite(vessel, "shot");
|
|
9051
|
+
}
|
|
9052
|
+
if (vessel.lit) {
|
|
9053
|
+
if (leaking(vessel)) torchSprites(vessel);
|
|
9054
|
+
else vessel.lit = false;
|
|
9055
|
+
}
|
|
9056
|
+
if (vessel.fireUntil !== void 0 && t < vessel.fireUntil && room && vessel.firePoint) {
|
|
9057
|
+
const fire2 = def.fire;
|
|
9058
|
+
room.world.hit(vessel.objectId, { type: "heat", point: vessel.firePoint, temperature: fire2?.temperature ?? 700, radius: fire2?.radius ?? 1.4, duration: dt, source: "p13_fire" });
|
|
9059
|
+
if (vessel.explodeAt === void 0 && t >= vessel.nextHeatLog) {
|
|
9060
|
+
vessel.nextHeatLog = t + 1;
|
|
9061
|
+
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 });
|
|
9062
|
+
}
|
|
9063
|
+
}
|
|
9064
|
+
}
|
|
9065
|
+
stepShells(dt);
|
|
9066
|
+
if (aftermathDue.length > 0) {
|
|
9067
|
+
const ready = aftermathDue.filter((entry) => entry.at <= t);
|
|
9068
|
+
aftermathDue = aftermathDue.filter((entry) => entry.at > t);
|
|
9069
|
+
for (const entry of ready) aftermath(entry.point);
|
|
9070
|
+
}
|
|
9071
|
+
};
|
|
9072
|
+
const needRoom = () => {
|
|
9073
|
+
if (room) return true;
|
|
9074
|
+
say(log.no_room);
|
|
9075
|
+
return false;
|
|
9076
|
+
};
|
|
9077
|
+
const withVessel = () => {
|
|
9078
|
+
const vessel = target();
|
|
9079
|
+
if (!vessel || vessel.exploded) say(log.no_vessel);
|
|
9080
|
+
return vessel && !vessel.exploded ? vessel : void 0;
|
|
9081
|
+
};
|
|
9082
|
+
const shoot = () => {
|
|
9083
|
+
if (!needRoom() || !room || !host) return false;
|
|
9084
|
+
const vessel = withVessel();
|
|
9085
|
+
if (!vessel) return false;
|
|
9086
|
+
if (host.tool().weapon !== SHOOTER) room.setTool(SHOOTER);
|
|
9087
|
+
room.waitUntilReady();
|
|
9088
|
+
const eye = host.camera.position;
|
|
9089
|
+
const aim = centreOf(vessel);
|
|
9090
|
+
room.look([eye.x, eye.y, eye.z], aim);
|
|
9091
|
+
say(fillText2(log.shoot, { distance: num(dist([eye.x, eye.y, eye.z], aim), 1) }), { phase: "shoot", objectId: vessel.objectId, weapon: SHOOTER });
|
|
9092
|
+
room.use();
|
|
9093
|
+
return true;
|
|
9094
|
+
};
|
|
9095
|
+
const valve = () => {
|
|
9096
|
+
if (!needRoom() || !room) return false;
|
|
9097
|
+
const vessel = withVessel();
|
|
9098
|
+
if (!vessel) return false;
|
|
9099
|
+
if (vessel.valveOpen) {
|
|
9100
|
+
if (vessel.explodeAt !== void 0) {
|
|
9101
|
+
say(log.valve_closed_late, { phase: "valve", objectId: vessel.objectId });
|
|
9102
|
+
return false;
|
|
9103
|
+
}
|
|
9104
|
+
room.world.seal(vessel.objectId, "tank");
|
|
9105
|
+
vessel.valveOpen = false;
|
|
9106
|
+
if (throughHoles(vessel) === 0) {
|
|
9107
|
+
vessel.leakAt = void 0;
|
|
9108
|
+
vessel.cause = void 0;
|
|
9109
|
+
vessel.leaks = [];
|
|
9110
|
+
vessel.lit = false;
|
|
9111
|
+
}
|
|
9112
|
+
say(fillText2(log.valve_closed, { liters: num(liters(vessel), 1) }), { phase: "valve_closed", objectId: vessel.objectId, liters: liters(vessel) });
|
|
9113
|
+
return true;
|
|
9114
|
+
}
|
|
9115
|
+
vessel.valveRequested = true;
|
|
9116
|
+
const opened = room.world.vent(vessel.objectId, "tank", { point: valveOf(vessel), normal: [0, 1, 0] });
|
|
9117
|
+
vessel.valveRequested = false;
|
|
9118
|
+
vessel.valveOpen = opened;
|
|
9119
|
+
return opened;
|
|
9120
|
+
};
|
|
9121
|
+
const light = () => {
|
|
9122
|
+
if (!needRoom() || !room) return false;
|
|
9123
|
+
const vessel = withVessel();
|
|
9124
|
+
if (!vessel) return false;
|
|
9125
|
+
const at = vessel.leaks[vessel.leaks.length - 1]?.point ?? valveOf(vessel);
|
|
9126
|
+
room.vfx.burst("fire", at, [0, 1, 0], 6, 0.8);
|
|
9127
|
+
if (!leaking(vessel)) {
|
|
9128
|
+
say(log.lighter_none, { phase: "lighter", objectId: vessel.objectId, leaking: false });
|
|
9129
|
+
return false;
|
|
9130
|
+
}
|
|
9131
|
+
ignite(vessel, "lighter");
|
|
9132
|
+
return true;
|
|
9133
|
+
};
|
|
9134
|
+
const fire = () => {
|
|
9135
|
+
if (!needRoom() || !room) return false;
|
|
9136
|
+
const vessel = withVessel();
|
|
9137
|
+
if (!vessel) return false;
|
|
9138
|
+
const base = vessel.root.getWorldPosition(new (T()).Vector3());
|
|
9139
|
+
const point = [base.x + FIRE_OFFSET[0], base.y + FIRE_OFFSET[1], base.z + FIRE_OFFSET[2]];
|
|
9140
|
+
const config = def.fire ?? { temperature: 700, radius: 1.4, seconds: 7 };
|
|
9141
|
+
vessel.firePoint = point;
|
|
9142
|
+
vessel.fireUntil = now() + config.seconds + 3;
|
|
9143
|
+
vessel.nextHeatLog = now();
|
|
9144
|
+
vessel.fireHandle = room.vfx.fire({ point, radius: FIRE_VISUAL_RADIUS, life: config.seconds + 3 });
|
|
9145
|
+
for (const spawned of room.spawned()) {
|
|
9146
|
+
if (spawned.itemId !== "camp_table") continue;
|
|
9147
|
+
const top = spawned.root.getWorldPosition(new (T()).Vector3());
|
|
9148
|
+
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" });
|
|
9149
|
+
}
|
|
9150
|
+
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 });
|
|
9151
|
+
return true;
|
|
9152
|
+
};
|
|
9153
|
+
const reset = () => {
|
|
9154
|
+
if (!needRoom() || !room) return false;
|
|
9155
|
+
room.openArea(EXPLOSIVES_AREA_ID);
|
|
9156
|
+
say(fillText2(log.reset, { scale: num(strength, 2) }), { phase: "reset", scale: strength });
|
|
9157
|
+
return true;
|
|
9158
|
+
};
|
|
9159
|
+
const actionIds = ["shoot", "valve", "light", "fire", "reset"];
|
|
9160
|
+
const actions = { shoot, valve, light, fire, reset };
|
|
9161
|
+
const sheet = () => {
|
|
9162
|
+
const energy = def.energy * strength / 1e3;
|
|
9163
|
+
return {
|
|
9164
|
+
kicker: text.sheet.kicker,
|
|
9165
|
+
title: text.sheet.title,
|
|
9166
|
+
body: text.sheet.body,
|
|
9167
|
+
facts: [
|
|
9168
|
+
{ label: text.sheet.facts.energy, value: `${num(energy, 0)} kJ (x${num(strength, 2)})` },
|
|
9169
|
+
{ label: text.sheet.facts.lethal, value: `${num(def.lethalRadius * strength, 1)} m` },
|
|
9170
|
+
{ label: text.sheet.facts.injury, value: `${num(def.injuryRadius * strength, 1)} m` },
|
|
9171
|
+
{ label: text.sheet.facts.gas, value: `${num(leakConfig?.liters ?? 0, 0)} l` },
|
|
9172
|
+
{ label: text.sheet.facts.mass, value: `${num(def.mass ?? 0, 0)} kg` },
|
|
9173
|
+
{ label: text.sheet.facts.torch, value: `${num(igniteDelay, 1)} s` },
|
|
9174
|
+
{ label: text.sheet.facts.shot_chain, value: `${num(typeof def.chain?.shot === "number" ? def.chain.shot : def.chain?.shot?.[0] ?? 0, 1)} s` },
|
|
9175
|
+
{ label: text.sheet.facts.fire_chain, value: `${num(chainFire, 1)} s` }
|
|
9176
|
+
],
|
|
9177
|
+
steps: text.sheet.steps,
|
|
9178
|
+
note: text.sheet.note,
|
|
9179
|
+
timeline: {
|
|
9180
|
+
segments: [
|
|
9181
|
+
{ name: text.sheet.segments.jet, start: 0, end: igniteDelay, color: "#e8eef2" },
|
|
9182
|
+
{ name: text.sheet.segments.torch, start: igniteDelay, end: chainFire, color: "#ff9a2e" },
|
|
9183
|
+
{ name: text.sheet.segments.blast, start: chainFire, end: chainFire + 1, color: "#ff4d2e" }
|
|
9184
|
+
],
|
|
9185
|
+
ticks: [0, igniteDelay, chainFire],
|
|
9186
|
+
metric: "clock"
|
|
9187
|
+
}
|
|
9188
|
+
};
|
|
9189
|
+
};
|
|
9190
|
+
const adopt = (data) => {
|
|
9191
|
+
const spawned = data;
|
|
9192
|
+
if (spawned.itemId !== `${WORLD_EXPLOSIVE_PREFIX}${def.id}`) return;
|
|
9193
|
+
const vessel = { objectId: spawned.objectId, root: spawned.root, scale: strength, leaks: [], valveRequested: false, valveOpen: false, lit: false, exploded: false, nextHeatLog: 0, torchFrame: 0 };
|
|
9194
|
+
vessels.set(vessel.objectId, vessel);
|
|
9195
|
+
if (wallParts.size === 0) for (const part of room?.world.state(WALL_ID)?.parts ?? []) wallParts.add(part.id);
|
|
9196
|
+
if (inArea && host) drawRings(vessel);
|
|
9197
|
+
};
|
|
9198
|
+
const change = (change2) => {
|
|
9199
|
+
if (change2.kind === "action") {
|
|
9200
|
+
if (actionIds.includes(change2.id)) actions[change2.id]();
|
|
9201
|
+
} else if (change2.kind === "layer" && change2.id === "rings") {
|
|
9202
|
+
ringsOn = change2.on;
|
|
9203
|
+
for (const vessel of vessels.values()) if (vessel.rings) vessel.rings.visible = ringsOn;
|
|
9204
|
+
} else if (change2.kind === "control" && change2.id === "blast_scale" && typeof change2.value === "number") {
|
|
9205
|
+
const first = initial;
|
|
9206
|
+
initial = false;
|
|
9207
|
+
if (change2.value === strength && !first) return;
|
|
9208
|
+
strength = change2.value;
|
|
9209
|
+
host?.bench?.sheet?.(sheet());
|
|
9210
|
+
if (first) return;
|
|
9211
|
+
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 });
|
|
9212
|
+
if (inArea) reset();
|
|
9213
|
+
}
|
|
9214
|
+
};
|
|
9215
|
+
return {
|
|
9216
|
+
room,
|
|
9217
|
+
def,
|
|
9218
|
+
blastScale: () => strength,
|
|
9219
|
+
areaOpened: () => {
|
|
9220
|
+
inArea = true;
|
|
9221
|
+
},
|
|
9222
|
+
controls: () => [
|
|
9223
|
+
...actionIds.map((id) => ({ kind: "action", id, name: text.actions[id].name, hint: text.actions[id].hint })),
|
|
9224
|
+
{ 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 }
|
|
9225
|
+
],
|
|
9226
|
+
layers: () => [{ id: "rings", name: text.layers.rings }],
|
|
9227
|
+
metrics: () => [
|
|
9228
|
+
{ id: "gas", name: text.metrics.gas, unit: "l", digits: 1, max: leakConfig?.liters ?? 24, read: () => liters(target() ?? { objectId: "" }) },
|
|
9229
|
+
{ id: "clock", name: text.metrics.clock, unit: "s", digits: 2, read: () => {
|
|
9230
|
+
const v = target();
|
|
9231
|
+
return v?.leakAt === void 0 ? 0 : (v.explodedAt ?? now()) - v.leakAt;
|
|
9232
|
+
} },
|
|
9233
|
+
{ id: "phase", name: text.metrics.phase, read: () => text.phases[phaseOf(target())] }
|
|
9234
|
+
],
|
|
9235
|
+
attach(lent) {
|
|
9236
|
+
host = lent;
|
|
9237
|
+
const offs = [];
|
|
9238
|
+
if (room) {
|
|
9239
|
+
offs.push(room.world.on("*", onEvent));
|
|
9240
|
+
offs.push(room.on("spawn", adopt));
|
|
9241
|
+
for (const spawned of room.spawned()) adopt(spawned);
|
|
9242
|
+
}
|
|
9243
|
+
offs.push(
|
|
9244
|
+
lent.on((event) => {
|
|
9245
|
+
if (event.type === "explosion") lastBlast = { point: event.point, energy: event.energy, radius: event.radius, cause: event.cause, ...event.explosive === void 0 ? {} : { explosive: event.explosive } };
|
|
9246
|
+
})
|
|
9247
|
+
);
|
|
9248
|
+
offs.push(lent.onFrame(frame));
|
|
9249
|
+
offs.push(lent.bench?.onChange(change) ?? (() => void 0));
|
|
9250
|
+
offs.push(
|
|
9251
|
+
lent.onClear?.(() => {
|
|
9252
|
+
for (const vessel of vessels.values()) clearRings(vessel);
|
|
9253
|
+
vessels.clear();
|
|
9254
|
+
clearShells();
|
|
9255
|
+
wallParts.clear();
|
|
9256
|
+
aftermathDue = [];
|
|
9257
|
+
lastBlast = void 0;
|
|
9258
|
+
inArea = false;
|
|
9259
|
+
}) ?? (() => void 0)
|
|
9260
|
+
);
|
|
9261
|
+
lent.bench?.sheet?.(sheet());
|
|
9262
|
+
return () => {
|
|
9263
|
+
for (const off of offs) off();
|
|
9264
|
+
for (const vessel of vessels.values()) clearRings(vessel);
|
|
9265
|
+
vessels.clear();
|
|
9266
|
+
clearShells();
|
|
9267
|
+
host = null;
|
|
9268
|
+
};
|
|
9269
|
+
},
|
|
9270
|
+
actions,
|
|
9271
|
+
vessel: () => {
|
|
9272
|
+
const v = target();
|
|
9273
|
+
if (!v) return void 0;
|
|
9274
|
+
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 };
|
|
9275
|
+
},
|
|
9276
|
+
lastBlast: () => lastBlast,
|
|
9277
|
+
witnesses
|
|
9278
|
+
};
|
|
9279
|
+
}
|
|
9280
|
+
|
|
9281
|
+
// src/explosives-scenarios.ts
|
|
9282
|
+
var TORCH_WINDOW = [0.7, 1];
|
|
9283
|
+
var BLAST_WITHIN = 1.6;
|
|
9284
|
+
var CYLINDER_ITEM = `${WORLD_EXPLOSIVE_PREFIX}propane_p13`;
|
|
9285
|
+
var PUSH_KINDS = [
|
|
9286
|
+
{ kind: "can", item: "item_canned_beans" },
|
|
9287
|
+
{ kind: "bottle", item: "item_water_bottle" },
|
|
9288
|
+
{ kind: "chair", item: "dining_chair" }
|
|
9289
|
+
];
|
|
9290
|
+
var PUSH_READ_AT = 0.8;
|
|
9291
|
+
var MOVED = 0.15;
|
|
9292
|
+
var HEAVY_ITEMS = ["camp_table"];
|
|
9293
|
+
var flat = (a, b) => Math.hypot(a[0] - b[0], a[2] - b[2]);
|
|
9294
|
+
var bearing = (centre, distance, degrees) => [centre[0] + Math.sin(degrees * Math.PI / 180) * distance, 0, centre[2] + Math.cos(degrees * Math.PI / 180) * distance];
|
|
9295
|
+
var PUSH_LAYOUT = {
|
|
9296
|
+
can: [[2.5, -52], [5, 12], [8, 78]],
|
|
9297
|
+
bottle: [[2.5, 26], [5, -62], [8, -78]],
|
|
9298
|
+
chair: [[2.5, -18], [5, 60], [8, -42]]
|
|
9299
|
+
};
|
|
9300
|
+
function pushScenario(all, bench, mode) {
|
|
9301
|
+
const text = all.scenarios;
|
|
9302
|
+
const things = all.things;
|
|
9303
|
+
return {
|
|
9304
|
+
id: `explosives_push_${mode}`,
|
|
9305
|
+
name: mode === "p13" ? text.push_p13 : text.push_grenade,
|
|
9306
|
+
run: ({ room, step, expect }) => {
|
|
9307
|
+
let centre;
|
|
9308
|
+
if (mode === "p13") {
|
|
9309
|
+
room.openArea(EXPLOSIVES_AREA_ID);
|
|
9310
|
+
const cylinder = room.spawned().find((spawned) => spawned.itemId === CYLINDER_ITEM);
|
|
9311
|
+
const at = cylinder?.root?.position;
|
|
9312
|
+
centre = [at?.x ?? 0, 0, at?.z ?? -10.66];
|
|
9313
|
+
} else {
|
|
9314
|
+
room.clear();
|
|
9315
|
+
centre = [0, 0, -6];
|
|
9316
|
+
}
|
|
9317
|
+
const placed = [];
|
|
9318
|
+
const lay = (kind, item, at) => {
|
|
9319
|
+
try {
|
|
9320
|
+
const spawned = room.spawn(item, at, 0);
|
|
9321
|
+
placed.push({ kind, objectId: spawned.objectId, itemId: item, from: at, root: spawned.root });
|
|
9322
|
+
} catch {
|
|
9323
|
+
}
|
|
9324
|
+
};
|
|
9325
|
+
for (const { kind, item } of PUSH_KINDS) PUSH_LAYOUT[kind].forEach(([distance, degrees]) => lay(kind, item, bearing(centre, distance, degrees)));
|
|
9326
|
+
if (mode === "grenade") {
|
|
9327
|
+
lay("zombie", "zombie_walker", bearing(centre, 3.4, -34));
|
|
9328
|
+
lay("survivor", "person_police_officer", bearing(centre, 7.5, 78));
|
|
9329
|
+
lay("camp_table", "camp_table", bearing(centre, 3, 90));
|
|
9330
|
+
}
|
|
9331
|
+
step(0.6);
|
|
9332
|
+
const start = (body) => [body.root.position.x, body.root.position.y, body.root.position.z];
|
|
9333
|
+
const origin = new Map(placed.map((body) => [body.objectId, start(body)]));
|
|
9334
|
+
const partsOf = (objectId) => room.world.state(objectId)?.parts.length ?? 0;
|
|
9335
|
+
for (const spawned of room.spawned()) {
|
|
9336
|
+
const known = placed.some((body2) => body2.objectId === spawned.objectId);
|
|
9337
|
+
const role = spawned.itemId.startsWith("zombie_") ? "zombie" : spawned.itemId.startsWith("person_") ? "survivor" : HEAVY_ITEMS.includes(spawned.itemId) ? spawned.itemId : void 0;
|
|
9338
|
+
if (known || !role || mode !== "p13") continue;
|
|
9339
|
+
const root = spawned.root;
|
|
9340
|
+
const body = { kind: role, objectId: spawned.objectId, itemId: spawned.itemId, from: [root.position.x, root.position.y, root.position.z], root };
|
|
9341
|
+
placed.push(body);
|
|
9342
|
+
origin.set(body.objectId, body.from);
|
|
9343
|
+
}
|
|
9344
|
+
const partsBefore = new Map(placed.map((body) => [body.objectId, partsOf(body.objectId)]));
|
|
9345
|
+
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) });
|
|
9346
|
+
if (mode === "p13") bench.actions.shoot();
|
|
9347
|
+
else {
|
|
9348
|
+
room.setTool("frag_grenade");
|
|
9349
|
+
step(0.5);
|
|
9350
|
+
room.look([0, 1.6, 9.5], [0, 1, -2.5]);
|
|
9351
|
+
room.use();
|
|
9352
|
+
}
|
|
9353
|
+
let waited = 0;
|
|
9354
|
+
while (!bench.lastBlast() && waited < 9) {
|
|
9355
|
+
step(0.05);
|
|
9356
|
+
waited += 0.05;
|
|
9357
|
+
}
|
|
9358
|
+
const blast = bench.lastBlast();
|
|
9359
|
+
expect(!!blast, text.push_told, { waited });
|
|
9360
|
+
if (!blast) return;
|
|
9361
|
+
step(PUSH_READ_AT);
|
|
9362
|
+
const rows = placed.map((body) => {
|
|
9363
|
+
const from = origin.get(body.objectId) ?? body.from;
|
|
9364
|
+
const now = start(body);
|
|
9365
|
+
const distance = flat(from, blast.point);
|
|
9366
|
+
const away = Math.max(1e-6, distance);
|
|
9367
|
+
const outward = ((now[0] - from[0]) * (from[0] - blast.point[0]) + (now[2] - from[2]) * (from[2] - blast.point[2])) / away;
|
|
9368
|
+
return { ...body, distance, outward, moved: flat(now, from) };
|
|
9369
|
+
});
|
|
9370
|
+
const torn = (row) => PUSH_KINDS.some((entry) => entry.kind === row.kind) && partsOf(row.objectId) < (partsBefore.get(row.objectId) ?? 0);
|
|
9371
|
+
const reached = rows.filter((row) => row.distance < blast.radius * 0.85 && !torn(row));
|
|
9372
|
+
const movedAway = reached.filter((row) => row.outward >= MOVED);
|
|
9373
|
+
const least = reached.length === 0 ? 0 : Math.min(...reached.map((row) => row.outward));
|
|
9374
|
+
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) });
|
|
9375
|
+
for (const { kind } of PUSH_KINDS) {
|
|
9376
|
+
const ofKind = reached.filter((row) => row.kind === kind).sort((a, b) => a.distance - b.distance);
|
|
9377
|
+
if (ofKind.length < 2) continue;
|
|
9378
|
+
const near = ofKind[0];
|
|
9379
|
+
const far = ofKind[ofKind.length - 1];
|
|
9380
|
+
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 });
|
|
9381
|
+
}
|
|
9382
|
+
for (const row of reached.filter((entry) => entry.kind === "zombie" || entry.kind === "survivor")) {
|
|
9383
|
+
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 });
|
|
9384
|
+
}
|
|
9385
|
+
const table = reached.find((row) => HEAVY_ITEMS.includes(row.kind));
|
|
9386
|
+
if (table) expect(table.moved >= MOVED, fillText2(text.push_heavy, { what: things[table.itemId] ?? table.itemId, moved: table.moved.toFixed(2) }), { table });
|
|
9387
|
+
}
|
|
9388
|
+
};
|
|
9389
|
+
}
|
|
9390
|
+
function explosivesScenarios(locale, bench) {
|
|
9391
|
+
const text = explosivesTextOf(locale).scenarios;
|
|
9392
|
+
const record = (room) => {
|
|
9393
|
+
const events = [];
|
|
9394
|
+
const stop = room.world.on("*", (event) => {
|
|
9395
|
+
if (event.object.startsWith(CYLINDER_ITEM)) events.push({ type: event.type, time: event.time, object: event.object });
|
|
9396
|
+
});
|
|
9397
|
+
return { events, stop };
|
|
9398
|
+
};
|
|
9399
|
+
const first = (events, type, after = -Infinity) => events.find((event) => event.type === type && event.time >= after);
|
|
9400
|
+
return [
|
|
9401
|
+
{
|
|
9402
|
+
id: "explosives_p13_shot",
|
|
9403
|
+
name: text.shot,
|
|
9404
|
+
run: ({ room, step, expect }) => {
|
|
9405
|
+
room.openArea(EXPLOSIVES_AREA_ID);
|
|
9406
|
+
const { events, stop } = record(room);
|
|
9407
|
+
bench.actions.shoot();
|
|
9408
|
+
step(2.4);
|
|
9409
|
+
stop();
|
|
9410
|
+
const leak = first(events, "leak");
|
|
9411
|
+
const explode = first(events, "explode");
|
|
9412
|
+
const view = bench.vessel();
|
|
9413
|
+
expect(!!leak, text.shot_leak, { events: events.map((event) => event.type) });
|
|
9414
|
+
const torchAfter = view?.litAt !== void 0 && view.leakAt !== void 0 ? view.litAt - view.leakAt : -1;
|
|
9415
|
+
expect(torchAfter >= TORCH_WINDOW[0] && torchAfter <= TORCH_WINDOW[1] + 0.05, `${text.shot_ignite} (${torchAfter.toFixed(2)} s)`, { torchAfter });
|
|
9416
|
+
const blastAfter = leak && explode ? explode.time - leak.time : -1;
|
|
9417
|
+
expect(blastAfter > 0 && blastAfter <= BLAST_WITHIN, `${text.shot_explode} (${blastAfter.toFixed(2)} s)`, { blastAfter });
|
|
9418
|
+
const witnesses = bench.witnesses();
|
|
9419
|
+
const zombie = witnesses.find((w) => w.itemId.startsWith("zombie_"));
|
|
9420
|
+
expect(!!zombie && (zombie.alive === false || (zombie.vitality ?? 1) < 0.5), `${text.shot_zombie} (${zombie?.distance.toFixed(1)} m)`, { zombie });
|
|
9421
|
+
const survivor = witnesses.find((w) => w.kind === "person" && !w.itemId.startsWith("zombie_"));
|
|
9422
|
+
expect(!!survivor && survivor.alive === true && (survivor.vitality ?? 1) < 1, `${text.shot_survivor} (${survivor?.distance.toFixed(1)} m, ${survivor?.alive ? "alive" : "dead"})`, { survivor });
|
|
9423
|
+
const wall = witnesses.find((w) => w.kind === "wall");
|
|
9424
|
+
expect((wall?.marks ?? 0) + (wall?.broken ?? 0) + ((wall?.pieces ?? 1) - 1) > 0, text.shot_wall, { wall });
|
|
9425
|
+
}
|
|
9426
|
+
},
|
|
9427
|
+
{
|
|
9428
|
+
id: "explosives_p13_valve",
|
|
9429
|
+
name: text.valve,
|
|
9430
|
+
run: ({ room, step, expect }) => {
|
|
9431
|
+
room.openArea(EXPLOSIVES_AREA_ID);
|
|
9432
|
+
const { events, stop } = record(room);
|
|
9433
|
+
bench.actions.valve();
|
|
9434
|
+
step(4);
|
|
9435
|
+
expect(!!first(events, "leak"), text.valve_leak);
|
|
9436
|
+
expect(!first(events, "primed") && !first(events, "explode"), text.valve_quiet, { events: events.map((event) => event.type) });
|
|
9437
|
+
const before = room.world.time;
|
|
9438
|
+
bench.actions.light();
|
|
9439
|
+
step(2.2);
|
|
9440
|
+
stop();
|
|
9441
|
+
expect(bench.vessel()?.lit === true || bench.vessel()?.litAt !== void 0, text.valve_lit);
|
|
9442
|
+
const explode = first(events, "explode", before);
|
|
9443
|
+
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 });
|
|
9444
|
+
}
|
|
9445
|
+
},
|
|
9446
|
+
{
|
|
9447
|
+
id: "explosives_p13_fire",
|
|
9448
|
+
name: text.fire,
|
|
9449
|
+
run: ({ room, step, expect }) => {
|
|
9450
|
+
room.openArea(EXPLOSIVES_AREA_ID);
|
|
9451
|
+
const { events, stop } = record(room);
|
|
9452
|
+
bench.actions.fire();
|
|
9453
|
+
step(8);
|
|
9454
|
+
stop();
|
|
9455
|
+
expect(!!first(events, "explode"), text.fire_explode, { events: events.map((event) => event.type) });
|
|
9456
|
+
}
|
|
9457
|
+
},
|
|
9458
|
+
{
|
|
9459
|
+
id: "explosives_p13_chain",
|
|
9460
|
+
name: text.chain,
|
|
9461
|
+
run: ({ room, step, expect }) => {
|
|
9462
|
+
room.openArea(EXPLOSIVES_AREA_ID);
|
|
9463
|
+
const near = [1, 0, -10];
|
|
9464
|
+
const second = room.spawn(CYLINDER_ITEM, near, 0);
|
|
9465
|
+
const { events, stop } = record(room);
|
|
9466
|
+
bench.actions.shoot();
|
|
9467
|
+
step(4);
|
|
9468
|
+
stop();
|
|
9469
|
+
const blasts = events.filter((event) => event.type === "explode");
|
|
9470
|
+
const other = blasts.find((event) => event.object === second.objectId);
|
|
9471
|
+
expect(blasts.length >= 2 && !!other, `${text.chain_explode} (${blasts.length} blasts)`, { blasts: blasts.map((event) => `${event.object}@${event.time.toFixed(2)}`) });
|
|
9472
|
+
}
|
|
9473
|
+
},
|
|
9474
|
+
pushScenario(explosivesTextOf(locale), bench, "p13"),
|
|
9475
|
+
pushScenario(explosivesTextOf(locale), bench, "grenade")
|
|
9476
|
+
];
|
|
9477
|
+
}
|
|
9478
|
+
|
|
9479
|
+
// src/explosives-area.ts
|
|
9480
|
+
var DEFAULT_EXPLOSIVES_SCENE = {
|
|
9481
|
+
furniture: [
|
|
9482
|
+
{ item: "fridge", at: [-1.65, 0, -10.55] },
|
|
9483
|
+
{ item: "kitchen_cabinet_base", at: [-0.78, 0, -10.6] },
|
|
9484
|
+
{ item: "stove", at: [0.72, 0, -10.58] },
|
|
9485
|
+
{ item: "camp_table", at: [2.4, 0, -10.2] }
|
|
9486
|
+
],
|
|
9487
|
+
cylinder: [0.02, 0, -10.66],
|
|
9488
|
+
zombie: { item: "zombie_walker", at: [-1.6, 0, -8.1] },
|
|
9489
|
+
survivor: { item: "person_police_officer", at: [6, 0, -5.3] },
|
|
9490
|
+
eye: [0, 1.7, -4.7]
|
|
9491
|
+
};
|
|
9492
|
+
var EXPLOSIVE_ID = "propane_p13";
|
|
9493
|
+
var AIM_HEIGHT2 = 0.25;
|
|
9494
|
+
function createExplosivesTestModule(options = {}) {
|
|
9495
|
+
const locale = options.locale ?? "pt-BR";
|
|
9496
|
+
const arsenal = options.arsenal ?? createArsenal2({ locale });
|
|
9497
|
+
const text = explosivesTextOf(locale);
|
|
9498
|
+
const scene = { ...DEFAULT_EXPLOSIVES_SCENE, ...options.scene };
|
|
9499
|
+
const def = arsenal.explosives.get(EXPLOSIVE_ID);
|
|
9500
|
+
if (!def) throw new Error(`explosives area: the arsenal has no "${EXPLOSIVE_ID}"`);
|
|
9501
|
+
const bench = createExplosivesBench({ locale, text, def, scene, room: options.room, blastScale: options.blastScale ?? 1, profile: EXPLOSIONS[EXPLOSIVE_ID] });
|
|
9502
|
+
return {
|
|
9503
|
+
id: EXPLOSIVES_AREA_ID,
|
|
9504
|
+
name: text.name,
|
|
9505
|
+
group: text.group,
|
|
9506
|
+
bench,
|
|
9507
|
+
// 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
|
|
9508
|
+
items: () => {
|
|
9509
|
+
const item = worldExplosiveItem(def, text.cylinder, { scale: () => bench.blastScale() });
|
|
9510
|
+
return item ? [item] : [];
|
|
9511
|
+
},
|
|
9512
|
+
area: () => {
|
|
9513
|
+
bench.areaOpened();
|
|
9514
|
+
const placements = [
|
|
9515
|
+
...scene.furniture.map((piece) => ({ item: piece.item, position: piece.at, ...piece.rotationY === void 0 ? {} : { rotationY: piece.rotationY } })),
|
|
9516
|
+
{ item: `${WORLD_EXPLOSIVE_PREFIX}${EXPLOSIVE_ID}`, position: scene.cylinder },
|
|
9517
|
+
// facing the cylinder (the front of an item looks to +z, the cylinder is at -z)
|
|
9518
|
+
{ item: scene.zombie.item, position: scene.zombie.at, rotationY: Math.PI },
|
|
9519
|
+
{ item: scene.survivor.item, position: scene.survivor.at, rotationY: Math.PI }
|
|
9520
|
+
];
|
|
9521
|
+
const centre = [scene.cylinder[0], AIM_HEIGHT2, scene.cylinder[2]];
|
|
9522
|
+
return { placements, view: { eye: scene.eye, target: centre } };
|
|
9523
|
+
},
|
|
9524
|
+
controls: () => bench.controls(),
|
|
9525
|
+
layers: () => bench.layers(),
|
|
9526
|
+
metrics: () => bench.metrics(),
|
|
9527
|
+
scenarios: (scenarioLocale) => explosivesScenarios(scenarioLocale ?? locale, bench),
|
|
9528
|
+
attach: (host) => bench.attach(host)
|
|
9529
|
+
};
|
|
9530
|
+
}
|
|
9531
|
+
|
|
9532
|
+
// src/projectile-model.ts
|
|
9533
|
+
function projectileModel(THREE, query) {
|
|
9534
|
+
if (query.kind !== "rocket") return null;
|
|
9535
|
+
const launcher = buildWeaponModelById(THREE, query.weapon);
|
|
9536
|
+
if (!launcher || launcher.parts.family !== "rocket") return null;
|
|
9537
|
+
const round2 = launcher.parts.makeRound ? launcher.parts.makeRound() : buildFlyingRocket(modelKit(THREE), launcher.parts.launcherType);
|
|
9538
|
+
return { model: round2, axis: [1, 0, 0] };
|
|
9539
|
+
}
|
|
6938
9540
|
export {
|
|
6939
9541
|
ARCHETYPE_MODELS,
|
|
6940
9542
|
ATTACHMENT_MODELS,
|
|
@@ -6942,12 +9544,16 @@ export {
|
|
|
6942
9544
|
CLASS_REFERENCE_ENERGY,
|
|
6943
9545
|
COUNTER_ITEM_ID,
|
|
6944
9546
|
DEFAULT_ENERGY_CURVE,
|
|
9547
|
+
DEFAULT_EXPLOSIVES_SCENE,
|
|
6945
9548
|
DEFAULT_THUMBNAIL_BASE,
|
|
9549
|
+
EXPLOSIVES_AREA_ID,
|
|
9550
|
+
EXPLOSIVES_TEXT,
|
|
6946
9551
|
GAME_DISTANCE_CURVE,
|
|
6947
9552
|
GAME_SOUND_CLASSES,
|
|
6948
9553
|
MECHANICAL_SOUNDS,
|
|
6949
9554
|
MODEL_ENTRIES,
|
|
6950
9555
|
MOUNTED_M134_ITEM_ID,
|
|
9556
|
+
P13_VALVE_MESH,
|
|
6951
9557
|
WEAPON_ITEM_PREFIX,
|
|
6952
9558
|
WEAPON_THUMBNAILS,
|
|
6953
9559
|
WORLD_EFFECTS,
|
|
@@ -6956,6 +9562,7 @@ export {
|
|
|
6956
9562
|
applyCondition,
|
|
6957
9563
|
buildCartridge,
|
|
6958
9564
|
buildCasing,
|
|
9565
|
+
buildItemWeaponModel,
|
|
6959
9566
|
buildProp,
|
|
6960
9567
|
buildRound,
|
|
6961
9568
|
buildToolClips,
|
|
@@ -6963,23 +9570,30 @@ export {
|
|
|
6963
9570
|
buildWeaponModelById,
|
|
6964
9571
|
cartridgeShape,
|
|
6965
9572
|
computeShotSound,
|
|
9573
|
+
createExplosivesTestModule,
|
|
6966
9574
|
createWeaponAudio,
|
|
6967
9575
|
createWeaponFx,
|
|
6968
9576
|
createWeaponsTestModule,
|
|
6969
9577
|
createWorldEffects,
|
|
6970
9578
|
deriveFrames,
|
|
9579
|
+
explosivesTextOf,
|
|
6971
9580
|
fitAttachmentModels,
|
|
6972
9581
|
hasWeaponModel,
|
|
9582
|
+
itemModelOfWeapon,
|
|
6973
9583
|
mechanicalSoundOf,
|
|
6974
9584
|
modelKit,
|
|
6975
9585
|
mountFrame,
|
|
6976
9586
|
partRegistry,
|
|
9587
|
+
projectileModel,
|
|
9588
|
+
registerWeaponItemModel,
|
|
6977
9589
|
rigStock,
|
|
6978
9590
|
rigUnderbarrel,
|
|
6979
9591
|
weaponProp,
|
|
6980
9592
|
weaponThumbnailCredit,
|
|
6981
9593
|
weaponThumbnailUrl,
|
|
6982
9594
|
weaponsLayout,
|
|
6983
|
-
worldExplosiveItem
|
|
9595
|
+
worldExplosiveItem,
|
|
9596
|
+
worldExplosiveShell,
|
|
9597
|
+
worldExplosiveValve
|
|
6984
9598
|
};
|
|
6985
9599
|
//# sourceMappingURL=index.js.map
|