name, '', $stats === '' ? [] : $stats ); $this->id = intval( $id ); $this->code = $code; $this->is_hostile = $is_hostile; $this->spawn_id = intval( $spawn_id ); $this->location_id = intval( $location_id ); } public function get_enemy_definition(): ?Enemy_Definition { return Enemy_Definition::get_enemy_definition_by_code( $this->code ); } public function makeHostile(): void { $this->is_hostile = true; $this->save_to_db(); } /** * Returns the correct English indefinite article ("a" or "an") * based on the pronunciation rules (best-effort heuristic). * * @param string $word The word to check (English). * * @return string "a" or "an" */ function get_indefinite_article( string $word ): string { $word = trim( strtolower( $word ) ); // Early exit for empty string if ( $word === '' ) { return 'a'; } // Words that begin with a *silent h* take "an" $silent_h_exceptions = [ 'honest', 'honor', 'honour', 'heir', 'hour', 'herb' ]; // "herb" (AmE) foreach ( $silent_h_exceptions as $hword ) { if ( strpos( $word, $hword ) === 0 ) { return 'an'; } } // Words that begin with a *"you"-sound* (e.g., university, European) take "a" $you_sound_patterns = [ '/^u[bcfhjkqrstnljm]/', // university, union, unique, etc. '/^eu/', // European, eulogy '/^one/', // one, once '/^ou/', // ounce -> still vowel, skip? careful, skip this ]; foreach ( $you_sound_patterns as $pattern ) { if ( preg_match( $pattern, $word ) ) { return 'a'; } } // Default: if word starts with a vowel letter -> "an" if ( preg_match( '/^[aeiou]/', $word ) ) { return 'an'; } // Otherwise: "a" return 'a'; } public function get_name_with_article(): string { $enemy_definition = Enemy_Definition::get_enemy_definition_by_code( $this->code ); $name = $enemy_definition->name; return trim( $this->get_indefinite_article($name) . ' ' . $name ); } public function get_name(): string { $enemy_definition = Enemy_Definition::get_enemy_definition_by_code( $this->code ); return $enemy_definition->name; } public function get_description(): string { $enemy_definition = Enemy_Definition::get_enemy_definition_by_code( $this->code ); return $enemy_definition->description; } public static function create_enemy( $code, $stats, $is_hostile, $spawn_id, $location_id ): void { global $wpdb; $table_name = $wpdb->prefix . 'adventurebook_enemies'; $wpdb->insert( $table_name, array(// phpcs:ignore WordPress.DB.DirectDatabaseQuery 'code' => $code, 'stats' => json_encode( $stats ), 'spawn_id' => $spawn_id, 'is_hostile' => $is_hostile, 'location_id' => $location_id, ) ); } public static function get_enemy_by_id( $id ): ?Enemy { global $wpdb; $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM " . $wpdb->prefix . "adventurebook_enemies WHERE id = %d", $id ), ARRAY_A );// phpcs:ignore WordPress.DB.DirectDatabaseQuery if ( $result === null ) { return null; } return new Enemy( $result['id'], $result['code'], json_decode( $result['stats'], true ), $result['is_hostile'], $result['spawn_id'], $result['location_id'] ); } /** * Get all enemies for a specific page and character. * * @param int $page_id The ID of the page. * @param int $character_id The ID of the character (for character-specific spawns). * * @return Enemy[] An array of Enemy objects. */ public static function get_enemies_for_page( $page_id, $character_id ): array { // get all enemies and JOIN enemy_spawns to filter by page_id and character_id global $wpdb; $results = $wpdb->get_results( $wpdb->prepare(// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.NoCaching "SELECT e.* FROM " . $wpdb->prefix . "adventurebook_enemies e JOIN " . $wpdb->prefix . "adventurebook_enemy_spawns es ON e.spawn_id = es.id WHERE e.location_id = %d AND es.character_id = %d", $page_id, $character_id ), ARRAY_A ); $enemies = array(); foreach ( $results as $result ) { $enemies[] = new Enemy( $result['id'], $result['code'], json_decode( $result['stats'], true ), $result['is_hostile'], $result['spawn_id'], $result['location_id'] ); } return $enemies; } public static function delete_enemy_by_id( $id ): void { global $wpdb; $table_name = $wpdb->prefix . 'adventurebook_enemies'; $wpdb->delete( $table_name, array( 'id' => $id ) );// phpcs:ignore WordPress.DB.DirectDatabaseQuery } public static function delete_enemies_for_spawn_id( $spawn_id ): void { global $wpdb; $table_name = $wpdb->prefix . 'adventurebook_enemies'; $wpdb->delete( $table_name, array( 'spawn_id' => $spawn_id ) );// phpcs:ignore WordPress.DB.DirectDatabaseQuery } /** * Deletes all enemies that are associated with a spawn_id (>0) that no longer exists in the enemy_spawns table. * * @return int The number of deleted enemies. */ public static function delete_orphaned_enemies(): int { global $wpdb; $table_name = $wpdb->prefix . 'adventurebook_enemies'; $spawn_table_name = $wpdb->prefix . 'adventurebook_enemy_spawns'; $wpdb->query("DELETE e FROM $table_name e LEFT JOIN $spawn_table_name es ON e.spawn_id = es.id WHERE e.spawn_id > 0 AND es.id IS NULL");// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared return $wpdb->rows_affected; } /** * Get all enemies associated with a specific spawn point. * * @param int $spawn_id The ID of the spawn point. * * @return Enemy[] An array of Enemy objects. */ public static function get_enemies_for_spawn_id( $spawn_id ): array { global $wpdb; $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM " . $wpdb->prefix . "adventurebook_enemies WHERE spawn_id = %d", $spawn_id ), ARRAY_A );// phpcs:ignore WordPress.DB.DirectDatabaseQuery $enemies = array(); foreach ( $results as $result ) { $enemies[] = new Enemy( $result['id'], $result['code'], json_decode( $result['stats'], true ), $result['is_hostile'], $result['spawn_id'], $result['location_id'] ); } return $enemies; } /** * Get all enemies for a specific location and character. * * @param int $location_id The ID of the location. * @param int $character_id The ID of the character (for character-specific spawns). * * @return Enemy[] An array of Enemy objects. */ public static function get_enemies_for_location_id( $location_id, $character_id ): array { // get all enemies and JOIN enemy_spawns to filter by location_id and character_id global $wpdb; $results = $wpdb->get_results( $wpdb->prepare(// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.NoCaching "SELECT e.* FROM " . $wpdb->prefix . "adventurebook_enemies e JOIN " . $wpdb->prefix . "adventurebook_enemy_spawns es ON e.spawn_id = es.id WHERE e.location_id = %d AND es.character_id = %d", $location_id, $character_id ), ARRAY_A ); $enemies = array(); foreach ( $results as $result ) { $enemies[] = new Enemy( $result['id'], $result['code'], json_decode( $result['stats'], true ), $result['is_hostile'], $result['spawn_id'], $result['location_id'] ); } return $enemies; } public static function location_has_hostile_enemies( $location_id, $character_id ): bool { global $wpdb; $result = $wpdb->get_var( $wpdb->prepare(// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.DirectDatabaseQuery.NoCaching "SELECT COUNT(*) FROM " . $wpdb->prefix . "adventurebook_enemies e JOIN " . $wpdb->prefix . "adventurebook_enemy_spawns es ON e.spawn_id = es.id WHERE e.location_id = %d AND es.character_id = %d AND e.is_hostile = 1", $location_id, $character_id ) ); return intval( $result ) > 0; } public function get_html(): string { $enemy_definition = $this->get_enemy_definition(); if ( $enemy_definition === null ) { return '

Unknown Enemy

'; } $html = '
'; // Bild $html .= '
'; $html .= $enemy_definition->image_url ? '' : ''; // if the enemy is hostile, show a health bar under the image // the health bar is a simple div with a red background and a width based on the enemy's current health if ( $this->is_hostile ) { $cur_health = $this->stats['CurHealth'] ?? 0; $max_health = $this->stats['Health'] ?? 1; $health_perc = ( $cur_health / $max_health ) * 100; $html .= '
'; $html .= '
'; $html .= '
'; } $html .= '
'; $html .= '
' . esc_html( $enemy_definition->name ); // show the stat 'StressDiceMultiplicator' as little yellow stars behind the name if it is > 0 if ( isset( $this->stats['StressDiceMultiplicator'] ) && $this->stats['StressDiceMultiplicator'] > 0 ) { $stars = str_repeat( '★', intval( $this->stats['StressDiceMultiplicator'] ) ); $html .= ' ' . esc_html( $stars ) . ''; } // if the enemy is hostile, show a red exclamation mark if ( $this->is_hostile ) { $html .= ' ⚠'; } $html .= '
'; $html .= '
' . esc_html( $enemy_definition->description ) . '
'; $html .= '
'; return $html; } public function save_to_db(): void { global $wpdb; $table_name = $wpdb->prefix . 'adventurebook_enemies'; $wpdb->update( $table_name, array(// phpcs:ignore WordPress.DB.DirectDatabaseQuery 'code' => $this->code, 'stats' => json_encode( $this->stats ), 'spawn_id' => $this->spawn_id, 'is_hostile' => $this->is_hostile, 'location_id' => $this->location_id, ), array( 'id' => $this->id ) ); } public function can_be_attacked(): bool { $enemy_definition = $this->get_enemy_definition(); return $enemy_definition->can_be_attacked; } public function xp_value(): int { $enemy_definition = $this->get_enemy_definition(); return $enemy_definition->xp_value; } }