# 0.0.7 ## [Fixed] - **Follow Mode stalled after the follower's first step on a gridless scene.** `#buildRoute` used `canvas.grid.measurePath(...).spaces` to walk back `followDistance` grid-spaces from the leader's newest breadcrumb and pick the follower's goal square. `.spaces` is documented core behaviour to always be `0` on a gridless grid, so the loop's `(total - waypoints[i].spaces) < gap` condition read `0 < gap` on its very first iteration and broke before `goalIdx` could ever advance past `0`. After the follower's first real step it ended up standing on that same breadcrumb, `startIdx` became `0` too, the route collapsed to a single point, and `#buildRoute` returned `{route: null, goal: null}` — silently, forever, on every subsequent leader move. The link was never dropped, so it looked like Follow Mode had simply stopped working rather than erroring. - Both call sites in `follow-mode.js` now read `.distance` instead of `.spaces` — `.distance` stays meaningful on square, hex and gridless grids alike — and convert `gap` (still expressed to the user as grid spaces, via the `followDistance` setting) to scene distance units by multiplying by `canvas.grid.distance` before comparing. `#animationDuration` gets the same substitution, for the same reason: its `.spaces` read was also always `0` off-grid, silently flattening every gridless walk to a fixed per-space duration. - On a gridded scene, `distance / canvas.grid.distance` is numerically equivalent to the old `.spaces` count, so behaviour on square and hex scenes is unchanged. ## [Notes] - Verified in a running client: a disposable world with a gridless scene, two tokens linked via Follow Mode, and the leader moved five times in a row — the follower advanced on every step instead of freezing after the first. Repeated on a square-grid scene as a regression check with the same passing result. - The group commands (`PartyCruncher` — Gather/"Get Over Here", Spread/"Get in Position", Save/Load Formation) never called `measurePath` or read `.spaces`, so they were never affected by this bug and needed no change. They have a separate, unrelated characteristic on gridless scenes — their `canvas.grid.size`-based cell math imposes a fixed square "phantom grid" quantization on positioning — which is a pre-existing design trait, not a regression, and is out of scope here. # 0.0.6 ## [Fixed] - **A follower stopped following for good after a few steps, and only on some tables.** Reported by a player following a token they did not own; not reproducible on the maintainer's own map. Two defects compounded, both in `follow-mode.js`. - `#buildRoute` returns `[follower's current position, ...leader breadcrumbs]`. Every segment after the first is ground the leader physically walked, so it is wall-legal by construction — but the **first** is a raw straight line from wherever the follower happens to be standing to the euclidean-nearest breadcrumb, and nothing routes it. `Token#constrainMovementPath` truncates at the first blocking wall, and when the collision point lands within a pixel of the origin it returns a path of length 1. That is precisely the `path.length < 2` case the engine read as "blocked". - That leg was guaranteed to be long right after linking. `#resetTrailForNewLink` truncates the leader's trail to a single point when a link is written, so on the leader's first step the trail holds two breadcrumbs and measures one space. With `followDistance` defaulting to **5**, no breadcrumb sat `gap` spaces back, the goal fell through to the oldest one, and the follower was aimed across up to five squares in one straight line. On an open map that line is clear and the follower simply snapped in behind — which is why it looked correct locally. In a corridor it truncated, and `FOLLOW_STUCK_LIMIT = 2` meant two leader steps later the link was **deleted**. - Aggravating on a real table and absent in a solo test: several tokens on the trail (`followAvoidOverlap` trims occupied goals, leaving the follower off-trail) and a player dragging their own token between leader steps. Both feed the same straight-line reconnect. - **A follow link written by a user who cannot modify the leader lost the leader's breadcrumbs twice.** `follow()` stores `leaderTag: null` when `canUserModify` fails, and the active GM backfills it. That backfill patches the follower again, which re-entered `_onUpdateToken` and fired `#resetTrailForNewLink` a second time. The reset is now gated on a whole-link write (`leaderTokenId` present in the change), so a single-property patch no longer discards the trail. Secondary to the geometry above, but it is the part of the report specific to following someone else's token. ## [Changed] - **A blocked follower is repositioned instead of losing its link.** `#registerBlocked` is gone; nothing in the module deletes a follow link for being stuck any more. `#recoverByDisplace` issues a single waypoint with `action: "displace"` to the goal square instead. - `displace` is the only movement action that ignores geometry outright — its config carries `walls: null`, whereas `blink` omits the key and inherits the `"move"` default, so `blink` would still collide. It is also `measure: false` with a zero cost multiplier and no ruler, so a recovery jump neither consumes a movement budget nor records distance travelled. That matters here, because every ordinary follow step *does* write to the token's movement history. - The destination is always the goal square immediately behind the leader, never an arbitrary point, so a follower can only ever cross a wall *towards* its leader. It will step through a closed door to rejoin; it cannot land in an unrelated room. - A displaced token's own trail is **deleted** rather than seeded with the arrival point. Appending the jump would hand its followers a wall-crossing segment to replay as footsteps; seeding the arrival alone is no better, because with a one-square gap that arrival becomes the next follower's goal — the square this token is landing on — and the chain stacks. An empty trail makes the rest of the chain hold for one step and re-sync on the leader's next, when there are real footsteps again. - **Follow spacing is a fixed setting, read on every step, rather than the spacing measured at activation.** `#measureGap` is gone and `link.gap` is no longer written; `#gap()` reads `followDistance` live, so changing the setting now applies to links that already exist. Links written by 0.0.5 keep working — the stale `gap` field is simply never read, so no migration is needed. - `followDistance` default **5 → 1**, range `0 – 5` → `1 – 5`. At 1 the goal is the square the leader has just vacated, which is always ground the leader covered; the floor of 1 was already enforced in code, so 0 was never reachable. Larger values aim the follower at older breadcrumbs, which is what made the first step after linking pathological. ## [Added] - Setting `followTeleportWhenBlocked` (world, default **on**), on the Follow Mode tab of *Actions & Sounds*. Off makes a cut-off follower wait for a clear line instead of being displaced. Either way the link survives — the setting chooses between catching up and waiting, never between following and not. - `#freeSquareFor`: falls back to the closest free square adjacent to the goal when the goal itself is taken. `#buildRoute` already trims occupied goals, but occupancy can change between planning and the displace, and the trim is skipped entirely when `followAvoidOverlap` is off. Uses `grid.getAdjacentOffsets`, which is hexagon-aware, with a ring of eight points at one grid unit as the gridless fallback. - `Logger.debug` output in `#buildRoute` and `#stepFollower` reporting trail length, start and goal index, gap, route length and the resulting path length — so a user reporting this class of problem can send a console log rather than a description. ## [Removed] - `#registerBlocked`, `#measureGap`, the `#stuck` map and the `FOLLOW_STUCK_LIMIT` constant. - Localization key `follow.blocked`. The event it announced — a follower giving up and dropping its link — no longer exists. ## [Notes] - **Not verified in a running client.** No Foundry client was reachable while this was written. The behaviour was traced through the v14 source (`Token#constrainMovementPath`, `TokenDocument#getCompleteMovementPath`, the movement-action defaults applied in `Game##initializeMovementActions`) and the module's own code paths, and the files parse — but nothing here has been observed running. The open questions are whether the recovery *reads* as following rather than as teleport spam, and whether a long chain squeezing through a doorway triggers it often enough to be distracting. - The recovery is deliberately silent (`Logger.debug` only, no notification), for that second reason. A conga line through a door can trigger it several times in one move. - Two environment conditions still stop Follow Mode dead and are worth ruling out before suspecting code: it runs **only on the active GM's client**, and **only for the scene that client is viewing**; and `followPauseInCombat` is on by default, so nothing follows while an encounter is started. # 0.0.5 ## [Fixed] - **Collecting a Classic Mode group could drag its party token into the map corner.** 0.0.4 anchored the collect on the members' centroid unconditionally. That is the right choice only when the party token is not on the map — and it misfires badly on a group carried over from the pre-stash model, whose members sit hidden at scene origin: the centroid of a stack at `(0, 0)` *is* `(0, 0)`, so the party token was teleported to the top-left corner along with them. The rule is now: a party token that is on the map **and visible** already marks where the group is, so it is the anchor and the members come to it; the centroid is used only when the party token is absent (stashed) or parked out of sight. This also repairs the mirrored legacy state — party token hidden at origin, members visible — where the centroid remains the correct answer. - Supersedes the corresponding `[Fixed]` bullet in 0.0.4, which describes the unconditional centroid. ## [Notes] - **The 0.0.4 migration note was wrong and has been corrected in place.** It told you to delete and recreate every existing group. Tracing the four possible legacy states showed that is unnecessary: a pre-existing group has no stash, so it reads as released, and the first **Toggle** collects it under the new model — members parked hidden at scene origin are un-hidden, flown to the leader, snapshotted and removed. The group repairs itself in one click. Leader Mode is the only shape the module can create (`promptForPartyToken` only offers tokens from the member list), and it needs nothing at all; Classic Mode is a legacy shape, and the anchor fix above covers it. The only lingering artefact is cosmetic: until the first Toggle, the Dashboard and Token HUD label an old collected group as released. The button still performs the correct action. ## [Docs] - README corrections. These shipped inside the 0.0.4 commit but were not recorded in its entry; listed here so the history is complete. Four of them were long-standing factual errors, not fallout from the stash rework: - "Built for **Foundry VTT V13+**" — `module.json` has declared `minimum: "14"` since 0.0.2. - *Get in Position* radius documented as "default: 30" in two places; the actual default in `config.js` is **6**. - A dead link to `API.md`, which was deleted in commit `e221701` (0.0.2) while the link stayed behind. Replaced with a pointer to the `PartyCruncher` and `FollowMode` globals, which is what actually exists. - Two passages describing the pre-stash behaviour ("every other token hides behind it"; Crunch/Explode being kept away from follow links by the `blink`/`displace` filter, when what protects them now is that token ids survive the round trip). - README additions covering what the new model means for a user: that collected members return identical (token id, formation slot, follow links, unlinked-token ActorDelta, initiative), that the movement actions are unavailable while a group is collected, and that a player owning only a member token has no vision while it is collected. # 0.0.4 ## [Changed] - **Collecting a group now removes its member tokens from the scene instead of parking them hidden at scene origin.** Crunch previously flew each member onto the leader and then teleported it to `{x: 0, y: 0}` with `hidden: true`, leaving a stack of ghost tokens in the map's top-left corner — still occupying cells for the overlap test, still counted in the scene, and revealed in full if anything cleared `hidden`. Members are now serialized and deleted; explode recreates them. - The whole change rests on `keepId`. A deleted token frees its `_id`, and `createEmbeddedDocuments(..., {keepId: true})` claims it back (`DatabaseCreateOperation#keepId`). Because ids survive the round trip, **nothing else changed shape**: `memberTokenIds`, saved formations, the `groupId`/`tokenId`/`role` token flags and Follow Mode links all keep working with no translation layer. - Snapshots are taken with `TokenDocument#toObject()`, so an **unlinked token keeps its ActorDelta** — its own HP, name, image and effects — rather than coming back as a fresh copy of the prototype token. - Crunch is now cheaper than it was. The old path ran two `teleport()` calls per token, each with its own `update()`, `move()` and `movementAction` swap; the new one flies the members in and issues a single batched `deleteEmbeddedDocuments`. - `_movementHistory` is deliberately dropped on restore: systems read it as movement already spent this round, and a token that has been off-canvas has not moved. - Group state is now a stored fact rather than an inference. "Is this group crunched?" was derived from `hidden` in four independent places (`context-menu.js`, `dashboard.js` twice, `party-cruncher.js`); all four now call a single `isGroupCrunched()` helper that reads the stash. - Dashboard group status no longer requires the group's scene to be the rendered one — it reads the scene document directly, so the status is also correct immediately after a scene transition, before PIXI placeables exist. - Scene transfer carries the stash to the destination instead of recreating members hidden at `(0,0)`. A group still **arrives collapsed**, as documented before, but now only the leader/party token is actually placed; every other member travels as a snapshot. Stashed members keep their ids across the move, so only the leader's id is remapped. - `Config.deleteGroup()` now clears the deleted group's stash entry from its scene, so a torn-down group cannot leave orphaned snapshots behind. ## [Added] - `scripts/token-stash.js`: the subsystem that takes tokens off a scene and puts them back unchanged. Storage is a flag on the **scene**, keyed by group id: `scene.flags["group-tokens"].stash[groupId] = {state, entries}`, where `state` is `"crunched"` (members are off-canvas) or `"exploded"` (Classic Mode's party token is off-canvas). The scene was chosen over the party token for a concrete reason — in Classic Mode the party token is itself stashable, and a token cannot carry the snapshot of its own deletion — and over the world setting so `Config.getGroups()` does not deserialize 25 token documents on every call. - **Initiative and combatants survive collect/release.** Core deletes a token's Combatants along with the token (`Combat._onDeleteTokens`), taking initiative with them. Each stash entry now carries a `Combatant#toObject()` snapshot per encounter, restored with `keepId` so combatant ids and turn order come back intact. Encounters deleted while the group was stashed are skipped. Combatants are dropped on a cross-scene transfer, which matches the previous behaviour — the old code deleted the origin tokens, which ended those encounters anyway. - `errMsg.groupIsCrunched` / `errMsg.noMembersToCollect`, and a `#requireExploded()` guard on `getOverHere`, `getInPosition`, `killThemAll`, `saveFormation` and `loadFormation`. These operations move member tokens around the map, which is impossible while the members are snapshots; they now say so instead of failing with "Tokens missing in scene". - A cross-scene guard on `toggleParty()`, mirroring the one `findParty()` already had. Crunch and explode read and write the stash on the group's own scene but operate on the rendered canvas, so toggling a group belonging elsewhere would have restored its members into the wrong scene. It now navigates to the group's scene first. ## [Fixed] - **Classic Mode crunched to the map corner.** `explodeParty` parked the party token at `{x: 0, y: 0}`, and `#getTarget` then returned that same token as the crunch anchor — so the next collect gathered the whole group into the top-left corner. Classic Mode now derives its anchor from the members' centroid, and the party token appears centred on the group it replaces. - Same-scene paste (Ctrl+C / Ctrl+V on a group leader) no longer forces GM-hidden members onto the drop point. The special case existed only for crunched tokens parked at scene origin, which no longer exist; every token present now keeps its offset relative to the leader. - Restoring stashed members no longer trips the module's own `preCreateToken` hook. Restored tokens still carry the `groupId` flag, so without a guard they looked exactly like a group token being pasted into a scene and the hook would cancel the creation and start a bogus transfer. - Collecting a group no longer breaks Follow Mode links pointing at a member. `FollowMode._onDeleteToken` clears the links of anyone following a deleted token; it now sits out stash operations, since the token is coming back with the same id. - Two flag deletions were written with the legacy `-=key` syntax, which v14 deprecates in favour of the operator form. They now pass `new foundry.data.operators.ForcedDeletion()` on the dotted path, matching the `#linkDeletion()` convention introduced in 0.0.3. ## [Removed] - `#determineRequiredAction()` and `#getTarget()` from `party-cruncher.js`, and `ContextMenu._isGroupCrunched()`. The stash makes the action unambiguous — members off the scene means release, otherwise collect — so the `hidden`-based state machine has nothing left to decide. - Localization keys `errMsg.cannotDetermineAction` and `errMsg.membersAndPartyAllHidden`, which only existed to report that the old state machine could not tell crunched from exploded. ## [Notes] - **There is no migration, and none is needed for groups the module can actually create.** A pre-existing group has no stash, so it reads as released, and the first **Toggle** collects it under the new model: members parked hidden at scene origin are un-hidden, flown to the leader, snapshotted and removed. The group repairs itself in one click. Only Classic Mode groups — where the party token is *not* one of the members, a shape the current creation flow cannot produce, since `promptForPartyToken` only offers tokens from the member list — carry a stale layout, and the anchor rule above repairs those too. Nothing has to be deleted and recreated. - Ordering inside `stashTokens()` is not negotiable and is enforced with a read-back: the snapshot is written and verified before a single token is deleted. A delete that ran before a failed write would destroy the tokens with nothing to restore from. - A player who owns only a member token has **no vision source while the group is collected**, because their token is genuinely gone. This was already broken under the old model — their vision came from the token parked in the map corner — but it is worth stating plainly. Use Leader Mode with a leader the players own, or grant Observer on the leader. - `Ctrl+Z` can resurrect a stashed token while its snapshot is still on the scene. Restore filters out any `_id` already present rather than colliding on `keepId`. - The daggerheart system's `party` actor has a comparable "clown car" (`module/applications/hud/tokenHUD.mjs`), and it was evaluated as a template. It was not copied: it rebuilds tokens from `actor.getTokenDocument()` rather than from a snapshot, so position, elevation, rotation, `hidden` and the entire ActorDelta of an unlinked token are lost, and every retrieval mints new ids — which this module cannot use, since it indexes everything by token id. # 0.0.3 ## [Added] - **Follow Mode** — a token walks in behind another token, respecting walls. Independent of the group system. Select a token you own, target another token, and toggle the chain button in the Token HUD. Available to players, not only the GM: the follow link is a flag on the follower's own token, so its owner can write it without any GM proxy or socket layer. - Chains are supported (token1 follows token2 follows token3) and animate together rather than one token at a time, because each link plans from the previous link's planned path instead of waiting for its animation. - Circular links are refused at creation by a real graph walk over the follow chain, and a runtime visited-set makes a loop structurally impossible even for a link forged by a macro or restored from a backup. - Links survive a scene change. A module-owned `followTag` flag on the leader is what makes this work: `scene-transfer.js` and Foundry's own copy/paste both rebuild tokens from `toObject()` with the `_id` deleted, so flags survive a transfer but token ids do not. Cached ids are repaired on `canvasReady`; a link whose leader is not on the current scene lies dormant and is never deleted. - Only the active GM's client computes and applies follower movement (`game.users.activeGM.isSelf`), so a table of four players issues each move once, and chain ordering is deterministic. - Followers do not react to `blink`/`displace` moves by default, which keeps a group Crunch/Explode from dragging a chain through walls, and do not react while a group action is running (`PartyCruncher.isBusy()`). - New settings, on a **Follow Mode** tab in the Actions & Sounds dialog: `followEnabled`, `followPlayersAllowed`, `followDistance`, `followSpeed`, `followPauseInCombat`, `followTeleports`, `followAvoidOverlap`, `followMaxChainDepth`. - `scripts/helpers.js`: the shared-helper module `CLAUDE.md` has always specified but which had never been created. Holds grid-agnostic, pixel-space geometry helpers (`centerToSnappedPosition`, `tokenBoundsAt`, `isPositionBlockedByToken`) built on `TokenDocument#getSize` / `#getCenterPoint` / `#getSnappedPosition`, all of which are hexagon-aware — unlike the cell-indexed helpers in `party-executor.js`, which assume a square grid and a 1x1 footprint. ## [Fixed] - A follower jumped onto its leader on the leader's **first** step after the link was made, then behaved correctly once the leader had walked a few more squares. Three independent defects, all of which only surfaced while the breadcrumb trail was short: - The follow distance was measured across the trail slice starting at the follower rather than across the whole trail. When that slice collapsed to a single breadcrumb — which is exactly what a two-point trail does for a follower standing nearer the leader's destination than its origin — the trim was skipped entirely and the leader's own square became the goal. The goal is now chosen by measuring back from the newest breadcrumb over the full trail, and a follower already level with or ahead of that goal holds position instead of being dragged forward. - The overlap back-off loop was guarded by `length > 1`, so a single occupied candidate was never rejected and the route was returned pointing straight at it. It now rejects the move outright when every candidate square is taken. - The cascade used one `Set` for two unrelated jobs — breaking cycles, and exempting tokens from the overlap test — and the leader was in it, so `isPositionBlockedByToken` skipped the one token a follower must never land on. Split into `visited` (cycle guard, includes the leader) and `moving` (overlap exemption, only followers about to vacate their square). The follow distance is additionally floored at one grid space. - A follower replayed breadcrumbs its leader had laid down *before* the link existed, so it set off across the map on the leader's next single step — indistinguishable from moving on its own the moment it was linked. A leader's trail is now truncated to its current position when a new link is written. Truncating rather than timestamping avoids comparing clocks across clients: the link may be written by a player's browser, while the trail only ever exists on the GM's. - Removing a follow link raised a v14 deprecation error (`You are specifying a forced deletion key "-=followLink" using legacy syntax`). The four deletion sites now share a `#linkDeletion()` helper that passes `new foundry.data.operators.ForcedDeletion()` as the value on the dotted path. - `ActionsSettings` buffered a checkbox as the string `"on"` rather than a boolean, because `#onBufferSetting` read `target.value` for every input type. It now reads `target.checked` for checkboxes. No shipped setting was affected — the dialog had no checkboxes until Follow Mode added some. ## [Notes] - Follow Mode replays the leader's traversed waypoints instead of aiming at a computed destination. This is not a stylistic choice: **Foundry v14 core has no pathfinder.** `Token#findMovementPath` is synchronous, its `cancel()` is a no-op, and it delegates to `constrainMovementPath`, which truncates at the first wall rather than routing around it (core's own comment there: *"Compute the path up until the next waypoint that is blocked by a wall"*). It exists as an override point for routing modules. Replaying the squares the leader physically walked is wall-legal by construction, so it needs no pathfinding at all. The module still calls `findMovementPath` as the final validator, so installing a routing module upgrades Follow Mode with no code change; the module deliberately does not override it, which would make it responsible for pathfinding in every ordinary token drag. - `routinglib` was evaluated and rejected: it is `compatibility.verified: "11"` and depends on `canvas.grid.w/h`, `canvas.grid.grid.*`, `canvas.grid.isHex` and `new VisionSource({})`, all removed in v12/v13. # 0.0.2 ## [Fixed] - Visual markers (PIXI badges and borders) now render correctly on Foundry v14, which ships PIXI v8. The legacy v7 Graphics API (`beginFill`, `endFill`, `lineStyle`, `drawCircle`, `drawRoundedRect`) was replaced with the v8 builder pattern (shape method → `fill()` / `stroke()`). `PIXI.Text` construction updated to the v8 options-object form. ## [Added] - `scripts/constants.js`: dependency-free leaf module exporting `MODULE_ID` and the three token flag keys (`FLAG_GROUP_ID`, `FLAG_TOKEN_ID`, `FLAG_ROLE`). All other scripts now import from this single source of truth instead of redeclaring the module id as a local string literal. ## [Changed] - `module.json`: added `"compatibility"` block (`minimum: "14"`, `verified: "14"`) declaring the module as Foundry v14-exclusive; removed duplicate `"authors"` field. # 0.0.1 ## [Fixed] - Group tokens entering a teleport region no longer throw "Failed to create Token in destination Scene". `RegionDocument.teleportToken` is wrapped to catch the error caused by `preCreateToken` cancelling single-token creation for group tokens; the hook's full group transfer handles everything. - After confirming a group transfer triggered by a region teleport, tokens now disappear from the origin scene immediately instead of after an 8-second delay. Root cause: `_waitForScene` was waiting for `canvasReady` on the destination scene, but `createEmbeddedDocuments`/`deleteEmbeddedDocuments` are pure DB operations that don't require canvas readiness. ## [Changed] - Group transfers now create tokens in collapsed (crunched) state: only the leader/party token is visible at the destination; all other members arrive hidden at (0,0). The GM can EXPLODE the group after transfer to spread members. - Ctrl+C / Ctrl+V on a group leader token no longer creates a standalone token — the full group transfer is now triggered correctly. Root cause: `stampGroupFlags` was only writing the `groupId` flag; `tokenId` and `role` flags are now also stamped, allowing the `preCreateToken` hook to identify the leader reliably. - Copy-pasting a group leader token in the **same scene** now repositions the entire group to the drop point instead of creating an orphan token. - Duplicate `setBusy(false)` calls in `toggleParty`, `groupParty`, and `findParty` — the `finally` block already resets busy state; the redundant post-try call has been removed. ## [Changed] - `GroupDataModel` now validates group data in `createGroup()` and `updateGroup()`, catching malformed entries before they reach settings. - All hardcoded English UI strings in Dashboard, context menu, and party prompt are now routed through `Config.localize()` / `Config.format()` for i18n support. - InstructionsMenu layout styles moved from inline JS (`style.setProperty` + `ResizeObserver`) to `instructions.css`, scoped via `#gt-instr-window`. ## [Removed] - Dead localization keys: `setting.modVersion`, `setting.hideChatInfo`, and the `chatInfoContent` block. - Redundant "Cancel" button from the HUD context menu. ## [Changed] - Improved text and button contrast across HUD and party-prompt dialogs; enforced sans-serif font on all buttons and inputs. - Refactored Dashboard from `DialogV2.wait()` to a proper `ApplicationV2` (`HandlebarsApplicationMixin`) singleton, using native action handlers and CSS-driven layout instead of JS-forced styles and `ResizeObserver`. - Refactored party-prompt and scene-transfer dialogs from `DialogV2` to `ApplicationV2`, eliminating duplicate button bars and `.gt-clean-dialog` CSS hacks. ## [Removed] - `forceUniqueTargetToken` setting and its "Behaviour" settings section; all members now always group/ungroup using the party token (leader) position as a fixed anchor. ## [Changed] - Dashboard button in Token HUD context menu is now always visible, regardless of group state. - Increased UI contrast across the context menu: darker button backgrounds with white text, white cancel button, and explicit white actor name. ## [Fixed] - Dashboard singleton now uses a synchronous static lock instead of a DOM check, eliminating race conditions that could open duplicate instances. - Explode spread now accounts for leader and member token sizes (N×N), preventing members from landing inside a large leader's footprint or overlapping each other. ## [Added] - Token HUD context menu now shows role-aware actions: leaders see **Toggle** (plus **Find** only when the group has no members yet); members see **Toggle** and **Find Leader of Group #N**; ungrouped tokens see per-group **Add to Group #N** buttons when groups exist on the scene, and the Dashboard fallback only when no groups are present. - `PartyCruncher.Dashboard()` static method: opens the Group Tokens Dashboard from macros or the browser console. - Zero-argument fallback for `findParty()` and `toggleParty()`: when called without a `groupId`, both methods resolve the first configured group on the current scene and warn if none exists. - `toggleParty()` without argument focuses the `partyTokenId` (leader/anchor token) before executing the toggle, so the GM sees what is about to change. - GM-only enforcement on all public `PartyCruncher` methods (`Dashboard`, `findParty`, `toggleParty`, `groupParty`): non-GMs receive a yellow warning notification and the call returns immediately. - Early yellow warning in `groupParty()` when fewer than 2 tokens are selected and no `preSelectedTokenIds` are provided, preventing the exception previously thrown by `#collectIdsFromTokenSelection`. - Cross-scene navigation for `findParty` and Dashboard `find-member`: if the GM is viewing a different scene than the group's origin, the view automatically switches to the correct scene before selecting tokens and panning the camera. ## [Removed] - `PartyCruncher.healthCheck()` removed; it relied on the blocking `alert()` API and provided no value beyond what the Logger already exposes in the console. ## [Changed] - Refactored `main.js` into four focused files: `party-cruncher.js` (public API), `party-executor.js` (movement engine), `party-prompt.js` (dialog + flags), and `main.js` (bootstrap only). - `scene-transfer.js` now imports `PartyCruncher` directly instead of relying on the `window` global. - Optional dependency state (`optionalDepsAvailable`) and module readiness (`ready`) moved to `Config` as static properties. ## [Changed] - Dashboard width increased to 660px. - Dashboard action buttons (Toggle, Find, Transfer, Clear) no longer close the dialog; only the window X closes it. - Dashboard now shows only the group leader's name per row; member list is revealed via an "Expand" toggle button. - Each member in the expanded list has a delete button to remove them from the group without closing the dashboard. - Added text labels to Find, Transfer, and Clear buttons to fix icon-only rendering issue. - Removed the "New Group" button from the dashboard. ## [Changed] - Complete UI redesign: replaced brown/gold fantasy theme with a clean neutral dark design system using CSS custom properties. - All dialogs now use a single consistent close mechanism — duplicate window-chrome `×` buttons are suppressed via CSS on dialogs that already provide Cancel/Close actions. - Removed redundant in-content header blocks (title + subtitle) from Dashboard, Group Config, Instructions, and Scene Transfer dialogs; the Foundry window chrome title is sufficient. - Replaced decorative gold circle group badges with flat numbered tags. - All button colours now follow semantic intent: blue=Toggle, indigo=Find, green=Create, red=Remove/Clear, amber=Transfer. - Extracted inline `