<?php
// === KP Logging – production gate ===
if (!defined(‘KP_DEBUG’)) {
define(‘KP_DEBUG’, false); // set true only when actively debugging
}
if (!function_exists(‘KP_LOG’)) {
function KP_LOG($msg, $level=’info’) {
if (!KP_DEBUG) return;
if (!is_string($msg)) $msg = json_encode($msg, JSON_UNESCAPED_SLASHES);
error_log(“[KP][$level] $msg”);
}
}
if (!function_exists(‘kp_include_patch’)) {
function kp_include_patch(string $name, $ctx = null): bool {
$file = __DIR__ . “/patches/{$name}.php”;
if (!is_file($file)) return false;
$prev = $GLOBALS[‘KP_CTX’] ?? null;
$GLOBALS[‘KP_CTX’] = $ctx;
$ok = @include $file; // syntax errors still bubble; warnings quiet
$GLOBALS[‘KP_CTX’] = $prev;
return $ok !== false;
}
}
// === No-cache for killer-pool state endpoint (prevents stale cross-device UI) ===
if (function_exists(‘add_filter’)) {
add_filter(‘rest_post_dispatch’, function ($result, $server, $request) {
$route = (is_object($request) && method_exists($request,’get_route’)) ? $request->get_route() : ”;
if (is_string($route) && strpos($route, ‘/killer-pool/v1/state’) !== false) {
if (is_object($server) && method_exists($server,’send_header’)) {
$server->send_header(‘Cache-Control’, ‘no-store, no-cache, must-revalidate, max-age=0’);
$server->send_header(‘Pragma’, ‘no-cache’);
$server->send_header(‘Expires’, ‘0’);
}
if (function_exists(‘header_remove’)) { @header_remove(‘Last-Modified’); }
}
return $result;
}, 10, 3);
}
/**
* Plugin Name: Killer Pool Tournament Complete
* Plugin URI: https://example.com
* Description: Complete Killer Pool tournament system with automatic Elementor page cloning and mobile controls.
* Version: 3.0.0
* Author: Killer Pool Dev Team
* License: GPL v2 or later
* Text Domain: killer-pool-complete
* Requires at least: 5.0
* Requires PHP: 7.4
*/
// Prevent direct access
if (!defined(‘ABSPATH’)) {
exit;
}
// Elementor integration will be loaded when needed
// Force bottom section centering on KP pages (admin + public).
add_action(‘wp_head’, function () {
if (!is_singular()) return;
global $post;
if (!$post) return;
$content = $post->post_content ?? ”;
$shortcodes = [‘killer_pool_admin’, ‘killer_pool’, ‘killer_pool_complete’];
$has_kp = false;
foreach ($shortcodes as $sc) {
if (function_exists(‘has_shortcode’) && has_shortcode($content, $sc)) { $has_kp = true; break; }
if (strpos($content, $sc) !== false) { $has_kp = true; break; }
}
if (!$has_kp) return;
echo ‘<style id=”kp-bottom-centering”>
/* Primary: center the tagged bottom container */
.kp-bottom{
display:flex !important;
flex-direction:column !important;
align-items:center !important;
justify-content:center !important;
text-align:center !important;
gap:10px;
}
.kp-bottom svg,
.kp-bottom img{ display:block !important; margin:0 auto !important; }
.kp-bottom p,
.kp-bottom a,
.kp-bottom .elementor-button-wrapper{ margin-left:auto !important; margin-right:auto !important; }
/* Safety: keep page tall enough when shortcode is near-empty */
.kp-bottom{ min-height: 120px; }
</style>’;
});
/** ———- Elementor Page Duplicator (from your proven cloner) ———- */
class KP_Elementor_Duplicator {
public function duplicate($post_id, $custom_title = ”, $custom_slug = ”, $auto_publish = true) {
KP_LOG(“CLONE: Starting duplicate for post_id={$post_id}, title='{$custom_title}’, slug='{$custom_slug}'”);
$original_post = get_post($post_id);
if (!$original_post || !is_object($original_post)) {
KP_LOG(“CLONE: Invalid source post {$post_id}”);
return new WP_Error(‘kp_invalid_source’, ‘Invalid or missing source post. Please check master page IDs in settings.’);
}
// New post data
$post_data = array(
‘post_status’ => $auto_publish ? ‘publish’ : ‘draft’,
‘post_type’ => $original_post->post_type,
‘post_content’ => $original_post->post_content,
‘post_excerpt’ => $original_post->post_excerpt,
‘post_author’ => get_current_user_id(),
‘post_parent’ => $original_post->post_parent,
‘post_password’ => $original_post->post_password,
‘comment_status’ => $original_post->comment_status,
‘ping_status’ => $original_post->ping_status,
‘menu_order’ => $original_post->menu_order,
‘to_ping’ => $original_post->to_ping,
);
// Title
$post_data[‘post_title’] = !empty($custom_title) ? $custom_title : $original_post->post_title . ‘ — [Cloned]’;
// Slug
if (!empty($custom_slug)) {
$post_data[‘post_name’] = $this->generate_unique_slug(sanitize_title($custom_slug), $original_post->post_type);
} else {
$post_data[‘post_name’] = $this->generate_unique_slug($original_post->post_name, $original_post->post_type);
}
// Insert
$new_post_id = wp_insert_post($post_data, true);
if (is_wp_error($new_post_id)) {
KP_LOG(“CLONE: wp_insert_post failed: ” . $new_post_id->get_error_message());
return $new_post_id;
}
KP_LOG(“CLONE: New post created with ID={$new_post_id}”);
// Copy everything exactly as your proven cloner does
$this->copy_taxonomies($post_id, $new_post_id, $original_post->post_type);
$this->copy_featured_image($post_id, $new_post_id);
$this->copy_all_meta($post_id, $new_post_id);
$this->copy_elementor_specific($post_id, $new_post_id);
$this->ensure_elementor_active($new_post_id);
$this->regenerate_elementor_css($new_post_id);
// Keep page template
$tpl = get_post_meta($post_id, ‘_wp_page_template’, true);
if ($tpl) {
delete_post_meta($new_post_id, ‘_wp_page_template’);
update_post_meta($new_post_id, ‘_wp_page_template’, $tpl);
}
KP_LOG(“CLONE: Duplicate complete for new_post_id={$new_post_id}”);
return $new_post_id;
}
private function generate_unique_slug($slug, $post_type) {
$base = $slug;
$i = 1;
while (get_page_by_path($slug, OBJECT, $post_type)) {
$slug = $base . ‘-‘ . $i;
$i++;
}
return $slug;
}
private function copy_taxonomies($original_id, $duplicate_id, $post_type) {
$taxonomies = get_object_taxonomies($post_type);
if (empty($taxonomies)) return;
foreach ($taxonomies as $taxonomy) {
$terms = wp_get_object_terms($original_id, $taxonomy, array(‘fields’ => ‘ids’));
if (is_wp_error($terms)) continue;
wp_set_object_terms($duplicate_id, $terms, $taxonomy);
}
}
private function copy_featured_image($original_id, $duplicate_id) {
$thumb_id = get_post_thumbnail_id($original_id);
if ($thumb_id) {
set_post_thumbnail($duplicate_id, $thumb_id);
}
}
private function copy_all_meta($original_id, $duplicate_id) {
global $wpdb;
$post_meta = $wpdb->get_results($wpdb->prepare(
“SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d”,
$original_id
));
if (empty($post_meta)) return;
$sql = “INSERT INTO {$wpdb->postmeta} (post_id, meta_key, meta_value) VALUES “;
$values = array();
foreach ($post_meta as $meta) {
if (in_array($meta->meta_key, array(‘_edit_lock’, ‘_edit_last’), true)) continue;
$values[] = $wpdb->prepare(“(%d, %s, %s)”, $duplicate_id, $meta->meta_key, wp_slash($meta->meta_value));
}
if (!empty($values)) {
$sql .= implode(‘, ‘, $values) . ‘;’;
$wpdb->query($sql);
}
// Fix Elementor template type
$source_type = get_post_meta($original_id, ‘_elementor_template_type’, true);
if ($source_type) {
delete_post_meta($duplicate_id, ‘_elementor_template_type’);
update_post_meta($duplicate_id, ‘_elementor_template_type’, $source_type);
}
}
private function copy_elementor_specific($original_id, $duplicate_id) {
// Page settings
$page_settings = get_post_meta($original_id, ‘_elementor_page_settings’, true);
if ($page_settings) {
update_post_meta($duplicate_id, ‘_elementor_page_settings’, $page_settings);
}
// Edit mode
$edit_mode = get_post_meta($original_id, ‘_elementor_edit_mode’, true);
update_post_meta($duplicate_id, ‘_elementor_edit_mode’, $edit_mode ? $edit_mode : ‘builder’);
// Data (JSON) – CRITICAL FIX: Restore wp_slash() from working version
$data = get_post_meta($original_id, ‘_elementor_data’, true);
if ($data) {
// Handle array (unserialized) or string
if (is_array($data)) {
$data_json = wp_json_encode($data);
$slashed_data = wp_slash($data_json);
} else {
$slashed_data = wp_slash($data);
}
update_post_meta($duplicate_id, ‘_elementor_data’, $slashed_data);
}
// Version
$version = get_post_meta($original_id, ‘_elementor_version’, true);
if ($version) {
update_post_meta($duplicate_id, ‘_elementor_version’, $version);
}
}
private function ensure_elementor_active($post_id) {
if (!did_action(‘elementor/loaded’)) return;
try {
if (class_exists(‘\Elementor\Plugin’)) {
\Elementor\Plugin::$instance->db->set_is_built_with_elementor($post_id, true);
if (method_exists(\Elementor\Plugin::$instance->frontend, ‘clear_cache’)) {
\Elementor\Plugin::$instance->frontend->clear_cache();
}
if (method_exists(\Elementor\Plugin::$instance->assets_manager, ‘clear_cache’)) {
\Elementor\Plugin::$instance->assets_manager->clear_cache();
}
\Elementor\Plugin::$instance->files_manager->clear_cache();
}
} catch (\Throwable $e) {}
}
private function regenerate_elementor_css($post_id) {
if (!did_action(‘elementor/loaded’)) return;
try {
// Ensure _elementor_data is slashed string before regen
$data = get_post_meta($post_id, ‘_elementor_data’, true);
if (is_array($data)) {
$data_json = wp_json_encode($data);
$slashed_data = wp_slash($data_json);
update_post_meta($post_id, ‘_elementor_data’, $slashed_data);
} elseif (is_string($data)) {
update_post_meta($post_id, ‘_elementor_data’, wp_slash($data));
}
// Force full re-render by getting document and saving with updated data
if (class_exists(‘\Elementor\Plugin’)) {
$doc = \Elementor\Plugin::$instance->documents->get($post_id);
if ($doc) {
// Re-generate CSS for the document
$doc->get_css();
$doc->save();
}
}
// Use fully qualified class name to avoid fatal errors
if (class_exists(‘\Elementor\Core\Files\CSS\Post’)) {
$css = \Elementor\Core\Files\CSS\Post::create($post_id);
if ($css && method_exists($css, ‘update’)) {
$css->update();
}
}
if (class_exists(‘\Elementor\Plugin’)) {
$doc = \Elementor\Plugin::$instance->documents->get($post_id);
if ($doc) {
$doc->save();
if (method_exists($doc, ‘get_css_wrapper’)) {
$wrapper = $doc->get_css_wrapper();
if ($wrapper && method_exists($wrapper, ‘update’)) {
$wrapper->update();
}
}
}
}
// Clear all Elementor caches
if (class_exists(‘\Elementor\Plugin’)) {
if (method_exists(\Elementor\Plugin::$instance->frontend, ‘clear_cache’)) {
\Elementor\Plugin::$instance->frontend->clear_cache();
}
if (method_exists(\Elementor\Plugin::$instance->files_manager, ‘clear_cache’)) {
\Elementor\Plugin::$instance->files_manager->clear_cache();
}
}
} catch (\Throwable $e) {
KP_LOG(“CLONE: regenerate_elementor_css EXCEPTION for {$post_id}: ” . $e->getMessage());
// Silently handle any Elementor-related errors
}
}
}
// —- Hint bump helper (safe if missing) —-
if (!function_exists(‘kp_bump_hint’)) {
function kp_bump_hint($event_id, $slug = null){
// Nudge both an option and a transient so any poller sees a fresh ts immediately
update_option(‘kp_hint_’ . (int)$event_id, time(), false);
if ($slug) {
set_transient(‘kp_hint_’ . sanitize_key($slug), time(), 0);
}
}
}
// —- Finish game if single survivor helper —-
if ( ! function_exists(‘kp_complete_finish_if_single_survivor’) ) {
function kp_complete_finish_if_single_survivor( $event_id ) {
global $wpdb;
$events_table = $wpdb->prefix . ‘kp_events’;
$players_table = $wpdb->prefix . ‘kp_players’;
// Verify active game
$event = $wpdb->get_row(
$wpdb->prepare(“SELECT game_state FROM $events_table WHERE id = %d”, (int)$event_id),
ARRAY_A
);
if ( ! $event || $event[‘game_state’] !== ‘active’ ) {
return false;
}
// ✅ Count alive using computed totals, not legacy `lives`
$alive_count = (int) $wpdb->get_var( $wpdb->prepare(
“SELECT COUNT(*)
FROM $players_table
WHERE event_id = %d
AND is_active = 1
AND (COALESCE(regular_lives,0) + COALESCE(black_ball_lives,0)) > 0″,
(int)$event_id
) );
if ( $alive_count === 1 ) {
// Optional: get the winner row (for logging / downstream use)
$winner = $wpdb->get_row( $wpdb->prepare(
“SELECT id, display_name
FROM $players_table
WHERE event_id = %d
AND is_active = 1
AND (COALESCE(regular_lives,0) + COALESCE(black_ball_lives,0)) > 0
LIMIT 1″,
(int)$event_id
), ARRAY_A );
// Mark event finished
$wpdb->update(
$events_table,
array( ‘game_state’ => ‘finished’ ),
array( ‘id’ => (int)$event_id ),
array( ‘%s’ ),
array( ‘%d’ )
);
if ( function_exists(‘KP_LOG’) ) {
$name = $winner[‘display_name’] ?? ‘unknown’;
KP_LOG(“Game finished: single survivor for event {$event_id} (winner: {$name})”);
}
// Nudge TV/admin clones to refresh, if available
if ( function_exists(‘kp_bump_hint’) ) {
kp_bump_hint( (int)$event_id, null );
}
return true;
}
return false;
}
}
// KP_PATCH:kp_elementor_integration
kp_include_patch(‘kp_elementor_integration’);
/** ———- Plugin Setup ———- */
// — Name normalization (UTF-8 safe)
function kp_norm_name($s) {
$s = wp_strip_all_tags($s ?? ”);
$s = preg_replace(‘/\s+/u’, ‘ ‘, trim($s));
if (function_exists(‘mb_strtolower’)) $s = mb_strtolower($s, ‘UTF-8’); else $s = strtolower($s);
return $s;
}
function kp_assert_unique_player_name($wpdb, $event_id, $name, $exclude_player_id = 0) {
$norm = kp_norm_name($name);
$rows = $wpdb->get_results(
$wpdb->prepare(
“SELECT id, display_name
FROM {$wpdb->prefix}kp_players
WHERE event_id = %d AND is_active = 1″,
$event_id
)
);
foreach ($rows as $r) {
if ((int)$r->id === (int)$exclude_player_id) continue;
if (kp_norm_name($r->display_name) === $norm) {
return new WP_Error(‘duplicate_player’, ‘A player with this name already exists.’);
}
}
return true;
}
// Plugin activation – create database tables
register_activation_hook(__FILE__, ‘kp_complete_install’);
function kp_complete_install() {
global $wpdb;
require_once(ABSPATH . ‘wp-admin/includes/upgrade.php’);
$charset_collate = $wpdb->get_charset_collate();
// Enhanced Events table with black ball settings
$events_table = $wpdb->prefix . ‘kp_events’;
$sql1 = “CREATE TABLE $events_table (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
slug varchar(200) NOT NULL,
name varchar(200) NOT NULL,
venue varchar(200) DEFAULT ”,
status varchar(20) DEFAULT ‘active’,
starting_lives tinyint DEFAULT 3,
max_lives tinyint DEFAULT 5,
game_state varchar(20) DEFAULT ‘waiting’,
current_player_id bigint(20) unsigned DEFAULT 0,
timer_enabled tinyint DEFAULT 0,
timer_duration int DEFAULT 60,
timer_auto_loss tinyint DEFAULT 0,
timer_buffer_enabled tinyint DEFAULT 0,
timer_buffer_seconds int DEFAULT 10,
timer_start_time bigint DEFAULT 0,
black_ball_enabled tinyint DEFAULT 1,
black_ball_max_lives tinyint DEFAULT 1,
admin_page_id bigint(20) unsigned DEFAULT 0,
public_page_id bigint(20) unsigned DEFAULT 0,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY slug (slug)
) $charset_collate;”;
// Enhanced Players table with black ball tracking
$players_table = $wpdb->prefix . ‘kp_players’;
$sql2 = “CREATE TABLE $players_table (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
event_id bigint(20) unsigned NOT NULL,
display_name varchar(200) NOT NULL,
regular_lives tinyint DEFAULT 3,
black_ball_lives tinyint DEFAULT 0,
total_black_balls int DEFAULT 0,
is_active tinyint DEFAULT 1,
player_number tinyint DEFAULT 0,
current_pool tinyint DEFAULT 1,
order_joined int DEFAULT 0,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY event_id (event_id)
) $charset_collate;”;
// Player Actions History table
$actions_table = $wpdb->prefix . ‘kp_player_actions’;
$sql3 = “CREATE TABLE $actions_table (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
event_id bigint(20) unsigned NOT NULL,
player_id bigint(20) unsigned NOT NULL,
turn_number int NOT NULL,
action_type varchar(20) NOT NULL,
lives_before tinyint DEFAULT 0,
lives_after tinyint DEFAULT 0,
notes varchar(500) DEFAULT ”,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY event_player (event_id, player_id),
KEY turn_order (event_id, turn_number)
) $charset_collate;”;
dbDelta($sql1);
dbDelta($sql2);
dbDelta($sql3);
// Set default master pages
if (!get_option(‘kp_admin_master_id’)) {
update_option(‘kp_admin_master_id’, 3273);
}
if (!get_option(‘kp_public_master_id’)) {
update_option(‘kp_public_master_id’, 3209);
}
}
// Add admin menu
add_action(‘admin_menu’, ‘kp_complete_menu’);
function kp_complete_menu() {
add_menu_page(
‘Killer Pool’,
‘Killer Pool’,
‘manage_options’,
‘killer-pool-complete’,
‘kp_complete_admin_page’,
‘dashicons-games’,
30
);
add_submenu_page(
‘killer-pool-complete’,
‘Create Event’,
‘Create Event’,
‘manage_options’,
‘killer-pool-create-complete’,
‘kp_complete_create_page’
);
add_submenu_page(
‘killer-pool-complete’,
‘All Events’,
‘All Events’,
‘manage_options’,
‘killer-pool-all-complete’,
‘kp_complete_all_page’
);
add_submenu_page(
‘killer-pool-complete’,
‘Settings’,
‘Settings’,
‘manage_options’,
‘killer-pool-settings-complete’,
‘kp_complete_settings_page’
);
}
// Settings page to configure master pages
function kp_complete_settings_page() {
// Allow WP media frame on this page
if (function_exists(‘wp_enqueue_media’)) wp_enqueue_media();
if ($_POST[‘save_settings’] ?? false) {
check_admin_referer(‘kp_settings’);
update_option(‘kp_admin_master_id’, absint($_POST[‘admin_master’] ?? 3273));
update_option(‘kp_public_master_id’, absint($_POST[‘public_master’] ?? 3209));
echo ‘<div class=”notice notice-success”><p>Settings saved!</p></div>’;
}
$admin_master = get_option(‘kp_admin_master_id’, 3273);
$public_master = get_option(‘kp_public_master_id’, 3209);
$pages = get_posts([
‘post_type’ => ‘page’,
‘posts_per_page’ => 200,
‘post_status’ => ‘publish’,
‘orderby’ => ‘title’,
‘order’ => ‘ASC’,
]);
echo ‘<div class=”wrap”>’;
echo ‘<h1>🎱 Killer Pool Settings</h1>’;
echo ‘<div style=”max-width: 600px; background: white; padding: 30px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);”>’;
echo ‘<h2>Master Template Pages</h2>’;
echo ‘<form method=”post”>’;
wp_nonce_field(‘kp_settings’);
echo ‘<table class=”form-table”>’;
echo ‘<tr>’;
echo ‘<th><label for=”admin_master”>Admin Master Page</label></th>’;
echo ‘<td>’;
echo ‘<select name=”admin_master” id=”admin_master” style=”width: 100%;”>’;
foreach ($pages as $page) {
$selected = selected($admin_master, $page->ID, false);
echo ‘<option value=”‘ . $page->ID . ‘” ‘ . $selected . ‘>#’ . $page->ID . ‘ – ‘ . esc_html($page->post_title) . ‘</option>’;
}
echo ‘</select>’;
echo ‘<p class=”description”>The Elementor page that contains the admin shortcode. Currently: #’ . $admin_master . ‘</p>’;
echo ‘</td>’;
echo ‘</tr>’;
echo ‘<tr>’;
echo ‘<th><label for=”public_master”>Public Master Page</label></th>’;
echo ‘<td>’;
echo ‘<select name=”public_master” id=”public_master” style=”width: 100%;”>’;
foreach ($pages as $page) {
$selected = selected($public_master, $page->ID, false);
echo ‘<option value=”‘ . $page->ID . ‘” ‘ . $selected . ‘>#’ . $page->ID . ‘ – ‘ . esc_html($page->post_title) . ‘</option>’;
}
echo ‘</select>’;
echo ‘<p class=”description”>The Elementor page that contains the public TV shortcode. Currently: #’ . $public_master . ‘</p>’;
echo ‘</td>’;
echo ‘</tr>’;
echo ‘</table>’;
echo ‘<p class=”submit”>’;
echo ‘<input type=”submit” name=”save_settings” class=”button button-primary” value=”Save Settings”>’;
echo ‘</p>’;
echo ‘</form>’;
echo ‘</div>’;
echo ‘</div>’;
}
// Enhanced Create Event Page with Automatic Page Cloning
function kp_complete_create_page() {
// Handle event creation with automatic page cloning
if ($_POST[‘create_event_complete’] ?? false) {
check_admin_referer(‘create_event_complete’);
$venue = sanitize_text_field($_POST[‘venue’] ?? ‘New Venue’);
$date = sanitize_text_field($_POST[‘event_date’] ?? date(‘Y-m-d’));
$time = sanitize_text_field($_POST[‘event_time’] ?? ‘7:00 PM’);
// Generate event details
$ymd = date(‘Y-m-d’, strtotime($date));
$dmy = date(‘d-m-Y’, strtotime($date));
$dow = date(‘D’, strtotime($date));
$venue_slug = sanitize_title($venue);
// Create unique slugs
$event_slug = ‘event-‘ . $ymd . ‘-‘ . $venue_slug;
$admin_slug = ‘admin-‘ . $ymd . ‘-‘ . $venue_slug;
// Create page titles
$event_title = $venue . ‘ · ‘ . $dow . ‘ ‘ . $time;
$admin_title = $venue . ‘ · Admin · ‘ . $ymd;
// Get master pages
$admin_master = get_option(‘kp_admin_master_id’, 3273);
$public_master = get_option(‘kp_public_master_id’, 3209);
$duplicator = new KP_Elementor_Duplicator();
try {
// Clone public page
$public_id = $duplicator->duplicate($public_master, $event_title, $event_slug, true);
// Clone admin page
$admin_id = $duplicator->duplicate($admin_master, $admin_title, $admin_slug, true);
} catch (Exception $e) {
error_log(‘Killer Pool: Cloning error – ‘ . $e->getMessage());
$public_id = new WP_Error(‘cloning_failed’, ‘Page cloning failed: ‘ . $e->getMessage());
$admin_id = new WP_Error(‘cloning_failed’, ‘Page cloning failed: ‘ . $e->getMessage());
}
// Always create the game event, even if cloning fails
$game_event = kp_complete_create_event($event_slug, $event_title, $venue);
$cloning_success = !is_wp_error($public_id) && !is_wp_error($admin_id);
if ($game_event) {
$update_data = [‘admin_page_id’ => $cloning_success ? $admin_id : 0,
‘public_page_id’ => $cloning_success ? $public_id : 0];
kp_complete_update_event($game_event[‘id’], $update_data);
}
if ($cloning_success) {
// Add iframe shortcodes to both pages (no complex injection needed)
kp_add_simple_iframe($public_id, $event_slug, ‘public’);
kp_add_simple_iframe($admin_id, $event_slug, ‘admin’);
// Update pages with cross-references and event data
update_post_meta($public_id, ‘_kp_event_slug’, $event_slug);
update_post_meta($public_id, ‘_kp_event_venue’, $venue);
update_post_meta($public_id, ‘_kp_event_date’, $ymd);
update_post_meta($public_id, ‘_kp_admin_pair’, $admin_id);
update_post_meta($admin_id, ‘_kp_event_slug’, $event_slug);
update_post_meta($admin_id, ‘_kp_event_venue’, $venue);
update_post_meta($admin_id, ‘_kp_event_date’, $ymd);
update_post_meta($admin_id, ‘_kp_public_pair’, $public_id);
// Success message with links
$public_url = get_permalink($public_id);
$admin_url = get_permalink($admin_id);
$manage_url = add_query_arg([‘page’ => ‘killer-pool-complete’, ‘event’ => $event_slug], admin_url(‘admin.php’));
echo ‘<div class=”notice notice-success” style=”padding: 20px;”>’;
echo ‘<h3>✅ Event Created Successfully!</h3>’;
echo ‘<p><strong>Pages automatically created with Elementor styling preserved:</strong></p>’;
echo ‘<p>’;
echo ‘<a href=”‘ . esc_url($public_url) . ‘” target=”_blank” class=”button button-primary”>📺 View Public TV Page</a> ‘;
echo ‘<a href=”‘ . esc_url($admin_url) . ‘” target=”_blank” class=”button button-secondary”>📱 View Admin Page</a> ‘;
echo ‘<a href=”‘ . esc_url($manage_url) . ‘” class=”button”>🎱 Manage Tournament</a>’;
echo ‘</p>’;
echo ‘<p><strong>Shortcodes (already embedded in pages):</strong></p>’;
echo ‘<p><code>
Tournament “esc_htmlevent_slug” not found.
echo ‘<p><code>
Tournament “esc_htmlevent_slug” not found.
echo ‘</div>’;
} else {
// Cloning failed, but event created – show partial success
$manage_url = add_query_arg([‘page’ => ‘killer-pool-complete’, ‘event’ => $event_slug], admin_url(‘admin.php’));
echo ‘<div class=”notice notice-warning” style=”padding: 20px;”>’;
echo ‘<h3>⚠️ Event Created, But Page Cloning Failed</h3>’;
echo ‘<p>Tournament data saved successfully, but automatic page creation encountered an error (likely missing master templates). You can manually create pages or check settings.</p>’;
echo ‘<p><a href=”‘ . esc_url($manage_url) . ‘” class=”button button-primary”>🎱 Manage Tournament</a></p>’;
echo ‘<p><strong>Manual Shortcodes:</strong></p>’;
echo ‘<p><code>
Tournament “esc_htmlevent_slug” not found.
echo ‘<p><code>
Tournament “esc_htmlevent_slug” not found.
if (is_wp_error($public_id)) {
echo ‘<p><strong>Error:</strong> ‘ . esc_html($public_id->get_error_message()) . ‘</p>’;
}
echo ‘</div>’;
}
}
echo ‘<div class=”wrap”>’;
echo ‘<h1>🎱 Create New Tournament</h1>’;
echo ‘<p class=”description”>This will automatically create both admin and public pages with your Elementor styling preserved.</p>’;
echo ‘<div style=”max-width: 600px; background: white; padding: 30px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);”>’;
echo ‘<h2>Event Details</h2>’;
echo ‘<form method=”post”>’;
wp_nonce_field(‘create_event_complete’);
echo ‘<table class=”form-table”>’;
echo ‘<tr>’;
echo ‘<th><label for=”venue”>Venue Name</label></th>’;
echo ‘<td><input type=”text” name=”venue” id=”venue” value=”” style=”width: 100%; padding: 10px;” placeholder=”e.g., Sin Bin Pool Hall” required></td>’;
echo ‘</tr>’;
echo ‘<tr>’;
echo ‘<th><label for=”event_date”>Event Date</label></th>’;
echo ‘<td><input type=”date” name=”event_date” id=”event_date” value=”‘ . date(‘Y-m-d’) . ‘” style=”width: 100%; padding: 10px;” required></td>’;
echo ‘</tr>’;
echo ‘<tr>’;
echo ‘<th><label for=”event_time”>Event Time</label></th>’;
echo ‘<td><input type=”text” name=”event_time” id=”event_time” value=”7:00 PM” style=”width: 100%; padding: 10px;” placeholder=”e.g., 7:00 PM” required></td>’;
echo ‘</tr>’;
echo ‘</table>’;
echo ‘<p class=”submit”>’;
echo ‘<input type=”submit” name=”create_event_complete” class=”button button-primary button-large” value=”🎯 Create Event & Pages”>’;
echo ‘</p>’;
echo ‘</form>’;
echo ‘<div style=”background: #f0f9ff; padding: 20px; border-radius: 8px; margin-top: 20px; border-left: 4px solid #3b82f6;”>’;
echo ‘<h3 style=”margin-top: 0;”>What happens when you create an event:</h3>’;
echo ‘<ul>’;
echo ‘<li>📋 <strong>Game data created</strong> – New tournament with unique slug</li>’;
echo ‘<li>📺 <strong>Public TV page cloned</strong> – From master page #’ . get_option(‘kp_public_master_id’, 3209) . ‘ with all Elementor styling</li>’;
echo ‘<li>📱 <strong>Admin control page cloned</strong> – From master page #’ . get_option(‘kp_admin_master_id’, 3273) . ‘ with mobile interface</li>’;
echo ‘<li>🔗 <strong>Shortcodes auto-embedded</strong> – Event-specific shortcodes automatically added to cloned pages</li>’;
echo ‘<li>🎱 <strong>Ready to use</strong> – Start adding players immediately!</li>’;
echo ‘</ul>’;
echo ‘</div>’;
echo ‘</div>’;
echo ‘</div>’;
}
// Main admin page – Tournament Control Panel
function kp_complete_admin_page() {
global $wpdb; // Ensure $wpdb available in shortcode/Elementor context
nocache_headers();
$event_slug = $_GET[‘event’] ?? ‘demo’;
$event = kp_complete_get_event($event_slug);
// Set event slug for JavaScript
// Set event slug + KP debug flags for JS (top of first script on this page)
echo ‘<script>
// —– Global config flags from WP options —–
window.KPC = window.KPC || {};
window.KPC.polling_enabled = ‘ . json_encode( (bool) get_option(‘kp_polling_enabled’, true) ) . ‘;
window.KPC.timer_enabled = ‘ . json_encode( (bool) get_option(‘kp_timer_enabled’, false) ) . ‘;
// Debug wrapper (quiet by default)
if (!(“KP_DEBUG” in window)) window.KP_DEBUG = false;
if (!(“kp_debug” in window)) window.kp_debug = (…a) => { if (window.KP_DEBUG) console.debug(“[KP]”, …a); };
// Event + REST base
window.kp_event_slug = ‘ . json_encode($event_slug) . ‘;
window.eventSlug = window.kp_event_slug;
window.restBase = window.restBase || ‘ . json_encode( rest_url(“killer-pool/v1/”) ) . ‘;
</script>’;
if (!$event) {
$event = kp_complete_create_event($event_slug, ‘Demo Event’);
}
$players = kp_complete_get_players($event[‘id’]);
// Enqueue media for video uploads
if (function_exists(‘wp_enqueue_media’)) {
wp_enqueue_media();
}
// Handle adding players
if ($_POST[‘add_players’] ?? false) {
check_admin_referer(‘add_players’);
$names = array_filter(array_map(‘trim’, explode(“\n”, $_POST[‘player_names’] ?? ”)));
$success_count = 0;
$error_names = [];
foreach ($names as $name) {
if (empty($name)) continue;
$result = kp_complete_add_player($event[‘id’], $name);
if (is_wp_error($result)) {
$error_names[] = esc_html($name) . ‘: ‘ . $result->get_error_message();
} else {
$success_count++;
}
}
if ($success_count > 0) {
echo ‘<div class=”notice notice-success”><p>✅ ‘ . $success_count . ‘ player’ . ($success_count > 1 ? ‘s’ : ”) . ‘ added successfully!</p></div>’;
}
if (!empty($error_names)) {
echo ‘<div class=”notice notice-warning”><p>⚠️ Some players could not be added:<br>’ . implode(‘<br>’, $error_names) . ‘</p></div>’;
}
$players = kp_complete_get_players($event[‘id’]);
}
// Handle START GAME
if ($_POST[‘start_game’] ?? false) {
check_admin_referer(‘start_game’);
kp_complete_start_game($event[‘id’]);
echo ‘<div class=”notice notice-success”><p>🎮 Game Started! Players are now in the pool system.</p></div>’;
$event = kp_complete_get_event($event_slug); // Refresh event data
}
// Handle NEXT PLAYER selection (animated)
if ($_POST[‘select_next_player’] ?? false) {
check_admin_referer(‘select_next_player’);
$next_player = kp_complete_select_next_player($event[‘id’]);
if ($next_player) {
echo ‘<div class=”notice notice-success”><p>🎯 Next player selected: ‘ . esc_html($next_player[‘display_name’]) . ‘</p></div>’;
$event = kp_complete_get_event($event_slug); // Refresh event data
}
}
// Handle timer settings
if ($_POST[‘save_timer_settings’] ?? false) {
check_admin_referer(‘save_timer_settings’);
$timer_settings = [
‘timer_enabled’ => isset($_POST[‘timer_enabled’]) ? 1 : 0,
‘timer_duration’ => intval($_POST[‘timer_duration’] ?? 60),
‘timer_auto_loss’ => isset($_POST[‘timer_auto_loss’]) ? 1 : 0,
‘timer_buffer_enabled’ => isset($_POST[‘timer_buffer_enabled’]) ? 1 : 0,
‘timer_buffer_seconds’ => intval($_POST[‘timer_buffer_seconds’] ?? 10)
];
kp_complete_update_event($event[‘id’], $timer_settings);
echo ‘<div class=”notice notice-success”><p>⏰ Timer settings saved!</p></div>’;
$event = kp_complete_get_event($event_slug); // Refresh event data
}
// Handle elimination video settings
if ($_POST[‘save_elimination_videos’] ?? false) {
check_admin_referer(‘save_elimination_videos’);
$elim_enabled = isset($_POST[‘elimination_enabled’]) ? 1 : 0;
update_option(‘kp_elim_enabled_event_’ . $event[‘id’], $elim_enabled);
$videos = [];
if (isset($_POST[‘elimination_videos’]) && is_array($_POST[‘elimination_videos’])) {
foreach ($_POST[‘elimination_videos’] as $video_id => $enabled) {
$video_id = intval($video_id);
if ($video_id > 0) {
// Validate attachment exists and is video
$attachment = get_post($video_id);
if ($attachment && $attachment->post_type === ‘attachment’ && strpos($attachment->post_mime_type, ‘video/’) === 0) {
$videos[] = [‘id’ => $video_id, ‘enabled’ => $enabled ? 1 : 0];
}
}
}
}
update_option(‘kp_elim_videos_event_’ . $event[‘id’], $videos);
echo ‘<div class=”notice notice-success”><p>🎬 Elimination video settings saved!</p></div>’;
$event = kp_complete_get_event($event_slug); // Refresh event data
}
// Handle black ball settings
if ($_POST[‘save_black_ball_settings’] ?? false) {
check_admin_referer(‘save_black_ball_settings’);
// Fetch current event to validate against starting_lives
$current_event = kp_complete_get_event($event_slug);
$black_ball_settings = [
‘black_ball_enabled’ => isset($_POST[‘black_ball_enabled’]) ? 1 : 0,
‘max_lives’ => intval($_POST[‘black_ball_max_lives’] ?? 5) // Remap UI “Maximum Lives Cap” to total hard cap
];
// Store per-player black ball cap as WP option (not DB column)
$per_player_limit = intval($_POST[‘black_ball_lives_per_player’] ?? 1);
update_option(‘kp_bb_cap_event_’ . $event[‘id’], $per_player_limit);
// Validate: if new max_lives < starting_lives, adjust starting_lives to match max
if ($black_ball_settings[‘max_lives’] < intval($current_event[‘starting_lives’] ?? 3)) {
$black_ball_settings[‘starting_lives’] = $black_ball_settings[‘max_lives’];
echo ‘<div class=”notice notice-warning”><p>⚠️ Maximum lives set below starting lives; starting lives adjusted to match.</p></div>’;
}
kp_complete_update_event($event[‘id’], $black_ball_settings);
echo ‘<div class=”notice notice-success”><p>🖤 Black Ball settings saved!</p></div>’;
$event = kp_complete_get_event($event_slug); // Refresh event data
}
// === Gameplay / Rotation Settings (Alternate final 2) ===
if ( isset($_POST[‘save_gameplay_settings’]) ) {
check_admin_referer(‘save_gameplay_settings’);
$alt_final2 = isset($_POST[‘alternate_final_two’]) ? 1 : 0;
update_option(‘kp_alt_final2_event_’ . $event[‘id’], $alt_final2);
echo ‘<div class=”notice notice-success”><p>🎯 Gameplay settings saved!</p></div>’;
// (optional) refresh event if you rely on it below:
// $event = kp_complete_get_event($event_slug);
}
// Handle player actions
if ($_POST[‘player_action’] ?? false) {
check_admin_referer(‘player_action’);
$player_id = intval($_POST[‘player_id’]);
$action = sanitize_text_field($_POST[‘action’]);
$player = kp_complete_get_player($player_id);
if ($player) {
$lives = intval($player[‘regular_lives’] ?? 0) + intval($player[‘black_ball_lives’] ?? 0);
KP_LOG(“Processing action for player $player_id”);
switch ($action) {
case ‘potted’:
// Log successful shot
kp_complete_log_player_action($event[‘id’], $player_id, ‘POTTED’, $lives, $lives, ‘Potted ball successfully’);
$message = “✅ {$player[‘display_name’]} potted a ball!”;
break;
case ‘missed’:
// Take regular lives first, then black ball lives
$regular_lives = intval($player[‘regular_lives’]);
$black_ball_lives = intval($player[‘black_ball_lives’]);
if ($regular_lives > 0) {
// Take from regular lives first
$new_regular = $regular_lives – 1;
$new_black_ball = $black_ball_lives;
$life_type = ‘regular’;
} else {
// Only take from black ball lives if no regular lives left
$new_regular = 0;
$new_black_ball = max(0, $black_ball_lives – 1);
$life_type = ‘black ball’;
}
$new_total_lives = $new_regular + $new_black_ball;
kp_complete_update_player($player_id, [
‘regular_lives’ => $new_regular,
‘black_ball_lives’ => $new_black_ball
]);
if ($new_total_lives == 0) {
kp_complete_update_player($player_id, [‘is_active’ => 0]);
$message = “💀 {$player[‘display_name’]} eliminated!”;
} else {
$message = “❌ {$player[‘display_name’]} missed – lost 1 {$life_type} life, now has {$new_total_lives} lives”;
}
// Log missed shot
kp_complete_log_player_action($event[‘id’], $player_id, ‘MISSED’, $lives, $new_total_lives, “Missed shot – lost 1 {$life_type} life”);
break;
case ‘black_ball’:
// Check if black ball is enabled
if (!$event[‘black_ball_enabled’]) {
// Black ball disabled – just a regular shot
$message = “🖤 {$player[‘display_name’]} potted black ball (bonus disabled)”;
// Log as regular shot
kp_complete_log_player_action($event[‘id’], $player_id, ‘POTTED’, $lives, $lives, ‘Potted black ball – no bonus (disabled)’);
break;
}
// FIXED: Get proper field separation for black ball logic
$regular_lives = intval($player[‘regular_lives’] ?? 0);
$black_ball_lives = intval($player[‘black_ball_lives’] ?? 0);
$max_lives_cap = intval($event[‘max_lives’] ?? 5); // Use max_lives for total hard cap
$current_black_balls = intval($wpdb->get_var($wpdb->prepare(“SELECT COALESCE(total_black_balls, 0) FROM {$wpdb->prefix}kp_players WHERE id = %d”, $player_id)) ?? 0);
// Get per-player black ball cap from WP options (not DB column)
$per_player_limit = intval(get_option(‘kp_bb_cap_event_’ . $event[‘id’], 1));
// Check all conditions for black ball bonus (BLACK BALL LIVES ONLY)
if ( ($regular_lives + $black_ball_lives) >= $max_lives_cap ) {
// Already at total hard cap
$message = “🖤 {$player[‘display_name’]} potted black ball – already at maximum total lives ({$max_lives_cap})”;
kp_complete_log_player_action($event[‘id’], $player_id, ‘POTTED’, $lives, $lives, ‘Potted black ball – at total max lives cap (‘ . $max_lives_cap . ‘)’);
} else if ($per_player_limit < 999 && $current_black_balls >= $per_player_limit) {
// Player has reached their personal black ball limit
$message = “🖤 {$player[‘display_name’]} potted black ball – reached bonus limit ({$current_black_balls}/{$per_player_limit} per player)”;
kp_complete_log_player_action($event[‘id’], $player_id, ‘POTTED’, $lives, $lives, ‘Potted black ball – at personal bonus limit (‘ . $per_player_limit . ‘)’);
} else {
// Award BLACK BALL LIFE ONLY (not regular life)
$new_black_ball_lives = $black_ball_lives + 1;
$new_total_lives = $regular_lives + $new_black_ball_lives;
$new_black_balls = $current_black_balls + 1;
// CRITICAL: Enforce total cap
if ($new_total_lives > $max_lives_cap) {
$message = “🖤 {$player[‘display_name’]} potted black ball – would exceed maximum total lives ({$max_lives_cap})”;
kp_complete_log_player_action($event[‘id’], $player_id, ‘POTTED’, $lives, $lives, ‘Potted black ball – would exceed total cap (‘ . $max_lives_cap . ‘)’);
} else {
// Award the black ball life
kp_complete_update_player($player_id, [
‘black_ball_lives’ => $new_black_ball_lives,
‘total_black_balls’ => $new_black_balls
]);
$message = “🖤 {$player[‘display_name’]} potted black ball – gained bonus life! Total: {$new_total_lives} lives ({$new_black_balls}/{$per_player_limit} black ball bonuses used)”;
// Log black ball bonus
kp_complete_log_player_action($event[‘id’], $player_id, ‘BLACK_BALL’, $lives, $new_total_lives, ‘Potted black ball – gained 1 BLACK BALL life (total: ‘ . $new_total_lives . ‘, bonus: ‘ . $new_black_balls . ‘/’ . $per_player_limit . ‘)’);
}
}
break;
case ‘foul’:
// Take regular lives first, then black ball lives
$regular_lives = intval($player[‘regular_lives’]);
$black_ball_lives = intval($player[‘black_ball_lives’]);
if ($regular_lives > 0) {
// Take from regular lives first
$new_regular = $regular_lives – 1;
$new_black_ball = $black_ball_lives;
$life_type = ‘regular’;
} else {
// Only take from black ball lives if no regular lives left
$new_regular = 0;
$new_black_ball = max(0, $black_ball_lives – 1);
$life_type = ‘black ball’;
}
$new_total_lives = $new_regular + $new_black_ball;
kp_complete_update_player($player_id, [
‘regular_lives’ => $new_regular,
‘black_ball_lives’ => $new_black_ball
]);
if ($new_total_lives == 0) {
kp_complete_update_player($player_id, [‘is_active’ => 0]);
$message = “💀 {$player[‘display_name’]} fouled and eliminated!”;
} else {
$message = “⚠️ {$player[‘display_name’]} fouled – lost 1 {$life_type} life, now has {$new_total_lives} lives”;
}
// Log foul
kp_complete_log_player_action($event[‘id’], $player_id, ‘FOUL’, $lives, $new_total_lives, “Committed foul – lost 1 {$life_type} life”);
break;
}
// === KP-GOV PATCH D_ADMIN: ledger ACTION_RECORDED (+optional ELIMINATED)
if (function_exists(‘kp_governor_ledger_log’)) {
// Re-resolve current shooter AFTER the action logic ran
$curr = kp_complete_get_current_player($event[‘id’]);
$pid = intval($curr[‘id’] ?? 0);
if ($pid > 0) {
$after = kp_complete_get_player($pid);
if ($after) {
$after_regular = intval($after[‘regular_lives’] ?? 0);
$after_black = intval($after[‘black_ball_lives’] ?? 0);
$after_total = $after_regular + $after_black;
$eliminated = ((int)($after[‘is_active’] ?? 1) === 0 || $after_total <= 0) ? 1 : 0;
kp_governor_ledger_log($event[‘id’], ‘ACTION_RECORDED’, array(
‘action’ => $action, // whatever you set earlier from POST (potted|missed|foul|black_potted)
‘player_id’ => $pid,
‘ts’ => time(),
‘lives’ => array(‘regular’ => $after_regular, ‘black’ => $after_black),
‘eliminated’ => $eliminated,
));
if ($eliminated) {
kp_governor_ledger_log($event[‘id’], ‘PLAYER_ELIMINATED’, array(
‘player_id’ => $pid,
‘ts’ => time(),
));
}
}
}
}
// === /KP-GOV PATCH D_ADMIN ===
// After processing the action, automatically select next player
$next_player = kp_complete_select_next_player($event[‘id’]);
if ($next_player) {
$message .= ” Next up: ” . $next_player[‘display_name’];
}
echo ‘<div class=”notice notice-success”><p>’ . $message . ‘</p></div>’;
$players = kp_complete_get_players($event[‘id’]);
}
}
// Reset game
if ($_POST[‘reset_game’] ?? false) {
check_admin_referer(‘reset_game’);
kp_complete_reset_event($event[‘id’]);
echo ‘<div class=”notice notice-success”><p>🔄 Game reset! All players restored to 3 lives.</p></div>’;
$players = kp_complete_get_players($event[‘id’]);
$event = kp_complete_get_event($event_slug); // Refresh event data
}
echo ‘<div class=”wrap” id=”kp-admin-wrap” style=”max-width: 1400px;”>’;
echo ‘<h1>🎱 Killer Pool Control Panel</h1>’;
// Event header with page links
echo ‘<div style=”background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%); color: white; padding: 20px; border-radius: 12px; margin-bottom: 25px; text-align: center;”>’;
echo ‘<h2 style=”margin: 0; font-size: 24px;”>’ . esc_html($event[‘name’]) . ‘</h2>’;
echo ‘<p style=”margin: 8px 0 15px 0; opacity: 0.9;”>Event: ‘ . esc_html($event_slug) . ‘ • ‘ . count($players) . ‘ players total</p>’;
// Quick links to cloned pages
if ($event[‘admin_page_id’]) {
$admin_url = get_permalink($event[‘admin_page_id’]);
echo ‘<a href=”‘ . esc_url($admin_url) . ‘” target=”_blank” style=”background: rgba(255,255,255,0.2); color: white; padding: 8px 16px; border-radius: 8px; text-decoration: none; margin: 5px; display: inline-block;”>📱 Mobile Admin Page</a>’;
}
if ($event[‘public_page_id’]) {
$public_url = get_permalink($event[‘public_page_id’]);
echo ‘<a href=”‘ . esc_url($public_url) . ‘” target=”_blank” style=”background: rgba(255,255,255,0.2); color: white; padding: 8px 16px; border-radius: 8px; text-decoration: none; margin: 5px; display: inline-block;”>📺 Public TV Page</a>’;
}
echo ‘</div>’;
// === Emergency “Force Next Shooter” panel — inline, always visible, above action panel ===
$__kp_fn_nonce = wp_create_nonce(‘wp_rest’);
$__kp_event_slug = isset($event_slug) ? $event_slug : (isset($slug) ? $slug : ”);
?>
<section id=”kp-force-next-panel”
data-slug=”<?php echo esc_attr($__kp_event_slug); ?>”
data-nonce=”<?php echo esc_attr($__kp_fn_nonce); ?>”
style=”margin:12px 0 10px 0;padding:12px 14px;border-radius:14px;
background:#ffffffee;backdrop-filter:saturate(1.05) blur(4px);
border:1px solid rgba(0,0,0,.08);box-shadow:0 3px 10px rgba(0,0,0,.06);”>
<div style=”display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex-wrap:wrap”>
<div style=”font:700 14px system-ui,Segoe UI,Roboto,Arial;color:#222″>Emergency Control</div>
<button id=”kp-force-next-button” type=”button”
style=”padding:10px 14px;border:0;border-radius:10px;
font:700 14px system-ui,Segoe UI,Roboto,Arial;color:#fff;
background:#ff7a00;box-shadow:0 2px 6px rgba(0,0,0,.18);cursor:pointer”>
⏭ Force Next Shooter
</button>
</div>
</section>
<script>
(function () {
const panel = document.getElementById(‘kp-force-next-panel’);
const btn = document.getElementById(‘kp-force-next-button’);
if (!panel || !btn) return;
const getSlug = () =>
panel.dataset.slug ||
window.eventSlug ||
window.currentEventSlug ||
window.kpEventSlug ||
window.__kpLastState?.event?.slug ||
document.querySelector(‘[data-event-slug]’)?.getAttribute(‘data-event-slug’);
const getNonce = () =>
panel.dataset.nonce || window.wpRestNonce || window.wpApiSettings?.nonce || ”;
const apiBase = () => {
const raw = window.restBase || window.kpRestBase || (window.wpApiSettings && window.wpApiSettings.root) || ‘/wp-json’;
const base = String(raw).replace(/\/+$/,”); // trim trailing slashes
return base.includes(‘/killer-pool/v1’) ? base : base + ‘/killer-pool/v1’;
};
const pingHint = async (slug) => {
try {
await fetch(apiBase() + ‘/hint/’ + encodeURIComponent(slug), {
method: ‘POST’,
credentials: ‘same-origin’,
headers: {
‘Content-Type’: ‘application/json’,
‘X-WP-Nonce’: getNonce()
},
body: ‘{}’
});
} catch (e) {
console.warn(‘KP: hint ping failed’, e);
}
};
// Optional cross-tab nudge
let bc = null;
try { bc = new BroadcastChannel(‘killer_pool_events’); } catch {}
async function forceNext() {
const slug = getSlug();
if (!slug) { alert(‘No event slug found’); return; }
const url = apiBase() + ‘/force-next-shooter/’ + encodeURIComponent(slug);
// UI busy
const prevTxt = btn.textContent;
btn.disabled = true; btn.textContent = ‘Forcing…’;
try {
const res = await fetch(url, {
method: ‘POST’,
headers: {
‘X-WP-Nonce’: getNonce(),
‘Accept’: ‘application/json’
}
});
let body = null; try { body = await res.json(); } catch {}
if (!res.ok || !(body?.success || body?.status === ‘ok’)) {
throw new Error(body?.message || (‘HTTP ‘ + res.status));
}
const name =
body?.current_player?.display_name ||
body?.chosen?.display_name ||
(‘Player ‘ + (body?.current_player?.id || body?.chosen?.id || ‘?’));
console.log(‘KP ADMIN: Force next OK →’, name, body);
// Notify TVs/tabs and bump hint so pollers refresh
try { bc?.postMessage({ slug, reason: ‘force-next’, ts: Date.now() }); } catch {}
await pingHint(slug);
btn.textContent = ‘✅ Forced’;
setTimeout(() => { btn.textContent = prevTxt; }, 1000);
alert(‘Forced next shooter: ‘ + name);
} catch (e) {
console.error(‘KP ADMIN: Force next failed’, e);
alert(‘Force next failed: ‘ + (e?.message || e));
} finally {
btn.disabled = false;
}
}
btn.addEventListener(‘click’, forceNext);
document.addEventListener(‘keydown’, (e) => {
const t = e.target;
const typing = t && (
t.tagName === ‘INPUT’ || t.tagName === ‘TEXTAREA’ || t.isContentEditable ||
(t.closest && t.closest(‘input,textarea,[contenteditable],[contenteditable=”true”]’))
);
if (typing) return;
if ((e.key === ‘n’ || e.key === ‘N’) && !e.ctrlKey && !e.metaKey && !e.altKey && !e.repeat) {
e.preventDefault();
forceNext();
}
});
})();
</script>
<?php
// ACTION BUTTONS FIRST – BEFORE EVERYTHING ELSE!
// KP_PATCH:kp_admin_actions
kp_include_patch(‘kp_admin_actions’);
// KP_PATCH:kp_chipbar
kp_include_patch(‘kp_chipbar’);
echo ‘<style>
/* CSS kill-switch for purple admin winner elements */
#kp-admin-winner { display:none !important; }
/* Quick Action Buttons: polished, mobile-safe, consistent */
.kp-admin-action {
display: inline-flex; align-items: center; justify-content: center;
gap: 10px;
border: 2px solid #000 !important; /* keep shape in dark mode */
border-radius: 14px !important;
box-sizing: border-box;
color: #fff !important;
font-weight: 800; letter-spacing: .2px;
user-select: none; -webkit-user-select: none;
-webkit-appearance: none; appearance: none;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
outline: none;
color-scheme: light;
transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease, opacity 120ms ease;
will-change: transform;
min-height: 80px; width: 100%;
/* Kill WP admin button chrome that causes white lines (without removing gradients) */
text-shadow: none !important;
box-shadow: 0 4px 15px rgba(0,0,0,0.2) !important;
background-clip: padding-box !important;
}
.kp-actions-grid .kp-admin-action,
#admin-action-box .kp-admin-action,
.kp-admin-action.button.button-primary,
.kp-admin-action.button.button-secondary {
border: 2px solid #000 !important; /* override WP .button-primary/.button-secondary */
border-radius: 14px !important;
text-shadow: none !important;
box-shadow: 0 4px 15px rgba(0,0,0,0.2) !important; /* replace inset white line */
background-clip: padding-box !important;
}
.kp-admin-action:focus { outline: none !important; }
.kp-admin-action:active { transform: scale(0.985); }
.kp-admin-action[disabled] { opacity: .65 !important; filter: saturate(.8) contrast(.95); cursor: not-allowed; }
/* Explicit backgrounds that won\’t flip in OS dark mode */
.kp-admin-action[data-admin-action=”potted”] { background: linear-gradient(135deg, #00c853 0%, #00a152 100%) !important; }
.kp-admin-action[data-admin-action=”missed”] { background: linear-gradient(135deg, #ff1744 0%, #d50000 100%) !important; }
.kp-admin-action[data-admin-action=”foul”] { background: linear-gradient(135deg, #f3f4f6 0%, #9ca3af 100%) !important; }
.kp-admin-action[data-admin-action=”black-potted”] { background: linear-gradient(135deg, #2979ff 0%, #1c54b2 100%) !important; }
/* Spinner inside buttons */
.kp-btn-spinner { width: 18px; height: 18px; border: 2px solid rgba(255,255,255,.35); border-top-color: #fff; border-radius: 50%;
animation: kp-spin 0.9s linear infinite; flex: 0 0 18px; }
@keyframes kp-spin { to { transform: rotate(360deg); } }
/* Emoji icons for action buttons */
.kp-emoji { font-size: 28px; line-height: 1; display: inline-block; }
.kp-emoji-spin { display: inline-block; animation: kp-spin 1s linear infinite; }
/* Icon-only labels on action buttons (fallback, no HTML changes needed) */
.kp-admin-action[data-admin-action] { color: transparent; }
.kp-admin-action[data-admin-action]::before { font-size: 28px; line-height: 1; display: inline-block; }
.kp-admin-action[data-admin-action=”potted”]::before { content: “\2713”; color: #fff; }
.kp-admin-action[data-admin-action=”missed”]::before { content: “\2716”; color: #000; }
.kp-admin-action[data-admin-action=”black-potted”]::before { content: “\01F3B1”; }
/* Use emoji variant to get yellow triangle with black ! */
.kp-admin-action[data-admin-action=”foul”]::before { content: “\26A0\FE0F”; }
/* Busy state animations: pulse for all, spin+pulse for black-potted */
@keyframes kp-pulse { 0%,100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.15); opacity: .85; } }
.kp-admin-action.kp-busy { opacity: .85; }
.kp-admin-action.kp-busy::before { animation: kp-pulse 1s ease-in-out infinite; }
.kp-admin-action.kp-busy[data-admin-action=”black-potted”]::before { animation: kp-spin .8s linear infinite, kp-pulse 1s ease-in-out infinite; }
/* Toast (auto-dismiss) */
.kp-toast { position: fixed; left: 50%; transform: translateX(-50%);
bottom: 24px; z-index: 10000; background: #111827; color: #fff; border: 2px solid #000;
padding: 10px 16px; border-radius: 10px; font-weight: 700; box-shadow: 0 6px 20px rgba(0,0,0,.25);
opacity: 0; transition: opacity 150ms ease, transform 150ms ease; pointer-events: none; }
.kp-toast.kp-show { opacity: 1; transform: translate(-50%, -4px); }
/* Container stability on mobile */
#admin-action-box { contain: content; }
.kp-actions-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; align-items: stretch; }
@media (max-width: 768px) {
#admin-action-box { position: relative !important; top: auto !important; }
.kp-actions-grid { grid-template-columns: 1fr 1fr; }
}
/* ========= CLEAN CSS GRID FIX FOR ADMIN PANEL ========= */
/* Players grid: single column by default; let children actually shrink */
.kp-players-grid {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 25px;
margin: 25px 0;
width: 100%;
box-sizing: border-box;
}
/* Grid items must be allowed to shrink below their min-content width */
.kp-players-grid > * {
min-width: 0; /* ← KEY FIX: Allows shrinking below min-content */
box-sizing: border-box;
width: 100%;
}
/* Handle long strings / tags / chips in grid content */
.kp-players-grid .content {
overflow-wrap: anywhere;
word-break: break-word;
}
/* Mobile responsive adjustments for grid */
@media (max-width: 768px) {
.kp-players-grid {
gap: 15px;
margin: 15px 0;
}
.kp-players-grid > * {
padding: 15px;
}
}
</style>’;
if (!empty($players)) {
$alive_players = array_filter($players, function($p) {
return $p[‘is_active’] && $p[‘lives’] > 0;
});
if (count($alive_players) === 1 && $event[‘game_state’] === ‘active’) {
kp_complete_update_event($event[‘id’], [‘game_state’ => ‘completed’]);
$event = kp_complete_get_event($event_slug);
}
$current_player = kp_complete_get_current_player($event[‘id’]);
// ✅ WINNER ANNOUNCEMENT
if (false && count($alive_players) === 1) {
$winner = array_values($alive_players)[0];
$winner_name = isset($winner[‘display_name’]) ? $winner[‘display_name’] : ‘Winner’;
echo ‘<div style=”background: linear-gradient(135deg, #6d28d9 0%, #7c3aed 50%, #a855f7 100%); color: white; padding: 25px; border-radius: 15px; margin: 20px 0; box-shadow: 0 6px 20px rgba(0,0,0,0.3);”>’;
echo ‘<h2 style=”margin: 0 0 15px 0; font-size: 24px; text-align: center;”>🏆 WINNER ANNOUNCED</h2>’;
echo ‘<div style=”font-size: 28px; font-weight: 800; text-align: center;”>’ . esc_html($winner_name) . ‘</div>’;
echo ‘<div style=”opacity: 0.9; text-align: center; margin-top: 8px;”>Tournament completed</div>’;
echo ‘</div>’;
}
// ✅ ACTION BUTTONS (only if still active + more than 1 alive)
elseif ($event[‘game_state’] === ‘active’ && $current_player && !empty($alive_players)) {
echo ‘<div id=”admin-action-box” style=”background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%); color: white; padding: 25px; border-radius: 15px; box-shadow: 0 6px 20px rgba(124, 58, 237, 0.3); margin: 20px 0; position: sticky; top: 20px; z-index: 100;”>’;
echo ‘<h2 style=”margin: 0 0 20px 0; font-size: 24px; text-align: center;”>🎯 QUICK ACTIONS – ALWAYS AT TOP</h2>’;
// Current shooter info – compact
echo ‘<div class=”current-player-info” style=”text-align: center; margin-bottom: 20px; background: rgba(255,255,255,0.1); padding: 15px; border-radius: 12px;”>’;
echo ‘<div style=”font-size: 20px; font-weight: 700; margin-bottom: 5px;”>’ . esc_html($current_player[‘display_name’]) . ‘</div>’;
echo ‘<div style=”opacity: 0.9; font-size: 14px;”>Ball #’ . $current_player[‘player_number’] . ‘ • ‘ . $current_player[‘lives’] . ‘ lives</div>’;
echo ‘</div>’;
// LARGE ACTION BUTTONS – Mobile optimized
echo ‘<div class=”kp-actions-grid” style=”display: grid; grid-template-columns: 1fr 1fr; gap: 15px;”>’;
echo ‘<button type=”button” class=”kp-admin-action button button-secondary” data-admin-action=”potted” aria-label=”Ball potted” style=”background: linear-gradient(135deg, #00c853 0%, #00a152 100%); color: white; padding: 25px; font-size: 28px; font-weight: 700; border-radius: 12px; cursor: pointer; min-height: 80px; width: 100%; box-shadow: 0 4px 15px rgba(0,0,0,0.2); touch-action: manipulation; -webkit-tap-highlight-color: transparent;”></button>’;
echo ‘<button type=”button” class=”kp-admin-action button” data-admin-action=”missed” aria-label=”Ball missed” style=”background: linear-gradient(135deg, #ff1744 0%, #d50000 100%); color: white; padding: 25px; font-size: 28px; font-weight: 700; border-radius: 12px; cursor: pointer; min-height: 80px; width: 100%; box-shadow: 0 4px 15px rgba(0,0,0,0.2);”></button>’;
echo ‘<button type=”button” class=”kp-admin-action button” data-admin-action=”foul” aria-label=”Foul (warning)” style=”background: linear-gradient(135deg, #f3f4f6 0%, #9ca3af 100%); color: #111827; padding: 25px; font-size: 28px; font-weight: 700; border-radius: 12px; cursor: pointer; min-height: 80px; width: 100%; box-shadow: 0 4px 15px rgba(0,0,0,0.2);”></button>’;
echo ‘<button type=”button” class=”kp-admin-action button button-primary” data-admin-action=”black-potted” aria-label=”Black ball potted” style=”background: linear-gradient(135deg, #2979ff 0%, #1c54b2 100%); color: white; padding: 25px; font-size: 28px; font-weight: 700; border-radius: 12px; cursor: pointer; min-height: 80px; width: 100%; box-shadow: 0 4px 15px rgba(0,0,0,0.2);”></button>’;
echo ‘</div>’;
echo ‘</div>’;
}
// Current Match History Section (only during active games)
if ($event[‘game_state’] === ‘active’) {
echo ‘<div id=”current-match-history” style=”background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); margin: 20px 0;”>’;
echo ‘<h3 style=”color: #1e40af; margin-bottom: 20px;”>📜 Current Match History</h3>’;
echo ‘<div id=”history-content” style=”max-height: 300px; overflow-y: auto;”>Loading current match history…</div>’;
echo ‘</div>’;
// JavaScript for live updates
if ($event[‘game_state’] === ‘active’) {
echo ‘<div id=”current-match-history” style=”background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); margin: 20px 0;”>’;
echo ‘<h3 style=”color: #1e40af; margin-bottom: 20px;”>📜 Current Match History</h3>’;
echo ‘<div id=”history-content” style=”max-height: 300px; overflow-y: auto;”>Loading current match history…</div>’;
echo ‘</div>’;
// JavaScript for live updates
echo ‘<script>
function refreshCurrentHistory(state) {
var panel = document.querySelector(“#current-match-history”);
if (!panel || panel.offsetParent === null) {
return; // hidden? skip
}
var ver = 0;
if (state && state.history_version !== undefined) {
ver = state.history_version;
} else if (state && state.last_action_id !== undefined) {
ver = state.last_action_id;
}
if (ver === window.kpHistLastVersion) {
return; // nothing new
}
window.kpHistLastVersion = ver;
if (window.kpHistInFlight) {
try {
if (window.kpHistAbort) window.kpHistAbort.abort();
} catch(e) {} // cancel older
}
window.kpHistInFlight = true;
window.kpHistAbort = new AbortController();
var self = this;
fetch(“/wp-json/killer-pool/v1/current-match-history/” + (window.eventSlug || “”), {
signal: window.kpHistAbort.signal,
headers: {
“Accept”: “application/json”,
“Cache-Control”: “no-store”,
“If-None-Match”: window.kpHistETag || “”
}
})
.then(function(response) {
if (response.status === 304) {
window.kpHistInFlight = false;
return; // not modified
}
if (!response.ok) {
throw new Error(“HTTP ” + response.status);
}
window.kpHistETag = response.headers.get(“ETag”) || window.kpHistETag;
return response.json();
})
.then(function(data) {
// Update the history display
var historyContent = document.querySelector(“#history-content”);
if (historyContent && data.history && data.history.length > 0) {
var content = “”;
for (var i = 0; i < data.history.length; i++) {
var action = data.history[i];
var actionText = “”;
switch (action.action_type) {
case “POTTED”: actionText = “Potted ball”; break;
case “MISSED”: actionText = “Missed (-1 life)”; break;
case “BLACK_BALL”: actionText = “Black ball (+1 life)”; break;
case “FOUL”: actionText = “Foul (-1 life)”; break;
case “TIMER_AUTO_LOSS”: actionText = “Time up (-1 life)”; break;
case “PLAYER_ADDED”: actionText = “Joined tournament”; break;
case “PLAYER_REMOVED”: actionText = “Removed from tournament”; break;
default: actionText = action.action_type.replace(/_/g, ” “).toLowerCase();
}
content += “<div style=\”padding: 8px; border-bottom: 1px solid #e5e7eb; font-size: 14px;\”>”;
content += “<strong>Turn ” + action.turn_number + “:</strong> ” + actionText;
if (action.notes) content += ” – ” + action.notes;
content += “</div>”;
}
historyContent.innerHTML = content;
}
window.kpHistInFlight = false;
})
.catch(function(e) {
if (e.name !== “AbortError”) {
console.warn(“History fetch failed:”, e);
}
window.kpHistInFlight = false;
});
}
// History updates now handled on-demand by checkForUpdates()
</script>’;
}
}
// ✅ START GAME SECTION (if not started)
if ($event[‘game_state’] === ‘waiting’) {
echo ‘<div id=”kp-start-cta” style=”background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); color: #1f2937; padding: 25px; border-radius: 14px; margin: 20px 0; box-shadow: 0 6px 20px rgba(251, 191, 36, 0.3);”>’;
echo ‘<h2 style=”margin: 0 0 15px 0;”>⏳ Ready to Start?</h2>’;
echo ‘<p style=”margin: 0 0 20px 0; font-size: 16px;”>Tournament is ready to begin!</p>’;
if (!empty($alive_players)) {
echo ‘<form method=”post” style=”display: inline;” onsubmit=”window.__kpKeepScroll=(window.scrollY || window.pageYOffset || document.documentElement.scrollTop);”>’;
wp_nonce_field(‘start_game’);
echo ‘<button type=”submit” name=”start_game” value=”1″ class=”button button-primary” style=”font-weight:700; font-size:18px; padding:14px 24px; border-radius:10px;”>🎮 START TOURNAMENT</button>’;
echo ‘</form>’;
}
echo ‘</div>’;
// Always-present Quick Actions container (hidden until active)
echo ‘<!– Always-present Quick Actions container (hidden until active) –>’;
echo ‘<div id=”admin-action-box” style=”display:none; background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%); color: white; padding: 25px; border-radius: 15px; box-shadow: 0 6px 20px rgba(124, 58, 237, 0.3); margin: 20px 0; position: sticky; top: 20px; z-index: 100;”>’;
echo ‘<h2 style=”margin: 0 0 20px 0; font-size: 24px; text-align: center;”>QUICK ACTIONS – ALWAYS AT TOP</h2>’;
if (empty($current_player) && !empty($alive_players) && ($event[‘game_state’] === ‘active’)) {
echo ‘<div style=”background:#f59e0b;color:#111827;padding:10px 12px;border-radius:10px;text-align:center;font-weight:700;margin-bottom:12px;”>’;
echo ‘⚠️ No current shooter selected’;
echo ‘</div>’;
echo ‘<form method=”post” style=”display:inline;”>’;
wp_nonce_field(‘select_next_player’);
echo ‘<input type=”hidden” name=”select_next_player” value=”1″ />’;
echo ‘<button type=”submit” class=”button button-primary” style=”background:linear-gradient(135deg,#fbbf24 0%, #f59e0b 100%); color:#111827; border-color:#111827; font-weight:800; border-radius:10px; padding:10px 14px;”>’;
echo ‘🔄 Force Reselect’;
echo ‘</button>’;
echo ‘</form>’;
}
// Current shooter info – compact
echo ‘<div class=”current-player-info” style=”text-align: center; margin-bottom: 20px; background: rgba(255,255,255,0.1); padding: 15px; border-radius: 12px;”>’;
echo ‘<div style=”font-size: 20px; font-weight: 700; margin-bottom: 5px;”>–</div>’;
echo ‘<div style=”opacity: 0.9; font-size: 14px;”>Ball #– • — lives</div>’;
echo ‘</div>’;
// LARGE ACTION BUTTONS – Mobile optimized
echo ‘<div class=”kp-actions-grid” style=”display: grid; grid-template-columns: 1fr 1fr; gap: 15px;”>’;
echo ‘<button type=”button” class=”kp-admin-action button button-secondary” data-admin-action=”potted” aria-label=”Ball potted” style=”background: linear-gradient(135deg, #00c853 0%, #00a152 100%); color: white; padding: 25px; font-size: 28px; font-weight: 700; border-radius: 12px; cursor: pointer; min-height: 80px; width: 100%; box-shadow: 0 4px 15px rgba(0,0,0,0.2);”></button>’;
echo ‘<button type=”button” class=”kp-admin-action button” data-admin-action=”missed” aria-label=”Ball missed” style=”background: linear-gradient(135deg, #ff1744 0%, #d50000 100%); color: white; padding: 25px; font-size: 28px; font-weight: 700; border-radius: 12px; cursor: pointer; min-height: 80px; width: 100%; box-shadow: 0 4px 15px rgba(0,0,0,0.2);”></button>’;
echo ‘<button type=”button” class=”kp-admin-action button” data-admin-action=”foul” aria-label=”Foul (warning)” style=”background: linear-gradient(135deg, #f3f4f6 0%, #9ca3af 100%); color: #111827; padding: 25px; font-size: 28px; font-weight: 700; border-radius: 12px; cursor: pointer; min-height: 80px; width: 100%; box-shadow: 0 4px 15px rgba(0,0,0,0.2);”></button>’;
echo ‘<button type=”button” class=”kp-admin-action button button-primary” data-admin-action=”black-potted” aria-label=”Black ball potted” style=”background: linear-gradient(135deg, #2979ff 0%, #1c54b2 100%); color: white; padding: 25px; font-size: 28px; font-weight: 700; border-radius: 12px; cursor: pointer; min-height: 80px; width: 100%; box-shadow: 0 4px 15px rgba(0,0,0,0.2);”></button>’;
echo ‘</div>’;
echo ‘</div>’;
}
}
// Add Players and Current Players sections BELOW action buttons
echo ‘<div class=”kp-players-grid”>’;
// Current Players Section
echo ‘<div id=”current-players-section” style=”background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.08);”>’;
echo ‘<h2 style=”margin-top: 0; color: #059669;”>🎯 Current Players (‘ . count($players) . ‘)</h2>’;
// KP_PATCH:kp_admin_current_players
kp_include_patch(‘kp_admin_current_players’);
if (!empty($players)) {
echo ‘<div style=”max-height: 500px; overflow-y: auto;”>’;
foreach ($players as $player) {
if (!$player[‘is_active’]) continue; // Skip inactive players in admin display
$lives = intval($player[‘lives’]);
$status = ‘Active’;
$status_color = ‘#059669’;
$bg_color = ‘#f0fdf4’;
echo ‘<div style=”padding: 15px; margin: 8px 0; background: ‘ . $bg_color . ‘; border-radius: 8px; border-left: 4px solid ‘ . $status_color . ‘;” data-player-id=”‘ . $player[‘id’] . ‘”>’;
// Main player info row
echo ‘<div style=”display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;”>’;
echo ‘<div>’;
// Step 8: Fix PHP notices – use json_encode() instead of esc_js() for safe string handling
$pid = (int)($player[‘id’] ?? 0);
$pname_attr = esc_attr($player[‘display_name’] ?? ”);
echo ‘<div class=”player-history-link” data-player-id=”‘ . $pid . ‘” data-player-name=”‘ . $pname_attr . ‘” style=”font-size: 18px; font-weight: 600; margin-bottom: 5px; cursor: pointer; color: #1e40af; text-decoration: underline;”>’ . esc_html($player[‘display_name’]) . ‘</div>’;
echo ‘<div style=”color: #6b7280; font-size: 14px;”>#’ . $player[‘id’] . ‘ • ‘ . $status . ‘</div>’;
echo ‘</div>’;
echo ‘<div style=”display: flex; align-items: center; gap: 8px;”>’;
// Lives display with separate colors for regular vs black ball lives
$regular_lives = intval($player[‘regular_lives’] ?? 0);
$black_ball_lives = intval($player[‘black_ball_lives’] ?? 0);
for ($i = 0; $i < 5; $i++) {
if ($i < $regular_lives) {
// Regular life – green
echo ‘<span style=”width: 14px; height: 14px; background: #10b981; border-radius: 50%; display: inline-block; box-shadow: 0 0 4px rgba(16, 185, 129, 0.5);”></span>’;
} elseif ($i < $regular_lives + $black_ball_lives) {
// Black ball life – dark/black
echo ‘<span style=”width: 14px; height: 14px; background: #1f2937; border: 2px solid #f59e0b; border-radius: 50%; display: inline-block; box-shadow: 0 0 4px rgba(245, 158, 11, 0.5);”></span>’;
} else {
// No life – empty
echo ‘<span style=”width: 14px; height: 14px; background: #e5e7eb; border-radius: 50%; display: inline-block;”></span>’;
}
}
echo ‘<span style=”margin-left: 10px; font-weight: 600; color: ‘ . $status_color . ‘;”>’ . $lives . ‘</span>’;
echo ‘</div>’;
echo ‘</div>’;
// Manual adjustment controls + remove player
echo ‘<div style=”display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-top: 10px;”>’;
// Life adjustment buttons
echo ‘<button type=”button” class=”life-adjust-btn” data-player-id=”‘ . $player[‘id’] . ‘” data-adjustment=”-1″ style=”background: #ef4444; color: white; border: none; padding: 8px 12px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer;” ‘ . ($lives <= 0 ? ‘disabled’ : ”) . ‘>-1 LIFE</button>’;
echo ‘<button type=”button” class=”life-adjust-btn” data-player-id=”‘ . $player[‘id’] . ‘” data-adjustment=”1″ style=”background: #10b981; color: white; border: none; padding: 8px 12px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer;” ‘ . ($lives >= intval($event[‘max_lives’] ?? 5) ? ‘disabled’ : ”) . ‘>+1 LIFE</button>’;
// Remove player button
echo ‘<button type=”button” class=”remove-btn” data-player-id=”‘ . $player[‘id’] . ‘” data-player-name=”‘ . esc_attr($player[‘display_name’]) . ‘” style=”background: #dc2626; color: white; border: none; padding: 8px 12px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer;”>REMOVE</button>’;
echo ‘</div>’;
echo ‘</div>’;
}
echo ‘</div>’;
} else {
echo ‘<div style=”text-align: center; padding: 40px; color: #6b7280; border: 2px dashed #d1d5db; border-radius: 8px;”>’;
echo ‘<p style=”font-size: 18px; margin: 0;”>No players yet!</p>’;
echo ‘</div>’;
}
echo ‘</div>’;
// Add Players Section
echo ‘<div style=”background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.08);”>’;
echo ‘<h2 style=”margin-top: 0; color: #1e40af;”>👥 Add Players</h2>’;
echo ‘<form method=”post” onsubmit=”sessionStorage.setItem(\’kpScrollPos\’, window.pageYOffset || document.documentElement.scrollTop);”>’;
wp_nonce_field(‘add_players’);
echo ‘<textarea name=”player_names” placeholder=”Enter player names, one per line: John Smith Mary Johnson Mike Wilson” rows=”6″ style=”width: 100%; padding: 15px; border: 2px solid #d1d5db; border-radius: 8px; font-size: 16px; margin-bottom: 15px; font-family: inherit;”></textarea>’;
echo ‘<input type=”submit” name=”add_players” value=”➕ ADD PLAYERS” style=”background: linear-gradient(135deg, #10b981 0%, #059669 100%); color: white; border: none; padding: 15px 25px; font-size: 16px; font-weight: 600; border-radius: 8px; cursor: pointer; width: 100%; min-height: 50px;”>’;
echo ‘</form>’;
echo ‘</div>’;
echo ‘</div>’; // End main grid
// REORGANIZED ADMIN INTERFACE – ACTION BUTTONS FIRST!
if (!empty($players)) {
$alive_players = array_filter($players, function($p) { return $p[‘is_active’] && $p[‘lives’] > 0; });
$pool_status = kp_complete_get_pool_status($event[‘id’]);
$current_player = kp_complete_get_current_player($event[‘id’]);
// GAME INFO SECTION (Below action buttons)
echo ‘<div style=”background: white; padding: 25px; border-radius: 15px; box-shadow: 0 4px 15px rgba(0,0,0,0.08); margin: 25px 0;”>’;
echo ‘<h3 style=”margin-top: 0; color: #1e40af;”>📊 Game Information</h3>’;
// KP_PATCH:kp_admin_info
kp_include_patch(‘kp_admin_info’);
if ($event[‘game_state’] === ‘active’ && !empty($alive_players)) {
// Pool Status Display – Compact
echo ‘<div style=”display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;”>’;
echo ‘<div style=”background: #f0f9ff; border: 2px solid #3b82f6; padding: 12px; border-radius: 8px; text-align: center;”>’;
echo ‘<div style=”font-size: 12px; color: #1e40af; margin-bottom: 5px;”>🪣 POOL 1</div>’;
echo ‘<div style=”font-size: 20px; font-weight: 700; color: #3b82f6;”>’ . $pool_status[‘pool1_count’] . ‘</div>’;
echo ‘</div>’;
echo ‘<div style=”background: #f0fdf4; border: 2px solid #10b981; padding: 12px; border-radius: 8px; text-align: center;”>’;
echo ‘<div style=”font-size: 12px; color: #059669; margin-bottom: 5px;”>🪣 POOL 2</div>’;
echo ‘<div style=”font-size: 20px; font-weight: 700; color: #10b981;”>’ . $pool_status[‘pool2_count’] . ‘</div>’;
echo ‘</div>’;
echo ‘</div>’;
}
// Black Ball Settings Section
echo ‘<div style=”margin-top: 30px; padding-top: 20px; border-top: 2px solid #f1f5f9;”>’;
echo ‘<h3 style=”color: #1e40af; margin-bottom: 20px;”>🖤 Black Ball Settings</h3>’;
echo ‘<form method=”post” style=”background: #f0f9ff; padding: 20px; border-radius: 12px; border: 2px solid #bfdbfe; margin-bottom: 30px;” onsubmit=”sessionStorage.setItem(\’kpScrollPos\’, window.pageYOffset || document.documentElement.scrollTop);”>’;
wp_nonce_field(‘save_black_ball_settings’);
echo ‘<div style=”display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 20px;”>’;
// Black Ball Enable/Disable
echo ‘<div>’;
echo ‘<label style=”display: flex; align-items: center; font-weight: 600; margin-bottom: 10px;”>’;
echo ‘<input type=”checkbox” name=”black_ball_enabled” value=”1″ ‘ . checked($event[‘black_ball_enabled’], 1, false) . ‘ style=”margin-right: 8px; transform: scale(1.2);”> Enable Black Ball Bonus’;
echo ‘</label>’;
echo ‘<p style=”font-size: 12px; color: #6b7280; margin: 0;”>Allow players to gain bonus lives from black ball</p>’;
echo ‘</div>’;
// Maximum Lives Cap (Hard Limit)
echo ‘<div>’;
echo ‘<label style=”font-weight: 600; margin-bottom: 8px; display: block;”>Maximum Lives Cap (Hard Limit)</label>’;
echo ‘<select name=”black_ball_max_lives” style=”width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 6px;”>’;
foreach ([3, 4, 5, 6, 7] as $max_lives) {
// Reflect actual hard cap stored in events.max_lives
$selected = selected($event[‘max_lives’], $max_lives, false);
echo ‘<option value=”‘ . $max_lives . ‘” ‘ . $selected . ‘>’ . $max_lives . ‘ total lives maximum</option>’;
}
echo ‘</select>’;
echo ‘<p style=”font-size: 12px; color: #6b7280; margin: 5px 0 0 0;”>Hard cap – no player can exceed this many lives regardless of black balls potted</p>’;
echo ‘</div>’;
// Black Ball Lives Per Player
echo ‘<div>’;
echo ‘<label style=”font-weight: 600; margin-bottom: 8px; display: block;”>Black Ball Lives Per Player</label>’;
echo ‘<select name=”black_ball_lives_per_player” style=”width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 6px;”>’;
$bb_per_player_options = [
1 => ‘1 bonus life max per player’,
2 => ‘2 bonus lives max per player’,
3 => ‘3 bonus lives max per player’,
999 => ‘Unlimited bonus lives per player’
];
foreach ($bb_per_player_options as $value => $label) {
$selected = selected(get_option(‘kp_bb_cap_event_’ . $event[‘id’], 1), $value, false);
echo ‘<option value=”‘ . $value . ‘” ‘ . $selected . ‘>’ . $label . ‘</option>’;
}
echo ‘</select>’;
echo ‘<p style=”font-size: 12px; color: #6b7280; margin: 5px 0 0 0;”>How many black ball bonus lives each individual player can earn per match</p>’;
echo ‘</div>’;
echo ‘</div>’;
echo ‘<button type=”submit” name=”save_black_ball_settings” value=”1″ style=”background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%); color: white; border: none; padding: 12px 24px; font-size: 14px; font-weight: 600; border-radius: 8px; cursor: pointer;”>🖤 Save Black Ball Settings</button>’;
echo ‘</form>’;
echo ‘</div>’;
// === 🎯 Gameplay / Rotation ===
echo ‘<div style=”margin-top: 30px; padding-top: 20px; border-top: 2px solid #f1f5f9;”>’;
echo ‘<h3 style=”color: #065f46; margin-bottom: 20px;”>🎯 Gameplay / Rotation</h3>’;
echo ‘<form method=”post” style=”background: #ecfeff; padding: 20px; border-radius: 12px; border: 2px solid #a5f3fc; margin-bottom: 30px;” onsubmit=”sessionStorage.setItem(\’kpScrollPos\’, window.pageYOffset || document.documentElement.scrollTop);”>’;
wp_nonce_field(‘save_gameplay_settings’);
$alt_final2 = intval(get_option(‘kp_alt_final2_event_’ . $event[‘id’], 1)); // default ON
$checked = $alt_final2 ? ‘checked’ : ”;
echo ‘<div style=”display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 20px;”>’;
echo ‘<div>’;
echo ‘<label style=”display: flex; align-items: center; font-weight: 600; margin-bottom: 10px;”>’;
echo ‘<input type=”checkbox” name=”alternate_final_two” value=”1″ ‘ . $checked . ‘ style=”margin-right: 8px; transform: scale(1.2);”> Alternate final 2 players?’;
echo ‘</label>’;
echo ‘<p style=”font-size: 12px; color: #6b7280; margin: 0;”>When ON (default): after the 3rd place is eliminated, the first draw follows the standard pool logic, then turns alternate strictly between the final two.</p>’;
echo ‘</div>’;
echo ‘</div>’;
echo ‘<button type=”submit” name=”save_gameplay_settings” value=”1″ style=”background: linear-gradient(135deg, #10b981 0%, #34d399 100%); color: white; padding: 10px 16px; border: none; border-radius: 8px; cursor: pointer;”>💾 Save Gameplay Settings</button>’;
echo ‘</form>’;
echo ‘</div>’;
// Timer Settings Section
echo ‘<div style=”margin-top: 30px; padding-top: 20px; border-top: 2px solid #f1f5f9;”>’;
echo ‘<h3 style=”color: #7c3aed; margin-bottom: 20px;”>⏰ Timer Settings</h3>’;
echo ‘<form method=”post” style=”background: #f8f9ff; padding: 20px; border-radius: 12px; border: 2px solid #e0e7ff;” onsubmit=”sessionStorage.setItem(\’kpScrollPos\’, window.pageYOffset || document.documentElement.scrollTop);”>’;
wp_nonce_field(‘save_timer_settings’);
echo ‘<div style=”display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 20px;”>’;
// Timer Enable/Disable
echo ‘<div>’;
echo ‘<label style=”display: flex; align-items: center; font-weight: 600; margin-bottom: 10px;”>’;
echo ‘<input type=”checkbox” name=”timer_enabled” value=”1″ ‘ . checked($event[‘timer_enabled’], 1, false) . ‘ style=”margin-right: 8px; transform: scale(1.2);”> Enable Timer’;
echo ‘</label>’;
echo ‘<p style=”font-size: 12px; color: #6b7280; margin: 0;”>Turn shot timer on/off</p>’;
echo ‘</div>’;
// Timer Duration
echo ‘<div>’;
echo ‘<label style=”font-weight: 600; margin-bottom: 8px; display: block;”>Duration (seconds)</label>’;
echo ‘<select name=”timer_duration” style=”width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 6px;”>’;
foreach ([5, 10, 20, 30, 60, 90, 120] as $duration) {
$selected = selected($event[‘timer_duration’], $duration, false);
echo ‘<option value=”‘ . $duration . ‘” ‘ . $selected . ‘>’ . $duration . ‘ seconds</option>’;
}
echo ‘</select>’;
echo ‘</div>’;
// Auto Life Loss
echo ‘<div>’;
echo ‘<label style=”display: flex; align-items: center; font-weight: 600; margin-bottom: 10px;”>’;
echo ‘<input type=”checkbox” name=”timer_auto_loss” value=”1″ ‘ . checked($event[‘timer_auto_loss’], 1, false) . ‘ style=”margin-right: 8px; transform: scale(1.2);”> Auto Lose Life’;
echo ‘</label>’;
echo ‘<p style=”font-size: 12px; color: #6b7280; margin: 0;”>Auto lose life when timer hits 0</p>’;
echo ‘</div>’;
// Buffer Warning Enable
echo ‘<div>’;
echo ‘<label style=”display: flex; align-items: center; font-weight: 600; margin-bottom: 10px;”>’;
echo ‘<input type=”checkbox” name=”timer_buffer_enabled” value=”1″ ‘ . checked($event[‘timer_buffer_enabled’], 1, false) . ‘ style=”margin-right: 8px; transform: scale(1.2);”> Buffer Warning’;
echo ‘</label>’;
echo ‘<p style=”font-size: 12px; color: #6b7280; margin: 0;”>Warning in final seconds</p>’;
echo ‘</div>’;
// Buffer Duration
echo ‘<div>’;
echo ‘<label style=”font-weight: 600; margin-bottom: 8px; display: block;”>Buffer Time (seconds)</label>’;
echo ‘<select name=”timer_buffer_seconds” style=”width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 6px;”>’;
foreach ([5, 10, 15, 20] as $buffer) {
$selected = selected($event[‘timer_buffer_seconds’], $buffer, false);
echo ‘<option value=”‘ . $buffer . ‘” ‘ . $selected . ‘>’ . $buffer . ‘ seconds</option>’;
}
echo ‘</select>’;
echo ‘</div>’;
echo ‘</div>’;
echo ‘<button type=”submit” name=”save_timer_settings” value=”1″ style=”background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%); color: white; border: none; padding: 12px 24px; font-size: 14px; font-weight: 600; border-radius: 8px; cursor: pointer;”>💾 Save Timer Settings</button>’;
echo ‘</form>’;
echo ‘</div>’;
// === 🎬 Elimination Videos ===
echo ‘<div style=”margin-top: 30px; padding-top: 20px; border-top: 2px solid #f1f5f9;”>’;
echo ‘<h3 style=”color: #7c3aed; margin-bottom: 20px;”>🎬 Elimination Videos</h3>’;
echo ‘<form method=”post” style=”background: #f8f9ff; padding: 20px; border-radius: 12px; border: 2px solid #e0e7ff;” onsubmit=”sessionStorage.setItem(\’kpScrollPos\’, window.pageYOffset || document.documentElement.scrollTop);”>’;
wp_nonce_field(‘save_elimination_videos’);
$elim_enabled = get_option(‘kp_elim_enabled_event_’ . $event[‘id’], 0);
$elim_videos = get_option(‘kp_elim_videos_event_’ . $event[‘id’], []);
echo ‘<div style=”display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 20px;”>’;
// Enable/Disable
echo ‘<div>’;
echo ‘<label style=”display: flex; align-items: center; font-weight: 600; margin-bottom: 10px;”>’;
echo ‘<input type=”checkbox” name=”elimination_enabled” value=”1″ ‘ . checked($elim_enabled, 1, false) . ‘ style=”margin-right: 8px; transform: scale(1.2);”> Enable Elimination Videos’;
echo ‘</label>’;
echo ‘<p style=”font-size: 12px; color: #6b7280; margin: 0;”>Play full-screen video clips when players are eliminated</p>’;
echo ‘</div>’;
echo ‘</div>’;
// Video Upload Section
echo ‘<div style=”margin-top: 20px;”>’;
echo ‘<h4 style=”margin-bottom: 15px; color: #1e40af;”>Video Clips</h4>’;
echo ‘<div id=”elimination-videos-container” style=”display: grid; gap: 15px;”>’;
if (!empty($elim_videos)) {
foreach ($elim_videos as $index => $video) {
$attachment = get_post($video[‘id’]);
if ($attachment) {
$url = wp_get_attachment_url($video[‘id’]);
$filename = basename(get_attached_file($video[‘id’]));
echo ‘<div class=”elimination-video-item” style=”display: flex; align-items: center; gap: 15px; padding: 15px; background: white; border-radius: 8px; border: 1px solid #d1d5db;”>’;
echo ‘<input type=”hidden” name=”elimination_videos[‘ . $video[‘id’] . ‘]” value=”‘ . ($video[‘enabled’] ? ‘1’ : ‘0’) . ‘”>’;
echo ‘<label style=”display: flex; align-items: center; gap: 8px;”>’;
echo ‘<input type=”checkbox” class=”video-enabled” data-video-id=”‘ . $video[‘id’] . ‘” ‘ . checked($video[‘enabled’], 1, false) . ‘>’;
echo ‘<span>Enabled</span>’;
echo ‘</label>’;
echo ‘<div style=”flex: 1;”>’;
echo ‘<div style=”font-weight: 600; color: #1e40af;”>’ . esc_html($attachment->post_title) . ‘</div>’;
echo ‘<div style=”font-size: 12px; color: #6b7280;”>’ . esc_html($filename) . ‘</div>’;
echo ‘<video style=”max-width: 200px; max-height: 100px; margin-top: 5px;” controls>’;
echo ‘<source src=”‘ . esc_url($url) . ‘” type=”‘ . esc_attr($attachment->post_mime_type) . ‘”>’;
echo ‘</video>’;
echo ‘</div>’;
echo ‘<button type=”button” class=”remove-video button” data-video-id=”‘ . $video[‘id’] . ‘” style=”background: #dc2626; color: white; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer;”>Remove</button>’;
echo ‘</div>’;
}
}
}
echo ‘</div>’;
echo ‘<div style=”margin-top: 15px;”>’;
echo ‘<button type=”button” id=”add-elimination-video” class=”button button-secondary” style=”padding: 10px 20px;”>➕ Add Video Clip</button>’;
echo ‘</div>’;
echo ‘</div>’;
echo ‘<button type=”submit” name=”save_elimination_videos” value=”1″ style=”background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%); color: white; border: none; padding: 12px 24px; font-size: 14px; font-weight: 600; border-radius: 8px; cursor: pointer; margin-top: 20px;”>💾 Save Elimination Video Settings</button>’;
echo ‘</form>’;
echo ‘</div>’;
// Match History Section
echo ‘<div style=”margin-top: 30px; padding-top: 20px; border-top: 2px solid #f1f5f9;”>’;
echo ‘<h3 style=”color: #1e40af; margin-bottom: 20px;”>📜 Match History</h3>’;
// PHP code to scan history folder
$history_dir = __DIR__ . ‘/history’;
$history_files = [];
if (is_dir($history_dir)) {
$files = glob($history_dir . ‘/match_*.json’);
foreach ($files as $file) {
$basename = basename($file);
if (preg_match(‘/match_(\d+)_(\d+)\.json/’, $basename, $matches)) {
$matchId = $matches[1];
$timestamp = $matches[2];
$history_files[] = [
‘file’ => $file,
‘matchId’ => $matchId,
‘timestamp’ => $timestamp,
‘display’ => “Match {$matchId} – ” . date(‘Y-m-d H:i:s’, $timestamp)
];
}
}
// Sort by timestamp descending
usort($history_files, function($a, $b) {
return $b[‘timestamp’] – $a[‘timestamp’];
});
}
if (!empty($history_files)) {
echo ‘<form method=”post” style=”background: #f0f9ff; padding: 20px; border-radius: 12px; border: 2px solid #bfdbfe;” onsubmit=”sessionStorage.setItem(\’kpScrollPos\’, window.pageYOffset || document.documentElement.scrollTop);”>’;
wp_nonce_field(‘view_match_history’);
echo ‘<div style=”margin-bottom: 20px;”>’;
echo ‘<label style=”font-weight: 600; margin-bottom: 8px; display: block;”>Select Match to View</label>’;
echo ‘<select name=”selected_match” style=”width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 6px;”>’;
echo ‘<option value=””>– Select a match –</option>’;
foreach ($history_files as $file_info) {
$selected = (isset($_POST[‘selected_match’]) && $_POST[‘selected_match’] === $file_info[‘file’]) ? ‘selected’ : ”;
echo ‘<option value=”‘ . esc_attr($file_info[‘file’]) . ‘” ‘ . $selected . ‘>’ . esc_html($file_info[‘display’]) . ‘</option>’;
}
echo ‘</select>’;
echo ‘</div>’;
echo ‘<button type=”submit” name=”view_match_history” value=”1″ style=”background: linear-gradient(135deg, #1e40af 0%, #3b82f6 100%); color: white; border: none; padding: 12px 24px; font-size: 14px; font-weight: 600; border-radius: 8px; cursor: pointer;”>📜 View Match Details</button>’;
echo ‘</form>’;
// Handle form submission
if ($_POST[‘view_match_history’] ?? false) {
check_admin_referer(‘view_match_history’);
$selected_file = sanitize_text_field($_POST[‘selected_match’]);
if (!empty($selected_file) && file_exists($selected_file) && strpos($selected_file, $history_dir) === 0) {
$match_data = json_decode(file_get_contents($selected_file), true);
if ($match_data) {
echo ‘<div style=”margin-top: 20px; padding: 20px; background: white; border-radius: 12px; border: 2px solid #e5e7eb;”>’;
echo ‘<h4 style=”margin-top: 0; color: #1e40af;”>Match Details</h4>’;
echo ‘<pre style=”background: #f9fafb; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 12px; max-height: 400px; overflow-y: auto;”>’ . esc_html(json_encode($match_data, JSON_PRETTY_PRINT)) . ‘</pre>’;
echo ‘</div>’;
} else {
echo ‘<div class=”notice notice-error”><p>❌ Invalid match data file.</p></div>’;
}
} else {
echo ‘<div class=”notice notice-error”><p>❌ Invalid or missing match file.</p></div>’;
}
}
} else {
echo ‘<div style=”background: #f9fafb; padding: 20px; border-radius: 12px; border: 2px solid #e5e7eb; text-align: center;”>’;
echo ‘<p style=”margin: 0; color: #6b7280;”>No match history files found. History will appear here once matches are recorded.</p>’;
echo ‘</div>’;
}
echo ‘</div>’;
// JavaScript for media picker
echo ‘<script>
jQuery(document).ready(function($) {
var mediaUploader;
$(“#add-elimination-video”).on(“click”, function(e) {
e.preventDefault();
if (mediaUploader) {
mediaUploader.open();
return;
}
mediaUploader = wp.media({
title: “Select Video”,
button: { text: “Add Video” },
library: { type: “video” },
multiple: false
});
mediaUploader.on(“select”, function() {
var attachment = mediaUploader.state().get(“selection”).first().toJSON();
if (attachment.type === “video”) {
addVideoToList(attachment);
} else {
alert(“Please select a video file.”);
}
});
mediaUploader.open();
});
function addVideoToList(attachment) {
var container = $(“#elimination-videos-container”);
var itemHtml = \'<div class=”elimination-video-item” style=”display: flex; align-items: center; gap: 15px; padding: 15px; background: white; border-radius: 8px; border: 1px solid #d1d5db;”>\’ +
\'<input type=”hidden” name=”elimination_videos[\’ + attachment.id + \’]” value=”1″>\’ +
\'<label style=”display: flex; align-items: center; gap: 8px;”>\’ +
\'<input type=”checkbox” class=”video-enabled” data-video-id=”\’ + attachment.id + \'” checked>\’ +
\'<span>Enabled</span>\’ +
\'</label>\’ +
\'<div style=”flex: 1;”>\’ +
\'<div style=”font-weight: 600; color: #1e40af;”>\’ + attachment.title + \'</div>\’ +
\'<div style=”font-size: 12px; color: #6b7280;”>\’ + attachment.filename + \'</div>\’ +
\'<video style=”max-width: 200px; max-height: 100px; margin-top: 5px;” controls>\’ +
\'<source src=”\’ + attachment.url + \'” type=”\’ + attachment.mime + \'”>\’ +
\'</video>\’ +
\'</div>\’ +
\'<button type=”button” class=”remove-video button” data-video-id=”\’ + attachment.id + \'” style=”background: #dc2626; color: white; border: none; padding: 8px 12px; border-radius: 6px; cursor: pointer;”>Remove</button>\’ +
\'</div>\’;
container.append(itemHtml);
}
$(document).on(“change”, “.video-enabled”, function() {
var videoId = $(this).data(“video-id”);
var hiddenInput = $(\’input[name=”elimination_videos[\’ + videoId + \’]”]\’);
hiddenInput.val($(this).is(“:checked”) ? “1” : “0”);
});
$(document).on(“click”, “.remove-video”, function() {
$(this).closest(“.elimination-video-item”).remove();
});
});
</script>’;
// Reset button
echo ‘<div style=”margin-top: 30px; padding-top: 20px; border-top: 2px solid #f1f5f9; text-align: center;”>’;
echo ‘<form method=”post” style=”display: inline;” onsubmit=”sessionStorage.setItem(\’kpScrollPos\’, window.pageYOffset || document.documentElement.scrollTop);”>’;
wp_nonce_field(‘reset_game’);
echo ‘<button type=”submit” name=”reset_game” value=”1″ onclick=”return confirm(\’Reset entire tournament?\’)” style=”background: #dc2626; color: white; border: none; padding: 15px 25px; font-size: 16px; font-weight: 600; border-radius: 8px; cursor: pointer;”>🔄 RESET GAME</button>’;
echo ‘</form>’;
echo ‘</div>’;
}
// ADD JAVASCRIPT FUNCTIONS FOR MANUAL CONTROLS
$rest_base_json = wp_json_encode( rest_url(‘killer-pool/v1/’) );
$wp_rest_nonce = wp_create_nonce(‘wp_rest’);
$wp_rest_nonce_json = wp_json_encode( $wp_rest_nonce );
$event_slug_json = wp_json_encode( (string) $event_slug );
// Inject PHP variables via separate HEREDOC block BEFORE the main NOWDOC
echo <<<JS
<script>
// — WP REST config (PHP-injected, safe) —
window.restBase = window.restBase || {$rest_base_json};
window.eventSlug = {$event_slug_json};
window.wpRestNonce = window.wpRestNonce || {$wp_rest_nonce_json};
</script>
JS;
echo <<<‘EOT’
<script>
function makeAbort(){ try { return new AbortController(); } catch(_) { return null; } }
async function kpFetch(path, opts = {}) {
const base = (typeof restBase === ‘string’ ? restBase : window.restBase || ”);
const url = base.replace(/\/+$/,’/’) + String(path).replace(/^\/+/, ”);
const bust = (url.includes(‘?’) ? ‘&’ : ‘?’) + ‘_ts=’ + Date.now();
const ac = (!(‘signal’ in opts) ? makeAbort() : null);
return fetch(url + bust, {
credentials: ‘same-origin’,
cache: ‘no-store’,
headers: { ‘Accept’:’application/json’, ‘Cache-Control’:’no-store’, …(opts.headers||{}) },
…(ac ? { signal: ac.signal } : {}),
…opts
});
}
// ——————– KP Unified Poll Controller (NO THROTTLING, FIXED INTERVAL) ——————–
(() => {
if (window.__KP_POLLING_INSTALLED__) return;
window.__KP_POLLING_INSTALLED__ = true;
const KPC = window.KPC || {};
const IS_TV = !!(document.body.classList.contains(‘kp-tv’) || /\/event-/.test(location.pathname));
// FIXED STABLE POLL RATE (NO adaptive faster/slower)
const INTERVAL = IS_TV
? parseInt(KPC.tv_min_ms || 1500, 10)
: parseInt(KPC.admin_min_ms || 2500, 10);
let inFlight = false;
let timer = null;
async function fetchState(){
const slug = window.eventSlug || ”;
const res = await kpFetch(‘state/’ + slug, { method:’GET’ });
if (!res.ok) throw new Error(‘state ‘ + res.status);
const state = await res.json();
if (window.location.search.includes(‘kpdebug=1’)) {
console.log(‘[CHIP DEBUG] fetchState: state fetched’);
console.log(‘[STATE DEBUG] keys:’, Object.keys(state || {}));
}
return state;
}
async function tick(){
if (inFlight) return;
inFlight = true;
try {
const state = await fetchState();
window.__KP_STATE = state;
try { if (typeof onStateUpdate === ‘function’) onStateUpdate(state); } catch(_){}
try { if (typeof updateFromState === ‘function’) updateFromState(state); } catch(_){}
try { window.__kpTimer?.applyFromState?.(state); } catch(_){}
} catch(e){
console.warn(“KP poll error:”, e);
} finally {
inFlight = false;
}
}
// Start fixed-rate polling
timer = setInterval(tick, INTERVAL);
tick();
window.__KPPOLL = {
getDelay: () => INTERVAL,
faster: () => {},
slower: () => {},
setDelay: () => {}
};
})();
// === KP Zero-Cost Shot Timer (UI-only; no interval when disabled) ===
(() => {
const T = { id: null, deadline: 0, skew: 0, lastSecs: null };
function nowMs() { return Date.now() + (T.skew || 0); }
function render(msLeft, durationMs, state) {
const secs = Math.max(0, Math.ceil(msLeft / 1000));
if (secs === T.lastSecs) return;
T.lastSecs = secs;
// Prefer your existing UI functions if present
try {
if (typeof updateTimerDisplay === ‘function’) {
// Your code expects (remainingMs, durationMs, event)
updateTimerDisplay(msLeft, durationMs, (state?.event || state));
return;
}
} catch (_) {}
// Fallback text target
const el = document.querySelector(‘[data-timer-remaining]’);
if (el) el.textContent = String(secs);
}
function stop() {
if (T.id) { clearInterval(T.id); T.id = null; }
T.deadline = 0;
T.lastSecs = null;
try { if (typeof hideTimer === ‘function’) hideTimer(); } catch (_) {}
}
function tick(durationMs, state) {
const msLeft = Math.max(0, T.deadline – nowMs());
render(msLeft, durationMs, state);
if (msLeft <= 0) {
stop();
try { if (typeof handleTimerExpired === ‘function’) handleTimerExpired(state?.event || state); } catch (_) {}
}
}
function start(deadlineMs, durationMs, state) {
if (T.id) { clearInterval(T.id); T.id = null; }
T.deadline = deadlineMs;
T.lastSecs = null;
// Show immediately using your UI helper if available
try {
if (typeof showTimer === ‘function’) {
const initialLeft = Math.max(0, deadlineMs – nowMs());
showTimer(initialLeft, durationMs, state?.event || state);
}
} catch (_) {}
tick(durationMs, state); // initial render
T.id = setInterval(() => tick(durationMs, state), 250); // smooth UI tick
}
function applyFromState(state) {
try {
const e = state?.event || state;
const enabled = !!(e?.timer_enabled && e?.timer_start_time > 0 && e?.game_state === ‘active’);
if (!enabled) { stop(); return; }
// Sync to server time if provided
if (typeof state?.server_time === ‘number’) {
T.skew = (state.server_time * 1000) – Date.now();
}
const startMs = Number(e.timer_start_time) * 1000;
const durationMs = Number(e.timer_duration) * 1000;
const deadline = startMs + durationMs;
if (deadline !== T.deadline) {
start(deadline, durationMs, state);
} else if (!T.id) {
// Timer was paused (e.g., tab hidden) — resume
T.id = setInterval(() => tick(durationMs, state), 250);
}
} catch (_) { /* keep calm and carry on */ }
}
// Pause when hidden; resume on return using fresh state
document.addEventListener(“visibilitychange”, () => {
if (!document.hidden) {
// Tab restored → recalc immediately using last known state
if (window.__KP_STATE) {
applyFromState(window.__KP_STATE);
}
}
});
// Expose to the unified poller (which already calls applyFromState)
window.__kpTimer = { applyFromState, stop };
})();
// Legacy shim: expose `restBase` var for old code paths if needed
if (typeof restBase === ‘undefined’ && typeof window !== ‘undefined’ && typeof window.restBase !== ‘undefined’) {
var restBase = window.restBase;
}
kp_debug(“Admin script initialized with restBase:”, restBase);
kp_debug(“Event slug:”, window.eventSlug);
let adminTimerInterval = null;
// Lightweight toast + global debounce for quick actions
let kpToastTimer = null;
function kpShowToast(message, type = ‘success’) {
try {
if (!message) return;
if (kpToastTimer) { clearTimeout(kpToastTimer); kpToastTimer = null; }
let el = document.getElementById(‘kp-toast’);
if (!el) {
el = document.createElement(‘div’);
el.id = ‘kp-toast’;
el.className = ‘kp-toast’;
el.setAttribute(‘role’, ‘status’);
el.setAttribute(‘aria-live’, ‘polite’);
document.body.appendChild(el);
}
el.textContent = message;
el.style.background = (type === ‘error’) ? ‘#991b1b’ : (type === ‘warn’) ? ‘#92400e’ : ‘#111827’;
void el.offsetHeight; // force reflow
el.classList.add(‘kp-show’);
kpToastTimer = setTimeout(() => { el.classList.remove(‘kp-show’); }, 1600);
} catch (_) {}
}
let kpActionLock = false;
let kpLastActionAt = 0;
// ENHANCED DELEGATED EVENT HANDLER: Specifically targets kp-admin-action class
async function handleAdminAction(e) {
// Check if the clicked element or its parent has the kp-admin-action class
const button = e.target.closest(“.kp-admin-action”);
if (!button) return;
// Prevent default behavior and stop propagation
e.preventDefault();
e.stopPropagation();
// Prevent double-taps (global + per-button) and accidental double fires
const now = Date.now();
if (kpActionLock || (now – kpLastActionAt) < 700 || button.dataset.busy === “1”) return;
kpActionLock = true;
kpLastActionAt = now;
button.dataset.busy = “1”;
const action = button.getAttribute(“data-admin-action”);
if (!action) {
console.error(“KP Admin Action: No data-admin-action attribute found on button”);
button.dataset.busy = “0”;
return;
}
const originalHTML = button.innerHTML;
const originalStyle = button.getAttribute(“style”) || “”;
// Show loading state
button.innerHTML = ‘<span class=”kp-emoji kp-emoji-spin” aria-hidden=”true”>🎱</span>’;
button.style.opacity = “0.7”;
button.disabled = true;
console.log(“KP Admin Action triggered:”, action, “for event:”, window.eventSlug);
console.log(“Button element:”, button);
console.log(“Available variables – restBase:”, typeof window.restBase, “eventSlug:”, typeof window.eventSlug, “wpRestNonce:”, typeof window.wpRestNonce);
try {
// Ensure required variables are available
if (typeof window.restBase === ‘undefined’ || !window.restBase) {
throw new Error(“REST API base URL not available”);
}
if (typeof window.eventSlug === ‘undefined’ || !window.eventSlug) {
throw new Error(“Event slug not available”);
}
if (typeof window.wpRestNonce === ‘undefined’ || !window.wpRestNonce) {
throw new Error(“WordPress REST nonce not available”);
}
// Build API URL – map action to endpoint
let endpoint;
switch(action) {
case “potted”:
endpoint = “potted”;
break;
case “missed”:
endpoint = “missed”;
break;
case “foul”:
endpoint = “foul”;
break;
case “black-potted”:
endpoint = “black-potted”;
break;
default:
throw new Error(“Unknown action: ” + action);
}
const actionUrl = window.restBase + endpoint + “/” + encodeURIComponent(window.eventSlug) + “?t=” + Date.now();
console.log(“Calling admin action API:”, actionUrl);
const response = await fetch(actionUrl, {
method: “POST”,
cache: “no-store”,
headers: {
“Content-Type”: “application/json”,
“Accept”: “application/json”,
“Cache-Control”: “no-store”
},
body: JSON.stringify({})
});
console.log(“Admin action API response status:”, response.status);
if (!response.ok) {
const errorText = await response.text();
console.error(“API Error Response:”, errorText);
throw new Error(`HTTP ${response.status}: ${response.statusText} – ${errorText}`);
}
const result = await response.json();
console.log(“Admin action API response data:”, result);
if (result.success) {
// Show success feedback
button.innerHTML = ‘<span class=”kp-emoji”>✅</span>’;
button.style.background = “linear-gradient(135deg, #10b981 0%, #059669 100%)”;
button.style.color = “white”;
// Update current player display if new state returned
if (result.state && result.state.current_player) {
console.log(“Updating action box with new current player:”, result.state.current_player.display_name);
var norm = kpNorm(result);
kpSyncAdminActionBox(norm);
}
// Refresh players section to show updated lives/status
if (typeof refreshPlayersSection === ‘function’) {
await refreshPlayersSection();
try { kpNotifyTV(‘action’); } catch(_) {}
// Extra safety: re-pull and re-render action box
try {
const r2 = await kpFetch(“state/” + window.eventSlug);
if (r2 && r2.ok) {
const st = await r2.json();
if (st) {
var norm2 = kpNorm(st);
kpSyncAdminActionBox(norm2);
}
}
} catch (_) {}
}
// Reset button after brief success display
setTimeout(() => {
button.innerHTML = originalHTML;
button.style.opacity = “1”;
button.setAttribute(“style”, originalStyle);
button.disabled = false;
}, 1500);
} else {
throw new Error(result.message || “Action failed on server”);
}
} catch (err) {
console.error(“Admin action failed:”, err);
// Show error state
button.innerHTML = ‘<span class=”kp-emoji”>❌</span>’;
button.style.background = “linear-gradient(135deg, #ef4444 0%, #dc2626 100%)”;
button.style.color = “white”;
// Reset button after error display
setTimeout(() => {
button.innerHTML = originalHTML;
button.style.opacity = “1”;
button.setAttribute(“style”, originalStyle);
button.disabled = false;
}, 2500);
// Show error message to user
alert(“❌ Action failed: ” + err.message + “\\n\\nPlease check the console for details and ensure you’re logged in as an admin.”);
} finally {
// Always reset busy state
button.dataset.busy = “0”;
}
}
// Override with polished, debounced handler to avoid double taps
handleAdminAction = async function(e) {
const button = e && e.target ? e.target.closest(‘.kp-admin-action’) : null;
if (!button) return;
e.preventDefault(); e.stopPropagation();
const now = Date.now();
if (kpActionLock || (now – kpLastActionAt) < 700 || button.dataset.busy === ‘1’) return;
kpActionLock = true; kpLastActionAt = now; button.dataset.busy = ‘1’;
const action = button.getAttribute(‘data-admin-action’);
if (!action) { button.dataset.busy = ‘0’; kpActionLock = false; return; }
const originalStyle = button.getAttribute(‘style’) || ”;
const rect = button.getBoundingClientRect();
button.style.minWidth = rect.width + ‘px’;
button.style.minHeight = rect.height + ‘px’;
const allBtns = Array.from(document.querySelectorAll(‘.kp-admin-action’));
allBtns.forEach(b => { b.disabled = true; b.dataset.busy = ‘1’; });
// Busy state via CSS animation on existing icon
button.classList.add(‘kp-busy’);
button.style.opacity = ‘0.85’;
try {
const endpoint = action;
const actionUrl = (window.restBase || ”) + endpoint + ‘/’ + encodeURIComponent(window.eventSlug) + ‘?t=’ + Date.now();
const response = await fetch(actionUrl, { method: ‘POST’, cache: ‘no-store’, headers: { ‘Content-Type’:’application/json’, ‘Accept’:’application/json’, ‘Cache-Control’:’no-store’ }, body: JSON.stringify({}) });
if (!response.ok) { throw new Error(‘HTTP ‘ + response.status + ‘: ‘ + response.statusText); }
const result = await response.json();
if (result && result.success) {
// Success: no icon swap; show toast only
if (result.message) kpShowToast(result.message, ‘success’);
if (result.state && result.state.current_player) {
try { var norm = kpNorm(result); kpSyncAdminActionBox(norm); } catch(_) {}
}
if (typeof refreshPlayersSection === ‘function’) { try { await refreshPlayersSection(); } catch(_) {} }
if (typeof fastPollBurst === ‘function’) { try { fastPollBurst(); } catch(_) {} }
setTimeout(() => {
button.classList.remove(‘kp-busy’);
button.style.opacity = ‘1’;
button.setAttribute(‘style’, originalStyle);
allBtns.forEach(b => { b.disabled = false; b.dataset.busy = ‘0’; b.style.minWidth = ”; b.style.minHeight = ”; });
kpActionLock = false;
}, 300);
} else {
throw new Error((result && result.message) ? result.message : ‘Action failed’);
}
} catch (err) {
// Error: show toast only, keep icon stable
kpShowToast(‘Action failed: ‘ + (err && err.message ? err.message : ‘Unknown error’), ‘error’);
setTimeout(() => {
button.classList.remove(‘kp-busy’);
button.style.opacity = ‘1’;
button.setAttribute(‘style’, originalStyle);
allBtns.forEach(b => { b.disabled = false; b.dataset.busy = ‘0’; b.style.minWidth = ”; b.style.minHeight = ”; });
kpActionLock = false;
}, 600);
} finally {
setTimeout(() => { button.dataset.busy = ‘0’; kpActionLock = false; }, 200);
}
};
// Attach unified pointer event to avoid duplicate click/touch
document.addEventListener(‘pointerup’, handleAdminAction, { passive: false });
// Legacy event handler for any remaining data-admin-action buttons without kp-admin-action class
document.addEventListener(“click”, async function(e) {
const target = e.target.closest(“button”);
if (!target) return;
// Handle admin action buttons (only if not already handled by kp-admin-action)
if (target.hasAttribute(“data-admin-action”) && !target.classList.contains(“kp-admin-action”)) {
e.preventDefault();
e.stopPropagation();
if (target.dataset.busy === “1”) return; // Prevent double-clicks
target.dataset.busy = “1”;
const action = target.getAttribute(“data-admin-action”);
const originalStyle = target.getAttribute(‘style’) || ”;
// Show busy state
target.classList.add(‘kp-busy’);
target.style.opacity = “0.85”;
console.log(“Admin action triggered (legacy):”, action);
try {
const actionUrl = restBase + action + “/” + encodeURIComponent(window.eventSlug) + “?t=” + Date.now();
console.log(“Calling action API:”, actionUrl);
const response = await fetch(actionUrl, {
method: “POST”,
cache: “no-store”,
headers: {
“Content-Type”: “application/json”,
“Accept”: “application/json”,
“X-WP-Nonce”: wpRestNonce,
“Cache-Control”: “no-store”
},
body: JSON.stringify({})
});
console.log(“Action API response status:”, response.status);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
console.log(“Action API response data:”, result);
if (result.success) {
// Success toast only
if (result.message) kpShowToast(result.message, ‘success’);
// Refresh the action box with new current player
if (result.state && result.state.current_player) {
var norm = kpNorm(result);
kpSyncAdminActionBox(norm);
}
// Refresh players section to show updated lives/status
await refreshPlayersSection();
// Reset busy state
setTimeout(() => {
target.classList.remove(‘kp-busy’);
target.style.opacity = “1”;
target.setAttribute(‘style’, originalStyle);
}, 300);
} else {
throw new Error(result.message || “Action failed”);
}
} catch (err) {
console.error(“Admin action failed:”, err);
// Error toast only
kpShowToast(‘Action failed: ‘ + (err && err.message ? err.message : ‘Unknown error’), ‘error’);
// Reset busy state after delay
setTimeout(() => {
target.classList.remove(‘kp-busy’);
target.style.opacity = “1”;
target.setAttribute(‘style’, originalStyle);
}, 600);
} finally {
target.dataset.busy = “0”;
}
return;
}
// Handle life adjustment buttons with busy state protection
if (target.classList.contains(“life-adjust-btn”)) {
e.preventDefault();
if (target.dataset.busy === “1”) return; // Prevent double-clicks
target.dataset.busy = “1”;
const playerId = parseInt(target.getAttribute(“data-player-id”), 10);
const adjustment = parseInt(target.getAttribute(“data-adjustment”), 10);
const playerName = target.getAttribute(“data-player-name”) || “”;
if (playerId && (adjustment === 1 || adjustment === -1)) {
await adjustPlayerLife(playerId, adjustment, playerName);
}
target.dataset.busy = “0”;
return;
}
// Handle remove player buttons with essential protection against duplicates
if (target.classList.contains(“remove-btn”)) {
e.preventDefault();
e.stopPropagation();
if (typeof e.stopImmediatePropagation === “function”) e.stopImmediatePropagation();
if (target.dataset.busy === “1”) return;
target.dataset.busy = “1”;
const playerId = parseInt(target.getAttribute(“data-player-id”), 10);
const playerName = target.getAttribute(“data-player-name”) || “”;
console.log(“Remove button clicked for player ID:”, playerId);
if (!playerId) {
target.dataset.busy = “0”;
return;
}
// SIMPLE CONFIRMATION THAT WORKS
var msg = “Remove ” + (playerName || “this player”) + ” from tournament?\\n\\nThis cannot be undone!”;
if (!window.confirm(msg)) {
target.dataset.busy = “0”;
return;
}
// REQUEST WITH UNIQUE ID TO PREVENT DUPLICATES
const url = restBase + “remove-player/” + encodeURIComponent(window.eventSlug) + “?t=” + Date.now();
console.log(“Calling remove API:”, url);
try {
const res = await fetch(url, {
method: “POST”,
cache: “no-store”,
headers: {
“Content-Type”: “application/json”,
“Accept”: “application/json”,
“X-WP-Nonce”: wpRestNonce,
“Cache-Control”: “no-store”
},
body: JSON.stringify({
player_id: playerId,
purge_history: true,
_rid: String(Date.now()) // UNIQUE REQUEST ID TO PREVENT RACE CONDITIONS
})
});
console.log(“Remove API response status:”, res.status);
const d = await res.json();
console.log(“Remove API response data:”, d);
if (!res.ok || !d.success) {
alert(“❌ Error: ” + ((d && d.message) || “Failed to remove player”));
} else {
// Instant removal for UX
const card = document.querySelector(‘[data-player-id=”‘ + playerId + ‘”]’);
if (card) card.remove();
await refreshPlayersSection(); // Refresh to hide if still visible
alert(“✅ ” + d.message);
// No full reload needed; JS refresh handles it
}
} catch (err) {
console.error(“Remove error”, err);
alert(“❌ Failed to remove player. Please try again.”);
} finally {
target.dataset.busy = “0”;
}
return;
}
});
// Pointer/tap shim for life +/- and remove: trigger existing click handler
document.addEventListener(‘pointerup’, function(e){
const btn = e.target && e.target.closest && e.target.closest(‘.life-adjust-btn, .remove-btn’);
if (!btn) return;
// avoid repeated trigger
if (btn.dataset.kpTapTrigger === ‘1’) return;
btn.dataset.kpTapTrigger = ‘1’;
try { btn.click(); } catch(_) {}
setTimeout(function(){ try { delete btn.dataset.kpTapTrigger; } catch(_){} }, 350);
}, { passive: false });
// Updated adjustPlayerLife: fresh name + await refresh
async function adjustPlayerLife(playerId, adjustment, hintName) {
if (!Number.isInteger(playerId) || !Number.isInteger(adjustment)) return;
let name = await getFreshPlayerName(playerId);
if (!name && hintName) name = hintName;
if (!name) {
const anyEl = document.querySelector(‘[data-player-id=”‘ + playerId + ‘”][data-player-name]’);
name = anyEl ? anyEl.getAttribute(“data-player-name”) : “this player”;
}
const label = adjustment > 0 ? “+1” : “-1”;
if (!confirm(label + ” life for ” + name + “?”)) return;
try {
const url = restBase + “adjust-life/” + encodeURIComponent(window.eventSlug) + “?t=” + Date.now();
const res = await fetch(url, {
method: “POST”,
cache: “no-store”,
headers: {
“Content-Type”: “application/json”,
“Accept”: “application/json”,
“X-WP-Nonce”: wpRestNonce,
“Cache-Control”: “no-store”
},
body: JSON.stringify({ player_id: playerId, adjustment })
});
let data = {};
try { data = await res.json(); } catch {}
if (!res.ok || !data?.success) {
alert(“⚠️ Could not adjust life. ” + (data?.message || “Please try again.”));
console.warn(“adjust-life failed”, { status: res.status, data });
return;
}
alert(“✅ Life updated for ” + name);
await refreshPlayersSection(); // <– really waits now
try { kpNotifyTV(‘action’); } catch(_) {}
} catch (err) {
console.error(“adjust-life error”, err);
alert(“⚠️ Network error while adjusting life.”);
}
}
// Add helper function to get fresh player name from server
async function getFreshPlayerName(playerId) {
try {
const url = restBase + ‘state/’ + encodeURIComponent(window.eventSlug) + ‘?t=’ + Date.now();
const r = await fetch(url, { cache: ‘no-store’, headers: { ‘X-WP-Nonce’: wpRestNonce } });
if (!r.ok) throw new Error(“HTTP ” + r.status);
const data = await r.json();
var norm = kpNorm(data);
kpSyncAdminActionBox(norm);
const players = Array.isArray(data.players) ? data.players : [];
const p = players.find(x => String(x.id) === String(playerId));
return p?.display_name ? String(p.display_name) : “”;
} catch (e) {
console.warn(“getFreshPlayerName failed:”, e);
return “”;
}
}
// Awaitable, race-safe refresh
async function refreshPlayersSection() {
console.log(“KP DEBUG JS REFRESH START: Beginning refreshPlayersSection”);
// Wait for any in-flight refresh to finish first
if (window.__kpRefreshInFlight) {
console.log(“KP DEBUG JS REFRESH: Waiting for in-flight refresh”);
try { await window.__kpRefreshInFlight; } catch (_) {}
}
const playersContainer = document.getElementById(‘current-players-section’);
if (!playersContainer) {
console.warn(“Current players section not found – trying fallback selector”);
const h2 = document.querySelector(‘h2’);
if (h2 && h2.textContent.includes(‘Current Players’)) {
playersContainer = h2.parentNode;
} else {
console.warn(“Players section not found at all”);
return;
}
}
console.log(“KP DEBUG JS REFRESH: Fetching state for event ” + window.eventSlug);
const run = (async () => {
const url = restBase + ‘state/’ + encodeURIComponent(window.eventSlug) + ‘?t=’ + Date.now();
const r = await fetch(url, { cache: ‘no-store’, headers: { ‘X-WP-Nonce’: wpRestNonce } });
if (!r.ok) throw new Error(“HTTP ” + r.status);
const data = await r.json();
renderActionBox((data.current_player || null), data);
console.log(“KP DEBUG JS REFRESH FETCH: Raw players data:”, data.players ? data.players.length : 0, “players”);
// keep all players, filter active (is_active = 1 and lives > 0) for rendering
const raw = Array.isArray(data.players) ? data.players : [];
const active = raw.filter(p => (p && parseInt(p.is_active, 10) === 1 && parseInt(p.lives, 10) > 0));
// de-dupe by id on the active set
const uniq = […new Map(active.map(p => [String(p.id), p])).values()];
console.log(“KP DEBUG JS REFRESH: After strict filter, ” + uniq.length + ” unique active players by ID”);
let html = ‘<h2 style=”margin-top: 0; color: #059669;”>🎯 Current Players (‘ + uniq.length + ‘)</h2>’;
if (uniq.length > 0) {
html += ‘<div style=”max-height: 500px; overflow-y: auto;”>’;
const maxCap = parseInt((data && data.event && data.event.max_lives) || 5, 10);
uniq.forEach(player => {
const lives = parseInt(player.lives, 10) || 0;
const statusColor = lives > 0 ? “#059669” : “#dc2626”; // green for active, red for out
const bgColor = lives > 0 ? “#f0fdf4” : “#fef2f2”; // green bg for active, red bg for out
const statusLabel = lives > 0 ? ‘Active’ : ‘OUT’;
const safeName = (player.display_name || ”).replace(/”/g, ‘”‘);
console.log(“KP DEBUG JS REFRESH BUILD: Player ” + player.id + ” (” + safeName + “) – active: ” + player.is_active + “, lives: ” + lives);
html += ‘<div style=”padding:12px;margin:8px 0;background:’ + bgColor + ‘;border-radius:8px;border-left:4px solid ‘ + statusColor + ‘;” data-player-id=”‘ + player.id + ‘”>’;
html += ‘<div style=”display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;”>’;
html += ‘<div>’;
html += ‘<div style=”font-size:18px;font-weight:700;” title=”‘ + safeName + ‘”>’ + (player.display_name || ”) + ‘</div>’;
html += ‘<div style=”color:#6b7280;font-size:14px;”>#’ + player.id + ‘ • ‘ + statusLabel + ‘</div>’;
html += ‘</div>’;
html += ‘<div style=”display:flex;align-items:center;gap:8px;”>’;
// Lives display with separate colors for regular vs black ball lives
const regular_lives = parseInt(player.regular_lives || 0, 10);
const black_ball_lives = parseInt(player.black_ball_lives || 0, 10);
for (let i = 0; i < 5; i++) {
if (i < regular_lives) {
html += ‘<span style=”width:14px;height:14px;background:#10b981;border-radius:50%;display:inline-block;box-shadow:0 0 4px rgba(16,185,129,.5);”></span>’; // Regular life – green
} else if (i < regular_lives + black_ball_lives) {
html += ‘<span style=”width:14px;height:14px;background:#1f2937;border:2px solid #f59e0b;border-radius:50%;display:inline-block;box-shadow:0 0 4px rgba(245,158,11,.5);”></span>’; // Black ball life – dark/black
} else {
html += ‘<span style=”width:14px;height:14px;background:#e5e7eb;border-radius:50%;display:inline-block;”></span>’; // No life – empty
}
}
html += ‘<span style=”margin-left:10px;font-weight:700;color:’ + statusColor + ‘”> ‘ + lives + ‘</span>’;
html += ‘</div>’;
html += ‘</div>’;
const minusDisabled = lives <= 0 ? ‘ disabled’ : ”;
const plusDisabled = lives >= maxCap ? ‘ disabled’ : ”;
// NOTE: add data-player-name on ALL THREE buttons
html += ‘<div style=”display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-top:10px;”>’;
html += ‘<button type=”button” class=”life-adjust-btn” data-player-id=”‘ + player.id + ‘” data-player-name=”‘ + safeName + ‘” data-adjustment=”-1″ style=”background:#ef4444;color:#fff;border:none;padding:8px 12px;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer;”‘ + minusDisabled + ‘>-1 LIFE</button>’;
html += ‘<button type=”button” class=”life-adjust-btn” data-player-id=”‘ + player.id + ‘” data-player-name=”‘ + safeName + ‘” data-adjustment=”1″ style=”background:#10b981;color:#fff;border:none;padding:8px 12px;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer;”‘ + plusDisabled + ‘>+1 LIFE</button>’;
html += ‘<button type=”button” class=”remove-btn” data-player-id=”‘ + player.id + ‘” data-player-name=”‘ + safeName + ‘” style=”background:#dc2626;color:#fff;border:none;padding:8px 12px;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer;”>REMOVE</button>’;
html += ‘</div>’;
html += ‘</div>’;
});
html += ‘</div>’;
} else {
html += ‘<p style=”color:#6b7280;margin:8px 0 0″>No active players.</p>’;
}
console.log(“KP DEBUG JS REFRESH: Updating DOM with ” + uniq.length + ” active players”);
playersContainer.innerHTML = html;
// Log after DOM update – check for any immediate events
console.log(“KP DEBUG JS REFRESH: DOM updated, re-binding event listeners”);
console.log(“KP DEBUG JS REFRESH: Active buttons count – life-adjust: ” + document.querySelectorAll(‘.life-adjust-btn’).length + “, remove: ” + document.querySelectorAll(‘.remove-btn’).length + “, add input: ” + !!document.getElementById(‘newPlayerName’));
})();
window.__kpRefreshInFlight = run
.catch(err => {
console.error(“refreshPlayersSection error:”, err);
throw err;
})
.finally(() => {
window.__kpRefreshInFlight = null;
console.log(“KP DEBUG JS REFRESH END: Refresh completed”);
});
return window.__kpRefreshInFlight;
}
// Add late player function – fixed to use global variables
function addLatePlayer() {
const nameInput = document.getElementById(“newPlayerName”);
const playerName = (nameInput?.value || “”).trim();
if (!playerName) {
alert(“Please enter a player name”);
nameInput && nameInput.focus();
return;
}
if (!confirm(“Add player: ” + playerName + ” with 3 lives?”)) return;
const addUrl = restBase + “add-player/” + encodeURIComponent(window.eventSlug) + “?t=” + Date.now();
fetch(addUrl, {
method: “POST”,
cache: “no-store”,
headers: {
“Content-Type”: “application/json”,
“Accept”: “application/json”,
“X-WP-Nonce”: wpRestNonce,
“Cache-Control”: “no-store”
},
body: JSON.stringify({ player_name: playerName })
})
.then(r => r.json())
.then(result => {
if (result && result.success) {
alert(“✅ ” + result.message);
if (nameInput) nameInput.value = “”;
refreshPlayersSection();
} else {
alert(“❌ Error: ” + ((result && result.message) || “Unknown error”));
}
})
.catch(err => {
console.error(“Add player failed:”, err);
alert(“❌ Failed to add player. Please try again.”);
location.reload();
});
}
// Legacy removePlayer removed – use only delegated .remove-btn handler
(() => {
const urlDebug = /[?&]kpdebug=1/i.test(location.search);
if (urlDebug && typeof console !== ‘undefined’ && console.debug) {
const orig = console.debug.bind(console);
console.debug = (…a) => orig(‘[KP]’, …a);
}
})();
// Show player history modal
function showPlayerHistory(playerId, playerName) {
const historyUrl = restBase + “player-history/” + encodeURIComponent(window.eventSlug) + “/” + playerId;
fetch(historyUrl, {
method: “GET”,
headers: { “Accept”: “application/json” }
})
.then(r => r.json())
.then(result => {
if (result && result.success) {
const history = result.formatted_history;
showHistoryModal(playerName, history);
} else {
alert(“❌ Could not load player history”);
}
})
.catch(err => {
console.error(“Get history failed:”, err);
alert(“❌ Failed to load player history. Please try again.”);
});
}
// Attach event listeners to static player history links on page load
document.addEventListener(‘DOMContentLoaded’, function() {
var links = document.querySelectorAll(‘.player-history-link’);
for (var i = 0; i < links.length; i++) {
var link = links[i];
link.addEventListener(‘click’, function(e) {
e.preventDefault();
var playerId = parseInt(this.getAttribute(‘data-player-id’), 10);
var playerName = this.getAttribute(‘data-player-name’);
showPlayerHistory(playerId, playerName);
});
}
// AJAX handler for Start Tournament form
const startForm = document.getElementById(‘start-game-form’); if (startForm) startForm.addEventListener(‘submit’, async function(e) {
e.preventDefault();
const form = e.target;
const submitBtn = form.querySelector(‘button[type=”submit”]’);
const originalText = submitBtn.innerHTML;
const eventSlug = form.getAttribute(‘data-event-slug’);
if (!eventSlug) {
alert(‘Event slug not found’);
return;
}
// Show loading
submitBtn.disabled = true;
submitBtn.innerHTML = ‘<span class=”kp-btn-spinner”></span> Starting…’;
try {
const response = await fetch(window.restBase + ‘start-game/’ + encodeURIComponent(eventSlug), {
method: ‘POST’,
headers: {
‘X-WP-Nonce’: window.wpRestNonce
}
});
const result = await response.json();
if (result.success) {
kpShowToast(result.message, ‘success’);
// Hide start CTA
const startCta = document.getElementById(‘kp-start-cta’);
if (startCta) startCta.style.display = ‘none’;
// Show action box
const actionBox = document.getElementById(‘admin-action-box’);
if (actionBox) actionBox.style.display = ”;
// Refresh players and action box
if (typeof refreshPlayersSection === ‘function’) {
await refreshPlayersSection();
}
if (typeof fastPollBurst === ‘function’) {
fastPollBurst();
}
kpNotifyTV(‘action’); // <— ADD THIS LINE
// Re-render action box with new state
if (result.state) {
var norm = kpNorm(result);
kpSyncAdminActionBox(norm);
}
// === KP-GOV: stabilize after start, clear winner, seed shooter ===
try {
// 1) Clear any winner UI that may have persisted
(function clearWinnerUI(){
const w = document.getElementById(‘kp-winner’);
if (w) w.remove();
document.querySelectorAll(‘.kp-winner-panel,.kp-winner-banner’).forEach(n=>n.remove());
})();
// 2) Bump governor epoch so TVs definitely see a fresh match
const slug = window.eventSlug || ”;
await fetch(‘/wp-json/kp-governor/v1/begin-next-epoch?slug=’ + encodeURIComponent(slug), {
method: ‘GET’, credentials: ‘same-origin’, headers: { ‘Cache-Control’:’no-store’ }
}).catch(()=>{});
// 3) If shooter is still unknown right after start, seed one select-next
const base = (typeof window.restBase === ‘string’ ? window.restBase : (window.restBase || ”));
if (base && slug) {
const st = await fetch(base.replace(/\/+$/,’/’) + ‘state/’ + encodeURIComponent(slug) + ‘?t=’ + Date.now(),
{ credentials:’same-origin’, headers:{‘Accept’:’application/json’,’Cache-Control’:’no-store’} }
).then(r=>r.ok?r.json():null).catch(()=>null);
const hasShooter = !!(st && (st.current_player || st?.event?.current_player_id || st?.game?.current_player_id));
if (!hasShooter) {
await fetch(base.replace(/\/+$/,’/’) + ‘select-next/’ + encodeURIComponent(slug), {
method:’POST’, credentials:’same-origin’,
headers: { ‘X-WP-Nonce’: (window.wpRestNonce||”), ‘Accept’:’application/json’,’Cache-Control’:’no-store’ }
}).catch(()=>{});
}
}
// 4) Wake TVs that listen to admin-side hints
if (typeof fastPollBurst === ‘function’) fastPollBurst();
kpNotifyTV(‘action’);
} catch(e) {
console.debug(‘[KP ADMIN START-STAB] post-start sequence non-fatal:’, e && (e.message||e));
}
// === /KP-GOV start stabilization ===
} else {
throw new Error(result.message || ‘Failed to start game’);
}
} catch (err) {
console.error(‘Start game error:’, err);
kpShowToast(‘Failed to start game: ‘ + err.message, ‘error’);
submitBtn.innerHTML = originalText;
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = originalText;
}
});
});
// Display history modal
function showHistoryModal(playerName, history) {
const overlay = document.createElement(“div”);
overlay.style.cssText = `
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.7);
z-index: 10000;
display: flex; align-items: center; justify-content: center;
padding: 20px;
`;
const modal = document.createElement(“div”);
modal.style.cssText = `
background: white; padding: 30px; border-radius: 15px;
max-width: 500px; width: 100%;
max-height: 80vh; overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
`;
const modalHTML = ‘<div style=”display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;border-bottom:2px solid #f1f5f9;padding-bottom:15px;”>’ +
‘<h3 style=”margin:0;color:#1e40af;font-size:24px;”>🎱 ‘ + playerName + ‘</h3>’ +
‘<button onclick=”closeHistoryModal()” style=”background:#dc2626;color:white;border:none;padding:8px 12px;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer;”>✕</button>’ +
‘</div>’ +
‘<div style=”margin-bottom:20px;”>’ +
‘<h4 style=”margin:0 0 15px 0;color:#059669;”>📋 Action History</h4>’ +
‘<div style=”background:#f9fafb;padding:20px;border-radius:8px;border:2px solid #e5e7eb;”>’ +
‘<p style=”margin:0;font-size:16px;line-height:1.6;color:#374151;”>’ + (history || “No actions recorded yet”) + ‘</p>’ +
‘</div>’ +
‘</div>’ +
‘<div style=”text-align:center;”>’ +
‘<div style=”text-align:center;”>’ +
‘<button onclick=”closeHistoryModal()” style=”background:#3b82f6;color:white;border:none;padding:12px 24px;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer;”>Close</button>’ +
‘</div>’;
overlay.appendChild(modal);
document.body.appendChild(overlay);
window.currentHistoryModal = overlay;
overlay.addEventListener(“click”, function(e) {
if (e.target === overlay) closeHistoryModal();
});
document.addEventListener(“keydown”, function onEsc(e) {
if (e.key === “Escape”) {
document.removeEventListener(“keydown”, onEsc);
closeHistoryModal();
}
});
}
function closeHistoryModal() {
if (window.currentHistoryModal) {
window.currentHistoryModal.remove();
window.currentHistoryModal = null;
}
}
function renderActionBox(currentPlayer, state) {
const box = document.getElementById(‘admin-action-box’);
if (!box) return;
const shooterName = currentPlayer?.display_name || currentPlayer?.name || (‘#’ + (currentPlayer?.id ?? ‘—’));
const livesTotal = Number(currentPlayer?.regular_lives || 0) + Number(currentPlayer?.black_ball_lives || 0);
const ballNo = currentPlayer?.player_number ? `Ball #${currentPlayer.player_number}` : ”;
const infoDiv = box.querySelector(‘.current-player-info’);
if (infoDiv) {
infoDiv.innerHTML = `
<div style=”font-size: 20px; font-weight: 700; margin-bottom: 5px;”>${shooterName}</div>
<div style=”opacity: 0.9; font-size: 14px;”>
${ballNo}${ballNo && livesTotal !== null ? ‘ • ‘ : ”}${livesTotal} lives
</div>
<div style=”margin-top:6px; font-size:14px; opacity:0.8;”>
⏱️ <span id=”kp-admin-timer”>–</span>
</div>
`.replace(/\${([^}]+)}/g, (match, p1) => {
if (typeof window[p1] !== ‘undefined’) return window[p1];
return match;
});
}
// Enable/disable buttons
const gameState = state?.event?.game_state || state?.pool_status;
const isActive = gameState === ‘active’;
box.querySelectorAll(‘[data-admin-action]’).forEach(btn => {
btn.disabled = !isActive || !currentPlayer;
});
// Start or reset the timer display
setupAdminTimer(state?.event);
}
// === ADMIN: hook unified poller into UI ===
window.onStateUpdate = function onStateUpdate(data) {
try {
// Normalize game_state from either top-level or nested event
const topGS = (data && typeof data.game_state !== ‘undefined’) ? data.game_state : null;
const eventGS = (data && data.event && typeof data.event.game_state !== ‘undefined’) ? data.event.game_state : null;
const gs = (eventGS !== null) ? eventGS : (topGS !== null ? topGS : ‘waiting’);
// Show/hide start CTA vs action box (mirrors your old admin logic)
(function syncStartPanels(){
try {
const startCta = document.getElementById(‘kp-start-cta’);
const actionBox = document.getElementById(‘admin-action-box’);
if (gs === ‘active’) {
if (startCta) startCta.style.display = ‘none’;
if (actionBox) actionBox.style.display = ”;
} else {
if (startCta) startCta.style.display = ”;
if (actionBox) actionBox.style.display = ‘none’;
}
} catch(_){}
})();
// Update current player / buttons
try { renderActionBox((data.current_player || null), data); } catch(_){}
// Optional: refresh “Current Match History” panel if present
try {
if (typeof refreshCurrentHistory === ‘function’) {
refreshCurrentHistory(); // cheap; your API is light
}
} catch(_){}
// Timers or other mirrors if you have them
try { if (typeof handleTimer === ‘function’) handleTimer(data.event || null); } catch(_){}
} catch (e) {
console.warn(‘ADMIN onStateUpdate error:’, e);
}
};
function setupAdminTimer(event) {
}
function setupAdminTimer(event) {
// Respect admin toggle: if polling is disabled, kill any timer & exit
const KPC = window.KPC || {};
if (KPC.polling_enabled === false) {
if (window.__KP_ADMIN_VIS_HANDLER__) {
document.removeEventListener(‘visibilitychange’, window.__KP_ADMIN_VIS_HANDLER__);
window.__KP_ADMIN_VIS_HANDLER__ = null;
}
if (adminTimerInterval) { clearInterval(adminTimerInterval); adminTimerInterval = null; }
return;
}
// If no timer data, clean up and exit
if (!event || !event.timer_start_time || !event.timer_duration) {
if (window.__KP_ADMIN_VIS_HANDLER__) {
document.removeEventListener(‘visibilitychange’, window.__KP_ADMIN_VIS_HANDLER__);
window.__KP_ADMIN_VIS_HANDLER__ = null;
}
if (adminTimerInterval) { clearInterval(adminTimerInterval); adminTimerInterval = null; }
return;
}
const timerEl = document.getElementById(‘kp-admin-timer’);
if (!timerEl) return;
if (adminTimerInterval) { clearInterval(adminTimerInterval); adminTimerInterval = null; }
function updateTimer() {
const now = Math.floor(Date.now() / 1000);
const elapsed = now – Number(event.timer_start_time);
const remaining = Math.max(0, Number(event.timer_duration) – elapsed);
timerEl.textContent = remaining > 0 ? `${remaining}s` : ‘0s’;
}
updateTimer();
// adminTimerInterval = setInterval(updateTimer, 1000);
if (window.__KP_ADMIN_VIS_HANDLER__) {
document.removeEventListener(‘visibilitychange’, window.__KP_ADMIN_VIS_HANDLER__);
}
window.__KP_ADMIN_VIS_HANDLER__ = () => {
if (document.hidden) {
if (adminTimerInterval) { clearInterval(adminTimerInterval); adminTimerInterval = null; }
} else {
if (!adminTimerInterval) {
updateTimer();
// adminTimerInterval = setInterval(updateTimer, 1000);
}
}
};
}
// — minimal normalizer (read-only) —
function kpNorm(raw) {
if (!raw || typeof raw !== ‘object’) return { game_state: ‘waiting’, state: { game_state: ‘waiting’ }, event: { game_state: ‘waiting’ } };
var gs = raw.game_state || (raw.state && raw.state.game_state) || (raw.event && raw.event.game_state) || ‘waiting’;
return {
game_state: gs,
state: raw.state || { game_state: gs },
event: raw.event || { game_state: gs },
raw: raw
};
}
// — minimal UI sync for admin action box —
function kpSyncAdminActionBox(norm) {
try {
var gs = norm.game_state || ‘waiting’;
var startCta = document.getElementById(‘kp-start-cta’);
if (startCta) startCta.style.display = (gs === ‘active’) ? ‘none’ : ”;
var box = document.getElementById(‘admin-action-box’);
if (!box) return;
if (gs === ‘active’) {
box.style.display = ”;
if (typeof renderActionBox === ‘function’) {
// Pass EXACTLY what the older renderer expects:
// prefer (currentPlayer, state); fallback to single-object legacy.
var st = norm.state || {};
var cp = st.current_player || null;
if (renderActionBox.length >= 2) {
renderActionBox(cp, st);
} else {
var legacy = Object.assign({}, st);
legacy.current_player = cp;
renderActionBox(legacy);
}
}
// Always (re)mount handlers *after* rendering so buttons are live
if (typeof kp_admin_mount_action_handlers === ‘function’) {
kp_admin_mount_action_handlers();
}
} else {
box.style.display = ‘none’;
}
} catch (e) {
// keep polling alive even if UI hiccups
console.warn(‘kpSyncAdminActionBox:’, e && e.message ? e.message : String(e));
}
}
// ================================================================
// ADMIN STATE HANDLER (Unified Poller Version)
// This completely replaces legacy admin polling / fast burst / timers
// ================================================================
(function adminUnifiedStateHandler(){
console.log(“KP ADMIN: Unified state handler active”);
let lastStateString = null; // Used for diffing active players
let lastAliveCount = null;
let lastGameState = null;
// ———————————————————–
// When unified poller emits fresh state → update admin UI
// ———————————————————–
window.addEventListener(“kp:state”, async function(ev){
const data = ev.detail;
if (!data) return;
// Make sure admin always reads the canonical unified state
try { window.__kpLastState = data; } catch(_) {}
const gameState =
data?.game_state ??
data?.event?.game_state ??
‘waiting’;
const activePlayers = Array.isArray(data.players)
? data.players.filter(p => (parseInt(p.is_active)==1 && (parseInt(p.regular_lives)+parseInt(p.black_ball_lives))>0))
: [];
const aliveCount = activePlayers.length;
// ——————————————————-
// SHOOTER WATCHDOG (admin only)
// ——————————————————-
const ok = !!data.current_player;
const videoBusy = !!(window.KP_ELIM && KP_ELIM.playing);
let guard = document.querySelector(‘[data-kp-admin-guard]’);
if (gameState === ‘active’ && !ok && !videoBusy) {
if (!guard) {
guard = document.createElement(‘div’);
guard.setAttribute(‘data-kp-admin-guard’,”);
guard.style.cssText = `
position:sticky;top:8px;z-index:1000;margin:8px 0;
padding:10px 12px;border-radius:10px;background:#f59e0b;
color:#111827;font-weight:700;border:2px solid rgba(0,0,0,0.1);
`;
guard.innerHTML = `
⚠️ No current shooter — auto-retrying. Controls remain active.
<button id=”kp-force-shooter”
style=”margin-left:12px;padding:6px 10px;border-radius:6px;
background:#2563eb;color:#fff;font-weight:600;”>
Force Next Shooter
</button>
`;
const mount = document.querySelector(‘.wrap’) || document.body;
mount.insertBefore(guard, mount.firstChild);
const btn = document.getElementById(‘kp-force-shooter’);
if (btn) {
btn.onclick = async function(){
try {
const url = window.restBase + ‘force-next-shooter/’ + window.eventSlug;
const resp = await fetch(url, {
method: ‘POST’,
headers: { ‘X-WP-Nonce’: window.wpRestNonce }
});
const res = await resp.json();
if (res?.chosen) {
alert(“Next shooter forced: ” + (res.chosen.display_name || (“Player ” + res.chosen.id)));
} else {
alert(“Could not force shooter”);
}
} catch(e){
alert(“Force shooter failed”);
}
};
}
}
} else if (guard) {
guard.remove();
}
// ——————————————————-
// ACTION BOX UPDATE
// ——————————————————-
if (typeof renderActionBox === “function”) {
renderActionBox((data.current_player || null), data);
if (typeof kp_admin_mount_action_handlers === “function”)
kp_admin_mount_action_handlers();
}
// ——————————————————-
// WINNER / FINISHED STATE HANDLING
// ——————————————————-
const actionBox = document.getElementById(“admin-action-box”);
if (gameState === “finished”) {
if (actionBox) actionBox.style.display = “none”;
let winnerBanner = document.getElementById(“kp-winner”);
if (!winnerBanner) {
const winner = Array.isArray(data.alive_players) && data.alive_players.length === 1
? data.alive_players[0]
: null;
const winnerName =
winner?.display_name ||
winner?.name ||
`Player #${winner?.id || ”}`;
winnerBanner = document.createElement(“div”);
winnerBanner.id = “kp-winner”;
winnerBanner.style.cssText =
“background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%);” +
“color:#1f2937;padding:50px 20px;border-radius:20px;” +
“text-align:center;margin:20px 0;”;
winnerBanner.innerHTML = `
<div style=”font-size: clamp(1.5rem,3vw,2.5rem);font-weight:800;margin-bottom:20px;”>
🏆 TOURNAMENT WINNER! 🏆
</div>
<div style=”font-size: clamp(2.5rem,5vw,4rem);font-weight:900;margin-bottom:30px;”>
${winnerName}
</div>
<div style=”font-size:2rem;animation: celebrate 3s ease-in-out infinite;”>
🎉 🎊 🏆 🎉 🎊
</div>
`;
const container = document.querySelector(‘.wrap’) || document.body;
container.appendChild(winnerBanner);
}
} else {
// Hide winner banner if present
const winnerBanner = document.getElementById(“kp-winner”);
if (winnerBanner) winnerBanner.style.display = “none”;
// Show action box if not finished
if (actionBox) actionBox.style.display = (gameState === “active” ? “” : “none”);
}
// ——————————————————-
// PLAYER STATE DIFFING (lives, eliminations, etc.)
// ——————————————————-
const stateString = JSON.stringify(activePlayers);
if (stateString !== lastStateString) {
console.log(“KP ADMIN: Detected player state change → refreshing players”);
lastStateString = stateString;
if (typeof refreshPlayersSection === “function”) {
await refreshPlayersSection();
}
}
lastAliveCount = aliveCount;
lastGameState = gameState;
});
console.log(“KP ADMIN: Listening for unified kp:state updates”);
})();
<!– KP: start of history feature –>
<script>
(function(){
if (window.__KP_HISTORY_CHIPS_ONLY__) return;
window.__KP_HISTORY_CHIPS_ONLY__ = true;
// ———- tap into existing state flow ———-
function hasTopLevel(o){
return !!(o && (o.players || o.participants || o.current_player || o.event || o.history));
}
function flatten(o){
if (hasTopLevel(o)) return o;
if (!o || typeof o !== ‘object’) return null;
const cand = o.data || o.state || o.payload || o.kp || null;
return hasTopLevel(cand) ? cand : null;
}
// Keep cache/DOM clean while game_state = “waiting”
function publish(state){
const flat = flatten(state) || window.__kpLastState || null;
if (!flat) return;
// Read game_state from top level or event
const gs = (typeof flat.game_state !== ‘undefined’)
? flat.game_state
: (flat.event && typeof flat.event.game_state !== ‘undefined’
? flat.event.game_state
: null);
// If reset (waiting), clear and stop here (no re-cache, no dispatch)
if (gs === ‘waiting’) {
try { window.__kpLastState = null; } catch(_){}
try { window.__kpHistory = []; } catch(_){}
try {
const chipbar = document.getElementById(‘kp-chipbar’);
if (chipbar) chipbar.innerHTML = ”;
const hist = document.querySelector(‘.kp-history-grid’);
if (hist) hist.innerHTML = ”;
document.querySelectorAll(‘[data-shooting=”1″]’).forEach(el => {
try { el.removeAttribute(‘data-shooting’); } catch(_){}
});
} catch(_){}
return;
}
// Normal flow when not waiting
try { window.__kpLastState = flat; } catch(_){}
try { window.dispatchEvent(new CustomEvent(‘kp:state’, { detail: flat })); } catch(_){}
}
function patch(name){
const fn = window[name];
if (typeof fn !== ‘function’ || fn.__kp_patched__) return false;
const wrapped = function(){
let ret;
try { ret = fn.apply(this, arguments); }
finally {
const maybe = (arguments && arguments[0] && typeof arguments[0] === ‘object’) ? arguments[0] : null;
publish(maybe);
}
return ret;
};
wrapped.__kp_patched__ = true;
window[name] = wrapped;
console.log(‘KP HISTORY: patched’, name);
return true;
}/* === KP HISTORY CHIPS ONLY shim + global waiting sweeper (drop-in) === */
(function(){
if (window.__KP_HISTORY_CHIPS_ONLY__) return;
window.__KP_HISTORY_CHIPS_ONLY__ = true;
// ———- tap into existing state flow ———-
function hasTopLevel(o){
return !!(o && (o.players || o.participants || o.current_player || o.event || o.history));
}
function flatten(o){
if (hasTopLevel(o)) return o;
if (!o || typeof o !== ‘object’) return null;
var cand = o.data || o.state || o.payload || o.kp || null;
return hasTopLevel(cand) ? cand : null;
}
// Keep cache/DOM clean while game_state = “waiting”
function publish(state){
var flat = flatten(state) || window.__kpLastState || null;
if (!flat) return;
// Read game_state from top level or event
var gs = (typeof flat.game_state !== ‘undefined’)
? flat.game_state
: (flat.event && typeof flat.event.game_state !== ‘undefined’
? flat.event.game_state
: null);
// If reset (waiting), clear and stop here (no re-cache, no dispatch)
if (gs === ‘waiting’) {
try { window.__kpLastState = null; } catch(_){}
try { window.__kpHistory = []; } catch(_){}
try {
var chipbar = document.getElementById(‘kp-chipbar’);
if (chipbar) chipbar.innerHTML = ”;
var hist = document.querySelector(‘.kp-history-grid’);
if (hist) hist.innerHTML = ”;
var shots = document.querySelectorAll(‘[data-shooting=”1″]’);
for (var i=0;i<shots.length;i++){ try{ shots[i].removeAttribute(‘data-shooting’); }catch(_){} }
} catch(_){}
return; // do NOT broadcast while waiting
}
// Normal flow when not waiting
try { window.__kpLastState = flat; } catch(_){}
try { window.dispatchEvent(new CustomEvent(‘kp:state’, { detail: flat })); } catch(_){}
}
function patch(name){
var fn = window[name];
if (typeof fn !== ‘function’ || fn.__kp_patched__) return false;
var wrapped = function(){
var ret;
try { ret = fn.apply(this, arguments); }
finally {
var maybe = (arguments && arguments[0] && typeof arguments[0] === ‘object’) ? arguments[0] : null;
publish(maybe);
}
return ret;
};
wrapped.__kp_patched__ = true;
window[name] = wrapped;
console.log(‘KP HISTORY: patched’, name);
return true;
}
[‘refreshCurrentHistory’,’onStateUpdate’,’updateAdminFromState’,’updateDOMElements’].forEach(patch);
document.addEventListener(‘DOMContentLoaded’, function(){
[‘refreshCurrentHistory’,’onStateUpdate’,’updateAdminFromState’,’updateDOMElements’].forEach(patch);
publish(window.__kpLastState || null);
});
})();
/* — GLOBAL “waiting” sweeper: clears chips/history & cache regardless of shim — */
(function(){
function kpSweeper(s){
var gs = (typeof s.game_state !== ‘undefined’)
? s.game_state
: (s.event && typeof s.event.game_state !== ‘undefined’
? s.event.game_state
: null);
if (gs === ‘waiting’) {
try { window.__kpLastState = null; } catch(_){}
try { window.__kpHistory = []; } catch(_){}
try {
var chipbar = document.getElementById(‘kp-chipbar’);
if (chipbar) chipbar.innerHTML = ”;
var hist = document.querySelector(‘.kp-history-grid’);
if (hist) hist.innerHTML = ”;
var shots = document.querySelectorAll(‘[data-shooting=”1″]’);
for (var i=0;i<shots.length;i++){ try{ shots[i].removeAttribute(‘data-shooting’); }catch(_){} }
} catch(_){}
}
}
window.addEventListener(‘kp:state’, function(ev){ kpSweeper(ev && ev.detail ? ev.detail : {}); }, { passive:true });
})();
[‘refreshCurrentHistory’,’onStateUpdate’,’updateAdminFromState’,’updateDOMElements’].forEach(patch);
document.addEventListener(‘DOMContentLoaded’, function(){
[‘refreshCurrentHistory’,’onStateUpdate’,’updateAdminFromState’,’updateDOMElements’].forEach(patch);
publish(window.__kpLastState || null);
});
// ———- chip bar UI ———-
const $ = (s,r=document)=>r.querySelector(s);
function anchor(){
// Prefer the action panel; fall back to the main KP mount; then body.
return document.querySelector([
‘#admin-action-box’,
‘#kp-admin-actions’,
‘.kp-admin-actions’,
‘form#player-action-form’,
‘form[name=”player_action”]’,
‘form[action*=”player_action”]’
].join(‘, ‘))
|| document.querySelector(‘#kp-root, .killer-pool-container’)
|| document.body;
}
function ensureBar(){
// Remove any old vertical grid from previous experiments
document.querySelectorAll(‘.kp-history-grid’).forEach(n=>n.remove());
let bar = document.getElementById(‘kp-chipbar’);
if (!bar) {
bar = document.createElement(‘div’);
bar.id = ‘kp-chipbar’;
bar.setAttribute(‘data-kp’,’chipbar’);
}
let kpRootElement = document.getElementById(‘kp-root’);
// Ensure bar is inserted after #kp-root if not already
if (kpRootElement) {
kpRootElement.insertAdjacentElement(‘afterend’, bar);
}
}
(function(){ // styles once
const id=’kp-history-chips-styles’;
if (document.getElementById(id)) return;
const css=document.createElement(‘style’); css.id=id;
css.textContent = `
#kp-root{
position: relative;
}
#kp-chipbar{
position: relative;
display:flex; flex-direction: row; flex-wrap:wrap; align-items:center; gap:.5rem;
padding:.35rem .5rem; margin:.5rem 0;
background:rgba(255,255,255,.75); backdrop-filter:saturate(1.1) blur(4px);
border:1px solid rgba(0,0,0,.06); border-radius:.6rem;
}
#kp-chipbar .kp-chip{
display:inline-flex; align-items:center; gap:.4rem;
padding:.25rem .55rem; border-radius:999px; font-size:.92rem; color:#000;
border:1px solid rgba(0,0,0,.1); background:#fff;
}
`;
document.head.appendChild(css);
})();
// REMOVE legacy “Current Match History” widgets (ADMIN only, by exact IDs)
(function kpHideLegacyHistoryPanels(){
const ADMIN_ANCHOR_Q =
‘#admin-action-box,#kp-admin-actions,.kp-admin-actions,form#player-action-form,form[name=”player_action”],form[action*=”player_action”]’;
if (!document.querySelector(ADMIN_ANCHOR_Q)) return; // TV page? do nothing
function nuke() {
// There can be multiple (invalid duplicate IDs), so select all by attribute
document.querySelectorAll(‘[id=”current-match-history”]’).forEach(n => n.remove());
document.querySelectorAll(‘[id=”history-content”]’).forEach(n => n.remove());
}
try { nuke(); } catch(_){}
// Keep it clean after Elementor/JS re-renders
if (!window.__KP_HIDE_LEGACY_HIST__) {
const obs = new MutationObserver(() => { try { nuke(); } catch(_) {} });
obs.observe(document.body, { childList: true, subtree: true });
window.__KP_HIDE_LEGACY_HIST__ = obs;
}
})();
/*
// — Slim Chip Bar v2: keep only “emoji + player name”; remove solo + shooter/8-ball chips
(function kpSlimChipbar(){
function hasName(t){
// keep only if there is some letter/number (EN + Thai + digits)
return /[A-Za-z\u0E00-\u0E7F0-9]/u.test((t || ”).trim());
}
function containsEmoji(ch, emoji){
return Array.from(ch.querySelectorAll(‘img.emoji’))
.some(img => (img.getAttribute(‘alt’) || ”) === emoji);
}
function shouldRemove(ch){
// Remove any shooter chip or any 8-ball chip
if (containsEmoji(ch, ‘🎯’)) return true; // e.g., “🎯 bill”
if (containsEmoji(ch, ‘🎱’)) return true; // drop all 8-ball chips
// Remove solo emoji chips (✅/❌/⚫/💥 etc. with NO name)
if (!hasName(ch.textContent)) return true;
// Keep “emoji + name”: e.g., “✅ ben”, “❌ bill (-1)”, “⚫ ben (+1)”
return false;
}
function filter(){
const bar = document.getElementById(‘kp-chipbar’);
if (!bar) return;
bar.querySelectorAll(‘.kp-chip’).forEach(ch => { if (shouldRemove(ch)) ch.remove(); });
}
try { filter(); } catch(_){}
// Re-apply whenever chips are updated
try {
const bar = document.getElementById(‘kp-chipbar’);
if (bar && !bar.__kpSlimObs){
const obs = new MutationObserver(() => { try { filter(); } catch(_) {} });
obs.observe(bar, { childList: true, subtree: true, characterData: true });
bar.__kpSlimObs = obs;
}
} catch(_){}
})();
*/
const MAX_CHIPS=24;
const EMOJI={ pot:’✅’, miss:’❌’, black:’⚫’, elim:’💥’, winner:’👑’, turn:’🎯’ };
const idOf = p => p?.id ?? p?.player_id ?? p?.name ?? null;
const nameOf = p => p?.display_name || p?.name || (idOf(p) != null ? (‘#’+idOf(p)) : ‘Player’);
const lives = p => Number(p?.regular_lives||0)+Number(p?.black_ball_lives||0);
// ———- precise delta logic ———-
let primed=false;
let prevPlayers = new Map(); // id -> snapshot player
let prevShooter = null;
let prevFinished = false;
let prevWinnerName = null;
function mapById(arr){
const m=new Map();
(arr||[]).forEach(p=>{ const id=idOf(p); if(id!=null) m.set(String(id), p); });
return m;
}
function computeEvents(state){
const out=[];
const currList = state?.players || state?.participants || [];
const curr = mapById(currList);
// Shooter change — only if shooter id changed
const curShooterId = idOf(state?.current_player);
if (primed && curShooterId != null && curShooterId !== prevShooter) {
const sp = curr.get(String(curShooterId)) || state.current_player;
out.push({ type:’turn’, name: nameOf(sp), delta:null });
}
// Eliminations — existed before, missing now
if (primed) {
for (const [id, pPrev] of prevPlayers) {
if (!curr.has(id)) out.push({ type:’elim’, name: nameOf(pPrev), delta:null });
}
}
// Lives deltas — only when total lives changed
if (primed) {
for (const [id, pCur] of curr) {
const pPrev = prevPlayers.get(id);
if (!pPrev) continue; // new joins: no chip
const d = lives(pCur) – lives(pPrev);
if (d !== 0) out.push({ type: d>0 ? ‘black’ : ‘miss’, name: nameOf(pCur), delta:d });
}
}
// Winner — only when event transitions to finished or winner changes
const isFinished = (state?.event?.status === ‘finished’);
const winName = state?.winner?.name || null;
if (primed && ( (isFinished && !prevFinished) || (winName && winName !== prevWinnerName) )) {
out.push({ type:’winner’, name: winName || ‘Winner’, delta:null });
}
// update snapshots for next tick
prevPlayers = curr;
prevShooter = curShooterId ?? prevShooter;
prevFinished = isFinished;
prevWinnerName = winName;
// First tick primes without emitting any chips
if (!primed) { primed = true; return []; }
return out;
}
function renderChips(evts){
if (!evts.length) return;
ensureBar();
const bar = document.getElementById(‘kp-chipbar’); if (!bar) return;
const frag=document.createDocumentFragment();
// keep existing, clamp to MAX_CHIPS
const keep = Array.from(bar.children).slice(-MAX_CHIPS);
bar.innerHTML=”; keep.forEach(el=>frag.appendChild(el));
for (const e of evts){
const chip=document.createElement(‘div’); chip.className=’kp-chip’;
const delta = (e.delta==null||e.delta===0)? ” : (e.delta>0?`(+${e.delta})`:`(${e.delta})`);
chip.textContent = `${EMOJI[e.type]||’•’} ${e.name} ${delta}`.trim();
frag.appendChild(chip);
}
bar.appendChild(frag);
}
function onState(state){
const events = computeEvents(state);
renderChips(events);
}
// single listener
window.addEventListener(‘kp:state’, function(ev){
const flat = flatten(ev.detail||{}) || window.__kpLastState || null;
if (!flat) return;
// onState(flat); // DISABLED: Conflicting EMOJI chip system removed to allow CHIP HISTORY to be the only active system
});
// first paint primes from any existing state
publish(window.__kpLastState||null);
})();
</script>
EOT;
echo ‘</div>’;
echo ‘</div>’;
}
// Complete Tournament Deletion System
function kp_complete_delete_event($event_id) {
global $wpdb;
$events_table = $wpdb->prefix . ‘kp_events’;
$players_table = $wpdb->prefix . ‘kp_players’;
$actions_table = $wpdb->prefix . ‘kp_player_actions’;
// Get event data before deletion
$event = $wpdb->get_row($wpdb->prepare(“SELECT * FROM $events_table WHERE id = %d”, $event_id), ARRAY_A);
if (!$event) return false;
// Delete in proper order (cascading delete)
// 1. Delete player actions (for referential integrity)
$wpdb->delete($actions_table, [‘event_id’ => $event_id]);
// 2. Delete players
$wpdb->delete($players_table, [‘event_id’ => $event_id]);
// 3. Delete event record
$wpdb->delete($events_table, [‘id’ => $event_id]);
return $event; // Return event data for cleanup
}
function kp_complete_delete_event_complete($event_id, $delete_pages = false) {
$event = kp_complete_delete_event($event_id);
if (!$event) return false;
// Optionally delete cloned pages
if ($delete_pages) {
$pages_to_delete = [$event[‘admin_page_id’], $event[‘public_page_id’]];
foreach ($pages_to_delete as $page_id) {
if ($page_id) {
wp_delete_post($page_id, true); // True = permanent delete
}
}
}
return true;
}
// Enhanced All Events Page with Complete Delete Management Tools
function kp_complete_all_page() {
global $wpdb;
$table = $wpdb->prefix . ‘kp_events’;
// Handle individual delete – separate from bulk delete
if ($_POST[‘delete_single_event’] ?? false) {
$event_id = intval($_POST[‘event_id’] ?? 0);
if (!$event_id || !wp_verify_nonce(($_POST[‘_wpnonce’] ?? ”), ‘delete_single_’ . $event_id)) {
echo ‘<div class=”notice notice-error”><p>❌ Security check failed – link has expired. Please refresh the page and try again.</p></div>’;
return; // Prevent further processing
}
$delete_pages = isset($_POST[‘delete_pages’]);
// Get event info before deletion
$event = $wpdb->get_row($wpdb->prepare(“SELECT * FROM $table WHERE id = %d”, $event_id), ARRAY_A);
if (!$event) {
echo ‘<div class=”notice notice-error”><p>❌ Tournament not found.</p></div>’;
} elseif ($event[‘status’] === ‘active’) {
echo ‘<div class=”notice notice-error”><p>❌ Cannot delete active tournament. Close it first.</p></div>’;
} else {
if (kp_complete_delete_event_complete($event_id, $delete_pages)) {
$message = ‘✅ Tournament “‘ . esc_html($event[‘name’]) . ‘” deleted successfully’;
if ($delete_pages) {
$message .= ‘ (including WordPress pages)’;
}
echo ‘<div class=”notice notice-success”><p>’ . $message . ‘</p></div>’;
} else {
echo ‘<div class=”notice notice-error”><p>❌ Failed to delete tournament.</p></div>’;
}
}
}
// Handle bulk apply actions
if (!empty($_POST[‘bulk_apply’]) || !empty($_POST[‘bulk_delete’])) {
KP_LOG(‘Bulk delete POST received: ‘ . print_r($_POST, true));
KP_LOG(‘Selected events: ‘ . print_r($_POST[‘selected_events’] ?? [], true));
if (empty($_POST[‘bulk_delete_nonce’]) || !wp_verify_nonce($_POST[‘bulk_delete_nonce’], ‘bulk_delete’)) {
echo ‘<div class=”notice notice-error”><p>❌ Security check failed. Please try again.</p></div>’;
return; // STOP PROCESSING ON NONCE FAILURE
}
// Accept only the intended field name
$selected_events = array_map(‘intval’, (array)($_POST[‘selected_events’] ?? []));
$selected_events = array_values(array_filter($selected_events));
$bulk_action = sanitize_text_field($_POST[‘bulk_action’] ?? ”);
if (empty($selected_events)) {
echo ‘<div class=”notice notice-warning”><p>⚠️ No tournaments selected for ‘ . esc_html($bulk_action) . ‘ action.</p></div>’;
} elseif ($bulk_action !== ‘delete’ && $bulk_action !== ‘delete_with_pages’) {
echo ‘<div class=”notice notice-warning”><p>⚠️ Please choose a valid bulk action.</p></div>’;
} else {
$delete_pages = ($bulk_action === ‘delete_with_pages’);
$success_count = 0;
$errors = [];
foreach ($selected_events as $event_id) {
// Skip if active tournament (must close first)
$event = $wpdb->get_row($wpdb->prepare(“SELECT * FROM $table WHERE id = %d”, $event_id), ARRAY_A);
if (!$event) {
$errors[] = ‘Tournament ID ‘ . $event_id . ‘ not found’;
continue;
}
if ($event[‘status’] === ‘active’) {
$errors[] = $event[‘name’] . ‘ (active – must close first)’;
continue;
}
if (kp_complete_delete_event_complete($event_id, $delete_pages)) {
$success_count++;
} else {
$errors[] = $event[‘name’];
}
}
if ($success_count > 0) {
$message = ‘✅ Successfully ‘ . ($delete_pages ? ‘deleted ‘ . $success_count . ‘ tournament’ . ($success_count !== 1 ? ‘s’ : ”) . ‘ (including WordPress pages)’ : ‘removed ‘ . $success_count . ‘ tournament’ . ($success_count !== 1 ? ‘s’ : ”) . ‘ (pages preserved)’);
echo ‘<div class=”notice notice-success”><p>’ . $message . ‘</p></div>’;
}
if (!empty($errors)) {
echo ‘<div class=”notice notice-error”><p>❌ Failed to delete: ‘ . implode(‘, ‘, $errors) . ‘</p></div>’;
}
}
}
// Handle mass cleanup
if ($_POST[‘mass_cleanup’] ?? false) {
check_admin_referer(‘mass_cleanup’);
$cleanup_type = sanitize_text_field($_POST[‘cleanup_type’] ?? ‘test’);
if ($cleanup_type === ‘all’) {
// Close ALL tournaments
$wpdb->update($table, [‘status’ => ‘closed’], [‘status’ => ‘active’]);
$count = $wpdb->rows_affected;
echo ‘<div class=”notice notice-success”><p>✅ Closed all ‘ . $count . ‘ active tournaments!</p></div>’;
} elseif ($cleanup_type === ‘test’) {
// Close tournaments with “test” in name or demo events
$test_events = $wpdb->get_results(“SELECT id FROM $table WHERE (name LIKE ‘%test%’ OR name LIKE ‘%demo%’ OR slug LIKE ‘%test%’ OR slug LIKE ‘%demo%’) AND status = ‘active'”, ARRAY_A);
if (!empty($test_events)) {
$test_ids = array_column($test_events, ‘id’);
$placeholders = implode(‘,’, array_fill(0, count($test_ids), ‘%d’));
$wpdb->query($wpdb->prepare(“UPDATE $table SET status = ‘closed’ WHERE id IN ($placeholders)”, …$test_ids));
echo ‘<div class=”notice notice-success”><p>✅ Closed ‘ . count($test_ids) . ‘ test tournaments!</p></div>’;
} else {
echo ‘<div class=”notice notice-info”><p>ℹ️ No test tournaments found to close.</p></div>’;
}
}
}
// Handle individual game close
if ($_POST[‘close_game’] ?? false) {
check_admin_referer(‘close_game’);
$event_id = intval($_POST[‘event_id’]);
$wpdb->update($table, [‘status’ => ‘closed’], [‘id’ => $event_id]);
echo ‘<div class=”notice notice-success”><p>✅ Tournament closed successfully!</p></div>’;
}
// Fix: Remove the old bulk form structure that was causing conflicts
$events = $wpdb->get_results(“SELECT e.*, COUNT(p.id) as player_count FROM $table e LEFT JOIN {$wpdb->prefix}kp_players p ON e.id = p.event_id GROUP BY e.id ORDER BY e.created_at DESC”, ARRAY_A);
// Count active tournaments for higher tour count warning
$active_count = $wpdb->get_var(“SELECT COUNT(*) FROM $table WHERE status = ‘active'”);
echo ‘<div class=”wrap”>’;
echo ‘<h1>🎱 All Tournaments</h1>’;
if ($active_count > 10) {
echo ‘<div class=”notice notice-warning” style=”border-left-color: #f59e0b;”>’;
echo ‘<p><strong>⚠️ High Active Tournament Count:</strong> You have ‘ . $active_count . ‘ active tournaments. This affects the Live Killer menu count. Consider closing test tournaments.</p>’;
echo ‘</div>’;
}
if (empty($events)) {
echo ‘<div style=”text-align: center; padding: 60px; background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);”>’;
echo ‘<p style=”font-size: 20px; color: #6b7280; margin-bottom: 20px;”>No tournaments yet!</p>’;
echo ‘<a href=”‘ . admin_url(‘admin.php?page=killer-pool-create-complete’) . ‘” class=”button button-primary” style=”padding: 15px 30px; font-size: 16px; height: auto;”>🎯 Create First Tournament</a>’;
echo ‘</div>’;
} else {
// Toolbar with mass actions
echo ‘<div style=”display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; background: white; padding: 20px; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);”>’;
echo ‘<div>’;
echo ‘<a href=”‘ . admin_url(‘admin.php?page=killer-pool-create-complete’) . ‘” class=”button button-primary”>➕ Create New Tournament</a>’;
echo ‘</div>’;
// Mass cleanup tools – no form nesting
echo ‘<div>’;
echo ‘<form method=”post” onsubmit=”return confirm(\’Are you sure you want to close multiple tournaments?\’);”>’;
wp_nonce_field(‘mass_cleanup’);
echo ‘<select name=”cleanup_type” style=”margin-right: 10px; padding: 6px 10px; border-radius: 6px; border: 1px solid #ccc;”>’;
echo ‘<option value=”test”>Close Test Tournaments</option>’;
echo ‘<option value=”all”>Close ALL Active Tournaments</option>’;
echo ‘</select>’;
echo ‘<input type=”submit” name=”mass_cleanup” value=”🧹 Mass Cleanup” class=”button button-secondary” style=”padding: 6px 15px;”>’;
echo ‘</form>’;
echo ‘</div>’;
echo ‘</div>’;
// ======= BULK DELETE FORM – SEPARATE FROM TABLE =======
echo ‘<form id=”kp-bulk-form” method=”post” action=”” onsubmit=”return kp_confirmBulkDelete();”>’;
wp_nonce_field(‘bulk_delete’, ‘bulk_delete_nonce’);
echo ‘<input type=”hidden” name=”bulk_delete” value=”1″>’;
// Bulk actions toolbar
echo ‘<div style=”background: #f8f9fa; padding: 15px; margin-bottom: 15px; border-radius: 8px; border: 2px solid #e9ecef;”>’;
echo ‘<div style=”display: flex; align-items: center; gap: 15px;”>’;
echo ‘<select name=”bulk_action” id=”bulk_action_selector” form=”kp-bulk-form” style=”margin-right: 10px; padding: 6px 10px; border-radius: 6px; border: 1px solid #ccc;”>’;
echo ‘<option value=””>Bulk Actions</option>’;
echo ‘<option value=”delete”>🗑️ DELETE SELECTED</option>’;
echo ‘<option value=”delete_with_pages”>🚫 DELETE WITH PAGES</option>’;
echo ‘</select>’;
echo ‘<button type=”submit” name=”bulk_apply” form=”kp-bulk-form” class=”button button-primary” style=”padding: 6px 15px;”>Apply</button>’;
echo ‘</div>’;
// Page deletion options
echo ‘<div style=”margin-top: 12px; font-size: 12px; color: #6b7280;”>’;
echo ‘<strong>🗑️ DELETE:</strong> Removes tournament data only (keeps WordPress pages)<br>’;
echo ‘<strong>🚫 DELETE WITH PAGES:</strong> Removes tournament data AND cloned WordPress pages’;
echo ‘</div>’;
echo ‘</div>’;
echo ‘</form>’; // Close bulk form BEFORE table
// ======= TOURNAMENT TABLE – OUTSIDE BULK FORM =======
echo ‘<table class=”wp-list-table widefat fixed striped” style=”background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1);”>’;
echo ‘<thead><tr>’;
echo ‘<th style=”width: 40px;”><input type=”checkbox” id=”select_all” onclick=”kp_toggleAllCheckboxes(this)”></th>’;
echo ‘<th>Tournament Details</th><th>Status</th><th>Players</th><th>Pages</th><th>Created</th><th>Actions</th>’;
echo ‘</tr></thead>’;
echo ‘<tbody>’;
foreach ($events as $event) {
$status_color = $event[‘status’] === ‘active’ ? ‘#10b981’ : ‘#6b7280’;
$status_bg = $event[‘status’] === ‘active’ ? ‘#f0fdf4’ : ‘#f9fafb’;
echo ‘<tr>’;
echo ‘<td style=”text-align: center;”>’;
if ($event[‘status’] === ‘closed’) { // Only allow deletion of closed tournaments
echo ‘<input type=”checkbox”
name=”selected_events[]”
value=”‘ . $event[‘id’] . ‘”
form=”kp-bulk-form”
class=”tournament-checkbox”>’;
} else {
echo ‘<span style=”color: #ccc;” title=”Must close tournament before deletion”>🔒</span>’;
}
echo ‘</td>’;
echo ‘<td>’;
echo ‘<strong>’ . esc_html($event[‘name’]) . ‘</strong><br>’;
echo ‘<small style=”color: #6b7280;”>Slug: ‘ . esc_html($event[‘slug’]) . ‘</small><br>’;
echo ‘<small style=”color: #6b7280;”>Venue: ‘ . esc_html($event[‘venue’]) . ‘</small>’;
echo ‘</td>’;
echo ‘<td>’;
echo ‘<span style=”background: ‘ . $status_bg . ‘; color: ‘ . $status_color . ‘; padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 600; text-transform: uppercase;”>’;
echo esc_html($event[‘status’]);
echo ‘</span><br>’;
echo ‘<small style=”color: #6b7280;”>’ . esc_html($event[‘game_state’]) . ‘</small>’;
echo ‘</td>’;
echo ‘<td style=”text-align: center;”>’;
echo ‘<div style=”font-size: 18px; font-weight: 600; color: #1e40af;”>’ . intval($event[‘player_count’]) . ‘</div>’;
echo ‘<div style=”font-size: 12px; color: #6b7280;”>players</div>’;
echo ‘</td>’;
echo ‘<td>’;
if ($event[‘public_page_id’]) {
echo ‘<a href=”‘ . get_permalink($event[‘public_page_id’]) . ‘” target=”_blank” style=”display: inline-block; margin: 2px; padding: 4px 8px; background: #3b82f6; color: white; text-decoration: none; border-radius: 4px; font-size: 11px;”>📺 PUBLIC</a><br>’;
}
if ($event[‘admin_page_id’]) {
echo ‘<a href=”‘ . get_permalink($event[‘admin_page_id’]) . ‘” target=”_blank” style=”display: inline-block; margin: 2px; padding: 4px 8px; background: #059669; color: white; text-decoration: none; border-radius: 4px; font-size: 11px;”>📱 ADMIN</a>’;
}
echo ‘</td>’;
echo ‘<td style=”font-size: 14px; color: #6b7280;”>’ . date(‘M j, Y’, strtotime($event[‘created_at’])) . ‘</td>’;
echo ‘<td>’;
// ACTIONS – ALL INDIVIDUAL FORMS COMPLETELY SEPARATE
echo ‘<a href=”‘ . add_query_arg([‘page’ => ‘killer-pool-complete’, ‘event’ => $event[‘slug’]], admin_url(‘admin.php’)) . ‘” class=”button button-primary button-small” style=”margin: 2px;”>⚙️ MANAGE</a><br>’;
// Close tournament form (completely separate)
if ($event[‘status’] === ‘active’) {
echo ‘<form method=”post” style=”display: inline-block; margin: 2px;”>’;
wp_nonce_field(‘close_game’);
echo ‘<input type=”hidden” name=”close_game” value=”1″>’;
echo ‘<input type=”hidden” name=”event_id” value=”‘ . $event[‘id’] . ‘”>’;
echo ‘<button type=”submit” class=”button button-secondary button-small” onclick=”return confirm(\’Close tournament: ‘ . esc_js($event[‘name’]) . ‘?\’);”>🔒 CLOSE</button>’;
echo ‘</form><br>’;
}
// Individual delete forms (completely separate)
if ($event[‘status’] === ‘closed’) {
echo ‘<div style=”display:block;margin-top:6px”>’;
// DELETE (no pages)
echo ‘<form method=”post” style=”display: inline-block; margin-right: 8px;” onsubmit=”return kp_confirmDelete(\” . esc_js($event[‘name’]) . ‘\’);”>’;
wp_nonce_field(‘delete_single_’ . $event[‘id’]);
echo ‘<input type=”hidden” name=”delete_single_event” value=”1″>’;
echo ‘<input type=”hidden” name=”event_id” value=”‘ . $event[‘id’] . ‘”>’;
echo ‘<button type=”submit” class=”button button-danger button-small” style=”background:#dc2626;color:white;”>🗑 DELETE</button>’;
echo ‘</form>’;
// DELETE WITH PAGES
echo ‘<form method=”post” style=”display: inline-block;” onsubmit=”return kp_confirmDelete(\” . esc_js($event[‘name’]) . ‘ + pages\’);”>’;
wp_nonce_field(‘delete_single_’ . $event[‘id’]);
echo ‘<input type=”hidden” name=”delete_single_event” value=”1″>’;
echo ‘<input type=”hidden” name=”event_id” value=”‘ . $event[‘id’] . ‘”>’;
echo ‘<input type=”hidden” name=”delete_pages” value=”1″>’;
echo ‘<button type=”submit” class=”button button-danger button-small” style=”background:#7f1d1d;color:white;”>🗑 DELETE + PAGES</button>’;
echo ‘</form>’;
echo ‘</div>’;
}
echo ‘</td>’;
echo ‘</tr>’;
}
echo ‘</tbody></table>’;
// Bulk delete completion – Enhanced JavaScript
echo ‘<script>
function kp_toggleAllCheckboxes(source) {
const checkboxes = document.querySelectorAll(“.tournament-checkbox”);
checkboxes.forEach(cb => cb.checked = source.checked);
kp_updateSelectionCount();
}
function kp_confirmBulkDelete() {
const selected = document.querySelectorAll(“.tournament-checkbox:checked”).length;
const action = document.getElementById(“bulk_action_selector”).value;
if (selected === 0) {
alert(“Please select tournaments to delete.”);
return false;
}
if (!action) {
alert(“Please select an action.”);
return false;
}
const pages = action === “delete_with_pages” ? ” + associated WordPress pages” : “”;
const message = “⚠️ PERMANENTLY DELETE ” + selected + ” tournament” + (selected > 1 ? “s” : “”) + pages + “?\n\nThis action cannot be undone!”;
return confirm(message);
}
function kp_confirmDelete(tournamentName) {
const message = “⚠️ PERMANENTLY DELETE \”” + tournamentName + “\”?\n\nThis action cannot be undone!”;
return confirm(message);
}
// Enhanced checkbox management
document.addEventListener(“DOMContentLoaded”, function() {
const checkboxes = document.querySelectorAll(“.tournament-checkbox”);
const mainCheckbox = document.getElementById(“select_all”);
// Initialize checkbox states
kp_updateSelectionCount();
// Add event listeners to individual checkboxes
checkboxes.forEach(cb => {
cb.addEventListener(“change”, kp_updateSelectionCount);
});
// Add event listener to main checkbox
if (mainCheckbox) {
mainCheckbox.addEventListener(“change”, function() {
kp_toggleAllCheckboxes(this);
});
}
});
function kp_updateSelectionCount() {
const selected = document.querySelectorAll(“.tournament-checkbox:checked”).length;
const mainCheckbox = document.getElementById(“select_all”);
const allCheckboxes = document.querySelectorAll(“.tournament-checkbox”);
if (!mainCheckbox) return;
// Update main checkbox state
if (selected === 0) {
mainCheckbox.indeterminate = false;
mainCheckbox.checked = false;
} else if (selected === allCheckboxes.length) {
mainCheckbox.indeterminate = false;
mainCheckbox.checked = true;
} else {
mainCheckbox.indeterminate = true;
mainCheckbox.checked = false;
}
// Update bulk action selector state
const bulkSelector = document.getElementById(“bulk_action_selector”);
if (bulkSelector) {
bulkSelector.disabled = (selected === 0);
}
}
</script>’;
// Status summary
$active_tournaments = array_filter($events, function($e) { return $e[‘status’] === ‘active’; });
$closed_tournaments = array_filter($events, function($e) { return $e[‘status’] === ‘closed’; });
echo ‘<div style=”display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 20px;”>’;
echo ‘<div style=”background: #f0fdf4; border: 2px solid #10b981; padding: 15px; border-radius: 8px;”>’;
echo ‘<h4 style=”margin: 0 0 10px 0; color: #059669;”>✅ Active Tournaments</h4>’;
echo ‘<div style=”font-size: 24px; font-weight: 700; color: #10b981;”>’ . count($active_tournaments) . ‘</div>’;
if (count($active_tournaments) > 0) {
echo ‘<small style=”color: #6b7280;”>Showing in Live Killer menu</small>’;
}
echo ‘</div>’;
echo ‘<div style=”background: #f9fafb; border: 2px solid #6b7280; padding: 15px; border-radius: 8px;”>’;
echo ‘<h4 style=”margin: 0 0 10px 0; color: #6b7280;”>📁 Closed Tournaments</h4>’;
echo ‘<div style=”font-size: 24px; font-weight: 700; color: #6b7280;”>’ . count($closed_tournaments) . ‘</div>’;
if (count($closed_tournaments) > 0) {
echo ‘<small style=”color: #6b7280;”>Archived tournaments</small>’;
}
echo ‘</div>’;
echo ‘</div>’;
}
echo ‘</div>’;
}
// Database Helper Functions
function kp_complete_get_event($slug) {
global $wpdb;
$table = $wpdb->prefix . ‘kp_events’;
return $wpdb->get_row($wpdb->prepare(“SELECT * FROM $table WHERE slug = %s”, $slug), ARRAY_A);
}
function kp_event_id_from_slug($slug) {
$event = kp_complete_get_event($slug);
return $event ? $event[‘id’] : false;
}
function kp_complete_create_event($slug, $name, $venue = ”) {
global $wpdb;
$table = $wpdb->prefix . ‘kp_events’;
$result = $wpdb->insert($table, [
‘slug’ => $slug,
‘name’ => $name,
‘venue’ => $venue,
‘starting_lives’ => 3, // Default starting lives
‘max_lives’ => 5, // Default total max lives (hard cap)
‘status’ => ‘active’
]);
return $result ? kp_complete_get_event($slug) : false;
}
function kp_complete_update_event($event_id, $data) {
global $wpdb;
$table = $wpdb->prefix . ‘kp_events’;
// Validation: if updating max_lives or starting_lives, ensure starting_lives <= max_lives
if (isset($data[‘max_lives’]) || isset($data[‘starting_lives’])) {
$current = $wpdb->get_row($wpdb->prepare(“SELECT starting_lives, max_lives FROM $table WHERE id = %d”, $event_id), ARRAY_A);
$new_starting = $data[‘starting_lives’] ?? intval($current[‘starting_lives’] ?? 3);
$new_max = $data[‘max_lives’] ?? intval($current[‘max_lives’] ?? 5);
if ($new_max < $new_starting) {
// Adjust starting_lives to match max_lives
$data[‘starting_lives’] = $new_max;
KP_LOG(“Update: Adjusted starting_lives to {$new_max} to match max_lives for event {$event_id}”);
}
}
return $wpdb->update($table, $data, [‘id’ => $event_id]);
}
function kp_complete_get_players($event_id) {
global $wpdb;
$table = $wpdb->prefix . ‘kp_players’;
// Get all players (active/inactive), calculate total lives from regular + black ball lives
$sql = $wpdb->prepare(
“SELECT *, (regular_lives + black_ball_lives) as lives FROM $table WHERE event_id = %d ORDER BY order_joined ASC”,
$event_id
);
$players = $wpdb->get_results($sql, ARRAY_A);
return array_values($players);
}
function kp_complete_get_player($player_id) {
global $wpdb;
$table = $wpdb->prefix . ‘kp_players’;
return $wpdb->get_row($wpdb->prepare(“SELECT * FROM $table WHERE id = %d”, $player_id), ARRAY_A);
}
// Check if a player was manually removed (is_active=0 but still has lives)
function kp_complete_was_manually_removed($player_id) {
$player = kp_complete_get_player($player_id);
// Manually removed players have is_active=0 but lives>0
// Auto-eliminated players have is_active=0 and lives=0
return $player && intval($player[‘is_active’]) === 0 && ((intval($player[‘regular_lives’] ?? 0) + intval($player[‘black_ball_lives’] ?? 0)) > 0);
}
function kp_complete_add_player($event_id, $name, $starting_lives_param = null) {
global $wpdb;
$table = $wpdb->prefix . ‘kp_players’;
$events_table = $wpdb->prefix . ‘kp_events’;
// Fetch event to get starting_lives
$event = $wpdb->get_row($wpdb->prepare(“SELECT starting_lives FROM $events_table WHERE id = %d”, $event_id), ARRAY_A);
$starting_lives = $starting_lives_param ?? intval($event[‘starting_lives’] ?? 3);
// Check for duplicate name
$check = kp_assert_unique_player_name($wpdb, $event_id, $name);
if (is_wp_error($check)) return $check;
// sanitize
$name = trim( wp_strip_all_tags( (string)$name ) );
if ($name === ”) {
return new WP_Error(‘kp_bad_name’, ‘Player name required’, [‘status’ => 400]);
}
// Find any ACTIVE existing player for this name in this event (CASE-INSENSITIVE)
// Use LOWER() match so it works regardless of table collation.
// NO REACTIVATION: We ONLY check for active players and allow reusing names of removed (inactive) players
$existing = $wpdb->get_row(
$wpdb->prepare(
“SELECT * FROM $table WHERE event_id = %d AND LOWER(display_name) = LOWER(%s) AND is_active = 1 LIMIT 1”,
$event_id, $name
),
ARRAY_A
);
if ($existing) {
// Active player with this name already exists – block duplicate
return new WP_Error(‘kp_exists’, ‘Player already in this event’, [‘status’ => 409]);
}
// No active player with this name exists – insert a new one (even if there are inactive players with the same name)
$order = (int) $wpdb->get_var($wpdb->prepare(
“SELECT COALESCE(MAX(order_joined),0)+1 FROM $table WHERE event_id=%d”, $event_id
));
$player_number = (int) $wpdb->get_var($wpdb->prepare(
“SELECT COALESCE(MAX(player_number),0)+1 FROM $table WHERE event_id=%d”, $event_id
));
$ins = $wpdb->insert(
$table,
[
‘event_id’ => (int)$event_id,
‘display_name’ => $name,
‘regular_lives’ => (int)$starting_lives,
‘black_ball_lives’=> 0,
‘total_black_balls’ => 0, // Explicitly set to 0 to avoid NULL
‘lives’ => (int)$starting_lives,
‘is_active’ => 1,
‘player_number’ => $player_number,
‘current_pool’ => 1,
‘order_joined’ => $order,
],
[ ‘%d’,’%s’,’%d’,’%d’,’%d’,’%d’,’%d’,’%d’,’%d’,’%d’,’%d’ ]
);
if ($ins) {
$player_id = (int)$wpdb->insert_id;
return $player_id;
} else {
return new WP_Error(‘kp_insert_failed’,’Could not add player’, [‘status’=>500]);
}
}
function kp_complete_update_player($player_id, $data) {
global $wpdb;
$table = $wpdb->prefix . ‘kp_players’;
// Remove ‘lives’ from data if present, as it’s computed in queries
unset($data[‘lives’]);
$result = $wpdb->update($table, $data, [‘id’ => $player_id]);
return $result;
}
function kp_complete_reset_event($event_id) {
$event_id = (int)$event_id;
global $wpdb;
$table = $wpdb->prefix . ‘kp_players’;
$events_table = $wpdb->prefix . ‘kp_events’;
// Fetch event starting_lives
$event = $wpdb->get_row($wpdb->prepare(“SELECT starting_lives FROM $events_table WHERE id = %d”, $event_id), ARRAY_A);
$starting_lives = intval($event[‘starting_lives’] ?? 3);
// Reset active players only to starting_lives regular lives, 0 black ball lives, 0 total_black_balls
// Do NOT reactivate previously removed players (is_active = 0)
$wpdb->update(
$table,
[
‘regular_lives’ => $starting_lives,
‘black_ball_lives’ => 0,
‘total_black_balls’ => 0, // Reset black ball count
// ‘lives’ column is legacy; all logic computes lives from regular + black
‘current_pool’ => 1 // Reset everyone to Pool A for the new game
],
[
‘event_id’ => $event_id,
‘is_active’ => 1,
]
);
// Reset event to waiting state AND clear timer
$reset_result = $wpdb->update($events_table, [
‘game_state’ => ‘waiting’,
‘current_player_id’ => 0,
‘timer_start_time’ => 0, // Clear timer state on reset
], [‘id’ => $event_id]);
// Clear winner banner on reset
delete_option(‘kp_winner_’ . $event_id);
// === KP-GOV: start a fresh epoch and stamp MATCH_RESET there (not in the old epoch) ===
// Only runs if the Governor plugin is active.
if (function_exists(‘kpgov_epoch_bump’)) {
// create/advance to a new epoch
$new_epoch = kpgov_epoch_bump($event_id);
// first event in that new epoch — human-readable reset marker
if (function_exists(‘kpgov_ledger_log’)) {
kpgov_ledger_log($event_id, ‘MATCH_RESET’, [
‘ts’ => time(),
‘epoch’ => $new_epoch,
‘source’ => ‘monolith_reset’
]);
} elseif (function_exists(‘kp_governor_ledger_log’)) {
// fallback to legacy alias if present
kp_governor_ledger_log($event_id, ‘MATCH_RESET’, [
‘ts’ => time(),
‘epoch’ => $new_epoch,
‘source’ => ‘monolith_reset’
]);
}
// clear the ACK pointer for the new epoch so force-next can’t skip ahead
if (function_exists(‘kpgov_key’)) {
update_option(kpgov_key($event_id, ‘ack_turn_seq’), 0, false);
} else {
// very safe fallback if helper not loaded for some reason
update_option(‘kp-gov:’ . $event_id . ‘:ack_turn_seq’, 0, false);
}
}
// === /KP-GOV epoch bump + reset stamp ===
// Hint/pollers nudge (keep yours)
if (function_exists(‘kp_bump_hint’)) {
kp_bump_hint($event_id);
}
// Only do a global cache flush while actively debugging.
// In production, rely on targeted invalidation instead.
if (defined(‘KP_DEBUG’) && KP_DEBUG && function_exists(‘wp_cache_flush’)) {
wp_cache_flush();
}
return $reset_result;
}
// Game State Management Functions
function kp_complete_start_game($event_id) {
global $wpdb;
$events_table = $wpdb->prefix . ‘kp_events’;
// Read current state first so we can detect a fresh start
$prev_state = $wpdb->get_var($wpdb->prepare(
“SELECT game_state FROM $events_table WHERE id = %d”,
$event_id
));
// Set game state to active
$wpdb->update($events_table, [‘game_state’ => ‘active’], [‘id’ => $event_id]);
// === KP-GOV PATCH F: ledger MATCH_STARTED (idempotent per cycle) ===
if (function_exists(‘kp_governor_ledger_log’)) {
// Only log when transitioning from a non-active state
if ($prev_state !== ‘active’) {
kp_governor_ledger_log($event_id, ‘MATCH_STARTED’, array(‘ts’ => time()));
}
}
// === /KP-GOV PATCH F ===
// Select first player (this will also log TURN_SELECTED via Patch C)
return kp_complete_select_next_player($event_id);
}
function kp_complete_select_next_player($event_id) {
global $wpdb;
KP_LOG(“SELECT NEXT PLAYER: Starting for event {$event_id}”);
$players_table = $wpdb->prefix . ‘kp_players’;
$events_table = $wpdb->prefix . ‘kp_events’;
// Admin toggle: final-2 alternation (ON by default)
$alt_final2 = intval(get_option(‘kp_alt_final2_event_’ . $event_id, 1));
// Fresh alive list
$alive_players = $wpdb->get_results($wpdb->prepare(
“SELECT id, display_name, current_pool, is_active,
(regular_lives + black_ball_lives) AS total_lives
FROM $players_table
WHERE event_id = %d AND is_active = 1
AND (regular_lives + black_ball_lives) > 0″,
$event_id
), ARRAY_A);
$alive_count = is_array($alive_players) ? count($alive_players) : 0;
// Final two: optional alternate turns
if ($alive_count === 2 && $alt_final2) {
$event = $wpdb->get_row($wpdb->prepare(
“SELECT id, current_player_id, timer_enabled FROM $events_table WHERE id = %d”,
$event_id
), ARRAY_A);
$choices = array_values($alive_players);
$pick_id = $choices[0][‘id’];
if (!empty($event[‘current_player_id’])) {
if ($choices[0][‘id’] == $event[‘current_player_id’]) {
$pick_id = $choices[1][‘id’];
} else {
$pick_id = $choices[0][‘id’];
}
}
// Use the helper so all callers behave consistently
return kp_complete_set_current_player($event_id, $pick_id);
}
// Normal rotation: MUST exhaust Pool 1 (A) before touching Pool 2 (B)
$pool1 = $wpdb->get_results($wpdb->prepare(
“SELECT * FROM $players_table
WHERE event_id = %d AND is_active = 1
AND (regular_lives + black_ball_lives) > 0
AND current_pool = 1″,
$event_id
), ARRAY_A);
if (!empty($pool1)) {
$selected = $pool1[array_rand($pool1)];
// Move selected to Pool 2 for after their shot
$wpdb->update($players_table, array(‘current_pool’ => 2), array(‘id’ => $selected[‘id’]));
// === KP-GOV PATCH C: ledger TURN_SELECTED (authoritative)
if (function_exists(‘kp_governor_ledger_log’)) {
kp_governor_ledger_log($event_id, ‘TURN_SELECTED’, array(
‘player_id’ => intval($selected[‘id’]),
‘basis’ => ‘AB’,
‘ts’ => time(),
));
}
// === /KP-GOV PATCH C ===
// Set as current using the helper (starts timer if enabled)
return kp_complete_set_current_player($event_id, $selected[‘id’]);
}
// Pool 1 empty → refill from Pool 2 back into Pool 1, then draw
$wpdb->query($wpdb->prepare(
“UPDATE $players_table
SET current_pool = 1
WHERE event_id = %d AND is_active = 1
AND (regular_lives + black_ball_lives) > 0
AND current_pool = 2″,
$event_id
));
// Re-pull Pool 1 after refill
$pool1 = $wpdb->get_results($wpdb->prepare(
“SELECT * FROM $players_table
WHERE event_id = %d AND is_active = 1
AND (regular_lives + black_ball_lives) > 0
AND current_pool = 1″,
$event_id
), ARRAY_A);
if (!empty($pool1)) {
$selected = $pool1[array_rand($pool1)];
// Move selected to Pool 2 for after their shot
$wpdb->update($players_table, array(‘current_pool’ => 2), array(‘id’ => $selected[‘id’]));
// === KP-GOV PATCH C: ledger TURN_SELECTED (authoritative)
if (function_exists(‘kp_governor_ledger_log’)) {
kp_governor_ledger_log($event_id, ‘TURN_SELECTED’, array(
‘player_id’ => intval($selected[‘id’]),
‘basis’ => ‘AB’,
‘ts’ => time(),
));
}
// === /KP-GOV PATCH C ===
return kp_complete_set_current_player($event_id, $selected[‘id’]);
}
// Nobody left – clear current player and timer
$wpdb->update($events_table, [‘current_player_id’ => 0, ‘timer_start_time’ => 0], [‘id’ => $event_id]);
return null;
}
function kp_complete_get_current_player($event_id) {
global $wpdb;
$events_table = $wpdb->prefix . ‘kp_events’;
$players_table = $wpdb->prefix . ‘kp_players’;
$event = $wpdb->get_row($wpdb->prepare(“SELECT * FROM $events_table WHERE id = %d”, $event_id), ARRAY_A);
if (!$event || !$event[‘current_player_id’]) {
return false;
}
$player = $wpdb->get_row($wpdb->prepare(
“SELECT *, (COALESCE(regular_lives, 0) + COALESCE(black_ball_lives, 0)) AS lives FROM $players_table WHERE id = %d”,
$event[‘current_player_id’]
), ARRAY_A);
if (!$player || intval($player[‘is_active’]) !== 1 || intval($player[‘lives’]) <= 0) {
// Clear invalid current player
$wpdb->update($events_table, [‘current_player_id’ => 0, ‘timer_start_time’ => 0], [‘id’ => $event_id]);
return false;
}
return $player;
}
// Minimal helper to set the current player and (re)start timer if enabled.
function kp_complete_set_current_player($event_id, $player_id) {
global $wpdb;
$events_table = $wpdb->prefix . ‘kp_events’;
$players_table = $wpdb->prefix . ‘kp_players’;
$event = $wpdb->get_row($wpdb->prepare(
“SELECT id, timer_enabled FROM $events_table WHERE id = %d”,
$event_id
), ARRAY_A);
$update = array(
‘current_player_id’ => intval($player_id),
‘timer_start_time’ => (!empty($event[‘timer_enabled’])) ? time() : 0
);
$wpdb->update($events_table, $update, array(‘id’ => $event_id), array(‘%d’,’%d’), array(‘%d’));
return $wpdb->get_row($wpdb->prepare(
“SELECT * FROM $players_table WHERE id = %d”,
$player_id
), ARRAY_A);
}
// === KP: Pool Status (unified lives, elim-safe) ===
function kp_complete_get_pool_status( $event_id ) {
global $wpdb;
$players_table = $wpdb->prefix . ‘kp_players’;
// Pull active, alive players ONCE and compute lives consistently
$rows = $wpdb->get_results(
$wpdb->prepare(
“SELECT
id,
event_id,
display_name,
regular_lives,
black_ball_lives,
total_black_balls,
is_active,
player_number,
current_pool,
order_joined,
created_at,
(COALESCE(regular_lives,0) + COALESCE(black_ball_lives,0)) AS lives
FROM {$players_table}
WHERE event_id = %d
AND is_active = 1
AND (COALESCE(regular_lives,0) + COALESCE(black_ball_lives,0)) > 0
ORDER BY player_number ASC, id ASC”,
(int) $event_id
),
ARRAY_A
);
$pool1_players = [];
$pool2_players = [];
foreach ( $rows as $row ) {
// normalize numeric types
$row[‘id’] = (int) $row[‘id’];
$row[‘event_id’] = (int) $row[‘event_id’];
$row[‘regular_lives’] = (int) $row[‘regular_lives’];
$row[‘black_ball_lives’] = (int) $row[‘black_ball_lives’];
$row[‘total_black_balls’]= (int) $row[‘total_black_balls’];
$row[‘is_active’] = (int) $row[‘is_active’];
$row[‘player_number’] = (int) $row[‘player_number’];
$row[‘current_pool’] = (int) $row[‘current_pool’];
$row[‘order_joined’] = (int) $row[‘order_joined’];
$row[‘lives’] = (int) $row[‘lives’];
if ( $row[‘current_pool’] === 1 ) {
$pool1_players[] = $row;
} else {
$pool2_players[] = $row;
}
}
$pool1_count = (string) count($pool1_players); // keep counts as strings to match existing payloads
$pool2_count = (string) count($pool2_players);
$upcoming_pool = count($pool1_players) > 0 ? 1 : (count($pool2_players) > 0 ? 2 : 0);
return [
‘pool1_count’ => $pool1_count,
‘pool2_count’ => $pool2_count,
‘upcoming_pool’ => $upcoming_pool,
‘pool1_players’ => $pool1_players,
‘pool2_players’ => $pool2_players,
];
}
// Front end TV Page shortcode
// Auto-inject shortcode into public master page only (admin uses iframe)
add_filter(‘the_content’, ‘kp_auto_inject_shortcodes’);
function kp_auto_inject_shortcodes($content) {
if (is_admin()) return $content;
$post_id = get_the_ID();
$public_master = get_option(‘kp_public_master_id’, 3209);
// Only inject shortcode into public page – admin page uses iframe
if ($post_id == $public_master) {
if (!strpos($content, ‘
Tournament “master” not found.
}
}
return $content;
}
// Simple shortcode addition to cloned pages
function kp_add_simple_iframe($page_id, $event_slug, $type) {
$page = get_post($page_id);
if (!$page) {
return false;
}
$content = $page->post_content;
// Add the appropriate shortcode for each type
if ($type === ‘public’) {
$shortcode = ‘
Tournament “event_slug” not found.
} else {
$shortcode = ‘
Tournament “event_slug” not found.
}
// Clean any existing shortcodes and add new one
$content = preg_replace(‘/\
Tournament “demo” not found.
$content = trim($content) . “\n\n” . $shortcode;
// Update page
wp_update_post([
‘ID’ => $page_id,
‘post_content’ => $content
]);
// Store metadata
update_post_meta($page_id, ‘_kp_event_slug’, $event_slug);
return true;
}
// Register shortcodes
add_shortcode(‘killer_pool’, ‘kp_complete_public_shortcode’);
add_shortcode(‘killer_pool_admin’, ‘kp_complete_admin_shortcode’);
add_shortcode(‘killer_pool_iframe’, ‘kp_complete_iframe_shortcode’);
// Removed body class addition as inline styles in shortcode provide direct override
// Public TV display shortcode
function kp_complete_public_shortcode($atts) {
$atts = shortcode_atts([‘event’ => ‘demo’], $atts);
$slug = sanitize_title($atts[‘event’]);
// Try to get event slug from page meta if not explicitly provided
if (!$atts[‘event’] || $atts[‘event’] === ‘demo’) {
$post_id = get_the_ID();
if ($post_id) {
$meta_slug = get_post_meta($post_id, ‘_kp_event_slug’, true);
if ($meta_slug) {
$slug = sanitize_title($meta_slug);
}
}
}
$event = kp_complete_get_event($slug);
if (!$event) {
return ‘<div style=”background: #f8f9fa; padding: 20px; border-radius: 8px; text-align: center; border: 2px solid #dee2e6;”><p>Tournament “‘ . esc_html($slug) . ‘” not found.</p></div>’;
}
// Resolve elimination videos for this event
$elim_enabled = get_option(‘kp_elim_enabled_event_’ . $event[‘id’], 0);
$elim_videos_data = [‘enabled’ => $elim_enabled, ‘clips’ => []];
if ($elim_enabled) {
$elim_videos = get_option(‘kp_elim_videos_event_’ . $event[‘id’], []);
foreach ($elim_videos as $video) {
if ($video[‘enabled’]) {
$url = wp_get_attachment_url($video[‘id’]);
if ($url) {
$elim_videos_data[‘clips’][] = $url;
}
}
}
}
$players = kp_complete_get_players($event[‘id’]);
$alive_players = array_filter($players, function($p) { return $p[‘is_active’] && $p[‘lives’] > 0; });
$current_player = kp_complete_get_current_player($event[‘id’]);
$pool_status = kp_complete_get_pool_status($event[‘id’]);
// Prepare JSON-encoded variables BEFORE output buffer
$rest_base_json = wp_json_encode( rest_url(‘killer-pool/v1/’) );
$timer_api_url_json = wp_json_encode( esc_url( rest_url( ‘killer-pool/v1/timer-expired/’ . $slug ) ) );
$wp_rest_nonce = wp_create_nonce(‘wp_rest’);
$wp_rest_nonce_json = wp_json_encode( $wp_rest_nonce );
$current_event_slug_json = wp_json_encode( (string) $slug );
ob_start();
?>
<style>
/* Elimination video overlay styles */
#kp-elim-overlay {
position: fixed;
inset: 0;
background: #000;
display: none;
z-index: 99999;
}
#kp-elim-overlay.is-on {
display: flex;
align-items: center;
justify-content: center;
}
#kp-elim-video {
width: 100vw;
height: 100vh;
object-fit: cover;
}
#kp-elim-video { will-change: transform; transform: translateZ(0); }
body.kp-public-page .elementor-footer, body.kp-public-page #footer, body.kp-public-page .site-footer, body.kp-public-page footer {
display: block !important;
position: static !important;
float: none !important;
clear: both !important;
width: 100% !important;
margin: 0 auto !important;
padding: 20px 0 !important;
text-align: center !important;
justify-content: center !important;
align-items: center !important;
}
body.kp-public-page .elementor-location-footer,
body.kp-public-page [data-elementor-type=”footer”] {
text-align: center !important;
display: flex !important;
justify-content: center !important;
width: 100% !important;
}
body.kp-public-page .elementor-footer *, body.kp-public-page #footer *, body.kp-public-page .site-footer *,
body.kp-public-page .elementor-location-footer *, body.kp-public-page [data-elementor-type=”footer”] * {
text-align: center !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
position: static !important;
}
body.kp-public-page .kp-public-wrapper {
min-height: 600px !important;
}
body.kp-public-page * [style*=”position: sticky”], body.kp-public-page * [style*=”position: fixed”] {
position: static !important;
}
body.kp-public-page, body.kp-public-page .site, body.kp-public-page .entry-content {
overflow-x: hidden !important;
}
body.kp-public-page .entry-content .kp-public-wrapper {
min-height: 600px !important;
display: block !important;
float: none !important;
}
</style>
<div class=”kp-public-wrapper kp-tv-root” style=”background: linear-gradient(135deg, #1e293b 0%, #334155 100%); color: white; padding: 20px; border-radius: 20px; margin: 20px 0; min-height: 600px; position: relative;”>
<!– Elimination video overlay –>
<div id=”kp-elim-overlay” aria-hidden=”true”>
<video id=”kp-elim-video” playsinline preload=”auto”></video>
</div>
<!– TV Header – Compact with LIVE badge in corner –>
<div style=”position: relative; text-align: center; margin-bottom: 20px;”>
<h1 style=”font-size: clamp(1.5rem, 4vw, 2.5rem); margin: 0 0 5px 0; background: linear-gradient(45deg, #fbbf24, #f59e0b); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;”>
🎱 Killer Pool Tournament
</h1>
<h2 style=”margin: 0; color: #e2e8f0; font-size: clamp(1rem, 2.5vw, 1.5rem);”><?php echo esc_html($event[‘name’]); ?></h2>
<!– Fullscreen Toggle Button – Top-right, remote-friendly –>
<button id=”kp-fullscreen-toggle” tabindex=”0″ aria-label=”Toggle full screen” style=”position: absolute; top: 10px; right: 10px; background: rgba(0,0,0,0.7); color: white; border: 2px solid rgba(255,255,255,0.3); border-radius: 50%; width: 50px; height: 50px; cursor: pointer; font-size: 20px; z-index: 10; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease;”>⛶</button>
<!– LIVE badge – top left corner –>
<div style=”position: absolute; top: 0; left: 0; background: #dc2626; display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 15px; font-size: 12px;”>
<span style=”width: 8px; height: 8px; background: white; border-radius: 50%; animation: pulse 2s infinite;”></span>
<span style=”font-weight: 700;”>LIVE</span>
</div>
</div>
<!– Compact Stats Bar –>
<div style=”display: flex; justify-content: space-around; background: rgba(15, 23, 42, 0.8); padding: 15px; border-radius: 12px; margin-bottom: 20px; font-size: 14px;”>
<div style=”text-align: center;”>
<div style=”color: #94a3b8; margin-bottom: 5px;”>Players</div>
<div style=”font-weight: 700;” data-player-count><?php echo count($alive_players); ?> / <?php echo count($players); ?></div>
</div>
<div style=”text-align: center;”>
<div style=”color: #94a3b8; margin-bottom: 5px;”>State</div>
<div style=”font-weight: 700; color: #10b981;” data-game-state>
<?php
if (count($players) === 0) {
echo ‘⚙️ SETUP’;
} elseif (count($alive_players) <= 1 && $event[‘game_state’] === ‘active’) {
echo ‘🏆 FINISHED’;
} elseif ($event[‘game_state’] === ‘waiting’) {
echo ‘⏳ STARTING’;
} else {
echo ‘🎲 ACTIVE’;
}
?>
</div>
</div>
<div style=”text-align: center;”>
<div style=”color: #94a3b8; margin-bottom: 5px;”>Pool 1</div>
<div style=”font-weight: 700;” data-pool1-count><?php echo $pool_status[‘pool1_count’]; ?> left</div>
</div>
</div>
<!– Dynamic content area – will be populated by JavaScript real-time updates –>
<div class=”kp-main-content” style=”margin-bottom: 30px;”>
<?php if ($event[‘game_state’] === ‘waiting’): ?>
<!– Initial loading state – will be replaced by JavaScript –>
<div style=”background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); color: #1f2937; padding: 50px 20px; border-radius: 20px; text-align: center;”>
<div style=”font-size: clamp(2rem, 4vw, 3rem); font-weight: 800; margin-bottom: 20px;”>⏳ Tournament Starting Soon!</div>
<div style=”font-size: clamp(1.2rem, 2.5vw, 1.5rem); margin-bottom: 30px;”>Players are being added…</div>
<div style=”font-size: 1.5rem;”>🎱 🎯 🎪</div>
</div>
<?php if (!empty($players)): ?>
<!– Player tiles in waiting state –>
<div style=”background: rgba(15, 23, 42, 0.9); padding: 20px; border-radius: 15px; border: 2px solid rgba(71, 85, 105, 0.3); margin-top: 20px;”>
<div style=”text-align: center; font-size: 16px; font-weight: 600; margin-bottom: 15px; color: #94a3b8;”>Current Players</div>
<div class=”kp-tile-grid”>
<?php foreach ($players as $player): ?>
<?php if ($player[‘is_active’] == 0) continue; // Only current active players ?>
<?php
$lives = intval($player[‘lives’]);
$regular_lives = intval($player[‘regular_lives’] ?? 0);
$black_ball_lives = intval($player[‘black_ball_lives’] ?? 0);
$is_alive = $lives > 0;
$bg_color = $is_alive ? ‘rgba(15, 23, 42, 0.8)’ : ‘rgba(15, 23, 42, 0.6)’;
$text_color = $is_alive ? ‘#10b981’ : ‘#6b7280’;
$border_color = $is_alive ? ‘rgba(16, 185, 129, 0.3)’ : ‘rgba(107, 114, 128, 0.2)’;
$badge = !$is_alive ? ‘<div style=”background: #dc2626; color: white; padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; margin-top: 4px;”>OUT</div>’ : ”;
// Graphical lives spans
$life_spans = ‘<div class=”lives-display”>’;
for ($j = 0; $j < 5; $j++) {
if ($j < $regular_lives) {
$life_spans .= ‘<span style=”width: 12px; height: 12px; background: #10b981; border-radius: 50%; display: inline-block; margin: 0 2px; box-shadow: 0 0 4px rgba(16, 185, 129, 0.5);”></span>’;
} elseif ($j < $regular_lives + $black_ball_lives) {
$life_spans .= ‘<span style=”width: 12px; height: 12px; background: #1f2937; border: 2px solid #f59e0b; border-radius: 50%; display: inline-block; margin: 0 2px; box-shadow: 0 0 4px rgba(245, 158, 11, 0.5);”></span>’;
} else {
$life_spans .= ‘<span style=”width: 12px; height: 12px; background: #e5e7eb; border-radius: 50%; display: inline-block; margin: 0 2px;”></span>’;
}
}
$life_spans .= ‘</div>’;
?>
<div class=”kp-tile” style=”background: <?php echo $bg_color; ?>; border: 2px solid <?php echo $border_color; ?>;” role=”img” aria-label=”Player <?php echo esc_attr($player[‘display_name’]); ?> with <?php echo $lives; ?> lives” data-player-id=”<?php echo $player[‘id’]; ?>” data-lives=”<?php echo $lives; ?>” data-eliminated=”<?php echo $is_alive ? ‘0’ : ‘1’; ?>” data-pool=”<?php echo intval($player[‘current_pool’] ?? 1); ?>”>
<div class=”kp-eligible-dot”></div>
<div class=”player-name” style=”color: <?php echo $text_color; ?>; font-size: 12px; font-weight: 600; margin-bottom: 4px;”><?php echo esc_html($player[‘display_name’]); ?></div>
<?php echo $life_spans; ?>
<span style=”margin-top: 4px; font-weight: 600; color: <?php echo $text_color; ?>;”><?php echo $lives; ?></span>
<?php echo $badge; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<?php elseif (count($alive_players) > 1 && $current_player): ?>
<!– Initial current player state – will be replaced by JavaScript –>
<div data-current-player style=”background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%); padding: 30px; border-radius: 20px; text-align: center; border: 3px solid rgba(255, 255, 255, 0.2); display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 0;”>
<div style=”background: rgba(255, 255, 255, 0.2); padding: 8px 20px; border-radius: 20px; font-size: 14px; font-weight: 700; letter-spacing: 2px;”>NOW SHOOTING</div>
<div data-player-name style=”font-size: clamp(2rem, 4vw, 3.5rem); font-weight: 800; margin: -2px 0 10px 0; line-height: 0.85;”><?php echo esc_html($current_player[‘display_name’]); ?></div>
<?php
// KP_PATCH:kp_tv_indicators
kp_include_patch(‘kp_tv_indicators’);
?>
<div style=”opacity: 0.9; font-size: 18px;”>Ball #<span data-player-ball><?php echo $current_player[‘player_number’]; ?></span> • <span data-player-lives><?php echo $current_player[‘lives’]; ?></span> lives remaining</div>
</div>
<?php elseif (count($alive_players) > 1 && !$current_player): ?>
<?php /* Legacy banner removed — JS guard handles this now. */ ?>
<?php elseif (is_array($alive_players ?? null) && count($alive_players) === 1): ?>
<!– Winner state –>
<?php
// reindex to ensure [0] exists, then grab safely
$alive_players = array_values($alive_players);
$winner = $alive_players[0] ?? null;
?>
<div id=”kp-winner” style=”background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); color: #1f2937; padding: 50px 20px; border-radius: 20px; text-align: center;”>
<div style=”font-size: clamp(1.5rem, 3vw, 2.5rem); font-weight: 800; margin-bottom: 20px;”>🏆 TOURNAMENT WINNER! 🏆</div>
<div style=”font-size: clamp(2.5rem, 5vw, 4rem); font-weight: 900; margin-bottom: 30px;”>
<?php echo esc_html($winner[‘display_name’] ?? ‘Winner’); ?>
</div>
<div style=”font-size: 2rem; animation: celebrate 3s ease-in-out infinite;”>🎉 🎊 🏆 🎉 🎊</div>
</div>
<?php endif; ?>
</div>
<!– Small Tiles Now Primary (Large Removed) –>
<?php
// KP_PATCH:kp_tiles_css
kp_include_patch(‘kp_tiles_css’);
?>
<style>
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.2); }
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
@keyframes celebrate {
0%, 100% { transform: scale(1) rotate(0deg); }
25% { transform: scale(1.1) rotate(2deg); }
75% { transform: scale(1.1) rotate(-2deg); }
}
/* Small Player Tiles Grid Fix */
.kp-tile-grid {
display: grid;
grid-template-columns: repeat(8, 1fr); /* keep 8 per row on large screens */
gap: 8px;
}
/* Mobile phones */
@media (max-width: 600px) {
.kp-tile-grid {
grid-template-columns: repeat(2, 1fr); /* 2 tiles per row */
}
.kp-tile {
font-size: clamp(14px, 4vw, 20px); /* readable on small screens */
padding: 6px;
}
}
/* Tablets (optional middle ground) */
@media (min-width: 601px) and (max-width: 1024px) {
.kp-tile-grid {
grid-template-columns: repeat(4, 1fr); /* 4 tiles per row on tablets */
}
}
.kp-tile {
min-height: 120px !important;
position: relative; /* Allow absolute positioning of children */
}
.kp-tile-grid .kp-tile {
min-width: 0;
background: rgba(15, 23, 42, 0.8);
border: 2px solid rgba(71, 85, 105, 0.3);
border-radius: 8px;
padding: 25px 12px 8px 12px; /* Increased top padding for higher content position */
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center; /* Center name/lives in available space */
gap: 4px;
width: 100%;
box-sizing: border-box;
min-height: 100px;
}
.kp-tile .player-name {
font-size: 12px;
font-weight: 600;
color: #e2e8f0;
margin-bottom: 4px;
margin-top: -15px; /* Pull up from center */
transform: translateY(-50%); /* Stronger shift up by 50% for visibility */
}
.kp-tile .lives-display {
display: flex;
gap: 2px;
justify-content: center;
margin-top: -15px; /* Pull up from center */
transform: translateY(-50%); /* Stronger shift up by 50% for visibility */
}
.kp-tile .shooting-indicator {
position: absolute;
top: -15px; /* Overlap above tile border */
left: 0;
right: 0;
margin: 0 auto;
font-size: 41px; /* 70% larger than original 24px */
animation: bounce 1s ease-in-out infinite;
text-align: center;
z-index: 10;
pointer-events: none; /* Prevent interference */
display: block;
width: fit-content;
}
/* === KP SHOOT FLOAT (SMALL TILES ONLY): START === */
.kp-tile-grid .kp-tile .kp-player-namewrap {
position: relative;
display: inline-block;
line-height: 1.2;
}
.kp-tile-grid .kp-tile .kp-player-name {
display: inline-block;
vertical-align: middle;
}
/* Real span path (preferred if present) */
.kp-tile-grid .kp-tile .kp-shoot-emoji {
position: absolute;
top: -0.8em;
left: 50%;
transform: translateX(-50%);
font-size: 1.2em;
pointer-events: none;
opacity: 0; /* hidden until shooting */
z-index: 3;
}
/* Pseudo-element fallback if we cannot add a span */
.kp-tile-grid .kp-tile.is-shooting .kp-player-namewrap::after {
content: attr(data-kp-emoji, “🎱”);
position: absolute;
top: -0.8em;
left: 50%;
transform: translateX(-50%);
font-size: 1.2em;
pointer-events: none;
z-index: 3;
animation: kp-bob 1.1s ease-in-out infinite;
}
/* When a real span exists, show it only while shooting */
.kp-tile-grid .kp-tile.is-shooting .kp-shoot-emoji {
opacity: 1;
animation: kp-bob 1.1s ease-in-out infinite;
}
/* Ensure the emoji can bob out of the tile without clipping */
.kp-tile-grid,
.kp-tile-grid .kp-tile,
.kp-tile-grid .kp-tile .kp-player-namewrap {
overflow: visible;
}
@keyframes kp-bob {
0%, 100% { transform: translate(-50%, 0); }
50% { transform: translate(-50%, -6px); }
}
@media (prefers-reduced-motion: reduce) {
.kp-tile-grid .kp-tile.is-shooting .kp-player-namewrap::after,
.kp-tile-grid .kp-tile.is-shooting .kp-shoot-emoji {
animation: none;
}
}
/* === KP SHOOT FLOAT (SMALL TILES ONLY): END === */
/* KP: eligible marker */
:root{
–kp-eligible-opacity: 1;
/* Reduced by 50% for smaller marker */
–kp-eligible-size-min: 8px;
–kp-eligible-size-max: 14px;
–kp-eligible-top: 6px;
–kp-eligible-right: 6px;
}
@media (prefers-reduced-motion: reduce){
:root{ –kp-eligible-opacity: 1; }
}
.kp-eligible-marker{
position:absolute;
top: var(–kp-eligible-top);
right: var(–kp-eligible-right);
line-height:1;
font-size: clamp(var(–kp-eligible-size-min), 2.2vw, var(–kp-eligible-size-max));
pointer-events:none;
z-index: 3; /* Above tile contents but below overlays */
opacity: 0;
transition: opacity .18s ease;
filter: drop-shadow(0 1px 1px rgba(0,0,0,.35));
will-change: opacity;
}
.kp-eligible-marker.is-eligible{ opacity: var(–kp-eligible-opacity); color: red !important; font-size: 1.5em; }
.kp-tile.is-shooting .kp-eligible-marker{ opacity: 0; }
/* Refill pending visual cue: static orange dots */
.refill-pending .kp-eligible-marker.is-eligible,
.refill-pending .kp-eligible-dot {
color: #f59e0b !important;
animation: none !important;
}
/* === TV Draw Animation (public view only) === */
.kp-draw-target{
position:absolute;
top:0; left:0;
width:36px; height:36px;
line-height:36px;
text-align:center;
transform: translate(-50%, -50%);
font-size:28px;
content: none;
pointer-events:none;
z-index: 9999;
filter: drop-shadow(0 4px 10px rgba(0,0,0,.4));
animation: kp-bob 1.1s ease-in-out infinite;
transition: transform 220ms cubic-bezier(.22,.61,.36,1), opacity 140ms ease;
}
.kp-draw-target.kp-hide{ opacity: 0; }
.kp-tile.kp-draw-highlight{
box-shadow: 0 0 0 3px rgba(124,58,237,.85), 0 8px 18px rgba(0,0,0,.35);
border-color: #7c3aed !important;
}
/* Hide timer UI while draw is running (server timer continues unaffected) */
.kp-tv-root.kp-draw-running [data-timer-container]{
display:none !important;
}
</style>
<?php
// KP_PATCH:kp_select_animation
kp_include_patch(‘kp_select_animation’);
?>
<?php
// Config script for elimination videos
$elim_videos_json = wp_json_encode($elim_videos_data);
echo <<<EOT
<script>
window.KILLER_ELIM_VIDS = {$elim_videos_json};
console.log(“KiloDebug: Elimination videos config loaded:”, window.KILLER_ELIM_VIDS);
</script>
EOT;
?>
<?php echo <<<EOT
<script>
window.restBase = window.restBase || {$rest_base_json};
window.timerApiUrl = window.timerApiUrl || {$timer_api_url_json};
window.eventSlug = {$current_event_slug_json};
window.wpRestNonce = window.wpRestNonce || {$wp_rest_nonce_json};
console.log(“KiloDebug: Heredoc script loaded”);
console.log(“KiloDebug: Variables defined – restBase:”, restBase, “typeof:”, typeof restBase);
console.log(“Timer API URL:”, timerApiUrl);
console.log(“Event slug:”, window.eventSlug);
console.log(“Nonce present:”, typeof wpRestNonce !== ‘undefined’ && wpRestNonce ? ‘YES’ : ‘NO’);
</script>
EOT; ?>
<?php echo <<<‘JS’
<script>
// Fan-out coalescing fetch shim
const _kpFetchCache = new Map();
window.kpFetch = function(path, options = {}) {
const base = window.restBase || (typeof restBase !== ‘undefined’ ? restBase : ”);
const rawUrl = base + path; // without ?_t
const url = rawUrl + (path.includes(‘?’) ? ‘&’ : ‘?’) + ‘_t=’ + Date.now();
const opts = { cache: ‘no-store’, …options };
if (window.wpRestNonce) {
opts.headers = { …opts.headers, ‘X-WP-Nonce’: window.wpRestNonce };
}
const key = rawUrl + ‘::’ + JSON.stringify({ method: opts.method || ‘GET’, body: opts.body || null });
if (_kpFetchCache.has(key)) {
return _kpFetchCache.get(key);
}
const promise = fetch(url, opts).finally(() => _kpFetchCache.delete(key));
_kpFetchCache.set(key, promise);
return promise;
};
// — KP single-flight /state/<slug> fetch with fan-out and last-good cache
(function(){
if (window.kpStateFetch) return; // don’t install twice
let _stateInflight = null;
let _lastGood = { text:null, status:200, headers:{‘Content-Type’:’application/json’} };
function fresh(text, status, headers) {
return new Response(text, { status, headers: new Headers(headers) });
}
window.kpStateFetch = function kpStateFetch({ signal } = {}) {
const base = window.restBase || (typeof restBase!==’undefined’ ? restBase : ”);
const url = base + ‘state/’ + encodeURIComponent(window.eventSlug);
// If a real request is already running, everyone gets a fresh cloned Response
if (_stateInflight) {
return _stateInflight.then(({text,status,headers}) => fresh(text,status,headers));
}
const headers = { ‘Accept’:’application/json’, ‘Cache-Control’:’no-store’ };
if (window.wpRestNonce) headers[‘X-WP-Nonce’] = window.wpRestNonce;
_stateInflight = fetch(url, { credentials:’same-origin’, cache:’no-store’, headers, signal })
.then(async res => {
const text = await res.clone().text().catch(()=>”);
const status = res.status || 200;
const hdrs = {};
try { const ct = res.headers.get(‘Content-Type’); if (ct) hdrs[‘Content-Type’] = ct; } catch(_){}
if (res.ok && text) _lastGood = { text, status, headers: hdrs };
return { text, status, headers: hdrs };
})
.catch(err => _lastGood.text ? _lastGood : Promise.reject(err))
.finally(() => { setTimeout(() => { _stateInflight = null; }, 0); });
return _stateInflight.then(({text,status,headers}) => fresh(text,status,headers));
};
console.log(‘KP: single-flight /state per page installed’);
})();
console.log(‘KP TV: fan-out coalescing kpFetch shim installed’);
</script>
JS;
?>
<script>
// Fullscreen Toggle Functionality – Wrapped in DOMContentLoaded
document.addEventListener(‘DOMContentLoaded’, function() {
const button = document.getElementById(‘kp-fullscreen-toggle’);
if (!button) {
console.error(‘Fullscreen button not found in DOM’);
return;
}
console.log(‘Fullscreen button found, initializing…’);
let isFullscreen = false;
function updateButtonState() {
isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
console.log(‘Fullscreen state updated:’, isFullscreen);
if (isFullscreen) {
button.innerHTML = ‘✕’;
button.setAttribute(‘aria-label’, ‘Exit full screen’);
button.classList.add(‘fullscreen-exit’);
} else {
button.innerHTML = ‘⛶’;
button.setAttribute(‘aria-label’, ‘Enter full screen’);
button.classList.remove(‘fullscreen-exit’);
}
}
function toggleFullscreen() {
console.log(‘Toggle fullscreen clicked, current state:’, isFullscreen);
const wrapper = document.querySelector(‘.kp-public-wrapper’);
const elem = wrapper || document.documentElement; // Prefer wrapper, fallback to document
if (!isFullscreen) {
// Try standard API first
if (elem.requestFullscreen) {
elem.requestFullscreen().then(() => console.log(‘Entered fullscreen’)).catch(err => {
console.error(‘Standard fullscreen error:’, err);
// Fallback to document if wrapper fails
if (wrapper !== document.documentElement && document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
}
});
}
// Vendor prefixes
else if (elem.webkitRequestFullscreen) {
elem.webkitRequestFullscreen().then(() => console.log(‘Entered webkit fullscreen’)).catch(err => console.error(‘Webkit fullscreen error:’, err));
} else if (elem.mozRequestFullScreen) {
elem.mozRequestFullScreen().then(() => console.log(‘Entered moz fullscreen’)).catch(err => console.error(‘Moz fullscreen error:’, err));
} else if (elem.msRequestFullscreen) {
elem.msRequestFullscreen().then(() => console.log(‘Entered ms fullscreen’)).catch(err => console.error(‘MS fullscreen error:’, err));
} else {
console.warn(‘Fullscreen API not supported’);
button.disabled = true;
button.title = ‘Fullscreen not supported’;
}
} else {
// Exit fullscreen (targets active element)
if (document.exitFullscreen) {
document.exitFullscreen().then(() => console.log(‘Exited fullscreen’));
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen().then(() => console.log(‘Exited webkit fullscreen’));
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen().then(() => console.log(‘Exited moz fullscreen’));
} else if (document.msExitFullscreen) {
document.msExitFullscreen().then(() => console.log(‘Exited ms fullscreen’));
} else {
console.warn(‘Exit fullscreen not supported’);
}
}
}
// Event listeners
button.addEventListener(‘click’, (e) => {
e.preventDefault();
toggleFullscreen();
});
button.addEventListener(‘keydown’, (e) => {
if (e.key === ‘Enter’ || e.key === ‘ ‘) {
e.preventDefault();
toggleFullscreen();
}
});
// Handle fullscreen change with vendor prefixes
document.addEventListener(‘fullscreenchange’, updateButtonState);
document.addEventListener(‘webkitfullscreenchange’, updateButtonState);
document.addEventListener(‘mozfullscreenchange’, updateButtonState);
document.addEventListener(‘MSFullscreenChange’, updateButtonState);
// Initial state
updateButtonState();
// Escape key to exit
document.addEventListener(‘keydown’, (e) => {
if (e.key === ‘Escape’ && isFullscreen) {
setTimeout(updateButtonState, 100);
}
});
// Check support and disable if not
const supportsFullscreen = !!(document.fullscreenEnabled || document.webkitFullscreenEnabled || document.mozFullScreenEnabled || document.msFullscreenEnabled);
if (!supportsFullscreen) {
console.warn(‘Fullscreen API not supported in this browser’);
button.disabled = true;
button.title = ‘Fullscreen not supported in this browser’;
} else {
console.log(‘Fullscreen API supported’);
}
});
// Smart real-time updates – NO MORE PAGE REFRESHING!
(function() {
// Track a stable snapshot (ignore server timestamp to prevent needless re-renders)
let lastData = null; // stores JSON string of state without the transient timestamp
let lastGameState = null;
let currentTimer = null;
let timerInterval = null;
let pollTimer = null;
let lastDelay = 1000;
// Draw animation controller (TV only)
const KP_DRAW = {
running: false,
overlay: null,
pending: null,
lastShooter: null
};
function tvRoot(){ return document.querySelector(‘.kp-tv-root’) || document.body; }
function ensureDrawOverlay(){
if (KP_DRAW.overlay && KP_DRAW.overlay.parentNode) return KP_DRAW.overlay;
const el = document.createElement(‘div’);
el.className = ‘kp-draw-target kp-hide’;
el.setAttribute(‘aria-hidden’,’true’);
tvRoot().appendChild(el);
KP_DRAW.overlay = el;
// DEBUG: Log overlay creation and dimensions
console.log(‘KP DEBUG: Overlay created with dimensions:’, el.offsetWidth, el.offsetHeight);
console.log(‘KP DEBUG: Overlay position:’, el.getBoundingClientRect());
return el;
}
function centerOf(el){
try{
const r = el.getBoundingClientRect();
const host = tvRoot().getBoundingClientRect();
const center = { x: r.left – host.left + r.width/2, y: r.top – host.top + r.height/2 };
// DEBUG: Log center calculations
console.log(‘KP DEBUG: centerOf calculation for element:’, el.dataset.playerId || ‘unknown’, ‘center:’, center, ‘rect:’, r, ‘host:’, host);
return center;
}catch(_){ return {x:0,y:0}; }
}
function syncDatasetsFor(state){
try{
const root = tvRoot();
const currentId = (state.current_player && state.current_player.id) ? String(state.current_player.id) : ”;
const p1 = (state.pool_status && typeof state.pool_status.pool1_count !== ‘undefined’) ? String(state.pool_status.pool1_count) : ”;
const p2 = (state.pool_status && typeof state.pool_status.pool2_count !== ‘undefined’) ? String(state.pool_status.pool2_count) : ”;
root.dataset.currentPlayerId = currentId;
root.dataset.pool1 = p1;
root.dataset.pool2 = p2;
root.dataset.finalTwo = (Array.isArray(state.alive_players) && state.alive_players.length === 2) ? ‘1’ : ‘0’;
}catch(_){}
}
function eligibleTilesFor(state){
try {
syncDatasetsFor(state);
if (typeof window.kpMarkEligible === ‘function’) {
window.kpMarkEligible(state);
}
const markers = Array.from(document.querySelectorAll(‘.kp-eligible-marker.is-eligible’));
const tiles = markers.map(m => m.closest(‘.kp-tile’)).filter(Boolean);
// unique
const uniq = [];
const seen = new Set();
tiles.forEach(t => { const id=t && t.getAttribute(‘data-player-id’); if (!seen.has(id)){ seen.add(id); uniq.push(t); } });
return uniq;
} catch(_) { return []; }
}
// DOM-only snapshot of currently eligible tiles; does not recompute from a new state.
function eligibleTilesFromDOM(){
try {
const markers = Array.from(document.querySelectorAll(‘.kp-eligible-marker.is-eligible’));
const tiles = markers.map(m => m.closest(‘.kp-tile’)).filter(Boolean);
const uniq = [];
const seen = new Set();
tiles.forEach(t => { const id=t && t.getAttribute(‘data-player-id’); if (!seen.has(id)){ seen.add(id); uniq.push(t); } });
return uniq;
} catch(_) { return []; }
}
async function runKpDrawAnimation(newShooterId, state){
try{
if (KP_DRAW.running) { KP_DRAW.pending = state; return; }
const root = tvRoot();
KP_DRAW.running = true;
KP_DRAW.pending = state;
root.classList.add(‘kp-draw-running’);
// DEBUG: Log animation start and state
console.log(‘KP DEBUG: Animation started for shooter ID:’, newShooterId);
console.log(‘KP DEBUG: Animation state:’, state);
// Make sure overlay exists
const overlay = ensureDrawOverlay();
overlay.classList.remove(‘kp-hide’);
// DEBUG: Log overlay visibility change
console.log(‘KP DEBUG: Overlay visibility changed, now visible’);
// Use currently-rendered eligible markers so rings remain visible during the animation
let elig = eligibleTilesFromDOM();
let shooterTile = document.querySelector(‘.kp-tile[data-player-id=”‘ + String(newShooterId) + ‘”]’);
// Fallback if DOM markers not ready yet
if ((!shooterTile || (elig && elig.length === 0)) && state && Array.isArray(state.alive_players)) {
try {
const aliveIds = new Set(
state.alive_players.map(p => String((p.id ?? p.player_id ?? p.playerId ?? ”))).filter(Boolean)
);
const allTiles = Array.from(document.querySelectorAll(‘.kp-tile[data-player-id]’));
const aliveTiles = allTiles.filter(t => aliveIds.has(String(t.dataset.playerId)));
if (!shooterTile) {
shooterTile = aliveTiles.find(t => String(t.dataset.playerId) === String(newShooterId)) || null;
}
if (elig.length === 0) {
elig = aliveTiles;
}
} catch(_) {}
}
if (!shooterTile || !elig || elig.length === 0) {
finishDrawImmediate();
return;
}
// Build hop sequence: more, quicker hops within the same 5s window
const hops = [];
// Aim for a faster-feeling animation: up to 18 hops (+1 final to shooter)
const hopCount = Math.max(12, Math.min(18, elig.length * 2));
for (let i=0;i<hopCount;i++) {
hops.push( elig[Math.floor(Math.random()*elig.length)] );
}
hops.push(shooterTile);
// Start position: first hop or current overlay position
const first = hops[0] || shooterTile;
let pos = centerOf(first);
overlay.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
// DEBUG: Log initial position
console.log(‘KP DEBUG: Initial overlay position:’, pos);
// Sequentially hop ~5s total
const stepMs = Math.floor(5000 / hops.length);
console.log(‘KP DEBUG: Animation sequence – total hops:’, hops.length, ‘step duration:’, stepMs + ‘ms’);
for (let i=0;i<hops.length;i++){
await new Promise(res => setTimeout(res, stepMs));
const tile = hops[i];
// Glow current target
document.querySelectorAll(‘.kp-tile.kp-draw-highlight’).forEach(t=>t.classList.remove(‘kp-draw-highlight’));
if (tile) tile.classList.add(‘kp-draw-highlight’);
pos = centerOf(tile || shooterTile);
overlay.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
// DEBUG: Log each hop position
console.log(‘KP DEBUG: Hop’, i+1, ‘to position:’, pos, ’tile:’, tile ? tile.dataset.playerId : ‘none’);
}
// Landed: brief hold
await new Promise(res => setTimeout(res, 250));
document.querySelectorAll(‘.kp-tile.kp-draw-highlight’).forEach(t=>t.classList.remove(‘kp-draw-highlight’));
// DEBUG: Log landing and final position
console.log(‘KP DEBUG: Animation landed, final position:’, pos);
// Apply the pending state once after landing
const apply = KP_DRAW.pending || state;
if (apply) {
try { updateDOMElements(apply); } catch(_){ }
try { if (typeof handleTimer === ‘function’) handleTimer(apply.event || null); } catch(_){ }
// Update last snapshot key so regular polling doesn’t thrash
try {
const { timestamp: _ts, …snapshot } = (apply || {});
lastData = JSON.stringify(snapshot);
} catch(_) {
try { lastData = JSON.stringify(apply || {}); } catch(_) {}
}
KP_DRAW.lastShooter = (apply.current_player && apply.current_player.id) ? String(apply.current_player.id) : KP_DRAW.lastShooter;
}
// DEBUG: Log before cleanup
console.log(‘KP DEBUG: Applying state update and preparing cleanup’);
// Tidy up
overlay.classList.add(‘kp-hide’);
root.classList.remove(‘kp-draw-running’);
KP_DRAW.pending = null;
KP_DRAW.running = false;
// DEBUG: Log animation completion
console.log(‘KP DEBUG: Animation completed and cleaned up’);
} catch(e){
finishDrawImmediate();
}
function finishDrawImmediate(){
try{
const apply = KP_DRAW.pending || state;
if (apply) {
updateDOMElements(apply);
if (typeof handleTimer === ‘function’) handleTimer(apply.event || null);
KP_DRAW.lastShooter = (apply.current_player && apply.current_player.id) ? String(apply.current_player.id) : KP_DRAW.lastShooter;
try { const { timestamp: _ts, …snapshot } = (apply || {}); lastData = JSON.stringify(snapshot); } catch(_) {}
}
} catch(_){ }
try { const root = tvRoot(); root.classList.remove(‘kp-draw-running’); } catch(_){ }
try { if (KP_DRAW.overlay) KP_DRAW.overlay.classList.add(‘kp-hide’); } catch(_){ }
KP_DRAW.pending = null;
KP_DRAW.running = false;
}
}
// — KP poll guards —
let __kpStateReq = null; // AbortController when a fetch is in-flight
let __kpBackoffMs = 350; // base delay between polls; auto-backoff on errors
let __kpLastOkAt = 0; // timestamp of last successful state
// KP WATCHDOG: detect missing current_player while game is active
let KP_WATCH = {
missingSince: 0,
deadlineMs: 8000,
lastWarnedAt: 0
};
function markMissingShooter() {
if (!KP_WATCH.missingSince) KP_WATCH.missingSince = Date.now();
}
function clearMissingShooter() {
KP_WATCH.missingSince = 0;
}
function kpListRev(arr){
const s = (arr||[]).map(String).join(‘|’);
let h = 5381; for (let i=0;i<s.length;i++) { h = ((h<<5) + h) ^ s.charCodeAt(i); }
return (h >>> 0); // uint32
}
function kpRevUrl(url, rev){
if (!url) return ”;
const sep = url.includes(‘?’) ? ‘&’ : ‘?’;
return url + sep + ‘kpv=’ + rev;
}
function setElimSrcFromState(nextUrl, rev){
if (!window.KP_ELIM || !KP_ELIM.video) return;
const v = KP_ELIM.video;
// Build desired URL with revision
function kpRevUrl(u, r){
if (!u) return ”;
try { const uo = new URL(u, location.origin); uo.searchParams.set(‘kpv’, String(r||0)); return uo.toString(); }
catch { return u + (u.includes(‘?’)?’&’:’?’) + ‘kpv=’ + (r||0); }
}
const desired = kpRevUrl(nextUrl, rev);
if (!desired) return;
// Idempotency: skip if already set to the same source
if (v.currentSrc && v.currentSrc.indexOf(desired) !== -1) return;
// Full reset to dump stale buffers
try { v.pause(); } catch {}
v.removeAttribute(‘src’);
// (no load here)
v.src = desired;
try { v.load(); } catch(_) {} // ✅ post-src load
const onReady = () => {
v.removeEventListener(‘canplaythrough’, onReady, { once:true });
v.play().catch(()=>{});
};
v.addEventListener(‘canplaythrough’, onReady, { once:true });
}
async function updateTournamentData() {
// Prevent overlapping /state fetches
if (__kpStateReq) { return; }
const KP_LOG = /[?&]kpdebug=1/i.test(location.search);
// Skip if tab is hidden to save resources
if (document && typeof document.hidden !== ‘undefined’ && document.hidden) {
__kpStateReq = null; return;
}
var url = null; // Legacy URL disabled — unified poller handles state fetch
var timestamp = Date.now();
try {
if (KP_LOG) console.log(“KP TV: updateTournamentData called – restBase:”, (typeof restBase !== ‘undefined’ ? ‘OK’ : ‘MISSING’));
if (KP_LOG) console.log(‘KP TV: Fetching tournament data for event:’, window.eventSlug);
if (KP_LOG) console.log(‘KP TV: kpFetch available:’, typeof window.kpFetch, ‘wpRestNonce:’, wpRestNonce ? ‘present’ : ‘missing’);
if (typeof restBase === ‘undefined’ || !restBase) {
throw new Error(‘restBase is not defined’);
}
if (typeof window.eventSlug === ‘undefined’ || !window.eventSlug) {
throw new Error(‘eventSlug is not defined’);
}
const ctrl = new AbortController();
__kpStateReq = ctrl;
const to = setTimeout(() => { try { ctrl.abort(‘timeout’); } catch(_){} }, 5000);
let response;
try {
response = await window.kpStateFetch({ signal: ctrl.signal });
} finally {
clearTimeout(to);
__kpStateReq = null;
}
if (KP_LOG) console.log(‘KP TV: API Response status:’, response.status, ‘URL constructed:’, url);
if (!response.ok) {
const errorText = await response.text().catch(() => ‘Unknown error’);
console.error(‘KP TV: API Error Response:’, response.status, response.statusText, errorText);
throw new Error(‘HTTP ‘ + response.status + ‘: ‘ + response.statusText + ‘ – ‘ + errorText);
}
var data = await response.json();
// after fetching state:
// — KP: trigger elim video only on NEW eliminations and not in ‘waiting’ —
(function(){
const isTV = !!(document.body.classList.contains(‘kp-public-page’) || document.getElementById(‘kp-elim-video’));
if (!isTV) return;
// Count eliminated players
window.__KP_ELIM_COUNT__ = window.__KP_ELIM_COUNT__ || 0;
const nowElims = Array.isArray(data?.eliminated_players) ? data.eliminated_players.length : 0;
const justEliminated = nowElims > window.__KP_ELIM_COUNT__;
const gameActive = (data?.game_state && data.game_state !== ‘waiting’);
if (justEliminated && gameActive) {
const list = data?.elim_clips || [];
const clipUrl = list.length ? list[0] : ”; // or your per-player selection
const rev = data?.elim_video_rev || 0;
if (clipUrl && typeof setElimSrcFromState === ‘function’) {
setElimSrcFromState(clipUrl, rev);
} else if (clipUrl && typeof setElimSrc === ‘function’) {
setElimSrc(clipUrl, rev);
}
}
window.__KP_ELIM_COUNT__ = nowElims;
})();
__kpBackoffMs = 350; // keep it snappy when healthy
__kpLastOkAt = Date.now();
// Unified gs for TV (prefer event.game_state, fallback to top-level)
var topGS = (data && typeof data.game_state !== ‘undefined’) ? data.game_state : null;
var eventGS = (data && data.event && typeof data.event.game_state !== ‘undefined’) ? data.event.game_state : null;
var gs = (eventGS !== null) ? eventGS : (topGS !== null ? topGS : ‘waiting’);
if (typeof console !== ‘undefined’) {
if (KP_LOG) console.log(‘TV POLL state fields:’, ‘top=’, topGS, ‘event=’, eventGS, ‘gs=’, gs);
}
var topGS = (data && typeof data.game_state !== ‘undefined’) ? data.game_state : null;
var eventGS = (data && data.event && typeof data.event.game_state !== ‘undefined’) ? data.event.game_state : null;
var gs = (eventGS !== null) ? eventGS : (topGS !== null ? topGS : ‘waiting’);
if (KP_LOG) console.log(‘TV POLL state fields:’, ‘top=’, topGS, ‘event=’, eventGS, ‘gs=’, gs);
if (KP_LOG) console.log(‘Tournament data received for state:’, (data.event ? data.event.game_state : data.game_state), ‘Players:’, (data.players ? data.players.length : 0));
// Detect transition waiting -> active, switch layout WITHOUT full reload
if (lastGameState === ‘waiting’ && gs === ‘active’) {
if (KP_LOG) console.log(‘Tournament started — soft start (no full reload)’);
// Force a full DOM rebuild on first active tick
try {
updateDOMElements(data);
if (typeof handleTimer === ‘function’) handleTimer(data.event || null);
// mark snapshot so the diff logic doesn’t skip later
try { const { timestamp: _ts, …snapshot } = (data || {}); lastData = JSON.stringify(snapshot); }
catch(_) { lastData = JSON.stringify(data || {}); }
showConnectionError(false);
} catch(e) {
console.warn(‘Soft start apply failed (will repoll quickly):’, e);
}
// =======================================================================
// LEGACY ACTIVE-STATE POLLING DISABLED
// Unified Poller handles all state updates.
// =======================================================================
// try {
// if (pollTimer) clearInterval(pollTimer);
// pollTimer = setInterval(updateTournamentData, 3000);
// if (KP_LOG) console.log(‘KP TV: Started polling every 3 seconds’);
// } catch(e) {
// console.error(‘KP TV: Failed to start polling:’, e);
// }
console.log(‘KP TV: Active-state legacy polling disabled — unified poller taking over’);
// Do NOT return — let the rest of the logic on this tick continue
}
// Track lastGameState if present
lastGameState = gs;
// Draw animation trigger: shooter change (active, >2 alive), TV-only
const newShooterId = (data && data.current_player && data.current_player.id) ? String(data.current_player.id) : ”;
const aliveCount = Array.isArray(data?.alive_players) ? data.alive_players.length : 0;
// Derive new current id (adapt names if needed)
const newCurrentId =
(data.current_player && (data.current_player.id ?? data.current_player.player_id)) ??
(typeof currentPlayerId !== ‘undefined’ ? currentPlayerId : null) ??
null;
// Keep the previous id across polls
window.kpPrevCurrentId = (typeof window.kpPrevCurrentId === ‘undefined’)
? null : window.kpPrevCurrentId;
// Only flash when it actually changes – use existing animation system
// Skip this fast path for the very first selection; let the initial-selection gate handle it
if (KP_DRAW.lastShooter !== null && newCurrentId && newCurrentId !== window.kpPrevCurrentId) {
try {
if (typeof runKpDrawAnimation === ‘function’) {
runKpDrawAnimation(newCurrentId, data || window.__kpLastState || null);
}
} catch (e) {
console.warn(‘KP: Animation failed, continuing updates:’, e);
}
}
window.kpPrevCurrentId = newCurrentId;
if (KP_DRAW.running) {
KP_DRAW.pending = data; // keep latest
__kpStateReq = null; return; // defer updates while animation runs
}
// Initial selection animation (first time, 3+ alive, not ‘waiting’)
if (gs !== ‘waiting’ && typeof aliveCount === ‘number’ && aliveCount >= 3 && newShooterId && KP_DRAW.lastShooter === null) {
KP_DRAW.pending = data;
await runKpDrawAnimation(newShooterId, data);
__kpStateReq = null; return; // updates applied by animation finisher
}
if (gs === ‘active’ && aliveCount > 2 && KP_DRAW.lastShooter && newShooterId && newShooterId !== KP_DRAW.lastShooter) {
// If only one eligible remains in Pool 1, skip 5s animation and update instantly
const eligCount = Number((data && data.pool_status && data.pool_status.pool1_count) ?? 0);
if (eligCount <= 1) {
try {
// Build a comparison snapshot (ignore timestamp) for lastData
let quickCompareKey;
try { const { timestamp: _ts, …snapshot } = (data || {}); quickCompareKey = JSON.stringify(snapshot); }
catch (_) { quickCompareKey = JSON.stringify(data || {}); }
// Fast apply without animation
updateDOMElements(data);
lastData = quickCompareKey;
try { window.__kpLastState = data; } catch(_) {}
showConnectionError(false);
} catch(_) {}
__kpStateReq = null; return;
}
KP_DRAW.pending = data;
await runKpDrawAnimation(newShooterId, data);
__kpStateReq = null; return; // updates applied by animation finisher
}
// Build a comparison snapshot that ignores transient timestamp
try {
const { timestamp: _ts, …snapshot } = (data || {});
var compareKey = JSON.stringify(snapshot);
} catch (_) {
var compareKey = JSON.stringify(data || {});
}
// Update DOM only if meaningful state changed
if (!lastData || compareKey !== lastData) {
console.log(‘Data changed – updating DOM’);
updateDOMElements(data);
lastData = compareKey;
// Expose latest state only after DOM reflects it (no-animation path)
try { window.__kpLastState = data; } catch(_) {}
showConnectionError(false);
} else {
console.log(‘No data changes detected’);
}
// Timer updates (if your view uses them)
if (typeof handleTimer === ‘function’) {
handleTimer(data.event || null);
}
// =======================================================================
// LEGACY TRANSITION-BASED POLLING DISABLED
// Unified Poller handles all state updates and timing.
// =======================================================================
// const pool1 = Number(data.pool_status?.pool1_count ?? 0);
// const delay = pool1 <= 1 ? 500 : 1000;
// if (delay !== lastDelay) {
// if (pollTimer) clearInterval(pollTimer);
// pollTimer = setInterval(updateTournamentData, delay);
// if (KP_LOG) console.log(‘KP TV: Updated polling interval to’, delay, ‘ms’);
// lastDelay = delay;
// }
// Keep lastDelay logic intact so surrounding code does not break
const pool1 = Number(data.pool_status?.pool1_count ?? 0);
lastDelay = lastDelay; // no-op, but preserves variable usage
} catch (err) {
console.error(‘Update check failed:’, (err && err.message) ? err.message : String(err));
console.error(‘Network details:’, {
url: url,
nonce: wpRestNonce ? ‘present’ : ‘missing’,
eventSlug: window.eventSlug,
timestamp: timestamp,
error: (err && err.toString) ? err.toString() : String(err)
});
showConnectionError(true);
__kpBackoffMs = Math.min(2000, __kpBackoffMs + 250); // gentle backoff on trouble
}
__kpStateReq = null;
}
function updateDOMElements(data) {
console.log(‘Updating DOM with state:’, data.event.game_state, ‘Players:’, data.alive_players.length);
// DOM hook: Check for eliminations on any poll update
if (window.KILLER_ELIM_VIDS?.enabled && window.KILLER_ELIM_VIDS?.clips?.length > 0) {
// Keep a tiny snapshot of the prior state so we can tell what changed
if (typeof window.__kpPrevMap === ‘undefined’) {
window.__kpPrevMap = new Map(); // id -> { lives, active }
window.__kpPrevAlive = null; // number of alive players
}
function __kpSnapshot(state) {
const arr = Array.isArray(state?.players) ? state.players : [];
const m = new Map();
let alive = 0;
for (const p of arr) {
const id = String(p?.id ?? ”);
if (!id) continue;
const lives = (parseInt(p?.regular_lives||0,10) + parseInt(p?.black_ball_lives||0,10));
const active = parseInt(p?.is_active||0,10);
if (active === 1 && lives > 0) alive++;
m.set(id, { lives, active });
}
return { m, alive };
}
function __kpJustEliminated(state) {
const { m: cur, alive: curAlive } = __kpSnapshot(state);
// Best signal: alive count dropped
if (window.__kpPrevAlive !== null && curAlive < window.__kpPrevAlive) {
window.__kpPrevMap = cur; window.__kpPrevAlive = curAlive;
return true;
}
// Fallback: someone went from alive -> dead/inactive or disappeared
let eliminated = false;
if (window.__kpPrevMap.size) {
for (const [id, old] of window.__kpPrevMap.entries()) {
const neu = cur.get(id);
const wasAlive = (old.lives > 0 && old.active === 1);
const nowDeadOrGone = (!neu) || (neu.lives <= 0) || (neu.active === 0);
if (wasAlive && nowDeadOrGone) { eliminated = true; break; }
}
}
window.__kpPrevMap = cur; window.__kpPrevAlive = curAlive;
return eliminated;
}
if (__kpJustEliminated(data)) {
// Find the eliminated player from the state data
const eliminatedPlayer = data.players?.find(p =>
p.is_active === 0 &&
(p.regular_lives + p.black_ball_lives) === 0
);
const playerId = eliminatedPlayer?.id || 0;
const eliminationTime = Date.now(); // Use current time as elimination timestamp
const url = kpSelectEliminationClip(window.KILLER_ELIM_VIDS.clips, playerId, eliminationTime);
console.log(‘KP ELIM: Elimination detected in DOM update! Playing clip:’, url, ‘for player:’, playerId);
if (url) {
window.KP_ELIM?.playElimClip(url);
}
}
}
// Update main content area based on game state (small tiles only)
updateMainContent(data);
// Update current player
updateCurrentPlayer(data.current_player, data.alive_players);
// Update player stats
updatePlayerStats(data.alive_players, data.players.length);
// Update pool counts
updatePoolStatus(data.pool_status);
// Update game state
updateGameState(data.event, data.alive_players);
// WATCHDOG: guard against missing shooter mid-game
const gs = data?.event?.game_state || ‘waiting’;
const hasShooter = !!(data && data.current_player);
if (gs === ‘active’ && !hasShooter) {
markMissingShooter();
} else {
clearMissingShooter();
}
if (KP_WATCH.missingSince && Date.now() – KP_WATCH.missingSince > KP_WATCH.deadlineMs) {
if (!KP_WATCH.lastWarnedAt || Date.now() – KP_WATCH.lastWarnedAt > 10000) {
KP_WATCH.lastWarnedAt = Date.now();
console.warn(‘KP TV WATCHDOG: Active game without shooter >’, KP_WATCH.deadlineMs, ‘ms. UI remains responsive.’);
}
showConnectionError(true);
} else {
showConnectionError(false);
}
// KP: keep root datasets in sync for marker fallbacks and compute markers
try {
const root = document.querySelector(‘.kp-tv-root’) || document.body;
const currentId = (data.current_player && data.current_player.id) ? String(data.current_player.id) : ”;
const p1 = data.pool_status && typeof data.pool_status.pool1_count !== ‘undefined’ ? String(data.pool_status.pool1_count) : ”;
const p2 = data.pool_status && typeof data.pool_status.pool2_count !== ‘undefined’ ? String(data.pool_status.pool2_count) : ”;
root.dataset.currentPlayerId = currentId;
root.dataset.pool1 = p1;
root.dataset.pool2 = p2;
root.dataset.finalTwo = (Array.isArray(data.alive_players) && data.alive_players.length === 2) ? ‘1’ : ‘0’;
} catch(_) {}
if (typeof window.kpMarkEligible === ‘function’) {
window.kpMarkEligible(data);
}
// Cross-device elimination video playback: Check for new elimination_events from API
try {
if (window.KILLER_ELIM_VIDS?.enabled && window.KILLER_ELIM_VIDS?.clips?.length > 0 && Array.isArray(data.elimination_events)) {
// Track processed eliminations to avoid duplicate playback
if (typeof window.__kpProcessedEliminations === ‘undefined’) {
window.__kpProcessedEliminations = new Set();
}
for (const event of data.elimination_events) {
const eventKey = `${event.player_id}-${event.elimination_time}`;
if (!window.__kpProcessedEliminations.has(eventKey)) {
console.log(‘KP ELIM: Detected new elimination event for player’, event.player_name, ‘at’, event.elimination_time);
// Play a deterministic clip for this elimination
const eliminatedPlayer = data.elimination_events?.[0]; // Get the most recent elimination
const playerId = eliminatedPlayer?.player_id || 0;
const eliminationTime = eliminatedPlayer?.elimination_time ? new Date(eliminatedPlayer.elimination_time).getTime() : Date.now();
const randomClip = kpSelectEliminationClip(window.KILLER_ELIM_VIDS.clips, playerId, eliminationTime);
if (randomClip && typeof window.KP_ELIM?.playElimClip === ‘function’) {
window.KP_ELIM.playElimClip(randomClip);
}
// Mark as processed to prevent re-triggering on subsequent polls
window.__kpProcessedEliminations.add(eventKey);
}
}
}
} catch (elimError) {
console.warn(‘Safe-V1: Error in cross-device elimination detection:’, elimError);
}
// — Admin finish handler (no custom banner; reuse existing yellow UI) —
try {
const isAdmin = document.body.classList.contains(‘kp-admin-page’);
if (isAdmin) {
const finished = (data && data.event && data.event.game_state === ‘finished’);
const actionBox = document.getElementById(‘kp-admin-actions’);
if (finished) {
if (actionBox) actionBox.style.display = ‘none’;
document.body.classList.add(‘kp-finished’);
// Ask existing admin UI to render winner (yellow panel)
if (typeof refreshPlayersSection === ‘function’) {
refreshPlayersSection();
} else if (typeof updateAdminFromState === ‘function’) {
updateAdminFromState(data);
}
// No reloads.
} else {
// Not finished: normal admin UI
if (actionBox) actionBox.style.display = ”;
document.body.classList.remove(‘kp-finished’);
// If theme shows a static winner panel element, ensure it’s hidden when not finished
const yellow =
document.getElementById(‘kp-winner-panel’) ||
document.querySelector(‘.kp-winner-panel,.kp-winner-banner’);
if (yellow) yellow.style.display = ‘none’;
}
}
} catch (_) {}
}
function updateMainContent(data) {
const event = data.event;
const alivePlayers = data.alive_players;
const currentPlayer = data.current_player;
const poolStatus = data.pool_status;
// Find or create the main content area in a more robust way
let targetArea = document.querySelector(‘.kp-main-content’);
if (!targetArea) {
// Find the stats area and insert after it
const statsArea = document.querySelector(‘[data-game-state]’);
if (statsArea) {
const statsContainer = statsArea.closest(‘[style*=”grid”]’) || statsArea.closest(‘div[style*=”display”]’);
if (statsContainer && statsContainer.parentNode) {
targetArea = document.createElement(‘div’);
targetArea.className = ‘kp-main-content’;
targetArea.style.marginBottom = ’30px’;
// Insert after stats container safely
if (statsContainer.nextSibling) {
statsContainer.parentNode.insertBefore(targetArea, statsContainer.nextSibling);
} else {
statsContainer.parentNode.appendChild(targetArea);
}
}
}
}
// Fallback: if still no target area, just return and let static content show
if (!targetArea) {
console.log(‘Could not create main content area, using static display’);
return;
}
// Generate content based on game state
let htmlContent = ”;
// Safety: if game is ACTIVE but current_player is null, show non-blocking waiting banner
if ((event && event.game_state === ‘active’) && (!currentPlayer || !currentPlayer.id)) {
let allPlayersLives = ”;
if (Array.isArray(data.players) && data.players.length) {
allPlayersLives = ‘<div class=”kp-tile-grid”>’ + data.players.map(function(player){
const isActive = parseInt(player.is_active || 0, 10) === 1;
const regularLives = parseInt(player.regular_lives || player.lives || 0, 10) || 0;
const blackBallLives = parseInt(player.black_ball_lives || 0, 10) || 0;
const lives = regularLives + blackBallLives;
if (!isActive || lives <= 0) return ”;
const name = player.display_name || player.name || (‘#’ + (player.id ?? ‘—’));
return ‘<div class=”kp-player-tile” data-player-id=”‘+player.id+'”>’
+ ‘<div style=”font-weight:700;margin-bottom:4px;”>’+name+'</div>’
+ ‘<div class=”lives-display”>’
+ Array.from({length:5}).map(function(_,i){
if (i < regularLives) return ‘<span style=”width:12px;height:12px;background:#10b981;border:2px solid #064e3b;border-radius:50%;display:inline-block;margin:0 2px;”></span>’;
if (i < lives) return ‘<span style=”width:12px;height:12px;background:#111827;border:2px solid #f59e0b;border-radius:50%;display:inline-block;margin:0 2px;”></span>’;
return ‘<span style=”width:12px;height:12px;background:transparent;border:2px solid #374151;border-radius:50%;display:inline-block;margin:0 2px;”></span>’;
}).join(”)
+ ‘</div>’
+ ‘</div>’;
}).join(”) + ‘</div>’;
}
htmlContent =
‘<div style=”background:#f59e0b;color:#111827;padding:10px 12px;border-radius:10px;text-align:center;font-weight:700;”>’
+ ‘⚠️ Waiting for next shooter… (auto-retry)’
+ ‘</div>’
+ (allPlayersLives
? ‘<div style=”background: rgba(15, 23, 42, 0.9); padding: 20px; border-radius: 15px; border: 2px solid rgba(71, 85, 105, 0.3); margin-top: 20px;”>’
+ ‘<div style=”text-align:center;font-weight:600;margin-bottom:15px;color:#94a3b8;”>Current Players</div>’
+ allPlayersLives
+ ‘</div>’
: ”);
targetArea.innerHTML = htmlContent;
return; // keep polling; UI stays responsive
}
if (event.game_state === ‘waiting’) {
let allPlayersLives = ‘<div class=”kp-tile-grid”>’;
if (data.players && data.players.length > 0) {
data.players.forEach(player => {
if (parseInt(player.is_active, 10) === 0) return; // Only current active players
const lives = parseInt(player.lives, 10) || 0;
const regularLives = parseInt(player.regular_lives || 0, 10);
const blackBallLives = parseInt(player.black_ball_lives || 0, 10);
const isAlive = player.is_active && lives > 0;
const playerBg = isAlive ? ‘rgba(15, 23, 42, 0.8)’ : ‘rgba(15, 23, 42, 0.6)’;
const textColor = isAlive ? ‘#10b981’ : ‘#6b7280’;
const borderColor = isAlive ? ‘rgba(16, 185, 129, 0.3)’ : ‘rgba(107, 114, 128, 0.2)’;
const badge = !isAlive ? ‘<div style=”background: #dc2626; color: white; padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; margin-top: 4px;”>OUT</div>’ : ”;
// Graphical lives spans
let lifeSpans = ‘<div class=”lives-display”>’;
for (let j = 0; j < 5; j++) {
if (j < regularLives) {
lifeSpans += ‘<span style=”width: 12px; height: 12px; background: #10b981; border-radius: 50%; display: inline-block; margin: 0 2px; box-shadow: 0 0 4px rgba(16, 185, 129, 0.5);”></span>’;
} else if (j < regularLives + blackBallLives) {
lifeSpans += ‘<span style=”width: 12px; height: 12px; background: #1f2937; border: 2px solid #f59e0b; border-radius: 50%; display: inline-block; margin: 0 2px; box-shadow: 0 0 4px rgba(245, 158, 11, 0.5);”></span>’;
} else {
lifeSpans += ‘<span style=”width: 12px; height: 12px; background: #e5e7eb; border-radius: 50%; display: inline-block; margin: 0 2px;”></span>’;
}
}
lifeSpans += ‘</div>’;
allPlayersLives +=
‘<div class=”kp-tile” style=”background: ‘ + playerBg + ‘; border: 2px solid ‘ + borderColor + ‘;” role=”img” aria-label=”Player ‘ + (player.display_name || ‘Unknown’) + ‘ with ‘ + lives + ‘ lives” data-player-id=”‘ + player.id + ‘” data-lives=”‘ + lives + ‘” data-eliminated=”‘ + (isAlive ? ‘0’ : ‘1’) + ‘” data-pool=”‘ + (parseInt(player.current_pool || 1,10)) + ‘”>’ +
‘<div class=”kp-eligible-dot”></div>’ +
‘<div class=”player-name” style=”color: ‘ + textColor + ‘; font-size: 12px; font-weight: 600; margin-bottom: 4px;”>’ + (player.display_name || ‘Unknown’) + ‘</div>’ +
lifeSpans +
‘<span style=”margin-top: 4px; font-weight: 600; color: ‘ + textColor + ‘;”>’ + lives + ‘</span>’ +
badge +
‘</div>’;
});
}
allPlayersLives += ‘</div>’;
htmlContent =
‘<div style=”background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); color: #1f2937; padding: 50px 20px; border-radius: 20px; text-align: center;”>’ +
‘<div style=”font-size: clamp(2rem, 4vw, 3rem); font-weight: 800; margin-bottom: 20px;”>⏳ Tournament Starting Soon!</div>’ +
‘<div style=”font-size: clamp(1.2rem, 2.5vw, 1.5rem); margin-bottom: 30px;”>Players are being added…</div>’ +
‘<div style=”font-size: 1.5rem;”>🎱 🎯 🎪</div>’ +
‘</div>’ +
(data.players && data.players.length > 0 ?
‘<div style=”background: rgba(15, 23, 42, 0.9); padding: 20px; border-radius: 15px; border: 2px solid rgba(71, 85, 105, 0.3); margin-top: 20px;”>’ +
‘<div style=”text-align: center; font-size: 16px; font-weight: 600; margin-bottom: 15px; color: #94a3b8;”>Current Players</div>’ +
allPlayersLives +
‘</div>’ : ”
);
} else if (alivePlayers.length > 1 && currentPlayer) {
// Generate compact lives display for all players using fixed 8-column grid
let allPlayersLives = ‘<div class=”kp-tile-grid”>’;
data.players.forEach(player => {
const lives = parseInt(player.lives);
const isActive = parseInt(player.is_active || 0, 10) === 1;
if (!isActive || lives <= 0) { return; } // hide removed/inactive and eliminated players
const isCurrentPlayer = currentPlayer && player.id === currentPlayer.id;
const isAlive = isActive && lives > 0;
let playerBg = isCurrentPlayer ? ‘rgba(124, 58, 237, 0.2)’ : (isAlive ? ‘rgba(15, 23, 42, 0.8)’ : ‘rgba(15, 23, 42, 0.6)’);
let textColor = isAlive ? ‘#10b981’ : ‘#6b7280’;
let borderColor = isCurrentPlayer ? ‘#7c3aed’ : (isAlive ? ‘rgba(16, 185, 129, 0.3)’ : ‘rgba(107, 114, 128, 0.2)’);
let badge = ”;
if (!isAlive) {
badge = ‘<div style=”background: #dc2626; color: white; padding: 2px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; margin-top: 4px;”>OUT</div>’;
}
// Generate graphical life spans for consistency
let lifeSpans = ‘<div class=”lives-display”>’;
const regularLives = parseInt(player.regular_lives || 0);
const blackBallLives = parseInt(player.black_ball_lives || 0);
const totalLives = regularLives + blackBallLives;
for (let j = 0; j < 5; j++) {
if (j < regularLives) {
// Regular life – green circle
lifeSpans += ‘<span style=”width: 12px; height: 12px; background: #10b981; border-radius: 50%; display: inline-block; margin: 0 2px; box-shadow: 0 0 4px rgba(16, 185, 129, 0.5);”></span>’;
} else if (j < totalLives) {
// Black ball life – black circle with orange border
lifeSpans += ‘<span style=”width: 12px; height: 12px; background: #1f2937; border: 2px solid #f59e0b; border-radius: 50%; display: inline-block; margin: 0 2px; box-shadow: 0 0 4px rgba(245, 158, 11, 0.5);”></span>’;
} else {
// No life – empty gray circle
lifeSpans += ‘<span style=”width: 12px; height: 12px; background: #374151; border: 2px solid #4b5563; border-radius: 50%; display: inline-block; margin: 0 2px;”></span>’;
}
}
lifeSpans += ‘</div>’;
// Wrap in flex column for top-to-bottom layout; add placeholder for non-emoji alignment
const tileContent = `
${isCurrentPlayer ? ‘<div class=”shooting-indicator”>🎯</div>’ : ”}
<div class=”player-name” style=”color: ${textColor};”>${player.display_name || ‘Unknown’}</div>
${lifeSpans}
${!isCurrentPlayer ? badge : ”}
`;
allPlayersLives +=
‘<div class=”kp-tile’ + (isCurrentPlayer ? ‘ is-shooting’ : ”) + ‘” data-player-id=”‘ + player.id + ‘” data-lives=”‘ + lives + ‘” data-eliminated=”‘ + (isAlive ? ‘0’ : ‘1’) + ‘” data-pool=”‘ + (parseInt(player.current_pool || 1,10)) + ‘” style=”background: ‘ + playerBg + ‘; border: 2px solid ‘ + borderColor + ‘;”>’ +
‘<div class=”kp-eligible-dot”></div>’ +
tileContent +
‘</div>’;
});
allPlayersLives += ‘</div>’;
htmlContent =
‘<div>’ +
‘<!– Current Shooter –>’ +
(currentPlayer ?
‘<div data-current-player style=”background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%); padding: 25px; border-radius: 20px; text-align: center; border: 3px solid rgba(255, 255, 255, 0.2); margin-bottom: 20px;”>’ +
‘<div style=”background: rgba(255, 255, 255, 0.2); padding: 6px 16px; border-radius: 15px; font-size: 12px; font-weight: 700; margin-bottom: 12px; display: inline-block; letter-spacing: 2px;”>🎯 NOW SHOOTING</div>’ +
‘<div data-player-name style=”font-size: clamp(1.8rem, 3.5vw, 2.8rem); font-weight: 800; margin-bottom: 8px;”>’ + currentPlayer.display_name + ‘</div>’ +
‘<div style=”opacity: 0.9; font-size: 16px;”>Ball #<span data-player-ball>’ + currentPlayer.player_number + ‘</span> • <span data-player-lives>’ + currentPlayer.lives + ‘</span> lives remaining</div>’ +
‘</div>’ : ”
) +
‘<!– All Players Lives (Compact Grid – Primary View) –>’ +
‘<div style=”background: rgba(15, 23, 42, 0.9); padding: 20px; border-radius: 15px; border: 2px solid rgba(71, 85, 105, 0.3); margin-top: 10px;”>’ +
‘<div style=”text-align: center; font-size: 16px; font-weight: 600; margin-bottom: 15px; color: #94a3b8;”>All Players Overview</div>’ +
allPlayersLives +
‘</div>’ +
‘</div>’;
} else if (alivePlayers.length > 1 && !currentPlayer) {
// SAFE MODE: Active game but no current shooter from server.
// Never freeze UI – show status strip + grid.
let allPlayersLives = ‘<div class=”kp-tile-grid”>’;
(data.players || []).forEach(player => {
const regularLives = parseInt(player.regular_lives || player.lives || 0, 10) || 0;
const blackBallLives = parseInt(player.black_ball_lives || 0, 10) || 0;
const totalLives = regularLives + blackBallLives;
const isAlive = (parseInt(player.is_active, 10) === 1) && totalLives > 0;
const playerBg = isAlive ? ‘rgba(15, 23, 42, 0.8)’ : ‘rgba(15, 23, 42, 0.6)’;
const textColor = isAlive ? ‘#e2e8f0’ : ‘#6b7280’;
const borderColor = isAlive ? ‘rgba(16, 185, 129, 0.3)’ : ‘rgba(107, 114, 128, 0.2)’;
const badge = isAlive ? ” : ‘<div style=”background:#dc2626;color:#fff;padding:2px 6px;border-radius:4px;font-size:10px;font-weight:700;margin-top:4px;”>OUT</div>’;
let lifeSpans = ‘<div class=”lives-display”>’;
for (let j = 0; j < 5; j++) {
if (j < regularLives) {
lifeSpans += ‘<span style=”width:12px;height:12px;background:#10b981;border-radius:50%;display:inline-block;margin:0 2px;box-shadow:0 0 4px rgba(16,185,129,0.5);”></span>’;
} else if (j < totalLives) {
lifeSpans += ‘<span style=”width:12px;height:12px;background:#1f2937;border:2px solid #f59e0b;border-radius:50%;display:inline-block;margin:0 2px;box-shadow:0 0 4px rgba(245,158,11,0.5);”></span>’;
} else {
lifeSpans += ‘<span style=”width:12px;height:12px;background:#374151;border:2px solid #4b5563;border-radius:50%;display:inline-block;margin:0 2px;”></span>’;
}
}
lifeSpans += ‘</div>’;
allPlayersLives +=
‘<div class=”kp-tile” style=”background:’+playerBg+’;border:2px solid ‘+borderColor+’;”>’ +
‘<div class=”player-name” style=”color:’+textColor+’;”>’+ (player.display_name || ‘Unknown’) +'</div>’ +
lifeSpans +
badge +
‘</div>’;
});
allPlayersLives += ‘</div>’;
htmlContent =
‘<div>’ +
‘<div style=”background:linear-gradient(135deg,#f59e0b 0%,#d97706 100%);color:#111827;padding:12px 16px;border-radius:12px;text-align:center;border:2px solid rgba(255,255,255,0.2);margin-bottom:12px;font-weight:800;”>⚠️ Waiting for next shooter… (auto-retry)</div>’ +
‘<div style=”background:rgba(15,23,42,0.9);padding:20px;border-radius:15px;border:2px solid rgba(71,85,105,0.3);”>’ +
‘<div style=”text-align:center;font-size:16px;font-weight:600;margin-bottom:15px;color:#94a3b8;”>All Players Overview</div>’ +
allPlayersLives +
‘</div>’ +
‘</div>’;
} else if (
!document.getElementById(‘kp-winner’) && // <– skip if PHP winner exists
Array.isArray(alivePlayers) && alivePlayers.length === 1
) {
const winner = alivePlayers[0] ?? {};
const winnerName =
winner.display_name ||
winner.name ||
(winner.id ? `Player #${winner.id}` : ‘Winner’);
htmlContent =
‘<div id=”kp-winner” style=”background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); color: #1f2937; padding: 50px 20px; border-radius: 20px; text-align: center;”>’ +
‘<div style=”font-size: clamp(1.5rem, 3vw, 2.5rem); font-weight: 800; margin-bottom: 20px;”>🏆 TOURNAMENT WINNER! 🏆</div>’ +
‘<div style=”font-size: clamp(2.5rem, 5vw, 4rem); font-weight: 900; margin-bottom: 30px;”>’ + winnerName + ‘</div>’ +
‘<div style=”font-size: 2rem; animation: celebrate 3s ease-in-out infinite;”>🎉 🎊 🏆 🎉 🎊</div>’ +
‘</div>’;
}
// Only update if content actually changed
if (htmlContent && targetArea.innerHTML.trim() !== htmlContent.trim()) {
console.log(‘Updating main content for state:’, event.game_state);
targetArea.style.opacity = ‘0.7’;
targetArea.style.transition = ‘opacity 0.3s ease’;
setTimeout(() => {
targetArea.innerHTML = htmlContent;
// Immediately apply markers after DOM update
try {
if (typeof window.kpMarkEligible === ‘function’) {
window.kpMarkEligible(data);
}
} catch (_) {}
targetArea.style.opacity = ‘1’;
// After fade, re-apply to ensure
setTimeout(() => {
try {
if (typeof window.kpMarkEligible === ‘function’) {
window.kpMarkEligible(data);
}
} catch (_) {}
}, 300);
}, 200);
}
}
function updateCurrentPlayer(currentPlayer, alivePlayers) {
const currentSection = document.querySelector(‘[data-current-player]’);
if (!currentSection) return;
if (currentPlayer && alivePlayers.length > 1) {
currentSection.style.display = ‘block’;
const nameElement = currentSection.querySelector(‘[data-player-name]’);
const ballElement = currentSection.querySelector(‘[data-player-ball]’);
const livesElement = currentSection.querySelector(‘[data-player-lives]’);
if (nameElement && nameElement.textContent !== currentPlayer.display_name) {
nameElement.style.opacity = ‘0.5’;
setTimeout(() => {
nameElement.textContent = currentPlayer.display_name;
nameElement.style.opacity = ‘1’;
}, 200);
}
if (ballElement) ballElement.textContent = currentPlayer.player_number;
if (livesElement) livesElement.textContent = currentPlayer.lives;
} else {
currentSection.style.display = ‘none’;
}
}
function updatePlayerStats(players, event) {
const statsElement = document.querySelector(‘[data-player-count]’);
if (!statsElement) return;
// Reset starting count if waiting or finished
if (!window.KP_STARTING_COUNT || event.game_state === ‘waiting’ || event.game_state === ‘finished’) {
window.KP_STARTING_COUNT = 0;
}
// Lock in starting count when game goes live
if (window.KP_STARTING_COUNT === 0 && event.game_state === ‘live’) {
window.KP_STARTING_COUNT = (players || []).filter(p => parseInt(p.is_active || 0, 10) === 1).length;
}
const totalPlayers = window.KP_STARTING_COUNT || (players ? players.length : 0);
const survivors = (players || []).filter(p => {
const regularLives = parseInt(p.regular_lives ?? p.lives ?? 0, 10) || 0;
const blackBallLives = parseInt(p.black_ball_lives ?? 0, 10) || 0;
const lives = regularLives + blackBallLives;
return parseInt(p.is_active || 0, 10) === 1 && lives > 0;
}).length;
statsElement.textContent = survivors + ‘ / ‘ + totalPlayers;
}
function updatePoolStatus(poolStatus) {
const pool1Element = document.querySelector(‘[data-pool1-count]’);
const pool2Element = document.querySelector(‘[data-pool2-count]’);
if (pool1Element) pool1Element.textContent = poolStatus.pool1_count + ‘ left’;
if (pool2Element) pool2Element.textContent = poolStatus.pool2_count + ‘ used’;
}
function updateGameState(event, alivePlayers) {
const stateElement = document.querySelector(‘[data-game-state]’);
if (!stateElement) return;
let newState = ”;
if (alivePlayers.length <= 1) {
newState = ‘🏆 FINISHED’;
} else if (event.game_state === ‘waiting’) {
newState = ‘⏳ WAITING’;
} else {
newState = ‘🎲 ACTIVE’;
}
if (stateElement.textContent !== newState) {
stateElement.style.opacity = ‘0.5’;
setTimeout(() => {
stateElement.textContent = newState;
stateElement.style.opacity = ‘1’;
}, 200);
}
}
function updatePlayerCards(players, currentPlayer) {
console.log(“KiloDebug public JS updatePlayerCards: players=”, players, “currentPlayer=”, currentPlayer);
// Remove eliminated player cards
const existingCards = document.querySelectorAll(‘[data-player-id]’);
const activeIds = new Set(players.map(p => ” + p.id));
existingCards.forEach(card => {
const id = card.getAttribute(‘data-player-id’);
if (!activeIds.has(id)) {
card.style.opacity = ‘0’;
card.style.transition = ‘opacity 0.5s ease’;
setTimeout(() => {
if (card.parentNode) {
card.parentNode.removeChild(card);
}
}, 500);
}
});
// Hide existing cards for inactive players
players.forEach(player => {
const card = document.querySelector(‘[data-player-id=”‘ + player.id + ‘”]’);
if (card) {
const lives = parseInt(player.lives);
const isAlive = player.is_active && lives > 0;
if (!isAlive) {
card.style.display = ‘none’;
}
}
});
players.forEach(player => {
const card = document.querySelector(‘[data-player-id=”‘ + player.id + ‘”]’);
if (!card) return;
console.log(“KiloDebug public JS updating card for player ” + player.id + “: lives=” + player.lives + “, is_active=” + player.is_active + “, fetched vs previous”);
const lives = parseInt(player.lives);
const isAlive = player.is_active && lives > 0;
// Update card opacity and background for eliminated players
const newOpacity = isAlive ? ‘1’ : ‘0.6’;
const newCardBg = isAlive ? ‘rgba(15, 23, 42, 0.9)’ : ‘rgba(15, 23, 42, 0.6)’;
const newBorderColor = isAlive ? ‘rgba(71, 85, 105, 0.3)’ : ‘rgba(71, 85, 105, 0.2)’;
if (card.style.opacity !== newOpacity) {
card.style.transition = ‘all 0.5s ease’;
card.style.opacity = newOpacity;
card.style.background = newCardBg;
card.style.borderColor = newBorderColor;
}
// Update OUT badge for eliminated players
const existingBadge = card.querySelector(‘.out-badge’);
if (!isAlive && !existingBadge) {
// Add OUT badge
const playerNameDiv = card.querySelector(‘div[style*=”justify-content: space-between”]’);
if (playerNameDiv) {
const badge = document.createElement(‘div’);
badge.className = ‘out-badge’;
badge.style.cssText = ‘background: #dc2626; color: white; padding: 4px 8px; border-radius: 8px; font-size: 12px; font-weight: 700;’;
badge.textContent = ‘OUT’;
playerNameDiv.appendChild(badge);
}
} else if (isAlive && existingBadge) {
// Remove OUT badge if player is back in
existingBadge.remove();
}
// Update lives display – FORCE UPDATE with distinction for regular vs black ball lives
const livesContainer = card.querySelector(‘[data-lives-container]’);
if (livesContainer) {
const lifeSpans = livesContainer.querySelectorAll(‘[data-life]’);
const regularLives = parseInt(player.regular_lives || 0);
const blackBallLives = parseInt(player.black_ball_lives || 0);
const totalLives = regularLives + blackBallLives;
lifeSpans.forEach((span, index) => {
// Force update regardless of current state
span.style.transition = ‘all 0.3s ease’;
if (index < regularLives) {
// Regular life – green
span.style.background = ‘#10b981’;
span.style.boxShadow = ‘0 0 10px rgba(16, 185, 129, 0.5)’;
span.style.border = ‘none’;
} else if (index < totalLives) {
// Black ball life – black with orange border
span.style.background = ‘#1f2937’;
span.style.boxShadow = ‘0 0 10px rgba(245, 158, 11, 0.5)’;
span.style.border = ‘2px solid #f59e0b’;
} else {
// No life – gray
span.style.background = ‘#374151’;
span.style.boxShadow = ‘none’;
span.style.border = ‘2px solid #4b5563’;
}
});
}
// Update lives count text
const livesCountSpan = card.querySelector(‘span[style*=”font-weight: 600; color: #10b981″]’);
if (livesCountSpan) {
livesCountSpan.textContent = lives + ‘ lives’;
livesCountSpan.style.color = isAlive ? ‘#10b981’ : ‘#dc2626’;
}
// Highlight current player
const isCurrentPlayer = currentPlayer && player.id === currentPlayer.id;
if (isCurrentPlayer && isAlive) {
card.style.borderColor = ‘#7c3aed’;
card.style.background = ‘rgba(124, 58, 237, 0.1)’;
card.classList.add(‘is-shooting’); // For CSS fallback
// Add or update shooting indicator (positioned absolutely)
const existingIndicator = card.querySelector(‘.shooting-indicator’);
if (!existingIndicator) {
const indicator = document.createElement(‘div’);
indicator.className = ‘shooting-indicator’;
// No inline styles needed; CSS handles positioning
// Insert at the beginning of the tile to overlap
card.insertBefore(indicator, card.firstChild);
}
} else {
// Remove shooting indicator
const existingIndicator = card.querySelector(‘.shooting-indicator’);
if (existingIndicator) {
existingIndicator.remove();
}
card.classList.remove(‘is-shooting’); // For CSS fallback
// Reset to normal styling
if (isAlive) {
card.style.borderColor = ‘rgba(71, 85, 105, 0.3)’;
card.style.background = ‘rgba(15, 23, 42, 0.9)’;
}
}
});
}
function handleTimer(event) {
// Always hide timer if disabled or no active game
if (!event.timer_enabled || event.game_state !== ‘active’) {
hideTimer();
return;
}
// Only show timer if we have a start time and current player
if (event.timer_start_time && event.timer_start_time > 0 && event.current_player_id && event.current_player_id > 0) {
const startTime = parseInt(event.timer_start_time) * 1000;
const duration = parseInt(event.timer_duration) * 1000;
const elapsed = Date.now() – startTime;
const remaining = Math.max(0, duration – elapsed);
console.log(‘Starting timer:’, { remaining, duration });
showTimer(remaining, duration, event);
// Clear existing timer
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
// Start countdown only if there’s time remaining
if (remaining > 0) {
// timerInterval = setInterval(() => {
// const newElapsed = Date.now() – startTime;
// const newRemaining = Math.max(0, duration – newElapsed);
// if (newRemaining <= 0) {
// clearInterval(timerInterval);
// timerInterval = null;
// handleTimerExpired(event);
// } else {
// updateTimerDisplay(newRemaining, duration, event);
}
// }, 100);
else {
// Time already expired
handleTimerExpired(event);
}
} else {
}
}
function showTimer(remaining, duration, event) {
let timerContainer = document.querySelector(‘[data-timer-container]’);
if (!timerContainer) {
timerContainer = document.createElement(‘div’);
timerContainer.setAttribute(‘data-timer-container’, ”);
timerContainer.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: linear-gradient(135deg, #1f2937 0%, #374151 100%);
color: white;
padding: 20px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
z-index: 9999;
text-align: center;
min-width: 150px;
`;
document.body.appendChild(timerContainer);
}
updateTimerDisplay(remaining, duration, event);
}
function updateTimerDisplay(remaining, duration, event) {
const timerContainer = document.querySelector(‘[data-timer-container]’);
if (!timerContainer) return;
const seconds = Math.ceil(remaining / 1000);
const progress = remaining / duration;
const bufferSeconds = event.timer_buffer_enabled ? parseInt(event.timer_buffer_seconds) : 0;
const isInBuffer = seconds <= bufferSeconds;
let bgColor = ‘#10b981’; // Green
if (isInBuffer && event.timer_buffer_enabled) {
bgColor = seconds <= 5 ? ‘#dc2626’ : ‘#f59e0b’; // Red or orange
}
timerContainer.innerHTML = `
<div style=”font-size: 12px; opacity: 0.8; margin-bottom: 10px;”>SHOT TIMER</div>
<div style=”position: relative; width: 80px; height: 80px; margin: 0 auto 15px;”>
<svg width=”80″ height=”80″ style=”transform: rotate(-90deg);”>
<circle cx=”40″ cy=”40″ r=”35″ stroke=”rgba(255,255,255,0.2)” stroke-width=”6″ fill=”none”/>
<circle cx=”40″ cy=”40″ r=”35″ stroke=”${bgColor}” stroke-width=”6″ fill=”none”
stroke-dasharray=”${Math.PI * 70 * (0)} ${Math.PI * 70}”
style=”transition: stroke 0.3s ease, stroke-dasharray 0.1s ease;”/>
</svg>
<div style=”position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; font-weight: 800;”>${seconds}</div>
</div>
${seconds <= 10 ? ‘<div style=”font-size: 10px; color: #fbbf24; text-align: center;”>Getting Close!</div>’ : ”}
`;
timerContainer.style.animation = isInBuffer ? ‘pulse 0.5s ease-in-out infinite alternate’ : ‘none’;
}
function hideTimer() {
const timerContainer = document.querySelector(‘[data-timer-container]’);
if (timerContainer) {
timerContainer.remove();
}
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
}
function handleTimerExpired(event) {
if (event.timer_auto_loss) {
// Auto lose life – call API endpoint
fetch(timerApiUrl, {
method: ‘POST’,
headers: {
‘Content-Type’: ‘application/json’,
‘Accept’: ‘application/json’,
‘X-WP-Nonce’: wpRestNonce,
‘Cache-Control’: ‘no-store’
},
body: JSON.stringify({})
})
.then(response => response.json())
.then(result => {
console.log(‘Timer auto-loss result:’, result);
if (result.success) {
// Show temporary message
const timerContainer = document.querySelector(‘[data-timer-container]’);
if (timerContainer) {
timerContainer.innerHTML = `
<div style=”font-size: 12px; opacity: 0.8; margin-bottom: 10px;”>AUTO LOSS</div>
<div style=”font-size: 16px; font-weight: 800; color: #dc2626; margin-bottom: 5px;”>TIME UP!</div>
<div style=”font-size: 10px; color: #fbbf24;”>Player lost 1 life – Next player selected</div>
`;
timerContainer.style.background = ‘linear-gradient(135deg, #dc2626 0%, #ef4444 100%)’;
}
// Immediate refresh after auto-loss
updateTournamentData();
}
})
.catch(err => {
console.error(‘Timer auto-loss failed:’, err);
// Fallback to manual state
showExpiredTimer();
updateTournamentData(); // Still refresh on error
});
} else {
showExpiredTimer();
}
}
function showExpiredTimer() {
const timerContainer = document.querySelector(‘[data-timer-container]’);
if (timerContainer) {
timerContainer.innerHTML = `
<div style=”font-size: 12px; opacity: 0.8; margin-bottom: 10px;”>SHOT TIMER</div>
<div style=”font-size: 24px; font-weight: 800; color: #dc2626; margin-bottom: 10px;”>TIME!</div>
<div style=”font-size: 10px; color: #fbbf24;”>Waiting for admin…</div>
`;
timerContainer.style.background = ‘linear-gradient(135deg, #dc2626 0%, #ef4444 100%)’;
timerContainer.style.animation = ‘pulse 1s ease-in-out infinite’;
}
}
function showConnectionError(show) {
let errorIndicator = document.querySelector(‘[data-connection-error]’);
if (show) {
if (!errorIndicator) {
errorIndicator = document.createElement(‘div’);
errorIndicator.setAttribute(‘data-connection-error’, ”);
errorIndicator.style.cssText = `
position: fixed;
top: 10px;
left: 50%;
transform: translateX(-50%);
background: #dc2626;
color: white;
padding: 10px 20px;
border-radius: 8px;
z-index: 10000;
font-weight: bold;
`;
errorIndicator.textContent = ‘⚠️ Connection Lost – Retrying…’;
document.body.appendChild(errorIndicator);
}
} else {
if (errorIndicator) {
errorIndicator.remove();
}
}
// Update debug indicator
updateDebugIndicator(!show);
}
function updateDebugIndicator(connected) {
const debugIndicator = document.getElementById(‘kp-debug-indicator’);
const statusDot = document.getElementById(‘kp-connection-status’);
const debugText = document.getElementById(‘kp-debug-text’);
if (debugIndicator && statusDot) {
if (connected) {
statusDot.style.background = ‘#10b981’; // Green for connected
debugText.textContent = ‘TV ✓’;
} else {
statusDot.style.background = ‘#dc2626’; // Red for error
debugText.textContent = ‘TV ✗’;
}
// Show debug indicator if URL contains debug parameter
if (window.location.search.includes(‘kpdebug=1’)) {
debugIndicator.style.display = ‘flex’;
}
}
}
document.addEventListener(‘DOMContentLoaded’, () => {
if (typeof updateTournamentData === ‘function’) {
updateTournamentData();
// Initial marker application
if (typeof window.kpMarkEligible === ‘function’ && window.__kpLastState) {
window.kpMarkEligible(window.__kpLastState);
}
}
});
// ========================================================
// DYNAMIC POLLING DISABLED — Unified Poller Now Handles All State
// ========================================================
// pollTimer = setInterval(updateTournamentData, 3000);
// lastDelay = 1000;
// console.log(‘KP TV: Initial polling started at 3 seconds’);
console.log(‘KP TV: Legacy polling disabled — unified poller active’);
// Add manual test function to window for debugging
window.kpTestUpdate = function() {
console.log(‘KP TV: Manual test update triggered’);
updateTournamentData();
};
// Add test button if debug mode is enabled
if (window.location.search.includes(‘kpdebug=1’)) {
setTimeout(() => {
const testBtn = document.createElement(‘button’);
testBtn.textContent = ‘Test Update’;
testBtn.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
background: #1e40af;
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
cursor: pointer;
z-index: 10000;
font-weight: bold;
`;
testBtn.onclick = window.kpTestUpdate;
document.body.appendChild(testBtn);
console.log(‘KP TV: Test button added for debugging’);
}, 1000);
}
// ——————————————————
// TV: listen for Admin pings
// ——————————————————
try {
const bc = new BroadcastChannel(‘killer_pool_events’);
bc.onmessage = (ev) => {
if (ev?.data?.slug === window.eventSlug) {
console.log(‘KP TV: Received admin ping, reason=’, ev.data.reason, ‘ts=’, ev.data.ts);
try {
updateTournamentData();
// Also trigger hint-based update as backup
if (typeof window.kpTriggerHintUpdate === ‘function’) {
window.kpTriggerHintUpdate();
}
} catch(e) {
console.error(‘KP TV: Failed to update after admin ping:’, e);
}
} else if (ev?.data?.slug) {
console.log(‘KP TV: Ignoring ping for different event:’, ev.data.slug);
}
};
console.log(‘KP TV: BroadcastChannel listening for event:’, window.eventSlug);
// Test the channel
bc.postMessage({ test: ‘tv_ready’, slug: window.eventSlug, ts: Date.now() });
} catch (e) {
console.warn(‘KP TV: BroadcastChannel not supported:’, e.message);
// Fallback: rely entirely on polling
console.log(‘KP TV: Using polling-only fallback mode’);
}
// Wake-up triggers: if the tab was backgrounded, poll immediately on focus/show
// Wake-up triggers: if the tab was backgrounded, poll immediately on focus/show
try {
window.addEventListener(‘focus’, () => {
try { updateTournamentData(); } catch (_) {}
});
window.addEventListener(‘visibilitychange’, () => {
if (!document.hidden) {
try { updateTournamentData(); } catch (_) {}
}
});
window.addEventListener(‘pageshow’, () => {
try { updateTournamentData(); } catch (_) {}
});
} catch (_) {
// no-op fallback
}
})();
</script>
<?php
// KP_PATCH:kp_live_updates
kp_include_patch(‘kp_live_updates’);
?>
<script>
// Deterministic clip selection function for consistent video playback across clients
function kpSelectEliminationClip(clips, playerId, eliminationTime) {
if (!clips || clips.length === 0) return null;
if (!playerId || !eliminationTime) return clips[0]; // Fallback to first clip
// Create a deterministic hash from player_id and elimination_time
const hashInput = playerId.toString() + eliminationTime.toString();
let hash = 0;
for (let i = 0; i < hashInput.length; i++) {
const char = hashInput.charCodeAt(i);
hash = ((hash << 5) – hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
// Use absolute value and modulo to get deterministic index
const index = Math.abs(hash) % clips.length;
return clips[index];
}
// KP: Eligible marker feature (pool-aware). Feature-flagged for safe rollback.
(function(){
try {
window.KP_FLAGS = window.KP_FLAGS || {};
if (typeof window.KP_FLAGS.elMarkerEnabled === ‘undefined’) window.KP_FLAGS.elMarkerEnabled = 1;
// Decide which pool will be drawn from next.
// Pool 1 if any available; otherwise Pool 2 (which will be refilled next selection).
function getCurrentPool(state, root){
const p1 = Number(state?.pool_status?.pool1_count ?? root?.dataset?.pool1 ?? 0);
const p2 = Number(state?.pool_status?.pool2_count ?? root?.dataset?.pool2 ?? 0);
if (p1 > 0) return 1;
if (p2 > 0) return 2;
return 0; // no eligible players
}
// Compute and apply eligibility purely from latest server state
window.kpMarkEligible = function(state){
try {
if (!window.KP_FLAGS?.elMarkerEnabled) return;
const root = document.querySelector(‘.kp-tv-root’) || document.body;
// Game state + final two handling
const gsTop = state?.game_state;
const gsEvent = state?.event?.game_state;
const gameState = (typeof gsEvent !== ‘undefined’) ? gsEvent : (typeof gsTop !== ‘undefined’ ? gsTop : ‘waiting’);
const isFinalTwo = Array.isArray(state?.alive_players) && state.alive_players.length === 2;
// In final two, rings do not matter → hide all
if (isFinalTwo || gameState !== ‘active’) {
document.querySelectorAll(‘.kp-eligible-marker.is-eligible’)
.forEach(m => m.classList.remove(‘is-eligible’));
root.classList.remove(‘refill-pending’);
return;
}
const currentId = String(state?.current_player?.id ?? root?.dataset?.currentPlayerId ?? ”);
const upcomingPool = Number(state?.pool_status?.upcoming_pool ?? getCurrentPool(state, root));
const pool1Count = Number(state?.pool_status?.pool1_count ?? 0);
const isRefillPending = upcomingPool === 2 && pool1Count === 0;
const currentPool = upcomingPool;
if (isRefillPending) {
root.classList.add(‘refill-pending’);
} else {
root.classList.remove(‘refill-pending’);
}
const players = Array.isArray(state?.players) ? state.players : [];
const eligibleIds = new Set();
if (currentPool === 1 || currentPool === 2) {
for (let i = 0; i < players.length; i++) {
const p = players[i];
if (!p) continue;
const idStr = String(p.id);
const lives = Number(p.regular_lives || 0) + Number(p.black_ball_lives || 0);
const active = Number(p.is_active || 0) === 1;
const pool = Number(p.current_pool || 0);
if (active && lives > 0 && pool === currentPool && idStr !== currentId) {
eligibleIds.add(idStr);
}
}
}
// Apply to DOM tiles
document.querySelectorAll(‘.kp-tile’).forEach(tile => {
const marker = tile.querySelector(‘.kp-eligible-marker’);
if (!marker) return;
const id = String(tile.dataset.playerId || tile.getAttribute(‘data-player-id’) || ”);
if (eligibleIds.has(id)) {
marker.classList.add(‘is-eligible’);
} else {
marker.classList.remove(‘is-eligible’);
}
});
} catch(e){ /* silent */ }
};
window.kpToggleEligibleMarkers = function(on){
window.KP_FLAGS.elMarkerEnabled = on ? 1 : 0;
if (!on) {
document.querySelectorAll(‘.kp-eligible-marker.is-eligible’).forEach(m=>m.classList.remove(‘is-eligible’));
// Remove eligible dots when clearing eligibility
document.querySelectorAll(‘.kp-eligible-dot’).forEach(dot => dot.remove());
}
};
// Observe DOM changes under the main content and re-apply markers using the last known state
try {
const rootTarget = document.querySelector(‘.kp-main-content’) || document.querySelector(‘.kp-tv-root’) || document.body;
const mo = new MutationObserver(() => {
try {
if (typeof window.kpMarkEligible === ‘function’ && window.__kpLastState) {
window.kpMarkEligible(window.__kpLastState);
}
} catch (_) {}
});
mo.observe(rootTarget, { childList: true, subtree: true });
} catch (_) {}
} catch(e) { /* no-op */ }
})();
// === ELIMINATION VIDEO CONTROLLER ===
// Unmute after first rendered frame; if browser blocks audible autoplay,
// it auto-reverts to muted and keeps playing (no stall).
function kpAutoUnmuteAfterFirstFrame(v){
function unmute() {
try {
v.muted = true; // try enable audio
if (v.paused) { // audible autoplay blocked → resume muted
v.muted = true;
v.play().catch(()=>{});
}
} catch(_) {}
}
if (typeof v.requestVideoFrameCallback === ‘function’) {
v.requestVideoFrameCallback(() => unmute());
} else {
const onPlaying = () => requestAnimationFrame(unmute);
v.addEventListener(‘playing’, onPlaying, { once:true });
}
}
function kpRevUrl(base, rev){
if (!base) return ”;
const s = String(base);
const sep = s.includes(‘?’) ? ‘&’ : ‘?’;
return s + sep + ‘kpv=’ + (rev|0);
}
function kpListRev(arr){
const s = (arr||[]).map(String).join(‘|’);
let h = 5381; for (let i=0;i<s.length;i++) { h = ((h<<5) + h) ^ s.charCodeAt(i); }
return (h >>> 0); // uint32
}
function setElimSrcFromState(state){
const list = (state.elim_videos || []).filter(Boolean); // array in admin order
const current = list[0] || ”; // top of the list = source of truth
const rev = kpListRev(list); // changes on any order/content change
const final = kpRevUrl(current, rev);
// before setting src:
const newSig = (state.elim_videos||[]).join(‘|’);
if (window.__KP_LAST_ELIM_SIG__ !== newSig) {
window.__KP_LAST_ELIM_SIG__ = newSig;
kpElimCacheClear();
}
const v = KP_ELIM && KP_ELIM.video;
if (!v) return;
// If same URL already set with same rev, do nothing
if (v.currentSrc && v.currentSrc.indexOf(final) !== -1) return;
// Reset the element before switching sources (prevents stale buffers)
try { v.pause(); } catch(_) {}
v.removeAttribute(‘src’);
// (no load here)
v.src = final;
try { v.load(); } catch(_) {} // ✅ post-src load
}
function kpElimCacheClear(){
if (!window.KP_ELIM_CACHE) return;
for (const v of KP_ELIM_CACHE.values()) { try { URL.revokeObjectURL(v); } catch(_){} }
KP_ELIM_CACHE.clear();
}
window.KP_ELIM_CACHE = window.KP_ELIM_CACHE || new Map();
async function kpPrefetchElim(url){
try {
if (!url || KP_ELIM_CACHE.has(url)) return;
const r = await fetch(url, { credentials:’same-origin’, cache: ‘force-cache’ });
const blob = await r.blob();
const obj = URL.createObjectURL(blob);
KP_ELIM_CACHE.set(url, obj);
} catch(_) {}
}
(function() {
‘use strict’;
const KP_ELIM = {
overlay: null,
video: null,
queue: [],
playing: false,
watchdog: null,
stallTimer: null,
currentClip: null,
initialized: false
};
function init() {
if (KP_ELIM.initialized) return;
KP_ELIM.initialized = true;
KP_ELIM.overlay = document.getElementById(‘kp-elim-overlay’);
KP_ELIM.video = document.getElementById(‘kp-elim-video’);
try {
KP_ELIM.video.setAttribute(‘playsinline’, ”);
KP_ELIM.video.setAttribute(‘webkit-playsinline’, ”);
KP_ELIM.video.setAttribute(‘preload’, ‘auto’);
KP_ELIM.video.setAttribute(‘disablepictureinpicture’, ”);
KP_ELIM.video.controls = false; // do NOT add the attribute
KP_ELIM.video.muted = true; // ✅ TVs require muted autoplay
KP_ELIM.video.setAttribute(‘muted’, ”); // persist muted across re-inserts
// Optional but helps on some TV stacks:
try { KP_ELIM.video.disableRemotePlayback = true; KP_ELIM.video.setAttribute(‘disableRemotePlayback’,”); } catch(_) {}
} catch (_) {}
if (!KP_ELIM.overlay || !KP_ELIM.video) {
console.warn(‘KP ELIM: Overlay elements not found – overlay:’, !!KP_ELIM.overlay, ‘video:’, !!KP_ELIM.video);
return;
}
// — KP: tolerant video error recovery (optional) —————————-
(function attachElimOnError(){
if (KP_ELIM.__onErr) return;
KP_ELIM.__onErr = true;
const v = KP_ELIM.video;
v.addEventListener(‘error’, () => {
try { v.pause(); } catch {}
v.removeAttribute(‘src’);
v.load(); // clear broken buffer; do not auto-retry here
console.warn(‘KP ELIM: Video error cleared (source reset).’);
}, { passive:true });
})();
// Set up video event listeners
KP_ELIM.video.addEventListener(‘ended’, onVideoEnd);
KP_ELIM.video.addEventListener(‘error’, onVideoError);
KP_ELIM.video.addEventListener(‘canplay’, onVideoCanPlay);
KP_ELIM.video.addEventListener(‘stalled’, onVideoStalled);
// Expose playElimClip as a method of KP_ELIM
KP_ELIM.playElimClip = queueOrPlayElimClip;
console.log(‘KP ELIM: Controller initialized successfully’);
console.log(‘KP ELIM: Config check – enabled:’, window.KILLER_ELIM_VIDS?.enabled, ‘clips:’, window.KILLER_ELIM_VIDS?.clips?.length || 0);
}
function queueOrPlayElimClip(clipUrl) {
if (!window.KILLER_ELIM_VIDS || !window.KILLER_ELIM_VIDS.enabled) {
console.log(‘KP ELIM: Feature disabled’);
return false;
}
if (!clipUrl) {
console.log(‘KP ELIM: No clip URL provided’);
return false;
}
if (KP_ELIM.playing) {
// Queue for later
KP_ELIM.queue.push(clipUrl);
console.log(‘KP ELIM: Queued clip, currently playing’);
return true;
}
return playElimClip(clipUrl);
}
function playElimClip(clipUrl){
const v = KP_ELIM.video; // your video element
if (!v) return;
// Fast, safe start
v.setAttribute(‘playsinline’,”);
v.setAttribute(‘webkit-playsinline’,”);
v.autoplay = true;
v.preload = ‘auto’;
v.controls = false;
v.muted = true; // guarantees autoplay
v.pause();
v.removeAttribute(‘src’);
// (no load here)
if (clipUrl) v.src = KP_ELIM_CACHE.get(clipUrl) || clipUrl;
v.load(); // ✅ keep
// Show overlay
KP_ELIM.overlay.classList.add(‘is-on’);
KP_ELIM.playing = true;
KP_ELIM.currentClip = clipUrl;
// Watchdog only if it never starts producing frames
let watchdog = setTimeout(() => {
try { if (!v || v.readyState < 2) v.pause(); } catch(_) {}
console.warn(‘KP ELIM: Watchdog fired – ending playback fail-safe’);
try { endPlayback(); } catch(_) {}
}, 20000); // or 25000 on very slow TVs
[‘playing’,’loadeddata’,’canplay’,’ended’,’error’,’stalled’]
.forEach(ev => v.addEventListener(ev, () => { try { clearTimeout(watchdog); } catch(_) {} }, { once:true }));
[‘playing’,’loadeddata’,’canplay’,’ended’,’error’,’stalled’]
.forEach(ev => v.addEventListener(ev, () => { try { clearTimeout(watchdog); } catch(_) {} }, { once:true }));
// IMPORTANT: do not end on catch; retry once and keep going muted if needed.
window.KP_ELIM = window.KP_ELIM || {};
KP_ELIM.isPlaying = true;
v.play()
.then(() => {
kpAutoUnmuteAfterFirstFrame(v); // flip to sound on first frame
v.addEventListener(‘ended’, async () => { clearTimeout(watchdog); KP_ELIM.isPlaying = false; if (KP_ELIM.pendingRefresh) { KP_ELIM.pendingRefresh = false; try { if (typeof fetchStateOnce === ‘function’) await fetchStateOnce(); else if (typeof updateTournamentData === ‘function’) updateTournamentData(); } catch(_) {} } endPlayback(); }, { once:true });
})
.catch(() => {
v.play().then(() => {
kpAutoUnmuteAfterFirstFrame(v);
v.addEventListener(‘ended’, () => { clearTimeout(watchdog); endPlayback(); }, { once:true });
}).catch(() => { /* keep silent if policy insists; watchdog will clean up */ });
v.addEventListener(‘pause’, () => { /* keep isPlaying true while buffering */ }, { passive:true });
v.addEventListener(‘playing’,() => { /* still true */ }, { passive:true });
});
}
function onVideoCanPlay() {
clearTimeout(KP_ELIM.stallTimer);
console.log(‘KP ELIM: Video ready to play’);
}
function onVideoStalled() {
console.warn(‘KP ELIM: Video stalled’);
endPlayback();
}
function onVideoEnd() {
console.log(‘KP ELIM: Video ended naturally’);
endPlayback();
}
function onVideoError(err) {
console.warn(‘KP ELIM: Video error:’, err);
endPlayback();
}
function endPlayback() {
if (!KP_ELIM.playing) return;
// Invalidate any in-flight callbacks/timers
__kpPlaybackToken += 1;
KP_ELIM.playing = false;
try { clearTimeout(KP_ELIM.watchdog); } catch(_) {}
try { clearTimeout(KP_ELIM.stallTimer); } catch(_) {}
// Hide overlay
try { KP_ELIM.overlay.classList.remove(‘is-on’); } catch(_) {}
// Clean up video safely
try { KP_ELIM.video.pause(); } catch(_) {}
try {
KP_ELIM.video.removeAttribute(‘src’);
KP_ELIM.video.load();
} catch(_) {}
try { KP_ELIM.video.muted = false; } catch(_) {}
// Play next clip in queue, if any
if (KP_ELIM.queue && KP_ELIM.queue.length > 0) {
const nextClip = KP_ELIM.queue.shift();
setTimeout(() => startPlayback(nextClip), 500);
}
console.log(‘KP ELIM: Playback ended’);
}
function forceEndPlayback() {
console.warn(‘KP ELIM: Force ending playback’);
endPlayback();
}
// Hook into draw animation to detect eliminations (diff-based + await clip)
const originalRunKpDrawAnimation = window.runKpDrawAnimation;
if (typeof originalRunKpDrawAnimation === ‘function’) {
// Keep a tiny snapshot of the prior state so we can tell what changed
let __kpPrevMap = new Map(); // id -> { lives, active }
let __kpPrevAlive = null; // number of alive players
function __kpSnapshot(state) {
const arr = Array.isArray(state?.players) ? state.players : [];
const m = new Map();
let alive = 0;
for (const p of arr) {
const id = String(p?.id ?? ”);
if (!id) continue;
const lives = (parseInt(p?.regular_lives||0,10) + parseInt(p?.black_ball_lives||0,10));
const active = parseInt(p?.is_active||0,10);
if (active === 1 && lives > 0) alive++;
m.set(id, { lives, active });
}
return { m, alive };
}
function __kpJustEliminated(state) {
const { m: cur, alive: curAlive } = __kpSnapshot(state);
// Best signal: alive count dropped
if (__kpPrevAlive !== null && curAlive < __kpPrevAlive) {
__kpPrevMap = cur; __kpPrevAlive = curAlive;
return true;
}
// Fallback: someone went from alive -> dead/inactive or disappeared
let eliminated = false;
if (__kpPrevMap.size) {
for (const [id, old] of __kpPrevMap.entries()) {
const neu = cur.get(id);
const wasAlive = (old.lives > 0 && old.active === 1);
const nowDeadOrGone = (!neu) || (neu.lives <= 0) || (neu.active === 0);
if (wasAlive && nowDeadOrGone) { eliminated = true; break; }
}
}
__kpPrevMap = cur; __kpPrevAlive = curAlive;
return eliminated;
}
// Promise that resolves when the video finishes/errs (no changes to your player)
function awaitElimClip(url) {
return new Promise((resolve) => {
const v = document.getElementById(‘kp-elim-video’);
if (!url || !v || !window.KILLER_ELIM_VIDS?.enabled) {
console.log(‘KP ELIM: awaitElimClip skipped – missing requirements’);
return resolve();
}
console.log(‘KP ELIM: Starting video playback for:’, url);
// resolve on end or error — once
const done = () => {
console.log(‘KP ELIM: Video playback finished’);
v.removeEventListener(‘ended’, done);
v.removeEventListener(‘error’, done);
resolve();
};
v.addEventListener(‘ended’, done, { once: true });
v.addEventListener(‘error’, done, { once: true });
// trigger existing player
try {
playElimClip(url);
} catch(e) {
console.warn(‘KP ELIM: playElimClip error:’, e);
resolve();
}
});
}
window.runKpDrawAnimation = async function(newShooterId, state) {
try {
const clips = window.KILLER_ELIM_VIDS?.clips || [];
console.log(‘KP ELIM: Wrapper called – enabled:’, window.KILLER_ELIM_VIDS?.enabled, ‘clips:’, clips.length, ‘state players:’, state?.players?.length || 0);
if (window.KILLER_ELIM_VIDS?.enabled && clips.length > 0 && __kpJustEliminated(state)) {
// Find the eliminated player from the state
const eliminatedPlayer = state.players?.find(p =>
p.is_active === 0 &&
(p.regular_lives + p.black_ball_lives) === 0
);
const playerId = eliminatedPlayer?.id || 0;
const eliminationTime = Date.now();
const url = kpSelectEliminationClip(clips, playerId, eliminationTime);
console.log(‘KP ELIM: Elimination detected! Playing clip:’, url, ‘for player:’, playerId);
if (url) { await awaitElimClip(url); } // <– wait for the clip to finish
console.log(‘KP ELIM: Video playback completed, proceeding with draw’);
} else {
console.log(‘KP ELIM: No elimination detected or feature disabled’);
}
} catch (e) {
console.warn(‘KP ELIM: wrapper error’, e);
}
// hand off to your original ~5s draw
return await originalRunKpDrawAnimation.apply(this, arguments);
};
console.log(‘KP ELIM: Wrapped runKpDrawAnimation (diff-based, awaited)’);
}
// Initialize when DOM is ready
if (document.readyState === ‘loading’) {
document.addEventListener(‘DOMContentLoaded’, init);
} else {
init();
}
// Expose for debugging
window.KP_ELIM = KP_ELIM;
})();
let __kpPlaybackToken = 0;
</script>
<?php
// KP_PATCH:kp_elim_videos
kp_include_patch(‘kp_elim_videos’);
?>
<script>
(function() {
‘use strict’;
// Safe-V1: Enhanced error handling for elimination video system
// Wrap all KP_ELIM method calls in try-catch to prevent breaking admin actions
const originalUpdateDOMElements = window.updateDOMElements;
if (typeof originalUpdateDOMElements === ‘function’) {
window.updateDOMElements = function(data) {
try {
// Call original function
const result = originalUpdateDOMElements.apply(this, arguments);
// Safe elimination detection and video playback
if (window.KILLER_ELIM_VIDS?.enabled && window.KP_ELIM?.playElimClip) {
try {
// Check for eliminations in the data
const players = Array.isArray(data?.players) ? data.players : [];
const prevPlayers = window.__kpLastPlayers || [];
// Simple diff: check if alive count decreased
const currentAlive = players.filter(p => p.is_active && (p.regular_lives + p.black_ball_lives) > 0).length;
const prevAlive = prevPlayers.filter(p => p.is_active && (p.regular_lives + p.black_ball_lives) > 0).length;
if (currentAlive < prevAlive) {
console.log(‘Safe-V1: Elimination detected via DOM hook’);
// Get deterministic clip based on elimination event
const clips = window.KILLER_ELIM_VIDS.clips || [];
if (clips.length > 0) {
// Find the eliminated player
const eliminatedPlayer = players.find(p =>
p.is_active === 0 &&
(p.regular_lives + p.black_ball_lives) === 0
);
const playerId = eliminatedPlayer?.id || 0;
const eliminationTime = Date.now();
const randomClip = kpSelectEliminationClip(clips, playerId, eliminationTime);
window.KP_ELIM.playElimClip(randomClip);
}
}
// Update stored players
window.__kpLastPlayers = players;
} catch (elimError) {
console.warn(‘Safe-V1: Error in elimination detection:’, elimError);
}
}
return result;
} catch (error) {
console.error(‘Safe-V1: Error in updateDOMElements wrapper:’, error);
// Fallback to original function
return originalUpdateDOMElements.apply(this, arguments);
}
};
console.log(‘Safe-V1: Wrapped updateDOMElements with error handling’);
}
// Also wrap any other potential hook points
const originalRunKpDrawAnimation = window.runKpDrawAnimation;
if (typeof originalRunKpDrawAnimation === ‘function’) {
window.runKpDrawAnimation = function(newShooterId, state) {
try {
return originalRunKpDrawAnimation.apply(this, arguments);
} catch (error) {
console.error(‘Safe-V1: Error in runKpDrawAnimation wrapper:’, error);
return originalRunKpDrawAnimation.apply(this, arguments);
}
};
console.log(‘Safe-V1: Wrapped runKpDrawAnimation with error handling’);
}
console.log(‘Safe-V1: Enhanced error handling initialized’);
})();
</script>
<?php
?>
</div>
<?php
return ob_get_clean();
}
// Admin shortcode for embedded admin controls – NO WORDPRESS ADMIN REQUIRED
function kp_complete_admin_shortcode($atts) {
$atts = shortcode_atts([‘event’ => ‘demo’], $atts);
$slug = sanitize_title($atts[‘event’]);
// Try to get event slug from page meta if not explicitly provided
if (!$atts[‘event’] || $atts[‘event’] === ‘demo’) {
$post_id = get_the_ID();
if ($post_id) {
$meta_slug = get_post_meta($post_id, ‘_kp_event_slug’, true);
if ($meta_slug) {
$slug = sanitize_title($meta_slug);
}
}
}
// Get tournament data – if not found, show error
$event = kp_complete_get_event($slug);
if (!$event) {
return ‘<div style=”background: #f8f9fa; padding: 20px; border-radius: 8px; text-align: center; border: 2px solid #dee2e6;”><p>Tournament “‘ . esc_html($slug) . ‘” not found.</p></div>’;
}
// Completely preserve the original WordPress admin interface
// Just call the existing admin page function directly with NO login requirement
ob_start();
// COMPREHENSIVE FIX: FONT SIZES + FOOTER LAYOUT + CONTENT CONSISTENCY
echo ‘<style>
/* ========= FONT SIZE FIXES ========= */
h1 { font-size: 23px !important; font-weight: 400 !important; margin: 10px 0 15px 0 !important; line-height: 1.4 !important; }
h2 { font-size: 18px !important; font-weight: 600 !important; margin: 25px 0 10px 0 !important; line-height: 1.4 !important; }
h2#pool-stats-title, h2.shortcode-title, h2.wp-heading-inline { font-size: 18px !important; }
h3 { font-size: 16px !important; font-weight: 600 !important; margin: 20px 0 10px 0 !important; line-height: 1.4 !important; }
/* ========= BUTTON STYLING ========= */
.button-primary, button.button-primary, input[type=”submit”], .btn-primary {
background: #007cba !important;
border-color: #007cba !important;
color: #fff !important;
font-size: 13px !important;
min-height: 30px !important;
padding: 0 12px !important;
border-radius: 3px !important;
text-decoration: none !important;
}
.button-secondary, button.button-secondary {
background: #f6f7f8 !important;
border-color: #c3c4c7 !important;
color: #000 !important;
font-size: 13px !important;
box-shadow: inset 0 1px 0 #fff !important;
min-height: 30px !important;
padding: 0 12px !important;
border-radius: 3px !important;
}
/* ========= FORM STYLING ========= */
input[type=”text”], input[type=”number”], input[type=”date”], textarea, select {
font-size: 14px !important;
border: 1px solid #8c8f94 !important;
border-radius: 4px !important;
padding: 6px 10px !important;
background: #fff !important;
box-sizing: border-box !important;
}
textarea { min-height: 60px !important; resize: vertical !important; }
/* ========= NOTICE STYLING ========= */
.notice, div.updated, div.error, .update-nag {
border-radius: 4px !important;
margin: 20px 0 !important;
padding: 12px !important;
border-left: 4px solid !important;
}
.notice-success, div.updated { background: #d4edda !important; border-left-color: #28a745 !important; color: #155724 !important; }
.notice-info { background: #d1ecf1 !important; border-left-color: #17a2b8 !important; color: #0c5460 !important; }
.notice-warning { background: #fff3cd !important; border-left-color: #ffc107 !important; color: #856404 !important; }
.notice-error, div.error { background: #f8d7da !important; border-left-color: #dc3545 !important; color: #721c24 !important; }
/* ========= CARD STYLING ========= */
.postbox { border-radius: 8px !important; box-shadow: 0 1px 1px rgba(0,0,0,.04) !important; }
/* ========= CRITICAL FOOTER FIX – FORCE CENTER ALWAYS ========= */
.elementor-footer, #footer, .site-footer, footer {
display: block !important;
position: static !important;
float: none !important;
clear: both !important;
width: 100% !important;
margin: 0 auto !important;
padding: 20px 0 !important;
text-align: center !important;
justify-content: center !important;
align-items: center !important;
}
/* ELEMENTOR SPECIFIC FOOTER OVERRIDES */
.elementor-location-footer,
[data-elementor-type=”footer”] {
text-align: center !important;
display: flex !important;
justify-content: center !important;
width: 100% !important;
}
/* FORCE ALL FOOTER CONTENT CENTERS */
.elementor-footer *, #footer *, .site-footer *,
.elementor-location-footer *, [data-elementor-type=”footer”] * {
text-align: center !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
position: static !important;
}
/* CONTENT CONSISTENCY FOR EMPTY TOURnaments */
.kp-content-spacer { min-height: 600px !important; width: 100% !important; }
/* REMOVE POSITIONING CONFLICTS */
* [style*=”sticky”], * [style*=”fixed”] { position: static !important; }
/* BASE BODY STYLING */
body {
font-family: -apple-system, BlinkMacSystemFont, “Segoe UI”, Roboto, “Helvetica Neue”, Arial, sans-serif !important;
box-sizing: border-box !important;
}
</style>’;
// ADD CONTENT STRUCTURE FOR CONSISTENCY (fixes empty tournament footer layout)
echo ‘<div class=”kp-content-spacer” style=”min-height: 600px; width: 100%; display: block; float: none;”>’;
// Call kp_complete_admin_page() function with our event slug
$original_get_params = $_GET;
$_GET[‘page’] = ‘killer-pool-complete’;
$_GET[‘event’] = $slug;
// Execute the admin page function directly
kp_complete_admin_page();
echo ‘</div>’; // Close kp-content-spacer
// Restore original $_GET
$_GET = $original_get_params;
return ob_get_clean();
}
// Universal iframe shortcode (handles both public TV and admin)
function kp_complete_iframe_shortcode($atts) {
$atts = shortcode_atts([
‘event’ => ‘demo’,
‘type’ => ‘public’,
‘height’ => ‘900’
], $atts);
$slug = sanitize_title($atts[‘event’]);
$type = sanitize_text_field($atts[‘type’]);
$height = intval($atts[‘height’]);
if ($type === ‘public’) {
// For public TV display, create iframe to a special TV display page
$tv_url = add_query_arg([
‘kp_tv_display’ => ‘1’,
‘event’ => $slug
], home_url());
return ‘<div style=”margin: 20px 0;”>
<iframe src=”‘ . esc_url($tv_url) . ‘” style=”width: 100%; height: ‘ . $height . ‘px; border: 0; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);” loading=”lazy”></iframe>
</div>’;
} elseif ($type === ‘admin’) {
// For admin, iframe to WordPress admin (same as existing)
if (!current_user_can(‘manage_options’)) {
return ‘<div style=”background: #f8f9fa; padding: 20px; border-radius: 8px; text-align: center; border: 2px solid #dee2e6;”><p><em>Admin access required.</em></p></div>’;
}
$iframe_url = add_query_arg([
‘page’ => ‘killer-pool-complete’,
‘event’ => $slug,
‘noheader’ => 1
], admin_url(‘admin.php’));
return ‘<div style=”margin: 20px 0;”>
<iframe src=”‘ . esc_url($iframe_url) . ‘” style=”width: 100%; height: ‘ . $height . ‘px; border: 0; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1);” loading=”lazy”></iframe>
</div>’;
}
return ‘<div style=”background: #f8f9fa; padding: 20px; border-radius: 8px; text-align: center; border: 2px solid #dee2e6;”><p>Invalid iframe type specified.</p></div>’;
}
// Add REST API endpoints for real-time updates
add_action(‘rest_api_init’, ‘kp_complete_register_api’);
add_action(‘rest_api_init’, ‘kp_complete_register_admin_action_routes’);
function kp_complete_register_api() {
// Health check ping route – simpler pattern
register_rest_route(‘killer-pool/v1’, ‘ping’, array(
‘methods’ => ‘GET’,
‘permission_callback’ => ‘__return_true’,
‘callback’ => ‘kp_complete_ping_callback’
));
// Main game state endpoint – simpler pattern
register_rest_route(‘killer-pool/v1’, ‘state/(?P<slug>[a-zA-Z0-9\-_]+)’, array(
‘methods’ => ‘GET’,
‘callback’ => ‘kp_complete_api_get_event_data’,
‘permission_callback’ => ‘__return_true’
));
// REST endpoint: Force select next shooter from eligible alive players (A/B-aware)
// Force next shooter endpoint (missing earlier)
// KP-GOV PATCH A: secure force-next + governor veto
// === KP-GOV PATCH A: secure force-next + governor veto ===
// REST endpoint: Force select next shooter from eligible alive players (A/B-aware)
register_rest_route(‘killer-pool/v1’, ‘/force-next-shooter/(?P<slug>[a-zA-Z0-9\-\._]+)’, array(
// === KP-GOV PATCH A: secure force-next + governor veto ===
‘methods’ => ‘POST’,
‘callback’ => ‘kp_force_next_shooter’,
‘permission_callback’ => function ( WP_REST_Request $request ) {
// Verify REST nonce from header (WordPress sends it in X-WP-Nonce)
$nonce = $request->get_header( ‘X-WP-Nonce’ );
if ( ! $nonce || ! wp_verify_nonce( $nonce, ‘wp_rest’ ) ) {
return new WP_Error( ‘kp_gov_nonce’, ‘Invalid or missing nonce’, array( ‘status’ => 403 ) );
}
// Ensure authenticated admin
if ( ! is_user_logged_in() || ! current_user_can( ‘manage_options’ ) ) {
return new WP_Error( ‘kp_gov_auth’, ‘Admin authentication required’, array( ‘status’ => 403 ) );
}
return true;
},
// === /KP-GOV PATCH A ===
));
// === KP: Force Next Shooter — AB-correct, computed lives, deterministic ===
function kp_force_next_shooter( WP_REST_Request $request ) {
// === KP-GOV PATCH B: governor pre-check (lock + invariant veto) ===
if ( function_exists( ‘kp_governor_pre_force_next_veto’ ) ) {
$v = kp_governor_pre_force_next_veto( $request );
if ( is_wp_error( $v ) ) {
return $v;
}
}
// === /KP-GOV PATCH B ===
// 1) Resolve event
$slug = sanitize_title( $request[‘slug’] );
if ( ! $slug ) {
return new WP_Error(‘bad_request’, ‘Missing slug’, [‘status’ => 400]);
}
if ( ! function_exists(‘kp_complete_get_event’) || ! function_exists(‘kp_complete_select_next_player’) ) {
return new WP_Error(‘server_error’, ‘Missing helpers’, [‘status’ => 500]);
}
$event = kp_complete_get_event( $slug );
if ( ! $event || empty($event[‘id’]) ) {
return new WP_Error(‘not_found’, ‘Event not found’, [‘status’ => 404]);
}
$event_id = (int) $event[‘id’];
// 2) Delegate to the canonical selector (moves A→B, refills, filters by computed lives)
$ok = kp_complete_select_next_player( $event_id );
if ( ! $ok ) {
return new WP_Error(‘no_eligible_players’, ‘No eligible players remain’, [‘status’ => 409]);
}
// 3) Return a fresh FULL state (same shape your UI already expects)
if ( ! function_exists(‘kp_complete_api_get_event_data’) ) {
return new WP_Error(‘server_error’, ‘Missing state builder’, [‘status’ => 500]);
}
$req_state = new WP_REST_Request(‘GET’, ‘/killer-pool/v1/state’);
$req_state->set_param(‘slug’, $slug);
$state = kp_complete_api_get_event_data( $req_state );
if ( is_wp_error($state) ) {
return new WP_Error(‘server_error’, ‘Failed to build state’, [‘status’ => 500]);
}
// 4) Nudge TVs/admin clones (optional)
if ( function_exists(‘kp_bump_hint’) ) {
kp_bump_hint( $event_id, $slug );
}
return [
‘success’ => true,
‘action’ => ‘force_next’,
‘message’ => ‘Next shooter: ‘ . ($state[‘current_player’][‘display_name’] ?? ‘unknown’),
‘state’ => $state,
];
}
// Timer auto-loss endpoint
register_rest_route(‘killer-pool/v1’, ‘timer-expired/(?P<slug>[a-zA-Z0-9\-_]+)’, array(
‘methods’ => ‘POST’,
‘callback’ => ‘kp_complete_api_handle_timer_expired’,
‘permission_callback’ => ‘__return_true’
));
// Manual life adjustment endpoint
register_rest_route(‘killer-pool/v1’, ‘adjust-life/(?P<slug>[a-zA-Z0-9\-_]+)’, array(
‘methods’ => ‘POST’,
‘callback’ => ‘kp_complete_api_adjust_life’,
‘permission_callback’ => ‘__return_true’
));
// Add player during game endpoint
register_rest_route(‘killer-pool/v1’, ‘add-player/(?P<slug>[a-zA-Z0-9\-_]+)’, array(
‘methods’ => ‘POST’,
‘callback’ => ‘kp_complete_api_add_player’,
‘permission_callback’ => ‘__return_true’
));
// Remove player during game endpoint
register_rest_route(‘killer-pool/v1’, ‘remove-player/(?P<slug>[a-zA-Z0-9\-_]+)’, array(
‘methods’ => ‘POST’,
‘callback’ => ‘kp_complete_api_remove_player’,
‘permission_callback’ => function($request) {
return wp_verify_nonce($request->get_header(‘X-WP-Nonce’), ‘wp_rest’);
}
));
// Get player history endpoint
register_rest_route(‘killer-pool/v1’, ‘player-history/(?P<slug>[a-zA-Z0-9\-_]+)/(?P<player_id>\d+)’, array(
‘methods’ => ‘GET’,
‘callback’ => ‘kp_complete_api_get_player_history’,
‘permission_callback’ => ‘__return_true’
));
// Get current match history endpoint
register_rest_route(‘killer-pool/v1’, ‘current-match-history/(?P<slug>[a-zA-Z0-9\-_]+)’, array(
‘methods’ => ‘GET’,
‘callback’ => ‘kp_complete_api_get_current_match_history’,
‘permission_callback’ => ‘__return_true’
));
add_filter(‘rest_pre_serve_request’, function ($served, $result, $request, $server) {
// Only for your plugin routes
$route = $request->get_route();
if (strpos($route, ‘/killer-pool/v1/’) === 0) {
nocache_headers();
header(‘Cache-Control: no-store, no-cache, must-revalidate, max-age=0’);
header(‘Pragma: no-cache’);
header(‘Expires: 0’);
}
return $served;
}, 10, 4);
}
function kp_complete_register_admin_action_routes() {
$ns = ‘killer-pool/v1’;
register_rest_route($ns, ‘/potted/(?P<slug>[A-Za-z0-9\-_]+)’, array(
‘methods’ => ‘POST’,
‘callback’ => ‘kp_complete_api_admin_action_potted’,
‘permission_callback’ => ‘__return_true’,
));
register_rest_route($ns, ‘/missed/(?P<slug>[A-Za-z0-9\-_]+)’, array(
‘methods’ => ‘POST’,
‘callback’ => ‘kp_complete_api_admin_action_missed’,
‘permission_callback’ => ‘__return_true’,
));
register_rest_route($ns, ‘/foul/(?P<slug>[A-Za-z0-9\-_]+)’, array(
‘methods’ => ‘POST’,
‘callback’ => ‘kp_complete_api_admin_action_foul’,
‘permission_callback’ => ‘__return_true’,
));
register_rest_route($ns, ‘/black-potted/(?P<slug>[A-Za-z0-9\-_]+)’, array(
‘methods’ => ‘POST’,
‘callback’ => ‘kp_complete_api_admin_action_black_potted’,
‘permission_callback’ => ‘__return_true’,
));
}
// KP_PATCH:kp_api_enhancements
kp_include_patch(‘kp_api_enhancements’);
// KP_PATCH:kp_state_actions
kp_include_patch(‘kp_state_actions’);
// Separate callback function for ping
function kp_complete_ping_callback() {
return array(
‘ok’ => true,
‘time’ => time(),
‘rand’ => wp_generate_password(8, false),
‘plugin’ => ‘killer-pool-complete’,
‘routes_working’ => true
);
}
function kp_complete_api_get_event_data($request) {
// Ensure no client/proxy caches this JSON
nocache_headers();
header(‘Cache-Control: no-cache, no-store, must-revalidate, max-age=0, s-maxage=0’);
header(‘Pragma: no-cache’);
header(‘Expires: Thu, 01 Jan 1970 00:00:00 GMT’);
header(‘X-Accel-Expires: 0’); // nginx fastcgi/proxy
$slug = sanitize_title($request[‘slug’]);
$event = kp_complete_get_event($slug);
if (!$event) {
return new WP_Error(‘not_found’, ‘Event not found’, array(‘status’ => 404));
}
// Full pool status
$pool_status = kp_complete_get_pool_status($event[‘id’]);
// Server-side timer expiry check and auto-loss (non-invasive, runs on every state fetch to ensure final turns, especially final 2)
// Lock to prevent concurrent timer deductions
global $wpdb;
$events_table = $wpdb->prefix . ‘kp_events’;
$players_table = $wpdb->prefix . ‘kp_players’;
$wpdb->query(“SET SESSION innodb_lock_wait_timeout = 5”);
$locked_event = $wpdb->get_row($wpdb->prepare(
“SELECT * FROM $events_table WHERE id = %d FOR UPDATE”,
$event[‘id’]
), ARRAY_A);
if ($wpdb->last_error) {
// Lock failed: log and skip timer deduction for this fetch
error_log(“KP STATE TIMER LOCK ERROR: failed to acquire lock for event {$event[‘id’]}: {$wpdb->last_error}”);
$locked_event = null; // Skip timer check on lock failure
} else {
// Lock succeeded: debug-only
KP_LOG(“KP STATE TIMER: Successfully acquired lock for timer check on event {$event[‘id’]}”, ‘debug’);
$event = $locked_event; // Use locked data
}
// REMOVED: Timer auto-loss logic from GET /state endpoint
// Timer expiry now only handled via explicit POST actions to prevent
// unintended mutations during read operations (page reloads, etc.)
// This fixes the bug where page reloads could change current_player_id
$players = kp_complete_get_players($event[‘id’]);
$alive_players = array_filter($players, function($p) { return $p[‘is_active’] && $p[‘lives’] > 0; });
$eliminated_players = array_filter($players, function($p) { return !$p[‘is_active’] || $p[‘lives’] == 0; });
// === SERVER CLAMP: Snapshot current_player_id before any operations ===
$original_event_row = $wpdb->get_row($wpdb->prepare(“SELECT current_player_id FROM $events_table WHERE id = %d”, $event[‘id’]), ARRAY_A);
$original_current_player_id = $original_event_row[‘current_player_id’];
KP_LOG(“SERVER CLAMP: Snapshot current_player_id = {$original_current_player_id} for event {$event[‘id’]}”);
// GET /state must never modify database – prevent unintended mutations during read operations
// (e.g., clearing invalid current_player could cause UI state changes on page reload)
$event_row = $wpdb->get_row($wpdb->prepare(“SELECT current_player_id FROM $events_table WHERE id = %d”, $event[‘id’]), ARRAY_A);
$current_player_id = $event_row[‘current_player_id’];
if ($current_player_id) {
$player = $wpdb->get_row($wpdb->prepare(
“SELECT *, (COALESCE(regular_lives, 0) + COALESCE(black_ball_lives, 0)) AS lives FROM $players_table WHERE id = %d”,
$current_player_id
), ARRAY_A);
if ($player && intval($player[‘is_active’]) === 1 && intval($player[‘lives’]) > 0) {
$current_player = $player;
} else {
$current_player = null; // Invalid current_player – return null but don’t clear from DB
}
} else {
$current_player = null;
}
// Track eliminations for cross-device video synchronization
$elimination_events = array();
foreach ($eliminated_players as $player) {
// Only include recently eliminated players (within last 30 seconds)
$last_action = kp_get_player_last_elimination_time($event[‘id’], $player[‘id’]);
if ($last_action && (time() – strtotime($last_action)) <= 30) {
$elimination_events[] = array(
‘player_id’ => $player[‘id’],
‘player_name’ => $player[‘display_name’],
‘elimination_time’ => $last_action
);
}
}
// NEW LOG: Full players data returned
KP_LOG(“STATE FETCH for slug {$slug}: Total players=” . count($players) . “, Active=” . count($alive_players) . “, Inactive=” . count($eliminated_players) . “, Players array: ” . json_encode($players));
// Belt-and-suspenders: finish game if single survivor
$game_state = $event[‘game_state’] ?? ‘waiting’;
$winner = null;
if ($game_state === ‘active’ && count($alive_players) === 1) {
global $wpdb;
$events_table = $wpdb->prefix . ‘kp_events’;
$wpdb->update($events_table, [‘game_state’ => ‘finished’], [‘id’ => $event[‘id’]]);
if (function_exists(‘kp_bump_hint’)) { kp_bump_hint((int)$event[‘id’], $event[‘slug’] ?? null); }
$game_state = ‘finished’;
$winner = $alive_players[0];
KP_LOG(“STATE: Belt-and-suspenders finish for single survivor in event {$event[‘id’]}”);
}
// === CANONICAL STATE OUTPUT (Unified Schema) ===
nocache_headers();
header(‘Cache-Control: no-cache, no-store, must-revalidate, max-age=0, s-maxage=0’);
$canonical = array(
‘revision’ => (int) get_option(‘kp_rev_’ . $event[‘id’], 1),
‘timestamp’ => round(microtime(true) * 1000),
‘event’ => $event,
‘players’ => array_values($players),
‘alive_players’ => array_values($alive_players),
‘eliminated_players’ => array_values($eliminated_players),
‘current_player’ => $current_player ?: array(‘id’ => 0),
‘pool_status’ => $pool_status ?: array(),
‘pool1_players’ => $pool_status[‘pool1_players’] ?? array(),
‘pool2_players’ => $pool_status[‘pool2_players’] ?? array(),
‘pool1_count’ => $pool_status[‘pool1_count’] ?? 0,
‘pool2_count’ => $pool_status[‘pool2_count’] ?? 0,
‘players_count’ => ($pool_status[‘pool1_count’] ?? 0) + ($pool_status[‘pool2_count’] ?? 0),
‘actions’ => array(), // future: will be filled
‘elimination’ => array(
‘clips’ => $elim_videos_data[‘clips’] ?? array(),
‘video_rev’ => $elim_videos_data[‘video_rev’] ?? 0,
),
‘elimination_events’ => $elimination_events ?? array(),
‘winner’ => $winner,
‘meta’ => array(),
);
// === SCHEMA HARDENING & NULL SAFETY ===
// Always ensure arrays never return null
$canonical[‘players’] = is_array($canonical[‘players’]) ? $canonical[‘players’] : array();
$canonical[‘alive_players’] = is_array($canonical[‘alive_players’]) ? $canonical[‘alive_players’] : array();
$canonical[‘eliminated_players’] = is_array($canonical[‘eliminated_players’]) ? $canonical[‘eliminated_players’] : array();
$canonical[‘pool_status’] = is_array($canonical[‘pool_status’]) ? $canonical[‘pool_status’] : array();
$canonical[‘pool1_players’] = is_array($canonical[‘pool1_players’]) ? $canonical[‘pool1_players’] : array();
$canonical[‘pool2_players’] = is_array($canonical[‘pool2_players’]) ? $canonical[‘pool2_players’] : array();
// Ensure current player always exists
if (!is_array($canonical[‘current_player’])) {
$canonical[‘current_player’] = array(‘id’ => 0);
}
if (!isset($canonical[‘current_player’][‘id’])) {
$canonical[‘current_player’][‘id’] = 0;
}
// Ensure elimination structure always valid
if (!isset($canonical[‘elimination’]) || !is_array($canonical[‘elimination’])) {
$canonical[‘elimination’] = array(‘clips’ => array(), ‘video_rev’ => 0);
}
if (!isset($canonical[‘elimination’][‘clips’]) || !is_array($canonical[‘elimination’][‘clips’])) {
$canonical[‘elimination’][‘clips’] = array();
}
if (!isset($canonical[‘elimination’][‘video_rev’])) {
$canonical[‘elimination’][‘video_rev’] = 0;
}
// Ensure elimination_events exists
$canonical[‘elimination_events’] = is_array($canonical[‘elimination_events’])
? $canonical[‘elimination_events’]
: array();
// Force integer types where required
$canonical[‘revision’] = (int)$canonical[‘revision’];
$canonical[‘pool1_count’] = (int)($canonical[‘pool1_count’] ?? 0);
$canonical[‘pool2_count’] = (int)($canonical[‘pool2_count’] ?? 0);
$canonical[‘players_count’] = (int)($canonical[‘players_count’] ?? 0);
// === END NULL-SAFETY LAYER ===
// === ACTIONS[] HISTORY (structured, limited to last 200 turns) ===
global $wpdb;
$actions_table = $wpdb->prefix . ‘kp_player_actions’;
$raw_actions = $wpdb->get_results(
$wpdb->prepare(
“SELECT
pa.turn_number,
pa.action_type,
pa.lives_before,
pa.lives_after,
pa.player_id,
p.display_name AS player_name
FROM $actions_table pa
LEFT JOIN {$wpdb->prefix}kp_players p
ON pa.player_id = p.id
WHERE pa.event_id = %d
ORDER BY pa.turn_number ASC
LIMIT 200″,
$event[‘id’]
),
ARRAY_A
);
// Normalize actions for frontend
$canonical[‘actions’] = array_map(function($row) {
return array(
‘turn’ => (int)$row[‘turn_number’],
‘player_id’ => (int)$row[‘player_id’],
‘player_name’ => $row[‘player_name’] ?? ”,
‘action’ => $row[‘action_type’],
‘before’ => (int)$row[‘lives_before’],
‘after’ => (int)$row[‘lives_after’],
);
}, $raw_actions);
// — Compute stable hash —
$hash_source = wp_json_encode(
array(
‘event’ => $canonical[‘event’],
‘players’ => $canonical[‘players’],
‘alive_players’ => $canonical[‘alive_players’],
‘eliminated_players’=> $canonical[‘eliminated_players’],
‘current_player’ => $canonical[‘current_player’],
‘pool_status’ => $canonical[‘pool_status’],
‘elimination’ => $canonical[‘elimination’],
‘elimination_events’=> $canonical[‘elimination_events’],
‘winner’ => $canonical[‘winner’],
),
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
);
$canonical[‘hash’] = hash(‘sha256’, $hash_source);
return rest_ensure_response($canonical);
}
function kp_complete_api_handle_timer_expired($request) {
global $wpdb;
$events_table = $wpdb->prefix . ‘kp_events’;
$players_table = $wpdb->prefix . ‘kp_players’;
$slug = sanitize_title($request[‘slug’]);
$event = kp_complete_get_event($slug);
if (!$event) {
return new WP_Error(‘not_found’, ‘Event not found’, array(‘status’ => 404));
}
// Ensure timer + auto-loss enabled
if (empty($event[‘timer_enabled’]) || empty($event[‘timer_auto_loss’])) {
return array(‘success’ => false, ‘reason’ => ‘Timer auto-loss not enabled’);
}
// ATOMIC LOCK: SELECT event FOR UPDATE to prevent concurrent deductions
// Optimize locking to prevent timeouts and hangs
$wpdb->query(“SET SESSION innodb_lock_wait_timeout = 5”);
$locked_event = $wpdb->get_row($wpdb->prepare(
“SELECT * FROM $events_table WHERE id = %d FOR UPDATE”,
$event[‘id’]
), ARRAY_A);
if ($wpdb->last_error) {
error_log(“KP TIMER LOCK ERROR: ” . $wpdb->last_error . ” for event {$event[‘id’]}”);
// Skip timer deduction on lock failure to prevent hangs
$locked_event = null;
} else {
KP_LOG(“TIMER: Successfully acquired lock for timer check on event {$event[‘id’]}”);
}
if (!$locked_event || empty($locked_event[‘timer_start_time’])) {
return array(‘success’ => false, ‘reason’ => ‘No active timer’);
}
// Double-check timer still expired under lock
$start_time = intval($locked_event[‘timer_start_time’]);
$duration = intval($locked_event[‘timer_duration’]);
$current_time = time();
if ($current_time – $start_time <= $duration) {
// Timer not yet expired (race condition resolved)
return array(‘success’ => false, ‘reason’ => ‘Timer still active’);
}
// Current player under lock (fresh)
$current_player = kp_complete_get_current_player($event[‘id’]);
if (!$current_player) {
// Clear timer under lock
$wpdb->update($events_table, [‘timer_start_time’ => 0], [‘id’ => $event[‘id’]]);
return array(‘success’ => false, ‘reason’ => ‘No current player’);
}
$player_id = intval($current_player[‘id’]);
$regular = intval($current_player[‘regular_lives’] ?? 0);
$black = intval($current_player[‘black_ball_lives’] ?? 0);
$old_total = $regular + $black;
// Deduct 1 (prefer regular, then black) – ONLY if lives available
if ($old_total == 0) {
// No lives – just clear timer
$wpdb->update($events_table, [‘timer_start_time’ => 0], [‘id’ => $event[‘id’]]);
return array(‘success’ => false, ‘reason’ => ‘No lives to deduct’);
}
if ($regular > 0) {
$regular -= 1;
} else {
$black -= 1;
}
$new_total = $regular + $black;
$is_active = ($new_total > 0) ? 1 : 0;
// FULL TRANSACTION: Update player + reset timer + log in one atomic operation
$wpdb->query(‘START TRANSACTION’);
try {
// Update player
$player_updated = $wpdb->update(
$players_table,
array(
‘regular_lives’ => $regular,
‘black_ball_lives’ => $black,
‘is_active’ => $is_active
),
array(‘id’ => $player_id),
array(‘%d’, ‘%d’, ‘%d’),
array(‘%d’)
);
if ($player_updated === false) {
throw new Exception(‘Player update failed: ‘ . $wpdb->last_error);
}
// IMMEDIATELY reset timer under lock – prevents any further deductions
$timer_reset = $wpdb->update(
$events_table,
[‘timer_start_time’ => 0],
[‘id’ => $event[‘id’]]
);
if ($timer_reset === false) {
throw new Exception(‘Timer reset failed: ‘ . $wpdb->last_error);
}
// Log the deduction
$life_type = ($regular < intval($current_player[‘regular_lives’] ?? 0)) ? ‘regular’ : ‘black ball’;
kp_complete_log_player_action(
$event[‘id’],
$player_id,
‘TIMER_AUTO_LOSS’,
$old_total,
$new_total,
“Timer auto-loss: deducted 1 {$life_type} life (server-locked transaction)”
);
$wpdb->query(‘COMMIT’);
} catch (Exception $e) {
$wpdb->query(‘ROLLBACK’);
error_log(“KP TIMER DEDUCTION ERROR: ” . $e->getMessage());
return new WP_Error(‘transaction_fail’, $e->getMessage(), array(‘status’ => 500));
}
// Select next player AFTER transaction (safe now, timer already reset)
$next = kp_complete_select_next_player($event[‘id’]);
// Refresh event after selection to get accurate state
$event = kp_complete_get_event($slug);
// REMOVED: Timer auto-loss logic from POST /timer-expired endpoint
// Timer expiry now only handled via explicit user actions to prevent
// unintended mutations during page loads or automatic polling
// This fixes the bug where page reloads could change current_player_id
// Only do a global cache flush while actively debugging (avoid in production)
if (defined(‘KP_DEBUG’) && KP_DEBUG && function_exists(‘wp_cache_flush’)) {
wp_cache_flush();
}
KP_LOG(“TIMER DEDUCTION: Player {$player_id} old={$old_total} new={$new_total} (locked transaction, timer reset). Next: ” . ($next ? $next[‘id’] : ‘none’) . “, Game state: ” . $event[‘game_state’]);
kp_bump_hint($event[‘id’], $slug);
return array(
‘success’ => true,
‘deducted’ => true,
‘player_id’ => $player_id,
‘old_lives’ => $old_total,
‘new_lives’ => $new_total,
‘next_player’ => $next ? $next[‘id’] : null,
‘timer_reset’ => true,
‘game_state’ => $event[‘game_state’]
);
}
// Manual life adjustment API – REGULAR LIVES ONLY for ±1 buttons
function kp_complete_api_adjust_life($request) {
// Verify nonce for security
if (!wp_verify_nonce($request->get_header(‘X-WP-Nonce’), ‘wp_rest’)) {
return new WP_Error(‘rest_forbidden’, ‘Nonce verification failed’, array(‘status’ => 403));
}
$slug = sanitize_title($request[‘slug’]);
$event = kp_complete_get_event($slug);
if (!$event) {
return new WP_Error(‘not_found’, ‘Event not found’, array(‘status’ => 404));
}
$body = json_decode($request->get_body(), true);
$player_id = intval($body[‘player_id’] ?? 0);
$adjustment = intval($body[‘adjustment’] ?? 0); // +1 or -1
if (!$player_id || ($adjustment !== 1 && $adjustment !== -1)) {
return new WP_Error(‘invalid_params’, ‘Invalid player ID or adjustment’, array(‘status’ => 400));
}
$player = kp_complete_get_player($player_id);
if (!$player || $player[‘event_id’] != $event[‘id’]) {
return new WP_Error(‘player_not_found’, ‘Player not found’, array(‘status’ => 404));
}
// Update manual ±1 adjustments to deduct from total lives (prefer regular, then black ball)
$regular_lives = intval($player[‘regular_lives’] ?? 0);
$black_ball_lives = intval($player[‘black_ball_lives’] ?? 0);
$old_total_lives = $regular_lives + $black_ball_lives;
$cap = intval($event[‘max_lives’] ?? 5);
if ($adjustment > 0) {
// Adding life: ONLY add to regular_lives, respect total cap
if ($old_total_lives < $cap) {
$regular_lives++;
} else {
// Already at total cap – reject
return new WP_Error(‘at_cap’, ‘Player already at maximum lives (‘ . $cap . ‘)’, array(‘status’ => 400));
}
} else {
// Removing life: prefer regular, then black ball (like missed/foul)
if ($regular_lives > 0) {
$regular_lives–;
} else if ($black_ball_lives > 0) {
$black_ball_lives–;
} else {
// No lives left to remove
return new WP_Error(‘no_lives’, ‘No lives to remove’, array(‘status’ => 400));
}
}
$new_total_lives = $regular_lives + $black_ball_lives;
// Update player by ID with proper field separation
kp_complete_update_player($player_id, [
‘regular_lives’ => $regular_lives,
‘black_ball_lives’ => $black_ball_lives
]);
$updated_player = kp_complete_get_player($player_id);
// Reactivate player ONLY if they were auto-eliminated (lives reached 0)
// Do NOT reactivate manually removed players (is_active=0 but lives>0)
if ($old_total_lives == 0 && $new_total_lives > 0 && !kp_complete_was_manually_removed($player_id)) {
kp_complete_update_player($player_id, [‘is_active’ => 1]);
}
// Eliminate player if they now have 0 lives
if ($new_total_lives == 0) {
kp_complete_update_player($player_id, [‘is_active’ => 0]);
}
// AGGRESSIVE CACHE CLEARING: Multiple layers to ensure no cache residue
// FIX: Using exact same comprehensive cache clearing as working remove player function
$event_id = $event[‘id’];
$event_slug = $slug;
// Clear WP object cache
if (function_exists(‘wp_cache_delete’)) {
wp_cache_delete(“kp_state_{$event_id}”, ‘killer_pool’);
wp_cache_delete(“kp_state_slug_{$event_slug}”, ‘killer_pool’);
wp_cache_delete(“kp_players_{$event_id}”, ‘killer_pool’);
wp_cache_delete(“kp_event_{$event_id}”, ‘killer_pool’);
wp_cache_flush_group(‘killer_pool’); // Clear entire group
}
// Clear transients
if (function_exists(‘delete_transient’)) {
delete_transient(“kp_state_{$event_id}”);
delete_transient(“kp_state_slug_{$event_slug}”);
delete_transient(“kp_players_{$event_id}”);
// Clear any possible event-specific transients
delete_transient(“kp_event_{$event_id}”);
delete_transient(“kp_alive_{$event_id}”);
}
// Clear any related object caches
wp_cache_delete(‘alloptions’, ‘options’);
wp_cache_delete(‘notoptions’, ‘options’);
// Log the action
$action_type = $adjustment > 0 ? ‘MANUAL_LIFE_ADD’ : ‘MANUAL_LIFE_REMOVE’;
$notes = ‘Manual ±1 adjustment (regular lives only): ‘ . ($adjustment > 0 ? ‘+1 regular’ : ‘-1 regular’) . ‘ -> regular=’ . $regular_lives . ‘, black_ball=’ . $black_ball_lives;
kp_complete_log_player_action($event[‘id’], $player_id, $action_type, $old_total_lives, $new_total_lives, $notes);
kp_bump_hint($event[‘id’], $slug);
return array(
‘success’ => true,
‘player’ => $player[‘display_name’],
‘old_lives’ => $old_total_lives,
‘new_lives’ => $new_total_lives,
‘adjustment’ => $adjustment,
‘regular_lives’ => $regular_lives,
‘black_ball_lives’ => $black_ball_lives,
‘adjustment_type’ => ‘regular_only’,
‘status’ => $new_total_lives > 0 ? ‘active’ : ‘eliminated’,
‘caches_purged’ => true
);
}
// Add player during game API
function kp_complete_api_add_player($request) {
// Verify nonce for security
if (!wp_verify_nonce($request->get_header(‘X-WP-Nonce’), ‘wp_rest’)) {
KP_LOG(“ADD: Nonce verification failed for slug {$request[‘slug’]}”);
return new WP_Error(‘rest_forbidden’, ‘Nonce verification failed’, array(‘status’ => 403));
}
$body = json_decode($request->get_body(), true);
$player_name = isset($body[‘player_name’]) ? sanitize_text_field($body[‘player_name’]) : ”;
$request_id = uniqid(‘add_’, true);
$timestamp = microtime(true);
$slug = sanitize_title($request[‘slug’]);
KP_LOG(“ADD START: Request ID={$request_id}, Timestamp={$timestamp}, Slug={$slug}, Player Name='{$player_name}'”);
$event = kp_complete_get_event($slug);
if (!$event) {
KP_LOG(“ADD: Event not found for slug {$slug}”);
return new WP_Error(‘not_found’, ‘Event not found’, array(‘status’ => 404));
}
if (empty($player_name)) {
KP_LOG(“ADD: Empty player name”);
return new WP_Error(‘invalid_name’, ‘Player name is required’, array(‘status’ => 400));
}
if (empty($player_name)) {
KP_LOG(“ADD: Empty player name”);
return new WP_Error(‘invalid_name’, ‘Player name is required’, array(‘status’ => 400));
}
// Check if player name already exists
global $wpdb;
$players_table = $wpdb->prefix . ‘kp_players’;
$sql_existing = $wpdb->prepare(
“SELECT COUNT(*) FROM $players_table WHERE event_id = %d AND display_name = %s AND is_active = 1”,
$event[‘id’], $player_name
);
$existing = $wpdb->get_var($sql_existing);
if ($existing > 0) {
KP_LOG(“ADD: Duplicate active player name ‘{$player_name}’ for event {$event[‘id’]}, count={$existing}”);
return new WP_Error(‘duplicate_name’, ‘Player name already exists’, array(‘status’ => 409));
}
// NEW LOG: Check for inactive same-name players
$sql_inactive = $wpdb->prepare(
“SELECT COUNT(*) FROM $players_table WHERE event_id = %d AND display_name = %s AND is_active = 0”,
$event[‘id’], $player_name
);
$inactive_count = $wpdb->get_var($sql_inactive);
KP_LOG(“ADD: Inactive players with name ‘{$player_name}’: {$inactive_count}”);
// NEW LOG: Total players before add
$total_before = $wpdb->get_var($wpdb->prepare(“SELECT COUNT(*) FROM $players_table WHERE event_id = %d”, $event[‘id’]));
KP_LOG(“ADD BEFORE: Total players for event {$event[‘id’]}: {$total_before}”);
// Log before add
KP_LOG(“ADD BEFORE: No existing active player with name ‘{$player_name}'”);
// Add the new player with 3 lives
$add_result = kp_complete_add_player($event[‘id’], $player_name, 3);
$player_id = $wpdb->insert_id;
if (!$add_result) {
KP_LOG(“ADD: Failed to add player ‘{$player_name}’, insert_id={$player_id}”);
return new WP_Error(‘failed_to_add’, ‘Failed to add player’, array(‘status’ => 500));
}
// Get the new player data
$new_player = kp_complete_get_player($player_id);
KP_LOG(“ADD AFTER: New player {$player_id} created: ” . json_encode($new_player));
// NEW LOG: Total players after add
$total_after = $wpdb->get_var($wpdb->prepare(“SELECT COUNT(*) FROM $players_table WHERE event_id = %d”, $event[‘id’]));
KP_LOG(“ADD AFTER: Total players for event {$event[‘id’]}: {$total_after}”);
// Log the action
kp_complete_log_player_action($event[‘id’], $player_id, ‘PLAYER_ADDED’, 0, 3, ‘Late player added to tournament’);
KP_LOG(“ADD: Action log entry created for player {$player_id}”);
kp_bump_hint($event[‘id’], $slug);
KP_LOG(“ADD END: Request ID={$request_id}, Timestamp={$timestamp}, Success=true, Player ID={$player_id}”);
return array(
‘success’ => true,
‘player’ => $new_player,
‘message’ => “Player ‘{$player_name}’ added with 3 lives”
);
}
// Remove player during game API – RACE-PROOF AND IDEMPOTENT (SOFT DELETE)
function kp_complete_api_remove_player( $request ) {
global $wpdb;
nocache_headers();
// Add custom headers to ensure no caching anywhere
header(‘Cache-Control: no-cache, no-store, must-revalidate, max-age=0, s-maxage=0’);
header(‘Pragma: no-cache’);
header(‘Expires: Thu, 01 Jan 1970 00:00:00 GMT’);
header(‘X-Accel-Expires: 0’);
KP_LOG( ‘REMOVE API HIT: Slug=’ . $request[‘slug’] . ‘, Nonce=’ . ( $request->get_header( ‘X-WP-Nonce’ ) ? ‘present’ : ‘missing’ ) );
$slug = sanitize_title( $request[‘slug’] );
$event = kp_complete_get_event( $slug );
if ( ! $event ) {
KP_LOG( ‘REMOVE: Event not found for slug ‘ . $slug );
return new WP_Error( ‘not_found’, ‘Event not found’, [ ‘status’ => 404 ] );
}
$body = json_decode( $request->get_body(), true );
$player_id = intval( $body[‘player_id’] ?? 0 );
if ( ! $player_id ) {
KP_LOG( ‘REMOVE: Missing player_id’ );
return new WP_Error( ‘invalid’, ‘Missing player_id’, [ ‘status’ => 400 ] );
}
KP_LOG( ‘REMOVE: Attempting to remove player ‘ . $player_id . ‘ from event ‘ . $event[‘id’] );
$players_table = $wpdb->prefix . ‘kp_players’;
// Get player info first for logging
$player = $wpdb->get_row( $wpdb->prepare( “SELECT * FROM $players_table WHERE id = %d AND event_id = %d”, $player_id, $event[‘id’] ), ARRAY_A );
if ( ! $player ) {
KP_LOG( ‘REMOVE: Player ‘ . $player_id . ‘ not found in event ‘ . $event[‘id’] );
return new WP_Error( ‘not_found’, ‘Player not found’, [ ‘status’ => 404 ] );
}
KP_LOG( ‘REMOVE: Found player ‘ . $player[‘display_name’] . ‘ (active=’ . $player[‘is_active’] . ‘, lives=’ . $player[‘lives’] . ‘)’ );
// CRITICAL FIX: Use database transaction to ensure atomicity
$wpdb->query(‘START TRANSACTION’);
try {
// Soft delete: Mark player as inactive if active (idempotent)
$updated = $wpdb->update( $players_table, [‘is_active’ => 0], [‘id’ => $player_id, ‘event_id’ => $event[‘id’], ‘is_active’ => 1] );
if ( $updated === false ) {
KP_LOG( ‘REMOVE FAIL: Update failed for player ‘ . $player_id . ‘, rows updated: ‘ . ( $updated ?: 0 ) . ‘, last query: ‘ . $wpdb->last_query . ‘, last error: ‘ . $wpdb->last_error );
$wpdb->query(‘ROLLBACK’);
return new WP_Error( ‘update_failed’, ‘Failed to remove player: ‘ . $wpdb->last_error, [ ‘status’ => 500 ] );
}
$remove_message = $updated === 0 ? ‘Player already removed’ : ‘Player removed’;
KP_LOG( ‘REMOVE: Successfully marked player ‘ . $player_id . ‘ as inactive’ );
// Clear if current player
$events_table = $wpdb->prefix . ‘kp_events’;
$cleared = $wpdb->update( $events_table, [ ‘current_player_id’ => 0, ‘timer_start_time’ => 0 ], [ ‘id’ => $event[‘id’], ‘current_player_id’ => $player_id ] );
KP_LOG( ‘REMOVE: Current player cleared, rows affected: ‘ . ( $cleared ?? 0 ) );
// Delete player history to clean up (also in transaction)
$actions_table = $wpdb->prefix . ‘kp_player_actions’;
$history_deleted = $wpdb->delete( $actions_table, [ ‘event_id’ => $event[‘id’], ‘player_id’ => $player_id ] );
KP_LOG( ‘REMOVE: Deleted ‘ . $history_deleted . ‘ history records’ );
// Commit transaction
$wpdb->query(‘COMMIT’);
} catch (Exception $e) {
$wpdb->query(‘ROLLBACK’);
KP_LOG( ‘REMOVE EXCEPTION: ‘ . $e->getMessage() );
return new WP_Error( ‘transaction_failed’, ‘Remove transaction failed’, [ ‘status’ => 500 ] );
}
// VERIFICATION: Log status after update (no longer blocking)
$verification = $wpdb->get_row( $wpdb->prepare( “SELECT is_active FROM $players_table WHERE id = %d AND event_id = %d”, $player_id, $event[‘id’] ) );
KP_LOG( ‘REMOVE VERIFICATION: Player ‘ . $player_id . ‘ is_active=’ . ($verification ? $verification->is_active : ‘not found’) );
KP_LOG( ‘REMOVE VERIFICATION PASSED: Player confirmed inactive’ );
// AGGRESSIVE CACHE CLEARING: Multiple layers to ensure no cache residue
$event_id = $event[‘id’];
$event_slug = $slug;
// Clear WP object cache
if (function_exists(‘wp_cache_delete’)) {
wp_cache_delete(“kp_state_{$event_id}”, ‘killer_pool’);
wp_cache_delete(“kp_state_slug_{$event_slug}”, ‘killer_pool’);
wp_cache_delete(“kp_players_{$event_id}”, ‘killer_pool’);
wp_cache_delete(“kp_event_{$event_id}”, ‘killer_pool’);
wp_cache_flush_group(‘killer_pool’); // Clear entire group
}
// Clear transients
if (function_exists(‘delete_transient’)) {
delete_transient(“kp_state_{$event_id}”);
delete_transient(“kp_state_slug_{$event_slug}”);
delete_transient(“kp_players_{$event_id}”);
// Clear any possible event-specific transients
delete_transient(“kp_event_{$event_id}”);
delete_transient(“kp_alive_{$event_id}”);
}
// Clear any related object caches
wp_cache_delete(‘alloptions’, ‘options’);
wp_cache_delete(‘notoptions’, ‘options’);
// Log removal
kp_complete_log_player_action( $event[‘id’], $player_id, ‘PLAYER_REMOVED’, intval( $player[‘lives’] ?? 0 ), 0, ‘Soft deleted – marked as inactive with transaction safety (no reactivation possible)’ );
// Select next player if needed (after all DB operations complete)
$next = kp_complete_select_next_player( $event[‘id’] );
KP_LOG( ‘REMOVE: Next player selection result: ‘ . ( $next ? $next[‘display_name’] : ‘none’ ) );
KP_LOG( ‘REMOVE FULL SUCCESS: Player ‘ . $player_id . ‘ (‘ . $player[‘display_name’] . ‘) marked as inactive with full transaction safety’ );
// SMALL DELAY to ensure all operations are fully committed
usleep(50000); // 50ms delay
kp_bump_hint($event[‘id’], $slug);
return [
‘success’ => true,
‘message’ => $remove_message . ‘: ‘ . $player[‘display_name’],
‘player_id’ => $player_id,
‘updated_rows’ => $updated,
‘history_deleted’ => $history_deleted,
‘timestamp’ => time(), // Add timestamp to prevent cache hits
‘verification_passed’ => true
];
}
// Get player history API
function kp_complete_api_get_player_history($request) {
$slug = sanitize_title($request[‘slug’]);
$player_id = intval($request[‘player_id’]);
$event = kp_complete_get_event($slug);
if (!$event) {
return new WP_Error(‘not_found’, ‘Event not found’, array(‘status’ => 404));
}
$player = kp_complete_get_player($player_id);
if (!$player || $player[‘event_id’] != $event[‘id’]) {
return new WP_Error(‘player_not_found’, ‘Player not found’, array(‘status’ => 404));
}
// Get player action history
$history = kp_complete_get_player_history($event[‘id’], $player_id);
return array(
‘success’ => true,
‘player’ => $player,
‘history’ => $history,
‘formatted_history’ => kp_format_player_history($history)
);
}
// Get current match history API
function kp_complete_api_get_current_match_history($request) {
$slug = sanitize_title($request[‘slug’]);
$event = kp_complete_get_event($slug);
if (!$event) {
return new WP_Error(‘not_found’, ‘Event not found’, array(‘status’ => 404));
}
$history_dir = __DIR__ . ‘/history’;
if (!is_dir($history_dir)) {
return array(‘history’ => [], ‘message’ => ‘History directory not found’);
}
$files = glob($history_dir . ‘/match_’ . $event[‘id’] . ‘_*.json’);
if (empty($files)) {
return array(‘history’ => [], ‘message’ => ‘No history file found yet’);
}
// Sort by timestamp descending to get latest
usort($files, function($a, $b) {
$a_parts = explode(‘_’, basename($a));
$a_ts = (int) $a_parts[2];
$b_parts = explode(‘_’, basename($b));
$b_ts = (int) $b_parts[2];
return $b_ts – $a_ts;
});
$latest_file = $files[0];
$data = json_decode(file_get_contents($latest_file), true);
if (!$data || !is_array($data)) {
return array(‘history’ => [], ‘message’ => ‘Invalid history data’);
}
return array(
‘history’ => $data,
‘timestamp’ => time(),
‘file’ => basename($latest_file)
);
}
/**
* Internal helper: run the existing manual-action switch/logic against the *current shooter*.
* ACTION one of: ‘potted’ | ‘missed’ | ‘foul’ | ‘black_potted’
*/
function kp_complete_api_apply_admin_action_for_current_shooter($slug, $action, $extra = array()) {
$event = kp_complete_get_event($slug);
if (!$event) {
return new WP_Error(‘no_event’, ‘Event not found’, array(‘status’ => 404));
}
// Force action to apply to the CURRENT shooter (ignore client ids)
$current = kp_complete_get_current_player($event[‘id’]);
if (!$current || empty($current[‘id’])) {
return new WP_Error(‘no_current’, ‘No current player to apply action’, array(‘status’ => 409));
}
$player_id = (int) $current[‘id’];
$player = kp_complete_get_player($player_id);
if (!$player || $player[‘event_id’] != $event[‘id’]) {
return new WP_Error(‘invalid_player’, ‘Invalid player’, [‘status’ => 404]);
}
$regular_lives = intval($player[‘regular_lives’] ?? 0);
$black_ball_lives = intval($player[‘black_ball_lives’] ?? 0);
$lives = $regular_lives + $black_ball_lives;
$message = ”;
$log_notes = ”;
switch ($action) {
case ‘potted’:
// Log successful shot
kp_complete_log_player_action($event[‘id’], $player_id, ‘POTTED’, $lives, $lives, ‘Potted ball successfully’);
$message = “{$player[‘display_name’]} potted a ball!”;
break;
case ‘missed’:
// Take regular lives first, then black ball lives
$regular_lives = intval($player[‘regular_lives’]);
$black_ball_lives = intval($player[‘black_ball_lives’]);
if ($regular_lives > 0) {
// Take from regular lives first
$new_regular = $regular_lives – 1;
$new_black_ball = $black_ball_lives;
$life_type = ‘regular’;
} else {
// Only take from black ball lives if no regular lives left
$new_regular = 0;
$new_black_ball = max(0, $black_ball_lives – 1);
$life_type = ‘black ball’;
}
$new_total_lives = $new_regular + $new_black_ball;
kp_complete_update_player($player_id, [
‘regular_lives’ => $new_regular,
‘black_ball_lives’ => $new_black_ball
]);
if ($new_total_lives == 0) {
kp_complete_update_player($player_id, [‘is_active’ => 0]);
$message = “💀 {$player[‘display_name’]} eliminated!”;
} else {
$message = “❌ {$player[‘display_name’]} missed – lost 1 {$life_type} life, now has {$new_total_lives} lives”;
}
// Log missed shot
kp_complete_log_player_action($event[‘id’], $player_id, ‘MISSED’, $lives, $new_total_lives, “Missed shot – lost 1 {$life_type} life”);
break;
case ‘foul’:
// Take regular lives first, then black ball lives
$regular_lives = intval($player[‘regular_lives’]);
$black_ball_lives = intval($player[‘black_ball_lives’]);
if ($regular_lives > 0) {
// Take from regular lives first
$new_regular = $regular_lives – 1;
$new_black_ball = $black_ball_lives;
$life_type = ‘regular’;
} else {
// Only take from black ball lives if no regular lives left
$new_regular = 0;
$new_black_ball = max(0, $black_ball_lives – 1);
$life_type = ‘black ball’;
}
$new_total_lives = $new_regular + $new_black_ball;
kp_complete_update_player($player_id, [
‘regular_lives’ => $new_regular,
‘black_ball_lives’ => $new_black_ball
]);
if ($new_total_lives == 0) {
kp_complete_update_player($player_id, [‘is_active’ => 0]);
$message = “💀 {$player[‘display_name’]} fouled and eliminated!”;
} else {
$message = “⚠️ {$player[‘display_name’]} fouled – lost 1 {$life_type} life, now has {$new_total_lives} lives”;
}
// Log foul
kp_complete_log_player_action($event[‘id’], $player_id, ‘FOUL’, $lives, $new_total_lives, “Committed foul – lost 1 {$life_type} life”);
break;
case ‘black_potted’:
// Check if black ball is enabled
if (!$event[‘black_ball_enabled’]) {
// Black ball disabled – just a regular shot
$message = “🖤 {$player[‘display_name’]} potted black ball (bonus disabled)”;
// Log as regular shot
kp_complete_log_player_action($event[‘id’], $player_id, ‘POTTED’, $lives, $lives, ‘Potted black ball – no bonus (disabled)’);
break;
}
// FIXED: Get proper field separation for black ball logic
$regular_lives = intval($player[‘regular_lives’] ?? 0);
$black_ball_lives = intval($player[‘black_ball_lives’] ?? 0);
$max_lives_cap = intval($event[‘max_lives’] ?? 5); // Use max_lives for total hard cap
$current_black_balls = intval($player[‘total_black_balls’] ?? 0);
// Get per-player black ball cap from WP options (not DB column)
$per_player_limit = intval(get_option(‘kp_bb_cap_event_’ . $event[‘id’], 1));
// Check all conditions for black ball bonus (BLACK BALL LIVES ONLY)
if ( ($regular_lives + $black_ball_lives) >= $max_lives_cap ) {
// Already at total hard cap
$message = “🖤 {$player[‘display_name’]} potted black ball – already at maximum total lives ({$max_lives_cap})”;
kp_complete_log_player_action($event[‘id’], $player_id, ‘POTTED’, $lives, $lives, ‘Potted black ball – at total max lives cap (‘ . $max_lives_cap . ‘)’);
} elseif ($per_player_limit < 999 && $current_black_balls >= $per_player_limit) {
// Player has reached their personal black ball limit
$message = “🖤 {$player[‘display_name’]} potted black ball – reached bonus limit ({$current_black_balls}/{$per_player_limit} per player)”;
kp_complete_log_player_action($event[‘id’], $player_id, ‘POTTED’, $lives, $lives, ‘Potted black ball – at personal bonus limit (‘ . $per_player_limit . ‘)’);
} else {
// Award BLACK BALL LIFE ONLY (not regular life)
$new_black_ball_lives = $black_ball_lives + 1;
$new_total_lives = $regular_lives + $new_black_ball_lives;
$new_black_balls = $current_black_balls + 1;
// CRITICAL: Enforce total cap
if ($new_total_lives > $max_lives_cap) {
$message = “🖤 {$player[‘display_name’]} potted black ball – would exceed maximum total lives ({$max_lives_cap})”;
kp_complete_log_player_action($event[‘id’], $player_id, ‘POTTED’, $lives, $lives, ‘Potted black ball – would exceed total cap (‘ . $max_lives_cap . ‘)’);
} else {
// Award the black ball life
kp_complete_update_player($player_id, [
‘black_ball_lives’ => $new_black_ball_lives,
‘total_black_balls’ => $new_black_balls
]);
$message = “🖤 {$player[‘display_name’]} potted black ball – gained bonus life! Total: {$new_total_lives} lives ({$new_black_balls}/{$per_player_limit} black ball bonuses used)”;
// Log black ball bonus
kp_complete_log_player_action($event[‘id’], $player_id, ‘BLACK_BALL’, $lives, $new_total_lives, ‘Potted black ball – gained 1 BLACK BALL life (total: ‘ . $new_total_lives . ‘, bonus: ‘ . $new_black_balls . ‘/’ . $per_player_limit . ‘)’);
}
}
break;
default:
return new WP_Error(‘bad_action’, ‘Unknown action’, array(‘status’=>400));
}
// === KP-GOV PATCH D: ledger ACTION_RECORDED (+optional ELIMINATED)
if (function_exists(‘kp_governor_ledger_log’)) {
// Read fresh post-action lives (works for all branches: potted/missed/foul/black_potted)
$after = kp_complete_get_player($player_id);
$after_regular = intval($after[‘regular_lives’] ?? 0);
$after_black = intval($after[‘black_ball_lives’] ?? 0);
$after_total = $after_regular + $after_black;
// Eliminated if is_active flipped off OR total lives is now 0
$eliminated = ((int)($after[‘is_active’] ?? 1) === 0 || $after_total <= 0) ? 1 : 0;
kp_governor_ledger_log($event[‘id’], ‘ACTION_RECORDED’, array(
‘action’ => $action,
‘player_id’ => (int)$player_id,
‘ts’ => time(),
‘lives’ => array(‘regular’ => $after_regular, ‘black’ => $after_black),
‘eliminated’ => $eliminated,
));
if ($eliminated) {
kp_governor_ledger_log($event[‘id’], ‘PLAYER_ELIMINATED’, array(
‘player_id’ => (int)$player_id,
‘ts’ => time(),
));
}
}
// === /KP-GOV PATCH D ===
// After processing the action, automatically select next player
$next_player = kp_complete_select_next_player($event[‘id’]);
if ($next_player) {
$message .= ” Next up: ” . $next_player[‘display_name’];
}
// Check if game should finish due to single survivor
// Guarded: don’t fatal if helper isn’t loaded yet
if (function_exists(‘kp_complete_finish_if_single_survivor’)) {
kp_complete_finish_if_single_survivor((int)$event[‘id’]);
}
// Return up-to-date state (so admin can re-render)
$req = new WP_REST_Request(‘GET’, ‘/killer-pool/v1/state’);
$req->set_param(‘slug’, $slug);
$state = kp_complete_api_get_event_data($req);
// === REVISION BUMP (no DB table needed) ===
if (isset($event[‘id’])) {
$rev_key = ‘kp_rev_’ . $event[‘id’];
$current_rev = (int) get_option($rev_key, 1);
update_option($rev_key, $current_rev + 1, false);
}
return array(
‘success’ => true,
‘action’ => $action,
‘message’ => $message,
‘current’ => $current,
‘state’ => $state,
);
}
function kp_complete_api_admin_action_potted(WP_REST_Request $req) {
$slug = sanitize_key( $req->get_param(‘slug’) );
$event_id = kp_event_id_from_slug($slug);
$result = kp_complete_api_apply_admin_action_for_current_shooter($slug, ‘potted’);
if (!is_wp_error($result) && isset($result[‘success’]) && $result[‘success’]) {
kp_bump_hint($event_id, $slug);
}
return $result;
}
function kp_complete_api_admin_action_missed(WP_REST_Request $req) {
$slug = sanitize_key( $req->get_param(‘slug’) );
$event_id = kp_event_id_from_slug($slug);
// KP_PATCH:kp_tournament_rules
kp_include_patch(‘kp_tournament_rules’, [
‘fn’ => __FUNCTION__ ?? null,
‘endpoint’ => ‘missed’,
]);
$result = kp_complete_api_apply_admin_action_for_current_shooter($slug, ‘missed’);
if (!is_wp_error($result) && isset($result[‘success’]) && $result[‘success’]) {
kp_bump_hint($event_id, $slug);
}
return $result;
}
function kp_complete_api_admin_action_foul(WP_REST_Request $req) {
$slug = sanitize_key( $req->get_param(‘slug’) );
$event_id = kp_event_id_from_slug($slug);
// KP_PATCH:kp_tournament_rules
kp_include_patch(‘kp_tournament_rules’, [
‘fn’ => __FUNCTION__ ?? null,
‘endpoint’ => ‘foul’,
]);
$result = kp_complete_api_apply_admin_action_for_current_shooter($slug, ‘foul’);
if (!is_wp_error($result) && isset($result[‘success’]) && $result[‘success’]) {
kp_bump_hint($event_id, $slug);
}
return $result;
}
function kp_complete_api_admin_action_black_potted(WP_REST_Request $req) {
$slug = sanitize_key( $req->get_param(‘slug’) );
$event_id = kp_event_id_from_slug($slug);
// KP_PATCH:kp_tournament_rules
kp_include_patch(‘kp_tournament_rules’, [
‘fn’ => __FUNCTION__ ?? null,
‘endpoint’ => ‘black_potted’,
]);
$result = kp_complete_api_apply_admin_action_for_current_shooter($slug, ‘black_potted’);
if (!is_wp_error($result) && isset($result[‘success’]) && $result[‘success’]) {
kp_bump_hint($event_id, $slug);
}
return $result;
}
// Player action logging system
function kp_complete_log_player_action($event_id, $player_id, $action_type, $lives_before, $lives_after, $notes = ”) {
global $wpdb;
$actions_table = $wpdb->prefix . ‘kp_player_actions’;
// Get next turn number for this event
$turn_number = $wpdb->get_var($wpdb->prepare(
“SELECT COALESCE(MAX(turn_number), 0) + 1 FROM $actions_table WHERE event_id = %d”,
$event_id
));
return $wpdb->insert($actions_table, [
‘event_id’ => $event_id,
‘player_id’ => $player_id,
‘turn_number’ => $turn_number,
‘action_type’ => $action_type,
‘lives_before’ => $lives_before,
‘lives_after’ => $lives_after,
‘notes’ => $notes
]);
}
// Get player action history
function kp_complete_get_player_history($event_id, $player_id) {
global $wpdb;
$actions_table = $wpdb->prefix . ‘kp_player_actions’;
return $wpdb->get_results($wpdb->prepare(
“SELECT * FROM $actions_table WHERE event_id = %d AND player_id = %d ORDER BY turn_number ASC”,
$event_id, $player_id
), ARRAY_A);
}
// Format player history for display
function kp_format_player_history($history) {
if (empty($history)) {
return ‘No actions recorded yet’;
}
$formatted = array();
foreach ($history as $action) {
$turn = $action[‘turn_number’];
$type = $action[‘action_type’];
// Convert action types to simple format
switch ($type) {
case ‘POTTED’:
$formatted[] = “Turn $turn: POTTED BALL”;
break;
case ‘MISSED’:
$formatted[] = “Turn $turn: MISSED (-1 life)”;
break;
case ‘BLACK_BALL’:
$formatted[] = “Turn $turn: BLACK BALL (+1 life)”;
break;
case ‘FOUL’:
$formatted[] = “Turn $turn: FOUL (-1 life)”;
break;
case ‘MANUAL_LIFE_ADD’:
$formatted[] = “Turn $turn: MANUAL +1 (Admin correction)”;
break;
case ‘MANUAL_LIFE_REMOVE’:
$formatted[] = “Turn $turn: MANUAL -1 (Admin correction)”;
break;
case ‘TIMER_AUTO_LOSS’:
$formatted[] = “Turn $turn: TIME UP (-1 life)”;
break;
case ‘PLAYER_ADDED’:
$formatted[] = “Turn $turn: JOINED TOURNAMENT”;
break;
case ‘PLAYER_REMOVED’:
$formatted[] = “Turn $turn: REMOVED FROM TOURNAMENT”;
break;
default:
$formatted[] = “Turn $turn: ” . strtoupper(str_replace(‘_’, ‘ ‘, $type));
}
}
return implode(‘, ‘, $formatted);
}
// Handle TV display requests (for public iframes)
add_action(‘init’, ‘kp_handle_tv_display’);
function kp_handle_tv_display() {
if (isset($_GET[‘kp_tv_display’]) && $_GET[‘kp_tv_display’] == ‘1’) {
$event_slug = sanitize_text_field($_GET[‘event’] ?? ‘demo’);
// Output minimal HTML page with just the tournament display
header(‘Content-Type: text/html; charset=UTF-8’);
echo ‘<!DOCTYPE html>’;
echo ‘<html><head>’;
echo ‘<meta charset=”UTF-8″>’;
echo ‘<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>’;
echo ‘<title>Killer Pool Tournament – ‘ . esc_html($event_slug) . ‘</title>’;
echo ‘<style>body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, “Segoe UI”, Roboto, sans-serif; }</style>’;
echo ‘</head><body>’;
// Display the tournament directly
echo kp_complete_public_shortcode([‘event’ => $event_slug]);
// === KP TV BOOTSTRAP (inline; this page bypasses wp_footer) ===
?>
<script>
(function(){
// expose slug for this page
window.eventSlug = window.eventSlug || <?php echo json_encode($event_slug); ?>;
// ———- Elim video: reliable autoplay + late-src handling ———-
function ensureAttrs(v){
if(!v) return;
try{
v.muted = true; v.autoplay = true; v.setAttribute(‘autoplay’,”);
v.playsInline = true; v.setAttribute(‘playsinline’,”);
v.preload = ‘auto’; v.controls = false;
v.disablePictureInPicture = true;
}catch(_){}
}
// gated + robust tryPlay
async function tryPlay(v, { timeout = 20000 } = {}) {
if (!v) return false;
// —- guards: don’t start until overlay is on and src exists
const wrap = document.getElementById(‘kp-elim’);
const isOn = !!(wrap && wrap.classList.contains(‘is-on’));
const src = (v.currentSrc || v.getAttribute(‘src’) || ”).trim();
if (!isOn || !src) return false;
try { ensureAttrs(v); } catch(_) {}
v.muted = true;
let started = false;
let killer = setTimeout(() => {
if (!started) {
try { v.pause(); } catch(_) {}
console.warn(‘KP ELIM: play() timeout – ending playback’);
try { if (typeof endPlayback === ‘function’) endPlayback(); } catch(_) {}
}
}, timeout);
try {
await v.play();
started = true;
} catch {
try { v.muted = true; await v.play(); started = true; } catch {}
}
clearTimeout(killer);
v.addEventListener(‘stalled’, () => {
console.warn(‘KP ELIM: stalled – ending playback’);
try { if (typeof endPlayback === ‘function’) endPlayback(); } catch(_) {}
}, { once:true });
v.addEventListener(‘error’, () => {
console.warn(‘KP ELIM: error – ending playback’);
try { if (typeof endPlayback === ‘function’) endPlayback(); } catch(_) {}
}, { once:true });
return started;
}
function wireVideo(v){
if(!v || v.__kpW) return; v.__kpW = 1; ensureAttrs(v);
const kick = ()=>{ ensureAttrs(v); tryPlay(v); };
[‘loadedmetadata’,’canplay’,’canplaythrough’,’playing’].forEach(e=>v.addEventListener(e,kick));
try{
new MutationObserver(muts=>{
for(const m of muts){ if(m.type===’attributes’ && m.attributeName===’src’){ kick(); break; } }
}).observe(v,{attributes:true,attributeFilter:[‘src’]});
}catch(_){}
kick();
}
// Minimal KP_ELIM API if missing
window.KP_ELIM = window.KP_ELIM || {};
if (typeof window.KP_ELIM.playElimClip !== ‘function’) {
window.KP_ELIM.playElimClip = function(opts={}){
const v = document.getElementById(‘kp-elim-video’);
if(!v) return false;
if (opts.src) { try{ v.src = opts.src; }catch(_){} }
wireVideo(v);
return true;
};
}
// Always wire the video when it appears
(function attach(){
const v = document.getElementById(‘kp-elim-video’); if(v) wireVideo(v);
new MutationObserver(()=>{ const vv = document.getElementById(‘kp-elim-video’); if(vv) wireVideo(vv); })
.observe(document.documentElement,{childList:true,subtree:true});
})();
// ———- Blob fallback on video error (TVs with flaky stream/range) ———-
(function(){
function install(){
const v = document.getElementById(‘kp-elim-video’);
if(!v || v.__kpBlob) return; v.__kpBlob = 1;
let tried = false;
v.addEventListener(‘error’, async () => {
if (tried) return; tried = true;
const src = v.currentSrc || v.src; if(!src) return;
// Hold forceEnd while we fetch
let unpatch = ()=>{};
try{
const noop = function(){};
if (window.KP_ELIM && typeof KP_ELIM.forceEnd === ‘function’) {
const old = KP_ELIM.forceEnd; KP_ELIM.forceEnd = noop; unpatch = ()=>{ KP_ELIM.forceEnd = old; };
} else if (typeof window.forceEndPlayback === ‘function’) {
const old = window.forceEndPlayback; window.forceEndPlayback = noop; unpatch = ()=>{ window.forceEndPlayback = old; };
}
}catch(_){}
// Try to keep memory reasonable: skip very large files (>20MB)
try{
const head = await fetch(src,{method:’HEAD’,cache:’no-store’,credentials:’same-origin’}).catch(()=>null);
const len = head ? +(head.headers.get(‘content-length’)||0) : 0;
if (len && len > 20_000_000) { unpatch(); return; }
}catch(_){}
try{
const resp = await fetch(src,{cache:’no-store’,credentials:’same-origin’});
if(!resp.ok){ unpatch(); return; }
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
try{ v.src = url; }catch(_){}
try{ await v.play(); }catch(_){ try{ v.muted = true; await v.play(); }catch(__){} }
setTimeout(()=>{ try{ URL.revokeObjectURL(url); }catch(_){ } unpatch(); }, 15000);
}catch(_){ try{ unpatch(); }catch(__){} }
}, {passive:true});
}
install();
new MutationObserver(install).observe(document.documentElement,{childList:true,subtree:true});
})();
// Kill preload<video> warnings (not needed here)
window.addEventListener(‘load’,()=> {
document.querySelectorAll(‘link[rel=”preload”][as=”video”]’).forEach(el=>el.parentNode.removeChild(el));
}, {once:true});
})();
</script>
<script>
/* === KP TV WATCHDOG + STATE TIMEOUT (append-only) ===
Purpose:
• Abort any stalled /state fetch (>4s) so the poll loop never freezes.
• If no state has been applied for ~9s while visible, nudge exactly once.
Non-destructive: wraps reads only; does not change write calls or cadence.
*/
(() => {
if (window.__KP_TV_WATCHDOG__) return; window.__KP_TV_WATCHDOG__ = true;
const IS_TV = !!(document.body.classList.contains(‘kp-tv’) || /\/event-/.test(location.pathname));
if (!IS_TV) return; // TV pages only
const TIMEOUT_MS = 4000; // abort any single state request taking > 4s
const STALE_MS = 9000; // if no fresh state for 9s, nudge once
const WATCHDOG_MS = 3500; // watchdog cadence
function fetchWithTimeout(input, init = {}) {
const ac = (‘signal’ in init) ? null : ((‘AbortController’ in window) ? new AbortController() : null);
const tid = setTimeout(() => { try { ac?.abort?.(); } catch(_){} }, TIMEOUT_MS);
const merged = {…init, …(ac ? { signal: ac.signal } : {})};
return fetch(input, merged).finally(() => clearTimeout(tid));
}
// Patch kpFetch for READS of /state only; leave everything else untouched
const origKpFetch = window.kpFetch;
if (typeof origKpFetch === ‘function’) {
window.kpFetch = function(path, opts = {}) {
try {
const base = (typeof window.restBase === ‘string’ ? window.restBase : (window.restBase || ”));
const url = base.replace(/\/+$/,’/’) + String(path).replace(/^\/+/,”);
if (/\/killer-pool\/v1\/state\//.test(url)) {
const bust = (url.includes(‘?’) ? ‘&’ : ‘?’) + ‘_ts=’ + Date.now();
const headers = { ‘Accept’:’application/json’, ‘Cache-Control’:’no-store’, …(opts.headers||{}) };
return fetchWithTimeout(url + bust, { credentials:’same-origin’, cache:’no-store’, headers, …opts });
}
} catch(_) {}
return origKpFetch.apply(this, arguments);
};
}
// Liveness tracking
let lastGood = Date.now();
function markGood(){ lastGood = Date.now(); }
// Mark “good” whenever your app applies state
window.addEventListener(‘kp:state:applied’, markGood);
// Safe state apply shim
function applyStateManually(state) {
try { if (typeof window.onStateUpdate === ‘function’) window.onStateUpdate(state); } catch(_){}
try { if (typeof window.updateFromState === ‘function’) window.updateFromState(state); } catch(_){}
try { window.__kpTimer?.applyFromState?.(state); } catch(_){}
markGood();
}
// Single manual read (used only if stale)
async function manualPollOnce(){
if (document.hidden) return; // don’t poke when hidden
try {
const slug = window.eventSlug || ”;
const base = (typeof window.restBase === ‘string’ ? window.restBase : (window.restBase || ”));
if (!base) return;
const url = base.replace(/\/+$/,’/’) + ‘state/’ + slug;
const bust = (url.includes(‘?’) ? ‘&’ : ‘?’) + ‘_ts=’ + Date.now();
const res = await fetchWithTimeout(url + bust, {
method:’GET’, credentials:’same-origin’, cache:’no-store’,
headers: { ‘Accept’:’application/json’, ‘Cache-Control’:’no-store’ }
});
if (!res.ok) throw new Error(‘state ‘ + res.status);
const st = await res.json();
applyStateManually(st);
console.debug(‘[KP TV WATCHDOG] manual poll applied’);
} catch(e) {
console.debug(‘[KP TV WATCHDOG] manual poll failed:’, e && (e.message||e));
}
}
// Prefer your primary one-shot if exposed; else do manual read
function nudgePrimaryPoller(){
try { if (typeof window.pollStateOnce === ‘function’) return void window.pollStateOnce(); } catch(_){}
return manualPollOnce();
}
// Also mark-good whenever your native handlers run
(function hookApply(){
const wrap = (obj, key) => {
const fn = obj[key];
if (typeof fn !== ‘function’) return;
obj[key] = function(){ try { const out = fn.apply(this, arguments); markGood(); return out; } catch(e){ throw e; } };
};
wrap(window, ‘onStateUpdate’);
wrap(window, ‘updateFromState’);
})();
// Watchdog loop: if stale while visible, nudge exactly once
setInterval(() => {
const staleFor = Date.now() – lastGood;
if (!document.hidden && staleFor > STALE_MS) {
console.debug(‘[KP TV WATCHDOG] stale’, staleFor, ‘→ nudge’);
nudgePrimaryPoller();
}
}, WATCHDOG_MS);
markGood(); // baseline
})();
</script>
<?php
// DO NOT auto-refresh this TV page; it interrupts playback
// echo ‘<script>setTimeout(function(){ location.reload(); }, 30000);</script>’;
echo ‘</body></html>’;
exit;
}
}
/** ———- Live Killer Page System (Manual Menu Control) ———- */
// Note: Auto-menu injection removed. Use WordPress menu manager to add links.
// Page functionality (/live-killer/) remains fully intact for manual menu linking.
// Handle live killer routing
add_action(‘init’, ‘kp_handle_live_killer_routing’);
function kp_handle_live_killer_routing() {
// Handle /live-killer/ URL
if (isset($_SERVER[‘REQUEST_URI’]) && preg_match(‘#^/live-killer/?$#’, $_SERVER[‘REQUEST_URI’])) {
kp_display_live_killer_page();
exit;
}
}
// Display live killer landing page
function kp_display_live_killer_page() {
$active_tournaments = kp_get_active_tournaments();
// If only one tournament, redirect directly
if (count($active_tournaments) == 1) {
$tournament = $active_tournaments[0];
if ($tournament[‘public_page_id’]) {
wp_redirect(get_permalink($tournament[‘public_page_id’]));
exit;
}
}
// Display tournament selection page (TV optimized)
header(‘Content-Type: text/html; charset=UTF-8’);
echo ‘<!DOCTYPE html>’;
echo ‘<html><head>’;
echo ‘<meta charset=”UTF-8″>’;
echo ‘<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>’;
echo ‘<title>Live Killer Tournaments</title>’;
echo ‘<style>’;
echo ‘body { margin: 0; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, “Segoe UI”, Roboto, sans-serif; background: linear-gradient(135deg, #1e293b 0%, #334155 100%); color: white; min-height: 100vh; }’;
echo ‘.tournament-button { display: block; width: 100%; max-width: 600px; margin: 20px auto; padding: 30px; background: linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%); color: white; text-decoration: none; border-radius: 20px; text-align: center; font-size: 24px; font-weight: 700; transition: all 0.3s ease; border: 3px solid rgba(255,255,255,0.2); }’;
echo ‘.tournament-button:hover, .tournament-button:focus { transform: scale(1.05); box-shadow: 0 10px 30px rgba(124, 58, 237, 0.5); }’;
echo ‘.no-tournaments { text-align: center; padding: 60px 20px; background: rgba(15, 23, 42, 0.8); border-radius: 20px; margin: 40px auto; max-width: 600px; }’;
echo ‘</style>’;
echo ‘</head><body>’;
echo ‘<div style=”text-align: center; margin-bottom: 40px;”>’;
echo ‘<h1 style=”font-size: clamp(2.5rem, 6vw, 4rem); margin: 0 0 10px 0; background: linear-gradient(45deg, #fbbf24, #f59e0b); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;”>🎱 Live Killer Tournaments</h1>’;
echo ‘<p style=”font-size: 18px; opacity: 0.8;”>Choose a tournament to watch</p>’;
echo ‘</div>’;
if (empty($active_tournaments)) {
echo ‘<div class=”no-tournaments”>’;
echo ‘<h2 style=”margin: 0 0 20px 0;”>No Active Tournaments</h2>’;
echo ‘<p style=”margin: 0; font-size: 18px; opacity: 0.8;”>Check back later or create a new tournament from the admin panel.</p>’;
echo ‘</div>’;
} else {
foreach ($active_tournaments as $tournament) {
if ($tournament[‘public_page_id’]) {
$tournament_url = get_permalink($tournament[‘public_page_id’]);
$player_count = kp_get_tournament_player_count($tournament[‘id’]);
$alive_count = kp_get_tournament_alive_count($tournament[‘id’]);
echo ‘<a href=”‘ . esc_url($tournament_url) . ‘” class=”tournament-button”>’;
echo ‘<div style=”font-size: 28px; margin-bottom: 10px;”>📺 ‘ . esc_html($tournament[‘name’]) . ‘</div>’;
echo ‘<div style=”font-size: 16px; opacity: 0.9;”>’;
echo esc_html($tournament[‘venue’]) . ‘ • ‘ . $alive_count . ‘/’ . $player_count . ‘ players’;
if ($tournament[‘game_state’] === ‘waiting’) {
echo ‘ • Starting Soon’;
} elseif ($tournament[‘game_state’] === ‘active’) {
echo ‘ • Live Now’;
} else {
echo ‘ • Finished’;
}
echo ‘</div>’;
echo ‘</a>’;
}
}
}
// Back to homepage link
echo ‘<div style=”text-align: center; margin-top: 40px;”>’;
echo ‘<a href=”‘ . home_url() . ‘” style=”color: rgba(255,255,255,0.7); text-decoration: none; font-size: 16px;”>← Back to Homepage</a>’;
echo ‘</div>’;
// Auto-refresh every 30 seconds to show new tournaments
echo ‘<script>setTimeout(function(){ location.reload(); }, 30000);</script>’;
echo ‘
</body></html>’;
}
// Get active tournaments (for menu system)
function kp_get_active_tournaments() {
global $wpdb;
$events_table = $wpdb->prefix . ‘kp_events’;
return $wpdb->get_results(
“SELECT * FROM $events_table
WHERE status = ‘active’
AND public_page_id > 0
ORDER BY created_at DESC”,
ARRAY_A
);
}
// Get tournament player count
function kp_get_tournament_player_count($event_id) {
global $wpdb;
$players_table = $wpdb->prefix . ‘kp_players’;
return $wpdb->get_var($wpdb->prepare(
“SELECT COUNT(*) FROM $players_table WHERE event_id = %d”,
$event_id
));
}
// Get tournament alive player count
function kp_get_tournament_alive_count($event_id) {
global $wpdb;
$players_table = $wpdb->prefix . ‘kp_players’;
return $wpdb->get_var($wpdb->prepare(
“SELECT COUNT(*)
FROM $players_table
WHERE event_id = %d
AND is_active = 1
AND (COALESCE(regular_lives,0) + COALESCE(black_ball_lives,0)) > 0″,
$event_id
));
}
// Get player’s last elimination time for cross-device video sync
function kp_get_player_last_elimination_time($event_id, $player_id) {
global $wpdb;
$actions_table = $wpdb->prefix . ‘kp_player_actions’;
// Find the most recent elimination action (when lives_after became 0)
return $wpdb->get_var($wpdb->prepare(
“SELECT created_at
FROM $actions_table
WHERE event_id = %d
AND player_id = %d
AND lives_after = 0
AND action_type IN (‘MISSED’, ‘FOUL’, ‘TIMER_AUTO_LOSS’, ‘PLAYER_REMOVED’)
ORDER BY created_at DESC
LIMIT 1″,
$event_id, $player_id
));
}
// Add rewrite rule for clean URLs
add_action(‘init’, ‘kp_add_live_killer_rewrite’);
function kp_add_live_killer_rewrite() {
add_rewrite_rule(‘^live-killer/?$’, ‘index.php?kp_live_killer=1’, ‘top’);
}
// Handle query vars
add_filter(‘query_vars’, ‘kp_add_query_vars’);
function kp_add_query_vars($vars) {
$vars[] = ‘kp_live_killer’;
return $vars;
}
// Handle template redirect for clean URL approach (alternative to direct handling)
add_action(‘template_redirect’, ‘kp_live_killer_template_redirect’);
function kp_live_killer_template_redirect() {
if (get_query_var(‘kp_live_killer’)) {
kp_display_live_killer_page();
exit;
}
}
// Flush rewrite rules on plugin activation
register_activation_hook(__FILE__, ‘kp_flush_rewrite_rules’);
function kp_flush_rewrite_rules() {
kp_add_live_killer_rewrite();
flush_rewrite_rules();
}
// Global footer centering CSS for public pages with
Tournament “demo” not found.
add_action(‘wp_head’, ‘kp_force_centering_css’);
function kp_force_centering_css() {
// Check if current page has
Tournament “demo” not found.
global $post;
$is_public_page = false;
if ($post) {
$content = $post->post_content;
if (has_shortcode($post->post_content, ‘killer_pool’) || get_post_meta($post->ID, ‘_kp_event_slug’, true)) {
$is_public_page = true;
}
}
if ($is_public_page) {
// Add body class for specific targeting
add_filter(‘body_class’, function($classes) {
$classes[] = ‘kp-public-page’;
return $classes;
});
?>
<style id=”kp-footer-centering”>
/* CRITICAL FOOTER FIX – FORCE CENTER ALWAYS FOR PUBLIC PAGES */
body.kp-public-page .elementor-footer, body.kp-public-page #footer, body.kp-public-page .site-footer, body.kp-public-page footer {
display: block !important;
position: static !important;
float: none !important;
clear: both !important;
width: 100% !important;
margin: 0 auto !important;
padding: 20px 0 !important;
text-align: center !important;
justify-content: center !important;
align-items: center !important;
}
body.kp-public-page .elementor-location-footer,
body.kp-public-page [data-elementor-type=”footer”] {
text-align: center !important;
display: flex !important;
justify-content: center !important;
width: 100% !important;
}
body.kp-public-page .elementor-footer *, body.kp-public-page #footer *, body.kp-public-page .site-footer *,
body.kp-public-page .elementor-location-footer *, body.kp-public-page [data-elementor-type=”footer”] * {
text-align: center !important;
margin-left: auto !important;
margin-right: auto !important;
float: none !important;
position: static !important;
}
body.kp-public-page .kp-public-wrapper {
min-height: 600px !important;
}
body.kp-public-page * [style*=”position: sticky”], body.kp-public-page * [style*=”position: fixed”] {
position: static !important;
}
body.kp-public-page, body.kp-public-page .site, body.kp-public-page .entry-content {
overflow-x: hidden !important;
}
body.kp-public-page .entry-content .kp-public-wrapper {
min-height: 600px !important;
display: block !important;
float: none !important;
}
</style>
<?php
}
}
// KP_PATCH:kp_global_after
kp_include_patch(‘kp_global_after’);
/* =======================================================================
KP END-SECTION — DEDUPED DROP-IN
Replaces everything from: // —- KP LEAN MODE … to file end
======================================================================= */
/* ================================
SECTION A — HEADER-SAFE SCRIPT SHELL (single source of truth)
================================ */
if ( ! function_exists(‘kp_can_echo_footer_js’) ) {
function kp_can_echo_footer_js() : bool {
// Block non-view contexts
if ( function_exists(‘wp_doing_ajax’) && wp_doing_ajax() ) return false;
if ( defined(‘REST_REQUEST’) && REST_REQUEST ) return false;
if ( defined(‘XMLRPC_REQUEST’) && XMLRPC_REQUEST ) return false;
if ( defined(‘DOING_CRON’) && DOING_CRON ) return false;
if ( is_feed() || is_embed() ) return false;
// Allow both front-end and admin views
return true;
}
}
if ( ! function_exists(‘kp_echo_footer_js’) ) {
/**
* Print a <script> block safely in footer, once per $id, on both FE and admin.
* Usage: kp_echo_footer_js(‘my-id’, ‘console.log(“hi”)’);
*/
function kp_echo_footer_js(string $id, string $js) : void {
static $done = [];
if ( isset($done[$id]) ) return;
$done[$id] = true;
$printer = function () use ($id, $js) {
if ( ! kp_can_echo_footer_js() ) return;
echo “<script id=\”kp-$id\”>\n{$js}\n</script>”;
};
add_action(‘wp_footer’, $printer, 99);
add_action(‘admin_footer’, $printer, 99);
}
}
/* ================================
SECTION B — LEAN MODE (hint-based sync; lightweight & fast)
– REST route + one inline JS (via kp_echo_footer_js)
================================ */
// =============================================================
// KILLER POOL — UNIFIED POLLER (FINAL SAFE BLOCK – NO MULTILINE STRINGS)
// =============================================================
if (!function_exists(‘kp_unified_poller_slug_and_loader’)) {
function kp_unified_poller_slug_and_loader() {
// 1. Detect slug from URL
$slug = ”;
if (!empty($_GET[‘event’])) {
$slug = sanitize_text_field($_GET[‘event’]);
} elseif (!empty($_GET[‘kp_event’])) {
$slug = sanitize_text_field($_GET[‘kp_event’]);
} elseif (!empty($_GET[‘event_slug’])) {
$slug = sanitize_text_field($_GET[‘event_slug’]);
}
// Output slug first (print into <head> so it always loads before poller)
if ($slug) {
$inline_slug = “window.kpEventSlug = ” . json_encode($slug) . “;”;
add_action(‘wp_head’, function() use ($inline_slug) {
echo “<script id=’kp-0_kp_slug’>{$inline_slug}</script>”;
}, 1);
add_action(‘admin_head’, function() use ($inline_slug) {
echo “<script id=’kp-0_kp_slug’>{$inline_slug}</script>”;
}, 1);
}
// 2. Load kp-unified-poller.js
$kp_unified_js_url = plugin_dir_url(__FILE__) . ‘assets/js/kp-unified-poller.js’;
$inline_loader = “(function(){ try { if (typeof window.__KP_UNIFIED_LOADER__ !== ‘undefined’) return; window.__KP_UNIFIED_LOADER__ = 1; var s = document.createElement(‘script’); s.src = ‘” . esc_url_raw($kp_unified_js_url) . “‘; s.async = false; s.defer = false; document.body.appendChild(s); } catch(e) { console.warn(‘KP unified poller loader error’, e); } })();”;
add_action(‘wp_footer’, function() use ($inline_loader) {
echo “<script id=’kp-unified-poller-loader’>{$inline_loader}</script>”;
}, 999);
add_action(‘admin_footer’, function() use ($inline_loader) {
echo “<script id=’kp-unified-poller-loader’>{$inline_loader}</script>”;
}, 999);
}
// Execute loader
kp_unified_poller_slug_and_loader();
}
if ( ! function_exists(‘kp_register_hint_route’) ) {
function kp_register_hint_route() {
register_rest_route(‘killer-pool/v1’, ‘/hint/(?P<slug>[a-zA-Z0-9\-_.]+)’, [
[
‘methods’ => WP_REST_Server::READABLE, // GET
‘permission_callback’ => ‘__return_true’,
‘callback’ => function (WP_REST_Request $req) {
$raw = sanitize_text_field($req[‘slug’]);
// normalize admin-/tv- → event-
if (preg_match(‘/event-[a-z0-9\-]+/i’, $raw, $m)) { $slug = $m[0]; }
else { $slug = preg_replace(‘/^(admin|tv)-/i’,’event-‘,$raw); }
$ts = (int) get_transient(‘kp_hint_’ . $slug);
return new WP_REST_Response([‘ts’ => $ts ?: 0], 200);
},
],
[
‘methods’ => WP_REST_Server::CREATABLE, // POST
‘permission_callback’ => ‘__return_true’,
‘callback’ => function (WP_REST_Request $req) {
if (!is_user_logged_in() || !current_user_can(‘edit_posts’)) {
return new WP_REST_Response([‘error’ => ‘forbidden’], 403);
}
$raw = sanitize_text_field($req[‘slug’]);
if (preg_match(‘/event-[a-z0-9\-]+/i’, $raw, $m)) { $slug = $m[0]; }
else { $slug = preg_replace(‘/^(admin|tv)-/i’,’event-‘,$raw); }
$ts = time();
set_transient(‘kp_hint_’ . $slug, $ts, 30 * DAY_IN_SECONDS);
return new WP_REST_Response([‘ts’ => $ts], 200);
},
],
]);
}
}
add_action(‘rest_api_init’, ‘kp_register_hint_route’);
if ( ! function_exists(‘kp_install_lean_mode’) ) {
function kp_install_lean_mode() {
static $installed = false; if ($installed) return; $installed = true;
kp_echo_footer_js(‘lean-mode’, <<<‘JS’
(function(){
if (window.__KP_LEAN_MODE) return; window.__KP_LEAN_MODE = 1;
// Resolve REST base + slug (no optional chaining to avoid older engines barfing)
function restBase(){
var b = (window.restBase || ‘/wp-json/killer-pool/v1/’);
return String(b).replace(/\/+$/,’/’) ;
}
function readAttr(el, name){ return el && el.getAttribute ? el.getAttribute(name) : null; }
function slug(){
if (window.eventSlug) return window.eventSlug;
var el = document.querySelector(‘[data-event-slug]’);
var s = readAttr(el, ‘data-event-slug’);
return s || null;
}
// Canonicalize admin-/tv- → event-
function canon(raw){
if (!raw) return ”;
var m = String(raw).match(/event-[a-z0-9\-]+/i);
return m ? m[0] : String(raw).replace(/^(admin|tv)-/i,’event-‘);
}
var SLUG = canon(slug());
if (!SLUG) return;
var HINT_GET = restBase() + ‘hint/’ + SLUG;
window.SLUG = SLUG;
window.HINT_GET = HINT_GET;
// — ADMIN: send a tiny hint after any state-changing form submit
(function adminHint(){
if (!document || !document.body) return;
var isAdmin = !!document.querySelector(‘#wpadminbar’);
if (!isAdmin) return;
function sendHint(){
var url = HINT_GET;
var nonce = (window.wpApiSettings && window.wpApiSettings.nonce) ? window.wpApiSettings.nonce
: (window.KP_NONCE || ”);
try {
if (navigator && navigator.sendBeacon) {
var blob = new Blob([‘{}’], {type:’application/json’});
navigator.sendBeacon(url, blob);
} else {
fetch(url, {method:’POST’, credentials:’same-origin’, keepalive:true,
headers: {‘Content-Type’:’application/json’,’X-WP-Nonce’:nonce}, body:'{}’
})[“catch”](function(){});
}
} catch(_){}
}
document.addEventListener(‘submit’, function(ev){
var f = ev.target; if (!f || !f.tagName || f.tagName !== ‘FORM’) return;
var changing = f.querySelector(‘input[name=”add_player”], input[name=”player_action”], input[name=”adjust_life”], input[name=”remove_player”], input[name=”force_select”], input[name=”foul”], input[name=”missed”], input[name=”black”], input[name=”timer_expired”], input[name=”undo_last”], input[name=”winner_action”]’);
if (changing) { setTimeout(sendHint, 150); setTimeout(sendHint, 1200); }
}, true);
})();
// — TV/Public: poll cheap hint; only fetch state when hint changes (HARDENED)
(function tvWatch() {
if (window.__KP_HINT_WATCHER__) return;
window.__KP_HINT_WATCHER__ = true;
var lastTs = 0;
var backoff = 2000;
var consecutiveErrors = 0;
var unloading = false;
// Stop rescheduling while navigating/reloading
window.addEventListener(‘beforeunload’, function(){ unloading = true; });
window.addEventListener(‘pagehide’, function(){ unloading = true; });
function paceDelay() {
return (document.visibilityState === ‘hidden’) ? Math.max(backoff, 5000) : backoff;
}
async function loop() {
if (unloading) return;
// Abort long/hung requests so they don’t throw scary errors
var ctl = (typeof AbortController !== ‘undefined’) ? new AbortController() : null;
var sig = ctl ? { signal: ctl.signal } : {};
var killer = ctl ? setTimeout(function(){ try{ ctl.abort(); }catch(_){ } }, 9000) : null;
try {
// Quick offline guard (TVs can drop Wi-Fi briefly)
if (navigator && navigator.onLine === false) throw new Error(‘offline’);
var r = await fetch(HINT_GET, Object.assign({ cache:’no-store’, credentials:’same-origin’ }, sig));
if (!r.ok) throw new Error(‘HTTP ‘ + r.status);
var j = await r.json();
var ts = (j && j.ts) ? j.ts : 0;
if (ts > lastTs) {
lastTs = window.kpHintTs = ts;
if (window.KP_ELIM && window.KP_ELIM.isPlaying) {
window.KP_ELIM.pendingRefresh = true; // don’t reflow mid-clip
} else {
try { await fetchStateOnce(); } catch (_) {}
}
} else if (Math.random() < 0.1) {
console.log(‘KP TV: No hint changes (ts=’ + ts + ‘)’);
}
backoff = 2000; // healthy
consecutiveErrors = 0; // reset
} catch (e) {
// Treat reload aborts/offline as benign → stay quiet
var msg = e && e.message ? e.message : String(e);
var aborted = (e && e.name === ‘AbortError’) || /Failed to fetch/i.test(msg) || /offline/i.test(msg);
if (!aborted) {
consecutiveErrors++;
console.warn(‘KP TV: Hint poll error #’ + consecutiveErrors + ‘:’, msg);
if (consecutiveErrors >= 5) console.error(‘KP TV: Multiple consecutive hint poll failures’);
}
backoff = Math.min((backoff || 2000) * 1.2, 8000);
} finally {
if (killer) { try { clearTimeout(killer); } catch(_){ } }
if (!unloading) setTimeout(loop, paceDelay());
}
}
console.log(‘KP TV: Starting hint-based polling for’, SLUG, ‘→’, HINT_GET);
loop();
// Manual trigger (unchanged)
window.kpTriggerHintUpdate = function() {
console.log(‘KP TV: Manual hint update triggered’);
if (typeof updateTournamentData === ‘function’) {
try { updateTournamentData(); } catch (e) { console.error(‘KP TV: Manual update failed:’, e); }
}
if (typeof refreshPlayersSection === ‘function’) {
try { refreshPlayersSection(); } catch (_) {}
} else if (typeof updateAdminFromState === ‘function’) {
try { updateAdminFromState(); } catch (_) {}
}
};
})();
})();
JS
);
}
}
// Ensure the lean-mode JS is printed (and only once)
if (function_exists(‘kp_install_lean_mode’)) {
kp_install_lean_mode();
}
/* ================================
SECTION C — CHIPBAR CONTAINER (FE + Admin)
================================ */
if ( ! function_exists(‘kp_chipbar_echo_container’) ) {
function kp_chipbar_echo_container() : void {
if ( function_exists(‘kp_can_echo_footer_js’) && ! kp_can_echo_footer_js() ) return;
echo ‘<div id=”kp-chipbar” class=”kp-chipbar” aria-live=”polite”></div>’;
}
}
if ( ! has_action(‘wp_footer’, ‘kp_chipbar_echo_container’) ) {
add_action(‘wp_footer’, ‘kp_chipbar_echo_container’, 20);
}
if ( ! has_action(‘admin_footer’, ‘kp_chipbar_echo_container’) ) {
add_action(‘admin_footer’, ‘kp_chipbar_echo_container’, 20);
}
/* ================================
SECTION D — CHIPBAR MOVER (place #kp-chipbar under Admin Action Panel)
================================ */
if ( ! function_exists(‘kp_move_chipbar_below_action_panel’) ) {
function kp_move_chipbar_below_action_panel() : void { ?>
<script id=”kp-move-chipbar”>
(function(){
if (window.__KP_MOVE_CHIPBAR__) return;
window.__KP_MOVE_CHIPBAR__ = true;
const ANCHOR_SELECTORS = [
‘#admin-action-box’,
‘#kp-admin-actions’,
‘#kp-action-panel’,
‘.kp-admin-actions’,
‘.kp-admin-action-panel’,
‘.kp-admin-panel’,
‘[data-kp-admin-actions]’
];
const BAR_ID = ‘kp-chipbar’;
const RETRY_MS = 400;
function findAnchor(){
for (const sel of ANCHOR_SELECTORS){
const el = document.querySelector(sel);
if (el) return el;
}
return null;
}
function moveOnce(){
const bar = document.getElementById(BAR_ID);
const anchor = findAnchor();
if (!bar || !anchor) { setTimeout(moveOnce, RETRY_MS); return; }
if (anchor.nextElementSibling === bar) return;
try {
bar.style.position = ‘static’;
bar.style.marginTop = ‘8px’;
bar.style.maxWidth = ‘100%’;
anchor.insertAdjacentElement(‘afterend’, bar);
} catch(_){}
}
const mo = new MutationObserver(() => {
const bar = document.getElementById(BAR_ID);
const anchor = findAnchor();
if (bar && anchor && anchor.nextElementSibling !== bar) moveOnce();
});
try { mo.observe(document.body, { childList:true, subtree:true }); } catch(_){}
if (document.readyState === ‘loading’) {
document.addEventListener(‘DOMContentLoaded’, moveOnce, { once:true });
} else {
moveOnce();
}
})();
</script>
<style id=”kp-chipbar-mover-styles”>
#kp-chipbar { display:block; position:static !important; width:100%; }
</style>
<?php }
}
if ( ! has_action(‘wp_footer’, ‘kp_move_chipbar_below_action_panel’) ) {
add_action(‘wp_footer’, ‘kp_move_chipbar_below_action_panel’, 100);
}
if ( ! has_action(‘admin_footer’, ‘kp_move_chipbar_below_action_panel’) ) {
add_action(‘admin_footer’, ‘kp_move_chipbar_below_action_panel’, 100);
}
/* ================================
SECTION E — ELIM VIDEO attach (header-safe)
================================ */
kp_echo_footer_js(‘elim-video-attach’, <<<‘JS’
(function(){
if (window.__KP_ELIM_MINI_V2) return;
window.__KP_ELIM_MINI_V2 = 1;
function onKPPage(){
return document.body.classList.contains(‘kp-public-page’) || !!document.getElementById(‘kp-elim-video’);
}
if (!onKPPage()) return;
function ensureAttrs(v){
if(!v) return;
try{
v.muted = true; v.autoplay = true; v.setAttribute(‘autoplay’,”);
v.playsInline = true; v.setAttribute(‘playsinline’,”);
v.preload = ‘auto’; v.controls = false;
v.disablePictureInPicture = true;
}catch(_){}
}
async function tryPlay(v, { timeout = 20000 } = {}) {
if (!v) return false;
const wrap = document.getElementById(‘kp-elim’);
const isOn = !!(wrap && wrap.classList.contains(‘is-on’));
const src = (v.currentSrc || v.getAttribute(‘src’) || ”).trim();
if (!isOn || !src) return false;
try { ensureAttrs(v); } catch(_) {}
v.muted = true;
let started = false;
const killer = setTimeout(() => {
if (!started) {
try { v.pause(); } catch(_) {}
console.warn(‘KP ELIM: play() timeout – ending playback’);
try { if (typeof endPlayback === ‘function’) endPlayback(); } catch(_) {}
}
}, timeout);
try { await v.play(); started = true; }
catch { try { v.muted = true; await v.play(); started = true; } catch {} }
clearTimeout(killer);
if (started) {
v.addEventListener(‘playing’, function onPlaying(){
v.removeEventListener(‘playing’, onPlaying);
setTimeout(() => { try { v.muted = false; } catch(_) {} }, 300);
}, { once:true });
}
v.addEventListener(‘stalled’, () => {
console.warn(‘KP ELIM: stalled – ending playback’);
try { if (typeof endPlayback === ‘function’) endPlayback(); } catch(_) {}
}, { once:true });
v.addEventListener(‘error’, () => {
console.warn(‘KP ELIM: error – ending playback’);
try { if (typeof endPlayback === ‘function’) endPlayback(); } catch(_) {}
}, { once:true });
return started;
}
function wireVideo(v){
if (!v || v.__kpElimWired) return;
v.__kpElimWired = true;
ensureAttrs(v);
const kick = () => { ensureAttrs(v); tryPlay(v); };
[‘loadedmetadata’,’canplay’,’canplaythrough’,’playing’].forEach(evt =>
v.addEventListener(evt, kick, {once:false})
);
try {
const mo = new MutationObserver(muts => {
for (const m of muts) {
if (m.type === ‘attributes’ && m.attributeName === ‘src’) { kick(); break; }
}
});
mo.observe(v, {attributes:true, attributeFilter:[‘src’]});
} catch(_) {}
kick();
let retried = false;
v.addEventListener(‘error’, () => {
if (!retried) { retried = true; try { v.load(); } catch(_) {} setTimeout(kick, 150); }
});
}
// Minimal KP_ELIM if missing
window.KP_ELIM = window.KP_ELIM || {};
if (typeof window.KP_ELIM.playElimClip !== ‘function’) {
window.KP_ELIM.playElimClip = function(opts={}) {
const v = document.getElementById(‘kp-elim-video’);
if (!v) return false;
if (opts.src) { try { v.src = opts.src; } catch(_) {} }
wireVideo(v);
return true;
};
}
(function attach(){
const v = document.getElementById(‘kp-elim-video’);
if (v) wireVideo(v);
new MutationObserver(() => {
const vv = document.getElementById(‘kp-elim-video’);
if (vv) wireVideo(vv);
}).observe(document.documentElement, {childList:true, subtree:true});
})();
})();
JS);
/* ================================
SECTION F — CHIP HISTORY (only actions: ✅ ❌ 🎱) — de-duped & guarded
================================ */
kp_echo_footer_js(‘chip-history’, <<<‘JS’
(function(){
if (window.__KP_CHIPS__) return;
window.__KP_CHIPS__ = true;
// Minimal addChip if none exists
if (typeof window.addChip !== ‘function’) {
window.addChip = function(emoji, playerId, playerName){
try {
const bar = document.getElementById(‘kp-chipbar’);
if (!bar) return;
const el = document.createElement(‘div’);
el.className = ‘kp-chip’;
el.textContent = String(emoji || ”) + (playerName ? ‘ ‘ + playerName : ”);
el.dataset.kpTs = String(Date.now()); // for time-aware de-dupe
bar.appendChild(el);
} catch(_) {}
};
}
// Allow only action emojis (drop 🎯 etc.)
function isActionEmojiText(txt){ return /^(✅|❌|🎱)/.test(String(txt||”)); }
function normText(el){ return String((el && el.textContent) || ”).replace(/\s+/g,’ ‘).trim(); }
// Install guarded append/insert on #kp-chipbar
(function installChipbarGuards(){
const bar = document.getElementById(‘kp-chipbar’);
if (!bar) return;
const origAppend = bar.appendChild.bind(bar);
const origInsert = bar.insertBefore.bind(bar);
function findChip(node){
if (!node || node.nodeType !== 1) return null;
return node.classList.contains(‘kp-chip’) ? node : node.querySelector(‘.kp-chip’);
}
function wouldDuplicate(node, refNode){
const newChip = findChip(node);
if (!newChip) return false;
let prev = null;
if (refNode && refNode.previousElementSibling) {
prev = findChip(refNode.previousElementSibling);
} else {
const chips = bar.querySelectorAll(‘.kp-chip’);
prev = chips.length ? chips[chips.length-1] : null;
}
if (!prev) return false;
const sameText = normText(prev) === normText(newChip);
const now = Date.now();
const prevTs = parseInt(prev.dataset?.kpTs || ‘0’, 10) || 0;
const newTs = parseInt(newChip.dataset?.kpTs || String(now), 10) || now;
// Only swallow identicals within 300ms (allow legit fast repeats)
return sameText && (newTs – prevTs) < 300;
}
bar.appendChild = function(node){
const chip = findChip(node);
if (chip && !isActionEmojiText(normText(chip))) return node; // drop non-action
if (wouldDuplicate(node, null)) return node;
return origAppend(node);
};
bar.insertBefore = function(node, ref){
const chip = findChip(node);
if (chip && !isActionEmojiText(normText(chip))) return node; // drop non-action
if (wouldDuplicate(node, ref)) return node;
return origInsert(node, ref);
};
})();
// Map type→emoji
function emojiFor(t){
if (t === ‘pot’ || t === ‘potted’) return ‘✅’;
if (t === ‘miss’) return ‘❌’;
if (t === ‘black’ || t === ‘BLACK_BALL’ || t === ‘black_potted’) return ‘🎱’;
if (t === ‘eliminated’) return ‘💀’;
if (t === ‘winner’) return ‘🏆’;
return ‘🎯’;
}
function getPlayerNameFromState(state, id){
const arr = state?.players || state?.participants || state?.player_list || [];
const p = arr.find(x => String(x.player_id ?? x.id) === String(id));
return p?.display_name || p?.name || ”;
}
function extractAction(state){
const a = state?.last_action || state?.recent_action || state?.action || null;
if (a && (a.type || a.action_type) && (a.player_id != null)) return {
type: a.type || a.action_type,
player_id: a.player_id,
player_name: a.player_name || null,
ts: a.ts || a.timestamp || a.time || Date.now()
};
let arr = state?.elimination_events || state?.actions || state?.recent_actions || [];
if (!Array.isArray(arr) || arr.length === 0) {
arr = state?.event?.actions || state?.game?.actions || state?.tournament?.actions || [];
}
if (!Array.isArray(arr) || arr.length === 0) {
const actionKeys = Object.keys(state || {}).filter(key =>
key.toLowerCase().includes(‘action’) && Array.isArray(state[key])
);
if (actionKeys.length > 0) arr = state[actionKeys[0]];
}
const last = Array.isArray(arr) && arr.length ? arr[arr.length-1] : null;
if (last && (last.type || last.action_type) && (last.player_id != null)) return {
type: last.type || last.action_type,
player_id: last.player_id,
player_name: last.player_name || null,
ts: last.ts || last.timestamp || last.time || Date.now()
};
return null;
}
function pushChipFromState(state){
try {
const a = extractAction(state||{});
if (!a) return;
const stamp = Number(a.ts) || 0;
if (stamp && stamp === (window.__kpLastChipTs||0)) return;
window.__kpLastChipTs = stamp || Date.now();
const name = getPlayerNameFromState(state||{}, a.player_id);
let displayName = name;
if (a.type === ‘miss’) {
displayName += ‘ (-1)’;
} else if (a.type === ‘black’ || a.type === ‘BLACK_BALL’ || a.type === ‘black_potted’) {
if (a.old_lives !== undefined && a.new_lives !== undefined) {
const diff = a.new_lives – a.old_lives;
if (diff === 1) displayName += ‘ (+1)’;
else if (diff === 0) displayName += ‘ (0)’;
}
}
const emoji = emojiFor(a.type);
try { window.addChip(emoji, a.player_id, displayName); } catch(_) {}
} catch(_) {}
}
// Hook common updaters
(function hookState(){
const PREV = (typeof window.onStateUpdate === ‘function’) ? window.onStateUpdate : null;
window.onStateUpdate = function(d){
try {
pushChipFromState(d || window.__kpLastState || {});
const eliminations = d?.elimination_events || [];
eliminations.forEach(event => {
try { pushChipFromState({ elimination_events: [event] }); } catch(_) {}
});
} catch(_) {}
if (typeof PREV === ‘function’) return PREV(d);
};
})();
// Admin success: detect REST action URLs and push instantly
(function hookFetch(){
if (window.__KP_FETCH_HOOKED__) return; window.__KP_FETCH_HOOKED__ = true;
const origFetch = window.fetch;
if (typeof origFetch !== ‘function’) return;
window.fetch = function(){
const args = arguments;
const url = String(args && args[0] || ”);
return origFetch.apply(this, args).then(async (res) => {
try {
if (res && res.ok && /(\/potted\b|\/missed\b|\/black\b|player[_-]?action)/.test(url)) {
let t = null;
if (/\/potted\b/.test(url)) t = ‘pot’;
else if (/\/missed\b/.test(url)) t = ‘miss’;
else if (/\/black\b/.test(url)) t = ‘black’;
else {
try {
const clone = res.clone();
const data = await clone.json();
t = data?.action?.type || data?.type || null;
} catch(_){}
}
let id = null;
try { id = (window.__kpLastState?.current_player?.player_id) ?? null; } catch(_){}
const name = getPlayerNameFromState(window.__kpLastState || {}, id);
if (t) { try { window.addChip(emojiFor(t), id, name); } catch(_) {} }
}
} catch(_) {}
return res;
});
};
})();
})();
//=========================================
// KP GOVERNOR v4.1 — MASTER (single owner)
// Anchor: KP_GOVERNOR_V41_MASTER
//========================================= –>
(() => {
// Avoid double install
if (window.__KP_GOV_V41__) return;
window.__KP_GOV_V41__ = true;
const KP_LOG = /[?&]kpdebug=1/i.test(location.search);
// — ADMIN GUARD: never run on admin pages —
const isAdmin =
/[?&]kpadmin=1/i.test(location.search) ||
!!document.querySelector(‘#admin-action-box’) ||
/\/admin-\d{4}-\d{2}-\d{2}-/i.test(location.pathname);
if (isAdmin) return; // governor is TV-only
// Robust event slug detection (TV)
const slug =
window.eventSlug ||
document.querySelector(‘[data-event-slug]’)?.getAttribute(‘data-event-slug’) ||
((location.pathname.match(/event-[a-z0-9\-]+/i) || [])[0] || ”);
if (!slug) {
if (KP_LOG) console.warn(‘KP GOV v4.1: no slug; abort’);
return;
}
// — Neutralize legacy polling —
function killKnown() {
const names = [
‘kpPollTimer’,’pollInterval’,’pollTimer’,’timerInterval’,
‘__KP_TV_POLL’,’__KP_GOV_TMR’,’__kpAdminPoll’,’__KP_ADMIN_POLL’
];
for (const n of names) {
try {
if (window[n]) {
clearInterval(window[n]);
clearTimeout(window[n]);
window[n] = null;
}
} catch(_) {}
}
}
killKnown();
// Block any future setInterval(updateTournamentData)
(function patchSetInterval(){
if (window.__KP_GOV_V41_PATCHED__) return;
const _setInterval = window.setInterval.bind(window);
window.setInterval = function(fn, ms, …rest){
try {
const isLegacy =
(fn === window.updateTournamentData) ||
(typeof fn === ‘function’ &&
(fn.name === ‘updateTournamentData’ ||
String(fn).includes(‘updateTournamentData’)));
if (isLegacy) {
if (KP_LOG) console.info(‘KP GOV v4.1: blocked legacy setInterval(updateTournamentData,’, ms, ‘)’);
return _setInterval(()=>{}, 2147483647); // harmless dummy
}
} catch (_) {}
return _setInterval(fn, ms, …rest);
};
window.__KP_GOV_V41_PATCHED__ = true;
})();
// — Hint-first triggers —
try {
const bc = new BroadcastChannel(‘kp-hint’);
bc.onmessage = () => {
if (KP_LOG) console.log(‘KP GOV v4.1: hint bump’);
schedule(0);
};
} catch(_) {}
// Adaptive cadence
let aliveCount = 99;
let lastStateTs = 0;
let currentDelay = 4000;
let timer = null;
// Listen for state updates
document.addEventListener(‘kp:state’, (ev) => {
const s = ev.detail || {};
const e = s.event || s;
const ts = Number(e?.last_change_ts || e?.ts || 0);
if (ts) lastStateTs = ts;
if (Array.isArray(s.alive_players)) {
aliveCount = s.alive_players.length;
} else if (Array.isArray(e?.players)) {
aliveCount = e.players.filter(p => Number(p?.lives ?? p?.life ?? 0) > 0).length;
}
}, { passive: true });
function nextDelay(){
if (document.hidden) return 15000;
let target = (aliveCount <= 2 ? 1200 : 4000);
if (lastStateTs && (Date.now() – lastStateTs) < 8000) {
target = Math.min(target, 1200);
}
currentDelay = Math.max(800, Math.round(0.5 * currentDelay + 0.5 * target));
const jitter = 1 + (Math.random() * 0.10 – 0.05);
return Math.round(currentDelay * jitter);
}
async function tick(){
killKnown();
try {
if (typeof window.updateTournamentData === ‘function’) {
window.updateTournamentData();
} else {
const base = (window.restBase || ‘/wp-json/killer-pool/v1/’).replace(/\/+$/,’/’);
const res = await fetch(base + ‘state/’ + encodeURIComponent(slug) + ‘?_ts=’ + Date.now(), {
credentials: ‘same-origin’,
headers: { ‘Accept’: ‘application/json’ }
});
if (!res.ok) throw new Error(‘HTTP ‘ + res.status);
const s = await res.json();
document.dispatchEvent(new CustomEvent(‘kp:state’, { detail: s }));
}
} catch (err) {
if (KP_LOG) console.warn(‘KP GOV v4.1: tick error -> backoff’, err);
currentDelay = Math.min(20000, Math.round(currentDelay * 1.8));
}
schedule();
}
function schedule(ms){
if (timer) clearTimeout(timer);
const d = (typeof ms === ‘number’) ? ms : nextDelay();
if (KP_LOG)
console.log(‘KP GOV v4.1: next tick in’, d, ‘ms (alive=’, aliveCount, ‘, hidden=’, document.hidden, ‘)’);
timer = setTimeout(tick, d);
window.__KP_GOV_TMR = timer;
}
// — Nudge when visible/online (FIXED VERSION) —
document.addEventListener(‘visibilitychange’, () => {
if (!document.hidden) schedule(0);
}, { passive:true });
window.addEventListener(‘focus’, () => schedule(0), { passive:true });
window.addEventListener(‘online’, () => schedule(0), { passive:true });
// Control hook
window.KPGov = {
trigger:(ms=0)=>schedule(ms),
stop:()=>{ if (timer) clearTimeout(timer); }
};
if (KP_LOG) console.log(‘KP GOV v4.1: installed (master). slug=’, slug);
schedule(500);
})();
</script>
JS);