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;
}
}