0 ) { $wpdb->update( "{$wpdb->prefix}adventurebook_enemy_definitions",// phpcs:ignore WordPress.DB.DirectDatabaseQuery compact( 'code', 'name', 'description', 'is_hostile', 'can_be_attacked', 'xp_value', 'stats', 'inventory', 'image_url' ), [ 'id' => $id ] ); $message = __( 'Enemy updated.', 'adventurebook' ); } else { $wpdb->insert( "{$wpdb->prefix}adventurebook_enemy_definitions",// phpcs:ignore WordPress.DB.DirectDatabaseQuery compact( 'code', 'name', 'description', 'is_hostile', 'can_be_attacked', 'xp_value', 'stats', 'inventory', 'image_url' ) ); $message = __( 'Enemy created.', 'adventurebook' ); } } else { $message = __( 'Invalid nonce.', 'adventurebook' ); } } $type_filter = isset( $_POST['enemy_type_filter'] ) ? '&enemy_type_filter=' . urlencode( sanitize_text_field( $_POST['enemy_type_filter'] ) ) : ''; wp_safe_redirect( admin_url( 'admin.php?page=adventurebook-enemies' . $type_filter . '&message=' . urlencode( $message ) ) ); exit; } if ( ! class_exists( 'WP_List_Table' ) ) { require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php'; } class Adventurebook_Enemy_List_Table extends WP_List_Table { private array $enemies_data = []; private string $message = ''; function __construct() { parent::__construct( [ 'singular' => 'Enemy', 'plural' => 'Enemies', 'ajax' => false ] ); } function pagination($which) { if (isset($_GET['enemy_type_filter']) && $_GET['enemy_type_filter'] !== '') { $this->_pagination_args['add_args']['enemy_type_filter'] = sanitize_text_field($_GET['enemy_type_filter']); } parent::pagination($which); } function get_columns(): array { return [ 'cb' => '', 'name' => __( 'Name', 'adventurebook' ), 'lvl' => __( 'Might', 'adventurebook' ), 'id' => 'ID', 'code' => __( 'Code', 'adventurebook' ), 'type' => __( 'Type', 'adventurebook' ), 'stats' => __( 'Stats', 'adventurebook' ), 'inventory' => __( 'Loottable', 'adventurebook' ), 'image_url' => __( 'Image', 'adventurebook' ), ]; } function get_sortable_columns(): array { return [ 'id' => [ 'id', false ], 'lvl' => [ 'lvl', false ], 'code' => [ 'code', false ], 'type' => [ 'type', false ], 'name' => [ 'name', false ], ]; } function column_lvl( $item ) { /** @var Enemy_Definition $item */ return $item->get_combat_strength(); } function column_type( $item ) { /** @var Enemy_Definition $item */ return isset( $item->stats['Type'] ) ? esc_html( $item->stats['Type'] ) : ''; } function column_cb( $item ) { /** @var Enemy_Definition $item */ return sprintf( '', $item->id ); } function column_default( $item, $column_name ) { /** @var Enemy_Definition $item */ switch ( $column_name ) { case 'id': return intval( $item->id ); case 'code': return esc_html( $item->code ); case 'lvl': return $this->column_lvl( $item ); case 'type': return $this->column_type( $item ); case 'name': return $this->column_name( $item ); case 'stats': // Stats als lesbare Liste anzeigen if ( empty( $item->stats ) ) { return __( 'No stats', 'adventurebook' ); } $stats = $item->stats; if ( is_array( $stats ) && ! empty( $stats ) ) { $stats_list = []; foreach ( $stats as $key => $value ) { $stats_list[] = esc_html( $key ) . ': ' . esc_html( $value ); } // return as list embedded in "
  • " and "
  • " return '
  • ' . implode( '
  • ', $stats_list ) . '
  • '; } else { return __( 'No stats', 'adventurebook' ); } case 'inventory': // the inventory (= loot table) is a comma separated list of code:chance:min:max if ( empty( $item->inventory ) ) { return __( 'No loottable', 'adventurebook' ); } return $item->getInventoryAsHTML(); case 'image_url': return $item->image_url ? '' : ''; default: return ''; } } function column_name( Enemy_Definition $enemy ): string { $type_filter = isset($_GET['enemy_type_filter']) ? '&enemy_type_filter=' . urlencode(sanitize_text_field($_GET['enemy_type_filter'])) : ''; $actions = [ 'edit' => sprintf( '%s', $enemy->id, $type_filter, __('Edit', 'adventurebook') ), 'copy' => sprintf( '%s', $enemy->id, wp_create_nonce('adventurebook_copy_enemy_' . $enemy->id), $type_filter, __('Copy', 'adventurebook') ), 'delete' => sprintf( '%s', $enemy->id, wp_create_nonce('adventurebook_delete_enemy_' . $enemy->id), $type_filter, __('Really delete?', 'adventurebook'), __('Delete', 'adventurebook') ), ]; return sprintf( '%s %s', esc_html( $enemy->name ), $this->row_actions( $actions ) ); } function get_bulk_actions(): array { return [ 'bulk-delete' => __( 'Delete', 'adventurebook' ) ]; } function process_bulk_action() { // check for admin permissions if ( ! current_user_can( 'manage_options' ) ) { return; } global $wpdb; $action = $this->current_action(); if ( $action === 'bulk-delete' ) { // WP-Standard Bulk-Nonce for Enemies if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'bulk-enemies' ) ) { $this->message = __( 'Nonce invalid.', 'adventurebook' ); return; } $enemy_ids = isset( $_POST['enemy_ids'] ) ? array_map( 'intval', wp_unslash( $_POST['enemy_ids'] ) ) : []; if ( $enemy_ids ) { foreach ( $enemy_ids as $id ) { Enemy_Definition::delete_enemy_definition( $id ); } $this->message = sprintf( '%d Enemies deleted.', count( $enemy_ids ) ); } } } function prepare_enemies() { if ( isset ( $_REQUEST['s'] ) ) { // did we get ANY search request? If yes, check the nonce check_admin_referer( 'adventurebook_search_item' ); } global $wpdb; $enemy_type_filter = isset($_GET['enemy_type_filter']) ? sanitize_text_field($_GET['enemy_type_filter']) : ''; $this->process_bulk_action(); $per_page = 10; $current_page = $this->get_pagenum(); $offset = ( $current_page - 1 ) * $per_page; $allowed_orderby = [ 'id', 'code', 'name' ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $orderby = isset( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : 'id'; if ( ! in_array( $orderby, $allowed_orderby, true ) ) { $orderby = 'id'; } $allowed_order = [ 'ASC', 'DESC' ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $order = isset( $_GET['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ) : 'DESC'; if ( ! in_array( $order, $allowed_order, true ) ) { $order = 'DESC'; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $search = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; if ($search !== '') { // Suchmodus $like = '%' . $wpdb->esc_like($search) . '%'; $where = "WHERE (code LIKE %s OR name LIKE %s)"; $params = [$like, $like]; if ($enemy_type_filter !== '') { // check for filter "Undefined": we need to find all entries where the Type is either NULL or an empty string if ($enemy_type_filter === '__undefined__') { $where .= " AND (JSON_EXTRACT(stats, '$.Type') IS NULL OR JSON_EXTRACT(stats, '$.Type') = '')"; } else { $where .= " AND JSON_EXTRACT(stats, '$.Type') LIKE %s"; $params[] = '%' . $wpdb->esc_like($enemy_type_filter) . '%'; } } $total_enemies = (int) $wpdb->get_var( $wpdb->prepare("SELECT COUNT(*) FROM {$wpdb->prefix}adventurebook_enemy_definitions $where", ...$params) ); $params[] = $per_page; $params[] = $offset; $raw_data = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}adventurebook_enemy_definitions $where ORDER BY $orderby $order LIMIT %d OFFSET %d", ...$params ), ARRAY_A ); } else { // Kein Suchbegriff if ($enemy_type_filter !== '') { // check for filter "Undefined": we need to find all entries where the Type is either NULL or an empty string if( $enemy_type_filter === '__undefined__' ) { $total_enemies = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}adventurebook_enemy_definitions WHERE JSON_EXTRACT(stats, '$.Type') IS NULL OR JSON_EXTRACT(stats, '$.Type') = ''" ); $raw_data = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}adventurebook_enemy_definitions WHERE JSON_EXTRACT(stats, '$.Type') IS NULL OR JSON_EXTRACT(stats, '$.Type') = '' ORDER BY $orderby $order LIMIT %d OFFSET %d", $per_page, $offset ), ARRAY_A ); $this->set_pagination_args( [ 'total_items' => $total_enemies, 'per_page' => $per_page, 'total_pages' => ceil( $total_enemies / $per_page ), ] ); $this->_column_headers = [ $this->get_columns(), [], $this->get_sortable_columns() ]; $this->items = $this->enemies_data; // jetzt: Enemy_Definition Objekte bauen $this->enemies_data = []; foreach ( $raw_data as $row ) { $this->enemies_data[] = Enemy_Definition::get_enemy_definition_by_id( $row['id'] ); } $this->set_pagination_args( [ 'total_items' => $total_enemies, 'per_page' => $per_page, 'total_pages' => ceil( $total_enemies / $per_page ), ] ); $this->_column_headers = [ $this->get_columns(), [], $this->get_sortable_columns() ]; $this->items = $this->enemies_data; return; } $like_type = '%' . $wpdb->esc_like($enemy_type_filter) . '%'; $total_enemies = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}adventurebook_enemy_definitions WHERE JSON_EXTRACT(stats, '$.Type') LIKE %s", $like_type ) ); $raw_data = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}adventurebook_enemy_definitions WHERE JSON_EXTRACT(stats, '$.Type') LIKE %s ORDER BY $orderby $order LIMIT %d OFFSET %d", $like_type, $per_page, $offset ), ARRAY_A ); } else { // Kein Filter $total_enemies = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}adventurebook_enemy_definitions"); $raw_data = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}adventurebook_enemy_definitions ORDER BY $orderby $order LIMIT %d OFFSET %d", $per_page, $offset ), ARRAY_A ); } } // now convert all $raw_data entries to Enemy_Definition objects $this->enemies_data = []; foreach ( $raw_data as $row ) { $this->enemies_data[] = Enemy_Definition::get_enemy_definition_by_id( $row['id'] ); } if ( ab_get( 'orderby' ) === 'lvl' ) { usort( $this->enemies_data, function ( $a, $b ) use ( $order ) { $a_lvl = $a->get_combat_strength(); $b_lvl = $b->get_combat_strength(); if ( ab_get( 'order' ) === 'asc' ) { return $a_lvl <=> $b_lvl; } else { return $b_lvl <=> $a_lvl; } } ); } $this->set_pagination_args( [ 'total_items' => $total_enemies, 'per_page' => $per_page, 'total_pages' => ceil( $total_enemies / $per_page ), ] ); $this->_column_headers = [ $this->get_columns(), [], $this->get_sortable_columns() ]; $this->items = $this->enemies_data; } public function get_message() { return $this->message; } } // Admin page function adventurebook_enemies_page() { // check for admin permissions if ( ! current_user_can( 'manage_options' ) ) { return; } global $wpdb; // single delete if ( isset( $_GET['action'], $_GET['id'] ) && $_GET['action'] === 'delete' ) { // check nonce $id = intval( sanitize_text_field( wp_unslash( $_GET['id'] ) ) ); $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; if ( $nonce && wp_verify_nonce( $nonce, 'adventurebook_delete_enemy_' . $id ) ) { $wpdb->delete( "{$wpdb->prefix}adventurebook_enemy_definitions", [ 'id' => $id ] );// phpcs:ignore WordPress.DB.DirectDatabaseQuery $message = __( 'Enemy deleted.', 'adventurebook' ); } } // copy the enemy definition and set the code to _copy if ( isset( $_GET['action'], $_GET['enemy_id'] ) && $_GET['action'] === 'copy' ) { // check nonce $copy_id = intval( ab_get( 'enemy_id' ) ); $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; do_log('Copying enemy with id ' . $copy_id); if ( $nonce && wp_verify_nonce( $nonce, 'adventurebook_copy_enemy_' . $copy_id ) ) { do_log('Nonce verified'); $enemy_to_copy = Enemy_Definition::get_enemy_definition_by_id( $copy_id ); if ( $enemy_to_copy ) { $new_code = $enemy_to_copy->code . '_copy'; // ensure unique code $existing_enemy = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}adventurebook_enemy_definitions WHERE code = %s", $new_code ), ARRAY_A ); if ( $existing_enemy ) { $new_code .= '_' . time(); } $wpdb->insert( "{$wpdb->prefix}adventurebook_enemy_definitions",// phpcs:ignore WordPress.DB.DirectDatabaseQuery [ 'code' => $new_code, 'name' => $enemy_to_copy->name . ' (Copy)', 'description' => $enemy_to_copy->description, 'is_hostile' => $enemy_to_copy->is_hostile, 'can_be_attacked' => $enemy_to_copy->can_be_attacked, 'xp_value' => $enemy_to_copy->xp_value, 'stats' => json_encode( $enemy_to_copy->stats ), 'inventory' => json_encode( $enemy_to_copy->inventory ), 'image_url' => $enemy_to_copy->image_url, ] ); $message = __( 'Enemy copied.', 'adventurebook' ); } else { $message = __( 'Enemy to copy not found.', 'adventurebook' ); } } } $message = ''; $is_list_view = ! isset( $_GET['action'] ) || ! in_array( $_GET['action'], [ 'new', 'edit' ], true ); echo '
    '; if ( $is_list_view ) { $type_filter = isset($_GET['type_filter']) ? '&type_filter=' . urlencode(sanitize_text_field($_GET['type_filter'])) : ''; echo '

    Adventurebook Enemies ' . esc_html(__("New Enemy", "adventurebook")) . '

    '; } else { echo '

    ' . ( ( isset( $_GET['action'] ) && $_GET['action'] === 'edit' ) ? 'Edit Enemy' : 'New Enemy' ) . '

    '; } if ( $message ) { echo '

    ' . esc_html( $message ) . '

    '; if ( ! $is_list_view ) { $type_filter = isset($_GET['type_filter']) ? '&type_filter=' . urlencode(sanitize_text_field($_GET['type_filter'])) : ''; echo '

    Back to Enemy-List

    '; } echo '
    '; } // form new/edit if ( ! $is_list_view ) { /** @var Enemy_Definition $enemy */ $enemy = null; if ( isset( $_GET['action'], $_GET['id'] ) && $_GET['action'] === 'edit' ) { $id = intval( $_GET['id'] ); $enemy = Enemy_Definition::get_enemy_definition_by_id( $id ); } else { $enemy = new Enemy_Definition( 0, '', '', 0, 0, 0, '', '{}', '{}', '' ); } ?>
    ..., 'name' => ...] // for easier use in JS, create an array with code and name only $item_defs = array_map( static function ( $item ) { return [ 'code' => $item->code, 'name' => $item->name ?? $item->code, ]; }, $items ); // Loot Entries as array $loottable = $enemy->get_inventory_as_array(); // [['item_code'=>'rusty_key','percent'=>25,'min'=>1,'max'=>1], ...] ?> code ) && $enemy->code !== '' ) : ?>
    is_hostile ?? false, true ); ?>>
    can_be_attacked ?? false, true ); ?>>
    '', 'Initiative' => '0', 'Attack' => '0', 'Defense' => '0', 'Damage' => '0', 'Armor' => '0', 'Health' => '0', 'StressDiceMultiplicator' => '0' ]; foreach ( $predefined_stats as $key => $value ) : ?>
    stats as $key => $value ) : ?>

    code ); adventurebook_show_page_links( $pages ); ?>
    prepare_enemies(); if ( $list_table->get_message() ) { echo '

    ' . esc_html( $list_table->get_message() ) . '

    '; } echo '
    '; echo ''; $current_filter = isset($_GET['enemy_type_filter']) ? sanitize_text_field($_GET['enemy_type_filter']) : ''; // Dropdown für Type echo ''; echo ''; $list_table->search_box(__('Search Enemy', 'adventurebook'), 'enemy-search'); wp_nonce_field('adventurebook_search_item'); echo '
    '; // --- Ende Filter-Dropdown --- wp_nonce_field( 'adventurebook_search_item' ); echo ''; echo '
    '; $list_table->display(); echo '
    '; } echo '
    '; }