Compare commits
4 Commits
9d96ec5543
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a53c7ace53 | |||
| efe37b8a96 | |||
| 49423f40f5 | |||
| 9abc2a8b19 |
@@ -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.
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
testMatch: ['**/scripts/tests/*.test.js'],
|
||||
transform: {},
|
||||
};
|
||||
+72
-1
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"type": "module",
|
||||
"private": true
|
||||
}
|
||||
+1
-1
@@ -1 +1 @@
|
||||
MANIFEST-000084
|
||||
MANIFEST-000132
|
||||
|
||||
+8
-3
@@ -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
@@ -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)
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-000183
|
||||
MANIFEST-000231
|
||||
|
||||
+8
-3
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-000067
|
||||
MANIFEST-000115
|
||||
|
||||
+8
-3
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-000186
|
||||
MANIFEST-000234
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-000120
|
||||
MANIFEST-000168
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 @@
|
||||
MANIFEST-000128
|
||||
MANIFEST-000176
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-000120
|
||||
MANIFEST-000168
|
||||
|
||||
+7
-3
@@ -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)
|
||||
|
||||
@@ -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 @@
|
||||
MANIFEST-000101
|
||||
MANIFEST-000149
|
||||
|
||||
+8
-3
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-000128
|
||||
MANIFEST-000176
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
BIN
Binary file not shown.
+1
-1
@@ -1 +1 @@
|
||||
MANIFEST-000120
|
||||
MANIFEST-000168
|
||||
|
||||
+7
-3
@@ -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
@@ -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 @@
|
||||
MANIFEST-000128
|
||||
MANIFEST-000176
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
MANIFEST-000014
|
||||
@@ -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)
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
MANIFEST-000048
|
||||
MANIFEST-000291
|
||||
|
||||
+19
-69
@@ -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
@@ -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.
@@ -1 +1 @@
|
||||
MANIFEST-000074
|
||||
MANIFEST-000122
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Binary file not shown.
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 = [ '0–5% (désert)','6–15%','16–25%','26–35%','36–45%','46–55%','56–65%','66–75%','76–85%','86–95%','96–100%' ];
|
||||
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 d’importance é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)} milliards`
|
||||
: total >= 1e6 ? `${(total / 1e6).toFixed(1)} millions`
|
||||
: total >= 1e3 ? `${(total / 1e3).toFixed(0)} 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 d’astéroïdes : <b>${belts ?? '?'}</b> | 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, '&').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
_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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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 } },
|
||||
];
|
||||
@@ -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`,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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`);
|
||||
}
|
||||
@@ -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, '"').replace(/&/g, '&');
|
||||
}
|
||||
|
||||
_escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
const d = document.createElement('div');
|
||||
d.textContent = str;
|
||||
return d.innerHTML;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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
Reference in New Issue
Block a user