'adventurebook_page', 'post_parent' => $book_id, 'posts_per_page' => -1, 'post_status' => 'publish', 'orderby' => 'ID', 'order' => 'ASC', ]); if (!$is_admin && $character instanceof Character) { $visited = $character->get_visited_page_ids(); $pages = array_filter($pages, fn($p) => in_array($p->ID, $visited, false)); } if (empty($pages)) { return ['nodes' => [], 'edges' => [], 'regions' => []]; } // Startseite = Seite mit niedrigster ID $start_page = $pages[0]->ID; // ---- Direction setup --------------------------------------------------- $dir_map = [ 'north' => [0, -1], 'south' => [0, 1], 'east' => [1, 0], 'west' => [-1, 0], 'northeast' => [1, -1], 'northwest' => [-1, -1], 'southeast' => [1, 1], 'southwest' => [-1, 1], ]; $teleports = ['enter', 'exit', 'up', 'down']; $scale = 250; // ---- Voranalyse: welche Seiten haben echte Richtungen ------------------- $decisions_cache = []; $has_directional = []; foreach ($pages as $page) { $pid = $page->ID; $dec = get_post_meta($pid, '_adventurebook_decision_options', true); if (!is_array($dec)) { $decisions_cache[$pid] = []; continue; } $decisions_cache[$pid] = $dec; foreach ($dec as $d) { $target = intval($d['target'] ?? 0); if (!$target) { continue; } $dir = strtolower(trim($d['decision_dir'] ?? '')); $is_tp = in_array($dir, $teleports, true); $is_dir = isset($dir_map[$dir]) || $is_tp; if ($is_dir) { $has_directional[$pid] = true; $has_directional[$target] = true; } } } // ---- BFS build --------------------------------------------------------- $positions = []; $edges = []; $region_meta = []; $region_count = 0; $queue = [[ 'id' => $start_page, 'x' => 0, 'y' => 0, 'region' => 0, ]]; while ($queue) { $c = array_shift($queue); if (isset($positions[$c['id']])) { continue; } $positions[$c['id']] = [ 'x' => $c['x'], 'y' => $c['y'], 'region' => $c['region'], ]; $decisions = $decisions_cache[$c['id']] ?? []; if (empty($decisions)) { continue; } foreach ($decisions as $d) { $target = intval($d['target'] ?? 0); if (!$target) { continue; } $dir = strtolower(trim($d['decision_dir'] ?? '')); $is_tp = in_array($dir, $teleports, true); $affects_layout = isset($dir_map[$dir]) || $is_tp; $color = match ($dir) { 'north' => '#4a90e2', 'south' => '#e94e77', 'east' => '#8cc152', 'west' => '#f6bb42', 'northeast' => '#7fbfff', 'northwest' => '#0074d9', 'southeast' => '#ffb347', 'southwest' => '#ff7f50', 'up' => '#9b59b6', 'down' => '#8e44ad', 'enter' => '#5dade2', 'exit' => '#f39c12', 'ship' => '#34495e', default => '#999', }; if (($d['restriction_type'] ?? 'none') !== 'none') { $color = '#ff0000'; } $edges[] = [ 'from' => $c['id'], 'to' => $target, 'label' => $d['label'] ?? $dir, 'arrows' => 'to', 'color' => ['color' => $color], 'dashes' => ($d['restriction_type'] ?? 'none') !== 'none', 'smooth' => [ 'enabled' => true, 'type' => 'curvedCW', 'roundness' => 0.15, ], 'font' => [ 'color' => $color, 'vadjust' => -12, ], ]; // --- Positionierung --- if (!isset($positions[$target])) { if ($is_tp) { // Neue Region anlegen $region_count++; $region_meta[$region_count] = [ 'source_id' => $c['id'], 'source_x' => $c['x'], 'source_y' => $c['y'], 'dir' => $dir, ]; $queue[] = [ 'id' => $target, 'x' => 0, 'y' => 0, 'region' => $region_count, ]; } else { // ship/none-Verbindungen nur verwenden, wenn Ziel keine echte Richtung hat if ( !$affects_layout && !empty($has_directional[$target]) ) { continue; } [$dx, $dy] = $dir_map[$dir] ?? [0, 0]; $queue[] = [ 'id' => $target, 'x' => $c['x'] + $dx * $scale, 'y' => $c['y'] + $dy * $scale, 'region' => $c['region'], ]; } } } } // ---- Compute region bounds -------------------------------------------- $contents = []; foreach ($positions as $id => $p) { $r = $p['region']; if (!isset($contents[$r])) { $contents[$r] = [ 'min_x' => $p['x'], 'max_x' => $p['x'], 'min_y' => $p['y'], 'max_y' => $p['y'], ]; } else { $contents[$r]['min_x'] = min($contents[$r]['min_x'], $p['x']); $contents[$r]['max_x'] = max($contents[$r]['max_x'], $p['x']); $contents[$r]['min_y'] = min($contents[$r]['min_y'], $p['y']); $contents[$r]['max_y'] = max($contents[$r]['max_y'], $p['y']); } } $inner_padding = 150; $bounds = []; foreach ($contents as $r => $b) { $bounds[$r] = [ 'min_x' => $b['min_x'] - $inner_padding, 'max_x' => $b['max_x'] + $inner_padding, 'min_y' => $b['min_y'] - $inner_padding, 'max_y' => $b['max_y'] + $inner_padding, ]; } ksort($bounds); ksort($region_meta); // ---- Smart deterministic region placement ------------------------------ $offsets = [0 => ['x' => 0, 'y' => 0]]; $padding = 350; $intersects = fn(array $a, array $b, int $pad = 100): bool => !($a['max_x'] + $pad < $b['min_x'] || $a['min_x'] - $pad > $b['max_x'] || $a['max_y'] + $pad < $b['min_y'] || $a['min_y'] - $pad > $b['max_y']); foreach ($bounds as $r => $b) { if ($r === 0) continue; $m = $region_meta[$r] ?? null; if (!$m) continue; $src_r = $positions[$m['source_id']]['region'] ?? 0; $src_c = $contents[$src_r]; $o = $offsets[$src_r] ?? ['x' => 0, 'y' => 0]; $src_global = [ 'min_x' => $src_c['min_x'] + $o['x'], 'max_x' => $src_c['max_x'] + $o['x'], 'min_y' => $src_c['min_y'] + $o['y'], 'max_y' => $src_c['max_y'] + $o['y'], ]; $src_x = $m['source_x'] + $o['x']; $src_y = $m['source_y'] + $o['y']; $dist = [ 'top' => abs($src_y - $src_global['min_y']), 'bottom' => abs($src_global['max_y'] - $src_y), 'left' => abs($src_x - $src_global['min_x']), 'right' => abs($src_global['max_x'] - $src_x), ]; $priority = ['right', 'bottom', 'left', 'top']; $side = $priority[0]; $min_dist = INF; foreach ($priority as $key) { if (!isset($dist[$key])) continue; if ($dist[$key] < $min_dist - 0.001) { $min_dist = $dist[$key]; $side = $key; } } $pref = match ($m['dir']) { 'up', 'enter', 'north', 'northeast', 'northwest' => 'top', 'down', 'exit', 'south', 'southeast', 'southwest' => 'bottom', 'east' => 'right', 'west' => 'left', default => null, }; if ($pref && $dist[$pref] <= $min_dist * 1.25) { $side = $pref; } $w = $b['max_x'] - $b['min_x']; $h = $b['max_y'] - $b['min_y']; switch ($side) { case 'top': $ix = $src_x - $w / 2; $iy = $src_global['min_y'] - $h - $padding; break; case 'bottom': $ix = $src_x - $w / 2; $iy = $src_global['max_y'] + $padding; break; case 'left': $ix = $src_global['min_x'] - $w - $padding; $iy = $src_y - $h / 2; break; case 'right': default: $ix = $src_global['max_x'] + $padding; $iy = $src_y - $h / 2; break; } $target = [ 'min_x' => $ix, 'max_x' => $ix + $w, 'min_y' => $iy, 'max_y' => $iy + $h, ]; do { $collide = false; foreach ($offsets as $r2 => $o2) { if (!isset($bounds[$r2])) continue; $b2 = $bounds[$r2]; $b2 = [ 'min_x' => $b2['min_x'] + $o2['x'], 'max_x' => $b2['max_x'] + $o2['x'], 'min_y' => $b2['min_y'] + $o2['y'], 'max_y' => $b2['max_y'] + $o2['y'], ]; if ($intersects($target, $b2)) { $collide = true; switch ($side) { case 'top': $target['min_y'] -= $h + $padding; break; case 'bottom': $target['min_y'] += $h + $padding; break; case 'left': $target['min_x'] -= $w + $padding; break; case 'right': $target['min_x'] += $w + $padding; break; } $target['max_x'] = $target['min_x'] + $w; $target['max_y'] = $target['min_y'] + $h; break; } } } while ($collide); $offsets[$r] = [ 'x' => $target['min_x'] - $b['min_x'], 'y' => $target['min_y'] - $b['min_y'], ]; } // ---- Apply offsets ----------------------------------------------------- foreach ($positions as &$p) { $o = $offsets[$p['region']] ?? ['x' => 0, 'y' => 0]; $p['x'] += $o['x']; $p['y'] += $o['y']; } unset($p); foreach ($bounds as $r => &$b) { $o = $offsets[$r] ?? ['x' => 0, 'y' => 0]; $b['min_x'] += $o['x']; $b['max_x'] += $o['x']; $b['min_y'] += $o['y']; $b['max_y'] += $o['y']; } unset($b); // ---- Node creation with enemy/item/script info ------------------------- $get_enemy_labels = fn($id) => array_map( fn($s) => esc_html(Enemy_Definition::get_enemy_definition_by_code($s->code)->name ?? '') . ' (' . esc_html($s->respawn_time) . 's)', array_filter( Enemy_Spawn::get_enemy_spawns_by_page_id($id), fn($s) => !empty($s->code) ) ); $get_item_labels = fn($id) => array_map( fn($s) => esc_html(Adventurebook\Item_Definition::get_item_definition_by_code($s->code)->name ?? ''), array_filter( Item_Spawn::get_item_spawns_by_page_id($id), fn($s) => !empty($s->code) ) ); $nodes = []; foreach ($positions as $id => $p) { $en = $get_enemy_labels($id); $it = $get_item_labels($id); $script_code = get_post_meta($id, '_adventurebook_page_script', true); $script = $script_code ? Script::get_script_by_code($script_code) : null; $resources = $resources = get_post_meta( $id, '_adventurebook_page_resources', true ); $lines = []; if ($en) $lines[] = 'E: ' . implode(', ', $en); if ($it) $lines[] = 'I: ' . implode(', ', $it); if ($script) $lines[] = 'S: ' . esc_html($script->name); if ($resources) $lines[] = 'R: ' . esc_html($resources); $label = ($is_admin ? '(' . $id . ') ' : '') . get_the_title($id); if ($lines) $label .= "\n" . implode("\n", $lines); $nodes[] = [ 'id' => $id, 'label' => $label, 'x' => $p['x'], 'y' => $p['y'], 'fixed' => adventurebook_page_has_directional_decisions($id), 'color' => $en ? '#f8d7da' : ($it ? '#aaffaa' : '#e2e3e5'), 'title' => $label, 'font' => ['multi' => 'html', 'size' => 12], 'margin' => 8, ]; } return [ 'nodes' => $nodes, 'edges' => array_values($edges), 'regions' => $bounds, ]; }