@adatechnology/game-weather 0.1.2 → 0.2.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.
@@ -23,17 +23,27 @@ var GameWeather = (() => {
23
23
  __export(index_exports, {
24
24
  CLIMATES: () => CLIMATES,
25
25
  DAY_REAL_SECONDS: () => DAY_REAL_SECONDS,
26
+ DEFAULT_BRIDGE_RULES: () => DEFAULT_BRIDGE_RULES,
26
27
  DEFAULT_DRY_OUT_SECONDS: () => DEFAULT_DRY_OUT_SECONDS,
28
+ DEFAULT_RAIN_SOUND: () => DEFAULT_RAIN_SOUND,
27
29
  DEFAULT_START_HOUR: () => DEFAULT_START_HOUR,
28
30
  DEFAULT_TIME_SCALE: () => DEFAULT_TIME_SCALE,
29
31
  DEFAULT_WET_RISE_SECONDS: () => DEFAULT_WET_RISE_SECONDS,
32
+ DEFAULT_WIND_SOUND: () => DEFAULT_WIND_SOUND,
30
33
  DRY_VISIBILITY_METERS: () => DRY_VISIBILITY_METERS,
31
34
  FLASH_END_SECONDS: () => FLASH_END_SECONDS,
32
35
  GAME_DAY_SECONDS: () => GAME_DAY_SECONDS,
33
36
  NIGHT_SIGHT_LOSS: () => NIGHT_SIGHT_LOSS,
37
+ RAIN_HISS_GAME: () => RAIN_HISS_GAME,
38
+ RAIN_LOOP: () => RAIN_LOOP,
34
39
  SPEED_OF_SOUND: () => SPEED_OF_SOUND,
35
40
  STATE_VALUE_KEYS: () => STATE_VALUE_KEYS,
41
+ THUNDER: () => THUNDER,
42
+ WEATHER_BANK: () => WEATHER_BANK,
43
+ WEATHER_LOOPS: () => WEATHER_LOOPS,
44
+ WEATHER_SOUNDS: () => WEATHER_SOUNDS,
36
45
  WEATHER_STATES: () => WEATHER_STATES,
46
+ WIND_LOOP: () => WIND_LOOP,
37
47
  blendValues: () => blendValues,
38
48
  createLightningStream: () => createLightningStream,
39
49
  createRng: () => createRng,
@@ -41,19 +51,25 @@ var GameWeather = (() => {
41
51
  createStream: () => createStream,
42
52
  createTimeline: () => createTimeline,
43
53
  createWeather: () => createWeather,
54
+ createWeatherBridges: () => createWeatherBridges,
44
55
  createWeatherRegistry: () => createWeatherRegistry,
56
+ createWeatherSound: () => createWeatherSound,
45
57
  createWeatherTestModule: () => createWeatherTestModule,
46
58
  createWetness: () => createWetness,
47
59
  dayFractionAt: () => dayFractionAt,
48
60
  daylight: () => daylight,
49
61
  deriveEffects: () => deriveEffects,
62
+ environmentOf: () => environmentOf,
50
63
  flashIntensity: () => flashIntensity,
51
64
  heatIndexC: () => heatIndexC,
52
65
  heatStress: () => heatStress,
66
+ rainVolume: () => rainVolume,
53
67
  streamSeed: () => streamSeed,
68
+ strikePosition: () => strikePosition,
54
69
  thunderDelay: () => thunderDelay,
55
70
  valuesOf: () => valuesOf,
56
- weatherBenchTextOf: () => weatherBenchTextOf
71
+ weatherBenchTextOf: () => weatherBenchTextOf,
72
+ windVolume: () => windVolume
57
73
  });
58
74
 
59
75
  // src/blend.ts
@@ -829,6 +845,343 @@ var GameWeather = (() => {
829
845
  };
830
846
  }
831
847
 
848
+ // ../game-audio/dist/index.js
849
+ var DISTANCE_CURVES = {
850
+ /** The game's `sfx`: `vol / (1 + d² × 0.004)`, cutoff `/ (1 + d × 0.01)`, silent below 0.01 (jogo L62091–62092, L62101). */
851
+ game_sfx: { model: "inverse_square", k: 4e-3, cutoffK: 0.01, minGain: 0.01 },
852
+ /** The game's `grito`: `1 / (1 + d² × 0.006)`, silent below 0.02 (jogo L62129–62130). */
853
+ game_scream: { model: "inverse_square", k: 6e-3, minGain: 0.02 },
854
+ /** The game's `voz`: `1 / (1 + d² × 0.01)`, silent below 0.02 (jogo L62146–62147). */
855
+ game_voice: { model: "inverse_square", k: 0.01, minGain: 0.02 },
856
+ /** The game's `boom`: `1.4 / (1 + d² × 0.002)`, never silent (jogo L68280). */
857
+ game_boom: { model: "inverse_square", k: 2e-3, scale: 1.4 },
858
+ /** The art scene's `outAt`: `vol / (1 + d² × 0.008)` (cena L1320). */
859
+ art_scene: { model: "inverse_square", k: 8e-3 },
860
+ /** The game's animal calls: `max(0, 1 − d / 70) × 0.25`, silent at or below 0.005 (jogo L66627–66628). */
861
+ animals: { model: "linear", range: 70, scale: 0.25, minGain: 5e-3 }
862
+ };
863
+ var mix_profiles_default = [
864
+ {
865
+ id: "game",
866
+ master: 0.5,
867
+ noise: { seconds: 1.5, create: "lazy" },
868
+ sources: ["jogo:L62077-62081 master 0.5 to the output", "jogo:L62082-62088 noise 1.5 s made by the first sound that needs it"]
869
+ },
870
+ {
871
+ id: "art_scene",
872
+ master: 0.85,
873
+ compressor: { threshold: -12, ratio: 5 },
874
+ reverb: { seconds: 2.2, decay: 3.4, wet: 0.4 },
875
+ noise: { seconds: 2, create: "eager" },
876
+ sources: [
877
+ "cena:L1308 compressor -12 dB, ratio 5",
878
+ "cena:L1309 master 0.85",
879
+ "cena:L1310 convolver, impulse 2.2 s of (1 - i/len)^3.4",
880
+ "cena:L1311 wet 0.4",
881
+ "cena:L1312 noise 2 s made with the chain"
882
+ ]
883
+ }
884
+ ];
885
+ var MIX_PROFILES = Object.fromEntries(
886
+ mix_profiles_default.map((profile) => [profile.id, profile])
887
+ );
888
+ var game_sfx_default = {
889
+ sources: ["jogo:L62089-62107"],
890
+ note: "The game's sfx table P: [lowpass cutoff Hz, peak gain, seconds]. Played as a noise burst with the game's distance curve (falloff game_sfx) and a 0.05 s tail. Team names in `team`.",
891
+ classes: [
892
+ { id: "pistol", team: "pistola", bus: "weapons", cutoff: 1800, gain: 0.9, seconds: 0.18 },
893
+ { id: "shotgun", team: "escopeta", bus: "weapons", cutoff: 900, gain: 1.2, seconds: 0.35 },
894
+ { id: "rifle", team: "fuzil", bus: "weapons", cutoff: 1400, gain: 1.1, seconds: 0.28 },
895
+ { id: "long_rifle", team: "rifle", bus: "weapons", cutoff: 1100, gain: 1.3, seconds: 0.45 },
896
+ { id: "impact", team: "impacto", bus: "world", cutoff: 2500, gain: 0.25, seconds: 0.05 },
897
+ { id: "flesh", team: "carne", bus: "world", cutoff: 600, gain: 0.35, seconds: 0.08 },
898
+ { id: "step", team: "passo", bus: "world", cutoff: 400, gain: 0.08, seconds: 0.05 },
899
+ { id: "glass", team: "vidro", bus: "world", cutoff: 5e3, gain: 0.5, seconds: 0.3 },
900
+ { id: "wood", team: "madeira", bus: "world", cutoff: 700, gain: 0.5, seconds: 0.2 },
901
+ { id: "reload", team: "recarga", bus: "weapons", cutoff: 3e3, gain: 0.15, seconds: 0.06 },
902
+ { id: "empty", team: "vazio", bus: "weapons", cutoff: 4e3, gain: 0.12, seconds: 0.03 },
903
+ { id: "explosion", team: "explosao", bus: "world", cutoff: 200, gain: 1.6, seconds: 1.4 }
904
+ ],
905
+ fallback: { id: "unknown", team: "(any other name)", bus: "world", cutoff: 1e3, gain: 0.5, seconds: 0.1 }
906
+ };
907
+ var GAME_SFX = game_sfx_default;
908
+ var GAME_SFX_CLASSES = GAME_SFX.classes;
909
+ var GAME_SFX_FALLBACK = GAME_SFX.fallback;
910
+ function noiseClassRecipe(row, id = `sfx_${row.id}`) {
911
+ return {
912
+ id,
913
+ bus: row.bus,
914
+ order: "burst",
915
+ duration: row.seconds,
916
+ tail: 0.05,
917
+ layers: [
918
+ {
919
+ source: { kind: "noise", offset: "random" },
920
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: row.cutoff } }] },
921
+ envelope: [
922
+ [0, row.gain, "set"],
923
+ ["1d", 1e-3, "exp", "fixed"]
924
+ ]
925
+ }
926
+ ],
927
+ falloff: DISTANCE_CURVES.game_sfx,
928
+ sources: [...GAME_SFX.sources.map((source) => `${source} ${row.team}`)]
929
+ };
930
+ }
931
+ var GAME_SFX_BANK = { id: "game_sfx", sounds: [...GAME_SFX_CLASSES, GAME_SFX_FALLBACK].map((row) => noiseClassRecipe(row)) };
932
+ var ATTACK = 2e-3;
933
+ function lengthOf(d, attack) {
934
+ if (typeof d === "number") return attack + d;
935
+ if (Array.isArray(d)) return [attack + d[0], attack + d[1]];
936
+ return { ...d, add: (d.add ?? 0) + attack };
937
+ }
938
+ var envelope = (attack) => [
939
+ [0, 1e-4, "set", "fixed"],
940
+ [attack, 1, "exp"],
941
+ ["1d", 1e-4, "exp", "fixed"]
942
+ ];
943
+ function sceneNoise(o) {
944
+ const a = o.a ?? ATTACK;
945
+ const over = typeof o.d === "number" ? o.d : 0;
946
+ return {
947
+ filters: {
948
+ mode: "serial",
949
+ bank: [{ type: o.type ?? "lowpass", frequency: o.f2 === void 0 ? { set: o.f } : { set: o.f, to: o.f2, curve: "exp", over }, q: o.q ?? 0.7 }]
950
+ },
951
+ duration: lengthOf(o.d, a),
952
+ gain: o.vol,
953
+ ...o.delay === void 0 ? {} : { at: o.delay },
954
+ ...o.wet === void 0 ? {} : { wet: o.wet },
955
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
956
+ envelope: envelope(a)
957
+ };
958
+ }
959
+ function sceneTone(o) {
960
+ return {
961
+ source: { kind: "osc", wave: o.wave ?? "sine", frequency: { from: o.f, to: o.f2 ?? "from", curve: "exp", over: "duration" } },
962
+ duration: lengthOf(o.d, ATTACK),
963
+ gain: o.vol,
964
+ ...o.delay === void 0 ? {} : { at: o.delay },
965
+ ...o.wet === void 0 ? {} : { wet: o.wet },
966
+ envelope: envelope(ATTACK)
967
+ };
968
+ }
969
+ var BENCH_BANK = {
970
+ id: "audio_bench",
971
+ sounds: [
972
+ {
973
+ id: "bench_ping",
974
+ bus: "ui",
975
+ pan: true,
976
+ duration: 0.25,
977
+ layers: [
978
+ {
979
+ gain: 0.5,
980
+ wet: 0.3,
981
+ source: { kind: "osc", wave: "sine", frequency: { from: 880 } },
982
+ envelope: [
983
+ [0, 1e-4, "set", "fixed"],
984
+ [2e-3, 1, "exp"],
985
+ ["1d", 1e-4, "exp", "fixed"]
986
+ ]
987
+ }
988
+ ],
989
+ falloff: DISTANCE_CURVES.art_scene,
990
+ sources: ["sdk: bench probe, a 880 Hz ping through the art scene output stage"]
991
+ },
992
+ {
993
+ id: "bench_hiss",
994
+ bus: "ui",
995
+ pan: true,
996
+ duration: [0.3, 0.9],
997
+ layers: [
998
+ {
999
+ gain: 0.4,
1000
+ source: { kind: "noise", offset: "random", offsetScale: 1.5, loop: true },
1001
+ filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 3e3 }, q: 0.7 }] },
1002
+ envelope: [
1003
+ [0, 1e-4, "set", "fixed"],
1004
+ [2e-3, 1, "exp"],
1005
+ ["1d", 1e-4, "exp", "fixed"]
1006
+ ]
1007
+ }
1008
+ ],
1009
+ falloff: DISTANCE_CURVES.art_scene,
1010
+ sources: ["sdk: bench probe, a highpass hiss of random length"]
1011
+ }
1012
+ ]
1013
+ };
1014
+
1015
+ // src/audio/recipes.ts
1016
+ var RAIN_LOOP = {
1017
+ id: "rain_loop",
1018
+ bus: "weather",
1019
+ order: "chain_loop",
1020
+ source: { kind: "noise", loop: true },
1021
+ filters: {
1022
+ mode: "serial",
1023
+ bank: [
1024
+ { type: "highpass", frequency: { set: 600 } },
1025
+ { type: "lowpass", frequency: { set: 6e3 } }
1026
+ ]
1027
+ },
1028
+ sources: ["cena:L1314"]
1029
+ };
1030
+ var WIND_LOOP = {
1031
+ id: "wind_loop",
1032
+ bus: "weather",
1033
+ order: "chain_loop",
1034
+ source: { kind: "noise", loop: true },
1035
+ filters: { mode: "serial", bank: [{ type: "lowpass", frequency: { set: 320 } }] },
1036
+ modulators: [{ target: "gain", rate: 0.07, depth: 0.035 }],
1037
+ sources: ["cena:L1315"]
1038
+ };
1039
+ var RAIN_HISS_GAME = {
1040
+ id: "rain_hiss_game",
1041
+ bus: "weather",
1042
+ order: "game_loop",
1043
+ source: { kind: "noise", loop: true },
1044
+ filters: { mode: "serial", bank: [{ type: "highpass", frequency: { set: 2500 } }] },
1045
+ sources: ["jogo:L62116 chuva"]
1046
+ };
1047
+ var THUNDER = {
1048
+ id: "thunder",
1049
+ bus: "weather",
1050
+ pan: true,
1051
+ layers: [sceneTone({ f: 70, f2: 28, d: 1.2, vol: 1 }), sceneNoise({ f: 260, f2: 50, d: 3.2, vol: 0.9, wet: 0.8 })],
1052
+ falloff: { model: "inverse_square", k: 1e-5, scale: 1.4 },
1053
+ sources: ["jogo:L68278-68285 boom", "sdk: rumble and falloff"]
1054
+ };
1055
+ var WEATHER_BANK = { id: "weather", sounds: [THUNDER], loops: [RAIN_LOOP, WIND_LOOP, RAIN_HISS_GAME] };
1056
+ var WEATHER_SOUNDS = WEATHER_BANK.sounds ?? [];
1057
+ var WEATHER_LOOPS = WEATHER_BANK.loops ?? [];
1058
+
1059
+ // src/audio/sound.ts
1060
+ var DEFAULT_RAIN_SOUND = { light: 0.035, heavy: 0.06, lightAt: 2.5, heavyAt: 15 };
1061
+ var DEFAULT_WIND_SOUND = { base: 0.05, referenceSpeed: 3.8, max: 0.12 };
1062
+ function rainVolume(rain, options = {}) {
1063
+ if (!(rain > 0)) return 0;
1064
+ const { light, heavy, lightAt, heavyAt } = { ...DEFAULT_RAIN_SOUND, ...options };
1065
+ if (heavyAt <= lightAt) return rain >= heavyAt ? heavy : light;
1066
+ const t = Math.min(1, Math.max(0, (rain - lightAt) / (heavyAt - lightAt)));
1067
+ return light + (heavy - light) * t;
1068
+ }
1069
+ function windVolume(speed, options = {}) {
1070
+ const { base, referenceSpeed, max } = { ...DEFAULT_WIND_SOUND, ...options };
1071
+ return Math.min(max, base * Math.max(0, speed) / referenceSpeed);
1072
+ }
1073
+ function strikePosition(strike, origin = [0, 0, 0]) {
1074
+ const angle = strike.bearing * Math.PI / 180;
1075
+ return [origin[0] + Math.sin(angle) * strike.distance, origin[1], origin[2] - Math.cos(angle) * strike.distance];
1076
+ }
1077
+ function createWeatherSound(sink, options = {}) {
1078
+ if (sink && options.register !== false) sink.register?.(WEATHER_BANK);
1079
+ const rainSound = options.style === "game" ? "rain_hiss_game" : "rain_loop";
1080
+ const loops = /* @__PURE__ */ new Map();
1081
+ const set = (id, volume) => {
1082
+ let loop = loops.get(id);
1083
+ if (!loop) {
1084
+ if (!(volume > 0) || !sink?.loop) return;
1085
+ loop = sink.loop(id);
1086
+ loops.set(id, loop);
1087
+ }
1088
+ loop.set({ volume });
1089
+ };
1090
+ return {
1091
+ update(sample) {
1092
+ set(rainSound, rainVolume(sample.rain, options.rain));
1093
+ set("wind_loop", windVolume(sample.windSpeed, options.wind));
1094
+ },
1095
+ thunder(strike) {
1096
+ sink?.play({ sound: "thunder", position: strikePosition(strike, options.listener?.()), ...options.thunderVolume === void 0 ? {} : { volume: options.thunderVolume } });
1097
+ },
1098
+ dispose() {
1099
+ for (const loop of loops.values()) loop.stop();
1100
+ loops.clear();
1101
+ }
1102
+ };
1103
+ }
1104
+
1105
+ // src/bridges.ts
1106
+ var DEFAULT_BRIDGE_RULES = { filmRainFullAt: 2.5, washFrom: 15, washBelow: 0.5, washEvery: 1, windFull: 20 };
1107
+ function environmentOf(sample) {
1108
+ return {
1109
+ rain: sample.rain,
1110
+ windX: sample.windX,
1111
+ windZ: sample.windZ,
1112
+ windSpeed: sample.windSpeed,
1113
+ gust: sample.gust,
1114
+ fogFar: sample.fogFar,
1115
+ visibility: sample.effects.visibility,
1116
+ nightFactor: sample.nightFactor,
1117
+ temperatureC: sample.temperatureC,
1118
+ wetness: sample.groundWetness
1119
+ };
1120
+ }
1121
+ function createWeatherBridges(targets, options = {}) {
1122
+ const rules = { ...DEFAULT_BRIDGE_RULES, ...options };
1123
+ const sound = options.sound === false || !targets.audio ? void 0 : createWeatherSound(targets.audio, options.sound ?? {});
1124
+ let weather;
1125
+ let offThunder;
1126
+ let sinceWash = 0;
1127
+ let last = {};
1128
+ const push = () => {
1129
+ if (!weather) return;
1130
+ const sample = weather.sample();
1131
+ const { film, interactables, vegetation, vfx, environment } = targets;
1132
+ environment?.set(environmentOf(sample));
1133
+ if (film) {
1134
+ const rain = Math.min(1, Math.max(0, sample.rain / rules.filmRainFullAt));
1135
+ const drying = sample.effects.dryingScale;
1136
+ if (rain !== last.filmRain || drying !== last.drying) film.setWeather({ rain, drying });
1137
+ last.filmRain = rain;
1138
+ last.drying = drying;
1139
+ }
1140
+ if (interactables && sample.rain !== last.rain) interactables.water.setRain(sample.rain);
1141
+ const windChanged = sample.windX !== last.windX || sample.windZ !== last.windZ;
1142
+ if (windChanged) {
1143
+ interactables?.setWind(sample.windX, sample.windZ);
1144
+ vfx?.setWind([sample.windX, 0, sample.windZ]);
1145
+ if (vegetation) {
1146
+ const wind = { strength: Math.min(1, sample.windSpeed / rules.windFull), gust: Math.min(1, sample.gust / rules.windFull) };
1147
+ vegetation.setWind(sample.windSpeed > 1e-6 ? { direction: [sample.windX, sample.windZ], ...wind } : wind);
1148
+ }
1149
+ }
1150
+ last = { ...last, rain: sample.rain, windX: sample.windX, windZ: sample.windZ };
1151
+ sound?.update(sample);
1152
+ };
1153
+ const onThunder = (strike) => sound?.thunder(strike);
1154
+ return {
1155
+ bind(next) {
1156
+ offThunder?.();
1157
+ weather = next;
1158
+ offThunder = next.on("thunder", onThunder);
1159
+ last = {};
1160
+ sinceWash = 0;
1161
+ push();
1162
+ },
1163
+ update(dt) {
1164
+ push();
1165
+ if (!weather || !targets.film?.wash) return;
1166
+ if (weather.sample().rain < rules.washFrom) {
1167
+ sinceWash = 0;
1168
+ return;
1169
+ }
1170
+ sinceWash += dt;
1171
+ if (sinceWash >= rules.washEvery) {
1172
+ sinceWash = 0;
1173
+ targets.film.wash(rules.washBelow);
1174
+ }
1175
+ },
1176
+ dispose() {
1177
+ offThunder?.();
1178
+ offThunder = void 0;
1179
+ weather = void 0;
1180
+ sound?.dispose();
1181
+ }
1182
+ };
1183
+ }
1184
+
832
1185
  // src/bench/locale.ts
