WAYFARER PROJECT REVIEW ======================= Generated: 2026-09-29 21:57:58 Files: 75 FILE MANIFEST ------------- admin/builder-refactored.php admin/builder.php admin/games/edit.php admin/games/index.php assets/css/admin.css assets/css/builder.css assets/css/hierarchical-targets.css assets/css/status-subject-effects.css assets/js/builder.js assets/js/game-endings.js assets/js/game-travel.js assets/js/hierarchical-targets.js assets/js/starting-choices.js assets/js/status-lifecycle.js assets/js/status-subject-effects.js assets/js/target-search.js assets/js/trait-display.js assets/js/traits.js config/builder-types.php includes/builder/collection-table.php includes/builder/collection.php includes/builder/config-field.php includes/builder/editor.php includes/builder/field.php includes/builder/functions.php includes/builder/load-record.php includes/builder/location-tick-effects.php includes/builder/navigation.php includes/builder/region-hud.php includes/builder/region-travel.php includes/builder/relationship-field.php includes/builder/save-record.php includes/builder/status-lifecycle.php includes/builder/storylet-gameplay.php includes/builder/tick-effects.php includes/gameplay/condition-editor.php includes/gameplay/conditions.php includes/gameplay/effects-editor.php includes/gameplay/functions.php includes/gameplay/game-endings.php includes/gameplay/starting-choices.php includes/gameplay/status-lifecycle.php includes/gameplay/storylet-bindings.php includes/gameplay/tick-effects-editor.php includes/gameplay/travel-modifiers.php includes/header.php includes/traits/editor.php includes/traits/functions.php includes/traits/render.php play/api/presentation.php play/api/restart.php play/api/storylet.php play/api/tick.php play/assets/play.css play/assets/play.js play/includes/action-screen.php play/includes/applicable-traits.v15-disabled.php play/includes/author-mode.php play/includes/current-region.php play/includes/effects.php play/includes/ending-screen.php play/includes/entity-state.php play/includes/hud.php play/includes/player-state.php play/includes/presentation-screen.php play/includes/runtime-bootstrap.php play/includes/runtime.php play/includes/scene.php play/includes/setup.php play/includes/start-screen.php play/includes/statuses.php play/includes/storylet-bindings.php play/includes/storylets.php play/includes/tick-panel.php play/index.php ============================================================================== FILE: admin/builder-refactored.php ============================================================================== prepare(" SELECT id, name, config FROM wf_games WHERE id = ? "); $stmt->execute([$gameId]); $game = $stmt->fetch(); if (!$game) { die('Game not found.'); } // ================================================================ // LOAD BUILDER DEFINITION // ================================================================ if (!isset($builderTypes[$type])) { die('Unknown builder type.'); } $definition = $builderTypes[$type]; $table = $definition['table']; $listField = $definition['list_field']; $listColumns = $definition['list_columns'] ?? []; $fields = $definition['fields'] ?? []; $relationships = $definition['relationships'] ?? []; $configFields = $definition['config_fields'] ?? []; $scope = $definition['scope']; // ================================================================ // LOAD EDITOR RECORD // ================================================================ $editorState = loadBuilderRecord( $pdo, $definition, $gameId, $id ); $record = $editorState['record']; $configValues = $editorState['configValues']; $traits = $editorState['traits']; $error = ''; /* WAYFARER_STORYLET_BUILDER_V56 * A + New Branch Storylet link can preselect its parent and choose a sensible * sibling order without making parent_storylet_id part of the generic request. */ if ( $type === 'storylets' && !$id && !empty($_GET['parent_storylet_id']) ) { $requestedParentId = (int)$_GET['parent_storylet_id']; $parentStmt = $pdo->prepare( " SELECT id FROM wf_storylets WHERE id = ? AND game_id = ? LIMIT 1 " ); $parentStmt->execute([ $requestedParentId, $gameId, ]); if ($parentStmt->fetchColumn()) { $record['parent_storylet_id'] = $requestedParentId; $orderStmt = $pdo->prepare( " SELECT COALESCE(MAX(branch_sort_order), 0) FROM wf_storylets WHERE game_id = ? AND parent_storylet_id = ? " ); $orderStmt->execute([ $gameId, $requestedParentId, ]); $record['branch_sort_order'] = (int)$orderStmt->fetchColumn() + 10; } } // ================================================================ // SAVE / DELETE / DUPLICATE // WAYFARER_STORYLET_DUPLICATE_V83 // ================================================================ if ($_SERVER['REQUEST_METHOD'] === 'POST') { /* * WAYFARER DELETE ACTION * * Save remains the default action so existing save forms do not need * an additional hidden field. */ $postAction = $_POST['action'] ?? 'save'; if ($postAction === 'duplicate') { $duplicateId = !empty($_POST['id']) ? (int)$_POST['id'] : 0; if ( ($definition['table'] ?? '') !== 'wf_storylets' ) { $error = 'Only Storylets can be duplicated.'; } else { $duplicateResult = duplicateStoryletRecord( $pdo, $gameId, $duplicateId ); $error = $duplicateResult['error']; if ($duplicateResult['duplicated']) { header( 'Location: ' . $builderPage . '?game=' . $gameId . '&type=' . urlencode($type) . '&id=' . (int)$duplicateResult['id'] . '&tab=basics' . '&duplicated=1' ); exit; } } } elseif ($postAction === 'delete') { $deleteId = !empty($_POST['id']) ? (int)$_POST['id'] : 0; $deleteResult = deleteBuilderRecord( $pdo, $definition, $gameId, $deleteId ); $error = $deleteResult['error']; if ($deleteResult['deleted']) { header( 'Location: ' . $builderPage . '?game=' . $gameId . '&type=' . urlencode($type) ); exit; } } else { $saveResult = saveBuilderRecord( $pdo, $definition, $gameId, $_POST, $record, $configValues, $traits ); $record = $saveResult['record']; $configValues = $saveResult['configValues']; $traits = $saveResult['traits']; $error = $saveResult['error']; if ($saveResult['saved']) { /* * Media Library associations are applied only after the authored * object has saved successfully and therefore has a stable ID. */ $savedRecordId = (int)$saveResult['id']; $mediaResult = applyBuilderMediaSelections( $pdo, $gameId, $definition['table'], $savedRecordId, $_POST, $_FILES ); if (!$mediaResult['saved']) { /* * The authored object itself has already saved. Keep the * editor on that record and report the media failure * accurately rather than pretending the object save failed. */ $id = $savedRecordId; $record['id'] = $savedRecordId; $error = 'Record saved, but media was not changed: ' . $mediaResult['error']; } else { header( 'Location: ' . $builderPage . '?game=' . $gameId . '&type=' . urlencode($type) . '&id=' . (int)$saveResult['id'] . '&tab=' . rawurlencode( (string)($_POST['active_tab'] ?? 'basics') ) ); exit; } } } } // ================================================================ // LOAD COLLECTION + UI SUPPORT DATA // ================================================================ $collectionState = loadBuilderCollection( $pdo, $definition, $gameId ); $records = $collectionState['records']; $relationshipOptions = $collectionState['relationshipOptions']; $conditionData = $collectionState['conditionData']; ?> <?= builderEscape($game['name']) ?> - Wayfarer Builder

Wayfarer Builder

============================================================================== FILE: admin/games/edit.php ============================================================================== null, 'name' => '', 'slug' => '', 'description' => '', 'instructions' => '', 'player_slots' => 0, 'config' => null, 'status' => 'draft' ]; $traits = []; $playerTraits = []; $config = []; /* WAYFARER_PLAYER_TRAIT_DEFINITIONS_V52 */ $gameTickEffects = []; $gameplayTickEffects = []; $startingChoices = []; $startingChoicesEditor = []; $startingChoiceItemTargets = []; /* WAYFARER_STARTING_CHOICE_ITEM_QUANTITY_V54 */ $gameEndings = []; $gameEndingsEditor = []; /* WAYFARER_GAME_TRAVEL_MODIFIERS_V73 */ $gameTravelModifiers = []; $gameTravelModifiersEditor = []; $error = ''; // ================================================================ // LOAD EXISTING GAME // ================================================================ if (isset($_GET['id'])) { $stmt = $pdo->prepare(" SELECT * FROM wf_games WHERE id = ? "); $stmt->execute([(int)$_GET['id']]); $existingGame = $stmt->fetch(); if (!$existingGame) { die('Game not found.'); } $game = array_merge($game, $existingGame); if (!empty($game['config'])) { $decodedConfig = json_decode( $game['config'], true ); if (is_array($decodedConfig)) { $config = $decodedConfig; } } if ( isset($config['traits']) && is_array($config['traits']) ) { $traits = $config['traits']; } /* WAYFARER_PLAYER_TRAIT_DEFINITIONS_V52 */ if ( isset($config['player_traits']) && is_array($config['player_traits']) ) { $playerTraits = array_values( $config['player_traits'] ); } /* WAYFARER_LOAD_GAME_TICK_EFFECTS_V33 */ if ( isset($config['tick_effects']) && is_array($config['tick_effects']) ) { $gameTickEffects = array_values( $config['tick_effects'] ); } if ( isset($config['setup']['choices']) && is_array($config['setup']['choices']) ) { $startingChoices = array_values( $config['setup']['choices'] ); } /* WAYFARER_GAME_ENDINGS_V45 */ if ( isset($config['endings']) && is_array($config['endings']) ) { $gameEndings = array_values( $config['endings'] ); } /* WAYFARER_LOAD_GAME_TRAVEL_MODIFIERS_V73 */ if ( isset($config['travel']['modifiers']) && is_array($config['travel']['modifiers']) ) { $gameTravelModifiers = array_values( $config['travel']['modifiers'] ); } } $gameplayTickEffects = $gameTickEffects; $startingChoicesEditor = $startingChoices; $gameEndingsEditor = $gameEndings; $gameTravelModifiersEditor = $gameTravelModifiers; // ================================================================ // SAVE // ================================================================ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $game['id'] = !empty($_POST['id']) ? (int)$_POST['id'] : null; $game['name'] = trim($_POST['name'] ?? ''); $game['slug'] = trim($_POST['slug'] ?? ''); $game['description'] = trim($_POST['description'] ?? ''); $game['instructions'] = trim($_POST['instructions'] ?? ''); $game['player_slots'] = max(0, (int)($_POST['player_slots'] ?? 0)); $game['status'] = $_POST['status'] ?? 'draft'; // ------------------------------------------------------------ // BUILD GAME + PLAYER TRAIT DEFINITIONS // WAYFARER_PLAYER_TRAIT_DEFINITIONS_V52 // ------------------------------------------------------------ $traitResult = buildTraitsFromPost( $_POST['traits'] ?? [], ['status'] ); $traits = $traitResult['traits']; if ($traitResult['error'] !== '') { $error = $traitResult['error']; } $playerTraitResult = buildTraitsFromPost( $_POST['player_traits'] ?? [], ['status'] ); $playerTraits = $playerTraitResult['traits']; if ( $error === '' && $playerTraitResult['error'] !== '' ) { $error = 'Player Traits: ' . $playerTraitResult['error']; } /* WAYFARER_GAME_SETTINGS_UNIVERSAL_TARGETS_V56 */ $gameplayRuntimeTargets = gameplayRuntimeStateTargets( $pdo, (int)($game['id'] ?? 0), $traits, $playerTraits, (int)( $game['player_slots'] ?? 0 ) ); /* WAYFARER_BUILD_GAME_TRAVEL_MODIFIERS_V73 */ $postedGameTravelModifiers = isset($_POST['travel_modifiers']) && is_array($_POST['travel_modifiers']) ? array_values( $_POST['travel_modifiers'] ) : []; $gameTravelModifiersEditor = $postedGameTravelModifiers; /* * Build the editor structure even if another Game Settings section already * has a validation error. This keeps selected Travel Conditions intact on the * failed POST. Only the first validation error becomes the page error. */ $gameTravelModifierResult = wayfarerBuildGameTravelModifiersFromPost( $postedGameTravelModifiers, $gameplayRuntimeTargets ); $gameTravelModifiersEditor = $gameTravelModifierResult[ 'editor_modifiers' ]; if ( $error === '' && $gameTravelModifierResult['error'] !== '' ) { $error = $gameTravelModifierResult['error']; } if ( $gameTravelModifierResult['error'] === '' ) { $gameTravelModifiers = $gameTravelModifierResult[ 'modifiers' ]; } /* WAYFARER_STARTING_CHOICE_ITEM_QUANTITY_V54 */ $startingChoiceItemTargets = wayfarerStartingChoiceItemQuantityTargets( $pdo, (int)($game['id'] ?? 0) ); /* WAYFARER_BUILD_STARTING_CHOICES_V36 */ /* WAYFARER_STARTING_CHOICES_CANONICAL_EMPTY_V36_1 */ $startingChoicesSubmitted = isset($_POST['setup_choices_present']) || array_key_exists( 'setup_choices', $_POST ); $postedStartingChoices = isset($_POST['setup_choices']) && is_array($_POST['setup_choices']) ? array_values( $_POST['setup_choices'] ) : []; $startingChoicesEditor = $postedStartingChoices; if ($error === '') { $startingChoiceResult = wayfarerBuildStartingChoicesFromPost( $postedStartingChoices, $traits, $playerTraits, (int)( $game['player_slots'] ?? 0 ), $startingChoiceItemTargets, $gameplayRuntimeTargets ); if ( $startingChoiceResult['error'] !== '' ) { $error = $startingChoiceResult['error']; } else { $startingChoices = $startingChoiceResult['choices']; $startingChoicesEditor = $startingChoices; } } /* WAYFARER_BUILD_GAME_ENDINGS_V45 */ $postedGameEndings = isset($_POST['endings']) && is_array($_POST['endings']) ? array_values( $_POST['endings'] ) : []; $gameEndingsEditor = $postedGameEndings; if ($error === '') { $endingResult = wayfarerBuildGameEndingsFromPost( $pdo, (int)($game['id'] ?? 0), $postedGameEndings, $traits ); if ( $endingResult['error'] !== '' ) { $error = $endingResult['error']; } else { $gameEndings = $endingResult['endings']; $gameEndingsEditor = $gameEndings; } } /* WAYFARER_BUILD_GAME_TICK_EFFECTS_V33 */ $postedGameTickEffects = isset($_POST['tick_effects']) && is_array($_POST['tick_effects']) ? array_values( $_POST['tick_effects'] ) : []; $gameplayTickEffects = $postedGameTickEffects; if ($error === '') { $gameTickEffectResult = gameplayBuildTickEffectsFromPost( $gameplayRuntimeTargets, $postedGameTickEffects, $gameplayRuntimeTargets ); if ( $gameTickEffectResult['error'] !== '' ) { $error = $gameTickEffectResult['error']; } else { $gameTickEffects = $gameTickEffectResult['effects']; $gameplayTickEffects = $gameTickEffects; } } // ------------------------------------------------------------ // VALIDATE GAME // ------------------------------------------------------------ if ($error === '') { if ($game['name'] === '') { $error = 'Game name is required.'; } elseif ($game['slug'] === '') { $error = 'Slug is required.'; } } // ------------------------------------------------------------ // BUILD CONFIG JSON // ------------------------------------------------------------ if ($error === '') { /* * Preserve any other configuration already stored for the game. * Traits own only the "traits" section of config. */ if (empty($config) && !empty($game['config'])) { $decodedConfig = json_decode( $game['config'], true ); if (is_array($decodedConfig)) { $config = $decodedConfig; } } $startingRegionId = !empty($_POST['starting_region_id']) ? (int)$_POST['starting_region_id'] : null; if ($startingRegionId !== null) { $config['starting_region_id'] = $startingRegionId; } else { unset($config['starting_region_id']); } /* WAYFARER_GAME_TIME_SETTINGS_SAVE_V61 */ $timeMode = strtolower(trim((string)($_POST['time_tracking'] ?? 'none'))); if (!in_array($timeMode, ['none', 'days', 'hours'], true)) { $timeMode = 'none'; } if ($timeMode === 'none') { unset($config['time']); } else { $startingDay = max(1, (int)($_POST['time_starting_day'] ?? 1)); if ($timeMode === 'days') { $config['time'] = [ 'mode' => 'days', 'starting_day' => $startingDay, 'ticks_per_day' => max(1, (int)($_POST['time_ticks_per_day'] ?? 1)), ]; } else { $hoursPerDay = max(1, (int)($_POST['time_hours_per_day'] ?? 24)); $startingHour = max(0, (int)($_POST['time_starting_hour'] ?? 8)); if ($startingHour >= $hoursPerDay) { $startingHour = $hoursPerDay - 1; } $config['time'] = [ 'mode' => 'hours', 'starting_day' => $startingDay, 'starting_hour' => $startingHour, 'ticks_per_hour' => max(1, (int)($_POST['time_ticks_per_hour'] ?? 1)), 'hours_per_day' => $hoursPerDay, ]; } } $config['traits'] = array_values( $traits ); /* WAYFARER_PLAYER_TRAIT_DEFINITIONS_V52 */ $config['player_traits'] = array_values( $playerTraits ); /* WAYFARER_SAVE_GAME_TRAVEL_MODIFIERS_V73 */ if ($gameTravelModifiers) { if ( !isset($config['travel']) || !is_array($config['travel']) ) { $config['travel'] = []; } $config['travel']['modifiers'] = array_values( $gameTravelModifiers ); } else { if ( isset($config['travel']) && is_array($config['travel']) ) { unset( $config['travel']['modifiers'] ); if ($config['travel'] === []) { unset($config['travel']); } } } /* WAYFARER_SAVE_GAME_TICK_EFFECTS_V33 */ if ($gameTickEffects) { $config['tick_effects'] = array_values( $gameTickEffects ); } else { unset( $config['tick_effects'] ); } /* WAYFARER_SAVE_STARTING_CHOICES_V36 */ /* * WAYFARER_STARTING_CHOICES_CANONICAL_EMPTY_V36_1 * * If the Setup editor participated in this submission, its current * collection is the canonical collection. In particular, deleting the * final card must persist an explicit empty array rather than relying * on the absence of setup_choices in POST. */ if ($startingChoicesSubmitted) { if ( !isset($config['setup']) || !is_array($config['setup']) ) { $config['setup'] = []; } $config['setup']['choices'] = array_values( $startingChoices ); } /* WAYFARER_SAVE_GAME_ENDINGS_V45 */ $config['endings'] = array_values( $gameEndings ); $game['config'] = json_encode( $config, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); if ($game['config'] === false) { $error = 'Unable to encode game configuration.'; } } // ------------------------------------------------------------ // WRITE GAME // ------------------------------------------------------------ if ($error === '') { if ($game['id']) { $stmt = $pdo->prepare(" UPDATE wf_games SET name = ?, slug = ?, description = ?, instructions = ?, player_slots = ?, config = ?, status = ? WHERE id = ? "); $stmt->execute([ $game['name'], $game['slug'], $game['description'], $game['instructions'], $game['player_slots'], $game['config'], $game['status'], $game['id'] ]); } else { $uuid = bin2hex(random_bytes(16)); $stmt = $pdo->prepare(" INSERT INTO wf_games ( uuid, name, slug, description, instructions, player_slots, config, status ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) "); $stmt->execute([ $uuid, $game['name'], $game['slug'], $game['description'], $game['instructions'], $game['player_slots'], $game['config'], $game['status'] ]); $game['id'] = (int)$pdo->lastInsertId(); } /* * WAYFARER_STAY_ON_GAME_SETTINGS_V38 * * Successful saves remain on the same Game Settings record and reopen * the tab the author was using. */ $savedGameTab = (string)( $_POST['active_tab'] ?? 'basics' ); if ( !in_array( $savedGameTab, [ 'basics', 'setup', 'traits', 'gameplay', 'travel', 'endings', ], true ) ) { $savedGameTab = 'basics'; } header( 'Location: edit.php?id=' . (int)$game['id'] . '&tab=' . rawurlencode( $savedGameTab ) ); exit; } } // ================================================================ // LOAD REGIONS FOR STARTING REGION // ================================================================ $regions = []; if (!empty($game['id'])) { $regionStmt = $pdo->prepare(" SELECT id, name FROM wf_regions WHERE game_id = ? ORDER BY name "); $regionStmt->execute([ $game['id'] ]); $regions = $regionStmt->fetchAll(); } /* WAYFARER_GAME_SETTINGS_UNIVERSAL_TARGETS_RENDER_V56 */ $gameplayRuntimeTargets = gameplayRuntimeStateTargets( $pdo, (int)($game['id'] ?? 0), $traits, $playerTraits, (int)( $game['player_slots'] ?? 0 ) ); /* WAYFARER_STARTING_CHOICE_ITEM_QUANTITY_RENDER_V54 */ $startingChoiceItemTargets = wayfarerStartingChoiceItemQuantityTargets( $pdo, (int)($game['id'] ?? 0) ); ?> <?= $game['id'] ? 'Edit Game' : 'New Game' ?> - Wayfarer


