Skip to content

sheet/feat/ageofficial - #112

Merged
BronsonHall merged 5 commits into
Roll20:mainfrom
donbwhite:main
Aug 18, 2026
Merged

sheet/feat/ageofficial#112
BronsonHall merged 5 commits into
Roll20:mainfrom
donbwhite:main

Conversation

@donbwhite

@donbwhite donbwhite commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Submission Checklist

  • The Pull Request title contains the short name of the sheet being submitted.
  • The Pull Request title states the type of change being submitted (New/Update/Bugfix/etc.).
  • I have authorization from the game's publisher to make this an official sheet on Roll20 with their name attached.
  • This game is not a traditionally published game, but a copy of the game rules can be purchased/downloaded/found at: < >
  • This sheet is for an unofficial fan game, modification to an existing game, or a homebrew system.

Changes / Description (optional)

Add legacy Roll20 character import to the AGE sheet

Summary

Adds a legacy character importer to the AGE (Adventure Game Engine) sheet.
It reads the flat Roll20 attribute blob from an old-format character
(repeating_* rows + top-level scalars) and maps it into the Beacon SDK
Pinia stores, so players can migrate existing characters onto the new sheet.

Implemented in src/utility/legacyAdapter.ts, driven by a new Import modal in
App.vue, and gated behind a new Allow Import setting.

What it does

Import UI

  • A slide-out Import tab pinned to the right edge of the sheet. It tucks
    mostly off-screen and slides out on hover/keyboard-focus so it doesn't
    interfere with play. The icon is a CSS mask/background image that themes per
    game system (Fantasy AGE / Blue Rose vs. Modern AGE / Expanse), with a
    screen-reader-only label and an "Import" tooltip.
  • Gated behind a new Allow Import toggle in Sheet Settings (Game System
    row), default off — opt-in per character.
  • Clicking Import opens a modal that pulls from the live character and offers
    per-section checkboxes: Name, Abilities, Character Stats, Biography,
    Game Settings, Talents & Powers, Ability Focuses, Spells (Arcana),
    Equipment & Weapons, Currency.
  • Two apply modes: Append (add on top of existing data) and Overwrite
    (clear the selected sections first, then import). Overwrite only clears the
    sections the user checked.

Mapping (legacyAdapter.ts)

  • Ability scores, character stats (health/magic, armor, base-speed, weapon
    groups, xp, level), biography (ancestry, class, background, physical,
    history, goals), and game settings (game system, caster → show Arcana).
  • Talents & powers, ability focuses (per-ability repeating sections),
    specializations, and ancestry/class → quality items.
  • Spells/arcana → the magic store; equipment and attacks → inventory items
    (a legacy attack implies possession of the weapon, so it becomes an
    inventory weapon).
  • Robust parsing for real-world messiness: inconsistent money formats
    ("18g", "GP"+"6", reversed "10"+"G"), split/ranged attack ranges
    ("4 / 6", "16 yards"), and hyphenated repeating-section names.

Safety

  • A guard skips the import (with a console warning) when the blob has no legacy
    data, so an empty/not-yet-loaded character can never blank the sheet.
  • Persistence flows through the normal relay dehydrate/update cycle; the
    existing updateId check prevents the re-hydrate loop from clobbering an
    import.

Other changes

  • AbilitiesView.vue: the ability display was built from a one-time
    snapshot ref (only refreshed when the abilities modal closed), so external
    store changes never showed. Changed to a computed — a latent reactivity
    bug that the importer surfaced.
  • vitest.config.ts: added the @ alias (needed to unit-test modules that
    use it) and excluded nested .worktrees so the unit suite runs cleanly.
  • settingsStore.ts: new persisted allowImport flag (default false).

Testing

  • src/utility/legacyAdapter.spec.ts — 8 tests covering the flat import
    (warrior + mage sample characters), section selection, overwrite clearing,
    messy money/range parsing, and the empty-blob guard. All pass.
  • npm run type-check is clean for the changed files.

