is_alive() ) { return []; } $results = []; // determine order of combatants based on initiative $combatants = $this->determine_combatant_order( $character, $enemies ); // if the first combatant is the player and he has just entered the zone // we give him a free turn to act: no enemy will act in this round // this is saved in wp_transient 'adventurebook_just_entered_zone_' // readout the transient and delete it $transient_key = 'adventurebook_just_entered_zone_' . $character->id; // we do not need to read the transient, just check if it exists $just_entered_zone = get_transient( $transient_key ); if ( $just_entered_zone !== false ) { delete_transient( $transient_key ); } if ( $combatants[0]['type'] === 'character' && $just_entered_zone ) { // we remove all enemies from the combatants array $combatants = array_filter( $combatants, function( $c ) { return $c['type'] !== 'enemy'; } ); // output a message // but only if there is at least an enemy which is hostile $hostile_enemies = array_filter( $enemies, function( $enemy ) { return $enemy->is_hostile && $enemy->is_alive(); } ); if ( count( $hostile_enemies ) > 0 ) { $results[] = __('You surprise your enemies!', 'adventurebook' ); } } // now iterate through the combatants and let them act // all combatants implement the Adventurebook_Combatant interface // so we can call the DoAction method on them foreach ( $combatants as $combatant ) { // the character should have a next_action set by the player // enemies will determine their action automatically // for now we differentiate between character and enemy by type if ( $combatant['type'] === 'character' ) { $action = $character->get_next_action(); // the attack action has the form "attack_" if ( strpos( $action, 'attack_' ) === 0 ) { $enemy_id = intval( substr( $action, 7 ) ); $enemy = null; foreach ( $enemies as $e ) { if ( $e->id === $enemy_id ) { $enemy = $e; break; } } if ( $enemy ) { $results = [...$results, ...$this->player_attack_enemy( $character, $enemy )]; } else { /* translators: %s: the character name */ $results[] = sprintf(__('%s tries to attack an unknown enemy!', 'adventurebook'), $character->get_name() ); } } // set the action to ""null"" after it has been executed $character->set_next_action( "" ); } elseif ( $combatant['type'] === 'enemy' ) { $enemy = $combatant['entity']; $this->enemy_handle_action($enemy, $character); // for now enemies only attack if( $enemy->get_next_action() === 'attack_player' ) { $results = [...$results, ...$this->enemy_attack_player( $enemy, $character )]; } } } return $results; } /** * Determine the order of combatants based on their initiative. * * @param Character $character The character * @param array $enemies An array of enemies * @return array An ordered array of combatants based on initiative. */ public function determine_combatant_order( Character $character, array $enemies ): array { $combatants = []; $combatants[] = [ 'type' => 'character', 'entity' => $character, 'initiative' => $this->get_initiative_for_character( $character ) ]; foreach ( $enemies as $enemy ) { $combatants[] = [ 'type' => 'enemy', 'entity' => $enemy, 'initiative' => $this->get_initiative_for_enemy($enemy) ]; } // sort by initiative descending usort( $combatants, function( $a, $b ) { return $b['initiative'] <=> $a['initiative']; } ); return $combatants; } /** * Handle the player's attack on the enemy. * * @param Character $player The player character. * @param Enemy $enemy The enemy being attacked. * @return array An array of string describing the outcome of the attack. */ public function player_attack_enemy( Character $player, Enemy $enemy ) : array { $output = []; // in this simple system, the player has the stats "Health", "Damage" and "Armor" // he always hits the enemy and deals damage equal to his "Damage" stat minus the enemy's "Armor" stat, but at least 1 $player_damage = $player->get_total_stat_including_items('Damage'); $enemy_armor = $enemy->get_total_stat_including_items('Armor'); $damage_dealt = max( 1, $player_damage - $enemy_armor ); $enemy->take_damage( $damage_dealt ); // now the enemy is hostile for sure $enemy->makeHostile(); $output[] = sprintf( /* translators: 1: player name, 2: damage dealt, 3: enemy name */ __('%1$s deals %2$d damage to %3$s.', 'adventurebook'), $player->get_name(), $damage_dealt, $enemy->get_name_with_article() ); if ( ! $enemy->is_alive() ) { // if the enemy is defeated, we determine it's spawn and update the "last_defeated" timestamp $spawn = Enemy_Spawn::get_enemy_spawn_by_id($enemy->spawn_id); if ( $spawn ) { $spawn->update_last_defeated(); } // we also delete the enemy from the database Enemy::delete_enemy_by_id( $enemy->id ); // output a message $output[] = sprintf( /* translators: 1: enemy name */ __('%1$s has been defeated!', 'adventurebook'), ucfirst($enemy->get_name_with_article()) ); // add xp to the player $output[] = $player->add_xp( $enemy->xp_value() ); // show if the enemy dropped any items // examine the loottable of the enemy (enemy_definition->inventory) // the loot table is an array of key/value pairs: item_code:drop_chance_in_percent:min_quantity:max_quantity // e.g. "potion:50:1:2,sword:10:1:1" // we roll for each item in the loot table if it drops // if it does, we add it to the player's location as a new item $loot_found = false; $enemy_definition = $enemy->get_enemy_definition(); $loottable = $enemy_definition->inventory; if ( ! empty( $loottable ) ) { foreach ( $loottable as $loot_item ) { $parts = explode( ':', $loot_item ); if ( count( $parts ) === 4 ) { $item_code = $parts[0]; $drop_chance = intval( $parts[1] ); $min_quantity = intval( $parts[2] ); $max_quantity = intval( $parts[3] ); $roll = wp_rand( 1, 100 ); if ( $roll <= $drop_chance ) { $quantity = wp_rand( $min_quantity, $max_quantity ); // check if the item code is "money" // this will go directly to the player's wealth if ( $item_code === 'money' ) { $output[] = $player->add_wealth( $quantity, $this->get_wealth_unit() ); continue; } // add the item to the player's location $location_id = $player->location_id; for ( $i = 0; $i < $quantity; $i++ ) { Item::create_item( $player->id, $item_code, $location_id ); } $loot_found = true; } } } } if($loot_found) { $output[] = __('You have found something!', 'adventurebook' ); } } return $output; } public function enemy_handle_action( Enemy $enemy, Character $player ) { // if the enemy is not alive, it does nothing if ( ! $enemy->is_alive() ) { $enemy->set_next_action( '' ); return; } // if the enemy is hostile, it attacks the player - the next_action is "attack_player" if ( $enemy->is_hostile ) { $enemy->set_next_action( 'attack_player' ); } else { $enemy->set_next_action( '' ); } } public function get_combat_stats_output( AbstractCombatant $combatant ): array { // show a line in the form Ini: Dmg: Arm: $stats = []; $stats[] = __('Ini','adventurebook').': ' . $combatant->get_total_stat_including_items('Initiative'); $stats[] = __('Dmg','adventurebook').': ' . $combatant->get_total_stat_including_items('Damage'); $stats[] = __('AC','adventurebook').': ' . $combatant->get_total_stat_including_items('Armor'); return $stats; } public function get_combat_stats_output_long( AbstractCombatant $combatant ): array { // show a line in the form Ini: Dmg: Arm: $stats = []; $stats[] = __('Initiative','adventurebook').': ' . $combatant->get_total_stat_including_items('Initiative'); $stats[] = __('Damage','adventurebook').': ' . $combatant->get_total_stat_including_items('Damage'); $stats[] = __('Armor','adventurebook').': ' . $combatant->get_total_stat_including_items('Armor'); return $stats; } // each combat system restricts the values that are shown for an item public function get_item_stats_to_show( array $stats ): array { $allowed_stats = [ 'Value', 'Armor', 'Damage', 'Slot' ]; $stats_to_show = []; foreach ( $allowed_stats as $stat ) { if ( isset( $stats[ $stat ] ) ) { $stats_to_show[ $stat ] = $stats[ $stat ]; } } return $stats_to_show; } /** * the initial stats for a new character * CurHealth is determined by Health at character creation * * * @return array An associative array of initial character stats. */ public function get_initial_character_stats(): array { return [ 'Health' => 35, 'Wealth' => 0, 'XP' => 0, 'SLOTS' => [ 'Weapon' => 0, 'Shield' => 0, 'Armor' => 0, ] ]; } // get the name of the item stat that represents the item price public function get_item_stat_name_for_price(): string { return 'Value'; } // get the wealth/price unit, e.g. "gold", "coins", "gems", etc. public function get_wealth_unit(): string { return __('Gold', 'adventurebook'); } public function get_initiative_for_character(Character $character): int { // in this combat system we roll a 1d10 and add the character's initiative $roll = wp_rand(1, 10); return $roll + $character->get_total_stat_including_items('Initiative'); } public function get_initiative_for_enemy(Enemy $enemy): int { $roll = wp_rand(1, 10); return $roll + $enemy->get_total_stat_including_items('Initiative'); } public function enemy_attack_player( Enemy $enemy, Character $player ) : array { $output = []; // in this simple system, the enemy has the stats "Health", "Damage" and "Armor" // he always hits the character and deals damage equal to his "Damage" stat minus the player's "Armor" stat, but at least 1 $enemy_damage = $enemy->get_stats()['Damage'] ?? 0; $player_armor = $player->get_total_stat_including_items('Armor'); $damage_dealt = max( 1, $enemy_damage - $player_armor ); $output[] = ucfirst($enemy->get_name_with_article()) . ' deals ' . $damage_dealt . ' damage to ' . $player->get_name() . '. '; // if the inline editing option is enabled and the current user has admin rights, no damage is dealt if(get_option( 'adventurebook_enable_inline_editing', 0 ) == 1 && current_user_can( 'manage_options' ) ) { $output[] = __('(No damage dealt due to inline editing mode being enabled.)', 'adventurebook' ); return $output; } $player->take_damage( $damage_dealt ); if ( ! $player->is_alive() ) { // output a message $output[] = sprintf( /* translators: 1: player name */ __('%1$s has been defeated!', 'adventurebook'), $player->get_name() ); } return $output; } /** * Each combat system defines which item stats should be instantiated when a new item is created * e.g. "Charges" for a wand or potion * * @return array An array of item stat names to be instantiated with default values */ public function get_item_stats_to_be_instantiated(): array { return [ 'Charges', ]; } public function get_item_combat_stats_output( Item_Definition $item ): array { $stats = []; if ( $item->get_stat('Damage') !== 0 ) $stats[] = __('Dmg','adventurebook').': ' . $item->get_stat('Damage'); if ( $item->get_stat('Armor') !== 0 ) $stats[] = __('AC','adventurebook').': ' . $item->get_stat('Armor'); return $stats; } public function combatant_can_raise_skill( AbstractCombatant $combatant, string $skill_name ): bool { // TODO: Implement combatant_can_raise_skill() method. return false; } public function combatant_get_skill_cost_for_next_skill_level( AbstractCombatant $combatant, string $skill_code ): int { // TODO: Implement combatant_get_skill_cost_for_next_skill_level() method. return 0; } public function combatant_raise_skill( AbstractCombatant $combatant, string $skill_name ): array { // TODO: Implement combatant_raise_skill() method. return false; } #region SKILLS public function get_available_skills(): array { return []; } public function get_skill_by_code( string $skill_code ): array { return []; } public function combatant_use_skill( AbstractCombatant $combatant, string $skill_name ): array { return []; } #endregion public function get_combat_strength_output( Character $character ): string { // TODO: Implement get_combat_strength_output() method. return ''; } public function get_available_resources(): array { return []; } public function get_resource_yield_for_page(int $page_id, string $code): int { return 0; } public function get_skill_code_for_harvesting_resource(string $resource_code): ?string { return null; } }