wesnoth/data/core/macros/utils.cfg
Iris Morelle bee64fc78b Check unit/second_unit existence in FORCE_CHANCE_TO_HIT
Fixes an issue where the auxiliary event handlers for 'attack end' in
this macro might break if the affected unit or units are removed from
the map by a pre-existing 'attack end' event handler.

Unfortunately this still results in log warnings about failing to
auto-store unit/second_unit, but it's better than breaking WML execution
entirely.

    20190313 23:36:48 error engine: failed to auto-store $second_unit at (9,40)

The alternative I contemplated was to force unit/second_unit to be valid
in the event handlers by using empty filters, but then that would result
in the event handler being triggered *later* by another valid unit. This
is especially problematic since the event handler handles special
knowledge about the unit's WML ([specials][original_chance_to_hit]).
That option would not produce any spurious error messages but would
definitely cause new problems, especially when interacting with other
FCTH substitutions in the same scenario.

Ultimately FCTH's design is a bit questionable and relies too much on
state that may be broken by an external agent intentionally or otherwise
and produce unusual results.

Closes #3982.

[ci skip]
2019-03-13 23:49:34 -03:00

736 lines
21 KiB
INI

#textdomain wesnoth
# This file contains general utility macros for WML authors.
#
# Later macros in this file are built using earlier ones, which
# is why they live here rather than being broken out into topic-specific files.
# ! in comments is used in generating HTML documentation, ignore it otherwise.
#define QUANTITY NAME EASY_VALUE NORMAL_VALUE HARD_VALUE
# Macro to define a 'quantity' differently based on difficulty levels.
#ifdef EASY
{NAME}={EASY_VALUE}
#endif
#ifdef NORMAL
{NAME}={NORMAL_VALUE}
#endif
#ifdef HARD
{NAME}={HARD_VALUE}
#endif
#enddef
#define QUANTITY4 NAME EASY_VALUE NORMAL_VALUE HARD_VALUE NIGHTMARE_VALUE
# Four-difficulty version of QUANTITY
#ifdef EASY
{NAME}={EASY_VALUE}
#endif
#ifdef NORMAL
{NAME}={NORMAL_VALUE}
#endif
#ifdef HARD
{NAME}={HARD_VALUE}
#endif
#ifdef NIGHTMARE
{NAME}={NIGHTMARE_VALUE}
#endif
#enddef
# No tab or space-based indentation for these macros to avoid trouble when these macros are used
# in the middle of a quoted string literal
#
# wmlindent: start ignoring
# wmlscope: start conditionals
#ifdef EASY
#define ON_DIFFICULTY EASY_VALUE NORMAL_VALUE HARD_VALUE
{EASY_VALUE}#enddef
#endif
#ifdef NORMAL
#define ON_DIFFICULTY EASY_VALUE NORMAL_VALUE HARD_VALUE
{NORMAL_VALUE}#enddef
#endif
#ifdef HARD
#define ON_DIFFICULTY EASY_VALUE NORMAL_VALUE HARD_VALUE
{HARD_VALUE}#enddef
#endif
# wmlscope: stop conditionals
# wmlindent: stop ignoring
# wmlscope: prune ON_DIFFICULTY
# No tab or space-based indentation for these macros to avoid trouble when these macros are used
# in the middle of a quoted string literal
#
# wmlindent: start ignoring
# wmlscope: start conditionals
#ifdef EASY
#define ON_DIFFICULTY4 EASY_VALUE NORMAL_VALUE HARD_VALUE NIGHTMARE_VALUE
{EASY_VALUE}#enddef
#endif
#ifdef NORMAL
#define ON_DIFFICULTY4 EASY_VALUE NORMAL_VALUE HARD_VALUE NIGHTMARE_VALUE
{NORMAL_VALUE}#enddef
#endif
#ifdef HARD
#define ON_DIFFICULTY4 EASY_VALUE NORMAL_VALUE HARD_VALUE NIGHTMARE_VALUE
{HARD_VALUE}#enddef
#endif
#ifdef NIGHTMARE
#define ON_DIFFICULTY4 EASY_VALUE NORMAL_VALUE HARD_VALUE NIGHTMARE_VALUE
{NIGHTMARE_VALUE}#enddef
#endif
# wmlscope: stop conditionals
# wmlindent: stop ignoring
# wmlscope: prune ON_DIFFICULTY4
#define TURNS EASY_AMOUNT NORMAL_AMOUNT HARD_AMOUNT
# Macro to define number of turns for different difficulty levels.
{QUANTITY turns {EASY_AMOUNT} {NORMAL_AMOUNT} {HARD_AMOUNT}}
#enddef
#define TURNS4 EASY_AMOUNT NORMAL_AMOUNT HARD_AMOUNT NIGHTMARE_AMOUNT
# Four-difficulty version of TURNS
{QUANTITY4 turns {EASY_AMOUNT} {NORMAL_AMOUNT} {HARD_AMOUNT} {NIGHTMARE_AMOUNT}}
#enddef
#define GOLD EASY_AMOUNT NORMAL_AMOUNT HARD_AMOUNT
# Macro which will let you say {GOLD x y z} to set
# starting gold depending on easy/medium/hard - x/y/z
{QUANTITY gold {EASY_AMOUNT} {NORMAL_AMOUNT} {HARD_AMOUNT}}
#enddef
#define GOLD4 EASY_AMOUNT NORMAL_AMOUNT HARD_AMOUNT NIGHTMARE_AMOUNT
# Four-difficulty version of GOLD
{QUANTITY4 gold {EASY_AMOUNT} {NORMAL_AMOUNT} {HARD_AMOUNT} {NIGHTMARE_AMOUNT}}
#enddef
#define INCOME EASY_AMOUNT NORMAL_AMOUNT HARD_AMOUNT
# Macro which will let you say {INCOME x y z} to set
# per-turn income depending on easy/medium/hard - x/y/z
{QUANTITY income {EASY_AMOUNT} {NORMAL_AMOUNT} {HARD_AMOUNT}}
#enddef
#define INCOME4 EASY_AMOUNT NORMAL_AMOUNT HARD_AMOUNT NIGHTMARE_AMOUNT
# Four-difficulty version of INCOME
{QUANTITY4 income {EASY_AMOUNT} {NORMAL_AMOUNT} {HARD_AMOUNT} {NIGHTMARE_AMOUNT}}
#enddef
#define NO_INCOME
# Used to specify when a side should not have any income
# every turn.
income=-2
#enddef
#define ATTACK_DEPTH EASY_VALUE NORMAL_VALUE HARD_VALUE
# Macro to define AI attack depth for different difficulty levels
# (set it to 1-6)
{QUANTITY attack_depth {EASY_VALUE} {NORMAL_VALUE} {HARD_VALUE}}
#enddef
#define ATTACK_DEPTH4 EASY_VALUE NORMAL_VALUE HARD_VALUE NIGHTMARE_VALUE
# Four-difficulty version of ATTACK_DEPTH
{QUANTITY4 attack_depth {EASY_VALUE} {NORMAL_VALUE} {HARD_VALUE} {NIGHTMARE_VALUE}}
#enddef
#define NO_SCOUTS
# Macro to make an AI team not recruit scouts.
villages_per_scout=0
#enddef
#define RANDOM THING_VALUE
# Macro to quickly pick a random value (in the $random variable, to avoid
# cluttering up savegames with such temporary variables).
[set_variable]
name=random
rand={THING_VALUE}
[/set_variable]
#enddef
#define VARIABLE VAR VALUE
# Macro to initialize a variable. Strictly a syntatic shortcut.
[set_variable]
name={VAR}
value={VALUE}
[/set_variable]
#enddef
#define GLOBAL_VARIABLE NAMESPACE LOCAL_VAR_NAME GLOBAL_VAR_NAME SIDE
#Assigns a persistent variable with the contents of a standard variable.
[set_global_variable]
namespace={NAMESPACE}
from_local={LOCAL_VAR_NAME}
to_global={GLOBAL_VAR_NAME}
side={SIDE}
[/set_global_variable]
#enddef
#define VARIABLE_FROM_GLOBAL NAMESPACE GLOBAL_VAR_NAME LOCAL_VAR_NAME SIDE
#Retrieves the contents of a persistent variable and stores them in a standard variable.
[get_global_variable]
namespace={NAMESPACE}
from_global={GLOBAL_VAR_NAME}
to_local={LOCAL_VAR_NAME}
side={SIDE}
#immediate=no
[/get_global_variable]
#enddef
#define VARIABLE_OP VAR OP_NAME VALUE
# Macro to do mathematical operations on variables.
[set_variable]
name={VAR}
{OP_NAME}={VALUE}
[/set_variable]
#enddef
#define VARIABLE_CONDITIONAL VAR OP_NAME VALUE
# Macro to do conditional operations on variables.
[variable]
name={VAR}
{OP_NAME}={VALUE}
[/variable]
#enddef
#define CLEAR_VARIABLE VAR_NAME
# Macro to clear a variable previously set.
[clear_variable]
name={VAR_NAME}
[/clear_variable]
#enddef
#define CLEAR_GLOBAL_VARIABLE NAMESPACE MY_VARIABLE_NAME SIDE
# Clears a persistent variable entirely.
[clear_global_variable]
namespace={NAMESPACE}
global={MY_VARIABLE_NAME}
side={SIDE}
immediate=no
[/clear_global_variable]
#enddef
#define REPEAT NUMBER BODY_WML
# Macro to execute some WML a defined number of times.
#
# Example that causes screen to quake 5 times:
#! {REPEAT 5 (
#! {QUAKE "rumble.ogg"}
#! )}
{VARIABLE REPEAT_i 0}
[while]
[variable]
name=REPEAT_i
less_than={NUMBER}
[/variable]
[do]
{BODY_WML}
{VARIABLE_OP REPEAT_i add 1}
[/do]
[/while]
{CLEAR_VARIABLE REPEAT_i}
#enddef
#define LOOKUP_INDEX FROM_ARRAY_NAME WHERE_KEY_NAME WHERE_VALUE SAVE_AS_NAME
# Call this to lookup an array element that has a particular key-value pair
# then it saves the index of that element, or
# if the key-value pair is not found it saves the array's length
{VARIABLE {SAVE_AS_NAME} 0}
[while]
[variable]
name={SAVE_AS_NAME}
less_than=${FROM_ARRAY_NAME}.length
[/variable]
[variable]
name={FROM_ARRAY_NAME}[${SAVE_AS_NAME}].{WHERE_KEY_NAME}
not_equals={WHERE_VALUE}
[/variable]
[do]
{VARIABLE_OP {SAVE_AS_NAME} add 1}
[/do]
[/while]
#enddef
#define LOOKUP_VALUE FROM_ARRAY_NAME WHERE_KEY_NAME WHERE_VALUE SAVE_KEY_NAME DEFAULT_VALUE SAVE_AS_NAME
# Call this to look up an array element that has a particular key-value pair
# then it saves another key from that same element.
{LOOKUP_INDEX {FROM_ARRAY_NAME} {WHERE_KEY_NAME} {WHERE_VALUE} {SAVE_AS_NAME}}
[if]
[variable]
name={SAVE_AS_NAME}
numerical_equals=${FROM_ARRAY_NAME}.length
[/variable]
[then]
{VARIABLE {SAVE_AS_NAME} {DEFAULT_VALUE}}
[/then]
[else]
{VARIABLE {SAVE_AS_NAME} ${FROM_ARRAY_NAME}[${SAVE_AS_NAME}].{SAVE_KEY_NAME}}
[/else]
[/if]
#enddef
#define MODIFY_UNIT FILTER VAR VALUE
# Alters a unit variable (such as unit.x, unit.type,
# unit.side), handling all the storing and unstoring.
#
# Example that flips all spearmen to side 2:
#! {MODIFY_UNIT type=Spearman side 2}
[store_unit]
[filter]
{FILTER}
[/filter]
variable=MODIFY_UNIT_store
kill=yes
[/store_unit]
[foreach]
array=MODIFY_UNIT_store
[do]
[set_variable]
name=this_item.{VAR}
value={VALUE}
[/set_variable]
[unstore_unit]
variable=this_item
find_vacant=no
[/unstore_unit]
[/do]
[/foreach]
{CLEAR_VARIABLE MODIFY_UNIT_store}
#enddef
#define MOVE_UNIT_BY FILTER OFFSET_X OFFSET_Y
#TODO COMMENT
[store_unit]
[filter]
{FILTER}
[/filter]
variable=MOVE_UNIT_store
kill=yes
[/store_unit]
[for]
array=MOVE_UNIT_store
variable=MOVE_UNIT_BY_i
[do]
{VARIABLE_OP MOVE_UNIT_store[$MOVE_UNIT_BY_i].x add {OFFSET_X}}
{VARIABLE_OP MOVE_UNIT_store[$MOVE_UNIT_BY_i].y add {OFFSET_Y}}
[unstore_unit]
variable=MOVE_UNIT_store[$MOVE_UNIT_BY_i]
find_vacant=no
[/unstore_unit]
[/do]
[/for]
{CLEAR_VARIABLE MOVE_UNIT_store}
#enddef
#define MOVE_UNIT FILTER TO_X TO_Y
# Moves a unit from its current location to the given location, displaying
# movement normally.
#
# Note that setting the destination on an existing unit does not kill either
# one, but causes the unit to move to the nearest vacant hex instead.
[move_unit]
{FILTER}
to_x={TO_X}
to_y={TO_Y}
fire_event=no
[/move_unit]
#enddef
#define FULL_HEAL FILTER
[heal_unit]
[filter]
{FILTER}
[/filter]
amount=full
restore_statuses=yes
[/heal_unit]
#enddef
#define PUT_TO_RECALL_LIST FILTER
# This places a given unit back to the recall list of the side it is on.
# Note however, that the unit is not healed to full health, so when
# recalled (even if not until the next scenario) the unit may have less
# than his maximum hp left.
#
# An example that returns all units stepping on (20,38) back to the recall
# list:
#
#! [event]
#! name=moveto
#!
#! [filter]
#! x,y=20,38
#! [/filter]
#!
#! {PUT_TO_RECALL_LIST x,y=20,38}
#! [/event]
[put_to_recall_list]
{FILTER}
[/put_to_recall_list]
#enddef
#define RECRUIT_UNIT_VARIATIONS SIDE TYPE VARIATIONS_VALUE
# Allows a side to seemingly recruit variations of a given unit, such as the
# the Walking Corpse.
#
# An example which makes side 2 have a 50% chance of getting a normal WC
# and a 50% chance of getting either a drake or dwarf variation:
#! {RECRUIT_UNIT_VARIATIONS 2 "Walking Corpse" none,none,drake,dwarf}
[event]
name=prerecruit
first_time_only=no
[filter]
side={SIDE}
type={TYPE}
[/filter]
{VARIABLE_OP recruited_unit_random_variation rand {VARIATIONS_VALUE}}
[if]
[variable]
name=recruited_unit_random_variation
not_equals=none
[/variable]
[then]
[object]
duration=forever
silent=yes
[filter]
x,y=$x1,$y1
[/filter]
[effect]
apply_to=variation
name=$recruited_unit_random_variation
[/effect]
[effect]
apply_to=hitpoints
heal_full=yes
[/effect]
[/object]
[/then]
[/if]
{CLEAR_VARIABLE recruited_unit_random_variation}
[/event]
#enddef
#define SCATTER_UNITS NUMBER TYPES PADDING_RADIUS FILTER UNIT_WML
# Scatters the given kind of units randomly on a given area on the map.
#
# An example which scatters some loyal elves on forest hexes in
# x,y=10-30,20-40, at a minimum of three hexes apart from each other and
# never on top of or adjacent to any already existing units:
#! {SCATTER_UNITS 20 "Elvish Fighter,Elvish Archer,Elvish Shaman" 3 (
#! terrain=Gs^Fp
#! x=10-30
#! y=20-40
#!
#! [not]
#! [filter]
#! [/filter]
#! [/not]
#!
#! [not]
#! [filter_adjacent_location]
#! [filter]
#! [/filter]
#! [/filter_adjacent_location]
#! [/not]
#! ) (
#! side=2
#! generate_name=yes
#! random_traits=yes
#!
#! [modifications]
#! {TRAIT_LOYAL}
#! [/modifications]
#! )}
[set_variables]
name=unit_type_table
[split]
list={TYPES}
key=type
separator=,
[/split]
[/set_variables]
{VARIABLE unit_type_table_i 0}
[random_placement]
num_items={NUMBER}
variable=random_placement_location
allow_less=yes
min_distance={PADDING_RADIUS}
[filter_location]
{FILTER}
include_borders=no
[/filter_location]
[command]
[unit]
type=$unit_type_table[$unit_type_table_i].type
x,y=$random_placement_location.x,$random_placement_location.y
{UNIT_WML}
[/unit]
{VARIABLE unit_type_table_i ("$(($unit_type_table_i + 1) % $unit_type_table.length)")}
[/command]
[/random_placement]
{CLEAR_VARIABLE unit_type_table,unit_type_table_i,random_placement_location}
#enddef
#define FORCE_CHANCE_TO_HIT FILTER SECOND_FILTER CTH_NUMBER EXTRA_CONDITIONS_WML
# Invisibly forces certain units to always have a specific chance to hit
# when fighting against certain other units.
#
# Note that the player still only sees the regular damage calculations, so
# this is useful if you need to give an invisible helping hand to the player
# or AI. For example, if the player is forced to attack with only a couple
# of units at the beginning of a scenario, you can use this to ensure that
# simply having bad luck cannot ruin their attempt so easily. Also you might
# have enemy leaders which the player is not supposed to fight or be able to
# defeat due to storyline reasons, but could theoretically still kill with
# some clever trick, AI mistake or sheer exceptional luck.
#
# An example which forces Konrad's attacks to always hit Li'sar, but only
# after turn 10:
#! {FORCE_CHANCE_TO_HIT id=Konrad id="Li'sar" 100 (
#! [variable]
#! name=turn_number
#! greater_than=10
#! [/variable]
#! )}
[event]
name=attack
first_time_only=no
[filter]
{FILTER}
[/filter]
[filter_second]
{SECOND_FILTER}
[/filter_second]
[filter_condition]
[and]
{EXTRA_CONDITIONS_WML}
[/and]
[/filter_condition]
[foreach]
array=unit.attack
[do]
[if]
#This is to mute a warning message about retrieving a member of non-existant wml container.
[variable]
name=this_item.specials.length
greater_than=0
[/variable]
[variable]
name=this_item.specials.chance_to_hit.length
greater_than=0
[/variable]
[then]
[set_variables]
name=this_item.specials.original_chance_to_hit
to_variable=this_item.specials.chance_to_hit
[/set_variables]
{CLEAR_VARIABLE this_item.specials.chance_to_hit}
[/then]
[/if]
[set_variables]
name=this_item.specials.chance_to_hit
[value]
id=forced_cth
value={CTH_NUMBER}
cumulative=no
[/value]
[/set_variables]
[/do]
[/foreach]
[unstore_unit]
variable=unit
find_vacant=no
[/unstore_unit]
[event]
name=attack end
delayed_variable_substitution=yes
[if]
[variable]
name=unit.length
numerical_equals=0
[/variable]
[then]
# Unit vanished mid-fight
[return][/return]
[/then]
[/if]
[foreach]
array=unit.attack
[do]
{CLEAR_VARIABLE this_item.specials.chance_to_hit}
[set_variables]
name=this_item.specials.chance_to_hit
to_variable=this_item.specials.original_chance_to_hit
[/set_variables]
{CLEAR_VARIABLE this_item.specials.original_chance_to_hit}
[/do]
[/foreach]
[unstore_unit]
variable=unit
find_vacant=no
[/unstore_unit]
[/event]
[/event]
# The following event is a simple duplicates of the above ones, with the
# primary and secondary units reversed so that the effect is applied also on
# defense.
[event]
name=attack
first_time_only=no
[filter]
{SECOND_FILTER}
[/filter]
[filter_second]
{FILTER}
[/filter_second]
[filter_condition]
[and]
{EXTRA_CONDITIONS_WML}
[/and]
[/filter_condition]
[foreach]
array=second_unit.attack
[do]
[if]
[variable]
name=this_item.specials.length
greater_than=0
[/variable]
[variable]
name=this_item.specials.chance_to_hit.length
greater_than=0
[/variable]
[then]
[set_variables]
name=this_item.specials.original_chance_to_hit
to_variable=this_item.specials.chance_to_hit
[/set_variables]
{CLEAR_VARIABLE this_item.specials.chance_to_hit}
[/then]
[/if]
[set_variables]
name=this_item.specials.chance_to_hit
[value]
id=forced_cth
value={CTH_NUMBER}
cumulative=no
[/value]
[/set_variables]
[/do]
[/foreach]
[unstore_unit]
variable=second_unit
find_vacant=no
[/unstore_unit]
[event]
name=attack end
delayed_variable_substitution=yes
[if]
[variable]
name=second_unit.length
numerical_equals=0
[/variable]
[then]
# Unit vanished mid-fight
[return][/return]
[/then]
[/if]
[foreach]
array=second_unit.attack
[do]
{CLEAR_VARIABLE this_item.specials.chance_to_hit}
[set_variables]
name=this_item.specials.chance_to_hit
to_variable=this_item.specials.original_chance_to_hit
[/set_variables]
{CLEAR_VARIABLE this_item.specials.original_chance_to_hit}
[/do]
[/foreach]
[unstore_unit]
variable=second_unit
find_vacant=no
[/unstore_unit]
[/event]
[/event]
#enddef
#define LOOT AMOUNT SIDE
{VARIABLE amount_gold {AMOUNT}}
#TODO add message for the other players!
[message]
side_for={SIDE}
speaker=narrator
message= _ "You retrieve $amount_gold pieces of gold."
image=wesnoth-icon.png
sound=gold.ogg
[/message]
{CLEAR_VARIABLE amount_gold}
[gold]
side={SIDE}
amount={AMOUNT}
[/gold]
#enddef
#define CREDITS_SEPARATOR
[entry]
name = "•"
[/entry]
#enddef