Known limitations / follow-ups

  • Fantasy AGE validated. Field mappings are verified against Fantasy AGE
    characters. Modern AGE / Expanse / Blue Rose legacy field names are untested
    and may import partially.
  • healthMax / magicMax aren't imported (missing from the Roll20 export);
    the player enters the real max after import.
  • No dedup of quality items — messy legacy data (e.g., duplicated talent
    rows) imports verbatim.
  • Imported weapons don't map an ability for attack rolls (default Strength);
    languages, appearance, and computed aggregates (armor penalty/shield bonus)
    are dropped.
  • Pre-existing settingsStore.test.ts asserts an outdated dehydrate() shape
    and remains red (unrelated to this feature).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added legacy character import with selectable sections and append or overwrite options.
    • Added an “Allow Import” setting to control access to character imports.
    • Added support for importing abilities, details, inventory, weapons, spells, currency, and other character data.
  • Bug Fixes
    • Ability scores now update automatically when changes are made elsewhere in the app.
  • Tests
    • Added comprehensive coverage for legacy imports, selective updates, parsing, and invalid data handling.

donbwhite and others added 2 commits August 6, 2026 17:20
Implements the legacy sheet migration adapter (src/utility/legacyAdapter.ts),
converting a flat Roll20 attributes blob (scalars + repeating_* rows) into the
Beacon SDK stores.

Feature:
- Import modal with per-section checkboxes (Abilities, Character Stats,
  Biography, Game Settings, Talents & Powers, Ability Focuses, Spells/Arcana,
  Equipment & Weapons, Currency, Name) and Append/Overwrite modes. Overwrite
  clears only the selected sections; a guard prevents a non-legacy/empty blob
  from blanking the sheet.
- Slide-out Import tab with a game-system-specific masked icon, gated behind a
  new "Allow Import" setting (default off) in the Game System row.
- Robust parsing: mixed money formats, split/ranged attacks -> inventory
  weapons, base-speed, xp from the structured section, casters auto-show Arcana.
  healthMax/magicMax are left at default (missing from the Roll20 export).

Also:
- AbilitiesView: derive the display from a computed instead of a one-time
  snapshot ref so external store changes (imports) reflect immediately.
- vitest.config: add the @ alias and exclude nested .worktrees so the unit
  suite can run.
- Add legacyAdapter.spec.ts covering the flat import, section selection,
  overwrite clearing, money/range parsing, and the empty-blob guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add legacy Roll20 character import to the AGE sheet
@donbwhite
donbwhite requested a review from a team as a code owner August 6, 2026 21:26
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2be4215d-b2fb-4c9b-b883-1ceb90e7b6cb

📥 Commits

Reviewing files that changed from the base of the PR and between 76deb13 and 6ce0007.

📒 Files selected for processing (3)
  • ageofficial/src/App.vue
  • ageofficial/src/utility/legacyAdapter.ts
  • ageofficial/src/views/SettingsView.vue
🚧 Files skipped from review as they are similar to previous changes (3)
  • ageofficial/src/views/SettingsView.vue
  • ageofficial/src/App.vue
  • ageofficial/src/utility/legacyAdapter.ts

📝 Walkthrough

Walkthrough

The PR adds Roll20 legacy character import support with selectable append or overwrite behavior, persistent import settings, and a modal interface. It also makes ability-score rendering reactive and adds Vitest coverage and configuration updates.

Changes

Legacy character import

Layer / File(s) Summary
Legacy data mapping
ageofficial/src/utility/legacyAdapter.ts
The adapter parses flat attributes and repeating rows into character, settings, abilities, qualities, inventory, weapons, spells, and currency data.
Import selection and execution
ageofficial/src/utility/legacyAdapter.ts, ageofficial/src/utility/legacyAdapter.spec.ts, ageofficial/vitest.config.ts
The adapter supports section selection, append and overwrite modes, overwrite clearing, change reporting, invalid-data protection, and import orchestration. Tests cover mapped fields and parsing behavior. Vitest adds the @ alias and excludes .worktrees paths.
Import setting and modal
ageofficial/src/sheet/stores/settings/settingsStore.ts, ageofficial/src/views/SettingsView.vue, ageofficial/src/App.vue
The settings store persists allowImport. SettingsView.vue provides the checkbox. App.vue provides the conditional import button, section controls, modal, and import actions.

