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
= builderEscape($game['name']) ?>
Wayfarer Builder
← Games
==============================================================================
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
← Games
= $game['id'] ? 'Edit Game' : 'New Game' ?>
= htmlspecialchars($error) ?>
==============================================================================
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 |
|
= htmlspecialchars($game['name']) ?>
|
= htmlspecialchars($game['slug']) ?>
|
= htmlspecialchars($game['status']) ?>
|
= htmlspecialchars($game['updated_at']) ?>
|
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 +=
'';
}
);
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 =
'' +
'
' +
'' +
'' +
' ' +
'' +
'
';
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 =
'' +
'' +
'' +
'
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