id = $id; $this->code = $code; $this->name = $name; $this->commands = json_decode( $commands, true ); } public static function get_script_by_id( int $id ): ?Script { if ( $id <= 0 ) { return null; } global $wpdb; $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM " . $wpdb->prefix . "adventurebook_scripts WHERE id = %d", $id ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery if ( $result === null ) { return null; } return new Script( $result['id'], $result['code'], $result['name'], $result['commands'] ); } public static function get_script_by_code( string $code ): ?Script { global $wpdb; $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM " . $wpdb->prefix . "adventurebook_scripts WHERE code = %s", $code ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery if ( $result === null ) { return null; } return new Script( $result['id'], $result['code'], $result['name'], $result['commands'] ); } public static function get_all_scripts(): array { global $wpdb; $results = $wpdb->get_results( "SELECT * FROM " . $wpdb->prefix . "adventurebook_scripts", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $scripts = array(); foreach ( $results as $result ) { $scripts[] = new Script( $result['id'], $result['code'], $result['name'], $result['commands'] ); } // sort by name usort( $scripts, function ( $a, $b ) { return strcmp( $a->name, $b->name ); } ); return $scripts; } public static function create_script( string $code, string $name, array $commands ): void { global $wpdb; $table_name = $wpdb->prefix . 'adventurebook_scripts'; $wpdb->insert( $table_name, array( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 'code' => $code, 'name' => $name, 'commands' => json_encode( $commands ), ) ); } public static function create_script_from_textfield(string $code, string $name, string $commands_textfield): void { $commands_info = []; // Sanitize textarea input and split into lines $lines = explode("\n", $commands_textfield); foreach ($lines as $line) { // Each line should be in the format: key|command|parameters // Example: 10|output|Hello World!|red // 20|heal|10 // Trim whitespace and skip empty lines if (trim($line) === '') { continue; } // Split line into parts list( $key, $command, $parameters ) = explode( '|', trim( $line ), 3 ); $key = intval($key); $command?:$command = strtolower(trim($command)); $parameters = trim($parameters); if($key <= 0 || !$command) { // skip invalid lines continue; } // Add to commands array $commands_info[] = [ 'key' => $key, 'command' => $command, 'parameters' => $parameters, ]; } self::create_script($code, $name, $commands_info); } public static function update_script( int $id, string $code, string $name, array $commands ): void { global $wpdb; $table_name = $wpdb->prefix . 'adventurebook_scripts'; $wpdb->update( $table_name, array( // phpcs:ignore WordPress.DB.DirectDatabaseQuery 'code' => $code, 'name' => $name, 'commands' => json_encode( $commands ), ), array( 'id' => $id ) ); } public static function delete_script( int $id ): void { global $wpdb; $table_name = $wpdb->prefix . 'adventurebook_scripts'; $wpdb->delete( $table_name, array( 'id' => $id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery } /** * Move the internal pointer to the next command. * * @return true a next command is available, false when the script has ended. */ public function next() : bool { if ( $this->_current_key == - 1 ) { // script has ended return false; } if($this->_current_key == 0) { // start at the beginning $this->_current_key = $this->get_first_key(); if($this->_current_key == 0) { // no commands in this script return false; } return true; } $this->_current_key = $this->get_next_key( $this->_current_key ); return true; } public function run_script(Character $character, ?string $script_data = ''): array { // aktuelle Position aus den Game-Variablen $this->_current_key = intval($character->get_game_var("script_" . $this->code . "_key")); if ($this->_current_key <= 0) { // start at the beginning $this->_current_key = $this->get_first_key(); } $output = []; while ($this->_current_key != -1) { // find the command with the current key $command_info = $this->get_command_by_key($this->_current_key); if ($command_info === null) { // command not found → stop the script $this->_current_key = -1; break; } // execute the command via dynamic Command-Array // merge the output from execute command into the output array $command_output = $this->execute_command($command_info, $character, $script_data); $output = array_merge($output, $command_output); // speichere aktuelle Position, damit das Script beim nächsten Mal weiterläuft $character->set_game_var("script_" . $this->code . "_key", $this->_current_key); // move to next key, falls der Befehl nicht bereits gesprungen ist if ($this->_current_key != -1 && $this->_current_key == $command_info['key']) { $this->_current_key = $this->get_next_key($this->_current_key); } } return $output; } public function get_next_key( int $current_key ): int { $found_current = false; foreach ( $this->commands as $command ) { if ( $found_current ) { return intval( $command['key'] ); } if ( $command['key'] == $current_key ) { $found_current = true; } } return 0; } public function get_command_by_key( int $key ): ?array { foreach ( $this->commands as $command ) { if ( $command['key'] == $key ) { return $command; } } return null; } public function get_first_key(): int { if ( count( $this->commands ) > 0 ) { return intval( $this->commands[0]['key'] ); } return 0; } /** * Returns an array of all available commands, including premium commands if applicable. * * @param Character|null $character Optional character context for command functions. * @return array Associative array of command names to their corresponding callable functions. */ public function get_all_commands(?Character $character = null): array { $commands = $this->get_core_commands($character); // premium commands if defined if (defined('ADVENTUREBOOK_PREMIUM') && ADVENTUREBOOK_PREMIUM) { $pro_commands = include ADVENTUREBOOK_PATH . 'includes/premium/script_commands_premium.php'; $commands = array_merge($commands, $pro_commands); } // sort commands by key ksort($commands); return $commands; } public function get_core_commands(?Character $character = null): array { return [ /* adds an effect to the character Usage: add_effect effect_name|effect_type|duration|power (see AbstractCombatant::add_effect for details) */ 'add_effect' => function($parameters) use ($character) { $parts = explode('|', $parameters, 4); if(count($parts) == 4) { $effect_name = trim($parts[0]); $effect_type = trim($parts[1]); $duration = intval(trim($parts[2])); $power = intval(trim($parts[3])); $character->add_effect($effect_name, $effect_type, $duration, $power); } return []; }, /* Adds an item to the character's inventory Usage: add_item item_code */ 'add_item' => function($parameters) use ($character) { $item_definition = Item_Definition::get_item_definition_by_code($parameters); if($item_definition !== null) { $character->add_item_to_inventory($parameters); /* translators: %s is the item name */ return ['' . sprintf(__('You received item: %s', 'adventurebook'), $item_definition->name) . '']; } return []; }, /* Heals the character by the specified amount, or full if amount is -1 Usage: heal amount */ 'heal' => function($parameters) use ($character) { $character->heal(intval($parameters)); return []; }, /* Jumps to a specific script key if the character has the given effect Usage: if_effect_goto effect_name|key */ 'if_effect_goto' => function($parameters) use ($character) { $parts = explode('|', $parameters, 2); if(count($parts) == 2) { $effect_name = trim($parts[0]); $goto_key = intval(trim($parts[1])); if($character->has_effect($effect_name)) { $this->_current_key = $goto_key; } } return []; }, /* Jumps to a specific script key if the given item is at the current location Usage: if_item_is_here_goto item_code|key */ 'if_item_is_here_goto' => function($parameters) use ($character) { $parts = explode('|', $parameters, 2); if(count($parts) == 2) { $item_code = trim($parts[0]); $goto_key = intval(trim($parts[1])); $items_here = get_items_for_page($character->location_id, $character->id); foreach($items_here as $item) { if($item->code === $item_code) { $this->_current_key = $goto_key; break; } } } return []; }, /* Outputs text to the user, optionally in a specific color Usage: output text|color Example: output Hello World!|red output Hello World! */ 'output' => function($parameters) use ($character) { $parts = explode('|', $parameters, 2); $text = trim($parts[0]); $text = $this->prepare_text($text, $character); $color = count($parts) == 2 ? trim($parts[1]) : ''; if ($color !== '') { return ['' . esc_html($text) . '']; } return [esc_html($text)]; }, /* Removes an effect from the character Usage: remove_effect effect_name */ 'remove_effect' => function($parameters) use ($character) { $effect_name = trim($parameters); $character->remove_effect($effect_name); return []; }, /* Removes an item from the character's inventory and deletes it Usage: remove_item item_code */ 'remove_item' => function($parameters) use ($character) { $item_definition = Item_Definition::get_item_definition_by_code($parameters); if($item_definition !== null) { if($character->delete_itemcode_from_inventory($parameters)) { /* translators: %s is the item name */ return ['' . sprintf(__('You lost item: %s', 'adventurebook'), $item_definition->name) . '']; } else { /* translators: %s is the item name */ return ['' . sprintf(__('Item not found in inventory: %s', 'adventurebook'), $item_definition->name) . '']; } } return []; }, /* Shows a button that jumps to a specific script key when clicked Usage: show_key_link link_text|key Example: show_key_link Go to next part|10 */ 'show_key_link' => function($parameters) use ($character) { $parts = explode('|', $parameters, 2); if(count($parts) == 2) { $link_text = trim($parts[0]); $goto_key = intval(trim($parts[1])); return ['
']; } return []; }, /* Stops the script immediately Usage: stop */ 'stop' => function($parameters) { $this->_current_key = -1; return []; }, ]; } /** * @throws ReflectionException */ public function execute_command(array $command_info, Character $character, ?string $script_data = ''): array { $output = []; $key = $command_info['key']; $command = strtolower($command_info['command']); $parameters = $command_info['parameters']; $commands = $this->get_core_commands($character); // Pro-Commands laden, nur wenn definiert if (defined('ADVENTUREBOOK_PREMIUM') && ADVENTUREBOOK_PREMIUM) { $pro_commands = include ADVENTUREBOOK_PATH . 'includes/premium/script_commands_premium.php'; $commands = array_merge($commands, $pro_commands); } if (isset($commands[$command])) { // $character and $this (Script) are available in the command functions via "use" // check if the command has two or three parameters (some commands need the script_data) $refFunc = new ReflectionFunction( $commands[ $command ] ); if ( $refFunc->getNumberOfParameters() == 3 ) { $output = array_merge($output, $commands[ $command ]( $parameters, $character, $script_data ) ); } else { $output = array_merge($output, $commands[ $command ]( $parameters, $character, $this )); } } else { $output[] = 'Unknown command: ' . esc_html($command) . ''; } // move to next command if not changed if($this->_current_key != -1 && $this->_current_key == $key) { $this->_current_key = $this->get_next_key($this->_current_key); } return $output; } /** * Prepare text for output by replacing placeholders with actual values. * * Supported placeholders: * - {var:varname}: Replaced with the value of the game variable "varname". * - {stat:statname}: Replaced with the value of the character's stat "statname". * - {itemcount:itemcode}: Replaced with the count of the item "itemcode" in the character's inventory. * - {charactername}: Replaced with the name of the character. * * @param string $text The text to prepare. * @param Character $character The character for whom to prepare the text. * * @return string The prepared text, safe for HTML output. */ private function prepare_text( string $text, Character $character ): string { // prepare text for output, e.g. by escaping HTML special characters // if $text contains {var:varname}, replace it with the value of the game var "varname" preg_match_all( '/\{var:([a-zA-Z0-9_]+)\}/', $text, $matches ); if ( count( $matches ) == 2 ) { foreach ( $matches[1] as $var_name ) { $var_value = $character->get_game_var( $var_name ); $text = str_replace( '{var:' . $var_name . '}', $var_value, $text ); } } // if $text contains {stat:statname}, replace it with the value of the stat "statname" preg_match_all( '/\{stat:([a-zA-Z0-9_]+)\}/', $text, $matches ); if ( count( $matches ) == 2 ) { foreach ( $matches[1] as $stat_name ) { $stat_value = $character->get_stat( $stat_name ); $text = str_replace( '{stat:' . $stat_name . '}', $stat_value, $text ); } } // if $text contains {itemcount:itemcode}, replace it with the count of the item "itemcode" in the inventory preg_match_all( '/\{itemcount:([a-zA-Z0-9_]+)\}/', $text, $matches ); if ( count( $matches ) == 2 ) { foreach ( $matches[1] as $item_code ) { $item_count = $character->get_item_count_by_code( $item_code, false ); $text = str_replace( '{itemcount:' . $item_code . '}', $item_count, $text ); } } // if $text contains {charactername}, replace it with the name of the character $text = str_replace( '{charactername}', $character->get_name(), $text ); return esc_html( $text ); } /** * Get all scripts that are used by pages in the given adventurebook. * * @param int $adventurebook_id The ID of the adventurebook. * @return array An array of Script objects. */ public static function get_all_scripts_by_adventurebook_id(int $adventurebook_id): array { global $wpdb; // we need to look in every adventurebook_page for the _adventurebook_page_script option and then get the script $results = $wpdb->get_results($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching "SELECT DISTINCT s.* FROM " . $wpdb->prefix . "adventurebook_scripts s JOIN " . $wpdb->prefix . "posts p ON p.post_parent = %d JOIN " . $wpdb->prefix . "postmeta pm ON pm.post_id = p.ID AND pm.meta_key = '_adventurebook_page_script' AND pm.meta_value = s.code", $adventurebook_id ), ARRAY_A);// phpcs:ignore WordPress.DB.DirectDatabaseQuery $scripts = array(); foreach ($results as $result) { $scripts[] = new Script($result['id'], $result['code'], $result['name'], $result['commands']); } // we also need to look in every item that has a "Use" stat. The Value of the "Use" stat is a script_code. // get all item definitions that have a "Use" stat $item_definitions = Item_Definition::get_all_item_definitions(); foreach ($item_definitions as $item_definition) { /* @var Item_Definition $item_definition */ $use_script_code = $item_definition->get_stat('Use'); if ($use_script_code !== null && $use_script_code !== '') { $script = Script::get_script_by_code($use_script_code); // check if a script with the same code is already in the array // we can't just use array_unique because we have distinct objects (instances of Script) $found = false; foreach ($scripts as $s) { if ( $s->code === $use_script_code ) { $found = true; break; } } if (!$found && $script !== null) { $scripts[] = $script; } } } // sort by name usort($scripts, function ($a, $b) { return strcmp($a->name, $b->name); }); return $scripts; } /** * Get all item definitions that are referenced in this script via add_item or remove_item commands. * * @return Item_Definition[] Array of Item_Definition objects. */ public function get_all_item_definitions(): array { // iterate through all commands and find the item code parameters when the command is add_item or remove_item $item_codes = []; foreach ($this->commands as $command) { $cmd = strtolower( $command['command'] ); if ( $cmd === 'add_item' || $cmd === 'remove_item' ) { $item_code = trim( $command['parameters'] ); if ( $item_code !== '' && ! in_array( $item_code, $item_codes ) ) { $item_codes[] = $item_code; } } } // get the item definitions for the found item codes $item_definitions = []; foreach ( $item_codes as $item_code ) { $item_definition = Item_Definition::get_item_definition_by_code( $item_code ); if ( $item_definition !== null ) { $item_definitions[] = $item_definition; } } return $item_definitions; } }