4 Commits

Author SHA1 Message Date
uberwald a53c7ace53 Ready for release
Release Creation / build (release) Successful in 43s
2026-06-12 20:53:44 +02:00
uberwald efe37b8a96 MAp management and helpers 2026-06-02 00:16:08 +02:00
uberwald 49423f40f5 AJout gestion map 2026-06-01 22:51:48 +02:00
uberwald 9abc2a8b19 Use real skill names from system
Release Creation / build (release) Successful in 44s
2026-05-28 08:07:22 +02:00
103 changed files with 3190 additions and 246 deletions
+73
View File
@@ -0,0 +1,73 @@
# AGENTS.md — mgt2-compendium-amiral-denisov
## First read
- `.github/copilot-instructions.md` — packs, icons, module.json conventions (this file builds on it, don't repeat it)
## No package manager / no build
There is **no** `package.json`, no lockfile, no bundler. JavaScript is plain ESM loaded directly by FoundryVTT. Do not run `npm install` or any build command.
## Commands
5 chat commands registered at runtime (not declared in `module.json`):
| Command | Entrypoint | Dialog |
|---|---|---|
| `/commerce` | `scripts/commerce.js` | `CommerceDialog` (3 tabs) |
| `/pnj` | `scripts/npc.js` | `NpcDialog` (4 tabs) |
| `/rencontre` | `scripts/npc.js` | same dialog (tab 2) |
| `/mission` | `scripts/npc.js` | same dialog (tab 3) |
| `/sector` | `scripts/sector.js` | `SectorMapApp` (IFRAME Traveller Map, clics→chat) |
| `/subsector` | `scripts/sector.js` | `SectorMapApp` (IFRAME) |
Commands are registered via `ChatLogV2.CHAT_COMMANDS` — not Hooks.on("chatMessage") as the copilot-instructions say (that file is stale). See `commerce.js:15` and `npc.js:15` for the actual pattern.
## Framework quirks
- **ApplicationV2** + `HandlebarsApplicationMixin` for all dialogs.
- Foundry v13/v14 dual code paths: some hooks must handle both jQuery (v13) and DOM (v14) APIs.
- Dice rolls: `await new Roll(formula).evaluate()` — always async.
- Skill FQN format for `game.i18n`: e.g. `pilot.spacecraft`, `electronics.computers`, `gunner.turret`.
## Runtime systems (run at `ready` hook, GM only)
1. **Migration** (`mgt2eMigration.js`): converts legacy item types to mgt2e (armor→armour, equipment→item/augment, computer→hardware, etc.). Tracked via `game.settings` version flag. Forced re-run requires deleting the setting.
2. **NPC RollTable sync** (`npcRollTableSync.js`): writes D66 tables from `scripts/data/npcTables.js` into the `tables-pnj` compendium pack. Checksums to avoid redundant writes.
## Testing
```sh
# standalone, no npm needed — install jest globally or use npx
npx jest scripts/tests/travellerNpcGenerator.test.js
```
- Single file, 1096 lines, hand-rolled FoundryVTT mocks.
- Covers: utilities, lookups, validation, characteristic/skill generation, skill conversion, full NPC gen, ModuleCache, errors, config validation.
- No CI test step — you must run manually.
- No Jest config file — relies on defaults.
## Versioning
- `module.json``"version"` is the single source of truth.
- Git tag format: `v1.3.0` (CI strips `v`).
- No changelog file.
## CI/CD (Gitea)
- Only triggers on **`release: [published]`** — not on push/PR.
- Builds a zip archive of `module.json + assets/ + packs/ + scripts/ + styles/ + templates/` and uploads to the release.
- No test step in CI.
## Deprecated packs (still on disk, NOT in module.json)
- `packs/arme/` → superseded by `armes`
- `packs/carriere/` → superseded by `carrieres`
- `packs/talent-psy/` → superseded by `talents-psioniques`
Do not re-add them to `module.json`. They remain for historical data recovery only.
## All content is in French
Labels, comments, commit messages, UI strings, icon file names, rule references — everything. Check French naming before searching/grepping.
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
testMatch: ['**/scripts/tests/*.test.js'],
transform: {},
};
+72 -1
View File
@@ -11,6 +11,7 @@
"esmodules": [
"scripts/commerce.js",
"scripts/npc.js",
"scripts/sector.js",
"scripts/utils/travellerNpcUtils.js",
"scripts/data/travellerNpcGenerator.js",
"scripts/travellerNpcGenerator.js",
@@ -24,6 +25,65 @@
"styles/npc.css",
"styles/traveller-npc.css"
],
"packFolders": {
"name": "Amiral Denisov",
"sorting": "m",
"color": "#00435c",
"folders": [
{
"name": "Equipements",
"sorting": "a",
"color": "#00435c",
"packs": [
"armures",
"objet",
"equipement",
"ordinateur",
"contenant-sac-coffre",
"armes"
],
"folders": []
},
{
"name": "Références",
"sorting": "b",
"color": "#00435c",
"packs": [
"competences",
"maladie-poison-and-blessure",
"espece",
"talents-psioniques",
"carrieres"
],
"folders": []
},
{
"name": "PNJ & Items",
"sorting": "c",
"color": "#00435c",
"packs": [
"tables-pnj",
"pnj",
"competences",
"maladie-poison-and-blessure",
"espece",
"talents-psioniques",
"carrieres"
],
"folders": []
},
{
"name": "Journaux",
"sorting": "d",
"color": "#00435c",
"packs": [
"journal"
],
"folders": []
}
],
"packs": []
},
"packs": [
{
"name": "armures",
@@ -115,7 +175,7 @@
},
{
"name": "journal",
"label": "Journal Psioniques",
"label": "Journal Psioniques",
"path": "packs/journal",
"type": "JournalEntry",
"system": "mgt2e",
@@ -167,6 +227,17 @@
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
},
{
"name": "pnj",
"label": "PNJs",
"path": "packs/pnj",
"type": "Actor",
"system": "mgt2e",
"ownership": {
"PLAYER": "OBSERVER",
"ASSISTANT": "OWNER"
}
}
]
}
+4
View File
@@ -0,0 +1,4 @@
{
"type": "module",
"private": true
}
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000084
MANIFEST-000132
+8 -3
View File
@@ -1,3 +1,8 @@
2026/05/27-23:11:58.414164 7f3e6f7fe6c0 Recovering log #82
2026/05/27-23:11:58.424222 7f3e6f7fe6c0 Delete type=3 #80
2026/05/27-23:11:58.424274 7f3e6f7fe6c0 Delete type=0 #82
2026/06/12-20:52:58.023135 7fc410ffe6c0 Recovering log #130
2026/06/12-20:52:58.033792 7fc410ffe6c0 Delete type=3 #128
2026/06/12-20:52:58.033845 7fc410ffe6c0 Delete type=0 #130
2026/06/12-20:53:22.703483 7fc3c11be6c0 Level-0 table #135: started
2026/06/12-20:53:22.703533 7fc3c11be6c0 Level-0 table #135: 0 bytes OK
2026/06/12-20:53:22.710309 7fc3c11be6c0 Delete type=0 #133
2026/06/12-20:53:22.717089 7fc3c11be6c0 Manual compaction at level-0 from '!folders!673DRfEBYUliGnKJ' @ 72057594037927935 : 1 .. '!items!yoIqL0RQEnzNVJB6' @ 0 : 0; will stop at (end)
2026/06/12-20:53:22.724385 7fc3c11be6c0 Manual compaction at level-1 from '!folders!673DRfEBYUliGnKJ' @ 72057594037927935 : 1 .. '!items!yoIqL0RQEnzNVJB6' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/27-14:31:25.639035 7f3e6f7fe6c0 Recovering log #77
2026/05/27-14:31:25.648169 7f3e6f7fe6c0 Delete type=3 #75
2026/05/27-14:31:25.648196 7f3e6f7fe6c0 Delete type=0 #77
2026/05/27-14:33:21.786747 7f3e6effd6c0 Level-0 table #83: started
2026/05/27-14:33:21.786766 7f3e6effd6c0 Level-0 table #83: 0 bytes OK
2026/05/27-14:33:21.793932 7f3e6effd6c0 Delete type=0 #81
2026/05/27-14:33:21.800554 7f3e6effd6c0 Manual compaction at level-0 from '!folders!673DRfEBYUliGnKJ' @ 72057594037927935 : 1 .. '!items!yoIqL0RQEnzNVJB6' @ 0 : 0; will stop at (end)
2026/05/27-14:33:21.800579 7f3e6effd6c0 Manual compaction at level-1 from '!folders!673DRfEBYUliGnKJ' @ 72057594037927935 : 1 .. '!items!yoIqL0RQEnzNVJB6' @ 0 : 0; will stop at (end)
2026/06/12-20:51:44.594732 7fc3c3fff6c0 Recovering log #126
2026/06/12-20:51:44.605104 7fc3c3fff6c0 Delete type=3 #124
2026/06/12-20:51:44.605163 7fc3c3fff6c0 Delete type=0 #126
2026/06/12-20:52:05.597166 7fc3c11be6c0 Level-0 table #131: started
2026/06/12-20:52:05.597184 7fc3c11be6c0 Level-0 table #131: 0 bytes OK
2026/06/12-20:52:05.604422 7fc3c11be6c0 Delete type=0 #129
2026/06/12-20:52:05.618860 7fc3c11be6c0 Manual compaction at level-0 from '!folders!673DRfEBYUliGnKJ' @ 72057594037927935 : 1 .. '!items!yoIqL0RQEnzNVJB6' @ 0 : 0; will stop at (end)
2026/06/12-20:52:05.619120 7fc3c11be6c0 Manual compaction at level-1 from '!folders!673DRfEBYUliGnKJ' @ 72057594037927935 : 1 .. '!items!yoIqL0RQEnzNVJB6' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000183
MANIFEST-000231
+8 -3
View File
@@ -1,3 +1,8 @@
2026/05/27-23:11:58.290859 7f3e6f7fe6c0 Recovering log #181
2026/05/27-23:11:58.307573 7f3e6f7fe6c0 Delete type=3 #179
2026/05/27-23:11:58.307626 7f3e6f7fe6c0 Delete type=0 #181
2026/06/12-20:52:57.899810 7fc3c37fe6c0 Recovering log #229
2026/06/12-20:52:57.911135 7fc3c37fe6c0 Delete type=3 #227
2026/06/12-20:52:57.911180 7fc3c37fe6c0 Delete type=0 #229
2026/06/12-20:53:22.642554 7fc3c11be6c0 Level-0 table #234: started
2026/06/12-20:53:22.642590 7fc3c11be6c0 Level-0 table #234: 0 bytes OK
2026/06/12-20:53:22.648801 7fc3c11be6c0 Delete type=0 #232
2026/06/12-20:53:22.649072 7fc3c11be6c0 Manual compaction at level-0 from '!items!8xqChkoKK7i0c9M1' @ 72057594037927935 : 1 .. '!items!wpBopoosZiWXjlKD' @ 0 : 0; will stop at (end)
2026/06/12-20:53:22.649091 7fc3c11be6c0 Manual compaction at level-1 from '!items!8xqChkoKK7i0c9M1' @ 72057594037927935 : 1 .. '!items!wpBopoosZiWXjlKD' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/27-14:31:25.531736 7f3ebd7ff6c0 Recovering log #176
2026/05/27-14:31:25.541455 7f3ebd7ff6c0 Delete type=3 #174
2026/05/27-14:31:25.541507 7f3ebd7ff6c0 Delete type=0 #176
2026/05/27-14:33:21.722465 7f3e6effd6c0 Level-0 table #182: started
2026/05/27-14:33:21.722495 7f3e6effd6c0 Level-0 table #182: 0 bytes OK
2026/05/27-14:33:21.728923 7f3e6effd6c0 Delete type=0 #180
2026/05/27-14:33:21.735721 7f3e6effd6c0 Manual compaction at level-0 from '!items!8xqChkoKK7i0c9M1' @ 72057594037927935 : 1 .. '!items!wpBopoosZiWXjlKD' @ 0 : 0; will stop at (end)
2026/05/27-14:33:21.735741 7f3e6effd6c0 Manual compaction at level-1 from '!items!8xqChkoKK7i0c9M1' @ 72057594037927935 : 1 .. '!items!wpBopoosZiWXjlKD' @ 0 : 0; will stop at (end)
2026/06/12-20:51:44.470883 7fc3c37fe6c0 Recovering log #225
2026/06/12-20:51:44.481326 7fc3c37fe6c0 Delete type=3 #223
2026/06/12-20:51:44.481382 7fc3c37fe6c0 Delete type=0 #225
2026/06/12-20:52:05.534179 7fc3c11be6c0 Level-0 table #230: started
2026/06/12-20:52:05.534214 7fc3c11be6c0 Level-0 table #230: 0 bytes OK
2026/06/12-20:52:05.540979 7fc3c11be6c0 Delete type=0 #228
2026/06/12-20:52:05.541333 7fc3c11be6c0 Manual compaction at level-0 from '!items!8xqChkoKK7i0c9M1' @ 72057594037927935 : 1 .. '!items!wpBopoosZiWXjlKD' @ 0 : 0; will stop at (end)
2026/06/12-20:52:05.562210 7fc3c11be6c0 Manual compaction at level-1 from '!items!8xqChkoKK7i0c9M1' @ 72057594037927935 : 1 .. '!items!wpBopoosZiWXjlKD' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000067
MANIFEST-000115
+8 -3
View File
@@ -1,3 +1,8 @@
2026/05/27-23:11:58.442577 7f3e6f7fe6c0 Recovering log #65
2026/05/27-23:11:58.452097 7f3e6f7fe6c0 Delete type=3 #63
2026/05/27-23:11:58.452144 7f3e6f7fe6c0 Delete type=0 #65
2026/06/12-20:52:58.051634 7fc410ffe6c0 Recovering log #113
2026/06/12-20:52:58.062421 7fc410ffe6c0 Delete type=3 #111
2026/06/12-20:52:58.062480 7fc410ffe6c0 Delete type=0 #113
2026/06/12-20:53:22.717098 7fc3c11be6c0 Level-0 table #118: started
2026/06/12-20:53:22.717122 7fc3c11be6c0 Level-0 table #118: 0 bytes OK
2026/06/12-20:53:22.724248 7fc3c11be6c0 Delete type=0 #116
2026/06/12-20:53:22.822698 7fc3c11be6c0 Manual compaction at level-0 from '!items!57vgsVVCy9MRKM2M' @ 72057594037927935 : 1 .. '!items!vJInnoigCTJzuY2S' @ 0 : 0; will stop at (end)
2026/06/12-20:53:22.829756 7fc3c11be6c0 Manual compaction at level-1 from '!items!57vgsVVCy9MRKM2M' @ 72057594037927935 : 1 .. '!items!vJInnoigCTJzuY2S' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/27-14:31:25.663213 7f3e6f7fe6c0 Recovering log #60
2026/05/27-14:31:25.672282 7f3e6f7fe6c0 Delete type=3 #58
2026/05/27-14:31:25.672315 7f3e6f7fe6c0 Delete type=0 #60
2026/05/27-14:33:21.794049 7f3e6effd6c0 Level-0 table #66: started
2026/05/27-14:33:21.794078 7f3e6effd6c0 Level-0 table #66: 0 bytes OK
2026/05/27-14:33:21.800426 7f3e6effd6c0 Delete type=0 #64
2026/05/27-14:33:21.808266 7f3e6effd6c0 Manual compaction at level-0 from '!items!57vgsVVCy9MRKM2M' @ 72057594037927935 : 1 .. '!items!vJInnoigCTJzuY2S' @ 0 : 0; will stop at (end)
2026/05/27-14:33:21.831733 7f3e6effd6c0 Manual compaction at level-1 from '!items!57vgsVVCy9MRKM2M' @ 72057594037927935 : 1 .. '!items!vJInnoigCTJzuY2S' @ 0 : 0; will stop at (end)
2026/06/12-20:51:44.625465 7fc3c37fe6c0 Recovering log #109
2026/06/12-20:51:44.635539 7fc3c37fe6c0 Delete type=3 #107
2026/06/12-20:51:44.635595 7fc3c37fe6c0 Delete type=0 #109
2026/06/12-20:52:05.611307 7fc3c11be6c0 Level-0 table #114: started
2026/06/12-20:52:05.611325 7fc3c11be6c0 Level-0 table #114: 0 bytes OK
2026/06/12-20:52:05.618732 7fc3c11be6c0 Delete type=0 #112
2026/06/12-20:52:05.619090 7fc3c11be6c0 Manual compaction at level-0 from '!items!57vgsVVCy9MRKM2M' @ 72057594037927935 : 1 .. '!items!vJInnoigCTJzuY2S' @ 0 : 0; will stop at (end)
2026/06/12-20:52:05.619279 7fc3c11be6c0 Manual compaction at level-1 from '!items!57vgsVVCy9MRKM2M' @ 72057594037927935 : 1 .. '!items!vJInnoigCTJzuY2S' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000186
MANIFEST-000234
+8 -3
View File
@@ -1,3 +1,8 @@
2026/05/27-23:11:58.309946 7f3ebcffe6c0 Recovering log #184
2026/05/27-23:11:58.320486 7f3ebcffe6c0 Delete type=3 #182
2026/05/27-23:11:58.320525 7f3ebcffe6c0 Delete type=0 #184
2026/06/12-20:52:57.913632 7fc410ffe6c0 Recovering log #232
2026/06/12-20:52:57.923055 7fc410ffe6c0 Delete type=3 #230
2026/06/12-20:52:57.923103 7fc410ffe6c0 Delete type=0 #232
2026/06/12-20:53:22.662427 7fc3c11be6c0 Level-0 table #237: started
2026/06/12-20:53:22.662456 7fc3c11be6c0 Level-0 table #237: 0 bytes OK
2026/06/12-20:53:22.669591 7fc3c11be6c0 Delete type=0 #235
2026/06/12-20:53:22.676441 7fc3c11be6c0 Manual compaction at level-0 from '!items!04MdBSzwkYWUMJBC' @ 72057594037927935 : 1 .. '!items!yqjKyTCgpclCuHyK' @ 0 : 0; will stop at (end)
2026/06/12-20:53:22.676637 7fc3c11be6c0 Manual compaction at level-1 from '!items!04MdBSzwkYWUMJBC' @ 72057594037927935 : 1 .. '!items!yqjKyTCgpclCuHyK' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/27-14:31:25.544106 7f3ebd7ff6c0 Recovering log #179
2026/05/27-14:31:25.554048 7f3ebd7ff6c0 Delete type=3 #177
2026/05/27-14:31:25.554071 7f3ebd7ff6c0 Delete type=0 #179
2026/05/27-14:33:21.729016 7f3e6effd6c0 Level-0 table #185: started
2026/05/27-14:33:21.729040 7f3e6effd6c0 Level-0 table #185: 0 bytes OK
2026/05/27-14:33:21.735577 7f3e6effd6c0 Delete type=0 #183
2026/05/27-14:33:21.735798 7f3e6effd6c0 Manual compaction at level-0 from '!items!04MdBSzwkYWUMJBC' @ 72057594037927935 : 1 .. '!items!yqjKyTCgpclCuHyK' @ 0 : 0; will stop at (end)
2026/05/27-14:33:21.735816 7f3e6effd6c0 Manual compaction at level-1 from '!items!04MdBSzwkYWUMJBC' @ 72057594037927935 : 1 .. '!items!yqjKyTCgpclCuHyK' @ 0 : 0; will stop at (end)
2026/06/12-20:51:44.484787 7fc410ffe6c0 Recovering log #228
2026/06/12-20:51:44.494717 7fc410ffe6c0 Delete type=3 #226
2026/06/12-20:51:44.494769 7fc410ffe6c0 Delete type=0 #228
2026/06/12-20:52:05.541367 7fc3c11be6c0 Level-0 table #233: started
2026/06/12-20:52:05.541402 7fc3c11be6c0 Level-0 table #233: 0 bytes OK
2026/06/12-20:52:05.548343 7fc3c11be6c0 Delete type=0 #231
2026/06/12-20:52:05.562225 7fc3c11be6c0 Manual compaction at level-0 from '!items!04MdBSzwkYWUMJBC' @ 72057594037927935 : 1 .. '!items!yqjKyTCgpclCuHyK' @ 0 : 0; will stop at (end)
2026/06/12-20:52:05.562411 7fc3c11be6c0 Manual compaction at level-1 from '!items!04MdBSzwkYWUMJBC' @ 72057594037927935 : 1 .. '!items!yqjKyTCgpclCuHyK' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000120
MANIFEST-000168
+7 -3
View File
@@ -1,3 +1,7 @@
2026/05/27-23:11:58.375862 7f3e6ffff6c0 Recovering log #118
2026/05/27-23:11:58.385912 7f3e6ffff6c0 Delete type=3 #116
2026/05/27-23:11:58.385947 7f3e6ffff6c0 Delete type=0 #118
2026/06/12-20:52:57.981469 7fc410ffe6c0 Recovering log #166
2026/06/12-20:52:57.992240 7fc410ffe6c0 Delete type=3 #164
2026/06/12-20:52:57.992286 7fc410ffe6c0 Delete type=0 #166
2026/06/12-20:53:22.676645 7fc3c11be6c0 Level-0 table #171: started
2026/06/12-20:53:22.676670 7fc3c11be6c0 Level-0 table #171: 0 bytes OK
2026/06/12-20:53:22.683046 7fc3c11be6c0 Delete type=0 #169
2026/06/12-20:53:22.703339 7fc3c11be6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
+7 -7
View File
@@ -1,7 +1,7 @@
2026/05/27-14:31:25.604521 7f3ebd7ff6c0 Recovering log #114
2026/05/27-14:31:25.613630 7f3ebd7ff6c0 Delete type=3 #112
2026/05/27-14:31:25.613651 7f3ebd7ff6c0 Delete type=0 #114
2026/05/27-14:33:21.767043 7f3e6effd6c0 Level-0 table #119: started
2026/05/27-14:33:21.767067 7f3e6effd6c0 Level-0 table #119: 0 bytes OK
2026/05/27-14:33:21.773608 7f3e6effd6c0 Delete type=0 #117
2026/05/27-14:33:21.780095 7f3e6effd6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
2026/06/12-20:51:44.553830 7fc3c37fe6c0 Recovering log #162
2026/06/12-20:51:44.563465 7fc3c37fe6c0 Delete type=3 #160
2026/06/12-20:51:44.563519 7fc3c37fe6c0 Delete type=0 #162
2026/06/12-20:52:05.576474 7fc3c11be6c0 Level-0 table #167: started
2026/06/12-20:52:05.576496 7fc3c11be6c0 Level-0 table #167: 0 bytes OK
2026/06/12-20:52:05.583147 7fc3c11be6c0 Delete type=0 #165
2026/06/12-20:52:05.590265 7fc3c11be6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000128
MANIFEST-000176
+8 -3
View File
@@ -1,3 +1,8 @@
2026/05/27-23:11:58.349736 7f3e6ffff6c0 Recovering log #126
2026/05/27-23:11:58.359183 7f3e6ffff6c0 Delete type=3 #124
2026/05/27-23:11:58.359211 7f3e6ffff6c0 Delete type=0 #126
2026/06/12-20:52:57.954396 7fc410ffe6c0 Recovering log #174
2026/06/12-20:52:57.964106 7fc410ffe6c0 Delete type=3 #172
2026/06/12-20:52:57.964146 7fc410ffe6c0 Delete type=0 #174
2026/06/12-20:53:22.669694 7fc3c11be6c0 Level-0 table #179: started
2026/06/12-20:53:22.669720 7fc3c11be6c0 Level-0 table #179: 0 bytes OK
2026/06/12-20:53:22.676299 7fc3c11be6c0 Delete type=0 #177
2026/06/12-20:53:22.676452 7fc3c11be6c0 Manual compaction at level-0 from '!folders!8swFcTr6RH7BnGiu' @ 72057594037927935 : 1 .. '!items!zRJfxioYBRq4iSBR' @ 0 : 0; will stop at (end)
2026/06/12-20:53:22.676473 7fc3c11be6c0 Manual compaction at level-1 from '!folders!8swFcTr6RH7BnGiu' @ 72057594037927935 : 1 .. '!items!zRJfxioYBRq4iSBR' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/27-14:31:25.580201 7f3ebd7ff6c0 Recovering log #121
2026/05/27-14:31:25.589450 7f3ebd7ff6c0 Delete type=3 #119
2026/05/27-14:31:25.589494 7f3ebd7ff6c0 Delete type=0 #121
2026/05/27-14:33:21.747566 7f3e6effd6c0 Level-0 table #127: started
2026/05/27-14:33:21.747585 7f3e6effd6c0 Level-0 table #127: 0 bytes OK
2026/05/27-14:33:21.754474 7f3e6effd6c0 Delete type=0 #125
2026/05/27-14:33:21.760899 7f3e6effd6c0 Manual compaction at level-0 from '!folders!8swFcTr6RH7BnGiu' @ 72057594037927935 : 1 .. '!items!zRJfxioYBRq4iSBR' @ 0 : 0; will stop at (end)
2026/05/27-14:33:21.760915 7f3e6effd6c0 Manual compaction at level-1 from '!folders!8swFcTr6RH7BnGiu' @ 72057594037927935 : 1 .. '!items!zRJfxioYBRq4iSBR' @ 0 : 0; will stop at (end)
2026/06/12-20:51:44.524826 7fc3c37fe6c0 Recovering log #170
2026/06/12-20:51:44.535451 7fc3c37fe6c0 Delete type=3 #168
2026/06/12-20:51:44.535506 7fc3c37fe6c0 Delete type=0 #170
2026/06/12-20:52:05.562430 7fc3c11be6c0 Level-0 table #175: started
2026/06/12-20:52:05.562454 7fc3c11be6c0 Level-0 table #175: 0 bytes OK
2026/06/12-20:52:05.569802 7fc3c11be6c0 Delete type=0 #173
2026/06/12-20:52:05.590244 7fc3c11be6c0 Manual compaction at level-0 from '!folders!8swFcTr6RH7BnGiu' @ 72057594037927935 : 1 .. '!items!zRJfxioYBRq4iSBR' @ 0 : 0; will stop at (end)
2026/06/12-20:52:05.590432 7fc3c11be6c0 Manual compaction at level-1 from '!folders!8swFcTr6RH7BnGiu' @ 72057594037927935 : 1 .. '!items!zRJfxioYBRq4iSBR' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000120
MANIFEST-000168
+7 -3
View File
@@ -1,3 +1,7 @@
2026/05/27-23:11:58.388239 7f3e6f7fe6c0 Recovering log #118
2026/05/27-23:11:58.398684 7f3e6f7fe6c0 Delete type=3 #116
2026/05/27-23:11:58.398724 7f3e6f7fe6c0 Delete type=0 #118
2026/06/12-20:52:57.995403 7fc3c37fe6c0 Recovering log #166
2026/06/12-20:52:58.005793 7fc3c37fe6c0 Delete type=3 #164
2026/06/12-20:52:58.005851 7fc3c37fe6c0 Delete type=0 #166
2026/06/12-20:53:22.683153 7fc3c11be6c0 Level-0 table #171: started
2026/06/12-20:53:22.683178 7fc3c11be6c0 Level-0 table #171: 0 bytes OK
2026/06/12-20:53:22.690078 7fc3c11be6c0 Delete type=0 #169
2026/06/12-20:53:22.703354 7fc3c11be6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
+7 -7
View File
@@ -1,7 +1,7 @@
2026/05/27-14:31:25.615623 7f3ebd7ff6c0 Recovering log #114
2026/05/27-14:31:25.625348 7f3ebd7ff6c0 Delete type=3 #112
2026/05/27-14:31:25.625369 7f3ebd7ff6c0 Delete type=0 #114
2026/05/27-14:33:21.761003 7f3e6effd6c0 Level-0 table #119: started
2026/05/27-14:33:21.761022 7f3e6effd6c0 Level-0 table #119: 0 bytes OK
2026/05/27-14:33:21.766953 7f3e6effd6c0 Delete type=0 #117
2026/05/27-14:33:21.780083 7f3e6effd6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
2026/06/12-20:51:44.566595 7fc410ffe6c0 Recovering log #162
2026/06/12-20:51:44.577675 7fc410ffe6c0 Delete type=3 #160
2026/06/12-20:51:44.577730 7fc410ffe6c0 Delete type=0 #162
2026/06/12-20:52:05.583251 7fc3c11be6c0 Level-0 table #167: started
2026/06/12-20:52:05.583269 7fc3c11be6c0 Level-0 table #167: 0 bytes OK
2026/06/12-20:52:05.590142 7fc3c11be6c0 Delete type=0 #165
2026/06/12-20:52:05.590369 7fc3c11be6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000101
MANIFEST-000149
+8 -3
View File
@@ -1,3 +1,8 @@
2026/05/27-23:11:58.400455 7f3ebd7ff6c0 Recovering log #99
2026/05/27-23:11:58.410587 7f3ebd7ff6c0 Delete type=3 #97
2026/05/27-23:11:58.410645 7f3ebd7ff6c0 Delete type=0 #99
2026/06/12-20:52:58.008917 7fc3c3fff6c0 Recovering log #147
2026/06/12-20:52:58.019530 7fc3c3fff6c0 Delete type=3 #145
2026/06/12-20:52:58.019585 7fc3c3fff6c0 Delete type=0 #147
2026/06/12-20:53:22.690192 7fc3c11be6c0 Level-0 table #152: started
2026/06/12-20:53:22.690216 7fc3c11be6c0 Level-0 table #152: 0 bytes OK
2026/06/12-20:53:22.696652 7fc3c11be6c0 Delete type=0 #150
2026/06/12-20:53:22.703367 7fc3c11be6c0 Manual compaction at level-0 from '!journal!26ZqV9BvS47hrLmV' @ 72057594037927935 : 1 .. '!journal.pages!26ZqV9BvS47hrLmV.ZS4936SEQUT9IA8i' @ 0 : 0; will stop at (end)
2026/06/12-20:53:22.717074 7fc3c11be6c0 Manual compaction at level-1 from '!journal!26ZqV9BvS47hrLmV' @ 72057594037927935 : 1 .. '!journal.pages!26ZqV9BvS47hrLmV.ZS4936SEQUT9IA8i' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/27-14:31:25.627132 7f3ebd7ff6c0 Recovering log #95
2026/05/27-14:31:25.636967 7f3ebd7ff6c0 Delete type=3 #93
2026/05/27-14:31:25.637002 7f3ebd7ff6c0 Delete type=0 #95
2026/05/27-14:33:21.773684 7f3e6effd6c0 Level-0 table #100: started
2026/05/27-14:33:21.773700 7f3e6effd6c0 Level-0 table #100: 0 bytes OK
2026/05/27-14:33:21.779958 7f3e6effd6c0 Delete type=0 #98
2026/05/27-14:33:21.786739 7f3e6effd6c0 Manual compaction at level-0 from '!journal!26ZqV9BvS47hrLmV' @ 72057594037927935 : 1 .. '!journal.pages!26ZqV9BvS47hrLmV.ZS4936SEQUT9IA8i' @ 0 : 0; will stop at (end)
2026/05/27-14:33:21.800533 7f3e6effd6c0 Manual compaction at level-1 from '!journal!26ZqV9BvS47hrLmV' @ 72057594037927935 : 1 .. '!journal.pages!26ZqV9BvS47hrLmV.ZS4936SEQUT9IA8i' @ 0 : 0; will stop at (end)
2026/06/12-20:51:44.581216 7fc3c37fe6c0 Recovering log #143
2026/06/12-20:51:44.591542 7fc3c37fe6c0 Delete type=3 #141
2026/06/12-20:51:44.591593 7fc3c37fe6c0 Delete type=0 #143
2026/06/12-20:52:05.590481 7fc3c11be6c0 Level-0 table #148: started
2026/06/12-20:52:05.590505 7fc3c11be6c0 Level-0 table #148: 0 bytes OK
2026/06/12-20:52:05.597069 7fc3c11be6c0 Delete type=0 #146
2026/06/12-20:52:05.618846 7fc3c11be6c0 Manual compaction at level-0 from '!journal!26ZqV9BvS47hrLmV' @ 72057594037927935 : 1 .. '!journal.pages!26ZqV9BvS47hrLmV.ZS4936SEQUT9IA8i' @ 0 : 0; will stop at (end)
2026/06/12-20:52:05.619137 7fc3c11be6c0 Manual compaction at level-1 from '!journal!26ZqV9BvS47hrLmV' @ 72057594037927935 : 1 .. '!journal.pages!26ZqV9BvS47hrLmV.ZS4936SEQUT9IA8i' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000128
MANIFEST-000176
+8 -3
View File
@@ -1,3 +1,8 @@
2026/05/27-23:11:58.323722 7f3e6f7fe6c0 Recovering log #126
2026/05/27-23:11:58.334612 7f3e6f7fe6c0 Delete type=3 #124
2026/05/27-23:11:58.334668 7f3e6f7fe6c0 Delete type=0 #126
2026/06/12-20:52:57.926432 7fc3c37fe6c0 Recovering log #174
2026/06/12-20:52:57.936438 7fc3c37fe6c0 Delete type=3 #172
2026/06/12-20:52:57.936485 7fc3c37fe6c0 Delete type=0 #174
2026/06/12-20:53:22.649158 7fc3c11be6c0 Level-0 table #179: started
2026/06/12-20:53:22.649178 7fc3c11be6c0 Level-0 table #179: 0 bytes OK
2026/06/12-20:53:22.655504 7fc3c11be6c0 Delete type=0 #177
2026/06/12-20:53:22.676414 7fc3c11be6c0 Manual compaction at level-0 from '!items!QHovFMj93BC7bqBu' @ 72057594037927935 : 1 .. '!items!yleVHgRqGoYLvzxT' @ 0 : 0; will stop at (end)
2026/06/12-20:53:22.676616 7fc3c11be6c0 Manual compaction at level-1 from '!items!QHovFMj93BC7bqBu' @ 72057594037927935 : 1 .. '!items!yleVHgRqGoYLvzxT' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/27-14:31:25.557570 7f3ebd7ff6c0 Recovering log #121
2026/05/27-14:31:25.567130 7f3ebd7ff6c0 Delete type=3 #119
2026/05/27-14:31:25.567150 7f3ebd7ff6c0 Delete type=0 #121
2026/05/27-14:33:21.735841 7f3e6effd6c0 Level-0 table #127: started
2026/05/27-14:33:21.735866 7f3e6effd6c0 Level-0 table #127: 0 bytes OK
2026/05/27-14:33:21.741712 7f3e6effd6c0 Delete type=0 #125
2026/05/27-14:33:21.760883 7f3e6effd6c0 Manual compaction at level-0 from '!items!QHovFMj93BC7bqBu' @ 72057594037927935 : 1 .. '!items!yleVHgRqGoYLvzxT' @ 0 : 0; will stop at (end)
2026/05/27-14:33:21.760906 7f3e6effd6c0 Manual compaction at level-1 from '!items!QHovFMj93BC7bqBu' @ 72057594037927935 : 1 .. '!items!yleVHgRqGoYLvzxT' @ 0 : 0; will stop at (end)
2026/06/12-20:51:44.497744 7fc3c3fff6c0 Recovering log #170
2026/06/12-20:51:44.508610 7fc3c3fff6c0 Delete type=3 #168
2026/06/12-20:51:44.508662 7fc3c3fff6c0 Delete type=0 #170
2026/06/12-20:52:05.548467 7fc3c11be6c0 Level-0 table #175: started
2026/06/12-20:52:05.548492 7fc3c11be6c0 Level-0 table #175: 0 bytes OK
2026/06/12-20:52:05.555571 7fc3c11be6c0 Delete type=0 #173
2026/06/12-20:52:05.562235 7fc3c11be6c0 Manual compaction at level-0 from '!items!QHovFMj93BC7bqBu' @ 72057594037927935 : 1 .. '!items!yleVHgRqGoYLvzxT' @ 0 : 0; will stop at (end)
2026/06/12-20:52:05.562402 7fc3c11be6c0 Manual compaction at level-1 from '!items!QHovFMj93BC7bqBu' @ 72057594037927935 : 1 .. '!items!yleVHgRqGoYLvzxT' @ 0 : 0; will stop at (end)
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000120
MANIFEST-000168
+7 -3
View File
@@ -1,3 +1,7 @@
2026/05/27-23:11:58.337311 7f3ebcffe6c0 Recovering log #118
2026/05/27-23:11:58.347349 7f3ebcffe6c0 Delete type=3 #116
2026/05/27-23:11:58.347392 7f3ebcffe6c0 Delete type=0 #118
2026/06/12-20:52:57.939847 7fc4117ff6c0 Recovering log #166
2026/06/12-20:52:57.951112 7fc4117ff6c0 Delete type=3 #164
2026/06/12-20:52:57.951166 7fc4117ff6c0 Delete type=0 #166
2026/06/12-20:53:22.655606 7fc3c11be6c0 Level-0 table #171: started
2026/06/12-20:53:22.655631 7fc3c11be6c0 Level-0 table #171: 0 bytes OK
2026/06/12-20:53:22.662292 7fc3c11be6c0 Delete type=0 #169
2026/06/12-20:53:22.676430 7fc3c11be6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
+7 -7
View File
@@ -1,7 +1,7 @@
2026/05/27-14:31:25.569405 7f3ebd7ff6c0 Recovering log #114
2026/05/27-14:31:25.578632 7f3ebd7ff6c0 Delete type=3 #112
2026/05/27-14:31:25.578657 7f3ebd7ff6c0 Delete type=0 #114
2026/05/27-14:33:21.741780 7f3e6effd6c0 Level-0 table #119: started
2026/05/27-14:33:21.741799 7f3e6effd6c0 Level-0 table #119: 0 bytes OK
2026/05/27-14:33:21.747507 7f3e6effd6c0 Delete type=0 #117
2026/05/27-14:33:21.760892 7f3e6effd6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
2026/06/12-20:51:44.511682 7fc410ffe6c0 Recovering log #162
2026/06/12-20:51:44.521666 7fc410ffe6c0 Delete type=3 #160
2026/06/12-20:51:44.521720 7fc410ffe6c0 Delete type=0 #162
2026/06/12-20:52:05.555691 7fc3c11be6c0 Level-0 table #167: started
2026/06/12-20:52:05.555716 7fc3c11be6c0 Level-0 table #167: 0 bytes OK
2026/06/12-20:52:05.562097 7fc3c11be6c0 Delete type=0 #165
2026/06/12-20:52:05.562305 7fc3c11be6c0 Manual compaction at level-0 from 'undefined' @ 72057594037927935 : 1 .. 'undefined' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000128
MANIFEST-000176
+8 -3
View File
@@ -1,3 +1,8 @@
2026/05/27-23:11:58.362926 7f3ebd7ff6c0 Recovering log #126
2026/05/27-23:11:58.372894 7f3ebd7ff6c0 Delete type=3 #124
2026/05/27-23:11:58.372934 7f3ebd7ff6c0 Delete type=0 #126
2026/06/12-20:52:57.967451 7fc4117ff6c0 Recovering log #174
2026/06/12-20:52:57.978196 7fc4117ff6c0 Delete type=3 #172
2026/06/12-20:52:57.978251 7fc4117ff6c0 Delete type=0 #174
2026/06/12-20:53:22.696764 7fc3c11be6c0 Level-0 table #179: started
2026/06/12-20:53:22.696789 7fc3c11be6c0 Level-0 table #179: 0 bytes OK
2026/06/12-20:53:22.703214 7fc3c11be6c0 Delete type=0 #177
2026/06/12-20:53:22.703379 7fc3c11be6c0 Manual compaction at level-0 from '!folders!qrqRBmTP6UuS30DF' @ 72057594037927935 : 1 .. '!items!yFvuDyV00NdojxGt' @ 0 : 0; will stop at (end)
2026/06/12-20:53:22.710437 7fc3c11be6c0 Manual compaction at level-1 from '!folders!qrqRBmTP6UuS30DF' @ 72057594037927935 : 1 .. '!items!yFvuDyV00NdojxGt' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/27-14:31:25.591969 7f3ebd7ff6c0 Recovering log #121
2026/05/27-14:31:25.601963 7f3ebd7ff6c0 Delete type=3 #119
2026/05/27-14:31:25.601983 7f3ebd7ff6c0 Delete type=0 #121
2026/05/27-14:33:21.754559 7f3e6effd6c0 Level-0 table #127: started
2026/05/27-14:33:21.754583 7f3e6effd6c0 Level-0 table #127: 0 bytes OK
2026/05/27-14:33:21.760810 7f3e6effd6c0 Delete type=0 #125
2026/05/27-14:33:21.760994 7f3e6effd6c0 Manual compaction at level-0 from '!folders!qrqRBmTP6UuS30DF' @ 72057594037927935 : 1 .. '!items!yFvuDyV00NdojxGt' @ 0 : 0; will stop at (end)
2026/05/27-14:33:21.773676 7f3e6effd6c0 Manual compaction at level-1 from '!folders!qrqRBmTP6UuS30DF' @ 72057594037927935 : 1 .. '!items!yFvuDyV00NdojxGt' @ 0 : 0; will stop at (end)
2026/06/12-20:51:44.538990 7fc410ffe6c0 Recovering log #170
2026/06/12-20:51:44.550083 7fc410ffe6c0 Delete type=3 #168
2026/06/12-20:51:44.550141 7fc410ffe6c0 Delete type=0 #170
2026/06/12-20:52:05.569908 7fc3c11be6c0 Level-0 table #175: started
2026/06/12-20:52:05.569929 7fc3c11be6c0 Level-0 table #175: 0 bytes OK
2026/06/12-20:52:05.576375 7fc3c11be6c0 Delete type=0 #173
2026/06/12-20:52:05.590256 7fc3c11be6c0 Manual compaction at level-0 from '!folders!qrqRBmTP6UuS30DF' @ 72057594037927935 : 1 .. '!items!yFvuDyV00NdojxGt' @ 0 : 0; will stop at (end)
2026/06/12-20:52:05.590425 7fc3c11be6c0 Manual compaction at level-1 from '!folders!qrqRBmTP6UuS30DF' @ 72057594037927935 : 1 .. '!items!yFvuDyV00NdojxGt' @ 0 : 0; will stop at (end)
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
MANIFEST-000014
View File
+8
View File
@@ -0,0 +1,8 @@
2026/06/12-20:52:58.209985 7fc3c3fff6c0 Recovering log #12
2026/06/12-20:52:58.222276 7fc3c3fff6c0 Delete type=3 #10
2026/06/12-20:52:58.222315 7fc3c3fff6c0 Delete type=0 #12
2026/06/12-20:53:22.822733 7fc3c11be6c0 Level-0 table #17: started
2026/06/12-20:53:22.822761 7fc3c11be6c0 Level-0 table #17: 0 bytes OK
2026/06/12-20:53:22.829641 7fc3c11be6c0 Delete type=0 #15
2026/06/12-20:53:22.836426 7fc3c11be6c0 Manual compaction at level-0 from '!actors!DL3zXfXxWMOQhXJY' @ 72057594037927935 : 1 .. '!actors.items!JAHYXFedTxNlGYX8.nmIgA3CQkOSEpCSP' @ 0 : 0; will stop at (end)
2026/06/12-20:53:22.960032 7fc3c11be6c0 Manual compaction at level-1 from '!actors!DL3zXfXxWMOQhXJY' @ 72057594037927935 : 1 .. '!actors.items!JAHYXFedTxNlGYX8.nmIgA3CQkOSEpCSP' @ 0 : 0; will stop at (end)
+8
View File
@@ -0,0 +1,8 @@
2026/06/12-20:51:44.772467 7fc3c37fe6c0 Recovering log #8
2026/06/12-20:51:44.784739 7fc3c37fe6c0 Delete type=3 #6
2026/06/12-20:51:44.784799 7fc3c37fe6c0 Delete type=0 #8
2026/06/12-20:52:05.700099 7fc3c11be6c0 Level-0 table #13: started
2026/06/12-20:52:05.700122 7fc3c11be6c0 Level-0 table #13: 0 bytes OK
2026/06/12-20:52:05.707051 7fc3c11be6c0 Delete type=0 #11
2026/06/12-20:52:05.714022 7fc3c11be6c0 Manual compaction at level-0 from '!actors!DL3zXfXxWMOQhXJY' @ 72057594037927935 : 1 .. '!actors.items!JAHYXFedTxNlGYX8.nmIgA3CQkOSEpCSP' @ 0 : 0; will stop at (end)
2026/06/12-20:52:05.714257 7fc3c11be6c0 Manual compaction at level-1 from '!actors!DL3zXfXxWMOQhXJY' @ 72057594037927935 : 1 .. '!actors.items!JAHYXFedTxNlGYX8.nmIgA3CQkOSEpCSP' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000048
MANIFEST-000291
+19 -69
View File
@@ -1,69 +1,19 @@
2026/05/27-23:11:58.454344 7f3ebcffe6c0 Recovering log #44
2026/05/27-23:11:58.463285 7f3ebcffe6c0 Delete type=3 #40
2026/05/27-23:11:58.463300 7f3ebcffe6c0 Delete type=0 #44
2026/05/27-23:37:44.587645 7f3e6effd6c0 Level-0 table #51: started
2026/05/27-23:37:44.605872 7f3e6effd6c0 Level-0 table #51: 1343154 bytes OK
2026/05/27-23:37:44.612030 7f3e6effd6c0 Delete type=0 #49
2026/05/28-00:04:39.168329 7f3e6effd6c0 Level-0 table #53: started
2026/05/28-00:04:39.185209 7f3e6effd6c0 Level-0 table #53: 1393674 bytes OK
2026/05/28-00:04:39.191603 7f3e6effd6c0 Delete type=0 #50
2026/05/28-00:18:55.273857 7f3e6effd6c0 Level-0 table #55: started
2026/05/28-00:18:55.294346 7f3e6effd6c0 Level-0 table #55: 1451564 bytes OK
2026/05/28-00:18:55.300308 7f3e6effd6c0 Delete type=0 #52
2026/05/28-00:20:15.021651 7f3e6effd6c0 Level-0 table #57: started
2026/05/28-00:20:15.040525 7f3e6effd6c0 Level-0 table #57: 1496314 bytes OK
2026/05/28-00:20:15.046886 7f3e6effd6c0 Delete type=0 #54
2026/05/28-00:20:43.720521 7f3e6effd6c0 Level-0 table #59: started
2026/05/28-00:20:43.742455 7f3e6effd6c0 Level-0 table #59: 1546947 bytes OK
2026/05/28-00:20:43.748835 7f3e6effd6c0 Delete type=0 #56
2026/05/28-00:20:43.749387 7f3e6effd6c0 Compacting 4@0 + 1@1 files
2026/05/28-00:20:43.771709 7f3e6effd6c0 Generated table #60@0: 10302 keys, 1546947 bytes
2026/05/28-00:20:43.771721 7f3e6effd6c0 Compacted 4@0 + 1@1 files => 1546947 bytes
2026/05/28-00:20:43.777946 7f3e6effd6c0 compacted to: files[ 0 1 1 0 0 0 0 ]
2026/05/28-00:20:43.778171 7f3e6effd6c0 Delete type=2 #51
2026/05/28-00:20:43.778444 7f3e6effd6c0 Delete type=2 #53
2026/05/28-00:20:43.778777 7f3e6effd6c0 Delete type=2 #55
2026/05/28-00:20:43.778977 7f3e6effd6c0 Delete type=2 #57
2026/05/28-00:20:43.779160 7f3e6effd6c0 Delete type=2 #59
2026/05/28-00:21:51.457842 7f3e6effd6c0 Level-0 table #62: started
2026/05/28-00:21:51.479532 7f3e6effd6c0 Level-0 table #62: 1604425 bytes OK
2026/05/28-00:21:51.485724 7f3e6effd6c0 Delete type=0 #58
2026/05/28-00:30:27.802783 7f3e6effd6c0 Level-0 table #64: started
2026/05/28-00:30:27.823947 7f3e6effd6c0 Level-0 table #64: 1651065 bytes OK
2026/05/28-00:30:27.829721 7f3e6effd6c0 Delete type=0 #61
2026/05/28-00:35:01.919877 7f3e6effd6c0 Level-0 table #66: started
2026/05/28-00:35:01.987012 7f3e6effd6c0 Level-0 table #66: 1703060 bytes OK
2026/05/28-00:35:02.043369 7f3e6effd6c0 Delete type=0 #63
2026/05/28-00:37:26.052331 7f3e6effd6c0 Level-0 table #68: started
2026/05/28-00:37:26.076959 7f3e6effd6c0 Level-0 table #68: 1753179 bytes OK
2026/05/28-00:37:26.083119 7f3e6effd6c0 Delete type=0 #65
2026/05/28-00:37:26.083369 7f3e6effd6c0 Compacting 4@0 + 1@1 files
2026/05/28-00:37:26.103213 7f3e6effd6c0 Generated table #69@0: 11630 keys, 1753179 bytes
2026/05/28-00:37:26.103227 7f3e6effd6c0 Compacted 4@0 + 1@1 files => 1753179 bytes
2026/05/28-00:37:26.109075 7f3e6effd6c0 compacted to: files[ 0 1 1 0 0 0 0 ]
2026/05/28-00:37:26.109152 7f3e6effd6c0 Delete type=2 #60
2026/05/28-00:37:26.109336 7f3e6effd6c0 Delete type=2 #62
2026/05/28-00:37:26.109453 7f3e6effd6c0 Delete type=2 #64
2026/05/28-00:37:26.109722 7f3e6effd6c0 Delete type=2 #66
2026/05/28-00:37:26.109828 7f3e6effd6c0 Delete type=2 #68
2026/05/28-00:48:03.393850 7f3e6effd6c0 Level-0 table #71: started
2026/05/28-00:48:03.411976 7f3e6effd6c0 Level-0 table #71: 1806724 bytes OK
2026/05/28-00:48:03.419030 7f3e6effd6c0 Delete type=0 #67
2026/05/28-00:52:05.605289 7f3e6effd6c0 Level-0 table #73: started
2026/05/28-00:52:05.629208 7f3e6effd6c0 Level-0 table #73: 1856591 bytes OK
2026/05/28-00:52:05.635297 7f3e6effd6c0 Delete type=0 #70
2026/05/28-00:53:23.184161 7f3e6effd6c0 Level-0 table #75: started
2026/05/28-00:53:23.202618 7f3e6effd6c0 Level-0 table #75: 1901920 bytes OK
2026/05/28-00:53:23.208730 7f3e6effd6c0 Delete type=0 #72
2026/05/28-01:02:57.320650 7f3e6effd6c0 Level-0 table #77: started
2026/05/28-01:02:57.341951 7f3e6effd6c0 Level-0 table #77: 1962797 bytes OK
2026/05/28-01:02:57.347793 7f3e6effd6c0 Delete type=0 #74
2026/05/28-01:02:57.348132 7f3e6effd6c0 Compacting 4@0 + 1@1 files
2026/05/28-01:02:57.369982 7f3e6effd6c0 Generated table #78@0: 12958 keys, 1962797 bytes
2026/05/28-01:02:57.370013 7f3e6effd6c0 Compacted 4@0 + 1@1 files => 1962797 bytes
2026/05/28-01:02:57.376098 7f3e6effd6c0 compacted to: files[ 0 1 1 0 0 0 0 ]
2026/05/28-01:02:57.376330 7f3e6effd6c0 Delete type=2 #69
2026/05/28-01:02:57.376716 7f3e6effd6c0 Delete type=2 #71
2026/05/28-01:02:57.377097 7f3e6effd6c0 Delete type=2 #73
2026/05/28-01:02:57.377594 7f3e6effd6c0 Delete type=2 #75
2026/05/28-01:02:57.377833 7f3e6effd6c0 Delete type=2 #77
2026/06/12-20:52:58.065673 7fc3c3fff6c0 Recovering log #286
2026/06/12-20:52:58.076152 7fc3c3fff6c0 Delete type=3 #284
2026/06/12-20:52:58.076196 7fc3c3fff6c0 Delete type=0 #286
2026/06/12-20:53:22.724394 7fc3c11be6c0 Level-0 table #294: started
2026/06/12-20:53:22.813324 7fc3c11be6c0 Level-0 table #294: 5029605 bytes OK
2026/06/12-20:53:22.820890 7fc3c11be6c0 Delete type=0 #292
2026/06/12-20:53:22.829771 7fc3c11be6c0 Manual compaction at level-0 from '!tables!BbXMbmHKcLJrBCmk' @ 72057594037927935 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zzqLG98O4eFQZHp0' @ 0 : 0; will stop at (end)
2026/06/12-20:53:22.836453 7fc3c11be6c0 Manual compaction at level-1 from '!tables!BbXMbmHKcLJrBCmk' @ 72057594037927935 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zzqLG98O4eFQZHp0' @ 0 : 0; will stop at '!tables.results!xe7x4qufBpzLaEby.zzqLG98O4eFQZHp0' @ 1617840 : 1
2026/06/12-20:53:22.836460 7fc3c11be6c0 Compacting 1@1 + 3@2 files
2026/06/12-20:53:22.873874 7fc3c11be6c0 Generated table #295@1: 10562 keys, 2142429 bytes
2026/06/12-20:53:22.924542 7fc3c11be6c0 Generated table #296@1: 16816 keys, 2164017 bytes
2026/06/12-20:53:22.944331 7fc3c11be6c0 Generated table #297@1: 5500 keys, 721026 bytes
2026/06/12-20:53:22.944361 7fc3c11be6c0 Compacted 1@1 + 3@2 files => 5027472 bytes
2026/06/12-20:53:22.951445 7fc3c11be6c0 compacted to: files[ 0 0 3 0 0 0 0 ]
2026/06/12-20:53:22.951933 7fc3c11be6c0 Delete type=2 #288
2026/06/12-20:53:22.952420 7fc3c11be6c0 Delete type=2 #289
2026/06/12-20:53:22.952708 7fc3c11be6c0 Delete type=2 #290
2026/06/12-20:53:22.953103 7fc3c11be6c0 Delete type=2 #294
2026/06/12-20:53:22.960065 7fc3c11be6c0 Manual compaction at level-1 from '!tables.results!xe7x4qufBpzLaEby.zzqLG98O4eFQZHp0' @ 1617840 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zzqLG98O4eFQZHp0' @ 0 : 0; will stop at (end)
+19 -25
View File
@@ -1,25 +1,19 @@
2026/05/27-14:31:25.674162 7f3e6f7fe6c0 Recovering log #36
2026/05/27-14:31:25.684284 7f3e6f7fe6c0 Delete type=3 #21
2026/05/27-14:31:25.684327 7f3e6f7fe6c0 Delete type=0 #36
2026/05/27-14:32:13.276652 7f3e6effd6c0 Level-0 table #43: started
2026/05/27-14:32:13.292995 7f3e6effd6c0 Level-0 table #43: 1244894 bytes OK
2026/05/27-14:32:13.298790 7f3e6effd6c0 Delete type=0 #41
2026/05/27-14:33:21.808275 7f3e6effd6c0 Level-0 table #45: started
2026/05/27-14:33:21.824612 7f3e6effd6c0 Level-0 table #45: 1294793 bytes OK
2026/05/27-14:33:21.831380 7f3e6effd6c0 Delete type=0 #42
2026/05/27-14:33:21.838976 7f3e6effd6c0 Manual compaction at level-0 from '!tables!BbXMbmHKcLJrBCmk' @ 72057594037927935 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 109340 : 1
2026/05/27-14:33:21.838980 7f3e6effd6c0 Compacting 1@0 + 1@1 files
2026/05/27-14:33:21.852317 7f3e6effd6c0 Generated table #46@0: 8642 keys, 1294793 bytes
2026/05/27-14:33:21.852331 7f3e6effd6c0 Compacted 1@0 + 1@1 files => 1294793 bytes
2026/05/27-14:33:21.859689 7f3e6effd6c0 compacted to: files[ 0 1 1 0 0 0 0 ]
2026/05/27-14:33:21.859935 7f3e6effd6c0 Delete type=2 #43
2026/05/27-14:33:21.860058 7f3e6effd6c0 Delete type=2 #45
2026/05/27-14:33:21.877111 7f3e6effd6c0 Manual compaction at level-0 from '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 109340 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at (end)
2026/05/27-14:33:21.877144 7f3e6effd6c0 Manual compaction at level-1 from '!tables!BbXMbmHKcLJrBCmk' @ 72057594037927935 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 109340 : 1
2026/05/27-14:33:21.877150 7f3e6effd6c0 Compacting 1@1 + 1@2 files
2026/05/27-14:33:21.895679 7f3e6effd6c0 Generated table #47@1: 8642 keys, 1294793 bytes
2026/05/27-14:33:21.895699 7f3e6effd6c0 Compacted 1@1 + 1@2 files => 1294793 bytes
2026/05/27-14:33:21.901922 7f3e6effd6c0 compacted to: files[ 0 0 1 0 0 0 0 ]
2026/05/27-14:33:21.902059 7f3e6effd6c0 Delete type=2 #39
2026/05/27-14:33:21.902514 7f3e6effd6c0 Delete type=2 #46
2026/05/27-14:33:21.908609 7f3e6effd6c0 Manual compaction at level-1 from '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 109340 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zyJ49IY9JAmeIucJ' @ 0 : 0; will stop at (end)
2026/06/12-20:51:44.638713 7fc410ffe6c0 Recovering log #279
2026/06/12-20:51:44.649235 7fc410ffe6c0 Delete type=3 #277
2026/06/12-20:51:44.649296 7fc410ffe6c0 Delete type=0 #279
2026/06/12-20:52:05.627084 7fc3c11be6c0 Level-0 table #287: started
2026/06/12-20:52:05.691136 7fc3c11be6c0 Level-0 table #287: 4971536 bytes OK
2026/06/12-20:52:05.698145 7fc3c11be6c0 Delete type=0 #285
2026/06/12-20:52:05.714008 7fc3c11be6c0 Manual compaction at level-0 from '!tables!BbXMbmHKcLJrBCmk' @ 72057594037927935 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zzqLG98O4eFQZHp0' @ 0 : 0; will stop at (end)
2026/06/12-20:52:05.714299 7fc3c11be6c0 Manual compaction at level-1 from '!tables!BbXMbmHKcLJrBCmk' @ 72057594037927935 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zzqLG98O4eFQZHp0' @ 0 : 0; will stop at '!tables.results!xe7x4qufBpzLaEby.zzqLG98O4eFQZHp0' @ 1585250 : 1
2026/06/12-20:52:05.714307 7fc3c11be6c0 Compacting 1@1 + 3@2 files
2026/06/12-20:52:05.744344 7fc3c11be6c0 Generated table #288@1: 10660 keys, 2142130 bytes
2026/06/12-20:52:05.781422 7fc3c11be6c0 Generated table #289@1: 16841 keys, 2165292 bytes
2026/06/12-20:52:05.794598 7fc3c11be6c0 Generated table #290@1: 5045 keys, 662072 bytes
2026/06/12-20:52:05.794618 7fc3c11be6c0 Compacted 1@1 + 3@2 files => 4969494 bytes
2026/06/12-20:52:05.801998 7fc3c11be6c0 compacted to: files[ 0 0 3 0 0 0 0 ]
2026/06/12-20:52:05.802346 7fc3c11be6c0 Delete type=2 #281
2026/06/12-20:52:05.802678 7fc3c11be6c0 Delete type=2 #282
2026/06/12-20:52:05.802871 7fc3c11be6c0 Delete type=2 #283
2026/06/12-20:52:05.803095 7fc3c11be6c0 Delete type=2 #287
2026/06/12-20:52:05.821244 7fc3c11be6c0 Manual compaction at level-1 from '!tables.results!xe7x4qufBpzLaEby.zzqLG98O4eFQZHp0' @ 1585250 : 1 .. '!tables.results!xe7x4qufBpzLaEby.zzqLG98O4eFQZHp0' @ 0 : 0; will stop at (end)
Binary file not shown.
Binary file not shown.
View File
+1 -1
View File
@@ -1 +1 @@
MANIFEST-000074
MANIFEST-000122
+8 -3
View File
@@ -1,3 +1,8 @@
2026/05/27-23:11:58.427900 7f3ebd7ff6c0 Recovering log #72
2026/05/27-23:11:58.438233 7f3ebd7ff6c0 Delete type=3 #70
2026/05/27-23:11:58.438292 7f3ebd7ff6c0 Delete type=0 #72
2026/06/12-20:52:58.037556 7fc3c3fff6c0 Recovering log #120
2026/06/12-20:52:58.047938 7fc3c3fff6c0 Delete type=3 #118
2026/06/12-20:52:58.048005 7fc3c3fff6c0 Delete type=0 #120
2026/06/12-20:53:22.710451 7fc3c11be6c0 Level-0 table #125: started
2026/06/12-20:53:22.710474 7fc3c11be6c0 Level-0 table #125: 0 bytes OK
2026/06/12-20:53:22.716951 7fc3c11be6c0 Delete type=0 #123
2026/06/12-20:53:22.724368 7fc3c11be6c0 Manual compaction at level-0 from '!items!0ZfAXacF6oWS120o' @ 72057594037927935 : 1 .. '!items!xFUyR7XECD8QJcIw' @ 0 : 0; will stop at (end)
2026/06/12-20:53:22.822717 7fc3c11be6c0 Manual compaction at level-1 from '!items!0ZfAXacF6oWS120o' @ 72057594037927935 : 1 .. '!items!xFUyR7XECD8QJcIw' @ 0 : 0; will stop at (end)
+8 -8
View File
@@ -1,8 +1,8 @@
2026/05/27-14:31:25.650849 7f3e6f7fe6c0 Recovering log #67
2026/05/27-14:31:25.660624 7f3e6f7fe6c0 Delete type=3 #65
2026/05/27-14:31:25.660648 7f3e6f7fe6c0 Delete type=0 #67
2026/05/27-14:33:21.780104 7f3e6effd6c0 Level-0 table #73: started
2026/05/27-14:33:21.780129 7f3e6effd6c0 Level-0 table #73: 0 bytes OK
2026/05/27-14:33:21.786659 7f3e6effd6c0 Delete type=0 #71
2026/05/27-14:33:21.800546 7f3e6effd6c0 Manual compaction at level-0 from '!items!0ZfAXacF6oWS120o' @ 72057594037927935 : 1 .. '!items!xFUyR7XECD8QJcIw' @ 0 : 0; will stop at (end)
2026/05/27-14:33:21.808253 7f3e6effd6c0 Manual compaction at level-1 from '!items!0ZfAXacF6oWS120o' @ 72057594037927935 : 1 .. '!items!xFUyR7XECD8QJcIw' @ 0 : 0; will stop at (end)
2026/06/12-20:51:44.607543 7fc410ffe6c0 Recovering log #116
2026/06/12-20:51:44.622157 7fc410ffe6c0 Delete type=3 #114
2026/06/12-20:51:44.622215 7fc410ffe6c0 Delete type=0 #116
2026/06/12-20:52:05.604539 7fc3c11be6c0 Level-0 table #121: started
2026/06/12-20:52:05.604563 7fc3c11be6c0 Level-0 table #121: 0 bytes OK
2026/06/12-20:52:05.611212 7fc3c11be6c0 Delete type=0 #119
2026/06/12-20:52:05.618870 7fc3c11be6c0 Manual compaction at level-0 from '!items!0ZfAXacF6oWS120o' @ 72057594037927935 : 1 .. '!items!xFUyR7XECD8QJcIw' @ 0 : 0; will stop at (end)
2026/06/12-20:52:05.619129 7fc3c11be6c0 Manual compaction at level-1 from '!items!0ZfAXacF6oWS120o' @ 72057594037927935 : 1 .. '!items!xFUyR7XECD8QJcIw' @ 0 : 0; will stop at (end)
+36 -1
View File
@@ -51,15 +51,36 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
},
};
this._tradeGoods = null;
this._worldNames = {};
if (options.defaultWorld) {
const w = options.defaultWorld;
this._defaultWorld = w;
this._worldNames['pax.uwpDep'] = w.name || '';
this._worldNames['cargo.uwpDep'] = w.name || '';
this._worldNames['trade.uwp'] = w.name || '';
if (w.uwp) this._formData.pax.uwpDep = w.uwp;
if (w.zone) this._formData.pax.zoneDep = w.zone;
if (w.uwp) this._formData.cargo.uwpDep = w.uwp;
if (w.zone) this._formData.cargo.zoneDep = w.zone;
if (w.uwp) this._formData.trade.uwp = w.uwp;
if (w.zone) this._formData.trade.zone = w.zone;
this._activeTab = 'trade';
}
}
async _prepareContext() {
_registerHandlebarsHelpers();
return {
const ctx = {
...this._formData,
activeActor: buildActiveActorContext(),
activeTab: this._activeTab,
};
if (this._defaultWorld) {
ctx.defaultWorldName = this._defaultWorld.name;
ctx.defaultWorldLoc = `${this._defaultWorld.sector || ''} ${this._defaultWorld.hex || ''}`.trim();
}
return ctx;
}
async _onRender(context, options) {
@@ -128,6 +149,12 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
this._bindWorldSearch(html);
// Pré-remplir les champs recherche avec le nom du monde
if (this._defaultWorld?.name) {
html.find('.world-search-widget[data-role="dep"] .world-search-input, .world-block-full .world-search-input')
.val(this._defaultWorld.name);
}
html.on('click', (ev) => {
if (!$(ev.target).closest('.world-search-widget').length) {
html.find('.world-search-results').empty();
@@ -204,6 +231,7 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
$li.on('click', async () => {
$results.empty();
$input.val(w.name);
if (uwpTarget) this._worldNames[uwpTarget] = w.name;
const [detail, coords] = await Promise.all([
fetchWorldDetail(w.sector, w.hex).catch(() => null),
fetchWorldCoordinates(w.sector, w.hex).catch(() => null),
@@ -222,11 +250,13 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
html.find('[name="cargo.uwpDep"]').val(resolvedUwp);
html.find('[name="cargo.zoneDep"]').val(resolvedZone);
$cargoDep.data('coords', coords);
this._worldNames['cargo.uwpDep'] = w.name;
const $tradeWorld = html.find('.world-search-widget[data-uwp-target="trade.uwp"]');
$tradeWorld.find('.world-search-input').val(w.name);
html.find('[name="trade.uwp"]').val(resolvedUwp);
html.find('[name="trade.zone"]').val(resolvedZone);
this._worldNames['trade.uwp'] = w.name;
}
if (parsecsTarget) {
@@ -370,6 +400,8 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
});
if (!result.success) return ui.notifications.error(result.errors.join(' | '));
result.dep = { ...result.dep, name: this._worldNames['pax.uwpDep'] || '' };
result.dest = { ...result.dest, name: this._worldNames['pax.uwpDest'] || '' };
await this._postToChatResult(result);
}
@@ -394,6 +426,8 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
if (!result.success) return ui.notifications.error(result.errors.join(' | '));
result.cargoRevenue = result.lots.reduce((s, l) => s + l.revenue, 0);
result.dep = { ...result.dep, name: this._worldNames['cargo.uwpDep'] || '' };
result.dest = { ...result.dest, name: this._worldNames['cargo.uwpDest'] || '' };
await this._postToChatResult(result);
}
@@ -413,6 +447,7 @@ export class CommerceDialog extends HandlebarsApplicationMixin(ApplicationV2) {
if (!result.success) return ui.notifications.error(result.errors.join(' | '));
result.world = { ...result.world, name: this._worldNames['trade.uwp'] || '' };
this._tradeGoods = result;
const goodsDiv = html.find('.trade-goods-result');
const listDiv = html.find('.trade-goods-list');
+89 -3
View File
@@ -1,5 +1,6 @@
import { formatCredits } from './tradeHelper.js';
import { createNpcActor, generateClientMission, generateEncounter, generateQuickNpc } from './npcHelper.js';
import { createNpcActor, generateClientMission, generateEncounter, generateQuickNpc, formatSigned } from './npcHelper.js';
import { generateAllyEnemy } from './allyEnemyGenerator.js';
import { NPC_RELATIONS } from './data/npcTables.js';
import { generateAndCreateTravellerNpc } from './travellerNpcGenerator.js';
import { generateRandomName } from './data/travellerNpcGenerator.js';
@@ -68,6 +69,13 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
actorName: '',
openCreatedActor: DEFAULT_OPTIONS.openCreatedActor,
},
ae: {
relation: options.relation ?? 'contact',
includeSpecial: true,
createActor: false,
actorName: '',
openCreatedActor: true,
},
};
}
@@ -138,6 +146,12 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
await this._handleTravellerNpc();
});
html.find('[data-action="generate-ally-enemy"]').on('click', async (event) => {
event.preventDefault();
this._readForm(html);
await this._handleAllyEnemy();
});
html.find('[data-action="randomize-name"]').on('click', (event) => {
event.preventDefault();
this._randomizeTravellerName(html);
@@ -205,6 +219,13 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
this._formData.encounter.context = html.find('[name="encounter.context"]').val();
this._formData.encounter.includeFollowUp = html.find('[name="encounter.includeFollowUp"]').is(':checked');
// Données pour l'onglet Alliés/Ennemis
this._formData.ae.relation = html.find('[name="ae.relation"]').val();
this._formData.ae.includeSpecial = html.find('[name="ae.includeSpecial"]').is(':checked');
this._formData.ae.createActor = html.find('[name="ae.createActor"]').is(':checked');
this._formData.ae.actorName = html.find('[name="ae.actorName"]').val();
this._formData.ae.openCreatedActor = html.find('[name="ae.openCreatedActor"]').is(':checked');
// Données pour l'onglet PNJ Détaillé (Traveller)
this._formData.traveller.citizenCategory = html.find('[name="traveller.citizenCategory"]').val();
this._formData.traveller.experience = html.find('[name="traveller.experience"]').val();
@@ -281,6 +302,60 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
}
}
async _handleAllyEnemy() {
const button = $(this.element).find('[data-action="generate-ally-enemy"]');
const originalLabel = button.html();
try {
button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> Génération...');
const result = await generateAllyEnemy(this._formData.ae.relation, {
includeSpecial: this._formData.ae.includeSpecial,
});
if (result.success) {
if (this._formData.ae.createActor) {
const ae = this._formData.ae;
const actorName = ae.actorName?.trim() || `PNJ — ${result.relation.label}`;
const baseActorSystem = game.system?.id === 'mgt2e'
? await (await import('./travellerNpcGenerator.js')).getMgt2eBaseActorSystem()
: null;
const actorData = {
name: actorName,
type: 'npc',
img: 'systems/mgt2e/icons/cargo/passenger-middle.svg',
system: {
settings: foundry.utils.mergeObject(foundry.utils.deepClone(baseActorSystem?.settings ?? {}), {
hideUntrained: true, lockCharacteristics: true,
}),
sophont: foundry.utils.mergeObject(foundry.utils.deepClone(baseActorSystem?.sophont ?? {}), {
age: 18, homeworld: '', profession: result.relation.label,
}),
characteristics: foundry.utils.deepClone(baseActorSystem?.characteristics ?? {}),
hits: foundry.utils.deepClone(baseActorSystem?.hits ?? {}),
skills: foundry.utils.deepClone(baseActorSystem?.skills ?? {}),
},
flags: {
[MODULE_ID]: { generatedAllyEnemy: { relation: result.relation.key } },
},
};
const actor = await Actor.create(actorData, { renderSheet: false });
result.createdActor = { id: actor.id, name: actor.name };
if (ae.openCreatedActor) actor.sheet?.render(true);
ui.notifications.info(`Fiche PNJ créée : ${actor.name}`);
}
await this._postToChatResult(result);
} else {
ui.notifications.error('Erreur lors de la génération de la relation');
}
} catch (error) {
console.error(`${MODULE_ID} | Erreur AE:`, error);
ui.notifications.error(`Erreur: ${error.message}`);
} finally {
button.prop('disabled', false).html(originalLabel);
}
}
_randomizeTravellerName(html) {
const name = generateRandomName(this._formData.traveller.gender);
html.find('[name="traveller.firstName"]').val(name.firstName);
@@ -295,13 +370,15 @@ export class NpcDialog extends HandlebarsApplicationMixin(ApplicationV2) {
async _postToChatResult(data) {
registerHandlebarsHelpers();
// Déterminer quel template utiliser en fonction du type de données
let template = `modules/${MODULE_ID}/templates/npc-result.hbs`;
let resultType = 'npc-result';
if (data.type === 'traveller-npc' || data?.flags?.[MODULE_ID]?.type === 'traveller-npc-result') {
template = `modules/${MODULE_ID}/templates/traveller-npc-result.hbs`;
resultType = 'traveller-npc-result';
} else if (data.type === 'ally-enemy') {
template = `modules/${MODULE_ID}/templates/ally-enemy-result.hbs`;
resultType = 'ally-enemy-result';
}
const html = await foundry.applications.handlebars.renderTemplate(template, data);
@@ -369,4 +446,13 @@ function registerHandlebarsHelpers() {
if (!obj || !key) return '';
return obj[key] !== undefined ? obj[key] : '';
});
const RELATION_LABELS = Object.entries(NPC_RELATIONS).reduce((acc, [key, val]) => {
acc[key] = val.label;
return acc;
}, {});
Handlebars.registerHelper('lookupRelationKey', (key) => RELATION_LABELS[key] || key);
Handlebars.registerHelper('formatSigned', (value) => formatSigned(value));
}
+573
View File
@@ -0,0 +1,573 @@
/**
* MGT2 SectorMapApp
*
* Application interactive affichant une carte Traveller Map dans un IFRAME.
* Les clics sur la carte affichent les détails du monde dans le chat.
*/
import { searchWorlds } from './travellerMapApi.js';
import { TravelDialog } from './travelDialog.js';
const { ApplicationV2 } = foundry.applications.api;
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
export class SectorMapApp extends ApplicationV2 {
static DEFAULT_OPTIONS = {
id: 'mgt2-sector-map',
classes: ['mgt2-sector-map'],
position: { width: 960, height: 720 },
window: { icon: 'fas fa-map', resizable: true, controls: [] },
};
constructor(sector, subsector) {
super();
this._sector = sector;
this._subsector = subsector;
this._handler = null;
this._mapHex = null;
this._searchTimeout = null;
}
get title() {
if (!this._sector) return 'Carte stellaire — Traveller Map';
return this._subsector
? `Sous-secteur ${this._subsector}${this._sector}`
: `Secteur ${this._sector}`;
}
get _mapUrl() {
const base = 'https://travellermap.com';
if (!this._sector) return `${base}/?style=mongoose&hideui=1`;
if (this._subsector) {
return `${base}/?sector=${encodeURIComponent(this._sector)}&subsector=${encodeURIComponent(this._subsector)}&style=mongoose&hideui=1`;
}
let url = `${base}/go/${encodeURIComponent(this._sector)}?style=mongoose`;
if (this._mapHex) {
url += `&hex=${this._mapHex}&scale=512`;
}
return url;
}
/* ───── Rendu ───── */
_replaceHTML(result, config) {
const content = this.element?.querySelector('.window-content');
if (!content) return;
const html = typeof result === 'string' ? result : this._lastHTML;
content.innerHTML = typeof html === 'string' ? html : '';
}
_renderHTML() {
return `<div class="mgt2-sector-map-outer">
<div class="mgt2-sector-map-toolbar">
<div class="mgt2-sector-map-search">
<input type="text" class="mgt2-sector-map-input" placeholder="Rechercher un monde…" autocomplete="off">
<ul class="mgt2-sector-map-results"></ul>
</div>
<span class="mgt2-sector-map-label">${this._sector ? `${this._sector}${this._subsector ? ` — ss.${this._subsector}` : ''}` : 'Toute la carte'}</span>
<span class="mgt2-sector-map-hint">Cliquez sur un hex pour voir les détails du monde</span>
<button type="button" class="mgt2-sector-map-share" title="Partager une image fixe dans le chat"><i class="fas fa-image"></i> Partager</button>
<button type="button" class="mgt2-sector-map-sync" title="Ouvrir la carte interactive chez tous les joueurs"><i class="fas fa-users"></i> Synchroniser</button>
<button type="button" class="mgt2-sector-map-travel" title="Planifier un voyage"><i class="fas fa-route"></i> Voyage</button>
</div>
<iframe
src="${this._mapUrl}"
class="mgt2-sector-map-frame"
allow="clipboard-write"
referrerpolicy="no-referrer">
</iframe>
</div>`;
}
async _onRender(context, options) {
this._listen();
this.element?.querySelector('.mgt2-sector-map-share')?.addEventListener('click', () => {
this._shareMap();
});
this.element?.querySelector('.mgt2-sector-map-sync')?.addEventListener('click', () => {
this._syncAll();
});
this.element?.querySelector('.mgt2-sector-map-travel')?.addEventListener('click', () => {
this._openTravelDialog();
});
const input = this.element?.querySelector('.mgt2-sector-map-input');
const results = this.element?.querySelector('.mgt2-sector-map-results');
if (input && results) {
input.addEventListener('input', () => {
if (this._searchTimeout) clearTimeout(this._searchTimeout);
this._searchTimeout = setTimeout(() => this._doSearch(input, results), 300);
});
input.addEventListener('blur', () => {
setTimeout(() => { results.innerHTML = ''; }, 200);
});
input.addEventListener('focus', () => {
if (input.value.trim().length >= 2) this._doSearch(input, results);
});
}
}
/* ───── Écoute des clics IFRAME ───── */
_listen() {
if (this._handler) return;
this._handler = (event) => {
// Accept messages from travellermap or any origin (for testing)
const d = event.data || {};
const wx = d.x ?? d.location?.x;
const wy = d.y ?? d.location?.y;
if (wx == null || wy == null) return;
const x = Number(wx);
const y = Number(wy);
if (isNaN(x) || isNaN(y)) return;
console.log('SectorMapApp | click at', x, y, 'from', event.origin);
this._onMapClick({x, y}).catch(err => {
console.error('SectorMapApp | click handler failed:', err);
});
};
window.addEventListener('message', this._handler);
}
async _onMapClick(loc) {
const wx = loc?.x;
const wy = loc?.y;
if (wx == null || wy == null) return;
const coordResp = await fetch(
`https://travellermap.com/api/coordinates?x=${wx}&y=${wy}`
);
if (!coordResp.ok) { console.error('SectorMapApp | /api/coordinates failed', coordResp.status); return; }
const coord = await coordResp.json();
const { sx, sy, hx, hy } = coord;
if (sx == null || hx == null || hy == null) { console.error('SectorMapApp | no sx/hx/hy in', coord); return; }
const metaResp = await fetch(
`https://travellermap.com/api/metadata?sx=${sx}&sy=${sy}`
);
if (!metaResp.ok) { console.error('SectorMapApp | /api/metadata failed', metaResp.status); return; }
const meta = await metaResp.json();
const sectorName = meta.Names?.[0]?.Text;
if (!sectorName) { console.error('SectorMapApp | no Names[0].Text in metadata', meta); return; }
const hex = String(hx).padStart(2, '0') + String(hy).padStart(2, '0');
const resp = await fetch(
`https://travellermap.com/data/${encodeURIComponent(sectorName)}/${hex}`
);
if (!resp.ok) { console.error('SectorMapApp | /data failed', resp.status, sectorName, hex); return; }
const data = await resp.json();
const world = data.Worlds?.[0];
if (!world) { console.error('SectorMapApp | no Worlds in data', data); return; }
this._postWorldCard(world);
}
/* ───── Carte de chat ───── */
static _STARPORT = { A:'Excellent', B:'Bon', C:'Routinier', D:'Médiocre', E:'Frontière', X:'Aucun' };
static _SIZE = ['Aucun (Astéroïde)','1 600 km','3 200 km','4 800 km','6 400 km','8 000 km','9 600 km','11 200 km','12 800 km','14 400 km','16 000 km'];
static _ATMO = [
'Aucune (vide)','Trace','Très ténue (polluée)','Très ténue','Ténue (polluée)','Ténue','Standard','Standard (polluée)','Dense','Dense (polluée)',
'Exotique','Corrosive','Insidieuse','','',''];
static _HYDRO = [ '05% (désert)','615%','1625%','2635%','3645%','4655%','5665%','6675%','7685%','8695%','96100%' ];
static _POP = ['','Dizaines','Centaines','Milliers','Dizaines de milliers','Centaines de milliers','Millions','Dizaines de millions','Centaines de millions','Milliards','Dizaines de milliards','','','','','',''];
static _GOV = [
'Aucun','Compagnie / Corporation','Démocratie participative','Oligarchie auto-perpétuée',
'Démocratie représentative','Technocratie féodale','Gouvernement captif / Colonie',
'Balkanisation','Bureaucratie de service civil','Bureaucratie impersonnelle',
'Dictature charismatique','Dictature non-charismatique','Oligarchie charismatique',
'Dictature religieuse','Oligarchie religieuse','Gouvernement tribal'];
static _LAW = [
'Aucune', 'Armes de poing, explosifs, poison','Armes à énergie portatives','Mitrailleuses, armes auto',
'Armes d\'assaut légères, PM','Armes de poing individuelles','Toutes les armes à feu sauf neutralisateur',
'Fusils, neutralisateur','Armes blanches, neutralisateur','Armes hors du domicile','Armes interdites',
'Contrôle rigide','Aucune arme','Contrôle militariste sévère'];
static _TL = [
'Âge de pierre','Âge du bronze/fer','Médiéval','Grandes découvertes','Révolution industrielle',
'Production mécanisée','Ère nucléaire','Pré-stellaire (ère de l\'information)','Propulsion à saut (1re gen)',
'Propulsion à saut-2','Propulsion à saut-3','Propulsion à saut-4','Propulsion à saut-5',
'Propulsion à saut-6','Transporteur','Moyenne stellaire'];
static _hexVal(ch) {
const n = parseInt(ch, 36);
if (isNaN(n)) return -1;
return n;
}
static _uwpDigit(desc, val) {
return `<span class="uwp-dig" title="${desc}">${val}</span>`;
}
static _uwpBreakdown(uwp) {
if (!uwp || uwp.length < 2) return '';
const d = SectorMapApp._hexVal;
const s = d(uwp[0]), sz = d(uwp[1]), a = d(uwp[2]), h = d(uwp[3]);
const p = d(uwp[4]), g = d(uwp[5]), l = d(uwp[6]);
const t = uwp.length > 8 ? d(uwp[8]) : -1;
const lines = [];
const starport = SectorMapApp._STARPORT[uwp[0]];
lines.push(`<tr><td>${uwp[0]}</td><td>Starport</td><td>${starport ?? '—'}</td></tr>`);
if (uwp[1] === 'F' || uwp[1] === 'f') {
lines.push(`<tr><td>${uwp[1]}</td><td>Taille</td><td>Gaz géant</td></tr>`);
} else if (sz >= 0 && sz <= 10) {
const km = SectorMapApp._SIZE[sz];
const grav = sz === 0 ? '0g' : (sz < 10 ? `0.${sz}g` : '1.0g+');
lines.push(`<tr><td>${uwp[1]}</td><td>Taille</td><td>${km} (${grav})</td></tr>`);
}
if (a >= 0 && a <= 15) {
const atmo = SectorMapApp._ATMO[a] ?? '—';
lines.push(`<tr><td>${uwp[2]}</td><td>Atmosphère</td><td>${atmo}</td></tr>`);
}
if (h >= 0 && h <= 10) {
lines.push(`<tr><td>${uwp[3]}</td><td>Hydrosphère</td><td>${SectorMapApp._HYDRO[h]}</td></tr>`);
}
if (p >= 0 && p <= 15) {
lines.push(`<tr><td>${uwp[4]}</td><td>Population</td><td>${SectorMapApp._POP[p] ?? '—'}</td></tr>`);
}
if (g >= 0 && g <= 15) {
lines.push(`<tr><td>${uwp[5]}</td><td>Gouvernement</td><td>${SectorMapApp._GOV[g] ?? '—'}</td></tr>`);
}
if (l >= 0 && l <= 15) {
lines.push(`<tr><td>${uwp[6]}</td><td>Niveau légal</td><td>${SectorMapApp._LAW[l] ?? '—'}</td></tr>`);
}
if (t >= 0 && t <= 15) {
lines.push(`<tr><td>${uwp[8]}</td><td>Technologie</td><td>${SectorMapApp._TL[t] ?? '—'}</td></tr>`);
}
return `<table class="uwp-breakdown"><tbody>${lines.join('')}</tbody></table>`;
}
static _REMARKS_HELP = {
AB:'Anneau (ceinture)', AG:'Agricole', AN:'Site ancien', AS:'Astéroïde',
BA:'Bande astéroïdale', CP:'Sous-secteur capitale', CS:'Colonie',
CX:'Chasseur (Croiseur)', CY:'Colonie', DA:'Déchu', DE:'Désertique',
DI:'Interdit (Diebar)', FL:'Fluides Lo', FO:'Interdit (Forbidden)',
FR:'Gelé (Frozen)', GA:'Jardin (Garden)', HE:'Helios', HI:'Haute population',
HT:'Haute technologie', IC:'Mondes gelés (Ice)', IN:'Industrialisé',
LI:'Faible population', LO:'Faible population (Low)', LT:'Basse technologie (Low Tech)',
MI:'Militaire', MR:'Mine (ressources)', NA:'Non-agricole',
NI:'Non-industrialisé', OC:'Océanique', OX:'Oxydant',
PA:'Pré-agricole (Pre-Agricultural)', PH:'Phosphore',
PO:'Pauvre (Poor)', PR:'Pré-industriel (Pre-Industrial)',
PX:'Prisonnier (exil)', PZ:'Puzzle (énigmatique)',
RE:'Religieux (Religious)', RI:'Riche (Rich)',
SA:'Bande d\'astéroïdes (Satellite)', SC:'Sainte (colonie)',
SL:'Esclavage (Slave)', SO:'Soleil (Sol)', SP:'Désert (Despoiled)',
SR:'Réserve (Reserve)', ST:'Base stellaire', SU:'Secteur capitale',
TR:'Traces (Trace)', TU:'Tucannides', TZ:'Mondes Tz',
UN:'Inhabité (Uninhabited)', VA:'Vide (Vacuum)',
WA:'Monde aquatique (Water)', WT:'Monde d\'eau (Watery)',
};
static _STAR_TYPES = { O:'Bleu (hypergéante)', B:'Bleu-blanc', A:'Blanc', F:'Blanc-jaune', G:'Jaune (naine)', K:'Orange (naine)', M:'Rouge (naine)', L:'Brune', T:'Brune', Y:'Brune' };
static _STAR_CLASS = { 'I':'Supergéante', 'II':'Géante brillante', 'III':'Géante', 'IV':'Sous-géante', 'V':'Naine (séquence principale)', 'VI':'Sous-naine', 'VII':'Naine blanche' };
static _BASES_HELP = {
N:'Base navale', S:'Base scout', W:'Relais', D:'Dépôt naval',
T:'Base TAS', C:'Consulat', P:'Base pirate', R:'Base de réparation',
K:'Base navale (K.)', X:'Relais Xboat',
};
static _NOBILITY = {
B:'Chevalier (Baronet)', C:'Baron', D:'Marquis', E:'Comte', F:'Duc', G:'Archiduc', H:'Empereur',
};
static _IMPORTANCE = {
'-5':'Très mineur', '-4':'Mineur', '-3':'Mineur', '-2':'Très secondaire', '-1':'Secondaire',
'0':'Ordinaire', '1':'Important', '2':'Important', '3':'Très important', '4':'Majeure',
'5':'Majeure', '6':'Capitale',
};
static _foldRow(label, value, detail, titleAttr) {
const valAttr = titleAttr ? ` title="${titleAttr}"` : '';
return `<tr><td colspan="2">
<details>
<summary><span class="fold-label">${label}</span><span class="fold-value"${valAttr}>${value}</span></summary>
<div class="fold-content">${detail}</div>
</details>
</td></tr>`;
}
static _decodeImportance(ix) {
if (!ix) return '';
const m = String(ix).match(/\{?\s*(-?\d+)\s*\}?/);
if (!m) return SectorMapApp._foldRow('Importance', ix, '');
const val = m[1];
const desc = SectorMapApp._IMPORTANCE[val] || '—';
const detail = `<div class="fold-desc">Valeur dimportance économique et stratégique du monde.<br>${val} = ${desc}</div>`;
return SectorMapApp._foldRow('Importance', `{ ${val} } ${desc}`, detail);
}
static _decodeEconomics(ex) {
if (!ex) return '';
let s = String(ex).replace(/[()\s]/g, '');
const m = s.match(/^([\dA-F])([\dA-F])([\dA-F])([+-]\d+)$/i);
if (!m) return SectorMapApp._foldRow('Économie', ex, '');
const res = m[1], lab = m[2], inf = m[3], eff = m[4];
const detail = `<table class="fold-subtable">
<tr><td>Ressources</td><td>${res}</td></tr>
<tr><td>Main-d’œuvre</td><td>${lab}</td></tr>
<tr><td>Infrastructure</td><td>${inf}</td></tr>
<tr><td>Efficacité</td><td>${eff}</td></tr>
</table>`;
return SectorMapApp._foldRow('Économie', `( ${res} ${lab} ${inf} ${eff} )`, detail);
}
static _decodeCulture(cx) {
if (!cx) return '';
const s = String(cx).replace(/[\[\]\s]/g, '');
if (s.length < 4) return SectorMapApp._foldRow('Culture', cx, '');
const h = s[0].toUpperCase(), t = s[1].toUpperCase(), p = s[2].toUpperCase(), a = s[3].toUpperCase();
const detail = `<table class="fold-subtable">
<tr><td>Hétérogénéité</td><td>${h}</td></tr>
<tr><td>Traditionalisme</td><td>${t}</td></tr>
<tr><td>Progressisme</td><td>${p}</td></tr>
<tr><td>Agressivité</td><td>${a}</td></tr>
</table>`;
return SectorMapApp._foldRow('Culture', `[ ${h} ${t} ${p} ${a} ]`, detail);
}
static _decodePopulation(uwp, pbg) {
if (!uwp || uwp.length < 5) return '';
const popUwp = SectorMapApp._hexVal(uwp[4]);
if (popUwp < 0) return '';
const popPbg = pbg ? parseInt(pbg[0], 10) : null;
const multiplier = popPbg != null && !isNaN(popPbg) ? popPbg : 1;
const base = Math.pow(10, popUwp);
const total = multiplier * base;
const fmtBase = `10<sup>${popUwp}</sup>`;
const fmtMult = multiplier;
const fmtTotal = total >= 1e9 ? `${(total / 1e9).toFixed(1)}&nbsp;milliards`
: total >= 1e6 ? `${(total / 1e6).toFixed(1)}&nbsp;millions`
: total >= 1e3 ? `${(total / 1e3).toFixed(0)}&nbsp;000`
: String(total);
const belts = pbg ? parseInt(pbg[1], 10) : null;
const gas = pbg ? parseInt(pbg[2], 10) : null;
const detail = `<div class="fold-desc">Population = multiplicateur (PBG: <b>${popPbg}</b>) × 10<sup>chiffre UWP (${uwp[4]})</sup><br>
Ceintures dastéroïdes : <b>${belts ?? '?'}</b> &nbsp;|&nbsp; Géantes gazeuses : <b>${gas ?? '?'}</b></div>`;
return SectorMapApp._foldRow('Population', `${fmtMult} × ${fmtBase} = ${fmtTotal}`, detail);
}
static _decodeNobility(nob) {
if (!nob) return '';
const titles = [];
for (const ch of nob) {
const desc = SectorMapApp._NOBILITY[ch.toUpperCase()];
if (desc) titles.push(`${ch} (${desc})`);
}
if (!titles.length) return SectorMapApp._foldRow('Noblesse', nob, '');
const detail = `<div class="fold-desc">Titres de noblesse impériale présents sur ce monde.</div>`;
return SectorMapApp._foldRow('Noblesse', titles.join(', '), detail);
}
static _buildWorldCardHTML(w) {
const sector = w.Sector || '';
const hex = w.Hex || '';
const name = w.Name || '—';
const uwp = w.UWP || '???????-?';
const bases = w.Bases || '';
const remarks = w.Remarks || '';
const allegiance = w.Allegiance || '';
const stellar = w.Stellar || '';
const zone = w.Zone || '';
const pbg = w.PBG || '';
const zoneLabel = zone === 'R' ? 'Rouge'
: zone === 'A' ? 'Ambre'
: 'Verte';
const lines = [];
const uwpDetail = SectorMapApp._uwpBreakdown(uwp);
lines.push(SectorMapApp._foldRow('UWP', `<span class="mono">${uwp}</span>`, uwpDetail));
const ixRow = SectorMapApp._decodeImportance(w.Ix);
if (ixRow) lines.push(ixRow);
const exRow = SectorMapApp._decodeEconomics(w.Ex);
if (exRow) lines.push(exRow);
const cxRow = SectorMapApp._decodeCulture(w.Cx);
if (cxRow) lines.push(cxRow);
const popRow = SectorMapApp._decodePopulation(uwp, pbg);
if (popRow) lines.push(popRow);
const nobRow = SectorMapApp._decodeNobility(w.Nobility);
if (nobRow) lines.push(nobRow);
if (bases) {
const bCodes = bases.split(/[\s,;]+/);
const bList = bCodes.map(c => {
const desc = SectorMapApp._BASES_HELP[c.toUpperCase()];
return `<tr><td class="mono">${c}</td><td>${desc || '—'}</td></tr>`;
}).join('');
const bDetail = `<table class="fold-subtable"><tbody>${bList}</tbody></table>`;
lines.push(SectorMapApp._foldRow('Bases', bases, bDetail));
}
if (remarks) {
const rCodes = remarks.split(/[\s,;]+/);
const rList = rCodes.map(c => {
const desc = SectorMapApp._REMARKS_HELP[c.toUpperCase()];
return `<tr><td class="mono">${c}</td><td>${desc || '—'}</td></tr>`;
}).join('');
const rDetail = `<table class="fold-subtable"><tbody>${rList}</tbody></table>`;
lines.push(SectorMapApp._foldRow('Remarques', remarks, rDetail));
}
if (allegiance) {
const allegFull = w.AllegianceName || '';
const aDetail = `<div class="fold-desc">${allegFull || allegiance}</div>`;
lines.push(SectorMapApp._foldRow('Allégeance', allegFull ? `${allegiance} (${allegFull})` : allegiance, aDetail));
}
if (stellar) {
const sList = [];
let remaining = stellar.trim();
const reStar = /^([OBAFGKMLTY])(\d)\s*(VII|VI|V|IV|III|II|I)\s*/i;
while (remaining) {
const m = remaining.match(reStar);
if (m) {
const type = SectorMapApp._STAR_TYPES[m[1].toUpperCase()] || m[1];
const cls = SectorMapApp._STAR_CLASS[m[3]] || m[3];
sList.push(`<tr><td class="mono">${m[1]}${m[2]} ${m[3]}</td><td>${type} · ${cls}</td></tr>`);
remaining = remaining.slice(m[0].length).trimStart();
} else {
const next = remaining.indexOf(' ');
if (next < 0) break;
remaining = remaining.slice(next + 1).trimStart();
}
}
const sDetail = sList.length ? `<table class="fold-subtable"><tbody>${sList.join('')}</tbody></table>` : `<div class="fold-desc">${stellar}</div>`;
lines.push(SectorMapApp._foldRow('Étoile', `<span class="mono">${stellar}</span>`, sDetail));
}
return `<section class="mgt2-world-card">
<div class="mgt2-world-card-header">
<span class="mgt2-world-name">${name}</span>
<span class="mgt2-world-hex">${sector} ${hex}</span>
<span class="mgt2-world-zone zone-${zone.toLowerCase() || 'g'}">${zoneLabel}</span>
</div>
<table class="mgt2-world-card-body"><tbody>${lines.join('')}</tbody></table>
<div class="mgt2-world-card-actions">
<a class="mgt2-world-commerce" data-sector="${this._escapeAttr(sector)}" data-hex="${hex}" data-uwp="${uwp}" data-zone="${zone || 'normal'}" data-name="${this._escapeAttr(name)}">
<i class="fas fa-balance-scale"></i> Commerce
</a>
</div>
</section>`;
}
static _escapeAttr(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
_postWorldCard(w) {
const html = SectorMapApp._buildWorldCardHTML(w);
ChatMessage.create({
content: html,
whisper: [game.user.id],
});
}
/* ───── Recherche de monde ───── */
async _doSearch(input, results) {
const query = input.value.trim();
if (query.length < 2) { results.innerHTML = ''; return; }
const worlds = await searchWorlds(query);
if (!worlds.length) {
results.innerHTML = '<li class="no-result">Aucun monde trouvé</li>';
return;
}
results.innerHTML = worlds.slice(0, 12).map(w =>
`<li data-sector="${w.sector}" data-hex="${w.hex}" data-name="${w.name}">
<span class="world-name">${w.name}</span>
<span class="world-uwp">${w.uwp}</span>
<span class="world-sector">${w.sector}</span>
</li>`
).join('');
results.querySelectorAll('li[data-sector]').forEach(li => {
li.addEventListener('click', () => this._selectWorld(li, input, results));
});
}
async _selectWorld(li, input, results) {
const sector = li.dataset.sector;
const hex = li.dataset.hex;
const name = li.dataset.name;
results.innerHTML = '';
input.value = name;
this._sector = sector;
this._subsector = null;
this._mapHex = hex;
const iframe = this.element?.querySelector('.mgt2-sector-map-frame');
if (iframe) iframe.src = this._mapUrl;
ui.notifications.info(`Carte centrée sur ${name} (${sector} ${hex})`);
}
/* ───── Partage ───── */
_shareMap() {
if (!this._sector) {
ui.notifications.warn('Aucun secteur sélectionné — utilisez la recherche pour centrer sur un secteur');
return;
}
const posterUrl = `https://travellermap.com/api/poster?sector=${encodeURIComponent(this._sector)}${this._subsector ? `&subsector=${encodeURIComponent(this._subsector)}` : ''}&style=mongoose&scale=128&dpr=2`;
const label = this._subsector
? `Sous-secteur ${this._subsector}${this._sector}`
: `Secteur ${this._sector}`;
const html = `<section class="mgt2-shared-map">
<div class="mgt2-world-card-header">
<span class="mgt2-world-name">${label}</span>
</div>
<div class="mgt2-shared-map-image">
<img src="${posterUrl}" alt="${label}">
</div>
<div class="mgt2-shared-map-footer">
<a href="https://travellermap.com/go/${encodeURIComponent(this._sector)}" target="_blank" rel="noopener">
Ouvrir sur Traveller Map <i class="fas fa-external-link-alt"></i>
</a>
</div>
</section>`;
ChatMessage.create({ content: html, rollMode: 'public' });
ui.notifications.info(`Carte partagée avec les joueurs`);
}
_syncAll() {
game.socket.emit(`module.${MODULE_ID}`, {
type: 'sectorMapSync',
sector: this._sector,
subsector: this._subsector,
});
ui.notifications.info(`Carte synchronisée chez tous les joueurs`);
}
_openTravelDialog() {
const existing = Object.values(ui.windows).find(w => w.id === 'mgt2-travel-dialog');
if (existing) { existing.bringToTop(); return; }
const dialog = new TravelDialog();
dialog.render({ force: true });
}
/* ───── Nettoyage ───── */
close() {
if (this._handler) {
window.removeEventListener('message', this._handler);
this._handler = null;
}
return super.close();
}
}
+277
View File
@@ -0,0 +1,277 @@
import { NPC_RELATIONS } from './data/npcTables.js';
import {
RELATION_FORMULAS,
AFFINITY_INIMITY_MAP,
POWER_INFLUENCE_MAP,
AFFINITY_LABELS,
INIMITY_LABELS,
POWER_LABELS,
INFLUENCE_LABELS,
SPECIAL_CHARACTERISTICS_TABLE,
} from './data/allyEnemyTables.js';
export function mapRollToValue(roll, mapping) {
return mapping[roll] ?? 0;
}
export function getLabel(value, labels) {
return labels.find(l => l.value === Math.abs(value)) ?? labels[0];
}
export function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
async function rollFormula(formula) {
const roll = await new Roll(formula).evaluate();
return { formula, total: roll.total };
}
function getD66Entry(entries, total) {
return entries.find(e => e.d66 === total) ?? null;
}
async function rollD66(entries) {
const tens = await rollFormula('1d6');
const ones = await rollFormula('1d6');
const total = (tens.total * 10) + ones.total;
return {
total,
tens: tens.total,
ones: ones.total,
entry: getD66Entry(entries, total),
};
}
async function rollAffinityInimity(relationKey) {
const formulas = RELATION_FORMULAS[relationKey];
let affinityRoll = null;
let inimityRoll = null;
if (formulas.affinity !== '0') {
affinityRoll = await rollFormula(formulas.affinity);
}
if (formulas.inimity !== '0') {
inimityRoll = await rollFormula(formulas.inimity);
}
return {
affinityValue: affinityRoll ? mapRollToValue(affinityRoll.total, AFFINITY_INIMITY_MAP) : 0,
inimityValue: inimityRoll ? mapRollToValue(inimityRoll.total, AFFINITY_INIMITY_MAP) : 0,
affinityRoll,
inimityRoll,
formulas,
};
}
async function resolveSpecialCharacteristics(currentRelationKey, depth = 0) {
if (depth > 5) return [];
const d66Result = await rollD66(SPECIAL_CHARACTERISTICS_TABLE);
if (!d66Result.entry) return [];
const entry = d66Result.entry;
const result = {
d66: d66Result.total,
text: entry.text,
effects: entry.effects,
appliedDeltas: { affinity: 0, inimity: 0, power: 0, influence: 0 },
rerollNote: null,
swapNote: null,
narrativeText: entry.effects.action === 'narrativeOnly' ? entry.text : null,
newRelationKey: null,
subCharacteristics: [],
};
if (entry.effects.affinityMod) result.appliedDeltas.affinity = entry.effects.affinityMod;
if (entry.effects.inimityMod) result.appliedDeltas.inimity = entry.effects.inimityMod;
if (entry.effects.powerMod) result.appliedDeltas.power = entry.effects.powerMod;
if (entry.effects.influenceMod) result.appliedDeltas.influence = entry.effects.influenceMod;
if (entry.effects.action === 'extraRolls') {
const count = entry.effects.actionValue || 1;
for (let i = 0; i < count; i++) {
const extra = await resolveSpecialCharacteristics(currentRelationKey, depth + 1);
result.subCharacteristics.push(...extra);
}
}
return result;
}
export async function generateAllyEnemy(relationKey = 'contact', options = {}) {
const relation = NPC_RELATIONS[relationKey];
let currentRelationKey = relationKey;
const initial = await rollAffinityInimity(relationKey);
let affinityValue = initial.affinityValue;
let inimityValue = initial.inimityValue;
let affinityRoll = initial.affinityRoll;
let inimityRoll = initial.inimityRoll;
let currentFormulas = initial.formulas;
const powerRoll = await rollFormula('2d6');
const influenceRoll = await rollFormula('2d6');
let powerValue = mapRollToValue(powerRoll.total, POWER_INFLUENCE_MAP);
let influenceValue = mapRollToValue(influenceRoll.total, POWER_INFLUENCE_MAP);
let specialRoll = null;
let specialCharacteristics = [];
if (options.includeSpecial !== false) {
specialRoll = await rollFormula('2d6');
if (specialRoll.total >= 8) {
let queue = await resolveSpecialCharacteristics(currentRelationKey);
while (queue.length > 0) {
const sc = queue.shift();
if (sc.effects.action === 'extraRolls') {
queue.push(...sc.subCharacteristics);
}
if (sc.effects.action === 'moderateRelation') {
if (currentRelationKey === 'enemy') {
currentRelationKey = 'rival';
const rerolled = await rollAffinityInimity(currentRelationKey);
affinityValue = rerolled.affinityValue;
inimityValue = rerolled.inimityValue;
affinityRoll = rerolled.affinityRoll;
inimityRoll = rerolled.inimityRoll;
currentFormulas = rerolled.formulas;
sc.newRelationKey = currentRelationKey;
} else if (currentRelationKey === 'ally') {
currentRelationKey = 'contact';
const rerolled = await rollAffinityInimity(currentRelationKey);
affinityValue = rerolled.affinityValue;
inimityValue = rerolled.inimityValue;
affinityRoll = rerolled.affinityRoll;
inimityRoll = rerolled.inimityRoll;
currentFormulas = rerolled.formulas;
sc.newRelationKey = currentRelationKey;
}
}
if (sc.effects.action === 'intensifyRelation') {
if (currentRelationKey === 'rival') {
currentRelationKey = 'enemy';
const rerolled = await rollAffinityInimity(currentRelationKey);
affinityValue = rerolled.affinityValue;
inimityValue = rerolled.inimityValue;
affinityRoll = rerolled.affinityRoll;
inimityRoll = rerolled.inimityRoll;
currentFormulas = rerolled.formulas;
sc.newRelationKey = currentRelationKey;
} else if (currentRelationKey === 'contact') {
currentRelationKey = 'ally';
const rerolled = await rollAffinityInimity(currentRelationKey);
affinityValue = rerolled.affinityValue;
inimityValue = rerolled.inimityValue;
affinityRoll = rerolled.affinityRoll;
inimityRoll = rerolled.inimityRoll;
currentFormulas = rerolled.formulas;
sc.newRelationKey = currentRelationKey;
}
}
if (sc.effects.action === 'reRollAffinity') {
const reroll = await rollFormula('2d6');
const rerolledValue = mapRollToValue(reroll.total, AFFINITY_INIMITY_MAP);
if (rerolledValue > affinityValue) {
sc.rerollNote = `Affinité relancée : ${reroll.total}${rerolledValue} (était ${affinityValue})`;
sc.appliedDeltas.affinity = rerolledValue - affinityValue;
}
}
if (sc.effects.action === 'reRollInimity') {
const reroll = await rollFormula('2d6');
const rerolledValue = mapRollToValue(reroll.total, AFFINITY_INIMITY_MAP);
if (rerolledValue > inimityValue) {
sc.rerollNote = `Inimitié relancée : ${reroll.total}${rerolledValue} (était ${inimityValue})`;
sc.appliedDeltas.inimity = rerolledValue - inimityValue;
}
}
if (sc.effects.action === 'swapAffinityInimity') {
sc.swapNote = 'Affinité et Inimitié échangées';
const tmpAff = affinityValue;
const tmpInim = inimityValue;
sc.appliedDeltas.affinity = tmpInim - affinityValue;
sc.appliedDeltas.inimity = tmpAff - inimityValue;
}
if (sc.effects.action === 'setPowerToZero') {
sc.appliedDeltas.power = -powerValue;
}
if (sc.effects.action === 'createEnemy') {
sc.narrativeText = 'Un nouvel Ennemi commun au Voyageur et à cet individu est créé.';
}
if (sc.effects.action === 'createContactOrRival') {
const net = affinityValue - inimityValue;
sc.narrativeText = net > 0
? 'Un nouveau Contact est créé (Affinité supérieure à l\'Inimitié).'
: 'Un nouveau Rival est créé (Inimitié supérieure à l\'Affinité).';
}
let newAffinity = affinityValue + (sc.appliedDeltas.affinity || 0);
let newInimity = inimityValue + (sc.appliedDeltas.inimity || 0);
let newPower = powerValue + (sc.appliedDeltas.power || 0);
let newInfluence = influenceValue + (sc.appliedDeltas.influence || 0);
affinityValue = clamp(newAffinity, 0, 6);
inimityValue = clamp(newInimity, 0, 6);
powerValue = clamp(newPower, 0, 6);
influenceValue = clamp(newInfluence, 0, 6);
specialCharacteristics.push(sc);
}
}
}
const finalRelation = currentRelationKey !== relationKey
? NPC_RELATIONS[currentRelationKey]
: relation;
const netScore = affinityValue - inimityValue;
return {
success: true,
type: 'ally-enemy',
relation: { key: currentRelationKey, label: finalRelation.label, summary: finalRelation.summary },
originalRelationKey: relationKey,
relationChanged: currentRelationKey !== relationKey,
affinity: {
formula: currentFormulas.affinity,
roll: affinityRoll?.total ?? 0,
value: affinityValue,
label: getLabel(affinityValue, AFFINITY_LABELS).label,
description: getLabel(affinityValue, AFFINITY_LABELS).description,
},
inimity: {
formula: currentFormulas.inimity,
roll: inimityRoll?.total ?? 0,
value: inimityValue,
label: getLabel(inimityValue, INIMITY_LABELS).label,
description: getLabel(inimityValue, INIMITY_LABELS).description,
},
netScore,
power: {
value: powerValue,
label: getLabel(powerValue, POWER_LABELS).label,
description: getLabel(powerValue, POWER_LABELS).description,
},
influence: {
value: influenceValue,
label: getLabel(influenceValue, INFLUENCE_LABELS).label,
description: getLabel(influenceValue, INFLUENCE_LABELS).description,
},
specialRoll: specialRoll ? { roll: specialRoll.total, triggered: specialRoll.total >= 8 } : null,
specialCharacteristics,
};
}
+95
View File
@@ -0,0 +1,95 @@
export const RELATION_FORMULAS = {
ally: { affinity: '2d6', inimity: '0' },
contact: { affinity: '1d6+1', inimity: '1d6-1' },
rival: { affinity: '1d6-1', inimity: '1d6+1' },
enemy: { affinity: '0', inimity: '2d6' },
};
export const AFFINITY_INIMITY_MAP = {
2: 0, 3: 1, 4: 1, 5: 2, 6: 2,
7: 3, 8: 3, 9: 4, 10: 4, 11: 5, 12: 6,
};
export const POWER_INFLUENCE_MAP = {
2: 0, 3: 0, 4: 0, 5: 0,
6: 1, 7: 1, 8: 2, 9: 3, 10: 4, 11: 5, 12: 6,
};
export const AFFINITY_LABELS = [
{ value: 0, label: 'Aucune', description: 'Aucune affinité envers le Voyageur. Peut être un ennemi ou quelqu\'un d\'indifférent selon son Inimitié.' },
{ value: 1, label: 'Vaguement bienveillant', description: 'Bienveillance comparable à celle d\'un inconnu ordinaire. Petits gestes d\'entraide par courtoisie.' },
{ value: 2, label: 'Bienveillant', description: 'Aidera probablement le Voyageur si simple et sans danger, même sans récompense.' },
{ value: 3, label: 'Très bienveillant', description: 'N\'hésitera pas à prendre des risques modérés ou à offrir son aide de son propre chef.' },
{ value: 4, label: 'Ami loyal', description: 'Fera presque tout son possible pour aider, mais peut être retenu par d\'autres loyautés.' },
{ value: 5, label: 'Amour', description: 'Passera très probablement les intérêts du Voyageur avant les siens ou ceux d\'autrui.' },
{ value: 6, label: 'Fanatique', description: 'Fera tout ce que le Voyageur exige, quels que soient les risques.' },
];
export const INIMITY_LABELS = [
{ value: 0, label: 'Aucune', description: 'Aucune inimitié envers le Voyageur.' },
{ value: 1, label: 'Méfiant', description: 'Vaguement mal disposé mais ne fera pas d\'efforts particuliers pour faire obstacle.' },
{ value: 2, label: 'Malveillant', description: 'Peut commettre des actes de malveillance mineurs par pure mesquinerie.' },
{ value: 3, label: 'Très malveillant', description: 'Se donnera du mal pour faire obstacle au Voyageur par simple rancune.' },
{ value: 4, label: 'Haine', description: 'Fera presque tout pour avoir le dessus sur le Voyageur.' },
{ value: 5, label: 'Haine farouche', description: 'Complotera activement ou prendra de grands risques pour nuire au Voyageur.' },
{ value: 6, label: 'Haine aveugle', description: 'Peut s\'engager dans des actions autodestructrices pour nuire au Voyageur.' },
];
export const POWER_LABELS = [
{ value: 0, label: 'Négligeable', description: 'Ne dispose pratiquement d\'aucune ressource mobilisable en dehors de ses possessions personnelles.' },
{ value: 1, label: 'Faible', description: 'Quelques amis ou contacts. Équivalent d\'un groupe de Voyageurs typique.' },
{ value: 2, label: 'Utile', description: 'Possède un atout majeur : petit vaisseau, unité de mercenaires, équipe d\'avocats.' },
{ value: 3, label: 'Modérément puissant', description: 'Ressources très importantes : unité de mercenaires ou entreprise de taille moyenne.' },
{ value: 4, label: 'Puissant', description: 'Atouts majeurs : compagnie de transport marchand ou grand groupe commercial.' },
{ value: 5, label: 'Très puissant', description: 'Pouvoir colossal : haute sphère gouvernementale ou PDG d\'une grande compagnie.' },
{ value: 6, label: 'Acteur majeur', description: 'Pèse sur la politique interstellaire : amiral ou haut dignitaire.' },
];
export const INFLUENCE_LABELS = [
{ value: 0, label: 'Aucune influence', description: 'N\'a pratiquement aucune influence sur qui que ce soit.' },
{ value: 1, label: 'Faible influence', description: 'Peut faire jouer quelques faveurs auprès de fonctionnaires mineurs.' },
{ value: 2, label: 'Influence modérée', description: 'A un ou plusieurs notables locaux « dans la poche ».' },
{ value: 3, label: 'Influent', description: 'Exerce une influence sur des gens de pouvoir (fonctionnaires, négociants).' },
{ value: 4, label: 'Très influent', description: 'Influence interplanétaire, personnalités gouvernementales ou figures de la pègre.' },
{ value: 5, label: 'Extrêmement influent', description: 'Influence interstellaire, pression sur les législateurs.' },
{ value: 6, label: 'Incontournable', description: 'A l\'oreille de personnes extrêmement puissantes (noble dirigeant le sous-secteur).' },
];
export const SPECIAL_CHARACTERISTICS_TABLE = [
{ d66: 11, text: 'Cet individu a des raisons de pardonner au Voyageur ou de l\'apprécier plus que d\'ordinaire.', effects: { affinityMod: 1 } },
{ d66: 12, text: 'Les relations entre le Voyageur et cet individu se sont particulièrement détériorées.', effects: { inimityMod: 1, affinityMod: -1 } },
{ d66: 13, text: 'Un événement a altéré la relation entre le Voyageur et cet associé.', effects: { affinityMod: 1, inimityMod: -1 } },
{ d66: 14, text: 'Un incident augmente l\'Inimitié entre le Voyageur et cet individu.', effects: { inimityMod: 1 } },
{ d66: 15, text: 'La relation devient plus modérée. Un Ennemi devient un Rival et un Allié devient un Contact. Relancez l\'Affinité et l\'Inimitié.', effects: { action: 'moderateRelation' } },
{ d66: 16, text: 'La relation s\'intensifie. Un Rival devient un Ennemi et un Contact devient un Allié. Relancez l\'Affinité et l\'Inimitié.', effects: { action: 'intensifyRelation' } },
{ d66: 21, text: 'Cet individu gagne en pouvoir.', effects: { powerMod: 1 } },
{ d66: 22, text: 'Cet individu perd une partie de sa base de pouvoir.', effects: { powerMod: -1 } },
{ d66: 23, text: 'Cet individu gagne en influence.', effects: { influenceMod: 1 } },
{ d66: 24, text: 'L\'influence de cet individu diminue.', effects: { influenceMod: -1 } },
{ d66: 25, text: 'Cet individu gagne à la fois en pouvoir et en influence.', effects: { powerMod: 1, influenceMod: 1 } },
{ d66: 26, text: 'Cet individu perd à la fois en pouvoir et en influence.', effects: { powerMod: -1, influenceMod: -1 } },
{ d66: 31, text: 'Cet individu appartient à un groupe culturel ou religieux inhabituel.', effects: { action: 'narrativeOnly' } },
{ d66: 32, text: 'Cet individu appartient à une xéno-espèce rare.', effects: { action: 'narrativeOnly' } },
{ d66: 33, text: 'Cet individu est particulièrement atypique (intelligence artificielle ou entité profondément xéno).', effects: { action: 'narrativeOnly' } },
{ d66: 34, text: 'Cet individu représente en réalité une organisation (mouvement politique, entreprise).', effects: { action: 'narrativeOnly' } },
{ d66: 35, text: 'Cet individu est membre d\'une organisation dont la vision est généralement opposée à celle du Voyageur.', effects: { action: 'narrativeOnly' } },
{ d66: 36, text: 'Cet individu est une figure douteuse (criminel, pirate ou noble déchu). Le Voyageur sera jugé par association.', effects: { action: 'narrativeOnly' } },
{ d66: 41, text: 'Le Voyageur et cet individu se sont violemment brouillés. Relancez l\'Inimitié sur 2D et utilisez le nouveau résultat s\'il est supérieur.', effects: { action: 'reRollInimity' } },
{ d66: 42, text: 'Le Voyageur et cet individu se sont réconciliés. Relancez l\'Affinité sur 2D et appliquez le nouveau résultat s\'il est supérieur.', effects: { action: 'reRollAffinity' } },
{ d66: 43, text: 'Cet individu traverse une période difficile.', effects: { powerMod: -1 } },
{ d66: 44, text: 'Cet individu a été ruiné par un malheur causé par le Voyageur.', effects: { action: 'setPowerToZero', inimityMod: 1 } },
{ d66: 45, text: 'Cet individu a gagné en influence grâce à l\'aide du Voyageur.', effects: { influenceMod: 1, affinityMod: 1 } },
{ d66: 46, text: 'Cet individu a gagné du pouvoir aux dépens d\'un tiers qui blâme désormais le Voyageur.', effects: { powerMod: 1, action: 'createEnemy' } },
{ d66: 51, text: 'Cet individu a disparu dans des circonstances suspectes.', effects: { action: 'narrativeOnly' } },
{ d66: 52, text: 'Cet individu est injoignable, occupé à quelque chose d\'intéressant mais sans caractère suspect.', effects: { action: 'narrativeOnly' } },
{ d66: 53, text: 'Cet individu est en grave difficulté et aurait bien besoin de l\'aide du Voyageur.', effects: { action: 'narrativeOnly' } },
{ d66: 54, text: 'Cet individu a récemment bénéficié d\'une chance insolente.', effects: { action: 'narrativeOnly' } },
{ d66: 55, text: 'Cet individu est incarcéré ou piégé quelque part.', effects: { action: 'narrativeOnly' } },
{ d66: 56, text: 'Cet individu est retrouvé ou déclaré mort. Ce n\'est peut-être pas toute la vérité…', effects: { action: 'narrativeOnly' } },
{ d66: 61, text: 'Cet individu s\'est récemment marié ou a vécu un événement bouleversant sa vie.', effects: { action: 'narrativeOnly' } },
{ d66: 62, text: 'Cet individu a été renié par sa famille, a divorcé ou a vécu un événement tragique.', effects: { action: 'narrativeOnly' } },
{ d66: 63, text: 'Les relations de cet individu commencent à affecter le Voyageur. Créez un nouveau Contact si son Affinité est supérieure à son Inimitié, ou un Rival si l\'Inimitié est supérieure.', effects: { action: 'createContactOrRival' } },
{ d66: 64, text: 'La relation entre le Voyageur et cet associé est complètement redéfinie. Alliés↔Ennemis, Rivaux↔Contacts. Échangez les valeurs d\'Affinité et d\'Inimitié.', effects: { action: 'swapAffinityInimity' } },
{ d66: 65, text: 'Tirez deux autres caractéristiques spéciales.', effects: { action: 'extraRolls', actionValue: 2 } },
{ d66: 66, text: 'Tirez trois autres caractéristiques spéciales.', effects: { action: 'extraRolls', actionValue: 3 } },
];
+1
View File
@@ -37,6 +37,7 @@ Hooks.once('init', () => {
`modules/${MODULE_ID}/templates/npc-result.hbs`,
`modules/${MODULE_ID}/templates/traveller-npc-dialog.hbs`,
`modules/${MODULE_ID}/templates/traveller-npc-result.hbs`,
`modules/${MODULE_ID}/templates/ally-enemy-result.hbs`,
]);
}
+208
View File
@@ -0,0 +1,208 @@
/**
* MGT2 Commandes /sector et /subsector
*
* Ouvre l'application interactive SectorMapApp (IFRAME Traveller Map).
* Compatible Foundry VTT v13 et v14
*/
import { SectorMapApp } from './SectorMapApp.js';
import { postWorldCardToChat } from './worldCard.js';
import { CommerceDialog } from './CommerceDialog.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
const ChatLogV2 = foundry.applications?.sidebar?.tabs?.ChatLog;
let _pendingHandle = false;
/* ───── Fonctions partagées ───── */
async function openMap(sector, subsector) {
const app = new SectorMapApp(sector, subsector);
await app.render({ force: true });
}
async function handleSectorCommand(sector, subsector) {
if (_pendingHandle) return;
_pendingHandle = true;
try {
if (!game.user?.isGM) {
ui.notifications.error('Seul le MJ peut utiliser cette commande');
return;
}
await openMap(sector?.trim());
} catch (err) {
console.error(`${MODULE_ID} | Erreur /sector :`, err);
ui.notifications.error(`Erreur : ${err.message}`);
} finally {
_pendingHandle = false;
}
}
async function handleSystemCommand(sector, hex) {
if (!sector || !hex) {
ui.notifications.warn('Usage : /system <secteur> <hex> (ex: /system "Spinward Marches" 1910)');
return;
}
await postWorldCardToChat(sector, hex);
}
async function handleSubsectorCommand(raw) {
if (_pendingHandle) return;
_pendingHandle = true;
try {
const parts = raw?.trim().split(/\s+/);
if (!parts || parts.length < 2) {
ui.notifications.warn('Usage : /subsector <secteur> <sous-secteur> (ex: /subsector "Spinward Marches" C)');
return;
}
const sub = parts.pop();
const sector = parts.join(' ');
if (!sector || !sub) {
ui.notifications.warn('Usage : /subsector <secteur> <sous-secteur>');
return;
}
if (!game.user?.isGM) {
ui.notifications.error('Seul le MJ peut utiliser cette commande');
return;
}
await openMap(sector.trim(), sub.trim());
} catch (err) {
console.error(`${MODULE_ID} | Erreur /subsector :`, err);
ui.notifications.error(`Erreur : ${err.message}`);
} finally {
_pendingHandle = false;
}
}
/* ───── Commande /sector ───── */
if (ChatLogV2?.CHAT_COMMANDS) {
ChatLogV2.CHAT_COMMANDS['sector'] = {
rgx: /^\/sector(?:\s+(.*))?$/i,
fn: function() {
const raw = arguments[1]?.[1]?.trim?.();
handleSectorCommand(raw || undefined);
return false;
},
};
console.log(`${MODULE_ID} | Commande /sector enregistrée`);
ChatLogV2.CHAT_COMMANDS['subsector'] = {
rgx: /^\/subsector(?:\s+(.*))?$/i,
fn: function() {
const raw = arguments[1]?.[1]?.trim?.();
if (raw) {
handleSubsectorCommand(raw);
return false;
}
return true;
},
};
console.log(`${MODULE_ID} | Commande /subsector enregistrée`);
ChatLogV2.CHAT_COMMANDS['system'] = {
rgx: /^\/system\s+(.+?)\s+(\d{4})\s*$/i,
fn: function() {
const sector = arguments[1]?.[1]?.trim?.();
const hex = arguments[1]?.[2]?.trim?.();
if (sector && hex) {
handleSystemCommand(sector, hex);
return false;
}
return true;
},
};
console.log(`${MODULE_ID} | Commande /system enregistrée`);
}
/* ───── Hooks de secours (v13 / fallback v14) ───── */
Hooks.on('preCreateChatMessage', (message, data, options) => {
const c = message.content?.trim();
let m = c?.match(/^\/sector(?:\s+(.*))?$/i);
if (m) {
handleSectorCommand(m[1]?.trim());
return false;
}
m = c?.match(/^\/subsector(?:\s+(.*))?$/i);
if (m) {
handleSubsectorCommand(m[1]?.trim());
return false;
}
m = c?.match(/^\/system\s+(.+?)\s+(\d{4})\s*$/i);
if (m) {
handleSystemCommand(m[1]?.trim(), m[2]?.trim());
return false;
}
});
/* ───── Socket (synchronisation MJ → joueurs) ───── */
Hooks.once('ready', () => {
game.socket.on(`module.${MODULE_ID}`, (data) => {
if (data?.type !== 'sectorMapSync') return;
if (game.user?.isGM) return;
openMap(data.sector, data.subsector);
});
// Clics sur les liens de monde dans les journals de trajet
document.addEventListener('click', (event) => {
const link = event.target.closest('.mgt2-world-link');
if (!link) return;
event.preventDefault();
const sector = link.dataset.sector;
const hex = link.dataset.hex;
if (sector && hex) {
handleSystemCommand(sector, hex);
}
});
// Clics sur le bouton Commerce dans les cartes de monde
document.addEventListener('click', (event) => {
const btn = event.target.closest('.mgt2-world-commerce');
if (!btn) return;
event.preventDefault();
const uwp = btn.dataset.uwp;
const zone = btn.dataset.zone;
const name = btn.dataset.name;
const sector = btn.dataset.sector;
const hex = btn.dataset.hex;
if (uwp) {
const existing = Object.values(ui.windows).find(w => w.id === 'mgt2-commerce');
if (existing) { existing.bringToTop(); return; }
const dialog = new CommerceDialog({
defaultWorld: { uwp, zone, name, sector, hex },
initialTab: 'trade',
});
dialog.render({ force: true });
}
});
});
Hooks.on('chatMessage', (...args) => {
let msg;
if (args[0]?.content !== undefined) msg = args[0].content;
else if (typeof args[1] === 'string') msg = args[1];
else return;
let m = msg?.trim()?.match(/^\/sector(?:\s+(.*))?$/i);
if (m) {
handleSectorCommand(m[1]?.trim());
return false;
}
m = msg?.trim()?.match(/^\/subsector(?:\s+(.*))?$/i);
if (m) {
handleSubsectorCommand(m[1]?.trim());
return false;
}
m = msg?.trim()?.match(/^\/system\s+(.+?)\s+(\d{4})\s*$/i);
if (m) {
handleSystemCommand(m[1]?.trim(), m[2]?.trim());
return false;
}
});
+161
View File
@@ -0,0 +1,161 @@
import { strict as assert } from 'assert';
import {
RELATION_FORMULAS,
AFFINITY_INIMITY_MAP,
POWER_INFLUENCE_MAP,
AFFINITY_LABELS,
INIMITY_LABELS,
POWER_LABELS,
INFLUENCE_LABELS,
SPECIAL_CHARACTERISTICS_TABLE,
} from '../data/allyEnemyTables.js';
import { mapRollToValue, getLabel, clamp } from '../allyEnemyGenerator.js';
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
passed++;
console.log(` PASS ${name}`);
} catch (e) {
failed++;
console.error(` FAIL ${name}\n ${e.message}`);
}
}
function assertEqual(actual, expected, msg) {
assert.strictEqual(actual, expected, msg || `expected ${expected}, got ${actual}`);
}
// ──────────────────────────────────────
console.log('\nmapRollToValue');
// ──────────────────────────────────────
test('maps 2→0', () => assertEqual(mapRollToValue(2, AFFINITY_INIMITY_MAP), 0));
test('maps 3→1', () => assertEqual(mapRollToValue(3, AFFINITY_INIMITY_MAP), 1));
test('maps 5→2', () => assertEqual(mapRollToValue(5, AFFINITY_INIMITY_MAP), 2));
test('maps 7→3', () => assertEqual(mapRollToValue(7, AFFINITY_INIMITY_MAP), 3));
test('maps 9→4', () => assertEqual(mapRollToValue(9, AFFINITY_INIMITY_MAP), 4));
test('maps 11→5', () => assertEqual(mapRollToValue(11, AFFINITY_INIMITY_MAP), 5));
test('maps 12→6', () => assertEqual(mapRollToValue(12, AFFINITY_INIMITY_MAP), 6));
test('unknown roll → 0', () => assertEqual(mapRollToValue(13, AFFINITY_INIMITY_MAP), 0));
test('power 2-5→0', () => { assertEqual(mapRollToValue(2, POWER_INFLUENCE_MAP), 0); assertEqual(mapRollToValue(5, POWER_INFLUENCE_MAP), 0); });
test('power 6-7→1', () => { assertEqual(mapRollToValue(6, POWER_INFLUENCE_MAP), 1); assertEqual(mapRollToValue(7, POWER_INFLUENCE_MAP), 1); });
test('power 8→2', () => assertEqual(mapRollToValue(8, POWER_INFLUENCE_MAP), 2));
test('power 9→3', () => assertEqual(mapRollToValue(9, POWER_INFLUENCE_MAP), 3));
test('power 10→4', () => assertEqual(mapRollToValue(10, POWER_INFLUENCE_MAP), 4));
test('power 11→5', () => assertEqual(mapRollToValue(11, POWER_INFLUENCE_MAP), 5));
test('power 12→6', () => assertEqual(mapRollToValue(12, POWER_INFLUENCE_MAP), 6));
// ──────────────────────────────────────
console.log('\ngetLabel');
// ──────────────────────────────────────
test('finds matching affinity label', () => {
assertEqual(getLabel(3, AFFINITY_LABELS).label, 'Très bienveillant');
});
test('returns first for out-of-range', () => {
assertEqual(getLabel(99, AFFINITY_LABELS).label, 'Aucune');
});
test('finds inimity label', () => {
assertEqual(getLabel(4, INIMITY_LABELS).label, 'Haine');
});
test('finds power label', () => {
assertEqual(getLabel(5, POWER_LABELS).label, 'Très puissant');
});
test('finds influence label', () => {
assertEqual(getLabel(2, INFLUENCE_LABELS).label, 'Influence modérée');
});
// ──────────────────────────────────────
console.log('\nclamp');
// ──────────────────────────────────────
test('within range', () => assertEqual(clamp(3, 0, 6), 3));
test('below min', () => assertEqual(clamp(-1, 0, 6), 0));
test('above max', () => assertEqual(clamp(7, 0, 6), 6));
test('edge min', () => assertEqual(clamp(0, 0, 6), 0));
test('edge max', () => assertEqual(clamp(6, 0, 6), 6));
// ──────────────────────────────────────
console.log('\nRELATION_FORMULAS');
// ──────────────────────────────────────
test('ally: 2d6 affinity, 0 inimity', () => {
assertEqual(RELATION_FORMULAS.ally.affinity, '2d6');
assertEqual(RELATION_FORMULAS.ally.inimity, '0');
});
test('contact: 1d6+1 affinity, 1d6-1 inimity', () => {
assertEqual(RELATION_FORMULAS.contact.affinity, '1d6+1');
assertEqual(RELATION_FORMULAS.contact.inimity, '1d6-1');
});
test('rival: 1d6-1 affinity, 1d6+1 inimity', () => {
assertEqual(RELATION_FORMULAS.rival.affinity, '1d6-1');
assertEqual(RELATION_FORMULAS.rival.inimity, '1d6+1');
});
test('enemy: 0 affinity, 2d6 inimity', () => {
assertEqual(RELATION_FORMULAS.enemy.affinity, '0');
assertEqual(RELATION_FORMULAS.enemy.inimity, '2d6');
});
// ──────────────────────────────────────
console.log('\nLABELS — array lengths');
// ──────────────────────────────────────
test('AFFINITY_LABELS has 7 entries', () => assertEqual(AFFINITY_LABELS.length, 7));
test('INIMITY_LABELS has 7 entries', () => assertEqual(INIMITY_LABELS.length, 7));
test('POWER_LABELS has 7 entries', () => assertEqual(POWER_LABELS.length, 7));
test('INFLUENCE_LABELS has 7 entries', () => assertEqual(INFLUENCE_LABELS.length, 7));
// ──────────────────────────────────────
console.log('\nSPECIAL_CHARACTERISTICS_TABLE');
// ──────────────────────────────────────
test('has 36 D66 entries', () => assertEqual(SPECIAL_CHARACTERISTICS_TABLE.length, 36));
test('all entries have valid D66 range', () => {
for (const e of SPECIAL_CHARACTERISTICS_TABLE) {
if (e.d66 < 11 || e.d66 > 66) throw new Error(`entry d66=${e.d66} out of range`);
if (!e.text) throw new Error(`entry d66=${e.d66} missing text`);
if (!e.effects) throw new Error(`entry d66=${e.d66} missing effects`);
}
});
test('D66 65 is extraRolls 2', () => {
const e = SPECIAL_CHARACTERISTICS_TABLE.find(x => x.d66 === 65);
assertEqual(e.effects.action, 'extraRolls');
assertEqual(e.effects.actionValue, 2);
});
test('D66 66 is extraRolls 3', () => {
const e = SPECIAL_CHARACTERISTICS_TABLE.find(x => x.d66 === 66);
assertEqual(e.effects.action, 'extraRolls');
assertEqual(e.effects.actionValue, 3);
});
test('D66 11 has affinityMod 1', () => {
const e = SPECIAL_CHARACTERISTICS_TABLE.find(x => x.d66 === 11);
assertEqual(e.effects.affinityMod, 1);
});
test('D66 44 has setPowerToZero + inimityMod 1', () => {
const e = SPECIAL_CHARACTERISTICS_TABLE.find(x => x.d66 === 44);
assertEqual(e.effects.action, 'setPowerToZero');
assertEqual(e.effects.inimityMod, 1);
});
// ──────────────────────────────────────
console.log('\n');
// ──────────────────────────────────────
if (failed > 0) {
console.error(`\n ${failed} of ${passed + failed} tests FAILED\n`);
process.exit(1);
} else {
console.log(` All ${passed} tests passed\n`);
}
+274
View File
@@ -0,0 +1,274 @@
import { searchWorlds, calcParsecs } from './travellerMapApi.js';
const MODULE_ID = 'mgt2-compendium-amiral-denisov';
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
function cleanSectorName(sector) {
return sector?.replace(/\s*\([^)]*\)\s*$/, '').trim() || sector;
}
function worldCoord(sx, sy, hx, hy) {
return { x: (sx - 0) * 32 + (hx - 1), y: (sy - 0) * 40 + (hy - 40) };
}
export class TravelDialog extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
id: 'mgt2-travel-dialog',
classes: ['mgt2-travel-dialog'],
position: { width: 600, height: 500 },
window: { icon: 'fas fa-route', title: 'Planificateur de voyage', resizable: true, controls: [] },
};
static PARTS = {
main: {
template: `modules/${MODULE_ID}/templates/travel-dialog.hbs`,
},
};
constructor() {
super();
this._fromWorld = null;
this._toWorld = null;
this._lastWorlds = null;
this._lastSegments = null;
}
_onRender(context, options) {
this._wireSearch('from');
this._wireSearch('to');
this.element?.querySelector('[data-action="calculate"]')?.addEventListener('click', () => {
this._calculateRoute();
});
this.element?.querySelector('[data-action="create-journal"]')?.addEventListener('click', () => {
this._createJournal();
});
}
_wireSearch(prefix) {
const input = this.element.querySelector(`[name="travel-${prefix}"]`);
const results = this.element.querySelector(`.travel-${prefix}-results`);
if (!input || !results) return;
let timeout = null;
input.addEventListener('input', () => {
clearTimeout(timeout);
const q = input.value.trim();
if (q.length < 2) { results.innerHTML = ''; return; }
timeout = setTimeout(async () => {
const worlds = await searchWorlds(q);
if (!worlds?.length) {
results.innerHTML = '<li class="no-result">Aucun résultat</li>';
return;
}
results.innerHTML = worlds.slice(0, 10).map(w =>
`<li data-sector="${w.sector}" data-hex="${w.hex}" data-name="${w.name}">
<span class="world-name">${w.name}</span>
<span class="world-sector">${w.sector}</span>
<span class="world-hex">${w.hex}</span>
</li>`
).join('');
}, 300);
});
results.addEventListener('click', (e) => {
const li = e.target.closest('[data-sector]');
if (!li) return;
const data = { sector: li.dataset.sector, hex: li.dataset.hex, name: li.dataset.name };
if (prefix === 'from') this._fromWorld = data;
else this._toWorld = data;
input.value = `${data.name} (${data.sector} ${data.hex})`;
results.innerHTML = '';
});
input.addEventListener('blur', () => {
setTimeout(() => { results.innerHTML = ''; }, 200);
});
}
async _calculateRoute() {
if (!this._fromWorld || !this._toWorld) {
ui.notifications.warn('Veuillez sélectionner un monde de départ et un monde d\'arrivée');
return;
}
const jumpEl = this.element.querySelector('[name="travel-jump"]');
const jump = parseInt(jumpEl?.value, 10) || 2;
const resultsEl = this.element.querySelector('.travel-results');
if (!resultsEl) return;
resultsEl.innerHTML = '<div class="travel-loading"><i class="fas fa-spinner fa-spin"></i> Calcul de l\'itinéraire…</div>';
const startSector = cleanSectorName(this._fromWorld.sector);
const endSector = cleanSectorName(this._toWorld.sector);
const startLoc = `${startSector} ${this._fromWorld.hex}`;
const endLoc = `${endSector} ${this._toWorld.hex}`;
try {
const resp = await fetch(
`https://travellermap.com/api/route?start=${encodeURIComponent(startLoc)}&end=${encodeURIComponent(endLoc)}&jump=${jump}`
);
if (!resp.ok) {
if (resp.status === 404) {
const text = await resp.text().catch(() => 'Aucun itinéraire trouvé');
resultsEl.innerHTML = `<div class="travel-error">${this._escapeHtml(text)}</div>`;
} else {
resultsEl.innerHTML = `<div class="travel-error">Erreur API (${resp.status})</div>`;
}
return;
}
const data = await resp.json();
if (!Array.isArray(data) || data.length < 2) {
resultsEl.innerHTML = '<div class="travel-error">Aucun itinéraire trouvé</div>';
return;
}
this._displayRoute(data, resultsEl);
} catch (err) {
console.error('TravelDialog | Erreur:', err);
resultsEl.innerHTML = `<div class="travel-error">Erreur : ${this._escapeHtml(err.message)}</div>`;
}
}
_displayRoute(worlds, resultsEl) {
this._lastWorlds = worlds;
const segments = [];
let totalParsecs = 0;
for (let i = 0; i < worlds.length - 1; i++) {
const a = worlds[i];
const b = worlds[i + 1];
const fromC = worldCoord(a.SectorX, a.SectorY, a.HexX, a.HexY);
const toC = worldCoord(b.SectorX, b.SectorY, b.HexX, b.HexY);
const dist = calcParsecs(fromC, toC);
totalParsecs += dist;
segments.push({ from: a, to: b, dist });
}
this._lastSegments = segments;
let html = `<div class="travel-route-summary">
<i class="fas fa-route"></i>
<strong>${segments.length}</strong> saut${segments.length > 1 ? 's' : ''}
· <strong>${totalParsecs}</strong> parsecs
</div>`;
html += `<div class="travel-route-duration">
<i class="fas fa-clock"></i> Durée estimée : <strong>${segments.length}</strong> semaine${segments.length > 1 ? 's' : ''}
</div>`;
html += '<ol class="travel-jump-list">';
segments.forEach((seg) => {
const f = seg.from;
const t = seg.to;
html += `<li>
<div class="jump-segment">
<div class="jump-world jump-from">
<span class="jump-world-name">${f.Name || '?'}</span>
<span class="jump-world-detail">${f.Sector} ${f.Hex || ''}</span>
</div>
<div class="jump-arrow"><i class="fas fa-long-arrow-alt-right"></i></div>
<div class="jump-world jump-to">
<span class="jump-world-name">${t.Name || '?'}</span>
<span class="jump-world-detail">${t.Sector} ${t.Hex || ''}</span>
</div>
<div class="jump-distance">Saut-${seg.dist}</div>
</div>
</li>`;
});
html += '</ol>';
resultsEl.innerHTML = html;
const journalBtn = this.element?.querySelector('.travel-journal-actions');
if (journalBtn) journalBtn.style.display = 'block';
}
async _createJournal() {
const worlds = this._lastWorlds;
const segments = this._lastSegments;
if (!worlds?.length || !segments?.length) {
ui.notifications.warn('Calculez d\'abord un itinéraire');
return;
}
const from = worlds[0];
const to = worlds[worlds.length - 1];
const totalParsecs = segments.reduce((s, seg) => s + seg.dist, 0);
const totalJumps = segments.length;
const jumpRating = this.element?.querySelector('[name="travel-jump"]')?.value || '?';
const lines = [];
lines.push(`<h2>Journal de voyage</h2>`);
lines.push(`<p><strong>Départ :</strong> ${this._worldLink(from)}</p>`);
lines.push(`<p><strong>Destination :</strong> ${this._worldLink(to)}</p>`);
lines.push(`<p><strong>Moteur :</strong> J-${jumpRating}</p>`);
lines.push(`<hr>`);
lines.push(`<h3>Itinéraire (${totalJumps} saut${totalJumps > 1 ? 's' : ''}, ${totalParsecs} pc)</h3>`);
lines.push(`<table><thead><tr><th>#</th><th>Départ</th><th>→</th><th>Arrivée</th><th>Distance</th></tr></thead><tbody>`);
segments.forEach((seg, i) => {
const f = seg.from;
const t = seg.to;
lines.push(`<tr>
<td>${i + 1}</td>
<td>${this._worldLink(f)}</td>
<td>→</td>
<td>${this._worldLink(t)}</td>
<td>${seg.dist} pc</td>
</tr>`);
});
lines.push(`</tbody></table>`);
lines.push(`<hr>`);
lines.push(`<h3>Mondes visités</h3>`);
lines.push(`<ul>`);
const seen = new Set();
for (const w of worlds) {
const key = `${w.Sector}|${w.Hex}`;
if (seen.has(key)) continue;
seen.add(key);
lines.push(`<li>${this._worldLink(w)}${w.UWP ? ` — UWP: ${w.UWP}` : ''}</li>`);
}
lines.push(`</ul>`);
const content = lines.join('\n');
try {
const journal = await JournalEntry.create({
name: `Voyage : ${from.Name || '?'}${to.Name || '?'}`,
pages: [{
name: 'Itinéraire',
type: 'text',
text: { content, format: 2 },
}],
});
if (journal) {
ui.notifications.info(`Journal créé : ${journal.name}`);
journal.sheet?.render(true);
}
} catch (err) {
console.error('TravelDialog | Erreur création journal:', err);
ui.notifications.error('Erreur lors de la création du journal');
}
}
_worldLink(w) {
const name = w.Name || '?';
const sector = w.Sector || '';
const hex = w.Hex || '';
return `<a class="mgt2-world-link" data-sector="${this._escapeAttr(sector)}" data-hex="${this._escapeAttr(hex)}">${name}</a> <em>(${sector} ${hex})</em>`;
}
_escapeAttr(str) {
if (!str) return '';
return str.replace(/"/g, '&quot;').replace(/&/g, '&amp;');
}
_escapeHtml(str) {
if (!str) return '';
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
}
+46
View File
@@ -0,0 +1,46 @@
/**
* Fonctions réutilisables pour afficher une carte de monde dans le chat.
*/
import { SectorMapApp } from './SectorMapApp.js';
const BASE_URL = 'https://travellermap.com';
/**
* Récupère les données d'un monde via l'API et poste une carte détaillée dans le chat.
* @param {string} sector Nom du secteur
* @param {string} hex Code hex sur 4 chiffres
* @param {object} [options] { whisper: bool }
*/
export async function postWorldCardToChat(sector, hex, options = {}) {
if (!sector || !hex) {
ui.notifications.error('Secteur et hex requis');
return;
}
const url = `${BASE_URL}/data/${encodeURIComponent(sector)}/${encodeURIComponent(hex)}`;
let resp;
try {
resp = await fetch(url);
} catch (err) {
console.error('worldCard | fetch error:', err);
ui.notifications.error('Erreur réseau');
return;
}
if (!resp.ok) {
ui.notifications.error(`Monde introuvable : ${sector} ${hex}`);
return;
}
const data = await resp.json();
const world = data.Worlds?.[0];
if (!world) {
ui.notifications.error(`Aucune donnée pour ${sector} ${hex}`);
return;
}
const html = SectorMapApp._buildWorldCardHTML(world);
const msgData = { content: html };
if (options.whisper !== false) msgData.whisper = [game.user.id];
await ChatMessage.create(msgData);
}
+23
View File
@@ -734,3 +734,26 @@ button.btn-calculate:hover,
vertical-align: super;
margin-left: 2px;
}
/* Bandeau monde sélectionné (depuis carte de chat) */
.world-info-banner {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #e8e0d0;
border: 1px solid #c9a227;
border-radius: 4px;
margin-bottom: 10px;
font-size: 0.95em;
}
.world-info-banner i {
color: #c9a227;
}
.world-info-banner .world-info-loc {
color: #888;
font-size: 0.85em;
}
+784
View File
@@ -329,3 +329,787 @@ button.btn-calculate:hover,
border-color: #a9d0a9;
background: #eef8ee;
}
/* === MGT2 Alliés & Ennemis (result chat) ================================== */
.ae-special-entry {
background: #fbf8f1;
border: 1px solid #d7ccb0;
border-radius: 4px;
padding: 7px 9px;
margin: 6px 0;
font-size: 0.85em;
}
.ae-special-header {
display: flex;
gap: 6px;
align-items: baseline;
font-weight: 500;
color: #222;
}
.ae-special-detail {
margin-top: 4px;
color: #555;
font-size: 0.92em;
padding-left: 2px;
}
.ae-narrative {
font-style: italic;
color: #6a5422;
}
.ae-special-mods {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 5px;
}
.ae-mod {
display: inline-flex;
align-items: center;
padding: 1px 7px;
border-radius: 3px;
font-size: 0.82em;
font-weight: bold;
}
.ae-mod-pos {
background: #dff0d8;
color: #2a6a2a;
border: 1px solid #b8d498;
}
.ae-mod-neg {
background: #fce4e4;
color: #a33;
border: 1px solid #e8b4b4;
}
/* === MGT2 Sector Map ====================================================== */
#mgt2-sector-map .window-content {
padding: 0;
overflow: hidden;
background: #1a1a2e;
}
.mgt2-sector-map-outer {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.mgt2-sector-map-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 12px;
background: #2c2c3e;
border-bottom: 1px solid #c9a227;
flex-shrink: 0;
}
.mgt2-sector-map-label {
color: #d9b24c;
font-weight: bold;
font-size: 0.85em;
}
.mgt2-sector-map-hint {
color: #a99c7a;
font-size: 0.78em;
}
.mgt2-sector-map-search {
position: relative;
flex-shrink: 0;
}
.mgt2-sector-map-input {
width: 180px;
padding: 4px 8px;
font-size: 0.8em;
border: 1px solid #555;
border-radius: 3px;
background: #2c2c3e;
color: #d9b24c;
outline: none;
}
.mgt2-sector-map-input::placeholder {
color: #7a755a;
}
.mgt2-sector-map-input:focus {
border-color: #c9a227;
}
.mgt2-sector-map-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 280px;
overflow-y: auto;
background: #2c2c3e;
border: 1px solid #c9a227;
border-top: none;
border-radius: 0 0 4px 4px;
z-index: 999;
list-style: none;
margin: 0;
padding: 0;
}
.mgt2-sector-map-results li {
padding: 5px 8px;
cursor: pointer;
font-size: 0.78em;
color: #d8c79a;
border-bottom: 1px solid #3a3a50;
display: flex;
gap: 6px;
align-items: baseline;
}
.mgt2-sector-map-results li:last-child {
border-bottom: none;
}
.mgt2-sector-map-results li:hover {
background: #3a3a50;
}
.mgt2-sector-map-results .no-result {
color: #7a755a;
font-style: italic;
cursor: default;
}
.mgt2-sector-map-results .world-name {
font-weight: bold;
color: #d9b24c;
}
.mgt2-sector-map-results .world-uwp {
font-family: 'Courier New', monospace;
font-size: 0.9em;
color: #a99c7a;
}
.mgt2-sector-map-results .world-sector {
margin-left: auto;
color: #7a755a;
font-size: 0.9em;
}
.mgt2-sector-map-share,
.mgt2-sector-map-sync,
.mgt2-sector-map-travel {
background: #c9a227;
color: #1a1a2e;
border: none;
border-radius: 3px;
padding: 4px 12px;
font-size: 0.8em;
font-weight: bold;
cursor: pointer;
flex-shrink: 0;
}
.mgt2-sector-map-share:hover,
.mgt2-sector-map-sync:hover,
.mgt2-sector-map-travel:hover {
background: #d9b24c;
}
.mgt2-sector-map-frame {
width: 100%;
flex: 1;
border: none;
}
/* === MGT2 World Card (chat) ============================================== */
.mgt2-world-card {
background: #f5f0e8;
border: 1px solid #c9a227;
border-radius: 5px;
overflow: hidden;
font-family: 'Signika', sans-serif;
color: #222;
}
.mgt2-world-card-header {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: #2c2c3e;
border-bottom: 2px solid #c9a227;
}
.mgt2-world-name {
font-size: 1.1em;
font-weight: bold;
color: #d9b24c;
}
.mgt2-world-hex {
font-size: 0.78em;
color: #a99c7a;
font-weight: normal;
}
.mgt2-world-zone {
margin-left: auto;
font-size: 0.75em;
font-weight: bold;
padding: 2px 8px;
border-radius: 999px;
text-transform: uppercase;
}
.mgt2-world-zone.zone-g {
background: #2a6a2a;
color: #fff;
}
.mgt2-world-zone.zone-a {
background: #b8860b;
color: #fff;
}
.mgt2-world-zone.zone-r {
background: #8b0000;
color: #fff;
}
.mgt2-world-card-body {
width: 100%;
border-collapse: collapse;
}
.mgt2-world-card-body td {
padding: 0;
border-bottom: 1px solid #e0d8c8;
}
.mgt2-world-card-body .mono {
font-family: 'Courier New', monospace;
letter-spacing: 0.1em;
}
/* === Fold rows (details/summary) ========================================= */
.mgt2-world-card-body details {
display: block;
}
.mgt2-world-card-body summary {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 12px;
cursor: pointer;
list-style: none;
user-select: none;
}
.mgt2-world-card-body summary::-webkit-details-marker {
display: none;
}
.mgt2-world-card-body summary::before {
content: '▸';
color: #c9a227;
font-size: 0.75em;
flex-shrink: 0;
transition: transform 0.15s;
}
.mgt2-world-card-body details[open] summary::before {
transform: rotate(90deg);
}
.mgt2-world-card-body summary:hover {
background: #eae4d4;
}
.mgt2-world-card-actions {
padding: 6px 12px 8px;
text-align: right;
border-top: 1px solid #ddd0bc;
}
.mgt2-world-commerce {
display: inline-block;
padding: 4px 14px;
background: #c9a227;
color: #fff;
border-radius: 3px;
font-size: 0.85em;
font-weight: 600;
cursor: pointer;
text-decoration: none;
transition: background 0.15s;
}
.mgt2-world-commerce:hover {
background: #b89020;
color: #fff;
text-decoration: none;
}
.mgt2-world-commerce i {
margin-right: 4px;
}
.fold-label {
width: 100px;
flex-shrink: 0;
font-weight: bold;
color: #5f4300;
font-size: 0.83em;
}
.fold-value {
font-size: 0.83em;
color: #222;
}
.fold-content {
padding: 4px 12px 8px 28px;
font-size: 0.78em;
color: #555;
background: rgba(201, 162, 39, 0.04);
border-top: 1px solid #ece6da;
}
.fold-desc {
line-height: 1.5;
}
.fold-subtable {
width: 100%;
border-collapse: collapse;
}
.fold-subtable td {
padding: 2px 8px;
font-size: 0.95em;
border: none;
}
.fold-subtable td:first-child {
width: 120px;
font-weight: 600;
color: #5f4300;
}
.fold-subtable td:nth-child(2) {
font-family: 'Courier New', monospace;
color: #7a5c00;
letter-spacing: 0.1em;
}
/* === UWP breakdown inside fold =========================================== */
.uwp-breakdown {
width: 100%;
border-collapse: collapse;
}
.uwp-breakdown td {
padding: 2px 8px;
font-size: 0.95em;
border-bottom: 1px solid #ece6da;
border-top: none;
color: #555;
}
.uwp-breakdown td:first-child {
width: 24px;
font-family: 'Courier New', monospace;
font-weight: bold;
color: #7a5c00;
letter-spacing: 0.05em;
}
.uwp-breakdown td:nth-child(2) {
width: 110px;
font-weight: 600;
color: #5f4300;
}
.uwp-breakdown td:nth-child(3) {
color: #666;
}
.uwp-breakdown tr:last-child td {
border-bottom: none;
}
/* === MGT2 Shared Map (chat) ============================================== */
.mgt2-shared-map {
background: #f5f0e8;
border: 1px solid #c9a227;
border-radius: 5px;
overflow: hidden;
font-family: 'Signika', sans-serif;
color: #222;
}
.mgt2-shared-map .mgt2-sector-map-share {
display: none;
}
.mgt2-shared-map-image {
line-height: 0;
}
.mgt2-shared-map-image img {
display: block;
max-width: 100%;
height: auto;
}
/* ══════════════════════════════════════════════════════
Travel Dialog (planificateur de voyage)
══════════════════════════════════════════════════════ */
#mgt2-travel-dialog .window-content {
background: #f5f0e8;
font-family: 'Signika', sans-serif;
color: #222;
padding: 12px;
}
.travel-form {
display: flex;
flex-direction: column;
gap: 12px;
height: 100%;
}
.travel-worlds {
display: flex;
align-items: flex-end;
gap: 10px;
}
.travel-world-block {
flex: 1;
min-width: 0;
}
.travel-world-block label {
display: block;
font-weight: 600;
margin-bottom: 4px;
font-size: 0.9em;
color: #444;
}
.travel-world-block label i {
margin-right: 4px;
color: #c9a227;
}
.travel-search-widget input {
width: 100%;
padding: 6px 8px;
border: 1px solid #b5a68b;
border-radius: 3px;
background: #fff;
color: #222;
font-size: 0.9em;
box-sizing: border-box;
}
.travel-search-widget input:focus {
outline: none;
border-color: #c9a227;
box-shadow: 0 0 4px rgba(201, 162, 39, 0.4);
}
.travel-search-widget {
position: relative;
}
.travel-from-results,
.travel-to-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 100;
background: #fff;
border: 1px solid #b5a68b;
border-top: none;
max-height: 200px;
overflow-y: auto;
list-style: none;
margin: 0;
padding: 0;
border-radius: 0 0 3px 3px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.travel-from-results li,
.travel-to-results li {
padding: 6px 8px;
cursor: pointer;
display: flex;
gap: 8px;
align-items: baseline;
font-size: 0.85em;
color: #222;
border-bottom: 1px solid #eee;
}
.travel-from-results li:last-child,
.travel-to-results li:last-child {
border-bottom: none;
}
.travel-from-results li:hover,
.travel-to-results li:hover {
background: #e8e0d0;
}
.travel-from-results .no-result,
.travel-to-results .no-result {
color: #888;
cursor: default;
font-style: italic;
}
.travel-from-results .world-name,
.travel-to-results .world-name {
font-weight: 600;
white-space: nowrap;
}
.travel-from-results .world-sector,
.travel-to-results .world-sector {
color: #666;
font-size: 0.85em;
white-space: nowrap;
}
.travel-from-results .world-hex,
.travel-to-results .world-hex {
color: #888;
font-size: 0.8em;
font-family: monospace;
margin-left: auto;
}
.travel-jump-selector {
flex: 0 0 80px;
text-align: center;
}
.travel-jump-selector label {
display: block;
font-weight: 600;
margin-bottom: 4px;
font-size: 0.9em;
color: #444;
}
.travel-jump-selector select {
width: 100%;
padding: 6px;
border: 1px solid #b5a68b;
border-radius: 3px;
background: #fff;
color: #222;
font-size: 0.9em;
text-align: center;
}
.travel-actions {
text-align: center;
}
.travel-actions button {
padding: 8px 24px;
background: #c9a227;
color: #fff;
border: none;
border-radius: 4px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.travel-actions button:hover {
background: #b89020;
}
.travel-actions button i {
margin-right: 6px;
}
.travel-results {
flex: 1;
overflow-y: auto;
min-height: 60px;
border-top: 1px solid #d4c9b8;
padding-top: 12px;
}
.travel-loading {
text-align: center;
color: #888;
padding: 20px;
font-style: italic;
}
.travel-loading i {
margin-right: 8px;
}
.travel-error {
padding: 12px 16px;
background: #fce4e4;
border: 1px solid #e8b4b4;
border-radius: 4px;
color: #a33;
font-size: 0.9em;
}
.travel-route-summary {
padding: 10px 14px;
background: #e4eed4;
border: 1px solid #b8d498;
border-radius: 4px;
font-size: 1em;
margin-bottom: 8px;
}
.travel-route-summary i {
margin-right: 6px;
color: #5a8a2a;
}
.travel-route-duration {
padding: 6px 14px;
font-size: 0.9em;
color: #666;
margin-bottom: 8px;
}
.travel-route-duration i {
margin-right: 6px;
}
.travel-jump-list {
list-style: none;
margin: 0;
padding: 0;
}
.travel-jump-list li {
padding: 6px 0;
border-bottom: 1px solid #e8e0d0;
}
.travel-jump-list li:last-child {
border-bottom: none;
}
.jump-segment {
display: flex;
align-items: center;
gap: 8px;
}
.jump-world {
flex: 1;
min-width: 0;
}
.jump-world-name {
display: block;
font-weight: 600;
font-size: 0.95em;
}
.jump-world-detail {
display: block;
font-size: 0.8em;
color: #888;
}
.jump-to {
text-align: right;
}
.jump-arrow {
flex: 0 0 24px;
text-align: center;
color: #c9a227;
font-size: 1.1em;
}
.jump-distance {
flex: 0 0 60px;
text-align: center;
font-weight: 700;
font-size: 0.9em;
padding: 2px 8px;
background: #eee8d8;
border-radius: 3px;
color: #555;
}
/* Journal de trajet */
.travel-journal-actions {
text-align: center;
padding-top: 8px;
border-top: 1px solid #d4c9b8;
margin-top: 8px;
}
.travel-journal-actions button {
padding: 8px 24px;
background: #5a7a2a;
color: #fff;
border: none;
border-radius: 4px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.travel-journal-actions button:hover {
background: #4a6822;
}
.travel-journal-actions button i {
margin-right: 6px;
}
a.mgt2-world-link {
color: #6a3a8a;
font-weight: 600;
cursor: pointer;
text-decoration: underline;
text-decoration-style: dotted;
}
a.mgt2-world-link:hover {
color: #8a4aaa;
}
+97
View File
@@ -0,0 +1,97 @@
<div class="mgt2-npc-result">
<div class="npc-header">
<h3>
<i class="fas fa-handshake"></i>
{{#if relationChanged}}
{{lookupRelationKey originalRelationKey}}{{relation.label}}
{{else}}
{{relation.label}}
{{/if}}
</h3>
</div>
<div class="npc-pill-row">
<span class="npc-pill">{{relation.label}}</span>
<span class="npc-pill npc-pill-muted">Score net : {{netScore}}</span>
</div>
<div class="npc-card-grid">
<div class="npc-card-block">
<div class="npc-card-title">Affinité ({{affinity.value}}/6)</div>
<div class="npc-roll-line">{{affinity.formula}}{{affinity.roll}}</div>
<div class="npc-card-value">{{affinity.label}}</div>
<div class="npc-subline">{{affinity.description}}</div>
</div>
<div class="npc-card-block">
<div class="npc-card-title">Inimitié ({{inimity.value}}/6)</div>
<div class="npc-roll-line">{{inimity.formula}}{{inimity.roll}}</div>
<div class="npc-card-value">-{{inimity.value}}{{inimity.label}}</div>
<div class="npc-subline">{{inimity.description}}</div>
</div>
</div>
<div class="npc-card-grid">
<div class="npc-card-block">
<div class="npc-card-title">Pouvoir ({{power.value}}/6)</div>
<div class="npc-card-value">{{power.label}}</div>
<div class="npc-subline">{{power.description}}</div>
</div>
<div class="npc-card-block">
<div class="npc-card-title">Influence ({{influence.value}}/6)</div>
<div class="npc-card-value">{{influence.label}}</div>
<div class="npc-subline">{{influence.description}}</div>
</div>
</div>
{{#if specialCharacteristics.length}}
<div class="npc-section">
<div class="npc-section-title">Caractéristiques spéciales</div>
{{#each specialCharacteristics}}
<div class="ae-special-entry">
<div class="ae-special-header">
<span class="npc-inline-roll">D{{d66}}</span>
<span>{{text}}</span>
</div>
{{#if rerollNote}}
<div class="ae-special-detail">{{rerollNote}}</div>
{{/if}}
{{#if swapNote}}
<div class="ae-special-detail">{{swapNote}}</div>
{{/if}}
{{#if narrativeText}}
<div class="ae-special-detail ae-narrative">{{narrativeText}}</div>
{{/if}}
{{#if newRelationKey}}
<div class="ae-special-detail">La relation devient : {{lookupRelationKey newRelationKey}}</div>
{{/if}}
<div class="ae-special-mods">
{{#if appliedDeltas.affinity}}
<span class="ae-mod ae-mod-{{#if (gt appliedDeltas.affinity 0)}}pos{{else}}neg{{/if}}">
Affinité {{formatSigned appliedDeltas.affinity}}
</span>
{{/if}}
{{#if appliedDeltas.inimity}}
<span class="ae-mod ae-mod-{{#if (gt appliedDeltas.inimity 0)}}pos{{else}}neg{{/if}}">
Inimitié {{formatSigned appliedDeltas.inimity}}
</span>
{{/if}}
{{#if appliedDeltas.power}}
<span class="ae-mod ae-mod-{{#if (gt appliedDeltas.power 0)}}pos{{else}}neg{{/if}}">
Pouvoir {{formatSigned appliedDeltas.power}}
</span>
{{/if}}
{{#if appliedDeltas.influence}}
<span class="ae-mod ae-mod-{{#if (gt appliedDeltas.influence 0)}}pos{{else}}neg{{/if}}">
Influence {{formatSigned appliedDeltas.influence}}
</span>
{{/if}}
</div>
</div>
{{/each}}
</div>
{{/if}}
<div class="npc-footer">
<small>Généré par le module {{MODULE_ID}}</small>
</div>
</div>
+8
View File
@@ -273,6 +273,14 @@
<div class="tab {{#if (eq activeTab "trade")}}active{{/if}}" data-tab="trade">
<h3><i class="fas fa-balance-scale"></i> Commerce spéculatif</h3>
{{#if defaultWorldName}}
<div class="world-info-banner">
<i class="fas fa-globe"></i>
<strong>{{defaultWorldName}}</strong>
{{#if defaultWorldLoc}}<span class="world-info-loc">{{defaultWorldLoc}}</span>{{/if}}
</div>
{{/if}}
<div class="world-block world-block-full">
<div class="world-block-title"><i class="fas fa-store"></i> Monde fournisseur</div>
<div class="world-search-widget" data-uwp-target="trade.uwp" data-zone-target="trade.zone">

Some files were not shown because too many files have changed in this diff Show More