833
1186
  var EN = {
834
1187
  group: "Weather",
@@ -931,9 +1284,12 @@ var GameWeather = (() => {
931
1284
  function createWeatherTestModule(options = {}) {
932
1285
  const locale = options.locale ?? "pt-BR";
933
1286
  const text = weatherBenchTextOf(locale);
934
- const { locale: _locale, translate, weather: given, fog: _fog, ...weatherOptions } = options;
1287
+ const { locale: _locale, translate, weather: given, fog: _fog, targets: _targets, sound: _sound, visual: _visual, ...weatherOptions } = options;
935
1288
  void _locale;
936
1289
  void _fog;
1290
+ void _targets;
1291
+ void _sound;
1292
+ void _visual;
937
1293
  const state = {
938
1294
  seed: options.seed ?? 1,
939
1295
  hour: options.startHour ?? 8,
@@ -945,6 +1301,8 @@ var GameWeather = (() => {
945
1301
  let host = null;
946
1302
  let strikes = 0;
947
1303
  let offStrike;
1304
+ let bridges;
1305
+ let visual;
948
1306
  let weather = given ?? createWeather({ ...weatherOptions, autoSchedule: state.auto, ...state.auto ? { timeScale: FAST_SCALE } : {} });
949
1307
  const watch = () => {
950
1308
  offStrike?.();
@@ -957,19 +1315,8 @@ var GameWeather = (() => {
957
1315
  weather = createWeather({ ...weatherOptions, seed: state.seed, start: current, startHour: state.hour, autoSchedule: state.auto, timeScale: state.auto ? FAST_SCALE : NORMAL_SCALE });
958
1316
  strikes = 0;
959
1317
  watch();
1318
+ bridges?.bind(weather);
960
1319
  };
961
- const toEnvironment = (sample) => ({
962
- rain: sample.rain,
963
- windX: sample.windX,
964
- windZ: sample.windZ,
965
- windSpeed: sample.windSpeed,
966
- gust: sample.gust,
967
- fogFar: sample.fogFar,
968
- visibility: sample.effects.visibility,
969
- nightFactor: sample.nightFactor,
970
- temperatureC: sample.temperatureC,
971
- wetness: sample.groundWetness
972
- });
973
1320
  const applyFog = (far) => {
974
1321
  const scene = host?.scene;
975
1322
  const Fog = host?.THREE?.Fog;
@@ -980,12 +1327,13 @@ var GameWeather = (() => {
980
1327
  };
981
1328
  const publish = () => {
982
1329
  const sample = weather.sample();
983
- if (state.environment) host?.environment?.set(toEnvironment(sample));
984
- if (state.fog) applyFog(sample.fogFar);
1330
+ if (state.environment) host?.environment?.set(environmentOf(sample));
1331
+ if (state.fog && !visual) applyFog(sample.fogFar);
985
1332
  };
986
1333
  const onChange = (change) => {
987
1334
  if (change.kind === "layer") {
988
1335
  if (change.id === "environment" || change.id === "fog") state[change.id] = change.on;
1336
+ visual?.setLayer(change.id, change.on);
989
1337
  return;
990
1338
  }
991
1339
  if (change.kind === "action") return;
@@ -1023,7 +1371,8 @@ var GameWeather = (() => {
1023
1371
  ];
1024
1372
  const layers = () => [
1025
1373
  { id: "environment", name: text.layers.environment },
1026
- { id: "fog", name: text.layers.fog, default: state.fog }
1374
+ { id: "fog", name: text.layers.fog, default: state.fog },
1375
+ ...options.visual?.layers(locale) ?? []
1027
1376
  ];
1028
1377
  const metrics = () => [
1029
1378
  { id: "current_state", name: text.metrics.state, read: () => nameOf(weather.state()) },
@@ -1112,14 +1461,24 @@ var GameWeather = (() => {
1112
1461
  attach(lent) {
1113
1462
  host = lent;
1114
1463
  const off = lent.bench?.onChange(onChange);
1464
+ bridges = createWeatherBridges({ ...options.targets, ...lent.audio ? { audio: lent.audio } : {} }, { sound: options.sound ?? {} });
1465
+ bridges.bind(weather);
1466
+ visual = options.visual?.create({ host: lent, locale, weather: { sample: () => weather.sample() } });
1467
+ if (visual && !state.fog) visual.setLayer("fog", false);
1115
1468
  publish();
1116
1469
  const offFrame = lent.onFrame((dt) => {
1117
1470
  weather.advance(dt);
1118
1471
  publish();
1472
+ bridges?.update(dt);
1473
+ visual?.update(dt);
1119
1474
  });
1120
1475
  return () => {
1476
+ visual?.dispose();
1477
+ visual = void 0;
1121
1478
  off?.();
1122
1479
  offFrame();
1480
+ bridges?.dispose();
1481
+ bridges = void 0;
1123
1482
  host = null;
1124
1483
  };
1125
1484
  }