Example: stormgate

============================================================================== FILE: admin/games/index.php ============================================================================== query(" SELECT id, name, slug, status, updated_at FROM wf_games ORDER BY name "); $games = $stmt->fetchAll(); ?> Wayfarer

Wayfarer

Games

+ New Game

No games have been created yet.

Name Slug Status Last Updated Actions
Open Builder | Game Settings
============================================================================== FILE: assets/css/admin.css ============================================================================== /* |-------------------------------------------------------------------------- | Wayfarer Admin Theme |-------------------------------------------------------------------------- | Shared visual language for Wayfarer administrative pages. */ :root { --wf-border: #d8dbe2; --wf-border-strong: #b8bdc7; --wf-surface: #ffffff; --wf-surface-soft: #f6f7f9; --wf-page: #f3f4f6; --wf-text: #20242a; --wf-muted: #667085; --wf-accent: #2f5ea8; --wf-accent-hover: #244b88; --wf-accent-soft: #edf3fc; --wf-success-soft: #eef8f1; --wf-success-text: #28603a; --wf-radius: 8px; --wf-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); } .wayfarer-admin-page { box-sizing: border-box; margin: 0; padding: 28px; background: var(--wf-page); color: var(--wf-text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; font-size: 15px; line-height: 1.45; } .wayfarer-admin-page *, .wayfarer-admin-page *::before, .wayfarer-admin-page *::after { box-sizing: border-box; } .wayfarer-admin-page > * { max-width: 1180px; margin-left: auto; margin-right: auto; } .wayfarer-admin-page > h1 { margin-top: 0; margin-bottom: 2px; font-size: 2rem; line-height: 1.15; } .wayfarer-admin-page > h2 { margin-top: 0; margin-bottom: 20px; color: var(--wf-muted); font-size: 1rem; font-weight: 500; } .wayfarer-admin-page a { color: var(--wf-accent); } .wayfarer-admin-page a:hover { color: var(--wf-accent-hover); } /* ------------------------------------------------------------------ GAMES INDEX ------------------------------------------------------------------ */ .wayfarer-games-page > p { margin-top: 0; margin-bottom: 18px; } .admin-primary-action { display: inline-block; padding: 9px 14px; border: 1px solid #214b86; border-radius: 6px; background: var(--wf-accent); color: #ffffff !important; text-decoration: none; font-weight: 600; box-shadow: var(--wf-shadow); } .admin-primary-action:hover { background: var(--wf-accent-hover); color: #ffffff !important; } .admin-table { width: 100%; overflow: hidden; border: 1px solid var(--wf-border); border-collapse: separate; border-spacing: 0; border-radius: var(--wf-radius); background: var(--wf-surface); box-shadow: var(--wf-shadow); } .admin-table th, .admin-table td { padding: 12px 14px; border: 0; border-bottom: 1px solid var(--wf-border); text-align: left; vertical-align: middle; } .admin-table th { background: var(--wf-surface-soft); color: #404650; font-size: 0.78rem; font-weight: 700; letter-spacing: 0.045em; text-transform: uppercase; } .admin-table tr:last-child td { border-bottom: 0; } .admin-table tbody tr:hover td { background: #fafbfc; } .games-table td:first-child { font-weight: 700; } .games-table td:nth-child(2) { color: var(--wf-muted); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.9rem; } .games-table td:nth-child(3) { text-transform: capitalize; } .games-table td:last-child { white-space: nowrap; } .games-table td:last-child a { display: inline-block; margin: 2px 5px 2px 0; padding: 6px 9px; border: 1px solid var(--wf-border-strong); border-radius: 5px; background: var(--wf-surface); color: var(--wf-text); text-decoration: none; font-size: 0.9rem; font-weight: 600; } .games-table td:last-child a:hover { border-color: #9cabc0; background: var(--wf-accent-soft); color: var(--wf-accent-hover); } /* Make the primary builder action visually stronger. */ .games-table td:last-child a:first-child { border-color: #a9bddc; background: var(--wf-accent-soft); color: var(--wf-accent); } /* ------------------------------------------------------------------ RESPONSIVE ------------------------------------------------------------------ */ @media (max-width: 760px) { .wayfarer-admin-page { padding: 16px; } .games-table { display: block; overflow-x: auto; white-space: nowrap; } } @media (max-width: 520px) { .wayfarer-admin-page > h1 { font-size: 1.65rem; } .admin-table th, .admin-table td { padding: 10px; } } /* ========================================================================== GAME SETTINGS PAGE — v13.1 ========================================================================== */ .wayfarer-game-edit-page { padding-bottom: 60px; } .wayfarer-game-edit-page > h1, .wayfarer-game-edit-page > .admin-back-link, .wayfarer-game-edit-page > .admin-error-message, .wayfarer-game-edit-page > .admin-form-card { max-width: 980px; } .admin-back-link { margin-top: 0; margin-bottom: 14px; } .admin-back-link a { text-decoration: none; font-weight: 600; } .admin-error-message { padding: 11px 14px; border: 1px solid #e8b8be; border-radius: 6px; background: #fff4f5; color: #9a1b2a; font-weight: 600; } .admin-form-card { padding: 22px 26px 0; border: 1px solid var(--wf-border); border-radius: var(--wf-radius); background: var(--wf-surface); box-shadow: var(--wf-shadow); } .admin-form-card > p:first-of-type { margin-top: 0; } .admin-form-card h2, .admin-form-card h3 { color: var(--wf-text); } .admin-form-card h2 { margin-top: 26px; margin-bottom: 6px; font-size: 1.2rem; } .admin-form-card label { color: #303640; font-weight: 600; } .admin-form-card small { color: var(--wf-muted); font-weight: 400; } .admin-form-card input[type="text"], .admin-form-card input[type="number"], .admin-form-card input[type="password"], .admin-form-card input[type="file"], .admin-form-card select, .admin-form-card textarea { box-sizing: border-box; max-width: 100%; margin-top: 5px; padding: 8px 9px; border: 1px solid var(--wf-border-strong); border-radius: 5px; background: #fff; color: var(--wf-text); font: inherit; } .admin-form-card input[type="text"], .admin-form-card textarea { width: min(100%, 720px); } .admin-form-card textarea { min-height: 110px; resize: vertical; } .admin-form-card input:focus, .admin-form-card select:focus, .admin-form-card textarea:focus { outline: 2px solid rgba(47, 94, 168, 0.18); border-color: #7f9dc8; } .admin-form-card > hr { margin: 26px -26px 22px; border: 0; border-top: 1px solid var(--wf-border); } /* * This deliberately styles any fieldset in Game Settings rather than * depending on the old traits-container/add-trait implementation. */ .admin-form-card fieldset { margin: 16px 0; padding: 16px 18px; border: 1px solid var(--wf-border); border-radius: 7px; background: var(--wf-surface-soft); } .admin-form-card fieldset legend { padding: 0 6px; color: #404650; font-weight: 700; } .admin-form-card fieldset > p { margin-top: 10px; margin-bottom: 10px; } /* * Existing trait row hooks continue to get slightly tighter handling when * present, but they are not required. */ .admin-form-card .trait-row { margin-bottom: 14px !important; } .admin-form-card .trait-number-fields { display: flex; flex-wrap: wrap; gap: 16px; } .admin-form-card .trait-number-fields > p { margin-top: 0; margin-bottom: 0; } /* ------------------------------------------------------------------ BUTTONS ------------------------------------------------------------------ */ .admin-form-card button, .admin-primary-button, .admin-secondary-button, .admin-danger-button { padding: 8px 13px; border: 1px solid var(--wf-border-strong); border-radius: 6px; background: var(--wf-surface); color: var(--wf-text); font: inherit; font-weight: 600; cursor: pointer; } .admin-form-card button:hover { background: var(--wf-accent-soft); border-color: #9cabc0; } .admin-primary-button { border-color: #214b86 !important; background: var(--wf-accent) !important; color: #fff !important; } .admin-primary-button:hover { background: var(--wf-accent-hover) !important; } .admin-secondary-button { background: var(--wf-surface) !important; } .admin-danger-button, .admin-form-card .remove-trait { border-color: #d7a4ab !important; background: #fff !important; color: #a61b2b !important; } .admin-danger-button:hover, .admin-form-card .remove-trait:hover { background: #fff4f5 !important; } /* * Treat the last form paragraph as the Save footer. In the current editor, * the Save Game control lives there. */ .admin-form-card > p:last-child { display: flex; justify-content: flex-end; margin: 22px -26px 0; padding: 14px 26px; border-top: 1px solid var(--wf-border); border-radius: 0 0 var(--wf-radius) var(--wf-radius); background: var(--wf-surface-soft); } /* ------------------------------------------------------------------ RESPONSIVE ------------------------------------------------------------------ */ @media (max-width: 720px) { .admin-form-card { padding: 18px 18px 0; } .admin-form-card > hr { margin-left: -18px; margin-right: -18px; } .admin-form-card > p:last-child { margin-left: -18px; margin-right: -18px; padding-left: 18px; padding-right: 18px; } .admin-form-card .trait-number-fields { display: block; } } /* ========================================================================== GAME SETTINGS TABS — v22 Mirrors the authored-object tab language used by builder.php. ========================================================================== */ .admin-game-tabs-form { padding-top: 0; } .admin-game-tabs { display: flex; gap: 4px; margin: 0 -26px 20px; padding: 0 26px; border-bottom: 1px solid var(--wf-border); background: var(--wf-surface); border-radius: var(--wf-radius) var(--wf-radius) 0 0; } .admin-game-tab { position: relative; bottom: -1px; margin: 0; padding: 12px 14px !important; border: 1px solid transparent !important; border-bottom: 1px solid var(--wf-border) !important; border-radius: 0 !important; background: transparent !important; color: var(--wf-muted) !important; font: inherit; font-weight: 600 !important; cursor: pointer; } .admin-game-tab:hover { color: var(--wf-text) !important; background: var(--wf-surface-soft) !important; } .admin-game-tab.is-active { color: var(--wf-accent) !important; background: var(--wf-surface) !important; border-color: var(--wf-border) var(--wf-border) var(--wf-surface) !important; border-radius: 6px 6px 0 0 !important; } .admin-game-tab-panel[hidden] { display: none !important; } .admin-game-tab-panel > h2:first-child { margin-top: 0; } .admin-game-tab-panel > p:first-child { margin-top: 0; } .admin-game-panel-intro { color: var(--wf-muted); } .admin-game-placeholder-card { margin: 16px 0 4px; padding: 16px 18px; border: 1px solid var(--wf-border); border-radius: 7px; background: var(--wf-surface-soft); } .admin-game-placeholder-card > p:last-child { margin-bottom: 0; } .admin-game-save-bar { display: flex; justify-content: flex-end; margin: 22px -26px 0; padding: 14px 26px; border-top: 1px solid var(--wf-border); border-radius: 0 0 var(--wf-radius) var(--wf-radius); background: var(--wf-surface-soft); } /* * The old rule treated the form's last direct

as the save footer. * The tabbed editor now has an explicit save bar. */ .admin-game-tabs-form > p:last-child { display: block; margin: initial; padding: initial; border: 0; background: transparent; } @media (max-width: 720px) { .admin-game-tabs { overflow-x: auto; margin-left: -18px; margin-right: -18px; padding-left: 18px; padding-right: 18px; } .admin-game-tab { white-space: nowrap; } .admin-game-save-bar { margin-left: -18px; margin-right: -18px; padding-left: 18px; padding-right: 18px; } } /* ========================================================================== WAYFARER_GAME_SETTINGS_BUILDER_MATCH_V23 Make Game Settings use the same visual language as the object editor in builder.php: wide editor card, compact tabs, 720px fields, and persistent save footer. ========================================================================== */ .wayfarer-game-edit-page > h1, .wayfarer-game-edit-page > .admin-back-link, .wayfarer-game-edit-page > .admin-error-message, .wayfarer-game-edit-page > .admin-form-card { max-width: 1400px; } /* Match the Builder editor card. */ .wayfarer-game-edit-page .admin-game-tabs-form { padding: 20px 24px 0; border: 1px solid var(--wf-border); border-radius: var(--wf-radius); background: var(--wf-surface); box-shadow: var(--wf-shadow); } /* Builder tabs sit inside the editor padding rather than spanning the card. */ .wayfarer-game-edit-page .admin-game-tabs { display: flex; gap: 4px; margin: 0 0 20px; padding: 0; border-bottom: 1px solid var(--wf-border); background: transparent; border-radius: 0; } .wayfarer-game-edit-page .admin-game-tab { position: relative; bottom: -1px; margin: 0; padding: 9px 14px !important; border: 1px solid transparent !important; border-bottom: 1px solid var(--wf-border) !important; border-radius: 0 !important; background: transparent !important; color: var(--wf-muted) !important; font: inherit; font-weight: 600 !important; cursor: pointer; } .wayfarer-game-edit-page .admin-game-tab:hover { color: var(--wf-text) !important; background: var(--wf-surface-soft) !important; } .wayfarer-game-edit-page .admin-game-tab.is-active { color: var(--wf-accent) !important; background: var(--wf-surface) !important; border-color: var(--wf-border) var(--wf-border) var(--wf-surface) !important; border-radius: 6px 6px 0 0 !important; } /* Match Builder field sizing. */ .wayfarer-game-edit-page .admin-game-tab-panel label { font-weight: 600; } .wayfarer-game-edit-page .admin-game-tab-panel small { color: var(--wf-muted); font-weight: 400; } .wayfarer-game-edit-page .admin-game-tab-panel input[type="text"], .wayfarer-game-edit-page .admin-game-tab-panel input[type="number"], .wayfarer-game-edit-page .admin-game-tab-panel input[type="file"], .wayfarer-game-edit-page .admin-game-tab-panel select, .wayfarer-game-edit-page .admin-game-tab-panel textarea { box-sizing: border-box; max-width: 100%; margin-top: 4px; padding: 7px 8px; border: 1px solid var(--wf-border-strong); border-radius: 5px; background: var(--wf-surface); color: var(--wf-text); font: inherit; } .wayfarer-game-edit-page .admin-game-tab-panel input[type="text"], .wayfarer-game-edit-page .admin-game-tab-panel textarea { width: min(100%, 720px); } .wayfarer-game-edit-page .admin-game-tab-panel textarea { resize: vertical; } /* Do not let old Game Settings paragraph/footer rules distort tab contents. */ .wayfarer-game-edit-page .admin-game-tab-panel > p { display: block; justify-content: initial; margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0; border: 0; border-radius: 0; background: transparent; } /* Match builder.php's save bar exactly. */ .wayfarer-game-edit-page .admin-game-save-bar { display: flex; justify-content: flex-end; margin: 22px -24px 0; padding: 14px 24px; border-top: 1px solid var(--wf-border); border-radius: 0 0 var(--wf-radius) var(--wf-radius); background: var(--wf-surface-soft); } .wayfarer-game-edit-page .admin-game-save-bar .admin-primary-button { padding: 9px 16px; } /* Trait fieldsets should also resemble Builder fieldsets. */ .wayfarer-game-edit-page .admin-game-tab-panel fieldset { margin: 0 0 16px; padding: 14px 16px; border: 1px solid var(--wf-border); border-radius: 7px; background: var(--wf-surface); } .wayfarer-game-edit-page .admin-game-tab-panel fieldset legend { padding: 0 6px; } /* Responsive behavior mirrors Builder. */ @media (max-width: 900px) { .wayfarer-game-edit-page .admin-game-tabs-form { padding: 18px 18px 0; } .wayfarer-game-edit-page .admin-game-save-bar { margin-left: -18px; margin-right: -18px; padding-left: 18px; padding-right: 18px; } } @media (max-width: 560px) { .wayfarer-game-edit-page .admin-game-tabs { overflow-x: auto; margin-left: 0; margin-right: 0; padding-left: 0; padding-right: 0; } .wayfarer-game-edit-page .admin-game-tab { white-space: nowrap; } } ============================================================================== FILE: assets/css/builder.css ============================================================================== /* |-------------------------------------------------------------------------- | Wayfarer Builder |-------------------------------------------------------------------------- | Master/detail authored-object workspace. | This stylesheet is intentionally presentation-only. */ :root { --wf-border: #d8dbe2; --wf-border-strong: #b8bdc7; --wf-surface: #ffffff; --wf-surface-soft: #f6f7f9; --wf-text: #20242a; --wf-muted: #667085; --wf-accent: #2f5ea8; --wf-accent-soft: #edf3fc; --wf-danger: #a61b2b; --wf-danger-soft: #fff4f5; --wf-radius: 8px; --wf-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); } .wayfarer-builder-page { margin: 0; padding: 24px; background: #f3f4f6; color: var(--wf-text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; line-height: 1.45; } .wayfarer-builder-page > h1, .wayfarer-builder-page > p, .wayfarer-builder-page > nav, .wayfarer-builder-page > .builder-workspace, .wayfarer-builder-page > .builder-back-link { max-width: 1400px; margin-left: auto; margin-right: auto; } .wayfarer-builder-page > h1 { margin-bottom: 2px; } .wayfarer-builder-page > h1 + p { margin-top: 0; color: var(--wf-muted); } .wayfarer-builder-page > nav { margin-top: 18px; margin-bottom: 18px; padding: 10px 14px; background: var(--wf-surface); border: 1px solid var(--wf-border); border-radius: var(--wf-radius); box-shadow: var(--wf-shadow); } .wayfarer-builder-page > nav a, .wayfarer-builder-page > nav strong { display: inline-block; margin-right: 18px; } .wayfarer-builder-page > hr { display: none; } /* ------------------------------------------------------------------ MASTER / DETAIL ------------------------------------------------------------------ */ .builder-workspace { display: grid; grid-template-columns: minmax(260px, 340px) minmax(0, 1fr); gap: 18px; align-items: start; } .builder-collection-pane, .builder-editor-pane { background: var(--wf-surface); border: 1px solid var(--wf-border); border-radius: var(--wf-radius); box-shadow: var(--wf-shadow); } .builder-collection-pane { position: sticky; top: 18px; max-height: calc(100vh - 36px); overflow: hidden; } .builder-collection-toolbar { padding: 14px; border-bottom: 1px solid var(--wf-border); background: var(--wf-surface-soft); } .builder-new-object { display: inline-block; padding: 8px 11px; border: 1px solid var(--wf-border-strong); border-radius: 6px; background: var(--wf-surface); text-decoration: none; font-weight: 600; } .builder-collection-scroll { overflow: auto; max-height: calc(100vh - 92px); padding: 14px; } .builder-collection-scroll > h2 { margin-top: 0; margin-bottom: 12px; font-size: 1.15rem; } .builder-collection-scroll ul { margin: 0; padding: 0; list-style: none; } .builder-collection-scroll li + li { margin-top: 4px; } .builder-collection-scroll li a { display: block; padding: 8px 9px; border-radius: 5px; text-decoration: none; } .builder-collection-scroll li a:hover { background: var(--wf-accent-soft); } .builder-collection-scroll table { width: 100%; border-collapse: collapse; font-size: 0.88rem; } .builder-collection-scroll th, .builder-collection-scroll td { padding: 7px 6px; border: 1px solid var(--wf-border); text-align: left; vertical-align: top; } .builder-collection-scroll th button { border: 0; padding: 0; background: transparent; font: inherit; font-weight: 600; cursor: pointer; } .builder-editor-pane { min-width: 0; padding: 20px 24px 24px; } .builder-editor-pane > h2 { margin-top: 0; margin-bottom: 14px; } /* WAYFARER_OBJECT_CONTEXT_HEADING_V34 */ .builder-object-heading { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 8px; } .builder-object-context { color: var(--wf-muted); font-weight: 600; } .builder-object-context[hidden] { display: none; } .builder-object-context-separator { margin-right: 8px; color: var(--wf-border-strong); } /* ------------------------------------------------------------------ TABBED OBJECT EDITOR ------------------------------------------------------------------ */ .builder-object-form { margin: 0; } .builder-editor-tabs { display: flex; gap: 4px; margin: 0 0 20px; padding: 0; border-bottom: 1px solid var(--wf-border); } .builder-editor-tab { position: relative; bottom: -1px; padding: 9px 14px; border: 1px solid transparent; border-bottom: 1px solid var(--wf-border); background: transparent; color: var(--wf-muted); font: inherit; font-weight: 600; cursor: pointer; } .builder-editor-tab:hover { color: var(--wf-text); background: var(--wf-surface-soft); } .builder-editor-tab.is-active { color: var(--wf-accent); background: var(--wf-surface); border-color: var(--wf-border) var(--wf-border) var(--wf-surface); border-radius: 6px 6px 0 0; } .builder-tab-panel[hidden] { display: none !important; } .builder-tab-panel > p:first-child { margin-top: 0; } /* ------------------------------------------------------------------ EXISTING FIELD CONTROLS ------------------------------------------------------------------ */ .builder-editor-pane label { font-weight: 600; } .builder-editor-pane small { color: var(--wf-muted); font-weight: 400; } .builder-editor-pane input[type="text"], .builder-editor-pane input[type="number"], .builder-editor-pane input[type="file"], .builder-editor-pane select, .builder-editor-pane textarea { box-sizing: border-box; max-width: 100%; margin-top: 4px; padding: 7px 8px; border: 1px solid var(--wf-border-strong); border-radius: 5px; background: var(--wf-surface); color: var(--wf-text); font: inherit; } .builder-editor-pane input[type="text"], .builder-editor-pane textarea { width: min(100%, 720px); } .builder-editor-pane textarea { resize: vertical; } .builder-editor-pane fieldset { margin: 0 0 16px; padding: 14px 16px; border: 1px solid var(--wf-border); border-radius: 7px; } .builder-editor-pane legend { padding: 0 6px; } .builder-editor-pane table { max-width: 100%; border-collapse: collapse; } .builder-editor-pane th, .builder-editor-pane td { padding: 7px; } /* System settings stay visually inside Basics. */ .builder-editor-pane [data-system-trait-settings] { margin: 16px 0; padding: 14px 16px; border: 1px solid var(--wf-border); border-radius: 7px; background: var(--wf-surface-soft); } .builder-editor-pane [data-system-trait-settings] h3 { margin-top: 0; } /* ------------------------------------------------------------------ SAVE / DELETE ------------------------------------------------------------------ */ .builder-save-bar { display: flex; justify-content: flex-end; margin: 22px -24px -24px; padding: 14px 24px; border-top: 1px solid var(--wf-border); background: var(--wf-surface-soft); border-radius: 0 0 var(--wf-radius) var(--wf-radius); } .builder-save-bar button { padding: 9px 16px; border: 1px solid #214b86; border-radius: 6px; background: var(--wf-accent); color: #fff; font: inherit; font-weight: 600; cursor: pointer; } .builder-editor-pane [data-builder-delete-form] { margin-top: 28px; padding: 16px; border: 1px solid #e8b8be; border-radius: 7px; background: var(--wf-danger-soft); } .builder-editor-pane [data-builder-delete-form] h3 { margin-top: 0; color: var(--wf-danger); } .builder-editor-pane [data-builder-delete-form] button { padding: 8px 12px; border: 1px solid var(--wf-danger); border-radius: 5px; background: #fff; color: var(--wf-danger) !important; font: inherit; font-weight: 600; cursor: pointer; } .builder-back-link { margin-top: 18px; } /* ------------------------------------------------------------------ RESPONSIVE ------------------------------------------------------------------ */ @media (max-width: 900px) { .wayfarer-builder-page { padding: 14px; } .builder-workspace { grid-template-columns: 1fr; } .builder-collection-pane { position: static; max-height: none; } .builder-collection-scroll { max-height: 300px; } .builder-editor-pane { padding: 18px; } .builder-save-bar { margin-left: -18px; margin-right: -18px; margin-bottom: -18px; padding-left: 18px; padding-right: 18px; } } @media (max-width: 560px) { .builder-editor-tabs { overflow-x: auto; } .builder-editor-tab { white-space: nowrap; } } /* |-------------------------------------------------------------------------- | REGION TICK EFFECTS v18 |-------------------------------------------------------------------------- */ .builder-tick-effect-row { display: grid; grid-template-columns: minmax(220px, 2fr) minmax(130px, 1fr) minmax(110px, 1fr) auto; gap: 12px; align-items: end; margin: 0 0 12px; padding: 14px; border: 1px solid var(--wf-border); border-radius: 7px; background: var(--wf-surface-soft); } .builder-tick-effect-row > p { margin: 0; } .builder-tick-effect-row select, .builder-tick-effect-row input { width: 100%; box-sizing: border-box; } .builder-tick-effect-remove { align-self: end; } @media (max-width: 760px) { .builder-tick-effect-row { grid-template-columns: 1fr; } } /* |-------------------------------------------------------------------------- | TICK EFFECT RESULT BRANCHES v28 |-------------------------------------------------------------------------- | WAYFARER_TICK_EFFECT_BRANCHES_V28 */ .builder-tick-effect-row { display: block; margin: 0 0 16px; padding: 16px; } .builder-tick-effect-header { display: grid; grid-template-columns: minmax(260px, 1fr) auto; gap: 16px; align-items: end; margin-bottom: 16px; } .builder-tick-effect-target select { display: block; width: 100%; margin-top: 6px; } .builder-tick-result-list { display: grid; gap: 12px; } .builder-tick-result-branch { padding: 14px; border: 1px solid var(--wf-border); border-radius: 7px; background: var(--wf-surface); } .builder-tick-result-branch.is-otherwise { background: var(--wf-surface-soft); } .builder-tick-result-heading { display: flex; justify-content: space-between; gap: 12px; align-items: center; margin-bottom: 12px; } .builder-tick-result-order { display: flex; gap: 6px; align-items: center; } .builder-tick-result-condition { margin-bottom: 10px; } .builder-tick-result-then { margin: 10px 0 8px; } .builder-tick-result-fields { display: grid; grid-template-columns: minmax(150px, 0.8fr) minmax(120px, 0.7fr) minmax(120px, 0.7fr) minmax(180px, 1fr); gap: 12px; align-items: end; } .builder-tick-result-fields select, .builder-tick-result-fields input { display: block; width: 100%; margin-top: 6px; } .builder-tick-result-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; } .builder-condition-group { padding: 10px; border: 1px solid var(--wf-border); border-radius: 6px; background: var(--wf-surface-soft); } .builder-condition-group-header, .builder-condition-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } .builder-condition-children { display: grid; gap: 8px; margin: 10px 0; } .builder-condition-row { display: grid; grid-template-columns: minmax(180px, 1.4fr) minmax(180px, 1fr) minmax(120px, 1fr) auto; gap: 8px; align-items: end; } .builder-condition-row select, .builder-condition-row input, .builder-condition-group-header select { width: 100%; box-sizing: border-box; } @media (max-width: 760px) { .builder-tick-effect-header, .builder-tick-result-fields, .builder-condition-row { grid-template-columns: 1fr; } .builder-tick-result-heading { align-items: flex-start; flex-direction: column; } } /* ------------------------------------------------------------------ STARTING CHOICES WAYFARER_STARTING_CHOICES_V36 ------------------------------------------------------------------ */ .builder-starting-choices-editor { margin-top: 24px; } .builder-starting-choices-heading, .builder-starting-choice-header, .builder-starting-option-header, .builder-starting-effects-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } .builder-starting-choices-heading h3 { margin: 0 0 4px; } .builder-starting-choices-heading p { margin: 0; } .builder-starting-choice { margin-top: 16px; padding: 16px; border: 1px solid var(--wf-border); border-radius: var(--wf-radius); background: var(--wf-surface-soft); } .builder-starting-choice-fields { display: grid; grid-template-columns: minmax(0, 1fr) 140px; gap: 12px; margin-top: 14px; } .builder-starting-choice-fields label, .builder-starting-option-fields label, .builder-starting-effect label { display: grid; gap: 5px; font-weight: 600; } .builder-starting-choice-fields input, .builder-starting-choice-fields select, .builder-starting-option-fields input, .builder-starting-option-fields textarea, .builder-starting-effect input, .builder-starting-effect select { width: 100%; font: inherit; } .builder-starting-options { display: grid; gap: 12px; margin: 16px 0 12px; } .builder-starting-option { padding: 14px; border: 1px solid var(--wf-border); border-radius: 7px; background: var(--wf-surface); } .builder-starting-option-fields { display: grid; grid-template-columns: minmax(180px, 0.7fr) minmax(260px, 1.3fr); gap: 12px; margin-top: 12px; } .builder-starting-effects { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--wf-border); } .builder-starting-effects-heading { margin-bottom: 8px; } .builder-starting-effects-heading small { color: var(--wf-muted); } .builder-starting-effect { display: grid; grid-template-columns: minmax(180px, 1.4fr) minmax(110px, 0.65fr) minmax(110px, 0.65fr) minmax(110px, 0.65fr) minmax(180px, 1fr) auto; gap: 10px; align-items: end; margin-bottom: 8px; } @media (max-width: 800px) { .builder-starting-choice-fields, .builder-starting-option-fields, .builder-starting-effect { grid-template-columns: 1fr; } } /* ------------------------------------------------------------------ ONE-TIME EFFECTS WAYFARER_ONE_TIME_EFFECTS_EDITOR_V42 ------------------------------------------------------------------ */ .builder-effects-panel > hr { margin: 28px 0; border: 0; border-top: 1px solid var(--wf-border, #d8dbe2); } .builder-effects-list { display: grid; gap: 10px; margin: 14px 0; } .builder-effect-row { display: grid; grid-template-columns: minmax(220px, 1.5fr) minmax(130px, 0.7fr) minmax(110px, 0.6fr) auto; gap: 10px; align-items: end; padding: 12px; border: 1px solid var(--wf-border, #d8dbe2); border-radius: 8px; background: #ffffff; } .builder-effect-row label { display: grid; gap: 5px; } .builder-effect-row select, .builder-effect-row input { width: 100%; } .builder-effect-remove { align-self: end; } @media (max-width: 850px) { .builder-effect-row { grid-template-columns: 1fr; } .builder-effect-remove { justify-self: start; } } /* ------------------------------------------------------------------ GENERALIZED EFFECTS WAYFARER_GENERALIZED_EFFECT_EDITOR_V43 ------------------------------------------------------------------ */ /* * v42 displayed an Effect as one horizontal operation row. v43 Effects own * ordered result branches, so use the same card language as Tick Effects. */ .builder-effect-row { display: block; grid-template-columns: none; padding: 14px; } .builder-effect-row .builder-tick-effect-header { margin-bottom: 12px; } .builder-effect-value-fields { display: grid; grid-template-columns: minmax(120px, 0.75fr) minmax(120px, 0.75fr) minmax(120px, 0.75fr); gap: 10px; align-items: end; } .builder-effect-random-range { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .builder-effect-random-range > label { display: grid; gap: 5px; } [data-effect-fixed-wrap][hidden], [data-effect-random-wrap][hidden], [data-effect-value-mode-wrap][hidden] { display: none !important; } /* WAYFARER_DATATYPE_TARGET_UI_V58 */ [data-condition-value-input][hidden], [data-condition-value-select][hidden], [data-starting-value-mode-wrap][hidden], [data-starting-fixed-wrap][hidden], [data-starting-random-wrap][hidden], [data-starting-fixed-input][hidden], [data-starting-fixed-choice][hidden], [data-tick-value-mode-wrap][hidden], [data-tick-branch-value-wrap][hidden], [data-tick-random-wrap][hidden], [data-tick-fixed-choice][hidden], [data-effect-fixed-choice][hidden] { display: none !important; } @media (max-width: 850px) { .builder-effect-value-fields { grid-template-columns: 1fr; } .builder-effect-random-range { grid-template-columns: 1fr; } } /* ------------------------------------------------------------------ STORYLET BRANCHES WAYFARER_STORYLET_BUILDER_V56 ------------------------------------------------------------------ */ .builder-storylet-branches { max-width: 100%; overflow-x: auto; } .builder-storylet-branches table { width: 100%; border-collapse: collapse; } .builder-storylet-branches th, .builder-storylet-branches td { padding: 9px 10px; border: 1px solid var(--wf-border); text-align: left; vertical-align: top; } .builder-storylet-branches th { background: var(--wf-surface-soft); } /* ------------------------------------------------------------------ STATUS LIFECYCLE WAYFARER_STATUS_LIFECYCLE_UI_V88 ------------------------------------------------------------------ */ .wayfarer-status-lifecycle-heading, .wayfarer-status-stage-heading, .wayfarer-status-outcomes-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } .wayfarer-status-lifecycle-heading h4, .wayfarer-status-outcomes-heading strong { margin: 0; } .wayfarer-status-stage { margin-top: 16px; padding: 16px; border: 1px solid var(--wf-border); border-radius: 8px; background: var(--wf-surface-soft); } .wayfarer-status-stage-actions { display: flex; flex-wrap: wrap; gap: 6px; } .wayfarer-status-stage-fields, .wayfarer-status-outcome-fields { display: grid; gap: 12px; margin-top: 14px; } .wayfarer-status-stage-fields { grid-template-columns: minmax(180px, 1fr) repeat(4, minmax(130px, 0.7fr)); } .wayfarer-status-outcome-fields { grid-template-columns: minmax(180px, 1fr) minmax(90px, 0.45fr) minmax(210px, 1fr) minmax(150px, 0.7fr) minmax(220px, 1fr); } .wayfarer-status-stage-fields label, .wayfarer-status-outcome-fields label { display: grid; gap: 5px; } .wayfarer-status-stage-fields input, .wayfarer-status-stage-fields select, .wayfarer-status-outcome-fields input, .wayfarer-status-outcome-fields select { width: 100%; } .wayfarer-status-outcomes-heading { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--wf-border); } .wayfarer-status-outcomes-heading small { display: block; margin-top: 2px; } .wayfarer-status-outcome { margin-top: 10px; padding: 12px; border: 1px solid var(--wf-border); border-radius: 7px; background: var(--wf-surface); } .wayfarer-status-outcome > button { margin-top: 10px; } .wayfarer-status-lifecycle-note { margin-top: 18px; } @media (max-width: 1050px) { .wayfarer-status-stage-fields, .wayfarer-status-outcome-fields { grid-template-columns: 1fr 1fr; } } @media (max-width: 700px) { .wayfarer-status-lifecycle-heading, .wayfarer-status-stage-heading, .wayfarer-status-outcomes-heading { flex-direction: column; } .wayfarer-status-stage-fields, .wayfarer-status-outcome-fields { grid-template-columns: 1fr; } } ============================================================================== FILE: assets/css/hierarchical-targets.css ============================================================================== /* * Wayfarer Hierarchical Target Picker * WAYFARER_HIERARCHICAL_TARGETS_V80 * WAYFARER_HIERARCHICAL_LAYOUT_V81 */ .wayfarer-hierarchy-picker { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; width: 100%; min-width: 0; margin-top: 6px; } .wayfarer-hierarchy-level { min-width: 0; } .wayfarer-hierarchy-label { display: block; margin-bottom: 4px; font-weight: 600; } .wayfarer-hierarchy-search, .wayfarer-hierarchy-select { box-sizing: border-box; width: 100%; min-width: 0; } .wayfarer-hierarchy-search { margin-bottom: 5px; } .wayfarer-hierarchy-level[hidden] { display: none !important; } /* * The canonical target select stays in the form so all existing save/runtime * behavior remains unchanged. V81 moves the generated hierarchy outside its * original label, then hides that now-redundant source label. */ .wayfarer-hierarchy-canonical, .wayfarer-hierarchy-source-label { display: none !important; } /* * CONDITIONS * * Row 1: Type -> Target -> Property hierarchy. * Row 2: Comparison | Value | Remove. * * A hidden Value control (for Location anchors, etc.) naturally collapses out * of the second row without changing the underlying Condition shape. */ .builder-condition-row.has-wayfarer-hierarchy { display: grid !important; grid-template-columns: minmax(180px, 1fr) minmax(180px, 1fr) auto !important; gap: 12px !important; align-items: end !important; } .builder-condition-row.has-wayfarer-hierarchy > .wayfarer-hierarchy-picker { grid-column: 1 / -1; margin-top: 0; } .builder-condition-row.has-wayfarer-hierarchy > label:not(.wayfarer-hierarchy-source-label) { min-width: 0; } .builder-condition-row.has-wayfarer-hierarchy > label:not(.wayfarer-hierarchy-source-label) > select, .builder-condition-row.has-wayfarer-hierarchy > label:not(.wayfarer-hierarchy-source-label) > input { box-sizing: border-box; max-width: 100%; } .builder-condition-row.has-wayfarer-hierarchy > [data-remove-condition-node] { align-self: end; } /* * ONE-TIME EFFECTS / TICK EFFECTS * * The hierarchy gets the flexible column; Remove remains a compact action * column. This prevents the three hierarchy levels from fighting the old * single-select header width. */ .builder-tick-effect-header.has-wayfarer-hierarchy { display: grid !important; grid-template-columns: minmax(0, 1fr) auto !important; gap: 12px !important; align-items: end !important; } .builder-tick-effect-header.has-wayfarer-hierarchy > .wayfarer-hierarchy-picker { grid-column: 1; margin-top: 0; } .builder-tick-effect-header.has-wayfarer-hierarchy > [data-remove-effect], .builder-tick-effect-header.has-wayfarer-hierarchy > [data-remove-tick-effect] { grid-column: 2; align-self: end; } @media (max-width: 900px) { .wayfarer-hierarchy-picker { grid-template-columns: 1fr; } .builder-condition-row.has-wayfarer-hierarchy, .builder-tick-effect-header.has-wayfarer-hierarchy { grid-template-columns: 1fr !important; } .builder-condition-row.has-wayfarer-hierarchy > .wayfarer-hierarchy-picker, .builder-tick-effect-header.has-wayfarer-hierarchy > .wayfarer-hierarchy-picker, .builder-tick-effect-header.has-wayfarer-hierarchy > [data-remove-effect], .builder-tick-effect-header.has-wayfarer-hierarchy > [data-remove-tick-effect] { grid-column: 1; } } ============================================================================== FILE: assets/css/status-subject-effects.css ============================================================================== /* WAYFARER_STATUS_SUBJECT_EFFECTS_UI_V89 */ .wayfarer-status-stage-effects, .wayfarer-status-outcome-effects { margin-top: 16px; } .wayfarer-status-effects-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--wf-border); } .wayfarer-status-effects-heading small { display: block; margin-top: 2px; } .wayfarer-status-effect { display: grid; grid-template-columns: minmax(170px, 1fr) minmax(170px, 1fr) minmax(120px, 0.7fr) minmax(120px, 0.7fr) minmax(110px, 0.65fr) minmax(110px, 0.65fr) auto; gap: 10px; align-items: end; margin-top: 10px; padding: 11px; border: 1px solid var(--wf-border); border-radius: 7px; background: #fff; } .wayfarer-status-effect label { display: grid; gap: 5px; min-width: 0; } .wayfarer-status-effect select, .wayfarer-status-effect input { width: 100%; min-width: 0; } .wayfarer-status-effect-inline-number { display: grid; grid-template-columns: minmax(70px, 1fr) auto; gap: 6px; align-items: center; } .wayfarer-status-effect > button { align-self: end; } @media (max-width: 1150px) { .wayfarer-status-effect { grid-template-columns: 1fr 1fr 1fr; } } @media (max-width: 700px) { .wayfarer-status-effects-heading { flex-direction: column; } .wayfarer-status-effect { grid-template-columns: 1fr; } } ============================================================================== FILE: assets/js/builder.js ============================================================================== const wayfarerConditionData = window.wayfarerConditionData || {}; function updateConditionalConfigFields() { document .querySelectorAll('[data-config-field]') .forEach(function (container) { const relationshipName = container.dataset.conditionRelationship; const requiredValue = container.dataset.conditionEquals; if (!relationshipName) { container.style.display = ''; return; } const relationshipSelect = document.querySelector( '[data-relationship="' + relationshipName + '"]' ); if (!relationshipSelect) { container.style.display = 'none'; return; } const selectedId = relationshipSelect.value; const actualValue = wayfarerConditionData[relationshipName]?.[selectedId]; container.style.display = actualValue === requiredValue ? '' : 'none'; }); } document .querySelectorAll('[data-relationship]') .forEach(function (select) { select.addEventListener( 'change', updateConditionalConfigFields ); }); // ================================================================ // SYSTEM TRAIT GROUPS // ================================================================ function updateSystemTraitGroups(container) { const selectorField = container.dataset.selectorField; if (!selectorField) { return; } const selector = document.querySelector( '[name="' + selectorField + '"]' ); if (!selector) { return; } const selectedGroup = selector.value; container .querySelectorAll('[data-system-trait-group]') .forEach(function (group) { const active = group.dataset.systemTraitGroup === selectedGroup; group.style.display = active ? '' : 'none'; group .querySelectorAll('input, select, textarea') .forEach(function (input) { input.disabled = !active; }); }); } document .querySelectorAll('[data-system-trait-settings]') .forEach(function (container) { const selectorField = container.dataset.selectorField; const selector = selectorField ? document.querySelector( '[name="' + selectorField + '"]' ) : null; if (selector) { selector.addEventListener( 'change', function () { updateSystemTraitGroups(container); } ); } updateSystemTraitGroups(container); }); // ================================================================ // COLLECTION SORTING // ================================================================ document .querySelectorAll('[data-sort-column]') .forEach(function (button) { button.addEventListener('click', function () { const table = button.closest('[data-collection-table]'); if (!table) { return; } const tbody = table.querySelector('tbody'); const columnName = button.dataset.sortColumn; const sortType = button.dataset.sortType || 'text'; const currentDirection = button.dataset.sortDirection || 'none'; const nextDirection = currentDirection === 'asc' ? 'desc' : 'asc'; table .querySelectorAll('[data-sort-column]') .forEach(function (otherButton) { otherButton.dataset.sortDirection = 'none'; }); button.dataset.sortDirection = nextDirection; const rows = Array.from(tbody.querySelectorAll('tr')); rows.sort(function (a, b) { const aCell = a.querySelector( '[data-column="' + columnName + '"]' ); const bCell = b.querySelector( '[data-column="' + columnName + '"]' ); const aValue = aCell?.dataset.sortValue ?? ''; const bValue = bCell?.dataset.sortValue ?? ''; let comparison = 0; if (sortType === 'number') { const aNumber = aValue === '' ? Number.POSITIVE_INFINITY : Number(aValue); const bNumber = bValue === '' ? Number.POSITIVE_INFINITY : Number(bValue); comparison = aNumber - bNumber; } else { comparison = aValue.localeCompare( bValue, undefined, { numeric: true, sensitivity: 'base' } ); } return nextDirection === 'asc' ? comparison : -comparison; }); rows.forEach(function (row) { tbody.appendChild(row); }); }); }); updateConditionalConfigFields(); // ================================================================ // MEDIA LIBRARY CONTROLS // ================================================================ function updateBuilderMediaSlot(slotContainer) { const checkedAction = slotContainer.querySelector( 'input[type="radio"][name^="media_action"]:checked' ); const action = checkedAction ? checkedAction.value : 'none'; const existingSelect = slotContainer.querySelector( '[data-media-existing]' ); if (existingSelect) { existingSelect.disabled = action !== 'existing'; } slotContainer .querySelectorAll( '[data-media-upload-fields] input' ) .forEach(function (input) { input.disabled = action !== 'upload'; }); } document .querySelectorAll('[data-media-slot]') .forEach(function (slotContainer) { slotContainer .querySelectorAll( 'input[type="radio"][name^="media_action"]' ) .forEach(function (radio) { radio.addEventListener( 'change', function () { updateBuilderMediaSlot( slotContainer ); } ); }); updateBuilderMediaSlot( slotContainer ); }); // ================================================================ // AUTHORED OBJECT CONTEXT HEADING // WAYFARER_OBJECT_CONTEXT_HEADING_V34 // ================================================================ function initializeBuilderObjectContextHeading() { const editor = document.querySelector( '[data-builder-editor]' ); if (!editor) { return; } const heading = editor.querySelector( '[data-builder-object-heading]' ); if (!heading) { return; } const fieldName = heading.dataset.builderObjectLabelField || 'name'; const form = editor.querySelector( 'form' ); if (!form) { return; } const field = Array .from( form.elements ) .find(function (element) { return ( element && element.name === fieldName ); }); if (!field) { return; } const context = heading.querySelector( '[data-builder-object-context]' ); const label = heading.querySelector( '[data-builder-object-context-label]' ); if (!context || !label) { return; } function refreshContextHeading() { const value = String( field.value || '' ).trim(); label.textContent = value; context.hidden = value === ''; } field.addEventListener( 'input', refreshContextHeading ); field.addEventListener( 'change', refreshContextHeading ); refreshContextHeading(); } initializeBuilderObjectContextHeading(); // ================================================================ // AUTHORED OBJECT EDITOR TABS // ================================================================ function initializeBuilderEditorTabs() { const editor = document.querySelector( '[data-builder-editor]' ); if (!editor) { return; } /* * The editor component has one primary save form. The separate delete * form, when present, lives later in the editor pane and is deliberately * left outside the tab system. */ const form = Array .from(editor.children) .find(function (child) { return child.tagName === 'FORM'; }); if ( !form || form.dataset.builderTabbed === '1' ) { return; } form.dataset.builderTabbed = '1'; form.classList.add( 'builder-object-form' ); /* * WAYFARER_ACTIVE_TAB_V20 * * Persist the selected editor tab through POST/redirect/reload. */ const activeTabInput = document.createElement( 'input' ); activeTabInput.type = 'hidden'; activeTabInput.name = 'active_tab'; const requestedTab = new URLSearchParams( window.location.search ).get( 'tab' ) || 'basics'; activeTabInput.value = requestedTab; form.appendChild( activeTabInput ); const originalChildren = Array.from( form.children ); const panels = { basics: document.createElement('div'), media: document.createElement('div'), traits: document.createElement('div'), /* WAYFARER_CONDITIONS_PANEL_V45_1 */ conditions: document.createElement('div'), /* WAYFARER_LINEAR_TRAVEL_TAB_V72 */ travel: document.createElement('div'), /* WAYFARER_STATUS_LIFECYCLE_TAB_V88 */ progression: document.createElement('div'), tick_effects: document.createElement('div'), /* WAYFARER_STORYLET_BUILDER_V56 */ /* WAYFARER_STORYLET_BRANCH_PANEL_KEY_V57 */ branches: document.createElement('div') }; panels.basics.className = 'builder-tab-panel'; panels.media.className = 'builder-tab-panel'; panels.traits.className = 'builder-tab-panel'; panels.conditions.className = 'builder-tab-panel'; panels.travel.className = 'builder-tab-panel'; panels.progression.className = 'builder-tab-panel'; panels.branches.className = 'builder-tab-panel'; panels.basics.dataset.builderPanel = 'basics'; panels.media.dataset.builderPanel = 'media'; panels.traits.dataset.builderPanel = 'traits'; panels.conditions.dataset.builderPanel = 'conditions'; panels.travel.dataset.builderPanel = 'travel'; panels.progression.dataset.builderPanel = 'progression'; panels.branches.dataset.builderPanel = 'branches'; panels.tick_effects.className = 'builder-tab-panel'; panels.tick_effects.dataset.builderPanel = 'tick_effects'; let section = 'basics'; let saveContainer = null; let hasMedia = false; let hasTraits = false; let hasConditions = false; let hasTravel = false; let hasProgression = false; let hasTickEffects = false; let hasBranches = false; originalChildren.forEach( function (child) { /* * Keep hidden record identifiers directly in the form. */ if ( child.tagName === 'INPUT' && child.type === 'hidden' ) { return; } /* * Save becomes a stable footer beneath the tab panels. */ const submitButton = child.querySelector ? child.querySelector( 'button[type="submit"], input[type="submit"]' ) : null; if (submitButton) { saveContainer = child; return; } /* * Ignore the separator immediately before a tab heading. * Other HR elements are harmless but unnecessary inside the * cleaned-up editor. */ if (child.tagName === 'HR') { child.remove(); return; } if (child.tagName === 'H3') { const heading = child.textContent .trim() .toLowerCase(); if (heading === 'media') { section = 'media'; hasMedia = true; child.remove(); return; } if (heading === 'traits') { section = 'traits'; hasTraits = true; child.remove(); return; } } /* * WAYFARER_CONDITIONS_PANEL_V45_1 * * Conditions answer whether an authored object is eligible; * Effects answer what changes when it triggers. */ if ( child.hasAttribute && child.hasAttribute( 'data-builder-conditions-panel' ) ) { section = 'conditions'; hasConditions = true; } /* WAYFARER_LINEAR_TRAVEL_TAB_V72 */ if ( child.hasAttribute && child.hasAttribute( 'data-builder-travel-panel' ) ) { section = 'travel'; hasTravel = true; } /* WAYFARER_STATUS_LIFECYCLE_TAB_V88 */ if ( child.hasAttribute && child.hasAttribute( 'data-builder-status-lifecycle' ) ) { section = 'progression'; hasProgression = true; } /* * WAYFARER_ONE_TIME_EFFECTS_EDITOR_V42 * * Location Effects and Tick Effects share one Effects panel. */ if ( child.hasAttribute && ( child.hasAttribute( 'data-builder-effects-panel' ) || child.hasAttribute( 'data-builder-tick-effects' ) ) ) { section = 'tick_effects'; hasTickEffects = true; } /* WAYFARER_STORYLET_BRANCHES_PANEL_V56 */ if ( child.hasAttribute && child.hasAttribute( 'data-builder-branches-panel' ) ) { section = 'branches'; hasBranches = true; } /* * WAYFARER_LINEAR_TRAVEL_TAB_V72 * * Distance Label is existing Region travel configuration, so move * it into the Travel tab when that tab exists. Other config fields * retain the original Basics behavior. */ if ( child.hasAttribute && child.hasAttribute( 'data-config-field' ) ) { const configFieldName = child.dataset.configField || ''; const hasTravelEditor = !!form.querySelector( '[data-builder-travel-panel]' ); if ( configFieldName === 'distance_label' && hasTravelEditor ) { section = 'travel'; hasTravel = true; } else { section = 'basics'; } } /* Keep the existing Region HUD in Basics. */ if ( child.hasAttribute && child.hasAttribute( 'data-region-hud-editor' ) ) { section = 'basics'; } /* * A Media fieldset is definitive even if future markup changes * around the Media heading. */ if ( child.hasAttribute && child.hasAttribute( 'data-media-slot' ) ) { section = 'media'; hasMedia = true; } panels[section] .appendChild( child ); } ); /* * If Traits exist, anything moved after the Traits heading belongs there. * System traits do not use that heading and therefore stay in Basics. */ if ( panels.traits.children.length > 0 ) { hasTraits = true; } if ( panels.media.children.length > 0 ) { hasMedia = true; } if ( panels.conditions.children.length > 0 ) { hasConditions = true; } if ( panels.travel.children.length > 0 ) { hasTravel = true; } if ( panels.progression.children.length > 0 ) { hasProgression = true; } if ( panels.tick_effects.children.length > 0 ) { hasTickEffects = true; } if ( panels.branches.children.length > 0 ) { hasBranches = true; } const tabBar = document.createElement( 'div' ); tabBar.className = 'builder-editor-tabs'; tabBar.setAttribute( 'role', 'tablist' ); const availablePanels = [ { key: 'basics', label: 'Basics', show: true }, { key: 'media', label: 'Media', show: hasMedia }, { key: 'traits', label: 'Traits', show: hasTraits }, { key: 'travel', label: 'Travel', show: hasTravel }, { key: 'progression', label: 'Progression', show: hasProgression }, { key: 'conditions', label: 'Conditions', show: hasConditions }, { key: 'tick_effects', label: 'Effects', show: hasTickEffects }, { key: 'branches', label: 'Branches', show: hasBranches } ].filter( function (item) { return item.show; } ); function activatePanel(panelKey) { activeTabInput.value = panelKey; tabBar .querySelectorAll( '[data-builder-tab]' ) .forEach( function (button) { const active = button.dataset.builderTab === panelKey; button.classList.toggle( 'is-active', active ); button.setAttribute( 'aria-selected', active ? 'true' : 'false' ); } ); Object .entries(panels) .forEach( function (entry) { const key = entry[0]; const panel = entry[1]; panel.hidden = key !== panelKey; } ); } availablePanels.forEach( function (item) { const button = document.createElement( 'button' ); button.type = 'button'; button.className = 'builder-editor-tab'; button.dataset.builderTab = item.key; button.setAttribute( 'role', 'tab' ); button.textContent = item.label; button.addEventListener( 'click', function () { activatePanel( item.key ); } ); tabBar.appendChild( button ); } ); /* * Insert the new structure after hidden inputs so all form fields remain * part of the same form and submit exactly as before. */ form.appendChild( tabBar ); availablePanels.forEach( function (item) { form.appendChild( panels[item.key] ); } ); if (saveContainer) { saveContainer.classList.add( 'builder-save-bar' ); form.appendChild( saveContainer ); } const initialPanelKey = availablePanels.some( function (item) { return item.key === requestedTab; } ) ? requestedTab : 'basics'; activatePanel( initialPanelKey ); } initializeBuilderEditorTabs(); // ================================================================ // SHARED GAMEPLAY TICK EFFECTS CONTROLS v25 // WAYFARER_GAMEPLAY_MODULAR_V25 // ================================================================ function initializeTickEffects() { const editor = document.querySelector( '[data-builder-editor]' ); if (!editor) { return; } const wrapper = editor.querySelector( '[data-builder-tick-effects]' ); if (!wrapper) { return; } if ( wrapper.dataset.tickEffectsReady === '1' ) { return; } wrapper.dataset.tickEffectsReady = '1'; // ------------------------------------------------------------ // ROW ADD / REMOVE // ------------------------------------------------------------ const list = wrapper.querySelector( '[data-tick-effects-list]' ); const addButton = wrapper.querySelector( '[data-add-tick-effect]' ); const template = wrapper.querySelector( '[data-tick-effect-template]' ); let nextIndex = 0; wrapper .querySelectorAll( '[data-tick-effect-row]' ) .forEach( function (row) { const index = Number( row.dataset.tickIndex ); if ( Number.isFinite(index) && index >= nextIndex ) { nextIndex = index + 1; } } ); function updateLinearTargetAvailability() { const navigationSelect = editor.querySelector( 'select[name="navigation_type"]' ); const navigationType = navigationSelect ? navigationSelect.value : wrapper.dataset.navigationType; const linear = navigationType === 'linear'; wrapper .querySelectorAll( '[data-linear-navigation-target]' ) .forEach( function (option) { option.disabled = !linear; } ); if (!linear) { wrapper .querySelectorAll( '[data-tick-effect-target]' ) .forEach( function (select) { if ( select.value === 'navigation:position' ) { select.value = ''; } } ); } } if ( addButton && list && template ) { addButton.addEventListener( 'click', function () { const html = template.innerHTML.replace( /__INDEX__/g, String( nextIndex++ ) ); const temporary = document.createElement( 'div' ); temporary.innerHTML = html.trim(); const row = temporary.firstElementChild; if (!row) { return; } list.appendChild( row ); updateLinearTargetAvailability(); } ); } wrapper.addEventListener( 'click', function (event) { const removeButton = event.target.closest( '[data-remove-tick-effect]' ); if (!removeButton) { return; } const row = removeButton.closest( '[data-tick-effect-row]' ); if (row) { row.remove(); } } ); const navigationSelect = editor.querySelector( 'select[name="navigation_type"]' ); if (navigationSelect) { navigationSelect.addEventListener( 'change', updateLinearTargetAvailability ); } updateLinearTargetAvailability(); /* * WAYFARER_EMBEDDED_TICK_EFFECTS_V33 * * Game Settings already owns a Gameplay tab. In embedded mode we keep * this shared editor exactly where PHP rendered it while retaining the * normal Tick Effect, condition, and result-branch controls. */ if ( wrapper.hasAttribute( 'data-tick-effects-embedded' ) ) { return; } // ------------------------------------------------------------ // MOVE THE SECTION INTO ITS OWN BUILDER TAB // ------------------------------------------------------------ const form = wrapper.closest( 'form' ); if (!form) { return; } const tabBar = form.querySelector( '.builder-editor-tabs' ); if (!tabBar) { /* * Builder without the v10 tab layout: * leave the Tick Effects section inline. */ return; } if ( form.querySelector( '[data-builder-panel="tick_effects"]' ) ) { return; } const panel = document.createElement( 'div' ); panel.className = 'builder-tab-panel'; panel.dataset.builderPanel = 'tick_effects'; panel.hidden = true; panel.appendChild( wrapper ); const saveBar = form.querySelector( '.builder-save-bar' ); if (saveBar) { form.insertBefore( panel, saveBar ); } else { form.appendChild( panel ); } const button = document.createElement( 'button' ); button.type = 'button'; button.className = 'builder-editor-tab'; button.dataset.builderTab = 'tick_effects'; button.setAttribute( 'role', 'tab' ); button.setAttribute( 'aria-selected', 'false' ); /* WAYFARER_GAMEPLAY_TAB_LABEL_V25_1 */ button.textContent = 'Effects'; tabBar.appendChild( button ); function activateTickEffectsTab() { tabBar .querySelectorAll( '[data-builder-tab]' ) .forEach( function (tabButton) { const active = tabButton === button; tabButton.classList.toggle( 'is-active', active ); tabButton.setAttribute( 'aria-selected', active ? 'true' : 'false' ); } ); form .querySelectorAll( '[data-builder-panel]' ) .forEach( function (tabPanel) { tabPanel.hidden = tabPanel.dataset.builderPanel !== 'tick_effects'; } ); } button.addEventListener( 'click', activateTickEffectsTab ); /* * The original tab controller does not know about the panel added * afterward, so explicitly hide our panel whenever an original tab * is clicked. */ tabBar .querySelectorAll( '[data-builder-tab]:not([data-builder-tab="tick_effects"])' ) .forEach( function (originalButton) { originalButton.addEventListener( 'click', function () { panel.hidden = true; button.classList.remove( 'is-active' ); button.setAttribute( 'aria-selected', 'false' ); } ); } ); } // ================================================================ // SHARED ONE-TIME EFFECTS CONTROLS // WAYFARER_ONE_TIME_EFFECTS_EDITOR_V42 // WAYFARER_GENERALIZED_EFFECT_EDITOR_V43 // ================================================================ function wayfarerEffectConditionTargets( wrapper ) { const targetJson = wrapper.querySelector( '[data-effect-condition-targets-json]' ); if (!targetJson) { return {}; } try { return JSON.parse( targetJson.textContent || '{}' ); } catch (error) { return {}; } } /* WAYFARER_DATATYPE_EFFECT_UI_V58 */ function wayfarerEffectTargetCatalog( wrapper ) { const targetJson = wrapper ? wrapper.querySelector( '[data-effect-target-catalog-json]' ) : null; if (!targetJson) { return {}; } try { return JSON.parse( targetJson.textContent || '{}' ); } catch (error) { return {}; } } function wayfarerEffectTargetDefinition( branch ) { const effectRow = branch ? branch.closest( '[data-effect-row]' ) : null; const wrapper = effectRow ? effectRow.closest( '[data-builder-effects]' ) : null; const targetSelect = effectRow ? effectRow.querySelector( '[data-effect-target]' ) : null; const catalog = wayfarerEffectTargetCatalog( wrapper ); return targetSelect ? (catalog[targetSelect.value] || {}) : {}; } function wayfarerEffectEnsureChoiceSelect( fixedWrap, numberInput ) { if (!fixedWrap || !numberInput) { return null; } let select = fixedWrap.querySelector( '[data-effect-fixed-choice]' ); if (!select) { select = document.createElement( 'select' ); select.dataset.effectFixedChoice = '1'; select.name = numberInput.name; select.hidden = true; select.disabled = true; fixedWrap.appendChild( select ); } return select; } function wayfarerEffectBuildConditionEditor( host, name, targets ) { const editor = document.createElement( 'div' ); editor.className = 'builder-condition-editor'; editor.dataset.conditionEditor = '1'; editor.appendChild( wayfarerConditionCreateGroup( name, targets, null, true ) ); host.replaceWith( editor ); wayfarerConditionInitializeEditor( editor, targets ); } function wayfarerEffectUpdateResultFields( branch ) { const operation = branch.querySelector( '[data-effect-branch-operation]' ); const modeWrap = branch.querySelector( '[data-effect-value-mode-wrap]' ); const mode = branch.querySelector( '[data-effect-value-mode]' ); const fixedWrap = branch.querySelector( '[data-effect-fixed-wrap]' ); const randomWrap = branch.querySelector( '[data-effect-random-wrap]' ); if (!operation) { return; } const definition = wayfarerEffectTargetDefinition( branch ); let dataType = String( definition.data_type || definition.trait_type || 'number' ).toLowerCase(); if (dataType === 'binary') { dataType = 'boolean'; } const semanticType = String( definition.semantic_type || 'trait' ); const allowedOperations = Array.isArray( definition.effect_operations ) && definition.effect_operations.length ? definition.effect_operations : ( dataType === 'number' ? ['set', 'add', 'subtract'] : ['set'] ); const supportsRandom = Object.prototype.hasOwnProperty.call( definition, 'supports_random' ) ? Boolean( definition.supports_random ) : dataType === 'number'; const operationLabels = { set: 'Set', add: semanticType === 'item_quantity' ? 'Add' : 'Add', subtract: semanticType === 'item_quantity' ? 'Subtract' : 'Subtract', none: 'No Change' }; const previousOperation = operation.value; const resultOperations = allowedOperations.concat( ['none'] ); operation.innerHTML = ''; resultOperations.forEach( function (operationValue) { const option = document.createElement( 'option' ); option.value = operationValue; option.textContent = operationLabels[operationValue] || operationValue; operation.appendChild( option ); } ); operation.value = resultOperations.includes( previousOperation ) ? previousOperation : allowedOperations[0]; const noChange = operation.value === 'none'; let valueMode = mode ? mode.value : 'fixed'; if (!supportsRandom) { valueMode = 'fixed'; if (mode) { mode.value = 'fixed'; } } if (modeWrap) { modeWrap.hidden = noChange || !supportsRandom; modeWrap .querySelectorAll( 'select, input' ) .forEach( function (field) { field.disabled = noChange || !supportsRandom; } ); } const showFixed = !noChange && ( !supportsRandom || valueMode === 'fixed' ); if (fixedWrap) { fixedWrap.hidden = !showFixed; const numberInput = fixedWrap.querySelector( 'input[name]' ); const choiceSelect = wayfarerEffectEnsureChoiceSelect( fixedWrap, numberInput ); const valueOptions = definition.value_options && typeof definition.value_options === 'object' ? definition.value_options : null; if ( showFixed && valueOptions && numberInput && choiceSelect ) { const previousValue = choiceSelect.hidden ? numberInput.value : choiceSelect.value; choiceSelect.innerHTML = ''; Object .entries(valueOptions) .forEach( function (entry) { const option = document.createElement( 'option' ); option.value = entry[0]; option.textContent = entry[1]; choiceSelect.appendChild( option ); } ); if ( Object.prototype.hasOwnProperty.call( valueOptions, String(previousValue) ) ) { choiceSelect.value = String(previousValue); } choiceSelect.hidden = false; choiceSelect.disabled = false; numberInput.hidden = true; numberInput.disabled = true; } else { if ( choiceSelect && numberInput && !choiceSelect.hidden ) { numberInput.value = choiceSelect.value; } if (choiceSelect) { choiceSelect.hidden = true; choiceSelect.disabled = true; } if (numberInput) { numberInput.hidden = !showFixed; numberInput.disabled = !showFixed; numberInput.type = dataType === 'number' ? 'number' : 'text'; if (dataType === 'number') { numberInput.step = 'any'; } else { numberInput.removeAttribute( 'step' ); } } } } if (randomWrap) { const showRandom = !noChange && supportsRandom && valueMode === 'random'; randomWrap.hidden = !showRandom; randomWrap .querySelectorAll( 'input' ) .forEach( function (field) { field.disabled = !showRandom; } ); } } function wayfarerEffectReindexBranches( effectRow ) { const effectIndex = effectRow.dataset.effectIndex; const list = effectRow.querySelector( '[data-effect-branch-list]' ); if ( effectIndex === undefined || !list ) { return; } const branches = Array .from( list.children ) .filter( function (child) { return ( child.matches && child.matches( '[data-effect-branch]' ) ); } ); branches.forEach( function ( branch, branchIndex ) { branch.dataset.branchIndex = String( branchIndex ); branch .querySelectorAll( '[name]' ) .forEach( function (field) { field.name = field.name.replace( /effects\[\d+\]\[branches\]\[\d+\]/, 'effects[' + effectIndex + '][branches][' + branchIndex + ']' ); } ); } ); } function wayfarerEffectRefreshBranchControls( effectRow ) { const list = effectRow.querySelector( '[data-effect-branch-list]' ); if (!list) { return; } const branches = Array .from( list.children ) .filter( function (child) { return ( child.matches && child.matches( '[data-effect-branch]' ) ); } ); const conditionals = branches.filter( function (branch) { return ( branch.dataset.branchKind === 'conditional' ); } ); conditionals.forEach( function ( branch, index ) { const up = branch.querySelector( '[data-move-effect-branch-up]' ); const down = branch.querySelector( '[data-move-effect-branch-down]' ); if (up) { up.disabled = index === 0; } if (down) { down.disabled = index === conditionals.length - 1; } } ); const hasOtherwise = branches.some( function (branch) { return ( branch.dataset.branchKind === 'otherwise' ); } ); const addOtherwise = effectRow.querySelector( '[data-add-effect-otherwise]' ); if (addOtherwise) { addOtherwise.disabled = hasOtherwise; } branches.forEach( wayfarerEffectUpdateResultFields ); wayfarerEffectReindexBranches( effectRow ); } function initializeEffects() { document .querySelectorAll( '[data-builder-effects]' ) .forEach( function (wrapper) { if ( wrapper.dataset.effectsReady === '1' ) { return; } wrapper.dataset.effectsReady = '1'; const list = wrapper.querySelector( '[data-effects-list]' ); const addButton = wrapper.querySelector( '[data-add-effect]' ); const effectTemplate = wrapper.querySelector( '[data-effect-template]' ); const conditionalTemplate = wrapper.querySelector( '[data-effect-conditional-template]' ); const otherwiseTemplate = wrapper.querySelector( '[data-effect-otherwise-template]' ); const conditionTargets = wayfarerEffectConditionTargets( wrapper ); let nextIndex = 0; wrapper .querySelectorAll( '[data-effect-row]' ) .forEach( function (row) { const index = Number( row.dataset.effectIndex ); if ( Number.isFinite( index ) && index >= nextIndex ) { nextIndex = index + 1; } wayfarerEffectRefreshBranchControls( row ); } ); if ( addButton && list && effectTemplate ) { addButton.addEventListener( 'click', function () { const effectIndex = nextIndex++; const html = effectTemplate.innerHTML .replaceAll( '__EFFECT_INDEX__', String( effectIndex ) ); const temporary = document.createElement( 'div' ); temporary.innerHTML = html.trim(); const row = temporary.firstElementChild; if (!row) { return; } list.appendChild( row ); wayfarerEffectRefreshBranchControls( row ); } ); } wrapper.addEventListener( 'change', function (event) { const target = event.target.closest( '[data-effect-target]' ); if (target) { const effectRow = target.closest( '[data-effect-row]' ); if (effectRow) { wayfarerEffectRefreshBranchControls( effectRow ); } return; } const operation = event.target.closest( '[data-effect-branch-operation]' ); const valueMode = event.target.closest( '[data-effect-value-mode]' ); if ( !operation && !valueMode ) { return; } const branch = event.target.closest( '[data-effect-branch]' ); if (branch) { wayfarerEffectUpdateResultFields( branch ); } } ); wrapper.addEventListener( 'click', function (event) { const effectRow = event.target.closest( '[data-effect-row]' ); const removeEffect = event.target.closest( '[data-remove-effect]' ); if (removeEffect) { if (!effectRow) { return; } if ( !window.confirm( 'Remove this Effect?' ) ) { return; } effectRow.remove(); return; } if (!effectRow) { return; } const addConditional = event.target.closest( '[data-add-effect-conditional]' ); if (addConditional) { if (!conditionalTemplate) { return; } const branchList = effectRow.querySelector( '[data-effect-branch-list]' ); if (!branchList) { return; } const effectIndex = effectRow.dataset.effectIndex; const branchIndex = branchList .querySelectorAll( ':scope > [data-effect-branch]' ) .length; const html = conditionalTemplate.innerHTML .replaceAll( '__EFFECT_INDEX__', effectIndex ) .replaceAll( '__BRANCH_INDEX__', String( branchIndex ) ); const temporary = document.createElement( 'div' ); temporary.innerHTML = html.trim(); const branch = temporary.firstElementChild; if (!branch) { return; } const otherwise = Array .from( branchList.children ) .find( function (child) { return ( child.dataset && child.dataset.branchKind === 'otherwise' ); } ); if (otherwise) { branchList.insertBefore( branch, otherwise ); } else { branchList.appendChild( branch ); } wayfarerEffectReindexBranches( effectRow ); const newIndex = branch.dataset.branchIndex; const host = branch.querySelector( '[data-new-effect-condition-editor]' ); if (host) { wayfarerEffectBuildConditionEditor( host, 'effects[' + effectIndex + '][branches][' + newIndex + '][conditions]', conditionTargets ); } wayfarerEffectRefreshBranchControls( effectRow ); return; } const addOtherwise = event.target.closest( '[data-add-effect-otherwise]' ); if (addOtherwise) { if (!otherwiseTemplate) { return; } const branchList = effectRow.querySelector( '[data-effect-branch-list]' ); if ( !branchList || branchList.querySelector( '[data-branch-kind="otherwise"]' ) ) { return; } const effectIndex = effectRow.dataset.effectIndex; const branchIndex = branchList .querySelectorAll( ':scope > [data-effect-branch]' ) .length; const html = otherwiseTemplate.innerHTML .replaceAll( '__EFFECT_INDEX__', effectIndex ) .replaceAll( '__BRANCH_INDEX__', String( branchIndex ) ); const temporary = document.createElement( 'div' ); temporary.innerHTML = html.trim(); const branch = temporary.firstElementChild; if (!branch) { return; } branchList.appendChild( branch ); wayfarerEffectRefreshBranchControls( effectRow ); return; } const removeBranch = event.target.closest( '[data-remove-effect-branch]' ); if (removeBranch) { const branch = removeBranch.closest( '[data-effect-branch]' ); if (branch) { branch.remove(); wayfarerEffectRefreshBranchControls( effectRow ); } return; } const moveUp = event.target.closest( '[data-move-effect-branch-up]' ); const moveDown = event.target.closest( '[data-move-effect-branch-down]' ); if ( !moveUp && !moveDown ) { return; } const branch = event.target.closest( '[data-effect-branch]' ); const branchList = effectRow.querySelector( '[data-effect-branch-list]' ); if ( !branch || !branchList || branch.dataset.branchKind !== 'conditional' ) { return; } const conditionals = Array .from( branchList.children ) .filter( function (child) { return ( child.dataset && child.dataset.branchKind === 'conditional' ); } ); const index = conditionals.indexOf( branch ); if ( moveUp && index > 0 ) { branchList.insertBefore( branch, conditionals[ index - 1 ] ); } if ( moveDown && index >= 0 && index < conditionals.length - 1 ) { const next = conditionals[ index + 1 ]; branchList.insertBefore( next, branch ); } wayfarerEffectRefreshBranchControls( effectRow ); } ); } ); } initializeEffects(); initializeTickEffects();initializeTickEffects(); // ================================================================ // REUSABLE CONDITION EDITOR v27 // WAYFARER_CONDITION_EDITOR_V27 // ================================================================ function wayfarerConditionEscape(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function wayfarerConditionTargetOptions( targets ) { const groups = {}; Object .entries( targets || {} ) .forEach( function (entry) { const key = entry[0]; const target = entry[1] || {}; /* * WAYFARER_LOCATION_CONDITION_TARGETS_V75 * WAYFARER_BUILDER_JS_REPAIR_V79 * * Hidden compatibility targets are retained in JSON so an * existing saved row can resolve its datatype, but they are * not offered when the author adds a new Condition. */ if (target.condition_hidden) { return; } const group = target.group || 'Targets'; if (!groups[group]) { groups[group] = []; } const helper = String( target.helper || '' ).trim(); const baseLabel = target.label || key; groups[group].push({ key: key, label: helper !== '' ? baseLabel + ' — ' + helper : baseLabel }); } ); let html = ''; Object .entries(groups) .forEach( function (entry) { html += ''; entry[1] .forEach( function (target) { html += ''; } ); html += ''; } ); return html; } /* WAYFARER_DATATYPE_CONDITION_UI_V58 */ function wayfarerConditionOperatorLabel( operator ) { const labels = { equals: 'Equals', not_equals: 'Does not equal', greater_than: 'Greater than', greater_than_or_equal: 'Greater than or equal', less_than: 'Less than', less_than_or_equal: 'Less than or equal' }; return labels[operator] || operator; } function wayfarerConditionUpdateRow( row, targets ) { if (!row) { return; } const targetSelect = row.querySelector( '[data-condition-target]' ); const operatorSelect = row.querySelector( '[data-condition-operator]' ); const valueInput = row.querySelector( '[data-condition-value-input]' ); const valueSelect = row.querySelector( '[data-condition-value-select]' ); const valueWrap = row.querySelector( '[data-condition-value-wrap]' ); if (!targetSelect || !operatorSelect) { return; } const definition = (targets || {})[targetSelect.value] || {}; const dataType = String( definition.data_type || definition.trait_type || 'text' ).toLowerCase() === 'binary' ? 'boolean' : String( definition.data_type || definition.trait_type || 'text' ).toLowerCase(); const allowedOperators = Array.isArray( definition.condition_operators ) && definition.condition_operators.length ? definition.condition_operators : [ 'equals', 'not_equals', 'greater_than', 'greater_than_or_equal', 'less_than', 'less_than_or_equal' ]; const previousOperator = operatorSelect.value; operatorSelect.innerHTML = ''; allowedOperators.forEach( function (operator) { const option = document.createElement( 'option' ); option.value = operator; const operatorLabels = definition.condition_operator_labels && typeof definition.condition_operator_labels === 'object' ? definition.condition_operator_labels : {}; option.textContent = operatorLabels[operator] || wayfarerConditionOperatorLabel( operator ); operatorSelect.appendChild( option ); } ); operatorSelect.value = allowedOperators.includes( previousOperator ) ? previousOperator : allowedOperators[0]; const valueOptions = definition.value_options && typeof definition.value_options === 'object' ? definition.value_options : null; if (valueWrap) { valueWrap.hidden = Boolean( definition.condition_hide_value ); } if (valueOptions && valueSelect) { const previousValue = valueSelect.hidden && valueInput ? valueInput.value : valueSelect.value; valueSelect.innerHTML = ''; Object .entries(valueOptions) .forEach( function (entry) { const option = document.createElement( 'option' ); option.value = entry[0]; option.textContent = entry[1]; valueSelect.appendChild( option ); } ); if ( Object.prototype.hasOwnProperty.call( valueOptions, String(previousValue) ) ) { valueSelect.value = String(previousValue); } valueSelect.hidden = false; valueSelect.disabled = false; if (valueInput) { valueInput.hidden = true; valueInput.disabled = true; } return; } if ( valueSelect && valueInput && !valueSelect.hidden ) { valueInput.value = valueSelect.value; } if (valueSelect) { valueSelect.hidden = true; valueSelect.disabled = true; } if (valueInput) { valueInput.hidden = false; valueInput.disabled = false; valueInput.type = dataType === 'number' ? 'number' : 'text'; if (dataType === 'number') { valueInput.step = 'any'; } else { valueInput.removeAttribute( 'step' ); } } } function wayfarerConditionNextChildIndex( group ) { let maxIndex = -1; const children = Array .from( group.children ) .find( function (child) { return child.hasAttribute( 'data-condition-children' ); } ); if (children) { Array .from( children.children ) .forEach( function (node) { let index = Number( node.dataset.conditionChildIndex ); if (!Number.isFinite(index)) { const namedInput = node.querySelector( '[name]' ); const match = namedInput ? namedInput.name.match( /\[children\]\[(\d+)\]/ ) : null; index = match ? Number(match[1]) : -1; } if ( Number.isFinite(index) && index > maxIndex ) { maxIndex = index; } } ); } return maxIndex + 1; } function wayfarerConditionCreateCondition( name, targets, childIndex ) { const row = document.createElement( 'div' ); row.className = 'builder-condition-row'; row.dataset.conditionNode = '1'; row.dataset.conditionType = 'condition'; row.dataset.conditionChildIndex = String(childIndex); row.innerHTML = '' + '' + '' + '' + ''; return row; } function wayfarerConditionCreateGroup( name, targets, childIndex, isRoot ) { const group = document.createElement( 'div' ); group.className = 'builder-condition-group' + ( isRoot ? ' builder-condition-group-root' : '' ); group.dataset.conditionNode = '1'; group.dataset.conditionType = 'group'; if ( childIndex !== null && childIndex !== undefined ) { group.dataset.conditionChildIndex = String(childIndex); } group.innerHTML = '' + '

' + '' + ' of the following' + ( isRoot ? '' : '' ) + '
' + '
' + '
' + ' ' + '' + '
'; return group; } function wayfarerConditionInitializeEditor( editor, targets ) { if ( !editor || editor.dataset.conditionEditorReady === '1' ) { return; } editor.dataset.conditionEditorReady = '1'; editor.addEventListener( 'change', function (event) { const targetSelect = event.target.closest( '[data-condition-target]' ); if (!targetSelect) { return; } wayfarerConditionUpdateRow( targetSelect.closest( '[data-condition-type="condition"]' ), targets ); } ); editor .querySelectorAll( '[data-condition-type="condition"]' ) .forEach( function (row) { wayfarerConditionUpdateRow( row, targets ); } ); editor.addEventListener( 'click', function (event) { const removeButton = event.target.closest( '[data-remove-condition-node]' ); if (removeButton) { const node = removeButton.closest( '[data-condition-node]' ); if (node) { node.remove(); } return; } const addCondition = event.target.closest( '[data-add-condition]' ); const addGroup = event.target.closest( '[data-add-condition-group]' ); if ( !addCondition && !addGroup ) { return; } const group = event.target.closest( '[data-condition-type="group"]' ); if (!group) { return; } const children = Array .from( group.children ) .find( function (child) { return child.hasAttribute( 'data-condition-children' ); } ); if (!children) { return; } const groupTypeInput = Array .from( group.children ) .find( function (child) { return ( child.tagName === 'INPUT' && child.type === 'hidden' && child.name.endsWith( '[type]' ) ); } ); if (!groupTypeInput) { return; } const groupName = groupTypeInput.name.replace( /\[type\]$/, '' ); const nextIndex = wayfarerConditionNextChildIndex( group ); const childName = groupName + '[children][' + nextIndex + ']'; const newNode = addGroup ? wayfarerConditionCreateGroup( childName, targets, nextIndex, false ) : wayfarerConditionCreateCondition( childName, targets, nextIndex ); children.appendChild( newNode ); if (newNode.dataset.conditionType === 'condition') { wayfarerConditionUpdateRow( newNode, targets ); } } ); } function initializeConditionEditors() { /* * WAYFARER_GENERIC_CONDITION_INIT_V45_2 * * The Condition Editor is shared infrastructure. It must work anywhere it * is rendered, not only when nested inside Tick Effects. * * Server-rendered editors carry their own allowed-target JSON. Dynamic * Tick Effect / Effect editors continue to initialize themselves through * their existing creation paths. */ document .querySelectorAll( '[data-condition-editor]' ) .forEach( function (editor) { let targets = {}; const targetJson = editor.querySelector( '[data-condition-editor-targets-json]' ); if (targetJson) { try { targets = JSON.parse( targetJson.textContent || '{}' ); } catch (error) { targets = {}; } } wayfarerConditionInitializeEditor( editor, targets ); } ); /* * Tick Effects retain their observer because newly-created Tick Effect * rows are added after page load and are not server-rendered editors. */ document .querySelectorAll( '[data-builder-tick-effects]' ) .forEach( function (wrapper) { let targets = {}; const targetJson = wrapper.querySelector( '[data-condition-targets-json]' ); if (targetJson) { try { targets = JSON.parse( targetJson.textContent || '{}' ); } catch (error) { targets = {}; } } wrapper .querySelectorAll( '[data-condition-editor]' ) .forEach( function (editor) { wayfarerConditionInitializeEditor( editor, targets ); } ); /* * New Tick Effect rows are cloned from a template after page * load, so convert their placeholder into a fresh root group. */ const list = wrapper.querySelector( '[data-tick-effects-list]' ); if ( list && !list.dataset.conditionObserverReady ) { list.dataset.conditionObserverReady = '1'; const observer = new MutationObserver( function (mutations) { mutations.forEach( function (mutation) { mutation.addedNodes .forEach( function (addedNode) { if ( !addedNode.querySelectorAll ) { return; } const row = addedNode.matches && addedNode.matches( '[data-tick-effect-row]' ) ? addedNode : addedNode.querySelector( '[data-tick-effect-row]' ); if (!row) { return; } const host = row.querySelector( '[data-new-condition-editor]' ); if (!host) { return; } const index = row.dataset.tickIndex; const editor = document.createElement( 'div' ); editor.className = 'builder-condition-editor'; editor.dataset.conditionEditor = '1'; editor.appendChild( wayfarerConditionCreateGroup( 'tick_effects[' + index + '][conditions]', targets, null, true ) ); host.replaceWith( editor ); wayfarerConditionInitializeEditor( editor, targets ); } ); } ); } ); observer.observe( list, { childList: true, subtree: true } ); } } ); } initializeConditionEditors(); // ================================================================ // TICK EFFECT RESULT BRANCHES v28 // WAYFARER_TICK_EFFECT_BRANCHES_V28 // ================================================================ function wayfarerTickEffectConditionTargets( wrapper ) { const targetJson = wrapper.querySelector( '[data-condition-targets-json]' ); if (!targetJson) { return {}; } try { return JSON.parse( targetJson.textContent || '{}' ); } catch (error) { return {}; } } /* WAYFARER_DATATYPE_TICK_EFFECT_UI_V58 */ function wayfarerTickEffectTargetCatalog( wrapper ) { const targetJson = wrapper ? wrapper.querySelector( '[data-tick-target-catalog-json]' ) : null; if (!targetJson) { return {}; } try { return JSON.parse( targetJson.textContent || '{}' ); } catch (error) { return {}; } } function wayfarerTickEffectTargetDefinition( branch ) { const effectRow = branch ? branch.closest( '[data-tick-effect-row]' ) : null; const wrapper = effectRow ? effectRow.closest( '[data-builder-tick-effects]' ) : null; const targetSelect = effectRow ? effectRow.querySelector( '[data-tick-effect-target]' ) : null; const catalog = wayfarerTickEffectTargetCatalog( wrapper ); return targetSelect ? (catalog[targetSelect.value] || {}) : {}; } function wayfarerTickEffectEnsureValueMode( branch, valueInput ) { if (!branch || !valueInput) { return null; } let wrap = branch.querySelector( '[data-tick-value-mode-wrap]' ); if (!wrap) { wrap = document.createElement( 'label' ); wrap.dataset.tickValueModeWrap = '1'; wrap.appendChild( document.createTextNode( 'Value Type' ) ); const select = document.createElement( 'select' ); select.dataset.tickValueMode = '1'; select.name = valueInput.name.replace( /\[value\]$/, '[value_mode]' ); select.innerHTML = '' + ''; const fields = valueInput.closest( '.builder-tick-result-fields' ); if ( fields && fields.dataset.tickInitialValueMode === 'random' ) { select.value = 'random'; } wrap.appendChild( select ); const valueWrap = branch.querySelector( '[data-tick-branch-value-wrap]' ); if (valueWrap && valueWrap.parentNode) { valueWrap.parentNode.insertBefore( wrap, valueWrap ); } } return wrap; } function wayfarerTickEffectEnsureChoiceSelect( valueWrap, valueInput ) { if (!valueWrap || !valueInput) { return null; } let select = valueWrap.querySelector( '[data-tick-fixed-choice]' ); if (!select) { select = document.createElement( 'select' ); select.dataset.tickFixedChoice = '1'; select.name = valueInput.name; select.hidden = true; select.disabled = true; valueWrap.appendChild( select ); } return select; } function wayfarerTickEffectEnsureRandomWrap( branch, valueInput ) { if (!branch || !valueInput) { return null; } let wrap = branch.querySelector( '[data-tick-random-wrap]' ); if (!wrap) { wrap = document.createElement( 'div' ); wrap.className = 'builder-effect-random-range'; wrap.dataset.tickRandomWrap = '1'; const baseName = valueInput.name.replace( /\[value\]$/, '' ); wrap.innerHTML = '' + ''; const fields = valueInput.closest( '.builder-tick-result-fields' ); if (fields) { const minInput = wrap.querySelector( '[data-tick-random-min]' ); const maxInput = wrap.querySelector( '[data-tick-random-max]' ); if (minInput) { minInput.value = fields.dataset.tickInitialMin || ''; } if (maxInput) { maxInput.value = fields.dataset.tickInitialMax || ''; } } const valueWrap = branch.querySelector( '[data-tick-branch-value-wrap]' ); if (valueWrap && valueWrap.parentNode) { valueWrap.parentNode.insertBefore( wrap, valueWrap.nextSibling ); } } return wrap; } function wayfarerTickEffectUpdateBranchValue( branch ) { const operation = branch.querySelector( '[data-tick-branch-operation]' ); const valueWrap = branch.querySelector( '[data-tick-branch-value-wrap]' ); if ( !operation || !valueWrap ) { return; } const input = valueWrap.querySelector( 'input[name]' ); if (!input) { return; } const definition = wayfarerTickEffectTargetDefinition( branch ); let dataType = String( definition.data_type || definition.trait_type || 'number' ).toLowerCase(); if (dataType === 'binary') { dataType = 'boolean'; } const allowedOperations = Array.isArray( definition.effect_operations ) && definition.effect_operations.length ? definition.effect_operations : ( dataType === 'number' ? ['set', 'add', 'subtract'] : ['set'] ); const supportsRandom = Object.prototype.hasOwnProperty.call( definition, 'supports_random' ) ? Boolean( definition.supports_random ) : dataType === 'number'; const previousOperation = operation.value; const resultOperations = allowedOperations.concat( ['none'] ); operation.innerHTML = ''; const operationLabels = { set: 'Set', add: 'Add', subtract: 'Subtract', none: 'No Change' }; resultOperations.forEach( function (operationValue) { const option = document.createElement( 'option' ); option.value = operationValue; option.textContent = operationLabels[operationValue] || operationValue; operation.appendChild( option ); } ); operation.value = resultOperations.includes( previousOperation ) ? previousOperation : allowedOperations[0]; const noChange = operation.value === 'none'; const modeWrap = wayfarerTickEffectEnsureValueMode( branch, input ); const mode = modeWrap ? modeWrap.querySelector( '[data-tick-value-mode]' ) : null; let valueMode = mode ? mode.value : 'fixed'; if (!supportsRandom) { valueMode = 'fixed'; if (mode) { mode.value = 'fixed'; } } if (modeWrap) { modeWrap.hidden = noChange || !supportsRandom; if (mode) { mode.disabled = noChange || !supportsRandom; } } const showFixed = !noChange && ( !supportsRandom || valueMode === 'fixed' ); valueWrap.hidden = !showFixed; const choiceSelect = wayfarerTickEffectEnsureChoiceSelect( valueWrap, input ); const valueOptions = definition.value_options && typeof definition.value_options === 'object' ? definition.value_options : null; if ( showFixed && valueOptions && choiceSelect ) { const previousValue = choiceSelect.hidden ? input.value : choiceSelect.value; choiceSelect.innerHTML = ''; Object .entries(valueOptions) .forEach( function (entry) { const option = document.createElement( 'option' ); option.value = entry[0]; option.textContent = entry[1]; choiceSelect.appendChild( option ); } ); if ( Object.prototype.hasOwnProperty.call( valueOptions, String(previousValue) ) ) { choiceSelect.value = String(previousValue); } choiceSelect.hidden = false; choiceSelect.disabled = false; input.hidden = true; input.disabled = true; } else { if ( choiceSelect && !choiceSelect.hidden ) { input.value = choiceSelect.value; } if (choiceSelect) { choiceSelect.hidden = true; choiceSelect.disabled = true; } input.hidden = !showFixed; input.disabled = !showFixed; input.type = dataType === 'number' ? 'number' : 'text'; if (dataType === 'number') { input.step = 'any'; } else { input.removeAttribute( 'step' ); } } const randomWrap = wayfarerTickEffectEnsureRandomWrap( branch, input ); if (randomWrap) { const showRandom = !noChange && supportsRandom && valueMode === 'random'; randomWrap.hidden = !showRandom; randomWrap .querySelectorAll( 'input' ) .forEach( function (field) { field.disabled = !showRandom; } ); } } function wayfarerTickEffectReindexBranches( effectRow ) { const effectIndex = effectRow.dataset.tickIndex; const list = effectRow.querySelector( '[data-tick-branch-list]' ); if ( effectIndex === undefined || !list ) { return; } const branches = Array.from( list.children ).filter( function (child) { return ( child.matches && child.matches( '[data-tick-effect-branch]' ) ); } ); branches.forEach( function ( branch, branchIndex ) { branch.dataset.branchIndex = String( branchIndex ); branch .querySelectorAll( '[name]' ) .forEach( function (field) { field.name = field.name.replace( /tick_effects\[\d+\]\[branches\]\[\d+\]/, 'tick_effects[' + effectIndex + '][branches][' + branchIndex + ']' ); } ); } ); } function wayfarerTickEffectRefreshBranchControls( effectRow ) { const list = effectRow.querySelector( '[data-tick-branch-list]' ); if (!list) { return; } const branches = Array.from( list.children ).filter( function (child) { return ( child.matches && child.matches( '[data-tick-effect-branch]' ) ); } ); const conditionalBranches = branches.filter( function (branch) { return ( branch.dataset.branchKind === 'conditional' ); } ); conditionalBranches.forEach( function ( branch, index ) { const up = branch.querySelector( '[data-move-tick-branch-up]' ); const down = branch.querySelector( '[data-move-tick-branch-down]' ); if (up) { up.disabled = index === 0; } if (down) { down.disabled = index === conditionalBranches.length - 1; } } ); const hasOtherwise = branches.some( function (branch) { return ( branch.dataset.branchKind === 'otherwise' ); } ); const addOtherwise = effectRow.querySelector( '[data-add-tick-otherwise-result]' ); if (addOtherwise) { addOtherwise.disabled = hasOtherwise; } branches.forEach( wayfarerTickEffectUpdateBranchValue ); wayfarerTickEffectReindexBranches( effectRow ); } function wayfarerTickEffectBuildConditionEditor( host, name, targets ) { const editor = document.createElement( 'div' ); editor.className = 'builder-condition-editor'; editor.dataset.conditionEditor = '1'; editor.appendChild( wayfarerConditionCreateGroup( name, targets, null, true ) ); host.replaceWith( editor ); wayfarerConditionInitializeEditor( editor, targets ); } function initializeTickEffectResultBranches() { document .querySelectorAll( '[data-builder-tick-effects]' ) .forEach( function (wrapper) { if ( wrapper.dataset.tickBranchesReady === '1' ) { return; } wrapper.dataset.tickBranchesReady = '1'; const conditionalTemplate = wrapper.querySelector( '[data-tick-conditional-branch-template]' ); const otherwiseTemplate = wrapper.querySelector( '[data-tick-otherwise-branch-template]' ); const conditionTargets = wayfarerTickEffectConditionTargets( wrapper ); wrapper .querySelectorAll( '[data-tick-effect-row]' ) .forEach( wayfarerTickEffectRefreshBranchControls ); wrapper.addEventListener( 'change', function (event) { const target = event.target.closest( '[data-tick-effect-target]' ); if (target) { const effectRow = target.closest( '[data-tick-effect-row]' ); if (effectRow) { wayfarerTickEffectRefreshBranchControls( effectRow ); } return; } const operation = event.target.closest( '[data-tick-branch-operation]' ); const valueMode = event.target.closest( '[data-tick-value-mode]' ); if (!operation && !valueMode) { return; } const branch = event.target.closest( '[data-tick-effect-branch]' ); if (branch) { wayfarerTickEffectUpdateBranchValue( branch ); } } ); wrapper.addEventListener( 'click', function (event) { const effectRow = event.target.closest( '[data-tick-effect-row]' ); if (!effectRow) { return; } const addConditional = event.target.closest( '[data-add-tick-conditional-result]' ); if (addConditional) { if (!conditionalTemplate) { return; } const list = effectRow.querySelector( '[data-tick-branch-list]' ); if (!list) { return; } const effectIndex = effectRow.dataset.tickIndex; const branchIndex = list.querySelectorAll( ':scope > [data-tick-effect-branch]' ).length; const html = conditionalTemplate.innerHTML .replaceAll( '__EFFECT_INDEX__', effectIndex ) .replaceAll( '__BRANCH_INDEX__', String( branchIndex ) ); const temporary = document.createElement( 'div' ); temporary.innerHTML = html.trim(); const branch = temporary.firstElementChild; if (!branch) { return; } const otherwise = Array .from( list.children ) .find( function (child) { return ( child.dataset && child.dataset.branchKind === 'otherwise' ); } ); if (otherwise) { list.insertBefore( branch, otherwise ); } else { list.appendChild( branch ); } wayfarerTickEffectReindexBranches( effectRow ); const newIndex = branch.dataset.branchIndex; const host = branch.querySelector( '[data-new-branch-condition-editor]' ); if (host) { wayfarerTickEffectBuildConditionEditor( host, 'tick_effects[' + effectIndex + '][branches][' + newIndex + '][conditions]', conditionTargets ); } wayfarerTickEffectRefreshBranchControls( effectRow ); return; } const addOtherwise = event.target.closest( '[data-add-tick-otherwise-result]' ); if (addOtherwise) { if (!otherwiseTemplate) { return; } const list = effectRow.querySelector( '[data-tick-branch-list]' ); if (!list) { return; } if ( list.querySelector( '[data-branch-kind="otherwise"]' ) ) { return; } const effectIndex = effectRow.dataset.tickIndex; const branchIndex = list.querySelectorAll( ':scope > [data-tick-effect-branch]' ).length; const html = otherwiseTemplate.innerHTML .replaceAll( '__EFFECT_INDEX__', effectIndex ) .replaceAll( '__BRANCH_INDEX__', String( branchIndex ) ); const temporary = document.createElement( 'div' ); temporary.innerHTML = html.trim(); const branch = temporary.firstElementChild; if (!branch) { return; } list.appendChild( branch ); wayfarerTickEffectRefreshBranchControls( effectRow ); return; } const removeBranch = event.target.closest( '[data-remove-tick-branch]' ); if (removeBranch) { const branch = removeBranch.closest( '[data-tick-effect-branch]' ); if (branch) { branch.remove(); wayfarerTickEffectRefreshBranchControls( effectRow ); } return; } const moveUp = event.target.closest( '[data-move-tick-branch-up]' ); const moveDown = event.target.closest( '[data-move-tick-branch-down]' ); if ( !moveUp && !moveDown ) { return; } const branch = event.target.closest( '[data-tick-effect-branch]' ); const list = effectRow.querySelector( '[data-tick-branch-list]' ); if ( !branch || !list || branch.dataset.branchKind !== 'conditional' ) { return; } const conditionals = Array.from( list.children ).filter( function (child) { return ( child.dataset && child.dataset.branchKind === 'conditional' ); } ); const index = conditionals.indexOf( branch ); if ( moveUp && index > 0 ) { list.insertBefore( branch, conditionals[index - 1] ); } if ( moveDown && index >= 0 && index < conditionals.length - 1 ) { const next = conditionals[index + 1]; list.insertBefore( next, branch ); } wayfarerTickEffectRefreshBranchControls( effectRow ); } ); /* * Existing Tick Effect code creates whole effect rows from its * template. Refresh branch controls after those rows appear. */ const list = wrapper.querySelector( '[data-tick-effects-list]' ); if (list) { const observer = new MutationObserver( function (mutations) { mutations.forEach( function (mutation) { mutation.addedNodes .forEach( function (node) { if ( !node.matches || !node.matches( '[data-tick-effect-row]' ) ) { return; } wayfarerTickEffectRefreshBranchControls( node ); } ); } ); } ); observer.observe( list, { childList: true } ); } } ); } initializeTickEffectResultBranches(); ============================================================================== FILE: assets/js/game-endings.js ============================================================================== /* |-------------------------------------------------------------------------- | Game Endings Editor | WAYFARER_GAME_ENDINGS_V45 |-------------------------------------------------------------------------- */ (function () { 'use strict'; const editor = document.querySelector( '[data-game-endings-editor]' ); if (!editor) { return; } const list = editor.querySelector( '[data-game-endings-list]' ); const template = editor.querySelector( '[data-game-ending-template]' ); const addButton = editor.querySelector( '[data-add-game-ending]' ); const targetJson = editor.querySelector( '[data-game-ending-condition-targets]' ); let targets = {}; if (targetJson) { try { targets = JSON.parse( targetJson.textContent || '{}' ); } catch (error) { targets = {}; } } function reindex() { if (!list) { return; } Array .from( list.querySelectorAll( ':scope > [data-game-ending]' ) ) .forEach( function ( ending, index ) { ending.dataset.endingIndex = String(index); ending .querySelectorAll( '[name]' ) .forEach( function (field) { field.name = field.name.replace( /endings\[\d+\]/, 'endings[' + index + ']' ); } ); } ); } if ( addButton && list && template ) { addButton.addEventListener( 'click', function () { const index = list.querySelectorAll( ':scope > [data-game-ending]' ).length; const html = template.innerHTML.replaceAll( '__INDEX__', String(index) ); const temporary = document.createElement( 'div' ); temporary.innerHTML = html.trim(); const ending = temporary.firstElementChild; if (!ending) { return; } list.appendChild( ending ); const host = ending.querySelector( '[data-new-ending-condition-editor]' ); if ( host && typeof wayfarerConditionCreateGroup === 'function' && typeof wayfarerConditionInitializeEditor === 'function' ) { const conditionEditor = document.createElement( 'div' ); conditionEditor.className = 'builder-condition-editor'; conditionEditor.dataset.conditionEditor = '1'; conditionEditor.appendChild( wayfarerConditionCreateGroup( 'endings[' + index + '][conditions]', targets, null, true ) ); host.replaceWith( conditionEditor ); wayfarerConditionInitializeEditor( conditionEditor, targets ); } reindex(); } ); } editor.addEventListener( 'click', function (event) { const removeButton = event.target.closest( '[data-remove-game-ending]' ); if (!removeButton) { return; } const ending = removeButton.closest( '[data-game-ending]' ); if (!ending) { return; } if ( !window.confirm( 'Remove this Ending?' ) ) { return; } ending.remove(); reindex(); } ); reindex(); })(); ============================================================================== FILE: assets/js/game-travel.js ============================================================================== /* * WAYFARER_GAME_TRAVEL_MODIFIERS_JS_V73 * WAYFARER_GENERIC_TRAVEL_MODIFIER_COPY_V74 * Dynamic Game Settings Travel Modifier cards. */ (function () { 'use strict'; const host = document.querySelector( '[data-game-travel-modifiers]' ); if (!host) { return; } const list = host.querySelector( '[data-game-travel-modifier-list]' ); const addButton = host.querySelector( '[data-add-game-travel-modifier]' ); const targetJson = host.querySelector( '[data-game-travel-targets-json]' ); if (!list || !addButton) { return; } let targets = {}; if (targetJson) { try { targets = JSON.parse( targetJson.textContent || '{}' ); } catch (error) { targets = {}; } } function nextIndex() { const current = Number( host.dataset.nextIndex || 0 ); const index = Number.isFinite(current) ? current : 0; host.dataset.nextIndex = String(index + 1); return index; } function refreshLegend(card) { const legend = card.querySelector( '[data-game-travel-modifier-legend]' ); const name = card.querySelector( '[data-game-travel-modifier-name]' ); if (!legend || !name) { return; } const value = String( name.value || '' ).trim(); legend.textContent = value !== '' ? value : 'Travel Modifier'; } host.addEventListener( 'input', function (event) { const name = event.target.closest( '[data-game-travel-modifier-name]' ); if (!name) { return; } const card = name.closest( '[data-game-travel-modifier]' ); if (card) { refreshLegend(card); } } ); host.addEventListener( 'click', function (event) { const removeButton = event.target.closest( '[data-remove-game-travel-modifier]' ); if (!removeButton) { return; } const card = removeButton.closest( '[data-game-travel-modifier]' ); if ( card && window.confirm( 'Remove this Travel Modifier?' ) ) { card.remove(); } } ); addButton.addEventListener( 'click', function () { const index = nextIndex(); const card = document.createElement( 'fieldset' ); card.dataset.gameTravelModifier = '1'; card.dataset.travelModifierIndex = String(index); card.innerHTML = 'Travel Modifier' + '' + '

' + '


Applied to the current Region\'s Base Distance Per Tick.

' + '

Apply when

' + '

No Conditions means this modifier always applies during dedicated Linear Travel.

' + '
' + '

'; const conditionEditor = card.querySelector( '[data-condition-editor]' ); if ( conditionEditor && typeof window.wayfarerConditionCreateGroup === 'function' ) { conditionEditor.appendChild( window.wayfarerConditionCreateGroup( 'travel_modifiers[' + index + '][conditions]', targets, null, true ) ); if ( typeof window.wayfarerConditionInitializeEditor === 'function' ) { window.wayfarerConditionInitializeEditor( conditionEditor, targets ); } } list.appendChild( card ); } ); })(); ============================================================================== FILE: assets/js/hierarchical-targets.js ============================================================================== /* * Wayfarer Hierarchical Target Picker * WAYFARER_HIERARCHICAL_TARGETS_V80 * * Progressive enhancement: * - The existing canonical . * Because that select lives inside the original Target