Reactive ability scores

Layer / File(s) Summary
Computed ability-score rendering
ageofficial/src/components/abilities/AbilitiesView.vue
The view derives its ability-score array from the store with computed and removes manual refresh logic.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AppVue
  participant importLegacyCharacter
  participant PiniaStores
  AppVue->>importLegacyCharacter: selected sections and import mode
  importLegacyCharacter->>PiniaStores: clear selected overwrite targets
  importLegacyCharacter->>PiniaStores: write mapped legacy data
  importLegacyCharacter-->>AppVue: completion and change report
Loading

Possibly related PRs

Suggested reviewers: bronsonhall, nbrooks-roll20

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title resembles a branch or path name and does not clearly describe the legacy Roll20 character import changes. Replace the title with a concise description such as “Add legacy Roll20 character import support.”
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (5)
ageofficial/src/utility/legacyAdapter.spec.ts (2)

110-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add an assertion for the imported specialization quality level.

The fixture sets "specialization1-degree": "Novice" at Line 25, and importLegacySpecialization lowercases the degree before comparing it. Line 147 checks only the item name. Assert qualityLevel as well so the case-normalization path stays covered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ageofficial/src/utility/legacyAdapter.spec.ts` around lines 110 - 167, Update
the specialization assertion in the “imports Bjordson (warrior)” test to locate
the imported “Berserker” quality and also assert its qualityLevel is the
normalized lowercase value “novice”, preserving the existing name assertion.

100-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore the console spies after each test.

vi.spyOn replaces the global console methods and this suite never restores them. The replacements persist for the rest of the worker process, so an unrelated spec file can lose console output or inherit call counts. Add an afterEach that calls vi.restoreAllMocks(), or set restoreMocks: true in ageofficial/vitest.config.ts.

♻️ Proposed fix
-import { describe, it, expect, beforeEach, vi } from "vitest";
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
     vi.spyOn(console, "info").mockImplementation(() => {});
   });
+
+  afterEach(() => {
+    vi.restoreAllMocks();
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ageofficial/src/utility/legacyAdapter.spec.ts` around lines 100 - 108, Add
cleanup for the console spies created in the legacy adapter suite’s beforeEach
by adding an afterEach that calls vi.restoreAllMocks(). Keep the existing setup
unchanged and ensure each test restores the global console methods and mock
state.
ageofficial/src/utility/legacyAdapter.ts (3)

295-300: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Talent degree parsing accepts fewer formats than the specialization parser.

Lines 299-300 compare talent.talentdegree only to the strings "2" and "3". importLegacySpecialization at Lines 229-234 also accepts the words expert and master, and normalizes case. Legacy talent rows use the same mixed conventions, so a talent stored as "Expert" or numeric 2 imports as novice.

Extract a shared degree-to-quality-level helper and use it in both places.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ageofficial/src/utility/legacyAdapter.ts` around lines 295 - 300, Extract a
shared degree-to-quality-level helper that normalizes case and maps both numeric
and textual values for novice, expert, and master. Replace the local parsing in
the talents loop and the corresponding logic in importLegacySpecialization with
this helper, preserving novice as the fallback for unrecognized values.

698-714: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the sections parameter as ImportSectionKey[].

Line 701 declares sections?: string[] and Line 714 casts to ImportSectionKey[]. The cast hides typos at every call site, and applySection silently ignores unknown keys. Declare the parameter as ImportSectionKey[] so the compiler validates the caller in App.vue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ageofficial/src/utility/legacyAdapter.ts` around lines 698 - 714, Update the
importLegacyCharacter function’s sections parameter from string[] to
ImportSectionKey[], and remove the downstream cast when deriving selected.
Preserve the existing default-section behavior while allowing TypeScript to
validate section keys at all call sites.

716-726: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Two full store serializations run on every import for the debug report only.

