@gamedevland/engine 0.0.8 → 0.0.10

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/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.10 — 2026-11-07
4
+
5
+ ### Localization and text
6
+
7
+ - Expand `LocaleCatalog` with Spanish, Brazilian Portuguese, German, French, Italian,
8
+ Turkish, Simplified Chinese, Japanese, and Korean, including regional-code normalization.
9
+ - Add `LocalesService.initialize()` with an explicit supported subset and default language.
10
+ Resolve saved manual preferences before device/platform hints, browser preferences, and
11
+ the configured default. Automatic detection does not write a manual preference.
12
+ - Persist explicit `setLang()` choices when storage is configured, tolerate unavailable
13
+ browser storage, and expose locale revisions and disposable change subscriptions.
14
+ - Preserve the English/Russian default subset for existing consumers and support partial
15
+ locale-specific text-style maps.
16
+ - Add measured `Text.fitTo(width, height?)` font fitting without changing node transforms,
17
+ including proportional line-height adjustment and validation of fitting bounds.
18
+
19
+ ### Audio
20
+
21
+ - Attempt music autoplay and recover blocked playback on the first trusted canvas
22
+ pointer/touch interaction or eligible keyboard interaction. Engine-owned capture listeners
23
+ run before scene input, work during boot, and are removed on audio disposal.
24
+ - Retry Web Audio resume on each user gesture even if an earlier automatic attempt remains
25
+ pending. Verify context state and handle rejected resume attempts and unavailable audio.
26
+ - Retain blocked HTML media music requests and reuse their sound IDs on native activation
27
+ or Howler unlock. Report confirmed playback separately from pending handles.
28
+ - Deduplicate active/loading music in both `AudioManager` and the default backend. Repeated
29
+ requests and scene changes preserve the handle and playback position; replacing a pending
30
+ track cancels its old Howler queue to prevent stale stops and orphaned playback.
31
+ - Preserve manual pause/stop, music/SFX preferences, channel/platform/background mute gates,
32
+ and terminal listener cleanup. Drop blocked one-shot SFX rather than replaying a backlog.
33
+ - Add optional `AudioBackend.isHandlePlaying()` and `isPlaybackReady()` hooks; existing
34
+ custom backends remain compatible.
35
+ - Reject unknown `channel.*` keys in the existing `system.audio.volumeConfigId` JSON contract.
36
+ Document centrally editable music/SFX channel and per-asset volumes in engine guidelines.
37
+
38
+ ### Verification
39
+
40
+ - Add deterministic locale preference/text fitting and browser audio activation regressions,
41
+ plus tests using real Howler queue/event code with a simulated HTML media device.
42
+ - Run these checks in the npm publishing workflow. Simulated browser/media tests do not
43
+ constitute physical iOS/Android or portal-device verification.
44
+
45
+ ## 0.0.9 — 2026-11-05
46
+
47
+ - Add opt-in `screen.dpr: 'native'` to target the viewport's physical pixels.
48
+ - Add `screen.render.maxCanvasPixels` to limit the backing canvas area.
49
+ - Apply size and resolution in one renderer resize. Existing numeric and `'auto'` DPR defaults stay the same.
50
+
3
51
  ## 0.0.8 — 2026-11-04
4
52
 
5
53
  - Publish the SDK source and package under MIT with public repository metadata.
@@ -14,4 +62,21 @@
14
62
 
15
63
  ## 0.0.7
16
64
 
65
+ - Expose runtime activity and native pointer metadata.
17
66
  - Dispose spawned prefab logical children idempotently and release their registry entries.
67
+
68
+ ## 0.0.5
69
+
70
+ - Prepare automated npm publishing.
71
+
72
+ ## 0.0.4
73
+
74
+ - Update package homepage and issue-tracker metadata.
75
+
76
+ ## 0.0.3
77
+
78
+ - Update repository metadata.
79
+
80
+ ## 0.0.2
81
+
82
+ - Add drag-end reasons and renderer-independent local node bounds.
package/README.md CHANGED
@@ -10,6 +10,14 @@ The npm package intentionally ships a compact public distribution:
10
10
 
