NotEvil
3aaf92b788
feat(UC-02): data-driven room themes — 6 JSON + consumer migration + cleanup
...
Phase 3: Extract 6 themes into JSON (oubliette, inferno, crypt, ice, sculk, sandstone)
Phase 4: Migrate HangingCagePiece to use RoomThemeRegistry.pickRandomOrFallback()
- Move 7 shared static methods from RoomTheme into HangingCagePiece
- Replace per-enum placeDecorations() with generic DecorationConfig-based placement
Phase 5: Delete RoomTheme.java (-1368L)
2026-04-16 01:39:40 +02:00
NotEvil
69f52eacf3
feat(UC-02): data-driven room theme infrastructure (Phase 1+2)
...
- BlockPalette: weighted random block selection with condition variants
- RoomThemeDefinition: immutable record with wallBlock/floorBlock/etc convenience API
- DecorationConfig: positioned block records for theme-specific decorations
- RoomThemeRegistry: volatile atomic snapshot + pickRandom(weight-based)
- RoomThemeParser: JSON parsing with BlockStateParser + random_property expansion
- RoomThemeReloadListener: scans data/<ns>/tiedup_room_themes/*.json
- Register listener in TiedUpMod.onAddReloadListeners()
2026-04-16 01:30:51 +02:00
a4fc05b503
Merge pull request 'chore/audit-s02-s05-state-cleanup' ( #13 ) from chore/audit-s02-s05-state-cleanup into develop
...
Reviewed-on: #13
2026-04-15 14:48:42 +00:00
NotEvil
7444853840
fix(S-02): remove unused host field from PlayerMovement (review)
2026-04-15 16:48:22 +02:00
NotEvil
22d79a452b
refactor(S-02/S-05): extract PlayerMovement component + fix thread safety
...
- Extract 11 movement fields from PlayerBindState into PlayerMovement component
- Replace volatile isStruggling/struggleStartTick pair with atomic StruggleSnapshot record
- Remove 5+2 misleading synchronized keywords (different monitors, all server-thread-only)
- Update all 36 MovementStyleManager field accesses to use getMovement() getters/setters
2026-04-15 16:48:22 +02:00
70f85b58a6
Merge pull request 'chore/audit-c01-i18n' ( #12 ) from chore/audit-c01-i18n into develop
...
Reviewed-on: #12
2026-04-15 14:48:07 +00:00
NotEvil
c34bac11b0
fix(C-01): review fixes — missing keys, duplicate, GPS shock i18n
...
- Add missing keephead_enabled/disabled translation keys (BUG-001)
- Remove duplicate gui.tiedup.close key at line 403 (RISK-004)
- Fix GPS shock: use null addon + send GPS_ZONE_VIOLATION separately (RISK-001)
2026-04-15 14:08:41 +02:00
NotEvil
fa5cfb913c
feat(C-01): i18n main commands — 148 translatable keys
...
Phase 3: Migrate Component.literal() in all remaining command files.
- NPCCommand (34), CellCommand (33), SocialCommand (16), CollarCommand (25),
KeyCommand (18), BountyCommand (6), KidnapSetCommand (2), CaptivityDebugCommand (7),
InventorySubCommand (3), TestAnimSubCommand (2), MasterTestSubCommand (7), DebtSubCommand (8)
- Strip all section sign color codes, use .withStyle(ChatFormatting)
- 148 new keys in en_us.json (command.tiedup.*)
- Debug/dynamic strings intentionally kept as literal
2026-04-15 13:54:26 +02:00
NotEvil
70965c2dda
feat(C-01): i18n subcommands — 33 translatable keys
...
Phase 2: Migrate all Component.literal() in 5 subcommand files.
- BindCommands, GagCommands, BlindfoldCommands, CollarCommands, AccessoryCommands
- Strip \u00a7a section signs, use .withStyle(ChatFormatting.GREEN)
- Add 33 keys to en_us.json (command.tiedup.*)
- Shared error key: command.tiedup.error.no_state
2026-04-15 13:28:02 +02:00
NotEvil
0662739fe0
feat(C-01): i18n SystemMessageManager — 83 translatable keys
...
Phase 1: Core system message migration to Component.translatable().
- Replace getMessageTemplate() hardcoded strings with getTranslationKey() key derivation
- All send methods now use Component.translatable() with positional args
- Add 83 keys to en_us.json (msg.tiedup.system.*)
- Add sendTranslatable() convenience for external callers with string args
- Migrate 3 external getTemplate() callers (PlayerShockCollar, CellRegistryV2)
- Add resistance_suffix key for sendWithResistance()
2026-04-15 13:24:05 +02:00
ac72f6aae7
Merge pull request 'chore/quickwin-fix' ( #11 ) from chore/quickwin-fix into develop
...
Reviewed-on: #11
2026-04-15 09:22:08 +00:00
NotEvil
d3bdb026f3
fix: restore missing /tiedup tie command registration (review)
2026-04-15 11:14:28 +02:00
NotEvil
c1e1f56058
refactor: split BondageSubCommand 1207L → 5 focused files (UC-01)
...
- BindCommands.java: tie, untie (156L)
- GagCommands.java: gag, ungag (140L)
- BlindfoldCommands.java: blindfold, unblind (142L)
- CollarCommands.java: collar, takecollar, enslave, free (288L)
- AccessoryCommands.java: putearplugs, takeearplugs, putclothes,
takeclothes, fullyrestrain, adjust (469L)
- BondageSubCommand.java: thin delegator (19L)
Zero logic changes — purely mechanical code move.
2026-04-15 11:09:48 +02:00
NotEvil
f945e9449b
feat(D-01/E): quickwins — debug toggle, HumanChairHelper move, collar equip
...
Q1: Remove F9 debug toggle from GltfAnimationApplier + delete dead
GltfRenderLayer + remove keybind registration
Q2: Move HumanChairHelper from state/ to util/ — pure utility with
no state dependency. 7 import updates.
Q3: Wire NECK collar equip flow in DataDrivenBondageItem:
- Target must be tied up (V1 rule preserved)
- Distance + line-of-sight validation
- Owner added to NBT before equip via CollarHelper.addOwner()
- V2EquipmentHelper handles conflict resolution
- ModSounds.COLLAR_PUT played on success
- OwnershipComponent.onEquipped registers in CollarRegistry
2026-04-15 10:57:01 +02:00
cc0ce89de5
Merge pull request 'feature/d01-branch-e-resistance' ( #10 ) from feature/d01-branch-e-resistance into develop
...
Reviewed-on: #10
2026-04-15 01:45:58 +00:00
NotEvil
db407ee68f
fix(D-01/E): padlock resistance double-count + knife progress persist (review)
...
BUG-001: PacketV2StruggleStart only adds padlock resistance bonus on
first session (when resistance >= base). Interrupted sessions that
persisted combined resistance no longer re-add the bonus, preventing
infinite resistance inflation.
BUG-002: PacketV2LockToggle UNLOCK clears knifeCutProgress and
accessoryStruggleResistance NBT keys, preventing partial knife-cut
progress from persisting across lock/unlock cycles.
2026-04-15 03:44:12 +02:00
NotEvil
d6bb030ad7
feat(D-01/E): resistance & lock system rework (E1-E7)
...
E1: Initialize currentResistance in NBT at equip time from
ResistanceComponent — eliminates MAX-scan fallback bug
E2: BuiltInLockComponent for organic items (already committed)
E3: canStruggle refactor — new model:
- ARMS: always struggle-able (no lock gating)
- Non-ARMS: only if locked OR built-in lock
- Removed dead isItemLocked() from StruggleState + overrides
E4: canUnequip already handled by BuiltInLockComponent.blocksUnequip()
via ComponentHolder delegation
E5: Help/assist mechanic deferred (needs UI design)
E6: Removed lock resistance from ILockable (5 methods + NBT key deleted)
- GenericKnife: new knifeCutProgress NBT for cutting locks
- StruggleAccessory: accessoryStruggleResistance NBT replaces lock resistance
- PacketV2StruggleStart: uses config-based padlock resistance
- All lock/unlock packets cleaned of initializeLockResistance/clearLockResistance
E7: Fixed 3 pre-existing bugs:
- B2: DataDrivenItemRegistry.clear() synchronized on RELOAD_LOCK
- B3: V2TyingPlayerTask validates heldStack before equip (prevents duplication)
- B5: EntityKidnapperMerchant.remove() cleans playerToMerchant map (memory leak)
2026-04-15 03:23:49 +02:00
NotEvil
199bf00aef
feat(D-01/E): BuiltInLockComponent for organic items (E2)
...
- New BuiltInLockComponent: blocksUnequip() returns true (permanent lock)
- ComponentType: add BUILT_IN_LOCK enum value
- 8 organic item JSONs updated (slime, vine, web, tape variants)
- DataDrivenBondageItem: add hasBuiltInLock() static helper
2026-04-15 03:11:15 +02:00
4a3ff438c2
Merge pull request 'feature/d01-branch-d-cleanup' ( #9 ) from feature/d01-branch-d-cleanup into develop
...
Reviewed-on: #9
2026-04-15 00:54:11 +00:00
NotEvil
4d128124e6
chore: gitignore docs/ to stop them from being tracked
2026-04-15 02:52:37 +02:00
NotEvil
3df979ceee
fix(D-01/D): checkup cleanup — 5 issues resolved
...
1. LockableComponent: remove duplicate "Lockable" tooltip line
(ILockable.appendLockTooltip already handles lock status display)
2. ILockable/IHasResistance Javadoc: update @link refs from deleted
V1 classes to V2 AbstractV2BondageItem/DataDrivenBondageItem
3. SettingsAccessor Javadoc: remove stale BindVariant @link references
4. DataDrivenBondageItem: update NECK block comment (remove branch ref)
5. Delete empty bondage3d/gags/ directory
2026-04-15 02:52:27 +02:00
NotEvil
a572513640
chore(D-01/D): remove dead V1 handleSelfAccessory method
2026-04-15 02:12:05 +02:00
NotEvil
dfa7024e21
fix(D-01/D): blindfold ID mismatch + dispenser V2 registration (review)
...
KidnapperTheme: fix "mask_blindfold" → "blindfold_mask" across 8 themes.
The incorrect ID produced ghost items with no definition.
DispenserBehaviors: register GenericBondageDispenseBehavior.forAnyDataDriven()
for the V2 DATA_DRIVEN_ITEM singleton. Dispatches by region from the stack's
definition. V1 per-variant registrations were deleted but V2 replacement
was missing.
2026-04-15 02:10:25 +02:00
NotEvil
9302b6ccaf
chore: remove docs from branch D tracking
2026-04-15 01:59:16 +02:00
NotEvil
099cd0d984
feat(D-01/D): V1 cleanup — delete 28 files, ~5400 lines removed
...
D1: ThreadLocal alert suppression moved from ItemCollar to CollarHelper.
onCollarRemoved() logic (kidnapper alert) moved to CollarHelper.
D2+D3: Deleted 17 V1 item classes + 4 V1-only interfaces:
ItemBind, ItemGag, ItemBlindfold, ItemCollar, ItemEarplugs, ItemMittens,
ItemColor, ItemClassicCollar, ItemShockCollar, ItemShockCollarAuto,
ItemGpsCollar, ItemChokeCollar, ItemHood, ItemMedicalGag,
IBondageItem, IHasGaggingEffect, IHasBlindingEffect, IAdjustable
D4: KidnapperTheme/KidnapperItemSelector/DispenserBehaviors migrated
from variant enums to string-based DataDrivenItemRegistry IDs.
D5: Deleted 11 variant enums + Generic* factories + ItemBallGag3D:
BindVariant, GagVariant, BlindfoldVariant, EarplugsVariant, MittensVariant,
GenericBind, GenericGag, GenericBlindfold, GenericEarplugs, GenericMittens
D6: ModItems cleaned — all V1 bondage registrations removed.
D7: ModCreativeTabs rewritten — iterates DataDrivenItemRegistry.
D8+D9: All V2 helpers cleaned (V1 fallbacks removed), orphan imports removed.
Zero V1 bondage code references remain (only Javadoc comments).
All bondage items are now data-driven via 47 JSON definitions.
2026-04-15 01:55:16 +02:00
fccb99ef9a
Merge pull request 'feature/d01-branch-c-migration' ( #8 ) from feature/d01-branch-c-migration into develop
...
Reviewed-on: #8
2026-04-14 22:57:05 +00:00
NotEvil
b04497b5a1
chore: remove docs from branch C (should not be tracked here)
2026-04-15 00:55:02 +02:00
NotEvil
3515c89f82
fix(D-01/C): missing sync + worldgen empty registry race (review)
...
StruggleSessionManager: add V2EquipmentHelper.sync(player) after bind
resistance update to prevent data loss on server restart during struggle
HangingCagePiece: add fallback ResourceLocation arrays for worldgen when
DataDrivenItemRegistry is empty (race with reload listener on initial
world creation). Registry-first with hardcoded fallbacks.
2026-04-15 00:26:07 +02:00
NotEvil
3d61c9e9e6
feat(D-01/C): consumer migration — 85 files migrated to V2 helpers
...
Phase 1 (state): PlayerBindState, PlayerCaptorManager, PlayerEquipment,
PlayerDataRetrieval, PlayerLifecycle, PlayerShockCollar, StruggleAccessory
Phase 2 (client): AnimationTickHandler, NpcAnimationTickHandler, 5 render
handlers, DamselModel, 3 client mixins, SelfBondageInputHandler,
SlaveManagementScreen, ActionPanel, SlaveEntryWidget, ModKeybindings
Phase 3 (entities): 28 entity/AI files migrated to CollarHelper,
BindModeHelper, PoseTypeHelper, createStack()
Phase 4 (network): PacketSlaveAction, PacketMasterEquip,
PacketAssignCellToCollar, PacketNpcCommand, PacketFurnitureForcemount
Phase 5 (events): RestraintTaskTickHandler, PetPlayRestrictionHandler,
PlayerEnslavementHandler, ChatEventHandler, LaborAttackPunishmentHandler
Phase 6 (commands): BondageSubCommand, CollarCommand, NPCCommand,
KidnapSetCommand
Phase 7 (compat): MCAKidnappedAdapter, MCA mixins
Phase 8 (misc): GagTalkManager, PetRequestManager, HangingCagePiece,
BondageItemBlockEntity, TrappedChestBlockEntity, DispenserBehaviors,
BondageItemLoaderUtility, RestraintApplicator, StruggleSessionManager,
MovementStyleResolver, CampLifecycleManager
Some files retain dual V1/V2 checks (instanceof V1 || V2Helper) for
coexistence — V1-only branches removed in Branch D.
2026-04-15 00:16:50 +02:00
52d1044e9a
Merge pull request 'feature/d01-branch-b-definitions' ( #7 ) from feature/d01-branch-b-definitions into develop
...
Reviewed-on: #7
2026-04-14 20:23:53 +00:00
NotEvil
530b86a9a7
fix(D-01/B): hood missing MOUTH block + organic items lockable:false
...
- Hood: add MOUTH to blocked_regions — prevents double gag stacking
- 8 organic items (slime, vine, web, tape): set lockable:false at top
level for consistency with can_attach_padlock:false
2026-04-14 17:59:27 +02:00
NotEvil
258223bf68
feat(D-01/B): 47 data-driven item definitions + cleanup test files
...
16 binds: ropes, armbinder, dogbinder, chain, ribbon, slime, vine_seed,
web_bind, shibari, leather_straps, medical_straps, beam_cuffs,
duct_tape, straitjacket, wrap, latex_sack
19 gags: cloth_gag, ropes_gag, cleave_gag, ribbon_gag, ball_gag,
ball_gag_strap, tape_gag, wrap_gag, slime_gag, vine_gag, web_gag,
panel_gag, beam_panel_gag, chain_panel_gag, latex_gag, tube_gag,
bite_gag, sponge_gag, baguette_gag
2 blindfolds, 1 earplugs, 1 mittens
5 collars (classic, shock, shock_auto, gps, choke) with ownership component
3 combos (hood, medical_gag, ball_gag_3d)
All items config-driven via ResistanceComponent (id) and GaggingComponent
(material). Organic items have can_attach_padlock: false.
Removed 4 test files (test_component_gag, test_handcuffs, test_leg_cuffs).
2026-04-14 17:52:19 +02:00
NotEvil
679d7033f9
feat(D-01/B): add can_attach_padlock field to data-driven items (B0)
...
- DataDrivenItemDefinition: add canAttachPadlock boolean (default true)
- DataDrivenItemParser: parse "can_attach_padlock" from JSON
- DataDrivenBondageItem: add static canAttachPadlockTo(stack) method
- AnvilEventHandler: check V2 definition before allowing padlock attach
2026-04-14 17:47:32 +02:00
8bfd97ba57
Merge pull request 'feature/d01-branch-a-bridge' ( #6 ) from feature/d01-branch-a-bridge into develop
...
Reviewed-on: #6
2026-04-14 15:19:20 +00:00
NotEvil
df56ebb6bc
fix(D-01/A): adversarial review fixes — 4 logic bugs
...
1. NECK region explicitly blocked in interactLivingEntity() — prevents
V2 collars equipping without ownership setup (latent, no JSONs yet)
2. Swap rollback safety: if re-equip fails after swap failure, drop the
old bind as item entity instead of losing it silently
3. GaggingComponent: cache GagMaterial at construction time — eliminates
valueOf() log spam on every chat message with misconfigured material
4. Dual-bind prevention: check both V1 isTiedUp() AND V2 region occupied
in TyingInteractionHelper and PacketSelfBondage to prevent equipping
V2 bind on top of V1 bind
2026-04-14 16:48:50 +02:00
NotEvil
b97bdf367e
fix(D-01/A): V2 bind/collar resistance completely broken (CRITICAL)
...
PlayerEquipment.getCurrentBindResistance/setCurrentBindResistance and
getCurrentCollarResistance/setCurrentCollarResistance all checked
instanceof ItemBind/ItemCollar — V2 DataDrivenBondageItem silently
returned 0, making V2 items escapable in 1 struggle roll.
Fix: use instanceof IHasResistance which both V1 and V2 implement.
Also fix StruggleCollar.tighten() to read ResistanceComponent directly
for V2 collars instead of IHasResistance.getBaseResistance(entity)
which triggers the singleton MAX-scan across all equipped items.
Note: isItemLocked() dead code in StruggleState is a PRE-EXISTING bug
(x10 locked penalty never applied) — tracked for separate fix.
2026-04-14 16:44:59 +02:00
NotEvil
eb7f06bfc8
fix(D-01/A): 3 review bugs + null guards (BUG-001, BUG-002, BUG-003, RISK-003)
...
BUG-001: TyingInteractionHelper swap now checks V2EquipResult — on failure,
rolls back by re-equipping the old bind instead of leaving target untied
BUG-002: OwnershipComponent.onUnequipped no longer double-calls
unregisterWearer — onCollarRemoved already handles it. Suppressed-alert
path calls unregister directly since onCollarRemoved is skipped.
BUG-003: PacketSelfBondage handleV2SelfBind now clears completed tying
task from PlayerBindState to prevent blocking future tying interactions
RISK-003: StruggleCollar get/setResistanceState null-guard on player
2026-04-14 16:38:09 +02:00
NotEvil
5c4e4c2352
fix(D-01/A): double item consumption + unchecked cast in TyingInteractionHelper
...
QA-001: add instanceof V2TyingPlayerTask guard before cast to prevent
ClassCastException when a V1 TyingPlayerTask was still active
QA-002: remove stack.shrink(1) after tying completion — V2TyingPlayerTask
.onComplete() already consumes the held item via heldStack.shrink(1)
2026-04-14 16:35:05 +02:00
NotEvil
eee4825aba
feat(D-01/A): NPC speed reduction for V2 items (A12)
...
- onEquipped: apply RestraintEffectUtils speed reduction for non-Player
entities with ARMS region and legs bound. Full immobilization for
WRAP/LATEX_SACK pose types.
- onUnequipped: remove speed reduction for non-Player entities
- Players use MovementStyleManager (V2 tick-based), not this legacy path
2026-04-14 16:07:41 +02:00
NotEvil
b359c6be35
feat(D-01/A): self-bondage region routing (A11)
...
- handleV2SelfBondage: split into region-based routing
- NECK → blocked (cannot self-collar)
- ARMS → handleV2SelfBind (tying task with progress bar)
- Other → handleV2SelfAccessory (instant equip)
- handleV2SelfAccessory: arms-bound check via BindModeHelper,
locked check for swap, V2EquipmentHelper for conflict resolution
2026-04-14 16:06:01 +02:00
NotEvil
19cc69985d
feat(D-01/A): V2-aware struggle system (A9)
...
StruggleBinds:
- canStruggle(): instanceof ItemBind → BindModeHelper.isBindItem()
- isItemLocked(): instanceof ItemBind → instanceof ILockable (fixes R4)
- onAttempt(): instanceof ItemShockCollar → CollarHelper.canShock() (fixes R5)
- tighten(): reads ResistanceComponent directly for V2, avoids MAX scan bug
StruggleCollar:
- getResistanceState/setResistanceState: instanceof ItemCollar → IHasResistance
- canStruggle(): instanceof ItemCollar → CollarHelper.isCollar() + ILockable
- onAttempt(): shock check via CollarHelper.canShock()
- successAction(): unlock via ILockable
- tighten(): resistance via IHasResistance
All V1 items continue working through the same interfaces they already implement.
2026-04-14 15:47:20 +02:00
NotEvil
737a4fd59b
feat(D-01/A): interaction routing + TyingInteractionHelper (A8)
...
- DataDrivenBondageItem.use(): shift+click cycles bind mode for ARMS items
- DataDrivenBondageItem.interactLivingEntity(): region-based routing
- ARMS → TyingInteractionHelper (tying task with progress bar)
- NECK → deferred to Branch C (no V2 collar JSONs yet)
- Other regions → instant equip via parent AbstractV2BondageItem
- TyingInteractionHelper: extracted tying flow using V2TyingPlayerTask
- Distance/LoS validation, swap if already tied, task lifecycle
2026-04-14 15:35:31 +02:00
NotEvil
b79225d684
feat(D-01/A): OwnershipComponent lifecycle hooks (A7)
...
- onEquipped: register collar owners in CollarRegistry (server-side only)
- onUnequipped: alert kidnappers + unregister from CollarRegistry
- Guards: client-side check, ServerLevel cast, empty owners skip, try-catch
- appendTooltip: nickname, owner count, shock/GPS/choke capabilities
- Delegates alert suppression to ItemCollar.isRemovalAlertSuppressed()
2026-04-14 15:29:31 +02:00
NotEvil
751bad418d
feat(D-01/A): poseType, helpers, OWNERSHIP ComponentType (A4, A5, A6)
...
- DataDrivenItemDefinition: add poseType field, parsed from JSON "pose_type"
- PoseTypeHelper: resolves PoseType from V2 definition or V1 ItemBind fallback
- BindModeHelper: static bind mode NBT utilities (isBindItem, hasArmsBound,
hasLegsBound, cycleBindModeId) — works for V1 and V2 items
- CollarHelper: complete static utility class for collar operations with
dual-path V2/V1 dispatch (ownership, features, shock, GPS, choke, alert)
- ComponentType: add OWNERSHIP enum value
- OwnershipComponent: stub class (lifecycle hooks added in next commit)
2026-04-14 15:23:08 +02:00
NotEvil
b81d3eed95
feat(D-01/A): config-driven components + tooltip hook (A1, A2, A3)
...
- ResistanceComponent: resistanceId delegates to SettingsAccessor at runtime,
fallback to hardcoded base for backward compat
- GaggingComponent: material field delegates to GagMaterial enum from ModConfig,
explicit comprehension/range overrides take priority
- IItemComponent: add default appendTooltip() method
- ComponentHolder: iterate components for tooltip contribution
- 6 components implement appendTooltip (lockable, resistance, gagging, shock,
gps, choking)
- DataDrivenBondageItem: call holder.appendTooltip() in appendHoverText()
2026-04-14 15:05:48 +02:00
fa4c332a10
Merge pull request 'feature/d01-component-system' ( #5 ) from feature/d01-component-system into develop
...
Reviewed-on: #5
2026-04-14 00:54:16 +00:00
NotEvil
7bd840705a
docs: add components section to ARTIST_GUIDE.md
...
Documents the 8 available gameplay components (lockable, resistance,
gagging, blinding, shock, gps, choking, adjustable) with config fields,
examples (GPS shock collar, adjustable blindfold), and usage tips.
2026-04-14 02:51:37 +02:00
NotEvil
90bc890b95
fix(D-01): synchronize reload paths and capture snapshot locally (RISK-001, RISK-002)
2026-04-14 02:46:09 +02:00
NotEvil
185ac63a44
feat(D-01): implement 5 remaining components (blinding, shock, gps, choking, adjustable)
2026-04-14 02:37:14 +02:00
NotEvil
dcc8493e5e
fix(D-01): pre-built map for O(1) ComponentType.fromKey() lookup (RISK-005)
...
Replace linear values() scan with a static unmodifiable HashMap lookup.
While only 3 entries currently exist, this establishes the correct
pattern for when more component types are added.
2026-04-14 02:29:46 +02:00