Lines 717 and 723 each call sheet.dehydrateStore(), and Line 724 deep-diffs the result. The only consumer is the console report. Also, console.warn at Line 707 and console.info at Line 725 are ungated. Skip the snapshot, the diff, and the logs when logMode is off.

As per coding guidelines: "Production console.log calls must be gated behind a logMode flag or removed before merging; the devRelay stub is an allowed exception."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ageofficial/src/utility/legacyAdapter.ts` around lines 716 - 726, Gate the
import debug-report path on logMode: only call sheet.dehydrateStore before and
after applying sections, diffStates, and logImportReport when logging is
enabled. Also guard the nearby console.warn and completion console.info calls
with the same logMode condition, while leaving the import behavior unchanged
when logging is disabled.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ageofficial/src/App.vue`:
- Around line 221-271: Enhance the import modal anchored by importModalOpen with
dialog semantics: add role="dialog", aria-modal="true", and a stable
aria-labelledby reference connecting the modal to its h3 heading. Implement
open-time focus on the modal, trap Tab/Shift+Tab within it, close on Escape, and
restore focus to the triggering element after cancelImport or confirmImport
closes the dialog.

In `@ageofficial/src/utility/legacyAdapter.ts`:
- Around line 602-621: Update the bio branch of clearSection so it does not
clear bio.gender, since importLegacyBioFlat writes legacy gender to bio.sex
only. Remove the bio.gender reset while preserving all other bio-field resets
and quality cleanup.
- Around line 502-533: Gate all diagnostic output in legacyAdapter.ts behind the
existing logMode flag by adding one shared guarded logging helper and routing
every console call through it. In
ageofficial/src/utility/legacyAdapter.ts:71-108, update loadLegacyAbilityScores,
loadLegacyCharacterDetails, and loadLegacyGroupings to gate or remove their
inspectors, including the raw attribute dump; in :502-533, guard logImportReport
and its group, warnings, tables, and logs; in :716-726, gate console.warn and
console.info and only run sheet.dehydrateStore() and diffStates when logMode is
enabled, since they only support the report.
- Around line 623-625: Update the overwrite handling for the "settings" case in
clearSection and the importLegacySettingsFlat flow so showArcana is explicitly
reset to false before applying imported settings, allowing non-caster legacy
characters to hide the Arcana section.
- Around line 278-289: Update the money-row handling in legacyAdapter’s money
iteration to accumulate amounts per denomination rather than overwrite
inventory.cash[key], preserving existing cash in append mode. In overwrite mode,
ensure clearSection("currency") resets all denominations before processing,
matching the existing reset behavior referenced near the currency-clearing
logic.
- Around line 180-186: Update the speed assignment in the legacy adapter to
write char.speed only when base-speed is present, preserving existing values
during append imports instead of defaulting to 10. Also convert the optional
attributes.character?.character?.xp value to the expected numeric type before
assigning char.xp, while leaving it unchanged when absent.
- Around line 383-398: Update the weapon classification in the attacks mapping
around parseRange so ranged is derived from the parsed shortRange value rather
than raw atk.range truthiness. Preserve the existing parseRange results and set
weaponType to Ranged only when shortRange indicates a valid range; otherwise
classify the weapon as Melee.

In `@ageofficial/src/views/SettingsView.vue`:
- Around line 88-94: Update the Allow Import checkbox markup in SettingsView by
adding a unique id to the input bound to settings.allowImport and assigning the
visible “Allow Import” text to a label with a matching for attribute. Preserve
the existing toggle styling and binding.

---

Nitpick comments:
In `@ageofficial/src/utility/legacyAdapter.spec.ts`:
- Around line 110-167: Update the specialization assertion in the “imports
Bjordson (warrior)” test to locate the imported “Berserker” quality and also
assert its qualityLevel is the normalized lowercase value “novice”, preserving
the existing name assertion.
- Around line 100-108: Add cleanup for the console spies created in the legacy
adapter suite’s beforeEach by adding an afterEach that calls
vi.restoreAllMocks(). Keep the existing setup unchanged and ensure each test
restores the global console methods and mock state.

