@scm-js/plugin-api 1.41.0 → 1.43.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/index.d.ts +733 -159
- package/package.json +1 -1
package/index.d.ts
CHANGED
|
@@ -979,6 +979,127 @@ export interface LocationPatch {
|
|
|
979
979
|
bottom?: number;
|
|
980
980
|
elevationFlags?: number;
|
|
981
981
|
}
|
|
982
|
+
export interface WireListChange<T> {
|
|
983
|
+
index: number;
|
|
984
|
+
before: T | null;
|
|
985
|
+
after: T | null;
|
|
986
|
+
}
|
|
987
|
+
export interface WireLocationChange {
|
|
988
|
+
index: number;
|
|
989
|
+
after: LocationRecord;
|
|
990
|
+
/** The name as text (`after.nameIndex` is resolved again where it lands); null for none. */
|
|
991
|
+
name: string | null;
|
|
992
|
+
/** The slot was empty before: a new location, which moves to a free slot if someone else took this one. */
|
|
993
|
+
created: boolean;
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* One undo step's worth of terrain and objects. The grids are flat number lists: terrain
|
|
997
|
+
* `[at, mtxm, tile, …]`, the others `[at, value, …]`. `reverse` marks the undo of an edit:
|
|
998
|
+
* its parts go in the reverse of the usual order, as undo walks them.
|
|
999
|
+
*/
|
|
1000
|
+
export interface SyncEditOp {
|
|
1001
|
+
kind: "edit";
|
|
1002
|
+
label: string;
|
|
1003
|
+
/** The map size the cells index; a grid part is skipped on a map of another size. */
|
|
1004
|
+
width: number;
|
|
1005
|
+
height: number;
|
|
1006
|
+
reverse?: boolean;
|
|
1007
|
+
terrain?: number[];
|
|
1008
|
+
isom?: number[];
|
|
1009
|
+
doodadTiles?: number[];
|
|
1010
|
+
fog?: number[];
|
|
1011
|
+
/** The whole ISOM lattice (base64 of the u16 cells) the edit gave the map, or null for taking it away. */
|
|
1012
|
+
isomWhole?: string | null;
|
|
1013
|
+
maskWhole?: string | null;
|
|
1014
|
+
units?: WireListChange<UnitRecord>[];
|
|
1015
|
+
doodads?: WireListChange<DoodadRecord>[];
|
|
1016
|
+
sprites?: WireListChange<SpriteRecord>[];
|
|
1017
|
+
locations?: WireLocationChange[];
|
|
1018
|
+
}
|
|
1019
|
+
declare const SYNC_FIELDS: readonly [
|
|
1020
|
+
"type",
|
|
1021
|
+
"fileVersion",
|
|
1022
|
+
"nameIndex",
|
|
1023
|
+
"descriptionIndex",
|
|
1024
|
+
"playerTypes",
|
|
1025
|
+
"editorPlayerTypes",
|
|
1026
|
+
"playerRaces",
|
|
1027
|
+
"playerColors",
|
|
1028
|
+
"playerRgb",
|
|
1029
|
+
"forces",
|
|
1030
|
+
"unitSettings",
|
|
1031
|
+
"unitAvailability",
|
|
1032
|
+
"upgradeSettings",
|
|
1033
|
+
"upgradeRestrictions",
|
|
1034
|
+
"techSettings",
|
|
1035
|
+
"techRestrictions",
|
|
1036
|
+
"wavs",
|
|
1037
|
+
"cuwp",
|
|
1038
|
+
"cuwpUsed",
|
|
1039
|
+
"triggers",
|
|
1040
|
+
"briefing",
|
|
1041
|
+
"switchNames"
|
|
1042
|
+
];
|
|
1043
|
+
export type SyncField = (typeof SYNC_FIELDS)[number];
|
|
1044
|
+
export interface SyncFieldsOp {
|
|
1045
|
+
kind: "fields";
|
|
1046
|
+
/** Each changed field's new value, packed (`pack`). */
|
|
1047
|
+
set: Partial<Record<SyncField, unknown>>;
|
|
1048
|
+
/** String slots that changed: `[index, before, after]`. */
|
|
1049
|
+
strings?: [
|
|
1050
|
+
number,
|
|
1051
|
+
string | null,
|
|
1052
|
+
string | null
|
|
1053
|
+
][];
|
|
1054
|
+
/** The table's length afterwards (trailing blank slots are dropped). */
|
|
1055
|
+
stringsLength?: number;
|
|
1056
|
+
/** The table's width and text encoding, when they changed. */
|
|
1057
|
+
stringsFormat?: {
|
|
1058
|
+
extended: boolean;
|
|
1059
|
+
encoding: TextEncoding;
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
export interface SyncExtrasOp {
|
|
1063
|
+
kind: "extras";
|
|
1064
|
+
/** `[member name, base64 bytes]`, or null bytes for a member taken out. */
|
|
1065
|
+
set: [
|
|
1066
|
+
string,
|
|
1067
|
+
string | null
|
|
1068
|
+
][];
|
|
1069
|
+
}
|
|
1070
|
+
export interface SyncResetOp {
|
|
1071
|
+
kind: "reset";
|
|
1072
|
+
label: string;
|
|
1073
|
+
/** The whole scenario, base64. */
|
|
1074
|
+
chk: string;
|
|
1075
|
+
}
|
|
1076
|
+
export type SyncOp = SyncEditOp | SyncFieldsOp | SyncExtrasOp | SyncResetOp;
|
|
1077
|
+
export type ListField = "triggers" | "briefing";
|
|
1078
|
+
export type ValueField = Exclude<SyncField, ListField>;
|
|
1079
|
+
export interface StringsFormat {
|
|
1080
|
+
extended: boolean;
|
|
1081
|
+
encoding: TextEncoding;
|
|
1082
|
+
}
|
|
1083
|
+
export interface Change<T> {
|
|
1084
|
+
before: T;
|
|
1085
|
+
after: T;
|
|
1086
|
+
}
|
|
1087
|
+
/** What one dialog commit changed, each part with both of its sides. */
|
|
1088
|
+
export interface TablesEdit {
|
|
1089
|
+
/** The changed tables as JSON text (`pack`ed). */
|
|
1090
|
+
values?: Partial<Record<ValueField, Change<string>>>;
|
|
1091
|
+
lists?: Partial<Record<ListField, Change<TriggerRecord[]>>>;
|
|
1092
|
+
/** String slots that changed: `[index, before, after]`. */
|
|
1093
|
+
strings?: [
|
|
1094
|
+
number,
|
|
1095
|
+
string | null,
|
|
1096
|
+
string | null
|
|
1097
|
+
][];
|
|
1098
|
+
stringsLength?: Change<number>;
|
|
1099
|
+
stringsFormat?: Change<StringsFormat>;
|
|
1100
|
+
/** The archive's files, when the commit changed them too (the Sound Editor). */
|
|
1101
|
+
extras?: Change<Map<string, Uint8Array>>;
|
|
1102
|
+
}
|
|
982
1103
|
/** The change lists of one edit; `HistoryEntry` adds the label. */
|
|
983
1104
|
export interface HistoryEdit {
|
|
984
1105
|
changes: TileChange[];
|
|
@@ -1017,10 +1138,16 @@ export interface HistoryEdit {
|
|
|
1017
1138
|
* on such a map). Undo removes the section again.
|
|
1018
1139
|
*/
|
|
1019
1140
|
createdMask?: Uint8Array;
|
|
1141
|
+
/**
|
|
1142
|
+
* What a dialog wrote — settings, triggers, the briefing, strings (editor/tableHistory.ts).
|
|
1143
|
+
* An entry with this part has no other: `applyEntry` leaves it alone, and the undo and
|
|
1144
|
+
* redo atoms apply it with `applyTables`.
|
|
1145
|
+
*/
|
|
1146
|
+
tables?: TablesEdit;
|
|
1020
1147
|
}
|
|
1021
1148
|
/**
|
|
1022
1149
|
* Why the map changed: an edit recorded in the history, an undo or redo of one, a dialog
|
|
1023
|
-
* writing its tables (settings, triggers, strings
|
|
1150
|
+
* writing its tables (settings, triggers, strings), a change to the
|
|
1024
1151
|
* whole document (resize, tileset change, raw section edit), or other people's edits on a
|
|
1025
1152
|
* shared map.
|
|
1026
1153
|
*/
|
|
@@ -1217,7 +1344,7 @@ export interface StackedLine {
|
|
|
1217
1344
|
pieces: number;
|
|
1218
1345
|
}
|
|
1219
1346
|
/** Every built-in dialog, by the id `openDialogAtom` takes; `DialogHost` maps each to its component. */
|
|
1220
|
-
export type DialogId = "newMap" | "openMap" | "saveAs" | "exportImage" | "mapProperties" | "resizeMap" | "mapRevision" | "playerSettings" | "forceSettings" | "playerColors" | "unitSettings" | "upgradeSettings" | "techSettings" | "stringEditor" | "soundEditor" | "switches" | "locationList" | "unitProperties" | "locationProperties" | "spriteProperties" | "triggerEditor" | "missionBriefing" | "cuwpEditor" | "replaceTerrain" | "autoStarts" | "testMap" | "symmetry" | "gridSettings" | "preferences" | "shortcuts" | "validateMap" | "statistics" | "importTriggers" | "exportTriggers" | "importStrings" | "exportStrings" | "find" | "about" | "confirmClose" | "recovery" | "previousVersions" | "plugins" | "confirmPlugin" | "pluginDialog" | "gameData" | "update";
|
|
1347
|
+
export type DialogId = "newMap" | "openMap" | "saveAs" | "exportImage" | "mapProperties" | "resizeMap" | "mapRevision" | "playerSettings" | "forceSettings" | "playerColors" | "unitSettings" | "upgradeSettings" | "techSettings" | "stringEditor" | "soundEditor" | "switches" | "locationList" | "unitProperties" | "locationProperties" | "spriteProperties" | "triggerEditor" | "missionBriefing" | "cuwpEditor" | "replaceTerrain" | "autoStarts" | "testMap" | "symmetry" | "gridSettings" | "preferences" | "shortcuts" | "validateMap" | "statistics" | "importTriggers" | "exportTriggers" | "importStrings" | "exportStrings" | "find" | "about" | "confirmClose" | "recovery" | "previousVersions" | "plugins" | "confirmPlugin" | "pluginDialog" | "gameData" | "update" | "whatsNew";
|
|
1221
1348
|
export interface MapImageOptions {
|
|
1222
1349
|
/** Output pixels per map tile. 32 matches the game's art 1:1; 1 is a minimap. */
|
|
1223
1350
|
pixelsPerTile: number;
|
|
@@ -1854,101 +1981,6 @@ export interface ChangeTilesetResult {
|
|
|
1854
1981
|
spritesDropped: number;
|
|
1855
1982
|
refilled: boolean;
|
|
1856
1983
|
}
|
|
1857
|
-
export interface WireListChange<T> {
|
|
1858
|
-
index: number;
|
|
1859
|
-
before: T | null;
|
|
1860
|
-
after: T | null;
|
|
1861
|
-
}
|
|
1862
|
-
export interface WireLocationChange {
|
|
1863
|
-
index: number;
|
|
1864
|
-
after: LocationRecord;
|
|
1865
|
-
/** The name as text (`after.nameIndex` is resolved again where it lands); null for none. */
|
|
1866
|
-
name: string | null;
|
|
1867
|
-
/** The slot was empty before: a new location, which moves to a free slot if someone else took this one. */
|
|
1868
|
-
created: boolean;
|
|
1869
|
-
}
|
|
1870
|
-
/**
|
|
1871
|
-
* One undo step's worth of terrain and objects. The grids are flat number lists: terrain
|
|
1872
|
-
* `[at, mtxm, tile, …]`, the others `[at, value, …]`. `reverse` marks the undo of an edit:
|
|
1873
|
-
* its parts go in the reverse of the usual order, as undo walks them.
|
|
1874
|
-
*/
|
|
1875
|
-
export interface SyncEditOp {
|
|
1876
|
-
kind: "edit";
|
|
1877
|
-
label: string;
|
|
1878
|
-
/** The map size the cells index; a grid part is skipped on a map of another size. */
|
|
1879
|
-
width: number;
|
|
1880
|
-
height: number;
|
|
1881
|
-
reverse?: boolean;
|
|
1882
|
-
terrain?: number[];
|
|
1883
|
-
isom?: number[];
|
|
1884
|
-
doodadTiles?: number[];
|
|
1885
|
-
fog?: number[];
|
|
1886
|
-
/** The whole ISOM lattice (base64 of the u16 cells) the edit gave the map, or null for taking it away. */
|
|
1887
|
-
isomWhole?: string | null;
|
|
1888
|
-
maskWhole?: string | null;
|
|
1889
|
-
units?: WireListChange<UnitRecord>[];
|
|
1890
|
-
doodads?: WireListChange<DoodadRecord>[];
|
|
1891
|
-
sprites?: WireListChange<SpriteRecord>[];
|
|
1892
|
-
locations?: WireLocationChange[];
|
|
1893
|
-
}
|
|
1894
|
-
declare const SYNC_FIELDS: readonly [
|
|
1895
|
-
"type",
|
|
1896
|
-
"fileVersion",
|
|
1897
|
-
"nameIndex",
|
|
1898
|
-
"descriptionIndex",
|
|
1899
|
-
"playerTypes",
|
|
1900
|
-
"editorPlayerTypes",
|
|
1901
|
-
"playerRaces",
|
|
1902
|
-
"playerColors",
|
|
1903
|
-
"playerRgb",
|
|
1904
|
-
"forces",
|
|
1905
|
-
"unitSettings",
|
|
1906
|
-
"unitAvailability",
|
|
1907
|
-
"upgradeSettings",
|
|
1908
|
-
"upgradeRestrictions",
|
|
1909
|
-
"techSettings",
|
|
1910
|
-
"techRestrictions",
|
|
1911
|
-
"wavs",
|
|
1912
|
-
"cuwp",
|
|
1913
|
-
"cuwpUsed",
|
|
1914
|
-
"triggers",
|
|
1915
|
-
"briefing",
|
|
1916
|
-
"switchNames"
|
|
1917
|
-
];
|
|
1918
|
-
export type SyncField = (typeof SYNC_FIELDS)[number];
|
|
1919
|
-
export interface SyncFieldsOp {
|
|
1920
|
-
kind: "fields";
|
|
1921
|
-
/** Each changed field's new value, packed (`pack`). */
|
|
1922
|
-
set: Partial<Record<SyncField, unknown>>;
|
|
1923
|
-
/** String slots that changed: `[index, before, after]`. */
|
|
1924
|
-
strings?: [
|
|
1925
|
-
number,
|
|
1926
|
-
string | null,
|
|
1927
|
-
string | null
|
|
1928
|
-
][];
|
|
1929
|
-
/** The table's length afterwards (trailing blank slots are dropped). */
|
|
1930
|
-
stringsLength?: number;
|
|
1931
|
-
/** The table's width and text encoding, when they changed. */
|
|
1932
|
-
stringsFormat?: {
|
|
1933
|
-
extended: boolean;
|
|
1934
|
-
encoding: TextEncoding;
|
|
1935
|
-
};
|
|
1936
|
-
}
|
|
1937
|
-
export interface SyncExtrasOp {
|
|
1938
|
-
kind: "extras";
|
|
1939
|
-
/** `[member name, base64 bytes]`, or null bytes for a member taken out. */
|
|
1940
|
-
set: [
|
|
1941
|
-
string,
|
|
1942
|
-
string | null
|
|
1943
|
-
][];
|
|
1944
|
-
}
|
|
1945
|
-
export interface SyncResetOp {
|
|
1946
|
-
kind: "reset";
|
|
1947
|
-
label: string;
|
|
1948
|
-
/** The whole scenario, base64. */
|
|
1949
|
-
chk: string;
|
|
1950
|
-
}
|
|
1951
|
-
export type SyncOp = SyncEditOp | SyncFieldsOp | SyncExtrasOp | SyncResetOp;
|
|
1952
1984
|
export type SymmetryMode = "none" | "h" | "v" | "hv" | "rot180" | "rot90" | "diag" | "adiag";
|
|
1953
1985
|
export type StartLayout = "ring" | "corners";
|
|
1954
1986
|
export interface StartPlacementResult {
|
|
@@ -2403,6 +2435,12 @@ export interface PluginApi {
|
|
|
2403
2435
|
* It is for a plugin that runs other code for a while and then takes it all back: the
|
|
2404
2436
|
* API Playground runs each snippet in one. The child speaks as the same plugin — same
|
|
2405
2437
|
* id, storage and log name.
|
|
2438
|
+
*
|
|
2439
|
+
* @example
|
|
2440
|
+
* // Register things for ten seconds, then take them all back.
|
|
2441
|
+
* const scope = api.scope();
|
|
2442
|
+
* scope.api.menu.add("Tools", { label: "Temporary item", run: () => scope.api.ui.status("Still here") });
|
|
2443
|
+
* setTimeout(() => scope.dispose(), 10_000);
|
|
2406
2444
|
*/
|
|
2407
2445
|
scope(): PluginScope;
|
|
2408
2446
|
}
|
|
@@ -2492,6 +2530,18 @@ export interface BuildStepInput {
|
|
|
2492
2530
|
* A step that throws, or that the user stops waiting for, does not stop a save: the map is
|
|
2493
2531
|
* written without the steps and a notice says why. Test Map stops instead, since a map
|
|
2494
2532
|
* without its built part is not the one to test. A bare `.chk` is never built.
|
|
2533
|
+
*
|
|
2534
|
+
* @example
|
|
2535
|
+
* api.document.buildSteps.add({
|
|
2536
|
+
* id: "stamp",
|
|
2537
|
+
* label: "Build stamp",
|
|
2538
|
+
* // Only maps that carry this file are built; every other Save is untouched.
|
|
2539
|
+
* applies: () => api.document.extras.get("my-plugin\\stamp.txt") !== null,
|
|
2540
|
+
* async run({ map, purpose }) {
|
|
2541
|
+
* api.log(`building ${map.length} bytes for ${purpose}`);
|
|
2542
|
+
* return map; // an archive in, an archive out
|
|
2543
|
+
* },
|
|
2544
|
+
* });
|
|
2495
2545
|
*/
|
|
2496
2546
|
export interface BuildStepSpec {
|
|
2497
2547
|
/** Unique within the plugin; the file remembers `plugin id/id`. */
|
|
@@ -2543,6 +2593,14 @@ export interface BuildStepsApi {
|
|
|
2543
2593
|
* The files stored in the map archive next to `staredit\scenario.chk`: custom sounds,
|
|
2544
2594
|
* graphics, and anything a plugin wants to keep with the map. Names are archive paths
|
|
2545
2595
|
* with backslashes (`staredit\wav\hello.wav`). They are written on Save.
|
|
2596
|
+
*
|
|
2597
|
+
* @example
|
|
2598
|
+
* // Keep a small JSON file inside the map, in a folder of the plugin's own.
|
|
2599
|
+
* const name = "my-plugin\\notes.json";
|
|
2600
|
+
* const bytes = api.document.extras.get(name);
|
|
2601
|
+
* const notes = bytes ? JSON.parse(new TextDecoder().decode(bytes)) : { opened: 0 };
|
|
2602
|
+
* notes.opened++;
|
|
2603
|
+
* api.document.extras.set(name, new TextEncoder().encode(JSON.stringify(notes)));
|
|
2546
2604
|
*/
|
|
2547
2605
|
export interface ExtrasApi {
|
|
2548
2606
|
list(): string[];
|
|
@@ -2619,6 +2677,13 @@ export interface DocumentApi {
|
|
|
2619
2677
|
/**
|
|
2620
2678
|
* The live scenario, for reading — see `ReadonlyScenario`. To change the map, use
|
|
2621
2679
|
* `edit`, `update` or `sections`.
|
|
2680
|
+
*
|
|
2681
|
+
* @example
|
|
2682
|
+
* const scn = api.document.scenario();
|
|
2683
|
+
* if (scn) {
|
|
2684
|
+
* const marines = scn.units.filter((u) => u.unitId === 0);
|
|
2685
|
+
* api.ui.status(`${marines.length} marines on a ${scn.width} × ${scn.height} map`);
|
|
2686
|
+
* }
|
|
2622
2687
|
*/
|
|
2623
2688
|
scenario(): ReadonlyScenario | null;
|
|
2624
2689
|
/**
|
|
@@ -2635,25 +2700,26 @@ export interface DocumentApi {
|
|
|
2635
2700
|
*
|
|
2636
2701
|
* @example
|
|
2637
2702
|
* // One undo entry called "Fill", however many operations it takes.
|
|
2638
|
-
*
|
|
2703
|
+
* await api.tileset.load(); // the async part first
|
|
2704
|
+
* const [, ground] = api.terrain.types(); // the tileset's second flat terrain; none without the graphics
|
|
2639
2705
|
* const result = api.document.edit("Fill", (tx) => {
|
|
2640
|
-
* tx.stampTerrain({ x0: 0, y0: 0, x1: 8, y1: 8 }, ground);
|
|
2706
|
+
* if (ground) tx.stampTerrain({ x0: 0, y0: 0, x1: 8, y1: 8 }, ground.id);
|
|
2641
2707
|
* tx.placeUnit(api.consts.unit.startLocation, 0, 4 * api.consts.tile, 4 * api.consts.tile);
|
|
2642
2708
|
* });
|
|
2643
2709
|
* api.ui.status(`${result.tiles} tiles, ${result.units} units`);
|
|
2644
2710
|
*/
|
|
2645
2711
|
edit<R>(label: string, build: (tx: EditTransaction) => Sync<R>): EditResult;
|
|
2646
2712
|
/**
|
|
2647
|
-
* The second kind of write: the tables and settings
|
|
2648
|
-
*
|
|
2649
|
-
*
|
|
2650
|
-
*
|
|
2651
|
-
* what
|
|
2713
|
+
* The second kind of write: the tables and settings the dialogs edit — triggers, the
|
|
2714
|
+
* string table, switch names, the scenario's own properties — as one transaction, the
|
|
2715
|
+
* way a settings dialog's OK applies its whole form at once. Operations apply as they
|
|
2716
|
+
* are called; the commit marks the map modified, bumps what the chrome reads and
|
|
2717
|
+
* records what changed as one undo entry under `label`.
|
|
2652
2718
|
* `build` is synchronous, as `edit`'s is, and closed when it returns.
|
|
2653
2719
|
*
|
|
2654
|
-
*
|
|
2655
|
-
*
|
|
2656
|
-
*
|
|
2720
|
+
* An update is not rolled back, so a builder that throws leaves what it wrote: the
|
|
2721
|
+
* sections it touched are committed — the map is modified, the chrome re-reads, the
|
|
2722
|
+
* undo entry holds what was written — and the error reaches you.
|
|
2657
2723
|
*
|
|
2658
2724
|
* @example
|
|
2659
2725
|
* api.document.update("Rename", (tx) => {
|
|
@@ -2663,7 +2729,13 @@ export interface DocumentApi {
|
|
|
2663
2729
|
update<R>(label: string, build: (tx: UpdateTransaction) => Sync<R>): UpdateResult;
|
|
2664
2730
|
undo(): string | null;
|
|
2665
2731
|
redo(): string | null;
|
|
2666
|
-
/**
|
|
2732
|
+
/**
|
|
2733
|
+
* The undo and redo stacks' tops — the labels the Edit menu shows — and their depths, without moving anything.
|
|
2734
|
+
*
|
|
2735
|
+
* @example
|
|
2736
|
+
* const { undo } = api.document.history();
|
|
2737
|
+
* if (undo && (await api.ui.confirm(`Undo "${undo}"?`))) api.document.undo();
|
|
2738
|
+
*/
|
|
2667
2739
|
history(): DocumentHistory;
|
|
2668
2740
|
/**
|
|
2669
2741
|
* The id of the map in front — the one `info()`, `scenario()` and every write are
|
|
@@ -2675,6 +2747,9 @@ export interface DocumentApi {
|
|
|
2675
2747
|
* Every open map, in the order the editor lists them, the one in front marked `active`.
|
|
2676
2748
|
* Empty with no map. The list changes only through `open`, `create`, `activate` and
|
|
2677
2749
|
* `close` (by any plugin or the user), each of which fires the `"document"` event.
|
|
2750
|
+
*
|
|
2751
|
+
* @example
|
|
2752
|
+
* for (const map of api.document.list()) api.log(map.id, map.name, map.active ? "(in front)" : "");
|
|
2678
2753
|
*/
|
|
2679
2754
|
list(): OpenDocumentInfo[];
|
|
2680
2755
|
/**
|
|
@@ -2692,6 +2767,10 @@ export interface DocumentApi {
|
|
|
2692
2767
|
* Scenario dialog comes first and the user may cancel. Resolves true once the file is
|
|
2693
2768
|
* the map in front, false when the user kept the current map or the file could not be
|
|
2694
2769
|
* read (the status bar says why).
|
|
2770
|
+
*
|
|
2771
|
+
* @example
|
|
2772
|
+
* const [file] = await api.ui.pickFiles({ accept: ".scx,.scm,.chk" });
|
|
2773
|
+
* if (file) await api.document.open(file, file.name, { into: "new" });
|
|
2695
2774
|
*/
|
|
2696
2775
|
open(file: File | Blob | Uint8Array, fileName?: string, options?: OpenDocumentOptions): Promise<boolean>;
|
|
2697
2776
|
/**
|
|
@@ -2700,11 +2779,19 @@ export interface DocumentApi {
|
|
|
2700
2779
|
* the open map or in place of it as `open` decides, through the same unsaved-changes
|
|
2701
2780
|
* gate. Resolves true once the new map is in front, false when the user kept the
|
|
2702
2781
|
* current one.
|
|
2782
|
+
*
|
|
2783
|
+
* @example
|
|
2784
|
+
* await api.document.create({ width: 128, height: 128, tileset: "jungle", name: "Sketch", startLocations: 4 });
|
|
2703
2785
|
*/
|
|
2704
2786
|
create(options: NewDocumentOptions): Promise<boolean>;
|
|
2705
2787
|
/**
|
|
2706
2788
|
* The open map as a file, as Save would write it — the remembered save options, the
|
|
2707
2789
|
* archive extras included — unless `saveOptions` says otherwise. Null when no map is open.
|
|
2790
|
+
*
|
|
2791
|
+
* @example
|
|
2792
|
+
* // The bytes Save would write, without saving: ready to upload or hand to another tool.
|
|
2793
|
+
* const file = await api.document.export({ format: "scx" });
|
|
2794
|
+
* if (file) api.ui.status(`${file.name}: ${file.size} bytes`);
|
|
2708
2795
|
*/
|
|
2709
2796
|
export(options?: ExportOptions): Promise<File | null>;
|
|
2710
2797
|
/**
|
|
@@ -2730,6 +2817,11 @@ export interface DocumentApi {
|
|
|
2730
2817
|
* is the one dial (32 is the game's art 1:1, 1 is a minimap); the other options
|
|
2731
2818
|
* default as the dialog's do. Needs the tileset graphics — without them, or without
|
|
2732
2819
|
* a map, null.
|
|
2820
|
+
*
|
|
2821
|
+
* @example
|
|
2822
|
+
* // A minimap-sized picture of the whole map, four pixels to a tile.
|
|
2823
|
+
* const png = await api.document.renderImage({ pixelsPerTile: 4 });
|
|
2824
|
+
* if (png) await api.ui.saveFile(png, "minimap.png");
|
|
2733
2825
|
*/
|
|
2734
2826
|
renderImage(options?: Partial<MapImageOptions>): Promise<Blob | null>;
|
|
2735
2827
|
/**
|
|
@@ -2781,6 +2873,11 @@ export interface RawEditResult {
|
|
|
2781
2873
|
*
|
|
2782
2874
|
* Indices are positions in `list()` and shift when a section is inserted or removed
|
|
2783
2875
|
* before them; take a fresh `list()` after every edit.
|
|
2876
|
+
*
|
|
2877
|
+
* @example
|
|
2878
|
+
* // The file's sections, largest first.
|
|
2879
|
+
* const sections = api.document.sections.list().sort((a, b) => b.size - a.size);
|
|
2880
|
+
* for (const s of sections) api.log(s.name, `${s.size} bytes`, s.spec ? "" : "(unknown to the editor)");
|
|
2784
2881
|
*/
|
|
2785
2882
|
export interface SectionsApi {
|
|
2786
2883
|
/** Every occurrence in file order; empty without a map. */
|
|
@@ -2865,7 +2962,13 @@ export interface EditTransaction {
|
|
|
2865
2962
|
groundAt(x: number, y: number): number;
|
|
2866
2963
|
/** One tile into both sections. */
|
|
2867
2964
|
setTile(x: number, y: number, id: number): void;
|
|
2868
|
-
/**
|
|
2965
|
+
/**
|
|
2966
|
+
* Many tiles into both sections; returns how many changed.
|
|
2967
|
+
*
|
|
2968
|
+
* @example
|
|
2969
|
+
* // Copy the tile in the top-left corner over a 4 × 4 square beside it.
|
|
2970
|
+
* api.document.edit("Repeat a tile", (tx) => tx.setTiles({ x0: 1, y0: 0, x1: 5, y1: 4 }, tx.tileAt(0, 0)));
|
|
2971
|
+
*/
|
|
2869
2972
|
setTiles(cells: Cells, id: number): number;
|
|
2870
2973
|
/**
|
|
2871
2974
|
* The Rect brush: flat left/right pairs by column parity, one random variation per
|
|
@@ -2878,6 +2981,15 @@ export interface EditTransaction {
|
|
|
2878
2981
|
* The isometric brush on one diamond: sets its ISOM value and regenerates the tiles
|
|
2879
2982
|
* around it, cliffs and shores included. Needs ISOM and the tileset; returns whether
|
|
2880
2983
|
* the terrain could be painted there.
|
|
2984
|
+
*
|
|
2985
|
+
* @example
|
|
2986
|
+
* await api.tileset.load();
|
|
2987
|
+
* const [terrain] = api.terrain.isomTypes();
|
|
2988
|
+
* if (terrain !== undefined) {
|
|
2989
|
+
* api.document.edit("Paint a patch", (tx) => {
|
|
2990
|
+
* for (const d of api.terrain.diamondsIn({ x0: 8, y0: 8, x1: 16, y1: 16 })) tx.paintIsom(d, terrain);
|
|
2991
|
+
* });
|
|
2992
|
+
* }
|
|
2881
2993
|
*/
|
|
2882
2994
|
paintIsom(d: Diamond, terrainId: number, extent?: number): boolean;
|
|
2883
2995
|
/**
|
|
@@ -2892,6 +3004,12 @@ export interface EditTransaction {
|
|
|
2892
3004
|
makeUnit(unitId: number, owner: number, x: number, y: number): UnitRecord;
|
|
2893
3005
|
addUnits(records: readonly UnitRecord[]): number[];
|
|
2894
3006
|
removeUnits(indices: readonly number[]): number;
|
|
3007
|
+
/**
|
|
3008
|
+
* @example
|
|
3009
|
+
* // Give every selected unit to Player 2.
|
|
3010
|
+
* const selected = api.selection.units();
|
|
3011
|
+
* api.document.edit("Give to Player 2", (tx) => tx.updateUnits(selected, () => ({ owner: 1 })));
|
|
3012
|
+
*/
|
|
2895
3013
|
updateUnits(indices: readonly number[], patch: (u: UnitRecord) => Partial<UnitRecord>): number;
|
|
2896
3014
|
/**
|
|
2897
3015
|
* A unit the way the Units palette places one: a building snaps its placement box to
|
|
@@ -2900,7 +3018,18 @@ export interface EditTransaction {
|
|
|
2900
3018
|
* if you want them. Returns the record's index.
|
|
2901
3019
|
*/
|
|
2902
3020
|
placeUnit(unitId: number, owner: number, x: number, y: number): number;
|
|
2903
|
-
/**
|
|
3021
|
+
/**
|
|
3022
|
+
* Whether the Units palette's placement checks, with its current options, allow a unit of this type centred there.
|
|
3023
|
+
*
|
|
3024
|
+
* @example
|
|
3025
|
+
* const marine = 0;
|
|
3026
|
+
* const x = 10 * api.consts.tile;
|
|
3027
|
+
* const y = 10 * api.consts.tile;
|
|
3028
|
+
* api.document.edit("Place a marine", (tx) => {
|
|
3029
|
+
* if (tx.canPlaceUnit(marine, x, y)) tx.placeUnit(marine, 0, x, y);
|
|
3030
|
+
* else tx.note("no room there");
|
|
3031
|
+
* });
|
|
3032
|
+
*/
|
|
2904
3033
|
canPlaceUnit(unitId: number, x: number, y: number): boolean;
|
|
2905
3034
|
makeSprite(kind: SpriteKind, id: number, owner: number, x: number, y: number, opts?: {
|
|
2906
3035
|
flipped?: boolean;
|
|
@@ -2931,13 +3060,38 @@ export interface EditTransaction {
|
|
|
2931
3060
|
* unless `parts` says otherwise; `mode` is `"merge"` unless given. Terrain and doodads
|
|
2932
3061
|
* from another tileset are refused, as a paste refuses them, and anything off the map is
|
|
2933
3062
|
* skipped, both said in the result's `notes`.
|
|
3063
|
+
*
|
|
3064
|
+
* @example
|
|
3065
|
+
* // Stamp the marked area again ten tiles to the right, as one undo step.
|
|
3066
|
+
* const area = api.selection.markedArea();
|
|
3067
|
+
* const clip = area && api.clipboard.capture({ rect: area });
|
|
3068
|
+
* if (area && clip) api.document.edit("Stamp", (tx) => { tx.paste(clip, area.x0 + 10, area.y0); });
|
|
2934
3069
|
*/
|
|
2935
3070
|
paste(clip: Clip, tx: number, ty: number, options?: PasteOptionsSpec): PasteResult;
|
|
2936
|
-
/**
|
|
3071
|
+
/**
|
|
3072
|
+
* A location in the lowest free slot (pixel bounds); returns the slot, or -1 when the table is full.
|
|
3073
|
+
*
|
|
3074
|
+
* @example
|
|
3075
|
+
* const t = api.consts.tile;
|
|
3076
|
+
* api.document.edit("Add a location", (tx) => {
|
|
3077
|
+
* const slot = tx.addLocation({ left: 4 * t, top: 4 * t, right: 8 * t, bottom: 8 * t }, "Beacon Alpha");
|
|
3078
|
+
* if (slot < 0) tx.note("the location table is full");
|
|
3079
|
+
* });
|
|
3080
|
+
*/
|
|
2937
3081
|
addLocation(bounds: Bounds, name?: string, elevationFlags?: number): number;
|
|
2938
3082
|
editLocation(index: number, patch: LocationPatch): boolean;
|
|
2939
3083
|
removeLocations(indices: readonly number[]): number;
|
|
2940
|
-
/**
|
|
3084
|
+
/**
|
|
3085
|
+
* Set (`"fog"`) or clear the `players` bits (bit n = player n + 1) over cells; creates MASK on first use.
|
|
3086
|
+
*
|
|
3087
|
+
* @example
|
|
3088
|
+
* // Fog the left half of the map for Player 1 and Player 2.
|
|
3089
|
+
* const info = api.document.info();
|
|
3090
|
+
* if (info) {
|
|
3091
|
+
* const half = { x0: 0, y0: 0, x1: info.width >> 1, y1: info.height };
|
|
3092
|
+
* api.document.edit("Fog the west", (tx) => tx.setFog(half, 0b11, "fog"));
|
|
3093
|
+
* }
|
|
3094
|
+
*/
|
|
2941
3095
|
setFog(cells: Cells, players: number, mode: FogMode): number;
|
|
2942
3096
|
/**
|
|
2943
3097
|
* Tools ▸ Replace Terrain: every tile matching `from` (a flat terrain by ISOM id, or
|
|
@@ -2971,7 +3125,15 @@ export interface EditTransaction {
|
|
|
2971
3125
|
* what the built-in brushes paint over. With the mode off, the cells as given.
|
|
2972
3126
|
*/
|
|
2973
3127
|
mirror(cells: Cells): number[];
|
|
2974
|
-
/**
|
|
3128
|
+
/**
|
|
3129
|
+
* A map pixel and its images under the symmetry mode, the original first.
|
|
3130
|
+
*
|
|
3131
|
+
* @example
|
|
3132
|
+
* // Follow Tools ▸ Symmetry: one marine at the point and one at each of its images.
|
|
3133
|
+
* api.document.edit("Marines", (tx) => {
|
|
3134
|
+
* for (const p of tx.mirrorPoint(320, 320)) tx.placeUnit(0, 0, p.x, p.y);
|
|
3135
|
+
* });
|
|
3136
|
+
*/
|
|
2975
3137
|
mirrorPoint(px: number, py: number): {
|
|
2976
3138
|
x: number;
|
|
2977
3139
|
y: number;
|
|
@@ -2982,6 +3144,12 @@ export interface EditTransaction {
|
|
|
2982
3144
|
* Tools ▸ Auto-place Start Locations: one per player on a ring or in the corners, each
|
|
2983
3145
|
* moved to the nearest spot the placement checks accept; `replace` removes the existing
|
|
2984
3146
|
* ones first. Players count from 1.
|
|
3147
|
+
*
|
|
3148
|
+
* @example
|
|
3149
|
+
* api.document.edit("Four starts", (tx) => {
|
|
3150
|
+
* const { placed } = tx.placeStartLocations({ players: 4, replace: true });
|
|
3151
|
+
* tx.note(`${placed.filter(Boolean).length} of 4 placed`);
|
|
3152
|
+
* });
|
|
2985
3153
|
*/
|
|
2986
3154
|
placeStartLocations(options: {
|
|
2987
3155
|
players: number;
|
|
@@ -3034,6 +3202,16 @@ export interface TriggerListUpdate {
|
|
|
3034
3202
|
* Parse the text format (`triggers.text.print`'s inverse) and append the result, or
|
|
3035
3203
|
* replace the list with `replace: true`. Strings the text names are interned as it
|
|
3036
3204
|
* parses. Throws with the line number when the text does not parse.
|
|
3205
|
+
*
|
|
3206
|
+
* @example
|
|
3207
|
+
* const source = `
|
|
3208
|
+
* Trigger("Player 1"){
|
|
3209
|
+
* Conditions:
|
|
3210
|
+
* Always();
|
|
3211
|
+
* Actions:
|
|
3212
|
+
* Display Text Message(Always Display, "Welcome.");
|
|
3213
|
+
* }`;
|
|
3214
|
+
* api.document.update("Add a greeting", (tx) => { tx.triggers.fromText(source); });
|
|
3037
3215
|
*/
|
|
3038
3216
|
fromText(source: string, options?: {
|
|
3039
3217
|
replace?: boolean;
|
|
@@ -3046,7 +3224,18 @@ export interface TriggerListUpdate {
|
|
|
3046
3224
|
*/
|
|
3047
3225
|
export interface StringsUpdate {
|
|
3048
3226
|
list(): (string | null)[];
|
|
3049
|
-
/**
|
|
3227
|
+
/**
|
|
3228
|
+
* The index of `text`: an identical entry when there is one, else a new one. 0 for `""`.
|
|
3229
|
+
*
|
|
3230
|
+
* @example
|
|
3231
|
+
* // Point the first trigger's first action at a new string, without touching the old one.
|
|
3232
|
+
* api.document.update("Reword", (tx) => {
|
|
3233
|
+
* const [trigger] = tx.triggers.list();
|
|
3234
|
+
* if (!trigger) return;
|
|
3235
|
+
* trigger.actions[0].text = tx.strings.intern("Hold the line!");
|
|
3236
|
+
* tx.triggers.replace(0, trigger);
|
|
3237
|
+
* });
|
|
3238
|
+
*/
|
|
3050
3239
|
intern(text: string): number;
|
|
3051
3240
|
/** Overwrite one slot. */
|
|
3052
3241
|
set(index: number, text: string): void;
|
|
@@ -3063,7 +3252,12 @@ export interface StringsUpdate {
|
|
|
3063
3252
|
}
|
|
3064
3253
|
export interface SwitchesUpdate {
|
|
3065
3254
|
names(): string[];
|
|
3066
|
-
/**
|
|
3255
|
+
/**
|
|
3256
|
+
* Name a switch (0-based); `""` clears the name. Creates SWNM on the first one.
|
|
3257
|
+
*
|
|
3258
|
+
* @example
|
|
3259
|
+
* api.document.update("Name a switch", (tx) => tx.switches.setName(0, "Gate is open"));
|
|
3260
|
+
*/
|
|
3067
3261
|
setName(index: number, name: string): void;
|
|
3068
3262
|
}
|
|
3069
3263
|
/**
|
|
@@ -3111,6 +3305,15 @@ export interface UpdateTransaction {
|
|
|
3111
3305
|
/** A line for the status bar, appended to the label. */
|
|
3112
3306
|
note(text: string): void;
|
|
3113
3307
|
}
|
|
3308
|
+
/**
|
|
3309
|
+
* @example
|
|
3310
|
+
* // Make Player 2 a Zerg computer.
|
|
3311
|
+
* const computer = api.names.playerTypes().find((t) => t.label === "Computer");
|
|
3312
|
+
* const zerg = api.names.races().find((r) => r.label === "Zerg");
|
|
3313
|
+
* if (computer && zerg) {
|
|
3314
|
+
* api.document.update("Player 2: Zerg computer", (tx) => { tx.players.set(1, { type: computer.value, race: zerg.value }); });
|
|
3315
|
+
* }
|
|
3316
|
+
*/
|
|
3114
3317
|
export interface PlayersUpdate {
|
|
3115
3318
|
/** All 12 slots, 0-based, with the effective colour and force. */
|
|
3116
3319
|
list(): PlayerSlotView[];
|
|
@@ -3128,6 +3331,10 @@ export interface UnitTypesUpdate {
|
|
|
3128
3331
|
* Patch one type. Setting any number turns "use default" off for it (seeding the untouched
|
|
3129
3332
|
* columns from the dat, as the dialog does); `useDefault: true` puts it back. Hit points are
|
|
3130
3333
|
* whole points. `name` is the custom name (`""` restores the default); `available` edits PUNI.
|
|
3334
|
+
*
|
|
3335
|
+
* @example
|
|
3336
|
+
* // Marines with 80 hit points and a name of their own.
|
|
3337
|
+
* api.document.update("Tougher marines", (tx) => { tx.unitTypes.set(0, { hitPoints: 80, name: "Veteran Marine" }); });
|
|
3131
3338
|
*/
|
|
3132
3339
|
set(unitId: number, patch: UnitTypePatch): boolean;
|
|
3133
3340
|
}
|
|
@@ -3172,6 +3379,11 @@ export interface CuwpUpdate {
|
|
|
3172
3379
|
* What the settings dialogs show, read without a transaction: the same views
|
|
3173
3380
|
* `document.update`'s `tx.players` … `tx.techs` hand out. Every list is empty and every
|
|
3174
3381
|
* single read null when no map is open.
|
|
3382
|
+
*
|
|
3383
|
+
* @example
|
|
3384
|
+
* for (const p of api.settings.players().slice(0, 8)) {
|
|
3385
|
+
* api.log(`Player ${p.slot + 1}: ${p.typeName}, ${p.raceName}, ${p.forceName ?? "no force"}`);
|
|
3386
|
+
* }
|
|
3175
3387
|
*/
|
|
3176
3388
|
export interface SettingsApi {
|
|
3177
3389
|
players(): PlayerSlotView[];
|
|
@@ -3197,6 +3409,12 @@ export interface SettingsApi {
|
|
|
3197
3409
|
* TrigEdit shows it, each naming the record field it lives in and the kind of value it
|
|
3198
3410
|
* is (a player group, a unit id, a location, a comparison, …). Everything that displays
|
|
3199
3411
|
* or edits a trigger reads this table, the editor's own dialogs included.
|
|
3412
|
+
*
|
|
3413
|
+
* @example
|
|
3414
|
+
* // What the first action of the first trigger is, with the name and kind of each argument.
|
|
3415
|
+
* const action = api.triggers.list()[0]?.actions[0];
|
|
3416
|
+
* const def = action && api.triggers.defs.action(action.type);
|
|
3417
|
+
* if (def) api.log(def.name, def.args.map((a) => `${a.label} (${a.kind}) in .${a.field}`));
|
|
3200
3418
|
*/
|
|
3201
3419
|
export interface TriggerDefsApi {
|
|
3202
3420
|
conditions(): ConditionDef[];
|
|
@@ -3209,7 +3427,14 @@ export interface TriggerDefsApi {
|
|
|
3209
3427
|
/** The value behind a label or one of its aliases, for parsing. */
|
|
3210
3428
|
choiceValue(kind: ArgKind, text: string): number | undefined;
|
|
3211
3429
|
}
|
|
3212
|
-
/**
|
|
3430
|
+
/**
|
|
3431
|
+
* Printing and parsing the text trigger format (File ▸ Import / Export ▸ Triggers).
|
|
3432
|
+
*
|
|
3433
|
+
* @example
|
|
3434
|
+
* // Save the triggers as text, the form File ▸ Export ▸ Triggers writes.
|
|
3435
|
+
* const text = api.triggers.text.print(api.triggers.list());
|
|
3436
|
+
* await api.ui.saveFile(new Blob([text], { type: "text/plain" }), "triggers.txt");
|
|
3437
|
+
*/
|
|
3213
3438
|
export interface TriggerTextApi {
|
|
3214
3439
|
print(triggers: readonly ReadonlyTrigger[], options?: {
|
|
3215
3440
|
briefing?: boolean;
|
|
@@ -3252,7 +3477,15 @@ export interface TriggersApi {
|
|
|
3252
3477
|
setPreserved(trigger: ReadonlyTrigger, on: boolean): TriggerRecord;
|
|
3253
3478
|
/** The indices of the triggers any of these player groups own. */
|
|
3254
3479
|
triggersFor(list: readonly ReadonlyTrigger[], groups: readonly number[]): number[];
|
|
3255
|
-
/**
|
|
3480
|
+
/**
|
|
3481
|
+
* The three lines the trigger list shows: players, conditions, actions.
|
|
3482
|
+
*
|
|
3483
|
+
* @example
|
|
3484
|
+
* for (const trigger of api.triggers.list()) {
|
|
3485
|
+
* const line = api.triggers.summarize(trigger);
|
|
3486
|
+
* api.log(`${line.players}: ${line.conditions} → ${line.actions}`);
|
|
3487
|
+
* }
|
|
3488
|
+
*/
|
|
3256
3489
|
summarize(trigger: ReadonlyTrigger, briefing?: boolean): {
|
|
3257
3490
|
players: string;
|
|
3258
3491
|
conditions: string;
|
|
@@ -3282,6 +3515,12 @@ export interface TriggersApi {
|
|
|
3282
3515
|
* (`Current Player` as the trigger's owners); a class unit (*Any unit*, *Men*, …)
|
|
3283
3516
|
* counts for nothing, and an EUD player is an address, not a cell. What a plugin that
|
|
3284
3517
|
* allocates counters or switches of its own must keep clear of.
|
|
3518
|
+
*
|
|
3519
|
+
* @example
|
|
3520
|
+
* // A switch no trigger touches, for a plugin that needs one of its own.
|
|
3521
|
+
* const taken = new Set(api.triggers.usage().switches);
|
|
3522
|
+
* const free = Array.from({ length: 256 }, (_, i) => i).find((i) => !taken.has(i));
|
|
3523
|
+
* api.log(free === undefined ? "every switch is in use" : `${api.names.switch(free)} is free`);
|
|
3285
3524
|
*/
|
|
3286
3525
|
usage(list?: readonly ReadonlyTrigger[]): {
|
|
3287
3526
|
cells: [
|
|
@@ -3329,6 +3568,25 @@ export interface TriggersApi {
|
|
|
3329
3568
|
* place of the form; the Text Trigger Editor fences them in comments; Import Triggers
|
|
3330
3569
|
* says what a replace would remove. The claim lives until `remove()` or the plugin's
|
|
3331
3570
|
* deactivation; call `refresh()` after a rebuild so the editors ask `locate` again.
|
|
3571
|
+
*
|
|
3572
|
+
* @example
|
|
3573
|
+
* // Fingerprints of the triggers this plugin generated, kept as it generates them.
|
|
3574
|
+
* const mine = new Set<string>();
|
|
3575
|
+
* const isMine = (t: Parameters<typeof api.triggers.fingerprint>[0]) => mine.has(api.triggers.fingerprint(t));
|
|
3576
|
+
* const claim = api.triggers.claim({
|
|
3577
|
+
* label: "the wave generator",
|
|
3578
|
+
* badge: "waves",
|
|
3579
|
+
* locate(list) {
|
|
3580
|
+
* const start = list.findIndex(isMine);
|
|
3581
|
+
* if (start < 0) return null;
|
|
3582
|
+
* let count = 1;
|
|
3583
|
+
* while (start + count < list.length && isMine(list[start + count])) count++;
|
|
3584
|
+
* return { start, count };
|
|
3585
|
+
* },
|
|
3586
|
+
* describe: () => "Made by the wave generator. Change the waves there, not here.",
|
|
3587
|
+
* });
|
|
3588
|
+
* // After regenerating: update `mine`, then
|
|
3589
|
+
* claim.refresh();
|
|
3332
3590
|
*/
|
|
3333
3591
|
claim(spec: TriggerClaimSpec): TriggerClaimHandle;
|
|
3334
3592
|
/**
|
|
@@ -3399,6 +3657,14 @@ export interface StartLocation {
|
|
|
3399
3657
|
*
|
|
3400
3658
|
* Everything here is a read: nothing changes the map, and nothing throws without one
|
|
3401
3659
|
* (an empty list, or null).
|
|
3660
|
+
*
|
|
3661
|
+
* @example
|
|
3662
|
+
* // Check Map's findings in the log, and the view taken to the first one that has a place.
|
|
3663
|
+
* const issues = api.query.validate();
|
|
3664
|
+
* for (const issue of issues) api.log(issue.level, issue.where, issue.text);
|
|
3665
|
+
* const target = issues.find((i) => i.target)?.target;
|
|
3666
|
+
* if (target?.kind === "dialog") api.ui.open(target.id);
|
|
3667
|
+
* else if (target) api.view.goTo(target);
|
|
3402
3668
|
*/
|
|
3403
3669
|
export interface QueryApi {
|
|
3404
3670
|
/** The topmost unit whose sprite box covers a map pixel, or -1. */
|
|
@@ -3417,7 +3683,13 @@ export interface QueryApi {
|
|
|
3417
3683
|
locationsIn(rect: Rect): number[];
|
|
3418
3684
|
/** The map's start locations, by player. */
|
|
3419
3685
|
startLocations(): StartLocation[];
|
|
3420
|
-
/**
|
|
3686
|
+
/**
|
|
3687
|
+
* Whether a unit type may be placed centred there, and what stops it; null with no map.
|
|
3688
|
+
*
|
|
3689
|
+
* @example
|
|
3690
|
+
* const verdict = api.query.placement(106, 640, 640); // a Terran Command Center
|
|
3691
|
+
* api.ui.status(verdict?.problem ? `Blocked: ${verdict.reason}` : "It fits.");
|
|
3692
|
+
*/
|
|
3421
3693
|
placement(unitId: number, x: number, y: number): PlacementVerdict | null;
|
|
3422
3694
|
/**
|
|
3423
3695
|
* StarEdit's ground check for a doodad with its top-left tile at (tx, ty): `ok`, or the
|
|
@@ -3433,13 +3705,32 @@ export interface QueryApi {
|
|
|
3433
3705
|
strings(): (string | null)[];
|
|
3434
3706
|
/** Check Map: every issue the editor knows how to spot, with a `target` to go to. */
|
|
3435
3707
|
validate(): Issue[];
|
|
3436
|
-
/**
|
|
3708
|
+
/**
|
|
3709
|
+
* Tools ▸ Statistics: tile, unit, resource and player counts. Null without a map.
|
|
3710
|
+
*
|
|
3711
|
+
* @example
|
|
3712
|
+
* const stats = api.query.statistics();
|
|
3713
|
+
* if (stats) api.ui.status(`${stats.units.total} units, ${stats.resources.fields} mineral fields, ${stats.triggers.count} triggers`);
|
|
3714
|
+
*/
|
|
3437
3715
|
statistics(): MapStatistics | null;
|
|
3438
|
-
/**
|
|
3716
|
+
/**
|
|
3717
|
+
* The Ctrl+F search over units, locations, sprites, strings and triggers.
|
|
3718
|
+
*
|
|
3719
|
+
* @example
|
|
3720
|
+
* // Select every unit whose name contains "marine".
|
|
3721
|
+
* const hits = api.query.find({ kind: "units", query: "marine" });
|
|
3722
|
+
* api.selection.setLayer("units");
|
|
3723
|
+
* api.selection.setUnits(hits.map((h) => h.index));
|
|
3724
|
+
*/
|
|
3439
3725
|
find(options: FindOptions): FindResult[];
|
|
3440
3726
|
/** Every record that refers to each string index. */
|
|
3441
3727
|
stringUsage(): Map<number, StringUsage[]>;
|
|
3442
|
-
/**
|
|
3728
|
+
/**
|
|
3729
|
+
* String slots nothing refers to.
|
|
3730
|
+
*
|
|
3731
|
+
* @example
|
|
3732
|
+
* for (const index of api.query.unusedStrings()) api.log(index, api.names.string(index));
|
|
3733
|
+
*/
|
|
3443
3734
|
unusedStrings(): number[];
|
|
3444
3735
|
}
|
|
3445
3736
|
/** Where `view.goTo` should take the user. Issues from `query.validate` carry one. */
|
|
@@ -3483,10 +3774,18 @@ export interface ViewApi {
|
|
|
3483
3774
|
* own work around the map takes that as its cue to stop.
|
|
3484
3775
|
*
|
|
3485
3776
|
* @example
|
|
3486
|
-
*
|
|
3777
|
+
* const shown = await api.view.reveal({ x0: 0, y0: 0, x1: 16, y1: 16 }, { fit: true });
|
|
3778
|
+
* if (!shown) api.log("the user moved the view first");
|
|
3487
3779
|
*/
|
|
3488
3780
|
reveal(rect: Rect, options?: RevealOptions): Promise<boolean>;
|
|
3489
|
-
/**
|
|
3781
|
+
/**
|
|
3782
|
+
* Scroll to an object (and select it, for a unit, sprite or location).
|
|
3783
|
+
*
|
|
3784
|
+
* @example
|
|
3785
|
+
* // Player 1's start location, selected and in the middle of the view.
|
|
3786
|
+
* const start = api.query.startLocations().find((s) => s.owner === 0);
|
|
3787
|
+
* if (start) api.view.goTo({ kind: "unit", index: start.index });
|
|
3788
|
+
*/
|
|
3490
3789
|
goTo(target: GoTo): void;
|
|
3491
3790
|
/** The tile under the pointer, as the status bar shows it. */
|
|
3492
3791
|
cursorTile(): {
|
|
@@ -3495,6 +3794,10 @@ export interface ViewApi {
|
|
|
3495
3794
|
};
|
|
3496
3795
|
/** The View menu's ticks: grid, locations, units, sprites, doodads, fog, … */
|
|
3497
3796
|
flags(): ViewFlags;
|
|
3797
|
+
/**
|
|
3798
|
+
* @example
|
|
3799
|
+
* api.view.setFlags({ grid: !api.view.flags().grid });
|
|
3800
|
+
*/
|
|
3498
3801
|
setFlags(patch: Partial<ViewFlags>): void;
|
|
3499
3802
|
/** Grid spacing in map pixels (32 = one tile). */
|
|
3500
3803
|
gridSize(): number;
|
|
@@ -3505,8 +3808,9 @@ export interface ViewApi {
|
|
|
3505
3808
|
* clean up.
|
|
3506
3809
|
*
|
|
3507
3810
|
* @example
|
|
3508
|
-
*
|
|
3509
|
-
* const
|
|
3811
|
+
* // Mark the top-left corner for Player 1's fog, and show where that was.
|
|
3812
|
+
* const rect = { x0: 0, y0: 0, x1: 8, y1: 8 };
|
|
3813
|
+
* const r = api.document.edit("Fog a corner", (tx) => tx.setFog(rect, 0b1, "fog"));
|
|
3510
3814
|
* if (r.changed) api.view.flash({ rect });
|
|
3511
3815
|
*/
|
|
3512
3816
|
flash(target: FlashTarget): void;
|
|
@@ -3542,6 +3846,13 @@ export interface CommandInfo {
|
|
|
3542
3846
|
* Named things a plugin can do, so a menu item, a hotkey, a context entry and another
|
|
3543
3847
|
* plugin all reach the same one. `menu.add`, `contextMenu.add` and `hotkeys.add` take a
|
|
3544
3848
|
* `command` id in place of a `run`.
|
|
3849
|
+
*
|
|
3850
|
+
* @example
|
|
3851
|
+
* api.commands.register({ id: "count", title: "Count units", run: () => api.document.scenario()?.units.length ?? 0 });
|
|
3852
|
+
* api.menu.add("Tools", { label: "Count units", command: "count" });
|
|
3853
|
+
* api.hotkeys.add("Ctrl+Alt+U", { command: "count" });
|
|
3854
|
+
* // Any plugin can run it by its full id, and gets what it returns.
|
|
3855
|
+
* api.log(api.commands.run(`${api.plugin.id}.count`));
|
|
3545
3856
|
*/
|
|
3546
3857
|
export interface CommandsApi {
|
|
3547
3858
|
register(spec: CommandSpec): Disposable;
|
|
@@ -3573,11 +3884,12 @@ export interface ServiceOptions {
|
|
|
3573
3884
|
* the plugin (`"account"` → `"scmjs-dev.account"`); one with a dot is taken as it is.
|
|
3574
3885
|
*
|
|
3575
3886
|
* @example
|
|
3576
|
-
*
|
|
3577
|
-
*
|
|
3578
|
-
*
|
|
3579
|
-
*
|
|
3580
|
-
*
|
|
3887
|
+
* interface Clock { now(): number }
|
|
3888
|
+
* // The provider. The name becomes "<plugin id>.clock".
|
|
3889
|
+
* api.services.provide<Clock>("clock", { now: () => Date.now() }, { version: 1 });
|
|
3890
|
+
* // A consumer, in any plugin, whichever of the two started first.
|
|
3891
|
+
* api.services.watch<Clock>(`${api.plugin.id}.clock`, (clock, info) => {
|
|
3892
|
+
* api.log(clock ? `clock v${info?.version}: ${clock.now()}` : "no clock yet");
|
|
3581
3893
|
* });
|
|
3582
3894
|
*/
|
|
3583
3895
|
export interface ServicesApi {
|
|
@@ -3607,6 +3919,27 @@ export interface RenderClipOptions {
|
|
|
3607
3919
|
* The pictures the viewport draws, for a plugin's own lists and previews: the same
|
|
3608
3920
|
* cached canvases, so asking for one costs nothing after the first time. Everything is
|
|
3609
3921
|
* null when the graphics it needs were never extracted — a plugin shows a name instead.
|
|
3922
|
+
*
|
|
3923
|
+
* @example
|
|
3924
|
+
* // A marine in Player 2's colours. The picture is the editor's own cached canvas, so draw
|
|
3925
|
+
* // from it instead of moving it into your page.
|
|
3926
|
+
* await api.graphics.load();
|
|
3927
|
+
* api.ui.panel({
|
|
3928
|
+
* title: "Marine",
|
|
3929
|
+
* mount(body) {
|
|
3930
|
+
* const show = () => {
|
|
3931
|
+
* const picture = api.graphics.unitImage(0, { owner: 1 });
|
|
3932
|
+
* if (!picture) { body.textContent = api.names.unit(0); return; } // not loaded, or no graphics
|
|
3933
|
+
* const canvas = api.ui.el("canvas", { width: picture.width, height: picture.height });
|
|
3934
|
+
* canvas.getContext("2d")?.drawImage(picture.image, 0, 0);
|
|
3935
|
+
* body.replaceChildren(canvas);
|
|
3936
|
+
* };
|
|
3937
|
+
* api.graphics.requestUnit(0);
|
|
3938
|
+
* const loaded = api.graphics.onImageLoaded(show);
|
|
3939
|
+
* show();
|
|
3940
|
+
* return () => loaded.dispose();
|
|
3941
|
+
* },
|
|
3942
|
+
* });
|
|
3610
3943
|
*/
|
|
3611
3944
|
export interface GraphicsApi {
|
|
3612
3945
|
/** Whether the tileset graphics and the unit tables are in memory. */
|
|
@@ -3668,6 +4001,15 @@ export interface GraphicsApi {
|
|
|
3668
4001
|
*
|
|
3669
4002
|
* Everything is null until the tables are loaded (`load`), and stays null when the game
|
|
3670
4003
|
* data was never extracted.
|
|
4004
|
+
*
|
|
4005
|
+
* @example
|
|
4006
|
+
* if (await api.data.load()) {
|
|
4007
|
+
* const units = api.data.units()!;
|
|
4008
|
+
* // units.dat keeps hit points in 256ths.
|
|
4009
|
+
* api.log(`Marine: ${units.hitPoints[0] / 256} HP, ${units.mineralCost[0]} minerals, ${api.data.race(0)}`);
|
|
4010
|
+
* } else {
|
|
4011
|
+
* api.log("no game data installed");
|
|
4012
|
+
* }
|
|
3671
4013
|
*/
|
|
3672
4014
|
export interface DataApi {
|
|
3673
4015
|
ready(): boolean;
|
|
@@ -3740,8 +4082,8 @@ export interface GameDataFiles {
|
|
|
3740
4082
|
* // A mod plugin: install its files once, then draw with them.
|
|
3741
4083
|
* const sets = await api.gameData.profiles();
|
|
3742
4084
|
* if (!sets.some((p) => p.id === "my-mod")) {
|
|
3743
|
-
* const files = await api.ui.pickFiles({ multiple: true });
|
|
3744
|
-
* if (files) await api.gameData.install({ id: "my-mod", name: "My Mod" }, { archives: files.map((f) => ({ name: f.name, data: f })) });
|
|
4085
|
+
* const files = await api.ui.pickFiles({ multiple: true }); // an empty list when the user cancels
|
|
4086
|
+
* if (files.length > 0) await api.gameData.install({ id: "my-mod", name: "My Mod" }, { archives: files.map((f) => ({ name: f.name, data: f })) });
|
|
3745
4087
|
* }
|
|
3746
4088
|
* await api.gameData.select("my-mod");
|
|
3747
4089
|
*/
|
|
@@ -3786,6 +4128,16 @@ export interface GameDataApi {
|
|
|
3786
4128
|
* type-checks and is then undefined. Anything a plugin needs while it runs has to come
|
|
3787
4129
|
* off `api`. Without this a plugin writes the hex itself, which is how a sprite's kind
|
|
3788
4130
|
* came to be read as `flags & 0x1000` in three places.
|
|
4131
|
+
*
|
|
4132
|
+
* @example
|
|
4133
|
+
* // Make the selected units invincible: the state bit, and the two masks that say it is set.
|
|
4134
|
+
* const { state, valid, used } = api.consts.unit;
|
|
4135
|
+
* const selected = api.selection.units();
|
|
4136
|
+
* api.document.edit("Invincible", (tx) => tx.updateUnits(selected, (u) => ({
|
|
4137
|
+
* stateFlags: u.stateFlags | state.Invincible,
|
|
4138
|
+
* validProperties: u.validProperties | valid.Invincible,
|
|
4139
|
+
* validStates: u.validStates | used.State,
|
|
4140
|
+
* })));
|
|
3789
4141
|
*/
|
|
3790
4142
|
export interface ConstsApi {
|
|
3791
4143
|
/** Map pixels to a tile. UNIT and THG2 store pixels; MTXM, MRGN and the brushes count tiles. */
|
|
@@ -3890,6 +4242,13 @@ export interface LocationConsts {
|
|
|
3890
4242
|
* Generating a whole run of triggers is usually better done through
|
|
3891
4243
|
* `tx.triggers.fromText`, which resolves names against the open map; these are for
|
|
3892
4244
|
* editing a field of an existing record, and for reading one back.
|
|
4245
|
+
*
|
|
4246
|
+
* @example
|
|
4247
|
+
* // "At most 30 seconds on the countdown timer", written field by field.
|
|
4248
|
+
* const { condition, comparison } = api.consts.triggers;
|
|
4249
|
+
* const timer = api.triggers.newCondition(condition.CountdownTimer);
|
|
4250
|
+
* timer.comparison = comparison.AtMost;
|
|
4251
|
+
* timer.amount = 30;
|
|
3893
4252
|
*/
|
|
3894
4253
|
export interface TriggerConsts {
|
|
3895
4254
|
/** Condition `type`. */
|
|
@@ -3943,7 +4302,13 @@ export interface ActiveBrush {
|
|
|
3943
4302
|
variation: number;
|
|
3944
4303
|
}
|
|
3945
4304
|
export interface TerrainApi {
|
|
3946
|
-
/**
|
|
4305
|
+
/**
|
|
4306
|
+
* Paintable flat terrains of the open map's tileset (empty without the graphics).
|
|
4307
|
+
*
|
|
4308
|
+
* @example
|
|
4309
|
+
* await api.tileset.load();
|
|
4310
|
+
* for (const t of api.terrain.types()) api.log(t.id, t.name, `height ${t.height}`, t.buildable ? "buildable" : "");
|
|
4311
|
+
*/
|
|
3947
4312
|
types(): TerrainType[];
|
|
3948
4313
|
/** Terrain ids the isometric brush can paint on this tileset. */
|
|
3949
4314
|
isomTypes(): number[];
|
|
@@ -3962,7 +4327,7 @@ export interface TerrainApi {
|
|
|
3962
4327
|
*
|
|
3963
4328
|
* @example
|
|
3964
4329
|
* const r = await api.terrain.checkIsom();
|
|
3965
|
-
* if (r?.stale)
|
|
4330
|
+
* if (r?.stale) api.document.edit("Rebuild ISOM", (tx) => { tx.rebuildIsom(); });
|
|
3966
4331
|
*/
|
|
3967
4332
|
checkIsom(): Promise<IsomReport | null>;
|
|
3968
4333
|
tileInfo(id: number): TileInfo | null;
|
|
@@ -3971,6 +4336,12 @@ export interface TerrainApi {
|
|
|
3971
4336
|
* the tile's own group when it is flat ground, else — under a cliff, a shore or a
|
|
3972
4337
|
* doodad — what the ISOM lattice says the diamond there is. Null off the map, without
|
|
3973
4338
|
* the tileset graphics, or when neither tells.
|
|
4339
|
+
*
|
|
4340
|
+
* @example
|
|
4341
|
+
* // Name the terrain under the pointer.
|
|
4342
|
+
* const { x, y } = api.view.cursorTile();
|
|
4343
|
+
* const id = api.terrain.terrainAt(x, y);
|
|
4344
|
+
* api.ui.status(api.terrain.types().find((t) => t.id === id)?.name ?? "unknown terrain");
|
|
3974
4345
|
*/
|
|
3975
4346
|
terrainAt(tx: number, ty: number): number | null;
|
|
3976
4347
|
/** The atlas average of a tile, packed `0xRRGGBB`, or null without graphics. */
|
|
@@ -3996,6 +4367,10 @@ export interface TerrainApi {
|
|
|
3996
4367
|
flatGroupOf(terrainId: number): number;
|
|
3997
4368
|
/** Tools ▸ Symmetry…: the mode the brushes paint and the palettes place under. */
|
|
3998
4369
|
symmetry(): SymmetryMode;
|
|
4370
|
+
/**
|
|
4371
|
+
* @example
|
|
4372
|
+
* if (api.terrain.symmetryAvailable("rot180")) api.terrain.setSymmetry("rot180");
|
|
4373
|
+
*/
|
|
3999
4374
|
setSymmetry(mode: SymmetryMode): void;
|
|
4000
4375
|
/** Whether a mode can run on the open map (the rotations and diagonals need a square one). */
|
|
4001
4376
|
symmetryAvailable(mode: SymmetryMode): boolean;
|
|
@@ -4019,6 +4394,11 @@ export interface TilesetApi {
|
|
|
4019
4394
|
* tileset changes nothing about the map: it is for `graphics.renderClip` drawing a clip
|
|
4020
4395
|
* that came from a map on that tileset (a stamp, a recording), which answers null until
|
|
4021
4396
|
* its tileset's graphics are loaded.
|
|
4397
|
+
*
|
|
4398
|
+
* @example
|
|
4399
|
+
* if (!(await api.tileset.load())) {
|
|
4400
|
+
* api.ui.toast({ kind: "warn", title: "No tileset graphics", detail: "Install them from Help ▸ Game Data…" });
|
|
4401
|
+
* }
|
|
4022
4402
|
*/
|
|
4023
4403
|
load(tileset?: TilesetId): Promise<boolean>;
|
|
4024
4404
|
raw(): LoadedTileset | null;
|
|
@@ -4060,6 +4440,14 @@ export interface PasteOptionsSpec {
|
|
|
4060
4440
|
* The Cut / Copy / Paste layer. The clip is self-contained (it outlives the map it came
|
|
4061
4441
|
* from and pastes into another) and shared with the user's own clipboard, so a plugin
|
|
4062
4442
|
* that copies here is copying for the user too.
|
|
4443
|
+
*
|
|
4444
|
+
* @example
|
|
4445
|
+
* // Copy the top-left 8 × 8 tiles and paste them 16 tiles to the right.
|
|
4446
|
+
* const clip = api.clipboard.copy({ rect: { x0: 0, y0: 0, x1: 8, y1: 8 } });
|
|
4447
|
+
* if (clip) {
|
|
4448
|
+
* api.ui.status(api.clipboard.summary(clip));
|
|
4449
|
+
* api.clipboard.paste(16, 0);
|
|
4450
|
+
* }
|
|
4063
4451
|
*/
|
|
4064
4452
|
export interface ClipboardApi {
|
|
4065
4453
|
/** What is on the clipboard, or null. */
|
|
@@ -4101,6 +4489,16 @@ export interface ClipboardApi {
|
|
|
4101
4489
|
/**
|
|
4102
4490
|
* The file formats File ▸ Import / Export read and write, so a plugin can keep triggers
|
|
4103
4491
|
* or strings outside the map in a form the editor (and SCMDraft) take back.
|
|
4492
|
+
*
|
|
4493
|
+
* @example
|
|
4494
|
+
* // The string table out to a text file, and back in after editing it elsewhere.
|
|
4495
|
+
* await api.ui.saveFile(new Blob([api.exchange.formatStrings()], { type: "text/plain" }), "strings.txt");
|
|
4496
|
+
* const [file] = await api.ui.pickFiles({ accept: ".txt" });
|
|
4497
|
+
* if (file) {
|
|
4498
|
+
* const text = await file.text();
|
|
4499
|
+
* const { errors } = api.exchange.parseStrings(text);
|
|
4500
|
+
* if (errors.length === 0) api.document.update("Import strings", (tx) => { tx.strings.import(text); });
|
|
4501
|
+
* }
|
|
4104
4502
|
*/
|
|
4105
4503
|
export interface ExchangeApi {
|
|
4106
4504
|
/** Raw 2400-byte TRIG records, SCMDraft's `.trg`; string indices are the map's own. */
|
|
@@ -4166,6 +4564,16 @@ export interface DoodadInfo {
|
|
|
4166
4564
|
*/
|
|
4167
4565
|
required: number[];
|
|
4168
4566
|
}
|
|
4567
|
+
/**
|
|
4568
|
+
* @example
|
|
4569
|
+
* // Place whatever the Units palette has picked, for the player it has picked.
|
|
4570
|
+
* const { unit, owner } = api.palette.active();
|
|
4571
|
+
* const at = await api.ui.pickTile({ prompt: `Click where the ${api.palette.unitName(unit)} goes` });
|
|
4572
|
+
* if (at) {
|
|
4573
|
+
* const t = api.consts.tile;
|
|
4574
|
+
* api.document.edit("Place", (tx) => { tx.placeUnit(unit, owner, at.x * t + t / 2, at.y * t + t / 2); });
|
|
4575
|
+
* }
|
|
4576
|
+
*/
|
|
4169
4577
|
export interface PaletteApi {
|
|
4170
4578
|
active(): PaletteChoice;
|
|
4171
4579
|
setActive(choice: Partial<PaletteChoice>): void;
|
|
@@ -4204,6 +4612,11 @@ export interface NamedValue {
|
|
|
4204
4612
|
* carry the game's tables itself. The per-map ones (`string`, `location`, `switch`,
|
|
4205
4613
|
* `player`) read the open scenario and answer a placeholder without one; the rest are
|
|
4206
4614
|
* the editor's own tables — the same names StarEdit shows.
|
|
4615
|
+
*
|
|
4616
|
+
* @example
|
|
4617
|
+
* api.log(api.names.unit(0), api.names.playerGroup(api.consts.triggers.player.Player1), api.names.location(api.consts.location.anywhere));
|
|
4618
|
+
* // The list forms are ready for a drop-down.
|
|
4619
|
+
* const upgrade = api.ui.widgets.select(api.names.upgrades());
|
|
4207
4620
|
*/
|
|
4208
4621
|
export interface NamesApi {
|
|
4209
4622
|
/** StarEdit's name for a units.dat id; `Any unit` / `Men` / `Buildings` / `Factories` for the trigger classes 229–232 (228 is the game's `None`). */
|
|
@@ -4264,6 +4677,21 @@ export interface NamesApi {
|
|
|
4264
4677
|
* alignment code on a line, stacking the pieces between them in one place: `stackedLines`
|
|
4265
4678
|
* finds those lines and `flattenStacks` lays them out left to right. `runs` is one of the
|
|
4266
4679
|
* renderers that cannot show them — `TextLine.align` is one alignment for the whole line.
|
|
4680
|
+
*
|
|
4681
|
+
* @example
|
|
4682
|
+
* const text = "\x07Mission\nKill them all"; // byte 0x07 colours the first line
|
|
4683
|
+
* api.log(api.text.plain(text)); // "Mission\nKill them all"
|
|
4684
|
+
* api.log(api.text.bleedingLines(text).map((l) => l.line)); // the second line inherits the colour
|
|
4685
|
+
* api.log(api.text.fixBleeding(text) === text); // false: a reset was written
|
|
4686
|
+
*
|
|
4687
|
+
* @example
|
|
4688
|
+
* // Repair every string in the map that has the problem.
|
|
4689
|
+
* const strings = api.query.strings();
|
|
4690
|
+
* api.document.update("Fix carried colours", (tx) => {
|
|
4691
|
+
* strings.forEach((s, i) => {
|
|
4692
|
+
* if (s && api.text.bleedingLines(s).length > 0) tx.strings.set(i, api.text.fixBleeding(s));
|
|
4693
|
+
* });
|
|
4694
|
+
* });
|
|
4267
4695
|
*/
|
|
4268
4696
|
export interface TextApi {
|
|
4269
4697
|
/** Every byte the game gives a meaning, in order; `rgb` is set for the colours only. */
|
|
@@ -4318,6 +4746,19 @@ export interface DialogTransfer {
|
|
|
4318
4746
|
files: File[];
|
|
4319
4747
|
text: string;
|
|
4320
4748
|
}
|
|
4749
|
+
/**
|
|
4750
|
+
* @example
|
|
4751
|
+
* const name = api.ui.widgets.text({ value: api.document.info()?.name ?? "" });
|
|
4752
|
+
* api.ui.dialog({
|
|
4753
|
+
* title: "Rename the map",
|
|
4754
|
+
* size: "sm",
|
|
4755
|
+
* mount: (body) => body.append(api.ui.widgets.form([{ label: "Name", field: name }])),
|
|
4756
|
+
* buttons: [
|
|
4757
|
+
* { label: "Cancel" },
|
|
4758
|
+
* { label: "Rename", primary: true, run: () => { api.document.update("Rename", (tx) => tx.properties({ name: name.value })); } },
|
|
4759
|
+
* ],
|
|
4760
|
+
* });
|
|
4761
|
+
*/
|
|
4321
4762
|
export interface DialogSpec {
|
|
4322
4763
|
title: string;
|
|
4323
4764
|
size?: DialogSize;
|
|
@@ -4378,6 +4819,19 @@ export interface DialogHandle {
|
|
|
4378
4819
|
* A panel floats over the map and does not block it: the user keeps drawing, scrolling
|
|
4379
4820
|
* and using hotkeys while it is open (except while typing in one of its fields). It
|
|
4380
4821
|
* is dragged by its title bar and closed with the × or `close()`.
|
|
4822
|
+
*
|
|
4823
|
+
* @example
|
|
4824
|
+
* // A unit count that follows the map, docked under the built-in panels.
|
|
4825
|
+
* api.ui.panel({
|
|
4826
|
+
* title: "Unit count",
|
|
4827
|
+
* dock: "right",
|
|
4828
|
+
* mount(body) {
|
|
4829
|
+
* const show = () => { body.textContent = `${api.document.scenario()?.units.length ?? 0} units`; };
|
|
4830
|
+
* show();
|
|
4831
|
+
* const sub = api.events.on("units", show);
|
|
4832
|
+
* return () => sub.dispose();
|
|
4833
|
+
* },
|
|
4834
|
+
* });
|
|
4381
4835
|
*/
|
|
4382
4836
|
export interface PanelSpec {
|
|
4383
4837
|
title: string;
|
|
@@ -4419,6 +4873,14 @@ export interface PanelHandle {
|
|
|
4419
4873
|
* One cell in the status bar (`ui.statusItem`): a line of text with the plugin's icon,
|
|
4420
4874
|
* a spinner while `busy`, and a click. It is how a plugin that works in the background
|
|
4421
4875
|
* stays visible without a panel — "AI · working 12 s", "3 problems", "Synced".
|
|
4876
|
+
*
|
|
4877
|
+
* @example
|
|
4878
|
+
* const count = () => api.query.validate().filter((i) => i.level !== "info").length;
|
|
4879
|
+
* const cell = api.ui.statusItem({ text: `${count()} problems`, onClick: () => api.ui.open("validateMap") });
|
|
4880
|
+
* api.events.on("commit", () => {
|
|
4881
|
+
* const n = count();
|
|
4882
|
+
* cell.set({ text: `${n} problems`, warn: n > 0 });
|
|
4883
|
+
* });
|
|
4422
4884
|
*/
|
|
4423
4885
|
export interface StatusItemSpec {
|
|
4424
4886
|
text: string;
|
|
@@ -4436,7 +4898,17 @@ export interface StatusItemHandle {
|
|
|
4436
4898
|
remove(): void;
|
|
4437
4899
|
isShown(): boolean;
|
|
4438
4900
|
}
|
|
4439
|
-
/**
|
|
4901
|
+
/**
|
|
4902
|
+
* A button on the map itself (`ui.mapButton`), in the row at its bottom-right corner.
|
|
4903
|
+
*
|
|
4904
|
+
* @example
|
|
4905
|
+
* let notes = 3;
|
|
4906
|
+
* const button = api.ui.mapButton({
|
|
4907
|
+
* label: "Notes",
|
|
4908
|
+
* badge: notes,
|
|
4909
|
+
* onClick: () => { notes = 0; button.set({ badge: null }); },
|
|
4910
|
+
* });
|
|
4911
|
+
*/
|
|
4440
4912
|
export interface MapButtonSpec {
|
|
4441
4913
|
/** A word or two; the button is as wide as it. */
|
|
4442
4914
|
label: string;
|
|
@@ -4505,13 +4977,16 @@ export interface DialogSlotSpec {
|
|
|
4505
4977
|
* which is the simpler shape when nothing needs undoing on Cancel.
|
|
4506
4978
|
*
|
|
4507
4979
|
* @example
|
|
4980
|
+
* const settings = api.storage.get("settings", { dock: "float" });
|
|
4981
|
+
* let pending: (() => void) | null = null;
|
|
4508
4982
|
* api.ui.preferencesPage({
|
|
4509
4983
|
* mount(body) {
|
|
4510
4984
|
* const w = api.ui.widgets;
|
|
4511
4985
|
* const dock = w.select([{ value: "float", label: "Floating" }, { value: "right", label: "Docked" }], { value: settings.dock });
|
|
4512
4986
|
* body.append(w.form([{ label: "Panel", field: dock }]), w.hint("Where the panel opens."));
|
|
4513
|
-
* pending = () =>
|
|
4987
|
+
* pending = () => api.storage.set("settings", { dock: dock.value });
|
|
4514
4988
|
* },
|
|
4989
|
+
* // OK and Apply write it; Cancel leaves the stored value alone.
|
|
4515
4990
|
* apply: () => pending?.(),
|
|
4516
4991
|
* });
|
|
4517
4992
|
*/
|
|
@@ -4599,6 +5074,23 @@ export type MapToolStopReason =
|
|
|
4599
5074
|
* release ahead of the active layer's own tools, hides the layer's brush ghost, and
|
|
4600
5075
|
* lets it draw an overlay. The map stays visible and scrollable, and a panel can stay
|
|
4601
5076
|
* open beside it — which is how a plugin gets a drawing mode of its own.
|
|
5077
|
+
*
|
|
5078
|
+
* @example
|
|
5079
|
+
* // Click to drop a marine, with a ring under the pointer. Esc or a right-click ends the tool.
|
|
5080
|
+
* let at: { px: number; py: number } | null = null;
|
|
5081
|
+
* const tool = api.ui.mapTool({
|
|
5082
|
+
* name: "Drop marines",
|
|
5083
|
+
* hint: "click to place, Esc to stop",
|
|
5084
|
+
* onMove(p) { at = p.inMap ? p : null; tool.redraw(); },
|
|
5085
|
+
* onDown(p) { api.document.edit("Drop a marine", (tx) => { tx.placeUnit(0, 0, p.px, p.py); }); },
|
|
5086
|
+
* draw(ctx, view) {
|
|
5087
|
+
* if (!at) return;
|
|
5088
|
+
* ctx.strokeStyle = "#ffd24a";
|
|
5089
|
+
* ctx.beginPath();
|
|
5090
|
+
* ctx.arc(view.x(at.px), view.y(at.py), view.tilePx / 2, 0, Math.PI * 2);
|
|
5091
|
+
* ctx.stroke();
|
|
5092
|
+
* },
|
|
5093
|
+
* });
|
|
4602
5094
|
*/
|
|
4603
5095
|
export interface MapToolSpec {
|
|
4604
5096
|
/** Shown in the viewport's HUD and the status bar while the tool runs. */
|
|
@@ -4639,6 +5131,23 @@ export type OverlayAbove =
|
|
|
4639
5131
|
* as usual. It hears the pointer through `onHover`, which is how a readout follows the
|
|
4640
5132
|
* mouse while the user places units. Register one at activation and keep the handle;
|
|
4641
5133
|
* it goes away with the plugin.
|
|
5134
|
+
*
|
|
5135
|
+
* @example
|
|
5136
|
+
* // A ring twelve tiles wide round every start location, listed under View ▸ Overlays.
|
|
5137
|
+
* const overlay = api.ui.overlay({
|
|
5138
|
+
* name: "Start rings",
|
|
5139
|
+
* above: "objects",
|
|
5140
|
+
* draw(ctx, view) {
|
|
5141
|
+
* ctx.strokeStyle = "#ffd24a";
|
|
5142
|
+
* ctx.lineWidth = 2;
|
|
5143
|
+
* for (const start of api.query.startLocations()) {
|
|
5144
|
+
* ctx.beginPath();
|
|
5145
|
+
* ctx.arc(view.x(start.x), view.y(start.y), 12 * view.tilePx, 0, Math.PI * 2);
|
|
5146
|
+
* ctx.stroke();
|
|
5147
|
+
* }
|
|
5148
|
+
* },
|
|
5149
|
+
* });
|
|
5150
|
+
* api.events.on("units", () => overlay.redraw());
|
|
4642
5151
|
*/
|
|
4643
5152
|
export interface OverlaySpec {
|
|
4644
5153
|
/** Shown in View ▸ Overlays and the Layers panel. Unique per plugin. In English, like a menu label: shown through the plugin's registered catalogues. */
|
|
@@ -4698,7 +5207,12 @@ export interface UiApi {
|
|
|
4698
5207
|
status(text: string): void;
|
|
4699
5208
|
/** The status bar line as it stands. */
|
|
4700
5209
|
statusText(): string;
|
|
4701
|
-
/**
|
|
5210
|
+
/**
|
|
5211
|
+
* A short notice over the map that leaves by itself — how Save reports; `ttl` 0 keeps it until dismissed.
|
|
5212
|
+
*
|
|
5213
|
+
* @example
|
|
5214
|
+
* api.ui.toast({ kind: "ok", title: "Export finished", detail: "12 triggers written." });
|
|
5215
|
+
*/
|
|
4702
5216
|
toast(toast: {
|
|
4703
5217
|
kind?: Toast["kind"];
|
|
4704
5218
|
title: string;
|
|
@@ -4741,10 +5255,11 @@ export interface UiApi {
|
|
|
4741
5255
|
* `dispose()` or the plugin.
|
|
4742
5256
|
*
|
|
4743
5257
|
* @example
|
|
5258
|
+
* // A button in Map Properties that tidies the name in the form. The user still presses OK.
|
|
4744
5259
|
* api.ui.dialogSlot("mapProperties", {
|
|
4745
5260
|
* mount(body, dlg) {
|
|
4746
|
-
* body.append(api.ui.widgets.button("
|
|
4747
|
-
* dlg.fields.name.set(
|
|
5261
|
+
* body.append(api.ui.widgets.button("Tidy name", { onClick: () => {
|
|
5262
|
+
* dlg.fields.name.set(dlg.fields.name.get().trim().replace(/\s+/g, " "));
|
|
4748
5263
|
* } }));
|
|
4749
5264
|
* },
|
|
4750
5265
|
* });
|
|
@@ -4788,6 +5303,11 @@ export interface UiApi {
|
|
|
4788
5303
|
* As `pickTile`, for an object: the user clicks a unit or a location. The viewport
|
|
4789
5304
|
* outlines and names what is under the pointer as it moves; a click on nothing keeps
|
|
4790
5305
|
* picking. Resolves with the kind and the record's index, or null as `pickArea` does.
|
|
5306
|
+
*
|
|
5307
|
+
* @example
|
|
5308
|
+
* const picked = await api.ui.pickObject({ prompt: "Click a unit", kinds: ["unit"] });
|
|
5309
|
+
* const unit = picked && api.document.scenario()?.units[picked.index];
|
|
5310
|
+
* if (unit) await api.ui.alert(`${api.names.unit(unit.unitId)}, owned by ${api.names.player(unit.owner)}`);
|
|
4791
5311
|
*/
|
|
4792
5312
|
pickObject(options?: PickObjectOptions): Promise<PickedObject | null>;
|
|
4793
5313
|
/**
|
|
@@ -4802,9 +5322,29 @@ export interface UiApi {
|
|
|
4802
5322
|
confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
4803
5323
|
/** Say something with a single OK. */
|
|
4804
5324
|
alert(message: string, options?: ConfirmOptions): Promise<void>;
|
|
4805
|
-
/**
|
|
5325
|
+
/**
|
|
5326
|
+
* Ask for a line of text; null when the user cancelled.
|
|
5327
|
+
*
|
|
5328
|
+
* @example
|
|
5329
|
+
* const name = await api.ui.prompt("Name the map", { value: api.document.info()?.name, confirmLabel: "Rename" });
|
|
5330
|
+
* if (name) api.document.update("Rename", (tx) => tx.properties({ name }));
|
|
5331
|
+
*/
|
|
4806
5332
|
prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
4807
|
-
/**
|
|
5333
|
+
/**
|
|
5334
|
+
* A progress bar over the map for long work, with an optional Cancel.
|
|
5335
|
+
*
|
|
5336
|
+
* @example
|
|
5337
|
+
* const triggers = api.triggers.list();
|
|
5338
|
+
* const job = api.ui.progress("Reading triggers", { cancellable: true });
|
|
5339
|
+
* try {
|
|
5340
|
+
* for (let i = 0; i < triggers.length && !job.cancelled(); i++) {
|
|
5341
|
+
* job.report(i / triggers.length, `Trigger ${i + 1} of ${triggers.length}`);
|
|
5342
|
+
* await new Promise((next) => setTimeout(next, 20)); // the slow part of your work goes here
|
|
5343
|
+
* }
|
|
5344
|
+
* } finally {
|
|
5345
|
+
* job.done();
|
|
5346
|
+
* }
|
|
5347
|
+
*/
|
|
4808
5348
|
progress(label: string, options?: ProgressOptions): ProgressHandle;
|
|
4809
5349
|
/** `el("div", { className: "row" }, …)`: the DOM helper the widgets are built from. */
|
|
4810
5350
|
el<K extends keyof HTMLElementTagNameMap>(tag: K, props?: Record<string, unknown>, ...children: WidgetChild[]): HTMLElementTagNameMap[K];
|
|
@@ -5103,6 +5643,18 @@ export interface WidgetsApi {
|
|
|
5103
5643
|
/** Small dimmed explanatory text. */
|
|
5104
5644
|
hint(text: string): HTMLElement;
|
|
5105
5645
|
separator(): HTMLElement;
|
|
5646
|
+
/**
|
|
5647
|
+
* @example
|
|
5648
|
+
* // Player 1's units in a dialog; picking one closes it and goes there.
|
|
5649
|
+
* const units = api.document.scenario()?.units ?? [];
|
|
5650
|
+
* const items = api.query.unitsOf(0).map((index) => ({ label: api.names.unit(units[index].unitId), value: index, index }));
|
|
5651
|
+
* api.ui.dialog({
|
|
5652
|
+
* title: "Player 1's units",
|
|
5653
|
+
* mount(body, dialog) {
|
|
5654
|
+
* body.append(api.ui.widgets.list(items, { height: 320, onPick: (index) => { dialog.close(); api.view.goTo({ kind: "unit", index }); } }));
|
|
5655
|
+
* },
|
|
5656
|
+
* });
|
|
5657
|
+
*/
|
|
5106
5658
|
list<T>(items: ListItem<T>[], options?: ListOptions<T>): HTMLElement;
|
|
5107
5659
|
/** A turning ring: on its own to put beside your own text, or with a `label` beside it. */
|
|
5108
5660
|
spinner(options?: SpinnerOptions): HTMLElement;
|
|
@@ -5172,18 +5724,21 @@ export interface MenuItemSpec {
|
|
|
5172
5724
|
}
|
|
5173
5725
|
export interface MenuApi {
|
|
5174
5726
|
/**
|
|
5175
|
-
*
|
|
5727
|
+
* Add an item to the editor's menu bar.
|
|
5176
5728
|
*
|
|
5177
5729
|
* @example
|
|
5178
5730
|
* api.menu.add("Tools", {
|
|
5179
|
-
* label: "Count units
|
|
5731
|
+
* label: "Count units…",
|
|
5180
5732
|
* icon: "plugin",
|
|
5733
|
+
* enabled: () => api.document.isOpen(),
|
|
5181
5734
|
* run: () => api.ui.alert(`Player 1 has ${api.query.unitsOf(0).length} units.`),
|
|
5182
5735
|
* });
|
|
5183
5736
|
*
|
|
5184
5737
|
* @example
|
|
5185
5738
|
* // A path whose last segment names no submenu makes one, at the end of that menu.
|
|
5186
|
-
* api.menu.add("Tools/My plugin", { label: "
|
|
5739
|
+
* api.menu.add("Tools/My plugin", { label: "About…", run: () => api.ui.alert(`${api.plugin.name} ${api.plugin.version ?? ""}`) });
|
|
5740
|
+
* // `after` puts an item directly under a built-in one, named by its English label.
|
|
5741
|
+
* api.menu.add("File", { label: "Open from My Site…", after: "Open Recent", run: () => api.ui.status("…") });
|
|
5187
5742
|
*/
|
|
5188
5743
|
add(path: MenuPath, item: MenuItemSpec): Disposable;
|
|
5189
5744
|
}
|
|
@@ -5219,12 +5774,23 @@ export interface ContextItemSpec {
|
|
|
5219
5774
|
command?: string;
|
|
5220
5775
|
}
|
|
5221
5776
|
export interface ContextMenuApi {
|
|
5777
|
+
/**
|
|
5778
|
+
* @example
|
|
5779
|
+
* // Right-click the map: copy the tile's position.
|
|
5780
|
+
* api.contextMenu.add("viewport", {
|
|
5781
|
+
* label: (ctx) => (ctx.tile ? `Copy position (${ctx.tile.x}, ${ctx.tile.y})` : "Copy position"),
|
|
5782
|
+
* enabled: (ctx) => ctx.tile !== null,
|
|
5783
|
+
* run: (ctx) => { if (ctx.tile) void navigator.clipboard.writeText(`${ctx.tile.x}, ${ctx.tile.y}`); },
|
|
5784
|
+
* });
|
|
5785
|
+
*/
|
|
5222
5786
|
add(surface: ContextSurface, item: ContextItemSpec): Disposable;
|
|
5223
5787
|
}
|
|
5224
5788
|
export interface HotkeyApi {
|
|
5225
|
-
/**
|
|
5789
|
+
/**
|
|
5790
|
+
* `"Ctrl+Shift+I"`, `"Alt+F9"`, `"F8"` — modifiers in any order, then a key name.
|
|
5791
|
+
*
|
|
5226
5792
|
* @example
|
|
5227
|
-
* api.hotkeys.add("Ctrl+
|
|
5793
|
+
* api.hotkeys.add("Ctrl+Alt+G", () => api.view.setFlags({ grid: !api.view.flags().grid }));
|
|
5228
5794
|
*/
|
|
5229
5795
|
add(combo: string, run: (() => void) | {
|
|
5230
5796
|
command: string;
|
|
@@ -5272,9 +5838,11 @@ export type PluginEvent =
|
|
|
5272
5838
|
* closing and switching maps are not commits — the `"document"` event says those.
|
|
5273
5839
|
*
|
|
5274
5840
|
* @example
|
|
5841
|
+
* // Flash where each change landed, and log what kind of change it was.
|
|
5275
5842
|
* api.events.on("commit", (e) => {
|
|
5276
|
-
* if (e.
|
|
5277
|
-
*
|
|
5843
|
+
* if (e.area) api.view.flash({ rect: e.area, kind: "attention" });
|
|
5844
|
+
* const parts = Object.entries(e.parts).filter(([, changed]) => changed).map(([part]) => part);
|
|
5845
|
+
* api.log(e.reason, e.label, parts);
|
|
5278
5846
|
* });
|
|
5279
5847
|
*/
|
|
5280
5848
|
export interface CommitEvent {
|
|
@@ -5396,9 +5964,15 @@ export interface DocumentEvent {
|
|
|
5396
5964
|
* `{x, select, …}` and, for Korean, `{name|을}` (the particle agrees with the value).
|
|
5397
5965
|
*
|
|
5398
5966
|
* @example
|
|
5399
|
-
* api.i18n.register({ ko: { "Count units
|
|
5400
|
-
*
|
|
5401
|
-
* api.
|
|
5967
|
+
* api.i18n.register({ ko: { "Count units…": "유닛 세기…", "{n} units": "유닛 {n}개" } });
|
|
5968
|
+
* // A menu label stays English: the menu shows it through the catalogue above.
|
|
5969
|
+
* api.menu.add("Tools", {
|
|
5970
|
+
* label: "Count units…",
|
|
5971
|
+
* run: () => api.ui.alert(api.i18n.t("{n} units", { n: api.document.scenario()?.units.length ?? 0 })),
|
|
5972
|
+
* });
|
|
5973
|
+
* // Anything you drew yourself is yours to redraw when the language changes.
|
|
5974
|
+
* const cell = api.ui.statusItem({ text: api.i18n.t("{n} units", { n: 0 }) });
|
|
5975
|
+
* api.events.on("language", () => cell.set({ text: api.i18n.t("{n} units", { n: 0 }) }));
|
|
5402
5976
|
*/
|
|
5403
5977
|
export interface I18nApi {
|
|
5404
5978
|
/** The editor's language, a BCP 47 primary tag: `"en"`, `"ko"`. */
|
|
@@ -5419,12 +5993,12 @@ export interface I18nApi {
|
|
|
5419
5993
|
export interface EventsApi {
|
|
5420
5994
|
/**
|
|
5421
5995
|
* @example
|
|
5422
|
-
* // Notifications, in activation order; a listener never intercepts what it hears.
|
|
5423
5996
|
* api.events.on("document", (e) => {
|
|
5424
|
-
* if (e.reason === "open")
|
|
5997
|
+
* if (e.reason === "open") api.ui.status(`Opened ${e.fileName ?? "a map"}`);
|
|
5425
5998
|
* });
|
|
5426
|
-
* api.events.on("
|
|
5427
|
-
*
|
|
5999
|
+
* const selection = api.events.on("selection", () => api.log(`${api.selection.units().length} units selected`));
|
|
6000
|
+
* // Stop listening early; otherwise the listener goes when the plugin is turned off.
|
|
6001
|
+
* setTimeout(() => selection.dispose(), 60_000);
|
|
5428
6002
|
*/
|
|
5429
6003
|
on(event: "document", listener: (event: DocumentEvent) => void): Disposable;
|
|
5430
6004
|
on(event: "commit", listener: (event: CommitEvent) => void): Disposable;
|
|
@@ -5432,12 +6006,12 @@ export interface EventsApi {
|
|
|
5432
6006
|
}
|
|
5433
6007
|
export interface StorageApi {
|
|
5434
6008
|
/**
|
|
5435
|
-
*
|
|
5436
|
-
* storage and listed in Preferences \u25b8 Browser storage.
|
|
6009
|
+
* Read a value, or `fallback` when the key was never set.
|
|
5437
6010
|
*
|
|
5438
6011
|
* @example
|
|
5439
|
-
* const
|
|
5440
|
-
* api.storage.set("options", { ...
|
|
6012
|
+
* const options = api.storage.get("options", { showGrid: true });
|
|
6013
|
+
* const kept = api.storage.set("options", { ...options, showGrid: false });
|
|
6014
|
+
* if (!kept) api.ui.toast({ kind: "error", title: "Could not save the options", detail: "The browser's storage is full." });
|
|
5441
6015
|
*/
|
|
5442
6016
|
get<T>(key: string, fallback: T): T;
|
|
5443
6017
|
/**
|