11
11
  Source files are available in [the SDK repository](https://github.com/gamedevland/sdk/tree/main/packages/engine/src); the npm archive contains the compiled distribution.
12
12
 
13
+ ## Changelog
14
+
15
+ **0.0.10** adds configurable language preferences, observable locale changes,
16
+ measured text fitting, automatic audio activation recovery, and music deduplication
17
+ across loading, user gestures, and scene changes. The default locale subset and
18
+ existing custom audio backends remain compatible.
19
+ [Full engine changelog](https://github.com/gamedevland/sdk/blob/main/packages/engine/CHANGELOG.md).
20
+
13
21
  ## Install
14
22
 
15
23
  ```bash
@@ -44,6 +52,20 @@ For `dragend` input events, `InputEventBase.dragEndReason` distinguishes a norma
44
52
 
45
53
  Layout nodes expose `getLocalBounds()` for renderer-independent local geometry queries.
46
54
 
55
+ ## Screen resolution
56
+
57
+ Logical landscape and portrait profiles define the game's coordinate system.
58
+ Set `system.screen.dpr: 'native'` to scale the backing canvas to the physical
59
+ pixels of the viewport, including when the game is embedded in an iframe.
60
+ For this mode, `maxDpr` caps the display density relative to CSS pixels.
61
+ Existing numeric `dpr` values and `'auto'` still multiply the logical canvas size.
62
+
63
+ Set `system.screen.render.maxCanvasPixels` to a positive integer to limit the
64
+ backing canvas area. The engine scales both axes together and preserves the
65
+ logical layout and pointer coordinates. Extremely small budgets may be exceeded
66
+ when required to keep both axes at least one pixel. This option caps the main
67
+ canvas independently of the optional `render.scaleFactor` pass.
68
+
47
69
  ## TypeScript
48
70
 
49
71
  ### Platform packaging and boot (0.0.8)
@@ -112,3 +134,70 @@ type declarations may conflict with newer TypeScript DOM library declarations in
112
134
  MIT. See [LICENSE](./LICENSE). Source code and contributions: [gamedevland/sdk](https://github.com/gamedevland/sdk).
113
135
 
114
136
  Earlier npm releases retain the license included in their respective archives. Third-party dependencies retain their own licenses.
137
+
138
+ ## Language preferences
139
+
140
+ `LocaleCatalog` recognizes `en`, `ru`, `es`, `pt-BR`, `de`, `fr`, `it`, `tr`,
141
+ `zh-CN`, `ja`, and `ko`, including regional browser codes. Games opt into their
142
+ own subset with `LocalesService.initialize({ supportedLocales, defaultLocale,
143
+ deviceLocale, storageKey })`. Existing games retain their English/Russian subset.
144
+ The priority is a saved manual choice, a device/platform hint, browser language
145
+ preferences, then the configured default. Initialization never persists an
146
+ automatic choice. `setLang()` persists manual choices, including selection of the
147
+ current language; unavailable browser storage does not prevent play. Dictionaries
148
+ must already be present in ConfigStore. `getRevision()` and `subscribe()` support
149
+ live UI updates; `subscribe()` returns an unsubscribe function. Text-style locale
150
+ overrides accept partial maps of supported languages.
151
+
152
+ `Text.fitTo(width, height?)` shrinks the current font to fit measured local bounds
153
+ without changing node transforms. Set the desired font, wrapping and content
154
+ before fitting; invoke it again after a language/content change.
155
+
156
+ ### Autoplay and first-interaction recovery
157
+
158
+ Request music normally with `audio.playMusic(id)` when assets and player preferences
159
+ are ready. Do not wait for a scene-specific tap. Engine initialization attaches
160
+ native capture listeners to its canvas before scene input is routed. The audio
161
+ manager attempts Web Audio context resume automatically; a rejected or pending
162
+ attempt is retained for the first trusted pointer/touch interaction or keyboard
163
+ interaction while game/body has focus. Gesture retries call `resume()` before any
164
+ await, independently of any pending automatic attempt. Synthetic events, secondary
165
+ pointers/buttons, repeated/modifier key input, and hidden documents do not activate
166
+ recovery. Listeners belong to `AudioManager` and are removed by `audio.dispose()`.
167
+
168
+ The Howler backend checks the actual context state. HTML media `playerror` preserves
169
+ requested music and retries its existing sound id from native activation or Howler's
170
+ `unlock` event. Repeated taps cannot create layered music. `isMusicPlaying()` reports
171
+ confirmed playback rather than a queued handle. Stale blocked one-shot SFX are
172
+ suppressed/dropped, not replayed after unlock. User/channel disabled flags, manual
173
+ pause/stop, and platform/background mute remain authoritative.
174
+
175
+ `AudioBackend` optionally exposes `isHandlePlaying(handle)` and `isPlaybackReady()`
176
+ for precise state and one-shot suppression; existing custom backends remain compatible.
177
+ The default backend implements both. `AudioManager.attachInteractionTarget(element)`
178
+ is idempotent and is called by Engine; standalone AudioManager hosts should attach
179
+ it to their own game canvas and dispose the manager on teardown.
180
+
181
+ All authored levels belong in a manually editable JSON file selected by
182
+ `system.audio.volumeConfigId`, including `channel.music`, `channel.sfx`, and per-asset
183
+ levels. The SDK validates finite values in `[0, 1]` and rejects unknown channel keys.
184
+ Do not duplicate these values in consumer playback calls. Per-play volume options
185
+ remain available for deliberate runtime envelopes/custom backend integrations.
186
+
187
+ Run `node scripts/verify-audio-activation.mjs` from the SDK workspace to exercise the
188
+ real audio classes with simulated browser contexts/media events. These are deterministic
189
+ unit checks; they do not replace Safari/iOS/Android and embedded-portal device checks.
190
+
191
+ ### Music request idempotency
192
+
193
+ Repeated `playMusic()` requests for the same active or loading track preserve its
194
+ handle and seek position. The backend independently deduplicates pending/playing
195
+ music and limits its music channel to one source. Music is engine-scoped and does
196
+ not restart when changing scenes. Source-wide stops must not be queued ahead of
197
+ an undecoded play: Howler can reuse a sound id and later stop the newer request.
198
+ Cancelling undecoded music unloads the old Howl queue and recreates a clean source.
199
+
200
+ Run `node scripts/verify-music-single-instance.mjs` to check the actual bundled
201
+ Howler queue/events against a simulated HTML media device, including cold load,
202
+ boot/main duplicates, unlock races, preserved seek, mute/unmute during decoding,
203
+ and cancellation. This supplements `verify-audio-activation.mjs`.
package/dist/sdk.d.ts CHANGED
@@ -343,6 +343,10 @@ export interface AudioBackend {
343
343
  createSource(source: AudioSourceDescriptor): void;
344
344
  hasSource(id: AudioId): boolean;
345
345
  isHandleActive(handle: AudioHandle): boolean;
346
+ /** Actual playback state, excluding queued/blocked requests. */
347
+ isHandlePlaying?(handle: AudioHandle): boolean;
348
+ /** False while Web Audio is suspended; used to discard stale one-shot SFX. */
349
+ isPlaybackReady?(): boolean;
346
350
  play(id: AudioId, options?: PlayAudioOptions): AudioHandle;
347
351
  pause(handle: AudioHandle): void;
348
352
  stop(handle: AudioHandle): void;
@@ -371,16 +375,13 @@ export interface IStateStore<T> {
371
375
  save(value: T): void;
372
376
  clear(): void;
373
377
  }
374
- export type SupportedLocaleCode = "en" | "ru";
375
- /**
376
- * Canonical supported locale codes used by runtime, build validation, and platform hints.
377
- */
378
+ export type SupportedLocaleCode = "en" | "ru" | "es" | "pt-BR" | "de" | "fr" | "it" | "tr" | "zh-CN" | "ja" | "ko";
379
+ /** Canonical locale codes shared by runtime, configs and build tools. */
378
380
  export declare class LocaleCatalog {
379
- static readonly EN: SupportedLocaleCode;
380
- static readonly RU: SupportedLocaleCode;
381
- static readonly DEFAULT: SupportedLocaleCode;
381
+ static readonly EN = "en";
382
+ static readonly RU = "ru";
383
+ static readonly DEFAULT = "en";
382
384
  private static readonly SUPPORTED_LIST;
383
- private static readonly SUPPORTED;
384
385
  static isSupported(locale: string): locale is SupportedLocaleCode;
385
386
  static getSupportedList(): ReadonlyArray<SupportedLocaleCode>;
386
387
  static normalize(input: string | null | undefined): SupportedLocaleCode | null;
@@ -418,7 +419,7 @@ export interface TextStyleDef {
418
419
  readonly dropShadow?: TextStyleDropShadowDef;
419
420
  readonly wordWrap?: boolean;
420
421
  readonly wordWrapWidth?: number;
421
- readonly byLocale?: Readonly<Record<"en" | "ru", TextStyleDef>>;
422
+ readonly byLocale?: Readonly<Partial<Record<SupportedLocaleCode, TextStyleDef>>>;
422
423
  }
423
424
  /** Map of style name to definition. */
424
425
  export type TextStylesConfig = Record<string, TextStyleDef>;
@@ -556,14 +557,32 @@ export declare class IndexedDbStorageAdapter {
556
557
  private openDatabase;
557
558
  private logStorageError;
558
559
  }
559
- /** Simple localization access service. */
560
+ export interface LocalePreferences {
561
+ readonly supportedLocales: readonly SupportedLocaleCode[];
562
+ readonly defaultLocale?: SupportedLocaleCode;
563
+ readonly deviceLocale?: string | null;
564
+ readonly preferredLocales?: readonly string[];
565
+ readonly storage?: IStorageAdapter;
566
+ readonly storageKey?: string;
567
+ }
568
+ /** Dictionary lookup and persistent, observable language preferences. */
560
569
  export declare class LocalesService {
561
570
  private readonly configStore;
562
571
  private current;
572
+ private supported;
573
+ private defaultLocale;
574
+ private storage;
575
+ private storageKey;
576
+ private revision;
577
+ private readonly listeners;
563
578
  private readonly fallback;
564
579
  private readonly loadedDictionaries;
565
580
  constructor(configStore: ConfigStore, fallback?: readonly string[]);
581
+ initialize(options: LocalePreferences): void;
566
582
  setLang(lang: string): void;
583
+ getRevision(): number;
584
+ subscribe(listener: (locale: SupportedLocaleCode) => void): () => void;
585
+ private apply;
567
586
  getLang(): SupportedLocaleCode;
568
587
  private hasKey;
569
588
  t(key: string, params?: Readonly<Record<string, string | number>>): string;
@@ -867,8 +886,12 @@ export declare class AudioManager {
867
886
  private musicVolumeInitialized;
868
887
  private musicVolumeOverridden;
869
888
  private disposed;
889
+ private autoplayAttempt;
890
+ private readonly interactionUnlock;
870
891
  static createDefault(assets: AssetManager, options?: AudioManagerOptions): AudioManager;
871
892
  constructor(assets: AssetManager, backend: AudioBackend, options?: AudioManagerOptions);
893
+ /** Engine-owned, scene-independent activation on the game's native input target. */
894
+ attachInteractionTarget(target: HTMLElement): void;
872
895
  playMusic(assetId: AudioId, options?: PlayAudioOptions): AudioHandle | undefined;
873
896
  playSfx(assetId: AudioId, options?: PlayAudioOptions): AudioHandle | undefined;
874
897
  preloadInBackground(assetId: AudioId): void;
@@ -902,6 +925,7 @@ export declare class AudioManager {
902
925
  private rememberMusicRequest;
903
926
  private restoreRememberedMusicIfNeeded;
904
927
  private clearActiveMusic;
928
+ private tryAutomaticUnlock;
905
929
  private clearRememberedMusicRequest;
906
930
  private ensureRememberedMusicSourceInBackground;
907
931
  private isChannelEnabled;
@@ -1587,6 +1611,8 @@ declare class Text$1 extends NodeBase implements TextNode {
1587
1611
  }): DisplayNode;
1588
1612
  get view(): PIXI.Text;
1589
1613
  setText(value: string): this;
1614
+ /** Shrink the current font to fit local bounds, preserving node transforms. */
1615
+ fitTo(width: number, height?: number): this;
1590
1616
  protected applyAnchor(anchorX: number, anchorY: number): this;
1591
1617
  }
1592
1618
  export declare abstract class BaseComponent<TNode extends NodeBase = NodeBase> {
@@ -2216,16 +2242,20 @@ export interface ScreenProfile {
2216
2242
  }
2217
2243
  export interface ScreenRenderConfig {
2218
2244
  scaleFactor?: number;
2245
+ /** Maximum backing canvas area in physical pixels. Independent of scaleFactor. */
2246
+ maxCanvasPixels?: number;
2219
2247
  }
2220
2248
  /**
2221
2249
  * Main configuration for screen behavior.
2222
2250
  * - `land`/`port`: virtual profiles for each orientation.
2223
- * - `dpr`: renderer resolution; 'auto' or a fixed number. Default is 1.
2251
+ * - `dpr`: fixed renderer resolution, 'auto' for browser DPR in virtual units,
2252
+ * or 'native' to target physical viewport pixels. Default is 1.
2253
+ * - `maxDpr`: caps the browser DPR in 'auto' and 'native' modes.
2224
2254
  */
2225
2255
  export interface ScreenConfig {
2226
2256
  land: ScreenProfile;
2227
2257
  port: ScreenProfile;
2228
- dpr?: "auto" | number;
2258
+ dpr?: "auto" | "native" | number;
2229
2259
  maxDpr?: number;
2230
2260
  render?: ScreenRenderConfig;
2231
2261
  }
@@ -2246,7 +2276,7 @@ export interface VirtualSize {
2246
2276
  export interface ScreenStateSnapshot {
2247
2277
  orientation: Orientation;
2248
2278
  viewport: ViewportSize;
2249
- /** Internal canvas size (the <canvas width/height> attributes), px. */
2279
+ /** Logical canvas size, before renderer resolution is applied. */
2250
2280
  canvas: {
2251
2281
  width: number;
2252
2282
  height: number;
@@ -2319,8 +2349,7 @@ export declare class ScreenManager {
2319
2349
  private applySnapshot;
2320
2350
  /**
2321
2351
  * Applies the current snapshot to the PIXI renderer and stage:
2322
- * - Sets device pixel ratio (renderer.resolution)
2323
- * - Resizes the internal canvas to the computed virtual size
2352
+ * - Resizes the renderer using the virtual size and backing canvas resolution
2324
2353
  * - Does NOT scale the stage: scaling is expected to be done via CSS size of the canvas
2325
2354
  */
2326
2355
  private applyToStage;
@@ -2331,12 +2360,7 @@ export declare class ScreenManager {
2331
2360
  onChange(handler: (snapshot: ScreenStateSnapshot) => void): () => void;
2332
2361
  /** Notifies subscribers with a cloned snapshot to prevent outside mutation. */
2333
2362
  private notifyChange;
2334
- /**
2335
- * Resolves the device pixel ratio to use for the renderer's resolution.
2336
- * - `dpr: 'auto'` uses the browser DPR (retina-aware).
2337
- * - numeric `dpr` uses the provided fixed value.
2338
- * - missing/invalid values fall back to 1.
2339
- */
2363
+ private resolveResolution;
2340
2364
  private resolveDpr;
2341
2365
  /**
2342
2366
  * Returns the center point of the virtual canvas in virtual coordinates.
@@ -4906,7 +4930,7 @@ export declare abstract class BaseTypedCommand<TPayload> extends BaseCommand {
4906
4930
  protected abstract execute(payload: TPayload): Promise<void> | void;
4907
4931
  run(data?: Record<string, unknown>): Promise<void> | void;
4908
4932
  }
4909
- export type EngineErrorCodeValue = "CORE_INVALID_ARGUMENT" | "CORE_MISSING_REQUIRED" | "CORE_INVALID_CONFIG" | "CORE_INVARIANT_VIOLATION" | "SCENE_INVALID_ARGUMENT" | "SCENE_INVALID_STATE" | "SCENE_LIFECYCLE_FAILURE" | "DI_INVALID_CONFIG" | "DI_PROVIDER_NOT_FOUND" | "DI_INVALID_PROVIDER" | "DI_DUPLICATE_PROVIDER" | "DI_FORBIDDEN_OVERRIDE" | "DI_UNRESOLVABLE_TOKEN" | "ASSET_MISSING_REQUIRED" | "ASSET_INVALID_CONFIG" | "ASSET_CONFLICT" | "ASSET_LOAD_FAILURE" | "LAYOUT_MISSING_REQUIRED" | "LAYOUT_INVALID_CONFIG" | "LAYOUT_CONFLICT" | "LAYOUT_INVARIANT_VIOLATION" | "AUDIO_MISSING_REQUIRED" | "AUDIO_INVALID_ARGUMENT" | "AUDIO_INVALID_CONFIG" | "AUDIO_CONFLICT" | "PARTICLE_MISSING_REQUIRED" | "PARTICLE_INVALID_ARGUMENT" | "PARTICLE_INVALID_CONFIG" | "PARTICLE_INVARIANT_VIOLATION" | "PARTICLE_RUNTIME_FAILURE" | "PREFAB_MISSING_REQUIRED" | "PREFAB_INVALID_CONFIG" | "PREFAB_CONFLICT" | "PREFAB_INVARIANT_VIOLATION" | "FSM_INVALID_CONFIG" | "FSM_INVALID_STATE" | "COMMAND_MISSING_REQUIRED" | "COMMAND_INVALID_PAYLOAD" | "CONFIG_MISSING_REQUIRED" | "CONFIG_INVALID_ARGUMENT" | "CONFIG_INVALID_SCHEMA";
4933
+ export type EngineErrorCodeValue = "CORE_INVALID_ARGUMENT" | "CORE_MISSING_REQUIRED" | "CORE_INVALID_CONFIG" | "CORE_INVARIANT_VIOLATION" | "SCENE_INVALID_ARGUMENT" | "SCENE_INVALID_STATE" | "SCENE_LIFECYCLE_FAILURE" | "DI_INVALID_CONFIG" | "DI_PROVIDER_NOT_FOUND" | "DI_INVALID_PROVIDER" | "DI_DUPLICATE_PROVIDER" | "DI_FORBIDDEN_OVERRIDE" | "DI_UNRESOLVABLE_TOKEN" | "ASSET_MISSING_REQUIRED" | "ASSET_INVALID_CONFIG" | "ASSET_CONFLICT" | "ASSET_LOAD_FAILURE" | "LAYOUT_MISSING_REQUIRED" | "LAYOUT_INVALID_CONFIG" | "LAYOUT_CONFLICT" | "LAYOUT_INVARIANT_VIOLATION" | "AUDIO_MISSING_REQUIRED" | "AUDIO_INVALID_ARGUMENT" | "AUDIO_INVALID_CONFIG" | "AUDIO_CONFLICT" | "AUDIO_PLAYBACK_BLOCKED" | "PARTICLE_MISSING_REQUIRED" | "PARTICLE_INVALID_ARGUMENT" | "PARTICLE_INVALID_CONFIG" | "PARTICLE_INVARIANT_VIOLATION" | "PARTICLE_RUNTIME_FAILURE" | "PREFAB_MISSING_REQUIRED" | "PREFAB_INVALID_CONFIG" | "PREFAB_CONFLICT" | "PREFAB_INVARIANT_VIOLATION" | "FSM_INVALID_CONFIG" | "FSM_INVALID_STATE" | "COMMAND_MISSING_REQUIRED" | "COMMAND_INVALID_PAYLOAD" | "CONFIG_MISSING_REQUIRED" | "CONFIG_INVALID_ARGUMENT" | "CONFIG_INVALID_SCHEMA";
4910
4934
  export declare class EngineErrorCode {
4911
4935
  static readonly CoreInvalidArgument = "CORE_INVALID_ARGUMENT";
4912
4936
  static readonly CoreMissingRequired = "CORE_MISSING_REQUIRED";
@@ -4933,6 +4957,7 @@ export declare class EngineErrorCode {
4933
4957
  static readonly AudioInvalidArgument = "AUDIO_INVALID_ARGUMENT";
4934
4958
  static readonly AudioInvalidConfig = "AUDIO_INVALID_CONFIG";
4935
4959
  static readonly AudioConflict = "AUDIO_CONFLICT";
4960
+ static readonly AudioPlaybackBlocked = "AUDIO_PLAYBACK_BLOCKED";
4936
4961
  static readonly ParticleMissingRequired = "PARTICLE_MISSING_REQUIRED";
4937
4962
  static readonly ParticleInvalidArgument = "PARTICLE_INVALID_ARGUMENT";
4938
4963
  static readonly ParticleInvalidConfig = "PARTICLE_INVALID_CONFIG";