In `@ageofficial/src/utility/legacyAdapter.ts`:
- Around line 295-300: Extract a shared degree-to-quality-level helper that
normalizes case and maps both numeric and textual values for novice, expert, and
master. Replace the local parsing in the talents loop and the corresponding
logic in importLegacySpecialization with this helper, preserving novice as the
fallback for unrecognized values.
- Around line 698-714: Update the importLegacyCharacter function’s sections
parameter from string[] to ImportSectionKey[], and remove the downstream cast
when deriving selected. Preserve the existing default-section behavior while
allowing TypeScript to validate section keys at all call sites.
- Around line 716-726: Gate the import debug-report path on logMode: only call
sheet.dehydrateStore before and after applying sections, diffStates, and
logImportReport when logging is enabled. Also guard the nearby console.warn and
completion console.info calls with the same logMode condition, while leaving the
import behavior unchanged when logging is disabled.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6d911d0b-571e-479c-afc7-0e5009d9900b

📥 Commits

Reviewing files that changed from the base of the PR and between 588ba54 and 76deb13.

📒 Files selected for processing (7)
  • ageofficial/src/App.vue
  • ageofficial/src/components/abilities/AbilitiesView.vue
  • ageofficial/src/sheet/stores/settings/settingsStore.ts
  • ageofficial/src/utility/legacyAdapter.spec.ts
  • ageofficial/src/utility/legacyAdapter.ts
  • ageofficial/src/views/SettingsView.vue
  • ageofficial/vitest.config.ts

Comment thread ageofficial/src/App.vue
Comment thread ageofficial/src/utility/legacyAdapter.ts
Comment thread ageofficial/src/utility/legacyAdapter.ts
Comment thread ageofficial/src/utility/legacyAdapter.ts
Comment thread ageofficial/src/utility/legacyAdapter.ts
Comment thread ageofficial/src/utility/legacyAdapter.ts
Comment thread ageofficial/src/utility/legacyAdapter.ts
Comment thread ageofficial/src/views/SettingsView.vue
BronsonHall
BronsonHall previously approved these changes Aug 11, 2026
@BronsonHall

Copy link
Copy Markdown
Contributor

Hey @donbwhite

Would you mind going through and either addressing or resolving these coderabbit comments?
Sorry for the extra step, none of them are blocking issues to my eyes, and if the comments are simply resolved without changes this PR will be good to go - just making sure you're aware of them.

@BronsonHall BronsonHall added the blocked Something needs to be addressed with this before it continues label Aug 11, 2026
donbwhite and others added 2 commits August 11, 2026 17:16
Legacy import (src/utility/legacyAdapter.ts):
- Write char.speed only when base-speed is present; keep existing value on
  append instead of clobbering it with the default 10.
- Convert legacy xp through toInt so char.xp is always numeric.
- Accumulate money rows per denomination instead of overwriting, so repeated
  rows and append mode add up.
- Classify weapons Ranged/Melee from the parsed shortRange, not raw range
  truthiness (placeholder "0"/"-" no longer read as ranged).
- Gate all diagnostic console output behind a logMode flag via shared helpers,
  and skip the before/after dehydrate + diff work when logging is off.
- Don't clear bio.gender in overwrite mode; the import maps legacy gender to
  bio.sex only, so clearing gender would delete a user value nothing rewrites.
- Reset settings.showArcana to false in overwrite mode so a non-caster legacy
  character hides the Arcana section.

Import modal (src/App.vue):
- Add role="dialog", aria-modal, aria-labelledby, initial focus, a Tab focus
  trap, Escape-to-close, and focus restoration to the triggering element.

Settings (src/views/SettingsView.vue):
- Associate the "Allow Import" text with its checkbox via id/for.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@donbwhite

Copy link
Copy Markdown
Contributor Author

@BronsonHall Updated the PR

@BronsonHall
BronsonHall merged commit 865e3a7 into Roll20:main Aug 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked Something needs to be addressed with this before it continues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants