get_initial_character_stats();
}
// call the parent constructor
parent::__construct( $name, '', $stats === '' ? [] : $stats );
$this->id = $id;
$this->user_id = $user_id;
$this->adventurebook_id = $adventurebook_id;
$this->location_id = $location_id;
$this->inventory = $inventory === '' ? [] : $inventory;
$this->next_action = $next_action;
$this->created_at = $created_at;
}
public static function create_character_by_params( $id, $user_id, $adventurebook_id, $name, $location_id = 0, $stats = '', $inventory = '', $next_action = '', $created_at = '' ): Character {
return new Character( $id, $user_id, $adventurebook_id, $name, $location_id, $stats, $inventory, $next_action, $created_at );
}
public static function create_character_by_result( $result ): Character {
return new Character(
$result['id'],
$result['user_id'],
$result['adventurebook_id'],
$result['name'],
$result['location_id'],
json_decode( $result['stats'], true ),
json_decode( $result['inventory'], true ),
$result['next_action'],
$result['created_at'] );
}
/**
* Save the character to the database.
* If the character already has an ID, it will be updated.
* If the character does not have an ID, it will be created.
*
* @return int The ID of the character.
*/
public function save_to_db(): int {
global $wpdb;
$table_name = $wpdb->prefix . 'adventurebook_characters';
// if this character already has an ID, we should update it instead of creating a new one
if ( $this->id > 0 ) {
$wpdb->update( $table_name, array( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
'user_id' => $this->user_id,
'name' => $this->name,
'adventurebook_id' => $this->adventurebook_id,
'location_id' => $this->location_id,
'stats' => json_encode( $this->stats ),
'inventory' => json_encode( $this->inventory ),
'next_action' => $this->next_action,
), array( 'id' => $this->id ) );
return $this->id;
}
// insert the new character into the database
$wpdb->insert( $table_name, array(// phpcs:ignore WordPress.DB.DirectDatabaseQuery
'user_id' => $this->user_id,
'name' => $this->name,
'adventurebook_id' => $this->adventurebook_id,
'location_id' => $this->location_id,
'stats' => json_encode( $this->stats ),
'inventory' => json_encode( $this->inventory ),
'next_action' => $this->next_action,
'created_at' => current_time( 'mysql' ),
) );
$new_character_id = $wpdb->insert_id;
$this->id = $new_character_id;
// iterate through all item_spawns and create a duplicate of it for this character
$item_spawn_points = Item_Spawn::get_all_item_spawn_points();
foreach ( $item_spawn_points as $item_spawn_point ) {
// create an item spawn point for each item spawn
Item_Spawn::create_item_spawn_point( $new_character_id, $item_spawn_point->id );
}
// iterate through all enemy spawn points and create a duplicate of it for this character
$enemy_spawn_points = Enemy_Spawn::get_all_enemy_spawn_points();
foreach ( $enemy_spawn_points as $enemy_spawn_point ) {
Enemy_Spawn::create_enemy_spawn_point( $new_character_id, $enemy_spawn_point->id );
}
return $new_character_id;
}
/** Delete the character from the database.
* This will also delete all items that belong to this character.
*/
public function delete_from_db(): void {
global $wpdb;
// delete the character from the database
$wpdb->delete( $wpdb->prefix . "adventurebook_characters", array( 'id' => $this->id ) );// phpcs:ignore WordPress.DB.DirectDatabaseQuery
// delete all items that belong to this character
Item::delete_items_by_character_id( $this->id );
// delete all enemy spawn points that belong to this character
Enemy_Spawn::delete_enemy_spawn_points_by_character_id( $this->id );
// get the adventurebook id of the character
$adventurebook_id = $this->adventurebook_id;
$user_id = get_current_user_id();
// if the deleted character was the current character, remove the current_character_id meta
$current_character_id = get_user_meta( $user_id, 'adventurebook_current_character_id_' . $adventurebook_id, true );
if ( $this->id == $current_character_id ) {
delete_user_meta( $user_id, 'adventurebook_current_character_id_' . $adventurebook_id );
}
// if the user still has characters, set the first character as the current character
$characters = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM " . $wpdb->prefix . "adventurebook_characters WHERE user_id = %d", $user_id ) );// phpcs:ignore WordPress.DB.DirectDatabaseQuery
if ( $characters ) {
update_user_meta( $user_id, 'adventurebook_current_character_id_' . $adventurebook_id, $characters[0]->id );
}
}
public static function get_characters_by_adventurebook_id( int $adventurebook_id ): array {
global $wpdb;
$results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM " . $wpdb->prefix . "adventurebook_characters WHERE adventurebook_id = %d", $adventurebook_id ), ARRAY_A );// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$characters = array();
foreach ( $results as $result ) {
$characters[] = Character::create_character_by_result( $result );
}
return $characters;
}
public static function get_characters_by_user_id( int $user_id ): array {
global $wpdb;
$results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM " . $wpdb->prefix . "adventurebook_characters WHERE user_id = %d", $user_id ), ARRAY_A );// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$characters = array();
foreach ( $results as $result ) {
$characters[] = Character::create_character_by_result( $result );
}
return $characters;
}
public static function get_character_by_id( int $id ): ?Character {
global $wpdb;
$result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM " . $wpdb->prefix . "adventurebook_characters WHERE id = %d", $id ), ARRAY_A );// phpcs:ignore WordPress.DB.DirectDatabaseQuery
if ( $result === null ) {
return null;
}
return Character::create_character_by_result( $result );
}
/**
* render the item list of this character as html
* needed for the character overview (management page)
*
* @return string
*/
public function get_item_list_html(): string {
global $adventurebook_current_character, $adventurebook_current_character_id, $adventurebook_id, $page_id, $wealth_unit;
// get the current combat system
$adventurebook_combat_system = adventurebook_get_combat_system_for_adventurebook( $this->adventurebook_id );
$inventory = $this->get_inventory();
$inventory = adventurebook_sort_items_by_name( $inventory );
// if the inventory is empty, we return here
if ( empty( $inventory ) ) {
$content = '
' . esc_html( __( 'Your inventory is empty.', 'adventurebook' ) ) . '';
return $content;
}
$content = '';
$prev_item = null;
$count = 0;
$render_item = function ( ?Item $item, int $count ) use ( &$content, $adventurebook_current_character, $adventurebook_combat_system, $adventurebook_current_character_id, $adventurebook_id, $page_id, $wealth_unit ) {
if ( ! $item ) {
return;
}
$content .= '- ';
$content .= $item->get_item_html( $adventurebook_combat_system, $count );
$content .= '
';
};
foreach ( $inventory as $item ) {
/* @var Item $item */
if ( $item->is_identical_to_item( $prev_item ) ) {
$count ++;
} else {
$render_item( $prev_item, $count ); // ggf. vorheriges Item ausgeben
$count = 1;
$prev_item = $item;
}
}
// render the last/only item
$render_item( $prev_item, $count );
return $content;
}
/**
* render the slotted item list of this character as html
* needed for the character overview (management page)
*
* slotted items are items that are in the stat "SLOTS" and are currently equipped
* slot "SLOTS" has the format ["Weapon" => item_id, "Shield" => item_id...]
* if the item_id is 0, the slot is empty.
*
* @return string
*/
public function get_slotted_item_list_html(): string {
$combat_system = adventurebook_get_combat_system_for_adventurebook( $this->adventurebook_id );
$slotted_items = $this->get_slotted_items();
$content = '';
foreach ( $slotted_items as $slot => $item ) {
/* @var Item $item */
// if the $slot is not the main slot of the item, we skip it
if($slot !== Item_Definition::get_item_definition_by_code( $item->code )->get_main_slot() ) {
continue;
}
if($slot == "Weapon" && $item->is_two_handed()) {
$slot .= ' (2h)';
}
// show the slot: name on the left side
$content .= '- ' . esc_html( $slot ) . ': '. esc_html( $item->get_name() ) . '';
// show the stats on the right side
$content .= '';
$stats = $combat_system->get_item_combat_stats_output( Item_Definition::get_item_definition_by_code( $item->code ) );
$content .= implode( ' ', $stats );
$content .= '';
$content .= '
';
}
$content .= '
';
if ( $slotted_items === [] ) {
$content = '- ' . esc_html( __( 'No items equipped.', 'adventurebook' ) ) . '
';
}
$content .= '
';
return $content;
}
/**
* Get all slotted items of this character
* Slotted items are items that are in the stat "SLOTS" and are currently equipped
* slot "SLOTS" has the format ["Weapon:item_id", "Shield:item_id"...]
* if the item_id is 0, the slot is empty.
*
* @return array an associative array with the slot name as key and the Item object as value
* e.g. ["Weapon" => Item, "Shield" => Item...]
* if a slot is empty, it will not be included in the array
*/
public function get_slotted_items(): array {
$slots_stat = $this->get_slots();
$slotted_items = [];
foreach ( $slots_stat as $slot_key => $item_id ) {
$item = Item::get_item_by_id( $item_id );
if ( $item ) {
$slotted_items[ $slot_key ] = $item;
}
}
return $slotted_items;
}
/**
* Uses an item with this item_id from the character's inventory
* The item must have a "Use" stat that contains the script code to be executed
* In addition it can have a "UseData" stat that contains additional data for the use script
*
* @param int $item_id
*
* @return array the script output of the use effect
*/
public function use_item( int $item_id ): array {
$output = [];
if ( $this->has_item_with_id( $item_id ) ) {
$item = Item::get_item_by_id( $item_id );
// check if the item has a "UseEffect" stat
if ( $item->is_usable() ) {
$use_script = $item->get_stat( "Use" );
$use_data = $item->get_stat( "UseData" );
// apply the use effect to the character
// the use effect is a script(code)
// e.g. "Use" => "script_code"
$script = Script::get_script_by_code( $use_script );
// echo the output of the script
if ( $script ) {
$output = $script->run_script( $this, $use_data );
// decrease the "Charges" stat of the item by 1, if it exists
// but only if the output does not contain a directive to not decrease charges
// in this case the output contains a string with "[preserve_charges]"
$do_not_decrease_charges = false;
foreach ( $output as $output_entry ) {
if ( is_string( $output_entry ) && strpos( $output_entry, '[preserve_charges]' ) !== false ) {
$do_not_decrease_charges = true;
break;
}
}
if ( $do_not_decrease_charges ) {
return $output;
}
// decrease charges, Charges = -1 means infinite charges
if ( isset( $item->stats['Charges'] ) ) {
if( intval( $item->get_stat( 'Charges' ) ) === -1 ) {
// infinite charges, do nothing
return $output;
}
$charges = intval( $item->get_stat( 'Charges' ) ) - 1;
} else {
$charges = 0;
}
// invalidate inventory cache, as the item charges have changed
adventurebook_invalidate_inventory_cache($this->id);
if ( $charges > 0 ) {
$item->stats['Charges'] = $charges;
$item->save_to_db();
} elseif ( $charges == 0 ) {
// if the item has no more charges, it is removed from the game
Item::delete_item( $item_id );
}
}
}
}
return $output;
}
public function has_item_with_id( int $item_id ): bool {
$inventory = $this->get_inventory( false );
foreach ( $inventory as $item ) {
if ( $item->id === $item_id ) {
return true;
}
}
return false;
}
/** Check if the character has an item with the given item code in his inventory
*
* @param string $item_code The code of the item to check for
*
* @return bool True if the character has the item, false otherwise
*/
public function has_item_with_code(string $item_code): bool {
$inventory = $this->get_inventory(false);
foreach ( $inventory as $item ) {
if ( $item->code === $item_code ) {
return true;
}
}
return false;
}
/** Move the character to a different zone
*
* @param int $zone_id The ID of the zone to move the character to
*
* upon moving to a different zone, all enemy spawns for this character in the new zone need to be checked
*
* @return void
*/
public
function move_to_zone(
int $zone_id
): void {
// we want to know if the character has just entered a new zone so that he can surprise enemies
// see CombatSystem::doCombat()
set_transient( 'adventurebook_just_entered_zone_' . $this->id, true );
$this->location_id = $zone_id;
$this->save_to_db();
// we save that we have visited the zone (for map purposes)
$this->set_zone_visited( $zone_id );
}
/** Set that the character has visited the given zone
*
* @param int $zone_id The ID of the zone to check
*/
public function set_zone_visited( int $zone_id ): void {
$visited_zones = $this->get_game_var( 'VisitedZones' );
if ( is_array( $visited_zones ) ) {
if ( ! in_array( $zone_id, $visited_zones, true ) ) {
$visited_zones[] = $zone_id;
$this->set_game_var( 'VisitedZones', $visited_zones );
$this->save_to_db();
}
} else {
$visited_zones = array( $zone_id );
$this->set_game_var( 'VisitedZones', $visited_zones );
$this->save_to_db();
}
}
public function get_visited_page_ids(): array {
// In diesem Plugin ist eine „Zone" identisch mit einer Seite
// (location_id = Seiten-ID). Besuche werden in move_to_zone()/
// set_zone_visited() unter 'VisitedZones' gespeichert — 'VisitedPages'
// wurde nie geschrieben. Daher hier VisitedZones lesen (das ist die
// Liste der besuchten Seiten-IDs; nötig u. a. für grüne Karten-Marker
// und die Auto-Karte für Spieler).
$visited_pages = $this->get_game_var( 'VisitedZones' );
if ( is_array( $visited_pages ) ) {
return array_map( 'intval', $visited_pages );
}
return array();
}
public function has_visited_zone( int $zone_id ): bool {
$visited_zones = $this->get_game_var( 'VisitedZones' );
if ( is_array( $visited_zones ) ) {
return in_array( $zone_id, $visited_zones, true );
}
return false;
}
/** Respawn the character by setting its current health to max health
* and saving the character to the database
*
* @return void
*/
public function respawn(): void {
// get the combat system for this adventurebook
$adventurebook_combat_system = adventurebook_get_combat_system_for_adventurebook( $this->adventurebook_id );
// set the current health to max health
$this->recalculate_max_health();
// save the character to the database
$this->save_to_db();
}
public function get_combat_system(): Combat_System {
return adventurebook_get_combat_system_for_adventurebook( $this->adventurebook_id );
}
public function get_respawn_zone_id(): int {
return intval( $this->get_stat( 'RespawnZone' ) );
}
public function get_respawn_zone_title(): string {
$respawn_zone_id = $this->get_respawn_zone_id();
if ( ! $respawn_zone_id ) {
return '';
}
$respawn_zone = get_post( $respawn_zone_id );
if ( ! $respawn_zone ) {
return '';
}
return $respawn_zone->post_title;
}
/** Set the spawn point of the character to the given zone ID
*
* @param int $zone_id The ID of the zone to set as spawn point. If 0, the current location is used.
*/
public function set_spawn_point( int $zone_id = 0 ): void {
if($zone_id == 0) {
$zone_id = $this->location_id;
}
$this->stats['RespawnZone'] = $zone_id;
$this->save_to_db();
}
static public function delete_character_by_id( int $id ): bool {
// first, get the character
$character = self::get_character_by_id( $id );
if ( $character ) {
// remove all items that belong to this character
Item::delete_items_by_character_id( $character->id );
// remove all enemy spawn points that belong to this character
Enemy_Spawn::delete_enemy_spawn_points_by_character_id( $character->id );
// delete the character from the database
$character->delete_from_db();
return true;
}
return false;
}
static public function delete_orphaned_characters(): int {
global $wpdb;
$deleted_count = 0;
$table_name = esc_sql($wpdb->prefix . 'adventurebook_characters');
$results = $wpdb->get_results( "SELECT * FROM $table_name", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
foreach ( $results as $result ) {
$user_id = intval( $result['user_id'] );
$user = get_user_by( 'id', $user_id );
if ( ! $user ) {
// user does not exist anymore, delete the character
if(self::delete_character_by_id( intval( $result['id'] ) )) {
$deleted_count++;
}
}
// get the adventurebook_id of the character
$adventurebook_id = intval( $result['adventurebook_id'] );
$adventurebook = get_post( $adventurebook_id );
if ( ! $adventurebook || $adventurebook->post_type !== 'adventurebook' ) {
// adventurebook does not exist anymore, delete the character
self::delete_character_by_id( intval( $result['id'] ) );
$deleted_count++;
}
}
return $deleted_count;
}
}