diff --git a/build.sh b/build.sh index ea192ccddc..aae78ffc90 100755 --- a/build.sh +++ b/build.sh @@ -146,6 +146,15 @@ else echo "Error: environment '$ENV' not found" && exit 1 fi +case "$ENV" in + osrs_*) + python3 ocean/osrs/scripts/osrs_asset_manifest.py generate-c-header \ + ocean/osrs/asset_manifest.json \ + --output ocean/osrs/osrs_assets_generated.h + bash ocean/osrs/scripts/setup-data.sh + ;; +esac + OUTPUT_NAME=${OUTPUT_NAME:-$ENV} SRC_FILE=${SRC_FILE:-$SRC_DIR/$ENV.c} @@ -267,6 +276,7 @@ if [ "$MODE" = "native" ]; then -I$CUDA_HOME/include $NCCL_IFLAG -I$RAYLIB_NAME/include \ "${ENV_COMPILE_FLAGS[@]}" \ -DENV_NAME=$ENV \ + -DPUFFER_ENV_NAME="\"$ENV\"" \ -DPUFFERLIB_BUILD_MAIN \ -Xcompiler=-DPLATFORM_DESKTOP \ -Xcompiler=-fopenmp \ @@ -287,6 +297,7 @@ elif [ "$MODE" = "profile" ]; then -I$CUDA_HOME/include $NCCL_IFLAG -I$RAYLIB_NAME/include \ "${ENV_COMPILE_FLAGS[@]}" \ -DENV_NAME=$ENV \ + -DPUFFER_ENV_NAME="\"$ENV\"" \ -Xcompiler=-DPLATFORM_DESKTOP \ $PRECISION \ -Xcompiler=-fopenmp \ diff --git a/config/osrs_colosseum.ini b/config/osrs_colosseum.ini new file mode 100644 index 0000000000..a0c4ba505a --- /dev/null +++ b/config/osrs_colosseum.ini @@ -0,0 +1,142 @@ +# OSRS Fortis Colosseum encounter. + +[base] +env_name = osrs_colosseum +async = 0 + +[env] +start_wave = 1 +damage_reward_coeff = 0.01 +wave_clear_bonus = 0.9480448239932152 +win_bonus = 0.6761689252871952 +death_penalty_coeff = 1.0 +timeout_penalty = 0.42966922179495604 +farm_safe_damage_cap = 1 +farm_cap_waves = 4 +boss_damage_reward_coeff = 0.0 +boss_phase_bonus = 0.930438578582552 +argmax_gear_reward_coeff = 0.003360076095091039 +offensive_boost_reward_coeff = 0.01150336488068926 +loadout_profile_mode = 2 +beginner_loadout_fraction = 0.5 +step_out_forecast_obs_enabled = 1 +threat_field_obs_enabled = 1 +forecast_horizon = 4 +mask_inventory_heads = 0 +prayer_oracle_mode = 0 +late_start_state_mode = 2 +late_start_supply_fraction_per_wave = 0.07 +bis_gear_oracle_mode = 0 +invuln_mode = 0 +episode_max_ticks_override = 0 +remove_brews = 0 +prayer_switch_fail_prob = 0.0 +player_damage_received_scale = 1.0 +damage_scale_anneal_start = 1.0 +damage_scale_anneal_ticks = 0 +classic_curriculum_mode = 1 +curriculum_num_tiers = 3 +curriculum_wave_1 = 4 +curriculum_frac_1 = 0.07367961709476761 +curriculum_wave_2 = 8 +curriculum_frac_2 = 0.03234710556986353 +curriculum_wave_3 = 12 +curriculum_frac_3 = 0.009776253765422502 + +[vec] +total_agents = 8192 +num_buffers = 8 +num_threads = 32 +action_mask_size = 452 +gpu_env = 0 + +[policy] +hidden_size = 2048 +num_layers = 2 +expansion_factor = 1 + +[train] +total_timesteps = 143245903 +horizon = 16 +min_lr_ratio = 0.32764059472071716 +learning_rate = 0.0003724286337024778 +momentum = 0.999 +ent_coef = 1e-07 +gamma = 0.9999997409988988 +gae_lambda = 0.41662511987289663 +vtrace_rho_clip = 2.0739707238638987 +vtrace_c_clip = 3.444175403341336 +prio_alpha = 0.2617270145386202 +prio_beta0 = 0.0 +clip_coef = 0.07092591964303953 +vf_coef = 5.0 +vf_clip_coef = 0.0001 +max_grad_norm = 2.5111866624698655 +replay_ratio = 2.0 +minibatch_size = 4096 + +[sweep] +max_suggestion_cost = 1200 +max_runs = 400 +gpus = 1 +early_stop_quantile = 0.25 + +[sweep.train.total_timesteps] +distribution = log_normal +min = 10_000_000 +max = 2_000_000_000 +scale = time + +[sweep.env.damage_reward_coeff] +distribution = uniform +min = 0.0 +max = 0.010 +scale = auto + +[sweep.env.win_bonus] +distribution = uniform +min = 0.0 +max = 1.0 +scale = auto + +[sweep.env.boss_phase_bonus] +distribution = uniform +min = 0.0 +max = 1.0 +scale = auto + +[sweep.env.death_penalty_coeff] +distribution = uniform +min = 0.0 +max = 1.0 +scale = auto + +[sweep.env.timeout_penalty] +distribution = uniform +min = 0.0 +max = 1.0 +scale = auto + +[sweep.env.argmax_gear_reward_coeff] +distribution = uniform +min = 0.0 +max = 0.05 +scale = auto + +[sweep.env.offensive_boost_reward_coeff] +distribution = uniform +min = 0.0 +max = 0.05 +scale = auto + +[sweep.train.vf_coef] +distribution = uniform +min = 0.1 +max = 8.0 +scale = auto + +[sweep.train.vf_clip_coef] +distribution = uniform +min = 0.0001 +max = 1.0 +scale = auto diff --git a/config/osrs_inferno.ini b/config/osrs_inferno.ini new file mode 100644 index 0000000000..f3b237c33b --- /dev/null +++ b/config/osrs_inferno.ini @@ -0,0 +1,368 @@ +# OSRS Inferno encounter. + +[base] +env_name = osrs_inferno +async = 0 + +[env] +start_wave = 1 +damage_reward_coeff = 0.003202292729630961 +offensive_prayer_reward_coeff = 0.0 +shield_penalty_coeff = 0.0011302588091111276 +tag_reward_coeff = 0.30118094577712423 +shield_tag_reward_coeff = 0.007639990039188423 +late_start_supply_profile_scale = 1.0 +curriculum_supply_jitter_mode = 0 +curriculum_supply_shared_jitter = 0.0 +curriculum_supply_brew_jitter = 0.0 +curriculum_supply_restore_jitter = 0.0 +curriculum_no_brew_mode = 0 +curriculum_no_brew_frac = 0.0 +loadout_profile_mode = 0 +budget_loadout_fraction = 0.0 +classic_curriculum_mode = 1 +reward_profile = 2 +supply_milestone_brew_reward_coeff = 0.0 +supply_milestone_restore_reward_coeff = 0.0 +death_penalty_coeff = 0.0 +terminal_penalty_enabled = 0 +step_out_forecast_obs_mode = 1 +phase_900_bonus = 0.0 +phase_600_bonus = 0.0 +phase_300_bonus = 0.0 +shield_penalty_episode_cap = 0.0 +curriculum_wave_1 = 67 +curriculum_frac_1 = 0.07966263361799769 +curriculum_wave_2 = 69 +curriculum_frac_2 = 0.10790967791778436 +curriculum_wave_3 = 70 +curriculum_frac_3 = 0.028988486162329408 +curriculum_wave_4 = 71 +curriculum_frac_4 = 0.12 +curriculum_wave_5 = 54 +curriculum_frac_5 = 0.2755245061325432 +curriculum_wave_6 = 61 +curriculum_frac_6 = 0.0 +curriculum_frac_7 = 0.0 +curriculum_frac_8 = 0.0 + +jad_damage_reward_coeff = 0.00015023162781065922 +zuk_healer_damage_reward_coeff = 0.00241665829508734 +set_damage_reward_coeff = 0.0007111102646880334 +jad_kill_bonus = 0.5859361656430043 +zuk_healer_kill_bonus = 0.1212805575396707 +set_kill_bonus = 0.198282607827071 +post_healer_zuk_damage_coeff = 0.0 +post_healer_set_damage_reward_coeff = 0.0 +post_healer_set_kill_bonus = 0.0 +post_healer_set_alive_tick_penalty_coeff = 0.0 +post_healer_set_alive_penalty_cap = 0.0 +zuk_healer_phase_hp_delta_coeff = 0.0 +zuk_untagged_healer_tick_penalty_coeff = 0.0 +zuk_untagged_healer_target_bonus_coeff = 0.0 +zuk_safe_untagged_healer_target_bonus_coeff = 0.0 +zuk_untagged_healer_nonmagic_attack_bonus_coeff = 0.2571120615359763 +zuk_healer_mage_attack_penalty_coeff = 0.10482684235829139 +zuk_safe_untagged_healer_target_mask = 0 +zuk_force_safe_untagged_healer_target_mask = 0 +zuk_healer_reward_mode = 0 +joseph_reward_mode = 1 +oracle_mode = 0 +post_jad_zuk_multiplier = 1.5 +jad_alive_zuk_multiplier = 0.0988386136648078 + +[vec] +total_agents = 4096 +num_buffers = 2 +num_threads = 16 +action_mask_size = 89 +gpu_env = 0 + +[policy] +hidden_size = 512 +num_layers = 2 +expansion_factor = 1 + +[train] +total_timesteps = 138326477 +horizon = 16 +min_lr_ratio = 0.000980071012242606 +learning_rate = 0.0008596917323041326 +momentum = 0.9641862728826048 +ent_coef = 0.0000779653735730281 +gamma = 0.9985 +gae_lambda = 0.8103042852547117 +vtrace_rho_clip = 1.329703794225126 +vtrace_c_clip = 2.660933625087891 +prio_alpha = 0.0999082172361816 +prio_beta0 = 0.290550947491407 +clip_coef = 0.008335857515250372 +vf_coef = 1.9655295251669234 +vf_clip_coef = 0.03 +max_grad_norm = 1.9495704455155376 +replay_ratio = 4.0 +minibatch_size = 4096 + +[sweep] +max_suggestion_cost = 4800 +max_runs = 400 +gpus = 1 +early_stop_quantile = 0.25 + +[sweep.train.total_timesteps] +distribution = log_normal +min = 50_000_000 +max = 1_000_000_000 +scale = time + +[sweep.train.learning_rate] +distribution = log_normal +min = 0.0005 +max = 0.004 +scale = 0.5 + +[sweep.train.ent_coef] +distribution = log_normal +min = 0.0000000001 +max = 0.0002 +scale = auto + +[sweep.train.gamma] +distribution = logit_normal +min = 0.9975 +max = 0.9999999999 +scale = auto + +[sweep.train.gae_lambda] +distribution = logit_normal +min = 0.25 +max = 0.999 +scale = auto + +[sweep.train.min_lr_ratio] +distribution = uniform +min = 0.0 +max = 0.1 +scale = auto + +[sweep.train.vtrace_rho_clip] +distribution = uniform +min = 0.8 +max = 2.0 +scale = auto + +[sweep.train.vtrace_c_clip] +distribution = uniform +min = 2.1 +max = 3.1 +scale = auto + +[sweep.train.prio_alpha] +distribution = logit_normal +min = 0.0 +max = 0.25 +scale = auto + +[sweep.train.prio_beta0] +distribution = logit_normal +min = 0.05 +max = 0.4 +scale = auto + +[sweep.train.clip_coef] +distribution = uniform +min = 0.001 +max = 0.12 +scale = auto + +[sweep.train.vf_coef] +distribution = log_normal +min = 0.02 +max = 4.0 +scale = auto + +[sweep.train.vf_clip_coef] +distribution = uniform +min = 0.03 +max = 0.25 +scale = auto + +[sweep.train.max_grad_norm] +distribution = uniform +min = 0.3 +max = 8.0 +scale = auto + +[sweep.train.replay_ratio] +distribution = uniform +min = 3.0 +max = 6.0 +scale = auto + +[sweep.env.curriculum_frac_1] +distribution = uniform +min = 0.04 +max = 0.16 +scale = auto + +[sweep.env.curriculum_frac_2] +distribution = uniform +min = 0.04 +max = 0.18 +scale = auto + +[sweep.env.curriculum_frac_3] +distribution = uniform +min = 0.01 +max = 0.07 +scale = auto + +[sweep.env.curriculum_frac_4] +distribution = uniform +min = 0.04 +max = 0.18 +scale = auto + +[sweep.env.curriculum_wave_5] +distribution = int_uniform +min = 52 +max = 59 +scale = auto + +[sweep.env.curriculum_frac_5] +distribution = uniform +min = 0.03 +max = 0.45 +scale = auto + +[sweep.env.curriculum_supply_jitter_mode] +distribution = int_uniform +min = 0 +max = 3 +scale = auto + +[sweep.env.curriculum_supply_shared_jitter] +distribution = uniform +min = 0.0 +max = 0.30 +scale = auto + +[sweep.env.curriculum_supply_brew_jitter] +distribution = uniform +min = 0.0 +max = 0.25 +scale = auto + +[sweep.env.curriculum_supply_restore_jitter] +distribution = uniform +min = 0.0 +max = 0.25 +scale = auto + +[sweep.env.curriculum_no_brew_mode] +distribution = int_uniform +min = 0 +max = 3 +scale = auto + +[sweep.env.curriculum_no_brew_frac] +distribution = uniform +min = 0.0 +max = 0.35 +scale = auto + +[sweep.env.damage_reward_coeff] +distribution = log_normal +min = 0.0015 +max = 0.006 +scale = auto + +[sweep.env.offensive_prayer_reward_coeff] +distribution = uniform +min = 0.0 +max = 1.0 +scale = auto + +[sweep.env.shield_penalty_coeff] +distribution = uniform +min = 0.0 +max = 0.003 +scale = auto + +[sweep.env.tag_reward_coeff] +distribution = log_normal +min = 0.05 +max = 1.25 +scale = auto + +[sweep.env.shield_tag_reward_coeff] +distribution = uniform +min = 0.0 +max = 0.08 +scale = auto + +[sweep.env.jad_damage_reward_coeff] +distribution = uniform +min = 0.0 +max = 0.004 +scale = auto + +[sweep.env.zuk_healer_damage_reward_coeff] +distribution = log_normal +min = 0.0002 +max = 0.006 +scale = auto + +[sweep.env.set_damage_reward_coeff] +distribution = log_normal +min = 0.0001 +max = 0.010 +scale = auto + +[sweep.env.jad_kill_bonus] +distribution = uniform +min = 0.05 +max = 0.90 +scale = auto + +[sweep.env.zuk_healer_kill_bonus] +distribution = uniform +min = 0.0 +max = 0.90 +scale = auto + +[sweep.env.set_kill_bonus] +distribution = uniform +min = 0.0 +max = 0.90 +scale = auto + +[sweep.env.zuk_untagged_healer_nonmagic_attack_bonus_coeff] +distribution = uniform +min = 0.0 +max = 0.70 +scale = auto + +[sweep.env.zuk_healer_mage_attack_penalty_coeff] +distribution = uniform +min = 0.0 +max = 0.30 +scale = auto + +[sweep.env.post_jad_zuk_multiplier] +distribution = uniform +min = 0.10 +max = 2.50 +scale = auto + +[sweep.env.jad_alive_zuk_multiplier] +distribution = uniform +min = 0.0 +max = 0.80 +scale = auto + +[sweep.env.step_out_forecast_obs_mode] +distribution = int_uniform +min = 1 +max = 3 +scale = auto diff --git a/ocean/osrs/.gitignore b/ocean/osrs/.gitignore new file mode 100644 index 0000000000..8fce603003 --- /dev/null +++ b/ocean/osrs/.gitignore @@ -0,0 +1 @@ +data/ diff --git a/ocean/osrs/README.md b/ocean/osrs/README.md new file mode 100644 index 0000000000..2c7c5cd4f1 --- /dev/null +++ b/ocean/osrs/README.md @@ -0,0 +1,23 @@ +# OSRS encounters + +## Build + +```bash +./build.sh osrs_colosseum +./build.sh osrs_inferno +``` + +## Train and eval + +```bash +./puffer train osrs_colosseum +./puffer eval osrs_colosseum +./puffer eval osrs_colosseum --load-model-path=checkpoints/osrs_colosseum/model.bin +./puffer sweep osrs_inferno +``` + +## Tests + +```bash +cc -std=c11 -O2 -I. -o /tmp/t ocean/osrs/tests/test_colosseum_golden.c -lm && /tmp/t +``` diff --git a/ocean/osrs/asset_manifest.json b/ocean/osrs/asset_manifest.json new file mode 100644 index 0000000000..7bb8e00e83 --- /dev/null +++ b/ocean/osrs/asset_manifest.json @@ -0,0 +1,182 @@ +{ + "format": "puffer-osrs-asset-manifest-v1", + "asset_version": "osrs-assets-v22", + "archive": { + "name": "osrs-assets-v22.tar.gz", + "url": "https://github.com/valtterivalo/PufferLib/releases/download/osrs-assets-v22/osrs-assets-v22.tar.gz", + "sha256": "48c6c0c0ebbb6454b00732977f987267797ad70cf1ad7ee8e0f0781014bb6c12", + "strip_components": 1 + }, + "required_groups": [ + { + "name": "core", + "files": [ + "equipment.models", + "equipment.anims", + "equipment.atlas", + "equipment.tanim", + "projectiles.models", + "projectiles.atlas", + "projectiles.anims", + "spotanims.bin", + "fonts/runescape.ttf", + "fonts/runescape_small.ttf", + "ui/interfaces.bin", + "ui/interface_manifest.json" + ] + }, + { + "name": "inferno", + "files": [ + "inferno.models", + "inferno.anims", + "inferno.terrain", + "inferno.objects", + "inferno.atlas", + "inferno_zuk.objects", + "inferno.cmap", + "inferno_npcs.models", + "inferno_npcs.anims", + "inferno_npcs.atlas" + ] + }, + { + "name": "zulrah", + "files": [ + "zulrah.models", + "zulrah.anims", + "zulrah.terrain", + "zulrah.objects", + "zulrah.atlas", + "zulrah.cmap" + ] + }, + { + "name": "colosseum", + "files": [ + "colosseum.terrain", + "colosseum.objects", + "colosseum.atlas", + "colosseum.cmap", + "colosseum_npcs.models", + "colosseum_npcs.anims", + "colosseum_npcs.atlas", + "sprites/colosseum/modifiers/5534.png", + "sprites/colosseum/modifiers/5535.png", + "sprites/colosseum/modifiers/5536.png", + "sprites/colosseum/modifiers/5537.png", + "sprites/colosseum/modifiers/5538.png", + "sprites/colosseum/modifiers/5539.png", + "sprites/colosseum/modifiers/5540.png", + "sprites/colosseum/modifiers/5541.png", + "sprites/colosseum/modifiers/5542.png", + "sprites/colosseum/modifiers/5543.png", + "sprites/colosseum/modifiers/5544.png", + "sprites/colosseum/modifiers/5545.png", + "sprites/colosseum/modifiers/5546.png", + "sprites/colosseum/modifiers/5547.png", + "sprites/colosseum/modifiers/5549.png", + "sprites/colosseum/modifiers/5550.png", + "sprites/colosseum/modifiers/5551.png", + "sprites/colosseum/modifiers/5552.png", + "sprites/colosseum/modifiers/5553.png", + "sprites/colosseum/modifiers/5554.png", + "sprites/colosseum/modifiers/5556.png", + "sprites/colosseum/modifiers/5558.png", + "sprites/colosseum/modifiers/5559.png", + "sprites/colosseum/modifiers/5562.png", + "sprites/colosseum/modifiers/5564.png", + "sprites/colosseum/modifiers/5565.png", + "sprites/colosseum/modifiers/5566.png", + "sprites/colosseum/modifiers/5567.png", + "sprites/colosseum/modifiers/5568.png", + "sprites/colosseum/modifiers/5569.png", + "sprites/colosseum/modifiers/5571.png", + "sprites/colosseum/modifiers/5573.png", + "sprites/colosseum/modifiers/5574.png", + "sprites/colosseum/modifiers/5577.png" + ] + }, + { + "name": "gui", + "files": [ + "sprites/gui/compass.png", + "sprites/gui/side_panel_bg.png", + "sprites/gui/orb_frame.png", + "sprites/gui/headicons_prayer_0.png", + "sprites/gui/hitmarks_0.png", + "sprites/gui/cross_yellow_1.png", + "sprites/gui/side_icon_inventory.png", + "sprites/gui/tradebacking_dark.png", + "sprites/gui/osrs_stretch_side_topbottom_0.png", + "sprites/gui/wornicons_11.png", + "sprites/gui/skill_icon_23.png", + "sprites/gui/prayeron_24.png", + "sprites/gui/magicon_47.png", + "sprites/gui/standard_spell_on_79.png", + "sprites/gui/minimap_alpha_mask.png", + "sprites/gui/prayerglow_0.png" + ] + }, + { + "name": "items", + "files": [ + "sprites/items/30070.png", + "sprites/items/28945.png", + "sprites/items/27641.png", + "sprites/items/item_stack_variants.tsv" + ] + }, + { + "name": "headers", + "files": [ + "item_models.h", + "player_models.h", + "npc_models.h", + "npc_models_inferno.h", + "npc_models_zulrah.h", + "npc_models_colosseum.h" + ] + }, + { + "name": "combat_visuals", + "files": [ + "projectiles.models", + "projectiles.atlas", + "projectiles.anims", + "spotanims.bin", + "equipment.anims", + "inferno.anims", + "zulrah.anims" + ] + }, + { + "name": "wilderness", + "files": [ + "wilderness.cmap", + "wilderness.terrain", + "wilderness.objects", + "wilderness.atlas" + ] + }, + { + "name": "pvp", + "files": [ + "equipment.models", + "equipment.anims", + "equipment.atlas", + "equipment.tanim", + "projectiles.models", + "projectiles.atlas", + "projectiles.anims", + "spotanims.bin", + "ui/interfaces.bin", + "ui/interface_manifest.json", + "wilderness.cmap", + "wilderness.terrain", + "wilderness.objects", + "wilderness.atlas" + ] + } + ] +} \ No newline at end of file diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc new file mode 100644 index 0000000000..2512da5983 --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_boss.inc @@ -0,0 +1,639 @@ +#line 76 "encounter_colosseum.h" + +static int col_sol_clamp_active(const ColosseumState* s) { + return s->wave == COLO_WAVE_BOSS && s->sol.started; +} + +static int col_in_boss_arena(const ColosseumState* s, int x, int y) { + return x > s->sol.boss_arena_min_x && x < s->sol.boss_arena_max_x && + y > s->sol.boss_arena_min_y && y < s->sol.boss_arena_max_y; +} + +static int col_sol_phase_for_hp(int hp) { + int max = COLO_SOL_HP_MAX; + if (hp > (max * 90) / 100) return 0; + if (hp > (max * 75) / 100) return 1; + if (hp > (max * 50) / 100) return 2; + if (hp > (max * 25) / 100) return 3; + if (hp > (max * 10) / 100) return 4; + return 5; +} + +static int col_sol_find_idx(const ColosseumState* s) { + for (int i = 0; i < COLO_MAX_NPCS; i++) { + const ColoNPC* npc = &s->npcs[i]; + if (col_npc_is_live_target(npc) && npc->type == COLO_SOL_HEREDIT) + return i; + } + return -1; +} + +static int col_player_overhead_protect_on(const ColosseumState* s) { + return s->player.prayer == PRAYER_PROTECT_MELEE || + s->player.prayer == PRAYER_PROTECT_RANGED || + s->player.prayer == PRAYER_PROTECT_MAGIC; +} + +static void col_sol_begin_boss_arena(ColosseumState* s) { + s->sol = (SolHereditState){0}; + s->sol.started = 1; + s->sol.boss_idx = -1; + s->sol.force_spear = 1; + s->sol.laser_cooldown = COLO_SOL_CRYSTAL_COOLDOWN_MIN; + s->sol.boss_arena_min_x = COLO_BOSS_ARENA_MIN_X; + s->sol.boss_arena_min_y = COLO_BOSS_ARENA_MIN_Y; + s->sol.boss_arena_max_x = COLO_BOSS_ARENA_MAX_X; + s->sol.boss_arena_max_y = COLO_BOSS_ARENA_MAX_Y; + + s->player.x = COLO_BOSS_PLAYER_START_X; + s->player.y = COLO_BOSS_PLAYER_START_Y; + s->player_dest_x = -1; + s->player_dest_y = -1; + osrs_interaction_clear(&s->interaction); + col_rebuild_player_collision_flags(s); +} + +static void col_sol_cast_aoe(ColosseumState* s, int idx, int kind, int variant); +static void col_sol_start_triple_parry(ColosseumState* s, int idx); +static void col_sol_start_grapple(ColosseumState* s); +static void col_sol_fire_lasers(ColosseumState* s); + +static int col_sol_attack_delay(int kind, int phase) { + switch (kind) { + case COLO_SOL_ATTACK_SPEAR: + return phase < 2 ? COLO_SOL_SPEAR_DELAY : COLO_SOL_SPEAR_DELAY_FAST; + case COLO_SOL_ATTACK_SHIELD: + return phase < 2 ? COLO_SOL_SHIELD_DELAY : COLO_SOL_SHIELD_DELAY_FAST; + case COLO_SOL_ATTACK_TRIPLE: + return phase == 2 ? COLO_SOL_TRIPLE_DELAY_MID : COLO_SOL_TRIPLE_DELAY; + case COLO_SOL_ATTACK_GRAPPLE: + return COLO_SOL_GRAPPLE_DELAY; + default: + assert(0 && "next delay queried for a non-attack"); + return COLO_SOL_SPEAR_DELAY; + } +} + +static int col_sol_attack_freeze(int kind, int phase) { + switch (kind) { + case COLO_SOL_ATTACK_SPEAR: return COLO_SOL_SPEAR_FREEZE; + case COLO_SOL_ATTACK_SHIELD: return COLO_SOL_SHIELD_FREEZE; + case COLO_SOL_ATTACK_GRAPPLE: return COLO_SOL_GRAPPLE_FREEZE; + case COLO_SOL_ATTACK_TRIPLE: + return 2 * COLO_SOL_TP_HIT_GAP + + (phase >= 3 ? COLO_SOL_TP_HIT3_GAP_LOW : COLO_SOL_TP_HIT_GAP); + default: + assert(0 && "freeze queried for a non-attack"); + return COLO_SOL_SPEAR_FREEZE; + } +} + +static int col_sol_select_attack(ColosseumState* s) { + SolHereditState* sol = &s->sol; + if (sol->force_spear) { + sol->force_spear = 0; + if (sol->special_cooldown > 0) sol->special_cooldown--; + return COLO_SOL_ATTACK_SPEAR; + } + + int pool[6]; + int n = 0; + pool[n++] = COLO_SOL_ATTACK_SPEAR; + pool[n++] = COLO_SOL_ATTACK_SPEAR; + pool[n++] = COLO_SOL_ATTACK_SHIELD; + pool[n++] = COLO_SOL_ATTACK_SHIELD; + if (sol->special_cooldown <= 0) { + if (sol->phase >= 1) pool[n++] = COLO_SOL_ATTACK_TRIPLE; + if (sol->phase >= 2) pool[n++] = COLO_SOL_ATTACK_GRAPPLE; + } + + int kind = pool[encounter_rand_int(&s->rng_state, n)]; + if (kind == COLO_SOL_ATTACK_TRIPLE || kind == COLO_SOL_ATTACK_GRAPPLE) + sol->special_cooldown = COLO_SOL_SPECIAL_COOLDOWN; + else if (sol->special_cooldown > 0) + sol->special_cooldown--; + return kind; +} + +static int col_sol_pick_variant(SolHereditState* sol, int kind) { + int variant = (sol->last_attack_kind == kind && sol->last_variant == 1) ? 2 : 1; + sol->last_attack_kind = kind; + sol->last_variant = variant; + return variant; +} + +static int col_sol_adjacent_to_player(const ColosseumState* s, int idx) { + const ColoNPC* boss = &s->npcs[idx]; + return encounter_dist_to_npc(s->player.x, s->player.y, boss->x, boss->y, + col_npc_effective_size(boss)) == 1; +} + +static void col_sol_tick_attack_engine(ColosseumState* s, int idx) { + SolHereditState* sol = &s->sol; + if (sol->immobile_ticks > 0) sol->immobile_ticks--; + if (sol->attack_delay > 0) sol->attack_delay--; + if (sol->immobile_ticks > 0 || sol->attack_delay > 0) return; + if (!col_sol_adjacent_to_player(s, idx)) return; + + int kind = col_sol_select_attack(s); + int variant = col_sol_pick_variant(sol, kind); + switch (kind) { + case COLO_SOL_ATTACK_SPEAR: + case COLO_SOL_ATTACK_SHIELD: + col_sol_cast_aoe(s, idx, kind, variant); + break; + case COLO_SOL_ATTACK_TRIPLE: + col_sol_start_triple_parry(s, idx); + break; + case COLO_SOL_ATTACK_GRAPPLE: + col_sol_start_grapple(s); + break; + default: + assert(0 && "selected a non-attack"); + break; + } + sol->immobile_ticks = col_sol_attack_freeze(kind, sol->phase); + sol->attack_delay = col_sol_attack_delay(kind, sol->phase); + s->npcs[idx].attacked_this_tick = 1; + s->npcs[idx].attack_style_this_tick = ATTACK_STYLE_MELEE; + + if (sol->crystal_count > 0 && sol->laser_cooldown < 0) + col_sol_fire_lasers(s); +} + +static void col_sol_move(ColosseumState* s, int idx) { + SolHereditState* sol = &s->sol; + ColoNPC* boss = &s->npcs[idx]; + boss->target_x = s->player.x; + boss->target_y = s->player.y; + if (sol->immobile_ticks > 0) return; + int size = col_npc_effective_size(boss); + if (encounter_dist_to_npc(s->player.x, s->player.y, boss->x, boss->y, size) <= 1) + return; + col_stamp_npc_collision_footprint(s, boss->x, boss->y, size, 0); + int moved = col_npc_path_step(s, &boss->x, &boss->y, size, + s->player.x - size / 2, s->player.y - size / 2); + col_stamp_npc_collision_footprint(s, boss->x, boss->y, size, 1); + boss->moved_this_tick = moved ? 1 : 0; +} + +static void col_sol_aoe_direction(int px, int py, int anchor_x, int anchor_y, + int* dx, int* dy) { + int closest_x = px, closest_y = py; + if (closest_x < anchor_x) closest_x = anchor_x; + if (closest_x > anchor_x + 4) closest_x = anchor_x + 4; + if (closest_y < anchor_y) closest_y = anchor_y; + if (closest_y > anchor_y + 4) closest_y = anchor_y + 4; + int ox = px - closest_x; + int oy = py - closest_y; + *dx = ox > 0 ? 1 : ox < 0 ? -1 : 0; + *dy = oy > 0 ? 1 : oy < 0 ? -1 : 0; + if (*dx == 0 && *dy == 0) { *dx = -1; *dy = -1; } +} + +static void col_sol_aoe_effective_dir(const SolHereditState* sol, int x, int y, + int* dx, int* dy) { + if (sol->aoe_dir_x != 0 || sol->aoe_dir_y != 0) { + *dx = sol->aoe_dir_x; + *dy = sol->aoe_dir_y; + return; + } + col_sol_aoe_direction(x, y, sol->aoe_x, sol->aoe_y, dx, dy); +} + +static int col_sol_aoe_tile_is_hazard(const SolHereditState* sol, int x, int y) { + int cx = sol->aoe_x + 2, cy = sol->aoe_y + 2; + + if (sol->aoe_attack == COLO_SOL_AOE_SHIELD1 || + sol->aoe_attack == COLO_SOL_AOE_SHIELD2) { + int dx = abs(x - cx), dy = abs(y - cy); + int cheb = dx > dy ? dx : dy; + int ring = sol->aoe_attack == COLO_SOL_AOE_SHIELD1 + ? COLO_SOL_SHIELD1_RING : COLO_SOL_SHIELD2_RING; + return cheb != ring; + } + if (sol->aoe_attack != COLO_SOL_AOE_SPEAR1 && + sol->aoe_attack != COLO_SOL_AOE_SPEAR2) return 0; + + int dx, dy; + col_sol_aoe_effective_dir(sol, x, y, &dx, &dy); + if (dx != 0 && dy != 0) { + int u = (x - (cx + 2 * dx)) * dx; + int v = (y - (cy + 2 * dy)) * dy; + if (sol->aoe_attack == COLO_SOL_AOE_SPEAR1) { + if (abs(x - cx) <= 2 && abs(y - cy) <= 2) return 1; + if (u == v - 1 && v >= 1 && v <= 1 + COLO_SOL_SPEAR_LINE_LEN) return 1; + if (v == u - 1 && u >= 1 && u <= 1 + COLO_SOL_SPEAR_LINE_LEN) return 1; + return 0; + } + if (abs(x - cx) <= 3 && abs(y - cy) <= 3) return 1; + if (u == v && u >= 2 && u <= 2 + COLO_SOL_SPEAR_LINE_LEN) return 1; + if (v == u + 3 && u >= -1 && u <= -1 + COLO_SOL_SPEAR_LINE_LEN) return 1; + if (u == v + 3 && v >= -1 && v <= -1 + COLO_SOL_SPEAR_LINE_LEN) return 1; + return 0; + } + + int forward = dx != 0 ? (x - cx) * dx : (y - cy) * dy; + int lateral = dx != 0 ? (y - cy) : (x - cx); + if (sol->aoe_attack == COLO_SOL_AOE_SPEAR1) { + if (lateral >= -2 && lateral <= 2 && + forward >= -2 && forward <= 3) return 1; + if (forward >= 4 && forward <= 4 + COLO_SOL_SPEAR_LINE_LEN) + return lateral == -1 || lateral == 1; + return 0; + } + if (lateral >= -3 && lateral <= 3 && + forward >= -3 && forward <= 3) return 1; + if (forward >= 4 && forward <= 4 + COLO_SOL_SPEAR_LINE_LEN) + return lateral == -2 || lateral == 0 || lateral == 2; + return 0; +} + +static void col_sol_cast_aoe(ColosseumState* s, int idx, int kind, int variant) { + SolHereditState* sol = &s->sol; + const ColoNPC* boss = &s->npcs[idx]; + if (kind == COLO_SOL_ATTACK_SPEAR) + sol->aoe_attack = variant == 1 ? COLO_SOL_AOE_SPEAR1 : COLO_SOL_AOE_SPEAR2; + else + sol->aoe_attack = variant == 1 ? COLO_SOL_AOE_SHIELD1 : COLO_SOL_AOE_SHIELD2; + sol->aoe_age = 0; + sol->aoe_x = boss->x; + sol->aoe_y = boss->y; + sol->aoe_dir_x = 0; + sol->aoe_dir_y = 0; +} + +static void col_sol_tick_aoe(ColosseumState* s) { + SolHereditState* sol = &s->sol; + if (sol->aoe_attack == COLO_SOL_AOE_NONE) return; + sol->aoe_age++; + if (sol->aoe_age == COLO_SOL_AOE_DAMAGE_AGE && + (sol->aoe_attack == COLO_SOL_AOE_SPEAR1 || + sol->aoe_attack == COLO_SOL_AOE_SPEAR2)) + col_sol_aoe_direction(s->player.x, s->player.y, sol->aoe_x, sol->aoe_y, + &sol->aoe_dir_x, &sol->aoe_dir_y); + if (sol->aoe_age == COLO_SOL_AOE_DAMAGE_AGE && + col_sol_aoe_tile_is_hazard(sol, s->player.x, s->player.y)) { + int dmg = COLO_SOL_SLAM_MIN_HIT + + encounter_rand_int(&s->rng_state, COLO_SOL_SLAM_RAND); + col_damage_player_from(s, dmg, COLO_SOL_HEREDIT, COLO_DMG_UNPRAYABLE); + s->last_hit_by_type = COLO_SOL_HEREDIT; + col_mod_on_player_damaged(s, dmg); + } + if (sol->aoe_age >= COLO_SOL_AOE_LIFETIME) sol->aoe_attack = COLO_SOL_AOE_NONE; +} + +static void col_sol_crystal_patrol( + const SolHereditState* sol, int edge, int* fixed, int* lo, int* hi +) { + switch (edge) { + case COLO_SOL_EDGE_NORTH: + *fixed = sol->boss_arena_max_y - 1; + *lo = sol->boss_arena_min_x + 2; + *hi = sol->boss_arena_max_x - 2; + return; + case COLO_SOL_EDGE_SOUTH: + *fixed = sol->boss_arena_min_y + 1; + *lo = sol->boss_arena_min_x + 2; + *hi = sol->boss_arena_max_x - 2; + return; + case COLO_SOL_EDGE_EAST: + *fixed = sol->boss_arena_max_x - 1; + *lo = sol->boss_arena_min_y + 2; + *hi = sol->boss_arena_max_y - 2; + return; + case COLO_SOL_EDGE_WEST: + *fixed = sol->boss_arena_min_x + 1; + *lo = sol->boss_arena_min_y + 2; + *hi = sol->boss_arena_max_y - 2; + return; + default: + fprintf(stderr, "invalid crystal edge %d\n", edge); + abort(); + } +} + +static int col_sol_crystal_in_line(const ColosseumState* s, const ColoSolCrystal* c) { + if (c->edge == COLO_SOL_EDGE_NORTH || c->edge == COLO_SOL_EDGE_SOUTH) + return s->player.x == c->x; + return s->player.y == c->y; +} + +static void col_sol_spawn_crystal(ColosseumState* s) { + SolHereditState* sol = &s->sol; + if (sol->crystal_count >= COLO_SOL_MAX_CRYSTALS) return; + ColoSolCrystal* c = &sol->crystals[sol->crystal_count]; + c->active = 1; + c->edge = sol->crystal_count; + int fixed, lo, hi; + col_sol_crystal_patrol(sol, c->edge, &fixed, &lo, &hi); + int along = lo + encounter_rand_int(&s->rng_state, hi - lo); + if (c->edge == COLO_SOL_EDGE_NORTH || c->edge == COLO_SOL_EDGE_SOUTH) { + c->x = along; + c->y = fixed; + } else { + c->x = fixed; + c->y = along; + } + c->dir = encounter_rand_int(&s->rng_state, 2) ? 1 : -1; + if (along == lo) c->dir = 1; + if (along == hi) c->dir = -1; + c->move_timer = COLO_SOL_CRYSTAL_MOVE_TICKS_SPAWN; + c->firing_freeze = 0; + sol->crystal_count++; +} + +static void col_sol_fire_lasers(ColosseumState* s) { + SolHereditState* sol = &s->sol; + for (int i = 0; i < sol->crystal_count; i++) + if (sol->crystals[i].active) + sol->crystals[i].firing_freeze = COLO_SOL_LASER_FREEZE; + sol->laser_cooldown = sol->phase >= 5 + ? COLO_SOL_CRYSTAL_COOLDOWN_ENRAGE + : COLO_SOL_CRYSTAL_COOLDOWN_MIN + + encounter_rand_int(&s->rng_state, COLO_SOL_CRYSTAL_COOLDOWN_RAND); +} + +static void col_sol_tick_crystals(ColosseumState* s) { + SolHereditState* sol = &s->sol; + for (int i = 0; i < sol->crystal_count; i++) { + ColoSolCrystal* c = &sol->crystals[i]; + if (!c->active) continue; + + if (c->firing_freeze <= 0 && c->move_timer <= 0) { + int fixed, lo, hi; + col_sol_crystal_patrol(sol, c->edge, &fixed, &lo, &hi); + int* along = (c->edge == COLO_SOL_EDGE_NORTH || + c->edge == COLO_SOL_EDGE_SOUTH) ? &c->x : &c->y; + *along += c->dir; + if (*along <= lo || *along >= hi) c->dir = -c->dir; + c->move_timer = COLO_SOL_CRYSTAL_MOVE_TICKS; + } + + if (c->firing_freeze == COLO_SOL_LASER_DAMAGE_FREEZE && + col_sol_crystal_in_line(s, c)) { + int dmg = COLO_SOL_LASER_MIN_HIT + + encounter_rand_int(&s->rng_state, COLO_SOL_LASER_RAND); + col_damage_player_from(s, dmg, COLO_SOL_HEREDIT, COLO_DMG_UNPRAYABLE); + s->last_hit_by_type = COLO_SOL_HEREDIT; + col_mod_on_player_damaged(s, dmg); + } + + c->move_timer--; + c->firing_freeze--; + } +} + +static void col_sol_add_pool(ColosseumState* s, int x, int y) { + if (col_static_blocked(x, y)) return; + SolHereditState* sol = &s->sol; + for (int i = 0; i < sol->hazard_tile_count; i++) + if (sol->hazard_tile_x[i] == x && sol->hazard_tile_y[i] == y) return; + int slot = sol->hazard_tile_count < COLO_SOL_HAZARD_TILES_MAX + ? sol->hazard_tile_count++ + : encounter_rand_int(&s->rng_state, COLO_SOL_HAZARD_TILES_MAX); + sol->hazard_tile_x[slot] = x; + sol->hazard_tile_y[slot] = y; +} + +typedef struct { + int x, y; +} ColSolMoltenCandidate; + +static int col_sol_sample_molten_candidates( + ColosseumState* s, + int spread, + int count, + int force_player_tile, + ColSolMoltenCandidate* out +) { + ColSolMoltenCandidate pool[81]; + int pool_count = 0; + int player_idx = -1; + for (int x = s->player.x - spread; x <= s->player.x + spread; x++) { + for (int y = s->player.y - spread; y <= s->player.y + spread; y++) { + if (!col_in_boss_arena(s, x, y)) continue; + if (col_static_blocked(x, y)) continue; + if (pool_count >= (int)(sizeof(pool) / sizeof(pool[0]))) break; + if (x == s->player.x && y == s->player.y) player_idx = pool_count; + pool[pool_count++] = (ColSolMoltenCandidate){ .x = x, .y = y }; + } + } + + int out_count = 0; + if (force_player_tile && player_idx >= 0 && out_count < count) { + out[out_count++] = pool[player_idx]; + pool[player_idx] = pool[--pool_count]; + } + + while (out_count < count && pool_count > 0) { + int idx = encounter_rand_int(&s->rng_state, pool_count); + out[out_count++] = pool[idx]; + pool[idx] = pool[--pool_count]; + } + + return out_count; +} + +static void col_sol_drop_beam_at(ColosseumState* s, int x, int y) { + SolHereditState* sol = &s->sol; + for (int b = 0; b < COLO_SOL_BEAM_MAX; b++) { + if (sol->beams[b].active) continue; + sol->beams[b] = (ColoSolBeam){ + .active = 1, .x = x, .y = y, + .ticks_to_pool = COLO_SOL_BEAM_TO_POOL_TICKS }; + return; + } +} + +static void col_sol_drop_beams(ColosseumState* s) { + ColSolMoltenCandidate tiles[COLO_SOL_BEAM_COUNT]; + int ntiles = col_sol_sample_molten_candidates( + s, COLO_SOL_BEAM_SPREAD, COLO_SOL_BEAM_COUNT, 1, tiles); + for (int n = 0; n < ntiles; n++) + col_sol_drop_beam_at(s, tiles[n].x, tiles[n].y); +} + +static void col_sol_tick_molten(ColosseumState* s) { + SolHereditState* sol = &s->sol; + if (sol->phase >= 5) { + if (sol->enrage_pool_timer > 0) sol->enrage_pool_timer--; + if (sol->enrage_pool_timer == 0) { + ColSolMoltenCandidate tile[1]; + int ntiles = col_sol_sample_molten_candidates( + s, COLO_SOL_ENRAGE_POOL_SPREAD, 1, 0, tile); + if (ntiles > 0) col_sol_drop_beam_at(s, tile[0].x, tile[0].y); + sol->enrage_pool_timer = COLO_SOL_MOLTEN_INTERVAL; + } + } + + for (int b = 0; b < COLO_SOL_BEAM_MAX; b++) { + if (!sol->beams[b].active) continue; + sol->beams[b].ticks_to_pool--; + if (sol->beams[b].ticks_to_pool > 0) continue; + col_sol_add_pool(s, sol->beams[b].x, sol->beams[b].y); + sol->beams[b].active = 0; + } + + for (int i = 0; i < sol->hazard_tile_count; i++) { + if (s->player.x != sol->hazard_tile_x[i] || s->player.y != sol->hazard_tile_y[i]) + continue; + int dmg = COLO_MOLTEN_SAND_MIN_HIT + + encounter_rand_int(&s->rng_state, COLO_MOLTEN_SAND_RAND); + col_damage_player_from(s, dmg, COLO_SOL_HEREDIT, COLO_DMG_UNPRAYABLE); + s->last_hit_by_type = COLO_SOL_HEREDIT; + col_mod_on_player_damaged(s, dmg); + break; + } +} + +static const int COLO_SOL_PARRY_LOOKBACK[COLO_SOL_PARRY_HITS] = { 3, 2, 2 }; + +static int col_sol_overhead_on_in_window(const ColosseumState* s, int lookback) { + for (int k = 1; k <= lookback; k++) { + int t = s->tick - k; + if (t < 0) continue; + if (s->sol.overhead_history[t % COLO_SOL_PRAYER_HISTORY]) return 1; + } + return 0; +} + +static void col_sol_start_triple_parry(ColosseumState* s, int idx) { + (void)idx; + SolHereditState* sol = &s->sol; + static const int SHORT_DMG[COLO_SOL_PARRY_HITS] = { 15, 25, 35 }; + static const int LONG_DMG[COLO_SOL_PARRY_HITS] = { 15, 30, 45 }; + int low = sol->phase >= 3; + const int* dmg = low ? LONG_DMG : SHORT_DMG; + int gap3 = low ? COLO_SOL_TP_HIT3_GAP_LOW : COLO_SOL_TP_HIT_GAP; + + sol->parry_land_in[0] = COLO_SOL_TP_HIT_GAP; + sol->parry_land_in[1] = 2 * COLO_SOL_TP_HIT_GAP; + sol->parry_land_in[2] = 2 * COLO_SOL_TP_HIT_GAP + gap3; + for (int h = 0; h < COLO_SOL_PARRY_HITS; h++) sol->parry_damage[h] = dmg[h]; + sol->parry_hits_left = COLO_SOL_PARRY_HITS; +} + +static void col_sol_tick_triple_parry(ColosseumState* s) { + SolHereditState* sol = &s->sol; + if (sol->parry_hits_left <= 0) return; + for (int h = 0; h < COLO_SOL_PARRY_HITS; h++) { + if (sol->parry_land_in[h] <= 0) continue; + sol->parry_land_in[h]--; + if (sol->parry_land_in[h] > 0) continue; + sol->parry_hits_left--; + + int early = col_sol_overhead_on_in_window(s, COLO_SOL_PARRY_LOOKBACK[h]); + int blocked = !early && s->player.prayer == PRAYER_PROTECT_MELEE; + s->tick_scratch.styles_faced_mask |= (1 << ATTACK_STYLE_MELEE); + s->tick_scratch.avoidable_threat_by_style[ATTACK_STYLE_MELEE] += + (float)sol->parry_damage[h]; + if (blocked) { + s->tick_scratch.prayer_correct++; + } else { + s->tick_scratch.offpray_damage_this_tick += (float)sol->parry_damage[h]; + col_damage_player_from(s, sol->parry_damage[h], COLO_SOL_HEREDIT, COLO_DMG_OFFPRAY); + s->last_hit_by_type = COLO_SOL_HEREDIT; + col_mod_on_player_damaged(s, sol->parry_damage[h]); + } + col_log_prayer_event(s, COLO_SOL_HEREDIT, blocked, blocked ? 0 : sol->parry_damage[h]); + s->player.prayer = PRAYER_NONE; + } +} + +static void col_sol_start_grapple(ColosseumState* s) { + SolHereditState* sol = &s->sol; + sol->grapple_active = 1; + sol->grapple_resolved = 0; + sol->grapple_body_slot = encounter_rand_int(&s->rng_state, COLO_NUM_GRAPPLE_SLOTS); + sol->grapple_timer = COLO_SOL_GRAPPLE_WINDOW; +} + +static int col_sol_grapple_parry_attempt(ColosseumState* s, int slot) { + SolHereditState* sol = &s->sol; + if (!sol->grapple_active || sol->grapple_resolved) return 0; + if (slot != sol->grapple_body_slot) return 0; + + int perfect = sol->grapple_timer <= 1; + sol->grapple_resolved = 1; + sol->grapple_active = 0; + sol->grapple_timer = 0; + sol->grapple_outcome = perfect + ? COLO_GRAPPLE_OUTCOME_PERFECT : COLO_GRAPPLE_OUTCOME_DEFENDED; + sol->grapple_outcome_tick = s->tick; + if (perfect) { + sol->next_attack_guaranteed_max = 1; + sol->guaranteed_max_ticks = COLO_SOL_PERFECT_MAX_TICKS; + } + return 1; +} + +static void col_sol_tick_grapple(ColosseumState* s) { + SolHereditState* sol = &s->sol; + if (!sol->grapple_active) return; + if (sol->grapple_timer > 0) sol->grapple_timer--; + if (sol->grapple_timer == 0 && !sol->grapple_resolved) { + int dmg = COLO_SOL_SLAM_MIN_HIT + + encounter_rand_int(&s->rng_state, COLO_SOL_SLAM_RAND); + col_damage_player_from(s, dmg, COLO_SOL_HEREDIT, COLO_DMG_UNPRAYABLE); + s->last_hit_by_type = COLO_SOL_HEREDIT; + col_mod_on_player_damaged(s, dmg); + sol->grapple_active = 0; + sol->grapple_resolved = 1; + sol->grapple_outcome = COLO_GRAPPLE_OUTCOME_FAILED; + sol->grapple_outcome_tick = s->tick; + } +} + +static void col_sol_enter_phase(ColosseumState* s, int new_phase) { + SolHereditState* sol = &s->sol; + sol->phase = new_phase; + col_sol_drop_beams(s); + if (new_phase >= 1 && new_phase <= 4) col_sol_spawn_crystal(s); + if (new_phase >= 5) sol->laser_cooldown = COLO_SOL_CRYSTAL_COOLDOWN_ENRAGE; + if (new_phase >= 5) { + + ColSolMoltenCandidate tiles[COLO_SOL_ENRAGE_OPEN_POOLS]; + int ntiles = col_sol_sample_molten_candidates( + s, COLO_SOL_ENRAGE_POOL_SPREAD, COLO_SOL_ENRAGE_OPEN_POOLS, 0, tiles); + for (int p = 0; p < ntiles; p++) + col_sol_drop_beam_at(s, tiles[p].x, tiles[p].y); + sol->enrage_pool_timer = COLO_SOL_MOLTEN_INTERVAL; + } + if (new_phase > s->log.boss_phase_reached) s->log.boss_phase_reached = new_phase; +} + +static void col_sol_boss_tick(ColosseumState* s, ColosseumContext* ctx) { + (void)ctx; + SolHereditState* sol = &s->sol; + if (s->wave != COLO_WAVE_BOSS || !sol->started) return; + + int idx = col_sol_find_idx(s); + sol->boss_idx = idx; + if (idx < 0) return; + + sol->overhead_history[s->tick % COLO_SOL_PRAYER_HISTORY] = + (uint8_t)col_player_overhead_protect_on(s); + + int target_phase = col_sol_phase_for_hp(s->npcs[idx].hp); + if (target_phase > sol->phase) { + while (sol->phase < target_phase) col_sol_enter_phase(s, sol->phase + 1); + sol->immobile_ticks = COLO_SOL_TRANSITION_FREEZE; + sol->force_spear = 1; + } + + if (sol->guaranteed_max_ticks > 0) { + sol->guaranteed_max_ticks--; + if (sol->guaranteed_max_ticks == 0) sol->next_attack_guaranteed_max = 0; + } + sol->laser_cooldown--; + + col_sol_tick_aoe(s); + col_sol_tick_crystals(s); + col_sol_tick_molten(s); + col_sol_tick_triple_parry(s); + col_sol_tick_grapple(s); + + col_sol_tick_attack_engine(s, idx); + col_sol_move(s, idx); +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc new file mode 100644 index 0000000000..1cfc44ecba --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_combat.inc @@ -0,0 +1,758 @@ +#line 76 "encounter_colosseum.h" + +static void col_apply_npc_death(ColosseumState* s, int idx) { + ColoNPC* npc = &s->npcs[idx]; + + if (col_type_is_hazard_entity(npc->type)) { + col_hazard_on_npc_death(s, idx); + col_deactivate_npc(s, idx); + return; + } + if (npc->death_ticks > 0) return; + assert(npc->active && npc->hp <= 0); + s->log.total_npc_kills++; + + if (s->death_charge_window_left > 0) { + s->player.special_energy += COLO_DEATH_CHARGE_SPEC_GAIN; + if (s->player.special_energy > 100) s->player.special_energy = 100; + s->death_charge_window_left = 0; + s->death_charge_cd = COLO_DEATH_CHARGE_CD; + } + + col_mod_volatility_on_death(s, npc->x, npc->y, col_npc_effective_size(npc)); + col_mod_on_owner_death(s, idx); + if (!osrs_npc_death_linger_start( + npc->hp, npc->active, &npc->death_ticks, + col_npc_death_linger_ticks(npc->type))) + return; + encounter_pending_hit_queue_clear(&npc->pending_hits); +} + +static void col_render_hit_events_add( + int* count, + int damage[ENCOUNTER_RENDER_HITS_MAX], + int hit_damage +) { + if (!count || !damage || *count < 0 || *count >= ENCOUNTER_RENDER_HITS_MAX) { + fprintf(stderr, "invalid colosseum render hit event\n"); + abort(); + } + damage[*count] = hit_damage > 0 ? hit_damage : 0; + *count += 1; +} + +static void col_apply_echo_boots_recoil( + ColosseumState* s, ColosseumContext* ctx, int attacker_slot, int final_damage +) { + int recoil_damage = osrs_echo_boots_recoil_damage( + col_live_effects(s), &s->player.item_effect_state, final_damage); + if (recoil_damage <= 0) return; + if (attacker_slot < 0 || attacker_slot >= COLO_MAX_NPCS) return; + + ColoNPC* npc = &s->npcs[attacker_slot]; + if (!col_npc_is_live_enemy(npc)) return; + if (npc->type == COLO_SOL_HEREDIT) return; + + osrs_consume_echo_boots_charge(&s->player); + int landed = 0, hit_damage = 0; + int prev_min_hp = npc->min_hp_seen; + int applied = encounter_damage_npc( + &npc->hp, &landed, &hit_damage, recoil_damage); + if (applied <= 0) return; + + npc->hit_landed_this_tick = landed; + npc->hit_damage += hit_damage; + npc->hit_was_successful_this_tick = 1; + col_render_hit_events_add( + &ctx->npc_render_hit_count[attacker_slot], + ctx->npc_render_hit_damage[attacker_slot], + hit_damage); + s->tick_scratch.damage_dealt += (float)applied; + float fresh = (float)(prev_min_hp - npc->hp); + if (fresh > 0.0f) { + npc->min_hp_seen = npc->hp; + s->tick_scratch.fresh_damage_dealt += fresh; + if (npc->spawned_as_reinforcement) + s->tick_scratch.fresh_damage_reinforcement += fresh; + s->current_wave_fresh_damage += fresh; + } + if (npc->hp <= 0) { + col_apply_npc_death(s, attacker_slot); + } else { + col_mod_on_npc_hp_changed(s, attacker_slot); + } +} + +static void col_resolve_player_projectiles_on_npcs_ctx( + ColosseumState* s, + ColosseumContext* ctx +) { + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s->npcs[i]; + if (!col_npc_is_live_target(npc)) continue; + for (int h = 0; h < npc->pending_hits.count; h++) { + EncounterPendingHit* ph = &npc->pending_hits.hits[h]; + int landed = 0, hit_damage = 0; + float dmg_acc = 0.0f; + int frozen = npc->frozen_ticks; + int prev_min_hp = npc->min_hp_seen; + if (encounter_resolve_npc_pending_hit( + ph, &npc->hp, &landed, &hit_damage, &frozen, NULL, &dmg_acc)) { + npc->hit_landed_this_tick = landed; + npc->hit_damage = hit_damage; + npc->hit_was_successful_this_tick = (hit_damage > 0); + col_render_hit_events_add( + &ctx->npc_render_hit_count[i], + ctx->npc_render_hit_damage[i], + hit_damage); + s->tick_scratch.damage_dealt += dmg_acc; + if (npc->type == COLO_SOL_HEREDIT) + s->tick_scratch.damage_boss += dmg_acc; + float fresh = (float)(prev_min_hp - npc->hp); + if (fresh > 0.0f) { + npc->min_hp_seen = npc->hp; + s->tick_scratch.fresh_damage_dealt += fresh; + if (npc->spawned_as_reinforcement) + s->tick_scratch.fresh_damage_reinforcement += fresh; + if (npc->type == COLO_SOL_HEREDIT) + s->tick_scratch.fresh_damage_boss += fresh; + else + s->current_wave_fresh_damage += fresh; + } + if (npc->hp <= 0) { + col_apply_npc_death(s, i); + break; + } + col_mod_on_npc_hp_changed(s, i); + encounter_pending_hit_queue_remove(&npc->pending_hits, h, "colosseum-npc"); + h--; + } + } + } +} + +static void col_resolve_player_projectiles_on_npcs(ColosseumState* s) { + col_resolve_player_projectiles_on_npcs_ctx(s, col_legacy_context()); +} + +static void col_tick_npc_death_lingers(ColosseumState* s) { + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s->npcs[i]; + if (!npc->active || npc->death_ticks <= 0) continue; + if (osrs_npc_death_linger_tick(&npc->death_ticks)) + col_deactivate_npc(s, i); + } +} + +static void col_log_prayer_event(ColosseumState* s, int npc_type, int prayed, int dmg) { + assert(npc_type >= 0 && npc_type < COLO_NUM_NPC_TYPES); + s->log.pray_faced_by_type[npc_type] += 1.0f; + if (prayed) s->log.pray_correct_by_type[npc_type] += 1.0f; + else s->log.offpray_damage_by_type[npc_type] += (float)dmg; +} + +static inline void col_damage_player_from( + ColosseumState* s, int dmg, int npc_type, ColoDamageChannel channel +) { + dmg = col_scale_incoming_damage(s, dmg); + float before = s->tick_scratch.damage_received; + encounter_damage_player(&s->player, dmg, &s->tick_scratch.damage_received); + float applied = s->tick_scratch.damage_received - before; + switch (channel) { + case COLO_DMG_OFFPRAY: + s->tick_scratch.landed_offpray_damage += applied; + break; + case COLO_DMG_UNPRAYABLE: + s->tick_scratch.landed_unprayable_damage += applied; + if (applied > 0.0f && npc_type >= 0 && npc_type < COLO_NUM_NPC_TYPES) + s->log.typeless_damage_by_type[npc_type] += applied; + break; + } + if (dmg > 0 && npc_type >= 0 && npc_type < COLO_NUM_NPC_TYPES) + s->log.total_damage_by_type[npc_type] += (float)dmg; +} + +typedef struct { + ColosseumState* state; + ColosseumContext* context; +} ColPendingHitObserverContext; + +static void col_pending_hit_prayer_observer( + void* user, const EncounterPendingHit* hit, int dmg, int prayed, int checked) { + ColPendingHitObserverContext* observer_ctx = (ColPendingHitObserverContext*)user; + ColosseumState* s = observer_ctx->state; + ColosseumContext* ctx = observer_ctx->context; + col_render_hit_events_add( + &ctx->player_render_hit_count, ctx->player_render_hit_damage, dmg); + if (dmg > 0 && hit->source_npc_type >= 0 && hit->source_npc_type < COLO_NUM_NPC_TYPES) { + s->log.total_damage_by_type[hit->source_npc_type] += (float)dmg; + + s->last_hit_by_type = hit->source_npc_type; + } + + col_apply_echo_boots_recoil(s, ctx, hit->source_npc_slot, dmg); + + if (dmg > 0) { + if (checked && prayed) s->tick_scratch.landed_prayed_damage += (float)dmg; + else s->tick_scratch.landed_offpray_damage += (float)dmg; + } + if (!checked && hit->source_npc_type == COLO_MANTICORE) { + if (hit->hit_success) col_mod_manticore_apply_venom(s, hit->hit_success); + } + if (!checked) return; + s->tick_scratch.styles_faced_mask |= (1 << hit->attack_style); + s->tick_scratch.avoidable_threat_by_style[hit->attack_style] += (float)hit->damage; + if (!prayed) s->tick_scratch.offpray_damage_this_tick += (float)dmg; + col_log_prayer_event(s, hit->source_npc_type, prayed, dmg); +} + +static void col_resolve_player_pending_hits_ctx(ColosseumState* s, ColosseumContext* ctx) { + int prayer_correct = 0; + int off_prayer = 0; + float dmg_received = 0.0f; + ColPendingHitObserverContext observer_ctx = { + .state = s, + .context = ctx, + }; + encounter_resolve_player_pending_hits_observed( + &s->player_pending_hits, &s->player, s->player.prayer, + &dmg_received, &prayer_correct, &off_prayer, + col_pending_hit_prayer_observer, &observer_ctx); + s->tick_scratch.damage_received += dmg_received; + s->tick_scratch.prayer_correct += prayer_correct; + + int blas = col_mod_tier(s, COLO_MOD_BLASPHEMY); + if (blas > 0 && dmg_received > 0.0f && s->player.current_prayer > 0) { + int drain = ((int)dmg_received * COLO_BLASPHEMY_DRAIN_PCT[blas]) / 100; + s->player.current_prayer -= drain; + if (s->player.current_prayer < 0) s->player.current_prayer = 0; + } + if (col_mod_active(s, COLO_MOD_DOOM)) s->doom_stacks += off_prayer; +} + +static void col_resolve_player_pending_hits(ColosseumState* s) { + col_resolve_player_pending_hits_ctx(s, col_legacy_context()); +} + +static int col_npc_dist_to_player(const ColosseumState* s, const ColoNPC* npc) { + return encounter_dist_to_npc( + s->player.x, s->player.y, npc->x, npc->y, col_npc_effective_size(npc)); +} + +static int col_npc_melee_adjacent_to_player(const ColosseumState* s, const ColoNPC* npc) { + return entity_has_line_of_sight( + NULL, 0, npc->x, npc->y, col_npc_effective_size(npc), + s->player.x, s->player.y, 1, 1); +} + +static int col_player_def_roll(ColosseumState* s, AttackStyle style, MeleeStyle melee_style) { + const EncounterLoadoutStats* ls = col_live_loadout_stats(s); + int def_level = col_mod_relentless_def_level(s, s->player.current_defence); + return encounter_player_def_roll_from_loadout( + def_level, s->player.current_magic, + ls->def_stab, ls->def_slash, ls->def_crush, ls->def_magic, ls->def_ranged, + style, melee_style); +} + +static int col_npc_roll_vs_player( + ColosseumState* s, const ColoNpcStats* stats, AttackStyle style, int max_hit, + int* hit_success +) { + int att_level, att_bonus; + if (style == ATTACK_STYLE_MELEE) { + att_level = stats->att_level; att_bonus = stats->melee_att_bonus; + } else if (style == ATTACK_STYLE_RANGED) { + att_level = stats->range_level; att_bonus = stats->range_att_bonus; + } else { + att_level = stats->magic_level; att_bonus = stats->magic_att_bonus; + } + int att_roll = osrs_npc_attack_roll(att_level, att_bonus); + int def_roll = col_player_def_roll(s, style, stats->melee_style); + + int force_hit = 0; + int max_hit_bonus = col_mod_relentless_max_hit_bonus(s, &force_hit); + int effective_max = max_hit + max_hit_bonus; + + return encounter_npc_roll_attack_ex( + att_roll, def_roll, effective_max, force_hit, &s->rng_state, hit_success); +} + +static int col_npc_projectile_delay(AttackStyle style, int distance) { + EncounterProjectileDelayOptions opts = {0}; + EncounterProjectileTiming timing = encounter_projectile_timing( + distance, 0, encounter_projectile_delay_kind_for_style(style), opts); + return timing.damage_delay_ticks; +} + +static void col_npc_queue_projectile( + ColosseumState* s, int idx, const ColoNpcStats* stats, AttackStyle style, int max_hit +) { + ColoNPC* npc = &s->npcs[idx]; + int dist = col_npc_dist_to_player(s, npc); + int hit_success = 0; + int dmg = col_npc_roll_vs_player(s, stats, style, max_hit, &hit_success); + int rolled_dmg = dmg; + int delay = col_npc_projectile_delay(style, dist); + int prayed = 0; + EncounterPendingHit hit = encounter_pending_hit_resolved_at_throw( + dmg, delay, style, s->player.prayer, npc->type, idx, hit_success, &prayed); + dmg = hit.damage; + if (prayed) s->tick_scratch.prayer_correct++; + col_log_prayer_event(s, npc->type, prayed, dmg); + s->tick_scratch.styles_faced_mask |= (1 << style); + s->tick_scratch.avoidable_threat_by_style[style] += (float)rolled_dmg; + if (!prayed) s->tick_scratch.offpray_damage_this_tick += (float)dmg; + col_push_player_pending_hit(s, hit); + npc->attacked_this_tick = 1; + npc->attack_style_this_tick = style; + s->tick_scratch.attacks_fired++; +} + +static void col_apply_instant_melee_hit( + ColosseumState* s, ColosseumContext* ctx, int attacker_slot, int npc_type, + int dmg, int hit_success +) { + (void)hit_success; + int rolled_dmg = dmg; + EncounterProtectResolve pr = encounter_resolve_protect_at_throw(dmg, s->player.prayer, ATTACK_STYLE_MELEE); + int prayed = pr.prayed; + dmg = pr.frozen_damage; + if (prayed) s->tick_scratch.prayer_correct++; + col_render_hit_events_add( + &ctx->player_render_hit_count, ctx->player_render_hit_damage, dmg); + s->tick_scratch.styles_faced_mask |= (1 << ATTACK_STYLE_MELEE); + s->tick_scratch.avoidable_threat_by_style[ATTACK_STYLE_MELEE] += (float)rolled_dmg; + if (!prayed) s->tick_scratch.offpray_damage_this_tick += (float)dmg; + col_log_prayer_event(s, npc_type, prayed, dmg); + + col_apply_echo_boots_recoil(s, ctx, attacker_slot, dmg); + col_damage_player_from(s, dmg, npc_type, COLO_DMG_OFFPRAY); + + int applied = col_scale_incoming_damage(s, dmg); + if (applied > 0) { + col_mod_on_player_damaged(s, applied); + s->last_hit_by_type = npc_type; + } +} + +static void col_npc_melee_attack( + ColosseumState* s, ColosseumContext* ctx, int idx, const ColoNpcStats* stats, int max_hit +) { + ColoNPC* npc = &s->npcs[idx]; + int hit_success = 0; + int dmg = col_npc_roll_vs_player(s, stats, ATTACK_STYLE_MELEE, max_hit, &hit_success); + col_apply_instant_melee_hit(s, ctx, idx, npc->type, dmg, hit_success); + npc->attacked_this_tick = 1; + npc->attack_style_this_tick = ATTACK_STYLE_MELEE; + s->tick_scratch.attacks_fired++; +} + +static void col_npc_attack_jaguar( + ColosseumState* s, ColosseumContext* ctx, int idx, const ColoNpcStats* stats +) { + for (int h = 0; h < 3; h++) + col_npc_melee_attack(s, ctx, idx, stats, stats->max_hit); +} + +static void col_npc_minotaur_heal_ally(ColosseumState* s, int idx) { + ColoNPC* mino = &s->npcs[idx]; + int mcx = mino->x + 1, mcy = mino->y + 1; + int best = -1; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + if (i == idx) continue; + ColoNPC* other = &s->npcs[i]; + if (!col_npc_is_live_target(other)) continue; + if (other->type == COLO_MINOTAUR) continue; + if (other->hp * 4 >= other->max_hp * 3) continue; + int osz = col_npc_effective_size(other); + int ocx = other->x + osz / 2, ocy = other->y + osz / 2; + int dx = ocx > mcx ? ocx - mcx : mcx - ocx; + int dy = ocy > mcy ? ocy - mcy : mcy - ocy; + if ((dx > dy ? dx : dy) > COLO_MINOTAUR_HEAL_RANGE) continue; + if (!col_tiles_have_los(s, mcx, mcy, ocx, ocy)) continue; + if (best < 0 || + other->hp * s->npcs[best].max_hp < s->npcs[best].hp * other->max_hp) + best = i; + } + if (best >= 0) s->npcs[best].hp = s->npcs[best].max_hp; +} + +static void col_npc_attack_javelin( + ColosseumState* s, + ColosseumContext* ctx, + int idx, + const ColoNpcStats* stats +) { + assert(ctx); + ctx->npc_render_attack_anim[idx] = COLO_NPC_RENDER_ATTACK_NORMAL; + ColoNPC* npc = &s->npcs[idx]; + ColoJavelinState* jv = colo_npc_javelin(npc); + jv->attack_count++; + if (jv->attack_count % 5 == 0) { + jv->skyfall_pending = 1; + jv->skyfall_tile_x = s->player.x; + jv->skyfall_tile_y = s->player.y; + jv->skyfall_timer = COLO_JAVELIN_SKYFALL_DELAY; + jv->skyfall_damage = encounter_rand_int( + &s->rng_state, COLO_JAVELIN_SKYFALL_MAX_HIT + 1); + npc->attacked_this_tick = 1; + npc->attack_style_this_tick = ATTACK_STYLE_RANGED; + ctx->npc_render_attack_anim[idx] = COLO_NPC_RENDER_ATTACK_JAVELIN_SKYFALL; + s->tick_scratch.attacks_fired++; + return; + } + col_npc_queue_projectile(s, idx, stats, ATTACK_STYLE_RANGED, stats->max_hit); +} + +static void col_npc_resolve_javelin_skyfall( + ColosseumState* s, + ColosseumContext* ctx, + int idx +) { + ColoNPC* npc = &s->npcs[idx]; + ColoJavelinState* jv = colo_npc_javelin(npc); + if (!jv->skyfall_pending) return; + jv->skyfall_timer--; + if (jv->skyfall_timer > 0) return; + jv->skyfall_pending = 0; + int on_tile = (s->player.x == jv->skyfall_tile_x && s->player.y == jv->skyfall_tile_y); + int dmg = on_tile ? jv->skyfall_damage : 0; + col_damage_player_from(s, dmg, npc->type, COLO_DMG_UNPRAYABLE); + if (dmg > 0) { + s->last_hit_by_type = npc->type; + col_mod_on_player_damaged(s, dmg); + } + col_mod_reentry_on_skyfall(s, jv->skyfall_tile_x, jv->skyfall_tile_y); +} + +static void col_manticore_delay_ready_peers(ColosseumState* s, int idx) { + for (int i = 0; i < COLO_MAX_NPCS; i++) { + if (i == idx) continue; + ColoNPC* other = &s->npcs[i]; + if (!col_npc_is_live_target(other) || other->type != COLO_MANTICORE) continue; + if (other->attack_timer > 1) continue; + if (other->type_state.manticore.cycle_step > 0) continue; + if (other->stun_timer > 0 || other->frozen_ticks > 0) continue; + int dist = col_npc_dist_to_player(s, other); + if (dist < 1 || dist > COLO_NPC_STATS[COLO_MANTICORE].attack_range) continue; + if (!col_npc_has_los_to_player(s, other)) continue; + other->attack_timer = COLO_MANTICORE_STAGGER_TICKS + 1; + } +} + +static int col_manticore_orb_max_hit(AttackStyle style) { + if (style == ATTACK_STYLE_RANGED) return COLO_MANTICORE_MAX_HIT_RANGED; + if (style == ATTACK_STYLE_MAGIC) return COLO_MANTICORE_MAX_HIT_MAGIC; + return COLO_MANTICORE_MAX_HIT_MELEE; +} + +static void col_npc_manticore_fire_orb(ColosseumState* s, int idx, const ColoNpcStats* stats) { +#ifdef COLO_PROFILE_ENABLED + int col_prof_enabled = COLO_PROFILE_ENABLED(); + double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; +#endif + ColoNPC* npc = &s->npcs[idx]; + ColoManticoreState* mc = colo_npc_manticore(npc); + AttackStyle style = mc->orb_style[mc->cycle_step]; + int max_hit = col_manticore_orb_max_hit(style); + int mayhem = col_mod_tier(s, COLO_MOD_MANTIMAYHEM); + int shots = (mayhem >= 1) ? 2 : 1; + for (int shot = 0; shot < shots; shot++) { + int hit_success = 0; + int dmg = col_npc_roll_vs_player(s, stats, style, max_hit, &hit_success); + int rolled_dmg = dmg; + int prayed = 0; + EncounterPendingHit hit = encounter_pending_hit_resolved_at_throw( + dmg, 1, style, s->player.prayer, npc->type, idx, hit_success, &prayed); + dmg = hit.damage; + if (prayed) s->tick_scratch.prayer_correct++; + col_log_prayer_event(s, npc->type, prayed, dmg); + s->tick_scratch.styles_faced_mask |= (1 << style); + s->tick_scratch.avoidable_threat_by_style[style] += (float)rolled_dmg; + if (!prayed) s->tick_scratch.offpray_damage_this_tick += (float)dmg; + col_push_player_pending_hit(s, hit); + } + npc->attacked_this_tick = 1; + npc->attack_style_this_tick = style; + s->tick_scratch.attacks_fired++; + mc->cycle_step++; + if (mc->cycle_step >= 3) { + mc->cycle_step = -1; + mc->orb_style[0] = ATTACK_STYLE_NONE; + mc->orb_style[1] = ATTACK_STYLE_NONE; + mc->orb_style[2] = ATTACK_STYLE_NONE; + + npc->attack_timer = stats->attack_speed - 2; + } +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_STEP_MANTICORE_BARRAGE); +#endif +} + +static void col_npc_manticore_advance_barrage(ColosseumState* s, int idx, const ColoNpcStats* stats) { + ColoNPC* npc = &s->npcs[idx]; + if (npc->type_state.manticore.cycle_step < 0) return; + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; + col_npc_manticore_fire_orb(s, idx, stats); +} + +static void col_npc_manticore_arm(ColosseumState* s, int idx) { + ColoManticoreState* mc = colo_npc_manticore(&s->npcs[idx]); + if (mc->cycle_step >= 0) return; + if (mc->orb_style[0] != ATTACK_STYLE_NONE) return; + for (int o = 0; o < 3; o++) mc->orb_style[o] = mc->fixed_orb_style[o]; + mc->cycle_step = 0; +} + +static void col_npc_attack_manticore(ColosseumState* s, int idx, const ColoNpcStats* stats) { + ColoNPC* npc = &s->npcs[idx]; + ColoManticoreState* mc = colo_npc_manticore(npc); + mc->cycle_step = 0; + col_npc_manticore_fire_orb(s, idx, stats); + col_manticore_delay_ready_peers(s, idx); +} + +static void col_npc_attack_ctx(ColosseumState* s, ColosseumContext* ctx, int idx) { + assert(ctx); + ColoNPC* npc = &s->npcs[idx]; + const ColoNpcStats* stats = &COLO_NPC_STATS[npc->type]; + + if (npc->type == COLO_SOL_HEREDIT) return; + + if (col_type_is_warbander(npc->type)) return; + + if (s->wave_attack_delay > 0) return; + + if (npc->type == COLO_MANTICORE && npc->type_state.manticore.cycle_step > 0) { + col_npc_manticore_advance_barrage(s, idx, stats); + return; + } + + if (npc->attack_timer > 0) npc->attack_timer--; + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; + + if (npc->type == COLO_MANTICORE) { + int mdist = col_npc_dist_to_player(s, npc); + if (mdist < 1 || mdist > stats->attack_range) return; + if (!col_npc_has_los_to_player(s, npc)) return; + int cycle_step_before_arm = npc->type_state.manticore.cycle_step; + col_npc_manticore_arm(s, idx); + if (cycle_step_before_arm < 0 && + npc->type_state.manticore.cycle_step == 0 && + npc->attack_timer > 0) { + ctx->manticore_charge_started[idx] = 1; + } + if (npc->attack_timer > 0) return; + col_npc_attack_manticore(s, idx, stats); + return; + } + + if (npc->attack_timer > 0) return; + + AttackStyle style = stats->default_style; + int dist = col_npc_dist_to_player(s, npc); + + if (npc->type == COLO_MINOTAUR) { + if (dist == 1) col_npc_melee_attack(s, ctx, idx, stats, stats->max_hit); + else col_npc_minotaur_heal_ally(s, idx); + npc->attack_timer = stats->attack_speed; + return; + } + + if (stats->attack_range > 1) { + if (dist < 1 || dist > stats->attack_range) return; + if (!col_npc_has_los_to_player(s, npc)) return; + } else { + + if (!col_npc_melee_adjacent_to_player(s, npc)) return; + } + + switch (npc->type) { + case COLO_JAGUAR_WARRIOR: + col_npc_attack_jaguar(s, ctx, idx, stats); + break; + case COLO_JAVELIN_COLOSSUS: + col_npc_attack_javelin(s, ctx, idx, stats); + break; + case COLO_SERPENT_SHAMAN: + case COLO_SHOCKWAVE_COLOSSUS: + col_npc_queue_projectile(s, idx, stats, style, stats->max_hit); + break; + default: + return; + } + npc->attack_timer = stats->attack_speed; +} + +static int col_warband_melee_adjacent(const ColosseumState* s, const ColoNPC* npc) { + int dx = npc->x > s->player.x ? npc->x - s->player.x : s->player.x - npc->x; + int dy = npc->y > s->player.y ? npc->y - s->player.y : s->player.y - npc->y; + return dx + dy == 1; +} + +static void col_warband_attack_phase(ColosseumState* s, ColosseumContext* ctx) { +#ifdef COLO_PROFILE_ENABLED + int col_prof_enabled = COLO_PROFILE_ENABLED(); + double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; +#endif + if (s->warband_cycle_anchor < 0) { +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_STEP_WARBAND_ATTACK); +#endif + return; + } + int phase = (s->tick - s->warband_cycle_anchor) % COLO_WARBAND_CYCLE_TICKS; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s->npcs[i]; + if (!col_npc_is_live_target(npc)) continue; + if (!col_type_is_warbander(npc->type)) continue; + int wait = (col_warband_window_offset(npc->type) - phase + + COLO_WARBAND_CYCLE_TICKS) % COLO_WARBAND_CYCLE_TICKS; + npc->attack_timer = wait; + if (wait != 0) continue; + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) continue; + if (npc->moved_this_tick || npc->moved_last_tick) continue; + if (!col_warband_melee_adjacent(s, npc)) continue; + const ColoNpcStats* stats = &COLO_NPC_STATS[npc->type]; + if (npc->type == COLO_FREMENNIK_BERSERKER) + col_npc_melee_attack(s, ctx, i, stats, stats->max_hit); + else + col_npc_queue_projectile(s, i, stats, stats->default_style, stats->max_hit); + } +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_STEP_WARBAND_ATTACK); +#endif +} + +static void col_live_threat_style_counts( + const ColosseumState* s, int* magic, int* ranged, int* melee +) { + *magic = 0; + *ranged = 0; + *melee = 0; + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s->npcs[n]; + if (!col_npc_is_live_enemy(npc)) continue; + const ColoNpcStats* ns = &COLO_NPC_STATS[npc->type]; + if (ns->attack_range < 1) continue; + int dist = col_npc_dist_to_player(s, npc); + if (dist < 1 || dist > ns->attack_range) continue; + if (ns->attack_range > 1 && !col_npc_has_los_to_player(s, npc)) continue; + AttackStyle style = ns->default_style; + if (npc->type == COLO_MANTICORE) { + const ColoManticoreState* mc = &npc->type_state.manticore; + int orb = mc->cycle_step >= 0 ? mc->cycle_step : 0; + if (orb < 3 && mc->orb_style[orb] != ATTACK_STYLE_NONE) style = mc->orb_style[orb]; + } + if (style == ATTACK_STYLE_MAGIC) (*magic)++; + else if (style == ATTACK_STYLE_RANGED) (*ranged)++; + else if (style == ATTACK_STYLE_MELEE) (*melee)++; + } +} + +static AttackStyle col_oracle_incoming_style(const ColosseumState* s, const ColoNPC* npc) { + if (!col_npc_is_live_enemy(npc)) return ATTACK_STYLE_NONE; + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return ATTACK_STYLE_NONE; + const ColoNpcStats* st = &COLO_NPC_STATS[npc->type]; + + if (col_type_is_warbander(npc->type)) { + if (s->warband_cycle_anchor < 0) return ATTACK_STYLE_NONE; + int phase = (s->tick - s->warband_cycle_anchor + COLO_WARBAND_CYCLE_TICKS) + % COLO_WARBAND_CYCLE_TICKS; + if (phase != col_warband_window_offset(npc->type)) return ATTACK_STYLE_NONE; + if (!col_warband_melee_adjacent(s, npc)) return ATTACK_STYLE_NONE; + return st->default_style; + } + + if (npc->type == COLO_MANTICORE) { + const ColoManticoreState* mc = &npc->type_state.manticore; + if (mc->cycle_step > 0) return mc->orb_style[mc->cycle_step]; + if (npc->attack_timer > 1) return ATTACK_STYLE_NONE; + int d = col_npc_dist_to_player(s, npc); + if (d < 1 || d > st->attack_range) return ATTACK_STYLE_NONE; + if (!col_npc_has_los_to_player(s, npc)) return ATTACK_STYLE_NONE; + return mc->fixed_orb_style[0]; + } + + if (npc->type == COLO_MINOTAUR) { + if (npc->attack_timer > 1) return ATTACK_STYLE_NONE; + return (col_npc_dist_to_player(s, npc) == 1) ? ATTACK_STYLE_MELEE : ATTACK_STYLE_NONE; + } + + if (npc->attack_timer > 1) return ATTACK_STYLE_NONE; + if (st->attack_range > 1) { + int d = col_npc_dist_to_player(s, npc); + if (d < 1 || d > st->attack_range) return ATTACK_STYLE_NONE; + if (!col_npc_has_los_to_player(s, npc)) return ATTACK_STYLE_NONE; + return st->default_style; + } + return col_npc_melee_adjacent_to_player(s, npc) ? ATTACK_STYLE_MELEE : ATTACK_STYLE_NONE; +} + +static void col_apply_prayer_oracle(ColosseumState* s) { + int magic = 0, melee = 0, ranged = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + AttackStyle style = col_oracle_incoming_style(s, &s->npcs[i]); + if (style == ATTACK_STYLE_MAGIC) magic = 1; + else if (style == ATTACK_STYLE_MELEE) melee = 1; + else if (style == ATTACK_STYLE_RANGED) ranged = 1; + } + if (magic) s->player.prayer = PRAYER_PROTECT_MAGIC; + else if (melee) s->player.prayer = PRAYER_PROTECT_MELEE; + else if (ranged) s->player.prayer = PRAYER_PROTECT_RANGED; +} + +static void col_tick_npcs_ctx(ColosseumState* s, ColosseumContext* ctx) { +#ifdef COLO_PROFILE_ENABLED + int col_prof_enabled = COLO_PROFILE_ENABLED(); + double col_prof_total_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; + double col_prof_t0 = col_prof_total_t0; +#endif + col_sol_boss_tick(s, ctx); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_STEP_SOL_BOSS); +#endif + + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s->npcs[i]; + if (col_npc_is_live_target(npc) && npc->type == COLO_JAVELIN_COLOSSUS) + col_npc_resolve_javelin_skyfall(s, ctx, i); + } +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_STEP_JAVELIN_SKYFALL); +#endif + + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s->npcs[i]; + if (!npc->active) continue; + + if (npc->death_ticks > 0) continue; + if (npc->stun_timer > 0) npc->stun_timer--; + if (npc->frozen_ticks > 0) npc->frozen_ticks--; + + if (col_type_is_hazard_entity(npc->type)) continue; + +#ifdef COLO_PROFILE_ENABLED + double col_prof_call_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; +#endif + col_npc_move_ctx(s, ctx, i); +#ifdef COLO_PROFILE_ENABLED + if (col_prof_enabled) + COLO_PROFILE_ADD( + COLO_PROF_STEP_NPC_MOVEMENT, + COLO_PROFILE_NOW_MS() - col_prof_call_t0); + col_prof_call_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; +#endif + col_npc_attack_ctx(s, ctx, i); +#ifdef COLO_PROFILE_ENABLED + if (col_prof_enabled) + COLO_PROFILE_ADD( + COLO_PROF_STEP_NPC_ATTACK, + COLO_PROFILE_NOW_MS() - col_prof_call_t0); +#endif + } +#ifdef COLO_PROFILE_ENABLED + if (col_prof_enabled) + COLO_PROFILE_ADD(COLO_PROF_STEP_NPC_TOTAL, COLO_PROFILE_NOW_MS() - col_prof_total_t0); +#endif +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc new file mode 100644 index 0000000000..bd89f9d163 --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_forecast.inc @@ -0,0 +1,1073 @@ +#line 76 "encounter_colosseum.h" + +#define COLO_STYLE_MASK_MELEE 1 +#define COLO_STYLE_MASK_RANGED 2 +#define COLO_STYLE_MASK_MAGIC 4 + +typedef struct { + const ColosseumState* state; + uint8_t (*npc_flags)[COLO_ARENA_HEIGHT]; + int player_x; + int player_y; + int sol_clamp_active; +} ColoForecastMoveCtx; + +typedef struct { + int held; + int dist; + int los; + int npc_x; + int npc_y; + int land_x; + int land_y; +} ColoForecastMoveResult; + +typedef struct { + int valid; + int land_x; + int land_y; +} ColoForecastLanding; + +typedef struct { + int first_attack_tick; + int first_style_mask; + int max_hit; + int ranged_magic_same_tick; +} ColoForecastObsSummary; + +static int col_forecast_style_mask(AttackStyle style) { + if (style == ATTACK_STYLE_MELEE) return COLO_STYLE_MASK_MELEE; + if (style == ATTACK_STYLE_RANGED) return COLO_STYLE_MASK_RANGED; + if (style == ATTACK_STYLE_MAGIC) return COLO_STYLE_MASK_MAGIC; + return 0; +} + +static int col_forecast_style_max_hit(ColoNpcType type, AttackStyle style) { + if (type == COLO_MANTICORE) return col_manticore_orb_max_hit(style); + return COLO_NPC_STATS[type].max_hit; +} + +static void col_step_out_forecast_record_style_mask( + ColoStepOutForecastAction* action, + ColoForecastObsSummary* summary, + int tick_idx, + ColoNpcType type, + int style_mask, + int max_hit +) { + if (tick_idx < 0 || tick_idx >= COLO_STEP_OUT_FORECAST_HORIZON) return; + ColoStepOutForecastTick* tick = &action->ticks[tick_idx]; + int styles = 0; + if (style_mask & COLO_STYLE_MASK_MELEE) { + tick->melee_count++; + if (max_hit > tick->max_hit) tick->max_hit = max_hit; + styles++; + } + if (style_mask & COLO_STYLE_MASK_RANGED) { + tick->ranged_count++; + if (max_hit > tick->max_hit) tick->max_hit = max_hit; + styles++; + } + if (style_mask & COLO_STYLE_MASK_MAGIC) { + tick->magic_count++; + if (max_hit > tick->max_hit) tick->max_hit = max_hit; + styles++; + } + if (styles >= 2 || ((tick->melee_count > 0) + + (tick->ranged_count > 0) + (tick->magic_count > 0)) >= 2) { + action->same_tick_mixed_style_conflict = 1; + } + if ((style_mask & COLO_STYLE_MASK_MELEE) && type == COLO_MANTICORE) + action->melee_fallback_exposure = 1; + if (!summary) return; + int tick_style_mask = 0; + if (tick->melee_count > 0) tick_style_mask |= COLO_STYLE_MASK_MELEE; + if (tick->ranged_count > 0) tick_style_mask |= COLO_STYLE_MASK_RANGED; + if (tick->magic_count > 0) tick_style_mask |= COLO_STYLE_MASK_MAGIC; + if (summary->first_attack_tick == 0) { + summary->first_attack_tick = tick_idx + 1; + summary->first_style_mask = tick_style_mask; + } else if (summary->first_attack_tick == tick_idx + 1) { + summary->first_style_mask = tick_style_mask; + } + if (tick->max_hit > summary->max_hit) summary->max_hit = tick->max_hit; + if (tick->ranged_count > 0 && tick->magic_count > 0) + summary->ranged_magic_same_tick = 1; +} + +static void col_step_out_forecast_record_style( + ColoStepOutForecastAction* action, + ColoForecastObsSummary* summary, + int tick_idx, + ColoNpcType type, + AttackStyle style +) { + int style_mask = col_forecast_style_mask(style); + if (style_mask == 0) return; + col_step_out_forecast_record_style_mask( + action, summary, tick_idx, type, style_mask, + col_forecast_style_max_hit(type, style)); +} + +static int col_step_out_forecast_tick_style_mask(const ColoStepOutForecastTick* tick) { + int mask = 0; + if (tick->melee_count > 0) mask |= COLO_STYLE_MASK_MELEE; + if (tick->ranged_count > 0) mask |= COLO_STYLE_MASK_RANGED; + if (tick->magic_count > 0) mask |= COLO_STYLE_MASK_MAGIC; + return mask; +} + +static int col_step_out_forecast_tick_has_event(const ColoStepOutForecastTick* tick) { + return tick->melee_count > 0 || + tick->ranged_count > 0 || + tick->magic_count > 0; +} + +static void col_step_out_forecast_finalize_action( + ColoStepOutForecastAction* action, int horizon) { + for (int t = 0; t < horizon - 1; t++) { + int ranged_then_magic = action->ticks[t].ranged_count > 0 && + action->ticks[t + 1].magic_count > 0; + int magic_then_ranged = action->ticks[t].magic_count > 0 && + action->ticks[t + 1].ranged_count > 0; + if (ranged_then_magic || magic_then_ranged) + action->ranged_magic_offtick_opportunity = 1; + } +} + +static int col_step_out_forecast_action_valid(const ColosseumState* s, int action_idx) { + if (action_idx == 0) return 1; + if (s->modifiers.draft_pending) return 0; + int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[action_idx]; + int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[action_idx]; + return col_player_walkable((void*)s, nx, ny); +} + +static ColoForecastLanding col_step_out_forecast_action_landing_ctx( + const ColosseumState* s, + int action_idx +) { + ColoForecastLanding landing = { + .valid = col_step_out_forecast_action_valid(s, action_idx), + .land_x = s->player.x, + .land_y = s->player.y, + }; + if (action_idx == 0) return landing; + + int target_dx = ENCOUNTER_MOVE_TARGET_DX[action_idx]; + int target_dy = ENCOUNTER_MOVE_TARGET_DY[action_idx]; + int tx = landing.land_x + target_dx; + int ty = landing.land_y + target_dy; + int abs_dx = target_dx < 0 ? -target_dx : target_dx; + int abs_dy = target_dy < 0 ? -target_dy : target_dy; + int max_steps = abs_dx > abs_dy ? abs_dx : abs_dy; + + for (int step = 0; step < max_steps; step++) { + if (landing.land_x == tx && landing.land_y == ty) break; + int dx = 0; + int dy = 0; + if (tx > landing.land_x) dx = 1; + else if (tx < landing.land_x) dx = -1; + if (ty > landing.land_y) dy = 1; + else if (ty < landing.land_y) dy = -1; + + int moved = 0; + if (dx != 0 && dy != 0 && + col_player_walkable((void*)s, landing.land_x + dx, landing.land_y + dy) && + col_player_walkable((void*)s, landing.land_x + dx, landing.land_y) && + col_player_walkable((void*)s, landing.land_x, landing.land_y + dy)) { + landing.land_x += dx; + landing.land_y += dy; + moved = 1; + } else if (dx != 0 && + col_player_walkable((void*)s, landing.land_x + dx, landing.land_y)) { + landing.land_x += dx; + moved = 1; + } else if (dy != 0 && + col_player_walkable((void*)s, landing.land_x, landing.land_y + dy)) { + landing.land_y += dy; + moved = 1; + } + if (!moved) break; + } + + return landing; +} + +static ColoForecastLanding col_step_out_forecast_set_action_landing_ctx( + const ColosseumState* s, + int action_idx, + ColoStepOutForecastAction* action +) { + ColoForecastLanding landing = + col_step_out_forecast_action_landing_ctx(s, action_idx); + action->valid = landing.valid; + action->land_x = landing.land_x; + action->land_y = landing.land_y; + return landing; +} + +static int col_step_out_forecast_duplicate_landing( + const ColoStepOutForecast* forecast, + int action_idx +) { + const ColoStepOutForecastAction* action = &forecast->actions[action_idx]; + for (int prior_idx = 0; prior_idx < action_idx; prior_idx++) { + const ColoStepOutForecastAction* prior = &forecast->actions[prior_idx]; + if (prior->valid && + prior->land_x == action->land_x && + prior->land_y == action->land_y) { + return prior_idx; + } + } + return -1; +} + +static int col_step_out_forecast_prepare_unique_action_ctx( + const ColosseumState* s, + ColoStepOutForecast* out, + ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS], + int action_idx, + int forecast_slot_count +) { + ColoStepOutForecastAction* action = &out->actions[action_idx]; + ColoForecastLanding landing = col_step_out_forecast_set_action_landing_ctx( + s, action_idx, action); + if (!action->valid || forecast_slot_count == 0) return 0; + + int duplicate_idx = col_step_out_forecast_duplicate_landing(out, action_idx); + if (duplicate_idx >= 0) { + int valid = action->valid; + *action = out->actions[duplicate_idx]; + action->valid = valid; + action->land_x = landing.land_x; + action->land_y = landing.land_y; + if (summaries) + summaries[action_idx] = summaries[duplicate_idx]; + return 0; + } + return 1; +} + +static int col_collect_step_out_forecast_slots( + const ColosseumState* s, + int* out +) { + int n = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + const ColoNPC* npc = &s->npcs[i]; + if (!npc->active || npc->death_ticks > 0 || npc->hp <= 0) continue; + if (col_type_is_hazard_entity(npc->type)) continue; + out[n++] = i; + } + return n; +} + +static void col_forecast_local_copy_npc_slots( + const ColosseumState* s, + ColoForecastNpcLocal out[COLO_MAX_NPCS], + const int slots[COLO_MAX_NPCS], + int slot_count +) { + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + const ColoNPC* src = &s->npcs[i]; + const ColoNpcStats* stats = &COLO_NPC_STATS[src->type]; + ColoForecastNpcLocal* dst = &out[i]; + *dst = (ColoForecastNpcLocal){0}; + dst->active = src->active; + dst->type = src->type; + dst->forecast_behavior = COLO_FORECAST_BEHAVIOR_STANDARD; + if (col_type_is_warbander(src->type)) + dst->forecast_behavior = COLO_FORECAST_BEHAVIOR_WARBAND; + else if (src->type == COLO_MANTICORE) + dst->forecast_behavior = COLO_FORECAST_BEHAVIOR_MANTICORE; + else if (src->type == COLO_SOL_HEREDIT) + dst->forecast_behavior = COLO_FORECAST_BEHAVIOR_SOL; + dst->x = src->x; + dst->y = src->y; + dst->size = src->size; + dst->hp = src->hp; + dst->attack_range = stats->attack_range; + dst->attack_speed = stats->attack_speed; + dst->can_move = stats->can_move; + dst->default_style = stats->default_style; + dst->attack_timer = src->attack_timer; + dst->stun_timer = src->stun_timer; + dst->frozen_ticks = src->frozen_ticks; + dst->death_ticks = src->death_ticks; + if (!src->active) continue; + if (dst->forecast_behavior == COLO_FORECAST_BEHAVIOR_MANTICORE) { + dst->cycle_step = src->type_state.manticore.cycle_step; + for (int orb = 0; orb < 3; orb++) { + dst->orb_style[orb] = src->type_state.manticore.orb_style[orb]; + dst->fixed_orb_style[orb] = + src->type_state.manticore.fixed_orb_style[orb]; + } + } else if (dst->forecast_behavior == COLO_FORECAST_BEHAVIOR_WARBAND) { + dst->formation_dir = src->type_state.warband.formation_dir; + } else if (src->type == COLO_JAVELIN_COLOSSUS) { + dst->javelin_attack_count = src->type_state.javelin.attack_count; + } else if (dst->forecast_behavior == COLO_FORECAST_BEHAVIOR_SOL) { + dst->sol_attack_delay = s->sol.attack_delay; + dst->sol_immobile_ticks = s->sol.immobile_ticks; + } + } +} + +static void col_forecast_local_stamp_npc( + uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT], + const ColoForecastNpcLocal* npc, + uint8_t value +) { + if (!npc->active || npc->death_ticks > 0 || npc->hp <= 0) return; + if (col_type_is_hazard_entity(npc->type)) return; + for (int dx = 0; dx < npc->size; dx++) { + for (int dy = 0; dy < npc->size; dy++) { + int gx, gy; + if (col_grid_index(npc->x + dx, npc->y + dy, &gx, &gy)) + npc_flags[gx][gy] = value; + } + } +} + +static void col_forecast_local_rebuild_npc_flags( + ColoForecastNpcLocal npcs[COLO_MAX_NPCS], + uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT], + const int slots[COLO_MAX_NPCS], + int slot_count +) { + memset(npc_flags, 0, sizeof(uint8_t) * COLO_ARENA_WIDTH * COLO_ARENA_HEIGHT); + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + col_forecast_local_stamp_npc(npc_flags, &npcs[i], 1); + } +} + +static int col_forecast_local_npc_blocked_ignore_player(void* user, int x, int y, int size) { + ColoForecastMoveCtx* ctx = (ColoForecastMoveCtx*)user; + const ColosseumState* s = ctx->state; + if (col_static_footprint_blocked_lookup(x, y, size)) return 1; + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + int tx = x + dx; + int ty = y + dy; + if (ctx->sol_clamp_active && !col_in_boss_arena(s, tx, ty)) return 1; + int gx, gy; + if (!col_grid_index(tx, ty, &gx, &gy)) return 1; + if (ctx->npc_flags[gx][gy]) return 1; + } + } + return 0; +} + +static int col_forecast_local_npc_blocked(void* user, int x, int y, int size) { + ColoForecastMoveCtx* ctx = (ColoForecastMoveCtx*)user; + if (col_forecast_local_npc_blocked_ignore_player(user, x, y, size)) return 1; + return ctx->player_x >= x && ctx->player_x < x + size && + ctx->player_y >= y && ctx->player_y < y + size; +} + +static int col_forecast_local_has_los_to_player( + const ColosseumState* s, + const ColoForecastNpcLocal* npc, + int player_x, + int player_y +) { + int sx = player_x; + int sy = player_y; + if (sx < npc->x) sx = npc->x; + if (sx > npc->x + npc->size - 1) sx = npc->x + npc->size - 1; + if (sy < npc->y) sy = npc->y; + if (sy > npc->y + npc->size - 1) sy = npc->y + npc->size - 1; + return col_tiles_have_los(s, sx, sy, player_x, player_y); +} + +static int col_forecast_local_dist_to_player( + const ColoForecastNpcLocal* npc, + int player_x, + int player_y +) { + return encounter_dist_to_npc(player_x, player_y, npc->x, npc->y, npc->size); +} + +static int col_forecast_local_melee_adjacent( + const ColoForecastNpcLocal* npc, + int player_x, + int player_y +) { + return entity_has_line_of_sight( + NULL, 0, npc->x, npc->y, npc->size, player_x, player_y, 1, 1); +} + +static int col_forecast_npc_threat_distance_impossible( + const ColoForecastNpcLocal* npc, + int player_x, + int player_y, + int horizon +) { + int attack_reach = npc->attack_range; + int steps_per_tick = npc->can_move ? 1 : 0; + if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_WARBAND) { + attack_reach = 1; + steps_per_tick = COLO_WARBAND_TILES_PER_TICK; + } else if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_SOL) { + attack_reach = 15; + steps_per_tick = npc->can_move ? 1 : 0; + } + int dist = col_forecast_local_dist_to_player(npc, player_x, player_y); + return dist > attack_reach + steps_per_tick * horizon; +} + +static int col_forecast_action_rollout_writes_no_threats( + const ColoForecastNpcLocal base_npcs[COLO_MAX_NPCS], + const int slots[COLO_MAX_NPCS], + int slot_count, + const ColoStepOutForecastAction* action, + int horizon +) { + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + if (!col_forecast_npc_threat_distance_impossible( + &base_npcs[i], action->land_x, action->land_y, horizon)) { + return 0; + } + } + return 1; +} + +static int col_forecast_warband_adjacent( + const ColoForecastNpcLocal* npc, + int player_x, + int player_y +) { + int dx = npc->x > player_x ? npc->x - player_x : player_x - npc->x; + int dy = npc->y > player_y ? npc->y - player_y : player_y - npc->y; + return dx + dy == 1; +} + +static int col_forecast_move_result_matches( + const ColoForecastMoveResult* move, + const ColoForecastNpcLocal* npc, + const ColoStepOutForecastAction* action +) { + return move && move->held && + move->npc_x == npc->x && move->npc_y == npc->y && + move->land_x == action->land_x && move->land_y == action->land_y; +} + +static ColoForecastMoveResult col_forecast_local_move_npc( + const ColosseumState* s, + ColoForecastNpcLocal npcs[COLO_MAX_NPCS], + uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT], + int idx, + ColoStepOutForecastAction* action, + int sol_clamp_active +) { + ColoForecastMoveResult result = {0}; + ColoForecastNpcLocal* npc = &npcs[idx]; + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return result; + if (!npc->can_move) return result; + if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_SOL && + npc->sol_immobile_ticks > 0) + return result; + + int ranged_hold = npc->attack_range > 1 && + npc->forecast_behavior != COLO_FORECAST_BEHAVIOR_SOL; + if (ranged_hold) { + int dist = col_forecast_local_dist_to_player(npc, action->land_x, action->land_y); + int los = 0; + if (dist >= 1 && dist <= npc->attack_range && + (los = col_forecast_local_has_los_to_player( + s, npc, action->land_x, action->land_y))) { + result.held = 1; + result.dist = dist; + result.los = los; + result.npc_x = npc->x; + result.npc_y = npc->y; + result.land_x = action->land_x; + result.land_y = action->land_y; + return result; + } + } + if (npc->attack_timer > npc->attack_speed) return result; + + ColoForecastMoveCtx move_ctx; + move_ctx.state = s; + move_ctx.npc_flags = npc_flags; + move_ctx.player_x = action->land_x; + move_ctx.player_y = action->land_y; + move_ctx.sol_clamp_active = sol_clamp_active; + + col_forecast_local_stamp_npc(npc_flags, npc, 0); + int tx = action->land_x; + int ty = action->land_y; + EncounterNpcStepPolicy step_policy = npc->attack_range > 1 + ? ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET + : ENCOUNTER_NPC_STEP_OSRS_AGGRO_STOP_AT_MELEE; + encounter_npc_blocked_fn blocked = col_forecast_local_npc_blocked_ignore_player; + int steps = 1; + uint32_t rng = s->rng_state; + if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_WARBAND) { + tx = action->land_x + COLO_WARBAND_FORM_OFFSET[npc->formation_dir][0]; + ty = action->land_y + COLO_WARBAND_FORM_OFFSET[npc->formation_dir][1]; + step_policy = ENCOUNTER_NPC_STEP_TRAVEL_TARGET; + blocked = col_forecast_local_npc_blocked; + steps = COLO_WARBAND_TILES_PER_TICK; + } + if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_SOL) { + step_policy = ENCOUNTER_NPC_STEP_STOP_AT_MELEE; + blocked = col_forecast_local_npc_blocked; + steps = 1; + } + + for (int step = 0; step < steps; step++) { + if (!encounter_npc_step_toward_policy( + &npc->x, &npc->y, tx, ty, npc->size, 1, + step_policy, blocked, &move_ctx, NULL, &rng)) { + break; + } + } + + col_forecast_local_stamp_npc(npc_flags, npc, 1); + return result; +} + +static int col_forecast_warband_phase(const ColosseumState* s, int tick_idx) { + if (s->warband_cycle_anchor < 0) return -1; + int phase = (s->tick + 1 + tick_idx - s->warband_cycle_anchor) % + COLO_WARBAND_CYCLE_TICKS; + if (phase < 0) phase += COLO_WARBAND_CYCLE_TICKS; + return phase; +} + +typedef struct { + int warband_phase[COLO_STEP_OUT_FORECAST_HORIZON]; + int multi_manticore_possible; + int sol_clamp_active; +} ColoForecastPrecomp; + +static ColoForecastPrecomp col_forecast_precompute( + const ColosseumState* s, + ColoForecastNpcLocal base_npcs[COLO_MAX_NPCS], + const int slots[COLO_MAX_NPCS], + int slot_count +) { + ColoForecastPrecomp pre; + for (int tick_idx = 0; tick_idx < COLO_STEP_OUT_FORECAST_HORIZON; tick_idx++) + pre.warband_phase[tick_idx] = col_forecast_warband_phase(s, tick_idx); + pre.sol_clamp_active = col_sol_clamp_active(s); + int manticore_count = 0; + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + const ColoForecastNpcLocal* npc = &base_npcs[i]; + if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_MANTICORE) + manticore_count++; + } + pre.multi_manticore_possible = manticore_count >= 2 ? 1 : 0; + return pre; +} + +static void col_forecast_manticore_delay_ready_peers( + const ColosseumState* s, + ColoForecastNpcLocal npcs[COLO_MAX_NPCS], + const int slots[COLO_MAX_NPCS], + int slot_count, + int idx, + const ColoStepOutForecastAction* action +) { + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + if (i == idx) continue; + ColoForecastNpcLocal* other = &npcs[i]; + if (other->forecast_behavior != COLO_FORECAST_BEHAVIOR_MANTICORE) + continue; + if (other->attack_timer > 1) continue; + if (other->cycle_step > 0) continue; + if (other->stun_timer > 0 || other->frozen_ticks > 0) continue; + int dist = col_forecast_local_dist_to_player( + other, action->land_x, action->land_y); + if (dist < 1 || dist > other->attack_range) continue; + if (!col_forecast_local_has_los_to_player( + s, other, action->land_x, action->land_y)) continue; + other->attack_timer = COLO_MANTICORE_STAGGER_TICKS + 1; + } +} + +static void col_forecast_local_fire_manticore_orb( + ColoForecastNpcLocal* npc, + ColoStepOutForecastAction* action, + ColoForecastObsSummary* summary, + int tick_idx +) { + if (npc->cycle_step < 0 || npc->cycle_step >= 3) return; + AttackStyle style = npc->orb_style[npc->cycle_step]; + col_step_out_forecast_record_style(action, summary, tick_idx, npc->type, style); + npc->cycle_step++; + if (npc->cycle_step >= 3) { + npc->cycle_step = -1; + npc->orb_style[0] = ATTACK_STYLE_NONE; + npc->orb_style[1] = ATTACK_STYLE_NONE; + npc->orb_style[2] = ATTACK_STYLE_NONE; + npc->attack_timer = npc->attack_speed - 2; + } +} + +static void col_forecast_local_attack_manticore( + const ColosseumState* s, + ColoForecastNpcLocal npcs[COLO_MAX_NPCS], + const int slots[COLO_MAX_NPCS], + int slot_count, + int idx, + ColoStepOutForecastAction* action, + ColoForecastObsSummary* summary, + int tick_idx, + const ColoForecastMoveResult* move_result, + int multi_manticore_possible +) { + ColoForecastNpcLocal* npc = &npcs[idx]; + if (npc->cycle_step > 0) { + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; + col_forecast_local_fire_manticore_orb(npc, action, summary, tick_idx); + return; + } + + if (npc->attack_timer > 0) npc->attack_timer--; + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; + + int move_result_matches = col_forecast_move_result_matches(move_result, npc, action); + int dist = move_result_matches ? move_result->dist : + col_forecast_local_dist_to_player(npc, action->land_x, action->land_y); + if (dist < 1 || dist > npc->attack_range) return; + int los = move_result_matches ? move_result->los : + col_forecast_local_has_los_to_player(s, npc, action->land_x, action->land_y); + if (!los) return; + if (npc->orb_style[0] == ATTACK_STYLE_NONE) { + for (int orb = 0; orb < 3; orb++) npc->orb_style[orb] = npc->fixed_orb_style[orb]; + npc->cycle_step = 0; + } + if (npc->attack_timer > 0) return; + col_forecast_local_fire_manticore_orb(npc, action, summary, tick_idx); + if (multi_manticore_possible) + col_forecast_manticore_delay_ready_peers( + s, npcs, slots, slot_count, idx, action); +} + +static void col_forecast_local_attack_warband( + ColoForecastNpcLocal* npc, + ColoStepOutForecastAction* action, + ColoForecastObsSummary* summary, + int tick_idx, + int phase +) { + if (phase < 0) return; + if (phase != col_warband_window_offset(npc->type)) return; + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; + if (!col_forecast_warband_adjacent(npc, action->land_x, action->land_y)) return; + col_step_out_forecast_record_style( + action, summary, tick_idx, npc->type, npc->default_style); +} + +static void col_forecast_local_attack_sol( + const ColosseumState* s, + ColoForecastNpcLocal* npc, + ColoStepOutForecastAction* action, + ColoForecastObsSummary* summary, + int tick_idx +) { + if (npc->sol_immobile_ticks > 0 || npc->sol_attack_delay > 0) return; + int dist = col_forecast_local_dist_to_player(npc, action->land_x, action->land_y); + if (dist == 1) { + col_step_out_forecast_record_style( + action, summary, tick_idx, npc->type, ATTACK_STYLE_MELEE); + npc->sol_immobile_ticks = COLO_SOL_SPEAR_FREEZE; + npc->sol_attack_delay = COLO_SOL_SPEAR_DELAY; + return; + } + if (dist > 1 && dist <= 15 && + col_forecast_local_has_los_to_player(s, npc, action->land_x, action->land_y)) { + col_step_out_forecast_record_style( + action, summary, tick_idx, npc->type, ATTACK_STYLE_RANGED); + npc->sol_immobile_ticks = COLO_SOL_SPEAR_FREEZE; + npc->sol_attack_delay = COLO_SOL_SPEAR_DELAY; + } +} + +static void col_forecast_local_attack_standard( + const ColosseumState* s, + ColoForecastNpcLocal* npc, + ColoStepOutForecastAction* action, + ColoForecastObsSummary* summary, + int tick_idx, + const ColoForecastMoveResult* move_result +) { + if (npc->attack_timer > 0) npc->attack_timer--; + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; + if (npc->attack_timer > 0) return; + + int move_result_matches = col_forecast_move_result_matches(move_result, npc, action); + int dist = move_result_matches ? move_result->dist : + col_forecast_local_dist_to_player(npc, action->land_x, action->land_y); + if (npc->type == COLO_MINOTAUR) { + if (dist == 1) { + col_step_out_forecast_record_style( + action, summary, tick_idx, npc->type, ATTACK_STYLE_MELEE); + } + npc->attack_timer = npc->attack_speed; + return; + } + if (npc->type == COLO_JAGUAR_WARRIOR) { + if (col_forecast_local_melee_adjacent(npc, action->land_x, action->land_y)) { + col_step_out_forecast_record_style( + action, summary, tick_idx, npc->type, ATTACK_STYLE_MELEE); + npc->attack_timer = npc->attack_speed; + } + return; + } + + if (npc->attack_range > 1) { + if (dist < 1 || dist > npc->attack_range) return; + int los = move_result_matches ? move_result->los : + col_forecast_local_has_los_to_player(s, npc, action->land_x, action->land_y); + if (!los) + return; + } else if (!col_forecast_local_melee_adjacent(npc, action->land_x, action->land_y)) { + return; + } + + if (npc->type == COLO_JAVELIN_COLOSSUS) { + npc->javelin_attack_count++; + if (npc->javelin_attack_count % 5 != 0) { + col_step_out_forecast_record_style( + action, summary, tick_idx, npc->type, ATTACK_STYLE_RANGED); + } + npc->attack_timer = npc->attack_speed; + return; + } + + if (npc->type == COLO_SERPENT_SHAMAN || + npc->type == COLO_SHOCKWAVE_COLOSSUS) { + col_step_out_forecast_record_style( + action, summary, tick_idx, npc->type, npc->default_style); + npc->attack_timer = npc->attack_speed; + } +} + +static void col_forecast_local_attack_npc( + const ColosseumState* s, + ColoForecastNpcLocal npcs[COLO_MAX_NPCS], + const int slots[COLO_MAX_NPCS], + int slot_count, + int idx, + ColoStepOutForecastAction* action, + ColoForecastObsSummary* summary, + int tick_idx, + const ColoForecastMoveResult* move_result, + const ColoForecastPrecomp* pre +) { + ColoForecastNpcLocal* npc = &npcs[idx]; + switch (npc->forecast_behavior) { + case COLO_FORECAST_BEHAVIOR_WARBAND: + col_forecast_local_attack_warband( + npc, action, summary, tick_idx, pre->warband_phase[tick_idx]); + return; + case COLO_FORECAST_BEHAVIOR_MANTICORE: + col_forecast_local_attack_manticore( + s, npcs, slots, slot_count, idx, action, summary, tick_idx, + move_result, pre->multi_manticore_possible); + return; + case COLO_FORECAST_BEHAVIOR_SOL: + col_forecast_local_attack_sol(s, npc, action, summary, tick_idx); + return; + case COLO_FORECAST_BEHAVIOR_STANDARD: + break; + } + col_forecast_local_attack_standard(s, npc, action, summary, tick_idx, move_result); +} + +static void col_step_out_forecast_readonly_tick( + const ColosseumState* s, + ColoForecastNpcLocal npcs[COLO_MAX_NPCS], + uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT], + ColoStepOutForecastAction* action, + ColoForecastObsSummary* summary, + int tick_idx, + const int slots[COLO_MAX_NPCS], + int slot_count, + const ColoForecastPrecomp* pre +) { + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + ColoForecastNpcLocal* npc = &npcs[i]; + if (npc->stun_timer > 0) npc->stun_timer--; + if (npc->frozen_ticks > 0) npc->frozen_ticks--; + if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_SOL) { + if (npc->sol_immobile_ticks > 0) npc->sol_immobile_ticks--; + if (npc->sol_attack_delay > 0) npc->sol_attack_delay--; + } + ColoForecastMoveResult move_result = + col_forecast_local_move_npc( + s, npcs, npc_flags, i, action, pre->sol_clamp_active); + col_forecast_local_attack_npc( + s, npcs, slots, slot_count, i, action, summary, tick_idx, + &move_result, pre); + } +} + +static void col_forecast_local_copy_slot_set( + ColoForecastNpcLocal dst[COLO_MAX_NPCS], + const ColoForecastNpcLocal src[COLO_MAX_NPCS], + const int slots[COLO_MAX_NPCS], + int slot_count +) { + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + dst[i] = src[i]; + } +} + +static int col_forecast_action_is_run_tile(int action_idx) { + return action_idx >= 9 && action_idx < ENCOUNTER_MOVE_ACTIONS; +} + +static void col_forecast_static_threat_npc( + const ColosseumState* s, + const ColoForecastNpcLocal* npc, + ColoStepOutForecastAction* action, + ColoForecastObsSummary* summary +) { + if (!npc->active || npc->death_ticks > 0 || npc->hp <= 0) return; + if (col_type_is_hazard_entity(npc->type)) return; + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; + int lx = action->land_x; + int ly = action->land_y; + int dist = col_forecast_local_dist_to_player(npc, lx, ly); + + if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_WARBAND) { + if (col_forecast_warband_adjacent(npc, lx, ly)) + col_step_out_forecast_record_style( + action, summary, 0, npc->type, npc->default_style); + return; + } + if (npc->forecast_behavior == COLO_FORECAST_BEHAVIOR_SOL) { + if (npc->sol_immobile_ticks > 0 || npc->sol_attack_delay > 0) return; + if (dist == 1) { + col_step_out_forecast_record_style( + action, summary, 0, npc->type, ATTACK_STYLE_MELEE); + } else if (dist > 1 && dist <= 15 && + col_forecast_local_has_los_to_player(s, npc, lx, ly)) { + col_step_out_forecast_record_style( + action, summary, 0, npc->type, ATTACK_STYLE_RANGED); + } + return; + } + if (npc->type == COLO_MINOTAUR || npc->type == COLO_JAGUAR_WARRIOR) { + if (dist == 1) + col_step_out_forecast_record_style( + action, summary, 0, npc->type, ATTACK_STYLE_MELEE); + return; + } + if (npc->attack_range > 1) { + if (dist < 1 || dist > npc->attack_range) return; + if (!col_forecast_local_has_los_to_player(s, npc, lx, ly)) return; + AttackStyle style = npc->default_style; + if (npc->type == COLO_MANTICORE) { + int orb = npc->cycle_step > 0 ? npc->cycle_step : 0; + AttackStyle cur = npc->cycle_step > 0 ? npc->orb_style[orb] + : npc->fixed_orb_style[0]; + if (cur != ATTACK_STYLE_NONE) style = cur; + } + col_step_out_forecast_record_style(action, summary, 0, npc->type, style); + return; + } + if (dist == 1) + col_step_out_forecast_record_style( + action, summary, 0, npc->type, npc->default_style); +} + +static void col_forecast_static_threat_action( + const ColosseumState* s, + const ColoForecastNpcLocal base_npcs[COLO_MAX_NPCS], + const int slots[COLO_MAX_NPCS], + int slot_count, + ColoStepOutForecastAction* action, + ColoForecastObsSummary* summary +) { + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + col_forecast_static_threat_npc(s, &base_npcs[i], action, summary); + } +} + +typedef struct { + int pillar; + int tier; + ColoSolarflareOrb orb; +} ColoForecastSolarflare; + +static ColoForecastSolarflare col_forecast_solarflare_precompute(const ColosseumState* s) { + ColoForecastSolarflare pre = { .pillar = -1, .tier = 0, .orb = {0} }; + if (!s->solarflare.active) return pre; + int tier = col_mod_tier(s, COLO_MOD_SOLARFLARE); + if (tier == 0) return pre; + int best_pillar = -1; + int best_distance = 1 << 30; + for (int p = 0; p < COLO_NUM_PILLARS; p++) { + int x, y; + col_solarflare_tile(s, p, s->solarflare.step, &x, &y); + int dx = abs(x - s->player.x); + int dy = abs(y - s->player.y); + int distance = dx > dy ? dx : dy; + if (distance < best_distance) { + best_distance = distance; + best_pillar = p; + } + } + pre.pillar = best_pillar; + pre.tier = tier; + pre.orb = s->solarflare; + return pre; +} + +static int col_forecast_solarflare_contact_tick( + const ColosseumState* s, + const ColoForecastSolarflare* pre, + int land_x, + int land_y, + int horizon +) { + if (pre->pillar < 0) return -1; + ColoSolarflareOrb orb = pre->orb; + for (int tick_idx = 0; tick_idx < horizon; tick_idx++) { + col_solarflare_advance_orb(&orb, pre->tier); + int x, y; + col_solarflare_tile(s, pre->pillar, orb.step, &x, &y); + if (x == land_x && y == land_y) return tick_idx; + } + return -1; +} + +static void col_build_step_out_forecast_horizon_mode_summary( + const ColosseumState* s, + ColoStepOutForecast* out, + ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS], + int horizon, + int run_tile_mode +) { + if (horizon < 1) horizon = 1; + if (horizon > COLO_STEP_OUT_FORECAST_HORIZON) + horizon = COLO_STEP_OUT_FORECAST_HORIZON; + memset(out, 0, sizeof(*out)); + if (summaries) + memset(summaries, 0, sizeof(ColoForecastObsSummary) * ENCOUNTER_MOVE_ACTIONS); +#ifdef COLO_PROFILE_ENABLED + int col_fc_prof = COLO_PROFILE_ENABLED(); + double col_fc_t = col_fc_prof ? COLO_PROFILE_NOW_MS() : 0.0; +#endif + int forecast_slots[COLO_MAX_NPCS]; + int forecast_slot_count = col_collect_step_out_forecast_slots(s, forecast_slots); + + ColoForecastNpcLocal base_npcs[COLO_MAX_NPCS]; + uint8_t base_npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; + ColoForecastPrecomp pre = {0}; + if (forecast_slot_count > 0) { + col_forecast_local_copy_npc_slots( + s, base_npcs, forecast_slots, forecast_slot_count); + col_forecast_local_rebuild_npc_flags( + base_npcs, base_npc_flags, forecast_slots, forecast_slot_count); + pre = col_forecast_precompute( + s, base_npcs, forecast_slots, forecast_slot_count); + } +#ifdef COLO_PROFILE_ENABLED + if (col_fc_prof) { double n = COLO_PROFILE_NOW_MS(); COLO_PROFILE_ADD(COLO_PROF_FC_SETUP, n - col_fc_t); col_fc_t = n; } +#endif + + for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { + if (!col_step_out_forecast_prepare_unique_action_ctx( + s, out, summaries, action_idx, forecast_slot_count)) { + continue; + } + ColoStepOutForecastAction* action = &out->actions[action_idx]; + ColoForecastObsSummary* summary = summaries ? &summaries[action_idx] : NULL; + if (col_forecast_action_rollout_writes_no_threats( + base_npcs, forecast_slots, forecast_slot_count, action, horizon)) { + continue; + } + if (run_tile_mode == COLO_FORECAST_RUN_TILE_STATIC_THREAT && + col_forecast_action_is_run_tile(action_idx)) { + col_forecast_static_threat_action( + s, base_npcs, forecast_slots, forecast_slot_count, action, summary); + col_step_out_forecast_finalize_action(action, horizon); + continue; + } + ColoForecastNpcLocal npcs[COLO_MAX_NPCS]; + uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; + col_forecast_local_copy_slot_set( + npcs, base_npcs, forecast_slots, forecast_slot_count); + memcpy(npc_flags, base_npc_flags, sizeof(base_npc_flags)); + for (int tick_idx = 0; tick_idx < horizon; tick_idx++) { + col_step_out_forecast_readonly_tick( + s, npcs, npc_flags, action, summary, tick_idx, + forecast_slots, forecast_slot_count, &pre); + } + col_step_out_forecast_finalize_action(action, horizon); + } +#ifdef COLO_PROFILE_ENABLED + if (col_fc_prof) { double n = COLO_PROFILE_NOW_MS(); COLO_PROFILE_ADD(COLO_PROF_FC_ROLLOUT, n - col_fc_t); col_fc_t = n; } +#endif + + ColoForecastSolarflare solarflare = col_forecast_solarflare_precompute(s); + for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { + ColoStepOutForecastAction* action = &out->actions[action_idx]; + action->solarflare_contact_tick = action->valid + ? col_forecast_solarflare_contact_tick( + s, &solarflare, action->land_x, action->land_y, horizon) + : -1; + } +#ifdef COLO_PROFILE_ENABLED + if (col_fc_prof) COLO_PROFILE_ADD(COLO_PROF_FC_SOLARFLARE, COLO_PROFILE_NOW_MS() - col_fc_t); +#endif +} + +static void col_build_step_out_forecast_horizon_mode( + const ColosseumState* s, + ColoStepOutForecast* out, + int horizon, + int run_tile_mode +) { + col_build_step_out_forecast_horizon_mode_summary( + s, out, NULL, horizon, run_tile_mode); +} + +static int col_write_step_out_forecast_obs_summary( + const ColoStepOutForecast* forecast, + const ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS], + int horizon, + float* obs, + int i +) { + for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { + const ColoStepOutForecastAction* action = &forecast->actions[action_idx]; + const ColoForecastObsSummary* summary = &summaries[action_idx]; + obs[i++] = action->valid ? 1.0f : 0.0f; + obs[i++] = (float)summary->first_attack_tick / (float)horizon; + obs[i++] = (float)summary->first_style_mask / 7.0f; + obs[i++] = (float)summary->max_hit / 150.0f; + obs[i++] = action->same_tick_mixed_style_conflict ? 1.0f : 0.0f; + obs[i++] = summary->ranged_magic_same_tick ? 1.0f : 0.0f; + obs[i++] = action->ranged_magic_offtick_opportunity ? 1.0f : 0.0f; + obs[i++] = action->melee_fallback_exposure ? 1.0f : 0.0f; + obs[i++] = action->solarflare_contact_tick >= 0 + ? (float)(horizon - action->solarflare_contact_tick) / (float)horizon + : 0.0f; + } + return i; +} + +static void col_build_step_out_forecast_ctx( + const ColosseumState* s, + ColoStepOutForecast* out +) { + col_build_step_out_forecast_horizon_mode( + s, out, COLO_STEP_OUT_FORECAST_HORIZON, COLO_FORECAST_RUN_TILE_FULL); +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc new file mode 100644 index 0000000000..785106a2d1 --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_helpers.inc @@ -0,0 +1,358 @@ +#line 76 "encounter_colosseum.h" + +static float col_compute_reward_ctx(ColosseumState* s, const ColosseumContext* ctx); +static void col_spawn_wave(ColosseumState* s); +static void col_tick_npcs_ctx(ColosseumState* s, ColosseumContext* ctx); +static void col_tick_player_ctx( + ColosseumState* s, ColosseumContext* ctx, const int* actions, int can_attack); +static void col_player_pretick(ColosseumState* s, ColosseumContext* ctx, const int* actions); +static void col_resolve_player_projectiles_on_npcs_ctx( + ColosseumState* s, ColosseumContext* ctx); +static void col_resolve_player_pending_hits_ctx(ColosseumState* s, ColosseumContext* ctx); +static void col_resolve_player_pending_hits(ColosseumState* s); +static void col_rebuild_player_collision_flags(ColosseumState* s); +static void col_refresh_current_obs_slots_ctx(ColosseumState* s, const ColosseumContext* ctx); + +static void col_sol_boss_tick(ColosseumState* s, ColosseumContext* ctx); +static void col_sol_begin_boss_arena(ColosseumState* s); +static int col_sol_clamp_active(const ColosseumState* s); +static int col_in_boss_arena(const ColosseumState* s, int x, int y); + +static inline int col_mod_active(const ColosseumState* s, ColoModifier mod); +static inline int col_mod_tier(const ColosseumState* s, ColoModifier mod); +static void col_modifiers_on_wave_spawn(ColosseumState* s); +static void col_modifier_open_draft(ColosseumState* s, int next_wave); +static void col_apply_late_start_entry_state( + ColosseumState* s, const ColosseumContext* ctx, int start_wave); + +static void col_abort_invalid_weapon_set(ColoWeaponSet set) { + fprintf(stderr, "invalid Colosseum weapon set %d\n", (int)set); + abort(); +} + +static AttackStyle col_weapon_set_attack_style(ColoWeaponSet set) { + switch (set) { + case COLO_GEAR_MELEE: + return ATTACK_STYLE_MELEE; + case COLO_GEAR_RANGED: + return ATTACK_STYLE_RANGED; + case COLO_GEAR_MAGIC: + return ATTACK_STYLE_MAGIC; + case COLO_NUM_WEAPON_SETS: + break; + } + col_abort_invalid_weapon_set(set); + return ATTACK_STYLE_MELEE; +} + +static FightStyle col_weapon_set_fight_style(ColoWeaponSet set) { + switch (set) { + case COLO_GEAR_MELEE: + return FIGHT_STYLE_AGGRESSIVE; + case COLO_GEAR_RANGED: + return FIGHT_STYLE_RAPID; + case COLO_GEAR_MAGIC: + return FIGHT_STYLE_ACCURATE; + case COLO_NUM_WEAPON_SETS: + break; + } + col_abort_invalid_weapon_set(set); + return FIGHT_STYLE_AGGRESSIVE; +} + +static int col_weapon_set_spell_base_damage(const ColosseumState* s, ColoWeaponSet set) { + if (set != COLO_GEAR_MAGIC) return 0; + if (s->active_loadout_profile < 0 || + s->active_loadout_profile >= COLO_NUM_LOADOUT_PROFILES) { + fprintf(stderr, "invalid Colosseum loadout profile %d\n", + (int)s->active_loadout_profile); + abort(); + } + return COLO_MAGIC_SPELL_BASE_DAMAGE[s->active_loadout_profile]; +} + +static AttackStyle col_equipped_weapon_attack_style(const Player* p) { + int style = get_item_attack_style(p->equipped[GEAR_SLOT_WEAPON]); + if (style == 1) return ATTACK_STYLE_MELEE; + if (style == 2) return ATTACK_STYLE_RANGED; + if (style == 3) return ATTACK_STYLE_MAGIC; + return ATTACK_STYLE_MELEE; +} + +static FightStyle col_equipped_weapon_fight_style(const Player* p) { + AttackStyle style = col_equipped_weapon_attack_style(p); + if (style == ATTACK_STYLE_RANGED) + return FIGHT_STYLE_RAPID; + if (style == ATTACK_STYLE_MAGIC) + return FIGHT_STYLE_ACCURATE; + return FIGHT_STYLE_AGGRESSIVE; +} + +static GearSet col_gear_set_for_attack_style(AttackStyle style) { + if (style == ATTACK_STYLE_RANGED) return GEAR_RANGED; + if (style == ATTACK_STYLE_MAGIC) return GEAR_MAGE; + return GEAR_MELEE; +} + +static int col_equipped_spell_base_damage(const ColosseumState* s) { + if (col_equipped_weapon_attack_style(&s->player) != ATTACK_STYLE_MAGIC) return 0; + return COLO_MAGIC_SPELL_BASE_DAMAGE[s->active_loadout_profile]; +} + +static void col_current_style_levels( + const Player* p, + AttackStyle style, + int* current_att_level, + int* current_str_level +) { + if (style == ATTACK_STYLE_RANGED) { + *current_att_level = p->current_ranged; + *current_str_level = p->current_ranged; + return; + } + if (style == ATTACK_STYLE_MAGIC) { + *current_att_level = p->current_magic; + *current_str_level = p->current_magic; + return; + } + *current_att_level = p->current_attack; + *current_str_level = p->current_strength; +} + +static void col_mark_live_loadout_dirty(ColosseumState* s) { + s->loadout_stats_dirty = 1; +} + +static void col_recompute_live_loadout_stats(ColosseumState* s) { + AttackStyle style = col_equipped_weapon_attack_style(&s->player); + FightStyle fight_style = col_equipped_weapon_fight_style(&s->player); + int current_att_level; + int current_str_level; + col_current_style_levels(&s->player, style, ¤t_att_level, ¤t_str_level); + s->player.current_gear = col_gear_set_for_attack_style(style); + s->player.visible_gear = s->player.current_gear; + encounter_compute_loadout_stats( + s->player.equipped, + style, + s->player.offensive_prayer, + current_att_level, + fight_style, + col_equipped_spell_base_damage(s), + &s->loadout_stats_live); + encounter_update_loadout_level( + &s->loadout_stats_live, + s->player.offensive_prayer, + current_att_level, + current_str_level); + encounter_derive_loadout_effect_profile(s->player.equipped, &s->effects_live); + s->loadout_stats_dirty = 0; +} + +static void col_ensure_live_loadout_stats(ColosseumState* s) { + if (s->loadout_stats_dirty) + col_recompute_live_loadout_stats(s); +} + +static const EncounterLoadoutStats* col_live_loadout_stats(ColosseumState* s) { + col_ensure_live_loadout_stats(s); + return &s->loadout_stats_live; +} + +static const OsrsEquipmentEffectProfile* col_live_effects(ColosseumState* s) { + col_ensure_live_loadout_stats(s); + return &s->effects_live; +} + +static inline EncounterProjectileTiming col_player_projectile_timing( + AttackStyle style, + uint8_t weapon, + int distance +) { + EncounterProjectileDelayKind kind = encounter_projectile_delay_kind_for_style(style); + EncounterProjectileDelayOptions options = {0}; + if (style == ATTACK_STYLE_RANGED && weapon == ITEM_TWISTED_BOW) + options.visual_delay_ticks = 1; + return encounter_projectile_timing(distance, 1, kind, options); +} + +static inline int col_npc_attack_anim_id(int npc_def_id) { + const NpcModelMapping* nm = npc_model_lookup((uint16_t)npc_def_id); + if (!nm) + return -1; + if (nm->attack_anim != 65535) + return (int)nm->attack_anim; + if (nm->walk_anim != 65535 && nm->walk_anim != nm->idle_anim) + return (int)nm->walk_anim; + return -1; +} + +typedef struct { + int anim_id; + int client_ticks; + int linger_ticks; +} ColoNpcDeathVisual; + +static const ColoNpcDeathVisual COLO_NPC_DEATH_VISUALS[COLO_NUM_NPC_TYPES] = { + [COLO_FREMENNIK_BERSERKER] = { .anim_id = 10857, .client_ticks = 63, .linger_ticks = 3 }, + [COLO_FREMENNIK_ARCHER] = { .anim_id = 10851, .client_ticks = 63, .linger_ticks = 3 }, + [COLO_FREMENNIK_SEER] = { .anim_id = 10854, .client_ticks = 63, .linger_ticks = 3 }, + [COLO_SERPENT_SHAMAN] = { .anim_id = 10860, .client_ticks = 47, .linger_ticks = 2 }, + [COLO_JAGUAR_WARRIOR] = { .anim_id = 10848, .client_ticks = 63, .linger_ticks = 3 }, + [COLO_JAVELIN_COLOSSUS] = { .anim_id = 10894, .client_ticks = 60, .linger_ticks = 2 }, + [COLO_SHOCKWAVE_COLOSSUS] = { .anim_id = 10895, .client_ticks = 60, .linger_ticks = 2 }, + [COLO_MINOTAUR] = { .anim_id = 10845, .client_ticks = 150, .linger_ticks = 5 }, + [COLO_MANTICORE] = { .anim_id = 10866, .client_ticks = 80, .linger_ticks = 3 }, + [COLO_SOL_HEREDIT] = { .anim_id = 10888, .client_ticks = 230, .linger_ticks = 8 }, + [COLO_HEALING_TOTEM] = { .anim_id = -1, .client_ticks = 0, .linger_ticks = 0 }, + [COLO_BEE_SWARM] = { .anim_id = -1, .client_ticks = 0, .linger_ticks = 0 }, +}; + +static inline const ColoNpcDeathVisual* col_npc_death_visual(ColoNpcType type) { + assert(type >= 0 && type < COLO_NUM_NPC_TYPES); + return &COLO_NPC_DEATH_VISUALS[type]; +} + +static inline int col_npc_death_anim_id(ColoNpcType type) { + return col_npc_death_visual(type)->anim_id; +} + +static inline int col_npc_death_linger_ticks(ColoNpcType type) { + return col_npc_death_visual(type)->linger_ticks; +} + +static inline int col_npc_is_live_target(const ColoNPC* npc) { + return npc && npc->active && npc->death_ticks == 0 && npc->hp > 0; +} + +static inline int col_npc_is_live_enemy(const ColoNPC* npc) { + return col_npc_is_live_target(npc) && !col_type_is_hazard_entity(npc->type); +} + +static void col_queue_npc_pending_hit( + ColosseumState* s, + int npc_slot, + int damage, + int ticks, + AttackStyle style, + int spell_type +) { + ColoNPC* npc = &s->npcs[npc_slot]; + EncounterPendingHit hit = { + .active = 1, + .damage = damage, + .ticks_remaining = ticks, + .attack_style = style, + .check_prayer = 0, + .spell_type = spell_type, + .source_npc_slot = -1, + }; + encounter_pending_hit_queue_push( + &npc->pending_hits, hit, "colosseum-npc", s->tick, npc_slot, npc->type); +} + +static inline int col_scale_incoming_damage(const ColosseumState* s, int dmg) { + float scale = s->active_player_damage_received_scale; + if (scale >= 1.0f) return dmg; + if (scale <= 0.0f) return 0; + int scaled = (int)((float)dmg * scale + 0.5f); + return scaled < 0 ? 0 : scaled; +} + +static void col_push_player_pending_hit(ColosseumState* s, EncounterPendingHit hit) { + hit.damage = col_scale_incoming_damage(s, hit.damage); + encounter_pending_hit_queue_push( + &s->player_pending_hits, hit, "colosseum-player", s->tick, + hit.source_npc_slot, hit.source_npc_type); +} + +static int col_default_action_debug_log(void) { + const char* value = getenv("OSRS_COLOSSEUM_ACTION_LOG"); + if (!value || value[0] == '\0') return 0; + if (strcmp(value, "1") == 0) return 1; + if (strcmp(value, "0") == 0) return 0; + fprintf(stderr, "OSRS_COLOSSEUM_ACTION_LOG must be 0 or 1, got %s\n", value); + abort(); +} + +static ColConfig col_default_config(void) { + return (ColConfig){ + .start_wave = 0, + .damage_reward_coeff = 0.0f, + .boss_damage_reward_coeff = 0.0f, + .wave_clear_bonus = 1.0f, + .boss_phase_bonus = 0.0f, + .win_bonus = 0.0f, + .prayer_correct_reward = 0.0f, + .offpray_damage_penalty_coeff = 0.0f, + .multistyle_exposure_penalty_coeff = 0.0f, + .argmax_gear_reward_coeff = 0.0f, + .offensive_boost_reward_coeff = 0.0f, + .stall_penalty_coeff = 0.0f, + .farm_safe_damage_cap = 0, + .farm_cap_waves = COLO_FARM_CAP_WAVES, + .avoided_damage_coeff = 0.0f, + .death_penalty_coeff = 0.0f, + .timeout_penalty = 0.0f, + .curriculum_agent = 0, + .step_out_forecast_obs_enabled = 1, + .threat_field_obs_enabled = 1, + .forecast_horizon = COLO_STEP_OUT_FORECAST_HORIZON, + .forecast_run_tile_mode = COLO_FORECAST_RUN_TILE_FULL, + .mask_inventory_heads = 0, + .action_debug_log = col_default_action_debug_log(), + .late_start_state_mode = 2, + .late_start_supply_fraction_per_wave = 0.07f, + .prayer_switch_fail_prob = 0.0f, + .player_damage_received_scale = 1.0f, + .damage_scale_anneal_start = 1.0f, + .damage_scale_anneal_ticks = 0, + }; +} + +static void col_init_context_typed(ColosseumContext* ctx) { + memset(ctx, 0, sizeof(*ctx)); + ctx->config = col_default_config(); +} + +static void col_init_context(EncounterContext* context) { + col_init_context_typed((ColosseumContext*)context); +} + +static void col_destroy_context(EncounterContext* context) { + (void)context; +} + +static ColosseumContext* col_legacy_context(void) { + static ColosseumContext ctx; + static Log log; + static int initialized = 0; + if (!initialized) { + col_init_context_typed(&ctx); + ctx.log = &log; + initialized = 1; + } + return &ctx; +} + +static void col_init_state_typed(ColosseumState* s, ColosseumContext* ctx) { + col_build_npc_stats(); + memset(s, 0, sizeof(*s)); + s->rng_state = 12345; + s->start_wave = ctx ? ctx->config.start_wave : 0; + s->episode_max_ticks = COLO_MAX_TICKS; + s->min_sol_hp_seen = COLO_SOL_HP_MAX; +} + +static void col_init_state(EncounterState* state, EncounterContext* context) { + col_init_state_typed((ColosseumState*)state, (ColosseumContext*)context); +} + +static EncounterState* col_create(void) { + ColosseumState* s = (ColosseumState*)calloc(1, sizeof(ColosseumState)); + col_init_state_typed(s, col_legacy_context()); + return (EncounterState*)s; +} + +static void col_destroy(EncounterState* state) { + free(state); +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc new file mode 100644 index 0000000000..21cbdf241d --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab.inc @@ -0,0 +1,423 @@ +#line 76 "encounter_colosseum.h" + +typedef enum { + COLO_LAB_COMMAND_NONE = 0, + COLO_LAB_COMMAND_RESET, + COLO_LAB_COMMAND_SET_PLAYER, + COLO_LAB_COMMAND_SPAWN_NPC, + COLO_LAB_COMMAND_MOVE_NPC, + COLO_LAB_COMMAND_DELETE_NPC, + COLO_LAB_COMMAND_KILL_NPC, + COLO_LAB_COMMAND_SET_NPC_HP, + COLO_LAB_COMMAND_SET_NPC_TIMER, + COLO_LAB_COMMAND_SPAWN_WAVE, + COLO_LAB_COMMAND_SET_BOSS_PHASE, + COLO_LAB_COMMAND_START_GRAPPLE, + COLO_LAB_COMMAND_SET_MODIFIER, + COLO_LAB_COMMAND_OPEN_DRAFT, + COLO_LAB_COMMAND_CLEAR_NPCS, + COLO_LAB_COMMAND_STEP_TICKS, +} ColoLabCommandKind; + +typedef enum { + COLO_LAB_LINE_NONE = 0, + COLO_LAB_LINE_DUMP, +} ColoLabLineResult; + +typedef EncounterLabOptionalInt ColoLabOptionalInt; +#define COLO_LAB_OPTIONAL_INT_UNSET ENCOUNTER_LAB_OPTIONAL_INT_UNSET +#define COLO_LAB_OPTIONAL_INT_SET ENCOUNTER_LAB_OPTIONAL_INT_SET + +typedef struct { + int x; + int y; + ColoLabOptionalInt hp; + ColoLabOptionalInt prayer; +} ColoLabPlayerCommand; + +typedef struct { + int slot; + ColoNpcType type; + int x; + int y; + ColoLabOptionalInt hp; + ColoLabOptionalInt timer; +} ColoLabSpawnNpcCommand; + +typedef struct { + int slot; + int x; + int y; +} ColoLabMoveNpcCommand; + +typedef struct { + int slot; +} ColoLabNpcSlotCommand; + +typedef struct { + int slot; + int hp; +} ColoLabNpcHpCommand; + +typedef struct { + int slot; + int timer; +} ColoLabNpcTimerCommand; + +typedef struct { + int wave; +} ColoLabWaveCommand; + +typedef struct { + int phase; +} ColoLabBossPhaseCommand; + +typedef struct { + int modifier; + int tier; +} ColoLabModifierCommand; + +typedef struct { + int option[COLO_MODIFIER_DRAFT_OPTIONS]; +} ColoLabDraftCommand; + +typedef struct { + int ticks; +} ColoLabStepTicksCommand; + +typedef struct { + uint32_t seed; +} ColoLabResetCommand; + +typedef struct { + ColoLabCommandKind kind; + union { + ColoLabResetCommand reset; + ColoLabPlayerCommand player; + ColoLabSpawnNpcCommand spawn_npc; + ColoLabMoveNpcCommand move_npc; + ColoLabNpcSlotCommand npc_slot; + ColoLabNpcHpCommand npc_hp; + ColoLabNpcTimerCommand npc_timer; + ColoLabWaveCommand wave; + ColoLabBossPhaseCommand boss_phase; + ColoLabModifierCommand modifier; + ColoLabDraftCommand draft; + ColoLabStepTicksCommand step_ticks; + } as; +} ColosseumLabCommand; + +typedef EncounterLabString ColoLabString; + +static void col_lab_abort(const char* fmt, ...) { + va_list args; + fprintf(stderr, "colosseum lab: "); + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + fprintf(stderr, "\n"); + abort(); +} + +static void col_lab_require_slot(int slot) { + if (slot < 0 || slot >= COLO_MAX_NPCS) + col_lab_abort("npc slot must be in [0,%d], got %d", COLO_MAX_NPCS - 1, slot); +} + +static void col_lab_require_type(int type) { + if (type < 0 || type >= COLO_NUM_NPC_TYPES) + col_lab_abort("npc type must be in [0,%d], got %d", COLO_NUM_NPC_TYPES - 1, type); +} + +static int col_lab_in_arena(int x, int y) { + return x >= COLO_ARENA_MIN_X && x <= COLO_ARENA_MAX_X && + y >= COLO_ARENA_MIN_Y && y <= COLO_ARENA_MAX_Y; +} + +static void col_lab_clear_transient(ColosseumState* s) { + col_context_clear_render_events(col_legacy_context()); + osrs_interaction_init(&s->interaction); + s->player_last_interaction_target_slot = -1; + s->player_last_interaction_age = 1; + s->player_dest_x = -1; + s->player_dest_y = -1; + encounter_pending_hit_queue_clear(&s->player_pending_hits); + s->tick_scratch = (ColoTickScratch){0}; + s->player_attack_npc_idx = -1; + s->player_attack_dmg = 0; + s->player_attack_style_id = ATTACK_STYLE_NONE; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s->npcs[i]; + npc->attacked_this_tick = 0; + npc->attack_style_this_tick = ATTACK_STYLE_NONE; + npc->moved_this_tick = 0; + npc->hit_landed_this_tick = 0; + npc->hit_damage = 0; + npc->hit_was_successful_this_tick = 0; + encounter_pending_hit_queue_clear(&npc->pending_hits); + } +} + +static void col_lab_refresh_geometry(ColosseumState* s) { + col_rebuild_player_collision_flags(s); + col_refresh_current_obs_slots_ctx(s, col_legacy_context()); +} + +static void col_lab_remove_all_npcs(ColosseumState* s) { + memset(s->npcs, 0, sizeof(s->npcs)); + memset(s->totems, 0, sizeof(s->totems)); + memset(s->bees, 0, sizeof(s->bees)); +} + +static void col_lab_clear_npcs(ColosseumState* s) { + col_lab_remove_all_npcs(s); + col_lab_clear_transient(s); + col_lab_refresh_geometry(s); +} + +static const char* col_lab_modifier_name(int mod); + +static void col_lab_apply_command(ColosseumState* s, const ColosseumLabCommand* cmd) { + if (!s || !cmd) col_lab_abort("null command"); + + switch (cmd->kind) { + case COLO_LAB_COMMAND_NONE: + return; + + case COLO_LAB_COMMAND_RESET: + col_reset((EncounterState*)s, cmd->as.reset.seed); + return; + + case COLO_LAB_COMMAND_SET_PLAYER: { + const ColoLabPlayerCommand* p = &cmd->as.player; + if (!col_lab_in_arena(p->x, p->y)) + col_lab_abort("player tile out of arena: (%d,%d)", p->x, p->y); + s->player.x = p->x; + s->player.y = p->y; + if (p->hp.kind == ENCOUNTER_LAB_OPTIONAL_INT_SET) { + int hp = p->hp.value; + if (hp < 0) col_lab_abort("player hp must be nonnegative"); + if (hp > s->player.base_hitpoints) + col_lab_abort("player hp %d exceeds max %d", hp, s->player.base_hitpoints); + s->player.current_hitpoints = hp; + } + if (p->prayer.kind == ENCOUNTER_LAB_OPTIONAL_INT_SET) + s->player.prayer = (OverheadPrayer)p->prayer.value; + col_lab_clear_transient(s); + col_lab_refresh_geometry(s); + return; + } + + case COLO_LAB_COMMAND_SPAWN_NPC: { + ColoLabSpawnNpcCommand spawn_local = cmd->as.spawn_npc; + ColoLabSpawnNpcCommand* spawn = &spawn_local; + if (spawn->slot < 0) { + spawn->slot = col_find_free_npc(s); + if (spawn->slot < 0) + col_lab_abort("no free npc slot (all %d in use)", COLO_MAX_NPCS); + } + col_lab_require_slot(spawn->slot); + col_lab_require_type(spawn->type); + if (!col_lab_in_arena(spawn->x, spawn->y)) + col_lab_abort("npc tile out of arena: (%d,%d)", spawn->x, spawn->y); + if (s->npcs[spawn->slot].active) + col_deactivate_npc(s, spawn->slot); + col_init_npc(s, spawn->slot, spawn->type, spawn->x, spawn->y); + if (spawn->hp.kind == ENCOUNTER_LAB_OPTIONAL_INT_SET) { + int hp = spawn->hp.value; + if (hp < 0) col_lab_abort("npc hp must be nonnegative"); + if (hp > s->npcs[spawn->slot].max_hp) + col_lab_abort("npc hp %d exceeds max hp %d", + hp, s->npcs[spawn->slot].max_hp); + s->npcs[spawn->slot].hp = hp; + } + if (spawn->timer.kind == ENCOUNTER_LAB_OPTIONAL_INT_SET) { + if (spawn->timer.value < 0) + col_lab_abort("npc timer must be nonnegative"); + s->npcs[spawn->slot].attack_timer = spawn->timer.value; + } + col_lab_clear_transient(s); + col_lab_refresh_geometry(s); + return; + } + + case COLO_LAB_COMMAND_MOVE_NPC: { + const ColoLabMoveNpcCommand* move = &cmd->as.move_npc; + col_lab_require_slot(move->slot); + if (!s->npcs[move->slot].active) + col_lab_abort("cannot move inactive npc slot %d", move->slot); + if (!col_lab_in_arena(move->x, move->y)) + col_lab_abort("npc tile out of arena: (%d,%d)", move->x, move->y); + int size = col_npc_effective_size(&s->npcs[move->slot]); + col_stamp_npc_collision_footprint(s, s->npcs[move->slot].x, + s->npcs[move->slot].y, size, 0); + s->npcs[move->slot].x = move->x; + s->npcs[move->slot].y = move->y; + s->npcs[move->slot].target_x = move->x; + s->npcs[move->slot].target_y = move->y; + col_stamp_npc_collision_footprint(s, move->x, move->y, size, 1); + col_lab_clear_transient(s); + col_lab_refresh_geometry(s); + return; + } + + case COLO_LAB_COMMAND_DELETE_NPC: { + int slot = cmd->as.npc_slot.slot; + col_lab_require_slot(slot); + col_deactivate_npc(s, slot); + col_lab_clear_transient(s); + col_lab_refresh_geometry(s); + return; + } + + case COLO_LAB_COMMAND_KILL_NPC: { + int slot = cmd->as.npc_slot.slot; + col_lab_require_slot(slot); + if (!s->npcs[slot].active) + col_lab_abort("cannot kill inactive npc slot %d", slot); + s->npcs[slot].hp = 0; + col_deactivate_npc(s, slot); + col_lab_clear_transient(s); + col_lab_refresh_geometry(s); + return; + } + + case COLO_LAB_COMMAND_SET_NPC_HP: { + int slot = cmd->as.npc_hp.slot; + int hp = cmd->as.npc_hp.hp; + col_lab_require_slot(slot); + if (!s->npcs[slot].active) + col_lab_abort("cannot set hp for inactive npc slot %d", slot); + if (hp < 0) col_lab_abort("npc hp must be nonnegative"); + if (hp > s->npcs[slot].max_hp) + col_lab_abort("npc hp %d exceeds max hp %d", hp, s->npcs[slot].max_hp); + s->npcs[slot].hp = hp; + col_lab_refresh_geometry(s); + return; + } + + case COLO_LAB_COMMAND_SET_NPC_TIMER: { + int slot = cmd->as.npc_timer.slot; + int timer = cmd->as.npc_timer.timer; + col_lab_require_slot(slot); + if (!s->npcs[slot].active) + col_lab_abort("cannot set timer for inactive npc slot %d", slot); + if (timer < 0) col_lab_abort("npc timer must be nonnegative"); + s->npcs[slot].attack_timer = timer; + col_lab_refresh_geometry(s); + return; + } + + case COLO_LAB_COMMAND_SPAWN_WAVE: { + int wave = cmd->as.wave.wave; + if (wave < 1 || wave > COLO_NUM_WAVES) + col_lab_abort("wave must be in [1,%d], got %d", COLO_NUM_WAVES, wave); + col_lab_remove_all_npcs(s); + s->modifiers.draft_pending = 0; + s->modifiers.draft_gates_spawn = 0; + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + s->modifiers.draft_options[o] = -1; + s->wave = wave - 1; + s->wave_spawn_target = s->wave; + s->wave_spawn_delay = 0; + + col_spawn_wave(s); + col_lab_clear_transient(s); + col_lab_refresh_geometry(s); + return; + } + + case COLO_LAB_COMMAND_SET_BOSS_PHASE: { + int phase = cmd->as.boss_phase.phase; + if (phase < 0 || phase >= COLO_SOL_NUM_PHASES) + col_lab_abort("boss phase must be in [0,%d], got %d", + COLO_SOL_NUM_PHASES - 1, phase); + int idx = col_sol_find_idx(s); + if (idx < 0) + col_lab_abort("no live Sol Heredit; spawn one or 'wave wave=12' first"); + + static const int PHASE_HP[COLO_SOL_NUM_PHASES] = { + COLO_SOL_HP_MAX, + (COLO_SOL_HP_MAX * 90) / 100, + (COLO_SOL_HP_MAX * 75) / 100, + (COLO_SOL_HP_MAX * 50) / 100, + (COLO_SOL_HP_MAX * 25) / 100, + (COLO_SOL_HP_MAX * 10) / 100, + }; + s->npcs[idx].hp = PHASE_HP[phase]; + if (!s->sol.started) col_sol_begin_boss_arena(s); + s->sol.boss_idx = idx; + + if (s->sol.phase < phase) { + while (s->sol.phase < phase) col_sol_enter_phase(s, s->sol.phase + 1); + s->sol.immobile_ticks = COLO_SOL_TRANSITION_FREEZE; + s->sol.force_spear = 1; + } + s->sol.phase = phase; + col_lab_refresh_geometry(s); + return; + } + + case COLO_LAB_COMMAND_START_GRAPPLE: { + int idx = col_sol_find_idx(s); + if (idx < 0) + col_lab_abort("no live Sol Heredit to grapple; 'wave wave=12' first"); + if (!s->sol.started) col_sol_begin_boss_arena(s); + s->sol.boss_idx = idx; + col_sol_start_grapple(s); + return; + } + + case COLO_LAB_COMMAND_SET_MODIFIER: { + int mod = cmd->as.modifier.modifier; + int tier = cmd->as.modifier.tier; + if (mod < 0 || mod >= COLO_NUM_REAL_MODIFIERS) + col_lab_abort("modifier id must be in [0,%d], got %d", + COLO_NUM_REAL_MODIFIERS - 1, mod); + int max_tier = COLO_MODIFIER_MAX_TIER[mod]; + if (tier < 0 || tier > max_tier) + col_lab_abort("tier for %s must be in [0,%d], got %d", + col_lab_modifier_name(mod), max_tier, tier); + if (tier == 0) { + s->modifiers.active_mask &= ~(1u << (unsigned)mod); + s->modifiers.tier[mod] = 0; + } else { + s->modifiers.active_mask |= (1u << (unsigned)mod); + s->modifiers.tier[mod] = (uint8_t)tier; + } + if ((ColoModifier)mod == COLO_MOD_FRAILTY) col_mod_apply_frailty_hp(s); + col_lab_refresh_geometry(s); + return; + } + + case COLO_LAB_COMMAND_OPEN_DRAFT: { + const ColoLabDraftCommand* draft = &cmd->as.draft; + int offered = 0; + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { + s->modifiers.draft_options[o] = draft->option[o]; + if (draft->option[o] >= 0) offered++; + } + if (offered == 0) col_lab_abort("draft needs at least one option"); + + s->modifiers.draft_pending = 1; + s->modifiers.draft_gates_spawn = 0; + return; + } + + case COLO_LAB_COMMAND_CLEAR_NPCS: + col_lab_clear_npcs(s); + return; + + case COLO_LAB_COMMAND_STEP_TICKS: { + int ticks = cmd->as.step_ticks.ticks; + if (ticks < 0) col_lab_abort("step_ticks must be nonnegative"); + int actions[COLO_NUM_ACTION_HEADS] = {0}; + for (int t = 0; t < ticks; t++) + col_step((EncounterState*)s, actions); + col_lab_refresh_geometry(s); + return; + } + } + + col_lab_abort("unknown command kind %d", cmd->kind); +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_json.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_json.inc new file mode 100644 index 0000000000..fb2ad9ba5e --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_json.inc @@ -0,0 +1,80 @@ +#line 76 "encounter_colosseum.h" + +static void col_lab_append_npcs_json(ColosseumState* s, ColoLabString* out) { + encounter_lab_string_append(out, "\"npcs\":["); + int emitted = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s->npcs[i]; + if (!npc->active) continue; + if (emitted > 0) encounter_lab_string_append(out, ","); + const EncounterPendingHit* pending = + encounter_pending_hit_queue_earliest(&npc->pending_hits); + int pending_ticks = pending ? pending->ticks_remaining : 0; + encounter_lab_string_append(out, + "{\"slot\":%d,\"type\":\"%s\",\"type_id\":%d,\"def_id\":%d," + "\"x\":%d,\"y\":%d,\"size\":%d,\"hp\":%d,\"max_hp\":%d," + "\"timer\":%d,\"stun\":%d,\"frozen\":%d,\"death_ticks\":%d," + "\"pending_count\":%d,\"pending_earliest_ticks\":%d}", + i, col_lab_npc_type_name(npc->type), npc->type, + COLO_NPC_DEF_IDS[npc->type], npc->x, npc->y, + col_npc_effective_size(npc), npc->hp, npc->max_hp, + npc->attack_timer, npc->stun_timer, npc->frozen_ticks, + npc->death_ticks, npc->pending_hits.count, pending_ticks); + emitted++; + } + encounter_lab_string_append(out, "]"); +} + +static void col_lab_append_modifiers_json(const ColosseumState* s, ColoLabString* out) { + encounter_lab_string_append(out, "\"modifiers\":{\"active\":["); + int emitted = 0; + for (int m = 0; m < COLO_NUM_REAL_MODIFIERS; m++) { + if (!(s->modifiers.active_mask & (1u << (unsigned)m))) continue; + if (emitted > 0) encounter_lab_string_append(out, ","); + encounter_lab_string_append(out, "{\"id\":%d,\"name\":\"%s\",\"tier\":%d}", + m, col_lab_modifier_name(m), s->modifiers.tier[m]); + emitted++; + } + encounter_lab_string_append(out, "],\"draft_pending\":%d,\"draft_options\":[", + s->modifiers.draft_pending); + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { + if (o > 0) encounter_lab_string_append(out, ","); + encounter_lab_string_append(out, "%d", s->modifiers.draft_options[o]); + } + encounter_lab_string_append(out, "]}"); +} + +static char* col_lab_alloc_json(ColosseumState* s) { + ColoLabString out; + encounter_lab_string_init(&out, "colosseum lab"); + encounter_lab_string_append(&out, + "{\"tick\":%d,\"wave\":%d," + "\"player\":{\"x\":%d,\"y\":%d,\"hp\":%d,\"max_hp\":%d,\"prayer\":\"%s\"," + "\"attack_timer\":%d,\"weapon_set\":%d,\"attacked\":%d," + "\"attack_target\":%d,\"attack_damage\":%d," + "\"loadout_profile\":%d,\"brew_doses\":%d,\"restore_doses\":%d," + "\"combat_doses\":%d,\"ranged_doses\":%d,\"surge_doses\":%d," + "\"surge_cooldown\":%d,\"spec_energy\":%d,\"spec_armed\":%d," + "\"thrall_active\":%d,\"thrall_target\":%d,\"thrall_lifetime\":%d," + "\"thrall_recast_cd\":%d,\"death_charge_window\":%d,\"death_charge_cd\":%d},", + s->tick, s->wave + 1, + s->player.x, s->player.y, s->player.current_hitpoints, + s->player.base_hitpoints, col_lab_overhead_name(s->player.prayer), + s->player.attack_timer, s->weapon_set, s->tick_scratch.player_attacked, + s->player_attack_npc_idx, s->player_attack_dmg, + s->active_loadout_profile, s->player.brew_doses, s->player.restore_doses, + s->player.combat_potion_doses, s->player.ranged_potion_doses, s->surge_doses, + s->surge_cooldown, s->player.special_energy, s->player.spec_armed, + s->thrall_active, s->thrall_target_slot, s->thrall_lifetime_left, + s->thrall_recast_cd, s->death_charge_window_left, s->death_charge_cd); + col_lab_append_modifiers_json(s, &out); + encounter_lab_string_append(&out, ","); + encounter_lab_string_append(&out, + "\"boss\":{\"started\":%d,\"phase\":%d,\"grapple_active\":%d," + "\"grapple_slot\":%d,\"parry_active\":%d},", + s->sol.started, s->sol.phase, s->sol.grapple_active, + s->sol.grapple_body_slot, s->sol.parry_hits_left > 0 ? 1 : 0); + col_lab_append_npcs_json(s, &out); + encounter_lab_string_append(&out, "}"); + return out.data; +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_parse.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_parse.inc new file mode 100644 index 0000000000..c02c12aaa5 --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_lab_parse.inc @@ -0,0 +1,456 @@ +#line 76 "encounter_colosseum.h" + +static const char* col_lab_npc_type_name(int type) { + switch (type) { + case COLO_FREMENNIK_BERSERKER: return "berserker"; + case COLO_FREMENNIK_ARCHER: return "archer"; + case COLO_FREMENNIK_SEER: return "seer"; + case COLO_SERPENT_SHAMAN: return "shaman"; + case COLO_JAGUAR_WARRIOR: return "jaguar"; + case COLO_JAVELIN_COLOSSUS: return "javelin"; + case COLO_SHOCKWAVE_COLOSSUS: return "shockwave"; + case COLO_MINOTAUR: return "minotaur"; + case COLO_MANTICORE: return "manticore"; + case COLO_SOL_HEREDIT: return "sol"; + case COLO_HEALING_TOTEM: return "totem"; + case COLO_BEE_SWARM: return "bee"; + default: return "unknown"; + } +} + +static int col_lab_parse_npc_type(const char* value) { + if (strcmp(value, "berserker") == 0) return COLO_FREMENNIK_BERSERKER; + if (strcmp(value, "archer") == 0) return COLO_FREMENNIK_ARCHER; + if (strcmp(value, "seer") == 0) return COLO_FREMENNIK_SEER; + if (strcmp(value, "shaman") == 0 || strcmp(value, "serpent") == 0) + return COLO_SERPENT_SHAMAN; + if (strcmp(value, "jaguar") == 0) return COLO_JAGUAR_WARRIOR; + if (strcmp(value, "javelin") == 0) return COLO_JAVELIN_COLOSSUS; + if (strcmp(value, "shockwave") == 0) return COLO_SHOCKWAVE_COLOSSUS; + if (strcmp(value, "minotaur") == 0) return COLO_MINOTAUR; + if (strcmp(value, "manticore") == 0) return COLO_MANTICORE; + if (strcmp(value, "sol") == 0 || strcmp(value, "heredit") == 0) + return COLO_SOL_HEREDIT; + if (strcmp(value, "totem") == 0) return COLO_HEALING_TOTEM; + if (strcmp(value, "bee") == 0 || strcmp(value, "bees") == 0) + return COLO_BEE_SWARM; + + char* end = NULL; + errno = 0; + long parsed = strtol(value, &end, 10); + if (errno == 0 && end && *end == '\0') { + col_lab_require_type((int)parsed); + return (int)parsed; + } + col_lab_abort("unknown npc type %s", value); + return 0; +} + +static const char* col_lab_modifier_name(int mod) { + switch (mod) { + case COLO_MOD_BEES: return "bees"; + case COLO_MOD_BLASPHEMY: return "blasphemy"; + case COLO_MOD_DOOM: return "doom"; + case COLO_MOD_DYNAMIC_DUO: return "dynamic_duo"; + case COLO_MOD_FRAILTY: return "frailty"; + case COLO_MOD_MANTIMAYHEM: return "mantimayhem"; + case COLO_MOD_MYOPIA: return "myopia"; + case COLO_MOD_REENTRY: return "reentry"; + case COLO_MOD_RED_FLAG: return "red_flag"; + case COLO_MOD_RELENTLESS: return "relentless"; + case COLO_MOD_SOLARFLARE: return "solarflare"; + case COLO_MOD_QUARTET: return "quartet"; + case COLO_MOD_TOTEMIC: return "totemic"; + case COLO_MOD_VOLATILITY: return "volatility"; + default: return "unknown"; + } +} + +static int col_lab_parse_modifier(const char* value) { + for (int m = 0; m < COLO_NUM_REAL_MODIFIERS; m++) + if (strcmp(value, col_lab_modifier_name(m)) == 0) return m; + + char* end = NULL; + errno = 0; + long parsed = strtol(value, &end, 10); + if (errno == 0 && end && *end == '\0' && + parsed >= 0 && parsed < COLO_NUM_REAL_MODIFIERS) { + return (int)parsed; + } + col_lab_abort("unknown modifier %s", value); + return 0; +} + +static const char* col_lab_overhead_name(int prayer) { + switch (prayer) { + case PRAYER_PROTECT_MELEE: return "melee"; + case PRAYER_PROTECT_RANGED: return "ranged"; + case PRAYER_PROTECT_MAGIC: return "magic"; + default: return "none"; + } +} + +static int col_lab_overhead_for_name(const char* value) { + if (strcmp(value, "melee") == 0) return PRAYER_PROTECT_MELEE; + if (strcmp(value, "ranged") == 0) return PRAYER_PROTECT_RANGED; + if (strcmp(value, "magic") == 0) return PRAYER_PROTECT_MAGIC; + if (strcmp(value, "none") == 0 || strcmp(value, "off") == 0) return PRAYER_NONE; + col_lab_abort("unknown prayer %s (want melee|ranged|magic|none)", value); + return PRAYER_NONE; +} + +static ColoLabOptionalInt col_lab_parse_optional_hp_value(const char* value) { + return encounter_lab_parse_optional_full_int("colosseum lab", value); +} + +typedef struct { + int slot; + int type; + int x; + int y; + ColoLabOptionalInt hp; + ColoLabOptionalInt timer; + ColoLabOptionalInt prayer; + int wave; + int phase; + int modifier; + int tier; + int draft[COLO_MODIFIER_DRAFT_OPTIONS]; + int ticks; + uint32_t seed; +} ColoLabParsedArgs; + +static ColoLabParsedArgs col_lab_parsed_args_default(void) { + ColoLabParsedArgs a = { + .slot = -1, + .type = -1, + .x = INT32_MIN, + .y = INT32_MIN, + .hp = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_UNSET }, + .timer = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_UNSET }, + .prayer = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_UNSET }, + .wave = -1, + .phase = -1, + .modifier = -1, + .tier = 1, + .ticks = 1, + .seed = 0, + }; + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) a.draft[o] = -1; + return a; +} + +static void col_lab_apply_script_arg( + ColoLabParsedArgs* args, + ColoLabCommandKind kind, + const char* command, + const char* key, + const char* value +) { + switch (kind) { + case COLO_LAB_COMMAND_RESET: + if (strcmp(key, "seed") == 0) { + args->seed = encounter_lab_parse_seed_value("colosseum lab", value); + return; + } + break; + + case COLO_LAB_COMMAND_SET_PLAYER: + if (strcmp(key, "x") == 0) { + args->x = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + if (strcmp(key, "y") == 0) { + args->y = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + if (strcmp(key, "hp") == 0) { args->hp = col_lab_parse_optional_hp_value(value); return; } + if (strcmp(key, "prayer") == 0) { + args->prayer = encounter_lab_optional_int_set(col_lab_overhead_for_name(value)); + return; + } + break; + + case COLO_LAB_COMMAND_SPAWN_NPC: + if (strcmp(key, "slot") == 0) { + args->slot = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + if (strcmp(key, "type") == 0) { args->type = col_lab_parse_npc_type(value); return; } + if (strcmp(key, "x") == 0) { + args->x = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + if (strcmp(key, "y") == 0) { + args->y = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + if (strcmp(key, "hp") == 0) { args->hp = col_lab_parse_optional_hp_value(value); return; } + if (strcmp(key, "timer") == 0) { + args->timer = encounter_lab_optional_int_set( + encounter_lab_parse_int_value("colosseum lab", value)); + return; + } + break; + + case COLO_LAB_COMMAND_MOVE_NPC: + if (strcmp(key, "slot") == 0) { + args->slot = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + if (strcmp(key, "x") == 0) { + args->x = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + if (strcmp(key, "y") == 0) { + args->y = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + break; + + case COLO_LAB_COMMAND_DELETE_NPC: + case COLO_LAB_COMMAND_KILL_NPC: + if (strcmp(key, "slot") == 0) { + args->slot = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + break; + + case COLO_LAB_COMMAND_SET_NPC_HP: + if (strcmp(key, "slot") == 0) { + args->slot = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + if (strcmp(key, "hp") == 0) { + args->hp = encounter_lab_optional_int_set( + encounter_lab_parse_int_value("colosseum lab", value)); + return; + } + break; + + case COLO_LAB_COMMAND_SET_NPC_TIMER: + if (strcmp(key, "slot") == 0) { + args->slot = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + if (strcmp(key, "timer") == 0) { + args->timer = encounter_lab_optional_int_set( + encounter_lab_parse_int_value("colosseum lab", value)); + return; + } + break; + + case COLO_LAB_COMMAND_SPAWN_WAVE: + if (strcmp(key, "wave") == 0) { + args->wave = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + break; + + case COLO_LAB_COMMAND_SET_BOSS_PHASE: + if (strcmp(key, "phase") == 0) { + args->phase = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + break; + + case COLO_LAB_COMMAND_SET_MODIFIER: + if (strcmp(key, "mod") == 0 || strcmp(key, "modifier") == 0) { + args->modifier = col_lab_parse_modifier(value); + return; + } + if (strcmp(key, "tier") == 0) { + args->tier = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + break; + + case COLO_LAB_COMMAND_OPEN_DRAFT: + if (strcmp(key, "opt0") == 0) { args->draft[0] = col_lab_parse_modifier(value); return; } + if (strcmp(key, "opt1") == 0) { args->draft[1] = col_lab_parse_modifier(value); return; } + if (strcmp(key, "opt2") == 0) { args->draft[2] = col_lab_parse_modifier(value); return; } + break; + + case COLO_LAB_COMMAND_STEP_TICKS: + if (strcmp(key, "ticks") == 0) { + args->ticks = encounter_lab_parse_int_value("colosseum lab", value); + return; + } + break; + + case COLO_LAB_COMMAND_START_GRAPPLE: + case COLO_LAB_COMMAND_CLEAR_NPCS: + case COLO_LAB_COMMAND_NONE: + break; + } + + col_lab_abort("unknown key %s for command %s", key, command); +} + +static ColosseumLabCommand col_lab_build_script_command( + ColoLabCommandKind kind, const ColoLabParsedArgs* args, const char* command +) { + ColosseumLabCommand out; + memset(&out, 0, sizeof(out)); + out.kind = kind; + + switch (kind) { + case COLO_LAB_COMMAND_NONE: + return out; + + case COLO_LAB_COMMAND_RESET: + out.as.reset.seed = args->seed; + return out; + + case COLO_LAB_COMMAND_SET_PLAYER: + if (args->x == INT32_MIN || args->y == INT32_MIN) + col_lab_abort("command %s requires x and y", command); + out.as.player.x = args->x; + out.as.player.y = args->y; + out.as.player.hp = args->hp; + out.as.player.prayer = args->prayer; + return out; + + case COLO_LAB_COMMAND_SPAWN_NPC: + if (args->x == INT32_MIN || args->y == INT32_MIN) + col_lab_abort("command %s requires x and y", command); + if (args->type < 0) + col_lab_abort("npc command requires type"); + out.as.spawn_npc.slot = args->slot; + out.as.spawn_npc.type = (ColoNpcType)args->type; + out.as.spawn_npc.x = args->x; + out.as.spawn_npc.y = args->y; + out.as.spawn_npc.hp = args->hp; + out.as.spawn_npc.timer = args->timer; + return out; + + case COLO_LAB_COMMAND_MOVE_NPC: + if (args->x == INT32_MIN || args->y == INT32_MIN) + col_lab_abort("command %s requires x and y", command); + if (args->slot < 0) col_lab_abort("command %s requires slot", command); + out.as.move_npc.slot = args->slot; + out.as.move_npc.x = args->x; + out.as.move_npc.y = args->y; + return out; + + case COLO_LAB_COMMAND_DELETE_NPC: + case COLO_LAB_COMMAND_KILL_NPC: + if (args->slot < 0) col_lab_abort("command %s requires slot", command); + out.as.npc_slot.slot = args->slot; + return out; + + case COLO_LAB_COMMAND_SET_NPC_HP: + if (args->slot < 0) col_lab_abort("command %s requires slot", command); + if (args->hp.kind != ENCOUNTER_LAB_OPTIONAL_INT_SET) + col_lab_abort("set_npc_hp requires hp"); + out.as.npc_hp.slot = args->slot; + out.as.npc_hp.hp = args->hp.value; + return out; + + case COLO_LAB_COMMAND_SET_NPC_TIMER: + if (args->slot < 0) col_lab_abort("command %s requires slot", command); + if (args->timer.kind != ENCOUNTER_LAB_OPTIONAL_INT_SET) + col_lab_abort("set_npc_timer requires timer"); + out.as.npc_timer.slot = args->slot; + out.as.npc_timer.timer = args->timer.value; + return out; + + case COLO_LAB_COMMAND_SPAWN_WAVE: + if (args->wave < 0) col_lab_abort("wave requires wave=N"); + out.as.wave.wave = args->wave; + return out; + + case COLO_LAB_COMMAND_SET_BOSS_PHASE: + if (args->phase < 0) col_lab_abort("boss_phase requires phase=N"); + out.as.boss_phase.phase = args->phase; + return out; + + case COLO_LAB_COMMAND_START_GRAPPLE: + return out; + + case COLO_LAB_COMMAND_SET_MODIFIER: + if (args->modifier < 0) col_lab_abort("modifier requires mod=NAME"); + out.as.modifier.modifier = args->modifier; + out.as.modifier.tier = args->tier; + return out; + + case COLO_LAB_COMMAND_OPEN_DRAFT: + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + out.as.draft.option[o] = args->draft[o]; + return out; + + case COLO_LAB_COMMAND_CLEAR_NPCS: + return out; + + case COLO_LAB_COMMAND_STEP_TICKS: + out.as.step_ticks.ticks = args->ticks; + return out; + } + + col_lab_abort("unknown command kind %d", kind); + out.kind = COLO_LAB_COMMAND_NONE; + return out; +} + +static char* col_lab_alloc_json(ColosseumState* s); + +static ColoLabCommandKind col_lab_command_kind_for(const char* command) { + static const EncounterLabCommandAlias aliases[] = { + { "reset", COLO_LAB_COMMAND_RESET }, + { "player", COLO_LAB_COMMAND_SET_PLAYER }, + { "set_player", COLO_LAB_COMMAND_SET_PLAYER }, + { "npc", COLO_LAB_COMMAND_SPAWN_NPC }, + { "spawn_npc", COLO_LAB_COMMAND_SPAWN_NPC }, + { "move_npc", COLO_LAB_COMMAND_MOVE_NPC }, + { "delete_npc", COLO_LAB_COMMAND_DELETE_NPC }, + { "kill_npc", COLO_LAB_COMMAND_KILL_NPC }, + { "set_npc_hp", COLO_LAB_COMMAND_SET_NPC_HP }, + { "set_npc_timer", COLO_LAB_COMMAND_SET_NPC_TIMER }, + { "wave", COLO_LAB_COMMAND_SPAWN_WAVE }, + { "spawn_wave", COLO_LAB_COMMAND_SPAWN_WAVE }, + { "boss_phase", COLO_LAB_COMMAND_SET_BOSS_PHASE }, + { "phase", COLO_LAB_COMMAND_SET_BOSS_PHASE }, + { "grapple", COLO_LAB_COMMAND_START_GRAPPLE }, + { "start_grapple", COLO_LAB_COMMAND_START_GRAPPLE }, + { "modifier", COLO_LAB_COMMAND_SET_MODIFIER }, + { "mod", COLO_LAB_COMMAND_SET_MODIFIER }, + { "draft", COLO_LAB_COMMAND_OPEN_DRAFT }, + { "clear_npcs", COLO_LAB_COMMAND_CLEAR_NPCS }, + { "step", COLO_LAB_COMMAND_STEP_TICKS }, + { "step_ticks", COLO_LAB_COMMAND_STEP_TICKS }, + }; + return (ColoLabCommandKind)encounter_lab_lookup_command_kind( + "colosseum lab", command, aliases, sizeof(aliases) / sizeof(aliases[0])); +} + +static ColoLabLineResult col_lab_apply_script_line_alloc_json( + ColosseumState* s, const char* line, char** out_json +) { + if (!out_json) col_lab_abort("json output pointer is required"); + *out_json = NULL; + EncounterLabLine lex = encounter_lab_line_begin("colosseum lab", line); + if (!lex.command) { free(lex.buffer); return COLO_LAB_LINE_NONE; } + + if (strcmp(lex.command, "dump") == 0 || strcmp(lex.command, "dump_json") == 0) { + if (out_json) *out_json = col_lab_alloc_json(s); + free(lex.buffer); + return COLO_LAB_LINE_DUMP; + } + + ColoLabCommandKind kind = col_lab_command_kind_for(lex.command); + ColoLabParsedArgs args = col_lab_parsed_args_default(); + + for (char* token = encounter_lab_next_token("colosseum lab", &lex.cursor); + token != NULL; + token = encounter_lab_next_token("colosseum lab", &lex.cursor)) { + const char* key = NULL; + const char* value = NULL; + encounter_lab_parse_key_value("colosseum lab", token, &key, &value); + col_lab_apply_script_arg(&args, kind, lex.command, key, value); + } + + ColosseumLabCommand cmd = col_lab_build_script_command(kind, &args, lex.command); + col_lab_apply_command(s, &cmd); + free(lex.buffer); + return COLO_LAB_LINE_NONE; +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc new file mode 100644 index 0000000000..c3d8ab1897 --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_mask_render.inc @@ -0,0 +1,343 @@ +#line 76 "encounter_colosseum.h" + +static void col_write_mask_ctx(EncounterState* state, EncounterContext* context, float* mask) { + ColosseumState* s = (ColosseumState*)state; + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + col_refresh_current_obs_slots_ctx(s, ctx); + int offset = 0; + + int frozen = s->modifiers.draft_pending && !s->modifiers.draft_free_movement; + mask[offset++] = 1.0f; + for (int d = 1; d < ENCOUNTER_MOVE_ACTIONS; d++) { + int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[d]; + int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[d]; + mask[offset++] = (!frozen && col_player_walkable(s, nx, ny)) ? 1.0f : 0.0f; + } + + for (int n = 0; n < COLO_OBS_NPCS; n++) + mask[offset++] = (!frozen && col_obs_slot_is_targetable(s, n)) ? 1.0f : 0.0f; + + mask[offset++] = 1.0f; + mask[offset++] = s->player.prayer != PRAYER_NONE ? 1.0f : 0.0f; + mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; + mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; + mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; + + int inventory_heads_enabled = !ctx->config.mask_inventory_heads; + int cell_equip_slot[COLO_INVENTORY_DISPLAY_SLOTS]; + int cell_can_eat[COLO_INVENTORY_DISPLAY_SLOTS]; + int cell_can_drink[COLO_INVENTORY_DISPLAY_SLOTS]; + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { + cell_equip_slot[cell] = -1; + cell_can_eat[cell] = 0; + cell_can_drink[cell] = 0; + OsrsInventoryClickResolution r = + osrs_inventory_cell_click_interpret(&s->inventory_cells[cell], OSRS_CLICK_TICK_FIRST); + if (r.click_action == OSRS_CLICK_EQUIP) { + if (col_can_equip_from_cell(s, cell)) + cell_equip_slot[cell] = osrs_item_gear_slot(s->inventory_cells[cell].item_idx); + } else if (r.click_action == OSRS_CLICK_EAT) { + cell_can_eat[cell] = col_can_eat_consumable_kind(s, r.consumable_kind); + } else if (r.click_action == OSRS_CLICK_DRINK) { + cell_can_drink[cell] = s->inventory_cells[cell].dose > 0 && + s->player.potion_timer == 0 && + col_drink_has_effect(s, r.consumable_kind); + } + } + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + mask[offset++] = 1.0f; + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) + mask[offset++] = (inventory_heads_enabled && cell_equip_slot[cell] == slot) + ? 1.0f : 0.0f; + } + mask[offset++] = 1.0f; + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) + mask[offset++] = (inventory_heads_enabled && cell_can_eat[cell]) ? 1.0f : 0.0f; + mask[offset++] = 1.0f; + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) + mask[offset++] = (inventory_heads_enabled && cell_can_drink[cell]) ? 1.0f : 0.0f; + + mask[offset++] = 1.0f; + int spec_cost = osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON]); + mask[offset++] = (spec_cost > 0 && + (s->player.special_energy >= spec_cost || s->player.spec_armed)) + ? 1.0f : 0.0f; + mask[offset++] = s->player.spec_armed ? 1.0f : 0.0f; + + mask[offset++] = s->modifiers.draft_pending ? 0.0f : 1.0f; + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + mask[offset++] = (s->modifiers.draft_pending && s->modifiers.draft_options[o] >= 0) + ? 1.0f : 0.0f; + + mask[offset++] = 1.0f; + for (int g = 0; g < COLO_NUM_GRAPPLE_SLOTS; g++) + mask[offset++] = s->sol.grapple_active ? 1.0f : 0.0f; + + mask[offset++] = 1.0f; + mask[offset++] = (s->player.offensive_prayer != OFFENSIVE_PRAYER_NONE) ? 1.0f : 0.0f; + mask[offset++] = (s->player.current_prayer > 0 && + s->player.offensive_prayer != OFFENSIVE_PRAYER_PIETY) ? 1.0f : 0.0f; + mask[offset++] = (s->player.current_prayer > 0 && + s->player.offensive_prayer != OFFENSIVE_PRAYER_RIGOUR) ? 1.0f : 0.0f; + mask[offset++] = (s->player.current_prayer > 0 && + s->player.offensive_prayer != OFFENSIVE_PRAYER_AUGURY) ? 1.0f : 0.0f; + + mask[offset++] = 1.0f; + { + int target = col_player_target_slot(s); + mask[offset++] = (!frozen && s->thrall_recast_cd == 0 && + col_npc_is_thrall_target(s, target)) ? 1.0f : 0.0f; + } + mask[offset++] = (!frozen && s->death_charge_cd == 0 && + s->death_charge_window_left == 0) ? 1.0f : 0.0f; + + if (offset != COLO_ACTION_MASK_SIZE) { + fprintf(stderr, "colosseum mask index mismatch: wrote %d, expected %d\n", + offset, COLO_ACTION_MASK_SIZE); + abort(); + } +} + +static float col_get_reward_ctx(EncounterState* state, EncounterContext* context) { + (void)context; + return ((ColosseumState*)state)->reward; +} + +static int col_is_terminal_ctx(EncounterState* state, EncounterContext* context) { + (void)context; + return ((ColosseumState*)state)->episode_over; +} + +static int col_get_entity_count_ctx(EncounterState* state, EncounterContext* context) { + (void)context; + ColosseumState* s = (ColosseumState*)state; + int count = 1; + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (s->npcs[i].active) count++; + return count; +} + +static void* col_get_entity_ctx(EncounterState* state, EncounterContext* context, int index) { + (void)context; + ColosseumState* s = (ColosseumState*)state; + if (index == 0) return &s->player; + return NULL; +} + +static void col_prepare_render_player(const ColosseumState* s, int spec_fired_kind, Player* out) { + *out = s->player; + GearSet gear = col_gear_set_for_attack_style(col_equipped_weapon_attack_style(&s->player)); + out->current_gear = gear; + out->visible_gear = gear; + if (spec_fired_kind > 0 && spec_fired_kind < NUM_ITEMS) { + out->equipped[GEAR_SLOT_WEAPON] = (uint8_t)spec_fired_kind; + out->equipped[GEAR_SLOT_SHIELD] = osrs_suppress_shield_for_two_handed_weapon( + out->equipped[GEAR_SLOT_WEAPON], out->equipped[GEAR_SLOT_SHIELD]); + out->current_gear = GEAR_MELEE; + out->visible_gear = GEAR_MELEE; + } +} + +static int col_sol_render_attack_anim_id(const SolHereditState* sol) { + switch (sol->last_attack_kind) { + case COLO_SOL_ATTACK_SPEAR: return 10883; + case COLO_SOL_ATTACK_SHIELD: return 10885; + case COLO_SOL_ATTACK_GRAPPLE: return 10884; + case COLO_SOL_ATTACK_TRIPLE: return sol->phase >= 3 ? 10886 : 10887; + default: break; + } + return 10883; +} + +static int col_npc_render_attack_anim_id( + const ColosseumState* s, + const ColosseumContext* ctx, + int npc_slot, + const ColoNPC* npc, + int def_id +) { + assert(ctx); + if (npc->type == COLO_SOL_HEREDIT) + return col_sol_render_attack_anim_id(&s->sol); + if (npc->type == COLO_MANTICORE && ctx->manticore_charge_started[npc_slot]) + return 10868; + switch (ctx->npc_render_attack_anim[npc_slot]) { + case COLO_NPC_RENDER_ATTACK_NORMAL: + return col_npc_attack_anim_id(def_id); + case COLO_NPC_RENDER_ATTACK_JAVELIN_SKYFALL: + assert(npc->type == COLO_JAVELIN_COLOSSUS); + return COLO_JAVELIN_SKYFALL_ANIM_ID; + } + abort(); +} + +static void col_thrall_render_tile(const ColosseumState* s, int* out_x, int* out_y) { + static const int RING[16][2] = { + {0, 1}, {1, 0}, {-1, 0}, {0, -1}, + {1, 1}, {1, -1}, {-1, 1}, {-1, -1}, + {0, 2}, {2, 0}, {-2, 0}, {0, -2}, + {1, 2}, {2, 1}, {-1, 2}, {-2, 1} }; + for (int a = 0; a < 16; a++) { + int x = s->player.x + RING[a][0]; + int y = s->player.y + RING[a][1]; + if (!col_npc_blocked((void*)s, x, y, 1)) { + *out_x = x; + *out_y = y; + return; + } + } + *out_x = s->player.x; + *out_y = s->player.y + 1; +} + +static void col_emit_decoration_entity( + RenderEntity* out, int* n, int max_entities, int def_id, int npc_slot, + uint32_t instance_id, int anim_id, int x, int y, const char* debug_name +) { + if (*n >= max_entities) return; + RenderEntity* re = &out[(*n)++]; + OsrsNpcRenderEntitySpec spec = { + .npc_def_id = def_id, + .npc_slot = npc_slot, + .npc_instance_id = instance_id, + .npc_visible = 1, + .npc_size = 1, + .npc_anim_id = anim_id, + .x = x, + .y = y, + .dest_x = x, + .dest_y = y, + .current_hitpoints = 1, + .base_hitpoints = 1, + .attack_style_this_tick = ATTACK_STYLE_NONE, + .hit_landed_this_tick = 0, + .hit_damage = 0, + .hit_was_successful = 0, + .hit_spell_type = ENCOUNTER_SPELL_NONE, + .attack_target_entity_idx = 0, + }; + osrs_render_entity_from_npc_spec(&spec, re); + re->debug_npc_type_name = debug_name; +} + +static void col_fill_render_entities_ctx( + EncounterState* state, + EncounterContext* context, + RenderEntity* out, + int max_entities, + int* count +) { + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumState* s = (ColosseumState*)state; + const EncounterLoadoutStats* ls = col_live_loadout_stats(s); + s->player.gui_max_hit = ls->max_hit; + s->player.gui_attack_speed = ls->attack_speed; + s->player.gui_attack_range = ls->attack_range; + s->player.gui_strength_bonus = ls->strength_bonus; + + int n = 0; + if (n < max_entities) { + Player render_player; + col_prepare_render_player(s, ctx->player_spec_fired_kind, &render_player); + RenderEntity* re = &out[n++]; + osrs_render_entity_from_player_entity(&render_player, re); + if (ctx->player_render_hit_count < 0 || + ctx->player_render_hit_count > ENCOUNTER_RENDER_HITS_MAX) { + fprintf(stderr, "invalid player render hit count\n"); + abort(); + } + re->render_hit_count = ctx->player_render_hit_count; + for (int h = 0; h < ctx->player_render_hit_count; h++) + re->render_hit_damage[h] = ctx->player_render_hit_damage[h]; + } + + for (int i = 0; i < COLO_MAX_NPCS && n < max_entities; i++) { + ColoNPC* npc = &s->npcs[i]; + if (!npc->active) continue; + int def_id = COLO_NPC_DEF_IDS[npc->type]; + if (npc->type == COLO_MINOTAUR && col_mod_active(s, COLO_MOD_RED_FLAG)) + def_id = COLO_NPC_DEF_ID_MINOTAUR_ROUTEFIND; + int npc_anim_id = -1; + if (npc->death_ticks > 0) + npc_anim_id = col_npc_death_anim_id(npc->type); + else if ((npc->type == COLO_MANTICORE && ctx->manticore_charge_started[i]) || + npc->attacked_this_tick) + npc_anim_id = col_npc_render_attack_anim_id(s, ctx, i, npc, def_id); + RenderEntity* re = &out[n++]; + OsrsNpcRenderEntitySpec npc_spec = { + .npc_def_id = def_id, + .npc_slot = i, + .npc_instance_id = npc->render_id, + .npc_visible = npc->active, + .npc_size = npc->size, + .npc_anim_id = npc_anim_id, + .x = npc->x, + .y = npc->y, + .dest_x = npc->target_x, + .dest_y = npc->target_y, + .current_hitpoints = npc->hp, + .base_hitpoints = npc->max_hp, + .attack_style_this_tick = npc->attacked_this_tick + ? npc->attack_style_this_tick : ATTACK_STYLE_NONE, + .hit_landed_this_tick = npc->hit_landed_this_tick, + .hit_damage = npc->hit_damage, + .hit_was_successful = npc->hit_was_successful_this_tick, + .hit_spell_type = ENCOUNTER_SPELL_NONE, + .attack_target_entity_idx = 0, + }; + osrs_render_entity_from_npc_spec(&npc_spec, re); + if (ctx->npc_render_hit_count[i] < 0 || + ctx->npc_render_hit_count[i] > ENCOUNTER_RENDER_HITS_MAX) { + fprintf(stderr, "invalid npc render hit count\n"); + abort(); + } + re->render_hit_count = ctx->npc_render_hit_count[i]; + for (int h = 0; h < ctx->npc_render_hit_count[i]; h++) + re->render_hit_damage[h] = ctx->npc_render_hit_damage[i][h]; + re->is_running = col_type_is_warbander(npc->type) ? 1 : 0; + re->frozen_ticks = npc->frozen_ticks; + re->debug_npc_type_name = colo_npc_type_name(npc->type); + re->debug_attack_timer = npc->attack_timer; + re->debug_attack_style = npc->attack_style; + if (npc->type == COLO_MANTICORE) { + const ColoManticoreState* mc = &npc->type_state.manticore; + re->debug_manticore_state_active = 1; + re->debug_manticore_cycle_step = mc->cycle_step; + for (int orb = 0; orb < 3; orb++) + re->debug_manticore_orb_style[orb] = mc->orb_style[orb]; + } + } + + if (s->thrall_active && n < max_entities) { + int tx, ty; + col_thrall_render_tile(s, &tx, &ty); + col_emit_decoration_entity(out, &n, max_entities, + COLO_NPC_DEF_ID_THRALL, COLO_NPC_SLOT_THRALL, COLO_THRALL_RENDER_INSTANCE_ID, + ctx->thrall_attacked_this_tick ? 11101 : -1, tx, ty, "Arceuus Thrall"); + } + + if (s->solarflare.active) { + for (int p = 0; p < COLO_NUM_PILLARS && n < max_entities; p++) { + int x, y; + col_solarflare_tile(s, p, s->solarflare.step, &x, &y); + col_emit_decoration_entity(out, &n, max_entities, + COLO_NPC_DEF_ID_SOLARFLARE, COLO_NPC_SLOT_SOLARFLARE + p, + COLO_SOLARFLARE_RENDER_INSTANCE_ID + (uint32_t)p, + COLO_SOLARFLARE_IDLE_ANIM_ID, x, y, "Solar flare"); + } + } + + if (s->wave == COLO_WAVE_BOSS && s->sol.started) { + for (int c = 0; c < s->sol.crystal_count && n < max_entities; c++) { + const ColoSolCrystal* cr = &s->sol.crystals[c]; + if (!cr->active) continue; + col_emit_decoration_entity(out, &n, max_entities, + COLO_NPC_DEF_ID_BEAM_CRYSTAL, COLO_NPC_SLOT_BEAM_CRYSTAL + c, + COLO_BEAM_CRYSTAL_RENDER_INSTANCE_ID + (uint32_t)c, + COLO_BEAM_CRYSTAL_IDLE_ANIM_ID, cr->x, cr->y, "Beam crystal"); + } + } + + encounter_resolve_attack_target(out, n, s->interaction.target_slot); + *count = n; +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc new file mode 100644 index 0000000000..68a2b08bee --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_model.inc @@ -0,0 +1,1704 @@ +#line 76 "encounter_colosseum.h" + +#define COLO_ARENA_MIN_X 0 +#define COLO_ARENA_MAX_X 33 +#define COLO_ARENA_MIN_Y 0 +#define COLO_ARENA_MAX_Y 33 +#define COLO_ARENA_WIDTH (COLO_ARENA_MAX_X - COLO_ARENA_MIN_X + 1) +#define COLO_ARENA_HEIGHT (COLO_ARENA_MAX_Y - COLO_ARENA_MIN_Y + 1) + +#define COLO_PLAYER_START_X 7 +#define COLO_PLAYER_START_Y 18 + +#define COLO_MAX_TICKS 9000 +#define COLO_SPEEDRUN_MAX_TICKS 4000 +#define COLO_BEGINNER_MAX_TICKS 6000 +#define COLO_NUM_WAVES 12 +#define COLO_WAVE_BOSS (COLO_NUM_WAVES - 1) + +#define COLO_WAVE_SPAWN_DELAY_TICKS 5 + +#define COLO_BOSS_SPAWN_DELAY_TICKS 11 + +static inline int col_wave_entry_delay_ticks(int target_wave) { + return target_wave == COLO_WAVE_BOSS + ? COLO_BOSS_SPAWN_DELAY_TICKS : COLO_WAVE_SPAWN_DELAY_TICKS; +} +#define COLO_WAVE_READY_TICKS 2 +#define COLO_WAVE_ATTACK_GATE_TICKS 4 +#define COLO_REINFORCEMENT_TICKS 66 + +#define COLO_FARM_CAP_WAVES 4 +#define COLO_NUM_ACTION_HEADS 20 + +#define COLO_MAX_NPCS 24 +#define COLO_OBS_NPCS COLO_MAX_NPCS + +#define COLO_STEP_OUT_FORECAST_HORIZON 4 +#define COLO_STEP_OUT_FORECAST_ACTION_FEATURES 9 +#define COLO_STEP_OUT_FORECAST_OBS_SIZE \ + (ENCOUNTER_MOVE_ACTIONS * COLO_STEP_OUT_FORECAST_ACTION_FEATURES) + +typedef struct { + int melee_count; + int ranged_count; + int magic_count; + int max_hit; +} ColoStepOutForecastTick; + +typedef struct { + int valid; + int land_x; + int land_y; + ColoStepOutForecastTick ticks[COLO_STEP_OUT_FORECAST_HORIZON]; + int same_tick_mixed_style_conflict; + int ranged_magic_offtick_opportunity; + int melee_fallback_exposure; + int solarflare_contact_tick; +} ColoStepOutForecastAction; + +typedef struct { + ColoStepOutForecastAction actions[ENCOUNTER_MOVE_ACTIONS]; +} ColoStepOutForecast; + +typedef enum { + COLO_FORECAST_BEHAVIOR_STANDARD = 0, + COLO_FORECAST_BEHAVIOR_WARBAND, + COLO_FORECAST_BEHAVIOR_MANTICORE, + COLO_FORECAST_BEHAVIOR_SOL, +} ColoForecastBehaviorKind; + +typedef struct { + int active; + ColoNpcType type; + ColoForecastBehaviorKind forecast_behavior; + int x; + int y; + int size; + int hp; + int attack_range; + int attack_speed; + int can_move; + AttackStyle default_style; + int attack_timer; + int stun_timer; + int frozen_ticks; + int death_ticks; + int cycle_step; + AttackStyle orb_style[3]; + AttackStyle fixed_orb_style[3]; + int formation_dir; + int javelin_attack_count; + int sol_attack_delay; + int sol_immobile_ticks; +} ColoForecastNpcLocal; + +typedef struct { uint8_t lo, hi; } ColoWallSpan; +#define COLO_WALL_SPANS_PER_ROW 3 +static const ColoWallSpan COLO_WALL_SPANS[COLO_ARENA_HEIGHT][COLO_WALL_SPANS_PER_ROW] = { + [0] = { {0,13}, {15,19}, {21,34} }, + [1] = { {0,9}, {25,34} }, + [2] = { {0,7}, {27,34} }, + [3] = { {0,5}, {29,34} }, + [4] = { {0,5}, {29,34} }, + [5] = { {0,3}, {31,34} }, + [6] = { {0,3}, {31,34} }, + [7] = { {0,2}, {32,34} }, + [8] = { {0,2}, {32,34} }, + [9] = { {0,1}, {33,34} }, + [10] = { {0,1}, {33,34} }, + [11] = { {0,1}, {32,34} }, + [12] = { {0,1}, {31,34} }, + [13] = { {31,34} }, + [14] = { {31,34} }, + [15] = { {0,1}, {31,34} }, + [16] = { {0,1}, {31,34} }, + [17] = { {0,1}, {31,34} }, + [18] = { {0,1}, {31,34} }, + [19] = { {31,34} }, + [20] = { {31,34} }, + [21] = { {0,1}, {31,34} }, + [22] = { {0,1}, {32,34} }, + [23] = { {0,1}, {33,34} }, + [24] = { {0,1}, {33,34} }, + [25] = { {0,2}, {32,34} }, + [26] = { {0,2}, {32,34} }, + [27] = { {0,3}, {31,34} }, + [28] = { {0,3}, {31,34} }, + [29] = { {0,5}, {29,34} }, + [30] = { {0,6}, {29,34} }, + [31] = { {0,7}, {27,34} }, + [32] = { {0,9}, {25,34} }, + [33] = { {0,13}, {15,19}, {21,34} }, +}; + +#define COLO_NUM_PILLARS 4 +#define COLO_PILLAR_SIZE 3 +static const int COLO_PILLARS[COLO_NUM_PILLARS][2] = { + {8, 8}, {23, 8}, {8, 23}, {23, 23}, +}; + +static uint8_t COLO_STATIC_BLOCKED[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; + +#define COLO_ARENA_TILE_COUNT (COLO_ARENA_WIDTH * COLO_ARENA_HEIGHT) +#define COLO_STATIC_LOS_TABLE_BITS (COLO_ARENA_TILE_COUNT * COLO_ARENA_TILE_COUNT) +#define COLO_STATIC_LOS_TABLE_WORDS ((COLO_STATIC_LOS_TABLE_BITS + 63) / 64) +#define COLO_STATIC_FOOTPRINT_MAX_SIZE 5 +#define COLO_STATIC_FOOTPRINT_PAD (COLO_STATIC_FOOTPRINT_MAX_SIZE - 1) +#define COLO_STATIC_FOOTPRINT_MIN_X (COLO_ARENA_MIN_X - COLO_STATIC_FOOTPRINT_PAD) +#define COLO_STATIC_FOOTPRINT_MIN_Y (COLO_ARENA_MIN_Y - COLO_STATIC_FOOTPRINT_PAD) +#define COLO_STATIC_FOOTPRINT_MAX_X (COLO_ARENA_MAX_X + 1) +#define COLO_STATIC_FOOTPRINT_MAX_Y (COLO_ARENA_MAX_Y + 1) +#define COLO_STATIC_FOOTPRINT_WIDTH \ + (COLO_STATIC_FOOTPRINT_MAX_X - COLO_STATIC_FOOTPRINT_MIN_X + 1) +#define COLO_STATIC_FOOTPRINT_HEIGHT \ + (COLO_STATIC_FOOTPRINT_MAX_Y - COLO_STATIC_FOOTPRINT_MIN_Y + 1) +static uint64_t COLO_STATIC_LOS_BITS[COLO_STATIC_LOS_TABLE_WORDS]; +static uint8_t COLO_STATIC_FOOTPRINT_BLOCKED[COLO_STATIC_FOOTPRINT_MAX_SIZE] + [COLO_STATIC_FOOTPRINT_WIDTH][COLO_STATIC_FOOTPRINT_HEIGHT]; +static int COLO_STATIC_LOOKUP_TABLES_BUILT = 0; + +static void col_build_static_lookup_tables_once(void); + +static void col_build_static_arena(void) { + memset(COLO_STATIC_BLOCKED, 0, sizeof(COLO_STATIC_BLOCKED)); + for (int y = 0; y < COLO_ARENA_HEIGHT; y++) { + for (int sp = 0; sp < COLO_WALL_SPANS_PER_ROW; sp++) { + ColoWallSpan span = COLO_WALL_SPANS[y][sp]; + for (int x = span.lo; x < span.hi; x++) COLO_STATIC_BLOCKED[x][y] = 1; + } + } + for (int p = 0; p < COLO_NUM_PILLARS; p++) + for (int dx = 0; dx < COLO_PILLAR_SIZE; dx++) + for (int dy = 0; dy < COLO_PILLAR_SIZE; dy++) + COLO_STATIC_BLOCKED[COLO_PILLARS[p][0] + dx][COLO_PILLARS[p][1] + dy] = 1; + col_build_static_lookup_tables_once(); +} + +static inline int col_static_blocked(int x, int y) { + if (x < COLO_ARENA_MIN_X || x > COLO_ARENA_MAX_X || + y < COLO_ARENA_MIN_Y || y > COLO_ARENA_MAX_Y) return 1; + return COLO_STATIC_BLOCKED[x][y]; +} + +static inline int col_static_los_endpoint_in_bounds(int x, int y) { + return x >= COLO_ARENA_MIN_X && x <= COLO_ARENA_MAX_X && + y >= COLO_ARENA_MIN_Y && y <= COLO_ARENA_MAX_Y; +} + +static inline uint32_t col_static_los_index(int x0, int y0, int x1, int y1) { + uint32_t a = (uint32_t)((x0 - COLO_ARENA_MIN_X) * COLO_ARENA_HEIGHT + + (y0 - COLO_ARENA_MIN_Y)); + uint32_t b = (uint32_t)((x1 - COLO_ARENA_MIN_X) * COLO_ARENA_HEIGHT + + (y1 - COLO_ARENA_MIN_Y)); + return a * COLO_ARENA_TILE_COUNT + b; +} + +static inline void col_static_los_set(uint32_t idx) { + COLO_STATIC_LOS_BITS[idx >> 6] |= 1ULL << (idx & 63); +} + +static inline int col_static_los_get(int x0, int y0, int x1, int y1) { + uint32_t idx = col_static_los_index(x0, y0, x1, y1); + return (int)((COLO_STATIC_LOS_BITS[idx >> 6] >> (idx & 63)) & 1ULL); +} + +static inline int col_static_footprint_anchor_in_table(int x, int y) { + return x >= COLO_STATIC_FOOTPRINT_MIN_X && x <= COLO_STATIC_FOOTPRINT_MAX_X && + y >= COLO_STATIC_FOOTPRINT_MIN_Y && y <= COLO_STATIC_FOOTPRINT_MAX_Y; +} + +static int col_static_footprint_blocked_reference_slowpath(int x, int y, int size) { + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + if (col_static_blocked(x + dx, y + dy)) return 1; + } + } + return 0; +} + +static inline int col_static_footprint_blocked_lookup(int x, int y, int size) { + if (size < 1 || size > COLO_STATIC_FOOTPRINT_MAX_SIZE) + return col_static_footprint_blocked_reference_slowpath(x, y, size); + if (!COLO_STATIC_LOOKUP_TABLES_BUILT) col_build_static_arena(); + if (!col_static_footprint_anchor_in_table(x, y)) return 1; + return COLO_STATIC_FOOTPRINT_BLOCKED[size - 1] + [x - COLO_STATIC_FOOTPRINT_MIN_X] + [y - COLO_STATIC_FOOTPRINT_MIN_Y]; +} + +static int col_los_tile_blocked_reference(void* ctx, int x, int y) { + (void)ctx; + return col_static_blocked(x, y); +} + +static int col_tiles_have_los_reference_slowpath(int x0, int y0, int x1, int y1) { + OsrsLosQuery query = osrs_los_tile(col_los_tile_blocked_reference, NULL); + return osrs_los_tile_ray_clear(&query, x0, y0, x1, y1); +} + +static void col_build_static_lookup_tables_once(void) { + if (COLO_STATIC_LOOKUP_TABLES_BUILT) return; + for (int x0 = COLO_ARENA_MIN_X; x0 <= COLO_ARENA_MAX_X; x0++) { + for (int y0 = COLO_ARENA_MIN_Y; y0 <= COLO_ARENA_MAX_Y; y0++) { + for (int x1 = COLO_ARENA_MIN_X; x1 <= COLO_ARENA_MAX_X; x1++) { + for (int y1 = COLO_ARENA_MIN_Y; y1 <= COLO_ARENA_MAX_Y; y1++) { + if (col_tiles_have_los_reference_slowpath(x0, y0, x1, y1)) + col_static_los_set(col_static_los_index(x0, y0, x1, y1)); + } + } + } + } + for (int size = 1; size <= COLO_STATIC_FOOTPRINT_MAX_SIZE; size++) { + for (int x = COLO_STATIC_FOOTPRINT_MIN_X; x <= COLO_STATIC_FOOTPRINT_MAX_X; x++) { + for (int y = COLO_STATIC_FOOTPRINT_MIN_Y; y <= COLO_STATIC_FOOTPRINT_MAX_Y; y++) { + COLO_STATIC_FOOTPRINT_BLOCKED[size - 1] + [x - COLO_STATIC_FOOTPRINT_MIN_X] + [y - COLO_STATIC_FOOTPRINT_MIN_Y] = + (uint8_t)col_static_footprint_blocked_reference_slowpath(x, y, size); + } + } + } + COLO_STATIC_LOOKUP_TABLES_BUILT = 1; +} + +static void col_static_los_table_selftest(void) { + col_build_static_arena(); + for (int x0 = COLO_ARENA_MIN_X; x0 <= COLO_ARENA_MAX_X; x0++) { + for (int y0 = COLO_ARENA_MIN_Y; y0 <= COLO_ARENA_MAX_Y; y0++) { + for (int x1 = COLO_ARENA_MIN_X; x1 <= COLO_ARENA_MAX_X; x1++) { + for (int y1 = COLO_ARENA_MIN_Y; y1 <= COLO_ARENA_MAX_Y; y1++) { + int table = col_static_los_get(x0, y0, x1, y1); + int slow = col_tiles_have_los_reference_slowpath(x0, y0, x1, y1); + if (table != slow) { + fprintf(stderr, + "colosseum LoS table mismatch (%d,%d)->(%d,%d): table=%d slow=%d\n", + x0, y0, x1, y1, table, slow); + abort(); + } + } + } + } + } + printf("colosseum LoS table selftest PASS: %u pairs\n", + (unsigned)COLO_STATIC_LOS_TABLE_BITS); +} + +static void col_static_footprint_table_selftest(void) { + col_build_static_arena(); + int checks = 0; + int min_x = COLO_STATIC_FOOTPRINT_MIN_X - COLO_STATIC_FOOTPRINT_MAX_SIZE; + int min_y = COLO_STATIC_FOOTPRINT_MIN_Y - COLO_STATIC_FOOTPRINT_MAX_SIZE; + int max_x = COLO_STATIC_FOOTPRINT_MAX_X + COLO_STATIC_FOOTPRINT_MAX_SIZE; + int max_y = COLO_STATIC_FOOTPRINT_MAX_Y + COLO_STATIC_FOOTPRINT_MAX_SIZE; + for (int size = 1; size <= COLO_STATIC_FOOTPRINT_MAX_SIZE; size++) { + for (int x = min_x; x <= max_x; x++) { + for (int y = min_y; y <= max_y; y++) { + int table = col_static_footprint_blocked_lookup(x, y, size); + int slow = col_static_footprint_blocked_reference_slowpath(x, y, size); + if (table != slow) { + fprintf(stderr, + "colosseum footprint table mismatch (%d,%d) size=%d: table=%d slow=%d\n", + x, y, size, table, slow); + abort(); + } + checks++; + } + } + } + printf("colosseum footprint table selftest PASS: %d checks\n", checks); +} + +#define COLO_NUM_SPAWN_ANCHORS 13 +#define COLO_SPAWN_ZONE_SIZE 3 +static const int COLO_SPAWN_ANCHORS[COLO_NUM_SPAWN_ANCHORS][2] = { + {3, 14}, {9, 16}, {3, 19}, {13, 19}, {16, 20}, {19, 19}, {17, 24}, + {13, 13}, {19, 13}, {16, 9}, {24, 17}, {28, 19}, {28, 14}, +}; + +#define COLO_SPAWN_EXCLUSION_CHEB 4 + +#define COLO_WARBAND_BOX_MIN_X 12 +#define COLO_WARBAND_BOX_MIN_Y 15 +#define COLO_WARBAND_BOX_MAX_X 18 +#define COLO_WARBAND_BOX_MAX_Y 21 + +#define COLO_WARBAND_CYCLE_TICKS 6 + +#define COLO_WARBAND_TILES_PER_TICK 2 + +typedef enum { + COLO_WARBAND_FORM_NORTH = 0, + COLO_WARBAND_FORM_EAST, + COLO_WARBAND_FORM_WEST, + COLO_WARBAND_FORM_SOUTH, +} ColoWarbandFormation; + +static const int COLO_WARBAND_FORM_OFFSET[4][2] = { + [COLO_WARBAND_FORM_NORTH] = { 0, 1 }, + [COLO_WARBAND_FORM_EAST] = { 1, 0 }, + [COLO_WARBAND_FORM_WEST] = { -1, 0 }, + [COLO_WARBAND_FORM_SOUTH] = { 0, -1 }, +}; + +#define COLO_GATE_MIN_X 15 +#define COLO_GATE_MAX_X 18 +#define COLO_GATE_LINE_NORTH_MIN_Y 16 +#define COLO_GATE_NORTH_SPAWN_ROW 32 +#define COLO_GATE_SOUTH_SPAWN_ROW 1 + +#define COLO_SOL_SPAWN_X 16 +#define COLO_SOL_SPAWN_Y 19 + +#define COLO_BOSS_ARENA_MIN_X 9 +#define COLO_BOSS_ARENA_MIN_Y 9 +#define COLO_BOSS_ARENA_MAX_X 24 +#define COLO_BOSS_ARENA_MAX_Y 24 +#define COLO_BOSS_ARENA_SIZE (COLO_BOSS_ARENA_MAX_X - COLO_BOSS_ARENA_MIN_X + 1) + +#define COLO_BOSS_PLAYER_START_X 16 +#define COLO_BOSS_PLAYER_START_Y 10 + +static const int COLO_NPC_DEF_IDS[COLO_NUM_NPC_TYPES] = { + [COLO_FREMENNIK_BERSERKER] = 12816, + [COLO_FREMENNIK_ARCHER] = 12814, + [COLO_FREMENNIK_SEER] = 12815, + [COLO_SERPENT_SHAMAN] = 12811, + [COLO_JAGUAR_WARRIOR] = 12810, + [COLO_JAVELIN_COLOSSUS] = 12817, + [COLO_SHOCKWAVE_COLOSSUS] = 12819, + [COLO_MINOTAUR] = 12812, + [COLO_MANTICORE] = 12818, + [COLO_SOL_HEREDIT] = 12821, + [COLO_HEALING_TOTEM] = 12825, + [COLO_BEE_SWARM] = 12823, +}; + +#define COLO_NPC_DEF_ID_MINOTAUR_ROUTEFIND 12813 + +#define COLO_NPC_DEF_ID_THRALL 10880 + +#define COLO_NPC_SLOT_THRALL COLO_MAX_NPCS +#define COLO_THRALL_RENDER_INSTANCE_ID 0xC01055EEu + +#define COLO_NPC_DEF_ID_SOLARFLARE 12826 +#define COLO_SOLARFLARE_IDLE_ANIM_ID 10817 +#define COLO_NPC_SLOT_SOLARFLARE (COLO_MAX_NPCS + 1) +#define COLO_SOLARFLARE_RENDER_INSTANCE_ID 0xC0105F1Au + +#define COLO_NPC_DEF_ID_BEAM_CRYSTAL 12824 +#define COLO_BEAM_CRYSTAL_IDLE_ANIM_ID 10799 +#define COLO_NPC_SLOT_BEAM_CRYSTAL (COLO_MAX_NPCS + 1 + COLO_NUM_PILLARS) +#define COLO_BEAM_CRYSTAL_RENDER_INSTANCE_ID 0xC0105EC5u + +static int col_type_is_hazard_entity(ColoNpcType type) { + return type == COLO_HEALING_TOTEM || type == COLO_BEE_SWARM; +} + +static const char* colo_npc_type_name(int type) { + switch (type) { + case COLO_FREMENNIK_BERSERKER: return "Fremennik Berserker"; + case COLO_FREMENNIK_ARCHER: return "Fremennik Archer"; + case COLO_FREMENNIK_SEER: return "Fremennik Seer"; + case COLO_SERPENT_SHAMAN: return "Serpent Shaman"; + case COLO_JAGUAR_WARRIOR: return "Jaguar Warrior"; + case COLO_JAVELIN_COLOSSUS: return "Javelin Colossus"; + case COLO_SHOCKWAVE_COLOSSUS: return "Shockwave Colossus"; + case COLO_MINOTAUR: return "Minotaur"; + case COLO_MANTICORE: return "Manticore"; + case COLO_SOL_HEREDIT: return "Sol Heredit"; + case COLO_HEALING_TOTEM: return "Healing totem"; + case COLO_BEE_SWARM: return "Bee swarm"; + default: return "unknown"; + } +} + +typedef struct { + int hp; + int attack_speed; + int attack_range; + int size; + AttackStyle default_style; + MeleeStyle melee_style; + int can_melee; + + int att_level, str_level, def_level, range_level, magic_level; + int melee_att_bonus, range_att_bonus, magic_att_bonus; + int melee_str_bonus, ranged_str_bonus; + int stab_def, slash_def, crush_def, magic_def_bonus, ranged_def_bonus; + + int max_hit; + AttackStyle player_style_that_max_hits; + int can_move; +} ColoNpcStats; + +typedef struct { + int attack_range; + AttackStyle default_style; + MeleeStyle melee_style; + int can_melee; + AttackStyle player_style_that_max_hits; + int can_move; +} ColoNpcOverlay; + +static const MonsterStats COLO_NPC_BASE[COLO_NUM_NPC_TYPES] = { + [COLO_FREMENNIK_BERSERKER] = { .npc_id=12816, .name="Fremennik berserker", + .hp=48, .att_level=110,.str_level=110,.def_level=80,.magic_level=110,.range_level=110, + .attack_speed=6,.size=1,.max_hit=29, + .melee_att_bonus=150,.melee_str_bonus=90,.magic_att_bonus=0,.magic_str_bonus=0,.range_att_bonus=0,.ranged_str_bonus=0, + .stab_def=50,.slash_def=50,.crush_def=50,.magic_def=0,.ranged_def=75 }, + [COLO_FREMENNIK_ARCHER] = { .npc_id=12814, .name="Fremennik archer", + .hp=50, .att_level=110,.str_level=110,.def_level=80,.magic_level=110,.range_level=110, + .attack_speed=6,.size=1,.max_hit=14, + .melee_att_bonus=150,.melee_str_bonus=0,.magic_att_bonus=0,.magic_str_bonus=0,.range_att_bonus=150,.ranged_str_bonus=10, + .stab_def=0,.slash_def=0,.crush_def=0,.magic_def=75,.ranged_def=50 }, + [COLO_FREMENNIK_SEER] = { .npc_id=12815, .name="Fremennik seer", + .hp=50, .att_level=110,.str_level=110,.def_level=80,.magic_level=110,.range_level=110, + .attack_speed=6,.size=1,.max_hit=12, + .melee_att_bonus=0,.melee_str_bonus=0,.magic_att_bonus=150,.magic_str_bonus=0,.range_att_bonus=0,.ranged_str_bonus=0, + .stab_def=50,.slash_def=50,.crush_def=50,.magic_def=30,.ranged_def=0 }, + [COLO_SERPENT_SHAMAN] = { .npc_id=12811, .name="Serpent shaman", + .hp=125, .att_level=100,.str_level=90,.def_level=90,.magic_level=220,.range_level=160, + .attack_speed=5,.size=1,.max_hit=28, + .melee_att_bonus=0,.melee_str_bonus=0,.magic_att_bonus=50,.magic_str_bonus=15,.range_att_bonus=0,.ranged_str_bonus=0, + .stab_def=30,.slash_def=30,.crush_def=30,.magic_def=15,.ranged_def=50 }, + [COLO_JAGUAR_WARRIOR] = { .npc_id=12810, .name="Jaguar warrior", + .hp=125, .att_level=200,.str_level=330,.def_level=125,.magic_level=100,.range_level=160, + .attack_speed=5,.size=2,.max_hit=47, + .melee_att_bonus=0,.melee_str_bonus=25,.magic_att_bonus=0,.magic_str_bonus=0,.range_att_bonus=0,.ranged_str_bonus=0, + .stab_def=30,.slash_def=30,.crush_def=30,.magic_def=15,.ranged_def=45 }, + [COLO_JAVELIN_COLOSSUS] = { .npc_id=12817, .name="Javelin Colossus", + .hp=225, .att_level=200,.str_level=300,.def_level=190,.magic_level=225,.range_level=360, + .attack_speed=5,.size=3,.max_hit=48, + .melee_att_bonus=0,.melee_str_bonus=0,.magic_att_bonus=0,.magic_str_bonus=0,.range_att_bonus=25,.ranged_str_bonus=20, + .stab_def=15,.slash_def=15,.crush_def=15,.magic_def=20,.ranged_def=30 }, + [COLO_SHOCKWAVE_COLOSSUS] = { .npc_id=12819, .name="Shockwave Colossus", + .hp=125, .att_level=120,.str_level=190,.def_level=150,.magic_level=350,.range_level=220, + .attack_speed=5,.size=3,.max_hit=56, + .melee_att_bonus=0,.melee_str_bonus=0,.magic_att_bonus=55,.magic_str_bonus=35,.range_att_bonus=0,.ranged_str_bonus=0, + .stab_def=15,.slash_def=15,.crush_def=15,.magic_def=5,.ranged_def=35 }, + [COLO_MINOTAUR] = { .npc_id=12812, .name="Minotaur", + .hp=225, .att_level=300,.str_level=360,.def_level=190,.magic_level=250,.range_level=120, + .attack_speed=5,.size=3,.max_hit=74, + .melee_att_bonus=15,.melee_str_bonus=64,.magic_att_bonus=0,.magic_str_bonus=0,.range_att_bonus=0,.ranged_str_bonus=0, + .stab_def=0,.slash_def=0,.crush_def=0,.magic_def=0,.ranged_def=12 }, + [COLO_MANTICORE] = { .npc_id=12818, .name="Manticore", + .hp=250, .att_level=300,.str_level=300,.def_level=250,.magic_level=300,.range_level=350, + .attack_speed=10,.size=3,.max_hit=36, + .melee_att_bonus=0,.melee_str_bonus=0,.magic_att_bonus=0,.magic_str_bonus=0,.range_att_bonus=0,.ranged_str_bonus=0, + .stab_def=0,.slash_def=0,.crush_def=0,.magic_def=10,.ranged_def=25 }, + [COLO_SOL_HEREDIT] = { .npc_id=12821, .name="Sol Heredit", + .hp=1500, .att_level=350,.str_level=400,.def_level=200,.magic_level=300,.range_level=350, + .attack_speed=5,.size=5,.max_hit=44, + .melee_att_bonus=250,.melee_str_bonus=5,.magic_att_bonus=0,.magic_str_bonus=0,.range_att_bonus=150,.ranged_str_bonus=5, + .stab_def=65,.slash_def=5,.crush_def=30,.magic_def=750,.ranged_def=825 }, + + [COLO_HEALING_TOTEM] = { .npc_id=12825, .name="Healing totem", + .hp=1, .att_level=1,.str_level=1,.def_level=1,.magic_level=1,.range_level=1, + .attack_speed=0,.size=1,.max_hit=0 }, + [COLO_BEE_SWARM] = { .npc_id=12823, .name="Bee swarm", + .hp=1, .att_level=1,.str_level=1,.def_level=1,.magic_level=1,.range_level=1, + .attack_speed=0,.size=2,.max_hit=0 }, +}; + +#define COLO_MANTICORE_MAX_HIT_RANGED 36 +#define COLO_MANTICORE_MAX_HIT_MAGIC 31 +#define COLO_MANTICORE_MAX_HIT_MELEE 31 + +static const ColoNpcOverlay COLO_NPC_OVERLAY[COLO_NUM_NPC_TYPES] = { + [COLO_FREMENNIK_BERSERKER] = { .attack_range=1, .default_style=ATTACK_STYLE_MELEE, .melee_style=MELEE_STYLE_STAB, .can_melee=1, .player_style_that_max_hits=ATTACK_STYLE_MAGIC, .can_move=1 }, + [COLO_FREMENNIK_ARCHER] = { .attack_range=1, .default_style=ATTACK_STYLE_RANGED, .melee_style=MELEE_STYLE_STAB, .can_melee=0, .player_style_that_max_hits=ATTACK_STYLE_MELEE, .can_move=1 }, + [COLO_FREMENNIK_SEER] = { .attack_range=1, .default_style=ATTACK_STYLE_MAGIC, .melee_style=MELEE_STYLE_STAB, .can_melee=0, .player_style_that_max_hits=ATTACK_STYLE_RANGED, .can_move=1 }, + [COLO_SERPENT_SHAMAN] = { .attack_range=10, .default_style=ATTACK_STYLE_MAGIC, .melee_style=MELEE_STYLE_STAB, .can_melee=0, .player_style_that_max_hits=ATTACK_STYLE_NONE, .can_move=1 }, + [COLO_JAGUAR_WARRIOR] = { .attack_range=1, .default_style=ATTACK_STYLE_MELEE, .melee_style=MELEE_STYLE_SLASH, .can_melee=1, .player_style_that_max_hits=ATTACK_STYLE_NONE, .can_move=1 }, + [COLO_JAVELIN_COLOSSUS] = { .attack_range=15, .default_style=ATTACK_STYLE_RANGED, .melee_style=MELEE_STYLE_STAB, .can_melee=0, .player_style_that_max_hits=ATTACK_STYLE_NONE, .can_move=1 }, + [COLO_SHOCKWAVE_COLOSSUS] = { .attack_range=15, .default_style=ATTACK_STYLE_MAGIC, .melee_style=MELEE_STYLE_STAB, .can_melee=0, .player_style_that_max_hits=ATTACK_STYLE_NONE, .can_move=1 }, + [COLO_MINOTAUR] = { .attack_range=1, .default_style=ATTACK_STYLE_MELEE, .melee_style=MELEE_STYLE_CRUSH, .can_melee=1, .player_style_that_max_hits=ATTACK_STYLE_NONE, .can_move=1 }, + [COLO_MANTICORE] = { .attack_range=15, .default_style=ATTACK_STYLE_RANGED, .melee_style=MELEE_STYLE_STAB, .can_melee=1, .player_style_that_max_hits=ATTACK_STYLE_NONE, .can_move=1 }, + [COLO_SOL_HEREDIT] = { .attack_range=1, .default_style=ATTACK_STYLE_MELEE, .melee_style=MELEE_STYLE_SLASH, .can_melee=1, .player_style_that_max_hits=ATTACK_STYLE_NONE, .can_move=1 }, + [COLO_HEALING_TOTEM] = { .attack_range=0, .default_style=ATTACK_STYLE_NONE, .melee_style=MELEE_STYLE_STAB, .can_melee=0, .player_style_that_max_hits=ATTACK_STYLE_NONE, .can_move=0 }, + [COLO_BEE_SWARM] = { .attack_range=0, .default_style=ATTACK_STYLE_NONE, .melee_style=MELEE_STYLE_STAB, .can_melee=0, .player_style_that_max_hits=ATTACK_STYLE_NONE, .can_move=0 }, +}; + +static ColoNpcStats COLO_NPC_STATS[COLO_NUM_NPC_TYPES]; + +static void col_build_npc_stats(void) { + col_build_static_arena(); + for (int i = 0; i < COLO_NUM_NPC_TYPES; i++) { + const MonsterStats* m = &COLO_NPC_BASE[i]; + const ColoNpcOverlay* o = &COLO_NPC_OVERLAY[i]; + ColoNpcStats* s = &COLO_NPC_STATS[i]; + + s->hp = m->hp; + s->attack_speed = m->attack_speed; + s->size = m->size; + s->att_level = m->att_level; + s->str_level = m->str_level; + s->def_level = m->def_level; + s->range_level = m->range_level; + s->magic_level = m->magic_level; + s->melee_att_bonus = m->melee_att_bonus; + s->range_att_bonus = m->range_att_bonus; + s->magic_att_bonus = m->magic_att_bonus; + s->melee_str_bonus = m->melee_str_bonus; + s->ranged_str_bonus = m->ranged_str_bonus; + s->stab_def = m->stab_def; + s->slash_def = m->slash_def; + s->crush_def = m->crush_def; + s->magic_def_bonus = m->magic_def; + s->ranged_def_bonus = m->ranged_def; + s->max_hit = m->max_hit; + + s->attack_range = o->attack_range; + s->default_style = o->default_style; + s->melee_style = o->melee_style; + s->can_melee = o->can_melee; + s->player_style_that_max_hits = o->player_style_that_max_hits; + s->can_move = o->can_move; + } +} + +#define COLO_MAX_NPCS_PER_WAVE 10 +#define COLO_MAX_REINFORCE_PER_WAVE 2 + +typedef struct { + uint8_t types[COLO_MAX_NPCS_PER_WAVE]; + int count; + uint8_t reinforce[COLO_MAX_REINFORCE_PER_WAVE]; + int reinforce_count; +} ColoWaveDef; + +static const ColoWaveDef COLO_WAVES[COLO_NUM_WAVES] = { + #define BZ COLO_FREMENNIK_BERSERKER + #define AR COLO_FREMENNIK_ARCHER + #define SE COLO_FREMENNIK_SEER + #define SH COLO_SERPENT_SHAMAN + #define JG COLO_JAGUAR_WARRIOR + #define JV COLO_JAVELIN_COLOSSUS + #define SW COLO_SHOCKWAVE_COLOSSUS + #define MT COLO_MINOTAUR + #define MC COLO_MANTICORE + #define SOL COLO_SOL_HEREDIT + #define W(start, reinf) { \ + .types = { COLO_WAVE_UNPACK start }, \ + .count = sizeof((uint8_t[]){ COLO_WAVE_UNPACK start }), \ + .reinforce = { COLO_WAVE_UNPACK reinf }, \ + .reinforce_count = sizeof((uint8_t[]){ COLO_WAVE_UNPACK reinf }) } + #define COLO_WAVE_UNPACK(...) __VA_ARGS__ + + [0] = W((BZ, AR, SE, SH), (JG)), + [1] = W((BZ, AR, SE, SH, JV), (JG)), + [2] = W((BZ, AR, SE, SH, JV, JV), (JG)), + [3] = W((BZ, AR, SE, SH, MC), (JG, SH)), + [4] = W((BZ, AR, SE, SH, JV, MC), (JG, SH)), + [5] = W((BZ, AR, SE, SH, JV, JV, MC), (JG, SH)), + [6] = W((BZ, AR, SE, JV, MC, SW), (MT)), + [7] = W((BZ, AR, SE, JV, JV, MC, SW), (MT)), + [8] = W((BZ, AR, SE, JV, MC, MC), (MT)), + [9] = W((BZ, AR, SE, JV, JV, MC, MC), (MT, SH)), + [10] = W((BZ, AR, SE, JV, MC, MC, SW), (MT, SH)), + [11] = W((SOL), ()), + + #undef BZ + #undef AR + #undef SE + #undef SH + #undef JG + #undef JV + #undef SW + #undef MT + #undef MC + #undef SOL + #undef W + #undef COLO_WAVE_UNPACK +}; + +#define COLO_NUM_MODIFIERS 16 +#define COLO_MODIFIER_DRAFT_OPTIONS 3 + +#define COLO_RFDD_LAST_DRAFT_WAVE 6 + +#define COLO_DRAFT_UPGRADE_WEIGHT 2 + +static const uint8_t COLO_MODIFIER_MAX_TIER[COLO_NUM_REAL_MODIFIERS] = { + [COLO_MOD_BEES] = 3, + [COLO_MOD_BLASPHEMY] = 3, + [COLO_MOD_DOOM] = 3, + [COLO_MOD_DYNAMIC_DUO] = 1, + [COLO_MOD_FRAILTY] = 3, + [COLO_MOD_MANTIMAYHEM] = 3, + [COLO_MOD_MYOPIA] = 3, + [COLO_MOD_REENTRY] = 3, + [COLO_MOD_RED_FLAG] = 1, + [COLO_MOD_RELENTLESS] = 3, + [COLO_MOD_SOLARFLARE] = 3, + [COLO_MOD_QUARTET] = 1, + [COLO_MOD_TOTEMIC] = 1, + [COLO_MOD_VOLATILITY] = 3, +}; + +static const uint8_t COLO_MODIFIER_PRE_BOSS_ONLY[COLO_NUM_REAL_MODIFIERS] = { + 0, 0, 0, 1, + 0, 1, 0, 1, + 1, 0, 0, 0, + 0, 0, +}; + +typedef struct { + uint32_t active_mask; + uint8_t tier[COLO_NUM_MODIFIERS]; + int draft_options[COLO_MODIFIER_DRAFT_OPTIONS]; + int draft_pending; + int draft_gates_spawn; + int draft_free_movement; + +} ColoModifierState; + +typedef enum { + COLO_HAZARD_NONE = 0, + COLO_HAZARD_ALIVE, + COLO_HAZARD_RESPAWNING, +} ColoHazardPhase; + +#define COLO_MAX_BEE_SWARMS 3 +#define COLO_BEE_MOVE_INTERVAL 12 + +#define COLO_BEE_MIN_DAMAGE 15 +#define COLO_BEE_MAX_DAMAGE 20 +#define COLO_BEE_RESPAWN_TICKS 50 + +typedef struct { + ColoHazardPhase phase; + int npc_slot; + int respawn_timer; + int move_timer; +} ColoBeeSwarm; + +#define COLO_MAX_TOTEMS COLO_MAX_NPCS +#define COLO_TOTEM_HEAL_PCT 30 +#define COLO_TOTEM_HEAL_INTERVAL 7 +#define COLO_TOTEM_RESPAWN_TICKS 200 +#define COLO_TOTEM_SOL_HEAL 75 + +typedef struct { + ColoHazardPhase phase; + int npc_slot; + int respawn_timer; + int heal_timer; +} ColoTotem; + +#define COLO_SOLARFLARE_RING_STEPS 16 +#define COLO_SOLARFLARE_MOVE_INTERVAL 2 +#define COLO_SOLARFLARE_MOVE_INTERVAL_T3 1 +#define COLO_SOLARFLARE_CORNER_PAUSE 7 +#define COLO_SOLARFLARE_CORNER_PAUSE_T3 2 +#define COLO_SOLARFLARE_MAX_HIT_T1 12 +#define COLO_SOLARFLARE_MAX_HIT_T2 18 +#define COLO_SOLARFLARE_MAX_HIT_T3 24 + +typedef struct { + int active; + int step; + int move_timer; + int pause_timer; +} ColoSolarflareOrb; + +static const int COLO_DOOM_CAP[4] = { 0, 15, 10, 5 }; + +#define COLO_VOLATILITY_MAX_HIT 25 + +typedef enum { + COLO_POOL_VOLATILITY = 0, + COLO_POOL_REENTRY, +} ColoMoltenPoolKind; + +typedef enum { + COLO_POOL_TEMPORARY = 0, + COLO_POOL_PERMANENT, +} ColoMoltenPoolLifetime; + +static const int COLO_RELENTLESS_DEF_BYPASS_PCT[4] = { 0, 33, 66, 100 }; +static const int COLO_RELENTLESS_MAX_HIT_BONUS[4] = { 0, 1, 3, 6 }; + +static const int COLO_BLASPHEMY_DRAIN_PCT[4] = { 0, 20, 40, 60 }; + +static const int COLO_FRAILTY_HP_CUT_PCT[4] = { 0, 10, 20, 40 }; + +static const int COLO_MYOPIA_RANGE_CUT[4] = { 0, 2, 4, 6 }; + +#define COLO_VENOM_START 6 +#define COLO_VENOM_STEP 2 +#define COLO_VENOM_CAP 20 +#define COLO_VENOM_INTERVAL 30 + +#define COLO_POISON_BEE_CONTACT_SEVERITY 5 +#define COLO_POISON_INTERVAL 30 + +typedef enum { + COLO_SOL_AOE_NONE = 0, + COLO_SOL_AOE_SPEAR1, + COLO_SOL_AOE_SPEAR2, + COLO_SOL_AOE_SHIELD1, + COLO_SOL_AOE_SHIELD2, +} ColoSolAoeAttack; + +typedef enum { + COLO_SOL_ATTACK_NONE = 0, + COLO_SOL_ATTACK_SPEAR, + COLO_SOL_ATTACK_SHIELD, + COLO_SOL_ATTACK_TRIPLE, + COLO_SOL_ATTACK_GRAPPLE, +} ColoSolAttackKind; + +typedef enum { + COLO_GRAPPLE_SLOT_BODY = 0, + COLO_GRAPPLE_SLOT_BACK, + COLO_GRAPPLE_SLOT_HANDS, + COLO_GRAPPLE_SLOT_LEGS, + COLO_GRAPPLE_SLOT_FEET, + COLO_NUM_GRAPPLE_SLOTS +} ColoGrappleSlot; + +typedef enum { + COLO_GRAPPLE_OUTCOME_NONE = 0, + COLO_GRAPPLE_OUTCOME_DEFENDED, + COLO_GRAPPLE_OUTCOME_PERFECT, + COLO_GRAPPLE_OUTCOME_FAILED, +} ColoGrappleOutcome; + +#define COLO_SOL_HAZARD_TILES_MAX 32 +#define COLO_SOL_MAX_CRYSTALS 4 +#define COLO_SOL_BEAM_MAX 32 +#define COLO_SOL_PARRY_HITS 3 +#define COLO_SOL_PRAYER_HISTORY 8 +#define COLO_SOL_NUM_PHASES 6 +#define COLO_SOL_HP_MAX 1500 + +typedef enum { + COLO_SOL_EDGE_NORTH, + COLO_SOL_EDGE_EAST, + COLO_SOL_EDGE_SOUTH, + COLO_SOL_EDGE_WEST, +} ColoSolCrystalEdge; + +typedef struct { + int active; + int edge; + int x, y; + int dir; + int move_timer; + int firing_freeze; +} ColoSolCrystal; + +typedef struct { + int active; + int x, y; + int ticks_to_pool; +} ColoSolBeam; + +typedef struct { + int started; + int phase; + int boss_idx; + + int attack_delay; + int immobile_ticks; + int special_cooldown; + int force_spear; + int last_attack_kind; + int last_variant; + + int aoe_attack; + int aoe_age; + int aoe_x, aoe_y; + int aoe_dir_x, aoe_dir_y; + + int parry_land_in[COLO_SOL_PARRY_HITS]; + int parry_damage[COLO_SOL_PARRY_HITS]; + int parry_hits_left; + uint8_t overhead_history[COLO_SOL_PRAYER_HISTORY]; + + int grapple_active; + int grapple_body_slot; + int grapple_timer; + int grapple_resolved; + int grapple_outcome; + int grapple_outcome_tick; + int next_attack_guaranteed_max; + int guaranteed_max_ticks; + + ColoSolCrystal crystals[COLO_SOL_MAX_CRYSTALS]; + int crystal_count; + int laser_cooldown; + + ColoSolBeam beams[COLO_SOL_BEAM_MAX]; + int hazard_tile_x[COLO_SOL_HAZARD_TILES_MAX]; + int hazard_tile_y[COLO_SOL_HAZARD_TILES_MAX]; + int hazard_tile_count; + int enrage_pool_timer; + + int boss_arena_min_x, boss_arena_min_y; + int boss_arena_max_x, boss_arena_max_y; +} SolHereditState; + +#define COLO_SOL_SPEAR_DELAY 7 +#define COLO_SOL_SPEAR_DELAY_FAST 6 +#define COLO_SOL_SHIELD_DELAY 6 +#define COLO_SOL_SHIELD_DELAY_FAST 5 +#define COLO_SOL_TRIPLE_DELAY 12 +#define COLO_SOL_TRIPLE_DELAY_MID 11 +#define COLO_SOL_GRAPPLE_DELAY 7 +#define COLO_SOL_SPECIAL_COOLDOWN 2 + +#define COLO_SOL_SPEAR_FREEZE 6 +#define COLO_SOL_SHIELD_FREEZE 4 +#define COLO_SOL_GRAPPLE_FREEZE 5 +#define COLO_SOL_TRANSITION_FREEZE 5 + +#define COLO_SOL_SLAM_MIN_HIT 20 +#define COLO_SOL_SLAM_RAND 25 + +#define COLO_SOL_AOE_DAMAGE_AGE 2 +#define COLO_SOL_AOE_LIFETIME 3 + +#define COLO_SOL_SPEAR_LINE_LEN 7 +#define COLO_SOL_SHIELD1_RING 4 +#define COLO_SOL_SHIELD2_RING 5 + +#define COLO_SOL_GRAPPLE_WINDOW 4 +#define COLO_SOL_PERFECT_MAX_TICKS 5 + +#define COLO_SOL_TP_HIT_GAP 3 +#define COLO_SOL_TP_HIT3_GAP_LOW 4 + +#define COLO_SOL_CRYSTAL_MOVE_TICKS_SPAWN 4 +#define COLO_SOL_CRYSTAL_MOVE_TICKS 2 +#define COLO_SOL_CRYSTAL_COOLDOWN_MIN 25 +#define COLO_SOL_CRYSTAL_COOLDOWN_RAND 10 +#define COLO_SOL_CRYSTAL_COOLDOWN_ENRAGE 12 +#define COLO_SOL_LASER_FREEZE 9 +#define COLO_SOL_LASER_BEAM_SHOW_MAX 6 +#define COLO_SOL_LASER_BEAM_SHOW_MIN 2 +#define COLO_SOL_LASER_DAMAGE_FREEZE 3 +#define COLO_SOL_LASER_MIN_HIT 60 +#define COLO_SOL_LASER_RAND 16 + +#define COLO_SOL_BEAM_COUNT 6 +#define COLO_SOL_BEAM_TO_POOL_TICKS 3 +#define COLO_SOL_BEAM_SPREAD 4 +#define COLO_SOL_ENRAGE_OPEN_POOLS 5 +#define COLO_SOL_ENRAGE_POOL_SPREAD 4 +#define COLO_SOL_MOLTEN_INTERVAL 3 + +#define COLO_MOLTEN_SAND_MIN_HIT 6 +#define COLO_MOLTEN_SAND_RAND 3 + +static int col_type_is_warbander(ColoNpcType type) { + return type == COLO_FREMENNIK_BERSERKER || type == COLO_FREMENNIK_ARCHER || + type == COLO_FREMENNIK_SEER; +} + +typedef struct { + int formation_dir; +} ColoWarbandState; + +typedef struct { + int cycle_step; + AttackStyle orb_style[3]; + AttackStyle fixed_orb_style[3]; +} ColoManticoreState; + +typedef struct { + int attack_count; + int skyfall_pending; + int skyfall_tile_x, skyfall_tile_y; + int skyfall_timer; + int skyfall_damage; +} ColoJavelinState; + +typedef struct { + ColoNpcType type; + int x, y; + int hp, max_hp; + int size; + uint32_t render_id; + int attack_timer; + AttackStyle attack_style; + int active; + int target_x, target_y; + int stun_timer; + int frozen_ticks; + int def_drained; + int min_hp_seen; + + union { + ColoManticoreState manticore; + ColoJavelinState javelin; + ColoWarbandState warband; + } type_state; + + EncounterPendingHitQueue pending_hits; + + int death_ticks; + int spawned_as_reinforcement; + + int attacked_this_tick; + AttackStyle attack_style_this_tick; + int moved_this_tick; + int moved_last_tick; + + int hit_landed_this_tick; + int hit_damage; + int hit_was_successful_this_tick; +} ColoNPC; + +static inline ColoManticoreState* colo_npc_manticore(ColoNPC* npc) { + assert(npc->type == COLO_MANTICORE); + return &npc->type_state.manticore; +} + +static inline ColoJavelinState* colo_npc_javelin(ColoNPC* npc) { + assert(npc->type == COLO_JAVELIN_COLOSSUS); + return &npc->type_state.javelin; +} + +static inline ColoWarbandState* colo_npc_warband(ColoNPC* npc) { + assert(col_type_is_warbander(npc->type)); + return &npc->type_state.warband; +} + +static inline int col_warband_window_offset(ColoNpcType type) { + switch (type) { + case COLO_FREMENNIK_BERSERKER: return 1; + case COLO_FREMENNIK_SEER: return 2; + case COLO_FREMENNIK_ARCHER: return 3; + default: + assert(0 && "warband window offset queried for a non-warbander"); + return 0; + } +} + +#define COLO_JAVELIN_SKYFALL_DELAY 6 +#define COLO_JAVELIN_SKYFALL_MAX_HIT 40 +#define COLO_JAVELIN_SKYFALL_ANIM_ID 10893 +#define COLO_MANTICORE_STAGGER_TICKS 5 + +#define COLO_MINOTAUR_HEAL_RANGE 7 + +static void colo_npc_init_type_state(ColoNPC* npc) { + switch (npc->type) { + case COLO_FREMENNIK_BERSERKER: + npc->type_state.warband = (ColoWarbandState){ .formation_dir = COLO_WARBAND_FORM_NORTH }; + break; + case COLO_FREMENNIK_SEER: + npc->type_state.warband = (ColoWarbandState){ .formation_dir = COLO_WARBAND_FORM_EAST }; + break; + case COLO_FREMENNIK_ARCHER: + npc->type_state.warband = (ColoWarbandState){ .formation_dir = COLO_WARBAND_FORM_WEST }; + break; + case COLO_MANTICORE: + npc->type_state.manticore = (ColoManticoreState){ + .cycle_step = -1, + .orb_style = { ATTACK_STYLE_NONE, ATTACK_STYLE_NONE, ATTACK_STYLE_NONE }, + .fixed_orb_style = { ATTACK_STYLE_NONE, ATTACK_STYLE_NONE, ATTACK_STYLE_NONE }, + }; + break; + case COLO_JAVELIN_COLOSSUS: + npc->type_state.javelin = (ColoJavelinState){0}; + break; + default: + break; + } +} + +typedef enum { + COLO_GEAR_MELEE = 0, + COLO_GEAR_RANGED, + COLO_GEAR_MAGIC, + COLO_NUM_WEAPON_SETS +} ColoWeaponSet; + +typedef struct { + float dpt; + uint8_t setup[NUM_GEAR_SLOTS]; +} ColoBestGear; + +#define COLO_BEST_GEAR_CACHE_WAYS 12 + +#define COLO_WEAPON_CHOICE_OBS_CACHE_WAYS 8 +#define COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS (2 * OSRS_INVENTORY_SIZE + 2) + +#define COLO_INVENTORY_OBS_CACHE_WAYS 8 +#define COLO_INVENTORY_OBS_CACHE_FLOATS \ + (OSRS_INVENTORY_SIZE * OSRS_INVENTORY_CELL_OBS_FEATURES + \ + NUM_GEAR_SLOTS * OSRS_EQUIPPED_SELF_OBS_FEATURES) + +#define COLO_THREAT_FIELD_OBS_CACHE_WAYS 4 +#define COLO_THREAT_FIELD_OBS_CACHE_FLOATS (2 * 17 * 17) + +typedef struct { + ColoBestGear best_gear[COLO_BEST_GEAR_CACHE_WAYS][COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; + uint64_t best_gear_sig[COLO_BEST_GEAR_CACHE_WAYS]; + int best_gear_valid[COLO_BEST_GEAR_CACHE_WAYS]; + int best_gear_next; + float weapon_choice[COLO_WEAPON_CHOICE_OBS_CACHE_WAYS][COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS]; + uint64_t weapon_choice_sig[COLO_WEAPON_CHOICE_OBS_CACHE_WAYS]; + int weapon_choice_valid[COLO_WEAPON_CHOICE_OBS_CACHE_WAYS]; + int weapon_choice_next; + float inventory[COLO_INVENTORY_OBS_CACHE_WAYS][COLO_INVENTORY_OBS_CACHE_FLOATS]; + uint64_t inventory_sig[COLO_INVENTORY_OBS_CACHE_WAYS]; + int inventory_valid[COLO_INVENTORY_OBS_CACHE_WAYS]; + int inventory_next; + float threat_field[COLO_THREAT_FIELD_OBS_CACHE_WAYS][COLO_THREAT_FIELD_OBS_CACHE_FLOATS]; + uint64_t threat_field_sig[COLO_THREAT_FIELD_OBS_CACHE_WAYS]; + int threat_field_valid[COLO_THREAT_FIELD_OBS_CACHE_WAYS]; + int threat_field_next; +} ColoObsMemoCaches; + +typedef enum { + COLO_LOADOUT_PROFILE_SPEEDRUN = 0, + COLO_LOADOUT_PROFILE_BEGINNER, + COLO_NUM_LOADOUT_PROFILES +} ColoLoadoutProfile; + +typedef enum { + COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY = 0, + COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, + COLO_LOADOUT_PROFILE_MODE_MIXED, +} ColoLoadoutProfileMode; + +typedef enum { + COLO_RESTORE_SUPER_RESTORE = 0, + COLO_RESTORE_SANFEW, +} ColoRestoreKind; + +static const uint8_t COLO_BEGINNER_MELEE_LOADOUT[NUM_GEAR_SLOTS] = { + [GEAR_SLOT_HEAD] = ITEM_SERPENTINE_HELM, + [GEAR_SLOT_CAPE] = ITEM_INFERNAL_CAPE, + [GEAR_SLOT_NECK] = ITEM_AMULET_OF_BLOOD_FURY, + [GEAR_SLOT_AMMO] = ITEM_GOD_BLESSING, + [GEAR_SLOT_WEAPON] = ITEM_OSMUMTENS_FANG, + [GEAR_SLOT_SHIELD] = ITEM_DRAGON_DEFENDER, + [GEAR_SLOT_BODY] = ITEM_FIGHTER_TORSO, + [GEAR_SLOT_LEGS] = ITEM_CRYSTAL_LEGS, + [GEAR_SLOT_HANDS] = ITEM_BARROWS_GLOVES, + [GEAR_SLOT_FEET] = ITEM_ECHO_BOOTS, + [GEAR_SLOT_RING] = ITEM_BERSERKER_RING_I, +}; + +static const uint8_t COLO_BEGINNER_RANGED_LOADOUT[NUM_GEAR_SLOTS] = { + [GEAR_SLOT_HEAD] = ITEM_CRYSTAL_HELM, + [GEAR_SLOT_CAPE] = ITEM_INFERNAL_CAPE, + [GEAR_SLOT_NECK] = ITEM_NECKLACE_OF_ANGUISH, + [GEAR_SLOT_AMMO] = ITEM_GOD_BLESSING, + [GEAR_SLOT_WEAPON] = ITEM_BOW_OF_FAERDHINEN, + [GEAR_SLOT_SHIELD] = ITEM_NONE, + [GEAR_SLOT_BODY] = ITEM_CRYSTAL_BODY, + [GEAR_SLOT_LEGS] = ITEM_CRYSTAL_LEGS, + [GEAR_SLOT_HANDS] = ITEM_BARROWS_GLOVES, + [GEAR_SLOT_FEET] = ITEM_ECHO_BOOTS, + [GEAR_SLOT_RING] = ITEM_BERSERKER_RING_I, +}; + +static const uint8_t COLO_BEGINNER_MAGIC_LOADOUT[NUM_GEAR_SLOTS] = { + [GEAR_SLOT_HEAD] = ITEM_SERPENTINE_HELM, + [GEAR_SLOT_CAPE] = ITEM_INFERNAL_CAPE, + [GEAR_SLOT_NECK] = ITEM_AMULET_OF_BLOOD_FURY, + [GEAR_SLOT_AMMO] = ITEM_GOD_BLESSING, + [GEAR_SLOT_WEAPON] = ITEM_TRIDENT_OF_SWAMP, + [GEAR_SLOT_SHIELD] = ITEM_DRAGON_DEFENDER, + [GEAR_SLOT_BODY] = ITEM_FIGHTER_TORSO, + [GEAR_SLOT_LEGS] = ITEM_CRYSTAL_LEGS, + [GEAR_SLOT_HANDS] = ITEM_BARROWS_GLOVES, + [GEAR_SLOT_FEET] = ITEM_ECHO_BOOTS, + [GEAR_SLOT_RING] = ITEM_BERSERKER_RING_I, +}; + +static const uint8_t COLO_SPEEDRUN_MELEE_LOADOUT[NUM_GEAR_SLOTS] = { + [GEAR_SLOT_HEAD] = ITEM_TORVA_FULL_HELM, + [GEAR_SLOT_CAPE] = ITEM_INFERNAL_CAPE, + [GEAR_SLOT_NECK] = ITEM_AMULET_OF_RANCOUR, + [GEAR_SLOT_AMMO] = ITEM_DRAGON_ARROWS, + [GEAR_SLOT_WEAPON] = ITEM_SCYTHE_OF_VITUR, + [GEAR_SLOT_SHIELD] = ITEM_NONE, + [GEAR_SLOT_BODY] = ITEM_OATHPLATE_CHEST, + [GEAR_SLOT_LEGS] = ITEM_OATHPLATE_LEGS, + [GEAR_SLOT_HANDS] = ITEM_FEROCIOUS_GLOVES, + [GEAR_SLOT_FEET] = ITEM_AVERNIC_TREADS, + [GEAR_SLOT_RING] = ITEM_ULTOR_RING, +}; + +static const uint8_t COLO_SPEEDRUN_RANGED_LOADOUT[NUM_GEAR_SLOTS] = { + [GEAR_SLOT_HEAD] = ITEM_MASORI_MASK_F, + [GEAR_SLOT_CAPE] = ITEM_DIZANAS_QUIVER, + [GEAR_SLOT_NECK] = ITEM_NECKLACE_OF_ANGUISH, + [GEAR_SLOT_AMMO] = ITEM_DRAGON_ARROWS, + [GEAR_SLOT_WEAPON] = ITEM_TWISTED_BOW, + [GEAR_SLOT_SHIELD] = ITEM_NONE, + [GEAR_SLOT_BODY] = ITEM_MASORI_BODY_F, + [GEAR_SLOT_LEGS] = ITEM_MASORI_CHAPS_F, + [GEAR_SLOT_HANDS] = ITEM_ZARYTE_VAMBRACES, + [GEAR_SLOT_FEET] = ITEM_AVERNIC_TREADS, + [GEAR_SLOT_RING] = ITEM_ULTOR_RING, +}; + +static const uint8_t COLO_SPEEDRUN_MAGIC_LOADOUT[NUM_GEAR_SLOTS] = { + [GEAR_SLOT_HEAD] = ITEM_TORVA_FULL_HELM, + [GEAR_SLOT_CAPE] = ITEM_INFERNAL_CAPE, + [GEAR_SLOT_NECK] = ITEM_OCCULT_NECKLACE, + [GEAR_SLOT_AMMO] = ITEM_DRAGON_ARROWS, + [GEAR_SLOT_WEAPON] = ITEM_TUMEKENS_SHADOW, + [GEAR_SLOT_SHIELD] = ITEM_NONE, + [GEAR_SLOT_BODY] = ITEM_OATHPLATE_CHEST, + [GEAR_SLOT_LEGS] = ITEM_OATHPLATE_LEGS, + [GEAR_SLOT_HANDS] = ITEM_CONFLICTION_GAUNTLETS, + [GEAR_SLOT_FEET] = ITEM_AVERNIC_TREADS, + [GEAR_SLOT_RING] = ITEM_ULTOR_RING, +}; + +static const uint8_t* const COLO_SPEEDRUN_LOADOUTS[COLO_NUM_WEAPON_SETS] = { + COLO_SPEEDRUN_MELEE_LOADOUT, + COLO_SPEEDRUN_RANGED_LOADOUT, + COLO_SPEEDRUN_MAGIC_LOADOUT, +}; + +static const uint8_t* const COLO_BEGINNER_LOADOUTS[COLO_NUM_WEAPON_SETS] = { + COLO_BEGINNER_MELEE_LOADOUT, + COLO_BEGINNER_RANGED_LOADOUT, + COLO_BEGINNER_MAGIC_LOADOUT, +}; + +static const int COLO_MAGIC_SPELL_BASE_DAMAGE[COLO_NUM_LOADOUT_PROFILES] = { + [COLO_LOADOUT_PROFILE_SPEEDRUN] = 34, + [COLO_LOADOUT_PROFILE_BEGINNER] = 31, +}; + +static const uint8_t* const* const COLO_LOADOUT_PROFILES[COLO_NUM_LOADOUT_PROFILES] = { + [COLO_LOADOUT_PROFILE_SPEEDRUN] = COLO_SPEEDRUN_LOADOUTS, + [COLO_LOADOUT_PROFILE_BEGINNER] = COLO_BEGINNER_LOADOUTS, +}; + +typedef struct { + int brew_doses; + int restore_doses; + int combat_doses; + int ranged_doses; + int surge_doses; + ColoRestoreKind restore_kind; +} ColoSupplyLoadout; + +static const ColoSupplyLoadout COLO_SUPPLY_LOADOUTS[COLO_NUM_LOADOUT_PROFILES] = { + [COLO_LOADOUT_PROFILE_SPEEDRUN] = { + .brew_doses = 4, .restore_doses = 28, .combat_doses = 4, + .ranged_doses = 4, .surge_doses = 4, + .restore_kind = COLO_RESTORE_SANFEW, + }, + [COLO_LOADOUT_PROFILE_BEGINNER] = { + .brew_doses = 24, .restore_doses = 32, .combat_doses = 8, + .ranged_doses = 8, .surge_doses = 0, + .restore_kind = COLO_RESTORE_SUPER_RESTORE, + }, +}; + +static const uint8_t* const* col_loadouts_for_profile(int profile) { + if (profile < 0 || profile >= COLO_NUM_LOADOUT_PROFILES) { + fprintf(stderr, "colosseum: invalid loadout profile %d\n", profile); + abort(); + } + return COLO_LOADOUT_PROFILES[profile]; +} + +#define COLO_INVENTORY_DISPLAY_SLOTS 28 +#if COLO_INVENTORY_DISPLAY_SLOTS != OSRS_INVENTORY_SIZE +#error "Colosseum inventory size must match shared OSRS inventory size" +#endif + +typedef OsrsInventoryCell ColoInvCell; + +static const int COLO_GAMEPLAY_INVENTORY_LOADOUT[COLO_NUM_LOADOUT_PROFILES][COLO_INVENTORY_DISPLAY_SLOTS] = { + [COLO_LOADOUT_PROFILE_SPEEDRUN] = { + 20997, 27238, 27235, 28947, + 26235, 27241, 19547, 13652, + 27275, 31106, 27610, 24780, + + 30875, 12002, 12006, 21003, + 23685, 23733, 6685, 4417, + + 10925, 10925, 10925, 10925, + 10925, 10925, 10925, 27281, + }, + [COLO_LOADOUT_PROFILE_BEGINNER] = { + 27610, 26219, 25865, 19547, + 11806, 13652, 23975, 23971, + 6685, 6685, 3024, 3024, + 6685, 6685, 3024, 3024, + 6685, 6685, 3024, 3024, + 12695, 2444, 3024, 3024, + 23685, 23733, 12006, 27281, + + }, +}; + +typedef void (*ColRawOsrsIdVisitor)(int raw_osrs_id, void* ctx); + +static const int COLO_NON_CONSUMABLE_DISPLAY_RAW_OSRS_IDS[] = { + 27610, + 12006, + 27281, +}; + +static int col_raw_osrs_id_seen(const int* seen, int seen_count, int raw_osrs_id) { + for (int i = 0; i < seen_count; i++) { + if (seen[i] == raw_osrs_id) return 1; + } + return 0; +} + +static void col_visit_raw_osrs_id_once( + int raw_osrs_id, + int* seen, + int* seen_count, + int seen_capacity, + ColRawOsrsIdVisitor visit, + void* ctx +) { + if (raw_osrs_id <= 0 || col_raw_osrs_id_seen(seen, *seen_count, raw_osrs_id)) { + return; + } + if (*seen_count >= seen_capacity) { + fprintf(stderr, "colosseum sprite id visitor capacity exceeded\n"); + abort(); + } + seen[(*seen_count)++] = raw_osrs_id; + visit(raw_osrs_id, ctx); +} + +static void col_for_each_reachable_consumable_dose_raw_osrs_id( + ColRawOsrsIdVisitor visit, + void* ctx +) { + if (!visit) { + fprintf(stderr, "colosseum consumable dose visitor is null\n"); + abort(); + } + + enum { + REGISTRY_COUNT = (int)( + sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY) / + sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY[0]) + ) + }; + int seen[REGISTRY_COUNT]; + int seen_count = 0; + + for (int p = 0; p < COLO_NUM_LOADOUT_PROFILES; p++) { + for (int slot = 0; slot < COLO_INVENTORY_DISPLAY_SLOTS; slot++) { + uint16_t raw_osrs_id = + (uint16_t)COLO_GAMEPLAY_INVENTORY_LOADOUT[p][slot]; + OsrsConsumableClick root = + osrs_consumable_click_lookup_raw_osrs_id(raw_osrs_id); + if (root.click_action != OSRS_CLICK_DRINK || root.dose_count == 0) { + continue; + } + + uint16_t current_raw_osrs_id = raw_osrs_id; + for (int step = 0; current_raw_osrs_id != 0; step++) { + if (step >= REGISTRY_COUNT) { + fprintf(stderr, "colosseum consumable dose chain cycles at raw id %u\n", + current_raw_osrs_id); + abort(); + } + OsrsConsumableClick current = + osrs_consumable_click_lookup_raw_osrs_id(current_raw_osrs_id); + if (current.click_action != OSRS_CLICK_DRINK || + current.dose_count == 0 || + current.consumable_kind != root.consumable_kind) { + fprintf(stderr, "colosseum consumable dose chain broke at raw id %u\n", + current_raw_osrs_id); + abort(); + } + col_visit_raw_osrs_id_once( + current_raw_osrs_id, + seen, + &seen_count, + REGISTRY_COUNT, + visit, + ctx); + current_raw_osrs_id = + osrs_consumable_raw_osrs_id_after_drink(current_raw_osrs_id); + } + } + } +} + +static void col_for_each_display_inventory_sprite_raw_osrs_id( + ColRawOsrsIdVisitor visit, + void* ctx +) { + if (!visit) { + fprintf(stderr, "colosseum display sprite visitor is null\n"); + abort(); + } + + enum { + DISPLAY_CAPACITY = + (int)(sizeof(COLO_NON_CONSUMABLE_DISPLAY_RAW_OSRS_IDS) / + sizeof(COLO_NON_CONSUMABLE_DISPLAY_RAW_OSRS_IDS[0])) + + (int)(sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY) / + sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY[0])) + }; + int seen[DISPLAY_CAPACITY]; + int seen_count = 0; + + for (int i = 0; i < (int)(sizeof(COLO_NON_CONSUMABLE_DISPLAY_RAW_OSRS_IDS) / + sizeof(COLO_NON_CONSUMABLE_DISPLAY_RAW_OSRS_IDS[0])); i++) { + col_visit_raw_osrs_id_once( + COLO_NON_CONSUMABLE_DISPLAY_RAW_OSRS_IDS[i], + seen, + &seen_count, + DISPLAY_CAPACITY, + visit, + ctx); + } + col_for_each_reachable_consumable_dose_raw_osrs_id(visit, ctx); +} + +#define COLO_THRALL_MAX_HIT 3 +#define COLO_THRALL_TICK 4 +#define COLO_THRALL_RECAST_CD 17 +static const int COLO_THRALL_LIFETIME_TICKS[COLO_NUM_LOADOUT_PROFILES] = { + [COLO_LOADOUT_PROFILE_SPEEDRUN] = 198, + [COLO_LOADOUT_PROFILE_BEGINNER] = 99, +}; + +#define COLO_DEATH_CHARGE_WINDOW 100 +#define COLO_DEATH_CHARGE_CD 100 +#define COLO_DEATH_CHARGE_SPEC_GAIN 15 + +static ColoLoadoutProfile col_sample_loadout_profile( + int mode, float beginner_fraction, uint32_t* rng_state +) { + if (mode == COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY) + return COLO_LOADOUT_PROFILE_SPEEDRUN; + if (mode == COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY) + return COLO_LOADOUT_PROFILE_BEGINNER; + if (mode != COLO_LOADOUT_PROFILE_MODE_MIXED) { + fprintf(stderr, "colosseum: invalid loadout_profile_mode %d\n", mode); + abort(); + } + if (!(beginner_fraction >= 0.0f && beginner_fraction <= 1.0f)) { + fprintf(stderr, "colosseum: beginner_loadout_fraction must be in [0,1], got %.6f\n", + beginner_fraction); + abort(); + } + return encounter_rand_float(rng_state) < beginner_fraction + ? COLO_LOADOUT_PROFILE_BEGINNER + : COLO_LOADOUT_PROFILE_SPEEDRUN; +} + +#define COLO_SURGE_RESTORE_AMOUNT 25 +#define COLO_SURGE_COOLDOWN_TICKS 500 + +typedef struct { + int start_wave; + float damage_reward_coeff; + float boss_damage_reward_coeff; + float wave_clear_bonus; + float boss_phase_bonus; + float win_bonus; + float prayer_correct_reward; + float offpray_damage_penalty_coeff; + + float multistyle_exposure_penalty_coeff; + + float argmax_gear_reward_coeff; + + float offensive_boost_reward_coeff; + + float stall_penalty_coeff; + + int farm_safe_damage_cap; + + int farm_cap_waves; + + float avoided_damage_coeff; + float death_penalty_coeff; + float timeout_penalty; + int curriculum_agent; + int loadout_profile_mode; + float beginner_loadout_fraction; + int step_out_forecast_obs_enabled; + int threat_field_obs_enabled; + int forecast_horizon; + int forecast_run_tile_mode; + int mask_inventory_heads; + int action_debug_log; + int late_start_state_mode; + + float late_start_supply_fraction_per_wave; + + int prayer_oracle_mode; + + int bis_gear_oracle_mode; + + int invuln_mode; + + int episode_max_ticks_override; + + int remove_brews; + + float prayer_switch_fail_prob; + + float player_damage_received_scale; + + float damage_scale_anneal_start; + + int damage_scale_anneal_ticks; + +} ColConfig; + +typedef enum { + COLO_FORECAST_RUN_TILE_FULL = 0, + COLO_FORECAST_RUN_TILE_STATIC_THREAT = 1, +} ColoForecastRunTileMode; + +typedef enum { + COLO_NPC_RENDER_ATTACK_NORMAL = 0, + COLO_NPC_RENDER_ATTACK_JAVELIN_SKYFALL, +} ColoNpcRenderAttackAnim; + +typedef struct { + ColConfig config; + const CollisionMap* collision_map; + int world_offset_x; + int world_offset_y; + Log* log; + const HumanCommand* human_commands; + int human_command_count; + int human_command_mode; + ColoNpcRenderAttackAnim npc_render_attack_anim[COLO_MAX_NPCS]; + int manticore_charge_started[COLO_MAX_NPCS]; + int player_render_hit_count; + int player_render_hit_damage[ENCOUNTER_RENDER_HITS_MAX]; + int npc_render_hit_count[COLO_MAX_NPCS]; + int npc_render_hit_damage[COLO_MAX_NPCS][ENCOUNTER_RENDER_HITS_MAX]; + + int thrall_attacked_this_tick; + int player_spec_fired_kind; + int player_venator_chain_count; + int player_venator_chain_slots[OSRS_VENATOR_MAX_CHAIN_HITS]; +} ColosseumContext; + +static inline void col_context_clear_render_events(ColosseumContext* ctx) { + memset(ctx->npc_render_attack_anim, 0, sizeof(ctx->npc_render_attack_anim)); + memset(ctx->manticore_charge_started, 0, sizeof(ctx->manticore_charge_started)); + ctx->player_render_hit_count = 0; + memset(ctx->player_render_hit_damage, 0, sizeof(ctx->player_render_hit_damage)); + memset(ctx->npc_render_hit_count, 0, sizeof(ctx->npc_render_hit_count)); + memset(ctx->npc_render_hit_damage, 0, sizeof(ctx->npc_render_hit_damage)); + ctx->thrall_attacked_this_tick = 0; + ctx->player_spec_fired_kind = 0; + ctx->player_venator_chain_count = 0; + for (int i = 0; i < OSRS_VENATOR_MAX_CHAIN_HITS; i++) + ctx->player_venator_chain_slots[i] = -1; +} + +static inline void col_context_record_venator_chain( + ColosseumContext* ctx, + const OsrsVenatorChain* chain +) { + if (!chain) abort(); + osrs_venator_validate_chain_length(chain->length); + ctx->player_venator_chain_count = (int)chain->length; + for (int i = 0; i < OSRS_VENATOR_MAX_CHAIN_HITS; i++) + ctx->player_venator_chain_slots[i] = -1; + for (int i = 0; i < (int)chain->length; i++) + ctx->player_venator_chain_slots[i] = chain->hits[i].slot; +} + +typedef struct { + float damage_dealt; + float damage_boss; + float damage_received; + int prayer_correct; + int attacks_fired; + int wave_completed; + int player_moved; + int player_attacked; + + float fresh_damage_dealt; + float fresh_damage_boss; + + float fresh_damage_reinforcement; + + int styles_faced_mask; + float offpray_damage_this_tick; + float avoidable_threat_by_style[4]; + + float landed_offpray_damage; + float landed_prayed_damage; + float landed_unprayable_damage; + float landed_self_damage; +} ColoTickScratch; + +typedef struct { + float episode_return; + int episode_length; + int wave_reached; + int waves_cleared; + int win; + int died; + float total_damage_dealt; + float total_damage_received; + float total_boss_damage; + int boss_phase_reached; + int total_npc_kills; + int total_prayer_correct; + int total_npc_attacks; + int modifiers_picked; + int active_tier_sum; + + float pray_faced_by_type[COLO_NUM_NPC_TYPES]; + float pray_correct_by_type[COLO_NUM_NPC_TYPES]; + float offpray_damage_by_type[COLO_NUM_NPC_TYPES]; + float total_damage_by_type[COLO_NUM_NPC_TYPES]; + + float death_by_type[COLO_NUM_NPC_TYPES]; + float death_fatal_damage; + + float offpray_damage_conflict; + float offpray_damage_solo; + float death_on_conflict_tick; + + float death_dmg_unprayable; + float death_dmg_offpray; + float death_dmg_prayed; + float death_dmg_self; + float death_heal_remaining; + float farm_damage; + float typeless_damage_by_type[COLO_NUM_NPC_TYPES]; + float outcome_score; + float max_wave_depth; +} ColosseumLog; + +typedef struct { + Player player; + + ColoTickScratch tick_scratch; + + ColoNPC npcs[COLO_MAX_NPCS]; + uint32_t next_npc_render_id; + int current_obs_slots[COLO_OBS_NPCS]; + + int wave; + int wave_spawn_target; + int tick; + int wave_spawn_delay; + int wave_ready_delay; + + int wave_attack_delay; + int episode_max_ticks; + int reinforcement_timer; + int warband_cycle_anchor; + + AttackStyle wave_manticore_orb_style[3]; + int wave_manticore_pattern_rolled; + int current_wave_total_killable; + + int current_wave_hp_pool; + float current_wave_fresh_damage; + int episode_over; + int time_limit_truncated; + ColoOutcome winner; + + ColoModifierState modifiers; + SolHereditState sol; + + int doom_stacks; + ColoBeeSwarm bees[COLO_MAX_BEE_SWARMS]; + ColoTotem totems[COLO_MAX_TOTEMS]; + ColoSolarflareOrb solarflare; + int player_venom; + int player_venom_timer; + int player_poison; + int player_poison_timer; + + int molten_x[COLO_SOL_HAZARD_TILES_MAX]; + int molten_y[COLO_SOL_HAZARD_TILES_MAX]; + int molten_kind[COLO_SOL_HAZARD_TILES_MAX]; + int molten_lifetime[COLO_SOL_HAZARD_TILES_MAX]; + int molten_burn_timer[COLO_SOL_HAZARD_TILES_MAX]; + int molten_count; + + float reward; + float episode_return; + int last_hit_by_type; + int rewarded_boss_phase; + float avoided_damage_already_rewarded; + int min_sol_hp_seen; + + OsrsInteraction interaction; + int player_last_interaction_target_slot; + int player_last_interaction_age; + ColoWeaponSet weapon_set; + EncounterLoadoutStats loadout_stats_live; + OsrsEquipmentEffectProfile effects_live; + int loadout_stats_dirty; + + ColoLoadoutProfile active_loadout_profile; + ColoSupplyLoadout full_supplies; + + int stat_drift_timer; + int divine_combat_timer; + int divine_ranged_timer; + int surge_doses; + int surge_cooldown; + OsrsInventoryCell inventory_cells[OSRS_INVENTORY_SIZE]; + + int thrall_active; + int thrall_target_slot; + int thrall_lifetime_left; + int thrall_attack_timer; + int thrall_recast_cd; + + int death_charge_window_left; + int death_charge_cd; + + int player_attack_npc_idx; + int player_attack_dmg; + AttackStyle player_attack_style_id; + EncounterProjectileTiming player_attack_timing; + + EncounterPendingHitQueue player_pending_hits; + + int player_dest_x, player_dest_y; + + uint8_t npc_collision_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; + uint8_t player_collision_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; + + ColosseumLog log; + + int start_wave; + uint32_t rng_state; + + float active_player_damage_received_scale; + + ColoObsMemoCaches obs_memos; +} ColosseumState; + +typedef enum { + COLO_DMG_OFFPRAY = 0, + COLO_DMG_UNPRAYABLE = 1, +} ColoDamageChannel; + +static inline void col_damage_player_from( + ColosseumState* s, int dmg, int npc_type, ColoDamageChannel channel); +static void col_apply_bis_gear_oracle(ColosseumState* s); + +static void col_build_live_inventory_display( + const ColosseumState* s, + int out[COLO_INVENTORY_DISPLAY_SLOTS] +) { + for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) + out[i] = s->inventory_cells[i].raw_osrs_id; +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc new file mode 100644 index 0000000000..d4c00c1408 --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_modifiers.inc @@ -0,0 +1,902 @@ +#line 76 "encounter_colosseum.h" + +static void col_mod_apply_frailty_hp(ColosseumState* s); + +static inline int col_mod_active(const ColosseumState* s, ColoModifier mod) { + return (s->modifiers.active_mask & (1u << (unsigned)mod)) ? 1 : 0; +} + +static inline int col_mod_tier(const ColosseumState* s, ColoModifier mod) { + return col_mod_active(s, mod) ? s->modifiers.tier[mod] : 0; +} + +static int col_modifier_eligible(const ColosseumState* s, ColoModifier mod, int next_wave) { + if (s->modifiers.tier[mod] >= COLO_MODIFIER_MAX_TIER[mod]) return 0; + if (COLO_MODIFIER_PRE_BOSS_ONLY[mod] && next_wave >= COLO_WAVE_BOSS) return 0; + if ((mod == COLO_MOD_RED_FLAG || mod == COLO_MOD_DYNAMIC_DUO) && + next_wave > COLO_RFDD_LAST_DRAFT_WAVE) return 0; + return 1; +} + +static int col_modifier_draft_weight(const ColosseumState* s, ColoModifier mod) { + int tier = s->modifiers.tier[mod]; + if (col_mod_active(s, mod) && tier >= 1 && tier < COLO_MODIFIER_MAX_TIER[mod]) + return COLO_DRAFT_UPGRADE_WEIGHT; + return 1; +} + +static void col_modifier_open_draft(ColosseumState* s, int next_wave) { + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) s->modifiers.draft_options[o] = -1; + + int pool[COLO_NUM_REAL_MODIFIERS]; + int weight[COLO_NUM_REAL_MODIFIERS]; + int pool_n = 0; + int total_weight = 0; + for (int m = 0; m < COLO_NUM_REAL_MODIFIERS; m++) { + if (!col_modifier_eligible(s, (ColoModifier)m, next_wave)) continue; + pool[pool_n] = m; + weight[pool_n] = col_modifier_draft_weight(s, (ColoModifier)m); + total_weight += weight[pool_n]; + pool_n++; + } + + int offered = 0; + while (offered < COLO_MODIFIER_DRAFT_OPTIONS && pool_n > 0) { + int roll = encounter_rand_int(&s->rng_state, total_weight); + int pick = 0; + while (roll >= weight[pick]) roll -= weight[pick++]; + s->modifiers.draft_options[offered++] = pool[pick]; + total_weight -= weight[pick]; + pool[pick] = pool[pool_n - 1]; + weight[pick] = weight[pool_n - 1]; + pool_n--; + } + + if (offered == 0) return; + s->modifiers.draft_pending = 1; + s->modifiers.draft_gates_spawn = 1; +} + +static void col_modifier_apply_selection(ColosseumState* s, int option) { + if (!s->modifiers.draft_pending) return; + if (option < 0 || option >= COLO_MODIFIER_DRAFT_OPTIONS) return; + int mod = s->modifiers.draft_options[option]; + if (mod < 0 || mod >= COLO_NUM_REAL_MODIFIERS) return; + + s->modifiers.active_mask |= (1u << (unsigned)mod); + if (s->modifiers.tier[mod] < COLO_MODIFIER_MAX_TIER[mod]) s->modifiers.tier[mod]++; + s->log.modifiers_picked++; + + s->modifiers.draft_pending = 0; + s->modifiers.draft_free_movement = 0; + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) s->modifiers.draft_options[o] = -1; + if (s->modifiers.draft_gates_spawn) { + s->modifiers.draft_gates_spawn = 0; + s->wave_spawn_delay = col_wave_entry_delay_ticks(s->wave_spawn_target); + } + + if ((ColoModifier)mod == COLO_MOD_FRAILTY) col_mod_apply_frailty_hp(s); +} + +static int col_modifier_pick_score(const ColosseumState* s, ColoModifier mod, int next_wave) { + int t = s->modifiers.tier[mod] + 1; + int ext = next_wave + 1; + switch (mod) { + case COLO_MOD_BLASPHEMY: return t == 1 ? 90 : t == 2 ? 85 : 80; + case COLO_MOD_FRAILTY: return t == 1 ? 88 : t == 2 ? 75 : 10; + case COLO_MOD_RELENTLESS: return t == 1 ? 82 : t == 2 ? 55 : 45; + case COLO_MOD_DOOM: return t == 1 ? 80 : t == 2 ? 50 : 8; + case COLO_MOD_MYOPIA: return t == 1 ? 78 : t == 2 ? 70 : 44; + case COLO_MOD_MANTIMAYHEM: return t == 1 ? 56 : t == 2 ? 72 : 12; + case COLO_MOD_SOLARFLARE: return t == 1 ? 52 : t == 2 ? 48 : 40; + case COLO_MOD_REENTRY: return t == 1 ? 50 : t == 2 ? (ext >= 10 ? 42 : 14) : 68; + case COLO_MOD_VOLATILITY: + return (t == 1 ? 51 : t == 2 ? 46 : 65) + (ext == 12 ? 30 : 0); + case COLO_MOD_DYNAMIC_DUO: return ext >= 11 ? 35 : 20; + case COLO_MOD_QUARTET: return 18; + case COLO_MOD_TOTEMIC: return ext == 12 ? 55 : 16; + case COLO_MOD_RED_FLAG: return ext >= 11 ? 19 : 11; + case COLO_MOD_BEES: return t == 1 ? 5 : t == 2 ? 4 : 3; + default: return 0; + } +} + +static void col_synthesize_draft_history(ColosseumState* s, int start_wave) { + for (int w = 0; w < start_wave; w++) { + col_modifier_open_draft(s, w); + if (!s->modifiers.draft_pending) continue; + int best_opt = -1; + int best_score = -1; + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { + int mod = s->modifiers.draft_options[o]; + if (mod < 0) continue; + int score = col_modifier_pick_score(s, (ColoModifier)mod, w); + if (score > best_score) { best_score = score; best_opt = o; } + } + s->modifiers.draft_gates_spawn = 0; + if (best_opt >= 0) col_modifier_apply_selection(s, best_opt); + else s->modifiers.draft_pending = 0; + } +} + +static void col_late_start_drink_noop(void* ctx, OsrsConsumableKind kind) { + (void)ctx; + (void)kind; +} + +static void col_late_start_drain_supplies( + ColosseumState* s, const ColosseumContext* ctx, int start_wave +) { + float per_wave = ctx->config.late_start_supply_fraction_per_wave; + if (per_wave <= 0.0f) return; + float f = 1.0f - per_wave * (float)start_wave + + 0.01f * (float)(encounter_rand_int(&s->rng_state, 11) - 5); + if (f < 0.10f) f = 0.10f; + if (f > 1.0f) f = 1.0f; + + int done[COLO_INVENTORY_DISPLAY_SLOTS] = {0}; + for (int c = 0; c < COLO_INVENTORY_DISPLAY_SLOTS; c++) { + if (done[c] || s->inventory_cells[c].dose == 0) continue; + int item = s->inventory_cells[c].item_idx; + int total = 0; + for (int k = c; k < COLO_INVENTORY_DISPLAY_SLOTS; k++) { + if (s->inventory_cells[k].item_idx == item && + s->inventory_cells[k].dose > 0) { + done[k] = 1; + total += s->inventory_cells[k].dose; + } + } + int drain = total - (int)((float)total * f + 0.5f); + for (int k = COLO_INVENTORY_DISPLAY_SLOTS - 1; k >= c && drain > 0; k--) { + if (s->inventory_cells[k].item_idx != item) continue; + while (s->inventory_cells[k].dose > 0 && drain > 0) { + OsrsInventoryClickResolution res = osrs_inventory_cell_click_interpret( + &s->inventory_cells[k], OSRS_CLICK_TICK_FIRST); + if (res.click_action != OSRS_CLICK_DRINK) break; + int scratch_timer = 0; + OsrsInventoryDrinkConsumeResult consumed = + osrs_inventory_cell_consume_drink_one_dose( + &s->inventory_cells[k], res, &scratch_timer, + col_late_start_drink_noop, s); + if (!consumed.consumed) break; + drain--; + } + } + } + col_sync_consumable_counters_from_inventory(s); +} + +#define COLO_WAVE_ENTRY_RESERVOIR_CAP 64 + +typedef struct { + OsrsInventoryCell cells[COLO_INVENTORY_DISPLAY_SLOTS]; + uint32_t active_mask; + uint8_t tier[COLO_NUM_REAL_MODIFIERS]; + int16_t hitpoints; + int16_t prayer_points; + uint8_t special_energy; +} ColoWaveEntrySample; + +typedef struct { + int seen; + int stored; + ColoWaveEntrySample samples[COLO_WAVE_ENTRY_RESERVOIR_CAP]; +} ColoWaveEntryReservoir; + +static ColoWaveEntryReservoir g_colo_wave_entry_res[COLO_NUM_WAVES]; +static char g_colo_wave_entry_lock; +static uint32_t g_colo_wave_entry_rng = 0x9e3779b9u; + +static void col_wave_entry_lock(void) { + while (__atomic_test_and_set(&g_colo_wave_entry_lock, __ATOMIC_ACQUIRE)) {} +} + +static void col_wave_entry_unlock(void) { + __atomic_clear(&g_colo_wave_entry_lock, __ATOMIC_RELEASE); +} + +static void col_record_wave_entry(const ColosseumState* s, int entered_wave) { + if (s->start_wave != 0) return; + if (entered_wave <= 0 || entered_wave >= COLO_NUM_WAVES) return; + col_wave_entry_lock(); + ColoWaveEntryReservoir* res = &g_colo_wave_entry_res[entered_wave]; + res->seen++; + int slot = -1; + if (res->stored < COLO_WAVE_ENTRY_RESERVOIR_CAP) { + slot = res->stored++; + } else { + int j = (int)(xorshift32(&g_colo_wave_entry_rng) % (uint32_t)res->seen); + if (j < COLO_WAVE_ENTRY_RESERVOIR_CAP) slot = j; + } + if (slot >= 0) { + ColoWaveEntrySample* out = &res->samples[slot]; + memcpy(out->cells, s->inventory_cells, sizeof(out->cells)); + out->active_mask = s->modifiers.active_mask; + for (int m = 0; m < COLO_NUM_REAL_MODIFIERS; m++) + out->tier[m] = (uint8_t)s->modifiers.tier[m]; + out->hitpoints = (int16_t)s->player.current_hitpoints; + out->prayer_points = (int16_t)s->player.current_prayer; + out->special_energy = (uint8_t)s->player.special_energy; + } + col_wave_entry_unlock(); +} + +static int col_apply_wave_entry_sample(ColosseumState* s, int start_wave) { + if (start_wave <= 0 || start_wave >= COLO_NUM_WAVES) return 0; + ColoWaveEntrySample sample; + int have = 0; + col_wave_entry_lock(); + ColoWaveEntryReservoir* res = &g_colo_wave_entry_res[start_wave]; + if (res->stored > 0) { + uint32_t r = xorshift32(&g_colo_wave_entry_rng); + sample = res->samples[r % (uint32_t)res->stored]; + have = 1; + } + col_wave_entry_unlock(); + if (!have) return 0; + + memcpy(s->inventory_cells, sample.cells, sizeof(sample.cells)); + col_sync_consumable_counters_from_inventory(s); + s->modifiers.active_mask = sample.active_mask; + for (int m = 0; m < COLO_NUM_REAL_MODIFIERS; m++) + s->modifiers.tier[m] = sample.tier[m]; + if (col_mod_active(s, COLO_MOD_FRAILTY)) col_mod_apply_frailty_hp(s); + if (sample.hitpoints > 0 && sample.hitpoints < s->player.current_hitpoints) + s->player.current_hitpoints = sample.hitpoints; + if (sample.prayer_points >= 0 && sample.prayer_points < s->player.current_prayer) + s->player.current_prayer = sample.prayer_points; + s->player.special_energy = sample.special_energy; + return 1; +} + +static void col_apply_late_start_entry_state( + ColosseumState* s, const ColosseumContext* ctx, int start_wave +) { + int mode = ctx->config.late_start_state_mode; + if (mode <= 0 || start_wave <= 0) return; + if (mode >= 2 && col_apply_wave_entry_sample(s, start_wave)) return; + col_synthesize_draft_history(s, start_wave); + col_late_start_drain_supplies(s, ctx, start_wave); +} + +static void col_mod_apply_frailty_hp(ColosseumState* s) { + int tier = col_mod_tier(s, COLO_MOD_FRAILTY); + int max_hp = 99 - (99 * COLO_FRAILTY_HP_CUT_PCT[tier]) / 100; + if (max_hp < 1) max_hp = 1; + s->player.base_hitpoints = max_hp; + if (s->player.current_hitpoints > max_hp) s->player.current_hitpoints = max_hp; +} + +static int col_mod_relentless_def_level(const ColosseumState* s, int def_level) { + int tier = col_mod_tier(s, COLO_MOD_RELENTLESS); + if (tier == 0) return def_level; + return (def_level * (100 - COLO_RELENTLESS_DEF_BYPASS_PCT[tier])) / 100; +} + +static int col_mod_relentless_max_hit_bonus(const ColosseumState* s, int* force_hit) { + int tier = col_mod_tier(s, COLO_MOD_RELENTLESS); + *force_hit = (tier >= 3) ? 1 : 0; + return COLO_RELENTLESS_MAX_HIT_BONUS[tier]; +} + +static int col_mod_myopia_range(const ColosseumState* s, int base_range) { + int cut = COLO_MYOPIA_RANGE_CUT[col_mod_tier(s, COLO_MOD_MYOPIA)]; + int r = base_range - cut; + return r < 1 ? 1 : r; +} + +static void col_mod_on_player_damaged(ColosseumState* s, int damage) { + if (damage <= 0) return; + + int blas = col_mod_tier(s, COLO_MOD_BLASPHEMY); + if (blas > 0 && s->player.current_prayer > 0) { + int drain = (damage * COLO_BLASPHEMY_DRAIN_PCT[blas]) / 100; + s->player.current_prayer -= drain; + if (s->player.current_prayer < 0) s->player.current_prayer = 0; + } + + if (col_mod_active(s, COLO_MOD_DOOM)) s->doom_stacks++; +} + +static int col_mod_doom_lethal(const ColosseumState* s) { + int tier = col_mod_tier(s, COLO_MOD_DOOM); + return tier > 0 && s->doom_stacks >= COLO_DOOM_CAP[tier]; +} + +static void col_mod_manticore_apply_venom(ColosseumState* s, int hit_success) { + + if (osrs_effect_profile_has(col_live_effects(s), OSRS_ITEM_EFFECT_VENOM_IMMUNE)) + return; + if (col_mod_tier(s, COLO_MOD_MANTIMAYHEM) < 2) return; + if (!hit_success) return; + s->player_poison = 0; + s->player_poison_timer = 0; + if (s->player_venom == 0) { + s->player_venom = COLO_VENOM_START; + s->player_venom_timer = COLO_VENOM_INTERVAL; + } else { + s->player_venom += COLO_VENOM_STEP; + if (s->player_venom > COLO_VENOM_CAP) s->player_venom = COLO_VENOM_CAP; + } +} + +static void col_mod_tick_venom(ColosseumState* s) { + if (s->player_venom <= 0) return; + if (s->player_venom_timer > 0) s->player_venom_timer--; + if (s->player_venom_timer > 0) return; + int dmg = s->player_venom; + col_damage_player_from(s, dmg, COLO_MANTICORE, COLO_DMG_UNPRAYABLE); + s->last_hit_by_type = COLO_MANTICORE; + col_mod_on_player_damaged(s, dmg); + s->player_venom += COLO_VENOM_STEP; + if (s->player_venom > COLO_VENOM_CAP) s->player_venom = COLO_VENOM_CAP; + s->player_venom_timer = COLO_VENOM_INTERVAL; +} + +static void col_mod_apply_bee_poison(ColosseumState* s) { + if (osrs_effect_profile_has(col_live_effects(s), OSRS_ITEM_EFFECT_VENOM_IMMUNE)) + return; + if (s->player_venom > 0) return; + if (s->player_poison <= 0) s->player_poison_timer = COLO_POISON_INTERVAL; + if (s->player_poison < COLO_POISON_BEE_CONTACT_SEVERITY) + s->player_poison = COLO_POISON_BEE_CONTACT_SEVERITY; +} + +static void col_mod_tick_poison(ColosseumState* s) { + if (s->player_poison <= 0) return; + if (s->player_poison_timer > 0) s->player_poison_timer--; + if (s->player_poison_timer > 0) return; + int dmg = (s->player_poison + 4) / 5; + col_damage_player_from(s, dmg, COLO_BEE_SWARM, COLO_DMG_UNPRAYABLE); + if (dmg > 0) { + s->last_hit_by_type = COLO_BEE_SWARM; + col_mod_on_player_damaged(s, dmg); + } + s->player_poison--; + if (s->player_poison <= 0) { + s->player_poison = 0; + s->player_poison_timer = 0; + } else { + s->player_poison_timer = COLO_POISON_INTERVAL; + } +} + +static int col_spawn_hazard_npc_beside( + ColosseumState* s, ColoNpcType type, const ColoNPC* owner +) { + int size = col_npc_effective_size(owner); + for (int ring = 0; ring < 2; ring++) { + int lo_x = owner->x - 1 - ring, hi_x = owner->x + size + ring; + int lo_y = owner->y - 1 - ring, hi_y = owner->y + size + ring; + for (int pass = 0; pass < 2; pass++) { + for (int y = lo_y; y <= hi_y; y++) { + for (int x = lo_x; x <= hi_x; x++) { + int on_shell = (x == lo_x || x == hi_x || y == lo_y || y == hi_y); + if (!on_shell) continue; + int is_east = (x == hi_x); + if ((pass == 0) != is_east) continue; + if (col_static_blocked(x, y)) continue; + if (col_sol_clamp_active(s) && !col_in_boss_arena(s, x, y)) continue; + return col_spawn_npc_at(s, type, x, y); + } + } + } + } + + return -1; +} + +static ColoTotem* col_totem_for_owner(ColosseumState* s, int owner_slot) { + assert(owner_slot >= 0 && owner_slot < COLO_MAX_TOTEMS); + return &s->totems[owner_slot]; +} + +static void col_hazard_on_npc_death(ColosseumState* s, int npc_slot) { + const ColoNPC* npc = &s->npcs[npc_slot]; + if (npc->type == COLO_HEALING_TOTEM) { + for (int o = 0; o < COLO_MAX_TOTEMS; o++) { + ColoTotem* tot = &s->totems[o]; + if (tot->phase != COLO_HAZARD_ALIVE || tot->npc_slot != npc_slot) continue; + tot->phase = COLO_HAZARD_RESPAWNING; + tot->npc_slot = -1; + tot->respawn_timer = COLO_TOTEM_RESPAWN_TICKS; + return; + } + return; + } + if (npc->type == COLO_BEE_SWARM) { + for (int b = 0; b < COLO_MAX_BEE_SWARMS; b++) { + ColoBeeSwarm* bee = &s->bees[b]; + if (bee->phase != COLO_HAZARD_ALIVE || bee->npc_slot != npc_slot) continue; + bee->phase = COLO_HAZARD_RESPAWNING; + bee->npc_slot = -1; + bee->respawn_timer = COLO_BEE_RESPAWN_TICKS; + return; + } + return; + } +} + +static void col_mod_on_owner_death(ColosseumState* s, int owner_slot) { + ColoTotem* tot = col_totem_for_owner(s, owner_slot); + if (tot->phase == COLO_HAZARD_ALIVE && tot->npc_slot >= 0 && + s->npcs[tot->npc_slot].active && + s->npcs[tot->npc_slot].type == COLO_HEALING_TOTEM) + col_deactivate_npc(s, tot->npc_slot); + *tot = (ColoTotem){ .phase = COLO_HAZARD_NONE }; +} + +static void col_totem_spawn_for_owner(ColosseumState* s, int owner_slot) { + ColoTotem* tot = col_totem_for_owner(s, owner_slot); + int slot = col_spawn_hazard_npc_beside(s, COLO_HEALING_TOTEM, &s->npcs[owner_slot]); + if (slot < 0) { + + tot->phase = COLO_HAZARD_RESPAWNING; + tot->npc_slot = -1; + tot->respawn_timer = 1; + return; + } + tot->phase = COLO_HAZARD_ALIVE; + tot->npc_slot = slot; + tot->heal_timer = COLO_TOTEM_HEAL_INTERVAL; +} + +static void col_mod_on_npc_hp_changed(ColosseumState* s, int npc_slot) { + if (!col_mod_active(s, COLO_MOD_TOTEMIC)) return; + const ColoNPC* npc = &s->npcs[npc_slot]; + if (!npc->active || npc->hp <= 0 || npc->max_hp <= 0) return; + if (col_type_is_hazard_entity(npc->type)) return; + if (npc->hp * 2 > npc->max_hp) return; + if (col_totem_for_owner(s, npc_slot)->phase != COLO_HAZARD_NONE) return; + col_totem_spawn_for_owner(s, npc_slot); +} + +static void col_mod_tick_totems(ColosseumState* s) { + for (int o = 0; o < COLO_MAX_TOTEMS; o++) { + ColoTotem* tot = &s->totems[o]; + if (tot->phase == COLO_HAZARD_NONE) continue; + + ColoNPC* owner = &s->npcs[o]; + if (!owner->active || owner->hp <= 0 || + col_type_is_hazard_entity(owner->type)) { + col_mod_on_owner_death(s, o); + continue; + } + + if (tot->phase == COLO_HAZARD_RESPAWNING) { + if (tot->respawn_timer > 0) tot->respawn_timer--; + if (tot->respawn_timer == 0) col_totem_spawn_for_owner(s, o); + continue; + } + + ColoNPC* totem_npc = (tot->npc_slot >= 0) ? &s->npcs[tot->npc_slot] : NULL; + if (!totem_npc || !totem_npc->active || totem_npc->type != COLO_HEALING_TOTEM) { + *tot = (ColoTotem){ .phase = COLO_HAZARD_NONE }; + continue; + } + + if (tot->heal_timer > 0) tot->heal_timer--; + if (tot->heal_timer > 0) continue; + tot->heal_timer = COLO_TOTEM_HEAL_INTERVAL; + if (owner->type == COLO_SOL_HEREDIT) { + owner->hp += COLO_TOTEM_SOL_HEAL; + } else { + if (owner->hp * 2 > owner->max_hp) continue; + owner->hp += (owner->max_hp * COLO_TOTEM_HEAL_PCT) / 100; + } + if (owner->hp > owner->max_hp) owner->hp = owner->max_hp; + } +} + +#define COLO_REENTRY_MOLTEN_MAX_HIT 15 +#define COLO_REENTRY_MOLTEN_BURN_INTERVAL 2 + +typedef struct { + int x; + int y; +} ColoMoltenPoolTile; + +static int col_mod_molten_pool_covered(const ColosseumState* s, int x, int y) { + for (int i = 0; i < s->molten_count; i++) + if (s->molten_x[i] == x && s->molten_y[i] == y) return 1; + return 0; +} + +static int col_mod_molten_pool_available(const ColosseumState* s, int x, int y) { + return !col_static_blocked(x, y) && !col_mod_molten_pool_covered(s, x, y); +} + +static ColoMoltenPoolTile col_mod_molten_pool_random_fallback(ColosseumState* s) { + ColoMoltenPoolTile candidates[COLO_ARENA_TILE_COUNT]; + int count = 0; + for (int x = COLO_ARENA_MIN_X; x <= COLO_ARENA_MAX_X; x++) { + for (int y = COLO_ARENA_MIN_Y; y <= COLO_ARENA_MAX_Y; y++) { + if (!col_mod_molten_pool_available(s, x, y)) continue; + candidates[count++] = (ColoMoltenPoolTile){ .x = x, .y = y }; + } + } + assert(count > 0); + return candidates[encounter_rand_int(&s->rng_state, count)]; +} + +static ColoMoltenPoolTile col_mod_molten_pool_tile_or_fallback( + ColosseumState* s, + int x, + int y +) { + if (col_mod_molten_pool_available(s, x, y)) + return (ColoMoltenPoolTile){ .x = x, .y = y }; + return col_mod_molten_pool_random_fallback(s); +} + +static void col_mod_add_molten_pool( + ColosseumState* s, + int x, + int y, + ColoMoltenPoolKind kind, + ColoMoltenPoolLifetime lifetime +) { + if (col_static_blocked(x, y)) return; + for (int i = 0; i < s->molten_count; i++) { + if (s->molten_x[i] != x || s->molten_y[i] != y) continue; + s->molten_kind[i] = kind; + if (lifetime == COLO_POOL_PERMANENT) s->molten_lifetime[i] = lifetime; + s->molten_burn_timer[i] = 0; + return; + } + int slot; + if (s->molten_count < COLO_SOL_HAZARD_TILES_MAX) slot = s->molten_count++; + else slot = encounter_rand_int(&s->rng_state, COLO_SOL_HAZARD_TILES_MAX); + s->molten_x[slot] = x; + s->molten_y[slot] = y; + s->molten_kind[slot] = kind; + s->molten_lifetime[slot] = lifetime; + s->molten_burn_timer[slot] = 0; +} + +static void col_mod_clear_wave_end_pools(ColosseumState* s) { + int out = 0; + for (int i = 0; i < s->molten_count; i++) { + if (s->molten_lifetime[i] != COLO_POOL_PERMANENT) continue; + if (out != i) { + s->molten_x[out] = s->molten_x[i]; + s->molten_y[out] = s->molten_y[i]; + s->molten_kind[out] = s->molten_kind[i]; + s->molten_lifetime[out] = s->molten_lifetime[i]; + s->molten_burn_timer[out] = s->molten_burn_timer[i]; + } + out++; + } + s->molten_count = out; +} + +static int col_mod_molten_pool_damage(ColosseumState* s, ColoMoltenPoolKind kind) { + if (kind == COLO_POOL_REENTRY) + return 1 + encounter_rand_int(&s->rng_state, COLO_REENTRY_MOLTEN_MAX_HIT); + return COLO_MOLTEN_SAND_MIN_HIT + + encounter_rand_int(&s->rng_state, COLO_MOLTEN_SAND_RAND); +} + +static void col_mod_tick_molten_pools(ColosseumState* s) { + int burned = 0; + for (int i = 0; i < s->molten_count; i++) { + int reentry = s->molten_kind[i] == COLO_POOL_REENTRY; + int player_on_pool = s->player.x == s->molten_x[i] && s->player.y == s->molten_y[i]; + if (!burned && player_on_pool && (!reentry || s->molten_burn_timer[i] == 0)) { + int dmg = col_mod_molten_pool_damage(s, (ColoMoltenPoolKind)s->molten_kind[i]); + if (reentry) s->molten_burn_timer[i] = COLO_REENTRY_MOLTEN_BURN_INTERVAL - 1; + col_damage_player_from(s, dmg, COLO_JAVELIN_COLOSSUS, COLO_DMG_UNPRAYABLE); + s->last_hit_by_type = s->last_hit_by_type < 0 ? COLO_JAVELIN_COLOSSUS : s->last_hit_by_type; + col_mod_on_player_damaged(s, dmg); + burned = 1; + continue; + } + if (reentry && s->molten_burn_timer[i] > 0) + s->molten_burn_timer[i]--; + } +} + +static void col_mod_reentry_on_skyfall(ColosseumState* s, int tile_x, int tile_y) { + int tier = col_mod_tier(s, COLO_MOD_REENTRY); + if (tier == 0) return; + ColoMoltenPoolLifetime lifetime = tier >= 2 ? COLO_POOL_PERMANENT : COLO_POOL_TEMPORARY; + col_mod_add_molten_pool(s, tile_x, tile_y, COLO_POOL_REENTRY, lifetime); + if (tier >= 2) { + ColoMoltenPoolTile sw = col_mod_molten_pool_tile_or_fallback( + s, tile_x - 1, tile_y - 1); + col_mod_add_molten_pool(s, sw.x, sw.y, COLO_POOL_REENTRY, COLO_POOL_PERMANENT); + } + if (tier >= 3) { + ColoMoltenPoolTile west = col_mod_molten_pool_tile_or_fallback( + s, tile_x - 1, tile_y); + col_mod_add_molten_pool(s, west.x, west.y, COLO_POOL_REENTRY, COLO_POOL_PERMANENT); + } +} + +static int col_volatility_reach_for_tier(int tier) { + if (tier <= 0) return 0; + return tier >= 2 ? 2 : 1; +} + +static int col_volatility_player_in_blast( + const ColosseumState* s, + int x, + int y, + int size, + int tier +) { + int reach = col_volatility_reach_for_tier(tier); + if (reach <= 0) return 0; + int span = size + 2 * reach; + return encounter_entity_footprints_overlap( + x - reach, y - reach, span, s->player.x, s->player.y, 1); +} + +static void col_mod_volatility_on_death(ColosseumState* s, int x, int y, int size) { + int tier = col_mod_tier(s, COLO_MOD_VOLATILITY); + if (tier == 0) return; + if (col_volatility_player_in_blast(s, x, y, size, tier)) { + int vol_dmg = col_scale_incoming_damage(s, COLO_VOLATILITY_MAX_HIT); + encounter_damage_player(&s->player, vol_dmg, &s->tick_scratch.damage_received); + + s->tick_scratch.landed_unprayable_damage += (float)vol_dmg; + col_mod_on_player_damaged(s, vol_dmg); + } + if (tier >= 3) + col_mod_add_molten_pool( + s, x + size / 2, y + size / 2, COLO_POOL_VOLATILITY, COLO_POOL_TEMPORARY); +} + +static void col_clamp_bee_swarm_to_arena(ColoNPC* npc) { + int size = col_npc_effective_size(npc); + int max_x = COLO_ARENA_MAX_X - size + 1; + int max_y = COLO_ARENA_MAX_Y - size + 1; + if (npc->x < COLO_ARENA_MIN_X) npc->x = COLO_ARENA_MIN_X; + if (npc->y < COLO_ARENA_MIN_Y) npc->y = COLO_ARENA_MIN_Y; + if (npc->x > max_x) npc->x = max_x; + if (npc->y > max_y) npc->y = max_y; +} + +static void col_bee_spawn(ColosseumState* s, int b) { + int pos = encounter_rand_int(&s->rng_state, COLO_NUM_SPAWN_ANCHORS); + int slot = col_spawn_npc_at(s, COLO_BEE_SWARM, + COLO_SPAWN_ANCHORS[pos][0], COLO_SPAWN_ANCHORS[pos][1]); + ColoNPC* npc = &s->npcs[slot]; + col_clamp_bee_swarm_to_arena(npc); + npc->target_x = npc->x; + npc->target_y = npc->y; + s->bees[b] = (ColoBeeSwarm){ + .phase = COLO_HAZARD_ALIVE, + .npc_slot = slot, + .move_timer = COLO_BEE_MOVE_INTERVAL, + }; +} + +static void col_mod_sync_bees(ColosseumState* s) { + int want = col_mod_tier(s, COLO_MOD_BEES); + for (int b = 0; b < COLO_MAX_BEE_SWARMS; b++) { + ColoBeeSwarm* bee = &s->bees[b]; + int npc_live = bee->phase == COLO_HAZARD_ALIVE && bee->npc_slot >= 0 && + s->npcs[bee->npc_slot].active && + s->npcs[bee->npc_slot].type == COLO_BEE_SWARM; + if (b < want) { + if (npc_live || bee->phase == COLO_HAZARD_RESPAWNING) continue; + col_bee_spawn(s, b); + } else { + if (npc_live) col_deactivate_npc(s, bee->npc_slot); + *bee = (ColoBeeSwarm){ .phase = COLO_HAZARD_NONE }; + } + } +} + +static int col_bee_swarm_contacts_player(const ColosseumState* s, const ColoNPC* npc) { + int size = col_npc_effective_size(npc); + return encounter_entity_footprints_overlap(npc->x, npc->y, size, s->player.x, s->player.y, 1); +} + +static void col_bee_swarm_next_tile_toward_player( + const ColosseumState* s, + const ColoNPC* npc, + int* out_x, + int* out_y +) { + if (!s || !npc || !out_x || !out_y) abort(); + int size = col_npc_effective_size(npc); + int x = npc->x; + int y = npc->y; + int hi_x = x + size - 1; + int hi_y = y + size - 1; + if (s->player.x < x) x--; + else if (s->player.x > hi_x) x++; + if (s->player.y < y) y--; + else if (s->player.y > hi_y) y++; + int max_x = COLO_ARENA_MAX_X - size + 1; + int max_y = COLO_ARENA_MAX_Y - size + 1; + if (x < COLO_ARENA_MIN_X) x = COLO_ARENA_MIN_X; + if (y < COLO_ARENA_MIN_Y) y = COLO_ARENA_MIN_Y; + if (x > max_x) x = max_x; + if (y > max_y) y = max_y; + *out_x = x; + *out_y = y; +} + +static int col_bee_swarm_contact_damage(ColosseumState* s) { + if (osrs_effect_profile_has(col_live_effects(s), OSRS_ITEM_EFFECT_VENOM_IMMUNE)) + return 0; + int span = COLO_BEE_MAX_DAMAGE - COLO_BEE_MIN_DAMAGE + 1; + return COLO_BEE_MIN_DAMAGE + encounter_rand_int(&s->rng_state, span); +} + +static void col_bee_swarm_step_toward_player(ColosseumState* s, ColoNPC* npc) { + int next_x, next_y; + col_bee_swarm_next_tile_toward_player(s, npc, &next_x, &next_y); + npc->x = next_x; + npc->y = next_y; + npc->target_x = s->player.x; + npc->target_y = s->player.y; +} + +static void col_mod_tick_bees(ColosseumState* s) { + for (int b = 0; b < COLO_MAX_BEE_SWARMS; b++) { + ColoBeeSwarm* bee = &s->bees[b]; + if (bee->phase == COLO_HAZARD_NONE) continue; + + if (bee->phase == COLO_HAZARD_RESPAWNING) { + if (bee->respawn_timer > 0) bee->respawn_timer--; + if (bee->respawn_timer == 0) col_bee_spawn(s, b); + continue; + } + + ColoNPC* npc = (bee->npc_slot >= 0) ? &s->npcs[bee->npc_slot] : NULL; + if (!npc || !npc->active || npc->type != COLO_BEE_SWARM) { + *bee = (ColoBeeSwarm){ .phase = COLO_HAZARD_NONE }; + continue; + } + + if (bee->move_timer > 0) bee->move_timer--; + if (bee->move_timer == 0) { + col_bee_swarm_step_toward_player(s, npc); + bee->move_timer = COLO_BEE_MOVE_INTERVAL; + } + + if (col_bee_swarm_contacts_player(s, npc)) { + col_mod_apply_bee_poison(s); + int dmg = col_bee_swarm_contact_damage(s); + col_damage_player_from(s, dmg, COLO_BEE_SWARM, COLO_DMG_UNPRAYABLE); + if (dmg > 0) { + s->last_hit_by_type = COLO_BEE_SWARM; + col_mod_on_player_damaged(s, dmg); + } + } + } +} + +static void col_solarflare_tile( + const ColosseumState* s, int pillar_index, int step, int* x, int* y +) { + if (pillar_index < 0 || pillar_index >= COLO_NUM_PILLARS || + step < 0 || step >= COLO_SOLARFLARE_RING_STEPS || !x || !y) { + fprintf(stderr, "invalid solarflare tile request\n"); + abort(); + } + + int px, py; + if (s->wave == COLO_WAVE_BOSS && s->sol.started) { + px = (pillar_index & 1) ? s->sol.boss_arena_max_x - 5 + : s->sol.boss_arena_min_x + 3; + py = (pillar_index & 2) ? s->sol.boss_arena_max_y - 5 + : s->sol.boss_arena_min_y + 3; + } else { + px = COLO_PILLARS[pillar_index][0]; + py = COLO_PILLARS[pillar_index][1]; + } + if (step < 4) { + *x = px - 1 + step; + *y = py - 1; + } else if (step < 8) { + *x = px + COLO_PILLAR_SIZE; + *y = py - 1 + (step - 4); + } else if (step < 12) { + *x = px + COLO_PILLAR_SIZE - (step - 8); + *y = py + COLO_PILLAR_SIZE; + } else { + *x = px - 1; + *y = py + COLO_PILLAR_SIZE - (step - 12); + } +} + +static int col_solarflare_step_is_corner(int step) { + if (step < 0 || step >= COLO_SOLARFLARE_RING_STEPS) { + fprintf(stderr, "invalid solarflare step\n"); + abort(); + } + return (step & 3) == 0; +} + +static void col_mod_sync_solarflare(ColosseumState* s) { + s->solarflare = (ColoSolarflareOrb){ + .active = col_mod_active(s, COLO_MOD_SOLARFLARE) ? 1 : 0, + }; +} + +static int col_mod_solarflare_max_hit(int tier) { + if (tier >= 3) return COLO_SOLARFLARE_MAX_HIT_T3; + if (tier == 2) return COLO_SOLARFLARE_MAX_HIT_T2; + return COLO_SOLARFLARE_MAX_HIT_T1; +} + +static void col_mod_apply_solarflare_contact(ColosseumState* s, int tier) { + for (int p = 0; p < COLO_NUM_PILLARS; p++) { + int x, y; + col_solarflare_tile(s, p, s->solarflare.step, &x, &y); + if (x != s->player.x || y != s->player.y) continue; + int dmg = encounter_rand_int(&s->rng_state, col_mod_solarflare_max_hit(tier) + 1); + dmg = col_scale_incoming_damage(s, dmg); + encounter_damage_player(&s->player, dmg, &s->tick_scratch.damage_received); + + s->tick_scratch.landed_unprayable_damage += (float)dmg; + if (dmg > 0) col_mod_on_player_damaged(s, dmg); + if (tier >= 3) s->player.prayer = PRAYER_NONE; + } +} + +static void col_solarflare_advance_orb(ColoSolarflareOrb* orb, int tier) { + if (orb->pause_timer > 0) { + orb->pause_timer--; + return; + } + if (orb->move_timer > 0) orb->move_timer--; + if (orb->move_timer != 0) return; + int next_step = orb->step + 1; + if (next_step == COLO_SOLARFLARE_RING_STEPS) next_step = 0; + orb->step = next_step; + orb->move_timer = (tier >= 3) + ? COLO_SOLARFLARE_MOVE_INTERVAL_T3 : COLO_SOLARFLARE_MOVE_INTERVAL; + if (col_solarflare_step_is_corner(orb->step)) { + if (tier == 1) orb->pause_timer = COLO_SOLARFLARE_CORNER_PAUSE; + if (tier >= 3) orb->pause_timer = COLO_SOLARFLARE_CORNER_PAUSE_T3; + } +} + +static void col_mod_tick_solarflare(ColosseumState* s) { + if (!s->solarflare.active) return; + int tier = col_mod_tier(s, COLO_MOD_SOLARFLARE); + if (tier == 0) { + s->solarflare = (ColoSolarflareOrb){0}; + return; + } + + col_solarflare_advance_orb(&s->solarflare, tier); + col_mod_apply_solarflare_contact(s, tier); +} + +static void col_modifiers_tick(ColosseumState* s) { +#ifdef COLO_PROFILE_ENABLED + int col_prof_enabled = COLO_PROFILE_ENABLED(); + double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; +#endif + col_mod_tick_bees(s); + col_mod_tick_solarflare(s); + col_mod_tick_molten_pools(s); + col_mod_tick_venom(s); + col_mod_tick_poison(s); + col_mod_tick_totems(s); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_STEP_MODIFIERS_HAZARDS); +#endif +} + +static void col_modifiers_on_wave_spawn(ColosseumState* s) { + s->doom_stacks = 0; + col_mod_clear_wave_end_pools(s); + col_mod_apply_frailty_hp(s); + col_mod_sync_bees(s); + col_mod_sync_solarflare(s); +} + +static int col_modifier_active_tier_sum(const ColosseumState* s) { + int sum = 0; + for (int m = 0; m < COLO_NUM_REAL_MODIFIERS; m++) + if (col_mod_active(s, (ColoModifier)m)) sum += s->modifiers.tier[m]; + return sum; +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc new file mode 100644 index 0000000000..2cfc0ae147 --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_movement.inc @@ -0,0 +1,360 @@ +#line 76 "encounter_colosseum.h" + +static int col_player_walkable(void* ctx, int x, int y) { + ColosseumState* s = (ColosseumState*)ctx; + if (col_static_blocked(x, y)) return 0; + if (col_sol_clamp_active(s) && !col_in_boss_arena(s, x, y)) return 0; + return 1; +} + +static int col_los_tile_blocked(void* ctx, int x, int y) { + (void)ctx; + return col_static_blocked(x, y); +} + +static int col_tiles_have_los(const ColosseumState* s, int x0, int y0, int x1, int y1) { + (void)s; + if (!COLO_STATIC_LOOKUP_TABLES_BUILT) col_build_static_arena(); + if (col_static_los_endpoint_in_bounds(x0, y0) && + col_static_los_endpoint_in_bounds(x1, y1)) + return col_static_los_get(x0, y0, x1, y1); + return col_tiles_have_los_reference_slowpath(x0, y0, x1, y1); +} + +static OsrsLosQuery col_player_los_query(ColosseumState* s) { + return osrs_los_tile(col_los_tile_blocked, s); +} + +static int col_npc_has_los_to_player(const ColosseumState* s, const ColoNPC* npc) { + int size = npc->size > 0 ? npc->size : COLO_NPC_STATS[npc->type].size; + int sx = s->player.x, sy = s->player.y; + if (sx < npc->x) sx = npc->x; + if (sx > npc->x + size - 1) sx = npc->x + size - 1; + if (sy < npc->y) sy = npc->y; + if (sy > npc->y + size - 1) sy = npc->y + size - 1; + return col_tiles_have_los(s, sx, sy, s->player.x, s->player.y); +} + +static int col_npc_blocked_ignore_player(void* ctx, int x, int y, int size) { + ColosseumState* s = (ColosseumState*)ctx; + if (col_static_footprint_blocked_lookup(x, y, size)) return 1; + int sol_clamp_active = col_sol_clamp_active(s); + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + if (sol_clamp_active && !col_in_boss_arena(s, x + dx, y + dy)) return 1; + int gx, gy; + if (!col_grid_index(x + dx, y + dy, &gx, &gy)) return 1; + if (s->npc_collision_flags[gx][gy]) return 1; + } + } + return 0; +} + +static int col_npc_blocked(void* ctx, int x, int y, int size) { + ColosseumState* s = (ColosseumState*)ctx; + if (col_npc_blocked_ignore_player(ctx, x, y, size)) return 1; + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + int gx, gy; + if (!col_grid_index(x + dx, y + dy, &gx, &gy)) return 1; + if (s->player_collision_flags[gx][gy]) return 1; + } + } + return 0; +} + +typedef struct { + ColosseumState* s; + int self_idx; +} ColoNpcMoveCtx; + +static int col_npc_move_blocked_ignore_player(void* ctx, int x, int y, int size) { + ColoNpcMoveCtx* move_ctx = (ColoNpcMoveCtx*)ctx; + return col_npc_blocked_ignore_player(move_ctx->s, x, y, size); +} + +static int col_npc_move_overlap_hold(void* ctx) { + ColoNpcMoveCtx* move_ctx = (ColoNpcMoveCtx*)ctx; + ColosseumState* s = move_ctx->s; + return s->player_last_interaction_age == 0 && + s->player_last_interaction_target_slot == move_ctx->self_idx; +} + +typedef struct { + ColosseumState* s; + int size; + uint16_t gen; + + int ignore_npcs; +} ColoNpcPathCtx; + +typedef struct { + uint16_t blocked_gen[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; + uint8_t blocked_value[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; + uint16_t counter; +} ColoNpcPathCache; + +static OSRS_THREAD_LOCAL ColoNpcPathCache col_npc_path_cache; + +static ColoNpcPathCtx col_npc_path_ctx_begin(ColosseumState* s, int size) { + col_npc_path_cache.counter++; + if (col_npc_path_cache.counter == 0) { + memset(col_npc_path_cache.blocked_gen, 0, + sizeof(col_npc_path_cache.blocked_gen)); + col_npc_path_cache.counter = 1; + } + return (ColoNpcPathCtx){ .s = s, .size = size, .gen = col_npc_path_cache.counter }; +} + +static int col_npc_path_blocked_uncached(ColoNpcPathCtx* pc, int x, int y) { + ColosseumState* s = pc->s; + if (s->player.x >= x && s->player.x < x + pc->size && + s->player.y >= y && s->player.y < y + pc->size) return 1; + if (pc->ignore_npcs) { + for (int dx = 0; dx < pc->size; dx++) { + for (int dy = 0; dy < pc->size; dy++) { + int tx = x + dx, ty = y + dy; + if (col_static_blocked(tx, ty)) return 1; + if (col_sol_clamp_active(s) && !col_in_boss_arena(s, tx, ty)) return 1; + } + } + return 0; + } + return col_npc_blocked_ignore_player(s, x, y, pc->size); +} + +static int col_npc_path_blocked(void* ctx, int x, int y) { + ColoNpcPathCtx* pc = (ColoNpcPathCtx*)ctx; + int gx, gy; + if (!col_grid_index(x, y, &gx, &gy)) + return col_npc_path_blocked_uncached(pc, x, y); + if (col_npc_path_cache.blocked_gen[gx][gy] != pc->gen) { + col_npc_path_cache.blocked_gen[gx][gy] = pc->gen; + col_npc_path_cache.blocked_value[gx][gy] = + (uint8_t)col_npc_path_blocked_uncached(pc, x, y); + } + return col_npc_path_cache.blocked_value[gx][gy]; +} + +#define COL_WARBAND_BFS_MEMO_BITS 16 +#define COL_WARBAND_BFS_MEMO_SIZE (1 << COL_WARBAND_BFS_MEMO_BITS) +static OSRS_THREAD_LOCAL uint64_t col_warband_bfs_memo_key[COL_WARBAND_BFS_MEMO_SIZE]; +static OSRS_THREAD_LOCAL uint8_t col_warband_bfs_memo_result[COL_WARBAND_BFS_MEMO_SIZE]; + +#ifdef COLO_BFS_INSTR +static long col_warband_bfs_memo_instr_hits; +static long col_warband_bfs_memo_instr_misses; +static int col_warband_bfs_memo_instr_registered; +static void col_warband_bfs_memo_instr_report(void) { + long total = col_warband_bfs_memo_instr_hits + col_warband_bfs_memo_instr_misses; + fprintf(stderr, "COLO_BFS_INSTR: memo hits=%ld misses=%ld (%.1f%% hit)\n", + col_warband_bfs_memo_instr_hits, col_warband_bfs_memo_instr_misses, + total ? 100.0 * col_warband_bfs_memo_instr_hits / total : 0.0); +} +#endif + +static uint64_t col_warband_bfs_memo_pack( + const ColosseumState* s, int x, int y, int tx, int ty) { + uint64_t key = 1; + key = key << 6 | (uint64_t)x; + key = key << 6 | (uint64_t)y; + key = key << 6 | (uint64_t)tx; + key = key << 6 | (uint64_t)ty; + key = key << 6 | (uint64_t)s->player.x; + key = key << 6 | (uint64_t)s->player.y; + if (col_sol_clamp_active(s)) { + key = key << 1 | 1; + key = key << 6 | (uint64_t)s->sol.boss_arena_min_x; + key = key << 6 | (uint64_t)s->sol.boss_arena_max_x; + key = key << 6 | (uint64_t)s->sol.boss_arena_min_y; + key = key << 6 | (uint64_t)s->sol.boss_arena_max_y; + } else { + key <<= 1; + } + return key; +} + +static uint32_t col_warband_bfs_memo_slot(uint64_t key) { + return (uint32_t)((key * 0x9E3779B97F4A7C15ULL) >> (64 - COL_WARBAND_BFS_MEMO_BITS)); +} + +static uint8_t col_warband_bfs_memo_encode(PathResult pr) { + return (uint8_t)((pr.found ? 1 : 0) | + (uint32_t)(pr.next_dx + 1) << 1 | + (uint32_t)(pr.next_dy + 1) << 3); +} + +static int col_npc_path_step_with_ctx(ColoNpcPathCtx* pc, int* x, int* y, int tx, int ty) { + if (tx < COLO_ARENA_MIN_X) tx = COLO_ARENA_MIN_X; + if (tx > COLO_ARENA_MAX_X) tx = COLO_ARENA_MAX_X; + if (ty < COLO_ARENA_MIN_Y) ty = COLO_ARENA_MIN_Y; + if (ty > COLO_ARENA_MAX_Y) ty = COLO_ARENA_MAX_Y; + if (*x == tx && *y == ty) return 0; + int direct_dx = (tx > *x) - (tx < *x); + int direct_dy = (ty > *y) - (ty < *y); + int direct_abs_dx = tx > *x ? tx - *x : *x - tx; + int direct_abs_dy = ty > *y ? ty - *y : *y - ty; + if (direct_abs_dx <= 1 && direct_abs_dy <= 1 && + !col_npc_path_blocked(pc, tx, ty) && + (direct_dx == 0 || direct_dy == 0 || + (!col_npc_path_blocked(pc, *x + direct_dx, *y) && + !col_npc_path_blocked(pc, *x, *y + direct_dy)))) { + *x = tx; + *y = ty; + return 1; + } + int memoable = pc->ignore_npcs && pc->size == 1 && + *x >= COLO_ARENA_MIN_X && *x <= COLO_ARENA_MAX_X && + *y >= COLO_ARENA_MIN_Y && *y <= COLO_ARENA_MAX_Y && + pc->s->player.x >= COLO_ARENA_MIN_X && pc->s->player.x <= COLO_ARENA_MAX_X && + pc->s->player.y >= COLO_ARENA_MIN_Y && pc->s->player.y <= COLO_ARENA_MAX_Y; + uint64_t memo_key = 0; + uint32_t memo_slot = 0; + if (memoable) { +#ifdef COLO_BFS_INSTR + if (!col_warband_bfs_memo_instr_registered) { + col_warband_bfs_memo_instr_registered = 1; + atexit(col_warband_bfs_memo_instr_report); + } +#endif + memo_key = col_warband_bfs_memo_pack(pc->s, *x, *y, tx, ty); + memo_slot = col_warband_bfs_memo_slot(memo_key); + if (col_warband_bfs_memo_key[memo_slot] == memo_key) { +#ifdef COLO_BFS_INSTR + col_warband_bfs_memo_instr_hits++; +#endif + uint8_t r = col_warband_bfs_memo_result[memo_slot]; + int found = r & 1; + int next_dx = (int)((r >> 1) & 3) - 1; + int next_dy = (int)((r >> 3) & 3) - 1; + if (!found || (next_dx == 0 && next_dy == 0)) return 0; + *x += next_dx; + *y += next_dy; + return 1; + } +#ifdef COLO_BFS_INSTR + col_warband_bfs_memo_instr_misses++; +#endif + } +#ifdef COLO_PROFILE_ENABLED + int col_prof_enabled = COLO_PROFILE_ENABLED(); + double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; +#endif + PathResult pr = pathfind_step_arena(NULL, 0, *x, *y, tx, ty, + col_npc_path_blocked, pc, + COLO_ARENA_MIN_X, COLO_ARENA_MIN_Y, COLO_ARENA_WIDTH, COLO_ARENA_HEIGHT); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_STEP_NPC_PATHFINDING); +#endif + if (memoable) { + col_warband_bfs_memo_key[memo_slot] = memo_key; + col_warband_bfs_memo_result[memo_slot] = col_warband_bfs_memo_encode(pr); + } + if (!pr.found || (pr.next_dx == 0 && pr.next_dy == 0)) return 0; + *x += pr.next_dx; + *y += pr.next_dy; + return 1; +} + +static int col_npc_path_step(ColosseumState* s, int* x, int* y, int size, int tx, int ty) { + ColoNpcPathCtx pc = col_npc_path_ctx_begin(s, size); + return col_npc_path_step_with_ctx(&pc, x, y, tx, ty); +} + +static void col_warband_pick_target( + ColosseumState* s, ColoNpcPathCtx* pc, ColoNPC* npc, int* tx, int* ty) { + int px = s->player.x, py = s->player.y; + int pref = colo_npc_warband(npc)->formation_dir; + int fx = px + COLO_WARBAND_FORM_OFFSET[pref][0]; + int fy = py + COLO_WARBAND_FORM_OFFSET[pref][1]; + *tx = fx; + *ty = fy; + if (!col_npc_path_blocked(pc, fx, fy)) return; + + static const int ADJ[8][2] = { + {0, 1}, {1, 0}, {-1, 0}, {0, -1}, + {1, 1}, {1, -1}, {-1, 1}, {-1, -1} }; + int best_key = 1 << 30; + for (int a = 0; a < 8; a++) { + int x = px + ADJ[a][0], y = py + ADJ[a][1]; + if (col_npc_path_blocked(pc, x, y)) continue; + int dx = npc->x > x ? npc->x - x : x - npc->x; + int dy = npc->y > y ? npc->y - y : y - npc->y; + int cheb = dx > dy ? dx : dy; + int key = (a >= 4 ? 4096 : 0) + cheb * 16 + a; + if (key < best_key) { best_key = key; *tx = x; *ty = y; } + } +} + +static void col_warband_move(ColosseumState* s, int idx) { + ColoNPC* npc = &s->npcs[idx]; + npc->target_x = s->player.x; + npc->target_y = s->player.y; + + ColoNpcPathCtx pc = col_npc_path_ctx_begin(s, 1); + pc.ignore_npcs = 1; + int tx, ty; + col_warband_pick_target(s, &pc, npc, &tx, &ty); + int steps = 0; + for (int k = 0; k < COLO_WARBAND_TILES_PER_TICK; k++) { + if (npc->x == tx && npc->y == ty) break; + if (!col_npc_path_step_with_ctx(&pc, &npc->x, &npc->y, tx, ty)) break; + steps++; + } + npc->moved_this_tick = steps > 0 ? 1 : 0; +} + +static void col_npc_move_ctx(ColosseumState* s, ColosseumContext* ctx, int idx) { + (void)ctx; + ColoNPC* npc = &s->npcs[idx]; + const ColoNpcStats* st = &COLO_NPC_STATS[npc->type]; + if (npc->type == COLO_SOL_HEREDIT) return; + if (!st->can_move) return; + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return; + + if (col_type_is_warbander(npc->type)) { + col_warband_move(s, idx); + return; + } + + int size = col_npc_effective_size(npc); + int tx = s->player.x, ty = s->player.y; + npc->target_x = tx; + npc->target_y = ty; + + col_stamp_npc_collision_footprint(s, npc->x, npc->y, size, 0); + ColoNpcMoveCtx move_ctx = { s, idx }; + + int is_ranged = st->attack_range > 1; + if (is_ranged) { + int dist = encounter_dist_to_npc(s->player.x, s->player.y, npc->x, npc->y, size); + if (dist >= 1 && dist <= st->attack_range && col_npc_has_los_to_player(s, npc)) { + col_stamp_npc_collision_footprint(s, npc->x, npc->y, size, 1); + npc->moved_this_tick = 0; + return; + } + } + if (npc->attack_timer > st->attack_speed) { + col_stamp_npc_collision_footprint(s, npc->x, npc->y, size, 1); + npc->moved_this_tick = 0; + return; + } + + int moved; + if (npc->type == COLO_MINOTAUR && col_mod_active(s, COLO_MOD_RED_FLAG)) { + + moved = 0; + if (encounter_dist_to_npc(tx, ty, npc->x, npc->y, size) > 1) + moved = col_npc_path_step(s, &npc->x, &npc->y, size, tx - 1, ty - 1); + } else { + EncounterNpcStepPolicy step_policy = is_ranged + ? ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET + : ENCOUNTER_NPC_STEP_OSRS_AGGRO_STOP_AT_MELEE; + moved = encounter_npc_step_toward_policy( + &npc->x, &npc->y, tx, ty, size, 1, step_policy, + col_npc_move_blocked_ignore_player, &move_ctx, + col_npc_move_overlap_hold, &s->rng_state); + } + col_stamp_npc_collision_footprint(s, npc->x, npc->y, size, 1); + npc->moved_this_tick = moved ? 1 : 0; +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc new file mode 100644 index 0000000000..950e59b68c --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_obs_mask.inc @@ -0,0 +1,1951 @@ +#line 76 "encounter_colosseum.h" + +#define COLO_PLAYER_OBS_SIZE 36 +#define COLO_PILLAR_OBS_SIZE (COLO_NUM_PILLARS * 3) +#define COLO_INVENTORY_CELL_OBS_FEATURES OSRS_INVENTORY_CELL_OBS_FEATURES +#define COLO_INVENTORY_OBS_SIZE \ + (COLO_INVENTORY_DISPLAY_SLOTS * COLO_INVENTORY_CELL_OBS_FEATURES) +#define COLO_EQUIPPED_SELF_OBS_SIZE \ + (NUM_GEAR_SLOTS * OSRS_EQUIPPED_SELF_OBS_FEATURES) +static_assert(COLO_INVENTORY_OBS_SIZE + COLO_EQUIPPED_SELF_OBS_SIZE == + COLO_INVENTORY_OBS_CACHE_FLOATS, + "inventory obs cache float count must match the inventory+equipped block size"); +#define COLO_FEATURES_PER_NPC 37 +#define COLO_NPC_OBS_SIZE (COLO_OBS_NPCS * COLO_FEATURES_PER_NPC) + +#define COLO_EXPECTED_DPT_NORM 15.0f + +#define COLO_MODIFIER_FLAGS_OBS_SIZE (2 * COLO_NUM_MODIFIERS + 1 + COLO_MODIFIER_DRAFT_OPTIONS) +#define COLO_MODIFIER_HAZARD_OBS_SIZE 38 +#define COLO_MODIFIER_OBS_SIZE (COLO_MODIFIER_FLAGS_OBS_SIZE + COLO_MODIFIER_HAZARD_OBS_SIZE) +#define COLO_WAVE_OBS_SIZE 4 +#define COLO_BOSS_OBS_SIZE 51 + +#define COLO_OBS_PENDING_HITS 16 +#define COLO_FEATURES_PER_HIT 7 +#define COLO_PENDING_HIT_OBS_SIZE (COLO_FEATURES_PER_HIT * COLO_OBS_PENDING_HITS) + +#define COLO_THREAT_LOS_OBS_SIZE 3 + +#define COLO_THRALL_DC_OBS_SIZE 6 + +#define COLO_CELL_WEAPON_DPT_OBS_SIZE OSRS_INVENTORY_SIZE +#define COLO_CELL_SPEC_OBS_SIZE OSRS_INVENTORY_SIZE +#define COLO_WIELDED_DPT_OBS_SIZE 2 +#define COLO_WEAPON_CHOICE_OBS_SIZE (COLO_CELL_WEAPON_DPT_OBS_SIZE + \ + COLO_CELL_SPEC_OBS_SIZE + COLO_WIELDED_DPT_OBS_SIZE) +static_assert(COLO_WEAPON_CHOICE_OBS_SIZE == COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS, + "weapon-choice obs cache float count must match the block size"); + +#define COLO_SPAWN_PHASE_OBS_SIZE 2 +#define COLO_ANCHOR_BLOCKED_OBS_SIZE COLO_NUM_SPAWN_ANCHORS +#define COLO_SPAWN_OBS_SIZE (COLO_SPAWN_PHASE_OBS_SIZE + COLO_ANCHOR_BLOCKED_OBS_SIZE) + +#define COLO_THREAT_FIELD_RADIUS 8 +#define COLO_THREAT_FIELD_DIM (2 * COLO_THREAT_FIELD_RADIUS + 1) +#define COLO_THREAT_FIELD_TILES (COLO_THREAT_FIELD_DIM * COLO_THREAT_FIELD_DIM) +#define COLO_THREAT_FIELD_OBS_SIZE (2 * COLO_THREAT_FIELD_TILES) +static_assert(COLO_THREAT_FIELD_OBS_SIZE == COLO_THREAT_FIELD_OBS_CACHE_FLOATS, + "threat-field obs cache float count must match the block size"); + +#define COLO_NUM_OBS (COLO_PLAYER_OBS_SIZE + COLO_PILLAR_OBS_SIZE + \ + COLO_INVENTORY_OBS_SIZE + COLO_EQUIPPED_SELF_OBS_SIZE + COLO_NPC_OBS_SIZE + \ + COLO_MODIFIER_OBS_SIZE + COLO_WAVE_OBS_SIZE + COLO_BOSS_OBS_SIZE + \ + COLO_PENDING_HIT_OBS_SIZE + COLO_STEP_OUT_FORECAST_OBS_SIZE + \ + COLO_THREAT_LOS_OBS_SIZE + COLO_THRALL_DC_OBS_SIZE + \ + COLO_WEAPON_CHOICE_OBS_SIZE + COLO_SPAWN_OBS_SIZE + \ + COLO_THREAT_FIELD_OBS_SIZE) + +#define COLO_OBS_AFTER_PLAYER COLO_PLAYER_OBS_SIZE +#define COLO_OBS_AFTER_PILLARS (COLO_OBS_AFTER_PLAYER + COLO_PILLAR_OBS_SIZE) +#define COLO_OBS_AFTER_INVENTORY (COLO_OBS_AFTER_PILLARS + COLO_INVENTORY_OBS_SIZE) +#define COLO_OBS_AFTER_EQUIPPED_SELF \ + (COLO_OBS_AFTER_INVENTORY + COLO_EQUIPPED_SELF_OBS_SIZE) +#define COLO_OBS_AFTER_NPCS (COLO_OBS_AFTER_EQUIPPED_SELF + COLO_NPC_OBS_SIZE) +#define COLO_OBS_AFTER_MODS (COLO_OBS_AFTER_NPCS + COLO_MODIFIER_OBS_SIZE) +#define COLO_OBS_AFTER_WAVE (COLO_OBS_AFTER_MODS + COLO_WAVE_OBS_SIZE) +#define COLO_OBS_AFTER_BOSS (COLO_OBS_AFTER_WAVE + COLO_BOSS_OBS_SIZE) +#define COLO_OBS_AFTER_PENDING_HITS (COLO_OBS_AFTER_BOSS + COLO_PENDING_HIT_OBS_SIZE) +#define COLO_OBS_AFTER_STEP_OUT_FORECAST \ + (COLO_OBS_AFTER_PENDING_HITS + COLO_STEP_OUT_FORECAST_OBS_SIZE) +#define COLO_OBS_AFTER_THREAT_LOS \ + (COLO_OBS_AFTER_STEP_OUT_FORECAST + COLO_THREAT_LOS_OBS_SIZE) +#define COLO_OBS_AFTER_THRALL_DC \ + (COLO_OBS_AFTER_THREAT_LOS + COLO_THRALL_DC_OBS_SIZE) +#define COLO_OBS_AFTER_WEAPON_CHOICE \ + (COLO_OBS_AFTER_THRALL_DC + COLO_WEAPON_CHOICE_OBS_SIZE) +#define COLO_OBS_AFTER_SPAWN \ + (COLO_OBS_AFTER_WEAPON_CHOICE + COLO_SPAWN_OBS_SIZE) +#define COLO_OBS_AFTER_THREAT_FIELD \ + (COLO_OBS_AFTER_SPAWN + COLO_THREAT_FIELD_OBS_SIZE) + +static inline float col_clamp01(float v) { + if (v < 0.0f) return 0.0f; + if (v > 1.0f) return 1.0f; + return v; +} + +static inline float col_obs_rel_x(int nx, int px) { + return (float)(nx - px) / (float)COLO_ARENA_WIDTH; +} + +static inline float col_obs_rel_y(int ny, int py) { + return (float)(ny - py) / (float)COLO_ARENA_HEIGHT; +} + +typedef struct { + OsrsVenatorMonster monsters[COLO_MAX_NPCS]; + int count; +} ColoVenatorPreviewTargets; + +static ColoNPC col_matchup_representative_npc(ColoNpcType type) { + if (type < 0 || type >= COLO_NUM_NPC_TYPES) abort(); + const ColoNpcStats* ns = &COLO_NPC_STATS[type]; + return (ColoNPC){ + .type = type, + .hp = ns->hp, + .max_hp = ns->hp, + .size = ns->size, + .active = 1, + .death_ticks = 0, + }; +} + +static void col_collect_venator_preview_targets( + const ColosseumState* s, + ColoVenatorPreviewTargets* out +) { + if (!s || !out) abort(); + memset(out, 0, sizeof(*out)); + for (int slot = 0; slot < COLO_MAX_NPCS; slot++) { + const ColoNPC* npc = &s->npcs[slot]; + if (!col_npc_is_live_enemy(npc)) continue; + out->monsters[out->count++] = col_venator_monster_from_npc(npc, slot); + } +} + +static int col_venator_extra_bounce_if_shot( + const ColosseumState* s, + const ColoVenatorPreviewTargets* targets, + int primary_slot +) { + if (!s || !targets || primary_slot < 0 || primary_slot >= COLO_MAX_NPCS) + abort(); + const ColoNPC* npc = &s->npcs[primary_slot]; + if (!col_npc_is_live_enemy(npc)) return 0; + OsrsVenatorMonster primary = col_venator_monster_from_npc(npc, primary_slot); + OsrsVenatorChain chain = + osrs_venator_resolve_chain(primary, targets->monsters, targets->count); + int extra = (int)chain.length - 1; + if (extra < 0 || extra > OSRS_VENATOR_MAX_CHAIN_HITS - 1) abort(); + return extra; +} + +typedef struct { + int found; + int index; + int x; + int y; + int distance; +} ColoNearestHazardTile; + +static inline ColoNearestHazardTile col_nearest_hazard_tile_none(void) { + return (ColoNearestHazardTile){ + .found = 0, + .index = -1, + .x = 0, + .y = 0, + .distance = 1 << 30, + }; +} + +static inline void col_nearest_hazard_tile_consider( + ColoNearestHazardTile* nearest, + int index, + int x, + int y, + int player_x, + int player_y +) { + int dx = abs(x - player_x); + int dy = abs(y - player_y); + int distance = dx > dy ? dx : dy; + if (distance < nearest->distance) { + nearest->found = 1; + nearest->index = index; + nearest->x = x; + nearest->y = y; + nearest->distance = distance; + } +} + +static inline void col_write_hazard_xy_value_if( + const ColosseumState* s, + int present, + int x, + int y, + float value, + float* obs, + int* idx +) { + int i = *idx; + if (present) { + obs[i++] = col_obs_rel_x(x, s->player.x); + obs[i++] = col_obs_rel_y(y, s->player.y); + obs[i++] = value; + } else { + obs[i++] = 0.0f; + obs[i++] = 0.0f; + obs[i++] = 0.0f; + } + *idx = i; +} + +static inline void col_write_nearest_hazard_xy_value( + const ColosseumState* s, + const ColoNearestHazardTile* nearest, + float value, + float* obs, + int* idx +) { + col_write_hazard_xy_value_if( + s, nearest->found, nearest->x, nearest->y, value, obs, idx); +} + +static inline void col_write_nearest_hazard_xy_values( + const ColosseumState* s, + const ColoNearestHazardTile* nearest, + float value0, + float value1, + float* obs, + int* idx +) { + int i = *idx; + if (nearest->found) { + obs[i++] = col_obs_rel_x(nearest->x, s->player.x); + obs[i++] = col_obs_rel_y(nearest->y, s->player.y); + obs[i++] = value0; + obs[i++] = value1; + } else { + obs[i++] = 0.0f; + obs[i++] = 0.0f; + obs[i++] = 0.0f; + obs[i++] = 0.0f; + } + *idx = i; +} + +static void col_refresh_current_obs_slots_ctx(ColosseumState* s, const ColosseumContext* ctx) { + (void)ctx; + for (int i = 0; i < COLO_OBS_NPCS; i++) s->current_obs_slots[i] = -1; + int slot = 0; + for (int i = 0; i < COLO_MAX_NPCS && slot < COLO_OBS_NPCS; i++) { + if (s->npcs[i].active) s->current_obs_slots[slot++] = i; + } +} + +static int col_obs_slot_is_targetable(const ColosseumState* s, int obs_slot) { + int npc_idx = s->current_obs_slots[obs_slot]; + if (npc_idx < 0 || npc_idx >= COLO_MAX_NPCS) return 0; + const ColoNPC* npc = &s->npcs[npc_idx]; + return col_npc_is_live_target(npc); +} + +static void col_tell_style_onehot(AttackStyle style, float* out3) { + out3[0] = (style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; + out3[1] = (style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; + out3[2] = (style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; +} + +typedef struct { + int active; + AttackStyle style; + int ticks; +} ColoNpcNextPrayerObs; + +static inline int col_prayer_attack_style(AttackStyle style) { + return style == ATTACK_STYLE_MELEE || + style == ATTACK_STYLE_RANGED || + style == ATTACK_STYLE_MAGIC; +} + +static inline ColoNpcNextPrayerObs col_npc_next_prayer_none(void) { + return (ColoNpcNextPrayerObs){ + .active = 0, + .style = ATTACK_STYLE_NONE, + .ticks = 0, + }; +} + +static void col_write_style_onehot_melee_ranged_magic(AttackStyle style, float* obs, int* idx) { + int i = *idx; + obs[i++] = (style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; + obs[i++] = (style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; + obs[i++] = (style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; + *idx = i; +} + +static int col_pending_hit_prayer_ticks(const EncounterPendingHit* hit) { + return hit->prayer_check_delay > 0 ? hit->prayer_check_delay : hit->ticks_remaining; +} + +static ColoNpcNextPrayerObs col_source_matched_pending_prayer( + const ColosseumState* s, + int npc_slot +) { + ColoNpcNextPrayerObs best = col_npc_next_prayer_none(); + for (int h = 0; h < s->player_pending_hits.count && h < ENCOUNTER_MAX_PENDING_HITS; h++) { + const EncounterPendingHit* hit = &s->player_pending_hits.hits[h]; + if (!hit->active) continue; + if (!hit->check_prayer) continue; + if (hit->source_npc_slot != npc_slot) continue; + if (!col_prayer_attack_style((AttackStyle)hit->attack_style)) continue; + int ticks = col_pending_hit_prayer_ticks(hit); + if (!best.active || ticks < best.ticks) { + best.active = 1; + best.style = (AttackStyle)hit->attack_style; + best.ticks = ticks; + } + } + return best; +} + +static int col_manticore_next_orb_ticks(const ColoNPC* npc) { + const ColoManticoreState* mc = &npc->type_state.manticore; + if (mc->cycle_step > 0) return 1; + return npc->attack_timer == 0 ? 1 : npc->attack_timer; +} + +static ColoNpcNextPrayerObs col_visible_manticore_next_prayer(const ColoNPC* npc) { + if (npc->type != COLO_MANTICORE) return col_npc_next_prayer_none(); + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return col_npc_next_prayer_none(); + const ColoManticoreState* mc = &npc->type_state.manticore; + int armed = mc->cycle_step >= 0 || mc->orb_style[0] != ATTACK_STYLE_NONE; + if (!armed) return col_npc_next_prayer_none(); + int orb = mc->cycle_step >= 0 ? mc->cycle_step : 0; + if (orb < 0 || orb >= 3) return col_npc_next_prayer_none(); + AttackStyle style = mc->orb_style[orb]; + if (!col_prayer_attack_style(style)) return col_npc_next_prayer_none(); + return (ColoNpcNextPrayerObs){ + .active = 1, + .style = style, + .ticks = col_manticore_next_orb_ticks(npc), + }; +} + +static int col_npc_current_attack_geometry( + const ColosseumState* s, + const ColoNPC* npc, + const ColoNpcStats* ns +) { + if (ns->attack_range < 1) return 0; + if (npc->type == COLO_SOL_HEREDIT) return 0; + if (col_type_is_warbander(npc->type)) return col_warband_melee_adjacent(s, npc); + if (npc->type == COLO_MINOTAUR) return col_npc_dist_to_player(s, npc) == 1; + if (ns->attack_range > 1) { + int dist = col_npc_dist_to_player(s, npc); + if (dist < 1 || dist > ns->attack_range) return 0; + return col_npc_has_los_to_player(s, npc); + } + return col_npc_melee_adjacent_to_player(s, npc); +} + +static int col_warband_next_window_wait(const ColosseumState* s, const ColoNPC* npc) { + int target = col_warband_window_offset(npc->type); + for (int ticks = 1; ticks <= COLO_WARBAND_CYCLE_TICKS; ticks++) { + int phase = (s->tick + ticks - s->warband_cycle_anchor) % COLO_WARBAND_CYCLE_TICKS; + if (phase < 0) phase += COLO_WARBAND_CYCLE_TICKS; + if (phase == target) return ticks; + } + assert(0 && "warband window must recur within one cycle"); + return 0; +} + +static ColoNpcNextPrayerObs col_deterministic_npc_next_prayer( + const ColosseumState* s, + const ColoNPC* npc, + const ColoNpcStats* ns +) { + if (!col_prayer_attack_style(ns->default_style)) return col_npc_next_prayer_none(); + if (npc->stun_timer > 0 || npc->frozen_ticks > 0) return col_npc_next_prayer_none(); + if (npc->type == COLO_MANTICORE) return col_npc_next_prayer_none(); + if (npc->type == COLO_JAVELIN_COLOSSUS && + (npc->type_state.javelin.attack_count + 1) % 5 == 0) + return col_npc_next_prayer_none(); + if (!col_npc_current_attack_geometry(s, npc, ns)) return col_npc_next_prayer_none(); + int ticks = npc->attack_timer == 0 ? 1 : npc->attack_timer; + if (col_type_is_warbander(npc->type)) { + if (s->warband_cycle_anchor < 0) return col_npc_next_prayer_none(); + ticks = col_warband_next_window_wait(s, npc); + } + return (ColoNpcNextPrayerObs){ + .active = 1, + .style = ns->default_style, + .ticks = ticks, + }; +} + +static ColoNpcNextPrayerObs col_npc_next_prayer_obs( + const ColosseumState* s, + const ColoNPC* npc, + int npc_slot +) { + ColoNpcNextPrayerObs pending = col_source_matched_pending_prayer(s, npc_slot); + if (pending.active) return pending; + ColoNpcNextPrayerObs manticore = col_visible_manticore_next_prayer(npc); + if (manticore.active) return manticore; + return col_deterministic_npc_next_prayer(s, npc, &COLO_NPC_STATS[npc->type]); +} + +static const ColoBeeSwarm* col_obs_bee_state_for_npc_slot( + const ColosseumState* s, + int npc_slot +) { + if (npc_slot < 0 || npc_slot >= COLO_MAX_NPCS) return NULL; + for (int b = 0; b < COLO_MAX_BEE_SWARMS; b++) { + const ColoBeeSwarm* bee = &s->bees[b]; + if (bee->phase == COLO_HAZARD_ALIVE && bee->npc_slot == npc_slot) return bee; + } + return NULL; +} + +static void col_write_npc_tells(const ColosseumState* s, const ColoNPC* npc, float* obs, int* idx) { + int i = *idx; + float tells[10] = {0}; + int npc_slot = (int)(npc - s->npcs); + switch (npc->type) { + case COLO_MANTICORE: { + const ColoManticoreState* mc = &npc->type_state.manticore; + int firing = mc->cycle_step >= 0; + int armed = firing || mc->orb_style[0] != ATTACK_STYLE_NONE; + if (armed) { + int start = firing ? mc->cycle_step : 0; + int speed = COLO_NPC_STATS[npc->type].attack_speed; + + tells[0] = firing ? 0.0f + : col_clamp01((float)npc->attack_timer / (float)(speed > 0 ? speed : 1)); + for (int slot = 0; slot < 3; slot++) { + int orb = start + slot; + if (orb < 3) col_tell_style_onehot(mc->orb_style[orb], &tells[1 + slot * 3]); + } + } + break; + } + case COLO_JAVELIN_COLOSSUS: { + const ColoJavelinState* jv = &npc->type_state.javelin; + tells[0] = jv->skyfall_pending ? 1.0f : 0.0f; + tells[1] = jv->skyfall_pending + ? col_clamp01((float)jv->skyfall_timer / (float)COLO_JAVELIN_SKYFALL_DELAY) + : 0.0f; + tells[2] = jv->skyfall_pending + ? col_obs_rel_x(jv->skyfall_tile_x, s->player.x) + : 0.0f; + tells[3] = jv->skyfall_pending + ? col_obs_rel_y(jv->skyfall_tile_y, s->player.y) + : 0.0f; + break; + } + case COLO_BEE_SWARM: { + const ColoBeeSwarm* bee = col_obs_bee_state_for_npc_slot(s, npc_slot); + int next_x, next_y; + int size = col_npc_effective_size(npc); + col_bee_swarm_next_tile_toward_player(s, npc, &next_x, &next_y); + tells[0] = bee + ? col_clamp01((float)bee->move_timer / (float)COLO_BEE_MOVE_INTERVAL) + : 0.0f; + tells[1] = col_bee_swarm_contacts_player(s, npc) ? 1.0f : 0.0f; + tells[2] = col_obs_rel_x(next_x, s->player.x); + tells[3] = col_obs_rel_y(next_y, s->player.y); + tells[4] = encounter_entity_footprints_overlap( + next_x, next_y, size, s->player.x, s->player.y, 1) ? 1.0f : 0.0f; + break; + } + case COLO_MINOTAUR: { + int size = col_npc_effective_size(npc); + int dist = encounter_dist_to_npc(s->player.x, s->player.y, npc->x, npc->y, size); + tells[0] = (dist > 1) ? 1.0f : 0.0f; + break; + } + case COLO_JAGUAR_WARRIOR: + tells[0] = 1.0f; + break; + default: + break; + } + for (int t = 0; t < 10; t++) obs[i++] = tells[t]; + *idx = i; +} + +static void col_write_npc_slot( + const ColosseumState* s, + const ColoNPC* npc, + int npc_slot, + const ColoVenatorPreviewTargets* venator_targets, + float* obs, + int* idx +) { + int i = *idx; + int px = s->player.x, py = s->player.y; + const ColoNpcStats* ns = &COLO_NPC_STATS[npc->type]; + int size = col_npc_effective_size(npc); + + for (int t = 0; t < COLO_NUM_NPC_TYPES; t++) + obs[i++] = (npc->type == t) ? 1.0f : 0.0f; + obs[i++] = col_obs_rel_x(npc->x, px); + obs[i++] = col_obs_rel_y(npc->y, py); + obs[i++] = npc->max_hp > 0 ? col_clamp01((float)npc->hp / (float)npc->max_hp) : 0.0f; + *idx = i; + col_write_style_onehot_melee_ranged_magic(npc->attack_style, obs, idx); + i = *idx; + obs[i++] = col_clamp01((float)npc->attack_timer / 8.0f); + int dist = encounter_dist_to_npc(px, py, npc->x, npc->y, size); + int range = ns->attack_range > 0 ? ns->attack_range : 1; + obs[i++] = (dist >= 1 && dist <= range) ? 1.0f : 0.0f; + + ColoNpcNextPrayerObs next_prayer = col_npc_next_prayer_obs(s, npc, npc_slot); + obs[i++] = next_prayer.active ? 1.0f : 0.0f; + *idx = i; + col_write_style_onehot_melee_ranged_magic( + next_prayer.active ? next_prayer.style : ATTACK_STYLE_NONE, obs, idx); + i = *idx; + obs[i++] = next_prayer.active ? col_clamp01((float)next_prayer.ticks / 10.0f) : 0.0f; + obs[i++] = col_npc_has_los_to_player(s, npc) ? 1.0f : 0.0f; + *idx = i; + col_write_npc_tells(s, npc, obs, idx); + i = *idx; + int extra_bounces = col_venator_extra_bounce_if_shot(s, venator_targets, npc_slot); + obs[i++] = (float)extra_bounces / (float)(OSRS_VENATOR_MAX_CHAIN_HITS - 1); + *idx = i; +} + +static void col_write_nearest_armed_line(const ColosseumState* s, float* obs, int* idx) { + ColoNearestHazardTile nearest = col_nearest_hazard_tile_none(); + for (int k = 0; k < s->sol.crystal_count; k++) { + const ColoSolCrystal* c = &s->sol.crystals[k]; + if (!c->active || c->firing_freeze < COLO_SOL_LASER_DAMAGE_FREEZE) continue; + if (!col_sol_crystal_in_line(s, c)) continue; + col_nearest_hazard_tile_consider( + &nearest, k, c->x, c->y, s->player.x, s->player.y); + } + float ticks_frac = nearest.found + ? col_clamp01((float)(s->sol.crystals[nearest.index].firing_freeze + - COLO_SOL_LASER_DAMAGE_FREEZE) / + (float)(COLO_SOL_LASER_FREEZE - COLO_SOL_LASER_DAMAGE_FREEZE)) + : 0.0f; + col_write_nearest_hazard_xy_values(s, &nearest, ticks_frac, 1.0f, obs, idx); +} + +static void col_write_nearest_molten(const ColosseumState* s, float* obs, int* idx) { + ColoNearestHazardTile nearest = col_nearest_hazard_tile_none(); + for (int k = 0; k < s->sol.hazard_tile_count; k++) { + col_nearest_hazard_tile_consider( + &nearest, k, s->sol.hazard_tile_x[k], s->sol.hazard_tile_y[k], + s->player.x, s->player.y); + } + col_write_nearest_hazard_xy_value(s, &nearest, 1.0f, obs, idx); +} + +static int col_player_in_aoe_danger(const ColosseumState* s) { + const SolHereditState* sol = &s->sol; + if (sol->aoe_attack == COLO_SOL_AOE_NONE) return 0; + return col_sol_aoe_tile_is_hazard(sol, s->player.x, s->player.y); +} + +static void col_write_nearest_crystal(const ColosseumState* s, float* obs, int* idx) { + ColoNearestHazardTile nearest = col_nearest_hazard_tile_none(); + for (int k = 0; k < s->sol.crystal_count; k++) { + const ColoSolCrystal* ck = &s->sol.crystals[k]; + if (!ck->active) continue; + col_nearest_hazard_tile_consider( + &nearest, k, ck->x, ck->y, s->player.x, s->player.y); + } + const ColoSolCrystal* c = nearest.found ? &s->sol.crystals[nearest.index] : NULL; + col_write_nearest_hazard_xy_values( + s, + &nearest, + c && c->firing_freeze > 0 ? 1.0f : 0.0f, + c ? col_clamp01((float)c->firing_freeze / (float)COLO_SOL_LASER_FREEZE) : 0.0f, + obs, + idx); +} + +static void col_write_nearest_beam(const ColosseumState* s, float* obs, int* idx) { + ColoNearestHazardTile nearest = col_nearest_hazard_tile_none(); + for (int k = 0; k < COLO_SOL_BEAM_MAX; k++) { + if (!s->sol.beams[k].active) continue; + col_nearest_hazard_tile_consider( + &nearest, k, s->sol.beams[k].x, s->sol.beams[k].y, + s->player.x, s->player.y); + } + float ticks_frac = nearest.found + ? col_clamp01((float)s->sol.beams[nearest.index].ticks_to_pool / + (float)COLO_SOL_BEAM_TO_POOL_TICKS) + : 0.0f; + col_write_nearest_hazard_xy_values(s, &nearest, ticks_frac, 1.0f, obs, idx); +} + +static void col_write_boss_obs(const ColosseumState* s, float* obs, int* idx) { + int i = *idx; + const SolHereditState* sol = &s->sol; + int px = s->player.x, py = s->player.y; + + obs[i++] = (float)sol->phase / (float)(COLO_SOL_NUM_PHASES - 1); + for (int p = 0; p < COLO_SOL_NUM_PHASES; p++) + obs[i++] = (sol->phase == p) ? 1.0f : 0.0f; + + obs[i++] = (sol->aoe_attack == COLO_SOL_AOE_NONE) ? 1.0f : 0.0f; + obs[i++] = (sol->aoe_attack == COLO_SOL_AOE_SPEAR1) ? 1.0f : 0.0f; + obs[i++] = (sol->aoe_attack == COLO_SOL_AOE_SPEAR2) ? 1.0f : 0.0f; + obs[i++] = (sol->aoe_attack == COLO_SOL_AOE_SHIELD1) ? 1.0f : 0.0f; + obs[i++] = (sol->aoe_attack == COLO_SOL_AOE_SHIELD2) ? 1.0f : 0.0f; + obs[i++] = col_clamp01((float)sol->aoe_age / (float)COLO_SOL_AOE_LIFETIME); + obs[i++] = col_player_in_aoe_danger(s) ? 1.0f : 0.0f; + if (sol->boss_idx >= 0) { + const ColoNPC* boss = &s->npcs[sol->boss_idx]; + int bsz = boss->size > 0 ? boss->size : 5; + obs[i++] = col_obs_rel_x(px, boss->x + bsz / 2); + obs[i++] = col_obs_rel_y(py, boss->y + bsz / 2); + } else { + obs[i++] = 0.0f; obs[i++] = 0.0f; + } + + if (sol->aoe_attack == COLO_SOL_AOE_NONE) { + obs[i++] = 0.0f; + obs[i++] = 0.0f; + } else { + int adx, ady; + col_sol_aoe_effective_dir(sol, px, py, &adx, &ady); + obs[i++] = (float)adx; + obs[i++] = (float)ady; + } + + obs[i++] = col_clamp01((float)sol->attack_delay / 12.0f); + obs[i++] = col_clamp01((float)sol->immobile_ticks / 10.0f); + obs[i++] = sol->special_cooldown <= 0 ? 1.0f : 0.0f; + obs[i++] = sol->force_spear ? 1.0f : 0.0f; + + int next_land = 0, next_dmg = 0; + for (int h = 0; h < COLO_SOL_PARRY_HITS; h++) { + if (sol->parry_land_in[h] <= 0) continue; + if (next_land == 0 || sol->parry_land_in[h] < next_land) { + next_land = sol->parry_land_in[h]; + next_dmg = sol->parry_damage[h]; + } + } + obs[i++] = sol->parry_hits_left > 0 ? 1.0f : 0.0f; + obs[i++] = col_clamp01((float)next_land / 10.0f); + obs[i++] = col_clamp01((float)next_dmg / 45.0f); + obs[i++] = col_clamp01((float)sol->parry_hits_left / (float)COLO_SOL_PARRY_HITS); + obs[i++] = sol->phase >= 3 ? 1.0f : 0.0f; + + obs[i++] = sol->grapple_active ? 1.0f : 0.0f; + for (int g = 0; g < COLO_NUM_GRAPPLE_SLOTS; g++) + obs[i++] = (sol->grapple_active && sol->grapple_body_slot == g) ? 1.0f : 0.0f; + obs[i++] = col_clamp01((float)sol->grapple_timer / (float)COLO_SOL_GRAPPLE_WINDOW); + obs[i++] = sol->next_attack_guaranteed_max ? 1.0f : 0.0f; + + obs[i++] = col_clamp01((float)sol->crystal_count / (float)COLO_SOL_MAX_CRYSTALS); + *idx = i; + col_write_nearest_crystal(s, obs, idx); + col_write_nearest_armed_line(s, obs, idx); + + col_write_nearest_molten(s, obs, idx); + + col_write_nearest_beam(s, obs, idx); +} + +static inline int col_pending_hit_ignores_prayer(const EncounterPendingHit* ph) { + return (!ph->check_prayer && ph->attack_style != ATTACK_STYLE_NONE) ? 1 : 0; +} + +static int col_collect_obs_pending_hits(const ColosseumState* s, int* out) { + int n = 0; + for (int h = 0; h < s->player_pending_hits.count && h < ENCOUNTER_MAX_PENDING_HITS; h++) { + if (!s->player_pending_hits.hits[h].active) continue; + out[n++] = h; + } + for (int a = 1; a < n; a++) { + int key = out[a]; + int kt = s->player_pending_hits.hits[key].ticks_remaining; + int b = a - 1; + while (b >= 0 && s->player_pending_hits.hits[out[b]].ticks_remaining > kt) { + out[b + 1] = out[b]; + b--; + } + out[b + 1] = key; + } + return n < COLO_OBS_PENDING_HITS ? n : COLO_OBS_PENDING_HITS; +} + +#define COLO_MODIFIER_MOLTEN_OBS_COUNT 4 + +typedef struct { + int found; + int npc_slot; + int x; + int y; + int size; + int ticks; + int distance; +} ColoVolatilityBlastPreview; + +static inline ColoVolatilityBlastPreview col_volatility_blast_preview_none(void) { + return (ColoVolatilityBlastPreview){ + .found = 0, + .npc_slot = -1, + .x = 0, + .y = 0, + .size = 0, + .ticks = 0, + .distance = 1 << 30, + }; +} + +static int col_pending_hit_will_damage_pre_move( + const EncounterPendingHit* hit, + OverheadPrayer prayer +) { + if (!hit->active) return 0; + if (hit->ticks_remaining > 1) return 0; + if (hit->damage <= 0) return 0; + if (hit->attack_style == ATTACK_STYLE_NONE) return 0; + if (hit->check_prayer && + encounter_prayer_correct_for_style(prayer, hit->attack_style)) + return 0; + return 1; +} + +static void col_write_top_modifier_molten(const ColosseumState* s, float* obs, int* idx) { + ColoNearestHazardTile nearest[COLO_MODIFIER_MOLTEN_OBS_COUNT]; + for (int i = 0; i < COLO_MODIFIER_MOLTEN_OBS_COUNT; i++) + nearest[i] = col_nearest_hazard_tile_none(); + for (int k = 0; k < s->molten_count; k++) { + int dx = abs(s->molten_x[k] - s->player.x); + int dy = abs(s->molten_y[k] - s->player.y); + int distance = dx > dy ? dx : dy; + for (int slot = 0; slot < COLO_MODIFIER_MOLTEN_OBS_COUNT; slot++) { + if (distance >= nearest[slot].distance) continue; + for (int move = COLO_MODIFIER_MOLTEN_OBS_COUNT - 1; move > slot; move--) + nearest[move] = nearest[move - 1]; + nearest[slot] = (ColoNearestHazardTile){ + .found = 1, + .index = k, + .x = s->molten_x[k], + .y = s->molten_y[k], + .distance = distance, + }; + break; + } + } + for (int k = 0; k < COLO_MODIFIER_MOLTEN_OBS_COUNT; k++) { + col_write_nearest_hazard_xy_value(s, &nearest[k], 1.0f, obs, idx); + } +} + +static void col_write_nearest_modifier_solarflare( + const ColosseumState* s, + float* obs, + int* idx +) { + ColoNearestHazardTile nearest = col_nearest_hazard_tile_none(); + if (s->solarflare.active) { + for (int p = 0; p < COLO_NUM_PILLARS; p++) { + int x, y; + col_solarflare_tile(s, p, s->solarflare.step, &x, &y); + col_nearest_hazard_tile_consider( + &nearest, p, x, y, s->player.x, s->player.y); + } + } + col_write_nearest_hazard_xy_value(s, &nearest, 1.0f, obs, idx); + int i = *idx; + if (nearest.found) { + int tier = col_mod_tier(s, COLO_MOD_SOLARFLARE); + int next_step = s->solarflare.step + 1; + if (next_step == COLO_SOLARFLARE_RING_STEPS) next_step = 0; + int next_x, next_y; + col_solarflare_tile(s, nearest.index, next_step, &next_x, &next_y); + int move_interval = tier >= 3 + ? COLO_SOLARFLARE_MOVE_INTERVAL_T3 : COLO_SOLARFLARE_MOVE_INTERVAL; + int pause_interval = tier >= 3 ? COLO_SOLARFLARE_CORNER_PAUSE_T3 : 1; + if (tier == 1) pause_interval = COLO_SOLARFLARE_CORNER_PAUSE; + obs[i++] = col_clamp01( + (float)s->solarflare.step / (float)COLO_SOLARFLARE_RING_STEPS); + obs[i++] = col_clamp01((float)s->solarflare.move_timer / (float)move_interval); + obs[i++] = col_clamp01((float)s->solarflare.pause_timer / (float)pause_interval); + obs[i++] = col_obs_rel_x(next_x, s->player.x); + obs[i++] = col_obs_rel_y(next_y, s->player.y); + } else { + for (int k = 0; k < 5; k++) obs[i++] = 0.0f; + } + *idx = i; +} + +static ColoVolatilityBlastPreview col_soonest_queued_volatility_blast( + const ColosseumState* s +) { + ColoVolatilityBlastPreview best = col_volatility_blast_preview_none(); + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s->npcs[n]; + if (!col_npc_is_live_enemy(npc)) continue; + for (int h = 0; h < npc->pending_hits.count; h++) { + const EncounterPendingHit* hit = &npc->pending_hits.hits[h]; + if (!hit->active) continue; + int ticks = hit->ticks_remaining < 0 ? 0 : hit->ticks_remaining; + int damage = 0; + for (int j = 0; j < npc->pending_hits.count; j++) { + const EncounterPendingHit* other = &npc->pending_hits.hits[j]; + if (!other->active) continue; + int other_ticks = other->ticks_remaining < 0 ? 0 : other->ticks_remaining; + if (other_ticks <= ticks) damage += other->damage; + } + if (damage < npc->hp) continue; + int size = col_npc_effective_size(npc); + int center_x = npc->x + size / 2; + int center_y = npc->y + size / 2; + int dx = abs(center_x - s->player.x); + int dy = abs(center_y - s->player.y); + int distance = dx > dy ? dx : dy; + if (best.found && + (ticks > best.ticks || + (ticks == best.ticks && distance >= best.distance))) + continue; + best = (ColoVolatilityBlastPreview){ + .found = 1, + .npc_slot = n, + .x = npc->x, + .y = npc->y, + .size = size, + .ticks = ticks, + .distance = distance, + }; + } + } + return best; +} + +static void col_write_volatility_blast_previews( + const ColosseumState* s, + float* obs, + int* idx +) { + int i = *idx; + int tier = col_mod_tier(s, COLO_MOD_VOLATILITY); + int reach = col_volatility_reach_for_tier(tier); + int target = col_player_target_slot(s); + if (tier > 0 && target >= 0 && target < COLO_MAX_NPCS && + col_npc_is_live_enemy(&s->npcs[target])) { + const ColoNPC* npc = &s->npcs[target]; + int size = col_npc_effective_size(npc); + int center_x = npc->x + size / 2; + int center_y = npc->y + size / 2; + obs[i++] = col_obs_rel_x(center_x, s->player.x); + obs[i++] = col_obs_rel_y(center_y, s->player.y); + obs[i++] = col_clamp01((float)reach / 2.0f); + obs[i++] = col_volatility_player_in_blast(s, npc->x, npc->y, size, tier) + ? 1.0f : 0.0f; + } else { + for (int k = 0; k < 4; k++) obs[i++] = 0.0f; + } + + ColoVolatilityBlastPreview queued = col_soonest_queued_volatility_blast(s); + if (tier > 0 && queued.found) { + int center_x = queued.x + queued.size / 2; + int center_y = queued.y + queued.size / 2; + obs[i++] = col_obs_rel_x(center_x, s->player.x); + obs[i++] = col_obs_rel_y(center_y, s->player.y); + obs[i++] = col_clamp01((float)queued.ticks / 10.0f); + obs[i++] = col_volatility_player_in_blast( + s, queued.x, queued.y, queued.size, tier) ? 1.0f : 0.0f; + } else { + for (int k = 0; k < 4; k++) obs[i++] = 0.0f; + } + *idx = i; +} + +static int col_count_pre_move_doom_instances(const ColosseumState* s) { + int count = 0; + for (int h = 0; h < s->player_pending_hits.count; h++) { + const EncounterPendingHit* hit = &s->player_pending_hits.hits[h]; + if (col_pending_hit_will_damage_pre_move(hit, s->player.prayer)) count++; + } + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s->npcs[n]; + if (!col_npc_is_live_target(npc) || npc->type != COLO_JAVELIN_COLOSSUS) + continue; + const ColoJavelinState* jv = &npc->type_state.javelin; + if (jv->skyfall_pending && jv->skyfall_timer <= 1 && + jv->skyfall_damage > 0 && + s->player.x == jv->skyfall_tile_x && + s->player.y == jv->skyfall_tile_y) + count++; + } + for (int b = 0; b < COLO_MAX_BEE_SWARMS; b++) { + const ColoBeeSwarm* bee = &s->bees[b]; + if (bee->phase != COLO_HAZARD_ALIVE || bee->npc_slot < 0) continue; + const ColoNPC* npc = &s->npcs[bee->npc_slot]; + if (!npc->active || npc->type != COLO_BEE_SWARM) continue; + int contact = 0; + if (bee->move_timer <= 1) { + int next_x, next_y; + col_bee_swarm_next_tile_toward_player(s, npc, &next_x, &next_y); + int size = col_npc_effective_size(npc); + contact = encounter_entity_footprints_overlap( + next_x, next_y, size, s->player.x, s->player.y, 1); + } else { + contact = col_bee_swarm_contacts_player(s, npc); + } + if (contact) count++; + } + if (s->solarflare.active && col_mod_tier(s, COLO_MOD_SOLARFLARE) > 0) { + int solarflare_contact_step = s->solarflare.step; + if (s->solarflare.pause_timer <= 0 && s->solarflare.move_timer <= 1) { + solarflare_contact_step++; + if (solarflare_contact_step == COLO_SOLARFLARE_RING_STEPS) + solarflare_contact_step = 0; + } + for (int p = 0; p < COLO_NUM_PILLARS; p++) { + int x, y; + col_solarflare_tile(s, p, solarflare_contact_step, &x, &y); + if (x == s->player.x && y == s->player.y) count++; + } + } + for (int m = 0; m < s->molten_count; m++) { + if (s->molten_x[m] == s->player.x && s->molten_y[m] == s->player.y) { + count++; + break; + } + } + if (s->player_venom > 0 && s->player_venom_timer <= 1) count++; + int volatility_tier = col_mod_tier(s, COLO_MOD_VOLATILITY); + if (volatility_tier > 0) { + ColoVolatilityBlastPreview queued = col_soonest_queued_volatility_blast(s); + if (queued.found && queued.ticks <= 1 && + col_volatility_player_in_blast( + s, queued.x, queued.y, queued.size, volatility_tier)) + count++; + } + return count; +} + +static void col_write_modifier_hazards(const ColosseumState* s, float* obs, int* idx) { + int i = *idx; + int doom_tier = col_mod_tier(s, COLO_MOD_DOOM); + int doom_cap = doom_tier > 0 ? COLO_DOOM_CAP[doom_tier] : 0; + int doom_pending = doom_cap > 0 ? col_count_pre_move_doom_instances(s) : 0; + obs[i++] = doom_tier > 0 ? 1.0f : 0.0f; + obs[i++] = doom_cap > 0 ? col_clamp01((float)s->doom_stacks / (float)doom_cap) : 0.0f; + obs[i++] = doom_cap > 0 && s->doom_stacks + 1 >= doom_cap ? 1.0f : 0.0f; + obs[i++] = doom_cap > 0 ? col_clamp01((float)doom_pending / (float)doom_cap) : 0.0f; + + obs[i++] = s->player_venom > 0 ? 1.0f : 0.0f; + obs[i++] = col_clamp01((float)s->player_venom / (float)COLO_VENOM_CAP); + obs[i++] = s->player_venom > 0 + ? col_clamp01((float)s->player_venom_timer / (float)COLO_VENOM_INTERVAL) + : 0.0f; + + ColoNearestHazardTile nearest_bee = col_nearest_hazard_tile_none(); + for (int b = 0; b < COLO_MAX_BEE_SWARMS; b++) { + if (s->bees[b].phase != COLO_HAZARD_ALIVE) continue; + const ColoNPC* npc = &s->npcs[s->bees[b].npc_slot]; + if (!npc->active || npc->type != COLO_BEE_SWARM) continue; + col_nearest_hazard_tile_consider( + &nearest_bee, b, npc->x, npc->y, s->player.x, s->player.y); + } + *idx = i; + col_write_nearest_hazard_xy_value(s, &nearest_bee, 1.0f, obs, idx); + i = *idx; + + *idx = i; + col_write_nearest_modifier_solarflare(s, obs, idx); + col_write_top_modifier_molten(s, obs, idx); + col_write_volatility_blast_previews(s, obs, idx); +} + +static void col_write_threat_los_counts(const ColosseumState* s, float* obs, int* idx) { + int i = *idx; + int magic, ranged, melee; + col_live_threat_style_counts(s, &magic, &ranged, &melee); + obs[i++] = col_clamp01((float)magic / 4.0f); + obs[i++] = col_clamp01((float)ranged / 4.0f); + obs[i++] = col_clamp01((float)melee / 4.0f); + *idx = i; +} + +static uint64_t col_threat_field_obs_signature(const ColosseumState* s) { + uint64_t h = 1469598103934665603ULL; + h = (h ^ (uint64_t)(uint32_t)s->player.x) * 1099511628211ULL; + h = (h ^ (uint64_t)(uint32_t)s->player.y) * 1099511628211ULL; + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s->npcs[n]; + if (!col_npc_is_live_enemy(npc)) continue; + int range = COLO_NPC_STATS[npc->type].attack_range; + if (range <= 1) continue; + uint64_t shooter = (uint64_t)(uint32_t)npc->x + | ((uint64_t)(uint32_t)npc->y << 8) + | ((uint64_t)(uint32_t)col_npc_effective_size(npc) << 16) + | ((uint64_t)(uint32_t)range << 24); + h = (h ^ shooter) * 1099511628211ULL; + } + const uint8_t* grid = &s->npc_collision_flags[0][0]; + const size_t grid_bytes = sizeof(s->npc_collision_flags); + size_t b = 0; + for (; b + 8 <= grid_bytes; b += 8) { + uint64_t word; + memcpy(&word, grid + b, 8); + h = (h ^ word) * 1099511628211ULL; + } + for (; b < grid_bytes; b++) + h = (h ^ (uint64_t)grid[b]) * 1099511628211ULL; + return h; +} + +static void col_write_threat_field_obs(ColosseumState* s, float* obs, int* idx) { + int i = *idx; + if (!COLO_STATIC_LOOKUP_TABLES_BUILT) col_build_static_arena(); + + ColoObsMemoCaches* memos = &s->obs_memos; + uint64_t sig = col_threat_field_obs_signature(s); + for (int w = 0; w < COLO_THREAT_FIELD_OBS_CACHE_WAYS; w++) { + if (memos->threat_field_valid[w] && memos->threat_field_sig[w] == sig) { + memcpy(&obs[i], memos->threat_field[w], + sizeof(float) * COLO_THREAT_FIELD_OBS_CACHE_FLOATS); + *idx = i + COLO_THREAT_FIELD_OBS_CACHE_FLOATS; + return; + } + } + int block_start = i; + + int shooter_x0[COLO_MAX_NPCS], shooter_y0[COLO_MAX_NPCS]; + int shooter_x1[COLO_MAX_NPCS], shooter_y1[COLO_MAX_NPCS]; + int shooter_ranges[COLO_MAX_NPCS]; + int n_shooters = 0; + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s->npcs[n]; + if (!col_npc_is_live_enemy(npc)) continue; + int range = COLO_NPC_STATS[npc->type].attack_range; + if (range <= 1) continue; + int size = col_npc_effective_size(npc); + shooter_x0[n_shooters] = npc->x; + shooter_y0[n_shooters] = npc->y; + shooter_x1[n_shooters] = npc->x + size - 1; + shooter_y1[n_shooters] = npc->y + size - 1; + shooter_ranges[n_shooters] = range; + n_shooters++; + } + for (int dy = -COLO_THREAT_FIELD_RADIUS; dy <= COLO_THREAT_FIELD_RADIUS; dy++) { + for (int dx = -COLO_THREAT_FIELD_RADIUS; dx <= COLO_THREAT_FIELD_RADIUS; dx++) { + int tx = s->player.x + dx, ty = s->player.y + dy; + int gx, gy; + int count = 0; + if (col_grid_index(tx, ty, &gx, &gy)) { + for (int k = 0; k < n_shooters; k++) { + int sx = tx < shooter_x0[k] ? shooter_x0[k] + : (tx > shooter_x1[k] ? shooter_x1[k] : tx); + int sy = ty < shooter_y0[k] ? shooter_y0[k] + : (ty > shooter_y1[k] ? shooter_y1[k] : ty); + int adx = tx - sx; if (adx < 0) adx = -adx; + int ady = ty - sy; if (ady < 0) ady = -ady; + int dist = adx > ady ? adx : ady; + if (dist < 1 || dist > shooter_ranges[k]) continue; + if (!col_static_los_get(sx, sy, tx, ty)) continue; + count++; + } + } + obs[i++] = col_clamp01((float)count / 4.0f); + } + } + for (int dy = -COLO_THREAT_FIELD_RADIUS; dy <= COLO_THREAT_FIELD_RADIUS; dy++) { + for (int dx = -COLO_THREAT_FIELD_RADIUS; dx <= COLO_THREAT_FIELD_RADIUS; dx++) { + int tx = s->player.x + dx, ty = s->player.y + dy; + int gx, gy; + int blocked = 1; + if (col_grid_index(tx, ty, &gx, &gy)) + blocked = col_static_blocked(tx, ty) || s->npc_collision_flags[gx][gy]; + obs[i++] = blocked ? 1.0f : 0.0f; + } + } + + int slot = memos->threat_field_next; + memcpy(memos->threat_field[slot], &obs[block_start], + sizeof(float) * COLO_THREAT_FIELD_OBS_CACHE_FLOATS); + memos->threat_field_sig[slot] = sig; + memos->threat_field_valid[slot] = 1; + memos->threat_field_next = (slot + 1) % COLO_THREAT_FIELD_OBS_CACHE_WAYS; + + *idx = i; +} + +static void col_write_thrall_dc(const ColosseumState* s, float* obs, int* idx) { + int i = *idx; + int lifetime_full = COLO_THRALL_LIFETIME_TICKS[s->active_loadout_profile]; + obs[i++] = s->thrall_active ? 1.0f : 0.0f; + obs[i++] = (s->thrall_active && lifetime_full > 0) + ? col_clamp01((float)s->thrall_lifetime_left / (float)lifetime_full) : 0.0f; + obs[i++] = col_clamp01((float)s->thrall_recast_cd / (float)COLO_THRALL_RECAST_CD); + obs[i++] = s->death_charge_window_left > 0 ? 1.0f : 0.0f; + obs[i++] = col_clamp01((float)s->death_charge_window_left / (float)COLO_DEATH_CHARGE_WINDOW); + obs[i++] = col_clamp01((float)s->death_charge_cd / (float)COLO_DEATH_CHARGE_CD); + *idx = i; +} + +static void col_write_pillar_obs(const ColosseumState* s, float* obs, int* idx) { + int i = *idx; + for (int p = 0; p < COLO_NUM_PILLARS; p++) { + obs[i++] = col_obs_rel_x(COLO_PILLARS[p][0], s->player.x); + obs[i++] = col_obs_rel_y(COLO_PILLARS[p][1], s->player.y); + obs[i++] = col_clamp01((float)COLO_PILLAR_SIZE / 7.0f); + } + *idx = i; +} + +static void col_equipment_bonus_projection( + const uint8_t equipment[NUM_GEAR_SLOTS], + float out[6] +) { + int slash = 0; + int melee_str = 0; + int ranged_att = 0; + int ranged_str = 0; + int magic = 0; + int defence = 0; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + uint8_t item_idx = equipment[slot]; + if (item_idx == ITEM_NONE) continue; + if (item_idx >= NUM_ITEMS) { + fprintf(stderr, "colosseum obs: invalid equipped item %u\n", item_idx); + abort(); + } + const Item* item = &ITEM_DATABASE[item_idx]; + slash += item->attack_slash; + melee_str += item->melee_strength; + ranged_att += item->attack_ranged; + ranged_str += item->ranged_strength; + magic += item->attack_magic + item->magic_damage; + defence += item->defence_stab + item->defence_slash + item->defence_crush + + item->defence_magic + item->defence_ranged; + } + out[0] = (float)slash / STAT_NORM_ATTACK; + out[1] = (float)melee_str / STAT_NORM_STRENGTH; + out[2] = (float)ranged_att / STAT_NORM_ATTACK; + out[3] = (float)ranged_str / STAT_NORM_STRENGTH; + out[4] = (float)magic / (STAT_NORM_ATTACK + STAT_NORM_MAGIC_DMG); + out[5] = (float)defence / (5.0f * STAT_NORM_DEFENCE); +} + +static void col_hypothetical_equipment_after_cell( + const ColosseumState* s, + int cell_idx, + uint8_t out[NUM_GEAR_SLOTS] +) { + memcpy(out, s->player.equipped, NUM_GEAR_SLOTS); + const ColoInvCell* cell = &s->inventory_cells[cell_idx]; + if (cell->item_idx == ITEM_NONE) return; + int gear_slot = osrs_item_gear_slot(cell->item_idx); + if (gear_slot < 0) return; + if (gear_slot == GEAR_SLOT_WEAPON && item_is_two_handed(cell->item_idx)) + out[GEAR_SLOT_SHIELD] = ITEM_NONE; + if (gear_slot == GEAR_SLOT_SHIELD && item_is_two_handed(out[GEAR_SLOT_WEAPON])) + out[GEAR_SLOT_WEAPON] = ITEM_NONE; + out[gear_slot] = cell->item_idx; +} + +static float col_expected_dpt_for_equipment_vs_npc( + ColosseumState* s, + const uint8_t equipment[NUM_GEAR_SLOTS], + const ColoNPC* npc, + int use_style_matched_prayer +) { + if (!s || !equipment || !npc) abort(); + if (!col_npc_is_live_target(npc) || col_type_is_hazard_entity(npc->type)) + return 0.0f; + + uint8_t weapon_item = equipment[GEAR_SLOT_WEAPON]; + AttackStyle style = ATTACK_STYLE_MELEE; + int item_style = get_item_attack_style(weapon_item); + if (item_style == 2) style = ATTACK_STYLE_RANGED; + else if (item_style == 3) style = ATTACK_STYLE_MAGIC; + else if (item_style != 1) return 0.0f; + + FightStyle fight_style = style == ATTACK_STYLE_RANGED ? FIGHT_STYLE_RAPID + : style == ATTACK_STYLE_MAGIC ? FIGHT_STYLE_ACCURATE + : FIGHT_STYLE_AGGRESSIVE; + int current_att_level; + int current_str_level; + col_current_style_levels( + &s->player, style, ¤t_att_level, ¤t_str_level); + + OffensivePrayer dpt_prayer = use_style_matched_prayer + ? encounter_offensive_prayer_for_style(style) + : s->player.offensive_prayer; + EncounterLoadoutStats stats; + encounter_compute_loadout_stats( + equipment, + style, + dpt_prayer, + current_att_level, + fight_style, + style == ATTACK_STYLE_MAGIC + ? col_weapon_set_spell_base_damage(s, COLO_GEAR_MAGIC) : 0, + &stats); + encounter_update_loadout_level( + &stats, + dpt_prayer, + current_att_level, + current_str_level); + + OsrsEquipmentEffectProfile effects; + encounter_derive_loadout_effect_profile(equipment, &effects); + const ColoNpcStats* ns = &COLO_NPC_STATS[npc->type]; + return col_expected_dpt_vs_npc( + &stats, + &effects, + weapon_item, + style, + col_item_melee_style(weapon_item), + npc, + ns); +} + +#define COLO_BEST_GEAR_MAX_CAND 16 + +typedef struct { + uint8_t items[NUM_GEAR_SLOTS][COLO_BEST_GEAR_MAX_CAND]; + int count[NUM_GEAR_SLOTS]; +} ColoGearCandidates; + +static void col_gear_candidates_add(ColoGearCandidates* c, int slot, uint8_t item) { + if (item == ITEM_NONE) return; + for (int k = 0; k < c->count[slot]; k++) + if (c->items[slot][k] == item) return; + if (c->count[slot] >= COLO_BEST_GEAR_MAX_CAND) { + fprintf(stderr, "colosseum best-gear candidate overflow at slot %d\n", slot); + abort(); + } + c->items[slot][c->count[slot]++] = item; +} + +static void col_build_gear_candidates(const ColosseumState* s, ColoGearCandidates* c) { + memset(c, 0, sizeof(*c)); + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + col_gear_candidates_add(c, slot, s->player.equipped[slot]); + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { + uint8_t item = s->inventory_cells[cell].item_idx; + if (item == ITEM_NONE) continue; + int gslot = osrs_item_gear_slot(item); + if (gslot < 0) continue; + col_gear_candidates_add(c, gslot, item); + } +} + +static int col_item_dominates_empty_for_style( + uint8_t item, AttackStyle style, MeleeStyle melee_style +) { + const Item* it = get_item(item); + if (!it) return 1; + switch (style) { + case ATTACK_STYLE_MAGIC: + return it->attack_magic >= 0 && it->magic_damage >= 0; + case ATTACK_STYLE_RANGED: + return it->attack_ranged >= 0 && it->ranged_strength >= 0; + case ATTACK_STYLE_MELEE: { + int atk = melee_style == MELEE_STYLE_SLASH ? it->attack_slash + : melee_style == MELEE_STYLE_CRUSH ? it->attack_crush + : it->attack_stab; + return atk >= 0 && it->melee_strength >= 0; + } + default: + return 1; + } +} + +#define COLO_LOADOUT_MEMO_SLOTS 256 +typedef struct { + uint8_t key[COLO_LOADOUT_MEMO_SLOTS][NUM_GEAR_SLOTS]; + EncounterLoadoutStats stats[COLO_LOADOUT_MEMO_SLOTS]; + OsrsEquipmentEffectProfile effects[COLO_LOADOUT_MEMO_SLOTS]; + uint8_t used[COLO_LOADOUT_MEMO_SLOTS]; + int hits; + int misses; +} ColoLoadoutMemo; + +static uint32_t col_loadout_hash(const uint8_t equipment[NUM_GEAR_SLOTS]) { + uint32_t h = 2166136261u; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + h ^= equipment[slot]; + h *= 16777619u; + } + return h; +} + +static void col_memo_loadout_stats( + ColoLoadoutMemo* memo, + const uint8_t equipment[NUM_GEAR_SLOTS], + AttackStyle style, + OffensivePrayer prayer, + int current_att_level, + int current_str_level, + FightStyle fight_style, + int spell_base_damage, + const EncounterLoadoutStats** out_stats, + const OsrsEquipmentEffectProfile** out_effects +) { + uint32_t slot = col_loadout_hash(equipment) % COLO_LOADOUT_MEMO_SLOTS; + for (int probe = 0; probe < COLO_LOADOUT_MEMO_SLOTS; probe++) { + uint32_t idx = (slot + (uint32_t)probe) % COLO_LOADOUT_MEMO_SLOTS; + if (memo->used[idx] && + memcmp(memo->key[idx], equipment, NUM_GEAR_SLOTS) == 0) { + memo->hits++; + *out_stats = &memo->stats[idx]; + *out_effects = &memo->effects[idx]; + return; + } + if (!memo->used[idx]) { + memo->misses++; + memcpy(memo->key[idx], equipment, NUM_GEAR_SLOTS); + encounter_compute_loadout_stats( + equipment, style, prayer, current_att_level, fight_style, + spell_base_damage, &memo->stats[idx]); + encounter_update_loadout_level( + &memo->stats[idx], prayer, current_att_level, current_str_level); + encounter_derive_loadout_effect_profile(equipment, &memo->effects[idx]); + memo->used[idx] = 1; + *out_stats = &memo->stats[idx]; + *out_effects = &memo->effects[idx]; + return; + } + } + + static EncounterLoadoutStats overflow_stats; + static OsrsEquipmentEffectProfile overflow_effects; + encounter_compute_loadout_stats( + equipment, style, prayer, current_att_level, fight_style, + spell_base_damage, &overflow_stats); + encounter_update_loadout_level( + &overflow_stats, prayer, current_att_level, current_str_level); + encounter_derive_loadout_effect_profile(equipment, &overflow_effects); + *out_stats = &overflow_stats; + *out_effects = &overflow_effects; +} + +static void col_build_best_gear_table( + ColosseumState* s, + ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES] +) { + if (!s || !best) abort(); + for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) + for (int type = 0; type < COLO_NUM_NPC_TYPES; type++) { + best[style][type].dpt = -1.0f; + memset(best[style][type].setup, ITEM_NONE, NUM_GEAR_SLOTS); + } + + ColoGearCandidates cand; + col_build_gear_candidates(s, &cand); + + ColoNPC rep[COLO_NUM_NPC_TYPES]; + for (int type = 0; type < COLO_NUM_NPC_TYPES; type++) + if (!col_type_is_hazard_entity((ColoNpcType)type)) + rep[type] = col_matchup_representative_npc((ColoNpcType)type); + + ColoLoadoutMemo memo; + memset(&memo, 0, sizeof(memo)); + + static const int combo_slots[] = { + GEAR_SLOT_HEAD, GEAR_SLOT_CAPE, GEAR_SLOT_NECK, GEAR_SLOT_AMMO, + GEAR_SLOT_BODY, GEAR_SLOT_LEGS, GEAR_SLOT_HANDS, GEAR_SLOT_FEET, + GEAR_SLOT_RING, + }; + const int n_combo_slots = (int)(sizeof(combo_slots) / sizeof(combo_slots[0])); + + for (int wc = 0; wc < cand.count[GEAR_SLOT_WEAPON]; wc++) { + uint8_t weapon = cand.items[GEAR_SLOT_WEAPON][wc]; + int wstyle = get_item_attack_style(weapon); + if (wstyle < 1 || wstyle > 3) continue; + ColoWeaponSet style_set = (ColoWeaponSet)(wstyle - 1); + AttackStyle style = col_weapon_set_attack_style(style_set); + FightStyle fight_style = col_weapon_set_fight_style(style_set); + OffensivePrayer prayer = encounter_offensive_prayer_for_style(style); + int spell_base = style == ATTACK_STYLE_MAGIC + ? col_weapon_set_spell_base_damage(s, COLO_GEAR_MAGIC) : 0; + int current_att_level, current_str_level; + col_current_style_levels(&s->player, style, ¤t_att_level, ¤t_str_level); + MeleeStyle melee_style = col_item_melee_style(weapon); + int two_h = item_is_two_handed(weapon); + + uint8_t shield_items[COLO_BEST_GEAR_MAX_CAND + 1]; + int shield_count; + if (two_h) { + shield_items[0] = ITEM_NONE; + shield_count = 1; + } else { + shield_count = 0; + for (int k = 0; k < cand.count[GEAR_SLOT_SHIELD]; k++) + shield_items[shield_count++] = cand.items[GEAR_SLOT_SHIELD][k]; + if (shield_count == 0) shield_items[shield_count++] = ITEM_NONE; + } + + uint8_t slot_opt[16][COLO_BEST_GEAR_MAX_CAND + 1]; + int slot_opt_count[16]; + for (int d = 0; d < n_combo_slots; d++) { + int slot = combo_slots[d]; + int cnt = cand.count[slot]; + int dominated = 0; + for (int k = 0; k < cnt; k++) { + slot_opt[d][k] = cand.items[slot][k]; + if (col_item_dominates_empty_for_style(cand.items[slot][k], style, melee_style)) + dominated = 1; + } + slot_opt_count[d] = cnt; + if (cnt == 0) { + slot_opt[d][0] = ITEM_NONE; + slot_opt_count[d] = 1; + } else if (!dominated) { + slot_opt[d][slot_opt_count[d]++] = ITEM_NONE; + } + } + + int digit_max[16]; + for (int d = 0; d < n_combo_slots; d++) + digit_max[d] = slot_opt_count[d]; + int shield_digit = n_combo_slots; + digit_max[shield_digit] = shield_count; + int n_digits = n_combo_slots + 1; + + int odo[16]; + for (int d = 0; d < n_digits; d++) odo[d] = 0; + + for (;;) { + uint8_t equipment[NUM_GEAR_SLOTS]; + memset(equipment, ITEM_NONE, NUM_GEAR_SLOTS); + equipment[GEAR_SLOT_WEAPON] = weapon; + for (int d = 0; d < n_combo_slots; d++) + equipment[combo_slots[d]] = slot_opt[d][odo[d]]; + equipment[GEAR_SLOT_SHIELD] = shield_items[odo[shield_digit]]; + + const EncounterLoadoutStats* stats; + const OsrsEquipmentEffectProfile* effects; + col_memo_loadout_stats( + &memo, equipment, style, prayer, current_att_level, current_str_level, + fight_style, spell_base, &stats, &effects); + + for (int type = 0; type < COLO_NUM_NPC_TYPES; type++) { + if (col_type_is_hazard_entity((ColoNpcType)type)) continue; + const ColoNpcStats* ns = &COLO_NPC_STATS[type]; + float dpt = col_expected_dpt_vs_npc( + stats, effects, weapon, style, melee_style, &rep[type], ns); + if (dpt > best[style_set][type].dpt) { + best[style_set][type].dpt = dpt; + memcpy(best[style_set][type].setup, equipment, NUM_GEAR_SLOTS); + } + } + + int d = 0; + for (; d < n_digits; d++) { + if (++odo[d] < digit_max[d]) break; + odo[d] = 0; + } + if (d == n_digits) break; + } + } +} + +static uint64_t col_best_gear_signature(const ColosseumState* s) { + uint64_t h = 1469598103934665603ULL; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + h = (h ^ (uint64_t)s->player.equipped[slot]) * 1099511628211ULL; + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) + h = (h ^ (uint64_t)s->inventory_cells[cell].item_idx) * 1099511628211ULL; + const int scalars[5] = { + s->player.current_attack, s->player.current_strength, + s->player.current_ranged, s->player.current_magic, + (int)s->active_loadout_profile, + }; + for (int k = 0; k < 5; k++) + h = (h ^ (uint64_t)(uint32_t)scalars[k]) * 1099511628211ULL; + return h; +} + +static const ColoBestGear (*col_get_best_gear_table(ColosseumState* s))[COLO_NUM_NPC_TYPES] { + if (!s) abort(); + ColoObsMemoCaches* memos = &s->obs_memos; + uint64_t sig = col_best_gear_signature(s); + for (int w = 0; w < COLO_BEST_GEAR_CACHE_WAYS; w++) { + if (memos->best_gear_valid[w] && memos->best_gear_sig[w] == sig) + return memos->best_gear[w]; + } + int slot = memos->best_gear_next; + col_build_best_gear_table(s, memos->best_gear[slot]); + memos->best_gear_sig[slot] = sig; + memos->best_gear_valid[slot] = 1; + memos->best_gear_next = (slot + 1) % COLO_BEST_GEAR_CACHE_WAYS; + return memos->best_gear[slot]; +} + +static void col_apply_bis_gear_oracle(ColosseumState* s) { + int target = col_player_target_slot(s); + if (target < 0 || target >= COLO_MAX_NPCS) return; + const ColoNPC* npc = &s->npcs[target]; + if (!col_npc_is_live_target(npc) || col_type_is_hazard_entity(npc->type)) return; + + int melee_ok = col_npc_melee_adjacent_to_player(s, npc); + const uint8_t* const* loadouts = col_loadouts_for_profile(s->active_loadout_profile); + float best_dpt = -1.0f; + ColoWeaponSet best_set = s->weapon_set; + for (int set = 0; set < COLO_NUM_WEAPON_SETS; set++) { + if (set == COLO_GEAR_MELEE && !melee_ok) continue; + float dpt = col_expected_dpt_for_equipment_vs_npc(s, loadouts[set], npc, 1); + if (dpt > best_dpt) { best_dpt = dpt; best_set = (ColoWeaponSet)set; } + } + + OffensivePrayer best_prayer = + encounter_offensive_prayer_for_style(col_weapon_set_attack_style(best_set)); + if (s->player.offensive_prayer != best_prayer) { + s->player.offensive_prayer = best_prayer; + col_mark_live_loadout_dirty(s); + } + col_apply_weapon_set(s, best_set); +} + +static ColoWeaponSet col_weapon_set_for_item(uint8_t item) { + int item_style = get_item_attack_style(item); + if (item_style == 2) return COLO_GEAR_RANGED; + if (item_style == 3) return COLO_GEAR_MAGIC; + return COLO_GEAR_MELEE; +} + +static uint64_t col_weapon_choice_obs_signature(ColosseumState* s) { + uint64_t h = col_best_gear_signature(s); + int has_target = 0, type = 0, def_drained = 0, size = 0; + int target = col_player_target_slot(s); + if (target >= 0 && target < COLO_MAX_NPCS) { + const ColoNPC* npc = &s->npcs[target]; + if (col_npc_is_live_target(npc) && !col_type_is_hazard_entity(npc->type)) { + has_target = 1; + type = (int)npc->type; + def_drained = npc->def_drained; + size = npc->size; + } + } + const int target_fields[4] = { has_target, type, def_drained, size }; + for (int k = 0; k < 4; k++) + h = (h ^ (uint64_t)(uint32_t)target_fields[k]) * 1099511628211ULL; + return h; +} + +static void col_write_weapon_choice_obs(ColosseumState* s, float* obs, int* io) { + int i = *io; + + ColoObsMemoCaches* memos = &s->obs_memos; + uint64_t sig = col_weapon_choice_obs_signature(s); + for (int w = 0; w < COLO_WEAPON_CHOICE_OBS_CACHE_WAYS; w++) { + if (memos->weapon_choice_valid[w] && memos->weapon_choice_sig[w] == sig) { + memcpy(&obs[i], memos->weapon_choice[w], + sizeof(float) * COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS); + *io = i + COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS; + return; + } + } + int block_start = i; + + const ColoNPC* npc = NULL; + int target = col_player_target_slot(s); + if (target >= 0 && target < COLO_MAX_NPCS) { + const ColoNPC* cand = &s->npcs[target]; + if (col_npc_is_live_target(cand) && !col_type_is_hazard_entity(cand->type)) + npc = cand; + } + const ColoBestGear (*best)[COLO_NUM_NPC_TYPES] = NULL; + float best_dpt = 0.0f; + if (npc) { + best = col_get_best_gear_table(s); + for (int st = 0; st < COLO_NUM_WEAPON_SETS; st++) + if (best[st][npc->type].dpt > best_dpt) + best_dpt = best[st][npc->type].dpt; + } + + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { + float v = 0.0f; + uint8_t item = s->inventory_cells[c].item_idx; + if (npc && item != ITEM_NONE && + osrs_item_gear_slot(item) == GEAR_SLOT_WEAPON) { + ColoWeaponSet set = col_weapon_set_for_item(item); + const ColoBestGear* bs = &best[set][npc->type]; + if (bs->dpt >= 0.0f) { + uint8_t setup[NUM_GEAR_SLOTS]; + memcpy(setup, bs->setup, NUM_GEAR_SLOTS); + setup[GEAR_SLOT_WEAPON] = item; + if (item_is_two_handed(item)) + setup[GEAR_SLOT_SHIELD] = ITEM_NONE; + float dpt = col_expected_dpt_for_equipment_vs_npc( + s, setup, npc, 1); + v = col_clamp01(dpt / COLO_EXPECTED_DPT_NORM); + } + } + obs[i++] = v; + } + + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { + uint8_t item = s->inventory_cells[c].item_idx; + obs[i++] = (item != ITEM_NONE && osrs_spec_cost(item) > 0) ? 1.0f : 0.0f; + } + + float cur = npc + ? col_expected_dpt_for_equipment_vs_npc(s, s->player.equipped, npc, 1) + : 0.0f; + obs[i++] = col_clamp01(cur / COLO_EXPECTED_DPT_NORM); + obs[i++] = (npc && best_dpt > 0.0f) ? col_clamp01(cur / best_dpt) : 0.0f; + + int slot = memos->weapon_choice_next; + memcpy(memos->weapon_choice[slot], &obs[block_start], + sizeof(float) * COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS); + memos->weapon_choice_sig[slot] = sig; + memos->weapon_choice_valid[slot] = 1; + memos->weapon_choice_next = (slot + 1) % COLO_WEAPON_CHOICE_OBS_CACHE_WAYS; + + *io = i; +} + +static void col_write_spawn_obs(ColosseumState* s, float* obs, int* io) { + int i = *io; + float until_spawn = (float)s->wave_spawn_delay; + float until_attack = s->wave_spawn_delay > 0 + ? (float)s->wave_spawn_delay + (float)COLO_WAVE_ATTACK_GATE_TICKS + : (float)s->wave_attack_delay; + obs[i++] = col_clamp01(until_spawn / 12.0f); + obs[i++] = col_clamp01(until_attack / 16.0f); + for (int a = 0; a < COLO_NUM_SPAWN_ANCHORS; a++) + obs[i++] = col_spawn_excluded_near_player(s, + COLO_SPAWN_ANCHORS[a][0], COLO_SPAWN_ANCHORS[a][1], + COLO_SPAWN_ZONE_SIZE) ? 1.0f : 0.0f; + *io = i; +} + +static int col_weapon_set_is_argmax_dpt_for_npc( + ColosseumState* s, + int npc_slot, + ColoWeaponSet set, + const ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES] +) { + if (npc_slot < 0 || npc_slot >= COLO_MAX_NPCS) return -1; + const ColoNPC* npc = &s->npcs[npc_slot]; + if (!col_npc_is_live_target(npc) || col_type_is_hazard_entity(npc->type)) + return -1; + if (set < 0 || set >= COLO_NUM_WEAPON_SETS) + col_abort_invalid_weapon_set(set); + + float selected = best[set][npc->type].dpt; + for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) + if (best[style][npc->type].dpt > selected) return 0; + return 1; +} + +static int col_current_set_is_argmax_dpt_for_target(ColosseumState* s) { + int target = col_player_target_slot(s); + if (target < 0 || target >= COLO_MAX_NPCS) return -1; + const ColoNPC* npc = &s->npcs[target]; + if (!col_npc_is_live_target(npc) || col_type_is_hazard_entity(npc->type)) + return -1; + const ColoBestGear (*best)[COLO_NUM_NPC_TYPES] = col_get_best_gear_table(s); + return col_weapon_set_is_argmax_dpt_for_npc(s, target, s->weapon_set, best); +} + +static int col_attacked_with_argmax_set(ColosseumState* s) { + if (!s->tick_scratch.player_attacked) return -1; + int npc_slot = s->player_attack_npc_idx; + if (npc_slot < 0 || npc_slot >= COLO_MAX_NPCS) return -1; + const ColoNPC* npc = &s->npcs[npc_slot]; + if (!col_npc_is_live_target(npc) || col_type_is_hazard_entity(npc->type)) + return -1; + const ColoBestGear (*best)[COLO_NUM_NPC_TYPES] = col_get_best_gear_table(s); + return col_weapon_set_is_argmax_dpt_for_npc(s, npc_slot, s->weapon_set, best); +} + +static void col_inventory_cell_post_use_deltas( + const ColosseumState* s, + int cell_idx, + const float before[6], + float out[6] +) { + for (int i = 0; i < 6; i++) out[i] = 0.0f; + if (s->inventory_cells[cell_idx].item_idx == ITEM_NONE) return; + + float after[6]; + uint8_t hypothetical[NUM_GEAR_SLOTS]; + col_hypothetical_equipment_after_cell(s, cell_idx, hypothetical); + col_equipment_bonus_projection(hypothetical, after); + for (int i = 0; i < 6; i++) out[i] = after[i] - before[i]; +} + +static uint64_t col_inventory_obs_signature(const ColosseumState* s) { + uint64_t h = 1469598103934665603ULL; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + h = (h ^ (uint64_t)s->player.equipped[slot]) * 1099511628211ULL; + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { + const ColoInvCell* c = &s->inventory_cells[cell]; + uint64_t packed = (uint64_t)c->item_idx + | ((uint64_t)c->raw_osrs_id << 8) + | ((uint64_t)c->dose << 24); + h = (h ^ packed) * 1099511628211ULL; + } + const int scalars[3] = { + s->player.base_hitpoints, s->player.base_prayer, s->player.base_attack, + }; + for (int k = 0; k < 3; k++) + h = (h ^ (uint64_t)(uint32_t)scalars[k]) * 1099511628211ULL; + return h; +} + +static void col_write_inventory_and_equipped_obs( + ColosseumState* s, + float* obs, + int* idx +) { + int i = *idx; + + ColoObsMemoCaches* memos = &s->obs_memos; + uint64_t sig = col_inventory_obs_signature(s); + for (int w = 0; w < COLO_INVENTORY_OBS_CACHE_WAYS; w++) { + if (memos->inventory_valid[w] && memos->inventory_sig[w] == sig) { + memcpy(&obs[i], memos->inventory[w], + sizeof(float) * COLO_INVENTORY_OBS_CACHE_FLOATS); + *idx = i + COLO_INVENTORY_OBS_CACHE_FLOATS; + return; + } + } + int block_start = i; + + float current_equipment_projection[6]; + col_equipment_bonus_projection(s->player.equipped, current_equipment_projection); + for (int cell_idx = 0; cell_idx < COLO_INVENTORY_DISPLAY_SLOTS; cell_idx++) { + float deltas[6]; + const ColoInvCell* cell = &s->inventory_cells[cell_idx]; + col_inventory_cell_post_use_deltas( + s, cell_idx, current_equipment_projection, deltas); + osrs_write_inventory_cell_affordance_features( + &obs[i], + cell->item_idx, + cell->raw_osrs_id, + cell->dose, + col_inventory_cell_holds_equipped_item(s, cell_idx), + deltas, + s->player.base_hitpoints, + s->player.base_prayer, + s->player.base_attack); + i += OSRS_INVENTORY_CELL_OBS_FEATURES; + } + assert(i == COLO_OBS_AFTER_INVENTORY); + + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + osrs_write_equipped_self_features(&obs[i], s->player.equipped[slot]); + i += OSRS_EQUIPPED_SELF_OBS_FEATURES; + } + assert(i == COLO_OBS_AFTER_EQUIPPED_SELF); + + int slot = memos->inventory_next; + memcpy(memos->inventory[slot], &obs[block_start], + sizeof(float) * COLO_INVENTORY_OBS_CACHE_FLOATS); + memos->inventory_sig[slot] = sig; + memos->inventory_valid[slot] = 1; + memos->inventory_next = (slot + 1) % COLO_INVENTORY_OBS_CACHE_WAYS; + + *idx = i; +} + +static void col_write_obs_ctx(EncounterState* state, EncounterContext* context, float* obs) { + ColosseumState* s = (ColosseumState*)state; + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); +#ifdef COLO_PROFILE_ENABLED + int col_prof_enabled = COLO_PROFILE_ENABLED(); + double col_prof_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; +#endif + col_refresh_current_obs_slots_ctx(s, ctx); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_REFRESH_SLOTS); +#endif + memset(obs, 0, COLO_NUM_OBS * sizeof(float)); + int i = 0; + int px = s->player.x, py = s->player.y; + const EncounterLoadoutStats* ls = col_live_loadout_stats(s); + + obs[i++] = col_clamp01((float)s->player.current_hitpoints / 99.0f); + obs[i++] = (float)(px - COLO_ARENA_MIN_X) / (float)COLO_ARENA_WIDTH; + obs[i++] = (float)(COLO_ARENA_MAX_X - px) / (float)COLO_ARENA_WIDTH; + obs[i++] = (float)(py - COLO_ARENA_MIN_Y) / (float)COLO_ARENA_HEIGHT; + obs[i++] = (float)(COLO_ARENA_MAX_Y - py) / (float)COLO_ARENA_HEIGHT; + obs[i++] = (s->player.prayer == PRAYER_PROTECT_MELEE) ? 1.0f : 0.0f; + obs[i++] = (s->player.prayer == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; + obs[i++] = (s->player.prayer == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; + obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_PIETY) ? 1.0f : 0.0f; + obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_RIGOUR) ? 1.0f : 0.0f; + obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_AUGURY) ? 1.0f : 0.0f; + obs[i++] = col_clamp01((float)s->player.current_prayer / 99.0f); + obs[i++] = col_clamp01((float)s->player.run_energy / 10000.0f); + obs[i++] = col_clamp01((float)s->player.special_energy / 100.0f); + obs[i++] = s->player.spec_armed ? 1.0f : 0.0f; + obs[i++] = col_clamp01((float)s->player.attack_timer / 8.0f); + obs[i++] = col_clamp01((float)s->player.food_timer / 3.0f); + obs[i++] = col_clamp01((float)s->player.potion_timer / 3.0f); + obs[i++] = osrs_interaction_active(&s->interaction) ? 1.0f : 0.0f; + obs[i++] = col_clamp01((float)ls->attack_range / 15.0f); + obs[i++] = col_clamp01((float)ls->max_hit / 80.0f); + obs[i++] = col_clamp01((float)ls->attack_speed / 6.0f); + obs[i++] = col_clamp01((float)s->player.current_defence / 99.0f); + obs[i++] = col_clamp01((float)ls->def_stab / 300.0f); + obs[i++] = col_clamp01((float)ls->def_slash / 300.0f); + obs[i++] = col_clamp01((float)ls->def_crush / 300.0f); + obs[i++] = col_clamp01((float)ls->def_magic / 300.0f); + obs[i++] = col_clamp01((float)ls->def_ranged / 300.0f); + obs[i++] = col_clamp01((float)ls->eff_level / 165.0f); + obs[i++] = col_clamp01((float)ls->attack_bonus / 200.0f); + obs[i++] = col_clamp01((float)ls->strength_bonus / 160.0f); + int colo_spec_cost = osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON]); + obs[i++] = col_clamp01((float)colo_spec_cost / 100.0f); + obs[i++] = colo_spec_cost > 0 ? 1.0f : 0.0f; + obs[i++] = s->active_loadout_profile == COLO_LOADOUT_PROFILE_SPEEDRUN ? 1.0f : 0.0f; + obs[i++] = s->active_loadout_profile == COLO_LOADOUT_PROFILE_BEGINNER ? 1.0f : 0.0f; + obs[i++] = col_clamp01((float)ls->spell_base_damage / 40.0f); + assert(i == COLO_OBS_AFTER_PLAYER); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_PREFIX); +#endif + + col_write_pillar_obs(s, obs, &i); + assert(i == COLO_OBS_AFTER_PILLARS); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_PILLARS); +#endif + + col_write_inventory_and_equipped_obs(s, obs, &i); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_INVENTORY); +#endif + + ColoVenatorPreviewTargets venator_targets; + col_collect_venator_preview_targets(s, &venator_targets); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_VENATOR); +#endif + + for (int n = 0; n < COLO_OBS_NPCS; n++) { + int npc_idx = s->current_obs_slots[n]; + if (npc_idx < 0 || npc_idx >= COLO_MAX_NPCS || !s->npcs[npc_idx].active) { + i += COLO_FEATURES_PER_NPC; + continue; + } + col_write_npc_slot( + s, &s->npcs[npc_idx], npc_idx, &venator_targets, obs, &i); + } + assert(i == COLO_OBS_AFTER_NPCS); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_NPC_SLOTS); +#endif + + for (int m = 0; m < COLO_NUM_MODIFIERS; m++) { + obs[i++] = (s->modifiers.active_mask & (1u << m)) ? 1.0f : 0.0f; + obs[i++] = col_clamp01((float)s->modifiers.tier[m] / 3.0f); + } + obs[i++] = s->modifiers.draft_pending ? 1.0f : 0.0f; + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { + int opt = s->modifiers.draft_options[o]; + obs[i++] = (s->modifiers.draft_pending && opt >= 0) + ? (float)(opt + 1) / (float)COLO_NUM_MODIFIERS : 0.0f; + } + col_write_modifier_hazards(s, obs, &i); + assert(i == COLO_OBS_AFTER_MODS); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_MODIFIERS); +#endif + + int npcs_remaining = 0; + for (int n = 0; n < COLO_MAX_NPCS; n++) + if (col_npc_is_live_enemy(&s->npcs[n])) npcs_remaining++; + obs[i++] = col_clamp01((float)(s->wave + 1) / (float)COLO_NUM_WAVES); + obs[i++] = col_clamp01((float)npcs_remaining / (float)COLO_MAX_NPCS); + obs[i++] = (s->reinforcement_timer > 0) + ? col_clamp01((float)s->reinforcement_timer / (float)COLO_REINFORCEMENT_TICKS) : 0.0f; + obs[i++] = (s->wave_spawn_delay > 0 || s->wave_ready_delay > 0) ? 1.0f : 0.0f; + assert(i == COLO_OBS_AFTER_WAVE); + + col_write_boss_obs(s, obs, &i); + assert(i == COLO_OBS_AFTER_BOSS); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_BOSS); +#endif + + int order[ENCOUNTER_MAX_PENDING_HITS]; + int nhits = col_collect_obs_pending_hits(s, order); + for (int h = 0; h < COLO_OBS_PENDING_HITS; h++) { + if (h < nhits) { + const EncounterPendingHit* ph = &s->player_pending_hits.hits[order[h]]; + obs[i++] = 1.0f; + obs[i++] = (ph->attack_style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; + obs[i++] = (ph->attack_style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; + obs[i++] = (ph->attack_style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; + obs[i++] = col_clamp01((float)ph->ticks_remaining / 10.0f); + obs[i++] = col_clamp01((float)ph->damage / 75.0f); + obs[i++] = col_pending_hit_ignores_prayer(ph) ? 1.0f : 0.0f; + } else { + i += COLO_FEATURES_PER_HIT; + } + } + assert(i == COLO_OBS_AFTER_PENDING_HITS); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_PENDING_HITS); +#endif + + if (ctx->config.step_out_forecast_obs_enabled) { + int horizon = ctx->config.forecast_horizon; + if (horizon < 1) horizon = 1; + if (horizon > COLO_STEP_OUT_FORECAST_HORIZON) + horizon = COLO_STEP_OUT_FORECAST_HORIZON; + ColoStepOutForecast forecast; + ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS]; + col_build_step_out_forecast_horizon_mode_summary( + s, &forecast, summaries, horizon, ctx->config.forecast_run_tile_mode); + i = col_write_step_out_forecast_obs_summary(&forecast, summaries, horizon, obs, i); + } else { + i += COLO_STEP_OUT_FORECAST_OBS_SIZE; + } + assert(i == COLO_OBS_AFTER_STEP_OUT_FORECAST); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_FORECAST); +#endif + + col_write_threat_los_counts(s, obs, &i); + assert(i == COLO_OBS_AFTER_THREAT_LOS); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_THREAT_LOS); +#endif + + col_write_thrall_dc(s, obs, &i); + assert(i == COLO_OBS_AFTER_THRALL_DC); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_THRALL_DC); +#endif + + col_write_weapon_choice_obs(s, obs, &i); + assert(i == COLO_OBS_AFTER_WEAPON_CHOICE); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_WEAPON_CHOICE); +#endif + + col_write_spawn_obs(s, obs, &i); + assert(i == COLO_OBS_AFTER_SPAWN); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_SPAWN); +#endif + + if (ctx->config.threat_field_obs_enabled) { + col_write_threat_field_obs(s, obs, &i); + } else { + i += COLO_THREAT_FIELD_OBS_SIZE; + } + assert(i == COLO_OBS_AFTER_THREAT_FIELD); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_OBS_THREAT_FIELD); +#endif + + if (i != COLO_NUM_OBS) { + fprintf(stderr, "colosseum obs index mismatch: wrote %d, expected %d\n", i, COLO_NUM_OBS); + abort(); + } +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc new file mode 100644 index 0000000000..90f14007d1 --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_player_actions.inc @@ -0,0 +1,1357 @@ +#line 76 "encounter_colosseum.h" + +#define COLO_HEAD_PRIMARY 0 +#define COLO_HEAD_PRAYER 1 + +#define COLO_HEAD_EQUIP_BASE 2 +#define COLO_HEAD_EQUIP_SLOT(slot) (COLO_HEAD_EQUIP_BASE + (slot)) +#define COLO_HEAD_EAT (COLO_HEAD_EQUIP_BASE + NUM_GEAR_SLOTS) +#define COLO_HEAD_DRINK (COLO_HEAD_EAT + 1) +#define COLO_HEAD_SPEC (COLO_HEAD_DRINK + 1) +#define COLO_HEAD_MODIFIER_SELECT (COLO_HEAD_SPEC + 1) +#define COLO_HEAD_GRAPPLE_PARRY (COLO_HEAD_MODIFIER_SELECT + 1) +#define COLO_HEAD_OFFENSIVE (COLO_HEAD_GRAPPLE_PARRY + 1) +#define COLO_HEAD_SPELL (COLO_HEAD_OFFENSIVE + 1) + +#define COLO_INV_CLICK_HEADS (NUM_GEAR_SLOTS + 2) +#define COLO_INV_CLICK_DIM 29 +#define COLO_PRIMARY_ATTACK_BASE ENCOUNTER_MOVE_ACTIONS +#define COLO_PRIMARY_DIM (ENCOUNTER_MOVE_ACTIONS + COLO_OBS_NPCS) +#define COLO_OVERHEAD_DIM ENCOUNTER_OVERHEAD_DIM_PVE +#define COLO_SPEC_DIM 3 +#define COLO_SPELL_DIM 3 + +#define COLO_OVERHEAD_NO_CHANGE ENCOUNTER_OVERHEAD_NO_CHANGE +#define COLO_OVERHEAD_OFF ENCOUNTER_OVERHEAD_OFF +#define COLO_OVERHEAD_MELEE ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE +#define COLO_OVERHEAD_RANGED ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED +#define COLO_OVERHEAD_MAGIC ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC + +#define COLO_SPELL_SUMMON_THRALL 1 +#define COLO_SPELL_DEATH_CHARGE 2 + +#define COLO_MODIFIER_SELECT_DIM (1 + COLO_MODIFIER_DRAFT_OPTIONS) + +#define COLO_GRAPPLE_PARRY_DIM (1 + COLO_NUM_GRAPPLE_SLOTS) + +#define COLO_ACTION_DIMS_INIT { \ + COLO_PRIMARY_DIM, \ + COLO_OVERHEAD_DIM, \ + COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, \ + COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, \ + COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, COLO_INV_CLICK_DIM, \ + COLO_INV_CLICK_DIM, \ + COLO_INV_CLICK_DIM, \ + COLO_SPEC_DIM, \ + COLO_MODIFIER_SELECT_DIM, \ + COLO_GRAPPLE_PARRY_DIM, \ + ENCOUNTER_OFFENSIVE_DIM, \ + COLO_SPELL_DIM \ +} +static const int COLO_ACTION_DIMS[COLO_NUM_ACTION_HEADS] = COLO_ACTION_DIMS_INIT; +static_assert(COLO_NUM_ACTION_HEADS == 2 + NUM_GEAR_SLOTS + 2 + 5, + "colosseum action head count must match the per-category layout"); +static_assert(COLO_HEAD_SPELL == COLO_NUM_ACTION_HEADS - 1, + "SPELL must be the last colosseum action head"); +#define COLO_ACTION_MASK_SIZE (ENCOUNTER_MOVE_ACTIONS + COLO_OVERHEAD_DIM + \ + COLO_OBS_NPCS + COLO_INV_CLICK_HEADS * COLO_INV_CLICK_DIM + COLO_SPEC_DIM + \ + COLO_MODIFIER_SELECT_DIM + COLO_GRAPPLE_PARRY_DIM + ENCOUNTER_OFFENSIVE_DIM + \ + COLO_SPELL_DIM) + +static int col_action_head_mask_offset(int head) { + int offset = 0; + for (int h = 0; h < head; h++) + offset += COLO_ACTION_DIMS[h]; + return offset; +} + +static int col_primary_attack_action_for_obs_slot(int obs_slot) { + if (obs_slot < 0 || obs_slot >= COLO_OBS_NPCS) { + fprintf(stderr, "colosseum primary attack obs slot out of range: %d\n", obs_slot); + abort(); + } + return COLO_PRIMARY_ATTACK_BASE + obs_slot; +} + +static int col_primary_obs_slot_from_action(int primary_action) { + if (primary_action < COLO_PRIMARY_ATTACK_BASE || + primary_action >= COLO_PRIMARY_DIM) { + fprintf(stderr, "colosseum primary attack action out of range: %d\n", + primary_action); + abort(); + } + return primary_action - COLO_PRIMARY_ATTACK_BASE; +} + +static EncounterStatDriftPins col_divine_stat_drift_pins(const ColosseumState* s) { + EncounterStatDriftPins pins = encounter_stat_drift_no_pins(); + if (s->divine_combat_timer > 0) { + pins = encounter_merge_stat_drift_pins( + pins, encounter_divine_super_combat_pins(&s->player)); + } + if (s->divine_ranged_timer > 0) { + pins = encounter_merge_stat_drift_pins( + pins, encounter_divine_ranging_pins(&s->player)); + } + return pins; +} + +static int col_enforce_divine_stat_floors(ColosseumState* s) { + return encounter_enforce_stat_drift_pins(&s->player, col_divine_stat_drift_pins(s)); +} + +static void col_finish_stat_moving_consumable(ColosseumState* s) { + col_enforce_divine_stat_floors(s); + col_mark_live_loadout_dirty(s); +} + +static int col_tick_divine_combat_timer(ColosseumState* s) { + if (s->divine_combat_timer <= 0) return 0; + s->divine_combat_timer--; + if (s->divine_combat_timer > 0) return 0; + s->player.current_attack = s->player.base_attack; + s->player.current_strength = s->player.base_strength; + s->player.current_defence = s->player.base_defence; + return 1; +} + +static int col_tick_divine_ranged_timer(ColosseumState* s) { + if (s->divine_ranged_timer <= 0) return 0; + s->divine_ranged_timer--; + if (s->divine_ranged_timer > 0) return 0; + s->player.current_ranged = s->player.base_ranged; + return 1; +} + +static void col_tick_live_stat_drift_and_divines(ColosseumState* s) { + int changed = 0; + changed |= col_tick_divine_combat_timer(s); + changed |= col_tick_divine_ranged_timer(s); + changed |= encounter_tick_stat_drift( + &s->player, &s->stat_drift_timer, col_divine_stat_drift_pins(s)); + if (changed) col_mark_live_loadout_dirty(s); +} + +static void col_apply_divine_combat_potion_effect(ColosseumState* s) { + encounter_damage_player(&s->player, 10, &s->tick_scratch.damage_received); + s->tick_scratch.landed_self_damage += 10.0f; + s->divine_combat_timer = ENCOUNTER_DIVINE_POTION_TICKS; +} + +static void col_apply_divine_ranged_potion_effect(ColosseumState* s) { + encounter_damage_player(&s->player, 10, &s->tick_scratch.damage_received); + s->tick_scratch.landed_self_damage += 10.0f; + s->divine_ranged_timer = ENCOUNTER_DIVINE_POTION_TICKS; +} + +static int col_overhead_switch_slips(uint32_t* rng_state, float fail_prob) { + uint64_t numerator = (uint64_t)(fail_prob * 65536.0f); + return encounter_roll_ratio_u16(rng_state, numerator, 65536); +} + +static void col_player_pretick(ColosseumState* s, ColosseumContext* ctx, const int* actions) { + OverheadPrayer prev_prayer = s->player.prayer; + OffensivePrayer prev_offensive = s->player.offensive_prayer; + int prayer_before = s->player.current_prayer; + int overhead_activated = encounter_apply_overhead_action(&s->player.prayer, + actions[COLO_HEAD_PRAYER]); + if (s->player.prayer != prev_prayer && + ctx->config.prayer_switch_fail_prob > 0.0f && + col_overhead_switch_slips(&s->rng_state, ctx->config.prayer_switch_fail_prob)) { + s->player.prayer = prev_prayer; + overhead_activated = 0; + } + if (overhead_activated) + s->player.prayer_just_activated = 1; + if (encounter_apply_offensive_action(&s->player.offensive_prayer, + actions[COLO_HEAD_OFFENSIVE])) + s->player.offensive_prayer_just_activated = 1; + encounter_drain_all_prayers(&s->player, encounter_player_prayer_bonus(&s->player)); + if (s->player.offensive_prayer != prev_offensive) + col_mark_live_loadout_dirty(s); + if (ctx->config.action_debug_log) { + fprintf(stderr, + "colo_action tick=%d pretick primary=%d prayer_action=%d " + "offensive_action=%d spell=%d prayer=%d->%d offensive=%d->%d pp=%d->%d\n", + s->tick, actions[COLO_HEAD_PRIMARY], actions[COLO_HEAD_PRAYER], + actions[COLO_HEAD_OFFENSIVE], + actions[COLO_HEAD_SPELL], prev_prayer, s->player.prayer, + prev_offensive, s->player.offensive_prayer, prayer_before, + s->player.current_prayer); + } +} + +static void col_apply_weapon_set(ColosseumState* s, ColoWeaponSet set) { + if (set < 0 || set >= COLO_NUM_WEAPON_SETS) + col_abort_invalid_weapon_set(set); + if (s->weapon_set == set) return; + GearSet gear = (set == COLO_GEAR_RANGED) ? GEAR_RANGED + : (set == COLO_GEAR_MAGIC) ? GEAR_MAGE + : GEAR_MELEE; + encounter_apply_loadout(&s->player, + col_loadouts_for_profile(s->active_loadout_profile)[set], gear); + s->weapon_set = set; + s->player.spec_armed = 0; + col_mark_live_loadout_dirty(s); +} + +static void col_sync_weapon_set_from_equipped_weapon(ColosseumState* s) { + AttackStyle style = col_equipped_weapon_attack_style(&s->player); + s->weapon_set = style == ATTACK_STYLE_RANGED ? COLO_GEAR_RANGED + : style == ATTACK_STYLE_MAGIC ? COLO_GEAR_MAGIC + : COLO_GEAR_MELEE; + s->player.current_gear = col_gear_set_for_attack_style(style); + s->player.visible_gear = s->player.current_gear; +} + +static int col_first_empty_inventory_cell(const ColosseumState* s, int except_slot) { + for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { + if (i == except_slot) continue; + if (osrs_inventory_cell_is_empty(&s->inventory_cells[i])) return i; + } + return -1; +} + +static int col_inventory_cell_holds_equipped_item(const ColosseumState* s, int cell_idx) { + const ColoInvCell* cell = &s->inventory_cells[cell_idx]; + if (cell->item_idx == ITEM_NONE) return 0; + int slot = osrs_item_gear_slot(cell->item_idx); + if (slot < 0) return 0; + return s->player.equipped[slot] == cell->item_idx; +} + +static int col_can_equip_from_cell(const ColosseumState* s, int cell_idx) { + const ColoInvCell* cell = &s->inventory_cells[cell_idx]; + if (cell->item_idx == ITEM_NONE) return 0; + if (col_inventory_cell_holds_equipped_item(s, cell_idx)) return 0; + + uint8_t item_idx = cell->item_idx; + int gear_slot = osrs_item_gear_slot(item_idx); + if (gear_slot < 0) return 0; + if (gear_slot == GEAR_SLOT_WEAPON && item_is_two_handed(item_idx) && + s->player.equipped[GEAR_SLOT_SHIELD] != ITEM_NONE && + s->player.equipped[GEAR_SLOT_WEAPON] != ITEM_NONE && + col_first_empty_inventory_cell(s, cell_idx) < 0) return 0; + if (gear_slot == GEAR_SLOT_SHIELD && + item_is_two_handed(s->player.equipped[GEAR_SLOT_WEAPON]) && + col_first_empty_inventory_cell(s, cell_idx) < 0) return 0; + return 1; +} + +static void col_equip_from_cell(ColosseumState* s, int cell_idx) { + if (!col_can_equip_from_cell(s, cell_idx)) return; + + ColoInvCell* cell = &s->inventory_cells[cell_idx]; + uint8_t item_idx = cell->item_idx; + int gear_slot = osrs_item_gear_slot(item_idx); + if (gear_slot < 0) { + fprintf(stderr, "colosseum equip: item %u has no gear slot\n", item_idx); + abort(); + } + uint8_t displaced = s->player.equipped[gear_slot]; + + if (gear_slot == GEAR_SLOT_WEAPON && item_is_two_handed(item_idx)) { + uint8_t shield = s->player.equipped[GEAR_SLOT_SHIELD]; + if (shield != ITEM_NONE) { + if (displaced == ITEM_NONE) { + displaced = shield; + } else { + int empty = col_first_empty_inventory_cell(s, cell_idx); + if (empty < 0) return; + s->inventory_cells[empty] = osrs_inventory_cell_from_item(shield); + } + s->player.equipped[GEAR_SLOT_SHIELD] = ITEM_NONE; + } + } else if (gear_slot == GEAR_SLOT_SHIELD && + item_is_two_handed(s->player.equipped[GEAR_SLOT_WEAPON])) { + displaced = s->player.equipped[GEAR_SLOT_WEAPON]; + s->player.equipped[GEAR_SLOT_WEAPON] = ITEM_NONE; + + s->player.spec_armed = 0; + } + + s->player.equipped[gear_slot] = item_idx; + *cell = osrs_inventory_cell_from_item(displaced); + if (gear_slot == GEAR_SLOT_WEAPON) s->player.spec_armed = 0; + osrs_refresh_player_equipment(&s->player); + col_sync_weapon_set_from_equipped_weapon(s); + col_mark_live_loadout_dirty(s); +} + +static int col_can_eat_consumable_kind( + const ColosseumState* s, + OsrsConsumableKind kind +) { + switch (kind) { + case OSRS_CONSUMABLE_SHARK_FOOD: + return s->player.food_timer == 0 && + s->player.current_hitpoints < s->player.base_hitpoints; + case OSRS_CONSUMABLE_KARAMBWAN: + return s->player.karambwan_timer == 0 && + s->player.current_hitpoints < s->player.base_hitpoints; + case OSRS_CONSUMABLE_BREW: + case OSRS_CONSUMABLE_SUPER_RESTORE: + case OSRS_CONSUMABLE_SANFEW: + case OSRS_CONSUMABLE_SUPER_COMBAT: + case OSRS_CONSUMABLE_DIVINE_COMBAT: + case OSRS_CONSUMABLE_RANGING: + case OSRS_CONSUMABLE_DIVINE_RANGING: + case OSRS_CONSUMABLE_SURGE: + case OSRS_CONSUMABLE_GUTHIX_REST: + case OSRS_CONSUMABLE_SATURATED_HEART: + case OSRS_CONSUMABLE_ANTIVENOM_PLUS: + case OSRS_CONSUMABLE_NONE: + return 0; + } + abort(); +} + +#define COLO_BOOST_REDRINK_LEVEL 105 + +static int col_restore_worth_drinking(const ColosseumState* s) { + const Player* p = &s->player; + int stats_drained = p->current_attack < p->base_attack || + p->current_strength < p->base_strength || + p->current_defence < p->base_defence || + p->current_ranged < p->base_ranged || + p->current_magic < p->base_magic; + int restore_amount = osrs_super_restore_amount(p->base_prayer); + int pray_worth = (p->base_prayer - p->current_prayer) >= (restore_amount + 1) / 2; + return stats_drained || pray_worth; +} + +static int col_drink_has_effect(const ColosseumState* s, OsrsConsumableKind kind) { + const Player* p = &s->player; + int afflicted = s->player_venom > 0 || s->player_poison > 0; + switch (kind) { + case OSRS_CONSUMABLE_BREW: + return p->current_hitpoints < p->base_hitpoints; + case OSRS_CONSUMABLE_SUPER_COMBAT: + case OSRS_CONSUMABLE_DIVINE_COMBAT: + return p->current_attack < COLO_BOOST_REDRINK_LEVEL || + p->current_strength < COLO_BOOST_REDRINK_LEVEL || + p->current_defence < COLO_BOOST_REDRINK_LEVEL; + case OSRS_CONSUMABLE_RANGING: + case OSRS_CONSUMABLE_DIVINE_RANGING: + return p->current_ranged < COLO_BOOST_REDRINK_LEVEL; + case OSRS_CONSUMABLE_SATURATED_HEART: + return p->current_magic < COLO_BOOST_REDRINK_LEVEL; + case OSRS_CONSUMABLE_SUPER_RESTORE: + return col_restore_worth_drinking(s); + case OSRS_CONSUMABLE_SANFEW: + return afflicted || col_restore_worth_drinking(s); + default: + return 1; + } +} + +static int col_inventory_cell_actionable(const ColosseumState* s, int cell_idx) { + const ColoInvCell* cell = &s->inventory_cells[cell_idx]; + OsrsInventoryClickResolution r = + osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); + if (r.click_action == OSRS_CLICK_EQUIP) return col_can_equip_from_cell(s, cell_idx); + if (r.click_action == OSRS_CLICK_DRINK) + return cell->dose > 0 && s->player.potion_timer == 0 && + col_drink_has_effect(s, r.consumable_kind); + if (r.click_action == OSRS_CLICK_EAT) + return col_can_eat_consumable_kind(s, r.consumable_kind); + return 0; +} + +static void col_empty_inventory_cell(ColosseumState* s, int cell_idx) { + s->inventory_cells[cell_idx] = osrs_inventory_cell_empty(); +} + +static void col_heal_player_capped(ColosseumState* s, int amount) { + s->player.current_hitpoints += amount; + if (s->player.current_hitpoints > s->player.base_hitpoints) + s->player.current_hitpoints = s->player.base_hitpoints; +} + +static void col_apply_food_cell(ColosseumState* s, OsrsConsumableKind kind) { + FoodType type; + if (kind == OSRS_CONSUMABLE_SHARK_FOOD) type = FOOD_SHARK; + else if (kind == OSRS_CONSUMABLE_KARAMBWAN) type = FOOD_KARAMBWAN; + else { + fprintf(stderr, "colosseum: consumable kind %d is not food\n", (int)kind); + abort(); + } + + EatResult r = osrs_eat_food(type, s->player.current_hitpoints, + s->player.base_hitpoints, osrs_player_food_timer(&s->player, type)); + if (!r.consumed) return; + if (type == FOOD_KARAMBWAN) { + s->player.karambwan_timer = 2; + s->player.food_timer = 3; + s->player.potion_timer = 3; + s->player.ate_karambwan_this_tick = 1; + s->player.last_karambwan_heal = r.hp_healed; + } else { + s->player.food_timer = 3; + s->player.ate_food_this_tick = 1; + s->player.last_food_heal = r.hp_healed; + } + col_heal_player_capped(s, r.hp_healed); +} + +static void col_apply_drink_one_dose_effect(void* ctx, OsrsConsumableKind kind) { + ColosseumState* s = (ColosseumState*)ctx; + switch (kind) { + case OSRS_CONSUMABLE_BREW: + encounter_apply_brew_heal( + &s->player, osrs_brew_heal_amount(s->player.base_hitpoints)); + encounter_apply_consumable_stat_effect( + &s->player, ENCOUNTER_CONSUMABLE_STAT_EFFECT_BREW_DRAIN); + col_finish_stat_moving_consumable(s); + return; + case OSRS_CONSUMABLE_SUPER_RESTORE: + encounter_add_prayer_restore( + &s->player, osrs_super_restore_amount(s->player.base_prayer)); + encounter_apply_consumable_stat_effect( + &s->player, ENCOUNTER_CONSUMABLE_STAT_EFFECT_RESTORE); + encounter_cap_prayer_restore(&s->player); + col_finish_stat_moving_consumable(s); + return; + case OSRS_CONSUMABLE_SANFEW: + encounter_add_prayer_restore( + &s->player, osrs_sanfew_restore_amount(s->player.base_prayer)); + encounter_apply_consumable_stat_effect( + &s->player, ENCOUNTER_CONSUMABLE_STAT_EFFECT_SANFEW); + s->player_venom = 0; + s->player_venom_timer = 0; + s->player_poison = 0; + s->player_poison_timer = 0; + encounter_cap_prayer_restore(&s->player); + col_finish_stat_moving_consumable(s); + return; + case OSRS_CONSUMABLE_SUPER_COMBAT: + encounter_super_combat_boost(&s->player); + col_enforce_divine_stat_floors(s); + col_mark_live_loadout_dirty(s); + return; + case OSRS_CONSUMABLE_DIVINE_COMBAT: + encounter_super_combat_boost(&s->player); + col_apply_divine_combat_potion_effect(s); + col_enforce_divine_stat_floors(s); + col_mark_live_loadout_dirty(s); + return; + case OSRS_CONSUMABLE_RANGING: + encounter_ranging_boost(&s->player); + col_enforce_divine_stat_floors(s); + col_mark_live_loadout_dirty(s); + return; + case OSRS_CONSUMABLE_DIVINE_RANGING: + encounter_ranging_boost(&s->player); + col_apply_divine_ranged_potion_effect(s); + col_enforce_divine_stat_floors(s); + col_mark_live_loadout_dirty(s); + return; + case OSRS_CONSUMABLE_SURGE: + s->player.special_energy += COLO_SURGE_RESTORE_AMOUNT; + if (s->player.special_energy > 100) s->player.special_energy = 100; + s->surge_cooldown = COLO_SURGE_COOLDOWN_TICKS; + return; + case OSRS_CONSUMABLE_GUTHIX_REST: + col_heal_player_capped(s, 5); + s->player_venom = 0; + s->player_venom_timer = 0; + s->player_poison = 0; + s->player_poison_timer = 0; + return; + case OSRS_CONSUMABLE_SATURATED_HEART: + encounter_apply_saturated_heart_boost(&s->player); + col_mark_live_loadout_dirty(s); + return; + case OSRS_CONSUMABLE_ANTIVENOM_PLUS: + s->player_venom = 0; + s->player_venom_timer = 0; + s->player_poison = 0; + s->player_poison_timer = 0; + return; + case OSRS_CONSUMABLE_NONE: + case OSRS_CONSUMABLE_SHARK_FOOD: + case OSRS_CONSUMABLE_KARAMBWAN: + fprintf(stderr, "colosseum: consumable kind %d is not drinkable\n", (int)kind); + abort(); + } + abort(); +} + +static int col_apply_drink_cell( + ColosseumState* s, + int cell_idx, + OsrsInventoryClickResolution resolution +) { + if (cell_idx < 0 || cell_idx >= COLO_INVENTORY_DISPLAY_SLOTS) return 0; + ColoInvCell* cell = &s->inventory_cells[cell_idx]; + if (resolution.click_action != OSRS_CLICK_DRINK) return 0; + OsrsInventoryDrinkConsumeResult consume = + osrs_inventory_cell_consume_drink_one_dose( + cell, + resolution, + &s->player.potion_timer, + col_apply_drink_one_dose_effect, + s); + if (!consume.consumed) return 0; + col_sync_consumable_counters_from_inventory(s); + return 1; +} + +typedef struct { + int equip_cell_by_slot[NUM_GEAR_SLOTS]; + int equip_order_by_slot[NUM_GEAR_SLOTS]; + int eat_cell; + int eat_order; + OsrsInventoryClickResolution eat_resolution; + int drink_cell; + int drink_order; + OsrsInventoryClickResolution drink_resolution; +} ColoInventoryTickIntent; + +static ColoInventoryTickIntent col_inventory_tick_intent_empty(void) { + ColoInventoryTickIntent intent; + memset(&intent, 0, sizeof(intent)); + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + intent.equip_cell_by_slot[slot] = -1; + intent.equip_order_by_slot[slot] = -1; + } + intent.eat_cell = -1; + intent.eat_order = -1; + intent.drink_cell = -1; + intent.drink_order = -1; + return intent; +} + +static ColoInventoryTickIntent col_resolve_inventory_tick_intent( + const ColosseumState* s, + const int* actions +) { + ColoInventoryTickIntent intent = col_inventory_tick_intent_empty(); + + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + int action = actions[COLO_HEAD_EQUIP_SLOT(slot)]; + if (action <= 0 || action > COLO_INVENTORY_DISPLAY_SLOTS) continue; + int cell_idx = action - 1; + if (!col_can_equip_from_cell(s, cell_idx)) continue; + int gear_slot = osrs_item_gear_slot(s->inventory_cells[cell_idx].item_idx); + if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) { + fprintf(stderr, "colosseum equip intent: invalid gear slot %d\n", gear_slot); + abort(); + } + if (gear_slot != slot) continue; + intent.equip_cell_by_slot[slot] = cell_idx; + intent.equip_order_by_slot[slot] = slot; + } + + int eat_action = actions[COLO_HEAD_EAT]; + if (eat_action > 0 && eat_action <= COLO_INVENTORY_DISPLAY_SLOTS) { + int cell_idx = eat_action - 1; + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_interpret( + &s->inventory_cells[cell_idx], OSRS_CLICK_TICK_FIRST); + if (resolution.click_action == OSRS_CLICK_EAT && + col_can_eat_consumable_kind(s, resolution.consumable_kind)) { + intent.eat_cell = cell_idx; + intent.eat_order = NUM_GEAR_SLOTS; + intent.eat_resolution = resolution; + } + } + + int drink_action = actions[COLO_HEAD_DRINK]; + if (drink_action > 0 && drink_action <= COLO_INVENTORY_DISPLAY_SLOTS) { + int cell_idx = drink_action - 1; + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_interpret( + &s->inventory_cells[cell_idx], OSRS_CLICK_TICK_FIRST); + if (resolution.click_action == OSRS_CLICK_DRINK && + s->inventory_cells[cell_idx].dose > 0 && + s->player.potion_timer == 0) { + intent.drink_cell = cell_idx; + intent.drink_order = NUM_GEAR_SLOTS + 1; + intent.drink_resolution = resolution; + } + } + + return intent; +} + +static int col_inventory_tick_intent_has_effect(const ColoInventoryTickIntent* intent) { + if (intent->eat_cell >= 0 || intent->drink_cell >= 0) return 1; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + if (intent->equip_cell_by_slot[slot] >= 0) return 1; + return 0; +} + +static int col_next_inventory_apply_order(const ColoInventoryTickIntent* intent) { + int sentinel = NUM_GEAR_SLOTS + 2; + int best = sentinel; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + int order = intent->equip_order_by_slot[slot]; + if (order >= 0 && order < best) best = order; + } + if (intent->eat_order >= 0 && intent->eat_order < best) best = intent->eat_order; + if (intent->drink_order >= 0 && intent->drink_order < best) best = intent->drink_order; + return best == sentinel ? -1 : best; +} + +static void col_apply_inventory_tick_intent( + ColosseumState* s, + ColoInventoryTickIntent* intent +) { + if (col_inventory_tick_intent_has_effect(intent)) + osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EQUIP); + + int order; + while ((order = col_next_inventory_apply_order(intent)) >= 0) { + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + if (intent->equip_order_by_slot[slot] != order) continue; + col_equip_from_cell(s, intent->equip_cell_by_slot[slot]); + intent->equip_order_by_slot[slot] = -1; + intent->equip_cell_by_slot[slot] = -1; + } + if (intent->eat_order == order) { + col_apply_food_cell(s, intent->eat_resolution.consumable_kind); + col_empty_inventory_cell(s, intent->eat_cell); + intent->eat_order = -1; + intent->eat_cell = -1; + } + if (intent->drink_order == order) { + (void)col_apply_drink_cell( + s, intent->drink_cell, intent->drink_resolution); + intent->drink_order = -1; + intent->drink_cell = -1; + } + } +} + +static void col_log_inventory_tick_intent( + const ColosseumState* s, + const ColoInventoryTickIntent* intent, + const int* actions, + int interrupted +) { + fprintf(stderr, "colo_action tick=%d equip_heads=", s->tick); + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + fprintf(stderr, "%s%d", slot == 0 ? "" : ",", + actions[COLO_HEAD_EQUIP_SLOT(slot)]); + fprintf(stderr, " eat_head=%d drink_head=%d equips=", + actions[COLO_HEAD_EAT], actions[COLO_HEAD_DRINK]); + int wrote = 0; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + if (intent->equip_cell_by_slot[slot] < 0) continue; + fprintf(stderr, "%sslot%d:cell%d", wrote ? "," : "", + slot, intent->equip_cell_by_slot[slot]); + wrote = 1; + } + fprintf(stderr, " eat_cell=%d drink_cell=%d interrupted=%d\n", + intent->eat_cell, intent->drink_cell, interrupted); +} + +static int col_npc_is_thrall_target(const ColosseumState* s, int slot) { + if (slot < 0 || slot >= COLO_MAX_NPCS) return 0; + const ColoNPC* npc = &s->npcs[slot]; + return col_npc_is_live_enemy(npc) && npc->type != COLO_SOL_HEREDIT; +} + +static int col_player_target_slot(const ColosseumState* s) { + if (!osrs_interaction_active(&s->interaction)) return -1; + return s->interaction.target_slot; +} + +static void col_summon_thrall(ColosseumState* s, int target_slot) { + s->thrall_active = 1; + s->thrall_target_slot = target_slot; + s->thrall_lifetime_left = COLO_THRALL_LIFETIME_TICKS[s->active_loadout_profile]; + s->thrall_attack_timer = COLO_THRALL_TICK; + s->thrall_recast_cd = COLO_THRALL_RECAST_CD; +} + +static void col_tick_thrall_and_death_charge(ColosseumState* s, ColosseumContext* ctx) { + if (s->thrall_recast_cd > 0) s->thrall_recast_cd--; + if (s->death_charge_cd > 0) s->death_charge_cd--; + if (s->death_charge_window_left > 0) s->death_charge_window_left--; + + if (s->thrall_active) { + s->thrall_lifetime_left--; + if (s->thrall_lifetime_left <= 0) { + s->thrall_active = 0; + s->thrall_target_slot = -1; + s->thrall_lifetime_left = 0; + s->thrall_attack_timer = 0; + return; + } + int player_target = col_player_target_slot(s); + if (col_npc_is_thrall_target(s, player_target)) + s->thrall_target_slot = player_target; + else if (!col_npc_is_thrall_target(s, s->thrall_target_slot)) + s->thrall_target_slot = -1; + + if (s->thrall_attack_timer > 0) s->thrall_attack_timer--; + if (s->thrall_attack_timer == 0) { + s->thrall_attack_timer = COLO_THRALL_TICK; + if (col_npc_is_thrall_target(s, s->thrall_target_slot)) { + int dmg = encounter_rand_int(&s->rng_state, COLO_THRALL_MAX_HIT + 1); + col_queue_npc_pending_hit(s, s->thrall_target_slot, dmg, 2, + ATTACK_STYLE_MAGIC, ENCOUNTER_SPELL_NONE); + ctx->thrall_attacked_this_tick = 1; + } + } + } +} + +typedef struct { + ColosseumState* s; + const ColosseumContext* ctx; +} ColoWalkCtx; + +static int col_tile_walkable(void* ctx, int x, int y) { + ColoWalkCtx* wc = (ColoWalkCtx*)ctx; + return col_player_walkable(wc->s, x, y); +} + +static int col_pathfind_blocked(void* ctx, int abs_x, int abs_y) { + ColoWalkCtx* wc = (ColoWalkCtx*)ctx; + int x = abs_x - wc->ctx->world_offset_x; + int y = abs_y - wc->ctx->world_offset_y; + if (col_static_blocked(x, y)) return 1; + if (col_sol_clamp_active(wc->s) && !col_in_boss_arena(wc->s, x, y)) return 1; + return 0; +} + +static int col_npc_collision_flag_at(const ColosseumState* s, int x, int y) { + int gx, gy; + if (!col_grid_index(x, y, &gx, &gy)) return 0; + return s->npc_collision_flags[gx][gy] != 0; +} + +static int col_player_attack_range(ColosseumState* s) { + const EncounterLoadoutStats* ls = col_live_loadout_stats(s); + int base = ls->attack_range > 0 ? ls->attack_range : 1; + return col_mod_myopia_range(s, base); +} + +static int col_lookup_player_attack_target(void* ctx, int target_slot, OsrsAttackTarget* out) { + ColosseumState* s = (ColosseumState*)ctx; + if (target_slot < 0 || target_slot >= COLO_MAX_NPCS) return 0; + ColoNPC* npc = &s->npcs[target_slot]; + if (!col_npc_is_live_target(npc)) return 0; + out->slot = target_slot; + out->x = npc->x; + out->y = npc->y; + out->size = col_npc_effective_size(npc); + out->attack_range = col_player_attack_range(s); + return 1; +} + +static int col_obs_slot_to_npc(const ColosseumState* s, int obs_slot) { + if (obs_slot < 0 || obs_slot >= COLO_OBS_NPCS) return -1; + return s->current_obs_slots[obs_slot]; +} + +static MeleeStyle col_item_melee_style(uint8_t weapon_item) { + const Item* weapon = get_item(weapon_item); + if (!weapon) return MELEE_STYLE_STAB; + MeleeStyle style = MELEE_STYLE_STAB; + int best = weapon->attack_stab; + if (weapon->attack_slash > best) { best = weapon->attack_slash; style = MELEE_STYLE_SLASH; } + if (weapon->attack_crush > best) { style = MELEE_STYLE_CRUSH; } + return style; +} + +static int col_weapon_has_item_effect(uint8_t weapon_item, uint32_t effect_mask) { + const Item* weapon = get_item(weapon_item); + return weapon && (weapon->effect_mask & effect_mask) != 0; +} + +static int col_npc_effective_def_level(const ColoNPC* npc, const ColoNpcStats* ns) { + int def = ns->def_level - npc->def_drained; + return def < 0 ? 0 : def; +} + +static void col_player_spec_attack(ColosseumState* s, int npc_slot) { + ColoNPC* npc = &s->npcs[npc_slot]; + const ColoNpcStats* ns = &COLO_NPC_STATS[npc->type]; + uint8_t item = s->player.equipped[GEAR_SLOT_WEAPON]; + assert(osrs_spec_cost(item) > 0 && + "spec attack dispatched with a non-special weapon equipped"); + const EncounterLoadoutStats* ls = col_live_loadout_stats(s); + const OsrsEquipmentEffectProfile* effects = col_live_effects(s); + + s->player.spec_armed = 0; + if (!encounter_use_spec(&s->player, osrs_spec_cost(item))) return; + + int eff_def = col_npc_effective_def_level(npc, ns); + int def_bonus = encounter_npc_melee_def_bonus( + ns->stab_def, ns->slash_def, ns->crush_def, col_item_melee_style(item)); + int def_roll = osrs_npc_def_roll(eff_def, def_bonus); + int att_roll = osrs_player_att_roll(ls->eff_level, ls->attack_bonus); + + SpecResult r = osrs_resolve_spec(item, att_roll, ls->max_hit, def_roll, + eff_def, &s->rng_state); + if (s->wave == COLO_WAVE_BOSS && s->sol.started && s->sol.next_attack_guaranteed_max) { + osrs_spec_result_force_max(&r, item, ls->max_hit, eff_def); + s->sol.next_attack_guaranteed_max = 0; + s->sol.guaranteed_max_ticks = 0; + } + + int total = 0; + int bf_heal = 0; + int num_hits = r.num_hits > 0 ? r.num_hits : 1; + for (int i = 0; i < num_hits; i++) { + col_queue_npc_pending_hit(s, npc_slot, r.damage[i], 0, + ATTACK_STYLE_MELEE, ENCOUNTER_SPELL_NONE); + total += r.damage[i]; + bf_heal += osrs_blood_fury_heal_amount(effects, ATTACK_STYLE_MELEE, + r.damage[i], &s->rng_state); + } + + if (r.def_drain > 0) { + npc->def_drained += r.def_drain; + if (npc->def_drained > ns->def_level) npc->def_drained = ns->def_level; + } + if (r.heal > 0) { + col_heal_player_capped(s, r.heal); + } + if (r.prayer_restore > 0) { + s->player.current_prayer += r.prayer_restore; + if (s->player.current_prayer > s->player.base_prayer) + s->player.current_prayer = s->player.base_prayer; + } + + OsrsPostAttackEffects post = osrs_finalize_attack_effects( + effects, &s->player.item_effect_state, item, + ATTACK_STYLE_MELEE, OSRS_MAGIC_ATTACK_NONE, osrs_target_ref_none(), 1, + 0, total > 0, total, &s->rng_state); + int spec_heal = post.heal_amount + bf_heal; + if (spec_heal > 0) { + col_heal_player_capped(s, spec_heal); + } + + s->player.attack_timer = ls->attack_speed > 0 ? ls->attack_speed : 4; + s->player_attack_npc_idx = npc_slot; + s->player_attack_dmg = total; + s->player_attack_style_id = ATTACK_STYLE_MELEE; + s->player_attack_timing = (EncounterProjectileTiming){ + .damage_delay_ticks = 0, + .visual_start_delay_ticks = 0, + .visual_duration_ticks = 1, + }; + s->player.attack_style_this_tick = ATTACK_STYLE_MELEE; + s->player.used_special_this_tick = 1; + s->tick_scratch.player_attacked = 1; + s->tick_scratch.attacks_fired++; +} + +#define COLO_SCYTHE_MAX_HITS 5 + +typedef struct { + int npc_slot; + int splat_rank; +} ColScytheResolvedHit; + +static int col_signum(int value) { + return (value > 0) - (value < 0); +} + +static int col_npc_occupies_tile(const ColoNPC* npc, int x, int y) { + int size = col_npc_effective_size(npc); + return x >= npc->x && x < npc->x + size && + y >= npc->y && y < npc->y + size; +} + +static int col_scythe_tile_target( + const ColosseumState* s, + int primary_slot, + int x, + int y +) { + const ColoNPC* primary = &s->npcs[primary_slot]; + if (col_npc_is_live_target(primary) && col_npc_occupies_tile(primary, x, y)) + return primary_slot; + + for (int i = 0; i < COLO_MAX_NPCS; i++) { + if (i == primary_slot) continue; + const ColoNPC* npc = &s->npcs[i]; + if (!col_npc_is_live_enemy(npc)) continue; + if (col_npc_occupies_tile(npc, x, y)) + return i; + } + return -1; +} + +static void col_scythe_front_arc_tiles( + const ColosseumState* s, + const ColoNPC* primary, + int tile_x[3], + int tile_y[3] +) { + int size = col_npc_effective_size(primary); + int closest_x = s->player.x; + int closest_y = s->player.y; + if (closest_x < primary->x) closest_x = primary->x; + else if (closest_x >= primary->x + size) closest_x = primary->x + size - 1; + if (closest_y < primary->y) closest_y = primary->y; + else if (closest_y >= primary->y + size) closest_y = primary->y + size - 1; + + int dx = col_signum(closest_x - s->player.x); + int dy = col_signum(closest_y - s->player.y); + if (dx == 0 && dy == 0) { + fprintf(stderr, "invalid scythe target direction\n"); + abort(); + } + + tile_x[0] = s->player.x + dx; + tile_y[0] = s->player.y + dy; + if (dx != 0 && dy != 0) { + tile_x[1] = s->player.x + dx; + tile_y[1] = s->player.y; + tile_x[2] = s->player.x; + tile_y[2] = s->player.y + dy; + return; + } + + int left_x = -dy; + int left_y = dx; + tile_x[1] = tile_x[0] + left_x; + tile_y[1] = tile_y[0] + left_y; + tile_x[2] = tile_x[0] - left_x; + tile_y[2] = tile_y[0] - left_y; +} + +static int col_resolve_scythe_hits( + const ColosseumState* s, + int primary_slot, + ColScytheResolvedHit out[COLO_SCYTHE_MAX_HITS] +) { + const ColoNPC* primary = &s->npcs[primary_slot]; + int count = 0; + + int primary_splats = osrs_scythe_splats_for_target_size(col_npc_effective_size(primary)); + for (int rank = 0; rank < primary_splats; rank++) { + out[count++] = (ColScytheResolvedHit){ + .npc_slot = primary_slot, + .splat_rank = rank, + }; + } + + if (col_npc_occupies_tile(primary, s->player.x, s->player.y)) + return count; + + int tile_x[3]; + int tile_y[3]; + col_scythe_front_arc_tiles(s, primary, tile_x, tile_y); + for (int t = 0; t < 3 && count < COLO_SCYTHE_MAX_HITS; t++) { + int target_slot = col_scythe_tile_target(s, primary_slot, tile_x[t], tile_y[t]); + if (target_slot < 0 || target_slot == primary_slot) continue; + int already = 0; + for (int j = 0; j < count; j++) + if (out[j].npc_slot == target_slot) { already = 1; break; } + if (already) continue; + out[count++] = (ColScytheResolvedHit){ + .npc_slot = target_slot, + .splat_rank = 0, + }; + } + return count; +} + +static OsrsVenatorMonster col_venator_monster_from_npc( + const ColoNPC* npc, + int slot +) { + if (!npc || !col_npc_is_live_enemy(npc)) abort(); + return osrs_venator_monster( + slot, + npc->x, + npc->y, + col_npc_effective_size(npc), + OSRS_VENATOR_MONSTER_ALIVE); +} + +static OsrsVenatorChain col_resolve_venator_chain( + const ColosseumState* s, + int primary_slot +) { + if (!s || primary_slot < 0 || primary_slot >= COLO_MAX_NPCS) abort(); + const ColoNPC* primary_npc = &s->npcs[primary_slot]; + OsrsVenatorMonster primary = + col_venator_monster_from_npc(primary_npc, primary_slot); + OsrsVenatorMonster candidates[COLO_MAX_NPCS]; + int candidate_count = 0; + for (int slot = 0; slot < COLO_MAX_NPCS; slot++) { + const ColoNPC* npc = &s->npcs[slot]; + if (!col_npc_is_live_enemy(npc)) continue; + candidates[candidate_count++] = col_venator_monster_from_npc(npc, slot); + } + return osrs_venator_resolve_chain(primary, candidates, candidate_count); +} + +static int col_npc_target_def_roll( + const ColoNPC* npc, + const ColoNpcStats* ns, + AttackStyle style, + MeleeStyle weapon_melee_style +) { + return encounter_npc_target_def_roll( + col_npc_effective_def_level(npc, ns), ns->magic_level, + ns->stab_def, ns->slash_def, ns->crush_def, + ns->magic_def_bonus, ns->ranged_def_bonus, + style, weapon_melee_style); +} + +static float col_expected_dpt_vs_npc( + const EncounterLoadoutStats* ls, + const OsrsEquipmentEffectProfile* effects, + uint8_t weapon_item, + AttackStyle style, + MeleeStyle melee_style, + const ColoNPC* npc, + const ColoNpcStats* ns +) { + if (!ls || !effects || !npc || !ns) abort(); + if (!col_npc_is_live_target(npc) || col_type_is_hazard_entity(npc->type)) + return 0.0f; + if (ls->attack_speed < 0 || ls->max_hit < 0) { + fprintf(stderr, "invalid Colosseum expected-DPT loadout\n"); + abort(); + } + if (style == ATTACK_STYLE_NONE || ls->attack_speed == 0 || ls->max_hit == 0) + return 0.0f; + + OsrsItemEffectState tmp_effect_state = {0}; + int base_att_roll = osrs_player_att_roll(ls->eff_level, ls->attack_bonus); + OsrsPreparedAttackEffects prepared = + osrs_prepare_attack_effects_for_melee_style( + effects, + &tmp_effect_state, + weapon_item, + style, + melee_style, + OSRS_MAGIC_ATTACK_NONE, + osrs_target_ref_none(), + 1, + base_att_roll, + ls->max_hit, + osrs_target_effect_context_magic(ns->magic_level, ns->magic_att_bonus), + MAXED_BASE_HITPOINTS, + MAXED_BASE_HITPOINTS); + + int target_def_roll = col_npc_target_def_roll(npc, ns, style, melee_style); + float hit_prob; + if (osrs_confliction_can_apply(effects, style, weapon_item, 1)) { + + float single = osrs_hit_chance(prepared.attack_roll, target_def_roll); + float dbl = osrs_hit_chance_double(prepared.attack_roll, target_def_roll); + float denom = 1.0f + dbl - single; + hit_prob = denom > 0.0f ? dbl / denom : dbl; + } else if (prepared.use_fang_accuracy || prepared.use_double_accuracy) { + hit_prob = osrs_hit_chance_double(prepared.attack_roll, target_def_roll); + } else { + hit_prob = osrs_hit_chance(prepared.attack_roll, target_def_roll); + } + + int splat_count = 1; + if (weapon_item == ITEM_SCYTHE_OF_VITUR) + splat_count = osrs_scythe_splats_for_target_size(col_npc_effective_size(npc)); + + float expected_damage = 0.0f; + int forced_max_hit = ns->player_style_that_max_hits != ATTACK_STYLE_NONE && + ns->player_style_that_max_hits == style; + for (int splat = 0; splat < splat_count; splat++) { + int splat_max = prepared.max_hit >> splat; + if (forced_max_hit) { + expected_damage += (float)splat_max; + } else { + expected_damage += + hit_prob * ((float)prepared.min_hit + (float)splat_max) * 0.5f; + } + } + + return expected_damage / (float)ls->attack_speed; +} + +static void col_player_attack_target_ctx( + ColosseumState* s, + ColosseumContext* ctx, + int npc_slot +) { + ColoNPC* npc = &s->npcs[npc_slot]; + const ColoNpcStats* ns = &COLO_NPC_STATS[npc->type]; + const EncounterLoadoutStats* ls = col_live_loadout_stats(s); + + if (!col_type_is_hazard_entity(npc->type) && s->player.spec_armed) { + col_player_spec_attack(s, npc_slot); + return; + } + + const OsrsEquipmentEffectProfile* effects = col_live_effects(s); + uint8_t weapon_item = s->player.equipped[GEAR_SLOT_WEAPON]; + int projectile_dist = encounter_projectile_distance( + s->player.x, s->player.y, 1, + npc->x, npc->y, col_npc_effective_size(npc), + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + EncounterProjectileTiming hit_timing = + col_player_projectile_timing(ls->style, weapon_item, projectile_dist); + int base_att_roll = osrs_player_att_roll(ls->eff_level, ls->attack_bonus); + MeleeStyle weapon_melee_style = col_item_melee_style(weapon_item); + OsrsPreparedAttackEffects prepared = osrs_prepare_attack_effects_for_melee_style( + effects, &s->player.item_effect_state, weapon_item, ls->style, + weapon_melee_style, OSRS_MAGIC_ATTACK_NONE, osrs_target_ref_none(), 1, + base_att_roll, ls->max_hit, + osrs_target_effect_context_magic(ns->magic_level, ns->magic_att_bonus), + s->player.current_hitpoints, s->player.base_hitpoints); + + ColScytheResolvedHit resolved_hits[COLO_SCYTHE_MAX_HITS]; + OsrsVenatorChain venator_chain = { OSRS_VENATOR_CHAIN_LENGTH_ONE }; + int resolved_count = 1; + resolved_hits[0] = (ColScytheResolvedHit){ .npc_slot = npc_slot, .splat_rank = 0 }; + int venator_attack = + !col_type_is_hazard_entity(npc->type) && + col_weapon_has_item_effect(weapon_item, OSRS_ITEM_EFFECT_VENATOR_BOUNCE); + if (venator_attack) { + venator_chain = col_resolve_venator_chain(s, npc_slot); + resolved_count = (int)venator_chain.length; + col_context_record_venator_chain(ctx, &venator_chain); + } else if (weapon_item == ITEM_SCYTHE_OF_VITUR && + !col_type_is_hazard_entity(npc->type)) { + resolved_count = col_resolve_scythe_hits(s, npc_slot, resolved_hits); + } + if (resolved_count <= 0) { + fprintf(stderr, "player attack resolved no targets\n"); + abort(); + } + + int ticks = ls->style == ATTACK_STYLE_MELEE + ? hit_timing.damage_delay_ticks + : (ls->attack_range > 1 ? 3 : 1); + int damage = 0; + int bf_heal = 0; + for (int splat = 0; splat < resolved_count; splat++) { + int hit_slot = venator_attack + ? venator_chain.hits[splat].slot + : resolved_hits[splat].npc_slot; + ColoNPC* hit_npc = &s->npcs[hit_slot]; + const ColoNpcStats* hit_ns = &COLO_NPC_STATS[hit_npc->type]; + int rps_max_hit = hit_ns->player_style_that_max_hits != ATTACK_STYLE_NONE && + hit_ns->player_style_that_max_hits == ls->style; + int target_def_roll = col_npc_target_def_roll( + hit_npc, hit_ns, ls->style, weapon_melee_style); + int splat_max = venator_attack + ? (splat == 0 + ? prepared.max_hit + : osrs_venator_bounce_max_hit(prepared.max_hit)) + : (prepared.max_hit >> resolved_hits[splat].splat_rank); + int splat_damage; + if (col_type_is_hazard_entity(hit_npc->type)) { + splat_damage = hit_npc->hp; + } else if (s->sol.next_attack_guaranteed_max) { + splat_damage = splat_max; + s->sol.next_attack_guaranteed_max = 0; + s->sol.guaranteed_max_ticks = 0; + } else if (rps_max_hit) { + splat_damage = splat_max; + } else { + splat_damage = osrs_roll_prepared_attack_damage( + &prepared, target_def_roll, splat_max, &s->rng_state); + } + int hit_ticks = venator_attack ? ticks + splat : ticks; + col_queue_npc_pending_hit(s, hit_slot, splat_damage, hit_ticks, + ls->style, ENCOUNTER_SPELL_NONE); + damage += splat_damage; + bf_heal += osrs_blood_fury_heal_amount(effects, ls->style, splat_damage, + &s->rng_state); + } + + OsrsPostAttackEffects post = osrs_finalize_attack_effects( + effects, &s->player.item_effect_state, weapon_item, ls->style, + OSRS_MAGIC_ATTACK_NONE, osrs_target_ref_none(), 1, 0, + damage > 0, damage, &s->rng_state); + int swing_heal = post.heal_amount + bf_heal; + if (swing_heal > 0) { + col_heal_player_capped(s, swing_heal); + } + + s->player.attack_timer = ls->attack_speed > 0 ? ls->attack_speed : 4; + s->player_attack_npc_idx = npc_slot; + s->player_attack_dmg = damage; + s->player_attack_style_id = ls->style; + s->player_attack_timing = hit_timing; + s->player.attack_style_this_tick = ls->style; + s->tick_scratch.player_attacked = 1; + s->tick_scratch.attacks_fired++; +} + +static void col_player_attack_target(ColosseumState* s, int npc_slot) { + col_player_attack_target_ctx(s, col_legacy_context(), npc_slot); +} + +static void col_tick_player_ctx( + ColosseumState* s, + ColosseumContext* ctx, + const int* actions, + int can_attack +) { + if (s->player_last_interaction_age == 0) + s->player_last_interaction_age = 1; + + osrs_tick_special_regen(&s->player); + + if (s->player.potion_timer > 0) s->player.potion_timer--; + if (s->player.food_timer > 0) s->player.food_timer--; + if (s->player.karambwan_timer > 0) s->player.karambwan_timer--; + if (encounter_tick_saturated_heart(&s->player)) + col_mark_live_loadout_dirty(s); + + if (!ctx->config.mask_inventory_heads) { + ColoInventoryTickIntent intent = col_resolve_inventory_tick_intent(s, actions); + if (ctx->config.action_debug_log) + col_log_inventory_tick_intent( + s, &intent, actions, col_inventory_tick_intent_has_effect(&intent)); + col_apply_inventory_tick_intent(s, &intent); + } + + int spec_act = actions[COLO_HEAD_SPEC]; + uint8_t equipped_weapon = s->player.equipped[GEAR_SLOT_WEAPON]; + int spec_cost = osrs_spec_cost(equipped_weapon); + if (spec_act == 1 && spec_cost > 0 && s->player.special_energy >= spec_cost) { + s->player.spec_armed = 1; + } else if (spec_act == 2) { + s->player.spec_armed = 0; + } + + int spell_act = s->modifiers.draft_pending ? 0 : actions[COLO_HEAD_SPELL]; + if (spell_act == COLO_SPELL_SUMMON_THRALL && s->thrall_recast_cd == 0) { + int target = col_player_target_slot(s); + if (col_npc_is_thrall_target(s, target)) col_summon_thrall(s, target); + } else if (spell_act == COLO_SPELL_DEATH_CHARGE && + s->death_charge_cd == 0 && s->death_charge_window_left == 0) { + s->death_charge_window_left = COLO_DEATH_CHARGE_WINDOW; + } + + int modifier_act = actions[COLO_HEAD_MODIFIER_SELECT]; + if (modifier_act > 0 && s->modifiers.draft_pending) + col_modifier_apply_selection(s, modifier_act - 1); + + if (s->modifiers.draft_pending && !s->modifiers.draft_free_movement) return; + + int grapple_act = actions[COLO_HEAD_GRAPPLE_PARRY]; + if (grapple_act > 0 && grapple_act <= COLO_NUM_GRAPPLE_SLOTS) + col_sol_grapple_parry_attempt(s, grapple_act - 1); + + int primary_act = actions[COLO_HEAD_PRIMARY]; + if (primary_act < 0 || primary_act >= COLO_PRIMARY_DIM) { + fprintf(stderr, "colosseum primary action out of range: %d\n", primary_act); + abort(); + } + + int has_new_target = 0; + int new_target_slot = -1; + int move_act = 0; + if (primary_act > 0 && primary_act < ENCOUNTER_MOVE_ACTIONS) { + move_act = primary_act; + } else if (primary_act >= COLO_PRIMARY_ATTACK_BASE) { + int obs_slot = col_primary_obs_slot_from_action(primary_act); + int npc_slot = col_obs_slot_to_npc(s, obs_slot); + if (npc_slot >= 0 && s->npcs[npc_slot].active) { + has_new_target = 1; + new_target_slot = npc_slot; + } + } + + int has_explicit_move = (move_act > 0 || s->player_dest_x >= 0); + OsrsPlayerMoveKind move_kind = OSRS_PLAYER_MOVE_NONE; + if (has_explicit_move) { + if (s->player_dest_x >= 0) move_kind = OSRS_PLAYER_MOVE_DESTINATION; + else if (move_act > 0 && move_act < ENCOUNTER_MOVE_ACTIONS) + move_kind = OSRS_PLAYER_MOVE_ACTION; + } + int player_x_before_step = s->player.x; + int player_y_before_step = s->player.y; + int npc_flag_before_step = + col_npc_collision_flag_at(s, player_x_before_step, player_y_before_step); + int interaction_before_step = osrs_interaction_active(&s->interaction) + ? s->interaction.target_slot : -1; + ColoWalkCtx wc = { s, ctx }; + OsrsLosQuery los_query = col_player_los_query(s); + OsrsPlayerStepInput step_input = { + .player = &s->player, + .interaction = &s->interaction, + .target_lookup = col_lookup_player_attack_target, + .target_ctx = s, + .has_new_target = has_new_target, + .new_target_slot = new_target_slot, + .move_kind = move_kind, + .move_action = move_act, + .dest_x = &s->player_dest_x, + .dest_y = &s->player_dest_y, + .arena = { + .collision_map = ctx->collision_map, + .world_offset_x = ctx->world_offset_x, + .world_offset_y = ctx->world_offset_y, + .is_walkable = col_tile_walkable, + .walkable_ctx = &wc, + .extra_blocked = col_pathfind_blocked, + .blocked_ctx = &wc, + .los_query = &los_query, + .arena_base_x = COLO_ARENA_MIN_X, + .arena_base_y = COLO_ARENA_MIN_Y, + .arena_w = COLO_ARENA_WIDTH, + .arena_h = COLO_ARENA_HEIGHT, + }, + }; + OsrsPlayerStepResult step_result = osrs_encounter_player_step(&step_input); + if (has_new_target && step_result.target_slot == new_target_slot) { + s->player_last_interaction_target_slot = new_target_slot; + s->player_last_interaction_age = 0; + } + if (ctx->config.action_debug_log) { + fprintf(stderr, + "colo_action tick=%d movement move_kind=%d move_action=%d " + "dest=%d,%d has_new_target=%d new_target=%d interaction=%d->%d " + "pos=%d,%d->%d,%d moved=%d explicit=%d chased=%d can_attack=%d\n", + s->tick, move_kind, move_act, s->player_dest_x, s->player_dest_y, + has_new_target, new_target_slot, interaction_before_step, + step_result.target_slot, player_x_before_step, player_y_before_step, + s->player.x, s->player.y, step_result.moved, + step_result.explicit_moved, step_result.chased_target, + step_result.can_attack); + fprintf(stderr, + "colo_action tick=%d movement_walkability npc_flag=%d->%d " + "walkable=%d->%d path_extra=%d->%d\n", + s->tick, npc_flag_before_step, + col_npc_collision_flag_at(s, s->player.x, s->player.y), + col_player_walkable(s, player_x_before_step, player_y_before_step), + col_player_walkable(s, s->player.x, s->player.y), + col_pathfind_blocked(&wc, + player_x_before_step + ctx->world_offset_x, + player_y_before_step + ctx->world_offset_y), + col_pathfind_blocked(&wc, + s->player.x + ctx->world_offset_x, + s->player.y + ctx->world_offset_y)); + } + col_rebuild_player_collision_flags(s); + + if (ctx->config.bis_gear_oracle_mode) col_apply_bis_gear_oracle(s); + + if (can_attack && s->player.attack_timer > 0) s->player.attack_timer--; + if (can_attack && osrs_interaction_active(&s->interaction) && s->player.attack_timer == 0) { + int slot = s->interaction.target_slot; + ColoNPC* npc = &s->npcs[slot]; + if (npc->active) { + int player_range = col_player_attack_range(s); + if (encounter_player_can_attack(s->player.x, s->player.y, + npc->x, npc->y, col_npc_effective_size(npc), + player_range, &los_query)) { + + int armed_kind = (!col_type_is_hazard_entity(npc->type) && + s->player.spec_armed) + ? s->player.equipped[GEAR_SLOT_WEAPON] : 0; + col_player_attack_target_ctx(s, ctx, slot); + if (armed_kind > 0 && s->player.used_special_this_tick) + ctx->player_spec_fired_kind = armed_kind; + } + } + } + + if (can_attack) col_tick_thrall_and_death_charge(s, ctx); +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc new file mode 100644 index 0000000000..e4a62a11b2 --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_render_snapshot.inc @@ -0,0 +1,1215 @@ +#line 76 "encounter_colosseum.h" + +static void col_put_int_ctx( + EncounterState* state, + EncounterContext* context, + const char* key, + int value +) { + ColosseumState* s = (ColosseumState*)state; + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + if (strcmp(key, "start_wave") == 0) { + int internal = value - 1; + if (internal < 0) internal = 0; + if (internal >= COLO_NUM_WAVES) internal = COLO_NUM_WAVES - 1; + s->start_wave = internal; + ctx->config.start_wave = internal; + } + else if (strcmp(key, "seed") == 0) s->rng_state = (uint32_t)value; + else if (strcmp(key, "world_offset_x") == 0) ctx->world_offset_x = value; + else if (strcmp(key, "world_offset_y") == 0) ctx->world_offset_y = value; + else if (strcmp(key, "player_dest_x") == 0) s->player_dest_x = value; + else if (strcmp(key, "player_dest_y") == 0) s->player_dest_y = value; + else if (strcmp(key, "human_command_mode") == 0) + ctx->human_command_mode = encounter_require_binary_config("colosseum", key, value); + else if (strcmp(key, "curriculum_agent") == 0) + ctx->config.curriculum_agent = + encounter_require_binary_config("colosseum", key, value); + else if (strcmp(key, "step_out_forecast_obs_enabled") == 0) + ctx->config.step_out_forecast_obs_enabled = + encounter_require_binary_config("colosseum", key, value); + else if (strcmp(key, "threat_field_obs_enabled") == 0) + ctx->config.threat_field_obs_enabled = + encounter_require_binary_config("colosseum", key, value); + else if (strcmp(key, "mask_inventory_heads") == 0) + ctx->config.mask_inventory_heads = + encounter_require_binary_config("colosseum", key, value); + else if (strcmp(key, "farm_safe_damage_cap") == 0) + ctx->config.farm_safe_damage_cap = + encounter_require_binary_config("colosseum", key, value); + else if (strcmp(key, "farm_cap_waves") == 0) { + if (value < 0 || value > COLO_NUM_WAVES) { + fprintf(stderr, "colosseum: farm_cap_waves must be 0..%d, got %d\n", + COLO_NUM_WAVES, value); + abort(); + } + ctx->config.farm_cap_waves = value; + } + else if (strcmp(key, "action_debug_log") == 0) + ctx->config.action_debug_log = + encounter_require_binary_config("colosseum", key, value); + else if (strcmp(key, "prayer_oracle_mode") == 0) + ctx->config.prayer_oracle_mode = + encounter_require_binary_config("colosseum", key, value); + else if (strcmp(key, "late_start_state_mode") == 0) { + if (value < 0 || value > 2) { + fprintf(stderr, "colosseum: late_start_state_mode must be 0..2, got %d\n", + value); + abort(); + } + ctx->config.late_start_state_mode = value; + } + else if (strcmp(key, "bis_gear_oracle_mode") == 0) + ctx->config.bis_gear_oracle_mode = + encounter_require_binary_config("colosseum", key, value); + else if (strcmp(key, "invuln_mode") == 0) + ctx->config.invuln_mode = + encounter_require_binary_config("colosseum", key, value); + else if (strcmp(key, "episode_max_ticks_override") == 0) { + if (value < 0) { + fprintf(stderr, "colosseum: episode_max_ticks_override must be >= 0, got %d\n", + value); + abort(); + } + ctx->config.episode_max_ticks_override = value; + } + else if (strcmp(key, "remove_brews") == 0) + ctx->config.remove_brews = + encounter_require_binary_config("colosseum", key, value); + else if (strcmp(key, "damage_scale_anneal_ticks") == 0) { + if (value < 0) { + fprintf(stderr, "colosseum: damage_scale_anneal_ticks must be >= 0, got %d\n", + value); + abort(); + } + ctx->config.damage_scale_anneal_ticks = value; + } + else if (strcmp(key, "forecast_horizon") == 0) { + if (value < 1 || value > COLO_STEP_OUT_FORECAST_HORIZON) { + fprintf(stderr, "colosseum: forecast_horizon must be 1..%d, got %d\n", + COLO_STEP_OUT_FORECAST_HORIZON, value); + abort(); + } + ctx->config.forecast_horizon = value; + } + else if (strcmp(key, "forecast_run_tile_mode") == 0) { + if (value < 0 || value > COLO_FORECAST_RUN_TILE_STATIC_THREAT) { + fprintf(stderr, "colosseum: forecast_run_tile_mode must be 0..%d, got %d\n", + COLO_FORECAST_RUN_TILE_STATIC_THREAT, value); + abort(); + } + ctx->config.forecast_run_tile_mode = value; + } + else if (strcmp(key, "loadout_profile_mode") == 0) { + if (value < 0 || value > COLO_LOADOUT_PROFILE_MODE_MIXED) { + fprintf(stderr, "colosseum: loadout_profile_mode must be 0..2, got %d\n", value); + abort(); + } + ctx->config.loadout_profile_mode = value; + } + else encounter_abort_unknown_config("colosseum", "int", key); +} + +static void col_put_float_ctx( + EncounterState* state, + EncounterContext* context, + const char* key, + float value +) { + (void)state; + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + if (strcmp(key, "damage_reward_coeff") == 0) ctx->config.damage_reward_coeff = value; + else if (strcmp(key, "late_start_supply_fraction_per_wave") == 0) { + if (value < 0.0f || value > 0.125f) { + fprintf(stderr, + "colosseum: late_start_supply_fraction_per_wave must be 0..0.125, got %f\n", + (double)value); + abort(); + } + ctx->config.late_start_supply_fraction_per_wave = value; + } + else if (strcmp(key, "boss_damage_reward_coeff") == 0) + ctx->config.boss_damage_reward_coeff = value; + else if (strcmp(key, "wave_clear_bonus") == 0) ctx->config.wave_clear_bonus = value; + else if (strcmp(key, "boss_phase_bonus") == 0) ctx->config.boss_phase_bonus = value; + else if (strcmp(key, "win_bonus") == 0) ctx->config.win_bonus = value; + else if (strcmp(key, "prayer_correct_reward") == 0) + ctx->config.prayer_correct_reward = value; + else if (strcmp(key, "offpray_damage_penalty_coeff") == 0) + ctx->config.offpray_damage_penalty_coeff = value; + else if (strcmp(key, "multistyle_exposure_penalty_coeff") == 0) + ctx->config.multistyle_exposure_penalty_coeff = value; + else if (strcmp(key, "argmax_gear_reward_coeff") == 0) + ctx->config.argmax_gear_reward_coeff = value; + else if (strcmp(key, "offensive_boost_reward_coeff") == 0) + ctx->config.offensive_boost_reward_coeff = value; + else if (strcmp(key, "stall_penalty_coeff") == 0) + ctx->config.stall_penalty_coeff = value; + else if (strcmp(key, "avoided_damage_coeff") == 0) + ctx->config.avoided_damage_coeff = value; + else if (strcmp(key, "death_penalty_coeff") == 0) ctx->config.death_penalty_coeff = value; + else if (strcmp(key, "timeout_penalty") == 0) ctx->config.timeout_penalty = value; + else if (strcmp(key, "beginner_loadout_fraction") == 0) { + if (!(value >= 0.0f && value <= 1.0f)) { + fprintf(stderr, "colosseum: beginner_loadout_fraction must be in [0,1], got %.6f\n", + value); + abort(); + } + ctx->config.beginner_loadout_fraction = value; + } + else if (strcmp(key, "prayer_switch_fail_prob") == 0) { + if (!(value >= 0.0f && value <= 1.0f)) { + fprintf(stderr, "colosseum: prayer_switch_fail_prob must be in [0,1], got %.6f\n", + value); + abort(); + } + ctx->config.prayer_switch_fail_prob = value; + } + else if (strcmp(key, "player_damage_received_scale") == 0) { + if (!(value >= 0.0f && value <= 1.0f)) { + fprintf(stderr, + "colosseum: player_damage_received_scale must be in [0,1], got %.6f\n", + value); + abort(); + } + ctx->config.player_damage_received_scale = value; + } + else if (strcmp(key, "damage_scale_anneal_start") == 0) { + if (!(value >= 0.0f && value <= 1.0f)) { + fprintf(stderr, + "colosseum: damage_scale_anneal_start must be in [0,1], got %.6f\n", + value); + abort(); + } + ctx->config.damage_scale_anneal_start = value; + } + else encounter_abort_unknown_config("colosseum", "float", key); +} + +static void col_put_ptr_ctx( + EncounterState* state, + EncounterContext* context, + const char* key, + void* value +) { + (void)state; + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + if (strcmp(key, "collision_map") == 0) ctx->collision_map = (const CollisionMap*)value; + else encounter_abort_unknown_config("colosseum", "ptr", key); +} + +static int col_get_tick_ctx(EncounterState* state, EncounterContext* context) { + (void)context; + return ((ColosseumState*)state)->tick; +} + +static int col_get_winner_ctx(EncounterState* state, EncounterContext* context) { + (void)context; + return ((ColosseumState*)state)->winner; +} + +static void* col_get_log_ctx(EncounterState* state, EncounterContext* context) { + ColosseumState* s = (ColosseumState*)state; + (void)context; + s->log.episode_return = s->episode_return; + s->log.episode_length = s->tick; + s->log.wave_reached = s->wave; + s->log.win = (s->episode_over && s->winner == COLO_OUTCOME_PLAYER_WON) ? 1 : 0; + s->log.died = (s->episode_over && s->winner == COLO_OUTCOME_PLAYER_DIED) ? 1 : 0; + s->log.boss_phase_reached = s->sol.phase; + s->log.active_tier_sum = col_modifier_active_tier_sum(s); + return &s->log; +} + +static const char* col_attack_style_debug_name(AttackStyle style) { + switch (style) { + case ATTACK_STYLE_NONE: return "none"; + case ATTACK_STYLE_MELEE: return "melee"; + case ATTACK_STYLE_RANGED: return "ranged"; + case ATTACK_STYLE_MAGIC: return "magic"; + } + abort(); +} + +static const OsrsCombatVisualRow* col_require_npc_combat_visual_projectile_row( + ColoNpcType type, + AttackStyle style +) { + assert(type >= 0 && type < COLO_NUM_NPC_TYPES); + int npc_def_id = COLO_NPC_DEF_IDS[type]; + const OsrsCombatVisualRow* row = osrs_combat_visual_find_npc_id( + (uint16_t)npc_def_id, style); + if (!row || (row->projectile.travel_spotanim_id <= 0 && + row->projectile.projectile_model_id <= 0)) { + fprintf(stderr, + "colosseum: missing real combat visual projectile for npc def id %d style %s\n", + npc_def_id, col_attack_style_debug_name(style)); + abort(); + } + return row; +} + +enum { + COLO_JAVELIN_SKYFALL_LAUNCH_TRAVEL_GFX_ID = 2674, + COLO_JAVELIN_SKYFALL_DROP_TRAVEL_GFX_ID = 2675, + COLO_JAVELIN_SKYFALL_IMPACT_GFX_ID = 2676, + COLO_JAVELIN_SKYFALL_SKY_HEIGHT = 1536, + COLO_JAVELIN_SKYFALL_GROUND_HEIGHT = 16, + COLO_JAVELIN_SKYFALL_LAUNCH_CURVE = -48, + COLO_JAVELIN_SKYFALL_DROP_CURVE = 48, + + COLO_JAVELIN_SKYFALL_DROP_GAME_TICKS = COLO_JAVELIN_SKYFALL_DELAY / 2, + COLO_JAVELIN_SKYFALL_APEX_GAME_TICKS = 1, + COLO_JAVELIN_PROJECTILE_RELEASE_DELAY_TICKS = 1, + + COLO_MAGIC_CAST_RELEASE_DELAY_TICKS = 1, +}; + +static float col_javelin_skyfall_shadow_scale(const ColoJavelinState* jv) { + if (!jv->skyfall_pending || jv->skyfall_timer <= 0 || + jv->skyfall_timer > COLO_JAVELIN_SKYFALL_DELAY) { + fprintf(stderr, "colosseum: invalid skyfall shadow timer %d pending %d\n", + jv->skyfall_timer, jv->skyfall_pending); + abort(); + } + float progress = (float)(COLO_JAVELIN_SKYFALL_DELAY - jv->skyfall_timer) / + (float)(COLO_JAVELIN_SKYFALL_DELAY - 1); + return 0.35f + 0.65f * progress; +} + +static void col_emit_javelin_skyfall_shadow(EncounterOverlay* ov, const ColoJavelinState* jv) { + encounter_emit_tile_shadow( + ov, + jv->skyfall_tile_x, + jv->skyfall_tile_y, + col_javelin_skyfall_shadow_scale(jv)); +} + +typedef struct { + int release_delay_ticks; + int set_delay; +} ColoProjectileReleaseProfile; + +static ColoProjectileReleaseProfile col_npc_projectile_release_profile( + ColoNpcType type, AttackStyle style +) { + switch (type) { + case COLO_JAVELIN_COLOSSUS: + if (style == ATTACK_STYLE_RANGED) + return (ColoProjectileReleaseProfile){ + .release_delay_ticks = COLO_JAVELIN_PROJECTILE_RELEASE_DELAY_TICKS }; + break; + case COLO_SERPENT_SHAMAN: + case COLO_SHOCKWAVE_COLOSSUS: + if (style == ATTACK_STYLE_MAGIC) + return (ColoProjectileReleaseProfile){ + .release_delay_ticks = COLO_MAGIC_CAST_RELEASE_DELAY_TICKS }; + break; + case COLO_MANTICORE: + + return (ColoProjectileReleaseProfile){ .set_delay = 1 }; + default: + break; + } + return (ColoProjectileReleaseProfile){ 0 }; +} + +static EncounterProjectileTiming col_npc_projectile_timing( + ColoNpcType type, + AttackStyle style, + int distance +) { + EncounterProjectileDelayKind kind = encounter_projectile_delay_kind_for_style(style); + ColoProjectileReleaseProfile profile = col_npc_projectile_release_profile(type, style); + + EncounterProjectileDelayOptions options = {0}; + options.set_delay = profile.set_delay; + int damage_delay = encounter_projectile_hit_delay(distance, 0, kind, options); + + int release = profile.release_delay_ticks; + int max_release = damage_delay - 1; + if (release > max_release) release = max_release; + if (release < 0) release = 0; + options.start_delay = release; + + EncounterProjectileTiming timing = + encounter_projectile_timing(distance, 0, kind, options); + + if (timing.visual_duration_ticks < 1 || + timing.visual_start_delay_ticks + timing.visual_duration_ticks != + timing.damage_delay_ticks) { + fprintf(stderr, + "colosseum: projectile timing invariant violated (type %d style %d dist %d: " + "release %d + flight %d != landing %d)\n", + (int)type, (int)style, distance, + timing.visual_start_delay_ticks, timing.visual_duration_ticks, + timing.damage_delay_ticks); + abort(); + } + return timing; +} + +static uint32_t col_manticore_orb_model_id(AttackStyle style) { + switch (style) { + case ATTACK_STYLE_RANGED: return 51221u; + case ATTACK_STYLE_MAGIC: return 51215u; + case ATTACK_STYLE_MELEE: return 51213u; + case ATTACK_STYLE_NONE: break; + } + fprintf(stderr, "colosseum: invalid manticore orb style %d for telegraph model\n", + (int)style); + abort(); +} + +static int col_manticore_orb_anim_id(AttackStyle style) { + switch (style) { + case ATTACK_STYLE_RANGED: return 10327; + case ATTACK_STYLE_MAGIC: return 10329; + case ATTACK_STYLE_MELEE: return 10328; + case ATTACK_STYLE_NONE: break; + } + fprintf(stderr, "colosseum: invalid manticore orb style %d for telegraph anim\n", + (int)style); + abort(); +} + +static void col_emit_javelin_skyfall_launch_projectile( + ColosseumState* s, + EncounterOverlay* ov, + int npc_slot +) { + ColoNPC* npc = &s->npcs[npc_slot]; + ColoJavelinState* jv = colo_npc_javelin(npc); + if (!jv->skyfall_pending) { + fprintf(stderr, "colosseum: skyfall launch render event without pending skyfall\n"); + abort(); + } + + int npc_size = col_npc_effective_size(npc); + encounter_require_projectile_slots(ov, 1); + + OsrsProjectileEventSpec projectile_spec = { + .src_x = npc->x, + .src_y = npc->y, + .dst_x = npc->x, + .dst_y = npc->y, + .style = encounter_attack_style_to_proj_style(ATTACK_STYLE_RANGED), + .damage = 0, + .duration_ticks = (COLO_JAVELIN_SKYFALL_DELAY - + COLO_JAVELIN_SKYFALL_DROP_GAME_TICKS - + COLO_JAVELIN_SKYFALL_APEX_GAME_TICKS) * 30, + .start_h = (int)(npc_size * 0.75f * 128), + .end_h = COLO_JAVELIN_SKYFALL_SKY_HEIGHT, + .curve = COLO_JAVELIN_SKYFALL_LAUNCH_CURVE, + .arc_height = 0.0f, + .src_size = npc_size, + .dst_size = npc_size, + .model_id = 0u, + .anim_id = -1, + .travel_gfx_id = COLO_JAVELIN_SKYFALL_LAUNCH_TRAVEL_GFX_ID, + .impact_gfx_id = 0, + .start_delay = 0, + }; + int projectile_idx = osrs_emit_projectile_with_spec( + ov, + &projectile_spec, + 0); + encounter_set_projectile_source_npc_slot(ov, projectile_idx, npc_slot); +} + +static void col_emit_manticore_orb_telegraphs( + EncounterOverlay* ov, + const ColoNPC* npc, + int npc_slot +) { + const ColoManticoreState* mc = &npc->type_state.manticore; + if (mc->cycle_step < 0) return; + if (mc->cycle_step >= 3) { + fprintf(stderr, "colosseum: invalid manticore cycle_step %d\n", mc->cycle_step); + abort(); + } + int npc_size = col_npc_effective_size(npc); + for (int orb = mc->cycle_step; orb < 3; orb++) { + AttackStyle style = mc->orb_style[orb]; + int stack_index = orb - mc->cycle_step; + encounter_emit_floating_model( + ov, + ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT, + npc_slot, + npc->x, + npc->y, + col_manticore_orb_model_id(style), + col_manticore_orb_anim_id(style), + 2.35f + (float)stack_index * 0.62f, + 0.0f, + 0.72f + 0.05f * (float)(npc_size - 1)); + } +} + +static void col_emit_javelin_skyfall_drop_projectile( + EncounterOverlay* ov, + const ColoJavelinState* jv +) { + encounter_require_projectile_slots(ov, 1); + OsrsProjectileEventSpec projectile_spec = { + .src_x = jv->skyfall_tile_x, + .src_y = jv->skyfall_tile_y, + .dst_x = jv->skyfall_tile_x, + .dst_y = jv->skyfall_tile_y, + .style = encounter_attack_style_to_proj_style(ATTACK_STYLE_RANGED), + .damage = 0, + .duration_ticks = COLO_JAVELIN_SKYFALL_DROP_GAME_TICKS * 30, + .start_h = COLO_JAVELIN_SKYFALL_SKY_HEIGHT, + .end_h = COLO_JAVELIN_SKYFALL_GROUND_HEIGHT, + .curve = COLO_JAVELIN_SKYFALL_DROP_CURVE, + .arc_height = 0.0f, + .src_size = 1, + .dst_size = 1, + .model_id = 0u, + .anim_id = -1, + .travel_gfx_id = COLO_JAVELIN_SKYFALL_DROP_TRAVEL_GFX_ID, + .impact_gfx_id = COLO_JAVELIN_SKYFALL_IMPACT_GFX_ID, + .start_delay = 0, + }; + (void)osrs_emit_projectile_with_spec( + ov, + &projectile_spec, + 0); +} + +static void col_emit_npc_attack_projectile( + ColosseumState* s, + ColosseumContext* ctx, + EncounterOverlay* ov, + int npc_slot +) { + ColoNPC* npc = &s->npcs[npc_slot]; + AttackStyle actual_style = npc->attack_style_this_tick; + if (actual_style == ATTACK_STYLE_NONE) + return; + if (actual_style == ATTACK_STYLE_MELEE && npc->type != COLO_MANTICORE) + return; + if (col_type_is_hazard_entity(npc->type)) + return; + if (npc->type == COLO_JAVELIN_COLOSSUS && + ctx->npc_render_attack_anim[npc_slot] == + COLO_NPC_RENDER_ATTACK_JAVELIN_SKYFALL) { + col_emit_javelin_skyfall_launch_projectile(s, ov, npc_slot); + return; + } + + int npc_size = col_npc_effective_size(npc); + int start_h = (int)(npc_size * 0.75f * 128); + int end_h = 64; + int target_tile_x = s->player.x; + int target_tile_y = s->player.y; + int target_x = s->player.x; + int target_y = s->player.y; + int target_size = 1; + int target_npc_slot = -1; + + int dist = encounter_projectile_distance( + npc->x, npc->y, npc_size, target_tile_x, target_tile_y, target_size, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + EncounterProjectileTiming timing = + col_npc_projectile_timing(npc->type, actual_style, dist); + int duration = timing.visual_duration_ticks * 30; + int start_delay = timing.visual_start_delay_ticks * 30; + + const OsrsCombatVisualRow* npc_visual = + col_require_npc_combat_visual_projectile_row(npc->type, actual_style); + const OsrsCombatProjectileProfile* projectile = &npc_visual->projectile; + uint32_t model_id = projectile->projectile_model_id > 0 + ? (uint32_t)projectile->projectile_model_id + : 0u; + int anim_id = projectile->projectile_model_id > 0 + ? projectile->projectile_anim_id + : OSRS_COMBAT_PROJECTILE_MISSING; + int travel_gfx_id = osrs_combat_projectile_value_or( + projectile->travel_spotanim_id, 0); + int launch_gfx_id = osrs_combat_projectile_value_or( + projectile->launch_spotanim_id, 0); + int impact_gfx_id = osrs_combat_projectile_value_or( + projectile->impact_spotanim_id, 0); + int curve = osrs_combat_projectile_value_or(projectile->projectile_angle, 16); + float arc = osrs_combat_projectile_profile_arc_height(projectile, actual_style); + + if (actual_style == ATTACK_STYLE_MAGIC && arc <= 0.0f) + arc = 1.0f; + encounter_require_projectile_slots(ov, 1); + OsrsProjectileEventSpec spec = { + .src_x = npc->x, + .src_y = npc->y, + .dst_x = target_x, + .dst_y = target_y, + .style = encounter_attack_style_to_proj_style(actual_style), + .damage = (int)s->tick_scratch.damage_received, + .duration_ticks = duration, + .start_h = start_h, + .end_h = end_h, + .curve = curve, + .arc_height = arc, + .src_size = npc_size, + .dst_size = target_size, + .model_id = model_id, + .anim_id = anim_id, + .travel_gfx_id = travel_gfx_id, + .launch_gfx_id = launch_gfx_id, + .impact_gfx_id = impact_gfx_id, + .start_delay = start_delay, + }; + + if (target_npc_slot >= 0) { + (void)osrs_emit_projectile_npc_to_npc( + ov, &spec, npc_slot, target_npc_slot); + } else { + (void)osrs_emit_projectile_npc_to_player(ov, &spec, npc_slot); + } +} + +static void col_emit_player_venator_bounce_projectiles( + ColosseumState* s, + ColosseumContext* ctx, + EncounterOverlay* ov, + const OsrsCombatProjectileProfile* profile, + int start_delay, + int duration +) { + if (ctx->player_venator_chain_count <= 1) return; + if (ctx->player_venator_chain_count > OSRS_VENATOR_MAX_CHAIN_HITS) abort(); + if (!profile || profile->projectile_model_id <= 0) { + fprintf(stderr, "colosseum: missing Venator bounce projectile model\n"); + abort(); + } + + int bounce_count = ctx->player_venator_chain_count - 1; + encounter_require_projectile_slots(ov, bounce_count); + for (int hop = 1; hop < ctx->player_venator_chain_count; hop++) { + int source_slot = ctx->player_venator_chain_slots[hop - 1]; + int target_slot = ctx->player_venator_chain_slots[hop]; + if (source_slot < 0 || source_slot >= COLO_MAX_NPCS || + target_slot < 0 || target_slot >= COLO_MAX_NPCS) { + fprintf(stderr, "colosseum: invalid Venator chain slot\n"); + abort(); + } + ColoNPC* source = &s->npcs[source_slot]; + ColoNPC* target = &s->npcs[target_slot]; + if (!source->active || !target->active) { + fprintf(stderr, "colosseum: Venator chain target inactive\n"); + abort(); + } + int source_size = col_npc_effective_size(source); + int target_size = col_npc_effective_size(target); + int source_h = (int)(source_size * 0.5f * 128); + int target_h = (int)(target_size * 0.5f * 128); + OsrsProjectileEventSpec spec = { + .src_x = source->x, + .src_y = source->y, + .dst_x = target->x, + .dst_y = target->y, + .style = encounter_attack_style_to_proj_style(ATTACK_STYLE_RANGED), + .damage = 0, + .duration_ticks = duration, + .start_h = source_h, + .end_h = target_h, + .curve = osrs_combat_projectile_value_or(profile->projectile_angle, 16), + .arc_height = osrs_combat_projectile_profile_arc_height( + profile, ATTACK_STYLE_RANGED), + .src_size = source_size, + .dst_size = target_size, + .model_id = (uint32_t)profile->projectile_model_id, + .anim_id = profile->projectile_anim_id, + .travel_gfx_id = 0, + .launch_gfx_id = 0, + .impact_gfx_id = osrs_combat_projectile_value_or( + profile->impact_spotanim_id, 0), + .start_delay = start_delay + (hop - 1) * duration, + }; + (void)osrs_emit_projectile_npc_to_npc( + ov, &spec, source_slot, target_slot); + } +} + +static void col_emit_player_attack_projectiles( + ColosseumState* s, + ColosseumContext* ctx, + EncounterOverlay* ov +) { + if (!s->tick_scratch.player_attacked || + s->player_attack_style_id == ATTACK_STYLE_MELEE) { + return; + } + + int target_idx = s->player_attack_npc_idx; + if (target_idx < 0 || target_idx >= COLO_MAX_NPCS) + return; + ColoNPC* target = &s->npcs[target_idx]; + if (!target->active) + return; + + uint8_t weapon = s->player.equipped[GEAR_SLOT_WEAPON]; + int target_size = col_npc_effective_size(target); + int p_start_h = 64; + int p_end_h = (int)(target_size * 0.5f * 128); + EncounterProjectileTiming timing = s->player_attack_timing; + if (timing.visual_duration_ticks <= 0) { + int dist = encounter_projectile_distance( + s->player.x, s->player.y, 1, target->x, target->y, target_size, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + timing = col_player_projectile_timing( + s->player_attack_style_id, weapon, dist); + } + int p_duration = timing.visual_duration_ticks * 30; + int p_start_delay = timing.visual_start_delay_ticks * 30; + + if (s->player_attack_style_id == ATTACK_STYLE_MAGIC) { + const OsrsCombatProjectileProfile* profile = + osrs_combat_visual_magic_projectile_profile(weapon); + if (!profile || profile->projectile_model_id <= 0) + return; + OsrsCombatProjectileEmitSpec emit_spec = { + .src_x = s->player.x, + .src_y = s->player.y, + .dst_x = target->x, + .dst_y = target->y, + .src_size = 1, + .dst_size = target_size, + .target_npc_slot = target_idx, + .attack_style = s->player_attack_style_id, + .damage = s->player_attack_dmg, + .duration_ticks = p_duration, + .start_delay = p_start_delay, + .fallback_start_h = p_start_h, + .fallback_end_h = p_end_h, + .curve = 16, + }; + (void)osrs_emit_combat_projectile_profile_player_to_npc( + ov, + profile, + &emit_spec); + return; + } + + const OsrsCombatProjectileProfile* base_profile = + osrs_combat_visual_ranged_projectile_profile( + weapon, OSRS_COMBAT_PROJECTILE_NONE); + const OsrsCombatVisualRow* effect = NULL; + if (s->player.used_special_this_tick && weapon < NUM_ITEMS) { + effect = osrs_combat_visual_find_special_projectile_item_id( + ITEM_DATABASE[weapon].item_id, ATTACK_STYLE_RANGED); + } + OsrsCombatProjectileSequencePart parts[OSRS_COMBAT_PROJECTILE_SEQUENCE_MAX]; + int part_count = osrs_combat_visual_build_projectile_sequence( + base_profile, effect, parts, OSRS_COMBAT_PROJECTILE_SEQUENCE_MAX); + if (part_count < 0) + abort(); + encounter_require_projectile_slots(ov, part_count); + for (int part_i = 0; part_i < part_count; part_i++) { + const OsrsCombatProjectileProfile* profile = &parts[part_i].projectile; + if (profile->projectile_model_id <= 0) + continue; + int visual_damage = part_i == part_count - 1 + ? s->player_attack_dmg + : 0; + OsrsCombatProjectileEmitSpec emit_spec = { + .src_x = s->player.x, + .src_y = s->player.y, + .dst_x = target->x, + .dst_y = target->y, + .src_size = 1, + .dst_size = target_size, + .target_npc_slot = target_idx, + .attack_style = s->player_attack_style_id, + .damage = visual_damage, + .duration_ticks = p_duration, + .start_delay = p_start_delay, + .fallback_start_h = p_start_h, + .fallback_end_h = p_end_h, + .curve = 16, + }; + (void)osrs_emit_combat_projectile_profile_player_to_npc( + ov, + profile, + &emit_spec); + } + col_emit_player_venator_bounce_projectiles( + s, ctx, ov, base_profile, p_start_delay + p_duration, 30); +} + +static void col_emit_thrall_projectile(ColosseumState* s, EncounterOverlay* ov) { + int slot = s->thrall_target_slot; + if (!col_npc_is_thrall_target(s, slot)) + return; + ColoNPC* target = &s->npcs[slot]; + int target_size = col_npc_effective_size(target); + + int src_x, src_y; + col_thrall_render_tile(s, &src_x, &src_y); + + int dist = encounter_projectile_distance( + src_x, src_y, 1, target->x, target->y, target_size, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + EncounterProjectileTiming timing = + col_player_projectile_timing(ATTACK_STYLE_MAGIC, 0, dist); + + encounter_require_projectile_slots(ov, 1); + OsrsProjectileEventSpec spec = { + .src_x = src_x, + .src_y = src_y, + .dst_x = target->x + target_size / 2, + .dst_y = target->y + target_size / 2, + .style = encounter_attack_style_to_proj_style(ATTACK_STYLE_MAGIC), + .damage = 0, + .duration_ticks = timing.visual_duration_ticks * 30, + .start_h = 64, + .end_h = (int)(target_size * 0.5f * 128), + .curve = 16, + .arc_height = 1.0f, + .src_size = 1, + .dst_size = target_size, + .model_id = 0, + .anim_id = -1, + .travel_gfx_id = 1907, + .launch_gfx_id = 0, + .impact_gfx_id = 1908, + .start_delay = timing.visual_start_delay_ticks * 30, + }; + int idx = osrs_emit_projectile_with_spec(ov, &spec, 1); + encounter_set_projectile_target_npc_slot(ov, idx, slot); +} + +enum { + COLO_SOL_CRYSTAL_CHARGE_MODEL_ID = 51245, + COLO_SOL_CRYSTAL_CHARGE_ANIM_ID = 10803, + COLO_SOL_SPHERE_TRAVEL_GFX_ID = 2693, + COLO_SOL_SPHERE_IMPACT_GFX_ID = 2697, + COLO_SOL_SAND_PILLAR_MODEL_ID = 51243, + COLO_SOL_SAND_PILLAR_ANIM_ID = 10812, +}; + +static void col_emit_sol_hazard_vfx(ColosseumState* s, EncounterOverlay* ov) { + if (s->wave != COLO_WAVE_BOSS || !s->sol.started) return; + const SolHereditState* sol = &s->sol; + + for (int c = 0; c < sol->crystal_count; c++) { + const ColoSolCrystal* cr = &sol->crystals[c]; + if (!cr->active) continue; + + if (cr->firing_freeze > COLO_SOL_LASER_BEAM_SHOW_MAX) { + encounter_emit_floating_model( + ov, ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT, + COLO_NPC_SLOT_BEAM_CRYSTAL + c, cr->x, cr->y, + COLO_SOL_CRYSTAL_CHARGE_MODEL_ID, COLO_SOL_CRYSTAL_CHARGE_ANIM_ID, + 1.1f, 0.0f, 1.0f); + } + + if (cr->firing_freeze == COLO_SOL_LASER_BEAM_SHOW_MIN) { + int dst_x = cr->x, dst_y = cr->y; + switch (cr->edge) { + case COLO_SOL_EDGE_NORTH: dst_y = sol->boss_arena_min_y + 1; break; + case COLO_SOL_EDGE_SOUTH: dst_y = sol->boss_arena_max_y - 1; break; + case COLO_SOL_EDGE_EAST: dst_x = sol->boss_arena_min_x + 1; break; + case COLO_SOL_EDGE_WEST: dst_x = sol->boss_arena_max_x - 1; break; + default: break; + } + encounter_require_projectile_slots(ov, 1); + OsrsProjectileEventSpec sphere_spec = { + .src_x = cr->x, + .src_y = cr->y, + .dst_x = dst_x, + .dst_y = dst_y, + .style = encounter_attack_style_to_proj_style(ATTACK_STYLE_MAGIC), + .damage = 0, + .duration_ticks = COLO_SOL_LASER_BEAM_SHOW_MIN * 30, + .start_h = 20, + .end_h = 20, + .curve = 0, + .arc_height = 0.0f, + .src_size = 1, + .dst_size = 1, + .model_id = 0u, + .anim_id = -1, + .travel_gfx_id = COLO_SOL_SPHERE_TRAVEL_GFX_ID, + .impact_gfx_id = COLO_SOL_SPHERE_IMPACT_GFX_ID, + .start_delay = 0, + }; + (void)osrs_emit_projectile_with_spec(ov, &sphere_spec, 0); + } + } + + for (int b = 0; b < COLO_SOL_BEAM_MAX; b++) { + if (!sol->beams[b].active) continue; + encounter_emit_floating_model( + ov, ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT, + COLO_NPC_SLOT_BEAM_CRYSTAL + COLO_SOL_MAX_CRYSTALS + b, + sol->beams[b].x, sol->beams[b].y, + COLO_SOL_SAND_PILLAR_MODEL_ID, COLO_SOL_SAND_PILLAR_ANIM_ID, + 0.05f, 0.0f, 1.0f); + } +} + +static void col_render_post_tick_ctx( + EncounterState* state, + EncounterContext* context, + EncounterOverlay* ov +) { + ColosseumState* s = (ColosseumState*)state; + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ov->tile_shadow_count = 0; + ov->floating_model_count = 0; + ov->active_modifier_count = 0; + ov->projectile_count = 0; + encounter_write_terminal_status_text( + s->episode_over, + s->winner, + COLO_OUTCOME_PLAYER_WON, + "Colosseum cleared", + colo_npc_type_name(s->last_hit_by_type), + ov->status_text, + sizeof(ov->status_text)); + ov->status_text_active = ov->status_text[0] != '\0'; + + for (int mod = 0; mod < COLO_NUM_REAL_MODIFIERS; mod++) { + int tier = col_mod_tier(s, (ColoModifier)mod); + if (tier <= 0) continue; + if (tier > COLO_MODIFIER_MAX_TIER[mod] || tier > 3) { + fprintf(stderr, "colosseum: active modifier %d has invalid tier %d\n", + mod, tier); + abort(); + } + encounter_emit_active_modifier(ov, mod, tier); + } + + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s->npcs[i]; + if (!npc->active || npc->type != COLO_JAVELIN_COLOSSUS) + continue; + ColoJavelinState* jv = colo_npc_javelin(npc); + if (!jv->skyfall_pending) + continue; + col_emit_javelin_skyfall_shadow(ov, jv); + if (jv->skyfall_timer == COLO_JAVELIN_SKYFALL_DROP_GAME_TICKS) + col_emit_javelin_skyfall_drop_projectile(ov, jv); + } + + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s->npcs[i]; + if (!npc->active || npc->type != COLO_MANTICORE) + continue; + col_emit_manticore_orb_telegraphs(ov, npc, i); + } + + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s->npcs[i]; + if (!npc->active || !npc->attacked_this_tick) + continue; + col_emit_npc_attack_projectile(s, ctx, ov, i); + } + col_emit_sol_hazard_vfx(s, ov); + col_emit_player_attack_projectiles(s, ctx, ov); + if (ctx->thrall_attacked_this_tick) + col_emit_thrall_projectile(s, ov); +} + +static void* col_get_player_for_input(void* state, int idx) { + ColosseumState* s = (ColosseumState*)state; + return (idx == 0) ? (void*)&s->player : NULL; +} + +static int col_find_target_obs_slot(const ColosseumState* s, int npc_slot) { + return encounter_find_observed_target_slot( + s->current_obs_slots, COLO_OBS_NPCS, npc_slot); +} + +static int col_is_human_targetable_npc_slot_ctx( + EncounterState* state, + EncounterContext* context, + int npc_slot +) { + ColosseumState* s = (ColosseumState*)state; + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + col_refresh_current_obs_slots_ctx(s, ctx); + int obs_slot = col_find_target_obs_slot(s, npc_slot); + if (obs_slot < 0) return 0; + return col_obs_slot_is_targetable(s, obs_slot); +} + +static int col_queue_inventory_click_action( + const ColosseumState* s, + int* actions, + int inventory_slot +) { + if (inventory_slot < 0 || inventory_slot >= COLO_INVENTORY_DISPLAY_SLOTS) return -1; + OsrsInventoryClickResolution r = + osrs_inventory_cell_click_interpret(&s->inventory_cells[inventory_slot], OSRS_CLICK_TICK_FIRST); + int action_head; + if (r.click_action == OSRS_CLICK_EQUIP) { + int gear_slot = osrs_item_gear_slot(s->inventory_cells[inventory_slot].item_idx); + if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) return -1; + action_head = COLO_HEAD_EQUIP_SLOT(gear_slot); + } else if (r.click_action == OSRS_CLICK_EAT) { + action_head = COLO_HEAD_EAT; + } else if (r.click_action == OSRS_CLICK_DRINK) { + action_head = COLO_HEAD_DRINK; + } else { + return -1; + } + actions[action_head] = inventory_slot + 1; + return action_head; +} + +static void col_apply_pending_draft_grapple_heads(HumanInput* hi, int* actions) { + if (hi->pending_modifier_select > 0 && + hi->pending_modifier_select <= COLO_MODIFIER_DRAFT_OPTIONS) + actions[COLO_HEAD_MODIFIER_SELECT] = hi->pending_modifier_select; + if (hi->pending_grapple_slot > 0 && hi->pending_grapple_slot <= COLO_NUM_GRAPPLE_SLOTS) + actions[COLO_HEAD_GRAPPLE_PARRY] = hi->pending_grapple_slot; +} + +static void col_translate_human_input_ctx( + HumanInput* hi, + int* actions, + EncounterState* state, + EncounterContext* context +) { + for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) actions[h] = 0; + + encounter_translate_movement(hi, actions, COLO_HEAD_PRIMARY, col_get_player_for_input, state); + encounter_translate_prayer(hi, actions, COLO_HEAD_PRAYER); + + ColosseumState* s = (ColosseumState*)state; + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + col_refresh_current_obs_slots_ctx(s, ctx); + + if (hi->pending_target_idx >= 0) { + int found_slot = col_find_target_obs_slot(s, hi->pending_target_idx); + actions[COLO_HEAD_PRIMARY] = + (found_slot >= 0 && col_obs_slot_is_targetable(s, found_slot)) + ? col_primary_attack_action_for_obs_slot(found_slot) : 0; + } else { + actions[COLO_HEAD_PRIMARY] = 0; + } + + if (hi->pending_spec) actions[COLO_HEAD_SPEC] = 1; + encounter_translate_offensive_prayer(hi, actions, COLO_HEAD_OFFENSIVE); + + col_apply_pending_draft_grapple_heads(hi, actions); +} + +static void col_translate_human_commands_ctx( + HumanInput* hi, + int* actions, + ColosseumState* s, + ColosseumContext* ctx +) { + for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) actions[h] = 0; + col_refresh_current_obs_slots_ctx(s, ctx); + + for (int i = 0; i < hi->commands.count; i++) { + const HumanCommand* cmd = &hi->commands.items[i]; + switch (cmd->kind) { + case HUMAN_COMMAND_WALK: + s->player_dest_x = cmd->world_x; + s->player_dest_y = cmd->world_y; + actions[COLO_HEAD_PRIMARY] = 0; + break; + case HUMAN_COMMAND_ATTACK_NPC: { + int found_slot = col_find_target_obs_slot(s, cmd->npc_slot); + actions[COLO_HEAD_PRIMARY] = + (found_slot >= 0 && col_obs_slot_is_targetable(s, found_slot)) + ? col_primary_attack_action_for_obs_slot(found_slot) : 0; + s->player_dest_x = -1; + s->player_dest_y = -1; + break; + } + case HUMAN_COMMAND_OVERHEAD_PRAYER: + actions[COLO_HEAD_PRAYER] = cmd->overhead_prayer; + break; + case HUMAN_COMMAND_EAT: + case HUMAN_COMMAND_DRINK: + case HUMAN_COMMAND_EQUIP_INVENTORY_ITEM: + col_queue_inventory_click_action(s, actions, cmd->inventory_slot); + break; + case HUMAN_COMMAND_SPEC_TOGGLE: + actions[COLO_HEAD_SPEC] = 1; + break; + case HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK: { + int action_head = + col_queue_inventory_click_action(s, actions, cmd->inventory_slot); + if (ctx->config.action_debug_log) { + fprintf(stderr, + "colo_action tick=%d human_inventory_click slot=%d head=%d action=%d\n", + s->tick, cmd->inventory_slot, action_head, + action_head >= 0 ? actions[action_head] : 0); + } + break; + } + case HUMAN_COMMAND_ITEM_ON_ITEM: + osrs_inventory_swap_cells( + s->inventory_cells, cmd->inventory_slot, cmd->target_inventory_slot); + break; + case HUMAN_COMMAND_OFFENSIVE_PRAYER: + actions[COLO_HEAD_OFFENSIVE] = cmd->offensive_prayer; + break; + case HUMAN_COMMAND_SPELL_TARGET: + case HUMAN_COMMAND_FIGHT_STYLE: + case HUMAN_COMMAND_SET_AUTOCAST: + case HUMAN_COMMAND_ITEM_ON_WIDGET: + case HUMAN_COMMAND_SPELL_ON_WIDGET: + case HUMAN_COMMAND_NONE: + break; + } + } + + col_apply_pending_draft_grapple_heads(hi, actions); +} + +static void col_step_human_commands_ctx( + EncounterState* state, + EncounterContext* context, + HumanInput* hi +) { + ColosseumState* s = (ColosseumState*)state; + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + int actions[COLO_NUM_ACTION_HEADS]; + ctx->human_command_mode = 1; + ctx->human_commands = hi->commands.items; + ctx->human_command_count = hi->commands.count; + col_translate_human_commands_ctx(hi, actions, s, ctx); + col_step_ctx(state, (EncounterContext*)ctx, actions); + ctx->human_commands = NULL; + ctx->human_command_count = 0; + human_input_clear_pending(hi); +} + +#define COLO_SNAPSHOT_MAGIC 0xC010C001u +#define COLO_SNAPSHOT_VERSION 20u + +typedef struct { + uint32_t magic; + uint32_t version; + uint32_t state_size; + uint32_t reserved; + ColosseumState state; +} ColoSnapshot; + +static void col_refresh_after_state_load(ColosseumState* s, ColosseumContext* ctx) { + col_build_npc_stats(); + col_mark_live_loadout_dirty(s); + col_ensure_live_loadout_stats(s); + col_rebuild_player_collision_flags(s); + col_refresh_current_obs_slots_ctx(s, ctx); +} + +static size_t col_snapshot_size_ctx(EncounterState* state, EncounterContext* context) { + (void)state; + (void)context; + return sizeof(ColoSnapshot); +} + +static void col_snapshot_ctx(EncounterState* state, EncounterContext* context, void* out) { + (void)context; + ColoSnapshot* snap = (ColoSnapshot*)out; + encounter_snapshot_write_frame( + snap, sizeof(*snap), COLO_SNAPSHOT_MAGIC, COLO_SNAPSHOT_VERSION, + sizeof(ColosseumState)); + encounter_snapshot_copy_state_to( + snap, offsetof(ColoSnapshot, state), state, sizeof(ColosseumState)); +} + +static void col_restore_ctx( + EncounterState* state, + EncounterContext* context, + const void* data, + size_t n +) { + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + ColosseumState* dst = (ColosseumState*)state; + encounter_snapshot_validate_frame( + "col_restore", data, n, sizeof(ColoSnapshot), + COLO_SNAPSHOT_MAGIC, COLO_SNAPSHOT_VERSION, sizeof(ColosseumState)); + encounter_snapshot_copy_state_from( + dst, data, offsetof(ColoSnapshot, state), sizeof(ColosseumState)); + col_context_clear_render_events(ctx); + col_refresh_after_state_load(dst, ctx); +} + +static int col_lab_apply_script_line_alloc_json_void( + void* state, const char* line, char** out_json +) { + return col_lab_apply_script_line_alloc_json((ColosseumState*)state, line, out_json); +} + +static int col_apply_lab_command_ctx( + EncounterState* state, + EncounterContext* context, + const char* line +) { + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + col_context_clear_render_events(ctx); + return encounter_apply_lab_command_dump_wrapper( + state, line, COLO_LAB_LINE_DUMP, col_lab_apply_script_line_alloc_json_void); +} + +static const EncounterDef ENCOUNTER_COLOSSEUM = { + .name = "colosseum", + .obs_size = COLO_NUM_OBS, + .num_action_heads = COLO_NUM_ACTION_HEADS, + .action_head_dims = COLO_ACTION_DIMS, + .mask_size = COLO_ACTION_MASK_SIZE, + + .state_size = sizeof(ColosseumState), + .context_size = sizeof(ColosseumContext), + .init_context = col_init_context, + .destroy_context = col_destroy_context, + .init_state = col_init_state, + + .create = col_create, + .destroy = col_destroy, + .reset = col_reset_ctx, + .step = col_step_ctx, + .step_human_commands = col_step_human_commands_ctx, + + .snapshot_size = col_snapshot_size_ctx, + .snapshot = col_snapshot_ctx, + .restore = col_restore_ctx, + + .cell_key_size = NULL, + .write_cell_key = NULL, + .progress_score = NULL, + + .write_obs = col_write_obs_ctx, + .write_mask = col_write_mask_ctx, + .get_reward = col_get_reward_ctx, + .is_terminal = col_is_terminal_ctx, + + .get_entity_count = col_get_entity_count_ctx, + .get_entity = col_get_entity_ctx, + .fill_render_entities = col_fill_render_entities_ctx, + + .put_int = col_put_int_ctx, + .put_float = col_put_float_ctx, + .put_ptr = col_put_ptr_ctx, + + .arena_base_x = COLO_ARENA_MIN_X, + .arena_base_y = COLO_ARENA_MIN_Y, + .arena_width = COLO_ARENA_WIDTH, + .arena_height = COLO_ARENA_HEIGHT, + + .translate_human_input = col_translate_human_input_ctx, + .is_human_targetable_npc_slot = col_is_human_targetable_npc_slot_ctx, + .apply_lab_command = col_apply_lab_command_ctx, + .head_move = COLO_HEAD_PRIMARY, + .head_prayer = COLO_HEAD_PRAYER, + .head_target = COLO_HEAD_PRIMARY, + + .render_post_tick = col_render_post_tick_ctx, + .get_log = col_get_log_ctx, + .get_tick = col_get_tick_ctx, + .get_winner = col_get_winner_ctx, +}; + +__attribute__((constructor)) +static void col_register(void) { + encounter_register(&ENCOUNTER_COLOSSEUM); +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc new file mode 100644 index 0000000000..3c2446398d --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reset_spawn.inc @@ -0,0 +1,567 @@ +#line 76 "encounter_colosseum.h" + +static int col_grid_index(int x, int y, int* gx, int* gy) { + *gx = x - COLO_ARENA_MIN_X; + *gy = y - COLO_ARENA_MIN_Y; + return *gx >= 0 && *gx < COLO_ARENA_WIDTH && *gy >= 0 && *gy < COLO_ARENA_HEIGHT; +} + +static int col_npc_effective_size(const ColoNPC* npc) { + return npc->size > 0 ? npc->size : COLO_NPC_STATS[npc->type].size; +} + +static void col_stamp_npc_collision_footprint(ColosseumState* s, int x, int y, int size, uint8_t v) { + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + int gx, gy; + if (col_grid_index(x + dx, y + dy, &gx, &gy)) + s->npc_collision_flags[gx][gy] = v; + } + } +} + +static void col_rebuild_player_collision_flags(ColosseumState* s) { + memset(s->player_collision_flags, 0, sizeof(s->player_collision_flags)); + int gx, gy; + if (col_grid_index(s->player.x, s->player.y, &gx, &gy)) + s->player_collision_flags[gx][gy] = 1; +} + +static int col_find_free_npc(ColosseumState* s) { + for (int i = 0; i < COLO_MAX_NPCS; i++) { + if (!s->npcs[i].active) return i; + } + return -1; +} + +static void col_reset_current_wave_score_progress(ColosseumState* s) { + s->current_wave_total_killable = 0; + s->current_wave_hp_pool = 0; + s->current_wave_fresh_damage = 0.0f; +} + +static void col_record_score_spawn(ColosseumState* s, ColoNpcType type) { + if (s->wave == COLO_WAVE_BOSS) return; + if (col_type_is_hazard_entity(type)) return; + s->current_wave_total_killable++; + s->current_wave_hp_pool += COLO_NPC_STATS[type].hp; +} + +static void col_deactivate_npc(ColosseumState* s, int idx) { + ColoNPC* npc = &s->npcs[idx]; + + if (!col_type_is_hazard_entity(npc->type) && !col_type_is_warbander(npc->type)) { + int size = col_npc_effective_size(npc); + col_stamp_npc_collision_footprint(s, npc->x, npc->y, size, 0); + } + npc->active = 0; +} + +static void col_roll_manticore_orb_cycle(ColosseumState* s, AttackStyle out[3]) { + int mantimayhem = col_mod_tier(s, COLO_MOD_MANTIMAYHEM); + if (mantimayhem >= 3) { + out[0] = ATTACK_STYLE_RANGED; + out[1] = ATTACK_STYLE_MAGIC; + out[2] = ATTACK_STYLE_MELEE; + for (int o = 2; o > 0; o--) { + int j = encounter_rand_int(&s->rng_state, o + 1); + AttackStyle tmp = out[o]; + out[o] = out[j]; + out[j] = tmp; + } + } else if (encounter_rand_int(&s->rng_state, 2) == 0) { + out[0] = ATTACK_STYLE_RANGED; + out[1] = ATTACK_STYLE_MAGIC; + out[2] = ATTACK_STYLE_MELEE; + } else { + out[0] = ATTACK_STYLE_MAGIC; + out[1] = ATTACK_STYLE_RANGED; + out[2] = ATTACK_STYLE_MELEE; + } +} + +static const AttackStyle* col_wave_manticore_orb_cycle(ColosseumState* s) { + if (!s->wave_manticore_pattern_rolled) { + col_roll_manticore_orb_cycle(s, s->wave_manticore_orb_style); + s->wave_manticore_pattern_rolled = 1; + } + return s->wave_manticore_orb_style; +} + +static void col_init_npc(ColosseumState* s, int idx, ColoNpcType type, int x, int y) { + ColoNPC* npc = &s->npcs[idx]; + memset(npc, 0, sizeof(*npc)); + npc->type = type; + colo_npc_init_type_state(npc); + if (type == COLO_MANTICORE) { + const AttackStyle* cycle = col_wave_manticore_orb_cycle(s); + ColoManticoreState* mc = colo_npc_manticore(npc); + for (int o = 0; o < 3; o++) mc->fixed_orb_style[o] = cycle[o]; + } + const ColoNpcStats* st = &COLO_NPC_STATS[type]; + npc->x = x; + npc->y = y; + npc->hp = st->hp; + npc->max_hp = st->hp; + npc->min_hp_seen = st->hp; + npc->size = st->size; + npc->render_id = ++s->next_npc_render_id; + npc->attack_timer = st->attack_speed; + npc->attack_style = st->default_style; + npc->active = 1; + npc->target_x = x; + npc->target_y = y; + npc->stun_timer = 0; + + if (!col_type_is_hazard_entity(type) && !col_type_is_warbander(type) && + col_npc_effective_size(npc) > 0) + col_stamp_npc_collision_footprint(s, x, y, col_npc_effective_size(npc), 1); +} + +static int col_spawn_npc_at(ColosseumState* s, ColoNpcType type, int x, int y) { + int slot = col_find_free_npc(s); + assert(slot >= 0 && "colosseum NPC slots exhausted"); + col_init_npc(s, slot, type, x, y); + col_record_score_spawn(s, type); + return slot; +} + +static int col_spawn_excluded_near_player(const ColosseumState* s, int x, int y, int size) { + return encounter_rect_distance( + s->player.x, s->player.y, 1, x, y, size) <= COLO_SPAWN_EXCLUSION_CHEB; +} + +static int col_spawn_footprint_free(const ColosseumState* s, int x, int y, int size) { + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + int tx = x + dx, ty = y + dy; + if (col_static_blocked(tx, ty)) return 0; + if (col_sol_clamp_active(s) && !col_in_boss_arena(s, tx, ty)) return 0; + int gx, gy; + if (!col_grid_index(tx, ty, &gx, &gy)) return 0; + if (s->npc_collision_flags[gx][gy]) return 0; + if (tx == s->player.x && ty == s->player.y) return 0; + } + } + return 1; +} + +static int col_spawn_anchor_candidates(const ColosseumState* s, int* out) { + int n = 0; + for (int a = 0; a < COLO_NUM_SPAWN_ANCHORS; a++) { + int ax = COLO_SPAWN_ANCHORS[a][0], ay = COLO_SPAWN_ANCHORS[a][1]; + if (col_spawn_excluded_near_player(s, ax, ay, COLO_SPAWN_ZONE_SIZE)) continue; + if (!col_spawn_footprint_free(s, ax, ay, COLO_SPAWN_ZONE_SIZE)) continue; + out[n++] = a; + } + return n; +} + +static int col_warband_spawn_tile_ok(const ColosseumState* s, int x, int y) { + if (col_static_blocked(x, y)) return 0; + if (col_sol_clamp_active(s) && !col_in_boss_arena(s, x, y)) return 0; + return !(x == s->player.x && y == s->player.y); +} + +static void col_draw_warband_box_tile(ColosseumState* s, int* ox, int* oy) { + int span_x = COLO_WARBAND_BOX_MAX_X - COLO_WARBAND_BOX_MIN_X + 1; + int span_y = COLO_WARBAND_BOX_MAX_Y - COLO_WARBAND_BOX_MIN_Y + 1; + for (int t = 0; t < 32; t++) { + int x = COLO_WARBAND_BOX_MIN_X + encounter_rand_int(&s->rng_state, span_x); + int y = COLO_WARBAND_BOX_MIN_Y + encounter_rand_int(&s->rng_state, span_y); + if (col_warband_spawn_tile_ok(s, x, y)) { *ox = x; *oy = y; return; } + } + for (int y = COLO_WARBAND_BOX_MIN_Y; y <= COLO_WARBAND_BOX_MAX_Y; y++) + for (int x = COLO_WARBAND_BOX_MIN_X; x <= COLO_WARBAND_BOX_MAX_X; x++) + if (col_warband_spawn_tile_ok(s, x, y)) { *ox = x; *oy = y; return; } + assert(0 && "warband centre box full"); +} + +static void col_spawn_warbander_formation( + ColosseumState* s, ColoNpcType type, int ax, int ay, + int dx, int dy, int dir) { + int x = ax + dx, y = ay + dy; + if (!col_warband_spawn_tile_ok(s, x, y)) { + int found = 0; + for (int r = 1; r <= 3 && !found; r++) { + for (int oy = -r; oy <= r && !found; oy++) { + for (int ox = -r; ox <= r && !found; ox++) { + if (ox > -r && ox < r && oy > -r && oy < r) continue; + if (col_warband_spawn_tile_ok(s, x + ox, y + oy)) { + x += ox; y += oy; found = 1; + } + } + } + } + if (!found) col_draw_warband_box_tile(s, &x, &y); + } + int slot = col_spawn_npc_at(s, type, x, y); + colo_npc_warband(&s->npcs[slot])->formation_dir = dir; +} + +static void col_spawn_pair_adjacent(ColosseumState* s, ColoNpcType type, int px, int py) { + int size = COLO_NPC_STATS[type].size; + const int off[8][2] = { + {size, 0}, {-size, 0}, {0, size}, {0, -size}, + {size, size}, {-size, -size}, {size, -size}, {-size, size} }; + for (int o = 0; o < 8; o++) { + int x = px + off[o][0], y = py + off[o][1]; + if (!col_spawn_footprint_free(s, x, y, size)) continue; + col_spawn_npc_at(s, type, x, y); + return; + } + for (int r = 1; r <= size + 2; r++) { + for (int dy = -r; dy <= r; dy++) { + for (int dx = -r; dx <= r; dx++) { + if (dx > -r && dx < r && dy > -r && dy < r) continue; + int x = px + dx, y = py + dy; + if (!col_spawn_footprint_free(s, x, y, size)) continue; + col_spawn_npc_at(s, type, x, y); + return; + } + } + } + int best = -1, best_dist = 1 << 30; + for (int a = 0; a < COLO_NUM_SPAWN_ANCHORS; a++) { + int ax = COLO_SPAWN_ANCHORS[a][0], ay = COLO_SPAWN_ANCHORS[a][1]; + if (col_spawn_excluded_near_player(s, ax, ay, COLO_SPAWN_ZONE_SIZE)) continue; + if (!col_spawn_footprint_free(s, ax, ay, COLO_SPAWN_ZONE_SIZE)) continue; + int dx = ax > px ? ax - px : px - ax; + int dy = ay > py ? ay - py : py - ay; + int cheb = dx > dy ? dx : dy; + if (cheb < best_dist) { best_dist = cheb; best = a; } + } + assert(best >= 0 && "no spawn spot for the Dynamic Duo pair"); + col_spawn_npc_at(s, type, COLO_SPAWN_ANCHORS[best][0], COLO_SPAWN_ANCHORS[best][1]); +} + +static void col_spawn_quartet_boss_wave(ColosseumState* s, ColoNpcType type) { + int span = COLO_BOSS_ARENA_SIZE; + int qx = -1, qy = -1; + for (int t = 0; t < 64 && qx < 0; t++) { + int x = COLO_BOSS_ARENA_MIN_X + encounter_rand_int(&s->rng_state, span); + int y = COLO_BOSS_ARENA_MIN_Y + encounter_rand_int(&s->rng_state, span); + if (col_spawn_excluded_near_player(s, x, y, 1)) continue; + if (!col_spawn_footprint_free(s, x, y, 1)) continue; + qx = x; qy = y; + } + for (int y = COLO_BOSS_ARENA_MIN_Y; y <= COLO_BOSS_ARENA_MAX_Y && qx < 0; y++) { + for (int x = COLO_BOSS_ARENA_MIN_X; x <= COLO_BOSS_ARENA_MAX_X && qx < 0; x++) { + if (col_spawn_excluded_near_player(s, x, y, 1)) continue; + if (!col_spawn_footprint_free(s, x, y, 1)) continue; + qx = x; qy = y; + } + } + assert(qx >= 0 && "no spawn tile for the wave-12 Quartet warbander"); + int slot = col_spawn_npc_at(s, type, qx, qy); + colo_npc_warband(&s->npcs[slot])->formation_dir = COLO_WARBAND_FORM_SOUTH; +} + +static const ColoNpcType COLO_WARBAND_TYPES[3] = { + COLO_FREMENNIK_BERSERKER, COLO_FREMENNIK_ARCHER, COLO_FREMENNIK_SEER }; + +static void col_spawn_wave(ColosseumState* s) { + col_reset_current_wave_score_progress(s); + + for (int i = 0; i < COLO_MAX_NPCS; i++) { + if (!s->npcs[i].active) continue; + if (s->npcs[i].type == COLO_BEE_SWARM) continue; + col_deactivate_npc(s, i); + } + memset(s->totems, 0, sizeof(s->totems)); + memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); + + s->warband_cycle_anchor = -1; + s->wave_manticore_pattern_rolled = 0; + + if (s->wave < 0 || s->wave >= COLO_NUM_WAVES) return; + const ColoWaveDef* def = &COLO_WAVES[s->wave]; + + if (s->wave == COLO_WAVE_BOSS) { + col_sol_begin_boss_arena(s); + for (int i = 0; i < def->count; i++) { + assert((ColoNpcType)def->types[i] == COLO_SOL_HEREDIT); + col_spawn_npc_at(s, COLO_SOL_HEREDIT, COLO_SOL_SPAWN_X, COLO_SOL_SPAWN_Y); + } + if (col_mod_active(s, COLO_MOD_QUARTET)) { + ColoNpcType extra = COLO_WARBAND_TYPES[encounter_rand_int(&s->rng_state, 3)]; + col_spawn_quartet_boss_wave(s, extra); + } + } else { + int archer_x = -1, archer_y = -1; + for (int i = 0; i < def->count; i++) { + if ((ColoNpcType)def->types[i] != COLO_FREMENNIK_ARCHER) continue; + col_draw_warband_box_tile(s, &archer_x, &archer_y); + int slot = col_spawn_npc_at(s, COLO_FREMENNIK_ARCHER, archer_x, archer_y); + colo_npc_warband(&s->npcs[slot])->formation_dir = COLO_WARBAND_FORM_WEST; + } + for (int i = 0; i < def->count; i++) { + ColoNpcType type = (ColoNpcType)def->types[i]; + if (!col_type_is_warbander(type) || type == COLO_FREMENNIK_ARCHER) continue; + assert(archer_x >= 0 && "warband def lists berserker/seer without an archer"); + if (type == COLO_FREMENNIK_SEER) + col_spawn_warbander_formation(s, type, archer_x, archer_y, + 2, 0, COLO_WARBAND_FORM_EAST); + else + col_spawn_warbander_formation(s, type, archer_x, archer_y, + 1, 1, COLO_WARBAND_FORM_NORTH); + } + + int candidates[COLO_NUM_SPAWN_ANCHORS]; + int ncand = col_spawn_anchor_candidates(s, candidates); + encounter_shuffle(candidates, ncand, &s->rng_state); + int cursor = 0; + for (int i = 0; i < def->count; i++) { + ColoNpcType type = (ColoNpcType)def->types[i]; + if (col_type_is_warbander(type)) continue; + int anchor = -1; + while (cursor < ncand) { + int a = candidates[cursor++]; + if (col_spawn_footprint_free( + s, COLO_SPAWN_ANCHORS[a][0], COLO_SPAWN_ANCHORS[a][1], + COLO_SPAWN_ZONE_SIZE)) { anchor = a; break; } + } + assert(anchor >= 0 && "primary spawn ran out of anchor candidates"); + int ax = COLO_SPAWN_ANCHORS[anchor][0], ay = COLO_SPAWN_ANCHORS[anchor][1]; + col_spawn_npc_at(s, type, ax, ay); + if (type == COLO_SHOCKWAVE_COLOSSUS && col_mod_active(s, COLO_MOD_DYNAMIC_DUO)) + col_spawn_pair_adjacent(s, type, ax, ay); + } + + if (col_mod_active(s, COLO_MOD_QUARTET)) { + ColoNpcType extra = COLO_WARBAND_TYPES[encounter_rand_int(&s->rng_state, 3)]; + assert(archer_x >= 0 && "quartet without an archer spawn"); + col_spawn_warbander_formation(s, extra, archer_x, archer_y, + 1, -1, COLO_WARBAND_FORM_SOUTH); + } + } + + s->reinforcement_timer = COLO_REINFORCEMENT_TICKS; + + s->wave_ready_delay = COLO_WAVE_READY_TICKS; + s->wave_attack_delay = COLO_WAVE_ATTACK_GATE_TICKS; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s->npcs[i]; + if (!npc->active) continue; + if (npc->type == COLO_BEE_SWARM || npc->type == COLO_SOL_HEREDIT) continue; + npc->attack_timer = 0; + } + col_modifiers_on_wave_spawn(s); +} + +#define COLO_REINFORCE_FIRED (-1) + +static void col_spawn_reinforcements(ColosseumState* s) { + if (s->wave < 0 || s->wave >= COLO_NUM_WAVES) return; + const ColoWaveDef* def = &COLO_WAVES[s->wave]; + if (def->reinforce_count <= 0) { + s->reinforcement_timer = COLO_REINFORCE_FIRED; + return; + } + + int north = s->player.y >= COLO_GATE_LINE_NORTH_MIN_Y; + int base_x = COLO_GATE_MIN_X; + for (int i = 0; i < def->reinforce_count; i++) { + ColoNpcType type = (ColoNpcType)def->reinforce[i]; + int size = COLO_NPC_STATS[type].size; + int y = north ? COLO_GATE_NORTH_SPAWN_ROW - (size - 1) : COLO_GATE_SOUTH_SPAWN_ROW; + int x = base_x; + for (int try_x = base_x; try_x + size - 1 <= COLO_GATE_MAX_X; try_x++) { + if (col_spawn_footprint_free(s, try_x, y, size)) { x = try_x; break; } + } + int slot = col_spawn_npc_at(s, type, x, y); + s->npcs[slot].spawned_as_reinforcement = 1; + base_x = x + size; + } + + s->reinforcement_timer = COLO_REINFORCE_FIRED; +} + +static void col_init_empty_inventory_cells(ColosseumState* s) { + for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { + s->inventory_cells[i] = osrs_inventory_cell_empty(); + } +} + +static void col_seed_inventory_cells_from_gameplay_loadout(ColosseumState* s) { + col_init_empty_inventory_cells(s); + const int* kit = COLO_GAMEPLAY_INVENTORY_LOADOUT[s->active_loadout_profile]; + for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { + uint16_t raw_id = (uint16_t)kit[i]; + if (raw_id == 0) continue; + + s->inventory_cells[i] = osrs_inventory_cell_from_raw_osrs_id(raw_id); + if (s->inventory_cells[i].item_idx == ITEM_NONE && + s->inventory_cells[i].dose == 0 && + raw_id != 27281) { + fprintf(stderr, "colosseum: gameplay inventory raw id %u has no cell mapping\n", + raw_id); + abort(); + } + } +} + +static void col_sync_consumable_counters_from_inventory(ColosseumState* s) { + int brew_doses = 0; + int restore_doses = 0; + int combat_doses = 0; + int ranged_doses = 0; + int surge_doses = 0; + int antivenom_doses = 0; + for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { + const ColoInvCell* cell = &s->inventory_cells[i]; + if (cell->dose == 0) continue; + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); + if (resolution.click_action != OSRS_CLICK_DRINK) continue; + switch (resolution.consumable_kind) { + case OSRS_CONSUMABLE_BREW: + brew_doses += cell->dose; + break; + case OSRS_CONSUMABLE_SUPER_RESTORE: + case OSRS_CONSUMABLE_SANFEW: + restore_doses += cell->dose; + break; + case OSRS_CONSUMABLE_SUPER_COMBAT: + case OSRS_CONSUMABLE_DIVINE_COMBAT: + combat_doses += cell->dose; + break; + case OSRS_CONSUMABLE_RANGING: + case OSRS_CONSUMABLE_DIVINE_RANGING: + ranged_doses += cell->dose; + break; + case OSRS_CONSUMABLE_SURGE: + surge_doses += cell->dose; + break; + case OSRS_CONSUMABLE_ANTIVENOM_PLUS: + antivenom_doses += cell->dose; + break; + case OSRS_CONSUMABLE_GUTHIX_REST: + case OSRS_CONSUMABLE_SATURATED_HEART: + case OSRS_CONSUMABLE_NONE: + case OSRS_CONSUMABLE_SHARK_FOOD: + case OSRS_CONSUMABLE_KARAMBWAN: + break; + } + } + s->player.brew_doses = brew_doses; + s->player.restore_doses = restore_doses; + s->player.combat_potion_doses = combat_doses; + s->player.ranged_potion_doses = ranged_doses; + s->player.antivenom_doses = antivenom_doses; + s->surge_doses = surge_doses; +} + +static void col_strip_brews_from_inventory(ColosseumState* s) { + for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { + ColoInvCell* cell = &s->inventory_cells[i]; + if (cell->dose == 0) continue; + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); + if (resolution.consumable_kind == OSRS_CONSUMABLE_BREW) + *cell = osrs_inventory_cell_empty(); + } +} + +static void col_reset_ctx(EncounterState* state, EncounterContext* context, uint32_t seed) { + col_build_npc_stats(); + ColosseumState* s = (ColosseumState*)state; + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + int saved_start = ctx->config.start_wave; + uint32_t saved_rng = s->rng_state; + col_context_clear_render_events(ctx); + + ColoObsMemoCaches saved_obs_memos = s->obs_memos; + memset(s, 0, sizeof(ColosseumState)); + s->obs_memos = saved_obs_memos; + s->start_wave = saved_start; + s->rng_state = encounter_resolve_seed(saved_rng, seed); + + s->player_dest_x = -1; + s->player_dest_y = -1; + s->last_hit_by_type = -1; + s->min_sol_hp_seen = COLO_SOL_HP_MAX; + + s->active_player_damage_received_scale = ctx->config.player_damage_received_scale; + for (int i = 0; i < COLO_OBS_NPCS; i++) s->current_obs_slots[i] = -1; + + s->player.entity_type = ENTITY_PLAYER; + encounter_init_maxed_player_combat_stats(&s->player, 99); + osrs_item_effect_state_init(&s->player.item_effect_state); + + s->weapon_set = COLO_GEAR_MELEE; + s->active_loadout_profile = col_sample_loadout_profile( + ctx->config.loadout_profile_mode, + ctx->config.beginner_loadout_fraction, + &s->rng_state); + switch (s->active_loadout_profile) { + case COLO_LOADOUT_PROFILE_SPEEDRUN: + s->episode_max_ticks = COLO_SPEEDRUN_MAX_TICKS; + break; + case COLO_LOADOUT_PROFILE_BEGINNER: + s->episode_max_ticks = COLO_BEGINNER_MAX_TICKS; + break; + default: + fprintf(stderr, "colosseum: invalid active_loadout_profile %d\n", + (int)s->active_loadout_profile); + abort(); + } + if (ctx->config.episode_max_ticks_override > 0) + s->episode_max_ticks = ctx->config.episode_max_ticks_override; + const uint8_t* const* loadouts = col_loadouts_for_profile(s->active_loadout_profile); + encounter_apply_loadout(&s->player, loadouts[COLO_GEAR_MELEE], GEAR_MELEE); + memset(s->player.inventory, ITEM_NONE, sizeof(s->player.inventory)); + memset(s->player.num_items_in_slot, 0, sizeof(s->player.num_items_in_slot)); + col_seed_inventory_cells_from_gameplay_loadout(s); + if (ctx->config.remove_brews) col_strip_brews_from_inventory(s); + + const ColoSupplyLoadout* supplies = &COLO_SUPPLY_LOADOUTS[s->active_loadout_profile]; + s->full_supplies = *supplies; + col_sync_consumable_counters_from_inventory(s); + s->surge_cooldown = 0; + + s->player.prayer = PRAYER_NONE; + s->player.offensive_prayer = OFFENSIVE_PRAYER_NONE; + osrs_interaction_init(&s->interaction); + s->player_last_interaction_target_slot = -1; + s->player_last_interaction_age = 1; + s->player.spec_armed = 0; + s->player.special_energy = 100; + s->player.run_energy = 10000; + + col_mark_live_loadout_dirty(s); + col_ensure_live_loadout_stats(s); + + s->thrall_target_slot = -1; + + s->player.x = COLO_PLAYER_START_X; + s->player.y = COLO_PLAYER_START_Y; + col_rebuild_player_collision_flags(s); + + int effective_start = s->start_wave; + if (effective_start < 0) effective_start = 0; + if (effective_start >= COLO_NUM_WAVES) effective_start = COLO_NUM_WAVES - 1; + s->wave = effective_start; + s->wave_spawn_target = effective_start; + s->wave_spawn_delay = 0; + s->wave_ready_delay = 0; + s->wave_attack_delay = 0; + s->log.wave_reached = effective_start; + + if (effective_start == 0) { + col_modifier_open_draft(s, 0); + if (s->modifiers.draft_pending) s->modifiers.draft_free_movement = 1; + else s->wave_spawn_delay = col_wave_entry_delay_ticks(0); + } else if (ctx->config.late_start_state_mode > 0) { + col_apply_late_start_entry_state(s, ctx, effective_start); + col_modifier_open_draft(s, effective_start); + if (!s->modifiers.draft_pending) + s->wave_spawn_delay = col_wave_entry_delay_ticks(effective_start); + } else { + s->wave_spawn_delay = col_wave_entry_delay_ticks(effective_start); + } +} + +static void col_reset(EncounterState* state, uint32_t seed) { + col_reset_ctx(state, (EncounterContext*)col_legacy_context(), seed); +} diff --git a/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc new file mode 100644 index 0000000000..b3d8a1d39a --- /dev/null +++ b/ocean/osrs/encounters/colosseum/encounter_colosseum_reward_step.inc @@ -0,0 +1,401 @@ +#line 76 "encounter_colosseum.h" + +static const ColoBestGear (*col_get_best_gear_table(ColosseumState* s))[COLO_NUM_NPC_TYPES]; +static float col_expected_dpt_for_equipment_vs_npc( + ColosseumState* s, const uint8_t equipment[NUM_GEAR_SLOTS], + const ColoNPC* npc, int use_style_matched_prayer); + +static float col_terminal_death_reward(const ColosseumContext* ctx) { + return -ctx->config.death_penalty_coeff; +} + +static float col_terminal_timeout_reward(const ColosseumContext* ctx) { + return -ctx->config.timeout_penalty; +} + +static void col_update_min_sol_hp_seen(ColosseumState* s) { + if (!s->sol.started) return; + int idx = col_sol_find_idx(s); + if (idx < 0) return; + int hp = s->npcs[idx].hp; + if (hp < 0) hp = 0; + if (hp < s->min_sol_hp_seen) s->min_sol_hp_seen = hp; +} + +static int col_sol_score_phase_for_min_hp(int hp) { + if (hp > 1350) return 0; + if (hp > 1125) return 1; + if (hp > 750) return 2; + if (hp > 375) return 3; + if (hp > 150) return 4; + return 5; +} + +static float col_sol_score_progress(const ColosseumState* s) { + if (!s->sol.started) return 0.0f; + int hp = s->min_sol_hp_seen; + if (hp < 0) hp = 0; + if (hp > COLO_SOL_HP_MAX) hp = COLO_SOL_HP_MAX; + + static const int phase_hi_hp[COLO_SOL_NUM_PHASES] = { + 1500, 1350, 1125, 750, 375, 150, + }; + static const int phase_lo_hp[COLO_SOL_NUM_PHASES] = { + 1350, 1125, 750, 375, 150, 0, + }; + int phase = col_sol_score_phase_for_min_hp(hp); + float denom = (float)(phase_hi_hp[phase] - phase_lo_hp[phase]); + float within_phase = ((float)phase_hi_hp[phase] - (float)hp) / denom; + if (within_phase < 0.0f) within_phase = 0.0f; + if (within_phase > 1.0f) within_phase = 1.0f; + return ((float)phase + within_phase) / (float)COLO_SOL_NUM_PHASES; +} + +static float col_current_wave_score_progress(const ColosseumState* s) { + if (s->wave == COLO_WAVE_BOSS || s->sol.started) return 0.0f; + if (s->current_wave_hp_pool <= 0) return 0.0f; + float progress = s->current_wave_fresh_damage / (float)s->current_wave_hp_pool; + if (progress < 0.0f) return 0.0f; + + if (progress >= 1.0f) return 0.0f; + return progress; +} + +static float col_episode_outcome_score(const ColosseumState* s) { + if (s->episode_over && s->winner == COLO_OUTCOME_PLAYER_WON) return 1.0f; + int waves_cleared = s->log.waves_cleared; + if (waves_cleared > COLO_WAVE_BOSS) waves_cleared = COLO_WAVE_BOSS; + float depth = (float)waves_cleared + + col_current_wave_score_progress(s) + + col_sol_score_progress(s); + float ratio = depth / (float)COLO_NUM_WAVES; + return 0.99f * ratio * ratio; +} + +static void col_record_terminal_outcome_score(ColosseumState* s) { + s->log.outcome_score = col_episode_outcome_score(s); +} + +static float col_episode_max_wave_depth(const ColosseumState* s) { + if (s->episode_over && s->winner == COLO_OUTCOME_PLAYER_WON) + return (float)COLO_NUM_WAVES; + int waves_cleared = s->log.waves_cleared; + if (waves_cleared > COLO_WAVE_BOSS) waves_cleared = COLO_WAVE_BOSS; + return (float)waves_cleared + + col_current_wave_score_progress(s) + + col_sol_score_progress(s); +} + +static int col_distinct_live_threat_styles(const ColosseumState* s) { + int magic, ranged, melee; + col_live_threat_style_counts(s, &magic, &ranged, &melee); + return (magic > 0) + (ranged > 0) + (melee > 0); +} + +static float col_rewardable_avoided_damage(ColosseumState* s) { + ColoTickScratch* t = &s->tick_scratch; + if (__builtin_popcount((unsigned)t->styles_faced_mask) != 1) return 0.0f; + int style = __builtin_ctz((unsigned)t->styles_faced_mask); + float raw_avoided = t->avoidable_threat_by_style[style] - t->offpray_damage_this_tick; + if (raw_avoided <= 0.0f) return 0.0f; + float progress_budget = s->log.total_damage_dealt - s->avoided_damage_already_rewarded; + if (progress_budget <= 0.0f) return 0.0f; + return raw_avoided < progress_budget ? raw_avoided : progress_budget; +} + +static float col_attacked_gear_quality_ratio(ColosseumState* s) { + if (!s->tick_scratch.player_attacked) return -1.0f; + int npc_slot = s->player_attack_npc_idx; + if (npc_slot < 0 || npc_slot >= COLO_MAX_NPCS) return -1.0f; + const ColoNPC* npc = &s->npcs[npc_slot]; + if (!col_npc_is_live_target(npc) || col_type_is_hazard_entity(npc->type)) + return -1.0f; + const ColoBestGear (*best)[COLO_NUM_NPC_TYPES] = col_get_best_gear_table(s); + float best_dpt = 0.0f; + for (int set = 0; set < COLO_NUM_WEAPON_SETS; set++) { + float d = best[set][npc->type].dpt; + if (d > best_dpt) best_dpt = d; + } + if (best_dpt <= 0.0f) return -1.0f; + + float actual = col_expected_dpt_for_equipment_vs_npc(s, s->player.equipped, npc, 1); + float ratio = actual / best_dpt; + if (ratio < 0.0f) ratio = 0.0f; + if (ratio > 1.0f) ratio = 1.0f; + return ratio; +} + +static int col_attacked_with_offensive_boost(ColosseumState* s) { + if (!s->tick_scratch.player_attacked) return -1; + int npc_slot = s->player_attack_npc_idx; + if (npc_slot < 0 || npc_slot >= COLO_MAX_NPCS) return -1; + const ColoNPC* npc = &s->npcs[npc_slot]; + if (!col_npc_is_live_target(npc) || col_type_is_hazard_entity(npc->type)) + return -1; + AttackStyle style = (AttackStyle)s->player_attack_style_id; + int cur_att = 0, cur_str = 0; + col_current_style_levels(&s->player, style, &cur_att, &cur_str); + int base_att, base_str; + if (style == ATTACK_STYLE_RANGED) { + base_att = base_str = s->player.base_ranged; + } else if (style == ATTACK_STYLE_MAGIC) { + base_att = base_str = s->player.base_magic; + } else { + base_att = s->player.base_attack; + base_str = s->player.base_strength; + } + return (cur_att > base_att || cur_str > base_str) ? 1 : 0; +} + +static float col_compute_reward_ctx(ColosseumState* s, const ColosseumContext* ctx) { + const ColConfig* cfg = &ctx->config; + ColoTickScratch* t = &s->tick_scratch; + + assert(cfg->player_damage_received_scale != 1.0f || + fabsf(t->landed_offpray_damage + t->landed_prayed_damage + + t->landed_unprayable_damage + t->landed_self_damage + - t->damage_received) < 0.5f); + + s->log.total_damage_dealt += t->damage_dealt; + s->log.total_boss_damage += t->damage_boss; + s->log.total_damage_received += t->damage_received; + s->log.total_prayer_correct += t->prayer_correct; + s->log.total_npc_attacks += t->attacks_fired; + col_update_min_sol_hp_seen(s); + if (s->sol.phase > s->log.boss_phase_reached) s->log.boss_phase_reached = s->sol.phase; + + float reward = 0.0f; + float rewarded_fresh = t->fresh_damage_dealt; + if (s->wave < cfg->farm_cap_waves) { + s->log.farm_damage += t->fresh_damage_reinforcement; + if (cfg->farm_safe_damage_cap) + rewarded_fresh -= t->fresh_damage_reinforcement; + } + reward += cfg->damage_reward_coeff * rewarded_fresh; + reward += cfg->boss_damage_reward_coeff * t->fresh_damage_boss; + reward += cfg->prayer_correct_reward * (float)t->prayer_correct; + reward -= cfg->offpray_damage_penalty_coeff * t->offpray_damage_this_tick; + if (cfg->multistyle_exposure_penalty_coeff != 0.0f) { + int excess_styles = col_distinct_live_threat_styles(s) - 1; + if (excess_styles > 0) + reward -= cfg->multistyle_exposure_penalty_coeff * (float)excess_styles; + } + if (cfg->argmax_gear_reward_coeff != 0.0f) { + float gear_quality = col_attacked_gear_quality_ratio(s); + if (gear_quality >= 0.0f) + reward += cfg->argmax_gear_reward_coeff * gear_quality; + } + if (cfg->offensive_boost_reward_coeff != 0.0f) { + if (col_attacked_with_offensive_boost(s) > 0) + reward += cfg->offensive_boost_reward_coeff; + } + float rewardable_avoided = col_rewardable_avoided_damage(s); + reward += cfg->avoided_damage_coeff * rewardable_avoided; + s->avoided_damage_already_rewarded += rewardable_avoided; + + if (s->sol.started && s->sol.phase > s->rewarded_boss_phase) { + reward += cfg->boss_phase_bonus * (float)(s->sol.phase - s->rewarded_boss_phase); + s->rewarded_boss_phase = s->sol.phase; + } + return reward; +} + +static float col_inventory_heal_remaining(const ColosseumState* s) { + float total = 0.0f; + for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) { + const OsrsInventoryCell* cell = &s->inventory_cells[i]; + if (cell->raw_osrs_id == 0) continue; + OsrsInventoryClickResolution r = + osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); + int per_dose = osrs_consumable_hp_heal_amount( + r.consumable_kind, s->player.base_hitpoints); + if (per_dose <= 0) continue; + int doses = r.dose_count > 0 ? (int)r.dose_count : 1; + total += (float)(per_dose * doses); + } + return total; +} + +static void col_record_death_attribution(ColosseumState* s) { + int kt = s->last_hit_by_type; + if (kt >= 0 && kt < COLO_NUM_NPC_TYPES) s->log.death_by_type[kt] += 1.0f; + s->log.death_fatal_damage += s->tick_scratch.damage_received; + if (__builtin_popcount(s->tick_scratch.styles_faced_mask) >= 2) + s->log.death_on_conflict_tick += 1.0f; + s->log.death_dmg_unprayable += s->tick_scratch.landed_unprayable_damage; + s->log.death_dmg_offpray += s->tick_scratch.landed_offpray_damage; + s->log.death_dmg_prayed += s->tick_scratch.landed_prayed_damage; + s->log.death_dmg_self += s->tick_scratch.landed_self_damage; + s->log.death_heal_remaining += col_inventory_heal_remaining(s); +} + +static void col_classify_tick_offpray(ColosseumState* s) { + float d = s->tick_scratch.offpray_damage_this_tick; + if (d <= 0.0f) return; + if (__builtin_popcount(s->tick_scratch.styles_faced_mask) >= 2) + s->log.offpray_damage_conflict += d; + else + s->log.offpray_damage_solo += d; +} + +static void col_step_ctx(EncounterState* state, EncounterContext* context, const int* actions) { + ColosseumState* s = (ColosseumState*)state; + ColosseumContext* ctx = context ? (ColosseumContext*)context : col_legacy_context(); + if (s->episode_over) return; + + s->reward = 0.0f; + s->tick_scratch = (ColoTickScratch){0}; + + s->active_player_damage_received_scale = ctx->config.player_damage_received_scale; + col_context_clear_render_events(ctx); + s->player_attack_timing = (EncounterProjectileTiming){0}; + encounter_clear_tick_flags(&s->player); + for (int i = 0; i < COLO_MAX_NPCS; i++) { + s->npcs[i].attacked_this_tick = 0; + s->npcs[i].attack_style_this_tick = ATTACK_STYLE_NONE; + s->npcs[i].moved_last_tick = s->npcs[i].moved_this_tick; + s->npcs[i].moved_this_tick = 0; + s->npcs[i].hit_landed_this_tick = 0; + s->npcs[i].hit_damage = 0; + s->npcs[i].hit_was_successful_this_tick = 0; + } + s->tick++; + + if (s->tick >= s->episode_max_ticks) { + (void)col_compute_reward_ctx(s, ctx); + s->episode_over = 1; + s->time_limit_truncated = 1; + s->winner = COLO_OUTCOME_PLAYER_DIED; + s->reward = col_terminal_timeout_reward(ctx); + col_record_terminal_outcome_score(s); + s->log.max_wave_depth = col_episode_max_wave_depth(s); + s->episode_return += s->reward; + return; + } + col_player_pretick(s, ctx, actions); + if (ctx->config.prayer_oracle_mode) col_apply_prayer_oracle(s); + s->player.prayer_display = s->player.prayer; + + if (s->wave_ready_delay > 0) s->wave_ready_delay--; + if (s->wave_attack_delay > 0) s->wave_attack_delay--; + + int spawn_wave_now = 0; + if (s->wave_spawn_delay > 0) { + s->wave_spawn_delay--; + if (s->wave_spawn_delay == 0) spawn_wave_now = 1; + } + + if (spawn_wave_now) { + s->wave = s->wave_spawn_target; + col_spawn_wave(s); + } + + int in_wave_gap = (s->wave_spawn_delay > 0 || spawn_wave_now || + s->modifiers.draft_pending); + int in_ready_gap = s->wave_ready_delay > 0; + + col_resolve_player_projectiles_on_npcs_ctx(s, ctx); + col_resolve_player_pending_hits_ctx(s, ctx); + col_tick_npc_death_lingers(s); + + if (!in_wave_gap && !in_ready_gap) { + + if (s->wave_attack_delay == 0 && s->warband_cycle_anchor < 0) + s->warband_cycle_anchor = s->tick - 1; + col_tick_live_stat_drift_and_divines(s); + col_rebuild_player_collision_flags(s); + col_tick_npcs_ctx(s, ctx); + col_modifiers_tick(s); + + if (s->surge_cooldown > 0) s->surge_cooldown--; + } + + col_classify_tick_offpray(s); + + if (ctx->config.invuln_mode) { + s->player.current_hitpoints = s->player.base_hitpoints; + } else if (s->player.current_hitpoints <= 0 || col_mod_doom_lethal(s)) { + (void)col_compute_reward_ctx(s, ctx); + col_record_death_attribution(s); + s->episode_over = 1; + s->winner = COLO_OUTCOME_PLAYER_DIED; + s->reward = col_terminal_death_reward(ctx); + col_record_terminal_outcome_score(s); + s->log.max_wave_depth = col_episode_max_wave_depth(s); + s->episode_return += s->reward; + return; + } + + int can_player_attack = !in_wave_gap; + int px = s->player.x, py = s->player.y; + col_tick_player_ctx(s, ctx, actions, can_player_attack); + s->tick_scratch.player_moved = (s->player.x != px || s->player.y != py) ? 1 : 0; + + if (!in_wave_gap && !in_ready_gap && s->wave_attack_delay == 0) + col_warband_attack_phase(s, ctx); + + s->reward = col_compute_reward_ctx(s, ctx); + + if (can_player_attack && ctx->config.stall_penalty_coeff != 0.0f) + s->reward -= ctx->config.stall_penalty_coeff; + int all_dead = 1; + int wave_cleared = 0; + + if (ctx->config.invuln_mode) { + s->player.current_hitpoints = s->player.base_hitpoints; + } else if (s->player.current_hitpoints <= 0 || col_mod_doom_lethal(s)) { + col_record_death_attribution(s); + s->episode_over = 1; + s->winner = COLO_OUTCOME_PLAYER_DIED; + s->reward = col_terminal_death_reward(ctx); + goto finish_step; + } + + if (spawn_wave_now || s->wave_spawn_delay > 0 || s->modifiers.draft_pending) + goto finish_step; + + for (int i = 0; i < COLO_MAX_NPCS; i++) { + if (col_npc_is_live_enemy(&s->npcs[i])) { + all_dead = 0; + break; + } + } + wave_cleared = all_dead; + if (s->wave == COLO_WAVE_BOSS && s->sol.started) + wave_cleared = col_sol_find_idx(s) < 0; + + if (!all_dead && s->reinforcement_timer > 0) { + s->reinforcement_timer--; + if (s->reinforcement_timer == 0) col_spawn_reinforcements(s); + } + + if (wave_cleared) { + s->tick_scratch.wave_completed = 1; + s->log.waves_cleared++; + s->reward += ctx->config.wave_clear_bonus; + if (s->wave + 1 >= COLO_NUM_WAVES) { + s->episode_over = 1; + s->winner = COLO_OUTCOME_PLAYER_WON; + s->reward += ctx->config.win_bonus; + } else { + + s->wave_spawn_target = s->wave + 1; + col_record_wave_entry(s, s->wave + 1); + col_modifier_open_draft(s, s->wave + 1); + if (!s->modifiers.draft_pending) + s->wave_spawn_delay = col_wave_entry_delay_ticks(s->wave_spawn_target); + s->log.wave_reached = s->wave + 1; + } + } + +finish_step: + if (s->episode_over) { + col_record_terminal_outcome_score(s); + s->log.max_wave_depth = col_episode_max_wave_depth(s); + } + s->episode_return += s->reward; +} + +static void col_step(EncounterState* state, const int* actions) { + col_step_ctx(state, (EncounterContext*)col_legacy_context(), actions); +} diff --git a/ocean/osrs/encounters/encounter_colosseum.h b/ocean/osrs/encounters/encounter_colosseum.h new file mode 100644 index 0000000000..d4d872275a --- /dev/null +++ b/ocean/osrs/encounters/encounter_colosseum.h @@ -0,0 +1,84 @@ +#ifndef ENCOUNTER_COLOSSEUM_H +#define ENCOUNTER_COLOSSEUM_H + +#include "../osrs_types.h" +#include "../osrs_items.h" +#include "../osrs_monsters_generated.h" +#include "../osrs_collision.h" +#include "../osrs_combat.h" +#include "../osrs_combat_visuals.h" +#include "../osrs_special_attacks.h" +#include "../osrs_pvp_gear.h" +#include "../osrs_encounter.h" +#include "../osrs_encounter_player.h" +#include "../osrs_encounter_visual_events.h" +#include "../osrs_player_consumables.h" +#include "../osrs_inventory_clicks.h" +#include "../osrs_interaction.h" +#include "../data/npc_models.h" +#include +#include +#include +#include +#include +#include +#include +#include + +typedef enum { + COLO_FREMENNIK_BERSERKER = 0, + COLO_FREMENNIK_ARCHER, + COLO_FREMENNIK_SEER, + COLO_SERPENT_SHAMAN, + COLO_JAGUAR_WARRIOR, + COLO_JAVELIN_COLOSSUS, + COLO_SHOCKWAVE_COLOSSUS, + COLO_MINOTAUR, + COLO_MANTICORE, + COLO_SOL_HEREDIT, + COLO_HEALING_TOTEM, + COLO_BEE_SWARM, + COLO_NUM_NPC_TYPES +} ColoNpcType; + +typedef enum { + COLO_OUTCOME_PLAYER_WON = 0, + COLO_OUTCOME_PLAYER_DIED = 1, +} ColoOutcome; + +typedef enum { + COLO_MOD_BEES = 0, + COLO_MOD_BLASPHEMY, + COLO_MOD_DOOM, + COLO_MOD_DYNAMIC_DUO, + COLO_MOD_FRAILTY, + COLO_MOD_MANTIMAYHEM, + COLO_MOD_MYOPIA, + COLO_MOD_REENTRY, + COLO_MOD_RED_FLAG, + COLO_MOD_RELENTLESS, + COLO_MOD_SOLARFLARE, + COLO_MOD_QUARTET, + COLO_MOD_TOTEMIC, + COLO_MOD_VOLATILITY, + COLO_NUM_REAL_MODIFIERS +} ColoModifier; + +#include "colosseum/encounter_colosseum_model.inc" +#include "colosseum/encounter_colosseum_helpers.inc" +#include "colosseum/encounter_colosseum_reset_spawn.inc" +#include "colosseum/encounter_colosseum_movement.inc" +#include "colosseum/encounter_colosseum_modifiers.inc" +#include "colosseum/encounter_colosseum_combat.inc" +#include "colosseum/encounter_colosseum_boss.inc" +#include "colosseum/encounter_colosseum_player_actions.inc" +#include "colosseum/encounter_colosseum_reward_step.inc" +#include "colosseum/encounter_colosseum_forecast.inc" +#include "colosseum/encounter_colosseum_obs_mask.inc" +#include "colosseum/encounter_colosseum_mask_render.inc" +#include "colosseum/encounter_colosseum_lab.inc" +#include "colosseum/encounter_colosseum_lab_parse.inc" +#include "colosseum/encounter_colosseum_lab_json.inc" +#include "colosseum/encounter_colosseum_render_snapshot.inc" + +#endif diff --git a/ocean/osrs/encounters/encounter_inferno.h b/ocean/osrs/encounters/encounter_inferno.h new file mode 100644 index 0000000000..385e8f4983 --- /dev/null +++ b/ocean/osrs/encounters/encounter_inferno.h @@ -0,0 +1,38 @@ +#ifndef ENCOUNTER_INFERNO_H +#define ENCOUNTER_INFERNO_H + +#include "../osrs_types.h" +#include "../osrs_items.h" +#include "../osrs_monsters_generated.h" +#include "../osrs_collision.h" +#include "../osrs_combat.h" +#include "../osrs_combat_visuals.h" +#include "../osrs_special_attacks.h" +#include "../osrs_pvp_gear.h" +#include "../osrs_encounter.h" +#include "../osrs_encounter_player.h" +#include "../osrs_encounter_visual_events.h" +#include "../osrs_interaction.h" +#include "../data/npc_models.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include "inferno/encounter_inferno_model.inc" +#include "inferno/encounter_inferno_helpers.inc" +#include "inferno/encounter_inferno_reset_spawn.inc" +#include "inferno/encounter_inferno_movement.inc" +#include "inferno/encounter_inferno_combat.inc" +#include "inferno/encounter_inferno_player_actions.inc" +#include "inferno/encounter_inferno_reward_step.inc" +#include "inferno/encounter_inferno_forecast.inc" +#include "inferno/encounter_inferno_lab.inc" +#include "inferno/encounter_inferno_obs_mask.inc" +#include "inferno/encounter_inferno_render_snapshot.inc" + +#endif diff --git a/ocean/osrs/encounters/encounter_nh_pvp.h b/ocean/osrs/encounters/encounter_nh_pvp.h new file mode 100644 index 0000000000..c4e6edc2ea --- /dev/null +++ b/ocean/osrs/encounters/encounter_nh_pvp.h @@ -0,0 +1,314 @@ +#ifndef ENCOUNTER_NH_PVP_H +#define ENCOUNTER_NH_PVP_H + +#include "../osrs_encounter.h" +#include "../osrs_encounter_visual_events.h" +#include "../osrs_env.h" + +/* order must match the HEAD_* indices in osrs_types.h */ +static const int NH_PVP_ACTION_DIMS[] = { + LOADOUT_DIM, COMBAT_DIM, OVERHEAD_DIM, + FOOD_DIM, POTION_DIM, KARAMBWAN_DIM, VENG_DIM, OFFENSIVE_DIM, MOVE_DIM +}; + +typedef struct { + OsrsEnv env; +} NhPvpState; + +typedef struct { + int unused; +} NhPvpContext; + +static void nh_pvp_translate_human_input(HumanInput* hi, int* actions, Player* agent, Player* target) { + for (int h = 0; h < NUM_ACTION_HEADS; h++) actions[h] = 0; + actions[HEAD_LOADOUT] = LOADOUT_KEEP; + + if (hi->pending_attack) { + if (hi->pending_spell == ATTACK_ICE) actions[HEAD_COMBAT] = ATTACK_ICE; + else if (hi->pending_spell == ATTACK_BLOOD) actions[HEAD_COMBAT] = ATTACK_BLOOD; + else actions[HEAD_COMBAT] = ATTACK_ATK; + } + encounter_translate_prayer(hi, actions, HEAD_OVERHEAD); + encounter_translate_offensive_prayer(hi, actions, HEAD_OFFENSIVE); + + if (hi->pending_food) actions[HEAD_FOOD] = FOOD_EAT; + if (hi->pending_potion > 0) actions[HEAD_POTION] = hi->pending_potion; + if (hi->pending_karambwan) actions[HEAD_KARAMBWAN] = KARAM_EAT; + if (hi->pending_veng) actions[HEAD_VENG] = VENG_CAST; + if (hi->pending_spec) { + AttackStyle style = (AttackStyle)get_item_attack_style(agent->equipped[GEAR_SLOT_WEAPON]); + if (style == ATTACK_STYLE_MELEE) actions[HEAD_LOADOUT] = LOADOUT_SPEC_MELEE; + else if (style == ATTACK_STYLE_RANGED) actions[HEAD_LOADOUT] = LOADOUT_SPEC_RANGE; + else if (style == ATTACK_STYLE_MAGIC) actions[HEAD_LOADOUT] = LOADOUT_SPEC_MAGIC; + } + (void)target; +} + +static EncounterState* nh_pvp_create(void) { + NhPvpState* s = (NhPvpState*)calloc(1, sizeof(NhPvpState)); + pvp_init(&s->env); + s->env.ocean_io.agent_obs = s->env._obs_buf; + s->env.ocean_io.agent_actions = s->env._acts_buf; + s->env.ocean_io.agent_rewards = s->env._rews_buf; + s->env.ocean_io.agent_terminals = s->env._terms_buf; + return (EncounterState*)s; +} + +static void nh_pvp_destroy(EncounterState* state) { + NhPvpState* s = (NhPvpState*)state; + pvp_close(&s->env); + free(s); +} + +static void nh_pvp_init_context(EncounterContext* context) { + (void)context; +} + +static void nh_pvp_destroy_context(EncounterContext* context) { + (void)context; +} + +static void nh_pvp_reset(EncounterState* state, EncounterContext* context, uint32_t seed) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + if (seed != 0) { + s->env.has_rng_seed = 1; + s->env.rng_seed = seed; + } + pvp_reset(&s->env); +} + +static void nh_pvp_step(EncounterState* state, EncounterContext* context, const int* actions) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + memcpy(s->env.ocean_io.agent_actions, actions, NUM_ACTION_HEADS * sizeof(int)); + pvp_step(&s->env); +} + +static void nh_pvp_step_human_commands( + EncounterState* state, + EncounterContext* context, + HumanInput* hi +) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + int saved_use_c_opponent_p0 = s->env.pvp_runtime.use_c_opponent_p0; + s->env.pvp_runtime.use_c_opponent_p0 = 0; + if (hi->pending_move_x >= 0 && hi->pending_move_y >= 0) { + s->env.pvp_runtime.walk_dest_x[0] = hi->pending_move_x; + s->env.pvp_runtime.walk_dest_y[0] = hi->pending_move_y; + } + nh_pvp_translate_human_input( + hi, + s->env.ocean_io.agent_actions, + &s->env.players[0], + &s->env.players[1]); + pvp_step(&s->env); + s->env.pvp_runtime.use_c_opponent_p0 = saved_use_c_opponent_p0; + /* pending_move must survive until arrival so later ticks keep re-arming + walk_dest for the same click */ + if (s->env.pvp_runtime.walk_dest_x[0] < 0 || s->env.pvp_runtime.walk_dest_y[0] < 0) { + human_input_clear_move(hi); + } + hi->pending_attack = 0; + hi->pending_spell = 0; + hi->pending_prayer = 0; + hi->pending_offensive_prayer = 0; + hi->pending_food = 0; + hi->pending_potion = 0; + hi->pending_karambwan = 0; + hi->pending_veng = 0; + hi->pending_spec = 0; +} + +static void nh_pvp_write_obs(EncounterState* state, EncounterContext* context, float* obs_out) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + memcpy(obs_out, s->env._obs_buf, SLOT_NUM_OBSERVATIONS * sizeof(float)); +} + +static void nh_pvp_write_mask( + EncounterState* state, + EncounterContext* context, + float* mask_out +) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + for (int i = 0; i < ACTION_MASK_SIZE; i++) { + mask_out[i] = (float)s->env._masks_buf[i]; + } +} + +static float nh_pvp_get_reward(EncounterState* state, EncounterContext* context) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + return s->env._rews_buf[0]; +} + +static int nh_pvp_is_terminal(EncounterState* state, EncounterContext* context) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + return s->env.episode_over; +} + +static int nh_pvp_get_entity_count(EncounterState* state, EncounterContext* context) { + (void)state; + (void)context; + return NUM_AGENTS; +} + +static void* nh_pvp_get_entity(EncounterState* state, EncounterContext* context, int index) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + return &s->env.players[index]; +} + +static void nh_pvp_fill_render_entities( + EncounterState* state, + EncounterContext* context, + RenderEntity* out, + int max_entities, + int* count +) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + int n = NUM_AGENTS < max_entities ? NUM_AGENTS : max_entities; + for (int i = 0; i < n; i++) { + osrs_render_entity_from_player_entity(&s->env.players[i], &out[i]); + out[i].attack_target_entity_idx = (n >= 2) ? (1 - i) : -1; + } + *count = n; +} + +static void nh_pvp_put_int( + EncounterState* state, + EncounterContext* context, + const char* key, + int value +) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + if (strcmp(key, "opponent_type") == 0) { + s->env.pvp_runtime.opponent.type = (OpponentType)value; + } else if (strcmp(key, "opponent_p0_type") == 0) { + s->env.pvp_runtime.opponent_p0.type = (OpponentType)value; + } else if (strcmp(key, "is_lms") == 0) { + s->env.is_lms = value; + } else if (strcmp(key, "use_c_opponent") == 0) { + s->env.pvp_runtime.use_c_opponent = value; + } else if (strcmp(key, "use_c_opponent_p0") == 0) { + s->env.pvp_runtime.use_c_opponent_p0 = value; + } else if (strcmp(key, "auto_reset") == 0) { + s->env.auto_reset = value; + } else if (strcmp(key, "gear_tier") == 0) { + if (value < 0) { + s->env.pvp_runtime.gear_tier_weights[0] = 0.60f; + s->env.pvp_runtime.gear_tier_weights[1] = 0.25f; + s->env.pvp_runtime.gear_tier_weights[2] = 0.10f; + s->env.pvp_runtime.gear_tier_weights[3] = 0.05f; + } else { + if (value > 3) { + fprintf(stderr, "nh_pvp: invalid gear_tier %d\n", value); + abort(); + } + for (int t = 0; t < 4; t++) + s->env.pvp_runtime.gear_tier_weights[t] = 0.0f; + s->env.pvp_runtime.gear_tier_weights[value] = 1.0f; + } + } else if (strcmp(key, "seed") == 0) { + s->env.has_rng_seed = 1; + s->env.rng_seed = (uint32_t)value; + } +} + +static void nh_pvp_put_float( + EncounterState* state, + EncounterContext* context, + const char* key, + float value +) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + if (strcmp(key, "shaping_scale") == 0) { + s->env.shaping.shaping_scale = value; + } +} + +static void nh_pvp_put_ptr( + EncounterState* state, + EncounterContext* context, + const char* key, + void* value +) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + if (strcmp(key, "collision_map") == 0) { + s->env.collision_map = value; + } +} + +static void* nh_pvp_get_log(EncounterState* state, EncounterContext* context) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + return &s->env.log; +} + +static int nh_pvp_get_tick(EncounterState* state, EncounterContext* context) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + return s->env.tick; +} + +static int nh_pvp_get_winner(EncounterState* state, EncounterContext* context) { + (void)context; + NhPvpState* s = (NhPvpState*)state; + return s->env.winner; +} + +static const EncounterDef ENCOUNTER_NH_PVP = { + .name = "nh_pvp", + .obs_size = SLOT_NUM_OBSERVATIONS, + .num_action_heads = NUM_ACTION_HEADS, + .action_head_dims = NH_PVP_ACTION_DIMS, + .mask_size = ACTION_MASK_SIZE, + .state_size = sizeof(NhPvpState), + .context_size = sizeof(NhPvpContext), + .init_context = nh_pvp_init_context, + .destroy_context = nh_pvp_destroy_context, + + .create = nh_pvp_create, + .destroy = nh_pvp_destroy, + .reset = nh_pvp_reset, + .step = nh_pvp_step, + .step_human_commands = nh_pvp_step_human_commands, + + .write_obs = nh_pvp_write_obs, + .write_mask = nh_pvp_write_mask, + .get_reward = nh_pvp_get_reward, + .is_terminal = nh_pvp_is_terminal, + + .get_entity_count = nh_pvp_get_entity_count, + .get_entity = nh_pvp_get_entity, + .fill_render_entities = nh_pvp_fill_render_entities, + + .put_int = nh_pvp_put_int, + .put_float = nh_pvp_put_float, + .put_ptr = nh_pvp_put_ptr, + + .render_post_tick = NULL, + .get_log = nh_pvp_get_log, + .get_tick = nh_pvp_get_tick, + .get_winner = nh_pvp_get_winner, + + .translate_human_input = NULL, + .head_move = -1, + .head_prayer = -1, + .head_target = -1, +}; + +__attribute__((constructor)) +static void nh_pvp_register(void) { + encounter_register(&ENCOUNTER_NH_PVP); +} + +#endif /* ENCOUNTER_NH_PVP_H */ diff --git a/ocean/osrs/encounters/encounter_zulrah.h b/ocean/osrs/encounters/encounter_zulrah.h new file mode 100644 index 0000000000..80e432438a --- /dev/null +++ b/ocean/osrs/encounters/encounter_zulrah.h @@ -0,0 +1,2999 @@ +#ifndef ENCOUNTER_ZULRAH_H +#define ENCOUNTER_ZULRAH_H + +#include "../osrs_encounter.h" +#include "../osrs_encounter_player.h" +#include "../osrs_encounter_visual_events.h" +#include "../osrs_interaction.h" +#include "../osrs_types.h" +#include "../osrs_items.h" +#include "../osrs_combat.h" +#include "../osrs_combat_visuals.h" +#include "../osrs_special_attacks.h" +#include "../osrs_pvp_gear.h" +#include "../osrs_consumables.h" +#include "../osrs_player_consumables.h" +#include "../osrs_damage.h" +#include "../osrs_collision.h" +#include "../osrs_monsters_generated.h" +#include "../data/npc_models.h" +#include +#include + +#define ZUL_ARENA_SIZE 28 +#define ZUL_NPC_SIZE 5 + +#define ZUL_PLATFORM_MIN 5 +#define ZUL_PLATFORM_MAX 22 + +#define ZUL_POS_NORTH 0 +#define ZUL_POS_SOUTH 1 +#define ZUL_POS_EAST 2 +#define ZUL_POS_WEST 3 +#define ZUL_NUM_POSITIONS 4 + +static const int ZUL_POSITIONS[ZUL_NUM_POSITIONS][2] = { + { 10, 12 }, + { 10, 1 }, + { 20, 10 }, + { 0, 10 }, +}; + +#define ZUL_PLAYER_START_X 11 +#define ZUL_PLAYER_START_Y 7 + +#define ZUL_MELEE_STARE_TICKS 3 +#define ZUL_MELEE_INTERVAL 6 +#define ZUL_MELEE_STUN_TICKS 5 + +#define ZUL_DAMAGE_CAP 50 +#define ZUL_DAMAGE_CAP_MIN 45 + +#define ZUL_SURFACE_TICKS_INITIAL 3 +#define ZUL_SURFACE_TICKS 2 +#define ZUL_DIVE_PHASE_TICKS 3 +#define ZUL_DIVE_ANIM_TICKS 2 +#define ZUL_RANGED_ANIM_TICKS 6 +#define ZUL_MAGIC_ANIM_TICKS 2 +#define ZUL_TAIL_ANIM_TICKS 7 +#define ZUL_DEATH_ANIM_TICKS 5 + +#define ZUL_MAX_CLOUDS 7 +#define ZUL_MAX_SNAKELINGS 4 +#define ZUL_CLOUD_SIZE 3 +#define ZUL_CLOUD_DURATION 30 +#define ZUL_CLOUD_DAMAGE_MIN 1 +#define ZUL_CLOUD_DAMAGE_MAX 5 + +#define ZUL_SNAKELING_HP 1 +#define ZUL_SNAKELING_SPEED 3 +#define ZUL_SNAKELING_LIFESPAN 67 + +#define ZUL_VENOM_INTERVAL 30 +#define ZUL_VENOM_START 6 +#define ZUL_VENOM_MAX 20 + +#define ZUL_SPAWN_INTERVAL 3 +#define ZUL_CLOUD_FLIGHT_1 3 +#define ZUL_CLOUD_FLIGHT_2 4 +#define ZUL_REWARD_WIN_DEFAULT 1.0f +#define ZUL_REWARD_LOSS_PENALTY_DEFAULT 0.0f +#define ZUL_REWARD_DAMAGE_DEALT_DEFAULT 0.02f +#define ZUL_REWARD_CORRECT_STYLE_DEFAULT 0.05f +#define ZUL_REWARD_DAMAGE_RECEIVED_PENALTY_DEFAULT 0.01f +#define ZUL_REWARD_CLOUD_OCCUPANCY_PENALTY_DEFAULT 0.08f +#define ZUL_SCORE_SPEED_BONUS_DEFAULT 0.3f +#define ZUL_TRIP_RESPAWN_DELAY_TICKS 12 +#define ZUL_MAX_ATTACK_EVENTS 8 +#define ZUL_MAX_CLOUD_EVENTS 4 + +#define ZUL_ANTIVENOM_DURATION 300 +#define ZUL_ANTIVENOM_DOSES 4 + +#define ZUL_THRALL_MAX_HIT 3 +#define ZUL_THRALL_SPEED 4 +#define ZUL_THRALL_DURATION 99 +#define ZUL_THRALL_COOLDOWN 17 + +#define ZUL_PLAYER_HP 99 +#define ZUL_PLAYER_PRAYER 77 +#define ZUL_PLAYER_FOOD 10 +#define ZUL_PLAYER_KARAMBWAN 4 +#define ZUL_PLAYER_RESTORE_DOSES 8 +#define ZUL_MAX_TICKS 600 + +#define ZUL_NUM_OBS 123 +#define ZUL_NUM_ACTION_HEADS 7 + +#define ZUL_MOVE_DIM ENCOUNTER_MOVE_ACTIONS +#define ZUL_ATTACK_DIM 3 +#define ZUL_PRAYER_DIM ENCOUNTER_OVERHEAD_DIM_PVE +#define ZUL_OFFENSIVE_DIM ENCOUNTER_OFFENSIVE_DIM +#define ZUL_FOOD_DIM 3 +#define ZUL_POTION_DIM 3 +#define ZUL_SPEC_DIM 2 + +#define ZUL_ACTION_MASK_SIZE (ZUL_MOVE_DIM + ZUL_ATTACK_DIM + ZUL_PRAYER_DIM + \ + ZUL_FOOD_DIM + ZUL_POTION_DIM + ZUL_SPEC_DIM + ZUL_OFFENSIVE_DIM) + +#define ZUL_HEAD_MOVE 0 +#define ZUL_HEAD_ATTACK 1 +#define ZUL_HEAD_PRAYER 2 +#define ZUL_HEAD_FOOD 3 +#define ZUL_HEAD_POTION 4 +#define ZUL_HEAD_SPEC 5 +#define ZUL_HEAD_OFFENSIVE 6 + +#define ZUL_MOVE_STAY 0 +#define ZUL_ATK_NONE 0 +#define ZUL_ATK_MAGE 1 +#define ZUL_ATK_RANGE 2 + +typedef enum { + ZUL_FORM_GREEN = 0, + ZUL_FORM_RED, + ZUL_FORM_BLUE, +} ZulrahForm; + +static const int ZUL_FORM_MONSTER_IDX[] = { + [ZUL_FORM_GREEN] = MON_ZULRAH_GREEN, + [ZUL_FORM_RED] = MON_ZULRAH_RED, + [ZUL_FORM_BLUE] = MON_ZULRAH_BLUE, +}; + +typedef enum { + ZUL_GEAR_MAGE = 0, + ZUL_GEAR_RANGE, +} ZulrahGearStyle; + +typedef enum { + ZA_END = 0, + ZA_RANGED, + ZA_MAGIC_RANGED, + ZA_MELEE, + ZA_JAD_RM, + ZA_JAD_MR, + ZA_CLOUDS, + ZA_SNAKELINGS, + ZA_SNAKECLOUD_ALT, + ZA_CLOUDSNAKE_ALT, +} ZulActionType; + +typedef struct { + uint8_t type; + uint8_t count; +} ZulAction; + +#define ZUL_MAX_PHASE_ACTIONS 6 + +typedef struct { + uint8_t position; + uint8_t form; + uint8_t stand; + uint8_t stall; + uint8_t phase_ticks; + ZulAction actions[ZUL_MAX_PHASE_ACTIONS]; +} ZulRotationPhase; + +#define ZUL_MAX_ROT_PHASES 13 +#define ZUL_NUM_ROTATIONS 4 + +#define ZUL_STAND_SOUTHWEST 0 +#define ZUL_STAND_WEST 1 +#define ZUL_STAND_CENTER 2 +#define ZUL_STAND_NORTHEAST_TOP 3 +#define ZUL_STAND_NORTHEAST_BOT 4 +#define ZUL_STAND_NORTHWEST_TOP 5 +#define ZUL_STAND_NORTHWEST_BOT 6 +#define ZUL_STAND_EAST_PILLAR_S 7 +#define ZUL_STAND_EAST_PILLAR 8 +#define ZUL_STAND_EAST_PILLAR_N 9 +#define ZUL_STAND_EAST_PILLAR_N2 10 +#define ZUL_STAND_WEST_PILLAR_S 11 +#define ZUL_STAND_WEST_PILLAR 12 +#define ZUL_STAND_WEST_PILLAR_N 13 +#define ZUL_STAND_WEST_PILLAR_N2 14 +#define ZUL_NUM_STAND_LOCATIONS 15 +#define ZUL_STAND_NONE 255 + +static const int ZUL_STAND_COORDS[ZUL_NUM_STAND_LOCATIONS][2] = { + { 8, 8 }, + { 6, 14 }, + { 13, 9 }, + { 18, 16 }, + { 17, 16 }, + { 6, 15 }, + { 8, 16 }, + { 16, 9 }, + { 16, 10 }, + { 16, 12 }, + { 16, 13 }, + { 8, 9 }, + { 8, 10 }, + { 8, 12 }, + { 8, 13 }, +}; + +#define ZA(t,c) { (uint8_t)(t), (uint8_t)(c) } +#define ZE { 0, 0 } +#define _N ZUL_STAND_NONE + +static const ZulRotationPhase ZUL_ROT1[11] = { + { ZUL_POS_NORTH, ZUL_FORM_GREEN, ZUL_STAND_NORTHEAST_TOP, _N, 28, { ZA(ZA_CLOUDS,4), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_RED, ZUL_STAND_NORTHEAST_TOP, _N, 21, { ZA(ZA_MELEE,2), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_BLUE, ZUL_STAND_EAST_PILLAR_N, ZUL_STAND_EAST_PILLAR_S, 18, { ZA(ZA_MAGIC_RANGED,4), ZE } }, + { ZUL_POS_SOUTH, ZUL_FORM_GREEN, ZUL_STAND_WEST_PILLAR_N, ZUL_STAND_WEST_PILLAR_N2, 39, { ZA(ZA_RANGED,5), ZA(ZA_SNAKELINGS,2), ZA(ZA_CLOUDS,2), ZA(ZA_SNAKELINGS,2), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_RED, ZUL_STAND_WEST_PILLAR_N, _N, 22, { ZA(ZA_MELEE,2), ZE } }, + { ZUL_POS_WEST, ZUL_FORM_BLUE, ZUL_STAND_WEST_PILLAR_S, ZUL_STAND_EAST_PILLAR_S, 20, { ZA(ZA_MAGIC_RANGED,5), ZE } }, + { ZUL_POS_SOUTH, ZUL_FORM_GREEN, ZUL_STAND_EAST_PILLAR, _N, 28, { ZA(ZA_CLOUDS,3), ZA(ZA_SNAKELINGS,4), ZE } }, + { ZUL_POS_SOUTH, ZUL_FORM_BLUE, ZUL_STAND_EAST_PILLAR, ZUL_STAND_EAST_PILLAR_N2, 36, { ZA(ZA_MAGIC_RANGED,5), ZA(ZA_SNAKECLOUD_ALT,5), ZE } }, + { ZUL_POS_WEST, ZUL_FORM_GREEN, ZUL_STAND_WEST_PILLAR_S, ZUL_STAND_EAST_PILLAR_S, 48, { ZA(ZA_JAD_RM,10), ZA(ZA_CLOUDS,4), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_RED, ZUL_STAND_NORTHEAST_TOP, _N, 21, { ZA(ZA_MELEE,2), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_GREEN, ZUL_STAND_NORTHEAST_TOP, _N, 28, { ZA(ZA_RANGED,5), ZA(ZA_CLOUDS,4), ZE } }, +}; + +static const ZulRotationPhase ZUL_ROT2[11] = { + { ZUL_POS_NORTH, ZUL_FORM_GREEN, ZUL_STAND_NORTHEAST_TOP, _N, 28, { ZA(ZA_CLOUDS,4), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_RED, ZUL_STAND_NORTHEAST_TOP, _N, 21, { ZA(ZA_MELEE,2), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_BLUE, ZUL_STAND_EAST_PILLAR_N, ZUL_STAND_EAST_PILLAR_S, 18, { ZA(ZA_MAGIC_RANGED,4), ZE } }, + { ZUL_POS_WEST, ZUL_FORM_GREEN, ZUL_STAND_WEST_PILLAR_S, _N, 28, { ZA(ZA_CLOUDS,3), ZA(ZA_SNAKELINGS,4), ZE } }, + { ZUL_POS_SOUTH, ZUL_FORM_BLUE, ZUL_STAND_WEST_PILLAR_N, ZUL_STAND_WEST_PILLAR_N2, 39, { ZA(ZA_MAGIC_RANGED,5), ZA(ZA_SNAKELINGS,2), ZA(ZA_CLOUDS,2), ZA(ZA_SNAKELINGS,2), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_RED, ZUL_STAND_WEST_PILLAR_N, _N, 21, { ZA(ZA_MELEE,2), ZE } }, + { ZUL_POS_EAST, ZUL_FORM_GREEN, ZUL_STAND_CENTER, ZUL_STAND_WEST_PILLAR_S, 20, { ZA(ZA_RANGED,5), ZE } }, + { ZUL_POS_SOUTH, ZUL_FORM_BLUE, ZUL_STAND_WEST_PILLAR_S, ZUL_STAND_WEST_PILLAR_N2, 36, { ZA(ZA_MAGIC_RANGED,5), ZA(ZA_SNAKECLOUD_ALT,5), ZE } }, + { ZUL_POS_WEST, ZUL_FORM_GREEN, ZUL_STAND_WEST_PILLAR_S, ZUL_STAND_EAST_PILLAR_S, 48, { ZA(ZA_JAD_RM,10), ZA(ZA_CLOUDS,4), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_RED, ZUL_STAND_NORTHEAST_TOP, _N, 21, { ZA(ZA_MELEE,2), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_GREEN, ZUL_STAND_NORTHEAST_TOP, _N, 28, { ZA(ZA_RANGED,5), ZA(ZA_CLOUDS,4), ZE } }, +}; + +static const ZulRotationPhase ZUL_ROT3[12] = { + { ZUL_POS_NORTH, ZUL_FORM_GREEN, ZUL_STAND_NORTHEAST_TOP, _N, 28, { ZA(ZA_CLOUDS,4), ZE } }, + { ZUL_POS_EAST, ZUL_FORM_GREEN, ZUL_STAND_NORTHEAST_TOP, _N, 30, { ZA(ZA_RANGED,5), ZA(ZA_SNAKELINGS,3), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_RED, ZUL_STAND_WEST, _N, 40, { ZA(ZA_CLOUDSNAKE_ALT,6), ZA(ZA_MELEE,2), ZE } }, + { ZUL_POS_WEST, ZUL_FORM_BLUE, ZUL_STAND_WEST, ZUL_STAND_EAST_PILLAR_S, 20, { ZA(ZA_MAGIC_RANGED,5), ZE } }, + { ZUL_POS_SOUTH, ZUL_FORM_GREEN, ZUL_STAND_EAST_PILLAR_S, ZUL_STAND_EAST_PILLAR_N2, 20, { ZA(ZA_RANGED,5), ZE } }, + { ZUL_POS_EAST, ZUL_FORM_BLUE, ZUL_STAND_EAST_PILLAR_S, ZUL_STAND_WEST_PILLAR_S, 20, { ZA(ZA_MAGIC_RANGED,5), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_GREEN, ZUL_STAND_WEST_PILLAR_N, _N, 25, { ZA(ZA_CLOUDS,3), ZA(ZA_SNAKELINGS,3), ZE } }, + { ZUL_POS_WEST, ZUL_FORM_GREEN, ZUL_STAND_WEST_PILLAR_N, _N, 20, { ZA(ZA_RANGED,5), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_BLUE, ZUL_STAND_EAST_PILLAR_N, ZUL_STAND_EAST_PILLAR_S, 36, { ZA(ZA_MAGIC_RANGED,5), ZA(ZA_CLOUDS,2), ZA(ZA_SNAKELINGS,3), ZE } }, + { ZUL_POS_EAST, ZUL_FORM_GREEN, ZUL_STAND_EAST_PILLAR_N, _N, 35, { ZA(ZA_JAD_MR,10), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_BLUE, ZUL_STAND_NORTHEAST_TOP, _N, 18, { ZA(ZA_SNAKELINGS,4), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_GREEN, ZUL_STAND_NORTHEAST_TOP, _N, 28, { ZA(ZA_RANGED,5), ZA(ZA_CLOUDS,4), ZE } }, +}; + +static const ZulRotationPhase ZUL_ROT4[13] = { + { ZUL_POS_NORTH, ZUL_FORM_GREEN, ZUL_STAND_NORTHEAST_TOP, _N, 28, { ZA(ZA_CLOUDS,4), ZE } }, + { ZUL_POS_EAST, ZUL_FORM_BLUE, ZUL_STAND_NORTHEAST_TOP, _N, 36, { ZA(ZA_SNAKELINGS,4), ZA(ZA_MAGIC_RANGED,6), ZE } }, + { ZUL_POS_SOUTH, ZUL_FORM_GREEN, ZUL_STAND_WEST_PILLAR_N, ZUL_STAND_WEST_PILLAR_N2, 24, { ZA(ZA_RANGED,4), ZA(ZA_CLOUDS,2), ZE } }, + { ZUL_POS_WEST, ZUL_FORM_BLUE, ZUL_STAND_WEST_PILLAR_N, _N, 30, { ZA(ZA_SNAKELINGS,4), ZA(ZA_MAGIC_RANGED,4), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_RED, ZUL_STAND_EAST_PILLAR_N, _N, 28, { ZA(ZA_MELEE,2), ZA(ZA_CLOUDS,2), ZE } }, + { ZUL_POS_EAST, ZUL_FORM_GREEN, ZUL_STAND_EAST_PILLAR, _N, 17, { ZA(ZA_RANGED,4), ZE } }, + { ZUL_POS_SOUTH, ZUL_FORM_GREEN, ZUL_STAND_EAST_PILLAR, _N, 34, { ZA(ZA_SNAKELINGS,6), ZA(ZA_CLOUDS,3), ZE } }, + { ZUL_POS_WEST, ZUL_FORM_BLUE, ZUL_STAND_WEST_PILLAR_S, _N, 33, { ZA(ZA_MAGIC_RANGED,5), ZA(ZA_SNAKELINGS,4), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_GREEN, ZUL_STAND_EAST_PILLAR_N, ZUL_STAND_EAST_PILLAR_S, 20, { ZA(ZA_RANGED,4), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_BLUE, ZUL_STAND_EAST_PILLAR_N, ZUL_STAND_EAST_PILLAR_S, 27, { ZA(ZA_MAGIC_RANGED,4), ZA(ZA_CLOUDS,3), ZE } }, + { ZUL_POS_EAST, ZUL_FORM_GREEN, ZUL_STAND_EAST_PILLAR_N, _N, 29, { ZA(ZA_JAD_MR,8), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_BLUE, ZUL_STAND_NORTHEAST_TOP, _N, 18, { ZA(ZA_SNAKELINGS,4), ZE } }, + { ZUL_POS_NORTH, ZUL_FORM_GREEN, ZUL_STAND_NORTHEAST_TOP, _N, 28, { ZA(ZA_RANGED,5), ZA(ZA_CLOUDS,4), ZE } }, +}; + +#undef _N + +static const ZulRotationPhase* const ZUL_ROTATIONS[ZUL_NUM_ROTATIONS] = { + ZUL_ROT1, ZUL_ROT2, ZUL_ROT3, ZUL_ROT4, +}; +static const int ZUL_ROT_LENGTHS[ZUL_NUM_ROTATIONS] = { 11, 11, 12, 13 }; + +#undef ZA +#undef ZE + +static const int ZUL_ACTION_HEAD_DIMS[ZUL_NUM_ACTION_HEADS] = { + ZUL_MOVE_DIM, ZUL_ATTACK_DIM, ZUL_PRAYER_DIM, + ZUL_FOOD_DIM, ZUL_POTION_DIM, ZUL_SPEC_DIM, ZUL_OFFENSIVE_DIM, +}; + +#define ZUL_NUM_GEAR_TIERS 3 +#define ZUL_GEAR_TIER_FIXED 0 +#define ZUL_GEAR_TIER_UNIFORM 1 +#define ZUL_GEAR_TIER_WEIGHTED 2 + +static const uint8_t ZUL_MAGE_LOADOUT[ZUL_NUM_GEAR_TIERS][NUM_GEAR_SLOTS] = { + { ITEM_MYSTIC_HAT, ITEM_GOD_CAPE, ITEM_GLORY, ITEM_AMETHYST_ARROW, + ITEM_TRIDENT_OF_SWAMP, ITEM_BOOK_OF_DARKNESS, ITEM_MYSTIC_TOP, ITEM_MYSTIC_BOTTOM, + ITEM_BARROWS_GLOVES, ITEM_MYSTIC_BOOTS, ITEM_RING_OF_RECOIL }, + { ITEM_AHRIMS_HOOD, ITEM_GOD_CAPE, ITEM_OCCULT_NECKLACE, ITEM_GOD_BLESSING, + ITEM_SANGUINESTI_STAFF, ITEM_MAGES_BOOK, ITEM_AHRIMS_ROBETOP, ITEM_AHRIMS_ROBESKIRT, + ITEM_TORMENTED_BRACELET, ITEM_INFINITY_BOOTS, ITEM_RING_OF_RECOIL }, + { ITEM_ANCESTRAL_HAT, ITEM_IMBUED_SARA_CAPE, ITEM_OCCULT_NECKLACE, ITEM_DRAGON_ARROWS, + ITEM_EYE_OF_AYAK, ITEM_ELIDINIS_WARD_F, ITEM_ANCESTRAL_TOP, ITEM_ANCESTRAL_BOTTOM, + ITEM_CONFLICTION_GAUNTLETS, ITEM_AVERNIC_TREADS, ITEM_RING_OF_SUFFERING_RI }, +}; + +static const uint8_t ZUL_RANGE_LOADOUT[ZUL_NUM_GEAR_TIERS][NUM_GEAR_SLOTS] = { + { ITEM_BLESSED_COIF, ITEM_AVAS_ACCUMULATOR, ITEM_GLORY, ITEM_AMETHYST_ARROW, + ITEM_MAGIC_SHORTBOW_I, ITEM_NONE, ITEM_BLACK_DHIDE_BODY, ITEM_BLACK_DHIDE_CHAPS, + ITEM_BARROWS_GLOVES, ITEM_MYSTIC_BOOTS, ITEM_RING_OF_RECOIL }, + { ITEM_CRYSTAL_HELM, ITEM_AVAS_ASSEMBLER, ITEM_NECKLACE_OF_ANGUISH, ITEM_GOD_BLESSING, + ITEM_BOW_OF_FAERDHINEN, ITEM_NONE, ITEM_CRYSTAL_BODY, ITEM_CRYSTAL_LEGS, + ITEM_BARROWS_GLOVES, ITEM_BLESSED_DHIDE_BOOTS, ITEM_RING_OF_RECOIL }, + { ITEM_MASORI_MASK_F, ITEM_DIZANAS_QUIVER, ITEM_NECKLACE_OF_ANGUISH, ITEM_DRAGON_ARROWS, + ITEM_TWISTED_BOW, ITEM_NONE, ITEM_MASORI_BODY_F, ITEM_MASORI_CHAPS_F, + ITEM_ZARYTE_VAMBRACES, ITEM_AVERNIC_TREADS, ITEM_RING_OF_SUFFERING_RI }, +}; + +static void zul_populate_player_inventory(Player* p, int gear_tier) { + const uint8_t* loadouts[] = { + ZUL_MAGE_LOADOUT[gear_tier], + ZUL_RANGE_LOADOUT[gear_tier], + }; + encounter_populate_inventory(p, loadouts, 2, NULL); +} + +#define ZUL_NUM_SNAKELING_POSITIONS 5 +static const int ZUL_SNAKELING_POSITIONS[ZUL_NUM_SNAKELING_POSITIONS][2] = { + { 7, 14 }, { 7, 10 }, { 12, 8 }, { 17, 10 }, { 17, 16 }, +}; + +static int zul_cloud_overlaps_safe_center(int x, int y, int sx, int sy) { + return x <= sx + 1 && x + ZUL_CLOUD_SIZE - 1 >= sx - 1 && + y <= sy + 1 && y + ZUL_CLOUD_SIZE - 1 >= sy - 1; +} + +static int zul_cloud_overlaps_safe_area(int x, int y, int stand_id, int stall_id) { + if (stand_id < ZUL_NUM_STAND_LOCATIONS) { + int sx = ZUL_STAND_COORDS[stand_id][0]; + int sy = ZUL_STAND_COORDS[stand_id][1]; + if (zul_cloud_overlaps_safe_center(x, y, sx, sy)) return 1; + } + if (stall_id < ZUL_NUM_STAND_LOCATIONS) { + int sx = ZUL_STAND_COORDS[stall_id][0]; + int sy = ZUL_STAND_COORDS[stall_id][1]; + if (zul_cloud_overlaps_safe_center(x, y, sx, sy)) return 1; + } + return 0; +} + +typedef struct { + int x, y; + int active; + int ticks_remaining; +} ZulrahCloud; + +#define ZUL_MAX_PENDING_CLOUDS 16 +typedef struct { + int x, y; + int delay; +} ZulrahPendingCloud; + +typedef struct { + Player entity; + uint32_t npc_instance_id; + int active; + int attack_timer; + int is_magic; + int lifespan; +} ZulrahSnakeling; + +typedef struct { + float win; + float loss_penalty; + float damage_dealt; + float correct_style; + float damage_received_penalty; + float cloud_occupancy_penalty; +} ZulrahRewardConfig; + +typedef enum { + ZUL_EPISODE_SINGLE_KILL = 0, + ZUL_EPISODE_TRIP = 1, +} ZulrahEpisodeMode; + +typedef enum { + ZUL_OUTCOME_PLAYER_WON = 0, + ZUL_OUTCOME_PLAYER_DIED = 1, +} ZulOutcome; + +typedef struct { + Player player; + Player zulrah; + uint32_t zulrah_npc_instance_id; + uint32_t next_npc_instance_id; + + int rotation_index; + int phase_index; + + int action_index; + int action_progress; + int action_timer; + int jad_is_magic_next; + + ZulrahForm current_form; + int zulrah_visible; + int zulrah_attacking; + + int melee_target_x, melee_target_y; + int melee_pending; + int melee_stare_timer; + + int phase_timer; + int surface_timer; + int is_diving; + int zulrah_anim_until_tick; + int zulrah_anim_event_tick; + int zulrah_death_ticks; + + int player_stunned_ticks; + + ZulrahCloud clouds[ZUL_MAX_CLOUDS]; + ZulrahPendingCloud pending_clouds[ZUL_MAX_PENDING_CLOUDS]; + ZulrahSnakeling snakelings[ZUL_MAX_SNAKELINGS]; + + ZulrahGearStyle player_gear; + OsrsInteraction interaction; + int player_dest_x, player_dest_y; + int player_dest_explicit; + int player_moved_this_tick; + int player_chased_target_this_tick; + + int venom_counter; + int venom_timer; + int antivenom_timer; + + int gear_tier; + int gear_tier_fixed; + int gear_tier_mode; + int episode_mode; + float gear_tier_weights[ZUL_NUM_GEAR_TIERS]; + ZulrahRewardConfig reward_config; + + EncounterLoadoutStats mage_stats; + EncounterLoadoutStats range_stats; + int human_command_mode; + EncounterLoadoutStats human_loadout_stats; + const HumanCommand* human_commands; + int human_command_count; + + int magic_def_drain; + + int thrall_active; + int thrall_attack_timer; + int thrall_duration_remaining; + int thrall_cooldown; + + void* collision_map; + int world_offset_x; + int world_offset_y; + + int tick; + int episode_over; + ZulOutcome winner; + int kills_this_episode; + int boss_killed_this_tick; + int player_lost_this_tick; + int kill_start_tick; + int respawn_timer; + float score_speed_bonus_sum; + uint32_t rng_state; + + float reward; + float episode_return; + float damage_dealt_this_tick; + float damage_received_this_tick; + int cloud_occupancy_this_tick; + float total_damage_dealt; + float total_damage_received; + int total_cloud_occupancy_ticks; + int total_active_cloud_ticks; + int total_pending_cloud_ticks; + float total_cloud_damage_received; + + int player_attacked_this_tick; + int player_attack_dmg; + int player_attack_style_id; + int player_attack_is_special; + EncounterProjectileTiming player_attack_timing; + + struct { + int src_x, src_y, dst_x, dst_y; + int style; + int damage; + } attack_events[ZUL_MAX_ATTACK_EVENTS]; + int attack_event_count; + + struct { + int src_x, src_y, dst_x, dst_y; + int flight_ticks; + } cloud_events[ZUL_MAX_CLOUD_EVENTS]; + int cloud_event_count; + + Log log; +} ZulrahState; + +static void zul_set_npc_anim_event(ZulrahState* s, int anim_id, int duration_ticks) { + osrs_npc_primary_anim_event_set( + &s->zulrah, + &s->zulrah_anim_event_tick, + &s->zulrah_anim_until_tick, + s->tick, + anim_id, + duration_ticks); +} + +static int zul_should_emit_npc_anim_event(const ZulrahState* s) { + return osrs_npc_primary_anim_event_should_emit( + &s->zulrah, s->zulrah_anim_event_tick, s->tick); +} + +static int zul_attack_anim_for_style(ZulrahState* s, int style) { + if (style == 2) { + int player_center_x = s->player.x; + int zulrah_center_x = s->zulrah.x + ZUL_NPC_SIZE / 2; + return player_center_x < zulrah_center_x + ? ZULRAH_ANIM_TAIL_LEFT + : ZULRAH_ANIM_TAIL_RIGHT; + } + if (style == 1) return ZULRAH_ANIM_ATTACK_MAGIC; + return ZULRAH_ANIM_ATTACK; +} + +static int zul_attack_anim_ticks_for_style(int style) { + if (style == 2) return ZUL_TAIL_ANIM_TICKS; + if (style == 1) return ZUL_MAGIC_ANIM_TICKS; + return ZUL_RANGED_ANIM_TICKS; +} + +static inline EncounterProjectileTiming zul_player_projectile_timing( + int style, uint8_t weapon, int is_special, int distance +) { + EncounterProjectileDelayKind kind = encounter_projectile_delay_kind_for_style((AttackStyle)style); + EncounterProjectileDelayOptions options = {0}; + if (style == ATTACK_STYLE_MAGIC && weapon == ITEM_EYE_OF_AYAK) { + kind = ENCOUNTER_PROJECTILE_DELAY_EYE_OF_AYAK; + } + if (style == ATTACK_STYLE_RANGED) { + if (weapon == ITEM_TOXIC_BLOWPIPE) { + kind = ENCOUNTER_PROJECTILE_DELAY_THROWN; + options.visual_delay_ticks = 1; + if (is_special) { + options.reduce_delay = -1; + options.visual_hit_early_ticks = 1; + } + } else if (weapon == ITEM_TWISTED_BOW) { + options.visual_delay_ticks = 1; + } + } + return encounter_projectile_timing(distance, 1, kind, options); +} + +static void zul_record_player_attack_visual( + ZulrahState* s, int style, int damage, int is_special +) { + int distance = encounter_projectile_distance( + s->player.x, s->player.y, 1, + s->zulrah.x, s->zulrah.y, ZUL_NPC_SIZE, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + s->player_attacked_this_tick = 1; + s->player_attack_dmg = damage; + s->player_attack_style_id = style; + s->player_attack_is_special = is_special; + s->player_attack_timing = zul_player_projectile_timing( + style, s->player.equipped[GEAR_SLOT_WEAPON], is_special, distance); +} + +static uint32_t zul_next_npc_instance_id(ZulrahState* s) { + s->next_npc_instance_id++; + if (s->next_npc_instance_id == 0) { + s->next_npc_instance_id = 1; + } + return s->next_npc_instance_id; +} + +static void zul_update_npc_anim_lifetime(ZulrahState* s) { + if (!s->zulrah_visible) return; + if (s->tick < s->zulrah_anim_until_tick) return; + if (s->is_diving || s->surface_timer > 0) return; + osrs_npc_primary_anim_event_expire( + &s->zulrah, s->zulrah_anim_until_tick, s->tick); + if (s->zulrah.npc_anim_id < 0) + s->zulrah_anim_event_tick = -1; +} + +static const EncounterLoadoutStats* zul_current_loadout_stats(ZulrahState* s, int is_mage); + +static int zul_lookup_player_attack_target( + void* ctx, + int target_slot, + OsrsAttackTarget* out +) { + ZulrahState* s = (ZulrahState*)ctx; + if (target_slot != 0 || !s->zulrah_visible || s->is_diving) + return 0; + const EncounterLoadoutStats* ls = zul_current_loadout_stats( + s, s->player_gear == ZUL_GEAR_MAGE); + *out = (OsrsAttackTarget){ + .slot = 0, + .x = s->zulrah.x, + .y = s->zulrah.y, + .size = ZUL_NPC_SIZE, + .attack_range = ls->attack_range, + }; + return 1; +} + +static int zul_sample_gear_tier(ZulrahState* s) { + if (s->gear_tier_mode == ZUL_GEAR_TIER_FIXED) { + return s->gear_tier_fixed; + } + + if (s->gear_tier_mode == ZUL_GEAR_TIER_UNIFORM) { + return encounter_rand_int(&s->rng_state, ZUL_NUM_GEAR_TIERS); + } + + float total = 0.0f; + for (int i = 0; i < ZUL_NUM_GEAR_TIERS; i++) { + if (s->gear_tier_weights[i] < 0.0f) { + fprintf(stderr, "zulrah gear_tier_weight_%d must be >= 0, got %.6f\n", + i, s->gear_tier_weights[i]); + abort(); + } + total += s->gear_tier_weights[i]; + } + + if (total <= 0.0f) { + fprintf(stderr, "zulrah weighted gear tier mode requires positive total weight\n"); + abort(); + } + + float threshold = encounter_rand_float(&s->rng_state) * total; + for (int i = 0; i < ZUL_NUM_GEAR_TIERS; i++) { + threshold -= s->gear_tier_weights[i]; + if (threshold <= 0.0f) return i; + } + return ZUL_NUM_GEAR_TIERS - 1; +} + +static inline int zul_on_platform_bounds(int x, int y) { + return x >= ZUL_PLATFORM_MIN && x <= ZUL_PLATFORM_MAX && + y >= ZUL_PLATFORM_MIN && y <= ZUL_PLATFORM_MAX; +} + +static inline int zul_on_platform(ZulrahState* s, int x, int y) { + if (!s->collision_map) return zul_on_platform_bounds(x, y); + int wx = x + s->world_offset_x; + int wy = y + s->world_offset_y; + return collision_tile_walkable((const CollisionMap*)s->collision_map, 0, wx, wy); +} + +#define zul_pathfind(s, sx, sy, dx, dy) \ + encounter_pathfind((const CollisionMap*)(s)->collision_map, \ + (s)->world_offset_x, (s)->world_offset_y, (sx), (sy), (dx), (dy), NULL, NULL) + +static int zul_tile_walkable(void* ctx, int x, int y) { + return zul_on_platform((ZulrahState*)ctx, x, y); +} + +static inline int zul_player_in_cloud(int cx, int cy, int px, int py) { + return px >= cx && px < cx + ZUL_CLOUD_SIZE && + py >= cy && py < cy + ZUL_CLOUD_SIZE; +} + +static int zul_active_cloud_count(const ZulrahState* s) { + int count = 0; + for (int i = 0; i < ZUL_MAX_CLOUDS; i++) + count += s->clouds[i].active ? 1 : 0; + return count; +} + +static int zul_pending_cloud_count(const ZulrahState* s) { + int count = 0; + for (int i = 0; i < ZUL_MAX_PENDING_CLOUDS; i++) + count += s->pending_clouds[i].delay > 0 ? 1 : 0; + return count; +} + +static int zul_rect_signed_delta_1d(int p, int lo, int size) { + int hi = lo + size - 1; + if (p < lo) return lo - p; + if (p > hi) return hi - p; + return 0; +} + +static void zul_cloud_escape_delta(int cx, int cy, int px, int py, int* dx, int* dy) { + int west = cx - 1 - px; + int east = cx + ZUL_CLOUD_SIZE - px; + int south = cy - 1 - py; + int north = cy + ZUL_CLOUD_SIZE - py; + int best_dx = west; + int best_dy = 0; + int best_abs = abs_int(west); + if (abs_int(east) < best_abs) { + best_dx = east; + best_dy = 0; + best_abs = abs_int(east); + } + if (abs_int(south) < best_abs) { + best_dx = 0; + best_dy = south; + best_abs = abs_int(south); + } + if (abs_int(north) < best_abs) { + best_dx = 0; + best_dy = north; + } + *dx = best_dx; + *dy = best_dy; +} + +static int zul_nearest_active_cloud_features( + const ZulrahState* s, + int* signed_dx, + int* signed_dy, + int* escape_dx, + int* escape_dy +) { + int found = 0; + int best_dist = 0; + *signed_dx = 0; + *signed_dy = 0; + *escape_dx = 0; + *escape_dy = 0; + for (int i = 0; i < ZUL_MAX_CLOUDS; i++) { + if (!s->clouds[i].active) continue; + int dx = zul_rect_signed_delta_1d(s->player.x, s->clouds[i].x, ZUL_CLOUD_SIZE); + int dy = zul_rect_signed_delta_1d(s->player.y, s->clouds[i].y, ZUL_CLOUD_SIZE); + int dist = max_int(abs_int(dx), abs_int(dy)); + int inside = zul_player_in_cloud( + s->clouds[i].x, s->clouds[i].y, s->player.x, s->player.y); + if (!found || inside || dist < best_dist) { + found = 1; + best_dist = dist; + *signed_dx = dx; + *signed_dy = dy; + if (inside) + zul_cloud_escape_delta( + s->clouds[i].x, s->clouds[i].y, + s->player.x, s->player.y, escape_dx, escape_dy); + else { + *escape_dx = 0; + *escape_dy = 0; + } + if (inside) break; + } + } + return found; +} + +static int zul_nearest_pending_cloud_features( + const ZulrahState* s, + int* dx, + int* dy, + int* delay +) { + int found = 0; + int best_dist = 0; + *dx = 0; + *dy = 0; + *delay = 0; + for (int i = 0; i < ZUL_MAX_PENDING_CLOUDS; i++) { + if (s->pending_clouds[i].delay <= 0) continue; + int pdx = s->pending_clouds[i].x - s->player.x; + int pdy = s->pending_clouds[i].y - s->player.y; + int dist = max_int(abs_int(pdx), abs_int(pdy)); + if (!found || dist < best_dist) { + found = 1; + best_dist = dist; + *dx = pdx; + *dy = pdy; + *delay = s->pending_clouds[i].delay; + } + } + return found; +} + +static int zul_active_cloud_overlaps_safe(const ZulrahState* s, int safe_id) { + if (safe_id >= ZUL_NUM_STAND_LOCATIONS) return 0; + for (int i = 0; i < ZUL_MAX_CLOUDS; i++) { + if (!s->clouds[i].active) continue; + if (zul_cloud_overlaps_safe_area( + s->clouds[i].x, s->clouds[i].y, safe_id, ZUL_STAND_NONE)) + return 1; + } + return 0; +} + +static int zul_pending_cloud_overlaps_safe(const ZulrahState* s, int safe_id) { + if (safe_id >= ZUL_NUM_STAND_LOCATIONS) return 0; + for (int i = 0; i < ZUL_MAX_PENDING_CLOUDS; i++) { + if (s->pending_clouds[i].delay <= 0) continue; + if (zul_cloud_overlaps_safe_area( + s->pending_clouds[i].x, s->pending_clouds[i].y, + safe_id, ZUL_STAND_NONE)) + return 1; + } + return 0; +} + +static int zul_tile_in_active_cloud(const ZulrahState* s, int x, int y) { + for (int i = 0; i < ZUL_MAX_CLOUDS; i++) { + if (!s->clouds[i].active) continue; + if (zul_player_in_cloud(s->clouds[i].x, s->clouds[i].y, x, y)) + return 1; + } + return 0; +} + +static int zul_tile_in_pending_cloud_by_delay( + const ZulrahState* s, + int x, + int y, + int max_delay +) { + for (int i = 0; i < ZUL_MAX_PENDING_CLOUDS; i++) { + if (s->pending_clouds[i].delay <= 0 || + s->pending_clouds[i].delay > max_delay) + continue; + if (zul_player_in_cloud(s->pending_clouds[i].x, s->pending_clouds[i].y, x, y)) + return 1; + } + return 0; +} + +static int zul_move_action_cloud_unsafe(const ZulrahState* s, int action) { + if (action < 0 || action >= ZUL_MOVE_DIM) { + fprintf(stderr, "zulrah move action out of range for cloud obs: %d\n", action); + abort(); + } + int x = s->player.x + ENCOUNTER_MOVE_TARGET_DX[action]; + int y = s->player.y + ENCOUNTER_MOVE_TARGET_DY[action]; + return zul_tile_in_active_cloud(s, x, y) || + zul_tile_in_pending_cloud_by_delay(s, x, y, 1); +} + +static int zul_form_npc_id(ZulrahForm f) { + return MONSTER_DATABASE[ZUL_FORM_MONSTER_IDX[f]].npc_id; +} + +static inline int zul_cap_damage(ZulrahState* s, int damage) { + if (damage > ZUL_DAMAGE_CAP) { + return ZUL_DAMAGE_CAP_MIN + encounter_rand_int(&s->rng_state, ZUL_DAMAGE_CAP - ZUL_DAMAGE_CAP_MIN + 1); + } + return damage; +} + +static void zul_apply_player_damage(ZulrahState* s, int damage, AttackStyle style, + Player* attacker) { + if (damage <= 0) return; + encounter_damage_player(&s->player, damage, &s->damage_received_this_tick); + s->total_damage_received += damage; + s->player.hit_style = style; + + if (attacker) { + osrs_ensure_player_equipment(&s->player); + DamageResult damage_result = osrs_apply_passive_damage_pipeline( + damage, + style, + s->player.prayer, + 0, + 0, + 0, + &s->player.equipment_effect_profile, + &s->player.item_effect_state, + &s->rng_state + ); + if (damage_result.recoil_damage > 0) { + int recoil = damage_result.recoil_damage; + if (s->player.equipment_effect_profile.recoil_source == OSRS_RECOIL_SOURCE_RING_OF_RECOIL && + recoil > s->player.item_effect_state.recoil_charges) { + recoil = s->player.item_effect_state.recoil_charges; + } + encounter_damage_player(attacker, recoil, NULL); + osrs_consume_recoil_charges(&s->player, recoil); + } + } +} + +static void zul_try_envenom(ZulrahState* s) { + if (s->venom_counter > 0) return; + if (s->antivenom_timer > 0) return; + if (encounter_rand_int(&s->rng_state, 4) != 0) return; + s->venom_counter = 1; + s->venom_timer = ZUL_VENOM_INTERVAL; +} + +static int zul_player_def_roll(ZulrahState* s, int attack_style) { + const EncounterLoadoutStats* ls = zul_current_loadout_stats( + s, s->player_gear == ZUL_GEAR_MAGE); + int roll = encounter_player_def_roll_from_loadout( + 99, 99, + ls->def_stab, ls->def_slash, ls->def_crush, ls->def_magic, ls->def_ranged, + attack_style, 2); + return roll > 0 ? roll : 0; +} + +static void zul_record_attack(ZulrahState* s, int src_x, int src_y, + int dst_x, int dst_y, int style, int damage) { + int anim_id = zul_attack_anim_for_style(s, style); + zul_set_npc_anim_event(s, anim_id, zul_attack_anim_ticks_for_style(style)); + if (s->attack_event_count >= ZUL_MAX_ATTACK_EVENTS) { + fprintf(stderr, "zulrah attack event capacity exceeded: %d\n", + ZUL_MAX_ATTACK_EVENTS); + abort(); + } + int i = s->attack_event_count++; + s->attack_events[i].src_x = src_x; + s->attack_events[i].src_y = src_y; + s->attack_events[i].dst_x = dst_x; + s->attack_events[i].dst_y = dst_y; + s->attack_events[i].style = style; + s->attack_events[i].damage = damage; +} + +static void zul_attack_ranged(ZulrahState* s) { + const MonsterStats* m = &MONSTER_DATABASE[MON_ZULRAH_GREEN]; + int npc_att_roll = osrs_npc_attack_roll(m->range_level, m->range_att_bonus); + int dmg = 0; + int did_hit = 0; + if (encounter_prayer_correct_for_style(s->player.prayer, ATTACK_STYLE_RANGED)) { + int def_roll = zul_player_def_roll(s, ATTACK_STYLE_RANGED); + did_hit = encounter_roll_hit_chance(&s->rng_state, npc_att_roll, def_roll); + } else { + int def_roll = zul_player_def_roll(s, ATTACK_STYLE_RANGED); + if (encounter_roll_hit_chance(&s->rng_state, npc_att_roll, def_roll)) { + did_hit = 1; + dmg = encounter_rand_int(&s->rng_state, m->max_hit + 1); + zul_apply_player_damage(s, dmg, ATTACK_STYLE_RANGED, &s->zulrah); + } + } + if (did_hit) zul_try_envenom(s); + zul_record_attack(s, s->zulrah.x, s->zulrah.y, + s->player.x, s->player.y, 0, dmg); +} + +static void zul_attack_magic(ZulrahState* s) { + int dmg = 0; + if (!encounter_prayer_correct_for_style(s->player.prayer, ATTACK_STYLE_MAGIC)) { + dmg = encounter_rand_int(&s->rng_state, MONSTER_DATABASE[MON_ZULRAH_BLUE].max_hit + 1); + zul_apply_player_damage(s, dmg, ATTACK_STYLE_MAGIC, &s->zulrah); + } + zul_try_envenom(s); + zul_record_attack(s, s->zulrah.x, s->zulrah.y, + s->player.x, s->player.y, 1, dmg); +} + +static void zul_attack_magic_ranged(ZulrahState* s) { + if (encounter_rand_int(&s->rng_state, 4) < 3) { + zul_attack_magic(s); + } else { + zul_attack_ranged(s); + } +} + +static int zul_on_pillar_safespot(int px, int py) { + if (py != 11) return 0; + return (px == 17 || px == 7); +} + +static void zul_melee_start(ZulrahState* s) { + s->melee_target_x = s->player.x; + s->melee_target_y = s->player.y; + s->melee_pending = 1; + s->melee_stare_timer = ZUL_MELEE_STARE_TICKS; +} + +static void zul_melee_hit(ZulrahState* s) { + s->melee_pending = 0; + int dmg = 0; + if (s->player.x == s->melee_target_x && s->player.y == s->melee_target_y + && !zul_on_pillar_safespot(s->player.x, s->player.y)) { + if (!encounter_prayer_correct_for_style(s->player.prayer, ATTACK_STYLE_MELEE)) { + dmg = 20 + encounter_rand_int(&s->rng_state, 11); + zul_apply_player_damage(s, dmg, ATTACK_STYLE_MELEE, &s->zulrah); + s->player_stunned_ticks = ZUL_MELEE_STUN_TICKS; + } + } + zul_record_attack(s, s->zulrah.x, s->zulrah.y, + s->melee_target_x, s->melee_target_y, 2, dmg); +} + +static void zul_attack_jad(ZulrahState* s) { + if (s->jad_is_magic_next) { + zul_attack_magic(s); + } else { + zul_attack_ranged(s); + } + s->jad_is_magic_next = !s->jad_is_magic_next; +} + +static inline void zul_form_def_bonuses(ZulrahForm form, int* def_magic, int* def_ranged) { + const MonsterStats* m = &MONSTER_DATABASE[ZUL_FORM_MONSTER_IDX[form]]; + *def_magic = m->magic_def; + *def_ranged = m->ranged_def; +} + +static AttackStyle zul_player_equipped_attack_style(const ZulrahState* s) { + AttackStyle style = (AttackStyle)get_item_attack_style(s->player.equipped[GEAR_SLOT_WEAPON]); + if (style == ATTACK_STYLE_MAGIC || + style == ATTACK_STYLE_RANGED || + style == ATTACK_STYLE_MELEE) { + return style; + } + return ATTACK_STYLE_RANGED; +} + +static void zul_refresh_human_loadout_stats(ZulrahState* s) { + AttackStyle style = zul_player_equipped_attack_style(s); + FightStyle fight_style = s->player.fight_style; + int spell_base_damage = (style == ATTACK_STYLE_MAGIC) ? 30 : 0; + encounter_compute_player_equipped_stats( + &s->player, style, fight_style, spell_base_damage, + &s->human_loadout_stats); +} + +static void zul_refresh_cached_loadout_stats(ZulrahState* s) { + int prayer_active = s->player.offensive_prayer != OFFENSIVE_PRAYER_NONE; + if (s->mage_stats.style == ATTACK_STYLE_MAGIC) { + encounter_update_loadout_level(&s->mage_stats, + prayer_active ? OFFENSIVE_PRAYER_AUGURY : OFFENSIVE_PRAYER_NONE, + s->player.current_magic, s->player.current_magic); + } + if (s->range_stats.style == ATTACK_STYLE_RANGED) { + encounter_update_loadout_level(&s->range_stats, + prayer_active ? OFFENSIVE_PRAYER_RIGOUR : OFFENSIVE_PRAYER_NONE, + s->player.current_ranged, s->player.current_ranged); + } + if (s->human_command_mode) + zul_refresh_human_loadout_stats(s); +} + +static const EncounterLoadoutStats* zul_current_loadout_stats(ZulrahState* s, int is_mage) { + if (s->human_command_mode) { + zul_refresh_human_loadout_stats(s); + return &s->human_loadout_stats; + } + return is_mage ? &s->mage_stats : &s->range_stats; +} + +static int zul_player_can_attack_zulrah( + ZulrahState* s, + const EncounterLoadoutStats* loadout_stats +) { + return encounter_player_can_attack( + s->player.x, s->player.y, + s->zulrah.x, s->zulrah.y, ZUL_NPC_SIZE, + loadout_stats->attack_range, osrs_los_open_query()); +} + +static int zul_zulrah_def_roll(ZulrahState* s, int is_mage) { + int def_magic = 0, def_ranged = 0; + zul_form_def_bonuses(s->current_form, &def_magic, &def_ranged); + if (is_mage) { + def_magic -= s->magic_def_drain; + if (def_magic < -64) def_magic = -64; + } + int def_bonus = is_mage ? def_magic : def_ranged; + int def_roll = (MONSTER_DATABASE[ZUL_FORM_MONSTER_IDX[s->current_form]].def_level + 8) * (def_bonus + 64); + if (def_roll < 0) def_roll = 0; + return def_roll; +} + +static int zul_player_attack_hits( + ZulrahState* s, int is_mage, const OsrsPreparedAttackEffects* attack_effects +) { + int att_roll = attack_effects->attack_roll; + int def_roll = zul_zulrah_def_roll(s, is_mage); + + return attack_effects->use_double_accuracy + ? encounter_roll_hit_chance_double(&s->rng_state, att_roll, def_roll) + : encounter_roll_hit_chance(&s->rng_state, att_roll, def_roll); +} + +static void zul_player_attack(ZulrahState* s, int is_mage) { + if (!s->zulrah_visible || s->is_diving) return; + if (s->player.attack_timer > 0) return; + if (s->player_stunned_ticks > 0) return; + + const EncounterLoadoutStats* ls = zul_current_loadout_stats(s, is_mage); + if (!zul_player_can_attack_zulrah(s, ls)) return; + + int gear_ok = s->human_command_mode + ? ((is_mage && ls->style == ATTACK_STYLE_MAGIC) || + (!is_mage && ls->style == ATTACK_STYLE_RANGED)) + : ((is_mage && s->player_gear == ZUL_GEAR_MAGE) || + (!is_mage && s->player_gear == ZUL_GEAR_RANGE)); + const MonsterStats* monster = &MONSTER_DATABASE[ZUL_FORM_MONSTER_IDX[s->current_form]]; + OsrsMagicAttackKind magic_kind = is_mage ? OSRS_MAGIC_ATTACK_POWERED_STAFF : OSRS_MAGIC_ATTACK_NONE; + OsrsPreparedAttackEffects attack_effects = osrs_prepare_attack_effects( + &s->player.equipment_effect_profile, + &s->player.item_effect_state, + s->player.equipped[GEAR_SLOT_WEAPON], + is_mage ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_RANGED, + magic_kind, + (OsrsTargetRef){ .kind = OSRS_TARGET_NPC, .id = 0 }, + 1, + osrs_player_att_roll(ls->eff_level, ls->attack_bonus), + ls->max_hit, + osrs_target_effect_context_magic( + monster->magic_level, + monster->magic_att_bonus), + s->player.current_hitpoints, + s->player.base_hitpoints + ); + s->player.attack_timer = ls->attack_speed; + if (!gear_ok) return; + + int dmg = 0; + int hit = zul_player_attack_hits(s, is_mage, &attack_effects); + if (hit) { + dmg = encounter_rand_int(&s->rng_state, attack_effects.max_hit + 1); + dmg = zul_cap_damage(s, dmg); + encounter_damage_player(&s->zulrah, dmg, &s->damage_dealt_this_tick); + s->total_damage_dealt += dmg; + } + { + OsrsPostAttackEffects post_effects = osrs_finalize_attack_effects( + &s->player.equipment_effect_profile, + &s->player.item_effect_state, + s->player.equipped[GEAR_SLOT_WEAPON], + is_mage ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_RANGED, + magic_kind, + (OsrsTargetRef){ .kind = OSRS_TARGET_NPC, .id = 0 }, + 1, + attack_effects.use_double_accuracy, + hit, + dmg, + &s->rng_state + ); + if (post_effects.heal_amount > 0) { + s->player.current_hitpoints += post_effects.heal_amount; + if (s->player.current_hitpoints > s->player.base_hitpoints) + s->player.current_hitpoints = s->player.base_hitpoints; + } + } + s->player.just_attacked = 1; + s->player.last_attack_style = is_mage ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_RANGED; + s->player.attack_style_this_tick = is_mage ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_RANGED; + zul_record_player_attack_visual( + s, s->player.attack_style_this_tick, dmg, 0); + + s->zulrah.hit_landed_this_tick = 1; + s->zulrah.hit_damage = dmg; + s->zulrah.hit_was_successful = (dmg > 0); +} + +static void zul_player_spec(ZulrahState* s) { + if (!s->zulrah_visible || s->is_diving) return; + if (s->player.attack_timer > 0) return; + if (s->player_stunned_ticks > 0) return; + + int is_mage = (s->player_gear == ZUL_GEAR_MAGE); + const EncounterLoadoutStats* ls = zul_current_loadout_stats(s, is_mage); + if (!zul_player_can_attack_zulrah(s, ls)) return; + + int weapon = s->human_command_mode + ? s->player.equipped[GEAR_SLOT_WEAPON] + : (is_mage + ? ZUL_MAGE_LOADOUT[s->gear_tier][GEAR_SLOT_WEAPON] + : ZUL_RANGE_LOADOUT[s->gear_tier][GEAR_SLOT_WEAPON]); + + int cost = osrs_spec_cost(weapon); + if (cost == 0) return; + if (s->player.special_energy < cost) return; + + const MonsterStats* m = &MONSTER_DATABASE[ZUL_FORM_MONSTER_IDX[s->current_form]]; + int def_roll = zul_zulrah_def_roll(s, is_mage); + + int att_roll = osrs_player_att_roll(ls->eff_level, ls->attack_bonus); + SpecResult sr = osrs_resolve_spec(weapon, att_roll, ls->max_hit, + def_roll, m->def_level, &s->rng_state); + + s->player.special_energy -= sr.spec_cost; + s->player.just_attacked = 1; + s->player.used_special_this_tick = 1; + s->player.last_attack_style = is_mage ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_RANGED; + s->player.attack_style_this_tick = s->player.last_attack_style; + s->player.attack_timer = sr.attack_speed_override ? sr.attack_speed_override : ls->attack_speed; + zul_record_player_attack_visual(s, s->player.attack_style_this_tick, 0, 1); + + int total_dmg = 0; + for (int i = 0; i < sr.num_hits; i++) { + int dmg = zul_cap_damage(s, sr.damage[i]); + encounter_damage_player(&s->zulrah, dmg, NULL); + total_dmg += dmg; + } + + if (sr.heal > 0) { + s->player.current_hitpoints += sr.heal; + if (s->player.current_hitpoints > s->player.base_hitpoints) + s->player.current_hitpoints = s->player.base_hitpoints; + } + + s->magic_def_drain += sr.magic_def_drain; + + s->damage_dealt_this_tick += total_dmg; + s->total_damage_dealt += total_dmg; + s->player_attack_dmg = total_dmg; + s->zulrah.hit_landed_this_tick = 1; + s->zulrah.hit_damage = total_dmg; + s->zulrah.hit_was_successful = (total_dmg > 0); +} + +static void zul_pick_snakeling_pos(ZulrahState* s, int* ox, int* oy) { + int order[ZUL_NUM_SNAKELING_POSITIONS]; + for (int i = 0; i < ZUL_NUM_SNAKELING_POSITIONS; i++) order[i] = i; + encounter_shuffle(order, ZUL_NUM_SNAKELING_POSITIONS, &s->rng_state); + for (int i = 0; i < ZUL_NUM_SNAKELING_POSITIONS; i++) { + int px = ZUL_SNAKELING_POSITIONS[order[i]][0]; + int py = ZUL_SNAKELING_POSITIONS[order[i]][1]; + if (zul_on_platform(s, px, py) && + !(px == s->player.x && py == s->player.y)) { + *ox = px; *oy = py; return; + } + } + *ox = s->player.x; + *oy = s->player.y; +} + +static void zul_spawn_snakeling(ZulrahState* s) { + for (int i = 0; i < ZUL_MAX_SNAKELINGS; i++) { + if (s->snakelings[i].active) continue; + ZulrahSnakeling* sn = &s->snakelings[i]; + memset(sn, 0, sizeof(ZulrahSnakeling)); + sn->active = 1; + sn->npc_instance_id = zul_next_npc_instance_id(s); + sn->entity.entity_type = ENTITY_NPC; + sn->entity.npc_size = 1; + sn->entity.npc_visible = 1; + sn->is_magic = encounter_rand_int(&s->rng_state, 2); + sn->entity.npc_def_id = sn->is_magic + ? MONSTER_DATABASE[MON_ZULRAH_SNAKELING_MAGIC].npc_id + : MONSTER_DATABASE[MON_ZULRAH_SNAKELING_MELEE].npc_id; + sn->entity.npc_anim_id = -1; + zul_pick_snakeling_pos(s, &sn->entity.x, &sn->entity.y); + sn->entity.current_hitpoints = ZUL_SNAKELING_HP; + sn->entity.base_hitpoints = ZUL_SNAKELING_HP; + sn->attack_timer = ZUL_SNAKELING_SPEED; + sn->lifespan = ZUL_SNAKELING_LIFESPAN; + + if (s->attack_event_count < 8) { + int ei = s->attack_event_count++; + s->attack_events[ei].src_x = s->zulrah.x; + s->attack_events[ei].src_y = s->zulrah.y; + s->attack_events[ei].dst_x = sn->entity.x; + s->attack_events[ei].dst_y = sn->entity.y; + s->attack_events[ei].style = 4; + s->attack_events[ei].damage = 0; + } + return; + } +} + +static void zul_snakeling_tick(ZulrahState* s) { + for (int i = 0; i < ZUL_MAX_SNAKELINGS; i++) { + ZulrahSnakeling* sn = &s->snakelings[i]; + if (!sn->active) continue; + sn->entity.npc_anim_id = -1; + + sn->lifespan--; + if (sn->lifespan <= 0) { sn->active = 0; continue; } + + int adx = abs_int(sn->entity.x - s->player.x); + int ady = abs_int(sn->entity.y - s->player.y); + int in_range = (adx <= 1 && ady <= 1); + if (!in_range) { + PathResult pr = zul_pathfind(s, sn->entity.x, sn->entity.y, + s->player.x, s->player.y); + if (pr.found && (pr.next_dx != 0 || pr.next_dy != 0)) { + int nx = sn->entity.x + pr.next_dx; + int ny = sn->entity.y + pr.next_dy; + if (zul_on_platform(s, nx, ny)) { + sn->entity.x = nx; sn->entity.y = ny; + } + } + } + + if (sn->attack_timer > 0) { sn->attack_timer--; continue; } + adx = abs_int(sn->entity.x - s->player.x); + ady = abs_int(sn->entity.y - s->player.y); + if (adx > 1 || ady > 1) continue; + + sn->attack_timer = ZUL_SNAKELING_SPEED; + sn->entity.npc_anim_id = sn->is_magic ? SNAKELING_ANIM_MAGIC : SNAKELING_ANIM_MELEE; + AttackStyle sn_style = sn->is_magic ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_MELEE; + if (encounter_prayer_correct_for_style(s->player.prayer, sn_style)) { + continue; + } + int sn_max = sn->is_magic ? MONSTER_DATABASE[MON_ZULRAH_SNAKELING_MAGIC].max_hit + : MONSTER_DATABASE[MON_ZULRAH_SNAKELING_MELEE].max_hit; + int dmg = encounter_rand_int(&s->rng_state, sn_max + 1); + AttackStyle st = sn->is_magic ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_MELEE; + zul_apply_player_damage(s, dmg, st, &sn->entity); + + if (sn->entity.current_hitpoints <= 0) { + sn->entity.current_hitpoints = 0; + sn->active = 0; + } + } +} + +static const ZulRotationPhase* zul_current_phase(ZulrahState* s) { + return &ZUL_ROTATIONS[s->rotation_index][s->phase_index]; +} + +static int zul_cloud_fits(ZulrahState* s, int x, int y) { + for (int dx = 0; dx < ZUL_CLOUD_SIZE; dx++) { + for (int dy = 0; dy < ZUL_CLOUD_SIZE; dy++) { + if (!zul_on_platform(s, x + dx, y + dy)) return 0; + } + } + return 1; +} + +static int zul_pick_cloud_pos(ZulrahState* s, int stand, int stall, int* ox, int* oy) { + int attempts = 0; + while (attempts++ < 100) { + int x = ZUL_PLATFORM_MIN + encounter_rand_int(&s->rng_state, ZUL_PLATFORM_MAX - ZUL_PLATFORM_MIN + 1); + int y = ZUL_PLATFORM_MIN + encounter_rand_int(&s->rng_state, ZUL_PLATFORM_MAX - ZUL_PLATFORM_MIN + 1); + + if (!zul_cloud_fits(s, x, y)) continue; + if (zul_cloud_overlaps_safe_area(x, y, stand, stall)) continue; + + int overlap = 0; + for (int j = 0; j < ZUL_MAX_CLOUDS && !overlap; j++) { + if (s->clouds[j].active && + abs(s->clouds[j].x - x) < ZUL_CLOUD_SIZE && + abs(s->clouds[j].y - y) < ZUL_CLOUD_SIZE) + overlap = 1; + } + for (int j = 0; j < ZUL_MAX_PENDING_CLOUDS && !overlap; j++) { + if (s->pending_clouds[j].delay > 0 && + abs(s->pending_clouds[j].x - x) < ZUL_CLOUD_SIZE && + abs(s->pending_clouds[j].y - y) < ZUL_CLOUD_SIZE) + overlap = 1; + } + if (overlap) continue; + + *ox = x; *oy = y; + return 1; + } + return 0; +} + +static void zul_queue_pending_cloud(ZulrahState* s, int x, int y, int delay) { + for (int i = 0; i < ZUL_MAX_PENDING_CLOUDS; i++) { + if (s->pending_clouds[i].delay <= 0) { + s->pending_clouds[i].x = x; + s->pending_clouds[i].y = y; + s->pending_clouds[i].delay = delay; + return; + } + } +} + +static void zul_activate_cloud(ZulrahState* s, int x, int y) { + for (int i = 0; i < ZUL_MAX_CLOUDS; i++) { + if (!s->clouds[i].active) { + s->clouds[i].x = x; + s->clouds[i].y = y; + s->clouds[i].active = 1; + s->clouds[i].ticks_remaining = ZUL_CLOUD_DURATION; + return; + } + } +} + +static void zul_emit_cloud_event(ZulrahState* s, int dst_x, int dst_y, int flight_ticks) { + if (s->cloud_event_count >= ZUL_MAX_CLOUD_EVENTS) { + fprintf(stderr, "zulrah cloud event capacity exceeded: %d\n", + ZUL_MAX_CLOUD_EVENTS); + abort(); + } + int i = s->cloud_event_count++; + s->cloud_events[i].src_x = s->zulrah.x; + s->cloud_events[i].src_y = s->zulrah.y; + s->cloud_events[i].dst_x = dst_x; + s->cloud_events[i].dst_y = dst_y; + s->cloud_events[i].flight_ticks = flight_ticks; +} + +static void zul_spawn_cloud(ZulrahState* s) { + const ZulRotationPhase* phase = zul_current_phase(s); + int stand = phase->stand; + int stall = phase->stall; + int x, y; + int spawned = 0; + if (zul_pick_cloud_pos(s, stand, stall, &x, &y)) { + zul_queue_pending_cloud(s, x, y, ZUL_CLOUD_FLIGHT_1); + zul_emit_cloud_event(s, x, y, ZUL_CLOUD_FLIGHT_1); + spawned = 1; + } + if (zul_pick_cloud_pos(s, stand, stall, &x, &y)) { + zul_queue_pending_cloud(s, x, y, ZUL_CLOUD_FLIGHT_2); + zul_emit_cloud_event(s, x, y, ZUL_CLOUD_FLIGHT_2); + spawned = 1; + } + if (spawned) + zul_set_npc_anim_event(s, ZULRAH_ANIM_ATTACK, ZUL_RANGED_ANIM_TICKS); +} + +static void zul_pending_cloud_tick(ZulrahState* s) { + for (int i = 0; i < ZUL_MAX_PENDING_CLOUDS; i++) { + if (s->pending_clouds[i].delay <= 0) continue; + s->pending_clouds[i].delay--; + if (s->pending_clouds[i].delay <= 0) { + zul_activate_cloud(s, s->pending_clouds[i].x, s->pending_clouds[i].y); + } + } +} + +static void zul_cloud_tick(ZulrahState* s) { + int active_count = 0; + int occupied = 0; + s->total_pending_cloud_ticks += zul_pending_cloud_count(s); + for (int i = 0; i < ZUL_MAX_CLOUDS; i++) { + if (!s->clouds[i].active) continue; + active_count++; + s->clouds[i].ticks_remaining--; + if (s->clouds[i].ticks_remaining <= 0) { s->clouds[i].active = 0; continue; } + + if (zul_player_in_cloud(s->clouds[i].x, s->clouds[i].y, + s->player.x, s->player.y)) { + occupied = 1; + int dmg = ZUL_CLOUD_DAMAGE_MIN + + encounter_rand_int(&s->rng_state, ZUL_CLOUD_DAMAGE_MAX - ZUL_CLOUD_DAMAGE_MIN + 1); + zul_apply_player_damage(s, dmg, ATTACK_STYLE_MAGIC, NULL); + s->total_cloud_damage_received += dmg; + } + } + s->total_active_cloud_ticks += active_count; + s->total_cloud_occupancy_ticks += occupied; + s->cloud_occupancy_this_tick = occupied; +} + +static void zul_venom_tick(ZulrahState* s) { + if (s->antivenom_timer > 0) s->antivenom_timer--; + + if (s->venom_counter == 0) return; + if (s->antivenom_timer > 0) return; + if (s->venom_timer > 0) { s->venom_timer--; return; } + int dmg = ZUL_VENOM_START + 2 * (s->venom_counter - 1); + if (dmg > ZUL_VENOM_MAX) dmg = ZUL_VENOM_MAX; + zul_apply_player_damage(s, dmg, ATTACK_STYLE_MAGIC, NULL); + s->venom_counter++; + s->venom_timer = ZUL_VENOM_INTERVAL; +} + +static void zul_thrall_tick(ZulrahState* s) { + if (!s->thrall_active) { + if (s->thrall_cooldown > 0) { s->thrall_cooldown--; return; } + s->thrall_active = 1; + s->thrall_duration_remaining = ZUL_THRALL_DURATION; + s->thrall_attack_timer = 1; + return; + } + + s->thrall_duration_remaining--; + if (s->thrall_duration_remaining <= 0) { + s->thrall_active = 0; + s->thrall_cooldown = ZUL_THRALL_COOLDOWN; + return; + } + + if (s->thrall_attack_timer > 0) { s->thrall_attack_timer--; return; } + s->thrall_attack_timer = ZUL_THRALL_SPEED; + + if (!s->zulrah_visible || s->is_diving) return; + + int dmg = encounter_rand_int(&s->rng_state, ZUL_THRALL_MAX_HIT + 1); + dmg = zul_cap_damage(s, dmg); + encounter_damage_player(&s->zulrah, dmg, &s->damage_dealt_this_tick); + s->total_damage_dealt += dmg; +} + +static void zul_fire_action(ZulrahState* s, ZulActionType type) { + switch (type) { + case ZA_RANGED: zul_attack_ranged(s); break; + case ZA_MAGIC_RANGED: zul_attack_magic_ranged(s); break; + case ZA_MELEE: zul_melee_start(s); break; + case ZA_JAD_RM: + case ZA_JAD_MR: zul_attack_jad(s); break; + case ZA_CLOUDS: zul_spawn_cloud(s); break; + case ZA_SNAKELINGS: zul_spawn_snakeling(s); break; + case ZA_SNAKECLOUD_ALT: + if (s->action_progress % 2 == 0) zul_spawn_snakeling(s); + else zul_spawn_cloud(s); + break; + case ZA_CLOUDSNAKE_ALT: + if (s->action_progress % 2 == 0) zul_spawn_cloud(s); + else zul_spawn_snakeling(s); + break; + case ZA_END: break; + } +} + +static int zul_action_interval(ZulActionType type) { + switch (type) { + case ZA_RANGED: + case ZA_MAGIC_RANGED: + case ZA_JAD_RM: + case ZA_JAD_MR: return MONSTER_DATABASE[MON_ZULRAH_GREEN].attack_speed; + case ZA_MELEE: return ZUL_MELEE_INTERVAL; + case ZA_CLOUDS: + case ZA_SNAKELINGS: + case ZA_SNAKECLOUD_ALT: + case ZA_CLOUDSNAKE_ALT: return ZUL_SPAWN_INTERVAL; + default: return 1; + } +} + +static int zul_action_is_attack(ZulActionType type) { + return type == ZA_RANGED || type == ZA_MAGIC_RANGED || type == ZA_MELEE || + type == ZA_JAD_RM || type == ZA_JAD_MR; +} + +static int zul_phase_action_ticks(const ZulRotationPhase* phase) { + int total = 0; + for (int i = 0; i < ZUL_MAX_PHASE_ACTIONS; i++) { + if (phase->actions[i].type == ZA_END) break; + total += phase->actions[i].count * zul_action_interval((ZulActionType)phase->actions[i].type); + } + return total; +} + +static void zul_enter_phase(ZulrahState* s) { + const ZulRotationPhase* phase = zul_current_phase(s); + s->current_form = (ZulrahForm)phase->form; + s->zulrah.npc_def_id = zul_form_npc_id(s->current_form); + s->zulrah.x = ZUL_POSITIONS[phase->position][0]; + s->zulrah.y = ZUL_POSITIONS[phase->position][1]; + s->zulrah_visible = 1; + s->zulrah.npc_visible = 1; + s->is_diving = 0; + + int is_initial = (s->phase_index == 0 && s->tick <= 1); + int surface_ticks = is_initial ? ZUL_SURFACE_TICKS_INITIAL : ZUL_SURFACE_TICKS; + zul_set_npc_anim_event( + s, + is_initial ? ZULRAH_ANIM_SURFACE : ZULRAH_ANIM_RISE, + surface_ticks); + s->surface_timer = surface_ticks; + + s->phase_timer = phase->phase_ticks; + + int action_ticks = zul_phase_action_ticks(phase); + int available = phase->phase_ticks - surface_ticks - ZUL_DIVE_PHASE_TICKS - action_ticks; + int initial_delay = (available > 1) ? available : 1; + + s->action_index = 0; + s->action_progress = 0; + s->action_timer = initial_delay; + + ZulActionType first_type = (ZulActionType)phase->actions[0].type; + if (first_type == ZA_JAD_RM) s->jad_is_magic_next = 0; + else if (first_type == ZA_JAD_MR) s->jad_is_magic_next = 1; + + s->zulrah_attacking = 0; +} + +static void zul_enter_dive(ZulrahState* s) { + s->is_diving = 1; + s->zulrah_attacking = 0; + zul_set_npc_anim_event(s, ZULRAH_ANIM_DIVE, ZUL_DIVE_ANIM_TICKS); +} + +static void zul_next_phase(ZulrahState* s) { + int rot_len = ZUL_ROT_LENGTHS[s->rotation_index]; + s->phase_index++; + + if (s->phase_index >= rot_len) { + s->rotation_index = encounter_rand_int(&s->rng_state, ZUL_NUM_ROTATIONS); + s->phase_index = 1; + } + + zul_enter_phase(s); +} + +static void zul_phase_tick(ZulrahState* s) { + if (!s->zulrah_visible) return; + + if (s->phase_timer > 0) s->phase_timer--; + + if (s->phase_timer <= 0) { + s->zulrah_visible = 0; + s->zulrah.npc_visible = 0; + zul_next_phase(s); + return; + } + + if (s->phase_timer <= ZUL_DIVE_PHASE_TICKS && !s->is_diving) { + zul_enter_dive(s); + } + if (s->is_diving) return; + + if (s->surface_timer > 0) { + s->surface_timer--; + return; + } + + const ZulRotationPhase* phase = zul_current_phase(s); + const ZulAction* act = &phase->actions[s->action_index]; + + if (act->type == ZA_END) { + s->zulrah_attacking = 0; + return; + } + + s->action_timer--; + if (s->action_timer > 0) return; + + zul_fire_action(s, (ZulActionType)act->type); + s->action_progress++; + + if (s->action_progress >= act->count) { + s->action_index++; + s->action_progress = 0; + + const ZulAction* next = &phase->actions[s->action_index]; + if (next->type == ZA_END) { + s->zulrah_attacking = 0; + return; + } + + s->zulrah_attacking = zul_action_is_attack((ZulActionType)next->type); + + if (next->type == ZA_JAD_RM) s->jad_is_magic_next = 0; + else if (next->type == ZA_JAD_MR) s->jad_is_magic_next = 1; + + s->action_timer = zul_action_interval((ZulActionType)next->type); + } else { + s->action_timer = zul_action_interval((ZulActionType)act->type); + } +} + +static void zul_process_prayer(ZulrahState* s, int overhead_action, int offensive_action) { + if (encounter_apply_overhead_action(&s->player.prayer, overhead_action)) { + s->player.prayer_just_activated = 1; + } + OffensivePrayer prev_offensive = s->player.offensive_prayer; + if (encounter_apply_offensive_action(&s->player.offensive_prayer, offensive_action)) { + s->player.offensive_prayer_just_activated = 1; + } + if (s->player.offensive_prayer != prev_offensive) + zul_refresh_cached_loadout_stats(s); +} + +static void zul_process_food(ZulrahState* s, int a) { + if (a == 0) return; + FoodType type; + switch (a) { + case 1: + type = FOOD_SHARK; + break; + case 2: + type = FOOD_KARAMBWAN; + break; + default: + fprintf(stderr, "unsupported Zulrah food action: %d\n", a); + abort(); + } + + osrs_player_eat_food_type(&s->player, type); +} + +static void zul_process_potion(ZulrahState* s, int a) { + if (a == 0) return; + if (a == 1) { + if (s->player.prayer_pot_doses <= 0) return; + DrinkResult r = osrs_drink_potion(POTION_PRAYER_RESTORE, s->player.current_prayer, + s->player.base_prayer, s->player.potion_timer); + if (!r.consumed) return; + s->player.prayer_pot_doses--; + s->player.potion_timer = 3; + s->player.current_prayer += r.prayer_restored; + if (s->player.current_prayer > s->player.base_prayer) + s->player.current_prayer = s->player.base_prayer; + } else if (a == 2) { + if (s->player.antivenom_doses <= 0) return; + DrinkResult r = osrs_drink_potion(POTION_ANTIVENOM_PLUS, 0, 0, s->player.potion_timer); + if (!r.consumed) return; + s->player.antivenom_doses--; + s->player.potion_timer = 3; + s->venom_counter = 0; + s->venom_timer = 0; + s->antivenom_timer = r.antivenom_ticks; + } +} + +static void zul_process_gear(ZulrahState* s, int atk) { + if (atk == ZUL_ATK_MAGE && s->player_gear != ZUL_GEAR_MAGE) { + s->player_gear = ZUL_GEAR_MAGE; + encounter_apply_loadout(&s->player, ZUL_MAGE_LOADOUT[s->gear_tier], GEAR_MAGE); + } else if (atk == ZUL_ATK_RANGE && s->player_gear != ZUL_GEAR_RANGE) { + s->player_gear = ZUL_GEAR_RANGE; + encounter_apply_loadout(&s->player, ZUL_RANGE_LOADOUT[s->gear_tier], GEAR_RANGED); + } +} + +static FightStyle zul_default_fight_style_for_style(AttackStyle style) { + if (style == ATTACK_STYLE_MAGIC) return FIGHT_STYLE_ACCURATE; + if (style == ATTACK_STYLE_RANGED) return FIGHT_STYLE_RAPID; + return FIGHT_STYLE_ACCURATE; +} + +static void zul_sync_human_gear_style(ZulrahState* s) { + AttackStyle style = zul_player_equipped_attack_style(s); + s->player_gear = (style == ATTACK_STYLE_MAGIC) ? ZUL_GEAR_MAGE : ZUL_GEAR_RANGE; +} + +static void zul_apply_human_player_commands(ZulrahState* s) { + int did_change_stats = 0; + for (int i = 0; i < s->human_command_count; i++) { + const HumanCommand* cmd = &s->human_commands[i]; + if (cmd->kind == HUMAN_COMMAND_EQUIP_INVENTORY_ITEM) { + if (cmd->gear_slot >= 0 && cmd->gear_slot < NUM_GEAR_SLOTS && + cmd->item_db_idx >= 0 && cmd->item_db_idx < NUM_ITEMS) { + int changed = slot_equip_item(&s->player, cmd->gear_slot, (uint8_t)cmd->item_db_idx); + if (changed) { + did_change_stats = 1; + if (cmd->gear_slot == GEAR_SLOT_WEAPON) { + AttackStyle style = zul_player_equipped_attack_style(s); + s->player.fight_style = zul_default_fight_style_for_style(style); + zul_sync_human_gear_style(s); + } + } + } + } else if (cmd->kind == HUMAN_COMMAND_FIGHT_STYLE) { + if (cmd->fight_style >= FIGHT_STYLE_ACCURATE && + cmd->fight_style <= FIGHT_STYLE_DEFENSIVE_AUTOCAST) { + s->player.fight_style = (FightStyle)cmd->fight_style; + did_change_stats = 1; + } + } + } + if (did_change_stats) + zul_refresh_human_loadout_stats(s); +} + +static void zul_write_obs(EncounterState* state, EncounterContext* context, float* obs) { + (void)context; + ZulrahState* s = (ZulrahState*)state; + memset(obs, 0, ZUL_NUM_OBS * sizeof(float)); + int i = 0; + + obs[i++] = (float)s->player.current_hitpoints / s->player.base_hitpoints; + obs[i++] = (float)s->player.current_prayer / s->player.base_prayer; + obs[i++] = (float)s->player.x / ZUL_ARENA_SIZE; + obs[i++] = (float)s->player.y / ZUL_ARENA_SIZE; + obs[i++] = (float)s->player.attack_timer / 5.0f; + obs[i++] = (float)s->player.food_count / ZUL_PLAYER_FOOD; + obs[i++] = (float)s->player.karambwan_count / ZUL_PLAYER_KARAMBWAN; + obs[i++] = (float)s->player.prayer_pot_doses / ZUL_PLAYER_RESTORE_DOSES; + obs[i++] = (float)s->player.food_timer / 3.0f; + obs[i++] = (float)s->player.potion_timer / 3.0f; + obs[i++] = (s->player_gear == ZUL_GEAR_MAGE) ? 1.0f : 0.0f; + obs[i++] = (s->player_gear == ZUL_GEAR_RANGE) ? 1.0f : 0.0f; + obs[i++] = (s->player.prayer == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; + obs[i++] = (s->player.prayer == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; + obs[i++] = (s->player.prayer == PRAYER_PROTECT_MELEE) ? 1.0f : 0.0f; + obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_PIETY) ? 1.0f : 0.0f; + obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_RIGOUR) ? 1.0f : 0.0f; + obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_AUGURY) ? 1.0f : 0.0f; + obs[i++] = (float)s->player_stunned_ticks / ZUL_MELEE_STUN_TICKS; + + obs[i++] = (float)s->zulrah.current_hitpoints / MONSTER_DATABASE[MON_ZULRAH_GREEN].hp; + obs[i++] = (float)(s->zulrah.x - s->player.x) / ZUL_ARENA_SIZE; + obs[i++] = (float)(s->zulrah.y - s->player.y) / ZUL_ARENA_SIZE; + obs[i++] = (s->current_form == ZUL_FORM_GREEN) ? 1.0f : 0.0f; + obs[i++] = (s->current_form == ZUL_FORM_RED) ? 1.0f : 0.0f; + obs[i++] = (s->current_form == ZUL_FORM_BLUE) ? 1.0f : 0.0f; + obs[i++] = s->zulrah_visible ? 1.0f : 0.0f; + obs[i++] = s->is_diving ? 1.0f : 0.0f; + obs[i++] = s->zulrah_attacking ? 1.0f : 0.0f; + obs[i++] = (float)s->action_timer / MONSTER_DATABASE[MON_ZULRAH_GREEN].attack_speed; + obs[i++] = (float)encounter_dist_to_npc(s->player.x, s->player.y, s->zulrah.x, s->zulrah.y, ZUL_NPC_SIZE) / ZUL_ARENA_SIZE; + obs[i++] = (float)s->rotation_index / (ZUL_NUM_ROTATIONS - 1); + obs[i++] = (float)s->phase_index / 12.0f; + obs[i++] = (s->melee_pending) ? 1.0f : 0.0f; + + obs[i++] = (s->venom_counter > 0) ? 1.0f : 0.0f; + obs[i++] = (float)s->venom_timer / ZUL_VENOM_INTERVAL; + + for (int c = 0; c < ZUL_MAX_CLOUDS; c++) { + obs[i++] = s->clouds[c].active ? (float)(s->clouds[c].x - s->player.x) / ZUL_ARENA_SIZE : 0.0f; + obs[i++] = s->clouds[c].active ? (float)(s->clouds[c].y - s->player.y) / ZUL_ARENA_SIZE : 0.0f; + obs[i++] = s->clouds[c].active ? 1.0f : 0.0f; + } + + for (int n = 0; n < ZUL_MAX_SNAKELINGS; n++) { + ZulrahSnakeling* sn = &s->snakelings[n]; + obs[i++] = sn->active ? (float)(sn->entity.x - s->player.x) / ZUL_ARENA_SIZE : 0.0f; + obs[i++] = sn->active ? (float)(sn->entity.y - s->player.y) / ZUL_ARENA_SIZE : 0.0f; + obs[i++] = sn->active ? 1.0f : 0.0f; + obs[i++] = sn->active ? (float)chebyshev_distance( + s->player.x, s->player.y, sn->entity.x, sn->entity.y) / ZUL_ARENA_SIZE : 0.0f; + } + + obs[i++] = (float)s->tick / ZUL_MAX_TICKS; + obs[i++] = s->damage_dealt_this_tick / 50.0f; + obs[i++] = s->damage_received_this_tick / 50.0f; + obs[i++] = s->total_damage_dealt / MONSTER_DATABASE[MON_ZULRAH_GREEN].hp; + + obs[i++] = (float)s->player.special_energy / 100.0f; + obs[i++] = (s->antivenom_timer > 0) ? 1.0f : 0.0f; + obs[i++] = (float)s->antivenom_timer / ZUL_ANTIVENOM_DURATION; + obs[i++] = (float)s->gear_tier / (ZUL_NUM_GEAR_TIERS - 1); + + const ZulRotationPhase* phase = zul_current_phase(s); + if (phase->stand < ZUL_NUM_STAND_LOCATIONS) { + obs[i++] = (float)(ZUL_STAND_COORDS[phase->stand][0] - s->player.x) / ZUL_ARENA_SIZE; + obs[i++] = (float)(ZUL_STAND_COORDS[phase->stand][1] - s->player.y) / ZUL_ARENA_SIZE; + } else { + obs[i++] = 0.0f; obs[i++] = 0.0f; + } + if (phase->stall < ZUL_NUM_STAND_LOCATIONS) { + obs[i++] = (float)(ZUL_STAND_COORDS[phase->stall][0] - s->player.x) / ZUL_ARENA_SIZE; + obs[i++] = (float)(ZUL_STAND_COORDS[phase->stall][1] - s->player.y) / ZUL_ARENA_SIZE; + } else { + obs[i++] = 0.0f; obs[i++] = 0.0f; + } + + int active_dx = 0; + int active_dy = 0; + int escape_dx = 0; + int escape_dy = 0; + zul_nearest_active_cloud_features( + s, &active_dx, &active_dy, &escape_dx, &escape_dy); + int pending_dx = 0; + int pending_dy = 0; + int pending_delay = 0; + zul_nearest_pending_cloud_features( + s, &pending_dx, &pending_dy, &pending_delay); + obs[i++] = (escape_dx != 0 || escape_dy != 0) ? 1.0f : 0.0f; + obs[i++] = (float)active_dx / (float)ZUL_ARENA_SIZE; + obs[i++] = (float)active_dy / (float)ZUL_ARENA_SIZE; + obs[i++] = (float)escape_dx / (float)ZUL_ARENA_SIZE; + obs[i++] = (float)escape_dy / (float)ZUL_ARENA_SIZE; + obs[i++] = (float)zul_active_cloud_count(s) / (float)ZUL_MAX_CLOUDS; + obs[i++] = (float)zul_pending_cloud_count(s) / (float)ZUL_MAX_PENDING_CLOUDS; + obs[i++] = (float)pending_dx / (float)ZUL_ARENA_SIZE; + obs[i++] = (float)pending_dy / (float)ZUL_ARENA_SIZE; + obs[i++] = (float)pending_delay / (float)ZUL_CLOUD_FLIGHT_2; + obs[i++] = zul_active_cloud_overlaps_safe(s, phase->stand) ? 1.0f : 0.0f; + obs[i++] = zul_active_cloud_overlaps_safe(s, phase->stall) ? 1.0f : 0.0f; + obs[i++] = zul_pending_cloud_overlaps_safe(s, phase->stand) ? 1.0f : 0.0f; + obs[i++] = zul_pending_cloud_overlaps_safe(s, phase->stall) ? 1.0f : 0.0f; + for (int m = 0; m < ZUL_MOVE_DIM; m++) + obs[i++] = zul_move_action_cloud_unsafe(s, m) ? 1.0f : 0.0f; + + if (i != ZUL_NUM_OBS) { + fprintf(stderr, "zulrah obs size mismatch: wrote %d expected %d\n", i, ZUL_NUM_OBS); + abort(); + } +} + +static void zul_write_mask(EncounterState* state, EncounterContext* context, float* mask) { + (void)context; + ZulrahState* s = (ZulrahState*)state; + for (int i = 0; i < ZUL_ACTION_MASK_SIZE; i++) mask[i] = 1.0f; + int off = 0; + + for (int m = 0; m < ZUL_MOVE_DIM; m++) { + if (m > 0) { + if (s->player_stunned_ticks > 0) { mask[off] = 0.0f; } + else { + int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[m]; + int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[m]; + if (!zul_on_platform(s, nx, ny)) mask[off] = 0.0f; + } + } + off++; + } + for (int a = 0; a < ZUL_ATTACK_DIM; a++) { + const EncounterLoadoutStats* attack_stats = + a == ZUL_ATK_MAGE ? &s->mage_stats : + (a == ZUL_ATK_RANGE ? &s->range_stats : NULL); + if (a > 0 && (!s->zulrah_visible || s->is_diving || + s->player.attack_timer > 0 || s->player_stunned_ticks > 0 || + !zul_player_can_attack_zulrah(s, attack_stats))) + mask[off] = 0.0f; + off++; + } + for (int p = 0; p < ZUL_PRAYER_DIM; p++) { + if (p == ENCOUNTER_OVERHEAD_OFF && s->player.prayer == PRAYER_NONE) + mask[off] = 0.0f; + if (p >= ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE && s->player.current_prayer <= 0) + mask[off] = 0.0f; + off++; + } + off++; + if (!osrs_player_can_eat_food_type(&s->player, FOOD_SHARK) || + osrs_player_food_wasted_hp(&s->player, FOOD_SHARK) > 0) + mask[off] = 0.0f; + off++; + if (!osrs_player_can_eat_food_type(&s->player, FOOD_KARAMBWAN) || + osrs_player_food_wasted_hp(&s->player, FOOD_KARAMBWAN) > 0) + mask[off] = 0.0f; + off++; + off++; + if (s->player.prayer_pot_doses <= 0 || s->player.potion_timer > 0 || + s->player.current_prayer >= s->player.base_prayer) + mask[off] = 0.0f; + off++; + if (s->player.antivenom_doses <= 0 || s->player.potion_timer > 0 || + s->antivenom_timer > 0) + mask[off] = 0.0f; + off++; + off++; + { + int weapon = s->player.equipped[GEAR_SLOT_WEAPON]; + int spec_cost = osrs_spec_cost(weapon); + if (spec_cost <= 0) + mask[off] = 0.0f; + } + off++; + for (int o = 0; o < ZUL_OFFENSIVE_DIM; o++) { + if (o == ENCOUNTER_OFFENSIVE_OFF && + s->player.offensive_prayer == OFFENSIVE_PRAYER_NONE) + mask[off] = 0.0f; + if (o >= ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY && s->player.current_prayer <= 0) + mask[off] = 0.0f; + off++; + } +} + +static float zul_compute_reward(ZulrahState* s) { + const ZulrahRewardConfig* cfg = &s->reward_config; + float r = 0.0f; + if (s->boss_killed_this_tick) { + r += cfg->win; + } + if (s->player_lost_this_tick) { + r -= cfg->loss_penalty; + } else { + if (s->damage_dealt_this_tick > 0.0f) { + float norm_dmg = s->damage_dealt_this_tick / 50.0f; + r += cfg->damage_dealt * norm_dmg; + int correct = (s->current_form == ZUL_FORM_BLUE && + s->player.attack_style_this_tick == ATTACK_STYLE_RANGED) || + ((s->current_form == ZUL_FORM_GREEN || + s->current_form == ZUL_FORM_RED) && + s->player.attack_style_this_tick == ATTACK_STYLE_MAGIC); + if (correct) r += cfg->correct_style * norm_dmg; + } + + if (s->damage_received_this_tick > 0.0f) + r -= cfg->damage_received_penalty * (s->damage_received_this_tick / 50.0f); + } + + if (s->cloud_occupancy_this_tick > 0) + r -= cfg->cloud_occupancy_penalty; + + return r; +} + +static ZulrahRewardConfig zul_default_reward_config(void) { + return (ZulrahRewardConfig){ + .win = ZUL_REWARD_WIN_DEFAULT, + .loss_penalty = ZUL_REWARD_LOSS_PENALTY_DEFAULT, + .damage_dealt = ZUL_REWARD_DAMAGE_DEALT_DEFAULT, + .correct_style = ZUL_REWARD_CORRECT_STYLE_DEFAULT, + .damage_received_penalty = ZUL_REWARD_DAMAGE_RECEIVED_PENALTY_DEFAULT, + .cloud_occupancy_penalty = ZUL_REWARD_CLOUD_OCCUPANCY_PENALTY_DEFAULT, + }; +} + +static EncounterState* zul_create(void) { + ZulrahState* s = (ZulrahState*)calloc(1, sizeof(ZulrahState)); + if (!s) abort(); + s->reward_config = zul_default_reward_config(); + return (EncounterState*)s; +} + +static void zul_destroy(EncounterState* state) { free(state); } + +static float zul_score_speed_bonus_for_duration(int duration) { + if (duration < 1) duration = 1; + if (duration > ZUL_MAX_TICKS) duration = ZUL_MAX_TICKS; + return (1.0f - (float)duration / (float)ZUL_MAX_TICKS) * + ZUL_SCORE_SPEED_BONUS_DEFAULT; +} + +static float zul_current_kill_progress(const ZulrahState* s) { + int max_hp = MONSTER_DATABASE[MON_ZULRAH_GREEN].hp; + if (max_hp <= 0 || s->zulrah.current_hitpoints <= 0) + return 0.0f; + return 1.0f - (float)s->zulrah.current_hitpoints / (float)max_hp; +} + +static void zul_clear_active_kill(ZulrahState* s) { + memset(s->clouds, 0, sizeof(s->clouds)); + memset(s->pending_clouds, 0, sizeof(s->pending_clouds)); + memset(s->snakelings, 0, sizeof(s->snakelings)); + s->attack_event_count = 0; + s->cloud_event_count = 0; + s->melee_pending = 0; + s->melee_stare_timer = 0; + s->phase_timer = 0; + s->surface_timer = 0; + s->is_diving = 0; + s->zulrah_anim_until_tick = 0; + s->zulrah_anim_event_tick = -1; + s->zulrah_death_ticks = 0; + s->zulrah_attacking = 0; + s->action_index = 0; + s->action_progress = 0; + s->action_timer = 0; + s->zulrah_visible = 0; + s->zulrah.current_hitpoints = 0; + s->magic_def_drain = 0; + osrs_interaction_init(&s->interaction); +} + +static void zul_start_active_kill(ZulrahState* s) { + zul_clear_active_kill(s); + s->zulrah.entity_type = ENTITY_NPC; + s->zulrah.npc_def_id = zul_form_npc_id(ZUL_FORM_GREEN); + s->zulrah.npc_size = ZUL_NPC_SIZE; + s->zulrah.npc_anim_id = -1; + s->zulrah.base_hitpoints = MONSTER_DATABASE[MON_ZULRAH_GREEN].hp; + s->zulrah.current_hitpoints = MONSTER_DATABASE[MON_ZULRAH_GREEN].hp; + s->zulrah_npc_instance_id = zul_next_npc_instance_id(s); + s->rotation_index = encounter_rand_int(&s->rng_state, ZUL_NUM_ROTATIONS); + s->phase_index = 0; + s->kill_start_tick = s->tick; + s->respawn_timer = 0; + zul_enter_phase(s); +} + +static void zul_record_boss_kill(ZulrahState* s) { + s->boss_killed_this_tick = 1; + s->kills_this_episode++; + int duration = s->tick - s->kill_start_tick; + s->score_speed_bonus_sum += zul_score_speed_bonus_for_duration(duration); + if (!osrs_npc_death_linger_start( + s->zulrah.current_hitpoints, + s->zulrah_visible, + &s->zulrah_death_ticks, + ZUL_DEATH_ANIM_TICKS)) { + fprintf(stderr, "zulrah death linger did not start for boss kill\n"); + abort(); + } + s->zulrah_visible = 1; + s->zulrah.npc_visible = 1; + s->zulrah_attacking = 0; + s->is_diving = 0; + s->surface_timer = 0; + s->phase_timer = 0; + s->action_timer = 0; + s->melee_pending = 0; + s->melee_stare_timer = 0; + zul_set_npc_anim_event(s, ZULRAH_ANIM_DEATH, ZUL_DEATH_ANIM_TICKS); +} + +static void zul_finish_boss_death(ZulrahState* s) { + if (s->episode_mode == ZUL_EPISODE_TRIP) { + zul_clear_active_kill(s); + s->respawn_timer = ZUL_TRIP_RESPAWN_DELAY_TICKS; + return; + } + s->episode_over = 1; + s->winner = ZUL_OUTCOME_PLAYER_WON; +} + +static void zul_record_player_loss(ZulrahState* s) { + s->player_lost_this_tick = 1; + s->episode_over = 1; + s->winner = ZUL_OUTCOME_PLAYER_DIED; +} + +static void zul_record_episode_timeout(ZulrahState* s) { + if (s->episode_mode == ZUL_EPISODE_TRIP && s->kills_this_episode > 0) { + s->episode_over = 1; + s->winner = ZUL_OUTCOME_PLAYER_WON; + } else { + zul_record_player_loss(s); + } +} + +static void zul_reset(EncounterState* state, EncounterContext* context, uint32_t seed) { + (void)context; + ZulrahState* s = (ZulrahState*)state; + Log saved_log = s->log; + void* saved_cmap = s->collision_map; + int saved_wx = s->world_offset_x; + int saved_wy = s->world_offset_y; + int saved_tier = s->gear_tier; + int saved_fixed_tier = s->gear_tier_fixed; + int saved_tier_mode = s->gear_tier_mode; + int saved_episode_mode = s->episode_mode; + float saved_tier_weights[ZUL_NUM_GEAR_TIERS]; + memcpy(saved_tier_weights, s->gear_tier_weights, sizeof(saved_tier_weights)); + ZulrahRewardConfig saved_reward_config = s->reward_config; + uint32_t saved_rng = s->rng_state; + memset(s, 0, sizeof(ZulrahState)); + s->log = saved_log; + s->collision_map = saved_cmap; + s->world_offset_x = saved_wx; + s->world_offset_y = saved_wy; + s->gear_tier = saved_tier; + s->gear_tier_fixed = saved_fixed_tier; + s->gear_tier_mode = saved_tier_mode; + s->episode_mode = saved_episode_mode; + memcpy(s->gear_tier_weights, saved_tier_weights, sizeof(saved_tier_weights)); + s->reward_config = saved_reward_config; + s->rng_state = encounter_resolve_seed(saved_rng, seed); + s->gear_tier = zul_sample_gear_tier(s); + + s->player.entity_type = ENTITY_PLAYER; + memset(s->player.equipped, ITEM_NONE, NUM_GEAR_SLOTS); + encounter_init_maxed_player_combat_stats(&s->player, ZUL_PLAYER_PRAYER); + s->player.x = ZUL_PLAYER_START_X; + s->player.y = ZUL_PLAYER_START_Y; + s->player.food_count = ZUL_PLAYER_FOOD; + s->player.karambwan_count = ZUL_PLAYER_KARAMBWAN; + s->player.prayer_pot_doses = ZUL_PLAYER_RESTORE_DOSES; + s->player.special_energy = 100; + s->player.antivenom_doses = ZUL_ANTIVENOM_DOSES; + osrs_item_effect_state_init(&s->player.item_effect_state); + if (s->gear_tier == 2) { + s->player.saturated_heart_count = 1; + encounter_apply_saturated_heart_boost(&s->player); + } + if (s->gear_tier >= 1) { + s->thrall_active = 1; + s->thrall_duration_remaining = ZUL_THRALL_DURATION; + s->thrall_attack_timer = ZUL_THRALL_SPEED; + } + osrs_interaction_init(&s->interaction); + s->player.spec_armed = 0; + s->player_gear = ZUL_GEAR_MAGE; + encounter_apply_loadout(&s->player, ZUL_MAGE_LOADOUT[s->gear_tier], GEAR_MAGE); + zul_populate_player_inventory(&s->player, s->gear_tier); + OffensivePrayer mage_prayer = (s->gear_tier >= 1) ? OFFENSIVE_PRAYER_AUGURY : OFFENSIVE_PRAYER_NONE; + OffensivePrayer range_prayer = (s->gear_tier >= 1) ? OFFENSIVE_PRAYER_RIGOUR : OFFENSIVE_PRAYER_NONE; + s->player.offensive_prayer = mage_prayer; + encounter_compute_loadout_stats(ZUL_MAGE_LOADOUT[s->gear_tier], ATTACK_STYLE_MAGIC, + mage_prayer, s->player.current_magic, FIGHT_STYLE_ACCURATE, 30, &s->mage_stats); + encounter_compute_loadout_stats(ZUL_RANGE_LOADOUT[s->gear_tier], ATTACK_STYLE_RANGED, + range_prayer, s->player.current_ranged, FIGHT_STYLE_RAPID, 0, &s->range_stats); + zul_start_active_kill(s); +} + +static void zul_step(EncounterState* state, EncounterContext* context, const int* actions) { + (void)context; + ZulrahState* s = (ZulrahState*)state; + if (s->episode_over) return; + + s->reward = 0.0f; + s->damage_dealt_this_tick = 0.0f; + s->damage_received_this_tick = 0.0f; + s->cloud_occupancy_this_tick = 0; + s->boss_killed_this_tick = 0; + s->player_lost_this_tick = 0; + s->player.just_attacked = 0; + s->player.hit_landed_this_tick = 0; + s->player.attack_style_this_tick = ATTACK_STYLE_NONE; + s->player.used_special_this_tick = 0; + s->player.ate_food_this_tick = 0; + s->player.ate_karambwan_this_tick = 0; + s->zulrah.hit_landed_this_tick = 0; + s->player_attacked_this_tick = 0; + s->player_attack_dmg = 0; + s->player_attack_style_id = ATTACK_STYLE_NONE; + s->player_attack_is_special = 0; + s->player_attack_timing = (EncounterProjectileTiming){0}; + s->player_moved_this_tick = 0; + s->player_chased_target_this_tick = 0; + s->attack_event_count = 0; + s->cloud_event_count = 0; + s->tick++; + + if (s->zulrah_death_ticks > 0) { + if (osrs_npc_death_linger_tick(&s->zulrah_death_ticks)) { + s->zulrah_visible = 0; + s->zulrah.npc_visible = 0; + zul_finish_boss_death(s); + } + goto finish; + } + + zul_update_npc_anim_lifetime(s); + + if (s->player.attack_timer > 0) s->player.attack_timer--; + if (s->player.food_timer > 0) s->player.food_timer--; + if (s->player.karambwan_timer > 0) s->player.karambwan_timer--; + if (s->player.potion_timer > 0) s->player.potion_timer--; + if (s->player_stunned_ticks > 0) s->player_stunned_ticks--; + int stats_changed = encounter_tick_saturated_heart(&s->player); + if (s->tick > 0 && s->tick % 60 == 0) + stats_changed |= encounter_decay_player_combat_stats_toward_base(&s->player); + if (stats_changed) + zul_refresh_cached_loadout_stats(s); + + if (s->melee_pending) { + s->melee_stare_timer--; + if (s->melee_stare_timer <= 0) zul_melee_hit(s); + } + + zul_process_prayer(s, actions[ZUL_HEAD_PRAYER], actions[ZUL_HEAD_OFFENSIVE]); + + if (s->human_command_mode) + zul_apply_human_player_commands(s); + + if (actions[ZUL_HEAD_SPEC] == 1) { + osrs_spec_toggle(&s->player.spec_armed); + } + + if (actions[ZUL_HEAD_FOOD] > 0) + osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EAT); + if (actions[ZUL_HEAD_POTION] > 0) + osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_DRINK); + zul_process_food(s, actions[ZUL_HEAD_FOOD]); + zul_process_potion(s, actions[ZUL_HEAD_POTION]); + + int atk_action = actions[ZUL_HEAD_ATTACK]; + if (!s->human_command_mode) { + if ((atk_action == ZUL_ATK_MAGE && s->player_gear != ZUL_GEAR_MAGE) || + (atk_action == ZUL_ATK_RANGE && s->player_gear != ZUL_GEAR_RANGE)) { + osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EQUIP); + } + zul_process_gear(s, atk_action); + } + + int has_new_target = 0; + if (atk_action == ZUL_ATK_MAGE || atk_action == ZUL_ATK_RANGE) { + has_new_target = 1; + } + + OsrsPlayerMoveKind move_kind = OSRS_PLAYER_MOVE_NONE; + if (s->player_dest_explicit) { + s->player_dest_explicit = 0; + move_kind = OSRS_PLAYER_MOVE_DESTINATION; + } else { + int m = actions[ZUL_HEAD_MOVE]; + if (m > 0 && m < ZUL_MOVE_DIM) { + s->player_dest_x = s->player.x + ENCOUNTER_MOVE_TARGET_DX[m]; + s->player_dest_y = s->player.y + ENCOUNTER_MOVE_TARGET_DY[m]; + move_kind = OSRS_PLAYER_MOVE_DESTINATION; + } else { + s->player_dest_x = -1; + s->player_dest_y = -1; + } + } + + OsrsPlayerStepResult step_result = osrs_encounter_player_step(&(OsrsPlayerStepInput){ + .player = &s->player, + .interaction = &s->interaction, + .target_lookup = zul_lookup_player_attack_target, + .target_ctx = s, + .has_new_target = has_new_target, + .new_target_slot = 0, + .move_kind = move_kind, + .target_move_policy = OSRS_PLAYER_TARGET_MOVE_EXPLICIT_FIRST, + .dest_x = &s->player_dest_x, + .dest_y = &s->player_dest_y, + .blocked_ticks = s->player_stunned_ticks, + .arena = { + .collision_map = (const CollisionMap*)s->collision_map, + .world_offset_x = s->world_offset_x, + .world_offset_y = s->world_offset_y, + .is_walkable = zul_tile_walkable, + .walkable_ctx = s, + .los_query = osrs_los_open_query(), + .arena_base_x = 0, + .arena_base_y = 0, + .arena_w = ZUL_ARENA_SIZE, + .arena_h = ZUL_ARENA_SIZE, + }, + }); + s->player_moved_this_tick = step_result.moved; + s->player_chased_target_this_tick = step_result.chased_target; + + if (s->respawn_timer > 0) { + s->respawn_timer--; + if (s->respawn_timer == 0) + zul_start_active_kill(s); + } + + if (osrs_interaction_active(&s->interaction) && + s->player.attack_timer == 0 && s->zulrah_visible && !s->is_diving && + s->player_stunned_ticks == 0) { + + if (s->player.spec_armed && s->player.special_energy >= osrs_spec_cost(s->player.equipped[GEAR_SLOT_WEAPON])) { + zul_player_spec(s); + osrs_spec_disarm(&s->player.spec_armed); + } else { + if (s->player_gear == ZUL_GEAR_MAGE) zul_player_attack(s, 1); + else zul_player_attack(s, 0); + } + } + + if (s->zulrah_visible && s->zulrah.current_hitpoints <= 0) { + zul_record_boss_kill(s); + goto finish; + } + + zul_pending_cloud_tick(s); + zul_cloud_tick(s); + if (s->player.current_hitpoints <= 0) { + zul_record_player_loss(s); + goto finish; + } + + zul_phase_tick(s); + + zul_snakeling_tick(s); + + zul_thrall_tick(s); + + zul_venom_tick(s); + + OffensivePrayer prev_off_drain = s->player.offensive_prayer; + encounter_drain_all_prayers( + &s->player, encounter_player_prayer_bonus(&s->player)); + if (s->player.offensive_prayer != prev_off_drain) + zul_refresh_cached_loadout_stats(s); + + if (s->player.current_hitpoints <= 0) { + zul_record_player_loss(s); + goto finish; + } + if (s->tick >= ZUL_MAX_TICKS) { + zul_record_episode_timeout(s); + goto finish; + } +finish: + s->reward = zul_compute_reward(s); + s->episode_return += s->reward; +} + +static void zul_heuristic_actions(ZulrahState* s, int* actions) { + for (int i = 0; i < ZUL_NUM_ACTION_HEADS; i++) actions[i] = 0; + + int hp = s->player.current_hitpoints; + + if (s->zulrah_visible && !s->is_diving) { + switch (s->current_form) { + case ZUL_FORM_GREEN: + if (s->player.prayer != PRAYER_PROTECT_RANGED) + actions[ZUL_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; + break; + case ZUL_FORM_BLUE: + if (s->player.prayer != PRAYER_PROTECT_MAGIC) + actions[ZUL_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC; + break; + case ZUL_FORM_RED: + if (s->player.prayer != PRAYER_PROTECT_MELEE) + actions[ZUL_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE; + break; + } + OffensivePrayer target_off = OFFENSIVE_PRAYER_NONE; + if (s->current_form == ZUL_FORM_BLUE) target_off = OFFENSIVE_PRAYER_AUGURY; + else if (s->current_form == ZUL_FORM_GREEN) target_off = OFFENSIVE_PRAYER_RIGOUR; + else if (s->current_form == ZUL_FORM_RED) target_off = OFFENSIVE_PRAYER_PIETY; + if (target_off != OFFENSIVE_PRAYER_NONE && s->player.offensive_prayer != target_off) { + if (target_off == OFFENSIVE_PRAYER_AUGURY) + actions[ZUL_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_AUGURY; + else if (target_off == OFFENSIVE_PRAYER_RIGOUR) + actions[ZUL_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR; + else if (target_off == OFFENSIVE_PRAYER_PIETY) + actions[ZUL_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY; + } + } + + if (s->player.potion_timer <= 0 && s->player.antivenom_doses > 0 && + s->antivenom_timer <= 5) { + actions[ZUL_HEAD_POTION] = 2; + return; + } + + if (hp < 60 && s->player.food_timer <= 0 && s->player.food_count > 0 && + hp <= s->player.base_hitpoints - osrs_food_heal_amount(FOOD_SHARK)) { + actions[ZUL_HEAD_FOOD] = 1; + } + else if (hp < 40 && s->player.food_timer <= 0 && s->player.karambwan_count > 0 && + hp <= s->player.base_hitpoints - osrs_food_heal_amount(FOOD_KARAMBWAN)) { + actions[ZUL_HEAD_FOOD] = 2; + } + + if (s->player.current_prayer < 30 && s->player.potion_timer <= 0 && + s->player.prayer_pot_doses > 0 && s->player.current_prayer < s->player.base_prayer) { + actions[ZUL_HEAD_POTION] = 1; + } + + { + const ZulRotationPhase* phase = zul_current_phase(s); + int stand = phase->stand; + if (stand < ZUL_NUM_STAND_LOCATIONS) { + int tx = ZUL_STAND_COORDS[stand][0]; + int ty = ZUL_STAND_COORDS[stand][1]; + if (tx != s->player.x || ty != s->player.y) { + s->player_dest_x = tx; + s->player_dest_y = ty; + s->player_dest_explicit = 1; + } + } + } + + if (s->zulrah_visible && !s->is_diving) { + if (s->current_form == ZUL_FORM_BLUE) { + actions[ZUL_HEAD_ATTACK] = ZUL_ATK_RANGE; + int spec_cost = osrs_spec_cost(ZUL_RANGE_LOADOUT[s->gear_tier][GEAR_SLOT_WEAPON]); + if (spec_cost > 0 && s->player.special_energy >= spec_cost && !s->player.spec_armed) { + actions[ZUL_HEAD_SPEC] = 1; + } + } else { + actions[ZUL_HEAD_ATTACK] = ZUL_ATK_MAGE; + } + } +} + +static float zul_get_reward(EncounterState* state, EncounterContext* context) { + (void)context; + return ((ZulrahState*)state)->reward; +} +static int zul_is_terminal(EncounterState* state, EncounterContext* context) { + (void)context; + return ((ZulrahState*)state)->episode_over; +} + +static int zul_get_entity_count(EncounterState* state, EncounterContext* context) { + (void)context; + ZulrahState* s = (ZulrahState*)state; + int n = 2; + for (int i = 0; i < ZUL_MAX_SNAKELINGS; i++) + if (s->snakelings[i].active) n++; + return n; +} +static void* zul_get_entity(EncounterState* state, EncounterContext* context, int index) { + (void)context; + ZulrahState* s = (ZulrahState*)state; + if (index == 0) return &s->player; + if (index == 1) return &s->zulrah; + int si = 0; + for (int i = 0; i < ZUL_MAX_SNAKELINGS; i++) { + if (s->snakelings[i].active) { + if (si + 2 == index) return &s->snakelings[i].entity; + si++; + } + } + fprintf(stderr, "zulrah get_entity invalid index %d\n", index); + abort(); +} + +static void zul_fill_render_entities( + EncounterState* state, + EncounterContext* context, + RenderEntity* out, + int max_entities, + int* count +) { + (void)context; + ZulrahState* s = (ZulrahState*)state; + int n = 0; + if (n < max_entities) osrs_render_entity_from_player_entity(&s->player, &out[n++]); + if (n < max_entities) { + osrs_render_entity_from_npc_player( + &s->zulrah, &out[n], 0, s->zulrah_npc_instance_id); + out[n].dest_x = s->zulrah.x + ZUL_NPC_SIZE / 2; + out[n].dest_y = s->zulrah.y + ZUL_NPC_SIZE / 2; + if (s->surface_timer > 0) + out[n].render_movement_kind = RENDER_MOVEMENT_TELEPORT; + if (s->is_diving && s->tick >= s->zulrah_anim_until_tick) + out[n].npc_visible = 0; + osrs_render_entity_suppress_pose_anims( + &out[n], ZULRAH_ANIM_IDLE, ZULRAH_ANIM_IDLE); + if (!zul_should_emit_npc_anim_event(s)) { + out[n].npc_anim_id = -1; + } + n++; + } + for (int i = 0; i < ZUL_MAX_SNAKELINGS && n < max_entities; i++) { + if (s->snakelings[i].active) { + osrs_render_entity_from_npc_player( + &s->snakelings[i].entity, + &out[n], + i + 1, + s->snakelings[i].npc_instance_id); + osrs_render_entity_suppress_pose_anims( + &out[n], SNAKELING_ANIM_IDLE, SNAKELING_ANIM_WALK); + n++; + int adx = abs(s->snakelings[i].entity.x - s->player.x); + int ady = abs(s->snakelings[i].entity.y - s->player.y); + if (adx <= 1 && ady <= 1) + out[n - 1].attack_target_entity_idx = 0; + } + } + if ((s->player_attacked_this_tick || s->player_chased_target_this_tick || + (osrs_interaction_active(&s->interaction) && !s->player_moved_this_tick)) && + s->zulrah_visible && !s->is_diving) { + encounter_resolve_attack_target(out, n, 0); + } + int attack_anim_active = s->zulrah.npc_anim_id >= 0 && + s->zulrah.npc_anim_id != ZULRAH_ANIM_SURFACE && + s->zulrah.npc_anim_id != ZULRAH_ANIM_RISE && + s->zulrah.npc_anim_id != ZULRAH_ANIM_DIVE && + s->zulrah.npc_anim_id != ZULRAH_ANIM_DEATH && + s->tick < s->zulrah_anim_until_tick; + if ((s->attack_event_count > 0 || s->cloud_event_count > 0 || + s->melee_pending || attack_anim_active) && + s->zulrah_visible && !s->is_diving && n > 1) + out[1].attack_target_entity_idx = 0; + *count = n; +} + +static void zul_put_int(EncounterState* state, EncounterContext* context, const char* key, int value) { + (void)context; + ZulrahState* s = (ZulrahState*)state; + if (strcmp(key, "seed") == 0) s->rng_state = (uint32_t)value; + else if (strcmp(key, "world_offset_x") == 0) s->world_offset_x = value; + else if (strcmp(key, "world_offset_y") == 0) s->world_offset_y = value; + else if (strcmp(key, "gear_tier") == 0) { + s->gear_tier_fixed = encounter_require_int_range_config( + "zulrah", key, value, 0, ZUL_NUM_GEAR_TIERS - 1); + s->gear_tier = s->gear_tier_fixed; + } + else if (strcmp(key, "gear_tier_mode") == 0) { + s->gear_tier_mode = encounter_require_int_range_config( + "zulrah", key, value, ZUL_GEAR_TIER_FIXED, ZUL_GEAR_TIER_WEIGHTED); + } + else if (strcmp(key, "episode_mode") == 0) { + s->episode_mode = encounter_require_int_range_config( + "zulrah", key, value, ZUL_EPISODE_SINGLE_KILL, ZUL_EPISODE_TRIP); + } + else if (strcmp(key, "player_dest_x") == 0) { + s->player_dest_x = value; + if (value >= 0) s->player_dest_explicit = 1; + } + else if (strcmp(key, "player_dest_y") == 0) { + s->player_dest_y = value; + if (value >= 0) s->player_dest_explicit = 1; + } + else if (strcmp(key, "human_command_mode") == 0) + s->human_command_mode = encounter_require_binary_config("zulrah", key, value); + else encounter_abort_unknown_config("zulrah", "int", key); +} +static float zul_require_nonnegative_float_config(const char* k, float v) { + if (v < 0.0f) { + fprintf(stderr, "zulrah config %s must be >= 0, got %.6f\n", k, v); + abort(); + } + return v; +} +static void zul_put_float(EncounterState* st, EncounterContext* context, const char* k, float v) { + (void)context; + ZulrahState* s = (ZulrahState*)st; + if (strncmp(k, "gear_tier_weight_", 17) == 0) { + int idx = k[17] - '0'; + if (k[18] != '\0' || idx < 0 || idx >= ZUL_NUM_GEAR_TIERS) { + encounter_abort_unknown_config("zulrah", "float", k); + } + s->gear_tier_weights[idx] = zul_require_nonnegative_float_config(k, v); + } + else if (strcmp(k, "reward_win") == 0) { + s->reward_config.win = zul_require_nonnegative_float_config(k, v); + } + else if (strcmp(k, "reward_loss_penalty") == 0) { + s->reward_config.loss_penalty = zul_require_nonnegative_float_config(k, v); + } + else if (strcmp(k, "reward_damage_dealt") == 0) { + s->reward_config.damage_dealt = zul_require_nonnegative_float_config(k, v); + } + else if (strcmp(k, "reward_correct_style") == 0) { + s->reward_config.correct_style = zul_require_nonnegative_float_config(k, v); + } + else if (strcmp(k, "reward_damage_received_penalty") == 0) { + s->reward_config.damage_received_penalty = + zul_require_nonnegative_float_config(k, v); + } + else if (strcmp(k, "reward_cloud_occupancy_penalty") == 0) { + s->reward_config.cloud_occupancy_penalty = + zul_require_nonnegative_float_config(k, v); + } + else encounter_abort_unknown_config("zulrah", "float", k); +} +static void zul_put_ptr(EncounterState* st, EncounterContext* context, const char* k, void* v) { + (void)context; + ZulrahState* s = (ZulrahState*)st; + if (strcmp(k, "collision_map") == 0) s->collision_map = v; + else encounter_abort_unknown_config("zulrah", "ptr", k); +} + +static void* zul_get_log(EncounterState* state, EncounterContext* context) { + (void)context; + ZulrahState* s = (ZulrahState*)state; + if (s->episode_over) { + s->log.episode_return += s->episode_return; + s->log.episode_length += (float)s->tick; + float kills = (float)s->kills_this_episode; + float win = (s->episode_mode == ZUL_EPISODE_TRIP) + ? kills + : ((s->winner == ZUL_OUTCOME_PLAYER_WON) ? 1.0f : 0.0f); + float partial = (s->episode_mode == ZUL_EPISODE_TRIP) + ? zul_current_kill_progress(s) + : 0.0f; + float speed_bonus = (s->episode_mode == ZUL_EPISODE_TRIP) + ? s->score_speed_bonus_sum + : (win > 0.0f + ? zul_score_speed_bonus_for_duration(s->tick) + : 0.0f); + s->log.wins += win; + s->log.zulrah_kills += kills; + s->log.damage_dealt += s->total_damage_dealt; + s->log.damage_received += s->total_damage_received; + s->log.cloud_occupancy_ticks += (float)s->total_cloud_occupancy_ticks; + s->log.cloud_occupancy_frac += s->tick > 0 + ? (float)s->total_cloud_occupancy_ticks / (float)s->tick + : 0.0f; + s->log.cloud_damage_received += s->total_cloud_damage_received; + s->log.active_cloud_count_ticks += (float)s->total_active_cloud_ticks; + s->log.pending_cloud_count_ticks += (float)s->total_pending_cloud_ticks; + int tier = s->gear_tier; + if (tier < 0 || tier >= ZUL_NUM_GEAR_TIERS) { + fprintf(stderr, "zulrah invalid sampled gear tier %d\n", tier); + abort(); + } + s->log.zulrah_tier_n[tier] += 1.0f; + s->log.zulrah_tier_wins[tier] += win; + s->log.zulrah_tier_score_sum[tier] += win + partial + speed_bonus; + s->log.zulrah_tier_damage_received[tier] += s->total_damage_received; + s->log.zulrah_tier_episode_length[tier] += (float)s->tick; + s->log.zulrah_tier_cloud_occupancy_ticks[tier] += + (float)s->total_cloud_occupancy_ticks; + s->log.zulrah_tier_cloud_damage_received[tier] += + s->total_cloud_damage_received; + s->log.n += 1.0f; + } + return &s->log; +} +static int zul_get_tick(EncounterState* state, EncounterContext* context) { + (void)context; + return ((ZulrahState*)state)->tick; +} + +static void zul_emit_player_projectile_profile( + ZulrahState* s, + EncounterOverlay* ov, + const OsrsCombatProjectileProfile* profile, + int style, + int damage, + int sequence_index, + int sequence_count +) { + if (!profile || profile->projectile_model_id <= 0) { + uint8_t weapon = s->player.equipped[GEAR_SLOT_WEAPON]; + int item_id = weapon < NUM_ITEMS ? ITEM_DATABASE[weapon].item_id : -1; + fprintf(stderr, "zulrah: missing player projectile model for item %d style %d\n", + item_id, style); + abort(); + } + + int target_size = ZUL_NPC_SIZE; + int fallback_start_h = 64; + int fallback_end_h = (int)(target_size * 0.5f * 128); + int p_duration = s->player_attack_timing.visual_duration_ticks * 30; + int p_start_delay = s->player_attack_timing.visual_start_delay_ticks * 30; + int visual_damage = sequence_index == sequence_count - 1 ? damage : 0; + + (void)osrs_emit_combat_projectile_profile_player_to_npc(ov, profile, &(OsrsCombatProjectileEmitSpec){ + .src_x = s->player.x, + .src_y = s->player.y, + .dst_x = s->zulrah.x, + .dst_y = s->zulrah.y, + .src_size = 1, + .dst_size = target_size, + .target_npc_slot = 0, + .attack_style = (AttackStyle)style, + .damage = visual_damage, + .duration_ticks = p_duration, + .start_delay = p_start_delay, + .fallback_start_h = fallback_start_h, + .fallback_end_h = fallback_end_h, + .curve = 16, + .splash_gfx_id = GFX_SPLASH, + }); +} + +static void zul_emit_player_attack_projectiles(ZulrahState* s, EncounterOverlay* ov) { + if (!s->player_attacked_this_tick || + s->player_attack_style_id == ATTACK_STYLE_MELEE) { + return; + } + + uint8_t weapon = s->player.equipped[GEAR_SLOT_WEAPON]; + int style = s->player_attack_style_id; + if (style == ATTACK_STYLE_MAGIC) { + const OsrsCombatProjectileProfile* profile = NULL; + if (s->player_attack_is_special && weapon < NUM_ITEMS) { + const OsrsCombatVisualRow* effect = + osrs_combat_visual_find_special_projectile_item_id( + ITEM_DATABASE[weapon].item_id, ATTACK_STYLE_MAGIC); + if (effect) profile = &effect->projectile; + } + if (!profile) profile = osrs_combat_visual_magic_projectile_profile(weapon); + zul_emit_player_projectile_profile( + s, ov, profile, style, s->player_attack_dmg, 0, 1); + return; + } + + const OsrsCombatProjectileProfile* base_profile = + osrs_combat_visual_ranged_projectile_profile( + weapon, OSRS_COMBAT_PROJECTILE_NONE); + const OsrsCombatVisualRow* effect = NULL; + if (s->player_attack_is_special && weapon < NUM_ITEMS) { + effect = osrs_combat_visual_find_special_projectile_item_id( + ITEM_DATABASE[weapon].item_id, ATTACK_STYLE_RANGED); + } + OsrsCombatProjectileSequencePart parts[OSRS_COMBAT_PROJECTILE_SEQUENCE_MAX]; + int part_count = osrs_combat_visual_build_projectile_sequence( + base_profile, effect, parts, OSRS_COMBAT_PROJECTILE_SEQUENCE_MAX); + if (part_count <= 0) { + int item_id = weapon < NUM_ITEMS ? ITEM_DATABASE[weapon].item_id : -1; + fprintf(stderr, "zulrah: missing ranged projectile sequence for item %d\n", + item_id); + abort(); + } + for (int i = 0; i < part_count; i++) { + zul_emit_player_projectile_profile( + s, ov, &parts[i].projectile, style, s->player_attack_dmg, + parts[i].sequence_index, parts[i].sequence_count); + } +} + +static void zul_render_post_tick(EncounterState* state, EncounterContext* context, EncounterOverlay* ov) { + (void)context; + ZulrahState* s = (ZulrahState*)state; + + ov->tile_shadow_count = 0; + ov->hazard_count = 0; + for (int i = 0; i < ZUL_MAX_CLOUDS && ov->hazard_count < ENCOUNTER_MAX_OVERLAY_TILES; i++) { + if (!s->clouds[i].active) continue; + ov->hazards[ov->hazard_count].x = s->clouds[i].x; + ov->hazards[ov->hazard_count].y = s->clouds[i].y; + ov->hazards[ov->hazard_count].active = 1; + ov->hazard_count++; + } + + ov->boss_x = s->zulrah.x; + ov->boss_y = s->zulrah.y; + ov->boss_visible = s->zulrah_visible; + ov->boss_form = (int)s->current_form; + ov->boss_size = ZUL_NPC_SIZE; + + ov->add_count = 0; + + ov->melee_target_active = s->melee_pending; + ov->melee_target_x = s->melee_target_x; + ov->melee_target_y = s->melee_target_y; + + ov->projectile_count = 0; + for (int i = 0; i < s->attack_event_count; i++) { + if (s->attack_events[i].style == 4) { + int pi = osrs_emit_projectile_with_spec(ov, &(OsrsProjectileEventSpec){ + .src_x = s->attack_events[i].src_x, + .src_y = s->attack_events[i].src_y, + .dst_x = s->attack_events[i].dst_x, + .dst_y = s->attack_events[i].dst_y, + .style = 4, + .damage = 0, + .duration_ticks = 40, + .start_h = 100, + .end_h = 0, + .curve = 12, + .arc_height = 0.0f, + .src_size = ZUL_NPC_SIZE, + .dst_size = 1, + .model_id = GFX_SNAKELING_SPAWN_MODEL, + .anim_id = GFX_SNAKELING_SPAWN_ANIM, + }, 0); + encounter_set_projectile_source_npc_slot(ov, pi, 0); + } else if (s->attack_events[i].style == 2) { + continue; + } else { + uint32_t zul_proj_model = (s->attack_events[i].style == 0) + ? GFX_RANGED_PROJ_MODEL : GFX_MAGIC_PROJ_MODEL; + int zul_proj_anim = (s->attack_events[i].style == 0) + ? GFX_RANGED_PROJ_ANIM : GFX_MAGIC_PROJ_ANIM; + (void)osrs_emit_projectile_npc_to_player(ov, &(OsrsProjectileEventSpec){ + .src_x = s->attack_events[i].src_x, + .src_y = s->attack_events[i].src_y, + .dst_x = s->attack_events[i].dst_x, + .dst_y = s->attack_events[i].dst_y, + .style = s->attack_events[i].style, + .damage = s->attack_events[i].damage, + .duration_ticks = 35, + .start_h = 480, + .end_h = 64, + .curve = 16, + .arc_height = 0.0f, + .src_size = ZUL_NPC_SIZE, + .dst_size = 1, + .model_id = zul_proj_model, + .anim_id = zul_proj_anim, + }, 0); + } + } + for (int i = 0; i < s->cloud_event_count; i++) { + int pi = osrs_emit_projectile_with_spec(ov, &(OsrsProjectileEventSpec){ + .src_x = s->cloud_events[i].src_x, + .src_y = s->cloud_events[i].src_y, + .dst_x = s->cloud_events[i].dst_x, + .dst_y = s->cloud_events[i].dst_y, + .style = 3, + .damage = 0, + .duration_ticks = s->cloud_events[i].flight_ticks * 30, + .start_h = 200, + .end_h = 0, + .curve = 10, + .arc_height = 3.0f, + .src_size = ZUL_NPC_SIZE, + .dst_size = 1, + .model_id = GFX_CLOUD_PROJ_MODEL, + .anim_id = GFX_CLOUD_PROJ_ANIM, + }, 0); + encounter_set_projectile_source_npc_slot(ov, pi, 0); + } + zul_emit_player_attack_projectiles(s, ov); +} +static int zul_get_winner(EncounterState* state, EncounterContext* context) { + (void)context; + return ((ZulrahState*)state)->winner; +} + +static int zul_attack_action_for_weapon(uint8_t weapon) { + AttackStyle style = (AttackStyle)get_item_attack_style(weapon); + return (style == ATTACK_STYLE_MAGIC) ? ZUL_ATK_MAGE : ZUL_ATK_RANGE; +} + +static void zul_translate_human_commands(HumanInput* hi, int* actions, ZulrahState* s) { + for (int h = 0; h < ZUL_NUM_ACTION_HEADS; h++) actions[h] = 0; + + uint8_t queued_weapon = s->player.equipped[GEAR_SLOT_WEAPON]; + int path_command_seen = 0; + for (int i = 0; i < hi->commands.count; i++) { + const HumanCommand* cmd = &hi->commands.items[i]; + if (cmd->kind == HUMAN_COMMAND_EQUIP_INVENTORY_ITEM && + cmd->gear_slot == GEAR_SLOT_WEAPON && + cmd->item_db_idx >= 0 && cmd->item_db_idx < NUM_ITEMS) { + queued_weapon = (uint8_t)cmd->item_db_idx; + } + + switch (cmd->kind) { + case HUMAN_COMMAND_WALK: + path_command_seen = 1; + s->player_dest_x = cmd->world_x; + s->player_dest_y = cmd->world_y; + s->player_dest_explicit = 1; + actions[ZUL_HEAD_ATTACK] = ZUL_ATK_NONE; + osrs_interaction_clear(&s->interaction); + break; + case HUMAN_COMMAND_ATTACK_NPC: + path_command_seen = 1; + actions[ZUL_HEAD_ATTACK] = zul_attack_action_for_weapon(queued_weapon); + s->player_dest_x = -1; + s->player_dest_y = -1; + s->player_dest_explicit = 0; + break; + case HUMAN_COMMAND_SPELL_TARGET: + path_command_seen = 1; + actions[ZUL_HEAD_ATTACK] = ZUL_ATK_MAGE; + s->player_dest_x = -1; + s->player_dest_y = -1; + s->player_dest_explicit = 0; + break; + case HUMAN_COMMAND_OVERHEAD_PRAYER: + actions[ZUL_HEAD_PRAYER] = cmd->overhead_prayer; + break; + case HUMAN_COMMAND_OFFENSIVE_PRAYER: + actions[ZUL_HEAD_OFFENSIVE] = cmd->offensive_prayer; + break; + case HUMAN_COMMAND_EAT: + actions[ZUL_HEAD_FOOD] = cmd->food == 1 ? 2 : 1; + break; + case HUMAN_COMMAND_DRINK: + if (cmd->potion == POTION_BREW) actions[ZUL_HEAD_FOOD] = 1; + else if (cmd->potion == POTION_RESTORE || + cmd->potion == POTION_PRAYER_POT) actions[ZUL_HEAD_POTION] = 1; + else if (cmd->potion == POTION_ANTIVENOM) actions[ZUL_HEAD_POTION] = 2; + break; + case HUMAN_COMMAND_SPEC_TOGGLE: + actions[ZUL_HEAD_SPEC] = 1; + break; + case HUMAN_COMMAND_EQUIP_INVENTORY_ITEM: + case HUMAN_COMMAND_FIGHT_STYLE: + case HUMAN_COMMAND_SET_AUTOCAST: + case HUMAN_COMMAND_ITEM_ON_ITEM: + case HUMAN_COMMAND_ITEM_ON_WIDGET: + case HUMAN_COMMAND_SPELL_ON_WIDGET: + case HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK: + case HUMAN_COMMAND_NONE: + break; + } + } + + if (!path_command_seen && hi->pending_move_x >= 0 && hi->pending_move_y >= 0) { + s->player_dest_x = hi->pending_move_x; + s->player_dest_y = hi->pending_move_y; + s->player_dest_explicit = 1; + actions[ZUL_HEAD_ATTACK] = ZUL_ATK_NONE; + osrs_interaction_clear(&s->interaction); + } +} + +static void zul_step_human_commands(EncounterState* state, EncounterContext* context, HumanInput* hi) { + ZulrahState* s = (ZulrahState*)state; + int actions[ZUL_NUM_ACTION_HEADS]; + s->human_command_mode = 1; + s->human_commands = hi->commands.items; + s->human_command_count = hi->commands.count; + zul_sync_human_gear_style(s); + zul_refresh_human_loadout_stats(s); + zul_translate_human_commands(hi, actions, s); + zul_step(state, context, actions); + s->human_commands = NULL; + s->human_command_count = 0; + human_input_clear_pending(hi); +} + +typedef struct { + int unused; +} ZulrahContext; + +static void zul_init_context(EncounterContext* context) { + (void)context; +} + +static void zul_destroy_context(EncounterContext* context) { + (void)context; +} + +static void zul_init_state_ctx(EncounterState* state, EncounterContext* context) { + (void)context; + memset(state, 0, sizeof(ZulrahState)); +} + +static const EncounterDef ENCOUNTER_ZULRAH = { + .name = "zulrah", + .obs_size = ZUL_NUM_OBS, + .num_action_heads = ZUL_NUM_ACTION_HEADS, + .action_head_dims = ZUL_ACTION_HEAD_DIMS, + .mask_size = ZUL_ACTION_MASK_SIZE, + .state_size = sizeof(ZulrahState), + .context_size = sizeof(ZulrahContext), + .init_context = zul_init_context, + .destroy_context = zul_destroy_context, + .init_state = zul_init_state_ctx, + .create = zul_create, + .destroy = zul_destroy, + .reset = zul_reset, + .step = zul_step, + .step_human_commands = zul_step_human_commands, + .write_obs = zul_write_obs, + .write_mask = zul_write_mask, + .get_reward = zul_get_reward, + .is_terminal = zul_is_terminal, + .get_entity_count = zul_get_entity_count, + .get_entity = zul_get_entity, + .fill_render_entities = zul_fill_render_entities, + .put_int = zul_put_int, + .put_float = zul_put_float, + .put_ptr = zul_put_ptr, + .arena_base_x = 0, + .arena_base_y = 0, + .arena_width = ZUL_ARENA_SIZE, + .arena_height = ZUL_ARENA_SIZE, + .head_move = ZUL_HEAD_MOVE, + .head_prayer = ZUL_HEAD_PRAYER, + .head_target = -1, + + .render_post_tick = zul_render_post_tick, + .get_log = zul_get_log, + .get_tick = zul_get_tick, + .get_winner = zul_get_winner, +}; + +__attribute__((constructor)) +static void zul_register(void) { + encounter_register(&ENCOUNTER_ZULRAH); +} + +#endif diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc b/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc new file mode 100644 index 0000000000..7c35fd2fe5 --- /dev/null +++ b/ocean/osrs/encounters/inferno/encounter_inferno_combat.inc @@ -0,0 +1,1289 @@ +#line 2424 "encounter_inferno.h" +static inline int inf_jad_attack_timer(int wave) { + return wave == 67 ? 9 : 8; +} + +static AttackStyle inf_player_equipped_attack_style(const InfernoState* s) { + uint8_t weapon = s->player.equipped[GEAR_SLOT_WEAPON]; + AttackStyle style = (AttackStyle)get_item_attack_style(weapon); + if (style == ATTACK_STYLE_MAGIC || + style == ATTACK_STYLE_RANGED || + style == ATTACK_STYLE_MELEE) { + return style; + } + return ATTACK_STYLE_RANGED; +} + +static void inf_refresh_human_loadout_stats(InfernoState* s) { + AttackStyle style = inf_player_equipped_attack_style(s); + int spell_base_damage = (style == ATTACK_STYLE_MAGIC) ? 30 : 0; + encounter_compute_player_equipped_stats( + &s->player, style, s->player.fight_style, spell_base_damage, + &s->human_loadout_stats); +} + +static const EncounterLoadoutStats* inf_current_loadout_stats( + InfernoState* s, + const InfernoContext* ctx +) { + if (ctx->human_command_mode) { + inf_refresh_human_loadout_stats(s); + return &s->human_loadout_stats; + } + return &s->loadout_stats[s->weapon_set]; +} + +static int inf_player_weapon_is(const InfernoState* s, uint8_t item) { + return s->player.equipped[GEAR_SLOT_WEAPON] == item; +} + +static void inf_damage_player(InfernoState* s, int damage); + +typedef struct { + EncounterLoadoutStats stats; + int spell; + int is_barrage; + uint8_t weapon; +} InfPlayerAttack; + +static int inf_is_barrage_spell(int spell) { + return spell == ENCOUNTER_SPELL_BLOOD || spell == ENCOUNTER_SPELL_ICE; +} + +static int inf_spell_magic_level_requirement(int spell) { + if (spell == ENCOUNTER_SPELL_ICE) return ICE_BARRAGE_LEVEL; + if (spell == ENCOUNTER_SPELL_BLOOD) return BLOOD_BARRAGE_LEVEL; + fprintf(stderr, "BUG: invalid Inferno spell %d\n", spell); + abort(); +} + +static int inf_spell_action_to_spell(int action_spell) { + if (action_spell == 1) return ENCOUNTER_SPELL_BLOOD; + if (action_spell == 2) return ENCOUNTER_SPELL_ICE; + return ENCOUNTER_SPELL_NONE; +} + +static int inf_spell_base_damage(int spell) { + if (!inf_is_barrage_spell(spell)) { + fprintf(stderr, "BUG: invalid Inferno spell %d\n", spell); + abort(); + } + return 30; +} + +static int inf_player_autocast_spell(const Player* p) { + return p->autocast_spell == ENCOUNTER_SPELL_ICE + ? ENCOUNTER_SPELL_ICE + : ENCOUNTER_SPELL_BLOOD; +} + +static FightStyle inf_player_spell_fight_style(const Player* p) { + return (p->autocast_defensive || p->fight_style == FIGHT_STYLE_DEFENSIVE_AUTOCAST) + ? FIGHT_STYLE_DEFENSIVE_AUTOCAST + : FIGHT_STYLE_AUTOCAST; +} + +static int inf_autocast_is_active(const InfernoState* s) { + return s->player.autocast_enabled && + item_supports_ancient_autocast(s->player.equipped[GEAR_SLOT_WEAPON]); +} + +static void inf_compute_manual_spell_stats( + InfernoState* s, + int spell, + EncounterLoadoutStats* out +) { + encounter_compute_player_equipped_stats( + &s->player, + ATTACK_STYLE_MAGIC, + inf_player_spell_fight_style(&s->player), + inf_spell_base_damage(spell), + out); + out->attack_speed = 5; + out->attack_range = 10; +} + +static int inf_resolve_player_attack_ctx( + InfernoState* s, + const InfernoContext* ctx, + int manual_spell, + InfPlayerAttack* out +) { + memset(out, 0, sizeof(*out)); + out->spell = ENCOUNTER_SPELL_NONE; + out->weapon = s->player.equipped[GEAR_SLOT_WEAPON]; + + if (inf_is_barrage_spell(manual_spell)) { + if (s->player.current_magic < inf_spell_magic_level_requirement(manual_spell)) + return 0; + inf_compute_manual_spell_stats(s, manual_spell, &out->stats); + out->spell = manual_spell; + out->is_barrage = 1; + return 1; + } + + const EncounterLoadoutStats* ls = inf_current_loadout_stats(s, ctx); + out->stats = *ls; + + if (ls->style != ATTACK_STYLE_MAGIC) + return 1; + + if (!inf_autocast_is_active(s)) + return 0; + + int autocast_spell = inf_player_autocast_spell(&s->player); + if (s->player.current_magic < inf_spell_magic_level_requirement(autocast_spell)) + return 0; + + out->spell = autocast_spell; + out->is_barrage = 1; + out->stats.attack_speed = 5; + out->stats.attack_range = 10; + return 1; +} + +static int inf_resolve_player_attack( + InfernoState* s, + int manual_spell, + InfPlayerAttack* out +) { + return inf_resolve_player_attack_ctx( + s, inf_legacy_context(), manual_spell, out); +} + +static int inf_player_has_any_barrage_spell_available(const InfernoState* s) { + return s->player.current_magic >= BLOOD_BARRAGE_LEVEL; +} + +static int inf_npc_type_can_be_tagged_off_shield(int type) { + return type == INF_NPC_JAD || + type == INF_NPC_RANGER || + type == INF_NPC_MAGER; +} + +static int inf_is_live_shield_slot(const InfernoState* s, int npc_idx) { + if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; + const InfNPC* npc = &s->npcs[npc_idx]; + return npc->active && + npc->death_ticks == 0 && + npc->hp > 0 && + npc->type == INF_NPC_ZUK_SHIELD; +} + +static int inf_is_shield_taggable_slot(const InfernoState* s, int npc_idx) { + if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; + const InfNPC* npc = &s->npcs[npc_idx]; + return npc->active && + npc->death_ticks == 0 && + npc->hp > 0 && + inf_npc_type_can_be_tagged_off_shield(npc->type) && + inf_is_live_shield_slot(s, npc->aggro_target); +} + +static int inf_player_behind_zuk_shield_now(const InfernoState* s) { + int shield_idx = s->zuk.shield_idx; + if (shield_idx < 0 || shield_idx >= INF_MAX_NPCS) return 0; + + const InfNPC* shield = &s->npcs[shield_idx]; + if (!shield->active || shield->death_ticks != 0 || shield->hp <= 0) + return 0; + + int shield_size = INF_NPC_STATS[INF_NPC_ZUK_SHIELD].size; + return s->player.x >= shield->x && + s->player.x < shield->x + shield_size && + s->player.y >= 41; +} + +static int inf_is_untagged_live_zuk_healer_slot( + const InfernoState* s, + int npc_idx +) { + if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; + + const InfNPC* npc = &s->npcs[npc_idx]; + if (!npc->active || npc->death_ticks != 0 || npc->hp <= 0 || + npc->type != INF_NPC_HEALER_ZUK) + return 0; + + if (npc->aggro_target < 0 || npc->aggro_target >= INF_MAX_NPCS) + return 0; + + const InfNPC* aggro = &s->npcs[npc->aggro_target]; + return aggro->active && aggro->type == INF_NPC_ZUK; +} + +static int inf_player_can_attack_npc_from_current_tile( + const InfernoState* s, + int npc_idx +) { + if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; + + const InfNPC* npc = &s->npcs[npc_idx]; + if (!npc->active || npc->death_ticks != 0 || npc->hp <= 0) return 0; + + const EncounterLoadoutStats* ls = &s->loadout_stats[s->weapon_set]; + OsrsLosQuery los_query = inf_player_los_query(s); + return encounter_player_can_attack( + s->player.x, + s->player.y, + npc->x, + npc->y, + npc->size, + ls->attack_range, + &los_query); +} + +static int inf_player_has_los_to_npc_from_current_tile( + const InfernoState* s, + int npc_idx +) { + if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; + + const InfNPC* npc = &s->npcs[npc_idx]; + if (!npc->active || npc->death_ticks != 0 || npc->hp <= 0 || + npc->type == INF_NPC_ZUK_SHIELD) + return 0; + + return entity_has_line_of_sight( + s->los_blockers, + s->los_blocker_count, + s->player.x, + s->player.y, + 1, + npc->x, + npc->y, + npc->size, + 0); +} + +typedef struct { + int can_attack_now; + int range_deficit; +} InfPlayerNpcLoadoutReach; + +static InfPlayerNpcLoadoutReach inf_player_loadout_reach_to_npc( + const InfernoState* s, + int npc_idx, + int weapon_set +) { + InfPlayerNpcLoadoutReach out = {0}; + if (!inf_npc_is_live_player_target(s, npc_idx)) + return out; + if (weapon_set < 0 || weapon_set >= INF_NUM_WEAPON_SETS) { + fprintf(stderr, "BUG: invalid Inferno weapon set %d\n", weapon_set); + abort(); + } + + const InfNPC* npc = &s->npcs[npc_idx]; + const EncounterLoadoutStats* ls = &s->loadout_stats[weapon_set]; + OsrsLosQuery los_query = inf_player_los_query(s); + int edge_distance = encounter_dist_to_npc( + s->player.x, s->player.y, npc->x, npc->y, npc->size); + out.range_deficit = edge_distance > ls->attack_range + ? edge_distance - ls->attack_range + : 0; + out.can_attack_now = encounter_player_can_attack( + s->player.x, + s->player.y, + npc->x, + npc->y, + npc->size, + ls->attack_range, + &los_query); + return out; +} + +static int inf_player_can_phantom_barrage_npc( + InfernoState* s, + const InfernoContext* ctx, + int npc_idx +) { + if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; + + const InfNPC* npc = &s->npcs[npc_idx]; + if (!npc->active || npc->death_ticks <= 0 || npc->hp > 0 || + npc->type == INF_NPC_ZUK_SHIELD) + return 0; + + if (!inf_player_has_any_barrage_spell_available(s)) return 0; + + OsrsLosQuery los_query = inf_player_los_query(s); + return encounter_player_can_attack( + s->player.x, + s->player.y, + npc->x, + npc->y, + npc->size, + 10, + &los_query); +} + +static int inf_npc_is_phantom_barrage_obs_candidate( + InfernoState* s, + const InfernoContext* ctx, + int npc_idx +) { + if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; + return s->npcs[npc_idx].death_ticks <= 2 && + inf_player_can_phantom_barrage_npc(s, ctx, npc_idx); +} + +static int inf_npc_is_phantom_barrage_cast_window( + InfernoState* s, + const InfernoContext* ctx, + int npc_idx +) { + if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; + return s->player.attack_timer <= 1 && + s->npcs[npc_idx].death_ticks == 1 && + inf_player_can_phantom_barrage_npc(s, ctx, npc_idx); +} + +static int inf_untagged_zuk_healer_target_is_safe_now( + const InfernoState* s, + const InfernoContext* ctx, + int npc_idx +) { + (void)ctx; + if (!inf_is_untagged_live_zuk_healer_slot(s, npc_idx)) return 1; + return inf_player_behind_zuk_shield_now(s) && + s->player.attack_timer == 0 && + inf_player_can_attack_npc_from_current_tile(s, npc_idx); +} + +static int inf_is_safe_untagged_zuk_healer_target_now( + const InfernoState* s, + const InfernoContext* ctx, + int npc_idx +) { + return inf_is_untagged_live_zuk_healer_slot(s, npc_idx) && + inf_untagged_zuk_healer_target_is_safe_now(s, ctx, npc_idx); +} + +static int inf_has_safe_untagged_zuk_healer_target_now( + const InfernoState* s, + const InfernoContext* ctx +) { + for (int obs_slot = 0; obs_slot < INF_OBS_NPCS; obs_slot++) { + int npc_idx = s->current_obs_slots[obs_slot]; + if (npc_idx >= 0 && npc_idx < INF_MAX_NPCS && + s->npcs[npc_idx].active && + s->npcs[npc_idx].death_ticks == 0 && + s->npcs[npc_idx].type != INF_NPC_ZUK_SHIELD && + inf_is_safe_untagged_zuk_healer_target_now(s, ctx, npc_idx)) { + return 1; + } + } + return 0; +} + +static int inf_npc_target_hit_delay( + const InfNPC* npc, const InfNPC* target, int style +) { + int dist = encounter_projectile_distance( + npc->x, npc->y, npc->size, target->x, target->y, target->size, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + EncounterProjectileTiming timing = inf_npc_projectile_timing(npc->type, style, dist); + if (npc->type == INF_NPC_JAD && timing.damage_delay_ticks > 0) + return INF_JAD_PROJECTILE_DELAY + timing.damage_delay_ticks; + return timing.damage_delay_ticks; +} + +static void inf_apply_npc_target_damage(InfernoState* s, int target_idx, int damage) { + if (target_idx < 0 || target_idx >= INF_MAX_NPCS) return; + InfNPC* target = &s->npcs[target_idx]; + if (!target->active) return; + + int target_hp_before = target->hp; + encounter_damage_npc(&target->hp, &target->hit_landed_this_tick, + &target->hit_damage, damage); + if (target->type == INF_NPC_ZUK_SHIELD) + s->tick_scratch.shield_damage += (float)(target_hp_before - target->hp); + + if (target->hp <= 0 && target->type == INF_NPC_ZUK_SHIELD) { + inf_deactivate_npc(s, target_idx); + s->zuk.shield_idx = -1; + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (s->npcs[i].aggro_target == target_idx) + s->npcs[i].aggro_target = -1; + } + } +} + +static void inf_queue_npc_target_hit( + InfernoState* s, int target_idx, int damage, int ticks_remaining +) { + if (ticks_remaining <= 0) { + inf_apply_npc_target_damage(s, target_idx, damage); + return; + } + for (int i = 0; i < INF_MAX_NPC_TARGET_HITS; i++) { + InfNpcTargetHit* hit = &s->npc_target_hits[i]; + if (hit->active) continue; + hit->active = 1; + hit->target_idx = target_idx; + hit->damage = damage; + hit->ticks_remaining = ticks_remaining; + return; + } + fprintf(stderr, "FATAL: Inferno NPC target hit queue overflow\n"); + abort(); +} + +static void inf_resolve_npc_target_hits(InfernoState* s) { + for (int i = 0; i < INF_MAX_NPC_TARGET_HITS; i++) { + InfNpcTargetHit* hit = &s->npc_target_hits[i]; + if (!hit->active) continue; + hit->ticks_remaining--; + if (hit->ticks_remaining > 0) continue; + inf_apply_npc_target_damage(s, hit->target_idx, hit->damage); + hit->active = 0; + } +} + +static int inf_apply_elysian_to_player_hit(InfernoState* s, int damage, int* reduced) { + *reduced = 0; + if (damage <= 0) return damage; + osrs_ensure_player_equipment(&s->player); + if (!osrs_effect_profile_has( + &s->player.equipment_effect_profile, OSRS_ITEM_EFFECT_ELYSIAN)) + return damage; + if (encounter_rand_int(&s->rng_state, 10) >= 7) + return damage; + *reduced = 1; + return damage * 75 / 100; +} + +static void inf_apply_echo_boots_recoil(InfernoState* s, int attacker_slot, int final_damage) { + osrs_ensure_player_equipment(&s->player); + int recoil_damage = osrs_echo_boots_recoil_damage( + &s->player.equipment_effect_profile, + &s->player.item_effect_state, + final_damage); + if (recoil_damage <= 0) return; + if (attacker_slot < 0 || attacker_slot >= INF_MAX_NPCS) return; + + InfNPC* npc = &s->npcs[attacker_slot]; + if (!npc->active || npc->death_ticks > 0 || npc->hp <= 0) return; + if (npc->type == INF_NPC_ZUK || npc->type == INF_NPC_ZUK_SHIELD) return; + + osrs_consume_echo_boots_charge(&s->player); + int applied = encounter_damage_npc( + &npc->hp, &npc->hit_landed_this_tick, &npc->hit_damage, + recoil_damage); + if (applied <= 0) return; + + s->tick_scratch.damage_dealt += (float)applied; + if (npc->type == INF_NPC_HEALER_ZUK) { + s->tick_scratch.damage_zuk_healers += (float)applied; + } else if (npc->type == INF_NPC_HEALER_JAD) { + s->tick_scratch.damage_jad_healers += (float)applied; + } else if (npc->type == INF_NPC_JAD) { + s->tick_scratch.damage_jad += (float)applied; + } else { + s->tick_scratch.damage_set += (float)applied; + } + + if (npc->hp <= 0) { + if (npc->type == INF_NPC_JAD) s->tick_scratch.kill_jad++; + else if (npc->type == INF_NPC_HEALER_ZUK) s->tick_scratch.kill_zuk_healer++; + else if (npc->type != INF_NPC_HEALER_JAD) s->tick_scratch.kill_set++; + inf_apply_npc_death(s, attacker_slot); + } +} + +static void inf_npc_attack_ctx(InfernoState* s, InfernoContext* ctx, int idx) { + InfNPC* npc = &s->npcs[idx]; + if (!npc->active) return; + const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; + if (npc->attack_timer > 0) npc->attack_timer--; + if (npc->stun_timer > 0) { npc->stun_timer--; return; } + if (npc->dig_freeze_timer > 0) return; + if (npc->dig_attack_delay > 0) return; + + int has_los_now = 0; + if (stats->attack_range > 1) { + has_los_now = inf_npc_has_los(s, idx); + } + + if (npc->type == INF_NPC_BLOB && + npc->blob_scanned_prayer < 0 && + has_los_now && + !npc->had_los_last_tick) { + npc->blob_scanned_prayer = (int)s->player.prayer; + if (s->player.prayer == PRAYER_PROTECT_MAGIC) npc->attack_style = ATTACK_STYLE_RANGED; + else if (s->player.prayer == PRAYER_PROTECT_RANGED) npc->attack_style = ATTACK_STYLE_MAGIC; + else npc->attack_style = (encounter_rand_int(&s->rng_state, 2) == 0) ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_RANGED; + npc->had_los_last_tick = has_los_now; + npc->attacked_this_tick = 1; + npc->attack_timer = stats->attack_speed; + return; + } + + npc->had_los_last_tick = has_los_now; + + if (npc->attack_timer > 0) return; + + if (npc->type == INF_NPC_ZUK_SHIELD) return; + + if (npc->type == INF_NPC_NIBBLER) { + for (int p = 0; p < INF_NUM_PILLARS; p++) { + if (!s->pillars[p].active) continue; + int ddx = npc->x - s->pillars[p].x; + int ddy = npc->y - s->pillars[p].y; + if (ddx >= -1 && ddx <= INF_PILLAR_SIZE && ddy >= -1 && ddy <= INF_PILLAR_SIZE) { + int dmg = encounter_rand_int(&s->rng_state, 5); + s->pillars[p].hp -= dmg; + if (s->pillars[p].hp <= 0) { + s->pillars[p].active = 0; + s->tick_scratch.pillar_lost = p; + inf_rebuild_los(s); + inf_invalidate_los_cache(s); + for (int n = 0; n < INF_MAX_NPCS; n++) { + if (!s->npcs[n].active) continue; + int ndx = s->npcs[n].x - s->pillars[p].x; + int ndy = s->npcs[n].y - s->pillars[p].y; + if (ndx >= -1 && ndx <= INF_PILLAR_SIZE && ndy >= -1 && ndy <= INF_PILLAR_SIZE) { + encounter_damage_npc(&s->npcs[n].hp, &s->npcs[n].hit_landed_this_tick, &s->npcs[n].hit_damage, 12); + inf_apply_npc_death(s, n); + } + } + { + int pdx = s->player.x - s->pillars[p].x; + int pdy = s->player.y - s->pillars[p].y; + if (pdx >= -1 && pdx <= INF_PILLAR_SIZE && pdy >= -1 && pdy <= INF_PILLAR_SIZE) { + inf_damage_player(s, 49); + } + } + } + npc->attacked_this_tick = 1; + npc->attack_timer = stats->attack_speed; + return; + } + } + return; + } + + if (npc->type == INF_NPC_HEALER_ZUK) { + InfHealerState* h = inf_npc_healer(npc); + if (npc->aggro_target >= 0) { + int ti = npc->aggro_target; + if (ti >= 0 && ti < INF_MAX_NPCS && s->npcs[ti].active && s->npcs[ti].type == INF_NPC_ZUK) { + h->target = ti; + h->timer = 3; + npc->attack_visual_target = ti; + } + } else { + h->target = -1; + h->timer = 0; + npc->attack_visual_target = -1; + inf_queue_zuk_healer_sparks(s, npc); + } + npc->attacked_this_tick = 1; + npc->attack_style_this_tick = ATTACK_STYLE_MAGIC; + npc->attack_timer = stats->attack_speed; + return; + } + + if (npc->type == INF_NPC_HEALER_JAD) { + InfHealerState* h = inf_npc_healer(npc); + int jad_idx = h->owner_idx; + if (npc->aggro_target >= 0) { + if (jad_idx >= 0 && s->npcs[jad_idx].active && + s->npcs[jad_idx].type == INF_NPC_JAD) { + h->target = jad_idx; + h->timer = 3; + npc->attack_visual_target = jad_idx; + } + npc->attacked_this_tick = 1; + npc->attack_timer = stats->attack_speed; + return; + } + if (entity_has_line_of_sight( + s->los_blockers, s->los_blocker_count, + npc->x, npc->y, 1, + s->player.x, s->player.y, 1, + 1)) { + int max_hit = osrs_npc_melee_max_hit(stats->str_level, stats->melee_str_bonus); + int att_roll = osrs_npc_attack_roll(stats->att_level, stats->melee_att_bonus); + const EncounterLoadoutStats* ls = inf_current_loadout_stats(s, ctx); + int def_roll = encounter_player_def_roll_from_loadout( + s->player.current_defence, s->player.current_magic, + ls->def_stab, ls->def_slash, ls->def_crush, ls->def_magic, ls->def_ranged, + ATTACK_STYLE_MELEE, MELEE_STYLE_CRUSH); + int dmg = encounter_npc_roll_attack(att_roll, def_roll, max_hit, &s->rng_state); + int prayer_matches = (s->player.prayer == PRAYER_PROTECT_MELEE); + if (prayer_matches) { dmg = 0; s->tick_scratch.prayer_correct = 1; } + else if (dmg > 0) { s->tick_scratch.off_prayer_hits++; } + inf_apply_echo_boots_recoil(s, idx, dmg); + inf_damage_player(s, dmg); + npc->attack_style_this_tick = ATTACK_STYLE_MELEE; + npc->attacked_this_tick = 1; + npc->attack_visual_target = -1; + npc->attack_timer = stats->attack_speed; + } + return; + } + + if (npc->aggro_target >= 0 && npc->aggro_target < INF_MAX_NPCS) { + InfNPC* target = &s->npcs[npc->aggro_target]; + if (target->active) { + AttackStyle actual_style = npc->attack_style; + if (npc->type == INF_NPC_JAD) { + actual_style = inf_npc_jad(npc)->attack_style; + if (actual_style == ATTACK_STYLE_NONE) + actual_style = inf_jad_roll_primary_style(&s->rng_state); + } + int max_hit = osrs_npc_max_hit(actual_style, + stats->str_level, stats->range_level, + stats->melee_str_bonus, stats->ranged_str_bonus, + stats->magic_base_dmg, stats->magic_dmg_pct); + if (stats->max_hit_cap > 0 && max_hit > stats->max_hit_cap) + max_hit = stats->max_hit_cap; + int dmg = encounter_rand_int(&s->rng_state, max_hit + 1); + int hit_delay = inf_npc_target_hit_delay(npc, target, actual_style); + inf_queue_npc_target_hit(s, npc->aggro_target, dmg, hit_delay); + npc->attacked_this_tick = 1; + npc->attack_style_this_tick = actual_style; + npc->attack_visual_target = npc->aggro_target; + npc->attack_timer = stats->attack_speed; + if (npc->type == INF_NPC_JAD) { + inf_npc_jad(npc)->attack_style = ATTACK_STYLE_NONE; + npc->attack_timer = inf_jad_attack_timer(s->wave); + } + return; + } + npc->aggro_target = -1; + } + + if (stats->attack_range > 1 && + !(npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) && + !has_los_now) return; + + int dist = encounter_dist_to_npc(s->player.x, s->player.y, + npc->x, npc->y, npc->size); + if (dist == 0 || dist > stats->attack_range) return; + if (stats->attack_range == 1 && + !inf_npc_melee_contact_allows_attack_at_tile( + s->player.x, s->player.y, npc->type, + npc->x, npc->y, npc->size, dist)) + return; + + if (npc->type == INF_NPC_BLOB) { + if (npc->blob_scanned_prayer < 0) { + npc->blob_scanned_prayer = (int)s->player.prayer; + if (s->player.prayer == PRAYER_PROTECT_MAGIC) npc->attack_style = ATTACK_STYLE_RANGED; + else if (s->player.prayer == PRAYER_PROTECT_RANGED) npc->attack_style = ATTACK_STYLE_MAGIC; + else npc->attack_style = (encounter_rand_int(&s->rng_state, 2) == 0) ? ATTACK_STYLE_MAGIC : ATTACK_STYLE_RANGED; + npc->attacked_this_tick = 1; + npc->attack_timer = stats->attack_speed; + return; + } + npc->blob_scanned_prayer = -1; + } + + AttackStyle actual_style = npc->attack_style; + + if (npc->type == INF_NPC_JAD) { + actual_style = inf_npc_jad(npc)->attack_style; + if (actual_style == ATTACK_STYLE_NONE) + actual_style = inf_jad_roll_primary_style(&s->rng_state); + } + + if (npc->type == INF_NPC_ZUK) { + int si = s->zuk.shield_idx; + int player_behind_shield = 0; + if (si >= 0 && s->npcs[si].active) { + InfNPC* shield = &s->npcs[si]; + player_behind_shield = (s->player.x >= shield->x && + s->player.x < shield->x + shield->size && + s->player.y >= 41); + } + + if (player_behind_shield) { + npc->attacked_this_tick = 1; + npc->attack_style_this_tick = ATTACK_STYLE_MAGIC; + npc->attack_visual_target = si; + } else { + int max_hit = osrs_npc_magic_max_hit(stats->magic_base_dmg, stats->magic_dmg_pct); + int dmg = encounter_rand_int(&s->rng_state, max_hit + 1); + inf_queue_player_pending_hit(s, idx, npc->type, (EncounterPendingHit){ + .damage = dmg, + .ticks_remaining = 3, + .attack_style = ATTACK_STYLE_NONE, + .check_prayer = 0, + .prayer_check_delay = 0, + .spell_type = 0, + .source_npc_type = npc->type, + .hit_success = 1, + .elysian_reduced = 0, + }); + s->last_hit_by_type = INF_NPC_ZUK; + npc->attacked_this_tick = 1; + npc->attack_style_this_tick = ATTACK_STYLE_MAGIC; + } + npc->attack_timer = s->zuk.enraged ? 7 : stats->attack_speed; + return; + } + + { + int style_mask = inf_attack_style_options_mask( + s, npc, stats, actual_style, dist); + actual_style = inf_choose_attack_style_for_tick( + &s->rng_state, style_mask); + } + + if (npc->type == INF_NPC_MAGER && + actual_style == ATTACK_STYLE_MAGIC && + inf_mager_resurrect_ctx(s, ctx, idx)) { + npc->attacked_this_tick = 1; + npc->attack_style_this_tick = ATTACK_STYLE_NONE; + npc->attack_timer = stats->attack_speed; + return; + } + + int max_hit = osrs_npc_max_hit(actual_style, + stats->str_level, stats->range_level, + stats->melee_str_bonus, stats->ranged_str_bonus, + stats->magic_base_dmg, stats->magic_dmg_pct); + if (stats->max_hit_cap > 0 && max_hit > stats->max_hit_cap) + max_hit = stats->max_hit_cap; + int is_delayed_jad = (npc->type == INF_NPC_JAD && + actual_style != ATTACK_STYLE_MELEE); + int dmg = is_delayed_jad ? max_hit : encounter_rand_int(&s->rng_state, max_hit + 1); + int accuracy_hit = 1; + + if (!is_delayed_jad) { + int att_lvl, att_bonus; + if (actual_style == ATTACK_STYLE_MELEE) { + att_lvl = stats->att_level; att_bonus = stats->melee_att_bonus; + } else if (actual_style == ATTACK_STYLE_RANGED) { + att_lvl = stats->range_level; att_bonus = stats->range_att_bonus; + } else { + att_lvl = stats->magic_level; att_bonus = stats->magic_att_bonus; + } + int att_roll = osrs_npc_attack_roll(att_lvl, att_bonus); + const EncounterLoadoutStats* ls = inf_current_loadout_stats(s, ctx); + int def_roll = encounter_player_def_roll_from_loadout( + s->player.current_defence, s->player.current_magic, + ls->def_stab, ls->def_slash, ls->def_crush, ls->def_magic, ls->def_ranged, + actual_style, stats->melee_style); + if (!encounter_roll_hit_chance(&s->rng_state, att_roll, def_roll)) { + dmg = 0; + accuracy_hit = 0; + } + } + + EncounterProjectileTiming hit_timing = + inf_npc_projectile_timing(npc->type, actual_style, dist); + int hit_delay = hit_timing.damage_delay_ticks; + if (is_delayed_jad) + hit_delay += INF_JAD_PROJECTILE_DELAY; + + if (actual_style == ATTACK_STYLE_MELEE) s->tick_scratch.styles_fired |= 1; + else if (actual_style == ATTACK_STYLE_RANGED) s->tick_scratch.styles_fired |= 2; + else if (actual_style == ATTACK_STYLE_MAGIC) s->tick_scratch.styles_fired |= 4; + s->tick_scratch.attacks_fired++; + s->attacks_by_type[npc->type]++; + + if (npc->type == INF_NPC_BAT) { + s->player.run_energy -= 300; + if (s->player.run_energy < 0) s->player.run_energy = 0; + } + + if (hit_delay == 0) { + int prayer_matches = encounter_prayer_correct_for_style(s->player.prayer, actual_style); + if (prayer_matches) { + dmg = 0; + s->tick_scratch.prayer_correct++; + s->prayer_correct_by_type[npc->type]++; + } else if (dmg > 0) { + s->tick_scratch.off_prayer_hits++; + } + int elysian_reduced = 0; + dmg = inf_apply_elysian_to_player_hit(s, dmg, &elysian_reduced); + if (elysian_reduced) s->player.elysian_proc_this_tick = 1; + inf_apply_echo_boots_recoil(s, idx, dmg); + s->dmg_from_type[npc->type] += (float)dmg; + if (dmg > 0) s->last_hit_by_type = npc->type; + inf_damage_player(s, dmg); + } else { + int is_jad = (npc->type == INF_NPC_JAD); + if (!is_jad) { + EncounterProtectResolve pr = + encounter_resolve_protect_at_throw(dmg, s->player.prayer, actual_style); + dmg = pr.frozen_damage; + if (pr.prayed) { + s->tick_scratch.prayer_correct++; + s->prayer_correct_by_type[npc->type]++; + } else if (dmg > 0) { + s->tick_scratch.off_prayer_hits++; + } + } + if (!is_delayed_jad) { + s->dmg_from_type[npc->type] += (float)dmg; + if (dmg > 0) s->last_hit_by_type = npc->type; + } + if (npc->type == INF_NPC_BAT && dmg > 0 && + encounter_rand_int(&s->rng_state, 2) == 0) { + if (s->player.current_attack > 0) s->player.current_attack--; + if (s->player.current_strength > 0) s->player.current_strength--; + if (s->player.current_defence > 0) s->player.current_defence--; + if (s->player.current_ranged > 0) s->player.current_ranged--; + if (s->player.current_magic > 0) s->player.current_magic--; + } + EncounterPendingHit hit; + if (is_jad) { + hit = (EncounterPendingHit){ + .damage = dmg, + .ticks_remaining = hit_delay, + .attack_style = actual_style, + .check_prayer = 1, + .prayer_check_delay = INF_JAD_PROJECTILE_DELAY + 1, + .spell_type = ENCOUNTER_SPELL_NONE, + .source_npc_type = npc->type, + .hit_success = accuracy_hit, + .elysian_reduced = 0, + }; + } else { + hit = encounter_pending_hit_resolved_at_throw( + dmg, hit_delay - 1, actual_style, s->player.prayer, + npc->type, 0 , accuracy_hit, NULL); + } + inf_queue_player_pending_hit(s, idx, npc->type, hit); + } + + npc->attacked_this_tick = 1; + npc->attack_style_this_tick = actual_style; + if (npc->type == INF_NPC_JAD) + inf_npc_jad(npc)->attack_style = ATTACK_STYLE_NONE; + npc->attack_timer = stats->attack_speed; + + if (npc->type == INF_NPC_JAD) + npc->attack_timer = inf_jad_attack_timer(s->wave); +} + +static void inf_npc_attack(InfernoState* s, int idx) { + inf_npc_attack_ctx(s, inf_legacy_context(), idx); +} + +static int inf_find_mager_respawn_tile( + InfernoState* s, const InfernoContext* ctx, int size, int* out_x, int* out_y +) { + InfMoveCtx mc = { s, ctx, -1 }; + for (int x = 26; x < 33; x++) { + for (int y = 24; y < 37; y++) { + if (inf_npc_blocked(&mc, x, y, size)) continue; + if (encounter_entity_footprints_overlap(x, y, size, + s->player.x, s->player.y, 1)) + continue; + *out_x = x; + *out_y = y; + return 1; + } + } + + *out_x = 21; + *out_y = 22; + return !inf_npc_blocked(&mc, *out_x, *out_y, size); +} + +static int inf_mager_resurrect_ctx(InfernoState* s, InfernoContext* ctx, int idx) { + InfNPC* npc = &s->npcs[idx]; + if (npc->type != INF_NPC_MAGER || !npc->active) return 0; + if (s->wave >= 68) return 0; + if (npc->resurrect_cooldown > 0) return 0; + if (s->dead_mob_count == 0) return 0; + + if (encounter_rand_int(&s->rng_state, 10) != 0) return 0; + + int di = encounter_rand_int(&s->rng_state, s->dead_mob_count); + InfDeadMob* dm = &s->dead_mobs[di]; + + int slot = inf_find_free_npc(s); + if (slot < 0) return 0; + + int rx = 21; + int ry = 22; + if (!inf_find_mager_respawn_tile(s, ctx, INF_NPC_STATS[dm->type].size, &rx, &ry)) + return 0; + + inf_init_npc(s, slot, dm->type, rx, ry); + s->npcs[slot].hp = dm->hp; + s->npcs[slot].max_hp = dm->max_hp; + s->npcs[slot].resurrection_count = 1; + s->npcs[slot].attack_timer = INF_NPC_STATS[dm->type].attack_speed; + + s->dead_mobs[di] = s->dead_mobs[s->dead_mob_count - 1]; + s->dead_mob_count--; + + npc->resurrect_cooldown = 8; + npc->resurrecting_this_tick = 1; + npc->resurrection_visual_target = slot; + npc->attack_visual_target = slot; + return 1; +} + +static int inf_mager_resurrect(InfernoState* s, int idx) { + return inf_mager_resurrect_ctx(s, inf_legacy_context(), idx); +} + +#define INF_JAD_HEALER_MAX_SPAWN_CANDIDATES 165 + +static int inf_try_sample_jad_healer_spawn( + InfernoState* s, const InfernoContext* ctx, const InfNPC* jad, + int min_dx, int max_dx, int min_dy, int max_dy, + int* out_x, int* out_y +) { + int offsets[INF_JAD_HEALER_MAX_SPAWN_CANDIDATES][2]; + int order[INF_JAD_HEALER_MAX_SPAWN_CANDIDATES]; + int count = 0; + for (int dx = min_dx; dx <= max_dx; dx++) { + for (int dy = min_dy; dy <= max_dy; dy++) { + assert(count < INF_JAD_HEALER_MAX_SPAWN_CANDIDATES); + offsets[count][0] = dx; + offsets[count][1] = dy; + order[count] = count; + count++; + } + } + encounter_shuffle(order, count, &s->rng_state); + + for (int i = 0; i < count; i++) { + int hx = jad->x + offsets[order[i]][0]; + int hy = jad->y + offsets[order[i]][1]; + if (encounter_entity_footprints_overlap(hx, hy, 1, jad->x, jad->y, jad->size)) + continue; + if (inf_npc_environment_blocked_ctx(s, ctx, hx, hy, 1)) + continue; + *out_x = hx; + *out_y = hy; + return 1; + } + + return 0; +} + +static void inf_sample_jad_healer_spawn( + InfernoState* s, + const InfernoContext* ctx, + const InfNPC* jad, + int* out_x, + int* out_y +) { + if (s->wave == 68 && + inf_try_sample_jad_healer_spawn(s, ctx, jad, 0, 5, 5, 8, out_x, out_y)) + return; + + if (inf_try_sample_jad_healer_spawn(s, ctx, jad, -5, 5, -4, 10, out_x, out_y)) + return; + + fprintf(stderr, "FATAL: no valid Jad healer spawn tile for Jad at (%d,%d) on wave %d\n", + jad->x, jad->y, s->wave + 1); + abort(); +} + +static void inf_jad_check_healers_ctx(InfernoState* s, const InfernoContext* ctx, int idx) { + InfNPC* npc = &s->npcs[idx]; + if (npc->type != INF_NPC_JAD || !npc->active) return; + if (inf_npc_jad(npc)->healer_spawned) return; + + if (npc->hp >= npc->max_hp / 2) return; + inf_npc_jad(npc)->healer_spawned = 1; + + int num_healers; + if (s->wave == 66) num_healers = 5; + else if (s->wave == 67) num_healers = 3; + else num_healers = 3; + + for (int h = 0; h < num_healers; h++) { + int slot = inf_find_free_npc(s); + if (slot < 0) break; + int hx = npc->x; + int hy = npc->y; + inf_sample_jad_healer_spawn(s, ctx, npc, &hx, &hy); + inf_init_npc(s, slot, INF_NPC_HEALER_JAD, hx, hy); + inf_npc_healer(&s->npcs[slot])->owner_idx = idx; + s->npcs[slot].aggro_target = idx; + } +} + +static void inf_jad_check_healers(InfernoState* s, int idx) { + inf_jad_check_healers_ctx(s, inf_legacy_context(), idx); +} + +static void inf_zuk_tick(InfernoState* s) { + if (!inf_is_final_wave(s)) return; + + int zuk_idx = inf_find_live_zuk_idx(s); + if (zuk_idx < 0) return; + InfNPC* zuk = &s->npcs[zuk_idx]; + + int si = s->zuk.shield_idx; + if (si >= 0 && s->npcs[si].active) { + InfNPC* shield = &s->npcs[si]; + if (s->zuk.shield_freeze > 0) { + s->zuk.shield_freeze--; + } else { + int ox = shield->x; + int oy = shield->y; + if (inf_npc_sets_collision_flag(shield->type)) + inf_unstamp_npc_collision_footprint(s, ox, oy, shield->size); + shield->x += s->zuk.shield_dir; + if (shield->x < 11) { + s->zuk.shield_freeze = 4; + s->zuk.shield_dir = 1; + } else if (shield->x > 35) { + s->zuk.shield_freeze = 4; + s->zuk.shield_dir = -1; + } + if (inf_npc_sets_collision_flag(shield->type)) + inf_stamp_npc_collision_footprint(s, shield->x, shield->y, shield->size); + } + } + + if (!s->zuk.has_paused && zuk->hp < 600) { + s->zuk.timer_paused = 1; + s->zuk.has_paused = 1; + } + + if (!s->zuk.jad_spawned && s->zuk.timer_paused && zuk->hp < 480 && + si >= 0 && s->npcs[si].active) { + s->zuk.jad_spawned = 1; + s->zuk.timer_paused = 0; + s->zuk.set_timer += 175; + inf_spawn_zuk_jad(s, si); + } + + if (!s->zuk.timer_paused) { + if (s->zuk.set_timer > 0) + s->zuk.set_timer--; + if (s->zuk.set_timer == 0) { + int m_slot = inf_find_free_npc(s); + if (m_slot >= 0) { + inf_init_npc(s, m_slot, INF_NPC_MAGER, 20, 36); + if (si >= 0) s->npcs[m_slot].aggro_target = si; + s->npcs[m_slot].stun_timer = 7; + } + int r_slot = inf_find_free_npc(s); + if (r_slot >= 0) { + inf_init_npc(s, r_slot, INF_NPC_RANGER, 29, 36); + if (si >= 0) s->npcs[r_slot].aggro_target = si; + s->npcs[r_slot].stun_timer = 9; + } + s->zuk.set_timer = s->zuk.set_interval; + } + } + + if (!s->zuk.healer_spawned && zuk->hp < 240) { + s->zuk.healer_spawned = 1; + s->zuk.enraged = 1; + inf_spawn_zuk_healers(s, zuk_idx); + } + + if (zuk->hp <= 0) { + for (int i = 0; i < INF_MAX_NPCS; i++) { + inf_deactivate_npc(s, i); + } + } +} + +static void inf_healer_apply_landed_heal(InfernoState* s, int idx) { + InfNPC* npc = &s->npcs[idx]; + InfHealerState* h = inf_npc_healer(npc); + int target_idx = h->target; + int heal_cap = (npc->type == INF_NPC_HEALER_ZUK) ? 25 : 20; + + h->target = -1; + if (target_idx < 0 || target_idx >= INF_MAX_NPCS) return; + if (!s->npcs[target_idx].active) return; + + int heal = encounter_rand_int(&s->rng_state, heal_cap); + int before = s->npcs[target_idx].hp; + s->npcs[target_idx].hp += heal; + if (s->npcs[target_idx].hp > s->npcs[target_idx].max_hp) + s->npcs[target_idx].hp = s->npcs[target_idx].max_hp; + float effective_heal = (float)(s->npcs[target_idx].hp - before); + s->tick_scratch.hp_restored += effective_heal; + if (s->npcs[target_idx].type == INF_NPC_JAD) + s->tick_scratch.hp_restored_jad += effective_heal; + if (s->npcs[target_idx].type == INF_NPC_ZUK) + s->tick_scratch.hp_restored_zuk += effective_heal; +} + +static int inf_redemption_threshold(const InfernoState* s) { + return (s->player.base_hitpoints - 1) / 10; +} + +static void inf_apply_redemption_if_due(InfernoState* s, int before_hp) { + Player* p = &s->player; + if (p->prayer != PRAYER_REDEMPTION || p->current_prayer <= 0) + return; + if (p->current_hitpoints <= 0) + return; + + int threshold = inf_redemption_threshold(s); + int before_band = before_hp > 0 && before_hp <= threshold; + int after_band = p->current_hitpoints > 0 && p->current_hitpoints <= threshold; + if (!before_band && !after_band) + return; + + int old_offensive = p->offensive_prayer; + int heal = p->base_prayer / 4; + p->current_hitpoints += heal; + if (p->current_hitpoints > p->base_hitpoints) + p->current_hitpoints = p->base_hitpoints; + p->current_prayer = 0; + p->prayer = PRAYER_NONE; + p->offensive_prayer = OFFENSIVE_PRAYER_NONE; + p->prayer_drain_counter = 0; + p->prayer_just_activated = 0; + p->offensive_prayer_just_activated = 0; + if (old_offensive != OFFENSIVE_PRAYER_NONE) + encounter_recompute_loadout_max_hits(s->loadout_stats, INF_NUM_WEAPON_SETS, p); +} + +static void inf_damage_player(InfernoState* s, int damage) { + int before_hp = s->player.current_hitpoints; + encounter_damage_player(&s->player, damage, &s->tick_scratch.damage_received); + inf_apply_redemption_if_due(s, before_hp); +} + +static void inf_queue_pending_spark( + InfernoState* s, int src_x, int src_y, int x, int y, int damage, int ticks_remaining +) { + for (int i = 0; i < INF_MAX_PENDING_SPARKS; i++) { + if (s->pending_sparks[i].active) continue; + s->pending_sparks[i].active = 1; + s->pending_sparks[i].src_x = src_x; + s->pending_sparks[i].src_y = src_y; + s->pending_sparks[i].x = x; + s->pending_sparks[i].y = y; + s->pending_sparks[i].damage = damage; + s->pending_sparks[i].ticks_remaining = ticks_remaining; + s->pending_sparks[i].visual_emitted = 0; + return; + } + fprintf(stderr, "inferno: pending spark queue overflow at %d slots\n", + INF_MAX_PENDING_SPARKS); + abort(); +} + +static void inf_queue_zuk_healer_sparks(InfernoState* s, const InfNPC* npc) { + int clamped_x = s->player.x; + if (clamped_x < npc->x - 5) clamped_x = npc->x - 5; + if (clamped_x > npc->x + 4) clamped_x = npc->x + 4; + + int direct_damage = 5 + encounter_rand_int(&s->rng_state, 6); + inf_queue_pending_spark(s, npc->x, npc->y, clamped_x, s->player.y, + direct_damage, 3); + + int spark1_x = npc->x + encounter_rand_int(&s->rng_state, 11) - 5; + int spark1_y = 40 + encounter_rand_int(&s->rng_state, 4); + int spark1_damage = 5 + encounter_rand_int(&s->rng_state, 6); + inf_queue_pending_spark(s, npc->x, npc->y, spark1_x, spark1_y, + spark1_damage, 3); + + int spark2_x = npc->x + encounter_rand_int(&s->rng_state, 11) - 5; + int spark2_y = 40 + encounter_rand_int(&s->rng_state, 4); + int spark2_damage = 5 + encounter_rand_int(&s->rng_state, 6); + inf_queue_pending_spark(s, npc->x, npc->y, spark2_x, spark2_y, + spark2_damage, 3); +} + +static void inf_resolve_pending_sparks(InfernoState* s) { + for (int i = 0; i < INF_MAX_PENDING_SPARKS; i++) { + if (!s->pending_sparks[i].active) continue; + s->pending_sparks[i].ticks_remaining--; + if (s->pending_sparks[i].ticks_remaining > 0) continue; + + if (encounter_entity_footprints_overlap( + s->pending_sparks[i].x - 1, s->pending_sparks[i].y - 1, 3, + s->player.x, s->player.y, 1)) { + inf_damage_player(s, s->pending_sparks[i].damage); + s->tick_scratch.spark_damage += (float)s->pending_sparks[i].damage; + s->last_hit_by_type = INF_NPC_HEALER_ZUK; + } + s->pending_sparks[i].active = 0; + } +} + +static void inf_tick_npcs_ctx(InfernoState* s, InfernoContext* ctx) { + + inf_resolve_npc_target_hits(s); + inf_zuk_tick(s); + + int blob_split_x[INF_MAX_NPCS]; + int blob_split_y[INF_MAX_NPCS]; + int blob_split_parent[INF_MAX_NPCS]; + int blob_split_count = 0; + + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (!s->npcs[i].active) continue; + + if ((s->npcs[i].type == INF_NPC_HEALER_JAD || s->npcs[i].type == INF_NPC_HEALER_ZUK) && + s->npcs[i].death_ticks == 0) { + InfHealerState* h = inf_npc_healer(&s->npcs[i]); + if (h->timer > 0) { + h->timer--; + if (h->timer == 0) + inf_healer_apply_landed_heal(s, i); + } + } + + if (s->npcs[i].death_ticks > 0) { + if (osrs_npc_death_linger_tick(&s->npcs[i].death_ticks)) { + if (s->npcs[i].spawn_blob_splits_on_removal && + blob_split_count < INF_MAX_NPCS) { + blob_split_x[blob_split_count] = s->npcs[i].x; + blob_split_y[blob_split_count] = s->npcs[i].y; + blob_split_parent[blob_split_count] = i; + blob_split_count++; + } + inf_deactivate_npc(s, i); + } + continue; + } + + if (s->npcs[i].frozen_ticks > 0) s->npcs[i].frozen_ticks--; + + if (s->npcs[i].type == INF_NPC_MAGER && s->npcs[i].resurrect_cooldown > 0) + s->npcs[i].resurrect_cooldown--; + + if (s->npcs[i].type == INF_NPC_MELEER) + inf_meleer_dig_check_ctx(s, ctx, i); + + inf_npc_move_ctx(s, ctx, i); + inf_npc_attack_ctx(s, ctx, i); + + if (s->npcs[i].type == INF_NPC_JAD) + inf_jad_check_healers_ctx(s, ctx, i); + } + + for (int i = 0; i < blob_split_count; i++) { + InfNPCType split_types[3] = { + INF_NPC_BLOB_RANGE, INF_NPC_BLOB_MELEE, INF_NPC_BLOB_MAGE + }; + int split_offsets[3][2] = { + {1, 1}, {0, 0}, {2, 2} + }; + for (int sp = 0; sp < 3; sp++) { + int slot = -1; + for (int n = 0; n < INF_MAX_NPCS; n++) { + if (n == blob_split_parent[i] || s->npcs[n].active) continue; + slot = n; + break; + } + if (slot < 0) break; + inf_init_npc( + s, slot, split_types[sp], + blob_split_x[i] + split_offsets[sp][0], + blob_split_y[i] + split_offsets[sp][1]); + s->npcs[slot].attack_timer = 4; + } + } +} + +static void inf_tick_npcs(InfernoState* s) { + inf_tick_npcs_ctx(s, inf_legacy_context()); +} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc b/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc new file mode 100644 index 0000000000..9760f0b2a1 --- /dev/null +++ b/ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc @@ -0,0 +1,1350 @@ +#line 5216 "encounter_inferno.h" +#define INF_PLAYER_OBS_SIZE 75 +#define INF_PILLAR_OBS_SIZE (INF_NUM_PILLARS * 5) +#define INF_BASE_NPC_OBS_SIZE 896 +#define INF_TOTAL_NPC_OBS_SIZE INF_BASE_NPC_OBS_SIZE +#define INF_STEP_OUT_FORECAST_HORIZON 4 +#define INF_STEP_OUT_FORECAST_ACTION_FEATURES 8 +#define INF_STEP_OUT_FORECAST_OBS_SIZE (ENCOUNTER_MOVE_ACTIONS * INF_STEP_OUT_FORECAST_ACTION_FEATURES) +#define INF_FEATURES_PER_HIT 5 +#define INF_SPARK_OBS_SLOTS INF_MAX_PENDING_SPARKS +#define INF_FEATURES_PER_SPARK 7 +#define INF_PENDING_HIT_OBS_SIZE (INF_FEATURES_PER_HIT * ENCOUNTER_MAX_PENDING_HITS) +#define INF_PENDING_SPARK_OBS_SIZE (INF_FEATURES_PER_SPARK * INF_SPARK_OBS_SLOTS) +#define INF_NUM_OBS (INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE + INF_TOTAL_NPC_OBS_SIZE + INF_STEP_OUT_FORECAST_OBS_SIZE + INF_PENDING_HIT_OBS_SIZE + INF_PENDING_SPARK_OBS_SIZE) + +typedef struct { + int melee_count; + int ranged_count; + int magic_count; + int blob_scan_count; + int ranger_count; + int mager_count; + int max_hit; +} InfStepOutForecastTick; + +typedef struct { + int valid; + int land_x; + int land_y; + InfStepOutForecastTick ticks[INF_STEP_OUT_FORECAST_HORIZON]; + int same_tick_mixed_style_conflict; + int ranger_mager_offtick_opportunity; + int melee_fallback_exposure; +} InfStepOutForecastAction; + +typedef struct { + InfStepOutForecastAction actions[ENCOUNTER_MOVE_ACTIONS]; +} InfStepOutForecast; + +typedef struct { + int action_feature_mismatches; + int tick_feature_mismatches; + int dangerous_false_negatives; + int dangerous_false_positives; + int exact_safe_fast_dangerous; + int exact_dangerous_actions; + int fast_dangerous_actions; + int sampled_actions; + int max_hit_abs_error_sum; + int max_hit_abs_error_max; +} InfStepOutForecastOracleDiff; + +typedef struct { + int active; + InfNPCType type; + int x; + int y; + int size; + int hp; + int attack_timer; + int stun_timer; + int frozen_ticks; + int dig_freeze_timer; + int dig_attack_delay; + int blob_scanned_prayer; + int had_los_last_tick; + int jad_attack_style; + int attack_style; + int aggro_target; + int los_to_player_cache; +} InfForecastNpcLocal; + +typedef struct { + const InfernoState* state; + const InfernoContext* ctx; + uint8_t (*npc_flags)[INF_ARENA_HEIGHT]; + int player_x; + int player_y; +} InfForecastMoveCtx; + +static float inf_zuk_attack_timer_obs(const InfernoState* s) { + if (!inf_is_final_wave(s)) return 0.0f; + + int min_timer = 999; + int zuk_idx = inf_find_live_zuk_idx(s); + if (zuk_idx >= 0) { + int timer = s->npcs[zuk_idx].attack_timer; + if (timer < 0) timer = 0; + min_timer = timer; + } + + for (int h = 0; h < s->player_pending_hits.count; h++) { + const EncounterPendingHit* hit = &s->player_pending_hits.hits[h]; + if (hit->source_npc_type != INF_NPC_ZUK) continue; + if (hit->ticks_remaining < min_timer) + min_timer = hit->ticks_remaining; + } + + return (min_timer < 999) ? (float)min_timer / 10.0f : 0.0f; +} + +static int inf_npc_targets_player_for_obs(const InfernoState* s, const InfNPC* npc) { + InfTargetArea target = inf_npc_current_target_area(s, npc); + return target.is_player; +} + +static int inf_step_out_forecast_action_valid(InfernoState* s, int action) { + if (action == 0) return 1; + int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[action]; + int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[action]; + return inf_in_arena(nx, ny) && !inf_blocked_by_pillar(s, nx, ny, 1); +} + +static void inf_step_out_forecast_action_landing_ctx( + const InfernoState* s, + const InfernoContext* ctx, + int action_idx, + Player* moved, + int* valid +) { + *moved = s->player; + *valid = inf_step_out_forecast_action_valid((InfernoState*)s, action_idx); + if (action_idx == 0) return; + InfWalkCtx walk_ctx = { (InfernoState*)s, ctx }; + encounter_move_to_target( + moved, + ENCOUNTER_MOVE_TARGET_DX[action_idx], + ENCOUNTER_MOVE_TARGET_DY[action_idx], + inf_tile_walkable, + &walk_ctx); +} + +static Player inf_step_out_forecast_set_action_landing_ctx( + const InfernoState* s, + const InfernoContext* ctx, + int action_idx, + InfStepOutForecastAction* action +) { + Player moved; + int valid; + inf_step_out_forecast_action_landing_ctx( + s, ctx, action_idx, &moved, &valid); + action->valid = valid; + action->land_x = moved.x; + action->land_y = moved.y; + return moved; +} + +static int inf_step_out_forecast_duplicate_landing( + const InfStepOutForecast* forecast, + int action_idx +) { + const InfStepOutForecastAction* action = &forecast->actions[action_idx]; + for (int prior_idx = 0; prior_idx < action_idx; prior_idx++) { + const InfStepOutForecastAction* prior = &forecast->actions[prior_idx]; + if (prior->valid && + prior->land_x == action->land_x && + prior->land_y == action->land_y) { + return prior_idx; + } + } + return -1; +} + +static int inf_step_out_forecast_prepare_unique_action_ctx( + const InfernoState* s, + const InfernoContext* ctx, + InfStepOutForecast* out, + int action_idx, + int forecast_slot_count +) { + InfStepOutForecastAction* action = &out->actions[action_idx]; + Player moved = inf_step_out_forecast_set_action_landing_ctx( + s, ctx, action_idx, action); + if (!action->valid || forecast_slot_count == 0) return 0; + + int duplicate_idx = inf_step_out_forecast_duplicate_landing( + out, action_idx); + if (duplicate_idx >= 0) { + int valid = action->valid; + *action = out->actions[duplicate_idx]; + action->valid = valid; + action->land_x = moved.x; + action->land_y = moved.y; + return 0; + } + return 1; +} + +static void inf_step_out_forecast_record_style_mask( + InfStepOutForecastAction* action, + int tick_idx, + InfNPCType type, + const InfNPCStats* stats, + int style_mask +) { + if (tick_idx < 0 || tick_idx >= INF_STEP_OUT_FORECAST_HORIZON) return; + InfStepOutForecastTick* tick = &action->ticks[tick_idx]; + int styles = 0; + if (style_mask & INF_STYLE_MASK_MELEE) { + tick->melee_count++; + int max_hit = inf_npc_style_max_hit(stats, ATTACK_STYLE_MELEE); + if (max_hit > tick->max_hit) tick->max_hit = max_hit; + styles++; + } + if (style_mask & INF_STYLE_MASK_RANGED) { + tick->ranged_count++; + int max_hit = inf_npc_style_max_hit(stats, ATTACK_STYLE_RANGED); + if (max_hit > tick->max_hit) tick->max_hit = max_hit; + styles++; + } + if (style_mask & INF_STYLE_MASK_MAGIC) { + tick->magic_count++; + int max_hit = inf_npc_style_max_hit(stats, ATTACK_STYLE_MAGIC); + if (max_hit > tick->max_hit) tick->max_hit = max_hit; + styles++; + } + if (type == INF_NPC_RANGER) tick->ranger_count++; + if (type == INF_NPC_MAGER) tick->mager_count++; + if (styles >= 2 || ((tick->melee_count > 0) + + (tick->ranged_count > 0) + (tick->magic_count > 0)) >= 2) { + action->same_tick_mixed_style_conflict = 1; + } + if ((style_mask & INF_STYLE_MASK_MELEE) && + (type == INF_NPC_RANGER || type == INF_NPC_MAGER)) { + action->melee_fallback_exposure = 1; + } +} + +static void inf_step_out_forecast_record_blob_scan( + InfStepOutForecastAction* action, int tick_idx +) { + if (tick_idx < 0 || tick_idx >= INF_STEP_OUT_FORECAST_HORIZON) return; + action->ticks[tick_idx].blob_scan_count++; +} + +static int inf_forecast_jad_unknown_style_mask(const InfNPC* npc) { + if (npc->type != INF_NPC_JAD) return 0; + if (inf_npc_jad_const(npc)->attack_style != ATTACK_STYLE_NONE) return 0; + return INF_STYLE_MASK_RANGED | INF_STYLE_MASK_MAGIC; +} + +static AttackStyle inf_forecast_blob_scan_style(OverheadPrayer prayer) { + if (prayer == PRAYER_PROTECT_MAGIC) return ATTACK_STYLE_RANGED; + if (prayer == PRAYER_PROTECT_RANGED) return ATTACK_STYLE_MAGIC; + return ATTACK_STYLE_RANGED; +} + +static void inf_step_out_forecast_npc_attack( + InfernoState* s, + int idx, + InfStepOutForecastAction* action, + int tick_idx +) { + InfNPC* npc = &s->npcs[idx]; + if (!npc->active || npc->death_ticks > 0 || npc->hp <= 0) return; + const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; + if (npc->attack_timer > 0) npc->attack_timer--; + if (npc->stun_timer > 0) { npc->stun_timer--; return; } + if (npc->dig_freeze_timer > 0 || npc->dig_attack_delay > 0) return; + if (npc->type == INF_NPC_ZUK_SHIELD || npc->type == INF_NPC_NIBBLER) + return; + if (npc->aggro_target >= 0) return; + if (!inf_npc_targets_player_for_obs(s, npc)) return; + + int has_los_now = 0; + if (npc->type == INF_NPC_BLOB && stats->attack_range > 1) { + has_los_now = inf_npc_has_los(s, idx); + if (npc->blob_scanned_prayer < 0 && + has_los_now && + !npc->had_los_last_tick) { + npc->blob_scanned_prayer = (int)s->player.prayer; + npc->attack_style = inf_forecast_blob_scan_style(s->player.prayer); + npc->had_los_last_tick = has_los_now; + npc->attack_timer = stats->attack_speed; + inf_step_out_forecast_record_blob_scan(action, tick_idx); + return; + } + npc->had_los_last_tick = has_los_now; + } + if (npc->attack_timer > 0) return; + + if (npc->type != INF_NPC_BLOB && stats->attack_range > 1) + has_los_now = inf_npc_has_los(s, idx); + + if (stats->attack_range > 1 && + !(npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) && + !has_los_now) return; + + int dist = encounter_dist_to_npc( + s->player.x, s->player.y, npc->x, npc->y, npc->size); + if (dist == 0 || dist > stats->attack_range) return; + if (stats->attack_range == 1 && + !inf_npc_melee_contact_allows_attack_at_tile( + s->player.x, s->player.y, npc->type, + npc->x, npc->y, npc->size, dist)) + return; + + if (npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer < 0) { + npc->blob_scanned_prayer = (int)s->player.prayer; + npc->attack_style = inf_forecast_blob_scan_style(s->player.prayer); + npc->attack_timer = stats->attack_speed; + inf_step_out_forecast_record_blob_scan(action, tick_idx); + return; + } + + int style_mask = inf_forecast_jad_unknown_style_mask(npc); + if (style_mask == 0) { + int planned_style = inf_npc_planned_style_for_current_state(npc); + if (planned_style == ATTACK_STYLE_NONE) return; + style_mask = inf_attack_style_options_mask( + s, npc, stats, planned_style, dist); + } + if (style_mask == 0) return; + + inf_step_out_forecast_record_style_mask( + action, tick_idx, npc->type, stats, style_mask); + + if (npc->type == INF_NPC_BLOB) + npc->blob_scanned_prayer = -1; + if (npc->type == INF_NPC_JAD) + inf_npc_jad(npc)->attack_style = ATTACK_STYLE_NONE; + npc->attack_timer = stats->attack_speed; + if (npc->type == INF_NPC_JAD) + npc->attack_timer = inf_jad_attack_timer(s->wave); +} + +static int inf_collect_step_out_forecast_slots( + const InfernoState* s, + int slots[INF_MAX_NPCS] +) { + int count = 0; + for (int i = 0; i < INF_MAX_NPCS; i++) { + const InfNPC* npc = &s->npcs[i]; + if (!npc->active || npc->death_ticks > 0) continue; + slots[count++] = i; + } + return count; +} + +static void inf_step_out_forecast_tick( + InfernoState* sim, + const InfernoContext* ctx, + InfStepOutForecastAction* action, + int tick_idx, + const int slots[INF_MAX_NPCS], + int slot_count +) { + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + InfNPC* npc = &sim->npcs[i]; + if (npc->frozen_ticks > 0) npc->frozen_ticks--; + if (npc->type == INF_NPC_MAGER && npc->resurrect_cooldown > 0) + npc->resurrect_cooldown--; + inf_npc_move_ctx(sim, ctx, i); + inf_step_out_forecast_npc_attack(sim, i, action, tick_idx); + } +} + +#ifdef INF_PROFILE_ENABLED +static void inf_step_out_forecast_tick_profiled( + InfernoState* sim, + const InfernoContext* ctx, + InfStepOutForecastAction* action, + int tick_idx, + const int slots[INF_MAX_NPCS], + int slot_count +) { + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + InfNPC* npc = &sim->npcs[i]; + if (npc->frozen_ticks > 0) npc->frozen_ticks--; + if (npc->type == INF_NPC_MAGER && npc->resurrect_cooldown > 0) + npc->resurrect_cooldown--; + double t0 = INF_PROFILE_NOW_MS(); + inf_npc_move_ctx(sim, ctx, i); + double t1 = INF_PROFILE_NOW_MS(); + INF_PROFILE_ADD(INF_PROF_FORECAST_NPC_MOVE, t1 - t0); + inf_step_out_forecast_npc_attack(sim, i, action, tick_idx); + INF_PROFILE_ADD( + INF_PROF_FORECAST_NPC_ATTACK, + INF_PROFILE_NOW_MS() - t1); + } +} +#endif + +static void inf_restore_step_out_forecast_player_collision_flags( + InfernoState* sim, + int prev_player_x, + int prev_player_y +) { + int gx, gy; + if (inf_grid_index(prev_player_x, prev_player_y, &gx, &gy)) + sim->player_collision_flags[gx][gy] = 0; + if (inf_grid_index(sim->player.x, sim->player.y, &gx, &gy)) + sim->player_collision_flags[gx][gy] = 1; +} + +static void inf_restore_step_out_forecast_action_state( + InfernoState* sim, + const InfernoState* s, + const int slots[INF_MAX_NPCS], + int slot_count, + const Player* moved +) { + int prev_player_x = sim->player.x; + int prev_player_y = sim->player.y; + sim->player.x = moved->x; + sim->player.y = moved->y; + sim->player.is_running = moved->is_running; + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + sim->npcs[i] = s->npcs[i]; + } + sim->rng_state = s->rng_state; + inf_invalidate_los_cache(sim); + memcpy( + sim->npc_collision_flags, + s->npc_collision_flags, + sizeof(sim->npc_collision_flags)); + inf_restore_step_out_forecast_player_collision_flags( + sim, prev_player_x, prev_player_y); +} + +#ifdef INF_POISON_FORECAST +static void inf_poison_step_out_forecast_restore_source( + InfernoState* restore_source, + const InfernoState* s, + const int slots[INF_MAX_NPCS], + int slot_count +) { + memset(restore_source, 0xAA, sizeof(*restore_source)); + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + restore_source->npcs[i] = s->npcs[i]; + } + restore_source->rng_state = s->rng_state; + memcpy( + restore_source->npc_collision_flags, + s->npc_collision_flags, + sizeof(restore_source->npc_collision_flags)); +} + +static void inf_poison_step_out_forecast_check_player_flags( + const InfernoState* sim, + const InfernoState* s, + const Player* moved +) { + InfernoState oracle = *s; + memset( + oracle.player_collision_flags, + 0xAA, + sizeof(oracle.player_collision_flags)); + oracle.player.x = moved->x; + oracle.player.y = moved->y; + oracle.player.is_running = moved->is_running; + inf_rebuild_player_collision_flags(&oracle); + if (memcmp( + sim->player_collision_flags, + oracle.player_collision_flags, + sizeof(sim->player_collision_flags)) != 0) { + fprintf(stderr, "inferno forecast player collision restore mismatch\n"); + abort(); + } +} +#endif + +static void inf_step_out_forecast_finalize_action( + InfStepOutForecastAction* action +) { + for (int t = 0; t < INF_STEP_OUT_FORECAST_HORIZON - 1; t++) { + int ranger_then_mager = action->ticks[t].ranger_count > 0 && + action->ticks[t + 1].mager_count > 0; + int mager_then_ranger = action->ticks[t].mager_count > 0 && + action->ticks[t + 1].ranger_count > 0; + if (ranger_then_mager || mager_then_ranger) + action->ranger_mager_offtick_opportunity = 1; + } +} + +static int inf_step_out_forecast_tick_style_mask( + const InfStepOutForecastTick* tick +) { + int mask = 0; + if (tick->melee_count > 0) mask |= INF_STYLE_MASK_MELEE; + if (tick->ranged_count > 0) mask |= INF_STYLE_MASK_RANGED; + if (tick->magic_count > 0) mask |= INF_STYLE_MASK_MAGIC; + return mask; +} + +static int inf_step_out_forecast_tick_has_event( + const InfStepOutForecastTick* tick +) { + return tick->melee_count > 0 || + tick->ranged_count > 0 || + tick->magic_count > 0 || + tick->blob_scan_count > 0; +} + +static int inf_step_out_forecast_npc_fire_tick_idx(const InfNPC* npc) { + int tick_idx = npc->attack_timer - 1; + return tick_idx > 0 ? tick_idx : 0; +} + +static void inf_step_out_forecast_fast_static_npc( + const InfernoState* s, + const InfNPC* npc, + InfStepOutForecastAction* action +) { + if (!npc->active || npc->death_ticks > 0 || npc->hp <= 0) return; + const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; + if (npc->stun_timer > 0) return; + if (npc->dig_freeze_timer > 0 || npc->dig_attack_delay > 0) return; + if (npc->type == INF_NPC_ZUK_SHIELD || npc->type == INF_NPC_NIBBLER) + return; + if (npc->aggro_target >= 0) return; + + int dist = encounter_dist_to_npc( + action->land_x, action->land_y, npc->x, npc->y, npc->size); + if (dist == 0 || dist > stats->attack_range) return; + if (stats->attack_range == 1 && + !inf_npc_melee_contact_allows_attack_at_tile( + action->land_x, action->land_y, npc->type, + npc->x, npc->y, npc->size, dist)) + return; + + int has_los_now = 1; + if (stats->attack_range > 1) + has_los_now = inf_npc_has_los_to_tile(s, npc, action->land_x, action->land_y); + + if (npc->type == INF_NPC_BLOB && + npc->blob_scanned_prayer < 0 && + has_los_now && + !npc->had_los_last_tick) { + inf_step_out_forecast_record_blob_scan(action, 0); + return; + } + + int tick_idx = inf_step_out_forecast_npc_fire_tick_idx(npc); + if (tick_idx >= INF_STEP_OUT_FORECAST_HORIZON) return; + + if (stats->attack_range > 1 && + !(npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) && + !has_los_now) return; + + if (npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer < 0) { + inf_step_out_forecast_record_blob_scan(action, tick_idx); + return; + } + + int style_mask = inf_forecast_jad_unknown_style_mask(npc); + if (style_mask == 0) { + int planned_style = inf_npc_planned_style_for_current_state(npc); + if (planned_style == ATTACK_STYLE_NONE) return; + style_mask = inf_attack_style_options_mask_at_tile( + action->land_x, action->land_y, npc, stats, planned_style, dist); + } + if (style_mask == 0) return; + + inf_step_out_forecast_record_style_mask( + action, tick_idx, npc->type, stats, style_mask); +} + +static void inf_forecast_local_stamp_npc( + uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT], + int x, + int y, + int size +) { + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + int gx, gy; + if (inf_grid_index(x + dx, y + dy, &gx, &gy)) { + assert(npc_flags[gx][gy] < UINT8_MAX); + npc_flags[gx][gy]++; + } + } + } +} + +static void inf_forecast_local_unstamp_npc( + uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT], + int x, + int y, + int size +) { + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + int gx, gy; + if (inf_grid_index(x + dx, y + dy, &gx, &gy) && + npc_flags[gx][gy] > 0) { + npc_flags[gx][gy]--; + } + } + } +} + +static int inf_forecast_local_npc_flags_blocked( + uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT], + int x, + int y, + int size +) { + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + int gx, gy; + if (inf_grid_index(x + dx, y + dy, &gx, &gy) && + npc_flags[gx][gy] > 0) { + return 1; + } + } + } + return 0; +} + +static int inf_forecast_local_overlaps_player( + int x, int y, int size, int player_x, int player_y +) { + return !(x >= player_x + 1 || x + size <= player_x || + y >= player_y + 1 || y + size <= player_y); +} + +static int inf_forecast_local_npc_blocked(void* ctx, int x, int y, int size) { + InfForecastMoveCtx* mc = (InfForecastMoveCtx*)ctx; + if (inf_npc_environment_blocked_ctx( + (InfernoState*)mc->state, mc->ctx, x, y, size)) + return 1; + if (inf_forecast_local_overlaps_player( + x, y, size, mc->player_x, mc->player_y)) + return 1; + return inf_forecast_local_npc_flags_blocked(mc->npc_flags, x, y, size); +} + +static int inf_forecast_local_npc_blocked_ignore_player( + void* ctx, + int x, + int y, + int size +) { + InfForecastMoveCtx* mc = (InfForecastMoveCtx*)ctx; + if (inf_npc_environment_blocked_ctx( + (InfernoState*)mc->state, mc->ctx, x, y, size)) + return 1; + return inf_forecast_local_npc_flags_blocked(mc->npc_flags, x, y, size); +} + +static void inf_forecast_local_copy_npcs( + const InfernoState* s, + InfForecastNpcLocal npcs[INF_MAX_NPCS] +) { + memset(npcs, 0, sizeof(InfForecastNpcLocal) * INF_MAX_NPCS); + for (int i = 0; i < INF_MAX_NPCS; i++) { + const InfNPC* npc = &s->npcs[i]; + if (!npc->active || npc->death_ticks > 0) continue; + npcs[i] = (InfForecastNpcLocal){ + .active = 1, + .type = npc->type, + .x = npc->x, + .y = npc->y, + .size = inf_npc_effective_size(npc), + .hp = npc->hp, + .attack_timer = npc->attack_timer, + .stun_timer = npc->stun_timer, + .frozen_ticks = npc->frozen_ticks, + .dig_freeze_timer = npc->dig_freeze_timer, + .dig_attack_delay = npc->dig_attack_delay, + .blob_scanned_prayer = npc->blob_scanned_prayer, + .had_los_last_tick = npc->had_los_last_tick, + .jad_attack_style = (npc->type == INF_NPC_JAD) + ? inf_npc_jad_const(npc)->attack_style + : ATTACK_STYLE_NONE, + .attack_style = npc->attack_style, + .aggro_target = npc->aggro_target, + .los_to_player_cache = -1, + }; + } +} + +static void inf_forecast_local_rebuild_npc_flags( + const InfForecastNpcLocal npcs[INF_MAX_NPCS], + uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT] +) { + memset(npc_flags, 0, INF_ARENA_WIDTH * INF_ARENA_HEIGHT * sizeof(uint8_t)); + for (int i = 0; i < INF_MAX_NPCS; i++) { + const InfForecastNpcLocal* npc = &npcs[i]; + if (!npc->active || npc->hp <= 0) continue; + if (!inf_npc_sets_collision_flag(npc->type)) continue; + inf_forecast_local_stamp_npc(npc_flags, npc->x, npc->y, npc->size); + } +} + +static int inf_forecast_local_has_los_to_area( + const InfernoState* s, + const InfForecastNpcLocal* npc, + int target_x, + int target_y, + int target_size +) { + const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; + return entity_has_line_of_sight(s->los_blockers, s->los_blocker_count, + npc->x, npc->y, npc->size, + target_x, target_y, target_size, + stats->attack_range); +} + +static int inf_forecast_local_has_los_to_tile( + const InfernoState* s, + const InfForecastNpcLocal* npc, + int target_x, + int target_y +) { + return inf_forecast_local_has_los_to_area(s, npc, target_x, target_y, 1); +} + +static int inf_forecast_local_has_los_to_player( + const InfernoState* s, + InfForecastNpcLocal* npc, + int target_x, + int target_y +) { + if (npc->los_to_player_cache >= 0) + return npc->los_to_player_cache; + npc->los_to_player_cache = inf_forecast_local_has_los_to_tile( + s, npc, target_x, target_y); + return npc->los_to_player_cache; +} + +static InfTargetArea inf_forecast_local_target_area( + const InfernoState* s, + const InfForecastNpcLocal npcs[INF_MAX_NPCS], + const InfForecastNpcLocal* npc, + int player_x, + int player_y +) { + if (npc->aggro_target >= 0 && npc->aggro_target < INF_MAX_NPCS && + npcs[npc->aggro_target].active) { + const InfForecastNpcLocal* target = &npcs[npc->aggro_target]; + return (InfTargetArea){ + .x = target->x, + .y = target->y, + .size = target->size, + .is_player = 0, + }; + } + + if (npc->type == INF_NPC_NIBBLER) { + int pillar_idx = s->nibbler_target_pillar; + if (pillar_idx >= 0 && pillar_idx < INF_NUM_PILLARS && + s->pillars[pillar_idx].active) { + return (InfTargetArea){ + .x = s->pillars[pillar_idx].x, + .y = s->pillars[pillar_idx].y, + .size = INF_PILLAR_SIZE, + .is_player = 0, + }; + } + } + + return (InfTargetArea){ + .x = player_x, + .y = player_y, + .size = 1, + .is_player = 1, + }; +} + +static int inf_forecast_local_planned_style( + const InfForecastNpcLocal* npc +) { + if (npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) { + OverheadPrayer scanned = (OverheadPrayer)npc->blob_scanned_prayer; + if (scanned == PRAYER_PROTECT_MAGIC) return ATTACK_STYLE_RANGED; + if (scanned == PRAYER_PROTECT_RANGED) return ATTACK_STYLE_MAGIC; + } + if (npc->type == INF_NPC_JAD) + return npc->jad_attack_style; + return npc->attack_style; +} + +static int inf_forecast_local_melee_fallback_possible_at_tile( + int player_x, + int player_y, + const InfForecastNpcLocal* npc, + const InfNPCStats* stats, + int planned_style, + int dist +) { + if (!stats->can_melee || planned_style == ATTACK_STYLE_MELEE || dist != 1) + return 0; + + return inf_npc_melee_contact_allows_attack_at_tile( + player_x, player_y, npc->type, npc->x, npc->y, npc->size, dist); +} + +static int inf_forecast_local_attack_style_options_mask_at_tile( + int player_x, + int player_y, + const InfForecastNpcLocal* npc, + const InfNPCStats* stats, + int planned_style, + int dist +) { + int mask = inf_attack_style_mask_bit(planned_style); + if (inf_forecast_local_melee_fallback_possible_at_tile( + player_x, player_y, npc, stats, planned_style, dist)) + mask |= INF_STYLE_MASK_MELEE; + return mask; +} + +static int inf_forecast_local_jad_unknown_style_mask( + const InfForecastNpcLocal* npc +) { + if (npc->type != INF_NPC_JAD) return 0; + if (npc->jad_attack_style != ATTACK_STYLE_NONE) return 0; + return INF_STYLE_MASK_RANGED | INF_STYLE_MASK_MAGIC; +} + +static void inf_forecast_local_record_overlap_danger( + const InfernoState* s, + InfForecastNpcLocal* npc, + InfStepOutForecastAction* action, + int tick_idx +) { + const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; + if (npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer < 0) { + npc->blob_scanned_prayer = (int)s->player.prayer; + inf_step_out_forecast_record_blob_scan(action, tick_idx); + } + + int style_mask = inf_forecast_local_jad_unknown_style_mask(npc); + if (style_mask == 0) { + int planned_style = inf_forecast_local_planned_style(npc); + style_mask = inf_attack_style_mask_bit(planned_style); + } + if (stats->can_melee) + style_mask |= INF_STYLE_MASK_MELEE; + if (style_mask == 0) return; + inf_step_out_forecast_record_style_mask( + action, tick_idx, npc->type, stats, style_mask); +} + +static void inf_forecast_local_move_npc( + const InfernoState* s, + const InfernoContext* ctx, + InfForecastNpcLocal npcs[INF_MAX_NPCS], + uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT], + int idx, + InfStepOutForecastAction* action, + int tick_idx +) { + InfForecastNpcLocal* npc = &npcs[idx]; + if (!npc->active || npc->hp <= 0) return; + if (npc->stun_timer > 0) return; + if (npc->dig_freeze_timer > 0) return; + if (npc->frozen_ticks > 0) return; + if (npc->type == INF_NPC_NIBBLER || npc->type == INF_NPC_ZUK_SHIELD) + return; + + const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; + if (!stats->can_move) return; + int uses_collision_flag = inf_npc_sets_collision_flag(npc->type); + if (uses_collision_flag) + inf_forecast_local_unstamp_npc(npc_flags, npc->x, npc->y, npc->size); + + if (inf_forecast_local_overlaps_player( + npc->x, npc->y, npc->size, action->land_x, action->land_y)) { + inf_forecast_local_record_overlap_danger(s, npc, action, tick_idx); + if (uses_collision_flag) + inf_forecast_local_stamp_npc(npc_flags, npc->x, npc->y, npc->size); + return; + } + + InfTargetArea target = inf_forecast_local_target_area( + s, npcs, npc, action->land_x, action->land_y); + if (target.is_player && npc->aggro_target >= 0) + npc->aggro_target = -1; + + int has_los = inf_forecast_local_has_los_to_area( + s, npc, target.x, target.y, target.size); + if (target.is_player) + npc->los_to_player_cache = has_los; + if (has_los) { + if (uses_collision_flag) + inf_forecast_local_stamp_npc(npc_flags, npc->x, npc->y, npc->size); + return; + } + if (npc->attack_timer > stats->attack_speed) { + if (uses_collision_flag) + inf_forecast_local_stamp_npc(npc_flags, npc->x, npc->y, npc->size); + return; + } + + InfForecastMoveCtx move_ctx = { + .state = s, + .ctx = ctx, + .npc_flags = npc_flags, + .player_x = action->land_x, + .player_y = action->land_y, + }; + EncounterNpcStepPolicy step_policy; + encounter_npc_blocked_fn blocked; + if (target.is_player) { + step_policy = stats->attack_range == 1 + ? ENCOUNTER_NPC_STEP_OSRS_AGGRO_STOP_AT_MELEE + : ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET; + blocked = inf_forecast_local_npc_blocked_ignore_player; + } else { + step_policy = stats->attack_range == 1 + ? ENCOUNTER_NPC_STEP_STOP_AT_MELEE + : ENCOUNTER_NPC_STEP_TRAVEL_TARGET; + blocked = inf_forecast_local_npc_blocked; + } + uint32_t rng = s->rng_state; + encounter_npc_step_toward_policy( + &npc->x, &npc->y, target.x, target.y, npc->size, + target.size, step_policy, blocked, &move_ctx, NULL, &rng); + npc->los_to_player_cache = -1; + + if (uses_collision_flag) + inf_forecast_local_stamp_npc(npc_flags, npc->x, npc->y, npc->size); +} + +static void inf_forecast_local_attack_npc( + const InfernoState* s, + InfForecastNpcLocal* npc, + InfStepOutForecastAction* action, + int tick_idx +) { + if (!npc->active || npc->hp <= 0) return; + const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; + if (npc->attack_timer > 0) npc->attack_timer--; + if (npc->stun_timer > 0) { npc->stun_timer--; return; } + if (npc->dig_freeze_timer > 0 || npc->dig_attack_delay > 0) return; + if (npc->type == INF_NPC_ZUK_SHIELD || npc->type == INF_NPC_NIBBLER) + return; + if (npc->aggro_target >= 0) return; + + if (inf_forecast_local_overlaps_player( + npc->x, npc->y, npc->size, action->land_x, action->land_y)) { + inf_forecast_local_record_overlap_danger(s, npc, action, tick_idx); + return; + } + + int has_los_now = 0; + if (npc->type == INF_NPC_BLOB && stats->attack_range > 1) { + has_los_now = inf_forecast_local_has_los_to_player( + s, npc, action->land_x, action->land_y); + if (npc->blob_scanned_prayer < 0 && + has_los_now && + !npc->had_los_last_tick) { + npc->blob_scanned_prayer = (int)s->player.prayer; + npc->attack_style = inf_forecast_blob_scan_style(s->player.prayer); + npc->had_los_last_tick = has_los_now; + npc->attack_timer = stats->attack_speed; + inf_step_out_forecast_record_blob_scan(action, tick_idx); + return; + } + npc->had_los_last_tick = has_los_now; + } + if (npc->attack_timer > 0) return; + + if (npc->type != INF_NPC_BLOB && stats->attack_range > 1) { + has_los_now = inf_forecast_local_has_los_to_player( + s, npc, action->land_x, action->land_y); + } + + if (stats->attack_range > 1 && + !(npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) && + !has_los_now) return; + + int dist = encounter_dist_to_npc( + action->land_x, action->land_y, npc->x, npc->y, npc->size); + if (dist == 0 || dist > stats->attack_range) return; + if (stats->attack_range == 1 && + !inf_npc_melee_contact_allows_attack_at_tile( + action->land_x, action->land_y, (InfNPCType)npc->type, + npc->x, npc->y, npc->size, dist)) + return; + + if (npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer < 0) { + npc->blob_scanned_prayer = (int)s->player.prayer; + npc->attack_style = inf_forecast_blob_scan_style(s->player.prayer); + npc->attack_timer = stats->attack_speed; + inf_step_out_forecast_record_blob_scan(action, tick_idx); + return; + } + + int style_mask = inf_forecast_local_jad_unknown_style_mask(npc); + if (style_mask == 0) { + int planned_style = inf_forecast_local_planned_style(npc); + if (planned_style == ATTACK_STYLE_NONE) return; + style_mask = inf_forecast_local_attack_style_options_mask_at_tile( + action->land_x, action->land_y, npc, + stats, planned_style, dist); + } + if (style_mask == 0) return; + + inf_step_out_forecast_record_style_mask( + action, tick_idx, npc->type, stats, style_mask); + + if (npc->type == INF_NPC_BLOB) + npc->blob_scanned_prayer = -1; + if (npc->type == INF_NPC_JAD) + npc->jad_attack_style = ATTACK_STYLE_NONE; + npc->attack_timer = stats->attack_speed; + if (npc->type == INF_NPC_JAD) + npc->attack_timer = inf_jad_attack_timer(s->wave); +} + +static int inf_forecast_npc_uses_readonly_sim(const InfNPC* npc) { + switch (npc->type) { + case INF_NPC_RANGER: + case INF_NPC_MAGER: + case INF_NPC_JAD: + return 1; + default: + return 0; + } +} + +static void inf_step_out_forecast_conservative_meleer_npc( + const InfNPC* npc, + InfStepOutForecastAction* action +) { + if (npc->type != INF_NPC_MELEER) return; + if (!npc->active || npc->death_ticks > 0 || npc->hp <= 0) return; + if (npc->dig_freeze_timer > 0 || npc->dig_attack_delay > 0) return; + + const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; + int dist = encounter_dist_to_npc( + action->land_x, action->land_y, npc->x, npc->y, npc->size); + if (dist > INF_STEP_OUT_FORECAST_HORIZON + stats->attack_range) return; + + int attack_ready_tick = npc->attack_timer > 0 ? npc->attack_timer - 1 : 0; + int frozen_move_delay = npc->frozen_ticks > 0 ? npc->frozen_ticks - 1 : 0; + int move_delay = npc->stun_timer > frozen_move_delay ? + npc->stun_timer : frozen_move_delay; + int contact_tick = dist <= stats->attack_range ? + 0 : move_delay + dist - stats->attack_range - 1; + int tick_idx = contact_tick > attack_ready_tick ? + contact_tick : attack_ready_tick; + if (tick_idx < 0 || tick_idx >= INF_STEP_OUT_FORECAST_HORIZON) return; + + inf_step_out_forecast_record_style_mask( + action, tick_idx, npc->type, stats, INF_STYLE_MASK_MELEE); +} + +static void inf_step_out_forecast_readonly_tick( + const InfernoState* s, + const InfernoContext* ctx, + InfForecastNpcLocal npcs[INF_MAX_NPCS], + uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT], + InfStepOutForecastAction* action, + int tick_idx, + const int slots[INF_MAX_NPCS], + int slot_count +) { + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + InfForecastNpcLocal* npc = &npcs[i]; + if (npc->frozen_ticks > 0) npc->frozen_ticks--; + inf_forecast_local_move_npc(s, ctx, npcs, npc_flags, i, action, tick_idx); + inf_forecast_local_attack_npc(s, npc, action, tick_idx); + } +} + +static void inf_build_step_out_forecast_fast_static_ctx( + const InfernoState* s, + const InfernoContext* ctx, + InfStepOutForecast* out +) { + memset(out, 0, sizeof(*out)); + int forecast_slots[INF_MAX_NPCS]; + int forecast_slot_count = inf_collect_step_out_forecast_slots( + s, forecast_slots); + for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { + if (!inf_step_out_forecast_prepare_unique_action_ctx( + s, ctx, out, action_idx, forecast_slot_count)) + continue; + InfStepOutForecastAction* action = &out->actions[action_idx]; + + for (int slot_idx = 0; slot_idx < forecast_slot_count; slot_idx++) { + int i = forecast_slots[slot_idx]; + inf_step_out_forecast_fast_static_npc(s, &s->npcs[i], action); + } + inf_step_out_forecast_finalize_action(action); + } +} + +static void inf_build_step_out_forecast_fast_readonly_ctx( + const InfernoState* s, + const InfernoContext* ctx, + InfStepOutForecast* out +) { + memset(out, 0, sizeof(*out)); + int forecast_slots[INF_MAX_NPCS]; + int forecast_slot_count = inf_collect_step_out_forecast_slots( + s, forecast_slots); + int readonly_slots[INF_MAX_NPCS]; + int readonly_slot_count = 0; + for (int slot_idx = 0; slot_idx < forecast_slot_count; slot_idx++) { + int i = forecast_slots[slot_idx]; + if (inf_forecast_npc_uses_readonly_sim(&s->npcs[i])) + readonly_slots[readonly_slot_count++] = i; + } + InfForecastNpcLocal base_npcs[INF_MAX_NPCS]; + uint8_t base_npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT]; + if (readonly_slot_count > 0) { + inf_forecast_local_copy_npcs(s, base_npcs); + inf_forecast_local_rebuild_npc_flags(base_npcs, base_npc_flags); + } + for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { + if (!inf_step_out_forecast_prepare_unique_action_ctx( + s, ctx, out, action_idx, forecast_slot_count)) + continue; + InfStepOutForecastAction* action = &out->actions[action_idx]; + + for (int slot_idx = 0; slot_idx < forecast_slot_count; slot_idx++) { + int i = forecast_slots[slot_idx]; + if (inf_forecast_npc_uses_readonly_sim(&s->npcs[i])) continue; + inf_step_out_forecast_fast_static_npc(s, &s->npcs[i], action); + inf_step_out_forecast_conservative_meleer_npc(&s->npcs[i], action); + } + + if (readonly_slot_count > 0) { + InfForecastNpcLocal npcs[INF_MAX_NPCS]; + uint8_t npc_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT]; + memcpy(npcs, base_npcs, sizeof(base_npcs)); + memcpy(npc_flags, base_npc_flags, sizeof(base_npc_flags)); + + for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { + inf_step_out_forecast_readonly_tick( + s, ctx, npcs, npc_flags, action, tick_idx, + readonly_slots, readonly_slot_count); + } + } + inf_step_out_forecast_finalize_action(action); + } +} + +static void inf_build_step_out_forecast_exact_ctx( + const InfernoState* s, + const InfernoContext* ctx, + InfStepOutForecast* out +) { +#ifdef INF_PROFILE_ENABLED + int inf_prof_enabled = INF_PROFILE_ENABLED(); + int prof_valid_actions = 0; + int prof_distinct_landings = 0; + if (inf_prof_enabled) + INF_PROFILE_ADD(INF_PROF_FORECAST_CALLS, 1.0); +#endif + memset(out, 0, sizeof(*out)); + int forecast_slots[INF_MAX_NPCS]; + int forecast_slot_count = inf_collect_step_out_forecast_slots( + s, forecast_slots); + + int forecast_has_active_pillar = 0; + for (int p = 0; p < INF_NUM_PILLARS; p++) { + if (s->pillars[p].active) { forecast_has_active_pillar = 1; break; } + } +#ifdef INF_PROFILE_ENABLED + double base_copy_t0 = inf_prof_enabled ? INF_PROFILE_NOW_MS() : 0.0; +#endif + InfernoState sim = *s; +#ifdef INF_PROFILE_ENABLED + if (inf_prof_enabled) { + INF_PROFILE_ADD( + INF_PROF_FORECAST_STATE_COPY, + INF_PROFILE_NOW_MS() - base_copy_t0); + } +#endif + const InfernoState* restore_source = s; +#ifdef INF_POISON_FORECAST + InfernoState poison_restore_source; + inf_poison_step_out_forecast_restore_source( + &poison_restore_source, s, forecast_slots, forecast_slot_count); + restore_source = &poison_restore_source; +#endif + for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { + InfStepOutForecastAction* action = &out->actions[action_idx]; +#ifdef INF_PROFILE_ENABLED + double landing_t0 = inf_prof_enabled ? INF_PROFILE_NOW_MS() : 0.0; +#endif + Player moved = inf_step_out_forecast_set_action_landing_ctx( + s, ctx, action_idx, action); +#ifdef INF_PROFILE_ENABLED + if (inf_prof_enabled) { + INF_PROFILE_ADD( + INF_PROF_FORECAST_LANDING, + INF_PROFILE_NOW_MS() - landing_t0); + if (action->valid) { + prof_valid_actions++; + int duplicate_landing = 0; + for (int prior_idx = 0; prior_idx < action_idx; prior_idx++) { + const InfStepOutForecastAction* prior = + &out->actions[prior_idx]; + if (prior->valid && + prior->land_x == action->land_x && + prior->land_y == action->land_y) { + duplicate_landing = 1; + break; + } + } + if (!duplicate_landing) + prof_distinct_landings++; + } + } +#endif + if (!action->valid || forecast_slot_count == 0) continue; + if (!forecast_has_active_pillar) continue; + +#ifdef INF_PROFILE_ENABLED + double state_copy_t0 = inf_prof_enabled ? INF_PROFILE_NOW_MS() : 0.0; +#endif + inf_restore_step_out_forecast_action_state( + &sim, restore_source, forecast_slots, forecast_slot_count, &moved); +#ifdef INF_POISON_FORECAST + inf_poison_step_out_forecast_check_player_flags(&sim, s, &moved); +#endif +#ifdef INF_PROFILE_ENABLED + if (inf_prof_enabled) { + INF_PROFILE_ADD( + INF_PROF_FORECAST_STATE_COPY, + INF_PROFILE_NOW_MS() - state_copy_t0); + } +#endif + + for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { +#ifdef INF_PROFILE_ENABLED + if (inf_prof_enabled) { + inf_step_out_forecast_tick_profiled( + &sim, ctx, action, tick_idx, forecast_slots, forecast_slot_count); + continue; + } +#endif + inf_step_out_forecast_tick( + &sim, ctx, action, tick_idx, forecast_slots, forecast_slot_count); + } + inf_step_out_forecast_finalize_action(action); + } +#ifdef INF_PROFILE_ENABLED + if (inf_prof_enabled) { + INF_PROFILE_ADD( + INF_PROF_FORECAST_VALID_ACTIONS, + (double)prof_valid_actions); + INF_PROFILE_ADD( + INF_PROF_FORECAST_DISTINCT_LANDINGS, + (double)prof_distinct_landings); + } +#endif +} + +static void inf_build_step_out_forecast_ctx( + const InfernoState* s, + const InfernoContext* ctx, + InfStepOutForecast* out +) { + inf_build_step_out_forecast_exact_ctx(s, ctx, out); +} + +static void inf_build_step_out_forecast_mode_ctx( + const InfernoState* s, + const InfernoContext* ctx, + InfStepOutForecast* out +) { + switch (ctx->config.step_out_forecast_obs_mode) { + case INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT: + inf_build_step_out_forecast_exact_ctx(s, ctx, out); + return; + case INF_STEP_OUT_FORECAST_MODE_FAST_STATIC_TILE: + inf_build_step_out_forecast_fast_static_ctx(s, ctx, out); + return; + case INF_STEP_OUT_FORECAST_MODE_FAST_READONLY_MOVE: + inf_build_step_out_forecast_fast_readonly_ctx(s, ctx, out); + return; + default: + fprintf(stderr, + "inferno: step_out_forecast_obs_mode must be 1, 2, or 3 for forecast build, got %d\n", + ctx->config.step_out_forecast_obs_mode); + abort(); + } +} + +static int inf_step_out_forecast_action_dangerous( + const InfStepOutForecastAction* action +) { + if (action->same_tick_mixed_style_conflict) return 1; + for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { + const InfStepOutForecastTick* tick = &action->ticks[tick_idx]; + if (tick->max_hit >= 46) return 1; + } + return 0; +} + +static void inf_compare_step_out_forecasts( + const InfStepOutForecast* exact, + const InfStepOutForecast* fast, + InfStepOutForecastOracleDiff* out +) { + memset(out, 0, sizeof(*out)); + for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { + const InfStepOutForecastAction* a = &exact->actions[action_idx]; + const InfStepOutForecastAction* b = &fast->actions[action_idx]; + out->sampled_actions++; + if (a->valid != b->valid || + a->land_x != b->land_x || + a->land_y != b->land_y || + a->same_tick_mixed_style_conflict != b->same_tick_mixed_style_conflict || + a->ranger_mager_offtick_opportunity != b->ranger_mager_offtick_opportunity || + a->melee_fallback_exposure != b->melee_fallback_exposure) { + out->action_feature_mismatches++; + } + int exact_danger = inf_step_out_forecast_action_dangerous(a); + int fast_danger = inf_step_out_forecast_action_dangerous(b); + out->exact_dangerous_actions += exact_danger; + out->fast_dangerous_actions += fast_danger; + if (exact_danger && !fast_danger) + out->dangerous_false_negatives++; + if (!exact_danger && fast_danger) { + out->dangerous_false_positives++; + out->exact_safe_fast_dangerous++; + } + for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { + const InfStepOutForecastTick* at = &a->ticks[tick_idx]; + const InfStepOutForecastTick* bt = &b->ticks[tick_idx]; + if (at->melee_count != bt->melee_count || + at->ranged_count != bt->ranged_count || + at->magic_count != bt->magic_count || + at->blob_scan_count != bt->blob_scan_count || + at->ranger_count != bt->ranger_count || + at->mager_count != bt->mager_count || + at->max_hit != bt->max_hit) { + out->tick_feature_mismatches++; + } + int max_hit_error = at->max_hit - bt->max_hit; + if (max_hit_error < 0) max_hit_error = -max_hit_error; + out->max_hit_abs_error_sum += max_hit_error; + if (max_hit_error > out->max_hit_abs_error_max) + out->max_hit_abs_error_max = max_hit_error; + } + } +} + +static void inf_build_step_out_forecast( + const InfernoState* s, + InfStepOutForecast* out +) { + inf_build_step_out_forecast_ctx(s, inf_legacy_context(), out); +} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc new file mode 100644 index 0000000000..5c87721040 --- /dev/null +++ b/ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc @@ -0,0 +1,801 @@ +#line 862 "encounter_inferno.h" +static EncounterPendingHit* inf_queue_npc_pending_hit( + InfernoState* s, + int npc_idx, + int ticks_remaining, + int damage, + AttackStyle attack_style, + int spell_type, + int hit_success +) { + if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) { + fprintf(stderr, "inferno: invalid pending-hit npc slot %d\n", npc_idx); + abort(); + } + + InfNPC* npc = &s->npcs[npc_idx]; + if (!npc->active) { + fprintf(stderr, + "inferno: pending-hit queued for inactive npc tick=%d slot=%d " + "type=%d delay=%d style=%d spell=%d damage=%d\n", + s->tick, npc_idx, npc->type, ticks_remaining, attack_style, + spell_type, damage); + abort(); + } + + EncounterPendingHit hit = { + .damage = damage, + .ticks_remaining = ticks_remaining, + .attack_style = attack_style, + .check_prayer = 0, + .spell_type = spell_type, + .hit_success = hit_success, + .elysian_reduced = 0, + }; + return encounter_pending_hit_queue_push( + &npc->pending_hits, hit, "inferno npc", s->tick, npc_idx, npc->type); +} + +static EncounterPendingHit* inf_queue_player_pending_hit( + InfernoState* s, + int npc_idx, + int npc_type, + EncounterPendingHit hit +) { + hit.source_npc_slot = npc_idx; + return encounter_pending_hit_queue_push( + &s->player_pending_hits, hit, "inferno player", s->tick, npc_idx, npc_type); +} + +static void inf_shuffle_spawns(InfernoState* s) { + for (int i = 0; i < INF_NUM_SPAWN_POS; i++) + s->spawn_order[i] = i; + encounter_shuffle(s->spawn_order, INF_NUM_SPAWN_POS, &s->rng_state); +} + +static void inf_rebuild_los(InfernoState* s) { + s->los_blocker_count = 0; + for (int i = 0; i < INF_NUM_PILLARS; i++) { + if (s->pillars[i].active) { + LOSBlocker* b = &s->los_blockers[s->los_blocker_count++]; + b->x = s->pillars[i].x; + b->y = s->pillars[i].y; + b->size = INF_PILLAR_SIZE; + b->los_mask = LOS_FULL_MASK; + } + } +} + +static OsrsLosQuery inf_player_los_query(const InfernoState* s) { + return osrs_los_blockers(s->los_blockers, s->los_blocker_count); +} + +typedef struct { + int x; + int y; + int size; + int is_player; +} InfTargetArea; + +static InfTargetArea inf_npc_current_target_area(const InfernoState* s, const InfNPC* npc) { + if (npc->type == INF_NPC_NIBBLER) { + int pillar_idx = s->nibbler_target_pillar; + if (pillar_idx >= 0 && pillar_idx < INF_NUM_PILLARS && + s->pillars[pillar_idx].active) { + return (InfTargetArea){ + .x = s->pillars[pillar_idx].x, + .y = s->pillars[pillar_idx].y, + .size = INF_PILLAR_SIZE, + .is_player = 0, + }; + } + for (int i = 0; i < INF_NUM_PILLARS; i++) { + if (s->pillars[i].active) { + return (InfTargetArea){ + .x = s->pillars[i].x, + .y = s->pillars[i].y, + .size = INF_PILLAR_SIZE, + .is_player = 0, + }; + } + } + } + + if (npc->aggro_target >= 0 && npc->aggro_target < INF_MAX_NPCS && + s->npcs[npc->aggro_target].active) { + const InfNPC* target = &s->npcs[npc->aggro_target]; + return (InfTargetArea){ + .x = target->x, + .y = target->y, + .size = target->size, + .is_player = 0, + }; + } + + return (InfTargetArea){ + .x = s->player.x, + .y = s->player.y, + .size = 1, + .is_player = 1, + }; +} + +static int inf_npc_has_los_to_area( + const InfernoState* s, + const InfNPC* npc, + int target_x, + int target_y, + int target_size +) { + const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; + return entity_has_line_of_sight(s->los_blockers, s->los_blocker_count, + npc->x, npc->y, npc->size, + target_x, target_y, target_size, + stats->attack_range); +} + +static int inf_npc_has_los_to_tile( + const InfernoState* s, const InfNPC* npc, int target_x, int target_y +) { + return inf_npc_has_los_to_area(s, npc, target_x, target_y, 1); +} + +static int inf_npc_has_los_direct(const InfernoState* s, int i) { + const InfNPC* npc = &s->npcs[i]; + InfTargetArea target = inf_npc_current_target_area(s, npc); + return inf_npc_has_los_to_area(s, npc, target.x, target.y, target.size); +} + +static int inf_npc_has_los(InfernoState* s, int i) { + if (s->npc_los_cache[i] >= 0) + return s->npc_los_cache[i]; + int result = inf_npc_has_los_direct(s, i); + s->npc_los_cache[i] = (int8_t)result; + return result; +} + +static inline void inf_invalidate_los_cache(InfernoState* s) { + memset(s->npc_los_cache, -1, sizeof(s->npc_los_cache)); +} + +static inline void inf_invalidate_npc_los_cache(InfernoState* s, int i) { + s->npc_los_cache[i] = -1; +} + +static inline int inf_is_final_wave(const InfernoState* s) { + return s->wave == INF_NUM_WAVES - 1; +} + +static inline int inf_zuk_healer_tags_first_reward_gate_active( + const InfernoState* s, + const InfernoContext* ctx +) { + return ctx->config.zuk_healer_reward_mode == INF_ZUK_HEALER_REWARD_MODE_TAGS_FIRST && + s->zuk.healer_spawned && s->total_zuk_healer_tags < 4; +} + +static int inf_find_live_zuk_idx(const InfernoState* s) { + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (s->npcs[i].active && s->npcs[i].type == INF_NPC_ZUK) return i; + } + return -1; +} + +static int inf_is_live_zuk_healer_slot(const InfernoState* s, int npc_slot) { + if (npc_slot < 0 || npc_slot >= INF_MAX_NPCS) return 0; + const InfNPC* npc = &s->npcs[npc_slot]; + return npc->active && + npc->hp > 0 && + npc->death_ticks == 0 && + npc->type == INF_NPC_HEALER_ZUK; +} + +static int inf_npc_type_is_set_pressure(int type) { + return type == INF_NPC_MAGER || + type == INF_NPC_MELEER || + type == INF_NPC_RANGER; +} + +enum { + INF_STYLE_MASK_MELEE = 1 << 0, + INF_STYLE_MASK_RANGED = 1 << 1, + INF_STYLE_MASK_MAGIC = 1 << 2, +}; + +static inline int inf_attack_style_mask_bit(int style) { + if (style == ATTACK_STYLE_MELEE) return INF_STYLE_MASK_MELEE; + if (style == ATTACK_STYLE_RANGED) return INF_STYLE_MASK_RANGED; + if (style == ATTACK_STYLE_MAGIC) return INF_STYLE_MASK_MAGIC; + return 0; +} + +static inline int inf_cardinal_contact_with_npc(int px, int py, int nx, int ny, int npc_size) { + int cx = px < nx ? nx : (px > nx + npc_size - 1 ? nx + npc_size - 1 : px); + int cy = py < ny ? ny : (py > ny + npc_size - 1 ? ny + npc_size - 1 : py); + int dx = px - cx; if (dx < 0) dx = -dx; + int dy = py - cy; if (dy < 0) dy = -dy; + return dx + dy == 1; +} + +static inline int inf_npc_melee_contact_allows_attack_at_tile( + int player_x, + int player_y, + InfNPCType npc_type, + int npc_x, + int npc_y, + int npc_size, + int dist +) { + if (dist != 1) return 0; + if (npc_type == INF_NPC_RANGER || npc_type == INF_NPC_MAGER) + return 1; + return inf_cardinal_contact_with_npc( + player_x, player_y, npc_x, npc_y, npc_size); +} + +static inline int inf_melee_fallback_possible_at_tile( + int player_x, int player_y, const InfNPC* npc, const InfNPCStats* stats, + int planned_style, int dist +) { + if (!stats->can_melee || planned_style == ATTACK_STYLE_MELEE || dist != 1) + return 0; + + return inf_npc_melee_contact_allows_attack_at_tile( + player_x, player_y, npc->type, npc->x, npc->y, npc->size, dist); +} + +static inline int inf_melee_fallback_possible( + const InfernoState* s, const InfNPC* npc, const InfNPCStats* stats, + int planned_style, int dist +) { + return inf_melee_fallback_possible_at_tile( + s->player.x, s->player.y, npc, stats, planned_style, dist); +} + +static inline int inf_attack_style_options_mask_at_tile( + int player_x, int player_y, const InfNPC* npc, const InfNPCStats* stats, + int planned_style, int dist +) { + int mask = inf_attack_style_mask_bit(planned_style); + if (inf_melee_fallback_possible_at_tile( + player_x, player_y, npc, stats, planned_style, dist)) + mask |= INF_STYLE_MASK_MELEE; + return mask; +} + +static inline int inf_attack_style_options_mask( + const InfernoState* s, const InfNPC* npc, const InfNPCStats* stats, + int planned_style, int dist +) { + return inf_attack_style_options_mask_at_tile( + s->player.x, s->player.y, npc, stats, planned_style, dist); +} + +static inline int inf_attack_style_from_mask(int style_mask) { + if (style_mask == INF_STYLE_MASK_MELEE) return ATTACK_STYLE_MELEE; + if (style_mask == INF_STYLE_MASK_RANGED) return ATTACK_STYLE_RANGED; + if (style_mask == INF_STYLE_MASK_MAGIC) return ATTACK_STYLE_MAGIC; + return ATTACK_STYLE_NONE; +} + +static inline int inf_style_count_from_mask(int style_mask) { + int mask = style_mask & (INF_STYLE_MASK_MELEE | + INF_STYLE_MASK_RANGED | INF_STYLE_MASK_MAGIC); + return ((mask & INF_STYLE_MASK_MELEE) != 0) + + ((mask & INF_STYLE_MASK_RANGED) != 0) + + ((mask & INF_STYLE_MASK_MAGIC) != 0); +} + +static inline int inf_npc_style_max_hit( + const InfNPCStats* stats, int style +) { + if (style == ATTACK_STYLE_NONE) return 0; + int max_hit = osrs_npc_max_hit(style, + stats->str_level, stats->range_level, + stats->melee_str_bonus, stats->ranged_str_bonus, + stats->magic_base_dmg, stats->magic_dmg_pct); + if (stats->max_hit_cap > 0 && max_hit > stats->max_hit_cap) + max_hit = stats->max_hit_cap; + return max_hit; +} + +static inline int inf_npc_style_mask_max_hit( + const InfNPCStats* stats, int style_mask +) { + int max_hit = 0; + if (style_mask & INF_STYLE_MASK_MELEE) { + int style_max = inf_npc_style_max_hit(stats, ATTACK_STYLE_MELEE); + if (style_max > max_hit) max_hit = style_max; + } + if (style_mask & INF_STYLE_MASK_RANGED) { + int style_max = inf_npc_style_max_hit(stats, ATTACK_STYLE_RANGED); + if (style_max > max_hit) max_hit = style_max; + } + if (style_mask & INF_STYLE_MASK_MAGIC) { + int style_max = inf_npc_style_max_hit(stats, ATTACK_STYLE_MAGIC); + if (style_max > max_hit) max_hit = style_max; + } + return max_hit; +} + +static inline int inf_attack_style_obs_preview(int style_mask) { + int primary_mask = style_mask & (INF_STYLE_MASK_RANGED | INF_STYLE_MASK_MAGIC); + int primary_style = inf_attack_style_from_mask(primary_mask); + if (primary_style != ATTACK_STYLE_NONE) + return primary_style; + return inf_attack_style_from_mask(style_mask); +} + +static inline int inf_attack_style_telegraph_mask( + const InfernoState* s, const InfNPC* npc, const InfNPCStats* stats, + int planned_style, int dist +) { + if (npc->type == INF_NPC_JAD) + return inf_attack_style_mask_bit(planned_style); + return inf_attack_style_options_mask(s, npc, stats, planned_style, dist); +} + +typedef struct { + int edge_distance; + int style_mask; + int telegraph_style_mask; + int can_attack_if_ready; + int can_attack_this_tick; +} InfNpcPlayerThreat; + +static inline int inf_npc_planned_style_for_current_state(const InfNPC* npc) { + if (npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) { + OverheadPrayer scanned = (OverheadPrayer)npc->blob_scanned_prayer; + if (scanned == PRAYER_PROTECT_MAGIC) return ATTACK_STYLE_RANGED; + if (scanned == PRAYER_PROTECT_RANGED) return ATTACK_STYLE_MAGIC; + } + if (npc->type == INF_NPC_JAD) + return inf_npc_jad_const(npc)->attack_style; + return npc->attack_style; +} + +static inline int inf_npc_type_has_player_pressure(InfNPCType type) { + return type != INF_NPC_NIBBLER && + type != INF_NPC_ZUK && + type != INF_NPC_ZUK_SHIELD && + type != INF_NPC_HEALER_ZUK; +} + +static inline InfNpcPlayerThreat inf_npc_player_threat( + const InfernoState* s, + const InfNPC* npc +) { + InfNpcPlayerThreat threat = {0}; + if (!npc->active || npc->death_ticks > 0) + return threat; + if (!inf_npc_type_has_player_pressure(npc->type)) + return threat; + + InfTargetArea target = inf_npc_current_target_area(s, npc); + if (!target.is_player) + return threat; + + const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; + int dist = encounter_dist_to_npc( + s->player.x, s->player.y, npc->x, npc->y, npc->size); + threat.edge_distance = dist; + if (dist == 0 || dist > stats->attack_range) + return threat; + if (stats->attack_range == 1 && + !inf_npc_melee_contact_allows_attack_at_tile( + s->player.x, s->player.y, npc->type, + npc->x, npc->y, npc->size, dist)) + return threat; + + if (stats->attack_range > 1 && + !(npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) && + !inf_npc_has_los_to_tile(s, npc, s->player.x, s->player.y)) + return threat; + + threat.can_attack_if_ready = 1; + + int planned_style = inf_npc_planned_style_for_current_state(npc); + if (planned_style != ATTACK_STYLE_NONE) { + threat.style_mask = inf_attack_style_options_mask( + s, npc, stats, planned_style, dist); + threat.telegraph_style_mask = inf_attack_style_telegraph_mask( + s, npc, stats, planned_style, dist); + } + + threat.can_attack_this_tick = npc->attack_timer <= 1 && + npc->stun_timer == 0 && + npc->dig_freeze_timer == 0 && + npc->dig_attack_delay == 0; + return threat; +} + +static inline int inf_npc_is_live_player_target( + const InfernoState* s, + int npc_idx +) { + if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; + const InfNPC* npc = &s->npcs[npc_idx]; + return npc->active && npc->death_ticks == 0 && npc->hp > 0 && + npc->type != INF_NPC_ZUK_SHIELD; +} + +typedef struct { + int if_ready_total; + int if_ready_melee; + int if_ready_ranged; + int if_ready_magic; + int if_ready_max_hit; + int if_ready_sum_max_hit; + int this_tick_total; + int this_tick_melee; + int this_tick_ranged; + int this_tick_magic; + int this_tick_max_hit; + int this_tick_sum_max_hit; + int timer_lte_1; + int timer_lte_2; + int timer_lte_4; + int if_ready_style_count; + int this_tick_style_count; + int nearest_timer; +} InfNpcPressureSummary; + +static inline int inf_npc_pressure_timer(const InfNPC* npc) { + return npc->attack_timer > 0 ? npc->attack_timer : 1; +} + +static inline int inf_npc_attack_unblocked_for_pressure(const InfNPC* npc) { + return npc->stun_timer == 0 && + npc->dig_freeze_timer == 0 && + npc->dig_attack_delay == 0; +} + +static inline InfNpcPressureSummary inf_npc_pressure_summary( + const InfernoState* s +) { + InfNpcPressureSummary out = { .nearest_timer = 999 }; + int if_ready_style_mask = 0; + int this_tick_style_mask = 0; + + for (int i = 0; i < INF_MAX_NPCS; i++) { + const InfNPC* npc = &s->npcs[i]; + InfNpcPlayerThreat threat = inf_npc_player_threat(s, npc); + if (!threat.can_attack_if_ready || !threat.style_mask) + continue; + + const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; + int max_hit = inf_npc_style_mask_max_hit(stats, threat.style_mask); + out.if_ready_total++; + if (threat.style_mask & INF_STYLE_MASK_MELEE) out.if_ready_melee++; + if (threat.style_mask & INF_STYLE_MASK_RANGED) out.if_ready_ranged++; + if (threat.style_mask & INF_STYLE_MASK_MAGIC) out.if_ready_magic++; + if (max_hit > out.if_ready_max_hit) out.if_ready_max_hit = max_hit; + out.if_ready_sum_max_hit += max_hit; + if_ready_style_mask |= threat.style_mask; + + if (inf_npc_attack_unblocked_for_pressure(npc)) { + int timer = inf_npc_pressure_timer(npc); + if (timer < out.nearest_timer) out.nearest_timer = timer; + if (timer <= 1) out.timer_lte_1++; + if (timer <= 2) out.timer_lte_2++; + if (timer <= 4) out.timer_lte_4++; + } + + if (!threat.can_attack_this_tick) + continue; + + out.this_tick_total++; + if (threat.style_mask & INF_STYLE_MASK_MELEE) out.this_tick_melee++; + if (threat.style_mask & INF_STYLE_MASK_RANGED) out.this_tick_ranged++; + if (threat.style_mask & INF_STYLE_MASK_MAGIC) out.this_tick_magic++; + if (max_hit > out.this_tick_max_hit) out.this_tick_max_hit = max_hit; + out.this_tick_sum_max_hit += max_hit; + this_tick_style_mask |= threat.style_mask; + } + + out.if_ready_style_count = inf_style_count_from_mask(if_ready_style_mask); + out.this_tick_style_count = inf_style_count_from_mask(this_tick_style_mask); + return out; +} + +static inline int inf_barrage_total_target_count( + const InfernoState* s, + int primary_idx +) { + if (primary_idx < 0 || primary_idx >= INF_MAX_NPCS) return 0; + const InfNPC* primary = &s->npcs[primary_idx]; + if (!primary->active || primary->death_ticks > 0 || + primary->type == INF_NPC_ZUK_SHIELD) + return 0; + + int count = 1; + for (int i = 0; i < INF_MAX_NPCS && count < BARRAGE_MAX_HITS; i++) { + if (i == primary_idx) continue; + const InfNPC* npc = &s->npcs[i]; + if (!npc->active || npc->death_ticks > 0 || + npc->type == INF_NPC_ZUK_SHIELD) + continue; + + int dx = npc->x - primary->x; + int dy = npc->y - primary->y; + if (dx >= -1 && dx <= 1 && dy >= -1 && dy <= 1) + count++; + } + return count; +} + +static inline int inf_pending_hit_obs_timer(const EncounterPendingHit* ph) { + if (ph->check_prayer && ph->prayer_check_delay > 0) + return ph->prayer_check_delay; + return ph->ticks_remaining; +} + +#define INF_JAD_PROJECTILE_DELAY 3 + +static inline EncounterProjectileDelayOptions inf_npc_projectile_options( + InfNPCType type, int style +) { + EncounterProjectileDelayOptions options = {0}; + if (type == INF_NPC_MAGER && style == ATTACK_STYLE_MAGIC) { + options.visual_delay_ticks = 2; + options.visual_hit_early_ticks = -1; + } else if (type == INF_NPC_RANGER && style == ATTACK_STYLE_RANGED) { + options.reduce_delay = -2; + options.visual_delay_ticks = 3; + } else if (type == INF_NPC_JAD && style != ATTACK_STYLE_MELEE) { + options.reduce_delay = INF_JAD_PROJECTILE_DELAY; + options.visual_hit_early_ticks = -1; + } else if (type == INF_NPC_ZUK) { + options.set_delay = 4; + options.visual_delay_ticks = 2; + } + return options; +} + +static inline EncounterProjectileTiming inf_npc_projectile_timing( + InfNPCType type, int style, int distance +) { + return encounter_projectile_timing( + distance, 0, encounter_projectile_delay_kind_for_style((AttackStyle)style), + inf_npc_projectile_options(type, style)); +} + +static inline EncounterProjectileTiming inf_player_projectile_timing( + int style, uint8_t weapon, int is_special, int distance +) { + EncounterProjectileDelayKind kind = encounter_projectile_delay_kind_for_style((AttackStyle)style); + EncounterProjectileDelayOptions options = {0}; + if (style == ATTACK_STYLE_RANGED) { + if (weapon == ITEM_TOXIC_BLOWPIPE) { + kind = ENCOUNTER_PROJECTILE_DELAY_THROWN; + options.visual_delay_ticks = 1; + if (is_special) { + options.reduce_delay = -1; + options.visual_hit_early_ticks = 1; + } + } else if (weapon == ITEM_TWISTED_BOW) { + options.visual_delay_ticks = 1; + } + } + return encounter_projectile_timing(distance, 1, kind, options); +} + +static inline AttackStyle inf_jad_roll_primary_style(uint32_t* rng_state) { + return (encounter_rand_int(rng_state, 2) == 0) + ? ATTACK_STYLE_RANGED + : ATTACK_STYLE_MAGIC; +} + +static inline int inf_npc_attack_anim_id(const InfNPC* npc, const NpcModelMapping* nm) { + if (npc->resurrecting_this_tick) + return INF_GEN_ANIM_MAGER_RESURRECT; + if (npc->type == INF_NPC_RANGER && + npc->attack_style_this_tick == ATTACK_STYLE_MELEE) + return INF_GEN_ANIM_RANGER_ATTACK_MELEE; + if (npc->type == INF_NPC_MAGER && + npc->attack_style_this_tick == ATTACK_STYLE_MELEE) + return INF_GEN_ANIM_MAGER_ATTACK_MELEE; + + if (npc->type == INF_NPC_JAD) { + switch (npc->attack_style_this_tick) { + case ATTACK_STYLE_MAGIC: + return INF_GEN_ANIM_JALTOK_JAD_ATTACK_MAGIC; + case ATTACK_STYLE_RANGED: + return INF_GEN_ANIM_JALTOK_JAD_ATTACK_RANGED; + case ATTACK_STYLE_MELEE: + return INF_GEN_ANIM_JALTOK_JAD_ATTACK_MELEE; + default: + return -1; + } + } + + if (!nm || nm->attack_anim == 65535) + return -1; + return (int)nm->attack_anim; +} + +static inline int inf_npc_death_anim_id(const InfNPC* npc, const NpcModelMapping* nm) { + switch (npc->type) { + case INF_NPC_NIBBLER: + return INF_GEN_ANIM_NIBBLER_DEATH; + case INF_NPC_BAT: + return INF_GEN_ANIM_BAT_DEATH; + case INF_NPC_BLOB: + case INF_NPC_BLOB_MELEE: + case INF_NPC_BLOB_RANGE: + case INF_NPC_BLOB_MAGE: + return INF_GEN_ANIM_BLOB_DEATH; + case INF_NPC_MELEER: + return INF_GEN_ANIM_MELEER_DEATH; + case INF_NPC_RANGER: + return INF_GEN_ANIM_RANGER_DEATH; + case INF_NPC_MAGER: + return INF_GEN_ANIM_MAGER_DEATH; + case INF_NPC_JAD: + return INF_GEN_ANIM_JALTOK_JAD_DEATH; + case INF_NPC_ZUK: + return INF_GEN_ANIM_TZKAL_ZUK_DEATH; + case INF_NPC_ZUK_SHIELD: + return INF_GEN_ANIM_ZUK_SHIELD_DEATH; + default: + return nm ? (int)nm->idle_anim : -1; + } +} + +static inline AttackStyle inf_choose_attack_style_for_tick( + uint32_t* rng_state, int style_mask +) { + AttackStyle primary_style = (AttackStyle)inf_attack_style_from_mask( + style_mask & ~INF_STYLE_MASK_MELEE); + if ((style_mask & INF_STYLE_MASK_MELEE) && primary_style != ATTACK_STYLE_NONE) + return (encounter_rand_int(rng_state, 2) == 0) ? ATTACK_STYLE_MELEE : primary_style; + return (AttackStyle)inf_attack_style_from_mask(style_mask); +} + +static inline int inf_dead_mob_is_resurrectable(InfNPCType type) { + switch (type) { + case INF_NPC_BAT: + case INF_NPC_BLOB: + case INF_NPC_MELEER: + case INF_NPC_RANGER: + case INF_NPC_MAGER: + return 1; + default: + return 0; + } +} + +static void inf_store_dead_mob(InfernoState* s, InfNPC* npc) { + if (s->dead_mob_count >= INF_MAX_DEAD_MOBS) return; + if (!inf_dead_mob_is_resurrectable(npc->type)) return; + if (npc->resurrection_count != 0) return; + + InfDeadMob* dm = &s->dead_mobs[s->dead_mob_count++]; + dm->type = npc->type; + dm->x = npc->x; + dm->y = npc->y; + dm->hp = npc->max_hp / 2; + dm->max_hp = npc->max_hp; +} + +static float inf_compute_reward_ctx(InfernoState* s, const InfernoContext* ctx); +static float inf_compute_reward(InfernoState* s); +static void inf_spawn_wave(InfernoState* s); +static void inf_tick_npcs_ctx(InfernoState* s, InfernoContext* ctx); +static void inf_tick_npcs(InfernoState* s); +static void inf_tick_player_ctx( + InfernoState* s, + InfernoContext* ctx, + const int* actions, + int can_attack); +static void inf_tick_player(InfernoState* s, const int* actions, int can_attack); +static void inf_apply_npc_death(InfernoState* s, int npc_idx); +static int inf_mager_resurrect_ctx(InfernoState* s, InfernoContext* ctx, int idx); +static void inf_queue_zuk_healer_sparks(InfernoState* s, const InfNPC* npc); +static void inf_resolve_pending_sparks(InfernoState* s); +static void inf_rebuild_player_collision_flags(InfernoState* s); +static void inf_refresh_current_obs_slots_ctx(InfernoState* s, const InfernoContext* ctx); +static void inf_refresh_current_obs_slots(InfernoState* s); + +static InfConfig inf_default_config(void) { + return (InfConfig){ + .start_wave = 0, + .loadout_profile_mode = INF_LOADOUT_PROFILE_MODE_MAX_ONLY, + .budget_loadout_fraction = 0.0f, + .damage_reward_coeff = 0.0f, + .offensive_prayer_reward_coeff = 0.0f, + .shield_penalty_coeff = 0.0f, + .tag_reward_coeff = 0.0f, + .late_start_supply_profile_scale = 1.0f, + .curriculum_agent = 0, + .curriculum_supply_jitter_mode = INF_CURRICULUM_SUPPLY_MODE_OFF, + .curriculum_supply_shared_jitter = 0.0f, + .curriculum_supply_brew_jitter = 0.0f, + .curriculum_supply_restore_jitter = 0.0f, + .curriculum_no_brew_mode = INF_CURRICULUM_SUPPLY_MODE_OFF, + .curriculum_no_brew_frac = 0.0f, + .supply_milestone_brew_reward_coeff = 0.0f, + .supply_milestone_restore_reward_coeff = 0.0f, + .death_penalty_coeff = 0.0f, + .terminal_penalty_enabled = 0, + .step_out_forecast_obs_enabled = 1, + .step_out_forecast_obs_mode = INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT, + .phase_900_bonus = 0.0f, + .phase_600_bonus = 0.0f, + .phase_300_bonus = 0.0f, + .shield_penalty_episode_cap = 0.0f, + .jad_damage_reward_coeff = 0.0f, + .zuk_healer_damage_reward_coeff = 0.0f, + .set_damage_reward_coeff = 0.0f, + .jad_kill_bonus = 0.0f, + .zuk_healer_kill_bonus = 0.0f, + .set_kill_bonus = 0.0f, + .post_healer_zuk_damage_coeff = 0.0f, + .post_healer_set_damage_reward_coeff = 0.0f, + .post_healer_set_kill_bonus = 0.0f, + .post_healer_set_alive_tick_penalty_coeff = 0.0f, + .post_healer_set_alive_penalty_cap = 0.0f, + .zuk_healer_phase_hp_delta_coeff = 0.0f, + .zuk_untagged_healer_tick_penalty_coeff = 0.0f, + .zuk_untagged_healer_target_bonus_coeff = 0.0f, + .zuk_safe_untagged_healer_target_bonus_coeff = 0.0f, + .zuk_untagged_healer_nonmagic_attack_bonus_coeff = 0.0f, + .zuk_healer_mage_attack_penalty_coeff = 0.0f, + .zuk_safe_untagged_healer_target_mask = 0, + .zuk_force_safe_untagged_healer_target_mask = 0, + .zuk_healer_reward_mode = INF_ZUK_HEALER_REWARD_MODE_BASELINE, + .post_jad_zuk_multiplier = 1.0f, + .jad_alive_zuk_multiplier = 1.0f, + .oracle_mode = 0, + .shield_tag_reward_coeff = 0.0f, + .joseph_reward_mode = INF_JOSEPH_REWARD_MODE_OFF, + }; +} + +static void inf_init_context_typed(InfernoContext* ctx) { + memset(ctx, 0, sizeof(*ctx)); + ctx->config = inf_default_config(); +} + +static void inf_init_context(EncounterContext* context) { + inf_init_context_typed((InfernoContext*)context); +} + +static void inf_destroy_context(EncounterContext* context) { + (void)context; +} + +static InfernoContext* inf_legacy_context(void) { + static InfernoContext ctx; + static Log log; + static int initialized = 0; + if (!initialized) { + inf_init_context_typed(&ctx); + ctx.log = &log; + initialized = 1; + } + return &ctx; +} + +static void inf_init_state_typed(InfernoState* s, InfernoContext* ctx) { + inf_build_npc_stats(); + memset(s, 0, sizeof(*s)); + s->rng_state = 12345; + s->start_wave = ctx ? ctx->config.start_wave : 0; + s->active_loadout_profile = INF_LOADOUT_PROFILE_MAX; + memset(s->npc_los_cache, -1, sizeof(s->npc_los_cache)); +} + +static void inf_init_state(EncounterState* state, EncounterContext* context) { + inf_init_state_typed((InfernoState*)state, (InfernoContext*)context); +} + +static EncounterState* inf_create(void) { + InfernoState* s = (InfernoState*)calloc(1, sizeof(InfernoState)); + inf_init_state_typed(s, inf_legacy_context()); + return (EncounterState*)s; +} + +static void inf_destroy(EncounterState* state) { + free(state); +} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc b/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc new file mode 100644 index 0000000000..6d1a4000a9 --- /dev/null +++ b/ocean/osrs/encounters/inferno/encounter_inferno_lab.inc @@ -0,0 +1,901 @@ +#line 5567 "encounter_inferno.h" +#define INF_LAB_PILLAR_CONTEXT_RADIUS (INF_PILLAR_SIZE + 2) + +typedef enum { + INF_LAB_COMMAND_NONE = 0, + INF_LAB_COMMAND_RESET, + INF_LAB_COMMAND_SET_PLAYER, + INF_LAB_COMMAND_SPAWN_NPC, + INF_LAB_COMMAND_MOVE_NPC, + INF_LAB_COMMAND_DELETE_NPC, + INF_LAB_COMMAND_KILL_NPC, + INF_LAB_COMMAND_SET_NPC_HP, + INF_LAB_COMMAND_SET_NPC_TIMER, + INF_LAB_COMMAND_SET_PILLAR, + INF_LAB_COMMAND_SPAWN_WAVE, + INF_LAB_COMMAND_CLEAR_NPCS, + INF_LAB_COMMAND_STEP_TICKS, +} InfLabCommandKind; + +typedef enum { + INF_LAB_LINE_NONE = 0, + INF_LAB_LINE_FORECAST, + INF_LAB_LINE_DUMP, +} InfLabLineResult; + +typedef enum { + INF_LAB_PILLAR_REMOVED = 0, + INF_LAB_PILLAR_ACTIVE, +} InfLabPillarState; + +typedef EncounterLabOptionalInt InfLabOptionalInt; +#define INF_LAB_OPTIONAL_INT_UNSET ENCOUNTER_LAB_OPTIONAL_INT_UNSET +#define INF_LAB_OPTIONAL_INT_SET ENCOUNTER_LAB_OPTIONAL_INT_SET + +typedef struct { + int x; + int y; +} InfLabTileCommand; + +typedef struct { + int slot; + InfNPCType type; + int x; + int y; + InfLabOptionalInt hp; + InfLabOptionalInt timer; +} InfLabSpawnNpcCommand; + +typedef struct { + int slot; + int x; + int y; +} InfLabMoveNpcCommand; + +typedef struct { + int slot; +} InfLabNpcSlotCommand; + +typedef struct { + int slot; + int hp; +} InfLabNpcHpCommand; + +typedef struct { + int slot; + int timer; +} InfLabNpcTimerCommand; + +typedef struct { + int pillar_idx; + InfLabPillarState state; + InfLabOptionalInt hp; +} InfLabPillarCommand; + +typedef struct { + int wave; +} InfLabWaveCommand; + +typedef struct { + int ticks; +} InfLabStepTicksCommand; + +typedef struct { + uint32_t seed; +} InfLabResetCommand; + +typedef struct { + InfLabCommandKind kind; + union { + InfLabResetCommand reset; + InfLabTileCommand tile; + InfLabSpawnNpcCommand spawn_npc; + InfLabMoveNpcCommand move_npc; + InfLabNpcSlotCommand npc_slot; + InfLabNpcHpCommand npc_hp; + InfLabNpcTimerCommand npc_timer; + InfLabPillarCommand pillar; + InfLabWaveCommand wave; + InfLabStepTicksCommand step_ticks; + } as; +} InfernoLabCommand; + +typedef EncounterLabString InfLabString; + +static void inf_lab_abort(const char* fmt, ...) { + va_list args; + fprintf(stderr, "inferno lab: "); + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + fprintf(stderr, "\n"); + abort(); +} + +static void inf_lab_require_slot(int slot) { + if (slot < 0 || slot >= INF_MAX_NPCS) + inf_lab_abort("npc slot must be in [0,%d], got %d", INF_MAX_NPCS - 1, slot); +} + +static void inf_lab_require_type(int type) { + if (type < 0 || type >= INF_NUM_NPC_TYPES) + inf_lab_abort("npc type must be in [0,%d], got %d", INF_NUM_NPC_TYPES - 1, type); +} + +static void inf_lab_require_player_tile(InfernoState* s, int x, int y) { + if (!inf_in_arena(x, y)) + inf_lab_abort("player tile out of arena: (%d,%d)", x, y); + if (inf_blocked_by_pillar(s, x, y, 1)) + inf_lab_abort("player tile is blocked by a pillar: (%d,%d)", x, y); +} + +static void inf_lab_clear_transient(InfernoState* s) { + osrs_interaction_init(&s->interaction); + s->player_last_interaction_target_slot = -1; + s->player_last_interaction_age = 1; + s->player_dest_x = -1; + s->player_dest_y = -1; + encounter_pending_hit_queue_clear(&s->player_pending_hits); + memset(s->pending_sparks, 0, sizeof(s->pending_sparks)); + memset(s->npc_target_hits, 0, sizeof(s->npc_target_hits)); + s->tick_scratch.offensive_prayer_correct_damage_roll = 0.0f; + s->tick_scratch.offensive_prayer_correct = 0; + s->tick_scratch.player_attacked = 0; + s->player_attack_npc_idx = -1; + s->player_attack_dmg = 0; + s->player_attack_style_id = ATTACK_STYLE_NONE; + for (int i = 0; i < INF_MAX_NPCS; i++) { + InfNPC* npc = &s->npcs[i]; + npc->attacked_this_tick = 0; + npc->attack_style_this_tick = ATTACK_STYLE_NONE; + npc->attack_visual_target = -1; + npc->moved_this_tick = 0; + npc->hit_landed_this_tick = 0; + npc->hit_damage = 0; + npc->hit_was_successful_this_tick = 0; + npc->hit_spell_type = ENCOUNTER_SPELL_NONE; + encounter_pending_hit_queue_clear(&npc->pending_hits); + } +} + +static void inf_lab_refresh_geometry(InfernoState* s) { + inf_rebuild_los(s); + inf_invalidate_los_cache(s); + inf_rebuild_entity_collision_flags(s); + inf_refresh_current_obs_slots(s); +} + +static void inf_lab_remove_all_npcs(InfernoState* s) { + memset(s->npcs, 0, sizeof(s->npcs)); + s->dead_mob_count = 0; +} + +static void inf_lab_clear_npcs(InfernoState* s) { + inf_lab_remove_all_npcs(s); + inf_lab_clear_transient(s); + inf_lab_refresh_geometry(s); +} + +static void inf_lab_apply_command(InfernoState* s, const InfernoLabCommand* cmd) { + if (!s || !cmd) inf_lab_abort("null command"); + + switch (cmd->kind) { + case INF_LAB_COMMAND_NONE: + return; + + case INF_LAB_COMMAND_RESET: + inf_reset((EncounterState*)s, cmd->as.reset.seed); + return; + + case INF_LAB_COMMAND_SET_PLAYER: + inf_lab_require_player_tile(s, cmd->as.tile.x, cmd->as.tile.y); + s->player.x = cmd->as.tile.x; + s->player.y = cmd->as.tile.y; + inf_lab_clear_transient(s); + inf_lab_refresh_geometry(s); + return; + + case INF_LAB_COMMAND_SPAWN_NPC: { + InfLabSpawnNpcCommand spawn_local = cmd->as.spawn_npc; + InfLabSpawnNpcCommand* spawn = &spawn_local; + if (spawn->slot < 0) { + spawn->slot = inf_find_free_npc(s); + if (spawn->slot < 0) + inf_lab_abort("no free npc slot (all %d in use)", INF_MAX_NPCS); + } + inf_lab_require_slot(spawn->slot); + inf_lab_require_type(spawn->type); + if (s->npcs[spawn->slot].active) + inf_deactivate_npc(s, spawn->slot); + inf_init_npc(s, spawn->slot, spawn->type, spawn->x, spawn->y); + if (spawn->hp.kind == ENCOUNTER_LAB_OPTIONAL_INT_SET) { + int hp = spawn->hp.value; + if (hp < 0) inf_lab_abort("npc hp must be nonnegative"); + if (hp > s->npcs[spawn->slot].max_hp) + inf_lab_abort("npc hp %d exceeds max hp %d", + hp, s->npcs[spawn->slot].max_hp); + s->npcs[spawn->slot].hp = hp; + } + if (spawn->timer.kind == ENCOUNTER_LAB_OPTIONAL_INT_SET) { + if (spawn->timer.value < 0) + inf_lab_abort("npc timer must be nonnegative"); + s->npcs[spawn->slot].attack_timer = spawn->timer.value; + } + inf_lab_clear_transient(s); + inf_lab_refresh_geometry(s); + return; + } + + case INF_LAB_COMMAND_MOVE_NPC: { + const InfLabMoveNpcCommand* move = &cmd->as.move_npc; + inf_lab_require_slot(move->slot); + if (!s->npcs[move->slot].active) + inf_lab_abort("cannot move inactive npc slot %d", move->slot); + s->npcs[move->slot].x = move->x; + s->npcs[move->slot].y = move->y; + s->npcs[move->slot].target_x = move->x; + s->npcs[move->slot].target_y = move->y; + inf_lab_clear_transient(s); + inf_lab_refresh_geometry(s); + return; + } + + case INF_LAB_COMMAND_DELETE_NPC: { + int slot = cmd->as.npc_slot.slot; + inf_lab_require_slot(slot); + inf_deactivate_npc(s, slot); + inf_lab_clear_transient(s); + inf_lab_refresh_geometry(s); + return; + } + + case INF_LAB_COMMAND_KILL_NPC: { + int slot = cmd->as.npc_slot.slot; + inf_lab_require_slot(slot); + if (!s->npcs[slot].active) + inf_lab_abort("cannot kill inactive npc slot %d", slot); + s->npcs[slot].hp = 0; + inf_apply_npc_death(s, slot); + inf_lab_refresh_geometry(s); + return; + } + + case INF_LAB_COMMAND_SET_NPC_HP: { + int slot = cmd->as.npc_hp.slot; + int hp = cmd->as.npc_hp.hp; + inf_lab_require_slot(slot); + if (!s->npcs[slot].active) + inf_lab_abort("cannot set hp for inactive npc slot %d", slot); + if (hp < 0) inf_lab_abort("npc hp must be nonnegative"); + if (hp > s->npcs[slot].max_hp) + inf_lab_abort("npc hp %d exceeds max hp %d", + hp, s->npcs[slot].max_hp); + s->npcs[slot].hp = hp; + inf_lab_refresh_geometry(s); + return; + } + + case INF_LAB_COMMAND_SET_NPC_TIMER: { + int slot = cmd->as.npc_timer.slot; + int timer = cmd->as.npc_timer.timer; + inf_lab_require_slot(slot); + if (!s->npcs[slot].active) + inf_lab_abort("cannot set timer for inactive npc slot %d", slot); + if (timer < 0) inf_lab_abort("npc timer must be nonnegative"); + s->npcs[slot].attack_timer = timer; + inf_lab_refresh_geometry(s); + return; + } + + case INF_LAB_COMMAND_SET_PILLAR: { + const InfLabPillarCommand* pillar = &cmd->as.pillar; + if (pillar->pillar_idx < 0 || pillar->pillar_idx >= INF_NUM_PILLARS) + inf_lab_abort("pillar idx must be in [0,%d], got %d", + INF_NUM_PILLARS - 1, pillar->pillar_idx); + s->pillars[pillar->pillar_idx].active = + pillar->state == INF_LAB_PILLAR_ACTIVE; + if (pillar->state == INF_LAB_PILLAR_ACTIVE) { + if (pillar->hp.kind == ENCOUNTER_LAB_OPTIONAL_INT_UNSET) { + s->pillars[pillar->pillar_idx].hp = INF_PILLAR_HP; + } else if (pillar->hp.value <= 0 || + pillar->hp.value > INF_PILLAR_HP) { + inf_lab_abort("pillar hp must be in [1,%d], got %d", + INF_PILLAR_HP, pillar->hp.value); + } else { + s->pillars[pillar->pillar_idx].hp = pillar->hp.value; + } + } else { + s->pillars[pillar->pillar_idx].hp = 0; + } + inf_lab_clear_transient(s); + inf_lab_refresh_geometry(s); + return; + } + + case INF_LAB_COMMAND_SPAWN_WAVE: + inf_require_valid_public_wave(cmd->as.wave.wave); + inf_lab_remove_all_npcs(s); + s->wave = cmd->as.wave.wave - 1; + s->wave_spawn_target = s->wave; + s->wave_spawn_delay = 0; + s->wave_ready_delay = INF_START_READY_TICKS; + inf_spawn_wave(s); + inf_lab_clear_transient(s); + inf_lab_refresh_geometry(s); + return; + + case INF_LAB_COMMAND_CLEAR_NPCS: + inf_lab_clear_npcs(s); + return; + + case INF_LAB_COMMAND_STEP_TICKS: { + int ticks = cmd->as.step_ticks.ticks; + if (ticks < 0) inf_lab_abort("step_ticks must be nonnegative"); + int actions[INF_NUM_ACTION_HEADS] = {0}; + for (int t = 0; t < ticks; t++) + inf_step((EncounterState*)s, actions); + inf_lab_refresh_geometry(s); + return; + } + } + + inf_lab_abort("unknown command kind %d", cmd->kind); +} + +static int inf_lab_nearest_pillar_idx(const InfernoState* s, int x, int y) { + int best_idx = -1; + int best_dist = INT32_MAX; + for (int p = 0; p < INF_NUM_PILLARS; p++) { + int cx = s->pillars[p].x + INF_PILLAR_SIZE / 2; + int cy = s->pillars[p].y + INF_PILLAR_SIZE / 2; + int dist = abs(cx - x) + abs(cy - y); + if (dist < best_dist) { + best_dist = dist; + best_idx = p; + } + } + return best_dist <= INF_LAB_PILLAR_CONTEXT_RADIUS ? best_idx : -1; +} + +static const char* inf_lab_npc_type_name(int type) { + switch (type) { + case INF_NPC_NIBBLER: return "nibbler"; + case INF_NPC_BAT: return "bat"; + case INF_NPC_BLOB: return "blob"; + case INF_NPC_BLOB_MELEE: return "blob_melee"; + case INF_NPC_BLOB_RANGE: return "blob_range"; + case INF_NPC_BLOB_MAGE: return "blob_mage"; + case INF_NPC_MELEER: return "meleer"; + case INF_NPC_RANGER: return "ranger"; + case INF_NPC_MAGER: return "mager"; + case INF_NPC_JAD: return "jad"; + case INF_NPC_ZUK: return "zuk"; + case INF_NPC_HEALER_JAD: return "healer_jad"; + case INF_NPC_HEALER_ZUK: return "healer_zuk"; + case INF_NPC_ZUK_SHIELD: return "zuk_shield"; + default: return "unknown"; + } +} + +static int inf_lab_parse_npc_type(const char* value) { + if (strcmp(value, "nibbler") == 0) return INF_NPC_NIBBLER; + if (strcmp(value, "bat") == 0) return INF_NPC_BAT; + if (strcmp(value, "blob") == 0) return INF_NPC_BLOB; + if (strcmp(value, "blob_melee") == 0) return INF_NPC_BLOB_MELEE; + if (strcmp(value, "blob_range") == 0) return INF_NPC_BLOB_RANGE; + if (strcmp(value, "blob_mage") == 0) return INF_NPC_BLOB_MAGE; + if (strcmp(value, "meleer") == 0 || strcmp(value, "melee") == 0) + return INF_NPC_MELEER; + if (strcmp(value, "ranger") == 0 || strcmp(value, "range") == 0) + return INF_NPC_RANGER; + if (strcmp(value, "mager") == 0 || strcmp(value, "mage") == 0) + return INF_NPC_MAGER; + if (strcmp(value, "jad") == 0) return INF_NPC_JAD; + if (strcmp(value, "zuk") == 0) return INF_NPC_ZUK; + if (strcmp(value, "healer_jad") == 0 || strcmp(value, "jad_healer") == 0) + return INF_NPC_HEALER_JAD; + if (strcmp(value, "healer_zuk") == 0 || strcmp(value, "zuk_healer") == 0) + return INF_NPC_HEALER_ZUK; + if (strcmp(value, "zuk_shield") == 0 || strcmp(value, "shield") == 0) + return INF_NPC_ZUK_SHIELD; + + char* end = NULL; + errno = 0; + long parsed = strtol(value, &end, 10); + if (errno == 0 && end && *end == '\0') { + inf_lab_require_type((int)parsed); + return (int)parsed; + } + inf_lab_abort("unknown npc type %s", value); + return 0; +} + +static InfLabOptionalInt inf_lab_parse_optional_hp_value(const char* value) { + return encounter_lab_parse_optional_full_int("inferno lab", value); +} + +typedef struct { + int slot; + int type; + int x; + int y; + InfLabOptionalInt hp; + InfLabOptionalInt timer; + int pillar_idx; + InfLabPillarState pillar_state; + int wave; + int ticks; + uint32_t seed; +} InfLabParsedArgs; + +static InfLabParsedArgs inf_lab_parsed_args_default(void) { + return (InfLabParsedArgs){ + .slot = -1, + .type = -1, + .x = INT32_MIN, + .y = INT32_MIN, + .hp = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_UNSET }, + .timer = { .kind = ENCOUNTER_LAB_OPTIONAL_INT_UNSET }, + .pillar_idx = -1, + .pillar_state = INF_LAB_PILLAR_ACTIVE, + .wave = -1, + .ticks = 0, + .seed = 0, + }; +} + +static void inf_lab_apply_script_arg( + InfLabParsedArgs* args, + InfLabCommandKind kind, + const char* command, + const char* key, + const char* value +) { + switch (kind) { + case INF_LAB_COMMAND_RESET: + if (strcmp(key, "seed") == 0) { + args->seed = encounter_lab_parse_seed_value("inferno lab", value); + return; + } + break; + + case INF_LAB_COMMAND_SET_PLAYER: + if (strcmp(key, "x") == 0) { + args->x = encounter_lab_parse_int_value("inferno lab", value); + return; + } + if (strcmp(key, "y") == 0) { + args->y = encounter_lab_parse_int_value("inferno lab", value); + return; + } + break; + + case INF_LAB_COMMAND_SPAWN_NPC: + if (strcmp(key, "slot") == 0) { + args->slot = encounter_lab_parse_int_value("inferno lab", value); + return; + } + if (strcmp(key, "type") == 0) { + args->type = inf_lab_parse_npc_type(value); + return; + } + if (strcmp(key, "x") == 0) { + args->x = encounter_lab_parse_int_value("inferno lab", value); + return; + } + if (strcmp(key, "y") == 0) { + args->y = encounter_lab_parse_int_value("inferno lab", value); + return; + } + if (strcmp(key, "hp") == 0) { + args->hp = inf_lab_parse_optional_hp_value(value); + return; + } + if (strcmp(key, "timer") == 0) { + args->timer = encounter_lab_optional_int_set( + encounter_lab_parse_int_value("inferno lab", value)); + return; + } + break; + + case INF_LAB_COMMAND_MOVE_NPC: + if (strcmp(key, "slot") == 0) { + args->slot = encounter_lab_parse_int_value("inferno lab", value); + return; + } + if (strcmp(key, "x") == 0) { + args->x = encounter_lab_parse_int_value("inferno lab", value); + return; + } + if (strcmp(key, "y") == 0) { + args->y = encounter_lab_parse_int_value("inferno lab", value); + return; + } + break; + + case INF_LAB_COMMAND_DELETE_NPC: + case INF_LAB_COMMAND_KILL_NPC: + if (strcmp(key, "slot") == 0) { + args->slot = encounter_lab_parse_int_value("inferno lab", value); + return; + } + break; + + case INF_LAB_COMMAND_SET_NPC_HP: + if (strcmp(key, "slot") == 0) { + args->slot = encounter_lab_parse_int_value("inferno lab", value); + return; + } + if (strcmp(key, "hp") == 0) { + args->hp = encounter_lab_optional_int_set( + encounter_lab_parse_int_value("inferno lab", value)); + return; + } + break; + + case INF_LAB_COMMAND_SET_NPC_TIMER: + if (strcmp(key, "slot") == 0) { + args->slot = encounter_lab_parse_int_value("inferno lab", value); + return; + } + if (strcmp(key, "timer") == 0) { + args->timer = encounter_lab_optional_int_set( + encounter_lab_parse_int_value("inferno lab", value)); + return; + } + break; + + case INF_LAB_COMMAND_SET_PILLAR: + if (strcmp(key, "idx") == 0 || strcmp(key, "pillar_idx") == 0) { + args->pillar_idx = encounter_lab_parse_int_value("inferno lab", value); + return; + } + if (strcmp(key, "active") == 0) { + int active = encounter_lab_parse_int_value("inferno lab", value); + if (active != 0 && active != 1) + inf_lab_abort("pillar active must be 0 or 1, got %d", active); + args->pillar_state = active + ? INF_LAB_PILLAR_ACTIVE + : INF_LAB_PILLAR_REMOVED; + return; + } + if (strcmp(key, "hp") == 0) { + args->hp = inf_lab_parse_optional_hp_value(value); + return; + } + break; + + case INF_LAB_COMMAND_SPAWN_WAVE: + if (strcmp(key, "wave") == 0 || strcmp(key, "public") == 0) { + args->wave = encounter_lab_parse_int_value("inferno lab", value); + return; + } + break; + + case INF_LAB_COMMAND_STEP_TICKS: + if (strcmp(key, "ticks") == 0) { + args->ticks = encounter_lab_parse_int_value("inferno lab", value); + return; + } + break; + + case INF_LAB_COMMAND_NONE: + case INF_LAB_COMMAND_CLEAR_NPCS: + break; + } + + inf_lab_abort("unknown key %s for command %s", key, command); +} + +static InfernoLabCommand inf_lab_build_script_command( + InfLabCommandKind kind, const InfLabParsedArgs* args, const char* command +) { + InfernoLabCommand out; + memset(&out, 0, sizeof(out)); + + switch (kind) { + case INF_LAB_COMMAND_NONE: + out.kind = INF_LAB_COMMAND_NONE; + return out; + + case INF_LAB_COMMAND_RESET: + out.kind = INF_LAB_COMMAND_RESET; + out.as.reset.seed = args->seed; + return out; + + case INF_LAB_COMMAND_SET_PLAYER: + if (args->x == INT32_MIN || args->y == INT32_MIN) + inf_lab_abort("command %s requires x and y", command); + out.kind = INF_LAB_COMMAND_SET_PLAYER; + out.as.tile.x = args->x; + out.as.tile.y = args->y; + return out; + + case INF_LAB_COMMAND_SPAWN_NPC: + if (args->x == INT32_MIN || args->y == INT32_MIN) + inf_lab_abort("command %s requires x and y", command); + if (args->type < 0) + inf_lab_abort("npc command requires type"); + out.kind = INF_LAB_COMMAND_SPAWN_NPC; + out.as.spawn_npc.slot = args->slot; + out.as.spawn_npc.type = (InfNPCType)args->type; + out.as.spawn_npc.x = args->x; + out.as.spawn_npc.y = args->y; + out.as.spawn_npc.hp = args->hp; + out.as.spawn_npc.timer = args->timer; + return out; + + case INF_LAB_COMMAND_MOVE_NPC: + if (args->x == INT32_MIN || args->y == INT32_MIN) + inf_lab_abort("command %s requires x and y", command); + if (args->slot < 0) + inf_lab_abort("command %s requires slot", command); + out.kind = INF_LAB_COMMAND_MOVE_NPC; + out.as.move_npc.slot = args->slot; + out.as.move_npc.x = args->x; + out.as.move_npc.y = args->y; + return out; + + case INF_LAB_COMMAND_DELETE_NPC: + case INF_LAB_COMMAND_KILL_NPC: + if (args->slot < 0) + inf_lab_abort("command %s requires slot", command); + out.kind = kind; + out.as.npc_slot.slot = args->slot; + return out; + + case INF_LAB_COMMAND_SET_NPC_HP: + if (args->slot < 0) + inf_lab_abort("command %s requires slot", command); + if (args->hp.kind != ENCOUNTER_LAB_OPTIONAL_INT_SET) + inf_lab_abort("set_npc_hp requires hp"); + out.kind = INF_LAB_COMMAND_SET_NPC_HP; + out.as.npc_hp.slot = args->slot; + out.as.npc_hp.hp = args->hp.value; + return out; + + case INF_LAB_COMMAND_SET_NPC_TIMER: + if (args->slot < 0) + inf_lab_abort("command %s requires slot", command); + if (args->timer.kind != ENCOUNTER_LAB_OPTIONAL_INT_SET) + inf_lab_abort("set_npc_timer requires timer"); + out.kind = INF_LAB_COMMAND_SET_NPC_TIMER; + out.as.npc_timer.slot = args->slot; + out.as.npc_timer.timer = args->timer.value; + return out; + + case INF_LAB_COMMAND_SET_PILLAR: + if (args->pillar_idx < 0) + inf_lab_abort("set_pillar requires idx"); + out.kind = INF_LAB_COMMAND_SET_PILLAR; + out.as.pillar.pillar_idx = args->pillar_idx; + out.as.pillar.state = args->pillar_state; + out.as.pillar.hp = args->hp; + return out; + + case INF_LAB_COMMAND_SPAWN_WAVE: + if (args->wave < 0) + inf_lab_abort("spawn_wave requires wave"); + out.kind = INF_LAB_COMMAND_SPAWN_WAVE; + out.as.wave.wave = args->wave; + return out; + + case INF_LAB_COMMAND_CLEAR_NPCS: + out.kind = INF_LAB_COMMAND_CLEAR_NPCS; + return out; + + case INF_LAB_COMMAND_STEP_TICKS: + out.kind = INF_LAB_COMMAND_STEP_TICKS; + out.as.step_ticks.ticks = args->ticks; + return out; + } + + inf_lab_abort("unknown command kind %d", kind); + out.kind = INF_LAB_COMMAND_NONE; + return out; +} + +static void inf_lab_append_forecast_tick_json( + InfLabString* out, const InfStepOutForecastTick* tick, int tick_idx +) { + int style_mask = inf_step_out_forecast_tick_style_mask(tick); + encounter_lab_string_append(out, + "{\"t\":%d,\"style_mask\":%d,\"melee\":%d,\"ranged\":%d," + "\"magic\":%d,\"blob_scan\":%d,\"ranger\":%d,\"mager\":%d," + "\"max_hit\":%d}", + tick_idx + 1, style_mask, tick->melee_count, tick->ranged_count, + tick->magic_count, tick->blob_scan_count, tick->ranger_count, + tick->mager_count, tick->max_hit); +} + +static int inf_lab_forecast_action_has_ranger_mager_same_tick( + const InfStepOutForecastAction* action +) { + for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { + if (action->ticks[tick_idx].ranger_count > 0 && + action->ticks[tick_idx].mager_count > 0) { + return 1; + } + } + return 0; +} + +static void inf_lab_append_forecast_json(InfernoState* s, InfLabString* out) { + InfStepOutForecast forecast; + inf_build_step_out_forecast_ctx(s, inf_legacy_context(), &forecast); + encounter_lab_string_append(out, "\"forecast\":{\"horizon\":%d,\"actions\":[", + INF_STEP_OUT_FORECAST_HORIZON); + for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { + const InfStepOutForecastAction* action = &forecast.actions[action_idx]; + if (action_idx > 0) encounter_lab_string_append(out, ","); + encounter_lab_string_append(out, + "{\"action\":%d,\"dx\":%d,\"dy\":%d,\"valid\":%d," + "\"land_x\":%d,\"land_y\":%d,\"same_tick_mixed\":%d," + "\"ranger_mager_same_tick\":%d,\"ranger_mager_offtick\":%d," + "\"melee_fallback\":%d,\"ticks\":[", + action_idx, + ENCOUNTER_MOVE_TARGET_DX[action_idx], + ENCOUNTER_MOVE_TARGET_DY[action_idx], + action->valid, action->land_x, action->land_y, + action->same_tick_mixed_style_conflict, + inf_lab_forecast_action_has_ranger_mager_same_tick(action), + action->ranger_mager_offtick_opportunity, + action->melee_fallback_exposure); + for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { + if (tick_idx > 0) encounter_lab_string_append(out, ","); + inf_lab_append_forecast_tick_json(out, &action->ticks[tick_idx], tick_idx); + } + encounter_lab_string_append(out, "]}"); + } + encounter_lab_string_append(out, "]}"); +} + +static void inf_lab_append_npcs_json(InfernoState* s, InfLabString* out) { + encounter_lab_string_append(out, "\"npcs\":["); + int emitted = 0; + for (int i = 0; i < INF_MAX_NPCS; i++) { + InfNPC* npc = &s->npcs[i]; + if (!npc->active) continue; + if (emitted > 0) encounter_lab_string_append(out, ","); + int los = npc->death_ticks > 0 ? 0 : inf_npc_has_los(s, i); + const EncounterPendingHit* pending = + encounter_pending_hit_queue_earliest(&npc->pending_hits); + int pending_ticks = pending ? pending->ticks_remaining : 0; + int pending_style = pending ? pending->attack_style : ATTACK_STYLE_NONE; + int pending_spell = pending ? pending->spell_type : ENCOUNTER_SPELL_NONE; + int pending_hit_success = pending ? pending->hit_success : 0; + int pending_damage = encounter_pending_hit_queue_damage_sum(&npc->pending_hits); + encounter_lab_string_append(out, + "{\"slot\":%d,\"type\":\"%s\",\"type_id\":%d,\"def_id\":%d," + "\"x\":%d,\"y\":%d,\"size\":%d,\"hp\":%d,\"max_hp\":%d," + "\"timer\":%d,\"stun\":%d,\"frozen\":%d,\"death_ticks\":%d," + "\"los_to_player\":%d,\"pending_count\":%d," + "\"pending_earliest_ticks\":%d,\"pending_style\":%d," + "\"pending_spell\":%d,\"pending_hit_success\":%d," + "\"pending_damage\":%d}", + i, inf_lab_npc_type_name(npc->type), npc->type, + INF_NPC_DEF_IDS[npc->type], npc->x, npc->y, + inf_npc_effective_size(npc), npc->hp, npc->max_hp, + npc->attack_timer, npc->stun_timer, npc->frozen_ticks, + npc->death_ticks, los, npc->pending_hits.count, + pending_ticks, pending_style, pending_spell, + pending_hit_success, pending_damage); + emitted++; + } + encounter_lab_string_append(out, "]"); +} + +static void inf_lab_append_pillars_json(const InfernoState* s, InfLabString* out) { + encounter_lab_string_append(out, "\"pillars\":["); + for (int p = 0; p < INF_NUM_PILLARS; p++) { + if (p > 0) encounter_lab_string_append(out, ","); + encounter_lab_string_append(out, + "{\"idx\":%d,\"x\":%d,\"y\":%d,\"size\":%d," + "\"active\":%d,\"hp\":%d}", + p, s->pillars[p].x, s->pillars[p].y, INF_PILLAR_SIZE, + s->pillars[p].active, s->pillars[p].hp); + } + encounter_lab_string_append(out, "]"); +} + +static char* inf_lab_alloc_json(InfernoState* s) { + InfLabString out; + encounter_lab_string_init(&out, "inferno lab"); + inf_invalidate_los_cache(s); + encounter_lab_string_append(&out, + "{\"tick\":%d,\"wave\":%d," + "\"player\":{\"x\":%d,\"y\":%d,\"hp\":%d,\"prayer\":%d," + "\"attack_timer\":%d,\"attacked\":%d,\"attack_target\":%d," + "\"attack_style\":%d,\"attack_damage\":%d},", + s->tick, s->wave + 1, + s->player.x, s->player.y, + s->player.current_hitpoints, s->player.current_prayer, + s->player.attack_timer, s->tick_scratch.player_attacked, + s->player_attack_npc_idx, s->player_attack_style_id, + s->player_attack_dmg); + inf_lab_append_pillars_json(s, &out); + encounter_lab_string_append(&out, ","); + inf_lab_append_npcs_json(s, &out); + encounter_lab_string_append(&out, ","); + inf_lab_append_forecast_json(s, &out); + encounter_lab_string_append(&out, "}"); + return out.data; +} + +static InfLabCommandKind inf_lab_command_kind_for(const char* command) { + static const EncounterLabCommandAlias aliases[] = { + { "reset", INF_LAB_COMMAND_RESET }, + { "player", INF_LAB_COMMAND_SET_PLAYER }, + { "set_player", INF_LAB_COMMAND_SET_PLAYER }, + { "npc", INF_LAB_COMMAND_SPAWN_NPC }, + { "spawn_npc", INF_LAB_COMMAND_SPAWN_NPC }, + { "move_npc", INF_LAB_COMMAND_MOVE_NPC }, + { "delete_npc", INF_LAB_COMMAND_DELETE_NPC }, + { "kill_npc", INF_LAB_COMMAND_KILL_NPC }, + { "set_npc_hp", INF_LAB_COMMAND_SET_NPC_HP }, + { "set_npc_timer", INF_LAB_COMMAND_SET_NPC_TIMER }, + { "pillar", INF_LAB_COMMAND_SET_PILLAR }, + { "set_pillar", INF_LAB_COMMAND_SET_PILLAR }, + { "wave", INF_LAB_COMMAND_SPAWN_WAVE }, + { "spawn_wave", INF_LAB_COMMAND_SPAWN_WAVE }, + { "clear_npcs", INF_LAB_COMMAND_CLEAR_NPCS }, + { "step", INF_LAB_COMMAND_STEP_TICKS }, + { "step_ticks", INF_LAB_COMMAND_STEP_TICKS }, + }; + return (InfLabCommandKind)encounter_lab_lookup_command_kind( + "inferno lab", command, aliases, sizeof(aliases) / sizeof(aliases[0])); +} + +static InfLabLineResult inf_lab_apply_script_line_impl( + InfernoState* s, const char* line, char** out_json +) { + EncounterLabLine lex = encounter_lab_line_begin("inferno lab", line); + if (!lex.command) { + free(lex.buffer); + return INF_LAB_LINE_NONE; + } + + if (strcmp(lex.command, "forecast") == 0) { + InfStepOutForecast forecast; + inf_build_step_out_forecast_ctx(s, inf_legacy_context(), &forecast); + (void)forecast; + free(lex.buffer); + return INF_LAB_LINE_FORECAST; + } + if (strcmp(lex.command, "dump") == 0 || strcmp(lex.command, "dump_json") == 0) { + if (out_json) *out_json = inf_lab_alloc_json(s); + free(lex.buffer); + return INF_LAB_LINE_DUMP; + } + + InfLabCommandKind kind = inf_lab_command_kind_for(lex.command); + InfLabParsedArgs args = inf_lab_parsed_args_default(); + + for (char* token = encounter_lab_next_token("inferno lab", &lex.cursor); + token != NULL; + token = encounter_lab_next_token("inferno lab", &lex.cursor)) { + const char* key = NULL; + const char* value = NULL; + encounter_lab_parse_key_value("inferno lab", token, &key, &value); + inf_lab_apply_script_arg(&args, kind, lex.command, key, value); + } + + InfernoLabCommand cmd = inf_lab_build_script_command(kind, &args, lex.command); + inf_lab_apply_command(s, &cmd); + free(lex.buffer); + return INF_LAB_LINE_NONE; +} + +static InfLabLineResult inf_lab_apply_script_line( + InfernoState* s, const char* line +) { + return inf_lab_apply_script_line_impl(s, line, NULL); +} + +static InfLabLineResult inf_lab_apply_script_line_alloc_json( + InfernoState* s, const char* line, char** out_json +) { + if (!out_json) inf_lab_abort("json output pointer is required"); + *out_json = NULL; + return inf_lab_apply_script_line_impl(s, line, out_json); +} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_model.inc b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc new file mode 100644 index 0000000000..58e54f8477 --- /dev/null +++ b/ocean/osrs/encounters/inferno/encounter_inferno_model.inc @@ -0,0 +1,912 @@ +#line 39 "encounter_inferno.h" +#define INF_ARENA_MIN_X 11 +#define INF_ARENA_MAX_X 39 +#define INF_ARENA_MIN_Y 14 +#define INF_ARENA_MAX_Y 43 +#define INF_ARENA_WIDTH (INF_ARENA_MAX_X - INF_ARENA_MIN_X + 1) +#define INF_ARENA_HEIGHT (INF_ARENA_MAX_Y - INF_ARENA_MIN_Y + 1) + +#define INF_PLAYER_START_X 25 +#define INF_PLAYER_START_Y 16 +#define INF_ZUK_PLAYER_START_X 25 +#define INF_ZUK_PLAYER_START_Y 42 +#define INF_ZUK_X 22 +#define INF_ZUK_Y 49 +#define INF_ZUK_SHIELD_X 23 +#define INF_ZUK_SHIELD_Y 44 + +#define INF_NUM_PILLARS 3 +#define INF_PILLAR_SIZE 3 +#define INF_PILLAR_HP 255 + +static const int INF_PILLAR_POS[INF_NUM_PILLARS][2] = { + { 21, 20 }, + { 11, 34 }, + { 28, 36 }, +}; + +#define INF_NUM_SPAWN_POS 9 +static const int INF_SPAWN_POS[INF_NUM_SPAWN_POS][2] = { + {12, 38}, {33, 38}, {14, 32}, {34, 31}, {27, 26}, + {16, 20}, {34, 18}, {12, 15}, {26, 15}, +}; + +#define INF_NIBBLER_SPAWN_X 20 +#define INF_NIBBLER_SPAWN_Y 31 + +#define INF_MAX_TICKS 18000 +#define INF_NUM_WAVES 69 +#define INF_WAVE_ZUK (INF_NUM_WAVES - 1) +#define INF_START_WAVE_ZUK_JAD (INF_WAVE_ZUK + 1) +#define INF_MAX_PUBLIC_START_WAVE (INF_NUM_WAVES + 2) +#define INF_NUM_ACTION_HEADS 9 + +typedef enum { + INF_NPC_NIBBLER = 0, + INF_NPC_BAT, + INF_NPC_BLOB, + INF_NPC_BLOB_MELEE, + INF_NPC_BLOB_RANGE, + INF_NPC_BLOB_MAGE, + INF_NPC_MELEER, + INF_NPC_RANGER, + INF_NPC_MAGER, + INF_NPC_JAD, + INF_NPC_ZUK, + INF_NPC_HEALER_JAD, + INF_NPC_HEALER_ZUK, + INF_NPC_ZUK_SHIELD, + INF_NUM_NPC_TYPES +} InfNPCType; + +typedef enum { + INF_OUTCOME_PLAYER_WON = 0, + INF_OUTCOME_PLAYER_DIED = 1, +} InfOutcome; + +static const int INF_NPC_DEF_IDS[INF_NUM_NPC_TYPES] = { + [INF_NPC_NIBBLER] = 7691, + [INF_NPC_BAT] = 7692, + [INF_NPC_BLOB] = 7693, + [INF_NPC_BLOB_MELEE] = 7696, + [INF_NPC_BLOB_RANGE] = 7695, + [INF_NPC_BLOB_MAGE] = 7694, + [INF_NPC_MELEER] = 7697, + [INF_NPC_RANGER] = 7698, + [INF_NPC_MAGER] = 7699, + [INF_NPC_JAD] = 7700, + [INF_NPC_ZUK] = 7706, + [INF_NPC_HEALER_JAD] = 7701, + [INF_NPC_HEALER_ZUK] = 7708, + [INF_NPC_ZUK_SHIELD] = 7707, +}; + +static const char* inf_npc_type_name(int type) { + switch (type) { + case INF_NPC_NIBBLER: return "Jal-Nib"; + case INF_NPC_BAT: return "Jal-MejRah"; + case INF_NPC_BLOB: return "Jal-Ak"; + case INF_NPC_BLOB_MELEE: return "Jal-AkRek-Ket"; + case INF_NPC_BLOB_RANGE: return "Jal-AkRek-Xil"; + case INF_NPC_BLOB_MAGE: return "Jal-AkRek-Mej"; + case INF_NPC_MELEER: return "Jal-ImKot"; + case INF_NPC_RANGER: return "Jal-Xil"; + case INF_NPC_MAGER: return "Jal-Zek"; + case INF_NPC_JAD: return "JalTok-Jad"; + case INF_NPC_ZUK: return "TzKal-Zuk"; + case INF_NPC_HEALER_JAD: return "Yt-HurKot"; + case INF_NPC_HEALER_ZUK: return "Jal-MejJak"; + case INF_NPC_ZUK_SHIELD: return "Ancestral Glyph"; + default: return "unknown"; + } +} + +typedef struct { + int hp; + int attack_speed; + int attack_range; + int size; + AttackStyle default_style; + MeleeStyle melee_style; + int can_melee; + + int att_level, str_level, def_level, range_level, magic_level; + + int melee_att_bonus; + int range_att_bonus; + int magic_att_bonus; + + int melee_str_bonus; + int ranged_str_bonus; + int magic_base_dmg; + int magic_dmg_pct; + + int magic_def_bonus; + int ranged_def_bonus; + + int max_hit_cap; + + int stun_on_spawn; + int can_move; +} InfNPCStats; + +typedef struct { + int attack_range; + AttackStyle default_style; + MeleeStyle melee_style; + int can_melee; + int magic_base_dmg; + int magic_dmg_pct; + int max_hit_cap; + int stun_on_spawn; + int can_move; +} InfNPCOverlay; + +static const MonsterIndex INF_NPC_TO_MON[INF_NUM_NPC_TYPES] = { + [INF_NPC_NIBBLER] = MON_JAL_NIB, + [INF_NPC_BAT] = MON_JAL_MEJRAH, + [INF_NPC_BLOB] = MON_JAL_AK, + [INF_NPC_BLOB_MELEE] = MON_JAL_AKREK_KET, + [INF_NPC_BLOB_RANGE] = MON_JAL_AKREK_XIL, + [INF_NPC_BLOB_MAGE] = MON_JAL_AKREK_MEJ, + [INF_NPC_MELEER] = MON_JAL_IMKOT, + [INF_NPC_RANGER] = MON_JAL_XIL, + [INF_NPC_MAGER] = MON_JAL_ZEK, + [INF_NPC_JAD] = MON_JALTOK_JAD, + [INF_NPC_ZUK] = MON_TZKAL_ZUK, + [INF_NPC_HEALER_JAD] = MON_YT_HURKOT, + [INF_NPC_HEALER_ZUK] = MON_JAL_MEJJAK, + [INF_NPC_ZUK_SHIELD] = MON_ZUK_SHIELD, +}; + +static const InfNPCOverlay INF_NPC_OVERLAY[INF_NUM_NPC_TYPES] = { + [INF_NPC_NIBBLER] = { 1, ATTACK_STYLE_MELEE, MELEE_STYLE_CRUSH, 0, 0, 0, 0, 1, 1 }, + [INF_NPC_BAT] = { 4, ATTACK_STYLE_RANGED, MELEE_STYLE_STAB, 0, 0, 0, 0, 0, 1 }, + [INF_NPC_BLOB] = { 15, ATTACK_STYLE_MAGIC, MELEE_STYLE_CRUSH, 1, 29, 100, 0, 0, 1 }, + [INF_NPC_BLOB_MELEE] = { 1, ATTACK_STYLE_MELEE, MELEE_STYLE_CRUSH, 0, 0, 0, 0, 0, 1 }, + [INF_NPC_BLOB_RANGE] = { 15, ATTACK_STYLE_RANGED, MELEE_STYLE_STAB, 0, 0, 0, 0, 0, 1 }, + [INF_NPC_BLOB_MAGE] = { 15, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB, 0, 18, 100, 0, 0, 1 }, + [INF_NPC_MELEER] = { 1, ATTACK_STYLE_MELEE, MELEE_STYLE_SLASH, 0, 0, 0, 0, 0, 1 }, + [INF_NPC_RANGER] = { 15, ATTACK_STYLE_RANGED, MELEE_STYLE_CRUSH, 1, 0, 0, 0, 0, 1 }, + [INF_NPC_MAGER] = { 15, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB, 1, 70, 100, 0, 0, 1 }, + [INF_NPC_JAD] = { 50, ATTACK_STYLE_RANGED, MELEE_STYLE_STAB, 1, 113, 100, 113, 0, 1 }, + [INF_NPC_ZUK] = { 99, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB, 0, 148, 100, 0, 8, 0 }, + [INF_NPC_HEALER_JAD] = { 1, ATTACK_STYLE_MELEE, MELEE_STYLE_CRUSH, 0, 0, 0, 0, 1, 1 }, + [INF_NPC_HEALER_ZUK] = { 99, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB, 0, 10, 100, 0, 1, 0 }, + [INF_NPC_ZUK_SHIELD] = { 0, ATTACK_STYLE_NONE, MELEE_STYLE_STAB, 0, 0, 0, 0, 1, 0 }, +}; + +static InfNPCStats INF_NPC_STATS[INF_NUM_NPC_TYPES]; + +static void inf_build_npc_stats(void) { + for (int i = 0; i < INF_NUM_NPC_TYPES; i++) { + const MonsterStats* m = &MONSTER_DATABASE[INF_NPC_TO_MON[i]]; + const InfNPCOverlay* o = &INF_NPC_OVERLAY[i]; + InfNPCStats* s = &INF_NPC_STATS[i]; + + s->hp = m->hp; + s->attack_speed = m->attack_speed; + if (i == INF_NPC_BLOB) s->attack_speed = 3; + s->size = m->size; + s->att_level = m->att_level; + s->str_level = m->str_level; + s->def_level = m->def_level; + s->range_level = m->range_level; + s->magic_level = m->magic_level; + s->melee_att_bonus = m->melee_att_bonus; + s->range_att_bonus = m->range_att_bonus; + s->magic_att_bonus = m->magic_att_bonus; + s->melee_str_bonus = m->melee_str_bonus; + s->ranged_str_bonus = m->ranged_str_bonus; + s->magic_def_bonus = m->magic_def; + s->ranged_def_bonus = m->ranged_def; + + s->attack_range = o->attack_range; + s->default_style = o->default_style; + s->melee_style = o->melee_style; + s->can_melee = o->can_melee; + s->magic_base_dmg = o->magic_base_dmg; + s->magic_dmg_pct = o->magic_dmg_pct; + s->max_hit_cap = o->max_hit_cap; + s->stun_on_spawn = o->stun_on_spawn; + s->can_move = o->can_move; + } +} + +#define INF_MAX_NPCS_PER_WAVE 9 + +typedef struct { + uint8_t types[INF_MAX_NPCS_PER_WAVE]; + int count; +} InfWaveDef; + +static const InfWaveDef INF_WAVES[INF_NUM_WAVES] = { + #define N INF_NPC_NIBBLER + #define B INF_NPC_BAT + #define BL INF_NPC_BLOB + #define M INF_NPC_MELEER + #define R INF_NPC_RANGER + #define MA INF_NPC_MAGER + #define J INF_NPC_JAD + #define Z INF_NPC_ZUK + #define W(...) { .types = { __VA_ARGS__ }, .count = sizeof((uint8_t[]){__VA_ARGS__}) } + + [0] = W(N,N,N, B), + [1] = W(N,N,N, B,B), + [2] = W(N,N,N, N,N,N), + [3] = W(N,N,N, BL), + [4] = W(N,N,N, B,BL), + [5] = W(N,N,N, B,B,BL), + [6] = W(N,N,N, BL,BL), + [7] = W(N,N,N, N,N,N), + + [8] = W(N,N,N, M), + [9] = W(N,N,N, B,M), + [10] = W(N,N,N, B,B,M), + [11] = W(N,N,N, BL,M), + [12] = W(N,N,N, B,BL,M), + [13] = W(N,N,N, B,B,BL,M), + [14] = W(N,N,N, BL,BL,M), + [15] = W(N,N,N, M,M), + [16] = W(N,N,N, N,N,N), + + [17] = W(N,N,N, R), + [18] = W(N,N,N, B,R), + [19] = W(N,N,N, B,B,R), + [20] = W(N,N,N, BL,R), + [21] = W(N,N,N, B,BL,R), + [22] = W(N,N,N, B,B,BL,R), + [23] = W(N,N,N, BL,BL,R), + [24] = W(N,N,N, M,R), + [25] = W(N,N,N, B,M,R), + [26] = W(N,N,N, B,B,M,R), + [27] = W(N,N,N, BL,M,R), + [28] = W(N,N,N, B,BL,M,R), + [29] = W(N,N,N, B,B,BL,M,R), + [30] = W(N,N,N, BL,BL,M,R), + [31] = W(N,N,N, M,M,R), + [32] = W(N,N,N, R,R), + [33] = W(N,N,N, N,N,N), + + [34] = W(N,N,N, MA), + [35] = W(N,N,N, B,MA), + [36] = W(N,N,N, B,B,MA), + [37] = W(N,N,N, BL,MA), + [38] = W(N,N,N, B,BL,MA), + [39] = W(N,N,N, B,B,BL,MA), + [40] = W(N,N,N, BL,BL,MA), + [41] = W(N,N,N, M,MA), + [42] = W(N,N,N, B,M,MA), + [43] = W(N,N,N, B,B,M,MA), + [44] = W(N,N,N, BL,M,MA), + [45] = W(N,N,N, B,BL,M,MA), + [46] = W(N,N,N, B,B,BL,M,MA), + [47] = W(N,N,N, BL,BL,M,MA), + [48] = W(N,N,N, M,M,MA), + [49] = W(N,N,N, R,MA), + [50] = W(N,N,N, B,R,MA), + [51] = W(N,N,N, B,B,R,MA), + [52] = W(N,N,N, BL,R,MA), + [53] = W(N,N,N, B,BL,R,MA), + [54] = W(N,N,N, B,B,BL,R,MA), + [55] = W(N,N,N, BL,BL,R,MA), + [56] = W(N,N,N, M,R,MA), + [57] = W(N,N,N, B,M,R,MA), + [58] = W(N,N,N, B,B,M,R,MA), + [59] = W(N,N,N, BL,M,R,MA), + [60] = W(N,N,N, B,BL,M,R,MA), + [61] = W(N,N,N, B,B,BL,M,R,MA), + [62] = W(N,N,N, BL,BL,M,R,MA), + [63] = W(N,N,N, M,M,R,MA), + [64] = W(N,N,N, R,R,MA), + [65] = W(N,N,N, MA,MA), + + [66] = W(J), + [67] = W(J,J,J), + [68] = W(Z), + + #undef N + #undef B + #undef BL + #undef M + #undef R + #undef MA + #undef J + #undef Z + #undef W +}; + +#define INF_MAX_NPCS 32 +#define INF_OBS_NPCS 37 +#define INF_START_READY_TICKS 6 + +#define INF_MAX_DEAD_MOBS 16 + +typedef struct { + InfNPCType type; + int x, y; + int hp, max_hp; +} InfDeadMob; + +#define INF_MAX_PENDING_SPARKS 32 + +typedef struct { + int active; + int src_x, src_y; + int x, y; + int damage; + int ticks_remaining; + int visual_emitted; +} InfPendingSpark; + +#define INF_MAX_NPC_TARGET_HITS 16 + +typedef struct { + int active; + int target_idx; + int damage; + int ticks_remaining; +} InfNpcTargetHit; + +typedef struct { + AttackStyle attack_style; + int healer_spawned; +} InfJadState; + +typedef struct { + int owner_idx; + int target; + int timer; +} InfHealerState; + +typedef struct { + InfNPCType type; + int x, y; + int hp, max_hp; + int size; + uint32_t render_id; + int attack_timer; + AttackStyle attack_style; + int active; + int target_x, target_y; + int stun_timer; + + int no_los_ticks; + int dig_freeze_timer; + int dig_attack_delay; + + int blob_scanned_prayer; + int had_los_last_tick; + + int resurrect_cooldown; + int resurrection_count; + + int frozen_ticks; + + union { + InfJadState jad; + InfHealerState healer; + } type_state; + + EncounterPendingHitQueue pending_hits; + + int death_ticks; + + int aggro_target; + + int attacked_this_tick; + AttackStyle attack_style_this_tick; + int attack_visual_target; + int resurrecting_this_tick; + int resurrection_visual_target; + int moved_this_tick; + int hit_landed_this_tick; + int hit_damage; + int hit_was_successful_this_tick; + int hit_spell_type; + int spawn_blob_splits_on_removal; +} InfNPC; + +static inline int inf_npc_is_healer(InfNPCType type) { + return type == INF_NPC_HEALER_JAD || type == INF_NPC_HEALER_ZUK; +} + +static inline InfHealerState* inf_npc_healer(InfNPC* npc) { + assert(inf_npc_is_healer(npc->type)); + return &npc->type_state.healer; +} + +static inline InfJadState* inf_npc_jad(InfNPC* npc) { + assert(npc->type == INF_NPC_JAD); + return &npc->type_state.jad; +} + +static inline const InfJadState* inf_npc_jad_const(const InfNPC* npc) { + assert(npc->type == INF_NPC_JAD); + return &npc->type_state.jad; +} + +static void inf_npc_init_type_state(InfNPC* npc) { + switch (npc->type) { + case INF_NPC_JAD: + npc->type_state.jad = (InfJadState){ + .attack_style = ATTACK_STYLE_NONE, .healer_spawned = 0 }; + break; + case INF_NPC_HEALER_JAD: + case INF_NPC_HEALER_ZUK: + npc->type_state.healer = (InfHealerState){ + .owner_idx = -1, .target = -1, .timer = 0 }; + break; + default: + break; + } +} + +typedef struct { + int x, y; + int hp; + int active; +} InfPillar; + +typedef struct { + int shield_idx; + int shield_dir; + int shield_freeze; + + int set_timer; + int set_interval; + int enraged; + + int healer_spawned; + int jad_spawned; + + int timer_paused; + int has_paused; +} InfZukState; + +typedef enum { + INF_GEAR_MAGE = 0, + INF_GEAR_LONG_RANGE, + INF_GEAR_BP, + INF_NUM_WEAPON_SETS +} InfWeaponSet; + +typedef enum { + INF_LOADOUT_PROFILE_MAX = 0, + INF_LOADOUT_PROFILE_BUDGET, + INF_NUM_LOADOUT_PROFILES +} InfLoadoutProfile; + +typedef enum { + INF_LOADOUT_PROFILE_MODE_MAX_ONLY = 0, + INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY, + INF_LOADOUT_PROFILE_MODE_MIXED_MAX_BUDGET, +} InfLoadoutProfileMode; + +static const uint8_t INF_MAX_MAGE_LOADOUT[NUM_GEAR_SLOTS] = { + [GEAR_SLOT_HEAD] = ITEM_MASORI_MASK_F, + [GEAR_SLOT_CAPE] = ITEM_DIZANAS_QUIVER, + [GEAR_SLOT_NECK] = ITEM_OCCULT_NECKLACE, + [GEAR_SLOT_AMMO] = ITEM_DRAGON_ARROWS, + [GEAR_SLOT_WEAPON] = ITEM_KODAI_WAND, + [GEAR_SLOT_SHIELD] = ITEM_ELYSIAN_SPIRIT_SHIELD, + [GEAR_SLOT_BODY] = ITEM_VIRTUS_ROBE_TOP, + [GEAR_SLOT_LEGS] = ITEM_VIRTUS_ROBE_BOTTOM, + [GEAR_SLOT_HANDS] = ITEM_CONFLICTION_GAUNTLETS, + [GEAR_SLOT_FEET] = ITEM_AVERNIC_TREADS, + [GEAR_SLOT_RING] = ITEM_VENATOR_RING, +}; + +static const uint8_t INF_MAX_RANGE_LONG_LOADOUT[NUM_GEAR_SLOTS] = { + [GEAR_SLOT_HEAD] = ITEM_MASORI_MASK_F, + [GEAR_SLOT_CAPE] = ITEM_DIZANAS_QUIVER, + [GEAR_SLOT_NECK] = ITEM_NECKLACE_OF_ANGUISH, + [GEAR_SLOT_AMMO] = ITEM_DRAGON_ARROWS, + [GEAR_SLOT_WEAPON] = ITEM_TWISTED_BOW, + [GEAR_SLOT_SHIELD] = ITEM_NONE, + [GEAR_SLOT_BODY] = ITEM_MASORI_BODY_F, + [GEAR_SLOT_LEGS] = ITEM_MASORI_CHAPS_F, + [GEAR_SLOT_HANDS] = ITEM_ZARYTE_VAMBRACES, + [GEAR_SLOT_FEET] = ITEM_AVERNIC_TREADS, + [GEAR_SLOT_RING] = ITEM_VENATOR_RING, +}; + +static const uint8_t INF_MAX_RANGE_FAST_LOADOUT[NUM_GEAR_SLOTS] = { + [GEAR_SLOT_HEAD] = ITEM_MASORI_MASK_F, + [GEAR_SLOT_CAPE] = ITEM_DIZANAS_QUIVER, + [GEAR_SLOT_NECK] = ITEM_NECKLACE_OF_ANGUISH, + [GEAR_SLOT_AMMO] = ITEM_DRAGON_DART, + [GEAR_SLOT_WEAPON] = ITEM_TOXIC_BLOWPIPE, + [GEAR_SLOT_SHIELD] = ITEM_NONE, + [GEAR_SLOT_BODY] = ITEM_MASORI_BODY_F, + [GEAR_SLOT_LEGS] = ITEM_MASORI_CHAPS_F, + [GEAR_SLOT_HANDS] = ITEM_ZARYTE_VAMBRACES, + [GEAR_SLOT_FEET] = ITEM_AVERNIC_TREADS, + [GEAR_SLOT_RING] = ITEM_VENATOR_RING, +}; + +static const uint8_t INF_BUDGET_MAGE_LOADOUT[NUM_GEAR_SLOTS] = { + [GEAR_SLOT_HEAD] = ITEM_CRYSTAL_HELM, + [GEAR_SLOT_CAPE] = ITEM_DIZANAS_QUIVER, + [GEAR_SLOT_NECK] = ITEM_OCCULT_NECKLACE, + [GEAR_SLOT_AMMO] = ITEM_GOD_BLESSING, + [GEAR_SLOT_WEAPON] = ITEM_DRAGON_HUNTER_WAND, + [GEAR_SLOT_SHIELD] = ITEM_CRYSTAL_SHIELD, + [GEAR_SLOT_BODY] = ITEM_AHRIMS_ROBETOP, + [GEAR_SLOT_LEGS] = ITEM_AHRIMS_ROBESKIRT, + [GEAR_SLOT_HANDS] = ITEM_CONFLICTION_GAUNTLETS, + [GEAR_SLOT_FEET] = ITEM_ECHO_BOOTS, + [GEAR_SLOT_RING] = ITEM_VENATOR_RING, +}; + +static const uint8_t INF_BUDGET_RANGE_LONG_LOADOUT[NUM_GEAR_SLOTS] = { + [GEAR_SLOT_HEAD] = ITEM_CRYSTAL_HELM, + [GEAR_SLOT_CAPE] = ITEM_DIZANAS_QUIVER, + [GEAR_SLOT_NECK] = ITEM_NECKLACE_OF_ANGUISH, + [GEAR_SLOT_AMMO] = ITEM_GOD_BLESSING, + [GEAR_SLOT_WEAPON] = ITEM_BOW_OF_FAERDHINEN, + [GEAR_SLOT_SHIELD] = ITEM_NONE, + [GEAR_SLOT_BODY] = ITEM_CRYSTAL_BODY, + [GEAR_SLOT_LEGS] = ITEM_CRYSTAL_LEGS, + [GEAR_SLOT_HANDS] = ITEM_BARROWS_GLOVES, + [GEAR_SLOT_FEET] = ITEM_ECHO_BOOTS, + [GEAR_SLOT_RING] = ITEM_VENATOR_RING, +}; + +static const uint8_t INF_BUDGET_RANGE_FAST_LOADOUT[NUM_GEAR_SLOTS] = { + [GEAR_SLOT_HEAD] = ITEM_CRYSTAL_HELM, + [GEAR_SLOT_CAPE] = ITEM_DIZANAS_QUIVER, + [GEAR_SLOT_NECK] = ITEM_NECKLACE_OF_ANGUISH, + [GEAR_SLOT_AMMO] = ITEM_DRAGON_DART, + [GEAR_SLOT_WEAPON] = ITEM_TOXIC_BLOWPIPE, + [GEAR_SLOT_SHIELD] = ITEM_NONE, + [GEAR_SLOT_BODY] = ITEM_CRYSTAL_BODY, + [GEAR_SLOT_LEGS] = ITEM_CRYSTAL_LEGS, + [GEAR_SLOT_HANDS] = ITEM_BARROWS_GLOVES, + [GEAR_SLOT_FEET] = ITEM_ECHO_BOOTS, + [GEAR_SLOT_RING] = ITEM_VENATOR_RING, +}; + +static const uint8_t* const INF_MAX_LOADOUTS[INF_NUM_WEAPON_SETS] = { + INF_MAX_MAGE_LOADOUT, + INF_MAX_RANGE_LONG_LOADOUT, + INF_MAX_RANGE_FAST_LOADOUT, +}; + +static const uint8_t* const INF_BUDGET_LOADOUTS[INF_NUM_WEAPON_SETS] = { + INF_BUDGET_MAGE_LOADOUT, + INF_BUDGET_RANGE_LONG_LOADOUT, + INF_BUDGET_RANGE_FAST_LOADOUT, +}; + +static const uint8_t* const* const INF_LOADOUT_PROFILES[INF_NUM_LOADOUT_PROFILES] = { + [INF_LOADOUT_PROFILE_MAX] = INF_MAX_LOADOUTS, + [INF_LOADOUT_PROFILE_BUDGET] = INF_BUDGET_LOADOUTS, +}; + +static const uint8_t* const* inf_loadouts_for_profile(int profile) { + if (profile < 0 || profile >= INF_NUM_LOADOUT_PROFILES) { + fprintf(stderr, "inferno: invalid active_loadout_profile %d\n", profile); + abort(); + } + return INF_LOADOUT_PROFILES[profile]; +} + +enum { + INF_ZUK_HEALER_REWARD_MODE_BASELINE = 0, + INF_ZUK_HEALER_REWARD_MODE_TAGS_FIRST = 1, +}; + +enum { + INF_JOSEPH_REWARD_MODE_OFF = 0, + INF_JOSEPH_REWARD_MODE_ON = 1, +}; + +enum { + INF_STEP_OUT_FORECAST_MODE_OFF = 0, + INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT = 1, + INF_STEP_OUT_FORECAST_MODE_FAST_STATIC_TILE = 2, + INF_STEP_OUT_FORECAST_MODE_FAST_READONLY_MOVE = 3, +}; + +enum { + INF_CURRICULUM_SUPPLY_MODE_OFF = 0, + INF_CURRICULUM_SUPPLY_MODE_ALL = 1, + INF_CURRICULUM_SUPPLY_MODE_ZUK = 2, + INF_CURRICULUM_SUPPLY_MODE_PRE_ZUK = 3, +}; + +typedef enum { + INF_IDLE_PHASE_SET = 0, + INF_IDLE_PHASE_JAD = 1, + INF_IDLE_PHASE_ZUK_PRE_JAD = 2, + INF_IDLE_PHASE_ZUK_JAD = 3, + INF_IDLE_PHASE_ZUK_HEALERS = 4, + INF_IDLE_PHASE_ZUK_POST_HEALERS = 5, +} InfIdleDiagnosticPhase; + +typedef struct { + int brew_doses; + int restore_doses; + int bastion_doses; + int stamina_doses; +} InfSupplyDoses; + +typedef struct { + float brew_fraction; + float restore_fraction; + float bastion_fraction; + float stamina_fraction; +} InfSupplyFractions; + +typedef struct { + int public_wave; + InfSupplyFractions fractions; +} InfSupplyProfileAnchor; + +typedef struct { + int start_wave; + InfLoadoutProfileMode loadout_profile_mode; + float budget_loadout_fraction; + float damage_reward_coeff; + float offensive_prayer_reward_coeff; + float shield_penalty_coeff; + float tag_reward_coeff; + float late_start_supply_profile_scale; + int curriculum_agent; + int curriculum_supply_jitter_mode; + float curriculum_supply_shared_jitter; + float curriculum_supply_brew_jitter; + float curriculum_supply_restore_jitter; + int curriculum_no_brew_mode; + float curriculum_no_brew_frac; + float supply_milestone_brew_reward_coeff; + float supply_milestone_restore_reward_coeff; + float death_penalty_coeff; + int terminal_penalty_enabled; + int step_out_forecast_obs_enabled; + int step_out_forecast_obs_mode; + float phase_900_bonus; + float phase_600_bonus; + float phase_300_bonus; + float shield_penalty_episode_cap; + + float jad_damage_reward_coeff; + float zuk_healer_damage_reward_coeff; + float set_damage_reward_coeff; + float jad_kill_bonus; + float zuk_healer_kill_bonus; + float set_kill_bonus; + float post_healer_zuk_damage_coeff; + float post_healer_set_damage_reward_coeff; + float post_healer_set_kill_bonus; + float post_healer_set_alive_tick_penalty_coeff; + float post_healer_set_alive_penalty_cap; + float zuk_healer_phase_hp_delta_coeff; + float zuk_untagged_healer_tick_penalty_coeff; + float zuk_untagged_healer_target_bonus_coeff; + float zuk_safe_untagged_healer_target_bonus_coeff; + float zuk_untagged_healer_nonmagic_attack_bonus_coeff; + float zuk_healer_mage_attack_penalty_coeff; + int zuk_safe_untagged_healer_target_mask; + int zuk_force_safe_untagged_healer_target_mask; + int zuk_healer_reward_mode; + float post_jad_zuk_multiplier; + float jad_alive_zuk_multiplier; + int oracle_mode; + float shield_tag_reward_coeff; + int joseph_reward_mode; +} InfConfig; + +typedef struct { + InfConfig config; + const CollisionMap* collision_map; + int world_offset_x; + int world_offset_y; + Log* log; + const HumanCommand* human_commands; + int human_command_count; + int human_command_mode; +} InfernoContext; + +typedef struct { + float damage_dealt; + float damage_resurrected; + float damage_zuk; + float damage_zuk_healers; + float damage_jad_healers; + float damage_jad; + float damage_set; + float shield_damage; + float spark_damage; + float damage_received; + float hp_restored; + float hp_restored_jad; + float hp_restored_zuk; + float offensive_prayer_correct_damage_roll; + int offensive_prayer_correct; + int kill_jad; + int kill_zuk_healer; + int kill_set; + int healer_tags; + int zuk_healer_tags; + int shield_tags; + int zuk_untagged_healer_targets; + int zuk_safe_untagged_healer_targets; + int zuk_untagged_healer_nonmagic_attacks; + int zuk_healer_mage_attack_fires; + int prayer_correct; + int off_prayer_hits; + int styles_fired; + int attacks_fired; + int wave_completed; + int pillar_lost; + int player_moved; + int player_attacked; + int behind_shield; + int brewed; + int blood_heal; +} InfTickScratch; + +typedef struct { + Player player; + + InfTickScratch tick_scratch; + + InfNPC npcs[INF_MAX_NPCS]; + uint32_t next_npc_render_id; + int current_obs_slots[INF_OBS_NPCS]; + InfPillar pillars[INF_NUM_PILLARS]; + InfZukState zuk; + + InfDeadMob dead_mobs[INF_MAX_DEAD_MOBS]; + int dead_mob_count; + + LOSBlocker los_blockers[INF_NUM_PILLARS]; + int los_blocker_count; + + int wave; + int wave_spawn_target; + int tick; + int wave_spawn_delay; + int wave_ready_delay; + int episode_over; + InfOutcome winner; + + float reward; + float episode_return; + float min_zuk_hp_seen; + int tick_at_le_300; + int tick_at_le_240; + int tick_at_le_150; + int tick_at_zuk_healer_spawn; + int tick_at_first_zuk_healer_tag; + int tick_at_all_zuk_healers_tagged; + int tick_at_all_zuk_healers_dead; + int tick_at_first_zuk_hit_after_all_healers_dead; + float damage_after_300; + float damage_after_240; + float damage_after_150; + float damage_after_all_zuk_healers_dead; + float post_healer_set_alive_penalty_total; + float post_healer_set_damage_reward_total; + float post_healer_set_kill_bonus_total; + float post_healer_set_pressure_total; + float zuk_hp_at_all_zuk_healers_dead; + float hp_restored_after_240; + float spark_damage_after_240; + float zuk_hp_max_after_healer_spawn; + int player_moved_last_tick; + + float total_damage_dealt; + float total_zuk_healer_damage; + float total_damage_received; + float total_hp_restored; + int total_zuk_healer_tags; + int total_zuk_healer_kills; + int total_zuk_untagged_healer_targets; + int total_zuk_safe_untagged_healer_targets; + int total_zuk_unsafe_untagged_healer_targets; + int total_zuk_untagged_healer_target_rewards; + int total_zuk_safe_untagged_healer_target_rewards; + int ticks_without_action; + int total_prayer_correct; + int total_npc_attacks; + int total_unavoidable_off; + int total_offensive_prayer_attacks; + int total_offensive_prayer_correct; + int offensive_prayer_attacks_by_style[4]; + int offensive_prayer_correct_by_style[4]; + int total_ranger_mager_same_tick_attacks; + int total_step_out_ranger_mager_same_tick_attacks; + int prayer_correct_by_type[INF_NUM_NPC_TYPES]; + int attacks_by_type[INF_NUM_NPC_TYPES]; + float dmg_from_type[INF_NUM_NPC_TYPES]; + int last_hit_by_type; + int killed_by_type[INF_NUM_NPC_TYPES]; + int total_idle_ticks; + int total_attack_ready_no_attack_ticks; + int total_target_available_no_attack_ticks; + int total_safe_attack_opportunity_missed_ticks; + int total_progressless_ticks; + float total_npc_pressure_if_ready_count; + float total_npc_pressure_this_tick_count; + float total_npc_pressure_if_ready_max_hit; + float total_npc_pressure_this_tick_max_hit; + float max_npc_pressure_incoming_hit; + int attack_ready_no_attack_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; + int target_available_no_attack_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; + int safe_attack_opportunity_missed_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; + int progressless_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; + int total_brews_used; + int total_blood_healed; + int total_npc_kills; + int total_gear_switches; + + int behind_shield_ticks; + int total_zuk_ticks; + int offshield_ticks_after_240; + int offshield_ticks_after_all_zuk_healers_dead; + + OsrsInteraction interaction; + int player_last_interaction_target_slot; + int player_last_interaction_age; + + InfWeaponSet weapon_set; + InfLoadoutProfile active_loadout_profile; + EncounterLoadoutStats loadout_stats[INF_NUM_WEAPON_SETS]; + EncounterLoadoutStats human_loadout_stats; + int stamina_active_ticks; + + int player_attack_npc_idx; + int player_attack_dmg; + AttackStyle player_attack_style_id; + EncounterProjectileTiming player_attack_timing; + + EncounterPendingHitQueue player_pending_hits; + InfPendingSpark pending_sparks[INF_MAX_PENDING_SPARKS]; + InfNpcTargetHit npc_target_hits[INF_MAX_NPC_TARGET_HITS]; + + int nibbler_target_pillar; + + int spawn_order[INF_NUM_SPAWN_POS]; + + int player_dest_x, player_dest_y; + + int8_t npc_los_cache[INF_MAX_NPCS]; + + uint8_t npc_collision_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT]; + uint8_t player_collision_flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT]; + + int start_wave; + uint32_t rng_state; + uint32_t supply_milestone_rewarded_mask; + uint8_t phase_900_fired; + uint8_t phase_600_fired; + uint8_t phase_300_fired; + float shield_penalty_episode_total; + + float pending_jad_kill_bonus; + float pending_zuk_healer_kill_bonus; + float pending_set_kill_bonus; + uint32_t rewarded_zuk_healer_target_mask; + uint8_t jad_killed_this_episode; + + int tick_at_first_zuk_healer_target; + int tick_at_first_zuk_healer_attack; + int total_zuk_healer_target_ticks; + int total_zuk_healer_attack_fires; + int total_zuk_healer_cannot_attack_ticks; + int total_zuk_healer_cooldown_ticks; + int total_zuk_healer_out_of_range_ticks; + int total_zuk_healer_attackable_ticks; + int total_action_mask_checks; + int zero_valid_action_head_count[9]; + int min_valid_action_count_by_head[9]; + int target_head_valid_healer_count; + int target_head_valid_zuk_count; + int target_head_valid_set_count; + int total_shield_tags; + float total_hp_restored_jad; + float total_hp_restored_zuk; +} InfernoState; diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc b/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc new file mode 100644 index 0000000000..33c458b3ba --- /dev/null +++ b/ocean/osrs/encounters/inferno/encounter_inferno_movement.inc @@ -0,0 +1,289 @@ +#line 2173 "encounter_inferno.h" +static int inf_in_arena(int x, int y) { + return x >= INF_ARENA_MIN_X && x <= INF_ARENA_MAX_X && + y >= INF_ARENA_MIN_Y && y <= INF_ARENA_MAX_Y; +} + +static int inf_footprint_in_arena(int x, int y, int size) { + return x >= INF_ARENA_MIN_X && x + size - 1 <= INF_ARENA_MAX_X && + y >= INF_ARENA_MIN_Y && y + size - 1 <= INF_ARENA_MAX_Y; +} + +static int inf_blocked_by_pillar_size1(InfernoState* s, int x, int y) { + for (int p = 0; p < INF_NUM_PILLARS; p++) { + if (!s->pillars[p].active) continue; + int px = s->pillars[p].x; + int py = s->pillars[p].y; + if (x >= px && x < px + INF_PILLAR_SIZE && + y >= py && y < py + INF_PILLAR_SIZE) + return 1; + } + return 0; +} + +static int inf_blocked_by_pillar(InfernoState* s, int x, int y, int size) { + if (size == 1) return inf_blocked_by_pillar_size1(s, x, y); + for (int p = 0; p < INF_NUM_PILLARS; p++) { + if (!s->pillars[p].active) continue; + if (los_aabb_overlap(x, y, size, + s->pillars[p].x, s->pillars[p].y, INF_PILLAR_SIZE)) + return 1; + } + return 0; +} + +typedef struct { + InfernoState* s; + const InfernoContext* ctx; +} InfWalkCtx; + +static int inf_pathfind_blocked(void* data, int abs_x, int abs_y) { + InfWalkCtx* wc = (InfWalkCtx*)data; + InfernoState* s = wc->s; + const InfernoContext* ctx = wc->ctx; + int lx = abs_x - ctx->world_offset_x; + int ly = abs_y - ctx->world_offset_y; + return inf_blocked_by_pillar(s, lx, ly, 1); +} + +static int inf_collision_flags_blocked( + uint8_t flags[INF_ARENA_WIDTH][INF_ARENA_HEIGHT], int x, int y, int size +) { + if (size == 1) { + int gx, gy; + return inf_grid_index(x, y, &gx, &gy) && flags[gx][gy]; + } + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + int gx, gy; + if (inf_grid_index(x + dx, y + dy, &gx, &gy) && flags[gx][gy]) + return 1; + } + } + return 0; +} + +typedef struct { + InfernoState* s; + const InfernoContext* ctx; + int self_idx; +} InfMoveCtx; + +static int inf_npc_environment_blocked_ctx( + InfernoState* s, + const InfernoContext* ctx, + int x, + int y, + int size +) { + if (size == 1) { + if (!inf_in_arena(x, y)) return 1; + if (inf_blocked_by_pillar_size1(s, x, y)) return 1; + if (ctx->collision_map && + !collision_tile_walkable( + ctx->collision_map, 0, + x + ctx->world_offset_x, + y + ctx->world_offset_y)) + return 1; + return 0; + } + if (!inf_footprint_in_arena(x, y, size)) return 1; + if (inf_blocked_by_pillar(s, x, y, size)) return 1; + if (ctx->collision_map) { + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + if (!collision_tile_walkable( + ctx->collision_map, 0, + x + dx + ctx->world_offset_x, + y + dy + ctx->world_offset_y)) + return 1; + } + } + } + return 0; +} + +static int inf_npc_blocked(void* ctx, int x, int y, int size) { + InfMoveCtx* mc = (InfMoveCtx*)ctx; + InfernoState* s = mc->s; + (void)mc->self_idx; + if (inf_npc_environment_blocked_ctx(s, mc->ctx, x, y, size)) return 1; + if (inf_collision_flags_blocked(s->player_collision_flags, x, y, size)) return 1; + return inf_collision_flags_blocked(s->npc_collision_flags, x, y, size); +} + +static int inf_npc_blocked_ignore_player(void* ctx, int x, int y, int size) { + InfMoveCtx* mc = (InfMoveCtx*)ctx; + InfernoState* s = mc->s; + (void)mc->self_idx; + if (inf_npc_environment_blocked_ctx(s, mc->ctx, x, y, size)) return 1; + return inf_collision_flags_blocked(s->npc_collision_flags, x, y, size); +} + +static int inf_npc_overlap_hold(void* ctx) { + const InfMoveCtx* mc = (const InfMoveCtx*)ctx; + const InfernoState* s = mc->s; + return s->player_last_interaction_age == 0 && + s->player_last_interaction_target_slot == mc->self_idx; +} + +static int inf_tile_walkable(void* ctx, int x, int y); + +static int inf_npc_environment_blocked(InfernoState* s, int x, int y, int size) { + return inf_npc_environment_blocked_ctx(s, inf_legacy_context(), x, y, size); +} + +static int inf_npc_terrain_blocked(InfernoState* s, int x, int y, int size) { + return inf_npc_environment_blocked_ctx(s, inf_legacy_context(), x, y, size); +} + +static void inf_npc_move_ctx(InfernoState* s, const InfernoContext* ctx, int idx) { + InfNPC* npc = &s->npcs[idx]; + if (!npc->active) return; + if (npc->stun_timer > 0) return; + if (npc->dig_freeze_timer > 0) return; + if (npc->frozen_ticks > 0) return; + + const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; + if (!stats->can_move) return; + int uses_collision_flag = inf_npc_sets_collision_flag(npc->type); + if (uses_collision_flag) + inf_unstamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); + + if (npc->type != INF_NPC_NIBBLER) { + InfMoveCtx mc = { s, ctx, idx }; + int stepped = encounter_npc_step_out_from_under( + &npc->x, &npc->y, npc->size, + s->player.x, s->player.y, + inf_npc_blocked_ignore_player, &mc, inf_npc_overlap_hold, &s->rng_state); + if (stepped == ENCOUNTER_NPC_UNDER_PLAYER_MOVED) { + npc->moved_this_tick = 1; + inf_invalidate_npc_los_cache(s, idx); + if (uses_collision_flag) + inf_stamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); + return; + } + if (stepped == ENCOUNTER_NPC_UNDER_PLAYER_HELD || + stepped == ENCOUNTER_NPC_UNDER_PLAYER_BLOCKED) { + if (uses_collision_flag) + inf_stamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); + return; + } + } + + InfTargetArea target = inf_npc_current_target_area(s, npc); + if (target.is_player && npc->aggro_target >= 0) npc->aggro_target = -1; + int tx = target.x; + int ty = target.y; + int target_size = target.size; + npc->target_x = tx; + npc->target_y = ty; + + if (npc->type != INF_NPC_NIBBLER) { + int has_los = target.is_player ? + inf_npc_has_los(s, idx) : + inf_npc_has_los_direct(s, idx); + if (!target.is_player) + s->npc_los_cache[idx] = (int8_t)has_los; + if (has_los) { + if (uses_collision_flag) + inf_stamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); + return; + } + } + if (npc->attack_timer > stats->attack_speed) { + if (uses_collision_flag) + inf_stamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); + return; + } + + int ox = npc->x, oy = npc->y; + InfMoveCtx mc = { s, ctx, idx }; + EncounterNpcStepPolicy step_policy; + encounter_npc_blocked_fn blocked; + if (target.is_player) { + step_policy = stats->attack_range == 1 + ? ENCOUNTER_NPC_STEP_OSRS_AGGRO_STOP_AT_MELEE + : ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET; + blocked = inf_npc_blocked_ignore_player; + } else { + step_policy = stats->attack_range == 1 + ? ENCOUNTER_NPC_STEP_STOP_AT_MELEE + : ENCOUNTER_NPC_STEP_TRAVEL_TARGET; + blocked = inf_npc_blocked; + } + encounter_npc_step_toward_policy( + &npc->x, &npc->y, tx, ty, npc->size, target_size, + step_policy, blocked, &mc, inf_npc_overlap_hold, &s->rng_state); + if (npc->x != ox || npc->y != oy) { + npc->moved_this_tick = 1; + inf_invalidate_npc_los_cache(s, idx); + } + if (uses_collision_flag) + inf_stamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); +} + +static void inf_npc_move(InfernoState* s, int idx) { + inf_npc_move_ctx(s, inf_legacy_context(), idx); +} + +static void inf_meleer_dig_check_ctx(InfernoState* s, const InfernoContext* ctx, int idx) { + InfNPC* npc = &s->npcs[idx]; + if (npc->type != INF_NPC_MELEER || !npc->active) return; + if (npc->dig_freeze_timer > 0) { + npc->dig_freeze_timer--; + if (npc->dig_freeze_timer == 0 && npc->dig_attack_delay == 0) { + int ox = npc->x, oy = npc->y; + if (inf_npc_sets_collision_flag(npc->type)) + inf_unstamp_npc_collision_footprint(s, ox, oy, npc->size); + int candidates[5][2] = { + { s->player.x - npc->size + 1, s->player.y - npc->size + 1 }, + { s->player.x, s->player.y }, + { s->player.x - npc->size + 1, s->player.y }, + { s->player.x, s->player.y - npc->size + 1 }, + { s->player.x - 1, s->player.y - 1 }, + }; + int landing_x = candidates[4][0]; + int landing_y = candidates[4][1]; + for (int i = 0; i < 4; i++) { + if (inf_npc_environment_blocked_ctx( + s, ctx, candidates[i][0], candidates[i][1], npc->size)) + continue; + landing_x = candidates[i][0]; + landing_y = candidates[i][1]; + break; + } + npc->x = landing_x; + npc->y = landing_y; + if (inf_npc_sets_collision_flag(npc->type)) + inf_stamp_npc_collision_footprint(s, npc->x, npc->y, npc->size); + npc->stun_timer = 2; + npc->dig_attack_delay = 6; + npc->no_los_ticks = 0; + } + return; + } + if (npc->dig_attack_delay > 0) { + npc->dig_attack_delay--; + return; + } + + if (!inf_npc_has_los(s, idx)) { + npc->no_los_ticks++; + } else { + npc->no_los_ticks = 0; + return; + } + + if (npc->no_los_ticks >= 50) { + npc->dig_freeze_timer = 6; + } else if (npc->no_los_ticks >= 38) { + if (encounter_rand_int(&s->rng_state, 10) == 0) { + npc->dig_freeze_timer = 6; + } + } +} + +static void inf_meleer_dig_check(InfernoState* s, int idx) { + inf_meleer_dig_check_ctx(s, inf_legacy_context(), idx); +} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc new file mode 100644 index 0000000000..e1fb75b01f --- /dev/null +++ b/ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc @@ -0,0 +1,869 @@ +#line 6591 "encounter_inferno.h" +typedef enum { + INF_TARGET_CATEGORY_NONE = 0, + INF_TARGET_CATEGORY_PLAYER, + INF_TARGET_CATEGORY_ZUK, + INF_TARGET_CATEGORY_SHIELD, + INF_TARGET_CATEGORY_OTHER_NPC, +} InfTargetCategory; + +static InfTargetCategory inf_npc_target_category( + const InfernoState* s, + const InfNPC* npc +) { + if (!npc || !npc->active) return INF_TARGET_CATEGORY_NONE; + if (npc->aggro_target < 0) return INF_TARGET_CATEGORY_PLAYER; + if (npc->aggro_target >= INF_MAX_NPCS) return INF_TARGET_CATEGORY_NONE; + + const InfNPC* target = &s->npcs[npc->aggro_target]; + if (!target->active) return INF_TARGET_CATEGORY_NONE; + if (target->type == INF_NPC_ZUK) return INF_TARGET_CATEGORY_ZUK; + if (target->type == INF_NPC_ZUK_SHIELD) return INF_TARGET_CATEGORY_SHIELD; + return INF_TARGET_CATEGORY_OTHER_NPC; +} + +static int inf_wave_phase_index(int wave_idx) { + int wave = wave_idx + 1; + if (wave <= 17) return 0; + if (wave <= 34) return 1; + if (wave <= 49) return 2; + if (wave <= 66) return 3; + if (wave <= 68) return 4; + return 5; +} + +static int inf_npc_is_phantom_barrage_targetable_now( + InfernoState* s, + const InfernoContext* ctx, + int npc_idx +) { + if (s->player.attack_timer != 0) return 0; + if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) return 0; + return s->npcs[npc_idx].death_ticks == 1 && + inf_player_can_phantom_barrage_npc(s, ctx, npc_idx); +} + +static int inf_pending_spark_obs_less( + const InfernoState* s, + const InfPendingSpark* a, + int ai, + const InfPendingSpark* b, + int bi +) { + if (a->ticks_remaining != b->ticks_remaining) + return a->ticks_remaining < b->ticks_remaining; + int adx = abs(a->x - s->player.x); + int ady = abs(a->y - s->player.y); + int bdx = abs(b->x - s->player.x); + int bdy = abs(b->y - s->player.y); + int ad = adx > ady ? adx : ady; + int bd = bdx > bdy ? bdx : bdy; + if (ad != bd) return ad < bd; + if (a->x != b->x) return a->x < b->x; + if (a->y != b->y) return a->y < b->y; + return ai < bi; +} + +static inline float inf_obs_norm_count(int count) { + return (float)count / (float)INF_OBS_NPCS; +} + +static inline float inf_obs_norm_damage_sum(int damage) { + float value = (float)damage / 300.0f; + return value > 1.0f ? 1.0f : value; +} + +static inline float inf_obs_norm_range_deficit(int deficit) { + float value = (float)deficit / 15.0f; + return value > 1.0f ? 1.0f : value; +} + +static inline float inf_obs_norm_npc_size(int size) { + return (float)size / 7.0f; +} + +static inline float inf_obs_norm_pillar_size(const InfPillar* pillar) { + return pillar->active ? (float)INF_PILLAR_SIZE / 7.0f : 0.0f; +} + +static const struct { InfNPCType type; int count; } +INF_OBS_SLOT_LAYOUT[INF_NUM_NPC_TYPES] = { + { INF_NPC_MAGER, 2 }, { INF_NPC_RANGER, 2 }, + { INF_NPC_MELEER, 2 }, { INF_NPC_BLOB, 2 }, + { INF_NPC_BAT, 2 }, { INF_NPC_BLOB_MAGE, 2 }, + { INF_NPC_BLOB_RANGE, 2 }, { INF_NPC_BLOB_MELEE, 2 }, + { INF_NPC_NIBBLER, 6 }, { INF_NPC_JAD, 3 }, + { INF_NPC_ZUK, 1 }, { INF_NPC_ZUK_SHIELD, 1 }, + { INF_NPC_HEALER_JAD, 6 }, { INF_NPC_HEALER_ZUK, 4 }, +}; + +static void inf_refresh_current_obs_slots_ctx(InfernoState* s, const InfernoContext* ctx) { + int obs_slots[INF_OBS_NPCS]; + for (int j = 0; j < INF_OBS_NPCS; j++) obs_slots[j] = -1; + + int slot_counts[INF_NUM_NPC_TYPES] = {0}; + int slot_offsets[INF_NUM_NPC_TYPES]; + int slot_max[INF_NUM_NPC_TYPES]; + + int offset = 0; + for (int t = 0; t < INF_NUM_NPC_TYPES; t++) { + slot_offsets[INF_OBS_SLOT_LAYOUT[t].type] = offset; + slot_max[INF_OBS_SLOT_LAYOUT[t].type] = INF_OBS_SLOT_LAYOUT[t].count; + offset += INF_OBS_SLOT_LAYOUT[t].count; + } + + for (int n = 0; n < INF_MAX_NPCS; n++) { + InfNPC* npc = &s->npcs[n]; + if (npc->active && npc->death_ticks == 0) { + int t = npc->type; + if (slot_counts[t] < slot_max[t]) { + obs_slots[slot_offsets[t] + slot_counts[t]] = n; + slot_counts[t]++; + } + } + } + + for (int n = 0; n < INF_MAX_NPCS; n++) { + InfNPC* npc = &s->npcs[n]; + if (npc->active && npc->death_ticks > 0 && + inf_npc_is_phantom_barrage_obs_candidate(s, ctx, n)) { + int t = npc->type; + if (slot_counts[t] < slot_max[t]) { + obs_slots[slot_offsets[t] + slot_counts[t]] = n; + slot_counts[t]++; + } + } + } + + for (int j = 0; j < INF_OBS_NPCS; j++) { + s->current_obs_slots[j] = obs_slots[j]; + } +} + +static void inf_refresh_current_obs_slots(InfernoState* s) { + inf_refresh_current_obs_slots_ctx(s, inf_legacy_context()); +} + +static void inf_write_obs_ctx( + EncounterState* state, + EncounterContext* context, + float* obs +) { + InfernoState* s = (InfernoState*)state; + InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); +#ifdef INF_PROFILE_ENABLED + int inf_prof_enabled = INF_PROFILE_ENABLED(); + double inf_prof_t0 = inf_prof_enabled ? INF_PROFILE_NOW_MS() : 0.0; +#endif + memset(obs, 0, INF_NUM_OBS * sizeof(float)); + int i = 0; + int px = s->player.x, py = s->player.y; + const EncounterLoadoutStats* ls = inf_current_loadout_stats(s, ctx); + InfSupplyDoses full_supplies = inf_full_starting_supplies(); + + obs[i++] = (float)s->player.current_hitpoints / 99.0f; + obs[i++] = (float)(px - INF_ARENA_MIN_X) / (float)INF_ARENA_WIDTH; + obs[i++] = (float)(INF_ARENA_MAX_X - px) / (float)INF_ARENA_WIDTH; + obs[i++] = (float)(py - INF_ARENA_MIN_Y) / (float)INF_ARENA_HEIGHT; + obs[i++] = (float)(INF_ARENA_MAX_Y - py) / (float)INF_ARENA_HEIGHT; + obs[i++] = (s->player.prayer == PRAYER_PROTECT_MELEE) ? 1.0f : 0.0f; + obs[i++] = (s->player.prayer == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; + obs[i++] = (s->player.prayer == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; + obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_PIETY) ? 1.0f : 0.0f; + obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_RIGOUR) ? 1.0f : 0.0f; + obs[i++] = (s->player.offensive_prayer == OFFENSIVE_PRAYER_AUGURY) ? 1.0f : 0.0f; + obs[i++] = (float)s->player.brew_doses / (float)full_supplies.brew_doses; + obs[i++] = (float)s->player.restore_doses / (float)full_supplies.restore_doses; + obs[i++] = (float)s->player.current_prayer / 99.0f; + obs[i++] = (float)s->wave / (float)INF_NUM_WAVES; + { + int phase = inf_wave_phase_index(s->wave); + for (int p = 0; p < 6; p++) + obs[i++] = (p == phase) ? 1.0f : 0.0f; + } + obs[i++] = inf_zuk_attack_timer_obs(s); + obs[i++] = (s->weapon_set == INF_GEAR_MAGE) ? 1.0f : 0.0f; + obs[i++] = (s->weapon_set == INF_GEAR_LONG_RANGE) ? 1.0f : 0.0f; + obs[i++] = (s->weapon_set == INF_GEAR_BP) ? 1.0f : 0.0f; + obs[i++] = (float)s->player.bastion_doses / (float)full_supplies.bastion_doses; + obs[i++] = (float)s->player.stamina_doses / (float)full_supplies.stamina_doses; + obs[i++] = (s->stamina_active_ticks > 0) ? 1.0f : 0.0f; + obs[i++] = (float)s->player.potion_timer / 3.0f; + obs[i++] = (float)s->player.attack_timer / 8.0f; + obs[i++] = (float)s->player.current_defence / 99.0f; + obs[i++] = (float)s->player.current_ranged / 99.0f; + obs[i++] = (float)s->player.current_magic / 99.0f; + obs[i++] = osrs_interaction_active(&s->interaction) ? 1.0f : 0.0f; + obs[i++] = (float)ls->attack_range / 15.0f; + obs[i++] = (float)s->dead_mob_count / (float)INF_MAX_DEAD_MOBS; + obs[i++] = (float)ls->max_hit / 80.0f; + obs[i++] = (float)ls->attack_speed / 6.0f; + obs[i++] = (float)ls->def_stab / 300.0f; + obs[i++] = (float)ls->def_magic / 300.0f; + obs[i++] = (float)ls->def_ranged / 300.0f; + obs[i++] = (float)s->player.special_energy / 100.0f; + + { + int min_timer = 999; + int min_style = 0; + int has_melee_2 = 0, has_ranged_2 = 0, has_magic_2 = 0; + + for (int h = 0; h < s->player_pending_hits.count; h++) { + EncounterPendingHit* ph = &s->player_pending_hits.hits[h]; + if (ph->check_prayer) { + int t = inf_pending_hit_obs_timer(ph); + if (t < min_timer) { + min_timer = t; + min_style = ph->attack_style; + } + if (t <= 2) { + if (ph->attack_style == ATTACK_STYLE_MELEE) has_melee_2 = 1; + if (ph->attack_style == ATTACK_STYLE_RANGED) has_ranged_2 = 1; + if (ph->attack_style == ATTACK_STYLE_MAGIC) has_magic_2 = 1; + } + } + } + + for (int n = 0; n < INF_MAX_NPCS; n++) { + InfNPC* npc = &s->npcs[n]; + if (!npc->active || npc->death_ticks > 0) continue; + InfNpcPlayerThreat threat = inf_npc_player_threat(s, npc); + int style_mask = threat.telegraph_style_mask; + if (!style_mask) continue; + if (npc->stun_timer > 0 || + npc->dig_freeze_timer > 0 || + npc->dig_attack_delay > 0) + continue; + int preview_style = inf_attack_style_obs_preview(style_mask); + + int t = npc->attack_timer; + if (t == 0) t = 1; + + if (t < min_timer) { + min_timer = t; + min_style = preview_style; + } + if (t <= 2) { + if (style_mask & INF_STYLE_MASK_MELEE) has_melee_2 = 1; + if (style_mask & INF_STYLE_MASK_RANGED) has_ranged_2 = 1; + if (style_mask & INF_STYLE_MASK_MAGIC) has_magic_2 = 1; + } + } + + int conflict_count = has_melee_2 + has_ranged_2 + has_magic_2; + obs[i++] = (min_timer < 999) ? (float)min_timer / 10.0f : 1.0f; + obs[i++] = (min_style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; + obs[i++] = (min_style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; + obs[i++] = (min_style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; + obs[i++] = (float)conflict_count / 3.0f; + } + + { + InfNpcPressureSummary pressure = inf_npc_pressure_summary(s); + obs[i++] = inf_obs_norm_count(pressure.if_ready_total); + obs[i++] = inf_obs_norm_count(pressure.if_ready_melee); + obs[i++] = inf_obs_norm_count(pressure.if_ready_ranged); + obs[i++] = inf_obs_norm_count(pressure.if_ready_magic); + obs[i++] = (float)pressure.if_ready_max_hit / 150.0f; + obs[i++] = inf_obs_norm_damage_sum(pressure.if_ready_sum_max_hit); + obs[i++] = inf_obs_norm_count(pressure.this_tick_total); + obs[i++] = inf_obs_norm_count(pressure.this_tick_melee); + obs[i++] = inf_obs_norm_count(pressure.this_tick_ranged); + obs[i++] = inf_obs_norm_count(pressure.this_tick_magic); + obs[i++] = (float)pressure.this_tick_max_hit / 150.0f; + obs[i++] = inf_obs_norm_damage_sum(pressure.this_tick_sum_max_hit); + obs[i++] = inf_obs_norm_count(pressure.timer_lte_1); + obs[i++] = inf_obs_norm_count(pressure.timer_lte_2); + obs[i++] = inf_obs_norm_count(pressure.timer_lte_4); + obs[i++] = (float)pressure.if_ready_style_count / 3.0f; + obs[i++] = (float)pressure.this_tick_style_count / 3.0f; + obs[i++] = (pressure.nearest_timer < 999) + ? (float)pressure.nearest_timer / 10.0f + : 1.0f; + } + + { + int is_zuk = (s->wave == 68); + obs[i++] = is_zuk ? 1.0f : 0.0f; + + if (is_zuk) { + int si = s->zuk.shield_idx; + int shield_active = (si >= 0 && s->npcs[si].active); + + obs[i++] = shield_active + ? ((s->zuk.shield_freeze > 0) ? 0.0f : (float)s->zuk.shield_dir) + : 0.0f; + obs[i++] = shield_active ? (float)s->zuk.shield_freeze / 5.0f : 0.0f; + int behind = 0; + float shield_offset = 0.0f; + if (shield_active) { + int sx = s->npcs[si].x; + int sz = INF_NPC_STATS[INF_NPC_ZUK_SHIELD].size; + int shield_center = sx + sz / 2; + shield_offset = (float)(px - shield_center) / 15.0f; + behind = (px >= sx && px < sx + sz && py >= 41); + } + obs[i++] = behind ? 1.0f : 0.0f; + obs[i++] = shield_offset; + obs[i++] = s->zuk.enraged ? 1.0f : 0.0f; + obs[i++] = (float)s->zuk.set_timer / 350.0f; + obs[i++] = s->zuk.timer_paused ? 1.0f : 0.0f; + obs[i++] = s->zuk.jad_spawned ? 1.0f : 0.0f; + obs[i++] = s->zuk.healer_spawned ? 1.0f : 0.0f; + } else { + for (int z = 0; z < 9; z++) obs[i++] = 0.0f; + } + } + + for (int p = 0; p < INF_NUM_PILLARS; p++) { + obs[i++] = s->pillars[p].active ? 1.0f : 0.0f; + obs[i++] = (float)s->pillars[p].hp / (float)INF_PILLAR_HP; + obs[i++] = (float)(s->pillars[p].x - px) / (float)INF_ARENA_WIDTH; + obs[i++] = (float)(s->pillars[p].y - py) / (float)INF_ARENA_HEIGHT; + obs[i++] = inf_obs_norm_pillar_size(&s->pillars[p]); + } + +#ifdef INF_PROFILE_ENABLED + INF_PROFILE_MARK(INF_PROF_OBS_PREFIX); +#endif + inf_refresh_current_obs_slots_ctx(s, ctx); +#ifdef INF_PROFILE_ENABLED + INF_PROFILE_MARK(INF_PROF_OBS_REFRESH_SLOTS); +#endif + + int slot_types[INF_OBS_NPCS]; + int st_idx = 0; + for (int t = 0; t < INF_NUM_NPC_TYPES; t++) { + for (int j = 0; j < INF_OBS_SLOT_LAYOUT[t].count; j++) + slot_types[st_idx++] = INF_OBS_SLOT_LAYOUT[t].type; + } + + for (int k = 0; k < INF_OBS_NPCS; k++) { + int n = s->current_obs_slots[k]; + int type = slot_types[k]; + + int has_style = (type == INF_NPC_BLOB || type == INF_NPC_JAD); + int has_scan = (type == INF_NPC_BLOB); + int has_los = (type != INF_NPC_NIBBLER && type != INF_NPC_MELEER && type != INF_NPC_HEALER_JAD && type != INF_NPC_ZUK_SHIELD); + int has_target_category = (type != INF_NPC_NIBBLER && type != INF_NPC_ZUK_SHIELD); + int has_timer = (type != INF_NPC_NIBBLER && type != INF_NPC_HEALER_JAD && type != INF_NPC_ZUK_SHIELD); + int has_meleer_dig = (type == INF_NPC_MELEER); + + int num_features = 12; + if (has_timer) num_features += 1; + if (has_style) num_features += 3; + if (has_los) num_features += 1; + if (has_scan) num_features += 3; + if (has_target_category) num_features += 4; + num_features += 1; + num_features += 6; + if (has_meleer_dig) num_features += 3; + + if (n >= 0) { + InfNPC* npc = &s->npcs[n]; + obs[i++] = (float)npc->hp / (float)npc->max_hp; + obs[i++] = (float)(npc->x - px) / (float)INF_ARENA_WIDTH; + obs[i++] = (float)(npc->y - py) / (float)INF_ARENA_HEIGHT; + obs[i++] = inf_obs_norm_npc_size(npc->size); + if (has_timer) obs[i++] = (float)npc->attack_timer / 10.0f; + + if (has_style) { + int style = (npc->type == INF_NPC_JAD) ? inf_npc_jad(npc)->attack_style : npc->attack_style; + obs[i++] = (style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; + obs[i++] = (style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; + obs[i++] = (style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; + } + + if (has_los) obs[i++] = inf_npc_has_los(s, n) ? 1.0f : 0.0f; + + if (has_scan) { + if (npc->blob_scanned_prayer >= 0) { + OverheadPrayer scanned = (OverheadPrayer)npc->blob_scanned_prayer; + obs[i++] = (scanned == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; + obs[i++] = (scanned == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; + obs[i++] = (scanned != PRAYER_PROTECT_MAGIC && scanned != PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; + } else { + obs[i++] = 0.0f; obs[i++] = 0.0f; obs[i++] = 0.0f; + } + } + + obs[i++] = (float)inf_barrage_total_target_count(s, n) / + (float)BARRAGE_MAX_HITS; + + { + InfNpcPlayerThreat threat = inf_npc_player_threat(s, npc); + obs[i++] = (float)threat.edge_distance / (float)INF_ARENA_HEIGHT; + obs[i++] = threat.can_attack_if_ready ? 1.0f : 0.0f; + obs[i++] = threat.can_attack_this_tick ? 1.0f : 0.0f; + obs[i++] = (float)npc->frozen_ticks / (float)BARRAGE_FREEZE_TICKS; + obs[i++] = inf_player_can_attack_npc_from_current_tile(s, n) ? 1.0f : 0.0f; + obs[i++] = inf_player_has_los_to_npc_from_current_tile(s, n) ? 1.0f : 0.0f; + InfPlayerNpcLoadoutReach mage = + inf_player_loadout_reach_to_npc(s, n, INF_GEAR_MAGE); + InfPlayerNpcLoadoutReach long_range = + inf_player_loadout_reach_to_npc(s, n, INF_GEAR_LONG_RANGE); + InfPlayerNpcLoadoutReach blowpipe = + inf_player_loadout_reach_to_npc(s, n, INF_GEAR_BP); + obs[i++] = mage.can_attack_now ? 1.0f : 0.0f; + obs[i++] = long_range.can_attack_now ? 1.0f : 0.0f; + obs[i++] = blowpipe.can_attack_now ? 1.0f : 0.0f; + obs[i++] = inf_obs_norm_range_deficit(mage.range_deficit); + obs[i++] = inf_obs_norm_range_deficit(long_range.range_deficit); + obs[i++] = inf_obs_norm_range_deficit(blowpipe.range_deficit); + } + + if (has_target_category) { + InfTargetCategory category = inf_npc_target_category(s, npc); + obs[i++] = (category == INF_TARGET_CATEGORY_PLAYER) ? 1.0f : 0.0f; + obs[i++] = (category == INF_TARGET_CATEGORY_ZUK) ? 1.0f : 0.0f; + obs[i++] = (category == INF_TARGET_CATEGORY_SHIELD) ? 1.0f : 0.0f; + obs[i++] = (category == INF_TARGET_CATEGORY_OTHER_NPC) ? 1.0f : 0.0f; + } + obs[i++] = (osrs_interaction_active(&s->interaction) && s->interaction.target_slot == n) ? 1.0f : 0.0f; + obs[i++] = inf_npc_is_phantom_barrage_targetable_now(s, ctx, n) ? 1.0f : 0.0f; + if (has_meleer_dig) { + float no_los = (float)npc->no_los_ticks / 50.0f; + if (no_los > 1.0f) no_los = 1.0f; + obs[i++] = no_los; + obs[i++] = (float)npc->dig_freeze_timer / 6.0f; + obs[i++] = (float)npc->dig_attack_delay / 6.0f; + } + } else { + for (int j = 0; j < num_features; j++) obs[i++] = 0.0f; + } + } +#ifdef INF_PROFILE_ENABLED + INF_PROFILE_MARK(INF_PROF_OBS_NPC_SLOTS); +#endif + + { + int expected_npc_end = INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE + + INF_TOTAL_NPC_OBS_SIZE; + if (i != expected_npc_end) { + fprintf(stderr, "FATAL: obs misaligned after NPC section: i=%d expected=%d\n", + i, expected_npc_end); + abort(); + } + } + + { + if (ctx->config.step_out_forecast_obs_mode != INF_STEP_OUT_FORECAST_MODE_OFF) { + InfStepOutForecast forecast; + inf_build_step_out_forecast_mode_ctx(s, ctx, &forecast); + for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { + const InfStepOutForecastAction* action = &forecast.actions[action_idx]; + int first_attack_tick = 0; + int first_style_mask = 0; + int max_hit = 0; + int ranger_mager_same_tick = 0; + for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { + const InfStepOutForecastTick* tick = &action->ticks[tick_idx]; + int style_mask = inf_step_out_forecast_tick_style_mask(tick); + if (first_attack_tick == 0 && + inf_step_out_forecast_tick_has_event(tick)) { + first_attack_tick = tick_idx + 1; + first_style_mask = style_mask; + } + if (tick->max_hit > max_hit) max_hit = tick->max_hit; + if (tick->ranger_count > 0 && tick->mager_count > 0) + ranger_mager_same_tick = 1; + } + obs[i++] = action->valid ? 1.0f : 0.0f; + obs[i++] = (float)first_attack_tick / (float)INF_STEP_OUT_FORECAST_HORIZON; + obs[i++] = (float)first_style_mask / 7.0f; + obs[i++] = (float)max_hit / 150.0f; + obs[i++] = action->same_tick_mixed_style_conflict ? 1.0f : 0.0f; + obs[i++] = ranger_mager_same_tick ? 1.0f : 0.0f; + obs[i++] = action->ranger_mager_offtick_opportunity ? 1.0f : 0.0f; + obs[i++] = action->melee_fallback_exposure ? 1.0f : 0.0f; + } + } else { + i += INF_STEP_OUT_FORECAST_OBS_SIZE; + } + } +#ifdef INF_PROFILE_ENABLED + INF_PROFILE_MARK(INF_PROF_OBS_FORECAST); +#endif + + { + int expected_forecast_end = INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE + + INF_TOTAL_NPC_OBS_SIZE + INF_STEP_OUT_FORECAST_OBS_SIZE; + if (i != expected_forecast_end) { + fprintf(stderr, "FATAL: obs misaligned after step-out forecast: i=%d expected=%d\n", + i, expected_forecast_end); + abort(); + } + } + + for (int h = 0; h < ENCOUNTER_MAX_PENDING_HITS; h++) { + if (h < s->player_pending_hits.count) { + EncounterPendingHit* ph = &s->player_pending_hits.hits[h]; + obs[i++] = 1.0f; + obs[i++] = (ph->attack_style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; + obs[i++] = (ph->attack_style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; + obs[i++] = (float)inf_pending_hit_obs_timer(ph) / 10.0f; + obs[i++] = (float)ph->damage / 150.0f; + } else { + for (int j = 0; j < INF_FEATURES_PER_HIT; j++) obs[i++] = 0.0f; + } + } +#ifdef INF_PROFILE_ENABLED + INF_PROFILE_MARK(INF_PROF_OBS_PENDING_HITS); +#endif + + int used_sparks[INF_MAX_PENDING_SPARKS] = {0}; + for (int slot = 0; slot < INF_SPARK_OBS_SLOTS; slot++) { + int best = -1; + for (int sp = 0; sp < INF_MAX_PENDING_SPARKS; sp++) { + if (used_sparks[sp] || !s->pending_sparks[sp].active) continue; + if (best < 0 || + inf_pending_spark_obs_less( + s, &s->pending_sparks[sp], sp, + &s->pending_sparks[best], best)) { + best = sp; + } + } + + if (best >= 0) { + const InfPendingSpark* spark = &s->pending_sparks[best]; + used_sparks[best] = 1; + obs[i++] = 1.0f; + obs[i++] = (float)(spark->x - px) / (float)INF_ARENA_WIDTH; + obs[i++] = (float)(spark->y - py) / (float)INF_ARENA_HEIGHT; + obs[i++] = (float)(spark->src_x - px) / (float)INF_ARENA_WIDTH; + obs[i++] = (float)(spark->src_y - py) / (float)INF_ARENA_HEIGHT; + obs[i++] = (float)spark->ticks_remaining / 10.0f; + obs[i++] = (float)spark->damage / 10.0f; + } else { + for (int j = 0; j < INF_FEATURES_PER_SPARK; j++) obs[i++] = 0.0f; + } + } +#ifdef INF_PROFILE_ENABLED + INF_PROFILE_MARK(INF_PROF_OBS_SPARKS); +#endif + + if (i != INF_NUM_OBS) { + fprintf(stderr, "BUG: inf_write_obs wrote %d features, expected %d\n", i, INF_NUM_OBS); + abort(); + } +} + +static int inf_find_target_obs_slot(const InfernoState* s, int npc_slot) { + if (npc_slot < 0 || npc_slot >= INF_MAX_NPCS) return -1; + return encounter_find_observed_target_slot( + s->current_obs_slots, INF_OBS_NPCS, npc_slot); +} + +static void inf_write_obs(EncounterState* state, float* obs) { + inf_write_obs_ctx(state, (EncounterContext*)inf_legacy_context(), obs); +} + +static int inf_obs_slot_is_targetable( + InfernoState* s, + const InfernoContext* ctx, + int obs_slot +) { + if (obs_slot < 0 || obs_slot >= INF_OBS_NPCS) return 0; + int npc_slot = s->current_obs_slots[obs_slot]; + if (npc_slot < 0 || npc_slot >= INF_MAX_NPCS) return 0; + if (s->npcs[npc_slot].active && + s->npcs[npc_slot].death_ticks == 0 && + s->npcs[npc_slot].type != INF_NPC_ZUK_SHIELD) + return 1; + return s->player.attack_timer <= 1 && + inf_npc_is_phantom_barrage_obs_candidate(s, ctx, npc_slot); +} + +static int inf_is_human_targetable_npc_slot_ctx( + EncounterState* state, + EncounterContext* context, + int npc_slot +) { + InfernoState* s = (InfernoState*)state; + InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + inf_refresh_current_obs_slots_ctx(s, ctx); + return inf_obs_slot_is_targetable(s, ctx, inf_find_target_obs_slot(s, npc_slot)); +} + +static int inf_is_human_targetable_npc_slot(EncounterState* state, int npc_slot) { + return inf_is_human_targetable_npc_slot_ctx( + state, (EncounterContext*)inf_legacy_context(), npc_slot); +} + +static void inf_write_mask_ctx( + EncounterState* state, + EncounterContext* context, + float* mask +) { + InfernoState* s = (InfernoState*)state; + InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + inf_refresh_current_obs_slots_ctx(s, ctx); + int offset = 0; + if (s->total_action_mask_checks == 0) { + for (int h = 0; h < 9; h++) { + if (s->min_valid_action_count_by_head[h] == 0) + s->min_valid_action_count_by_head[h] = 1000000; + } + } + + mask[offset++] = 1.0f; + for (int d = 1; d < ENCOUNTER_MOVE_ACTIONS; d++) { + int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[d]; + int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[d]; + mask[offset++] = (inf_in_arena(nx, ny) && !inf_blocked_by_pillar(s, nx, ny, 1)) + ? 1.0f : 0.0f; + } + + mask[offset++] = 1.0f; + mask[offset++] = s->player.prayer != PRAYER_NONE ? 1.0f : 0.0f; + mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; + mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; + mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; + mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; + + int has_forced_safe_healer_target = + ctx->config.zuk_force_safe_untagged_healer_target_mask && + inf_has_safe_untagged_zuk_healer_target_now(s, ctx); + + mask[offset++] = has_forced_safe_healer_target ? 0.0f : 1.0f; + for (int n = 0; n < INF_OBS_NPCS; n++) { + int npc_idx = s->current_obs_slots[n]; + int is_targetable = inf_obs_slot_is_targetable(s, ctx, n); + if (is_targetable && + (ctx->config.zuk_safe_untagged_healer_target_mask || + ctx->config.zuk_force_safe_untagged_healer_target_mask) && + !inf_untagged_zuk_healer_target_is_safe_now(s, ctx, npc_idx)) { + is_targetable = 0; + } + if (is_targetable && has_forced_safe_healer_target && + !inf_is_safe_untagged_zuk_healer_target_now(s, ctx, npc_idx)) { + is_targetable = 0; + } + mask[offset++] = is_targetable ? 1.0f : 0.0f; + } + + mask[offset++] = 1.0f; + mask[offset++] = (s->weapon_set != INF_GEAR_MAGE) ? 1.0f : 0.0f; + mask[offset++] = (s->weapon_set != INF_GEAR_LONG_RANGE) ? 1.0f : 0.0f; + mask[offset++] = (s->weapon_set != INF_GEAR_BP) ? 1.0f : 0.0f; + + mask[offset++] = 1.0f; + mask[offset++] = (s->player.brew_doses > 0 && + s->player.potion_timer == 0 && + s->player.current_hitpoints < s->player.base_hitpoints) + ? 1.0f : 0.0f; + + mask[offset++] = 1.0f; + { + int pray_missing = s->player.base_prayer - s->player.current_prayer; + int stats_drained = s->player.current_attack < 99 || s->player.current_strength < 99 || + s->player.current_defence < 99 || s->player.current_ranged < 99 || + s->player.current_magic < 99; + int pray_worth = pray_missing >= (INF_RESTORE_AMOUNT + 1) / 2; + mask[offset++] = (s->player.restore_doses > 0 && + s->player.potion_timer == 0 && + (stats_drained || pray_worth)) + ? 1.0f : 0.0f; + } + mask[offset++] = (s->player.bastion_doses > 0 && s->player.potion_timer == 0 && + s->player.current_ranged >= 99 && s->player.current_ranged <= 105) + ? 1.0f : 0.0f; + mask[offset++] = (s->player.stamina_doses > 0 && + s->player.potion_timer == 0 && + s->stamina_active_ticks == 0) + ? 1.0f : 0.0f; + + mask[offset++] = 1.0f; + mask[offset++] = s->player.current_magic >= BLOOD_BARRAGE_LEVEL ? 1.0f : 0.0f; + mask[offset++] = s->player.current_magic >= ICE_BARRAGE_LEVEL ? 1.0f : 0.0f; + + mask[offset++] = 1.0f; + mask[offset++] = (s->weapon_set == INF_GEAR_BP && + s->player.special_energy >= BLOWPIPE_SPEC_COST) + ? 1.0f : 0.0f; + + mask[offset++] = 1.0f; + mask[offset++] = s->player.offensive_prayer != OFFENSIVE_PRAYER_NONE ? 1.0f : 0.0f; + mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; + mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; + mask[offset++] = s->player.current_prayer > 0 ? 1.0f : 0.0f; + + int mask_offset = 0; + for (int h = 0; h < INF_NUM_ACTION_HEADS; h++) { + int valid = 0; + for (int a = 0; a < INF_ACTION_DIMS[h]; a++) { + if (mask[mask_offset + a] > 0.0f) + valid++; + } + if (valid == 0) + s->zero_valid_action_head_count[h]++; + if (valid < s->min_valid_action_count_by_head[h]) + s->min_valid_action_count_by_head[h] = valid; + mask_offset += INF_ACTION_DIMS[h]; + } + int target_offset = inf_action_head_mask_offset(INF_HEAD_TARGET) + 1; + for (int n = 0; n < INF_OBS_NPCS; n++) { + if (mask[target_offset + n] <= 0.0f) + continue; + int npc_idx = s->current_obs_slots[n]; + if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS) + continue; + int type = s->npcs[npc_idx].type; + if (type == INF_NPC_HEALER_ZUK) + s->target_head_valid_healer_count++; + else if (type == INF_NPC_ZUK) + s->target_head_valid_zuk_count++; + else if (type != INF_NPC_ZUK_SHIELD) + s->target_head_valid_set_count++; + } + s->total_action_mask_checks++; +} + +static void inf_write_mask(EncounterState* state, float* mask) { + inf_write_mask_ctx(state, (EncounterContext*)inf_legacy_context(), mask); +} + +static float inf_get_reward_ctx(EncounterState* state, EncounterContext* context) { + (void)context; + return ((InfernoState*)state)->reward; +} + +static int inf_is_terminal_ctx(EncounterState* state, EncounterContext* context) { + (void)context; + return ((InfernoState*)state)->episode_over; +} + +static int inf_get_entity_count_ctx(EncounterState* state, EncounterContext* context) { + (void)context; + InfernoState* s = (InfernoState*)state; + int count = 1; + for (int i = 0; i < INF_MAX_NPCS; i++) + if (s->npcs[i].active) count++; + return count; +} + +static void* inf_get_entity_ctx(EncounterState* state, EncounterContext* context, int index) { + (void)context; + InfernoState* s = (InfernoState*)state; + if (index == 0) return &s->player; + return NULL; +} + +static void inf_fill_render_entities_ctx( + EncounterState* state, + EncounterContext* context, + RenderEntity* out, + int max_entities, + int* count +) { + InfernoState* s = (InfernoState*)state; + InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + int n = 0; + + { + const EncounterLoadoutStats* ls = inf_current_loadout_stats(s, ctx); + s->player.gui_max_hit = ls->max_hit; + s->player.gui_attack_speed = ls->attack_speed; + s->player.gui_attack_range = ls->attack_range; + s->player.gui_strength_bonus = ls->strength_bonus; + } + + if (n < max_entities) { + osrs_render_entity_from_player_entity(&s->player, &out[n++]); + } + + for (int i = 0; i < INF_MAX_NPCS && n < max_entities; i++) { + InfNPC* npc = &s->npcs[i]; + if (!npc->active) continue; + + int attack_target_entity_idx = 0; + if (npc->type == INF_NPC_NIBBLER || npc->resurrecting_this_tick) { + attack_target_entity_idx = -1; + } else if (npc->aggro_target >= 0 && npc->aggro_target < INF_MAX_NPCS && + s->npcs[npc->aggro_target].active) { + attack_target_entity_idx = -1; + } + + int npc_anim_id = -1; + const NpcModelMapping* nm = npc_model_lookup(INF_NPC_DEF_IDS[npc->type]); + if (npc->death_ticks > 0) { + npc_anim_id = inf_npc_death_anim_id(npc, nm); + } else if (npc->type == INF_NPC_MELEER && + npc->dig_freeze_timer == 6) { + npc_anim_id = INF_GEN_ANIM_MELEER_DIG_DOWN; + } else if (npc->type == INF_NPC_MELEER && + npc->dig_attack_delay == 6) { + npc_anim_id = INF_GEN_ANIM_MELEER_DIG_UP; + } else if (npc->attacked_this_tick) { + npc_anim_id = inf_npc_attack_anim_id(npc, nm); + } + + RenderMovementKind render_movement_kind = + (npc->type == INF_NPC_MELEER && npc->dig_attack_delay == 6) + ? RENDER_MOVEMENT_TELEPORT + : RENDER_MOVEMENT_NORMAL; + + int dest_x = npc->target_x; + int dest_y = npc->target_y; + if (npc->type == INF_NPC_NIBBLER) { + int tp = s->nibbler_target_pillar; + if (tp >= 0 && tp < INF_NUM_PILLARS && s->pillars[tp].active) { + dest_x = s->pillars[tp].x + INF_PILLAR_SIZE / 2; + dest_y = s->pillars[tp].y + INF_PILLAR_SIZE / 2; + } else { + dest_x = npc->x; + dest_y = npc->y; + } + } else if (npc->resurrection_visual_target >= 0 && + npc->resurrection_visual_target < INF_MAX_NPCS && + s->npcs[npc->resurrection_visual_target].active) { + InfNPC* at = &s->npcs[npc->resurrection_visual_target]; + dest_x = at->x + at->size / 2; + dest_y = at->y + at->size / 2; + } else if (npc->aggro_target >= 0 && npc->aggro_target < INF_MAX_NPCS && + s->npcs[npc->aggro_target].active) { + InfNPC* at = &s->npcs[npc->aggro_target]; + dest_x = at->x + at->size / 2; + dest_y = at->y + at->size / 2; + } + + RenderEntity* re = &out[n++]; + OsrsNpcRenderEntitySpec npc_spec = { + .npc_def_id = INF_NPC_DEF_IDS[npc->type], + .npc_slot = i, + .npc_instance_id = npc->render_id, + .npc_visible = npc->active, + .npc_size = npc->size, + .npc_anim_id = npc_anim_id, + .x = npc->x, + .y = npc->y, + .dest_x = dest_x, + .dest_y = dest_y, + .render_movement_kind = render_movement_kind, + .current_hitpoints = npc->hp, + .base_hitpoints = npc->max_hp, + .attack_style_this_tick = npc->attacked_this_tick + ? npc->attack_style_this_tick + : ATTACK_STYLE_NONE, + .hit_landed_this_tick = npc->hit_landed_this_tick, + .hit_damage = npc->hit_damage, + .hit_was_successful = npc->hit_was_successful_this_tick, + .hit_spell_type = npc->hit_spell_type, + .attack_target_entity_idx = attack_target_entity_idx, + }; + osrs_render_entity_from_npc_spec(&npc_spec, re); + } + + encounter_resolve_attack_target(out, n, s->interaction.target_slot); + *count = n; +} + +static void inf_fill_render_entities( + EncounterState* state, + RenderEntity* out, + int max_entities, + int* count +) { + inf_fill_render_entities_ctx( + state, (EncounterContext*)inf_legacy_context(), out, max_entities, count); +} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc new file mode 100644 index 0000000000..9cce365979 --- /dev/null +++ b/ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc @@ -0,0 +1,1107 @@ +#line 3605 "encounter_inferno.h" +#define INF_HEAD_MOVE 0 +#define INF_HEAD_PRAYER 1 +#define INF_HEAD_TARGET 2 +#define INF_HEAD_GEAR 3 +#define INF_HEAD_EAT 4 +#define INF_HEAD_POTION 5 +#define INF_HEAD_SPELL 6 +#define INF_HEAD_SPEC 7 +#define INF_HEAD_OFFENSIVE 8 +#define INF_ACTION_DIMS_INIT { \ + ENCOUNTER_MOVE_ACTIONS, \ + ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION, \ + INF_OBS_NPCS+1, \ + 4, \ + 2, \ + 4, \ + 3, \ + 2, \ + ENCOUNTER_OFFENSIVE_DIM \ +} +static const int INF_ACTION_DIMS[INF_NUM_ACTION_HEADS] = INF_ACTION_DIMS_INIT; +#define INF_ACTION_MASK_SIZE (ENCOUNTER_MOVE_ACTIONS + ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION + INF_OBS_NPCS+1 + 4 + 2 + 4 + 3 + 2 + ENCOUNTER_OFFENSIVE_DIM) +#define INF_OVERHEAD_SET_REFRESH_REDEMPTION ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE + +static int inf_action_head_mask_offset(int head) { + int offset = 0; + for (int h = 0; h < head; h++) + offset += INF_ACTION_DIMS[h]; + return offset; +} + +static int inf_canonical_overhead_action(int action) { + return action == INF_OVERHEAD_SET_REFRESH_REDEMPTION + ? ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION + : action; +} + +static int inf_local_overhead_action(int action) { + if (action == ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION) + return INF_OVERHEAD_SET_REFRESH_REDEMPTION; + if (action == ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE) + return ENCOUNTER_OVERHEAD_NO_CHANGE; + return action; +} + +static void inf_record_offensive_prayer_attack( + InfernoState* s, + AttackStyle style, + float damage_roll +) { + OffensivePrayer required = encounter_offensive_prayer_for_style(style); + if (required == OFFENSIVE_PRAYER_NONE) return; + + int style_idx = (int)style; + assert(style_idx > ATTACK_STYLE_NONE && style_idx <= ATTACK_STYLE_MAGIC); + int correct = s->player.offensive_prayer == required; + s->tick_scratch.offensive_prayer_correct = correct; + s->total_offensive_prayer_attacks++; + s->total_offensive_prayer_correct += correct; + s->offensive_prayer_attacks_by_style[style_idx]++; + s->offensive_prayer_correct_by_style[style_idx] += correct; + if (correct && damage_roll > 0.0f) + s->tick_scratch.offensive_prayer_correct_damage_roll += damage_roll; +} + +static float inf_record_player_reward_damage( + InfernoState* s, + int npc_idx, + int damage_roll +) { + if (npc_idx < 0 || npc_idx >= INF_MAX_NPCS || damage_roll <= 0) return 0.0f; + + InfNPC* npc = &s->npcs[npc_idx]; + if (!npc->active || npc->death_ticks > 0 || npc->hp <= 0) return 0.0f; + + float damage = (float)(damage_roll > npc->hp ? npc->hp : damage_roll); + s->tick_scratch.damage_dealt += damage; + if (npc->resurrection_count != 0) { + s->tick_scratch.damage_resurrected += damage; + return damage; + } + + switch (npc->type) { + case INF_NPC_ZUK: + s->tick_scratch.damage_zuk += damage; + break; + case INF_NPC_HEALER_ZUK: + s->tick_scratch.damage_zuk_healers += damage; + break; + case INF_NPC_HEALER_JAD: + s->tick_scratch.damage_jad_healers += damage; + break; + case INF_NPC_JAD: + s->tick_scratch.damage_jad += damage; + break; + case INF_NPC_ZUK_SHIELD: + break; + default: + s->tick_scratch.damage_set += damage; + break; + } + return damage; +} + +static int inf_tile_walkable(void* data, int x, int y) { + InfWalkCtx* wc = (InfWalkCtx*)data; + InfernoState* s = wc->s; + const InfernoContext* ctx = wc->ctx; + if (!inf_in_arena(x, y)) return 0; + if (inf_blocked_by_pillar(s, x, y, 1)) return 0; + if (ctx->collision_map) + if (!collision_tile_walkable(ctx->collision_map, 0, + x + ctx->world_offset_x, y + ctx->world_offset_y)) + return 0; + return 1; +} + +typedef struct { + InfernoState* s; + InfernoContext* ctx; + int manual_spell; +} InfPlayerStepTargetCtx; + +static int inf_lookup_player_attack_target( + void* data, + int target_slot, + OsrsAttackTarget* out +) { + InfPlayerStepTargetCtx* tc = (InfPlayerStepTargetCtx*)data; + InfernoState* s = tc->s; + InfernoContext* ctx = tc->ctx; + if (target_slot < 0 || target_slot >= INF_MAX_NPCS) + return 0; + + InfNPC* npc = &s->npcs[target_slot]; + int target_can_manual_phantom = + inf_is_barrage_spell(tc->manual_spell) && + inf_npc_is_phantom_barrage_cast_window(s, ctx, target_slot); + if (!npc->active || + (npc->death_ticks > 0 && !target_can_manual_phantom)) + return 0; + + InfPlayerAttack attack; + int has_attack = inf_resolve_player_attack_ctx( + s, ctx, tc->manual_spell, &attack); + const EncounterLoadoutStats* ls = has_attack + ? &attack.stats + : inf_current_loadout_stats(s, ctx); + *out = (OsrsAttackTarget){ + .slot = target_slot, + .x = npc->x, + .y = npc->y, + .size = npc->size, + .attack_range = ls->attack_range, + }; + return 1; +} + +#define INF_BREW_HEAL osrs_brew_heal_amount(99) +#define INF_RESTORE_AMOUNT osrs_super_restore_amount(99) +#define INF_NPC_DEATH_LINGER_TICKS 3 + +static void inf_apply_npc_death(InfernoState* s, int npc_idx) { + InfNPC* npc = &s->npcs[npc_idx]; + if (!osrs_npc_death_linger_start( + npc->hp, npc->active, &npc->death_ticks, INF_NPC_DEATH_LINGER_TICKS)) + return; + encounter_pending_hit_queue_clear(&npc->pending_hits); + s->total_npc_kills++; + + if (npc->type == INF_NPC_BLOB) + npc->spawn_blob_splits_on_removal = 1; + inf_store_dead_mob(s, npc); + + if (npc->type == INF_NPC_JAD) { + for (int j = 0; j < INF_MAX_NPCS; j++) { + if (s->npcs[j].active && + s->npcs[j].type == INF_NPC_HEALER_JAD && + inf_npc_healer(&s->npcs[j])->owner_idx == npc_idx) { + inf_deactivate_npc(s, j); + } + } + } +} + +static void inf_apply_blood_barrage_heal_damage(InfernoState* s, int damage) { + if (damage <= 0) return; + int hp_before = s->player.current_hitpoints; + s->player.current_hitpoints += damage / 4; + if (s->player.current_hitpoints > s->player.base_hitpoints) + s->player.current_hitpoints = s->player.base_hitpoints; + s->tick_scratch.blood_heal += s->player.current_hitpoints - hp_before; +} + +typedef struct { + int target_slot; + int gear; + int overhead_style; + int offensive; +} InfOraclePick; + +static int inf_oracle_trigger_active(const InfernoState* s, int mode) { + int zuk_idx = inf_find_live_zuk_idx(s); + if (zuk_idx < 0) return 0; + int zuk_hp = s->npcs[zuk_idx].hp; + if (mode == 1 || mode == 2 || mode == 8) return zuk_hp <= 300; + if (mode == 3) return zuk_hp <= 240; + if (mode >= 4 && mode <= 7) return s->zuk.jad_spawned || zuk_hp <= 600; + if (mode == 9 || mode == 10 || mode == 11) + return s->zuk.healer_spawned && s->total_zuk_healer_tags < 4; + return 0; +} + +static InfOraclePick inf_oracle_pick_full(const InfernoState* s, int mode) { + InfOraclePick pick = { -1, -1, ATTACK_STYLE_NONE, OFFENSIVE_PRAYER_NONE }; + if (mode <= 0 || !inf_oracle_trigger_active(s, mode)) return pick; + + if (mode == 9) { + for (int o = 33; o < 37; o++) { + int n = s->current_obs_slots[o]; + if (n < 0 || n >= INF_MAX_NPCS) continue; + const InfNPC* npc = &s->npcs[n]; + if (!npc->active || npc->death_ticks != 0 || + npc->type != INF_NPC_HEALER_ZUK) + continue; + if (npc->aggro_target >= 0 && npc->aggro_target < INF_MAX_NPCS && + s->npcs[npc->aggro_target].active && + s->npcs[npc->aggro_target].type == INF_NPC_ZUK) { + pick.target_slot = o; + return pick; + } + } + return pick; + } + + if (mode == 10 || mode == 11) { + if (!inf_player_behind_zuk_shield_now(s)) return pick; + if (mode == 11 && s->player.attack_timer != 0) return pick; + + for (int o = 33; o < 37; o++) { + int n = s->current_obs_slots[o]; + if (!inf_is_untagged_live_zuk_healer_slot(s, n)) + continue; + if (inf_player_can_attack_npc_from_current_tile(s, n)) { + pick.target_slot = o; + return pick; + } + } + return pick; + } + + for (int o = 22; o < 25; o++) { + int n = s->current_obs_slots[o]; + if (n >= 0 && n < INF_MAX_NPCS && + s->npcs[n].active && s->npcs[n].death_ticks == 0 && + s->npcs[n].type == INF_NPC_JAD) { + pick.target_slot = o; + pick.gear = INF_GEAR_LONG_RANGE; + pick.offensive = OFFENSIVE_PRAYER_RIGOUR; + AttackStyle jas = inf_npc_jad_const(&s->npcs[n])->attack_style; + if (jas == ATTACK_STYLE_RANGED) pick.overhead_style = ATTACK_STYLE_RANGED; + else if (jas == ATTACK_STYLE_MAGIC) pick.overhead_style = ATTACK_STYLE_MAGIC; + return pick; + } + } + if (mode == 1) return pick; + + for (int o = 33; o < 37; o++) { + int n = s->current_obs_slots[o]; + if (n >= 0 && n < INF_MAX_NPCS && + s->npcs[n].active && s->npcs[n].death_ticks == 0 && + s->npcs[n].type == INF_NPC_HEALER_ZUK) { + pick.target_slot = o; + pick.gear = INF_GEAR_BP; + pick.offensive = OFFENSIVE_PRAYER_RIGOUR; + pick.overhead_style = ATTACK_STYLE_MAGIC; + return pick; + } + } + + for (int o = 0; o < 22; o++) { + int n = s->current_obs_slots[o]; + if (n < 0 || n >= INF_MAX_NPCS) continue; + if (!s->npcs[n].active || s->npcs[n].death_ticks != 0) continue; + int t = s->npcs[n].type; + if (t == INF_NPC_ZUK || t == INF_NPC_ZUK_SHIELD || + t == INF_NPC_JAD || + t == INF_NPC_HEALER_ZUK || t == INF_NPC_HEALER_JAD) + continue; + pick.target_slot = o; + pick.offensive = OFFENSIVE_PRAYER_RIGOUR; + if (t == INF_NPC_MAGER) { + pick.gear = INF_GEAR_LONG_RANGE; + pick.overhead_style = ATTACK_STYLE_MAGIC; + } else if (t == INF_NPC_RANGER) { + pick.gear = INF_GEAR_LONG_RANGE; + pick.overhead_style = ATTACK_STYLE_RANGED; + } else if (t == INF_NPC_MELEER) { + pick.gear = INF_GEAR_BP; + pick.overhead_style = ATTACK_STYLE_MELEE; + } else { + pick.gear = INF_GEAR_BP; + } + return pick; + } + + return pick; +} + +static int inf_oracle_overrides_target(int mode) { + return mode >= 1 && mode <= 11; +} + +static int inf_oracle_overrides_gear_offensive(int mode) { + return mode == 6 || mode == 7 || mode == 8; +} + +static int inf_oracle_overrides_overhead(int mode) { + return mode == 5 || mode == 7 || mode == 8; +} + +static int inf_oracle_overhead_action_for(int wanted_style) { + if (wanted_style == ATTACK_STYLE_RANGED) return ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; + if (wanted_style == ATTACK_STYLE_MAGIC) return ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC; + if (wanted_style == ATTACK_STYLE_MELEE) return ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE; + return ENCOUNTER_OVERHEAD_NO_CHANGE; +} + +static int inf_oracle_offensive_action_for(int wanted_offensive) { + if (wanted_offensive == OFFENSIVE_PRAYER_RIGOUR) { + return ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR; + } + if (wanted_offensive == OFFENSIVE_PRAYER_PIETY) { + return ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY; + } + if (wanted_offensive == OFFENSIVE_PRAYER_AUGURY) { + return ENCOUNTER_OFFENSIVE_SET_REFRESH_AUGURY; + } + return ENCOUNTER_OFFENSIVE_NO_CHANGE; +} + +static int inf_oracle_gear_action_for(const InfernoState* s, int wanted_gear) { + if (wanted_gear < 0) return 0; + if ((int)s->weapon_set == wanted_gear) return 0; + return wanted_gear + 1; +} + +static void inf_player_pretick(InfernoState* s, InfernoContext* ctx, const int* actions) { + OffensivePrayer prev_offensive = s->player.offensive_prayer; + int prayer_act = inf_canonical_overhead_action(actions[INF_HEAD_PRAYER]); + int offensive_act = actions[INF_HEAD_OFFENSIVE]; + if (ctx->config.oracle_mode > 0) { + InfOraclePick pick = inf_oracle_pick_full(s, ctx->config.oracle_mode); + if (pick.target_slot >= 0) { + if (inf_oracle_overrides_overhead(ctx->config.oracle_mode) && + pick.overhead_style != ATTACK_STYLE_NONE) { + prayer_act = inf_oracle_overhead_action_for(pick.overhead_style); + } + if (inf_oracle_overrides_gear_offensive(ctx->config.oracle_mode) && + pick.offensive != OFFENSIVE_PRAYER_NONE) { + offensive_act = inf_oracle_offensive_action_for(pick.offensive); + } + } + } + if (encounter_apply_overhead_action(&s->player.prayer, prayer_act)) { + s->player.prayer_just_activated = 1; + } + if (encounter_apply_offensive_action(&s->player.offensive_prayer, offensive_act)) { + s->player.offensive_prayer_just_activated = 1; + } + encounter_drain_all_prayers( + &s->player, encounter_player_prayer_bonus(&s->player)); + if (s->player.offensive_prayer != prev_offensive) { + encounter_recompute_loadout_max_hits(s->loadout_stats, INF_NUM_WEAPON_SETS, &s->player); + if (ctx->human_command_mode) + inf_refresh_human_loadout_stats(s); + } +} + +static FightStyle inf_default_fight_style_for_style(AttackStyle style) { + if (style == ATTACK_STYLE_MAGIC) return FIGHT_STYLE_AUTOCAST; + if (style == ATTACK_STYLE_RANGED) return FIGHT_STYLE_RAPID; + return FIGHT_STYLE_ACCURATE; +} + +static void inf_note_human_weapon_set(InfernoState* s) { + uint8_t weapon = s->player.equipped[GEAR_SLOT_WEAPON]; + const uint8_t* const* loadouts = + inf_loadouts_for_profile(s->active_loadout_profile); + for (int g = 0; g < INF_NUM_WEAPON_SETS; g++) { + if (loadouts[g][GEAR_SLOT_WEAPON] == weapon) { + s->weapon_set = (InfWeaponSet)g; + return; + } + } +} + +static void inf_apply_human_player_commands_ctx(InfernoState* s, InfernoContext* ctx) { + int did_change_equipment = 0; + for (int i = 0; i < ctx->human_command_count; i++) { + const HumanCommand* cmd = &ctx->human_commands[i]; + if (cmd->kind == HUMAN_COMMAND_EQUIP_INVENTORY_ITEM) { + if (cmd->gear_slot >= 0 && cmd->gear_slot < NUM_GEAR_SLOTS && + cmd->item_db_idx >= 0 && cmd->item_db_idx < NUM_ITEMS) { + int changed = slot_equip_item(&s->player, cmd->gear_slot, (uint8_t)cmd->item_db_idx); + if (changed) { + s->total_gear_switches++; + did_change_equipment = 1; + if (cmd->gear_slot == GEAR_SLOT_WEAPON) { + AttackStyle style = inf_player_equipped_attack_style(s); + if (item_supports_ancient_autocast(s->player.equipped[GEAR_SLOT_WEAPON])) { + s->player.fight_style = s->player.autocast_defensive + ? FIGHT_STYLE_DEFENSIVE_AUTOCAST + : FIGHT_STYLE_AUTOCAST; + } else { + s->player.fight_style = inf_default_fight_style_for_style(style); + } + inf_note_human_weapon_set(s); + } + } + } + } else if (cmd->kind == HUMAN_COMMAND_FIGHT_STYLE) { + if (cmd->fight_style >= FIGHT_STYLE_ACCURATE && + cmd->fight_style <= FIGHT_STYLE_DEFENSIVE_AUTOCAST) { + s->player.fight_style = (FightStyle)cmd->fight_style; + if (cmd->fight_style == FIGHT_STYLE_AUTOCAST || + cmd->fight_style == FIGHT_STYLE_DEFENSIVE_AUTOCAST) { + s->player.autocast_enabled = 1; + s->player.autocast_defensive = + cmd->fight_style == FIGHT_STYLE_DEFENSIVE_AUTOCAST; + } + did_change_equipment = 1; + } + } else if (cmd->kind == HUMAN_COMMAND_SET_AUTOCAST) { + if (inf_is_barrage_spell(cmd->autocast_spell)) { + s->player.autocast_enabled = 1; + s->player.autocast_defensive = cmd->autocast_defensive != 0; + s->player.autocast_spell = cmd->autocast_spell; + if (item_supports_ancient_autocast(s->player.equipped[GEAR_SLOT_WEAPON])) { + s->player.fight_style = s->player.autocast_defensive + ? FIGHT_STYLE_DEFENSIVE_AUTOCAST + : FIGHT_STYLE_AUTOCAST; + } + did_change_equipment = 1; + } + } + } + if (did_change_equipment) + inf_refresh_human_loadout_stats(s); +} + +static void inf_apply_human_player_commands(InfernoState* s) { + inf_apply_human_player_commands_ctx(s, inf_legacy_context()); +} + +static void inf_tick_player_ctx( + InfernoState* s, + InfernoContext* ctx, + const int* actions, + int can_attack +) { + s->tick_scratch.offensive_prayer_correct = 0; + if (s->player_last_interaction_age == 0) + s->player_last_interaction_age = 1; + + if (ctx->human_command_mode) { + inf_apply_human_player_commands_ctx(s, ctx); + } else { + int gear_act = actions[INF_HEAD_GEAR]; + if (inf_oracle_overrides_gear_offensive(ctx->config.oracle_mode)) { + InfOraclePick pick = inf_oracle_pick_full(s, ctx->config.oracle_mode); + if (pick.target_slot >= 0 && pick.gear >= 0) { + gear_act = inf_oracle_gear_action_for(s, pick.gear); + } + } + if (gear_act >= 1) s->total_gear_switches++; + if (gear_act >= 1 && gear_act <= 3) { + InfWeaponSet new_set = (InfWeaponSet)(gear_act - 1); + s->weapon_set = new_set; + GearSet gs = (new_set == INF_GEAR_MAGE) ? GEAR_MAGE : GEAR_RANGED; + const uint8_t* const* loadouts = + inf_loadouts_for_profile(s->active_loadout_profile); + encounter_apply_loadout(&s->player, loadouts[new_set], gs); + } + { + const uint8_t* const* loadouts = + inf_loadouts_for_profile(s->active_loadout_profile); + uint8_t current_weapon = s->player.equipped[GEAR_SLOT_WEAPON]; + if (current_weapon != loadouts[s->weapon_set][GEAR_SLOT_WEAPON]) { + for (int g = 0; g < INF_NUM_WEAPON_SETS; g++) { + if (loadouts[g][GEAR_SLOT_WEAPON] == current_weapon) { + s->weapon_set = (InfWeaponSet)g; + GearSet gs = (g == INF_GEAR_MAGE) ? GEAR_MAGE : GEAR_RANGED; + encounter_apply_loadout(&s->player, loadouts[g], gs); + break; + } + } + } + } + } + + int manual_spell = inf_spell_action_to_spell(actions[INF_HEAD_SPELL]); + + osrs_tick_special_regen(&s->player); + + if (actions[INF_HEAD_SPEC] == 1) + osrs_spec_toggle(&s->player.spec_armed); + + if (s->tick > 0 && s->tick % 60 == 0) { + int* stats[] = { &s->player.current_ranged, &s->player.current_magic, + &s->player.current_attack, &s->player.current_strength, + &s->player.current_defence }; + for (int si = 0; si < 5; si++) { + if (*stats[si] > 99) (*stats[si])--; + else if (*stats[si] < 99) (*stats[si])++; + } + encounter_recompute_loadout_max_hits(s->loadout_stats, INF_NUM_WEAPON_SETS, &s->player); + if (ctx->human_command_mode) + inf_refresh_human_loadout_stats(s); + } + + if (s->player.potion_timer > 0) s->player.potion_timer--; + if (s->stamina_active_ticks > 0) s->stamina_active_ticks--; + + int eat_act = actions[INF_HEAD_EAT]; + if (eat_act == 1 && s->player.brew_doses > 0 && s->player.potion_timer == 0 + && s->player.current_hitpoints < s->player.base_hitpoints) { + encounter_apply_brew_heal_and_timer(&s->player, INF_BREW_HEAL); + s->tick_scratch.brewed = 1; + encounter_apply_consumable_stat_effect( + &s->player, ENCOUNTER_CONSUMABLE_STAT_EFFECT_BREW_DRAIN); + encounter_recompute_loadout_max_hits(s->loadout_stats, INF_NUM_WEAPON_SETS, &s->player); + if (ctx->human_command_mode) + inf_refresh_human_loadout_stats(s); + } + + int pot_act = actions[INF_HEAD_POTION]; + if (pot_act == 1 && s->player.restore_doses > 0 && s->player.potion_timer == 0) { + encounter_add_prayer_restore(&s->player, INF_RESTORE_AMOUNT); + encounter_cap_prayer_restore(&s->player); + encounter_apply_consumable_stat_effect( + &s->player, ENCOUNTER_CONSUMABLE_STAT_EFFECT_RESTORE); + encounter_finish_potion_dose(&s->player, &s->player.restore_doses); + encounter_recompute_loadout_max_hits(s->loadout_stats, INF_NUM_WEAPON_SETS, &s->player); + if (ctx->human_command_mode) + inf_refresh_human_loadout_stats(s); + } else if (pot_act == 2 && s->player.bastion_doses > 0 && s->player.potion_timer == 0) { + encounter_bastion_boost(&s->player); + encounter_finish_potion_dose(&s->player, &s->player.bastion_doses); + encounter_recompute_loadout_max_hits(s->loadout_stats, INF_NUM_WEAPON_SETS, &s->player); + if (ctx->human_command_mode) + inf_refresh_human_loadout_stats(s); + } else if (pot_act == 3 && s->player.stamina_doses > 0 && s->player.potion_timer == 0) { + s->stamina_active_ticks = 200; + s->player.stamina_doses--; + s->player.potion_timer = 3; + } + + if (eat_act > 0) + osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_EAT); + if (pot_act > 0) + osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_DRINK); + + inf_refresh_current_obs_slots_ctx(s, ctx); + + int target = actions[INF_HEAD_TARGET]; + if (inf_oracle_overrides_target(ctx->config.oracle_mode)) { + InfOraclePick pick = inf_oracle_pick_full(s, ctx->config.oracle_mode); + if (pick.target_slot >= 0) target = pick.target_slot + 1; + } + int has_new_target = 0; + if (target > 0 && target <= INF_OBS_NPCS) { + int obs_idx = target - 1; + int npc_idx = s->current_obs_slots[obs_idx]; + int is_live_target = + npc_idx >= 0 && npc_idx < INF_MAX_NPCS && + s->npcs[npc_idx].active && + s->npcs[npc_idx].death_ticks == 0 && + s->npcs[npc_idx].type != INF_NPC_ZUK_SHIELD; + int is_phantom_target = + npc_idx >= 0 && npc_idx < INF_MAX_NPCS && + inf_is_barrage_spell(manual_spell) && + inf_npc_is_phantom_barrage_obs_candidate(s, ctx, npc_idx); + if (is_live_target || is_phantom_target) { + if (npc_idx < 32 && inf_is_untagged_live_zuk_healer_slot(s, npc_idx)) { + int is_safe_target = + inf_player_behind_zuk_shield_now(s) && + s->player.attack_timer == 0 && + inf_player_can_attack_npc_from_current_tile(s, npc_idx); + s->total_zuk_untagged_healer_targets++; + if (is_safe_target) + s->total_zuk_safe_untagged_healer_targets++; + else + s->total_zuk_unsafe_untagged_healer_targets++; + if ((s->rewarded_zuk_healer_target_mask & (1u << npc_idx)) == 0) { + if (ctx->config.zuk_untagged_healer_target_bonus_coeff > 0.0f) { + s->tick_scratch.zuk_untagged_healer_targets++; + s->total_zuk_untagged_healer_target_rewards++; + s->rewarded_zuk_healer_target_mask |= 1u << npc_idx; + } else if ( + ctx->config.zuk_safe_untagged_healer_target_bonus_coeff > 0.0f && + is_safe_target) { + s->tick_scratch.zuk_safe_untagged_healer_targets++; + s->total_zuk_safe_untagged_healer_target_rewards++; + s->rewarded_zuk_healer_target_mask |= 1u << npc_idx; + } + } + } + osrs_interaction_set(&s->interaction, npc_idx); + s->player_last_interaction_target_slot = npc_idx; + s->player_last_interaction_age = 0; + has_new_target = 1; + } + } + if ((ctx->config.oracle_mode == 10 || ctx->config.oracle_mode == 11) && + osrs_interaction_active(&s->interaction) && + inf_is_untagged_live_zuk_healer_slot(s, s->interaction.target_slot) && + (!inf_player_behind_zuk_shield_now(s) || + !inf_player_can_attack_npc_from_current_tile( + s, s->interaction.target_slot) || + (ctx->config.oracle_mode == 11 && s->player.attack_timer != 0))) { + osrs_interaction_clear(&s->interaction); + } + if (ctx->config.zuk_safe_untagged_healer_target_mask && + osrs_interaction_active(&s->interaction) && + !inf_untagged_zuk_healer_target_is_safe_now( + s, ctx, + s->interaction.target_slot)) { + osrs_interaction_clear(&s->interaction); + } + if (ctx->config.zuk_force_safe_untagged_healer_target_mask && + osrs_interaction_active(&s->interaction) && + inf_has_safe_untagged_zuk_healer_target_now(s, ctx) && + !inf_is_safe_untagged_zuk_healer_target_now( + s, ctx, + s->interaction.target_slot)) { + osrs_interaction_clear(&s->interaction); + } + int has_explicit_move = (actions[INF_HEAD_MOVE] > 0 || s->player_dest_x >= 0); + if (!has_new_target && has_explicit_move) + osrs_interaction_check_interrupt(&s->interaction, OSRS_IACT_MOVE); + if (osrs_interaction_active(&s->interaction)) { + int target_slot = s->interaction.target_slot; + int target_can_manual_phantom = + inf_is_barrage_spell(manual_spell) && + inf_npc_is_phantom_barrage_cast_window(s, ctx, target_slot); + if (!s->npcs[target_slot].active || + (s->npcs[target_slot].death_ticks > 0 && + !target_can_manual_phantom)) { + osrs_interaction_clear(&s->interaction); + } + } + + if (osrs_interaction_active(&s->interaction) && + inf_is_live_zuk_healer_slot(s, s->interaction.target_slot)) { + s->total_zuk_healer_target_ticks++; + if (s->tick_at_first_zuk_healer_target < 0) + s->tick_at_first_zuk_healer_target = s->tick; + } + + OsrsPlayerMoveKind move_kind = OSRS_PLAYER_MOVE_NONE; + int move_act = actions[INF_HEAD_MOVE]; + if (has_explicit_move && !osrs_interaction_active(&s->interaction)) { + if (s->player_dest_x >= 0) + move_kind = OSRS_PLAYER_MOVE_DESTINATION; + else if (move_act > 0 && move_act < ENCOUNTER_MOVE_ACTIONS) + move_kind = OSRS_PLAYER_MOVE_ACTION; + } + InfWalkCtx wc = { s, ctx }; + InfPlayerStepTargetCtx target_ctx = { + .s = s, + .ctx = ctx, + .manual_spell = manual_spell, + }; + OsrsLosQuery los_query = inf_player_los_query(s); + OsrsPlayerStepInput player_step_input = { + .player = &s->player, + .interaction = &s->interaction, + .target_lookup = inf_lookup_player_attack_target, + .target_ctx = &target_ctx, + .move_kind = move_kind, + .move_action = move_act, + .dest_x = &s->player_dest_x, + .dest_y = &s->player_dest_y, + .arena = { + .collision_map = ctx->collision_map, + .world_offset_x = ctx->world_offset_x, + .world_offset_y = ctx->world_offset_y, + .is_walkable = inf_tile_walkable, + .walkable_ctx = &wc, + .extra_blocked = inf_pathfind_blocked, + .blocked_ctx = &wc, + .los_query = &los_query, + .arena_base_x = INF_ARENA_MIN_X, + .arena_base_y = INF_ARENA_MIN_Y, + .arena_w = INF_ARENA_WIDTH, + .arena_h = INF_ARENA_HEIGHT, + }, + }; + (void)osrs_encounter_player_step(&player_step_input); + inf_rebuild_player_collision_flags(s); + + if (can_attack && s->player.attack_timer > 0) s->player.attack_timer--; + int has_zuk_healer_target = osrs_interaction_active(&s->interaction) && + inf_is_live_zuk_healer_slot(s, s->interaction.target_slot); + if (has_zuk_healer_target) { + if (!can_attack) { + s->total_zuk_healer_cannot_attack_ticks++; + } else if (s->player.attack_timer > 0) { + s->total_zuk_healer_cooldown_ticks++; + } else { + InfNPC* target_npc = &s->npcs[s->interaction.target_slot]; + InfPlayerAttack healer_attack; + int has_healer_attack = inf_resolve_player_attack_ctx(s, ctx, manual_spell, &healer_attack); + if (has_healer_attack && encounter_player_can_attack(s->player.x, s->player.y, + target_npc->x, target_npc->y, target_npc->size, + healer_attack.stats.attack_range, &los_query)) { + s->total_zuk_healer_attackable_ticks++; + } else { + s->total_zuk_healer_out_of_range_ticks++; + } + } + } + if (can_attack && osrs_interaction_active(&s->interaction) && s->player.attack_timer == 0) { + InfNPC* target_npc = &s->npcs[s->interaction.target_slot]; + if (target_npc->active) { + InfPlayerAttack attack; + int has_attack = inf_resolve_player_attack_ctx(s, ctx, manual_spell, &attack); + const EncounterLoadoutStats* ls = &attack.stats; + int is_magic_attack = attack.is_barrage; + int weapon_is_blowpipe = inf_player_weapon_is(s, ITEM_TOXIC_BLOWPIPE); + uint8_t player_weapon = attack.weapon; + int active_spell = attack.spell; + int is_manual_dying_barrage = + target_npc->death_ticks > 0 && inf_is_barrage_spell(manual_spell); + + if (target_npc->death_ticks > 0 && !is_manual_dying_barrage) { + osrs_interaction_clear(&s->interaction); + } else if (has_attack && encounter_player_can_attack(s->player.x, s->player.y, + target_npc->x, target_npc->y, target_npc->size, + ls->attack_range, &los_query)) { + int is_blowpipe_spec_attack = weapon_is_blowpipe && + s->player.spec_armed && s->player.special_energy >= BLOWPIPE_SPEC_COST; + EncounterProjectileDistanceMode distance_mode = is_magic_attack + ? ENCOUNTER_PROJECTILE_DISTANCE_TARGET_SW_TILE + : ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE; + int projectile_dist = encounter_projectile_distance( + s->player.x, s->player.y, 1, + target_npc->x, target_npc->y, target_npc->size, + distance_mode); + EncounterProjectileTiming hit_timing = inf_player_projectile_timing( + ls->style, player_weapon, is_blowpipe_spec_attack, projectile_dist); + int hit_delay = hit_timing.damage_delay_ticks; + + int total_dmg = 0; + + if (is_magic_attack) { + OsrsTargetRef target_ref = { + .kind = OSRS_TARGET_NPC, + .id = s->interaction.target_slot, + }; + OsrsMagicAttackKind magic_kind = (active_spell == ENCOUNTER_SPELL_ICE) + ? OSRS_MAGIC_ATTACK_ANCIENT_ICE + : OSRS_MAGIC_ATTACK_ANCIENT_BLOOD; + OsrsPreparedAttackEffects attack_effects = osrs_prepare_attack_effects( + &s->player.equipment_effect_profile, + &s->player.item_effect_state, + s->player.equipped[GEAR_SLOT_WEAPON], + ATTACK_STYLE_MAGIC, + magic_kind, + target_ref, + 1, + ls->eff_level * (ls->attack_bonus + 64), + ls->max_hit, + osrs_target_effect_context_none(), + s->player.current_hitpoints, + s->player.base_hitpoints + ); + + BarrageTarget btargets[INF_MAX_NPCS + 1]; + int bt_count = 0; + { + const InfNPCStats* ns = &INF_NPC_STATS[target_npc->type]; + btargets[bt_count++] = (BarrageTarget){ + .active = 1, .x = target_npc->x, .y = target_npc->y, + .magic_level = ns->magic_level, .magic_def_bonus = ns->magic_def_bonus, + .npc_idx = s->interaction.target_slot, + .frozen_ticks = &s->npcs[s->interaction.target_slot].frozen_ticks, + .hit = 0, .damage = 0 + }; + } + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (i == s->interaction.target_slot || !s->npcs[i].active) continue; + if (s->npcs[i].type == INF_NPC_ZUK_SHIELD) continue; + if (s->npcs[i].death_ticks > 0) continue; + const InfNPCStats* ns2 = &INF_NPC_STATS[s->npcs[i].type]; + btargets[bt_count++] = (BarrageTarget){ + .active = 1, .x = s->npcs[i].x, .y = s->npcs[i].y, + .magic_level = ns2->magic_level, .magic_def_bonus = ns2->magic_def_bonus, + .npc_idx = i, + .frozen_ticks = &s->npcs[i].frozen_ticks, + .hit = 0, .damage = 0 + }; + } + + BarrageResult br = osrs_barrage_resolve( + btargets, bt_count, attack_effects.attack_roll, attack_effects.max_hit, + &s->rng_state, active_spell, attack_effects.use_double_accuracy); + total_dmg = br.total_damage; + float reward_damage = 0.0f; + int phantom_primary_blood_damage = 0; + if (target_npc->death_ticks > 0) { + if (active_spell == ENCOUNTER_SPELL_BLOOD && btargets[0].hit) + phantom_primary_blood_damage = btargets[0].damage; + total_dmg -= btargets[0].damage; + btargets[0].hit = 0; + btargets[0].damage = 0; + } + inf_apply_blood_barrage_heal_damage( + s, phantom_primary_blood_damage); + osrs_finalize_attack_effects( + &s->player.equipment_effect_profile, + &s->player.item_effect_state, + s->player.equipped[GEAR_SLOT_WEAPON], + ATTACK_STYLE_MAGIC, + magic_kind, + target_ref, + 1, + attack_effects.use_double_accuracy, + btargets[0].hit, + btargets[0].damage, + &s->rng_state + ); + + for (int i = 0; i < bt_count; i++) { + if (!btargets[i].active || !btargets[i].rolled) continue; + int nidx = btargets[i].npc_idx; + if (s->npcs[nidx].death_ticks > 0) continue; + reward_damage += inf_record_player_reward_damage( + s, nidx, btargets[i].damage); + inf_queue_npc_pending_hit( + s, nidx, hit_delay, btargets[i].damage, + ATTACK_STYLE_MAGIC, active_spell, btargets[i].hit); + } + total_dmg = (int)reward_damage; + + } else if (is_blowpipe_spec_attack) { + if (!encounter_use_spec(&s->player, BLOWPIPE_SPEC_COST)) abort(); + osrs_spec_disarm(&s->player.spec_armed); + s->player.used_special_this_tick = 1; + const InfNPCStats* ns = &INF_NPC_STATS[target_npc->type]; + int base_att_roll = ls->eff_level * (ls->attack_bonus + 64); + total_dmg = osrs_blowpipe_spec_resolve( + base_att_roll, ls->max_hit, + ns->def_level, ns->ranged_def_bonus, &s->rng_state); + int heal = total_dmg * BLOWPIPE_SPEC_HEAL_PCT / 100; + s->player.current_hitpoints += heal; + if (s->player.current_hitpoints > s->player.base_hitpoints) + s->player.current_hitpoints = s->player.base_hitpoints; + int reward_damage = (int)inf_record_player_reward_damage( + s, s->interaction.target_slot, total_dmg); + total_dmg = reward_damage; + inf_queue_npc_pending_hit( + s, s->interaction.target_slot, hit_delay, total_dmg, + ATTACK_STYLE_RANGED, ENCOUNTER_SPELL_NONE, total_dmg > 0); + + } else if (ls->style == ATTACK_STYLE_RANGED) { + const InfNPCStats* ns = &INF_NPC_STATS[target_npc->type]; + OsrsPreparedAttackEffects attack_effects = osrs_prepare_attack_effects( + &s->player.equipment_effect_profile, + &s->player.item_effect_state, + s->player.equipped[GEAR_SLOT_WEAPON], + ATTACK_STYLE_RANGED, + OSRS_MAGIC_ATTACK_NONE, + (OsrsTargetRef){ .kind = OSRS_TARGET_NPC, .id = s->interaction.target_slot }, + 1, + ls->eff_level * (ls->attack_bonus + 64), + ls->max_hit, + osrs_target_effect_context_magic( + ns->magic_level, + ns->magic_att_bonus), + s->player.current_hitpoints, + s->player.base_hitpoints + ); + int def_roll = (ns->def_level + 9) * (ns->ranged_def_bonus + 64); + int hit_success = encounter_roll_hit_chance( + &s->rng_state, attack_effects.attack_roll, def_roll); + if (hit_success) { + total_dmg = encounter_rand_int(&s->rng_state, attack_effects.max_hit + 1); + } + int reward_damage = (int)inf_record_player_reward_damage( + s, s->interaction.target_slot, total_dmg); + total_dmg = reward_damage; + inf_queue_npc_pending_hit( + s, s->interaction.target_slot, hit_delay, total_dmg, + ATTACK_STYLE_RANGED, ENCOUNTER_SPELL_NONE, hit_success); + } + + s->player.attack_timer = ls->attack_speed; + inf_record_offensive_prayer_attack(s, ls->style, (float)total_dmg); + if (target_npc->type == INF_NPC_HEALER_ZUK) { + int target_was_untagged = target_npc->aggro_target >= 0; + s->total_zuk_healer_attack_fires++; + if (target_was_untagged && !is_magic_attack) + s->tick_scratch.zuk_untagged_healer_nonmagic_attacks++; + if (is_magic_attack) + s->tick_scratch.zuk_healer_mage_attack_fires++; + if (s->tick_at_first_zuk_healer_attack < 0) + s->tick_at_first_zuk_healer_attack = s->tick; + } + + s->tick_scratch.player_attacked = 1; + s->player_attack_npc_idx = s->interaction.target_slot; + s->player_attack_dmg = total_dmg; + s->player_attack_style_id = ls->style; + s->player_attack_timing = hit_timing; + + s->player.attack_style_this_tick = ls->style; + if (ls->style == ATTACK_STYLE_MAGIC) { + s->player.magic_type_this_tick = (active_spell == ENCOUNTER_SPELL_ICE) ? 1 : 2; + } + if (target_npc->death_ticks > 0) + osrs_interaction_clear(&s->interaction); + } + } + } +} + +static void inf_tick_player(InfernoState* s, const int* actions, int can_attack) { + inf_tick_player_ctx(s, inf_legacy_context(), actions, can_attack); +} + +static int inf_roll_delayed_jad_damage( + InfernoState* s, + const InfernoContext* ctx, + int attack_style +) { + const InfNPCStats* stats = &INF_NPC_STATS[INF_NPC_JAD]; + int max_hit = osrs_npc_max_hit(attack_style, + stats->str_level, stats->range_level, + stats->melee_str_bonus, stats->ranged_str_bonus, + stats->magic_base_dmg, stats->magic_dmg_pct); + if (stats->max_hit_cap > 0 && max_hit > stats->max_hit_cap) + max_hit = stats->max_hit_cap; + + int dmg = encounter_rand_int(&s->rng_state, max_hit + 1); + int att_lvl, att_bonus; + if (attack_style == ATTACK_STYLE_RANGED) { + att_lvl = stats->range_level; + att_bonus = stats->range_att_bonus; + } else { + att_lvl = stats->magic_level; + att_bonus = stats->magic_att_bonus; + } + + int att_roll = osrs_npc_attack_roll(att_lvl, att_bonus); + const EncounterLoadoutStats* ls = inf_current_loadout_stats(s, ctx); + int def_roll = encounter_player_def_roll_from_loadout( + s->player.current_defence, s->player.current_magic, + ls->def_stab, ls->def_slash, ls->def_crush, ls->def_magic, ls->def_ranged, + attack_style, stats->melee_style); + if (!encounter_roll_hit_chance(&s->rng_state, att_roll, def_roll)) + dmg = 0; + return dmg; +} + +static void inf_apply_delayed_prayer_check( + InfernoState* s, + const InfernoContext* ctx, + EncounterPendingHit* hit +) { + if (encounter_prayer_correct_for_style(s->player.prayer, hit->attack_style)) { + hit->damage = 0; + s->tick_scratch.prayer_correct++; + if (hit->source_npc_type >= 0 && hit->source_npc_type < INF_NUM_NPC_TYPES) + s->prayer_correct_by_type[hit->source_npc_type]++; + } else if (hit->source_npc_type == INF_NPC_JAD) { + hit->damage = inf_roll_delayed_jad_damage(s, ctx, hit->attack_style); + s->dmg_from_type[INF_NPC_JAD] += (float)hit->damage; + if (hit->damage > 0) { + s->last_hit_by_type = INF_NPC_JAD; + s->tick_scratch.off_prayer_hits++; + } + } else if (hit->damage > 0 && hit->attack_style != ATTACK_STYLE_NONE) { + s->tick_scratch.off_prayer_hits++; + } + hit->check_prayer = 0; +} + +static void inf_resolve_player_projectiles_on_npcs(InfernoState* s) { + int blood_heal_acc = 0; + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (!s->npcs[i].active || s->npcs[i].death_ticks > 0) continue; + InfNPC* npc = &s->npcs[i]; + for (int h = 0; h < npc->pending_hits.count;) { + EncounterPendingHit* ph = &npc->pending_hits.hits[h]; + int spell = ph->spell_type; + int hit_success = ph->hit_success; + int old_hit_landed = npc->hit_landed_this_tick; + int old_hit_damage = npc->hit_damage; + int hp_before = npc->hp; + int landed = encounter_resolve_npc_pending_hit( + ph, &npc->hp, &npc->hit_landed_this_tick, &npc->hit_damage, + &npc->frozen_ticks, &blood_heal_acc, NULL); + if (!landed) { + if (!ph->active) { + encounter_pending_hit_queue_remove(&npc->pending_hits, h, "inferno npc"); + continue; + } + h++; + continue; + } + + if (old_hit_landed) + npc->hit_damage += old_hit_damage; + + int t = npc->type; + int hp_after = npc->hp; + if (hp_before > 0 && hp_after <= 0) { + if (t == INF_NPC_JAD) s->tick_scratch.kill_jad++; + else if (t == INF_NPC_HEALER_ZUK) s->tick_scratch.kill_zuk_healer++; + else if (t != INF_NPC_ZUK && t != INF_NPC_ZUK_SHIELD && + t != INF_NPC_HEALER_JAD) s->tick_scratch.kill_set++; + } + npc->hit_spell_type = spell; + if (hit_success) + npc->hit_was_successful_this_tick = 1; + int shield_taggable = inf_is_shield_taggable_slot(s, i); + if (npc->aggro_target != -1) { + if (npc->type == INF_NPC_HEALER_ZUK || + npc->type == INF_NPC_HEALER_JAD) { + s->tick_scratch.healer_tags++; + if (npc->type == INF_NPC_HEALER_ZUK) + s->tick_scratch.zuk_healer_tags++; + } + if (shield_taggable) + s->tick_scratch.shield_tags++; + npc->aggro_target = -1; + npc->stun_timer = 2; + } + encounter_pending_hit_queue_remove(&npc->pending_hits, h, "inferno npc"); + inf_apply_npc_death(s, i); + if (!npc->active || npc->death_ticks > 0) + break; + } + } + inf_apply_blood_barrage_heal_damage(s, blood_heal_acc); +} + +static void inf_resolve_player_pending_hits(InfernoState* s) { + for (int i = 0; i < s->player_pending_hits.count; i++) { + EncounterPendingHit* hit = &s->player_pending_hits.hits[i]; + + hit->ticks_remaining--; + if (hit->ticks_remaining <= 0) { + int dmg = hit->damage; + if (hit->check_prayer) { + if (encounter_prayer_correct_for_style(s->player.prayer, hit->attack_style)) { + dmg = 0; + s->tick_scratch.prayer_correct++; + if (hit->source_npc_type >= 0 && hit->source_npc_type < INF_NUM_NPC_TYPES) + s->prayer_correct_by_type[hit->source_npc_type]++; + } else if (dmg > 0 && hit->attack_style != ATTACK_STYLE_NONE) { + s->tick_scratch.off_prayer_hits++; + } + } else if (dmg > 0 && hit->attack_style != ATTACK_STYLE_NONE && + hit->source_npc_type != INF_NPC_JAD) { + s->tick_scratch.off_prayer_hits++; + } + + if (dmg > 0 && + hit->source_npc_type >= 0 && + hit->source_npc_type < INF_NUM_NPC_TYPES) { + s->last_hit_by_type = hit->source_npc_type; + } + if (hit->elysian_reduced) { + s->player.elysian_proc_this_tick = 1; + } else { + int elysian_reduced = 0; + dmg = inf_apply_elysian_to_player_hit(s, dmg, &elysian_reduced); + if (elysian_reduced) s->player.elysian_proc_this_tick = 1; + } + inf_apply_echo_boots_recoil(s, hit->source_npc_slot, dmg); + inf_damage_player(s, dmg); + encounter_pending_hit_queue_remove(&s->player_pending_hits, i, "inferno player"); + i--; + } + } +} + +static void inf_resolve_jad_prayer_checks_after_player( + InfernoState* s, + const InfernoContext* ctx +) { + for (int i = 0; i < s->player_pending_hits.count; i++) { + EncounterPendingHit* hit = &s->player_pending_hits.hits[i]; + if (!hit->check_prayer || + hit->source_npc_type != INF_NPC_JAD || + hit->prayer_check_delay <= 0) { + continue; + } + hit->prayer_check_delay--; + if (hit->prayer_check_delay == 0) + inf_apply_delayed_prayer_check(s, ctx, hit); + } +} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc new file mode 100644 index 0000000000..114cf18997 --- /dev/null +++ b/ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc @@ -0,0 +1,1151 @@ +#line 7460 "encounter_inferno.h" +static void inf_require_valid_step_out_forecast_obs_mode(int value) { + if (value < INF_STEP_OUT_FORECAST_MODE_OFF || + value > INF_STEP_OUT_FORECAST_MODE_FAST_READONLY_MOVE) { + fprintf(stderr, + "inferno: step_out_forecast_obs_mode must be in [0,3], got %d\n", + value); + abort(); + } +} + +static void inf_put_int_ctx( + EncounterState* state, + EncounterContext* context, + const char* key, + int value +) { + InfernoState* s = (InfernoState*)state; + InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + if (strcmp(key, "start_wave") == 0) { + inf_require_valid_public_wave(value); + s->start_wave = value - 1; + ctx->config.start_wave = value - 1; + } + else if (strcmp(key, "seed") == 0) s->rng_state = (uint32_t)value; + else if (strcmp(key, "world_offset_x") == 0) ctx->world_offset_x = value; + else if (strcmp(key, "world_offset_y") == 0) ctx->world_offset_y = value; + else if (strcmp(key, "player_dest_x") == 0) s->player_dest_x = value; + else if (strcmp(key, "player_dest_y") == 0) s->player_dest_y = value; + else if (strcmp(key, "human_command_mode") == 0) + ctx->human_command_mode = encounter_require_binary_config("inferno", key, value); + else if (strcmp(key, "terminal_penalty_enabled") == 0) + ctx->config.terminal_penalty_enabled = + encounter_require_binary_config("inferno", key, value); + else if (strcmp(key, "curriculum_agent") == 0) + ctx->config.curriculum_agent = + encounter_require_binary_config("inferno", key, value); + else if (strcmp(key, "curriculum_supply_jitter_mode") == 0) { + inf_require_valid_curriculum_supply_mode(key, value); + ctx->config.curriculum_supply_jitter_mode = value; + } + else if (strcmp(key, "curriculum_no_brew_mode") == 0) { + inf_require_valid_curriculum_supply_mode(key, value); + ctx->config.curriculum_no_brew_mode = value; + } + else if (strcmp(key, "step_out_forecast_obs_enabled") == 0) { + int enabled = encounter_require_binary_config("inferno", key, value); + ctx->config.step_out_forecast_obs_enabled = enabled; + ctx->config.step_out_forecast_obs_mode = enabled ? + INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT : + INF_STEP_OUT_FORECAST_MODE_OFF; + } + else if (strcmp(key, "step_out_forecast_obs_mode") == 0) { + inf_require_valid_step_out_forecast_obs_mode(value); + ctx->config.step_out_forecast_obs_mode = value; + ctx->config.step_out_forecast_obs_enabled = + value != INF_STEP_OUT_FORECAST_MODE_OFF; + } + else if (strcmp(key, "loadout_profile_mode") == 0) { + inf_require_valid_loadout_profile_mode(value); + ctx->config.loadout_profile_mode = (InfLoadoutProfileMode)value; + } + else if (strcmp(key, "oracle_mode") == 0) { + if (value < 0 || value > 11) { + fprintf(stderr, "inferno: oracle_mode must be in [0,11], got %d\n", value); + abort(); + } + ctx->config.oracle_mode = value; + } + else if (strcmp(key, "zuk_healer_reward_mode") == 0) { + if (value < INF_ZUK_HEALER_REWARD_MODE_BASELINE || + value > INF_ZUK_HEALER_REWARD_MODE_TAGS_FIRST) { + fprintf(stderr, + "inferno: zuk_healer_reward_mode must be in [0,1], got %d\n", + value); + abort(); + } + ctx->config.zuk_healer_reward_mode = value; + } + else if (strcmp(key, "joseph_reward_mode") == 0) { + if (value < INF_JOSEPH_REWARD_MODE_OFF || + value > INF_JOSEPH_REWARD_MODE_ON) { + fprintf(stderr, + "inferno: joseph_reward_mode must be in [0,1], got %d\n", + value); + abort(); + } + ctx->config.joseph_reward_mode = value; + } + else if (strcmp(key, "zuk_safe_untagged_healer_target_mask") == 0) { + ctx->config.zuk_safe_untagged_healer_target_mask = + encounter_require_binary_config("inferno", key, value); + } + else if (strcmp(key, "zuk_force_safe_untagged_healer_target_mask") == 0) { + ctx->config.zuk_force_safe_untagged_healer_target_mask = + encounter_require_binary_config("inferno", key, value); + } + else encounter_abort_unknown_config("inferno", "int", key); +} + +static void inf_put_int(EncounterState* state, const char* key, int value) { + inf_put_int_ctx(state, (EncounterContext*)inf_legacy_context(), key, value); +} + +static void inf_put_float_ctx( + EncounterState* state, + EncounterContext* context, + const char* key, + float value +) { + (void)state; + InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + if (strcmp(key, "damage_reward_coeff") == 0) ctx->config.damage_reward_coeff = value; + else if (strcmp(key, "offensive_prayer_reward_coeff") == 0) { + inf_require_nonnegative_float_config(key, value); + ctx->config.offensive_prayer_reward_coeff = value; + } + else if (strcmp(key, "shield_penalty_coeff") == 0) ctx->config.shield_penalty_coeff = value; + else if (strcmp(key, "tag_reward_coeff") == 0) ctx->config.tag_reward_coeff = value; + else if (strcmp(key, "shield_tag_reward_coeff") == 0) ctx->config.shield_tag_reward_coeff = value; + else if (strcmp(key, "death_penalty_coeff") == 0) { + inf_require_nonnegative_float_config(key, value); + ctx->config.death_penalty_coeff = value; + } + else if (strcmp(key, "phase_900_bonus") == 0) ctx->config.phase_900_bonus = value; + else if (strcmp(key, "phase_600_bonus") == 0) ctx->config.phase_600_bonus = value; + else if (strcmp(key, "phase_300_bonus") == 0) ctx->config.phase_300_bonus = value; + else if (strcmp(key, "shield_penalty_episode_cap") == 0) ctx->config.shield_penalty_episode_cap = value; + else if (strcmp(key, "late_start_supply_profile_scale") == 0) { + inf_require_valid_supply_scale(value); + ctx->config.late_start_supply_profile_scale = value; + } + else if (strcmp(key, "curriculum_supply_shared_jitter") == 0) { + inf_require_unit_interval_float_config(key, value); + ctx->config.curriculum_supply_shared_jitter = value; + } + else if (strcmp(key, "curriculum_supply_brew_jitter") == 0) { + inf_require_unit_interval_float_config(key, value); + ctx->config.curriculum_supply_brew_jitter = value; + } + else if (strcmp(key, "curriculum_supply_restore_jitter") == 0) { + inf_require_unit_interval_float_config(key, value); + ctx->config.curriculum_supply_restore_jitter = value; + } + else if (strcmp(key, "curriculum_no_brew_frac") == 0) { + inf_require_unit_interval_float_config(key, value); + ctx->config.curriculum_no_brew_frac = value; + } + else if (strcmp(key, "budget_loadout_fraction") == 0) { + inf_require_valid_budget_loadout_fraction(value); + ctx->config.budget_loadout_fraction = value; + } + else if (strcmp(key, "supply_milestone_brew_reward_coeff") == 0) { + inf_require_nonnegative_float_config(key, value); + ctx->config.supply_milestone_brew_reward_coeff = value; + } + else if (strcmp(key, "supply_milestone_restore_reward_coeff") == 0) { + inf_require_nonnegative_float_config(key, value); + ctx->config.supply_milestone_restore_reward_coeff = value; + } + else if (strcmp(key, "jad_damage_reward_coeff") == 0) ctx->config.jad_damage_reward_coeff = value; + else if (strcmp(key, "zuk_healer_damage_reward_coeff") == 0) ctx->config.zuk_healer_damage_reward_coeff = value; + else if (strcmp(key, "set_damage_reward_coeff") == 0) ctx->config.set_damage_reward_coeff = value; + else if (strcmp(key, "jad_kill_bonus") == 0) ctx->config.jad_kill_bonus = value; + else if (strcmp(key, "zuk_healer_kill_bonus") == 0) ctx->config.zuk_healer_kill_bonus = value; + else if (strcmp(key, "set_kill_bonus") == 0) ctx->config.set_kill_bonus = value; + else if (strcmp(key, "post_healer_zuk_damage_coeff") == 0) ctx->config.post_healer_zuk_damage_coeff = value; + else if (strcmp(key, "post_healer_set_damage_reward_coeff") == 0) { + ctx->config.post_healer_set_damage_reward_coeff = value; + } + else if (strcmp(key, "post_healer_set_kill_bonus") == 0) { + ctx->config.post_healer_set_kill_bonus = value; + } + else if (strcmp(key, "post_healer_set_alive_tick_penalty_coeff") == 0) { + ctx->config.post_healer_set_alive_tick_penalty_coeff = value; + } + else if (strcmp(key, "post_healer_set_alive_penalty_cap") == 0) { + ctx->config.post_healer_set_alive_penalty_cap = value; + } + else if (strcmp(key, "zuk_healer_phase_hp_delta_coeff") == 0) ctx->config.zuk_healer_phase_hp_delta_coeff = value; + else if (strcmp(key, "zuk_untagged_healer_tick_penalty_coeff") == 0) { + ctx->config.zuk_untagged_healer_tick_penalty_coeff = value; + } + else if (strcmp(key, "zuk_untagged_healer_target_bonus_coeff") == 0) { + ctx->config.zuk_untagged_healer_target_bonus_coeff = value; + } + else if (strcmp(key, "zuk_safe_untagged_healer_target_bonus_coeff") == 0) { + ctx->config.zuk_safe_untagged_healer_target_bonus_coeff = value; + } + else if (strcmp(key, "zuk_untagged_healer_nonmagic_attack_bonus_coeff") == 0) { + ctx->config.zuk_untagged_healer_nonmagic_attack_bonus_coeff = value; + } + else if (strcmp(key, "zuk_healer_mage_attack_penalty_coeff") == 0) { + ctx->config.zuk_healer_mage_attack_penalty_coeff = value; + } + else if (strcmp(key, "post_jad_zuk_multiplier") == 0) ctx->config.post_jad_zuk_multiplier = value; + else if (strcmp(key, "jad_alive_zuk_multiplier") == 0) ctx->config.jad_alive_zuk_multiplier = value; + else encounter_abort_unknown_config("inferno", "float", key); +} + +static void inf_put_float(EncounterState* state, const char* key, float value) { + inf_put_float_ctx(state, (EncounterContext*)inf_legacy_context(), key, value); +} + +static void inf_put_ptr_ctx( + EncounterState* state, + EncounterContext* context, + const char* key, + void* value +) { + (void)state; + InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + if (strcmp(key, "collision_map") == 0) ctx->collision_map = (const CollisionMap*)value; + else encounter_abort_unknown_config("inferno", "ptr", key); +} + +static int inf_get_tick_ctx(EncounterState* state, EncounterContext* context) { + (void)context; + return ((InfernoState*)state)->tick; +} + +static int inf_get_winner_ctx(EncounterState* state, EncounterContext* context) { + (void)context; + return ((InfernoState*)state)->winner; +} + +static void* inf_get_log_ctx(EncounterState* state, EncounterContext* context) { + InfernoState* s = (InfernoState*)state; + InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + if (s->episode_over && ctx->log) { + ctx->log->episode_return += s->episode_return; + ctx->log->episode_length += (float)s->tick; + ctx->log->wins += (s->winner == INF_OUTCOME_PLAYER_WON) ? 1.0f : 0.0f; + ctx->log->damage_dealt += s->total_damage_dealt; + ctx->log->zuk_healer_damage += s->total_zuk_healer_damage; + ctx->log->damage_received += s->total_damage_received; + ctx->log->wave += (float)s->wave; + ctx->log->prayer_correct += (float)s->total_prayer_correct; + ctx->log->prayer_total += (float)s->total_npc_attacks; + ctx->log->offensive_prayer_attacks += + (float)s->total_offensive_prayer_attacks; + ctx->log->offensive_prayer_correct += + (float)s->total_offensive_prayer_correct; + for (int i = 0; i < 4; i++) { + ctx->log->offensive_prayer_attacks_by_style[i] += + (float)s->offensive_prayer_attacks_by_style[i]; + ctx->log->offensive_prayer_correct_by_style[i] += + (float)s->offensive_prayer_correct_by_style[i]; + } + ctx->log->idle_ticks += (float)s->total_idle_ticks; + ctx->log->attack_ready_no_attack_ticks += + (float)s->total_attack_ready_no_attack_ticks; + ctx->log->target_available_no_attack_ticks += + (float)s->total_target_available_no_attack_ticks; + ctx->log->safe_attack_opportunity_missed_ticks += + (float)s->total_safe_attack_opportunity_missed_ticks; + ctx->log->progressless_ticks += (float)s->total_progressless_ticks; + ctx->log->npc_pressure_if_ready_count += + s->total_npc_pressure_if_ready_count; + ctx->log->npc_pressure_this_tick_count += + s->total_npc_pressure_this_tick_count; + ctx->log->npc_pressure_max_incoming_hit += + s->max_npc_pressure_incoming_hit; + for (int i = 0; i < OSRS_INFERNO_IDLE_PHASE_COUNT; i++) { + ctx->log->attack_ready_no_attack_ticks_by_phase[i] += + (float)s->attack_ready_no_attack_ticks_by_phase[i]; + ctx->log->target_available_no_attack_ticks_by_phase[i] += + (float)s->target_available_no_attack_ticks_by_phase[i]; + ctx->log->safe_attack_opportunity_missed_ticks_by_phase[i] += + (float)s->safe_attack_opportunity_missed_ticks_by_phase[i]; + ctx->log->progressless_ticks_by_phase[i] += + (float)s->progressless_ticks_by_phase[i]; + } + ctx->log->brews_used += (float)s->total_brews_used; + ctx->log->n += 1.0f; + ctx->log->npc_kills += (float)s->total_npc_kills; + ctx->log->min_zuk_hp_seen += (s->winner == INF_OUTCOME_PLAYER_WON) + ? 0.0f + : (s->min_zuk_hp_seen > 0.0f ? s->min_zuk_hp_seen : 1200.0f); + } + return ctx->log; +} + +static const OsrsCombatVisualRow* inf_npc_combat_visual_row( + InfNPCType type, AttackStyle style +) { + assert(type >= 0 && type < INF_NUM_NPC_TYPES); + return osrs_combat_visual_find_npc_id( + (uint16_t)INF_NPC_DEF_IDS[type], style); +} + +static void inf_npc_projectile_fallback( + InfNPCType type, + AttackStyle style, + uint32_t* model_id, + int* anim_id +) { + assert(model_id && anim_id); + switch (type) { + case INF_NPC_BAT: + *model_id = INF_GFX_1374_MODEL; + *anim_id = INF_GFX_1374_ANIM; + return; + case INF_NPC_BLOB: + if (style == ATTACK_STYLE_RANGED) { + *model_id = INF_GFX_1383_MODEL; + *anim_id = INF_GFX_1383_ANIM; + } else if (style == ATTACK_STYLE_MAGIC) { + *model_id = INF_GFX_1384_MODEL; + *anim_id = OSRS_COMBAT_PROJECTILE_MISSING; + } + return; + case INF_NPC_BLOB_RANGE: + *model_id = INF_GFX_1379_MODEL; + *anim_id = INF_GFX_1379_ANIM; + return; + case INF_NPC_BLOB_MAGE: + *model_id = INF_GFX_1381_MODEL; + *anim_id = INF_GFX_1381_ANIM; + return; + default: + return; + } +} + +static void inf_render_post_tick_ctx( + EncounterState* state, + EncounterContext* context, + EncounterOverlay* ov +) { + (void)context; + InfernoState* s = (InfernoState*)state; + ov->tile_shadow_count = 0; + ov->projectile_count = 0; + encounter_write_terminal_status_text( + s->episode_over, + s->winner, + INF_OUTCOME_PLAYER_WON, + "Inferno cleared", + inf_npc_type_name(s->last_hit_by_type), + ov->status_text, + sizeof(ov->status_text)); + ov->status_text_active = ov->status_text[0] != '\0'; + + for (int i = 0; i < INF_MAX_NPCS; i++) { + InfNPC* npc = &s->npcs[i]; + if (!npc->active || !npc->attacked_this_tick) continue; + + if (npc->type == INF_NPC_NIBBLER) continue; + if (npc->resurrecting_this_tick) continue; + + if (npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) continue; + + const InfNPCStats* stats = &INF_NPC_STATS[npc->type]; + AttackStyle actual_style = npc->attack_style_this_tick; + + if (actual_style == ATTACK_STYLE_NONE && npc->type == INF_NPC_ZUK) + actual_style = ATTACK_STYLE_MAGIC; + + if (npc->type == INF_NPC_HEALER_ZUK && npc->attack_visual_target < 0) + continue; + + if (actual_style == ATTACK_STYLE_NONE) continue; + + if (actual_style == ATTACK_STYLE_MELEE) continue; + + int proj_style = encounter_attack_style_to_proj_style(actual_style); + int npc_size = stats->size; + int start_h = (int)(npc_size * 0.75f * 128); + int end_h = 64; + int curve = 16; + float arc = 0.0f; + + int target_tile_x = s->player.x; + int target_tile_y = s->player.y; + int target_size = 1; + int target_x = s->player.x, target_y = s->player.y; + int target_npc_slot = -1; + if (npc->attack_visual_target >= 0 && npc->attack_visual_target < INF_MAX_NPCS) { + InfNPC* vt = &s->npcs[npc->attack_visual_target]; + target_tile_x = vt->x; + target_tile_y = vt->y; + target_size = vt->size; + target_x = vt->x + vt->size / 2; + target_y = vt->y + vt->size / 2; + end_h = (int)(vt->size * 0.5f * 128); + target_npc_slot = npc->attack_visual_target; + } + int dist = encounter_projectile_distance( + npc->x, npc->y, npc_size, target_tile_x, target_tile_y, target_size, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + EncounterProjectileTiming timing = + inf_npc_projectile_timing(npc->type, actual_style, dist); + int duration = timing.visual_duration_ticks * 30; + int start_delay = timing.visual_start_delay_ticks * 30; + + const OsrsCombatVisualRow* npc_visual = + inf_npc_combat_visual_row(npc->type, actual_style); + uint32_t proj_model_id = 0; + int proj_anim_id = OSRS_COMBAT_PROJECTILE_MISSING; + int impact_gfx_id = 0; + if (npc_visual) { + if (npc_visual->projectile.projectile_model_id > 0) + proj_model_id = (uint32_t)npc_visual->projectile.projectile_model_id; + if (npc_visual->projectile.projectile_anim_id > 0) + proj_anim_id = npc_visual->projectile.projectile_anim_id; + if (npc_visual->projectile.impact_spotanim_id > 0) + impact_gfx_id = npc_visual->projectile.impact_spotanim_id; + } + + if (proj_model_id == 0) + inf_npc_projectile_fallback( + npc->type, actual_style, &proj_model_id, &proj_anim_id); + + switch (npc->type) { + case INF_NPC_JAD: + if (actual_style == ATTACK_STYLE_MAGIC) { + arc = 1.0f; + } else { + start_h = end_h; + } + start_delay = INF_JAD_PROJECTILE_DELAY * 30; + break; + case INF_NPC_HEALER_ZUK: + arc = 3.0f; + duration = (npc->attack_visual_target >= 0) ? 3 * 30 : 4 * 30; + start_delay = 0; + break; + default: break; + } + + if (npc->type == INF_NPC_JAD && actual_style == ATTACK_STYLE_MAGIC) { + encounter_require_projectile_slots(ov, 3); + uint32_t model_ids[3] = { + INF_GFX_448_MODEL, INF_GFX_449_MODEL, INF_GFX_450_MODEL + }; + int anim_ids[3] = { + INF_GFX_448_ANIM, INF_GFX_449_ANIM, INF_GFX_450_ANIM + }; + float offsets[3] = {1.0f, 0.5f, 0.0f}; + for (int j = 0; j < 3; j++) { + OsrsProjectileEventSpec triple_spec = { + .src_x = npc->x, + .src_y = npc->y, + .dst_x = target_x, + .dst_y = target_y, + .style = proj_style, + .damage = (int)s->tick_scratch.damage_received, + .duration_ticks = duration, + .start_h = start_h, + .end_h = end_h, + .curve = curve, + .arc_height = arc, + .src_size = npc_size, + .dst_size = target_size, + .model_id = model_ids[j], + .anim_id = anim_ids[j], + .start_delay = start_delay, + }; + int pi = target_npc_slot >= 0 + ? osrs_emit_projectile_npc_to_npc(ov, &triple_spec, i, target_npc_slot) + : osrs_emit_projectile_npc_to_player(ov, &triple_spec, i); + if (target_npc_slot >= 0) + encounter_set_projectile_target_npc_slot(ov, pi, target_npc_slot); + encounter_set_projectile_offset(ov, pi, 0.0f, offsets[j], 0.0f); + } + continue; + } + + if (target_npc_slot < 0) + impact_gfx_id = 0; + + if (npc->type == INF_NPC_HEALER_ZUK && impact_gfx_id <= 0) + impact_gfx_id = INF_GFX_659_ID; + OsrsProjectileEventSpec attack_spec = { + .src_x = npc->x, + .src_y = npc->y, + .dst_x = target_x, + .dst_y = target_y, + .style = proj_style, + .damage = (int)s->tick_scratch.damage_received, + .duration_ticks = duration, + .start_h = start_h, + .end_h = end_h, + .curve = curve, + .arc_height = arc, + .src_size = npc_size, + .dst_size = target_size, + .model_id = proj_model_id, + .anim_id = proj_anim_id, + .impact_gfx_id = impact_gfx_id, + .start_delay = start_delay, + }; + int pi = target_npc_slot >= 0 + ? osrs_emit_projectile_npc_to_npc(ov, &attack_spec, i, target_npc_slot) + : osrs_emit_projectile_npc_to_player(ov, &attack_spec, i); + + if (npc->type == INF_NPC_JAD && actual_style == ATTACK_STYLE_RANGED) + encounter_set_projectile_motion_mode( + ov, pi, ENCOUNTER_PROJECTILE_MOTION_TARGET_ANCHORED); + + } + + for (int i = 0; i < INF_MAX_PENDING_SPARKS; i++) { + InfPendingSpark* spark = &s->pending_sparks[i]; + if (!spark->active || spark->visual_emitted) continue; + + const OsrsCombatVisualRow* spark_visual = + inf_npc_combat_visual_row(INF_NPC_HEALER_ZUK, ATTACK_STYLE_MAGIC); + uint32_t spark_model_id = INF_GFX_660_MODEL; + int spark_anim_id = OSRS_COMBAT_PROJECTILE_MISSING; + int spark_impact_gfx_id = INF_GFX_659_ID; + if (spark_visual) { + if (spark_visual->projectile.projectile_model_id > 0) + spark_model_id = (uint32_t)spark_visual->projectile.projectile_model_id; + if (spark_visual->projectile.projectile_anim_id > 0) + spark_anim_id = spark_visual->projectile.projectile_anim_id; + if (spark_visual->projectile.impact_spotanim_id > 0) + spark_impact_gfx_id = spark_visual->projectile.impact_spotanim_id; + } + + OsrsProjectileEventSpec spark_spec = { + .src_x = spark->src_x, + .src_y = spark->src_y, + .dst_x = spark->x, + .dst_y = spark->y, + .style = encounter_attack_style_to_proj_style(ATTACK_STYLE_MAGIC), + .damage = spark->damage, + .duration_ticks = 4 * 30, + .start_h = 96, + .end_h = 64, + .curve = 16, + .arc_height = 3.0f, + .src_size = 1, + .dst_size = 1, + .model_id = spark_model_id, + .anim_id = spark_anim_id, + .impact_gfx_id = spark_impact_gfx_id, + }; + (void)osrs_emit_projectile_with_spec(ov, &spark_spec, 0); + spark->visual_emitted = 1; + } + + if (s->tick_scratch.player_attacked && + s->player_attack_style_id != ATTACK_STYLE_MELEE) { + int target_idx = s->player_attack_npc_idx; + if (target_idx >= 0 && target_idx < INF_MAX_NPCS) { + InfNPC* target = &s->npcs[target_idx]; + int target_size = INF_NPC_STATS[target->type].size; + int p_start_h = 64; + int p_end_h = (int)(target_size * 0.5f * 128); + int p_duration = s->player_attack_timing.visual_duration_ticks * 30; + int p_start_delay = s->player_attack_timing.visual_start_delay_ticks * 30; + uint8_t weapon = s->player.equipped[GEAR_SLOT_WEAPON]; + + if (s->player_attack_style_id != ATTACK_STYLE_MAGIC) { + const OsrsCombatProjectileProfile* base_profile = + osrs_combat_visual_ranged_projectile_profile( + weapon, OSRS_COMBAT_PROJECTILE_NONE); + const OsrsCombatVisualRow* effect = NULL; + if (s->player.used_special_this_tick && weapon < NUM_ITEMS) { + effect = osrs_combat_visual_find_special_projectile_item_id( + ITEM_DATABASE[weapon].item_id, ATTACK_STYLE_RANGED); + } + OsrsCombatProjectileSequencePart parts[OSRS_COMBAT_PROJECTILE_SEQUENCE_MAX]; + int part_count = osrs_combat_visual_build_projectile_sequence( + base_profile, effect, parts, OSRS_COMBAT_PROJECTILE_SEQUENCE_MAX); + if (part_count < 0) abort(); + for (int part_i = 0; part_i < part_count; part_i++) { + const OsrsCombatProjectileProfile* profile = &parts[part_i].projectile; + if (profile->projectile_model_id <= 0) continue; + int visual_damage = part_i == part_count - 1 + ? s->player_attack_dmg + : 0; + OsrsCombatProjectileEmitSpec emit_spec = { + .src_x = s->player.x, + .src_y = s->player.y, + .dst_x = target->x, + .dst_y = target->y, + .src_size = 1, + .dst_size = target_size, + .target_npc_slot = target_idx, + .attack_style = s->player_attack_style_id, + .damage = visual_damage, + .duration_ticks = p_duration, + .start_delay = p_start_delay, + .fallback_start_h = p_start_h, + .fallback_end_h = p_end_h, + .curve = 16, + }; + (void)osrs_emit_combat_projectile_profile_player_to_npc( + ov, profile, &emit_spec); + } + } + } + } +} + +static void inf_render_post_tick(EncounterState* state, EncounterOverlay* ov) { + inf_render_post_tick_ctx(state, (EncounterContext*)inf_legacy_context(), ov); +} + +static void* inf_get_player_for_input(void* state, int idx) { + InfernoState* s = (InfernoState*)state; + return (idx == 0) ? (void*)&s->player : NULL; +} + +static void inf_translate_human_input_ctx( + HumanInput* hi, + int* actions, + EncounterState* state, + EncounterContext* context +) { + for (int h = 0; h < INF_NUM_ACTION_HEADS; h++) actions[h] = 0; + + encounter_translate_movement(hi, actions, INF_HEAD_MOVE, inf_get_player_for_input, state); + encounter_translate_prayer(hi, actions, INF_HEAD_PRAYER); + actions[INF_HEAD_PRAYER] = inf_local_overhead_action(actions[INF_HEAD_PRAYER]); + encounter_translate_offensive_prayer(hi, actions, INF_HEAD_OFFENSIVE); + InfernoState* s = (InfernoState*)state; + InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + inf_refresh_current_obs_slots_ctx(s, ctx); + if (hi->pending_target_idx >= 0) { + int found_slot = inf_find_target_obs_slot(s, hi->pending_target_idx); + if (inf_obs_slot_is_targetable(s, ctx, found_slot)) { + actions[INF_HEAD_TARGET] = found_slot + 1; + } else { + actions[INF_HEAD_TARGET] = 0; + } + } else { + actions[INF_HEAD_TARGET] = 0; + } + + if (hi->pending_gear > 0) actions[INF_HEAD_GEAR] = hi->pending_gear; + + if (hi->pending_food || hi->pending_potion == POTION_BREW) + actions[INF_HEAD_EAT] = 1; + + if (hi->pending_potion == POTION_RESTORE) actions[INF_HEAD_POTION] = 1; + else if (hi->pending_potion == POTION_BASTION) actions[INF_HEAD_POTION] = 2; + else if (hi->pending_potion == POTION_STAMINA) actions[INF_HEAD_POTION] = 3; + + if (hi->pending_spell == ATTACK_BLOOD) actions[INF_HEAD_SPELL] = 1; + else if (hi->pending_spell == ATTACK_ICE) actions[INF_HEAD_SPELL] = 2; + + if (hi->pending_spec) actions[INF_HEAD_SPEC] = 1; +} + +static void inf_translate_human_input(HumanInput* hi, int* actions, EncounterState* state) { + inf_translate_human_input_ctx( + hi, actions, state, (EncounterContext*)inf_legacy_context()); +} + +static void inf_translate_human_commands_ctx( + HumanInput* hi, + int* actions, + InfernoState* s, + InfernoContext* ctx +) { + for (int h = 0; h < INF_NUM_ACTION_HEADS; h++) actions[h] = 0; + inf_refresh_current_obs_slots_ctx(s, ctx); + + for (int i = 0; i < hi->commands.count; i++) { + const HumanCommand* cmd = &hi->commands.items[i]; + switch (cmd->kind) { + case HUMAN_COMMAND_WALK: + s->player_dest_x = cmd->world_x; + s->player_dest_y = cmd->world_y; + actions[INF_HEAD_TARGET] = 0; + actions[INF_HEAD_SPELL] = 0; + break; + case HUMAN_COMMAND_ATTACK_NPC: { + int found_slot = inf_find_target_obs_slot(s, cmd->npc_slot); + actions[INF_HEAD_TARGET] = inf_obs_slot_is_targetable(s, ctx, found_slot) + ? found_slot + 1 : 0; + s->player_dest_x = -1; + s->player_dest_y = -1; + break; + } + case HUMAN_COMMAND_SPELL_TARGET: { + int found_slot = inf_find_target_obs_slot(s, cmd->npc_slot); + actions[INF_HEAD_TARGET] = inf_obs_slot_is_targetable(s, ctx, found_slot) + ? found_slot + 1 : 0; + if (cmd->spell == ATTACK_BLOOD) actions[INF_HEAD_SPELL] = 1; + else if (cmd->spell == ATTACK_ICE) actions[INF_HEAD_SPELL] = 2; + s->player_dest_x = -1; + s->player_dest_y = -1; + break; + } + case HUMAN_COMMAND_OVERHEAD_PRAYER: + actions[INF_HEAD_PRAYER] = inf_local_overhead_action(cmd->overhead_prayer); + break; + case HUMAN_COMMAND_OFFENSIVE_PRAYER: + actions[INF_HEAD_OFFENSIVE] = cmd->offensive_prayer; + break; + case HUMAN_COMMAND_EAT: + actions[INF_HEAD_EAT] = 1; + break; + case HUMAN_COMMAND_DRINK: + if (cmd->potion == POTION_BREW) actions[INF_HEAD_EAT] = 1; + else if (cmd->potion == POTION_RESTORE) actions[INF_HEAD_POTION] = 1; + else if (cmd->potion == POTION_BASTION) actions[INF_HEAD_POTION] = 2; + else if (cmd->potion == POTION_STAMINA) actions[INF_HEAD_POTION] = 3; + break; + case HUMAN_COMMAND_SPEC_TOGGLE: + actions[INF_HEAD_SPEC] = 1; + break; + case HUMAN_COMMAND_EQUIP_INVENTORY_ITEM: + case HUMAN_COMMAND_FIGHT_STYLE: + case HUMAN_COMMAND_SET_AUTOCAST: + case HUMAN_COMMAND_ITEM_ON_ITEM: + case HUMAN_COMMAND_ITEM_ON_WIDGET: + case HUMAN_COMMAND_SPELL_ON_WIDGET: + case HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK: + case HUMAN_COMMAND_NONE: + break; + } + } +} + +static void inf_translate_human_commands(HumanInput* hi, int* actions, InfernoState* s) { + inf_translate_human_commands_ctx(hi, actions, s, inf_legacy_context()); +} + +static void inf_step_human_commands_ctx( + EncounterState* state, + EncounterContext* context, + HumanInput* hi +) { + InfernoState* s = (InfernoState*)state; + InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + int actions[INF_NUM_ACTION_HEADS]; + ctx->human_command_mode = 1; + ctx->human_commands = hi->commands.items; + ctx->human_command_count = hi->commands.count; + inf_refresh_human_loadout_stats(s); + inf_translate_human_commands_ctx(hi, actions, s, ctx); + inf_step_ctx(state, (EncounterContext*)ctx, actions); + ctx->human_commands = NULL; + ctx->human_command_count = 0; + human_input_clear_pending(hi); +} + +static void inf_step_human_commands(EncounterState* state, HumanInput* hi) { + inf_step_human_commands_ctx( + state, (EncounterContext*)inf_legacy_context(), hi); +} + +typedef struct { + uint8_t wave; + uint8_t weapon_set; + uint8_t player_hp_bin; + uint8_t player_prayer_bin; + uint8_t brew_doses; + uint8_t restore_doses; + uint8_t overhead_prayer; + uint8_t offensive_prayer_attack_timer; + uint8_t player_x_quant; + uint8_t player_y_quant; + uint8_t zuk_hp_bin; + uint8_t zuk_phase_flags; + uint8_t active_jad_count; + uint8_t active_zuk_healer_count; + uint8_t active_set_count; + uint8_t jad_hp_bin; +} InfCellKey; + +typedef struct { + int live_jad_count; + int live_zuk_healer_count; + int live_set_count; + int live_jad_hp; + int live_jad_max_hp; +} InfLateAddCounts; + +static InfLateAddCounts inf_late_add_counts(const InfernoState* s) { + InfLateAddCounts counts; + memset(&counts, 0, sizeof(counts)); + + for (int i = 0; i < INF_MAX_NPCS; i++) { + const InfNPC* npc = &s->npcs[i]; + if (!npc->active || npc->hp <= 0) continue; + switch (npc->type) { + case INF_NPC_JAD: + counts.live_jad_count++; + counts.live_jad_hp += npc->hp; + counts.live_jad_max_hp += npc->max_hp > 0 ? npc->max_hp : npc->hp; + break; + case INF_NPC_HEALER_ZUK: + counts.live_zuk_healer_count++; + break; + case INF_NPC_MAGER: + case INF_NPC_MELEER: + case INF_NPC_RANGER: + counts.live_set_count++; + break; + default: + break; + } + } + + return counts; +} + +static size_t inf_cell_key_size_ctx(EncounterState* state, EncounterContext* context) { + (void)state; + (void)context; + return sizeof(InfCellKey); +} + +static size_t inf_cell_key_size(EncounterState* state) { + return inf_cell_key_size_ctx(state, (EncounterContext*)inf_legacy_context()); +} + +static uint8_t inf_cell_attack_timer_bucket(const InfernoState* s) { + int timer = s->player.attack_timer; + if (timer < 0) timer = 0; + if (timer > 15) timer = 15; + return (uint8_t)timer; +} + +static void inf_write_cell_key_ctx( + EncounterState* state, + EncounterContext* context, + void* out +) { + (void)context; + const InfernoState* s = (const InfernoState*)state; + InfCellKey* k = (InfCellKey*)out; + memset(k, 0, sizeof(InfCellKey)); + + k->wave = (uint8_t)(s->wave & 0xff); + k->weapon_set = (uint8_t)s->weapon_set; + + k->player_hp_bin = (uint8_t)(s->player.current_hitpoints / 10); + k->player_prayer_bin = (uint8_t)(s->player.current_prayer / 10); + k->brew_doses = (uint8_t)s->player.brew_doses; + k->restore_doses = (uint8_t)s->player.restore_doses; + k->overhead_prayer = (uint8_t)s->player.prayer; + k->offensive_prayer_attack_timer = (uint8_t)( + ((uint8_t)s->player.offensive_prayer & 0x0fu) | + (inf_cell_attack_timer_bucket(s) << 4)); + + int dx = s->player.x - INF_ARENA_MIN_X; + int dy = s->player.y - INF_ARENA_MIN_Y; + if (dx < 0) dx = 0; + if (dy < 0) dy = 0; + k->player_x_quant = (uint8_t)((dx / 2) & 0xff); + k->player_y_quant = (uint8_t)((dy / 2) & 0xff); + + int zuk_idx = inf_find_live_zuk_idx(s); + int zuk_hp = (zuk_idx >= 0) ? s->npcs[zuk_idx].hp : 0; + k->zuk_hp_bin = (uint8_t)((zuk_hp / 50) & 0xff); + + k->zuk_phase_flags = (uint8_t)( + (s->zuk.healer_spawned ? 0x01u : 0u) | + (s->zuk.jad_spawned ? 0x02u : 0u) | + (s->zuk.enraged ? 0x04u : 0u) | + (s->zuk.timer_paused ? 0x08u : 0u) + ); + + InfLateAddCounts counts = inf_late_add_counts(s); + k->active_jad_count = (uint8_t)( + counts.live_jad_count > 255 ? 255 : counts.live_jad_count); + k->active_zuk_healer_count = (uint8_t)( + counts.live_zuk_healer_count > 255 ? 255 : counts.live_zuk_healer_count); + k->active_set_count = (uint8_t)( + counts.live_set_count > 255 ? 255 : counts.live_set_count); + int jad_hp_bin = counts.live_jad_hp / 50; + k->jad_hp_bin = (uint8_t)(jad_hp_bin > 255 ? 255 : jad_hp_bin); +} + +static void inf_write_cell_key(EncounterState* state, void* out) { + inf_write_cell_key_ctx(state, (EncounterContext*)inf_legacy_context(), out); +} + +static float inf_progress_score_ctx(EncounterState* state, EncounterContext* context) { + (void)context; + const InfernoState* s = (const InfernoState*)state; + if (s->episode_over && s->winner == INF_OUTCOME_PLAYER_WON) return 2.0f; + + float min_zhp = (s->min_zuk_hp_seen > 0.0f) ? s->min_zuk_hp_seen : 1200.0f; + if (min_zhp < 0.0f) min_zhp = 0.0f; + if (min_zhp > 1200.0f) min_zhp = 1200.0f; + + float q = (1200.0f - min_zhp) / 1200.0f; + InfLateAddCounts counts = inf_late_add_counts(s); + + if (s->zuk.jad_spawned && counts.live_jad_count == 0) { + q += 0.10f; + if (min_zhp < 600.0f) { + q += 0.06f * ((600.0f - min_zhp) / 600.0f); + } + } else if (s->zuk.jad_spawned && counts.live_jad_max_hp > 0) { + float jad_damage_frac = + (float)(counts.live_jad_max_hp - counts.live_jad_hp) / + (float)counts.live_jad_max_hp; + if (jad_damage_frac < 0.0f) jad_damage_frac = 0.0f; + if (jad_damage_frac > 1.0f) jad_damage_frac = 1.0f; + q += 0.09f * jad_damage_frac; + } + + if (s->zuk.healer_spawned && counts.live_zuk_healer_count == 0) { + q += 0.08f; + } + + if (min_zhp <= 900.0f && counts.live_set_count == 0) { + q += 0.04f; + } + + return q; +} + +static float inf_progress_score(EncounterState* state) { + return inf_progress_score_ctx(state, (EncounterContext*)inf_legacy_context()); +} + +#define INF_SNAPSHOT_MAGIC 0x1FE00001u +#define INF_SNAPSHOT_VERSION 18u + +typedef struct { + uint32_t magic; + uint32_t version; + uint32_t state_size; + uint32_t reserved; + uint64_t config_fingerprint; + InfernoState state; +} InfSnapshot; + +static void inf_hash_bytes(uint64_t* h, const void* ptr, size_t n) { + const uint8_t* p = (const uint8_t*)ptr; + for (size_t i = 0; i < n; i++) { + *h ^= (uint64_t)p[i]; + *h *= 1099511628211ull; + } +} + +#define INF_HASH_CONFIG_FIELD(config, h, field) \ + inf_hash_bytes((h), &(config)->field, sizeof((config)->field)) + +static uint64_t inf_config_fingerprint(const InfConfig* config) { + uint64_t h = 1469598103934665603ull; + INF_HASH_CONFIG_FIELD(config, &h, start_wave); + INF_HASH_CONFIG_FIELD(config, &h, loadout_profile_mode); + INF_HASH_CONFIG_FIELD(config, &h, budget_loadout_fraction); + INF_HASH_CONFIG_FIELD(config, &h, damage_reward_coeff); + INF_HASH_CONFIG_FIELD(config, &h, offensive_prayer_reward_coeff); + INF_HASH_CONFIG_FIELD(config, &h, shield_penalty_coeff); + INF_HASH_CONFIG_FIELD(config, &h, tag_reward_coeff); + INF_HASH_CONFIG_FIELD(config, &h, late_start_supply_profile_scale); + INF_HASH_CONFIG_FIELD(config, &h, curriculum_agent); + INF_HASH_CONFIG_FIELD(config, &h, curriculum_supply_jitter_mode); + INF_HASH_CONFIG_FIELD(config, &h, curriculum_supply_shared_jitter); + INF_HASH_CONFIG_FIELD(config, &h, curriculum_supply_brew_jitter); + INF_HASH_CONFIG_FIELD(config, &h, curriculum_supply_restore_jitter); + INF_HASH_CONFIG_FIELD(config, &h, curriculum_no_brew_mode); + INF_HASH_CONFIG_FIELD(config, &h, curriculum_no_brew_frac); + INF_HASH_CONFIG_FIELD(config, &h, supply_milestone_brew_reward_coeff); + INF_HASH_CONFIG_FIELD(config, &h, supply_milestone_restore_reward_coeff); + INF_HASH_CONFIG_FIELD(config, &h, death_penalty_coeff); + INF_HASH_CONFIG_FIELD(config, &h, terminal_penalty_enabled); + INF_HASH_CONFIG_FIELD(config, &h, step_out_forecast_obs_mode); + INF_HASH_CONFIG_FIELD(config, &h, phase_900_bonus); + INF_HASH_CONFIG_FIELD(config, &h, phase_600_bonus); + INF_HASH_CONFIG_FIELD(config, &h, phase_300_bonus); + INF_HASH_CONFIG_FIELD(config, &h, shield_penalty_episode_cap); + INF_HASH_CONFIG_FIELD(config, &h, jad_damage_reward_coeff); + INF_HASH_CONFIG_FIELD(config, &h, zuk_healer_damage_reward_coeff); + INF_HASH_CONFIG_FIELD(config, &h, set_damage_reward_coeff); + INF_HASH_CONFIG_FIELD(config, &h, jad_kill_bonus); + INF_HASH_CONFIG_FIELD(config, &h, zuk_healer_kill_bonus); + INF_HASH_CONFIG_FIELD(config, &h, set_kill_bonus); + INF_HASH_CONFIG_FIELD(config, &h, post_healer_zuk_damage_coeff); + INF_HASH_CONFIG_FIELD(config, &h, post_healer_set_damage_reward_coeff); + INF_HASH_CONFIG_FIELD(config, &h, post_healer_set_kill_bonus); + INF_HASH_CONFIG_FIELD(config, &h, post_healer_set_alive_tick_penalty_coeff); + INF_HASH_CONFIG_FIELD(config, &h, post_healer_set_alive_penalty_cap); + INF_HASH_CONFIG_FIELD(config, &h, zuk_healer_phase_hp_delta_coeff); + INF_HASH_CONFIG_FIELD(config, &h, zuk_untagged_healer_tick_penalty_coeff); + INF_HASH_CONFIG_FIELD(config, &h, zuk_untagged_healer_target_bonus_coeff); + INF_HASH_CONFIG_FIELD(config, &h, zuk_safe_untagged_healer_target_bonus_coeff); + INF_HASH_CONFIG_FIELD(config, &h, zuk_untagged_healer_nonmagic_attack_bonus_coeff); + INF_HASH_CONFIG_FIELD(config, &h, zuk_healer_mage_attack_penalty_coeff); + INF_HASH_CONFIG_FIELD(config, &h, zuk_safe_untagged_healer_target_mask); + INF_HASH_CONFIG_FIELD(config, &h, zuk_force_safe_untagged_healer_target_mask); + INF_HASH_CONFIG_FIELD(config, &h, zuk_healer_reward_mode); + INF_HASH_CONFIG_FIELD(config, &h, post_jad_zuk_multiplier); + INF_HASH_CONFIG_FIELD(config, &h, jad_alive_zuk_multiplier); + INF_HASH_CONFIG_FIELD(config, &h, oracle_mode); + INF_HASH_CONFIG_FIELD(config, &h, shield_tag_reward_coeff); + INF_HASH_CONFIG_FIELD(config, &h, joseph_reward_mode); + return h; +} + +#undef INF_HASH_CONFIG_FIELD + +static void inf_refresh_after_state_load(InfernoState* s, InfernoContext* ctx) { + inf_build_npc_stats(); + const uint8_t* const* loadouts = + inf_loadouts_for_profile(s->active_loadout_profile); + encounter_compute_loadout_stats(loadouts[INF_GEAR_MAGE], ATTACK_STYLE_MAGIC, + s->player.offensive_prayer, s->player.current_magic, + inf_player_spell_fight_style(&s->player), 30, + &s->loadout_stats[INF_GEAR_MAGE]); + encounter_compute_loadout_stats(loadouts[INF_GEAR_LONG_RANGE], ATTACK_STYLE_RANGED, + s->player.offensive_prayer, s->player.current_ranged, + FIGHT_STYLE_RAPID, 0, &s->loadout_stats[INF_GEAR_LONG_RANGE]); + encounter_compute_loadout_stats(loadouts[INF_GEAR_BP], ATTACK_STYLE_RANGED, + s->player.offensive_prayer, s->player.current_ranged, + FIGHT_STYLE_RAPID, 0, &s->loadout_stats[INF_GEAR_BP]); + if (ctx->human_command_mode) + inf_refresh_human_loadout_stats(s); + inf_rebuild_los(s); + inf_rebuild_entity_collision_flags(s); + inf_invalidate_los_cache(s); + inf_refresh_current_obs_slots_ctx(s, ctx); +} + +static size_t inf_snapshot_size_ctx(EncounterState* state, EncounterContext* context) { + (void)state; + (void)context; + return sizeof(InfSnapshot); +} + +static size_t inf_snapshot_size(EncounterState* state) { + return inf_snapshot_size_ctx(state, (EncounterContext*)inf_legacy_context()); +} + +static void inf_snapshot_ctx(EncounterState* state, EncounterContext* context, void* out) { + InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + InfSnapshot* snap = (InfSnapshot*)out; + encounter_snapshot_write_frame( + snap, sizeof(*snap), INF_SNAPSHOT_MAGIC, INF_SNAPSHOT_VERSION, + sizeof(InfernoState)); + snap->config_fingerprint = inf_config_fingerprint(&ctx->config); + encounter_snapshot_copy_state_to( + snap, offsetof(InfSnapshot, state), state, sizeof(InfernoState)); +} + +static void inf_snapshot(EncounterState* state, void* out) { + inf_snapshot_ctx(state, (EncounterContext*)inf_legacy_context(), out); +} + +static void inf_restore_ctx( + EncounterState* state, + EncounterContext* context, + const void* data, + size_t n +) { + InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + const InfSnapshot* snap = (const InfSnapshot*)data; + encounter_snapshot_validate_frame( + "inf_restore", data, n, sizeof(InfSnapshot), + INF_SNAPSHOT_MAGIC, INF_SNAPSHOT_VERSION, sizeof(InfernoState)); + uint64_t live_fingerprint = inf_config_fingerprint(&ctx->config); + if (snap->config_fingerprint != live_fingerprint) { + fprintf(stderr, + "inf_restore: config fingerprint mismatch (got 0x%016llx, want 0x%016llx)\n", + (unsigned long long)snap->config_fingerprint, + (unsigned long long)live_fingerprint); + abort(); + } + + InfernoState* dst = (InfernoState*)state; + encounter_snapshot_copy_state_from( + dst, data, offsetof(InfSnapshot, state), sizeof(InfernoState)); + inf_refresh_after_state_load(dst, ctx); +} + +static void inf_restore(EncounterState* state, const void* data, size_t n) { + inf_restore_ctx(state, (EncounterContext*)inf_legacy_context(), data, n); +} + +static int inf_lab_apply_script_line_alloc_json_void( + void* state, const char* line, char** out_json +) { + return inf_lab_apply_script_line_alloc_json((InfernoState*)state, line, out_json); +} + +static int inf_apply_lab_command_ctx( + EncounterState* state, + EncounterContext* context, + const char* line +) { + (void)context; + return encounter_apply_lab_command_dump_wrapper( + state, line, INF_LAB_LINE_DUMP, inf_lab_apply_script_line_alloc_json_void); +} + +static const EncounterDef ENCOUNTER_INFERNO = { + .name = "inferno", + .obs_size = INF_NUM_OBS, + .num_action_heads = INF_NUM_ACTION_HEADS, + .action_head_dims = INF_ACTION_DIMS, + .mask_size = INF_ACTION_MASK_SIZE, + + .state_size = sizeof(InfernoState), + .context_size = sizeof(InfernoContext), + .init_context = inf_init_context, + .destroy_context = inf_destroy_context, + .init_state = inf_init_state, + + .create = inf_create, + .destroy = inf_destroy, + .reset = inf_reset_ctx, + .step = inf_step_ctx, + .step_human_commands = inf_step_human_commands_ctx, + + .snapshot_size = inf_snapshot_size_ctx, + .snapshot = inf_snapshot_ctx, + .restore = inf_restore_ctx, + + .cell_key_size = inf_cell_key_size_ctx, + .write_cell_key = inf_write_cell_key_ctx, + .progress_score = inf_progress_score_ctx, + + .write_obs = inf_write_obs_ctx, + .write_mask = inf_write_mask_ctx, + .get_reward = inf_get_reward_ctx, + .is_terminal = inf_is_terminal_ctx, + + .get_entity_count = inf_get_entity_count_ctx, + .get_entity = inf_get_entity_ctx, + .fill_render_entities = inf_fill_render_entities_ctx, + + .put_int = inf_put_int_ctx, + .put_float = inf_put_float_ctx, + .put_ptr = inf_put_ptr_ctx, + + .arena_base_x = INF_ARENA_MIN_X, + .arena_base_y = INF_ARENA_MIN_Y, + .arena_width = INF_ARENA_WIDTH, + .arena_height = INF_ARENA_HEIGHT, + + .translate_human_input = inf_translate_human_input_ctx, + .is_human_targetable_npc_slot = inf_is_human_targetable_npc_slot_ctx, + .apply_lab_command = inf_apply_lab_command_ctx, + .head_move = INF_HEAD_MOVE, + .head_prayer = INF_HEAD_PRAYER, + .head_target = INF_HEAD_TARGET, + + .render_post_tick = inf_render_post_tick_ctx, + .get_log = inf_get_log_ctx, + .get_tick = inf_get_tick_ctx, + .get_winner = inf_get_winner_ctx, +}; + +__attribute__((constructor)) +static void inf_register(void) { + encounter_register(&ENCOUNTER_INFERNO); +} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc new file mode 100644 index 0000000000..733110472c --- /dev/null +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc @@ -0,0 +1,696 @@ +#line 1436 "encounter_inferno.h" +static InfSupplyDoses inf_full_starting_supplies(void) { + return (InfSupplyDoses){ + .brew_doses = 24, + .restore_doses = 40, + .bastion_doses = 16, + .stamina_doses = 4, + }; +} + +static const InfSupplyProfileAnchor INF_SUPPLY_PROFILE_ANCHORS[] = { + { 1, { 1.0000f, 1.0000f, 1.0000f, 1.0000f } }, + { 20, { 1.0000f, 0.9500f, 1.0000f, 1.0000f } }, + { 40, { 0.9167f, 0.8750f, 1.0000f, 1.0000f } }, + { 61, { 0.8333f, 0.7500f, 1.0000f, 1.0000f } }, + { 64, { 0.5833f, 0.5000f, 0.7500f, 1.0000f } }, + { 68, { 0.5833f, 0.4250f, 0.6250f, 1.0000f } }, + { 69, { 0.5000f, 0.3000f, 0.3750f, 1.0000f } }, + { 70, { 0.4375f, 0.2250f, 0.3750f, 1.0000f } }, + { 71, { 0.3750f, 0.1500f, 0.3750f, 1.0000f } }, +}; + +static float inf_lerp_float(float a, float b, float t) { + return a + (b - a) * t; +} + +static void inf_require_unit_interval_float_config(const char* key, float value) { + if (value < 0.0f || value > 1.0f) { + fprintf(stderr, "inferno %s must be in [0, 1], got %.6f\n", key, value); + abort(); + } +} + +static void inf_require_valid_supply_scale(float scale) { + inf_require_unit_interval_float_config("late_start_supply_profile_scale", scale); +} + +static void inf_require_valid_curriculum_supply_mode(const char* key, int mode) { + if (mode < INF_CURRICULUM_SUPPLY_MODE_OFF || + mode > INF_CURRICULUM_SUPPLY_MODE_PRE_ZUK) { + fprintf(stderr, "inferno %s must be in [0, 3], got %d\n", key, mode); + abort(); + } +} + +static void inf_require_valid_budget_loadout_fraction(float fraction) { + inf_require_unit_interval_float_config("budget_loadout_fraction", fraction); +} + +static void inf_require_valid_loadout_profile_mode(int mode) { + if (mode < INF_LOADOUT_PROFILE_MODE_MAX_ONLY || + mode > INF_LOADOUT_PROFILE_MODE_MIXED_MAX_BUDGET) { + fprintf(stderr, "inferno loadout_profile_mode must be in [0, 2], got %d\n", + mode); + abort(); + } +} + +static InfLoadoutProfile inf_sample_loadout_profile( + InfLoadoutProfileMode mode, + float budget_fraction, + uint32_t* rng_state +) { + inf_require_valid_loadout_profile_mode(mode); + inf_require_valid_budget_loadout_fraction(budget_fraction); + + if (mode == INF_LOADOUT_PROFILE_MODE_MAX_ONLY) + return INF_LOADOUT_PROFILE_MAX; + if (mode == INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY) + return INF_LOADOUT_PROFILE_BUDGET; + return encounter_rand_float(rng_state) < budget_fraction + ? INF_LOADOUT_PROFILE_BUDGET + : INF_LOADOUT_PROFILE_MAX; +} + +static void inf_require_nonnegative_float_config(const char* key, float value) { + if (!(value >= 0.0f)) { + fprintf(stderr, "inferno %s must be >= 0, got %.6f\n", key, value); + abort(); + } +} + +static void inf_require_valid_public_wave(int public_wave) { + if (public_wave < 1 || public_wave > INF_MAX_PUBLIC_START_WAVE) { + fprintf(stderr, "inferno start_wave must be in [1, %d], got %d\n", + INF_MAX_PUBLIC_START_WAVE, public_wave); + abort(); + } +} + +static int inf_runtime_wave_from_start_wave(int internal_start_wave) { + return internal_start_wave >= INF_WAVE_ZUK ? INF_WAVE_ZUK : internal_start_wave; +} + +static int inf_initial_attack_timer_after_stun(int attack_speed, int stun_timer) { + return attack_speed + stun_timer; +} + +static InfSupplyFractions inf_supply_profile_fractions(int public_wave) { + inf_require_valid_public_wave(public_wave); + + const int n = (int)(sizeof(INF_SUPPLY_PROFILE_ANCHORS) / + sizeof(INF_SUPPLY_PROFILE_ANCHORS[0])); + for (int i = 1; i < n; i++) { + const InfSupplyProfileAnchor* lo = &INF_SUPPLY_PROFILE_ANCHORS[i - 1]; + const InfSupplyProfileAnchor* hi = &INF_SUPPLY_PROFILE_ANCHORS[i]; + if (public_wave <= hi->public_wave) { + float t = (float)(public_wave - lo->public_wave) / + (float)(hi->public_wave - lo->public_wave); + return (InfSupplyFractions){ + .brew_fraction = inf_lerp_float(lo->fractions.brew_fraction, + hi->fractions.brew_fraction, t), + .restore_fraction = inf_lerp_float(lo->fractions.restore_fraction, + hi->fractions.restore_fraction, t), + .bastion_fraction = inf_lerp_float(lo->fractions.bastion_fraction, + hi->fractions.bastion_fraction, t), + .stamina_fraction = inf_lerp_float(lo->fractions.stamina_fraction, + hi->fractions.stamina_fraction, t), + }; + } + } + + fprintf(stderr, "inferno supply profile has no anchor for wave %d\n", public_wave); + abort(); +} + +static int inf_supply_profile_anchor_index(int public_wave) { + int n = (int)(sizeof(INF_SUPPLY_PROFILE_ANCHORS) / + sizeof(INF_SUPPLY_PROFILE_ANCHORS[0])); + for (int i = 0; i < n; i++) { + if (INF_SUPPLY_PROFILE_ANCHORS[i].public_wave == public_wave) + return i; + } + return -1; +} + +static int inf_profiled_supply_count(int full_doses, float profile_fraction, float scale) { + assert(full_doses >= 0); + + float effective_fraction = 1.0f - scale * (1.0f - profile_fraction); + int doses = (int)((float)full_doses * effective_fraction + 0.5f); + if (doses < 0) doses = 0; + if (doses > full_doses) doses = full_doses; + return doses; +} + +static int inf_clamp_supply_doses(int doses, int full_doses) { + if (doses < 0) return 0; + if (doses > full_doses) return full_doses; + return doses; +} + +static int inf_round_signed_float_to_int(float value) { + return value >= 0.0f ? (int)(value + 0.5f) : (int)(value - 0.5f); +} + +static int inf_curriculum_supply_mode_applies(int mode, int public_wave) { + inf_require_valid_curriculum_supply_mode("curriculum supply mode", mode); + switch (mode) { + case INF_CURRICULUM_SUPPLY_MODE_OFF: + return 0; + case INF_CURRICULUM_SUPPLY_MODE_ALL: + return 1; + case INF_CURRICULUM_SUPPLY_MODE_ZUK: + return public_wave >= INF_NUM_WAVES; + case INF_CURRICULUM_SUPPLY_MODE_PRE_ZUK: + return public_wave < INF_NUM_WAVES; + default: + abort(); + } +} + +static void inf_apply_curriculum_supply_variation( + InfernoState* s, + const InfernoContext* ctx, + InfSupplyDoses full +) { + if (!ctx->config.curriculum_agent) return; + + int public_wave = s->start_wave + 1; + int jitter_applies = inf_curriculum_supply_mode_applies( + ctx->config.curriculum_supply_jitter_mode, public_wave); + float shared_jitter = ctx->config.curriculum_supply_shared_jitter; + float brew_jitter = ctx->config.curriculum_supply_brew_jitter; + float restore_jitter = ctx->config.curriculum_supply_restore_jitter; + + if (jitter_applies && + (shared_jitter > 0.0f || brew_jitter > 0.0f || restore_jitter > 0.0f)) { + float shared = (2.0f * encounter_rand_float(&s->rng_state) - 1.0f) * + shared_jitter; + float brew_delta = shared; + float restore_delta = shared; + if (brew_jitter > 0.0f) { + brew_delta += (2.0f * encounter_rand_float(&s->rng_state) - 1.0f) * + brew_jitter; + } + if (restore_jitter > 0.0f) { + restore_delta += (2.0f * encounter_rand_float(&s->rng_state) - 1.0f) * + restore_jitter; + } + + s->player.brew_doses = inf_clamp_supply_doses( + s->player.brew_doses + + inf_round_signed_float_to_int((float)full.brew_doses * brew_delta), + full.brew_doses); + s->player.restore_doses = inf_clamp_supply_doses( + s->player.restore_doses + + inf_round_signed_float_to_int((float)full.restore_doses * restore_delta), + full.restore_doses); + } + + int no_brew_applies = inf_curriculum_supply_mode_applies( + ctx->config.curriculum_no_brew_mode, public_wave); + if (no_brew_applies && + ctx->config.curriculum_no_brew_frac > 0.0f && + encounter_rand_float(&s->rng_state) < ctx->config.curriculum_no_brew_frac) { + s->player.brew_doses = 0; + } +} + +static InfSupplyDoses inf_supplies_for_start_wave(InfSupplyDoses full, + int internal_start_wave, + float scale) { + inf_require_valid_supply_scale(scale); + + int public_wave = internal_start_wave + 1; + + InfSupplyFractions fractions = inf_supply_profile_fractions(public_wave); + return (InfSupplyDoses){ + .brew_doses = inf_profiled_supply_count(full.brew_doses, + fractions.brew_fraction, scale), + .restore_doses = inf_profiled_supply_count(full.restore_doses, + fractions.restore_fraction, scale), + .bastion_doses = inf_profiled_supply_count(full.bastion_doses, + fractions.bastion_fraction, scale), + .stamina_doses = inf_profiled_supply_count(full.stamina_doses, + fractions.stamina_fraction, scale), + }; +} + +static float inf_supply_milestone_surplus_reward( + InfernoState* s, + const InfernoContext* ctx, + int public_wave +) { + if (ctx->config.supply_milestone_brew_reward_coeff <= 0.0f && + ctx->config.supply_milestone_restore_reward_coeff <= 0.0f) { + return 0.0f; + } + + int anchor_idx = inf_supply_profile_anchor_index(public_wave); + if (anchor_idx <= 0) return 0.0f; + + uint32_t anchor_bit = 1u << (uint32_t)anchor_idx; + if ((s->supply_milestone_rewarded_mask & anchor_bit) != 0) + return 0.0f; + s->supply_milestone_rewarded_mask |= anchor_bit; + + InfSupplyDoses full = inf_full_starting_supplies(); + InfSupplyDoses expected = inf_supplies_for_start_wave( + full, public_wave - 1, ctx->config.late_start_supply_profile_scale); + + int brew_surplus = s->player.brew_doses - expected.brew_doses; + int restore_surplus = s->player.restore_doses - expected.restore_doses; + if (brew_surplus < 0) brew_surplus = 0; + if (restore_surplus < 0) restore_surplus = 0; + + return + ctx->config.supply_milestone_brew_reward_coeff * + (float)brew_surplus / (float)full.brew_doses + + ctx->config.supply_milestone_restore_reward_coeff * + (float)restore_surplus / (float)full.restore_doses; +} + +static void inf_reset_ctx(EncounterState* state, EncounterContext* context, uint32_t seed) { + inf_build_npc_stats(); + InfernoState* s = (InfernoState*)state; + InfernoContext* ctx = (InfernoContext*)context; + if (!ctx) ctx = inf_legacy_context(); + int saved_start = ctx->config.start_wave; + uint32_t saved_rng = s->rng_state; + memset(s, 0, sizeof(InfernoState)); + s->start_wave = saved_start; + s->rng_state = encounter_resolve_seed(saved_rng, seed); + + s->player_dest_x = -1; + s->player_dest_y = -1; + s->player_last_interaction_target_slot = -1; + s->player_last_interaction_age = 1; + + s->tick_at_le_300 = -1; + s->tick_at_le_240 = -1; + s->tick_at_le_150 = -1; + s->tick_at_zuk_healer_spawn = -1; + s->tick_at_first_zuk_healer_tag = -1; + s->tick_at_all_zuk_healers_tagged = -1; + s->tick_at_all_zuk_healers_dead = -1; + s->tick_at_first_zuk_hit_after_all_healers_dead = -1; + s->tick_at_first_zuk_healer_target = -1; + s->tick_at_first_zuk_healer_attack = -1; + s->total_zuk_healer_target_ticks = 0; + s->total_zuk_healer_attack_fires = 0; + s->total_zuk_healer_cannot_attack_ticks = 0; + s->total_zuk_healer_cooldown_ticks = 0; + s->total_zuk_healer_out_of_range_ticks = 0; + s->total_zuk_healer_attackable_ticks = 0; + s->total_action_mask_checks = 0; + for (int h = 0; h < 9; h++) { + s->zero_valid_action_head_count[h] = 0; + s->min_valid_action_count_by_head[h] = 1000000; + } + s->target_head_valid_healer_count = 0; + s->target_head_valid_zuk_count = 0; + s->target_head_valid_set_count = 0; + + s->player.entity_type = ENTITY_PLAYER; + encounter_init_maxed_player_combat_stats(&s->player, 99); + osrs_item_effect_state_init(&s->player.item_effect_state); + s->weapon_set = INF_GEAR_MAGE; + s->active_loadout_profile = inf_sample_loadout_profile( + ctx->config.loadout_profile_mode, + ctx->config.budget_loadout_fraction, + &s->rng_state); + const uint8_t* const* loadouts = + inf_loadouts_for_profile(s->active_loadout_profile); + encounter_apply_loadout(&s->player, loadouts[INF_GEAR_MAGE], GEAR_MAGE); + { + encounter_populate_inventory(&s->player, loadouts, INF_NUM_WEAPON_SETS, NULL); + + encounter_clear_ammo_inventory_slot(&s->player); + } + InfSupplyDoses full_supplies = inf_full_starting_supplies(); + InfSupplyDoses start_supplies = inf_supplies_for_start_wave( + full_supplies, s->start_wave, ctx->config.late_start_supply_profile_scale); + s->player.brew_doses = start_supplies.brew_doses; + s->player.restore_doses = start_supplies.restore_doses; + s->player.bastion_doses = start_supplies.bastion_doses; + s->player.stamina_doses = start_supplies.stamina_doses; + inf_apply_curriculum_supply_variation(s, ctx, full_supplies); + s->stamina_active_ticks = 0; + s->player.prayer = PRAYER_NONE; + s->player.autocast_enabled = 1; + s->player.autocast_defensive = 0; + s->player.autocast_spell = ENCOUNTER_SPELL_BLOOD; + osrs_interaction_init(&s->interaction); + s->player.spec_armed = 0; + s->player.special_energy = 100; + s->player.run_energy = 10000; + s->last_hit_by_type = -1; + + encounter_compute_loadout_stats(loadouts[INF_GEAR_MAGE], ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, 30, &s->loadout_stats[INF_GEAR_MAGE]); + encounter_compute_loadout_stats(loadouts[INF_GEAR_LONG_RANGE], ATTACK_STYLE_RANGED, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, &s->loadout_stats[INF_GEAR_LONG_RANGE]); + encounter_compute_loadout_stats(loadouts[INF_GEAR_BP], ATTACK_STYLE_RANGED, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, &s->loadout_stats[INF_GEAR_BP]); + + int effective_start = inf_runtime_wave_from_start_wave(s->start_wave); + int is_zuk_wave = (effective_start >= INF_WAVE_ZUK); + s->player.x = is_zuk_wave ? INF_ZUK_PLAYER_START_X : INF_PLAYER_START_X; + s->player.y = is_zuk_wave ? INF_ZUK_PLAYER_START_Y : INF_PLAYER_START_Y; + inf_rebuild_player_collision_flags(s); + + for (int i = 0; i < INF_NUM_PILLARS; i++) { + s->pillars[i].x = INF_PILLAR_POS[i][0]; + s->pillars[i].y = INF_PILLAR_POS[i][1]; + if (effective_start >= 66) { + s->pillars[i].hp = 0; + s->pillars[i].active = 0; + } else { + s->pillars[i].hp = INF_PILLAR_HP; + s->pillars[i].active = 1; + } + } + inf_rebuild_los(s); + + s->dead_mob_count = 0; + + s->wave = effective_start; + s->wave_spawn_target = effective_start; + s->wave_spawn_delay = 0; + s->wave_ready_delay = INF_START_READY_TICKS; + inf_spawn_wave(s); + inf_invalidate_los_cache(s); +} + +static void inf_reset(EncounterState* state, uint32_t seed) { + inf_reset_ctx(state, (EncounterContext*)inf_legacy_context(), seed); +} + +static int inf_find_free_npc(InfernoState* s) { + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (!s->npcs[i].active) return i; + } + return -1; +} + +static int inf_grid_index(int x, int y, int* gx, int* gy) { + *gx = x - INF_ARENA_MIN_X; + *gy = y - INF_ARENA_MIN_Y; + return *gx >= 0 && *gx < INF_ARENA_WIDTH && *gy >= 0 && *gy < INF_ARENA_HEIGHT; +} + +static int inf_npc_sets_collision_flag(InfNPCType type) { + return type != INF_NPC_NIBBLER && type != INF_NPC_ZUK_SHIELD; +} + +static int inf_npc_effective_size(const InfNPC* npc) { + return npc->size > 0 ? npc->size : INF_NPC_STATS[npc->type].size; +} + +static void inf_unstamp_npc_collision_footprint(InfernoState* s, int x, int y, int size) { + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + int gx, gy; + if (inf_grid_index(x + dx, y + dy, &gx, &gy)) { + if (s->npc_collision_flags[gx][gy] > 0) + s->npc_collision_flags[gx][gy]--; + } + } + } +} + +static void inf_stamp_npc_collision_footprint(InfernoState* s, int x, int y, int size) { + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + int gx, gy; + if (inf_grid_index(x + dx, y + dy, &gx, &gy)) { + assert(s->npc_collision_flags[gx][gy] < UINT8_MAX); + s->npc_collision_flags[gx][gy]++; + } + } + } +} + +static void inf_clear_player_collision_flags(InfernoState* s) { + memset(s->player_collision_flags, 0, sizeof(s->player_collision_flags)); +} + +static void inf_stamp_player_collision_flags(InfernoState* s) { + int gx, gy; + if (inf_grid_index(s->player.x, s->player.y, &gx, &gy)) + s->player_collision_flags[gx][gy] = 1; +} + +static void inf_rebuild_player_collision_flags(InfernoState* s) { + inf_clear_player_collision_flags(s); + inf_stamp_player_collision_flags(s); +} + +static void inf_rebuild_entity_collision_flags(InfernoState* s) { + memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); + inf_rebuild_player_collision_flags(s); + for (int i = 0; i < INF_MAX_NPCS; i++) { + InfNPC* npc = &s->npcs[i]; + if (!npc->active) continue; + if (!inf_npc_sets_collision_flag(npc->type)) continue; + inf_stamp_npc_collision_footprint(s, npc->x, npc->y, inf_npc_effective_size(npc)); + } +} + +static void inf_deactivate_npc(InfernoState* s, int idx) { + if (idx < 0 || idx >= INF_MAX_NPCS) return; + InfNPC* npc = &s->npcs[idx]; + if (npc->active && inf_npc_sets_collision_flag(npc->type)) + inf_unstamp_npc_collision_footprint(s, npc->x, npc->y, inf_npc_effective_size(npc)); + encounter_pending_hit_queue_clear(&npc->pending_hits); + npc->active = 0; +} + +static void inf_init_npc(InfernoState* s, int idx, InfNPCType type, int x, int y) { + InfNPC* npc = &s->npcs[idx]; + const InfNPCStats* stats = &INF_NPC_STATS[type]; + memset(npc, 0, sizeof(InfNPC)); + + s->next_npc_render_id++; + if (s->next_npc_render_id == 0) + s->next_npc_render_id = 1; + + npc->type = type; + npc->hp = stats->hp; + npc->max_hp = stats->hp; + npc->size = stats->size; + npc->render_id = s->next_npc_render_id; + npc->attack_timer = stats->attack_speed; + npc->attack_style = stats->default_style; + npc->attack_style_this_tick = ATTACK_STYLE_NONE; + npc->active = 1; + npc->x = x; + npc->y = y; + npc->target_x = x; + npc->target_y = y; + npc->attack_visual_target = -1; + npc->resurrection_visual_target = -1; + npc->aggro_target = -1; + npc->blob_scanned_prayer = -1; + npc->had_los_last_tick = 0; + npc->stun_timer = stats->stun_on_spawn; + inf_npc_init_type_state(npc); + + if (inf_npc_sets_collision_flag(type)) + inf_stamp_npc_collision_footprint(s, x, y, stats->size); +} + +static int inf_find_first_active_npc_of_type(const InfernoState* s, InfNPCType type) { + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (s->npcs[i].active && s->npcs[i].type == type) return i; + } + return -1; +} + +static void inf_spawn_zuk_jad(InfernoState* s, int shield_idx) { + int j_slot = inf_find_free_npc(s); + if (j_slot >= 0) { + inf_init_npc(s, j_slot, INF_NPC_JAD, 24, 32); + s->npcs[j_slot].aggro_target = shield_idx; + s->npcs[j_slot].stun_timer = 7; + } +} + +static void inf_spawn_zuk_healers(InfernoState* s, int zuk_idx) { + static const int healer_pos[4][2] = { + {16, 48}, {20, 48}, {30, 48}, {34, 48} + }; + for (int h = 0; h < 4; h++) { + int slot = inf_find_free_npc(s); + if (slot >= 0) { + inf_init_npc(s, slot, INF_NPC_HEALER_ZUK, + healer_pos[h][0], healer_pos[h][1]); + s->npcs[slot].aggro_target = zuk_idx; + } + } +} + +static void inf_seed_joseph_zuk_checkpoint(InfernoState* s) { + if (s->start_wave < INF_START_WAVE_ZUK_JAD) return; + + int zuk_idx = inf_find_first_active_npc_of_type(s, INF_NPC_ZUK); + int shield_idx = s->zuk.shield_idx; + if (zuk_idx < 0 || shield_idx < 0 || shield_idx >= INF_MAX_NPCS || + !s->npcs[shield_idx].active) + return; + + InfNPC* zuk = &s->npcs[zuk_idx]; + zuk->stun_timer = 0; + s->zuk.has_paused = 1; + s->zuk.timer_paused = 0; + s->zuk.jad_spawned = 1; + s->zuk.shield_freeze = 0; + + if (s->start_wave == INF_START_WAVE_ZUK_JAD) { + s->zuk.set_timer = 247; + s->zuk.enraged = 0; + s->zuk.healer_spawned = 0; + zuk->hp = 479; + zuk->attack_timer = INF_NPC_STATS[INF_NPC_ZUK].attack_speed; + s->min_zuk_hp_seen = (float)zuk->hp; + inf_spawn_zuk_jad(s, shield_idx); + return; + } + + s->zuk.set_timer = s->zuk.set_interval; + s->zuk.enraged = 1; + s->zuk.healer_spawned = 1; + zuk->hp = 239; + zuk->attack_timer = 7; + s->min_zuk_hp_seen = (float)zuk->hp; + inf_spawn_zuk_healers(s, zuk_idx); +} + +static void inf_spawn_wave(InfernoState* s) { + if (s->wave >= INF_NUM_WAVES) return; + + const InfWaveDef* w = &INF_WAVES[s->wave]; + + for (int i = 0; i < INF_MAX_NPCS; i++) s->npcs[i].active = 0; + encounter_pending_hit_queue_clear(&s->player_pending_hits); + memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); + inf_rebuild_player_collision_flags(s); + + s->dead_mob_count = 0; + + inf_shuffle_spawns(s); + + { + int active_pillars[INF_NUM_PILLARS]; + int num_active = 0; + for (int p = 0; p < INF_NUM_PILLARS; p++) { + if (s->pillars[p].active) active_pillars[num_active++] = p; + } + s->nibbler_target_pillar = (num_active > 0) + ? active_pillars[encounter_rand_int(&s->rng_state, num_active)] : -1; + } + + if (s->wave >= 66) { + int pillars_changed = 0; + for (int p = 0; p < INF_NUM_PILLARS; p++) { + if (s->pillars[p].active) { + s->pillars[p].active = 0; + s->pillars[p].hp = 0; + pillars_changed = 1; + } + } + if (pillars_changed) inf_rebuild_los(s); + } + + if (s->wave == 66) { + s->player.x = 18; + s->player.y = 32; + inf_rebuild_player_collision_flags(s); + int slot = inf_find_free_npc(s); + if (slot >= 0) { + inf_init_npc(s, slot, INF_NPC_JAD, 23, 30); + s->npcs[slot].stun_timer = 1; + s->npcs[slot].attack_timer = 8; + } + return; + } + + if (s->wave == 67) { + s->player.x = 25; + s->player.y = 30; + inf_rebuild_player_collision_flags(s); + int stuns[3] = { 1, 4, 7 }; + for (int i = 2; i > 0; i--) { + int j = encounter_rand_int(&s->rng_state, i + 1); + int tmp = stuns[i]; stuns[i] = stuns[j]; stuns[j] = tmp; + } + static const int JAD_POS[3][2] = { + {18, 33}, + {28, 33}, + {23, 22}, + }; + for (int i = 0; i < 3; i++) { + int slot = inf_find_free_npc(s); + if (slot < 0) break; + inf_init_npc(s, slot, INF_NPC_JAD, JAD_POS[i][0], JAD_POS[i][1]); + s->npcs[slot].stun_timer = stuns[i]; + s->npcs[slot].attack_timer = + inf_initial_attack_timer_after_stun(9, stuns[i]); + } + return; + } + + if (s->wave == 68) { + int zuk_idx = inf_find_free_npc(s); + if (zuk_idx >= 0) { + inf_init_npc(s, zuk_idx, INF_NPC_ZUK, INF_ZUK_X, INF_ZUK_Y); + s->min_zuk_hp_seen = (float)s->npcs[zuk_idx].hp; + s->npcs[zuk_idx].stun_timer = 8; + s->npcs[zuk_idx].attack_timer = 14; + } + + int shield_idx = inf_find_free_npc(s); + if (shield_idx >= 0) { + inf_init_npc(s, shield_idx, INF_NPC_ZUK_SHIELD, INF_ZUK_SHIELD_X, INF_ZUK_SHIELD_Y); + s->zuk.shield_idx = shield_idx; + s->zuk.shield_dir = (encounter_rand_int(&s->rng_state, 2) == 0) ? 1 : -1; + s->zuk.shield_freeze = 1; + } + + s->zuk.set_timer = 72; + s->zuk.set_interval = 350; + s->zuk.enraged = 0; + s->zuk.healer_spawned = 0; + s->zuk.jad_spawned = 0; + s->zuk.timer_paused = 0; + s->zuk.has_paused = 0; + + s->player.x = INF_ZUK_PLAYER_START_X; + s->player.y = INF_ZUK_PLAYER_START_Y; + inf_rebuild_player_collision_flags(s); + inf_seed_joseph_zuk_checkpoint(s); + return; + } + + int spawn_idx = 0; + for (int i = 0; i < w->count && i < INF_MAX_NPCS; i++) { + InfNPCType type = (InfNPCType)w->types[i]; + int slot = inf_find_free_npc(s); + if (slot < 0) break; + + int sx, sy; + if (type == INF_NPC_NIBBLER) { + sx = INF_NIBBLER_SPAWN_X + encounter_rand_int(&s->rng_state, 3) - 1; + sy = INF_NIBBLER_SPAWN_Y + encounter_rand_int(&s->rng_state, 3) - 1; + } else { + int pi = s->spawn_order[spawn_idx % INF_NUM_SPAWN_POS]; + sx = INF_SPAWN_POS[pi][0]; + sy = INF_SPAWN_POS[pi][1]; + spawn_idx++; + } + + inf_init_npc(s, slot, type, sx, sy); + } +} diff --git a/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc new file mode 100644 index 0000000000..250b99fd56 --- /dev/null +++ b/ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc @@ -0,0 +1,693 @@ +#line 4619 "encounter_inferno.h" +static int inf_healer_is_actively_healing(const InfernoState* s, const InfNPC* npc) { + if (!npc->active || npc->death_ticks > 0) return 0; + if (npc->type != INF_NPC_HEALER_JAD && npc->type != INF_NPC_HEALER_ZUK) return 0; + if (npc->aggro_target < 0 || npc->aggro_target >= INF_MAX_NPCS) return 0; + return s->npcs[npc->aggro_target].active; +} + +static int inf_npc_type_is_actively_healed( + const InfernoState* s, + InfNPCType target_type +) { + for (int i = 0; i < INF_MAX_NPCS; i++) { + const InfNPC* healer = &s->npcs[i]; + if (!inf_healer_is_actively_healing(s, healer)) continue; + const InfNPC* target = &s->npcs[healer->aggro_target]; + if (target->type == target_type) return 1; + } + return 0; +} + +static int inf_count_untagged_zuk_healers(const InfernoState* s) { + int count = 0; + for (int i = 0; i < INF_MAX_NPCS; i++) { + const InfNPC* healer = &s->npcs[i]; + if (!inf_healer_is_actively_healing(s, healer)) continue; + if (healer->type != INF_NPC_HEALER_ZUK) continue; + + const InfNPC* target = &s->npcs[healer->aggro_target]; + if (target->type == INF_NPC_ZUK) count++; + } + return count; +} + +static int inf_count_post_healer_set_pressure(const InfernoState* s) { + int count = 0; + for (int i = 0; i < INF_MAX_NPCS; i++) { + const InfNPC* npc = &s->npcs[i]; + if (!npc->active || npc->death_ticks != 0 || npc->hp <= 0) + continue; + if (inf_npc_type_is_set_pressure(npc->type)) + count++; + } + return count; +} + +static inline float inf_dmg_below_threshold(float old_hp, float new_hp, float t) { + if (new_hp >= old_hp) return 0.0f; + float a = old_hp < t ? old_hp : t; + float b = new_hp < t ? new_hp : t; + float d = a - b; + return d > 0.0f ? d : 0.0f; +} + +typedef enum { + INF_ZUK_LOWWATERMARK_TRACK_ONLY, + INF_ZUK_LOWWATERMARK_REWARD_ALLOWED, + INF_ZUK_LOWWATERMARK_REWARD_BLOCKED_EXCEPT_THRESHOLD, +} InfZukLowWatermarkMode; + +static float inf_zuk_low_watermark_reward( + InfernoState* s, + const InfernoContext* ctx, + InfZukLowWatermarkMode mode +) { + int zuk_idx = inf_find_live_zuk_idx(s); + if (zuk_idx < 0) return 0.0f; + + float zuk_hp = (float)s->npcs[zuk_idx].hp; + if (zuk_hp >= s->min_zuk_hp_seen) return 0.0f; + + float old_min = s->min_zuk_hp_seen; + float dmg = old_min - zuk_hp; + int crosses_zuk_healer_threshold = old_min > 240.0f && zuk_hp <= 240.0f; + int should_pay_zuk_damage = + mode == INF_ZUK_LOWWATERMARK_REWARD_ALLOWED || + (mode == INF_ZUK_LOWWATERMARK_REWARD_BLOCKED_EXCEPT_THRESHOLD && + crosses_zuk_healer_threshold); + float reward = should_pay_zuk_damage ? ctx->config.damage_reward_coeff * dmg : 0.0f; + if (s->jad_killed_this_episode) { + reward *= ctx->config.post_jad_zuk_multiplier; + } else if (s->zuk.jad_spawned) { + reward *= ctx->config.jad_alive_zuk_multiplier; + } + s->min_zuk_hp_seen = zuk_hp; + + if (s->tick_at_le_300 < 0 && zuk_hp <= 300.0f) s->tick_at_le_300 = s->tick; + if (s->tick_at_le_240 < 0 && zuk_hp <= 240.0f) s->tick_at_le_240 = s->tick; + if (s->tick_at_le_150 < 0 && zuk_hp <= 150.0f) s->tick_at_le_150 = s->tick; + s->damage_after_300 += inf_dmg_below_threshold(old_min, zuk_hp, 300.0f); + s->damage_after_240 += inf_dmg_below_threshold(old_min, zuk_hp, 240.0f); + s->damage_after_150 += inf_dmg_below_threshold(old_min, zuk_hp, 150.0f); + + return reward; +} + +static float inf_joseph_heal_damped_damage(float damage, float max_hp, float total_healed) { + if (damage <= 0.0f) return 0.0f; + float denom = max_hp + 4.0f * total_healed; + if (denom <= 0.0f) return damage; + return damage * max_hp / denom; +} + +static float inf_zuk_healer_attack_shape_reward( + const InfernoState* s, + const InfernoContext* ctx +) { + return + ctx->config.zuk_untagged_healer_nonmagic_attack_bonus_coeff * + (float)s->tick_scratch.zuk_untagged_healer_nonmagic_attacks - + ctx->config.zuk_healer_mage_attack_penalty_coeff * + (float)s->tick_scratch.zuk_healer_mage_attack_fires; +} + +static float inf_offensive_prayer_damage_reward( + const InfernoState* s, + const InfernoContext* ctx +) { + return ctx->config.offensive_prayer_reward_coeff * + ctx->config.damage_reward_coeff * + s->tick_scratch.offensive_prayer_correct_damage_roll; +} + +static float inf_compute_joseph_reward( + const InfernoState* s, + const InfernoContext* ctx, + int healer_is_actively_healing +) { + float reward = 0.0f; + if (healer_is_actively_healing) { + reward = ctx->config.tag_reward_coeff * (float)s->tick_scratch.healer_tags; + } else { + float rewarded_other_damage = fmaxf( + 0.0f, + s->tick_scratch.damage_dealt - + s->tick_scratch.damage_jad - + s->tick_scratch.damage_zuk - + s->tick_scratch.damage_jad_healers); + reward = ctx->config.damage_reward_coeff * ( + rewarded_other_damage + + inf_joseph_heal_damped_damage( + s->tick_scratch.damage_jad, + (float)INF_NPC_STATS[INF_NPC_JAD].hp, + s->total_hp_restored_jad) + + inf_joseph_heal_damped_damage( + s->tick_scratch.damage_zuk, + (float)INF_NPC_STATS[INF_NPC_ZUK].hp, + s->total_hp_restored_zuk)); + reward += inf_offensive_prayer_damage_reward(s, ctx); + } + reward -= ctx->config.shield_penalty_coeff * s->tick_scratch.shield_damage; + reward += inf_zuk_healer_attack_shape_reward(s, ctx); + return reward; +} + +static float inf_compute_reward_ctx(InfernoState* s, const InfernoContext* ctx) { + s->total_damage_dealt += s->tick_scratch.damage_dealt; + s->total_zuk_healer_damage += s->tick_scratch.damage_zuk_healers; + s->total_damage_received += s->tick_scratch.damage_received; + s->total_hp_restored += s->tick_scratch.hp_restored; + s->total_hp_restored_jad += s->tick_scratch.hp_restored_jad; + s->total_hp_restored_zuk += s->tick_scratch.hp_restored_zuk; + + if (s->tick_scratch.kill_jad > 0) s->jad_killed_this_episode = 1; + + if (s->episode_over) { + if (s->winner == INF_OUTCOME_PLAYER_WON) return 1.0f; + if (ctx->config.terminal_penalty_enabled) return -1.0f; + return -ctx->config.death_penalty_coeff; + } + + int jad_is_actively_healed = + inf_npc_type_is_actively_healed(s, INF_NPC_JAD); + int zuk_is_actively_healed = + inf_npc_type_is_actively_healed(s, INF_NPC_ZUK); + int healer_is_actively_healing = + jad_is_actively_healed || zuk_is_actively_healed; + int tags_first_gate = inf_zuk_healer_tags_first_reward_gate_active(s, ctx); + + if (ctx->config.joseph_reward_mode == INF_JOSEPH_REWARD_MODE_ON) { + if (inf_is_final_wave(s)) + inf_zuk_low_watermark_reward(s, ctx, INF_ZUK_LOWWATERMARK_TRACK_ONLY); + return inf_compute_joseph_reward(s, ctx, healer_is_actively_healing); + } + + float reward = 0.0f; + if (inf_is_final_wave(s)) { + if (tags_first_gate) { + reward = inf_zuk_low_watermark_reward( + s, ctx, INF_ZUK_LOWWATERMARK_TRACK_ONLY); + reward += ctx->config.tag_reward_coeff * (float)s->tick_scratch.healer_tags; + reward += inf_zuk_healer_attack_shape_reward(s, ctx); + return reward; + } + int use_zuk_healer_phase_hp_delta = + s->zuk.healer_spawned && + ctx->config.zuk_healer_phase_hp_delta_coeff > 0.0f; + InfZukLowWatermarkMode watermark_mode = + INF_ZUK_LOWWATERMARK_REWARD_ALLOWED; + if (use_zuk_healer_phase_hp_delta) { + watermark_mode = INF_ZUK_LOWWATERMARK_TRACK_ONLY; + } else if (zuk_is_actively_healed) { + watermark_mode = INF_ZUK_LOWWATERMARK_REWARD_BLOCKED_EXCEPT_THRESHOLD; + } + reward = inf_zuk_low_watermark_reward(s, ctx, watermark_mode); + if (use_zuk_healer_phase_hp_delta) { + reward += ctx->config.zuk_healer_phase_hp_delta_coeff * + (s->tick_scratch.damage_zuk - s->tick_scratch.hp_restored_zuk); + } + reward += ctx->config.damage_reward_coeff * s->tick_scratch.damage_zuk_healers; + if (healer_is_actively_healing) { + float generic_heal_cost = s->tick_scratch.hp_restored; + if (use_zuk_healer_phase_hp_delta) + generic_heal_cost -= s->tick_scratch.hp_restored_zuk; + if (generic_heal_cost < 0.0f) generic_heal_cost = 0.0f; + reward -= ctx->config.damage_reward_coeff * generic_heal_cost; + } + if (s->zuk.healer_spawned && + ctx->config.zuk_untagged_healer_tick_penalty_coeff > 0.0f) { + reward -= ctx->config.zuk_untagged_healer_tick_penalty_coeff * + (float)inf_count_untagged_zuk_healers(s); + } + } else { + float rewardable_damage = s->tick_scratch.damage_dealt - + s->tick_scratch.damage_jad_healers - + s->tick_scratch.damage_resurrected; + if (jad_is_actively_healed) + rewardable_damage -= s->tick_scratch.damage_jad; + reward = ctx->config.damage_reward_coeff * + fmaxf(0.0f, rewardable_damage - s->tick_scratch.hp_restored); + } + + reward += inf_offensive_prayer_damage_reward(s, ctx); + reward += ctx->config.tag_reward_coeff * (float)s->tick_scratch.healer_tags; + reward += inf_zuk_healer_attack_shape_reward(s, ctx); + reward += ctx->config.shield_tag_reward_coeff * (float)s->tick_scratch.shield_tags; + reward += ctx->config.zuk_untagged_healer_target_bonus_coeff * + (float)s->tick_scratch.zuk_untagged_healer_targets; + reward += ctx->config.zuk_safe_untagged_healer_target_bonus_coeff * + (float)s->tick_scratch.zuk_safe_untagged_healer_targets; + + int zuk_idx = inf_find_live_zuk_idx(s); + int zuk_hp_now = (zuk_idx >= 0) ? s->npcs[zuk_idx].hp : 1200; + int late_add_reward_active = s->zuk.jad_spawned || zuk_hp_now <= 600; + if (late_add_reward_active) { + float rewardable_jad_damage = + jad_is_actively_healed ? 0.0f : s->tick_scratch.damage_jad; + reward += ctx->config.jad_damage_reward_coeff * rewardable_jad_damage; + reward += ctx->config.zuk_healer_damage_reward_coeff * s->tick_scratch.damage_zuk_healers; + reward += ctx->config.set_damage_reward_coeff * s->tick_scratch.damage_set; + if (s->tick_at_all_zuk_healers_dead >= 0) { + reward += ctx->config.post_healer_zuk_damage_coeff * s->tick_scratch.damage_zuk; + float post_healer_set_damage_reward = + ctx->config.post_healer_set_damage_reward_coeff * s->tick_scratch.damage_set; + float post_healer_set_kill_bonus = + (float)s->tick_scratch.kill_set * ctx->config.post_healer_set_kill_bonus; + reward += post_healer_set_damage_reward; + s->post_healer_set_damage_reward_total += + post_healer_set_damage_reward; + s->post_healer_set_kill_bonus_total += post_healer_set_kill_bonus; + s->pending_set_kill_bonus += post_healer_set_kill_bonus; + if (ctx->config.post_healer_set_alive_tick_penalty_coeff > 0.0f) { + int set_pressure = inf_count_post_healer_set_pressure(s); + s->post_healer_set_pressure_total += (float)set_pressure; + float penalty = ctx->config.post_healer_set_alive_tick_penalty_coeff * + (float)set_pressure; + if (ctx->config.post_healer_set_alive_penalty_cap > 0.0f) { + float remaining = ctx->config.post_healer_set_alive_penalty_cap - + s->post_healer_set_alive_penalty_total; + if (remaining < 0.0f) remaining = 0.0f; + if (penalty > remaining) penalty = remaining; + } + s->post_healer_set_alive_penalty_total += penalty; + reward -= penalty; + } + } + s->pending_jad_kill_bonus += + (float)s->tick_scratch.kill_jad * ctx->config.jad_kill_bonus; + s->pending_zuk_healer_kill_bonus += + (float)s->tick_scratch.kill_zuk_healer * ctx->config.zuk_healer_kill_bonus; + s->pending_set_kill_bonus += + (float)s->tick_scratch.kill_set * ctx->config.set_kill_bonus; + } + if (s->pending_jad_kill_bonus > 0.0f) { + float emit = fminf(0.07f, s->pending_jad_kill_bonus); + reward += emit; + s->pending_jad_kill_bonus -= emit; + } + if (s->pending_zuk_healer_kill_bonus > 0.0f) { + float emit = fminf(0.05f, s->pending_zuk_healer_kill_bonus); + reward += emit; + s->pending_zuk_healer_kill_bonus -= emit; + } + if (s->pending_set_kill_bonus > 0.0f) { + float emit = fminf(0.03f, s->pending_set_kill_bonus); + reward += emit; + s->pending_set_kill_bonus -= emit; + } + + float shield_penalty = ctx->config.shield_penalty_coeff * s->tick_scratch.shield_damage; + if (ctx->config.shield_penalty_episode_cap > 0.0f) { + float remaining = ctx->config.shield_penalty_episode_cap - + s->shield_penalty_episode_total; + if (remaining < 0.0f) remaining = 0.0f; + if (shield_penalty > remaining) shield_penalty = remaining; + } + s->shield_penalty_episode_total += shield_penalty; + reward -= shield_penalty; + + if (ctx->config.phase_900_bonus > 0.0f && !s->phase_900_fired + && s->min_zuk_hp_seen <= 900.0f && s->min_zuk_hp_seen > 0.0f) { + reward += ctx->config.phase_900_bonus; + s->phase_900_fired = 1; + } + if (ctx->config.phase_600_bonus > 0.0f && !s->phase_600_fired + && s->min_zuk_hp_seen <= 600.0f && s->min_zuk_hp_seen > 0.0f) { + reward += ctx->config.phase_600_bonus; + s->phase_600_fired = 1; + } + if (ctx->config.phase_300_bonus > 0.0f && !s->phase_300_fired + && s->min_zuk_hp_seen <= 300.0f && s->min_zuk_hp_seen > 0.0f) { + reward += ctx->config.phase_300_bonus; + s->phase_300_fired = 1; + } + return reward; +} + +static float inf_compute_reward(InfernoState* s) { + return inf_compute_reward_ctx(s, inf_legacy_context()); +} + +static float inf_terminal_loss_reward( + const InfernoState* s, + const InfernoContext* ctx +) { + (void)s; + return ctx->config.terminal_penalty_enabled ? -1.0f : 0.0f; +} + +static void inf_update_healer_transition_stats(InfernoState* s) { + s->total_shield_tags += s->tick_scratch.shield_tags; + + if (!s->zuk.healer_spawned) return; + + if (s->tick_at_zuk_healer_spawn < 0) + s->tick_at_zuk_healer_spawn = s->tick; + + int zuk_idx = inf_find_live_zuk_idx(s); + if (zuk_idx >= 0) { + float hp = (float)s->npcs[zuk_idx].hp; + if (s->tick_at_le_240 < 0 && hp <= 240.0f) + s->tick_at_le_240 = s->tick; + if (hp > s->zuk_hp_max_after_healer_spawn) + s->zuk_hp_max_after_healer_spawn = hp; + } + + s->total_zuk_healer_tags += s->tick_scratch.zuk_healer_tags; + if (s->total_zuk_healer_tags > 4) s->total_zuk_healer_tags = 4; + s->total_zuk_healer_kills += s->tick_scratch.kill_zuk_healer; + if (s->total_zuk_healer_kills > 4) s->total_zuk_healer_kills = 4; + + if (s->total_zuk_healer_tags > 0 && s->tick_at_first_zuk_healer_tag < 0) + s->tick_at_first_zuk_healer_tag = s->tick; + if (s->total_zuk_healer_tags >= 4 && s->tick_at_all_zuk_healers_tagged < 0) + s->tick_at_all_zuk_healers_tagged = s->tick; + + int live_zuk_healers = 0; + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (!s->npcs[i].active || s->npcs[i].hp <= 0) continue; + if (s->npcs[i].type == INF_NPC_HEALER_ZUK) + live_zuk_healers++; + } + if (live_zuk_healers == 0 && s->tick_at_all_zuk_healers_dead < 0) { + s->tick_at_all_zuk_healers_dead = s->tick; + s->zuk_hp_at_all_zuk_healers_dead = + zuk_idx >= 0 ? (float)s->npcs[zuk_idx].hp : 0.0f; + } + + if (s->tick_at_all_zuk_healers_dead >= 0 && s->tick_scratch.damage_zuk > 0.0f) { + s->damage_after_all_zuk_healers_dead += s->tick_scratch.damage_zuk; + if (s->tick_at_first_zuk_hit_after_all_healers_dead < 0) + s->tick_at_first_zuk_hit_after_all_healers_dead = s->tick; + } + + if (s->tick_at_le_240 >= 0) { + s->hp_restored_after_240 += s->tick_scratch.hp_restored; + s->spark_damage_after_240 += s->tick_scratch.spark_damage; + } +} + +static int inf_has_live_player_target(const InfernoState* s) { + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (inf_npc_is_live_player_target(s, i)) return 1; + } + return 0; +} + +static int inf_has_attackable_player_target(const InfernoState* s) { + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (inf_npc_is_live_player_target(s, i) && + inf_player_can_attack_npc_from_current_tile(s, i)) + return 1; + } + return 0; +} + +static int inf_has_live_npc_type(const InfernoState* s, InfNPCType type) { + for (int i = 0; i < INF_MAX_NPCS; i++) { + const InfNPC* npc = &s->npcs[i]; + if (npc->active && npc->death_ticks == 0 && npc->hp > 0 && + npc->type == type) + return 1; + } + return 0; +} + +static InfIdleDiagnosticPhase inf_idle_diagnostic_phase(const InfernoState* s) { + if (!inf_is_final_wave(s)) + return inf_has_live_npc_type(s, INF_NPC_JAD) + ? INF_IDLE_PHASE_JAD : INF_IDLE_PHASE_SET; + if (inf_has_live_npc_type(s, INF_NPC_HEALER_ZUK)) + return INF_IDLE_PHASE_ZUK_HEALERS; + if (inf_has_live_npc_type(s, INF_NPC_JAD)) + return INF_IDLE_PHASE_ZUK_JAD; + if (s->tick_at_all_zuk_healers_dead >= 0) + return INF_IDLE_PHASE_ZUK_POST_HEALERS; + return INF_IDLE_PHASE_ZUK_PRE_JAD; +} + +static int inf_player_has_immediate_threat(const InfernoState* s) { + if (s->tick_scratch.attacks_fired > 0) return 1; + for (int h = 0; h < s->player_pending_hits.count; h++) { + const EncounterPendingHit* ph = &s->player_pending_hits.hits[h]; + if (ph->check_prayer && inf_pending_hit_obs_timer(ph) <= 1) + return 1; + } + for (int i = 0; i < INF_MAX_NPCS; i++) { + const InfNPC* npc = &s->npcs[i]; + InfNpcPlayerThreat threat = inf_npc_player_threat(s, npc); + if (threat.telegraph_style_mask && threat.can_attack_this_tick) + return 1; + } + return 0; +} + +static int inf_progress_happened_this_tick(const InfernoState* s) { + return s->tick_scratch.damage_dealt > 0.0f || + s->tick_scratch.healer_tags > 0 || + s->tick_scratch.shield_tags > 0 || + s->tick_scratch.kill_jad > 0 || + s->tick_scratch.kill_zuk_healer > 0 || + s->tick_scratch.kill_set > 0 || + s->tick_scratch.wave_completed > 0; +} + +static void inf_record_idle_diagnostics( + InfernoState* s, + int can_player_attack, + int attack_ready_no_attack, + int target_available_no_attack, + int safe_attack_opportunity_missed +) { + if (!can_player_attack || !inf_has_live_player_target(s)) + return; + + InfIdleDiagnosticPhase phase = inf_idle_diagnostic_phase(s); + assert((int)phase >= 0 && (int)phase < OSRS_INFERNO_IDLE_PHASE_COUNT); + + if (attack_ready_no_attack) { + s->total_attack_ready_no_attack_ticks++; + s->attack_ready_no_attack_ticks_by_phase[phase]++; + } + if (target_available_no_attack) { + s->total_target_available_no_attack_ticks++; + s->target_available_no_attack_ticks_by_phase[phase]++; + } + if (safe_attack_opportunity_missed) { + s->total_safe_attack_opportunity_missed_ticks++; + s->safe_attack_opportunity_missed_ticks_by_phase[phase]++; + } + if (!inf_progress_happened_this_tick(s)) { + s->total_progressless_ticks++; + s->progressless_ticks_by_phase[phase]++; + } +} + +static void inf_step_ctx(EncounterState* state, EncounterContext* context, const int* actions) { + InfernoState* s = (InfernoState*)state; + InfernoContext* ctx = context ? (InfernoContext*)context : inf_legacy_context(); + if (s->episode_over) return; + + s->reward = 0.0f; + s->player_moved_last_tick = s->tick_scratch.player_moved; + s->tick_scratch = (InfTickScratch){ .pillar_lost = -1 }; + s->player_attack_timing = (EncounterProjectileTiming){0}; + encounter_clear_tick_flags(&s->player); + for (int i = 0; i < INF_MAX_NPCS; i++) { + s->npcs[i].attacked_this_tick = 0; + s->npcs[i].attack_style_this_tick = ATTACK_STYLE_NONE; + s->npcs[i].attack_visual_target = -1; + s->npcs[i].resurrecting_this_tick = 0; + s->npcs[i].resurrection_visual_target = -1; + s->npcs[i].moved_this_tick = 0; + s->npcs[i].hit_landed_this_tick = 0; + s->npcs[i].hit_damage = 0; + s->npcs[i].hit_was_successful_this_tick = 0; + s->npcs[i].hit_spell_type = 0; + } + s->tick++; + inf_player_pretick(s, ctx, actions); + + int spawn_wave_now = 0; + if (s->wave_spawn_delay > 0) { + s->wave_spawn_delay--; + if (s->wave_spawn_delay == 0) + spawn_wave_now = 1; + } + int in_wave_gap = (s->wave_spawn_delay > 0 || spawn_wave_now); + if (s->wave_ready_delay > 0) + s->wave_ready_delay--; + int in_ready_gap = s->wave_ready_delay > 0; + + inf_resolve_player_projectiles_on_npcs(s); + inf_resolve_player_pending_hits(s); + inf_resolve_pending_sparks(s); + + if (!in_wave_gap && !in_ready_gap) { + inf_rebuild_player_collision_flags(s); + inf_invalidate_los_cache(s); + inf_tick_npcs_ctx(s, ctx); + } + inf_update_healer_transition_stats(s); + + if (s->player.current_hitpoints <= 0) { + (void)inf_compute_reward_ctx(s, ctx); + if (s->last_hit_by_type >= 0 && s->last_hit_by_type < INF_NUM_NPC_TYPES) + s->killed_by_type[s->last_hit_by_type]++; + s->episode_over = 1; + s->winner = INF_OUTCOME_PLAYER_DIED; + s->reward = inf_terminal_loss_reward(s, ctx); + s->episode_return += s->reward; + return; + } + + int can_player_attack = !in_wave_gap && !in_ready_gap; + int attack_ready_no_attack = 0; + int target_available_no_attack = 0; + int safe_attack_opportunity_missed = 0; + int player_x_before_tick_player = s->player.x; + int player_y_before_tick_player = s->player.y; + inf_tick_player_ctx(s, ctx, actions, can_player_attack); + s->tick_scratch.player_moved = + (s->player.x != player_x_before_tick_player || + s->player.y != player_y_before_tick_player) ? 1 : 0; + if (s->tick_scratch.player_moved) + inf_invalidate_los_cache(s); + inf_resolve_jad_prayer_checks_after_player(s, ctx); + + { + int has_alive_npc = can_player_attack && inf_has_live_player_target(s); + if (has_alive_npc && s->player.attack_timer == 0 && !s->tick_scratch.player_attacked) + s->ticks_without_action++; + else + s->ticks_without_action = 0; + attack_ready_no_attack = s->ticks_without_action > 0; + if (attack_ready_no_attack) { + int target_available = inf_has_live_player_target(s); + int attackable_target = inf_has_attackable_player_target(s); + target_available_no_attack = target_available; + safe_attack_opportunity_missed = + attackable_target && !inf_player_has_immediate_threat(s); + } + } + + s->total_prayer_correct += s->tick_scratch.prayer_correct; + int ranger_attacked_this_tick = 0; + int mager_attacked_this_tick = 0; + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (s->npcs[i].attacked_this_tick && + s->npcs[i].aggro_target < 0 && + s->npcs[i].type != INF_NPC_NIBBLER && + !(s->npcs[i].type == INF_NPC_BLOB && + s->npcs[i].blob_scanned_prayer >= 0)) { + s->total_npc_attacks++; + if (s->npcs[i].type == INF_NPC_RANGER) + ranger_attacked_this_tick = 1; + if (s->npcs[i].type == INF_NPC_MAGER) + mager_attacked_this_tick = 1; + } + } + if (ranger_attacked_this_tick && mager_attacked_this_tick) { + s->total_ranger_mager_same_tick_attacks++; + if (s->player_moved_last_tick) + s->total_step_out_ranger_mager_same_tick_attacks++; + } + if (s->tick_scratch.attacks_fired > 0) { + int styles = s->tick_scratch.styles_fired; + int n_styles = ((styles >> 0) & 1) + ((styles >> 1) & 1) + ((styles >> 2) & 1); + if (n_styles >= 2 && s->tick_scratch.prayer_correct > 0) { + int off_prayer = s->tick_scratch.attacks_fired - s->tick_scratch.prayer_correct; + s->total_unavoidable_off += off_prayer; + } + } + if (s->ticks_without_action > 0) s->total_idle_ticks++; + s->total_brews_used += s->tick_scratch.brewed; + s->total_blood_healed += s->tick_scratch.blood_heal; + { + InfNpcPressureSummary pressure = inf_npc_pressure_summary(s); + s->total_npc_pressure_if_ready_count += + (float)pressure.if_ready_total; + s->total_npc_pressure_this_tick_count += + (float)pressure.this_tick_total; + s->total_npc_pressure_if_ready_max_hit += + (float)pressure.if_ready_max_hit; + s->total_npc_pressure_this_tick_max_hit += + (float)pressure.this_tick_max_hit; + int max_incoming_hit = pressure.if_ready_max_hit > + pressure.this_tick_max_hit ? pressure.if_ready_max_hit : + pressure.this_tick_max_hit; + if ((float)max_incoming_hit > s->max_npc_pressure_incoming_hit) + s->max_npc_pressure_incoming_hit = (float)max_incoming_hit; + } + + if (can_player_attack && s->wave == 68) { + s->total_zuk_ticks++; + int is_behind_shield = inf_player_behind_zuk_shield_now(s); + if (is_behind_shield) { + s->behind_shield_ticks++; + s->tick_scratch.behind_shield = 1; + } + if (!is_behind_shield && s->tick_at_le_240 >= 0) + s->offshield_ticks_after_240++; + if (!is_behind_shield && s->tick_at_all_zuk_healers_dead >= 0) + s->offshield_ticks_after_all_zuk_healers_dead++; + } + + s->reward = inf_compute_reward_ctx(s, ctx); + int all_dead = 1; + + if (s->player.current_hitpoints <= 0) { + if (s->last_hit_by_type >= 0 && s->last_hit_by_type < INF_NUM_NPC_TYPES) + s->killed_by_type[s->last_hit_by_type]++; + s->episode_over = 1; + s->winner = INF_OUTCOME_PLAYER_DIED; + s->reward = inf_terminal_loss_reward(s, ctx); + goto finish_step; + } + + if (spawn_wave_now) { + s->wave = s->wave_spawn_target; + inf_spawn_wave(s); + inf_invalidate_los_cache(s); + goto finish_step; + } + if (s->wave_spawn_delay > 0) goto finish_step; + + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (s->npcs[i].active) { all_dead = 0; break; } + } + if (all_dead) { + s->tick_scratch.wave_completed = 1; + int next_public_wave = s->wave + 2; + float wave_clear_reward = 1.0f + + inf_supply_milestone_surplus_reward(s, ctx, next_public_wave); + s->reward = wave_clear_reward; + if (s->wave + 1 >= INF_NUM_WAVES) { + s->episode_over = 1; + s->winner = INF_OUTCOME_PLAYER_WON; + } else { + s->wave_spawn_target = s->wave + 1; + s->wave_spawn_delay = 9; + } + } + + if (s->tick >= INF_MAX_TICKS) { + s->episode_over = 1; + s->winner = INF_OUTCOME_PLAYER_DIED; + s->reward = inf_terminal_loss_reward(s, ctx); + } + +finish_step: + inf_record_idle_diagnostics( + s, + can_player_attack, + attack_ready_no_attack, + target_available_no_attack, + safe_attack_opportunity_missed); + s->episode_return += s->reward; +} + +static void inf_step(EncounterState* state, const int* actions) { + inf_step_ctx(state, (EncounterContext*)inf_legacy_context(), actions); +} diff --git a/ocean/osrs/osrs_anim.h b/ocean/osrs/osrs_anim.h new file mode 100644 index 0000000000..0f0b9462b5 --- /dev/null +++ b/ocean/osrs/osrs_anim.h @@ -0,0 +1,805 @@ +#ifndef OSRS_ANIM_H +#define OSRS_ANIM_H + +#include "osrs_assets.h" +#include "osrs_binary_io.h" +#include +#include +#include +#include +#include + +#define ANIM2_MAGIC 0x324D4E41 +#define ANIM_FORMAT_VERSION_MIN 2 +#define ANIM_FORMAT_VERSION_MAX 3 +#define ANIM_HEADER_SIZE_V2 24 +#define ANIM_MAX_SLOTS 256 +#define ANIM_MAX_LABELS 256 +#define ANIM_SINE_COUNT 2048 +#define ANIM_MAX_BASES 65535 +#define ANIM_MAX_SEQUENCES 65535 + +static int anim_sine[ANIM_SINE_COUNT]; +static int anim_cosine[ANIM_SINE_COUNT]; +static int anim_trig_initialized = 0; + +static void anim_init_trig(void) { + if (anim_trig_initialized) return; + for (int i = 0; i < ANIM_SINE_COUNT; i++) { + double angle = (double)i * (2.0 * 3.14159265358979323846 / ANIM_SINE_COUNT); + anim_sine[i] = (int)(65536.0 * sin(angle)); + anim_cosine[i] = (int)(65536.0 * cos(angle)); + } + anim_trig_initialized = 1; +} + +typedef struct { + uint16_t base_id; + uint8_t slot_count; + uint8_t* types; + uint8_t* map_lengths; + uint8_t** frame_maps; +} AnimFrameBase; + +typedef struct { + uint8_t slot_index; + int16_t dx, dy, dz; +} AnimTransform; + +typedef enum { + ANIM_FRAME_LEGACY = 0, + ANIM_FRAME_MAYA_BAKED = 1, +} AnimFrameKind; + +typedef struct { + uint8_t kind; + uint16_t framebase_id; + uint8_t transform_count; + AnimTransform* transforms; + uint16_t maya_vertex_count; + int16_t* maya_vertices; +} AnimFrameData; + +typedef struct { + uint16_t delay; + AnimFrameData frame; +} AnimSequenceFrame; + +typedef struct { + uint16_t seq_id; + uint16_t frame_count; + uint8_t interleave_count; + uint8_t* interleave_order; + int8_t walk_flag; + AnimSequenceFrame* frames; +} AnimSequence; + +typedef enum { + ANIM_PLAY_LOOP = 0, + ANIM_PLAY_ONCE = 1, +} AnimPlaybackMode; + +typedef struct { + int seq_id; + AnimSequence* sequence; + int model_vert_count; + int frame_idx; + int ticks_in_frame; + int completed_loops; + AnimPlaybackMode mode; +} AnimPlayback; + +static inline void anim_playback_reset(AnimPlayback* pb) { + pb->seq_id = -1; + pb->sequence = NULL; + pb->model_vert_count = -1; + pb->frame_idx = 0; + pb->ticks_in_frame = 0; + pb->completed_loops = 0; + pb->mode = ANIM_PLAY_LOOP; +} + +static inline void anim_playback_restart( + AnimPlayback* pb, int seq_id, AnimPlaybackMode mode +) { + pb->seq_id = seq_id; + pb->mode = mode; + pb->sequence = NULL; + pb->frame_idx = 0; + pb->ticks_in_frame = 0; + pb->completed_loops = 0; +} + +static inline void anim_playback_set_seq( + AnimPlayback* pb, int seq_id, AnimPlaybackMode mode +) { + if (pb->seq_id == seq_id && pb->mode == mode) return; + anim_playback_restart(pb, seq_id, mode); +} + +static inline void anim_playback_advance(AnimPlayback* pb) { + AnimSequence* seq = pb->sequence; + if (!seq || seq->frame_count <= 0) return; + int fidx = pb->frame_idx % seq->frame_count; + int delay = seq->frames[fidx].delay > 0 ? seq->frames[fidx].delay : 1; + pb->ticks_in_frame++; + if (pb->ticks_in_frame >= delay) { + pb->ticks_in_frame = 0; + int next = (fidx + 1) % seq->frame_count; + pb->frame_idx = next; + if (next == 0) pb->completed_loops++; + } +} + +typedef struct { + AnimFrameBase* bases; + int base_count; + uint16_t* base_ids; + + AnimSequence* sequences; + int seq_count; +} AnimCache; + +typedef struct { + int16_t* verts; + int vert_count; + + int** groups; + int* group_counts; + + uint8_t* base_face_alphas; + uint8_t* face_alphas; + int face_count; + int** face_alpha_groups; + int* face_alpha_group_counts; +} AnimModelState; + +typedef struct { + const uint8_t* p; + const uint8_t* end; + const char* path; +} AnimReader; + +static void anim_reader_need(AnimReader* r, size_t n) { + if ((size_t)(r->end - r->p) < n) { + fprintf(stderr, "anim_cache_load: truncated %s\n", r->path); + abort(); + } +} + +static uint8_t anim_read_u8(AnimReader* r) { + anim_reader_need(r, 1); + uint8_t v = r->p[0]; + r->p++; + return v; +} + +static int8_t anim_read_i8(AnimReader* r) { + return (int8_t)anim_read_u8(r); +} + +static uint16_t anim_read_u16(AnimReader* r) { + anim_reader_need(r, 2); + uint16_t v = (uint16_t)(r->p[0]) | ((uint16_t)(r->p[1]) << 8); + r->p += 2; + return v; +} + +static int16_t anim_read_i16(AnimReader* r) { + return (int16_t)anim_read_u16(r); +} + +static uint32_t anim_read_u32(AnimReader* r) { + anim_reader_need(r, 4); + uint32_t v = (uint32_t)(r->p[0]) + | ((uint32_t)(r->p[1]) << 8) + | ((uint32_t)(r->p[2]) << 16) + | ((uint32_t)(r->p[3]) << 24); + r->p += 4; + return v; +} + +static void anim_skip(AnimReader* r, size_t n) { + anim_reader_need(r, n); + r->p += n; +} + +static AnimCache* anim_cache_load(const char* path) { + FILE* f = osrs_asset_fopen(path, "rb"); + if (!f) { + fprintf(stderr, "anim_cache_load: cannot open %s\n", path); + return NULL; + } + + long size = osrs_file_size_or_abort(f, path); + if (size < 8) { + fprintf(stderr, "anim_cache_load: file too small: %s (%ld bytes)\n", path, size); + abort(); + } + + uint8_t* buf = (uint8_t*)osrs_malloc_or_abort((size_t)size, "animation file"); + osrs_read_exact(f, buf, 1, (size_t)size, path, "animation file"); + fclose(f); + + AnimReader r = { buf, buf + size, path }; + uint32_t magic = anim_read_u32(&r); + uint32_t sequence_frames_read = 0; + if (magic != ANIM2_MAGIC) { + fprintf(stderr, "anim_cache_load: bad magic 0x%08X in %s, expected ANM2\n", + magic, path); + abort(); + } + + uint16_t version = anim_read_u16(&r); + uint16_t header_size = anim_read_u16(&r); + if (version < ANIM_FORMAT_VERSION_MIN || + version > ANIM_FORMAT_VERSION_MAX || + header_size < ANIM_HEADER_SIZE_V2) { + fprintf(stderr, + "anim_cache_load: unsupported ANM2 header version=%u size=%u in %s\n", + version, header_size, path); + abort(); + } + uint32_t base_count = anim_read_u32(&r); + uint32_t seq_count = anim_read_u32(&r); + uint32_t declared_sequence_frames = anim_read_u32(&r); + uint32_t flags = anim_read_u32(&r); + if (header_size > ANIM_HEADER_SIZE_V2) { + anim_skip(&r, (size_t)(header_size - ANIM_HEADER_SIZE_V2)); + } + if (base_count > ANIM_MAX_BASES || seq_count > ANIM_MAX_SEQUENCES) { + fprintf(stderr, + "anim_cache_load: invalid counts bases=%u sequences=%u in %s\n", + base_count, seq_count, path); + abort(); + } + + AnimCache* cache = (AnimCache*)osrs_calloc_or_abort( + 1, sizeof(AnimCache), "animation cache"); + cache->base_count = (int)base_count; + cache->seq_count = (int)seq_count; + + cache->bases = (AnimFrameBase*)osrs_calloc_or_abort( + cache->base_count, sizeof(AnimFrameBase), "animation framebases"); + cache->base_ids = (uint16_t*)osrs_malloc_or_abort( + cache->base_count * sizeof(uint16_t), "animation framebase ids"); + + for (int i = 0; i < cache->base_count; i++) { + AnimFrameBase* fb = &cache->bases[i]; + fb->base_id = anim_read_u16(&r); + cache->base_ids[i] = fb->base_id; + fb->slot_count = anim_read_u8(&r); + + fb->types = (uint8_t*)osrs_malloc_or_abort( + fb->slot_count, "animation framebase slot types"); + for (int s = 0; s < fb->slot_count; s++) { + fb->types[s] = anim_read_u8(&r); + } + + fb->map_lengths = (uint8_t*)osrs_malloc_or_abort( + fb->slot_count, "animation framebase map lengths"); + fb->frame_maps = (uint8_t**)osrs_malloc_or_abort( + fb->slot_count * sizeof(uint8_t*), "animation frame maps"); + for (int s = 0; s < fb->slot_count; s++) { + uint8_t ml = anim_read_u8(&r); + fb->map_lengths[s] = ml; + fb->frame_maps[s] = (uint8_t*)osrs_malloc_or_abort( + ml, "animation frame map labels"); + for (int j = 0; j < ml; j++) { + fb->frame_maps[s][j] = anim_read_u8(&r); + } + } + } + + cache->sequences = (AnimSequence*)osrs_calloc_or_abort( + cache->seq_count, sizeof(AnimSequence), "animation sequences"); + for (int i = 0; i < cache->seq_count; i++) { + AnimSequence* seq = &cache->sequences[i]; + seq->seq_id = anim_read_u16(&r); + seq->frame_count = anim_read_u16(&r); + + seq->interleave_count = anim_read_u8(&r); + if (seq->interleave_count > 0) { + seq->interleave_order = (uint8_t*)osrs_malloc_or_abort( + seq->interleave_count, "animation interleave order"); + for (int j = 0; j < seq->interleave_count; j++) { + seq->interleave_order[j] = anim_read_u8(&r); + } + } + + seq->walk_flag = anim_read_i8(&r); + + seq->frames = (AnimSequenceFrame*)osrs_calloc_or_abort( + seq->frame_count, sizeof(AnimSequenceFrame), "animation sequence frames"); + for (int fi = 0; fi < seq->frame_count; fi++) { + AnimSequenceFrame* sf = &seq->frames[fi]; + sf->delay = anim_read_u16(&r); + sf->frame.kind = version >= 3 ? anim_read_u8(&r) : ANIM_FRAME_LEGACY; + sequence_frames_read++; + + if (sf->frame.kind == ANIM_FRAME_LEGACY) { + sf->frame.framebase_id = anim_read_u16(&r); + sf->frame.transform_count = anim_read_u8(&r); + if (sf->frame.transform_count > 0) { + sf->frame.transforms = (AnimTransform*)osrs_malloc_or_abort( + sf->frame.transform_count * sizeof(AnimTransform), + "animation transforms"); + for (int t = 0; t < sf->frame.transform_count; t++) { + sf->frame.transforms[t].slot_index = anim_read_u8(&r); + sf->frame.transforms[t].dx = anim_read_i16(&r); + sf->frame.transforms[t].dy = anim_read_i16(&r); + sf->frame.transforms[t].dz = anim_read_i16(&r); + } + } + } else if (sf->frame.kind == ANIM_FRAME_MAYA_BAKED) { + sf->frame.framebase_id = 0xFFFF; + sf->frame.transform_count = 0; + sf->frame.maya_vertex_count = anim_read_u16(&r); + if (sf->frame.maya_vertex_count == 0) { + fprintf(stderr, + "anim_cache_load: Maya baked frame has zero vertices in %s\n", + path); + abort(); + } + sf->frame.maya_vertices = (int16_t*)osrs_malloc_or_abort( + (size_t)sf->frame.maya_vertex_count * 3 * sizeof(int16_t), + "Maya baked animation vertices"); + for (int v = 0; v < sf->frame.maya_vertex_count * 3; v++) { + sf->frame.maya_vertices[v] = anim_read_i16(&r); + } + } else { + fprintf(stderr, + "anim_cache_load: unknown frame kind %u in sequence %u from %s\n", + sf->frame.kind, seq->seq_id, path); + abort(); + } + } + } + if (declared_sequence_frames != sequence_frames_read) { + fprintf(stderr, + "anim_cache_load: ANM2 frame count mismatch declared=%u read=%u in %s\n", + declared_sequence_frames, sequence_frames_read, path); + abort(); + } + if (r.p != r.end) { + fprintf(stderr, "anim_cache_load: ignored %ld trailing bytes in %s\n", + (long)(r.end - r.p), path); + } + + free(buf); + anim_init_trig(); + + fprintf(stderr, + "anim_cache_load: loaded ANM%d flags=0x%08X %d framebases, " + "%d sequences, %u sequence frames from %s\n", + version, flags, cache->base_count, cache->seq_count, + sequence_frames_read, path); + return cache; +} + +static AnimSequence* anim_get_sequence(AnimCache* cache, uint16_t seq_id) { + if (!cache) return NULL; + for (int i = 0; i < cache->seq_count; i++) { + if (cache->sequences[i].seq_id == seq_id) { + return &cache->sequences[i]; + } + } + return NULL; +} + +static AnimFrameBase* anim_get_framebase(AnimCache* cache, uint16_t base_id) { + if (!cache) return NULL; + for (int i = 0; i < cache->base_count; i++) { + if (cache->bases[i].base_id == base_id) { + return &cache->bases[i]; + } + } + return NULL; +} + +static AnimModelState* anim_model_state_create_with_face_alpha( + const uint8_t* vertex_skins, + int base_vert_count, + const uint8_t* face_alpha_labels, + const uint8_t* base_face_alphas, + int face_count +) { + AnimModelState* state = (AnimModelState*)osrs_calloc_or_abort( + 1, sizeof(AnimModelState), "animation model state"); + state->vert_count = base_vert_count; + state->verts = (int16_t*)osrs_calloc_or_abort( + base_vert_count * 3, sizeof(int16_t), "animation model vertices"); + + state->groups = (int**)osrs_calloc_or_abort( + ANIM_MAX_LABELS, sizeof(int*), "animation model groups"); + state->group_counts = (int*)osrs_calloc_or_abort( + ANIM_MAX_LABELS, sizeof(int), "animation model group counts"); + + int label_counts[ANIM_MAX_LABELS] = {0}; + for (int v = 0; v < base_vert_count; v++) { + uint8_t label = vertex_skins[v]; + label_counts[label]++; + } + + for (int l = 0; l < ANIM_MAX_LABELS; l++) { + if (label_counts[l] > 0) { + state->groups[l] = (int*)osrs_malloc_or_abort( + label_counts[l] * sizeof(int), "animation model group vertices"); + state->group_counts[l] = 0; + } + } + + for (int v = 0; v < base_vert_count; v++) { + uint8_t label = vertex_skins[v]; + state->groups[label][state->group_counts[label]++] = v; + } + + if (face_count > 0 && face_alpha_labels && base_face_alphas) { + state->face_count = face_count; + state->base_face_alphas = (uint8_t*)osrs_malloc_or_abort( + face_count, "animation model base face alphas"); + state->face_alphas = (uint8_t*)osrs_malloc_or_abort( + face_count, "animation model face alphas"); + memcpy(state->base_face_alphas, base_face_alphas, face_count); + memcpy(state->face_alphas, base_face_alphas, face_count); + + state->face_alpha_groups = (int**)osrs_calloc_or_abort( + ANIM_MAX_LABELS, sizeof(int*), "animation face alpha groups"); + state->face_alpha_group_counts = (int*)osrs_calloc_or_abort( + ANIM_MAX_LABELS, sizeof(int), "animation face alpha group counts"); + + int face_label_counts[ANIM_MAX_LABELS] = {0}; + for (int face = 0; face < face_count; face++) { + uint8_t label = face_alpha_labels[face]; + if (label == 255) continue; + face_label_counts[label]++; + } + for (int label = 0; label < ANIM_MAX_LABELS; label++) { + if (face_label_counts[label] > 0) { + state->face_alpha_groups[label] = (int*)osrs_malloc_or_abort( + face_label_counts[label] * sizeof(int), + "animation face alpha group faces"); + state->face_alpha_group_counts[label] = 0; + } + } + for (int face = 0; face < face_count; face++) { + uint8_t label = face_alpha_labels[face]; + if (label == 255) continue; + state->face_alpha_groups[label][ + state->face_alpha_group_counts[label]++] = face; + } + } + + return state; +} + +static AnimModelState* anim_model_state_create( + const uint8_t* vertex_skins, + int base_vert_count +) { + return anim_model_state_create_with_face_alpha( + vertex_skins, base_vert_count, NULL, NULL, 0); +} + +static void anim_model_state_free(AnimModelState* state) { + if (!state) return; + free(state->verts); + for (int l = 0; l < ANIM_MAX_LABELS; l++) { + free(state->groups[l]); + } + free(state->groups); + free(state->group_counts); + if (state->face_alpha_groups) { + for (int l = 0; l < ANIM_MAX_LABELS; l++) { + free(state->face_alpha_groups[l]); + } + } + free(state->face_alpha_groups); + free(state->face_alpha_group_counts); + free(state->base_face_alphas); + free(state->face_alphas); + free(state); +} + +static void anim_apply_rest_pose( + AnimModelState* state, + const int16_t* base_verts_src +) { + memcpy(state->verts, base_verts_src, state->vert_count * 3 * sizeof(int16_t)); + if (state->face_alphas && state->base_face_alphas) { + memcpy(state->face_alphas, state->base_face_alphas, state->face_count); + } +} + +static int anim_clamp_alpha(int alpha) { + if (alpha < 0) return 0; + if (alpha > 255) return 255; + return alpha; +} + +static void anim_apply_alpha_transform( + AnimModelState* state, + const uint8_t* labels, + uint8_t map_len, + int dx +) { + if (!state->face_alphas || !state->face_alpha_groups || + !state->face_alpha_group_counts) { + return; + } + + int delta = dx * 8; + for (int m = 0; m < map_len; m++) { + uint8_t label = labels[m]; + for (int fi = 0; fi < state->face_alpha_group_counts[label]; fi++) { + int face = state->face_alpha_groups[label][fi]; + state->face_alphas[face] = (uint8_t)anim_clamp_alpha( + (int)state->face_alphas[face] + delta); + } + } +} + +static void anim_apply_single_transform( + AnimModelState* state, + int type, const uint8_t* labels, uint8_t map_len, + int dx, int dy, int dz, + int* pivot_x, int* pivot_y, int* pivot_z); + +static void anim_apply_frame( + AnimModelState* state, + const int16_t* base_verts_src, + const AnimFrameData* frame, + const AnimFrameBase* fb +) { + if (frame->kind != ANIM_FRAME_LEGACY) { + fprintf(stderr, "anim_apply_frame: non-legacy frame passed to legacy path\n"); + abort(); + } + anim_apply_rest_pose(state, base_verts_src); + + int pivot_x = 0, pivot_y = 0, pivot_z = 0; + + for (int t = 0; t < frame->transform_count; t++) { + uint8_t slot_idx = frame->transforms[t].slot_index; + if (slot_idx >= fb->slot_count) continue; + + anim_apply_single_transform( + state, fb->types[slot_idx], + fb->frame_maps[slot_idx], fb->map_lengths[slot_idx], + frame->transforms[t].dx, + frame->transforms[t].dy, + frame->transforms[t].dz, + &pivot_x, &pivot_y, &pivot_z); + } +} + +static void anim_apply_maya_baked_frame( + AnimModelState* state, + const AnimFrameData* frame +) { + if (frame->kind != ANIM_FRAME_MAYA_BAKED) { + fprintf(stderr, "anim_apply_maya_baked_frame: non-Maya frame passed\n"); + abort(); + } + if ((int)frame->maya_vertex_count != state->vert_count) { + + return; + } + memcpy(state->verts, frame->maya_vertices, + (size_t)state->vert_count * 3 * sizeof(int16_t)); + if (state->face_alphas && state->base_face_alphas) { + memcpy(state->face_alphas, state->base_face_alphas, state->face_count); + } +} + +static void anim_apply_single_transform( + AnimModelState* state, + int type, const uint8_t* labels, uint8_t map_len, + int dx, int dy, int dz, + int* pivot_x, int* pivot_y, int* pivot_z +) { + if (type == 0) { + int count = 0, sx = 0, sy = 0, sz = 0; + for (int m = 0; m < map_len; m++) { + uint8_t label = labels[m]; + for (int vi = 0; vi < state->group_counts[label]; vi++) { + int v = state->groups[label][vi]; + sx += state->verts[v * 3]; + sy += state->verts[v * 3 + 1]; + sz += state->verts[v * 3 + 2]; + count++; + } + } + if (count > 0) { + *pivot_x = sx / count + dx; + *pivot_y = sy / count + dy; + *pivot_z = sz / count + dz; + } else { + *pivot_x = dx; + *pivot_y = dy; + *pivot_z = dz; + } + } else if (type == 1) { + for (int m = 0; m < map_len; m++) { + uint8_t label = labels[m]; + for (int vi = 0; vi < state->group_counts[label]; vi++) { + int v = state->groups[label][vi]; + state->verts[v * 3] += (int16_t)dx; + state->verts[v * 3 + 1] += (int16_t)dy; + state->verts[v * 3 + 2] += (int16_t)dz; + } + } + } else if (type == 2) { + int ax = (dx & 0xFF) * 8, ay = (dy & 0xFF) * 8, az = (dz & 0xFF) * 8; + int sin_x = anim_sine[ax & 2047], cos_x = anim_cosine[ax & 2047]; + int sin_y = anim_sine[ay & 2047], cos_y = anim_cosine[ay & 2047]; + int sin_z = anim_sine[az & 2047], cos_z = anim_cosine[az & 2047]; + for (int m = 0; m < map_len; m++) { + uint8_t label = labels[m]; + for (int vi = 0; vi < state->group_counts[label]; vi++) { + int v = state->groups[label][vi]; + int vx = state->verts[v * 3] - *pivot_x; + int vy = state->verts[v * 3 + 1] - *pivot_y; + int vz = state->verts[v * 3 + 2] - *pivot_z; + int rx = (vx * cos_z + vy * sin_z) >> 16; + int ry = (vy * cos_z - vx * sin_z) >> 16; + vx = rx; vy = ry; + ry = (vy * cos_x - vz * sin_x) >> 16; + int rz = (vy * sin_x + vz * cos_x) >> 16; + vy = ry; vz = rz; + rx = (vx * cos_y + vz * sin_y) >> 16; + rz = (vz * cos_y - vx * sin_y) >> 16; + state->verts[v * 3] = (int16_t)(rx + *pivot_x); + state->verts[v * 3 + 1] = (int16_t)(vy + *pivot_y); + state->verts[v * 3 + 2] = (int16_t)(rz + *pivot_z); + } + } + } else if (type == 3) { + for (int m = 0; m < map_len; m++) { + uint8_t label = labels[m]; + for (int vi = 0; vi < state->group_counts[label]; vi++) { + int v = state->groups[label][vi]; + int vx = state->verts[v * 3] - *pivot_x; + int vy = state->verts[v * 3 + 1] - *pivot_y; + int vz = state->verts[v * 3 + 2] - *pivot_z; + state->verts[v * 3] = (int16_t)((vx * dx) / 128 + *pivot_x); + state->verts[v * 3 + 1] = (int16_t)((vy * dy) / 128 + *pivot_y); + state->verts[v * 3 + 2] = (int16_t)((vz * dz) / 128 + *pivot_z); + } + } + } else if (type == 5) { + anim_apply_alpha_transform(state, labels, map_len, dx); + } +} + +static void anim_apply_frame_interleaved( + AnimModelState* state, + const int16_t* base_verts_src, + const AnimFrameData* secondary_frame, const AnimFrameBase* secondary_fb, + const AnimFrameData* primary_frame, const AnimFrameBase* primary_fb, + const uint8_t* interleave_order, int interleave_count +) { + if (secondary_frame->kind != ANIM_FRAME_LEGACY || + primary_frame->kind != ANIM_FRAME_LEGACY) { + fprintf(stderr, "anim_apply_frame_interleaved: Maya frames cannot be interleaved\n"); + abort(); + } + anim_apply_rest_pose(state, base_verts_src); + + uint8_t secondary_slot[256]; + memset(secondary_slot, 0, sizeof(secondary_slot)); + for (int i = 0; i < interleave_count; i++) { + secondary_slot[interleave_order[i]] = 1; + } + + int pivot_x = 0, pivot_y = 0, pivot_z = 0; + for (int t = 0; t < primary_frame->transform_count; t++) { + uint8_t slot_idx = primary_frame->transforms[t].slot_index; + if (slot_idx >= primary_fb->slot_count) continue; + + int type = primary_fb->types[slot_idx]; + int in_interleave = secondary_slot[slot_idx]; + + if (!in_interleave || type == 0) { + anim_apply_single_transform( + state, type, + primary_fb->frame_maps[slot_idx], + primary_fb->map_lengths[slot_idx], + primary_frame->transforms[t].dx, + primary_frame->transforms[t].dy, + primary_frame->transforms[t].dz, + &pivot_x, &pivot_y, &pivot_z); + } + } + + pivot_x = 0; pivot_y = 0; pivot_z = 0; + for (int t = 0; t < secondary_frame->transform_count; t++) { + uint8_t slot_idx = secondary_frame->transforms[t].slot_index; + if (slot_idx >= secondary_fb->slot_count) continue; + + int type = secondary_fb->types[slot_idx]; + int in_interleave = secondary_slot[slot_idx]; + + if (in_interleave || type == 0) { + anim_apply_single_transform( + state, type, + secondary_fb->frame_maps[slot_idx], + secondary_fb->map_lengths[slot_idx], + secondary_frame->transforms[t].dx, + secondary_frame->transforms[t].dy, + secondary_frame->transforms[t].dz, + &pivot_x, &pivot_y, &pivot_z); + } + } +} + +static void anim_update_mesh( + float* mesh_vertices, + const AnimModelState* state, + const uint16_t* face_indices, + int face_count +) { + for (int fi = 0; fi < face_count; fi++) { + int a = face_indices[fi * 3]; + int b = face_indices[fi * 3 + 1]; + int c = face_indices[fi * 3 + 2]; + + int vi = fi * 9; + mesh_vertices[vi] = (float)state->verts[a * 3]; + mesh_vertices[vi + 1] = (float)(-state->verts[a * 3 + 1]); + mesh_vertices[vi + 2] = (float)state->verts[a * 3 + 2]; + + mesh_vertices[vi + 3] = (float)state->verts[b * 3]; + mesh_vertices[vi + 4] = (float)(-state->verts[b * 3 + 1]); + mesh_vertices[vi + 5] = (float)state->verts[b * 3 + 2]; + + mesh_vertices[vi + 6] = (float)state->verts[c * 3]; + mesh_vertices[vi + 7] = (float)(-state->verts[c * 3 + 1]); + mesh_vertices[vi + 8] = (float)state->verts[c * 3 + 2]; + } +} + +static void anim_update_mesh_alpha( + unsigned char* mesh_colors, + const AnimModelState* state, + int face_count +) { + if (!mesh_colors || !state || !state->face_alphas) return; + int count = face_count < state->face_count ? face_count : state->face_count; + for (int fi = 0; fi < count; fi++) { + unsigned char alpha = (unsigned char)(255 - state->face_alphas[fi]); + for (int corner = 0; corner < 3; corner++) { + mesh_colors[(fi * 3 + corner) * 4 + 3] = alpha; + } + } +} + +static void anim_cache_free(AnimCache* cache) { + if (!cache) return; + + for (int i = 0; i < cache->base_count; i++) { + AnimFrameBase* fb = &cache->bases[i]; + free(fb->types); + free(fb->map_lengths); + for (int s = 0; s < fb->slot_count; s++) { + free(fb->frame_maps[s]); + } + free(fb->frame_maps); + } + free(cache->bases); + free(cache->base_ids); + + for (int i = 0; i < cache->seq_count; i++) { + AnimSequence* seq = &cache->sequences[i]; + free(seq->interleave_order); + for (int fi = 0; fi < seq->frame_count; fi++) { + free(seq->frames[fi].frame.transforms); + free(seq->frames[fi].frame.maya_vertices); + } + free(seq->frames); + } + free(cache->sequences); + free(cache); +} + +#endif diff --git a/ocean/osrs/osrs_asset_raylib.h b/ocean/osrs/osrs_asset_raylib.h new file mode 100644 index 0000000000..1480a96918 --- /dev/null +++ b/ocean/osrs/osrs_asset_raylib.h @@ -0,0 +1,67 @@ +#ifndef OSRS_ASSET_RAYLIB_H +#define OSRS_ASSET_RAYLIB_H + +#include "osrs_assets.h" + +#if __has_include("raylib.h") +#include "raylib.h" +#elif __has_include("raylib-5.5_macos/include/raylib.h") +#include "raylib-5.5_macos/include/raylib.h" +#else +#error "raylib.h not found" +#endif + +#include +#include +#include +#include + +static inline const char* osrs_asset_ext(const char* path, const char* fallback) { + const char* dot = path ? strrchr(path, '.') : NULL; + return dot && dot[0] ? dot : fallback; +} + +static inline Image osrs_asset_load_image(const char* path) { + Image empty = {0}; + OsrsAssetBytes bytes = osrs_asset_read_all(path); + if (!bytes.data || bytes.size == 0) { + osrs_asset_bytes_free(&bytes); + return empty; + } + if (bytes.size > (size_t)INT_MAX) { + fprintf(stderr, "image asset too large: %s (%zu bytes)\n", path, bytes.size); + abort(); + } + Image image = LoadImageFromMemory( + osrs_asset_ext(path, ".png"), bytes.data, (int)bytes.size); + osrs_asset_bytes_free(&bytes); + return image; +} + +static inline Texture2D osrs_asset_load_texture(const char* path) { + Texture2D empty = {0}; + Image image = osrs_asset_load_image(path); + if (!image.data) return empty; + Texture2D texture = LoadTextureFromImage(image); + UnloadImage(image); + return texture; +} + +static inline Font osrs_asset_load_font(const char* path, int font_size) { + Font empty = {0}; + OsrsAssetBytes bytes = osrs_asset_read_all(path); + if (!bytes.data || bytes.size == 0) { + osrs_asset_bytes_free(&bytes); + return empty; + } + if (bytes.size > (size_t)INT_MAX) { + fprintf(stderr, "font asset too large: %s (%zu bytes)\n", path, bytes.size); + abort(); + } + Font font = LoadFontFromMemory( + osrs_asset_ext(path, ".ttf"), bytes.data, (int)bytes.size, font_size, NULL, 95); + osrs_asset_bytes_free(&bytes); + return font.texture.id != 0 ? font : empty; +} + +#endif diff --git a/ocean/osrs/osrs_assets.h b/ocean/osrs/osrs_assets.h new file mode 100644 index 0000000000..00da9508d0 --- /dev/null +++ b/ocean/osrs/osrs_assets.h @@ -0,0 +1,180 @@ +#ifndef OSRS_ASSETS_H +#define OSRS_ASSETS_H + +#include +#include +#include + +#include "osrs_assets_generated.h" +#include "osrs_binary_io.h" + +#define OSRS_ASSET_ROOT_DEFAULT "ocean/osrs/data" +#define OSRS_ASSET(path) osrs_asset_path(path) + +typedef struct { + unsigned char* data; + size_t size; +} OsrsAssetBytes; + +static inline const char* osrs_asset_root(void) { + const char* root = getenv("OSRS_ASSET_ROOT"); + return root && root[0] ? root : OSRS_ASSET_ROOT_DEFAULT; +} + +static inline int osrs_asset_has_prefix(const char* s, const char* prefix) { + size_t n = strlen(prefix); + return strncmp(s, prefix, n) == 0; +} + +static inline int osrs_asset_is_absolute_path(const char* path) { + return path && path[0] == '/'; +} + +static inline int osrs_asset_manifest_path_is_valid(const char* path) { + if (!path || !path[0]) return 0; + if (osrs_asset_is_absolute_path(path)) return 0; + if (strchr(path, '\\')) return 0; + + const char* part = path; + while (*part) { + const char* slash = strchr(part, '/'); + size_t len = slash ? (size_t)(slash - part) : strlen(part); + if (len == 0) return 0; + if (len == 1 && part[0] == '.') return 0; + if (len == 2 && part[0] == '.' && part[1] == '.') return 0; + if (!slash) return 1; + part = slash + 1; + } + return 0; +} + +static inline const char* osrs_asset_logical_path(const char* path) { + if (!path) return ""; + while (osrs_asset_has_prefix(path, "./")) path += 2; + const char* root = osrs_asset_root(); + size_t root_len = strlen(root); + if (root_len > 0 && strncmp(path, root, root_len) == 0 && + (path[root_len] == '/' || path[root_len] == '\0')) { + path += root_len; + if (path[0] == '/') path++; + } + if (osrs_asset_has_prefix(path, "data/")) path += 5; + const char* data_part = strstr(path, "/data/"); + if (data_part) path = data_part + 6; + return path; +} + +static inline const char* osrs_asset_path(const char* path) { + enum { OSRS_ASSET_PATH_RING = 64, OSRS_ASSET_PATH_MAX = 2048 }; + static char paths[OSRS_ASSET_PATH_RING][OSRS_ASSET_PATH_MAX]; + static int path_idx = 0; + + if (!path) { + fprintf(stderr, "osrs_asset_path: path is null\n"); + abort(); + } + + int idx = path_idx++ % OSRS_ASSET_PATH_RING; + if (osrs_asset_is_absolute_path(path)) { + int n = snprintf(paths[idx], sizeof(paths[idx]), "%s", path); + if (n < 0 || (size_t)n >= sizeof(paths[idx])) { + fprintf(stderr, "osrs_asset_path: absolute path too long: %s\n", path); + abort(); + } + return paths[idx]; + } + + const char* logical = osrs_asset_logical_path(path); + int n = snprintf(paths[idx], sizeof(paths[idx]), "%s/%s", + osrs_asset_root(), logical); + if (n < 0 || (size_t)n >= sizeof(paths[idx])) { + fprintf(stderr, "osrs_asset_path: path too long under %s: %s\n", + osrs_asset_root(), logical); + abort(); + } + return paths[idx]; +} + +static inline FILE* osrs_asset_fopen(const char* path, const char* mode) { + return fopen(osrs_asset_path(path), mode); +} + +static inline int osrs_asset_exists(const char* path) { + FILE* f = osrs_asset_fopen(path, "rb"); + if (!f) return 0; + fclose(f); + return 1; +} + +static inline void osrs_asset_require_group(OsrsAssetGroupKind kind) { + if ((size_t)kind >= OSRS_ASSET_GROUP_COUNT) { + fprintf(stderr, "osrs_asset_require_group: bad group: %d\n", (int)kind); + abort(); + } + const OsrsAssetGroup* group = &OSRS_ASSET_GROUPS[kind]; + + size_t invalid_count = 0; + size_t missing_count = 0; + for (size_t i = 0; i < group->path_count; i++) { + const char* path = group->paths[i]; + if (!osrs_asset_manifest_path_is_valid(path)) { + if (invalid_count == 0) { + fprintf(stderr, "OSRS asset group '%s' has invalid manifest paths:\n", + group->name); + } + fprintf(stderr, " %s\n", path ? path : "(null)"); + invalid_count++; + continue; + } + if (!osrs_asset_exists(path)) { + if (missing_count == 0) { + fprintf(stderr, "OSRS asset group '%s' is missing files under %s:\n", + group->name, osrs_asset_root()); + } + fprintf(stderr, " %s\n", osrs_asset_path(path)); + missing_count++; + } + } + + if (invalid_count || missing_count) { + fprintf(stderr, + "OSRS asset group '%s' failed validation: %zu invalid, %zu missing\n", + group->name, invalid_count, missing_count); + abort(); + } +} + +static inline OsrsAssetBytes osrs_asset_read_all(const char* path) { + OsrsAssetBytes out = {0}; + const char* full_path = osrs_asset_path(path); + FILE* f = osrs_asset_fopen(path, "rb"); + if (!f) return out; + long size = osrs_file_size_or_abort(f, full_path); + if (size == 0) { + fclose(f); + return out; + } + + out.data = (unsigned char*)malloc((size_t)size); + if (!out.data) { + fprintf(stderr, "%s: malloc failed for %ld bytes\n", full_path, size); + abort(); + } + out.size = (size_t)size; + size_t got = fread(out.data, 1, out.size, f); + if (got != out.size) { + fprintf(stderr, "%s: short read (%zu/%zu)\n", full_path, got, out.size); + abort(); + } + fclose(f); + return out; +} + +static inline void osrs_asset_bytes_free(OsrsAssetBytes* bytes) { + if (!bytes) return; + free(bytes->data); + bytes->data = NULL; + bytes->size = 0; +} + +#endif diff --git a/ocean/osrs/osrs_assets_generated.h b/ocean/osrs/osrs_assets_generated.h new file mode 100644 index 0000000000..f0aa4ae2b9 --- /dev/null +++ b/ocean/osrs/osrs_assets_generated.h @@ -0,0 +1,230 @@ +/* Generated by ocean/osrs/scripts/osrs_asset_manifest.py. */ +#ifndef OSRS_ASSETS_GENERATED_H +#define OSRS_ASSETS_GENERATED_H + +#include + +typedef enum { + OSRS_ASSET_GROUP_CORE = 0, + OSRS_ASSET_GROUP_INFERNO = 1, + OSRS_ASSET_GROUP_ZULRAH = 2, + OSRS_ASSET_GROUP_COLOSSEUM = 3, + OSRS_ASSET_GROUP_GUI = 4, + OSRS_ASSET_GROUP_ITEMS = 5, + OSRS_ASSET_GROUP_HEADERS = 6, + OSRS_ASSET_GROUP_COMBAT_VISUALS = 7, + OSRS_ASSET_GROUP_WILDERNESS = 8, + OSRS_ASSET_GROUP_PVP = 9, + OSRS_ASSET_GROUP_COUNT, +} OsrsAssetGroupKind; + +typedef struct { + const char* name; + const char* const* paths; + size_t path_count; +} OsrsAssetGroup; + +static const char* const OSRS_ASSET_CORE_PATHS[] = { + "equipment.models", + "equipment.anims", + "equipment.atlas", + "equipment.tanim", + "projectiles.models", + "projectiles.atlas", + "projectiles.anims", + "spotanims.bin", + "fonts/runescape.ttf", + "fonts/runescape_small.ttf", + "ui/interfaces.bin", + "ui/interface_manifest.json", +}; + +static const char* const OSRS_ASSET_INFERNO_PATHS[] = { + "inferno.models", + "inferno.anims", + "inferno.terrain", + "inferno.objects", + "inferno.atlas", + "inferno_zuk.objects", + "inferno.cmap", + "inferno_npcs.models", + "inferno_npcs.anims", + "inferno_npcs.atlas", +}; + +static const char* const OSRS_ASSET_ZULRAH_PATHS[] = { + "zulrah.models", + "zulrah.anims", + "zulrah.terrain", + "zulrah.objects", + "zulrah.atlas", + "zulrah.cmap", +}; + +static const char* const OSRS_ASSET_COLOSSEUM_PATHS[] = { + "colosseum.terrain", + "colosseum.objects", + "colosseum.atlas", + "colosseum.cmap", + "colosseum_npcs.models", + "colosseum_npcs.anims", + "colosseum_npcs.atlas", + "sprites/colosseum/modifiers/5534.png", + "sprites/colosseum/modifiers/5535.png", + "sprites/colosseum/modifiers/5536.png", + "sprites/colosseum/modifiers/5537.png", + "sprites/colosseum/modifiers/5538.png", + "sprites/colosseum/modifiers/5539.png", + "sprites/colosseum/modifiers/5540.png", + "sprites/colosseum/modifiers/5541.png", + "sprites/colosseum/modifiers/5542.png", + "sprites/colosseum/modifiers/5543.png", + "sprites/colosseum/modifiers/5544.png", + "sprites/colosseum/modifiers/5545.png", + "sprites/colosseum/modifiers/5546.png", + "sprites/colosseum/modifiers/5547.png", + "sprites/colosseum/modifiers/5549.png", + "sprites/colosseum/modifiers/5550.png", + "sprites/colosseum/modifiers/5551.png", + "sprites/colosseum/modifiers/5552.png", + "sprites/colosseum/modifiers/5553.png", + "sprites/colosseum/modifiers/5554.png", + "sprites/colosseum/modifiers/5556.png", + "sprites/colosseum/modifiers/5558.png", + "sprites/colosseum/modifiers/5559.png", + "sprites/colosseum/modifiers/5562.png", + "sprites/colosseum/modifiers/5564.png", + "sprites/colosseum/modifiers/5565.png", + "sprites/colosseum/modifiers/5566.png", + "sprites/colosseum/modifiers/5567.png", + "sprites/colosseum/modifiers/5568.png", + "sprites/colosseum/modifiers/5569.png", + "sprites/colosseum/modifiers/5571.png", + "sprites/colosseum/modifiers/5573.png", + "sprites/colosseum/modifiers/5574.png", + "sprites/colosseum/modifiers/5577.png", +}; + +static const char* const OSRS_ASSET_GUI_PATHS[] = { + "sprites/gui/compass.png", + "sprites/gui/side_panel_bg.png", + "sprites/gui/orb_frame.png", + "sprites/gui/headicons_prayer_0.png", + "sprites/gui/hitmarks_0.png", + "sprites/gui/cross_yellow_1.png", + "sprites/gui/side_icon_inventory.png", + "sprites/gui/tradebacking_dark.png", + "sprites/gui/osrs_stretch_side_topbottom_0.png", + "sprites/gui/wornicons_11.png", + "sprites/gui/skill_icon_23.png", + "sprites/gui/prayeron_24.png", + "sprites/gui/magicon_47.png", + "sprites/gui/standard_spell_on_79.png", + "sprites/gui/minimap_alpha_mask.png", + "sprites/gui/prayerglow_0.png", +}; + +static const char* const OSRS_ASSET_ITEMS_PATHS[] = { + "sprites/items/30070.png", + "sprites/items/28945.png", + "sprites/items/27641.png", + "sprites/items/item_stack_variants.tsv", +}; + +static const char* const OSRS_ASSET_HEADERS_PATHS[] = { + "item_models.h", + "player_models.h", + "npc_models.h", + "npc_models_inferno.h", + "npc_models_zulrah.h", + "npc_models_colosseum.h", +}; + +static const char* const OSRS_ASSET_COMBAT_VISUALS_PATHS[] = { + "projectiles.models", + "projectiles.atlas", + "projectiles.anims", + "spotanims.bin", + "equipment.anims", + "inferno.anims", + "zulrah.anims", +}; + +static const char* const OSRS_ASSET_WILDERNESS_PATHS[] = { + "wilderness.cmap", + "wilderness.terrain", + "wilderness.objects", + "wilderness.atlas", +}; + +static const char* const OSRS_ASSET_PVP_PATHS[] = { + "equipment.models", + "equipment.anims", + "equipment.atlas", + "equipment.tanim", + "projectiles.models", + "projectiles.atlas", + "projectiles.anims", + "spotanims.bin", + "ui/interfaces.bin", + "ui/interface_manifest.json", + "wilderness.cmap", + "wilderness.terrain", + "wilderness.objects", + "wilderness.atlas", +}; + +static const OsrsAssetGroup OSRS_ASSET_GROUPS[OSRS_ASSET_GROUP_COUNT] = { + [OSRS_ASSET_GROUP_CORE] = { + .name = "core", + .paths = OSRS_ASSET_CORE_PATHS, + .path_count = sizeof(OSRS_ASSET_CORE_PATHS) / sizeof(OSRS_ASSET_CORE_PATHS[0]), + }, + [OSRS_ASSET_GROUP_INFERNO] = { + .name = "inferno", + .paths = OSRS_ASSET_INFERNO_PATHS, + .path_count = sizeof(OSRS_ASSET_INFERNO_PATHS) / sizeof(OSRS_ASSET_INFERNO_PATHS[0]), + }, + [OSRS_ASSET_GROUP_ZULRAH] = { + .name = "zulrah", + .paths = OSRS_ASSET_ZULRAH_PATHS, + .path_count = sizeof(OSRS_ASSET_ZULRAH_PATHS) / sizeof(OSRS_ASSET_ZULRAH_PATHS[0]), + }, + [OSRS_ASSET_GROUP_COLOSSEUM] = { + .name = "colosseum", + .paths = OSRS_ASSET_COLOSSEUM_PATHS, + .path_count = sizeof(OSRS_ASSET_COLOSSEUM_PATHS) / sizeof(OSRS_ASSET_COLOSSEUM_PATHS[0]), + }, + [OSRS_ASSET_GROUP_GUI] = { + .name = "gui", + .paths = OSRS_ASSET_GUI_PATHS, + .path_count = sizeof(OSRS_ASSET_GUI_PATHS) / sizeof(OSRS_ASSET_GUI_PATHS[0]), + }, + [OSRS_ASSET_GROUP_ITEMS] = { + .name = "items", + .paths = OSRS_ASSET_ITEMS_PATHS, + .path_count = sizeof(OSRS_ASSET_ITEMS_PATHS) / sizeof(OSRS_ASSET_ITEMS_PATHS[0]), + }, + [OSRS_ASSET_GROUP_HEADERS] = { + .name = "headers", + .paths = OSRS_ASSET_HEADERS_PATHS, + .path_count = sizeof(OSRS_ASSET_HEADERS_PATHS) / sizeof(OSRS_ASSET_HEADERS_PATHS[0]), + }, + [OSRS_ASSET_GROUP_COMBAT_VISUALS] = { + .name = "combat_visuals", + .paths = OSRS_ASSET_COMBAT_VISUALS_PATHS, + .path_count = sizeof(OSRS_ASSET_COMBAT_VISUALS_PATHS) / sizeof(OSRS_ASSET_COMBAT_VISUALS_PATHS[0]), + }, + [OSRS_ASSET_GROUP_WILDERNESS] = { + .name = "wilderness", + .paths = OSRS_ASSET_WILDERNESS_PATHS, + .path_count = sizeof(OSRS_ASSET_WILDERNESS_PATHS) / sizeof(OSRS_ASSET_WILDERNESS_PATHS[0]), + }, + [OSRS_ASSET_GROUP_PVP] = { + .name = "pvp", + .paths = OSRS_ASSET_PVP_PATHS, + .path_count = sizeof(OSRS_ASSET_PVP_PATHS) / sizeof(OSRS_ASSET_PVP_PATHS[0]), + }, +}; + +#endif diff --git a/ocean/osrs/osrs_binary_io.h b/ocean/osrs/osrs_binary_io.h new file mode 100644 index 0000000000..04f7eb9f07 --- /dev/null +++ b/ocean/osrs/osrs_binary_io.h @@ -0,0 +1,70 @@ +#ifndef OSRS_BINARY_IO_H +#define OSRS_BINARY_IO_H + +#include +#include +#include + +static inline void* osrs_malloc_or_abort(size_t size, const char* label) { + if (size == 0) return NULL; + void* ptr = malloc(size); + if (!ptr) { + fprintf(stderr, "%s: malloc failed for %zu bytes\n", label, size); + abort(); + } + return ptr; +} + +static inline void* osrs_calloc_or_abort(size_t count, size_t size, const char* label) { + if (count == 0 || size == 0) return NULL; + if (count > SIZE_MAX / size) { + fprintf(stderr, "%s: allocation overflow for %zu * %zu bytes\n", + label, count, size); + abort(); + } + void* ptr = calloc(count, size); + if (!ptr) { + fprintf(stderr, "%s: calloc failed for %zu * %zu bytes\n", + label, count, size); + abort(); + } + return ptr; +} + +static inline void osrs_read_exact( + FILE* f, void* dst, size_t size, size_t count, const char* path, const char* field +) { + if (count == 0) return; + size_t got = fread(dst, size, count, f); + if (got != count) { + fprintf(stderr, "%s: short read for %s (%zu/%zu)\n", + path, field, got, count); + abort(); + } +} + +static inline void osrs_seek_or_abort(FILE* f, long offset, const char* path) { + if (fseek(f, offset, SEEK_SET) != 0) { + fprintf(stderr, "%s: fseek failed at offset %ld\n", path, offset); + abort(); + } +} + +static inline long osrs_file_size_or_abort(FILE* f, const char* path) { + if (fseek(f, 0, SEEK_END) != 0) { + fprintf(stderr, "%s: fseek end failed\n", path); + abort(); + } + long size = ftell(f); + if (size < 0) { + fprintf(stderr, "%s: ftell failed\n", path); + abort(); + } + if (fseek(f, 0, SEEK_SET) != 0) { + fprintf(stderr, "%s: fseek start failed\n", path); + abort(); + } + return size; +} + +#endif diff --git a/ocean/osrs/osrs_bolt_procs.h b/ocean/osrs/osrs_bolt_procs.h new file mode 100644 index 0000000000..398659a133 --- /dev/null +++ b/ocean/osrs/osrs_bolt_procs.h @@ -0,0 +1,60 @@ +#ifndef OSRS_BOLT_PROCS_H +#define OSRS_BOLT_PROCS_H + +#include "osrs_combat.h" +#include "osrs_items.h" + +typedef struct { + int proc_triggered; + int modified_damage; +} BoltProcResult; + +static inline BoltProcResult osrs_resolve_bolt_proc( + int bolt_item_idx, int base_damage, int hit_accurate, + int max_hit, int ranged_level, int target_current_hp, + int is_zcb_spec, uint32_t* rng_state +) { + BoltProcResult r = { 0, base_damage }; + + switch (bolt_item_idx) { + + case ITEM_DIAMOND_BOLTS_E: + case ITEM_DIAMOND_DRAGON_BOLTS_E: { + if (!hit_accurate && !is_zcb_spec) break; + if (is_zcb_spec || encounter_roll_ratio_u16(rng_state, 11, 100)) { + int effect_max = max_hit * (is_zcb_spec ? 126 : 115) / 100; + r.proc_triggered = 1; + r.modified_damage = encounter_rand_int(rng_state, effect_max + 1); + } + break; + } + + case ITEM_OPAL_DRAGON_BOLTS: { + if (is_zcb_spec || encounter_roll_ratio_u16(rng_state, 11, 200)) { + int bonus = ranged_level / (is_zcb_spec ? 9 : 10); + r.proc_triggered = 1; + r.modified_damage = base_damage + bonus; + } + break; + } + + case ITEM_RUBY_DRAGON_BOLTS_E: { + if (!hit_accurate) break; + if (is_zcb_spec || encounter_roll_ratio_u16(rng_state, 33, 500)) { + int cap = is_zcb_spec ? 110 : 100; + int effect_dmg = target_current_hp * (is_zcb_spec ? 22 : 20) / 100; + if (effect_dmg > cap) effect_dmg = cap; + r.proc_triggered = 1; + r.modified_damage = effect_dmg; + } + break; + } + + default: + break; + } + + return r; +} + +#endif diff --git a/ocean/osrs/osrs_collision.h b/ocean/osrs/osrs_collision.h new file mode 100644 index 0000000000..74cc3fcf31 --- /dev/null +++ b/ocean/osrs/osrs_collision.h @@ -0,0 +1,415 @@ +#ifndef OSRS_COLLISION_H +#define OSRS_COLLISION_H + +#include +#include +#include + +#include "osrs_assets.h" + +#define COLLISION_NONE 0x000000 +#define COLLISION_WALL_NORTH_WEST 0x000001 +#define COLLISION_WALL_NORTH 0x000002 +#define COLLISION_WALL_NORTH_EAST 0x000004 +#define COLLISION_WALL_EAST 0x000008 +#define COLLISION_WALL_SOUTH_EAST 0x000010 +#define COLLISION_WALL_SOUTH 0x000020 +#define COLLISION_WALL_SOUTH_WEST 0x000040 +#define COLLISION_WALL_WEST 0x000080 + +#define COLLISION_IMPENETRABLE_WALL_NORTH_WEST 0x000200 +#define COLLISION_IMPENETRABLE_WALL_NORTH 0x000400 +#define COLLISION_IMPENETRABLE_WALL_NORTH_EAST 0x000800 +#define COLLISION_IMPENETRABLE_WALL_EAST 0x001000 +#define COLLISION_IMPENETRABLE_WALL_SOUTH_EAST 0x002000 +#define COLLISION_IMPENETRABLE_WALL_SOUTH 0x004000 +#define COLLISION_IMPENETRABLE_WALL_SOUTH_WEST 0x008000 +#define COLLISION_IMPENETRABLE_WALL_WEST 0x010000 + +#define COLLISION_IMPENETRABLE_BLOCKED 0x020000 +#define COLLISION_BRIDGE 0x040000 +#define COLLISION_BLOCKED 0x200000 + +#define REGION_SIZE 64 +#define REGION_HEIGHT_LEVELS 4 + +typedef struct { + int flags[REGION_HEIGHT_LEVELS][REGION_SIZE][REGION_SIZE]; +} CollisionRegion; + +#define REGION_MAP_CAPACITY 256 + +typedef struct { + int key; + CollisionRegion* region; +} RegionMapEntry; + +typedef struct { + RegionMapEntry entries[REGION_MAP_CAPACITY]; + int count; +} CollisionMap; + +static inline int collision_region_hash(int x, int y) { + int region_x = x >> 6; + int region_y = y >> 6; + return region_x * 256 + region_y; +} + +static inline int collision_local(int coord) { + return coord & 0x3F; +} + +static inline void collision_map_init(CollisionMap* map) { + map->count = 0; + for (int i = 0; i < REGION_MAP_CAPACITY; i++) { + map->entries[i].key = -1; + map->entries[i].region = NULL; + } +} + +static inline CollisionMap* collision_map_create(void) { + CollisionMap* map = (CollisionMap*)malloc(sizeof(CollisionMap)); + collision_map_init(map); + return map; +} + +static inline CollisionRegion* collision_map_get(const CollisionMap* map, int key) { + int idx = key & (REGION_MAP_CAPACITY - 1); + for (int i = 0; i < REGION_MAP_CAPACITY; i++) { + int slot = (idx + i) & (REGION_MAP_CAPACITY - 1); + if (map->entries[slot].key == key) { + return map->entries[slot].region; + } + if (map->entries[slot].key == -1) { + return NULL; + } + } + return NULL; +} + +static inline void collision_map_put(CollisionMap* map, int key, CollisionRegion* region) { + int idx = key & (REGION_MAP_CAPACITY - 1); + for (int i = 0; i < REGION_MAP_CAPACITY; i++) { + int slot = (idx + i) & (REGION_MAP_CAPACITY - 1); + if (map->entries[slot].key == key) { + map->entries[slot].region = region; + return; + } + if (map->entries[slot].key == -1) { + map->entries[slot].key = key; + map->entries[slot].region = region; + map->count++; + return; + } + } + fprintf(stderr, "collision_map_put: map full (capacity %d)\n", REGION_MAP_CAPACITY); +} + +static inline void collision_map_free(CollisionMap* map) { + if (map == NULL) return; + for (int i = 0; i < REGION_MAP_CAPACITY; i++) { + if (map->entries[i].region != NULL) { + free(map->entries[i].region); + } + } + free(map); +} + +static inline int collision_get_flags(const CollisionMap* map, int height, int x, int y) { + if (map == NULL) return COLLISION_NONE; + int key = collision_region_hash(x, y); + const CollisionRegion* region = collision_map_get(map, key); + if (region == NULL) return COLLISION_NONE; + int lx = collision_local(x); + int ly = collision_local(y); + int h = height < 0 ? 0 : (height >= REGION_HEIGHT_LEVELS ? REGION_HEIGHT_LEVELS - 1 : height); + return region->flags[h][lx][ly]; +} + +static inline int collision_is_inactive(const CollisionMap* map, int height, int x, int y, int flag) { + return (collision_get_flags(map, height, x, y) & flag) == 0; +} + +static inline int collision_traversable_north(const CollisionMap* map, int height, int x, int y) { + if (map == NULL) return 1; + return collision_is_inactive(map, height, x, y + 1, + COLLISION_WALL_SOUTH | COLLISION_BLOCKED); +} + +static inline int collision_traversable_south(const CollisionMap* map, int height, int x, int y) { + if (map == NULL) return 1; + return collision_is_inactive(map, height, x, y - 1, + COLLISION_WALL_NORTH | COLLISION_BLOCKED); +} + +static inline int collision_traversable_east(const CollisionMap* map, int height, int x, int y) { + if (map == NULL) return 1; + return collision_is_inactive(map, height, x + 1, y, + COLLISION_WALL_WEST | COLLISION_BLOCKED); +} + +static inline int collision_traversable_west(const CollisionMap* map, int height, int x, int y) { + if (map == NULL) return 1; + return collision_is_inactive(map, height, x - 1, y, + COLLISION_WALL_EAST | COLLISION_BLOCKED); +} + +static inline int collision_traversable_north_east(const CollisionMap* map, int height, int x, int y) { + if (map == NULL) return 1; + return collision_is_inactive(map, height, x + 1, y + 1, + COLLISION_WALL_WEST | COLLISION_WALL_SOUTH | COLLISION_WALL_SOUTH_WEST | COLLISION_BLOCKED) + && collision_is_inactive(map, height, x + 1, y, + COLLISION_WALL_WEST | COLLISION_BLOCKED) + && collision_is_inactive(map, height, x, y + 1, + COLLISION_WALL_SOUTH | COLLISION_BLOCKED); +} + +static inline int collision_traversable_north_west(const CollisionMap* map, int height, int x, int y) { + if (map == NULL) return 1; + return collision_is_inactive(map, height, x - 1, y + 1, + COLLISION_WALL_EAST | COLLISION_WALL_SOUTH | COLLISION_WALL_SOUTH_EAST | COLLISION_BLOCKED) + && collision_is_inactive(map, height, x - 1, y, + COLLISION_WALL_EAST | COLLISION_BLOCKED) + && collision_is_inactive(map, height, x, y + 1, + COLLISION_WALL_SOUTH | COLLISION_BLOCKED); +} + +static inline int collision_traversable_south_east(const CollisionMap* map, int height, int x, int y) { + if (map == NULL) return 1; + return collision_is_inactive(map, height, x + 1, y - 1, + COLLISION_WALL_WEST | COLLISION_WALL_NORTH | COLLISION_WALL_NORTH_WEST | COLLISION_BLOCKED) + && collision_is_inactive(map, height, x + 1, y, + COLLISION_WALL_WEST | COLLISION_BLOCKED) + && collision_is_inactive(map, height, x, y - 1, + COLLISION_WALL_NORTH | COLLISION_BLOCKED); +} + +static inline int collision_traversable_south_west(const CollisionMap* map, int height, int x, int y) { + if (map == NULL) return 1; + return collision_is_inactive(map, height, x - 1, y - 1, + COLLISION_WALL_EAST | COLLISION_WALL_NORTH | COLLISION_WALL_NORTH_EAST | COLLISION_BLOCKED) + && collision_is_inactive(map, height, x - 1, y, + COLLISION_WALL_EAST | COLLISION_BLOCKED) + && collision_is_inactive(map, height, x, y - 1, + COLLISION_WALL_NORTH | COLLISION_BLOCKED); +} + +static inline int collision_tile_walkable(const CollisionMap* map, int height, int x, int y) { + if (map == NULL) return 1; + return (collision_get_flags(map, height, x, y) & COLLISION_BLOCKED) == 0; +} + +static inline int collision_traversable_step(const CollisionMap* map, int height, + int x, int y, int dx, int dy) { + if (map == NULL) return 1; + + if (dx == 0 && dy == 1) return collision_traversable_north(map, height, x, y); + if (dx == 0 && dy == -1) return collision_traversable_south(map, height, x, y); + if (dx == 1 && dy == 0) return collision_traversable_east(map, height, x, y); + if (dx == -1 && dy == 0) return collision_traversable_west(map, height, x, y); + if (dx == 1 && dy == 1) return collision_traversable_north_east(map, height, x, y); + if (dx == -1 && dy == 1) return collision_traversable_north_west(map, height, x, y); + if (dx == 1 && dy == -1) return collision_traversable_south_east(map, height, x, y); + if (dx == -1 && dy == -1) return collision_traversable_south_west(map, height, x, y); + + return 1; +} + +#define COLLISION_MAP_MAGIC 0x50414D43 +#define COLLISION_MAP_VERSION 1 + +static inline CollisionMap* collision_map_load(const char* path) { + FILE* f = osrs_asset_fopen(path, "rb"); + if (f == NULL) { + fprintf(stderr, "collision_map_load: cannot open %s\n", path); + return NULL; + } + + uint32_t magic, version, region_count; + if (fread(&magic, 4, 1, f) != 1 || magic != COLLISION_MAP_MAGIC) { + fprintf(stderr, "collision_map_load: bad magic in %s\n", path); + fclose(f); + return NULL; + } + if (fread(&version, 4, 1, f) != 1 || version != COLLISION_MAP_VERSION) { + fprintf(stderr, "collision_map_load: unsupported version %u in %s\n", version, path); + fclose(f); + return NULL; + } + if (fread(®ion_count, 4, 1, f) != 1) { + fprintf(stderr, "collision_map_load: truncated header in %s\n", path); + fclose(f); + return NULL; + } + + CollisionMap* map = collision_map_create(); + + for (uint32_t i = 0; i < region_count; i++) { + int32_t key; + if (fread(&key, 4, 1, f) != 1) { + fprintf(stderr, "collision_map_load: truncated at region %u in %s\n", i, path); + collision_map_free(map); + fclose(f); + return NULL; + } + + CollisionRegion* region = (CollisionRegion*)calloc(1, sizeof(CollisionRegion)); + size_t flags_size = sizeof(region->flags); + if (fread(region->flags, 1, flags_size, f) != flags_size) { + fprintf(stderr, "collision_map_load: truncated flags at region %u in %s\n", i, path); + free(region); + collision_map_free(map); + fclose(f); + return NULL; + } + + collision_map_put(map, key, region); + } + + fclose(f); + return map; +} + +#define LOS_FULL_MASK 0x20000 +#define LOS_EAST_MASK 0x01000 +#define LOS_WEST_MASK 0x10000 +#define LOS_NORTH_MASK 0x00400 +#define LOS_SOUTH_MASK 0x04000 +#define LOS_FP_SCALE 65536 +#define LOS_FP_HALF 32768 + +typedef struct { + int x, y; + int size; + uint32_t los_mask; +} LOSBlocker; + +static uint32_t los_check_tile(const LOSBlocker* blockers, int count, + int px, int py) { + for (int i = 0; i < count; i++) { + const LOSBlocker* b = &blockers[i]; + if (px >= b->x && px < b->x + b->size && + py >= b->y && py < b->y + b->size) { + return b->los_mask; + } + } + return 0; +} + +static int los_aabb_overlap(int x1, int y1, int s1, int x2, int y2, int s2) { + return !(x1 >= x2 + s2 || x1 + s1 <= x2 || y1 >= y2 + s2 || y1 + s1 <= y2); +} + +static int has_line_of_sight(const LOSBlocker* blockers, int blocker_count, + int x1, int y1, int x2, int y2, + int src_size, int range) { + int dx = x2 - x1; + int dy = y2 - y1; + + if (los_check_tile(blockers, blocker_count, x1, y1)) return 0; + if (los_check_tile(blockers, blocker_count, x2, y2)) return 0; + + if (los_aabb_overlap(x1, y1, src_size, x2, y2, 1)) return 0; + + int adx = dx < 0 ? -dx : dx; + int ady = dy < 0 ? -dy : dy; + + if (range > 0 && (adx > range || ady > range)) return 0; + + if (adx > ady) { + int x_tile = x1; + int y_fp = y1 * LOS_FP_SCALE + LOS_FP_HALF; + int slope = (dy * LOS_FP_SCALE) / adx; + int x_inc = (dx > 0) ? 1 : -1; + uint32_t x_mask = (dx > 0) ? (LOS_WEST_MASK | LOS_FULL_MASK) + : (LOS_EAST_MASK | LOS_FULL_MASK); + uint32_t y_mask = (dy < 0) ? (LOS_NORTH_MASK | LOS_FULL_MASK) + : (LOS_SOUTH_MASK | LOS_FULL_MASK); + if (dy < 0) y_fp -= 1; + + while (x_tile != x2) { + x_tile += x_inc; + int y_tile = y_fp >> 16; + if (los_check_tile(blockers, blocker_count, x_tile, y_tile) & x_mask) + return 0; + y_fp += slope; + int new_y = y_fp >> 16; + if (new_y != y_tile) { + if (los_check_tile(blockers, blocker_count, x_tile, new_y) & y_mask) + return 0; + } + } + } else if (ady > 0) { + int y_tile = y1; + int x_fp = x1 * LOS_FP_SCALE + LOS_FP_HALF; + int slope = (dx * LOS_FP_SCALE) / ady; + int y_inc = (dy > 0) ? 1 : -1; + uint32_t y_mask = (dy > 0) ? (LOS_SOUTH_MASK | LOS_FULL_MASK) + : (LOS_NORTH_MASK | LOS_FULL_MASK); + uint32_t x_mask = (dx < 0) ? (LOS_EAST_MASK | LOS_FULL_MASK) + : (LOS_WEST_MASK | LOS_FULL_MASK); + if (dx < 0) x_fp -= 1; + + while (y_tile != y2) { + y_tile += y_inc; + int x_tile = x_fp >> 16; + if (los_check_tile(blockers, blocker_count, x_tile, y_tile) & y_mask) + return 0; + x_fp += slope; + int new_x = x_fp >> 16; + if (new_x != x_tile) { + if (los_check_tile(blockers, blocker_count, new_x, y_tile) & x_mask) + return 0; + } + } + } + + return 1; +} + +static inline int los_intervals_overlap(int a0, int a1, int b0, int b1) { + return !(a1 < b0 || b1 < a0); +} + +static inline int entity_has_line_of_sight( + const LOSBlocker* blockers, int blocker_count, + int ax, int ay, int a_size, + int tx, int ty, int t_size, + int range +) { + if (range == 1) { + if (los_aabb_overlap(ax, ay, a_size, tx, ty, t_size)) return 0; + + int a_x0 = ax; + int a_x1 = ax + a_size - 1; + int a_y0 = ay; + int a_y1 = ay + a_size - 1; + int t_x0 = tx; + int t_x1 = tx + t_size - 1; + int t_y0 = ty; + int t_y1 = ty + t_size - 1; + + return (a_x1 + 1 == t_x0 && los_intervals_overlap(a_y0, a_y1, t_y0, t_y1)) || + (t_x1 + 1 == a_x0 && los_intervals_overlap(a_y0, a_y1, t_y0, t_y1)) || + (a_y1 + 1 == t_y0 && los_intervals_overlap(a_x0, a_x1, t_x0, t_x1)) || + (t_y1 + 1 == a_y0 && los_intervals_overlap(a_x0, a_x1, t_x0, t_x1)); + } + + int a_px = tx; + if (a_px < ax) a_px = ax; + if (a_px >= ax + a_size) a_px = ax + a_size - 1; + int a_py = ty; + if (a_py < ay) a_py = ay; + if (a_py >= ay + a_size) a_py = ay + a_size - 1; + + int t_px = ax; + if (t_px < tx) t_px = tx; + if (t_px >= tx + t_size) t_px = tx + t_size - 1; + int t_py = ay; + if (t_py < ty) t_py = ty; + if (t_py >= ty + t_size) t_py = ty + t_size - 1; + + return has_line_of_sight(blockers, blocker_count, a_px, a_py, t_px, t_py, 1, range); +} + +#endif diff --git a/ocean/osrs/osrs_combat.h b/ocean/osrs/osrs_combat.h new file mode 100644 index 0000000000..d2f16e48b3 --- /dev/null +++ b/ocean/osrs/osrs_combat.h @@ -0,0 +1,628 @@ +#ifndef OSRS_COMBAT_H +#define OSRS_COMBAT_H + +#include +#include +#include +#include +#include "osrs_types.h" +#include "osrs_items.h" + +/** Standard OSRS accuracy formula over pre-computed rolls + (eff_level * (bonus + 64)); returns hit probability in [0, 1]. */ +static inline float osrs_hit_chance(int att_roll, int def_roll) { + if (att_roll > def_roll) + return 1.0f - (float)(def_roll + 2) / (2.0f * (float)(att_roll + 1)); + else + return (float)att_roll / (2.0f * (float)(def_roll + 1)); +} + +/** Twisted bow accuracy multiplier; target_magic = + min(max(npc_magic_level, npc_magic_attack_bonus), 250). */ +static inline float osrs_tbow_acc_mult(int target_magic) { + int m = target_magic < 250 ? target_magic : 250; + float lin = (float)(3 * m); + float quad = lin / 10.0f; + float mult = (140.0f + (lin - 10.0f) / 100.0f - (quad - 100.0f) * (quad - 100.0f) / 100.0f) / 100.0f; + if (mult > 1.4f) mult = 1.4f; + if (mult < 0.0f) mult = 0.0f; + return mult; +} + +/** Twisted bow damage multiplier; same input as osrs_tbow_acc_mult. */ +static inline float osrs_tbow_dmg_mult(int target_magic) { + int m = target_magic < 250 ? target_magic : 250; + float lin = (float)(3 * m); + float quad = lin / 10.0f; + float mult = (250.0f + (lin - 14.0f) / 100.0f - (quad - 140.0f) * (quad - 140.0f) / 100.0f) / 100.0f; + if (mult > 2.5f) mult = 2.5f; + if (mult < 0.0f) mult = 0.0f; + return mult; +} + +static inline int encounter_rand_int(uint32_t* rng_state, int max) { + if (max <= 0) return 0; + return (int)(xorshift32(rng_state) % (unsigned)max); +} + +static inline float encounter_rand_float(uint32_t* rng_state) { + return (float)(xorshift32(rng_state) & 0xFFFF) / 65536.0f; +} + +static inline int encounter_roll_ratio_u16( + uint32_t* rng_state, + uint64_t numerator, + uint64_t denominator +) { + uint32_t roll = xorshift32(rng_state) & 0xFFFFu; + if (denominator == 0 || numerator == 0) return 0; + if (numerator >= denominator) return 1; + return (uint64_t)roll * denominator < numerator * 65536ull; +} + +static inline void osrs_hit_chance_fraction( + int att_roll, + int def_roll, + uint64_t* numerator, + uint64_t* denominator +) { + if (att_roll > def_roll) { + *numerator = (uint64_t)(2 * att_roll - def_roll); + *denominator = (uint64_t)(2 * (att_roll + 1)); + } else { + *numerator = (uint64_t)att_roll; + *denominator = (uint64_t)(2 * (def_roll + 1)); + } +} + +static inline int encounter_roll_hit_chance( + uint32_t* rng_state, + int att_roll, + int def_roll +) { + uint64_t numerator, denominator; + osrs_hit_chance_fraction(att_roll, def_roll, &numerator, &denominator); + return encounter_roll_ratio_u16(rng_state, numerator, denominator); +} + +static inline void osrs_hit_chance_double_fraction( + int att_roll, + int def_roll, + uint64_t* numerator, + uint64_t* denominator +) { + uint64_t a = (uint64_t)att_roll; + uint64_t d = (uint64_t)def_roll; + if (att_roll >= def_roll) { + uint64_t miss_num = (d + 2ull) * (2ull * d + 3ull); + uint64_t den = 6ull * (a + 1ull) * (a + 1ull); + *numerator = den > miss_num ? den - miss_num : 0; + *denominator = den; + } else { + *numerator = a * (4ull * a + 5ull); + *denominator = 6ull * (a + 1ull) * (d + 1ull); + } +} + +static inline int encounter_roll_hit_chance_double( + uint32_t* rng_state, + int att_roll, + int def_roll +) { + uint64_t numerator, denominator; + osrs_hit_chance_double_fraction(att_roll, def_roll, &numerator, &denominator); + return encounter_roll_ratio_u16(rng_state, numerator, denominator); +} + +#define ENCOUNTER_SPELL_NONE 0 +#define ENCOUNTER_SPELL_ICE 1 +#define ENCOUNTER_SPELL_BLOOD 2 + +#define BARRAGE_MAX_HITS 9 +#define BARRAGE_FREEZE_TICKS 32 + +typedef struct { + int active; /* in: 1 if this target slot is valid */ + int x, y; /* in: NPC SW corner tile */ + int magic_level; /* in: magic rolls vs magic level, not defence */ + int magic_def_bonus; /* in */ + int npc_idx; /* in: index into the caller's NPC array */ + int* frozen_ticks; /* in: NULL = no freeze tracking */ + int rolled; /* out */ + int hit; /* out */ + int damage; /* out: 0 if splashed */ +} BarrageTarget; + +typedef struct { + int total_damage; + int num_hits; + int num_successful; +} BarrageResult; + +/** Barrage vs targets[0] plus AoE over active targets within 1 tile of it. + ICE freezes at cast time through each target's frozen_ticks pointer; the + caller still queues the returned damage as delayed pending hits. */ +static inline BarrageResult osrs_barrage_resolve( + BarrageTarget* targets, int max_targets, + int att_roll, int max_hit, uint32_t* rng_state, + int spell_type, + int primary_use_double_accuracy +) { + BarrageResult result = { 0, 0, 0 }; + + if (max_targets < 1 || !targets[0].active) return result; + + int px = targets[0].x, py = targets[0].y; + { + int def_roll = (targets[0].magic_level + 9) * (targets[0].magic_def_bonus + 64); + targets[0].rolled = 1; + targets[0].hit = primary_use_double_accuracy + ? encounter_roll_hit_chance_double(rng_state, att_roll, def_roll) + : encounter_roll_hit_chance(rng_state, att_roll, def_roll); + targets[0].damage = targets[0].hit ? encounter_rand_int(rng_state, max_hit + 1) : 0; + result.total_damage += targets[0].damage; + result.num_hits++; + if (targets[0].hit) { + result.num_successful++; + if (spell_type == ENCOUNTER_SPELL_ICE && targets[0].frozen_ticks) + *targets[0].frozen_ticks = BARRAGE_FREEZE_TICKS; + } + } + + for (int i = 1; i < max_targets && result.num_hits < BARRAGE_MAX_HITS; i++) { + if (!targets[i].active) continue; + int dx = targets[i].x - px; + int dy = targets[i].y - py; + if (dx < -1 || dx > 1 || dy < -1 || dy > 1) continue; + + int def_roll = (targets[i].magic_level + 9) * (targets[i].magic_def_bonus + 64); + targets[i].rolled = 1; + targets[i].hit = encounter_roll_hit_chance(rng_state, att_roll, def_roll); + targets[i].damage = targets[i].hit ? encounter_rand_int(rng_state, max_hit + 1) : 0; + result.total_damage += targets[i].damage; + result.num_hits++; + if (targets[i].hit) { + result.num_successful++; + if (spell_type == ENCOUNTER_SPELL_ICE && targets[i].frozen_ticks) + *targets[i].frozen_ticks = BARRAGE_FREEZE_TICKS; + } + } + + return result; +} + +static inline int osrs_npc_melee_max_hit(int str_level, int melee_str_bonus) { + return ((str_level + 9) * (melee_str_bonus + 64) + 320) / 640; +} + +static inline int osrs_npc_ranged_max_hit(int range_level, int ranged_str_bonus) { + return (int)(0.5 + (double)(range_level + 9) * (ranged_str_bonus + 64) / 640.0); +} + +/** magic_dmg_pct is percent: 100 = 1.0x, 175 = 1.75x. */ +static inline int osrs_npc_magic_max_hit(int base_spell_dmg, int magic_dmg_pct) { + return base_spell_dmg * magic_dmg_pct / 100; +} + +static inline int osrs_npc_attack_roll(int att_level, int att_bonus) { + return (att_level + 9) * (att_bonus + 64); +} + +/** NPC defence roll the player attacks into; the caller passes the level the + style rolls against (drain-adjusted Defence for melee/ranged, Magic for magic). */ +static inline int osrs_npc_def_roll(int def_level, int def_bonus) { + return (def_level + 9) * (def_bonus + 64); +} + +static inline int encounter_npc_melee_def_bonus( + int stab_def, int slash_def, int crush_def, int melee_style +) { + if (melee_style == 1) return slash_def; + if (melee_style == 2) return crush_def; + return stab_def; +} + +/** Style-dispatched NPC defence roll: a monster's MAGIC defence rolls off its + Magic level, not its Defence level, so the caller passes both. */ +static inline int encounter_npc_target_def_roll( + int melee_ranged_def_level, + int magic_level, + int stab_def, + int slash_def, + int crush_def, + int magic_def_bonus, + int ranged_def_bonus, + int attack_style, + int melee_style +) { + if (attack_style == 3) + return osrs_npc_def_roll(magic_level, magic_def_bonus); + if (attack_style == 2) + return osrs_npc_def_roll(melee_ranged_def_level, ranged_def_bonus); + return osrs_npc_def_roll( + melee_ranged_def_level, + encounter_npc_melee_def_bonus(stab_def, slash_def, crush_def, melee_style)); +} + +/** Player defence roll vs an NPC attack: players get +8, not the NPC +9. */ +static inline int osrs_player_def_roll_vs_npc( + int def_level, int magic_level, int def_bonus, int attack_style +) { + int eff_def; + if (attack_style == 3) { + eff_def = (int)(magic_level * 0.7 + def_level * 0.3) + 8; + } else { + eff_def = def_level + 8; + } + return eff_def * (def_bonus + 64); +} + +static inline int encounter_player_def_bonus( + int def_stab, int def_slash, int def_crush, int def_magic, int def_ranged, + int attack_style, int melee_style +) { + if (attack_style == 2) return def_ranged; + if (attack_style == 3) return def_magic; + if (melee_style == 1) return def_slash; + if (melee_style == 2) return def_crush; + return def_stab; +} + +/** Loadout defence bonus for the incoming style, then the OSRS defence roll; + callers apply any encounter-specific Defence adjustment before this. */ +static inline int encounter_player_def_roll_from_loadout( + int def_level, + int magic_level, + int def_stab, + int def_slash, + int def_crush, + int def_magic, + int def_ranged, + int attack_style, + int melee_style +) { + int def_bonus = encounter_player_def_bonus( + def_stab, def_slash, def_crush, def_magic, def_ranged, + attack_style, melee_style); + return osrs_player_def_roll_vs_npc( + def_level, magic_level, def_bonus, attack_style); +} + +static inline int osrs_npc_max_hit( + int attack_style, + int str_level, int range_level, + int melee_str_bonus, int ranged_str_bonus, + int magic_base_dmg, int magic_dmg_pct +) { + if (attack_style == 1) + return osrs_npc_melee_max_hit(str_level, melee_str_bonus); + if (attack_style == 2) + return osrs_npc_ranged_max_hit(range_level, ranged_str_bonus); + if (attack_style == 3) + return osrs_npc_magic_max_hit(magic_base_dmg, magic_dmg_pct); + return 0; +} + +/** Damage roll FIRST (0..max_hit), THEN the accuracy roll; this draw order is + canonical for every encounter and goldens depend on it. force_hit SKIPS the + accuracy draw entirely rather than ignoring it, for the same reason. */ +static inline int encounter_npc_roll_attack_ex( + int att_roll, int def_roll, int max_hit, int force_hit, + uint32_t* rng_state, int* hit_success +) { + int dmg = encounter_rand_int(rng_state, max_hit + 1); + int hit = force_hit ? 1 : encounter_roll_hit_chance(rng_state, att_roll, def_roll); + if (hit_success) *hit_success = hit; + return hit ? dmg : 0; +} + +static inline int encounter_npc_roll_attack( + int att_roll, int def_roll, int max_hit, uint32_t* rng_state +) { + return encounter_npc_roll_attack_ex(att_roll, def_roll, max_hit, 0, rng_state, NULL); +} + +/** The prayer and style enums run in OPPOSITE orders (prayer MAGIC=1..MELEE=3, + style MELEE=1..MAGIC=3), hence the crossed constants. */ +static inline int encounter_prayer_correct_for_style(int prayer, int attack_style) { + return (attack_style == 1 && prayer == 3) || + (attack_style == 2 && prayer == 2) || + (attack_style == 3 && prayer == 1); +} + +/** Protect-prayer outcome and damage lock on the THROW tick: flicking after the + throw cannot change a hit already in flight. Jad-style deferred checks are + the exception (prayer_check_delay). */ +typedef struct { + int frozen_damage; + int prayed; +} EncounterProtectResolve; + +static inline EncounterProtectResolve encounter_resolve_protect_at_throw( + int raw_damage, int overhead_prayer, int attack_style +) { + int prayed = encounter_prayer_correct_for_style(overhead_prayer, attack_style); + return (EncounterProtectResolve){ .frozen_damage = prayed ? 0 : raw_damage, .prayed = prayed }; +} + +static inline int encounter_magic_hit_delay(int distance, int is_player) { + return (1 + distance) / 3 + 1 + (is_player ? 1 : 0); +} + +static inline int encounter_ranged_hit_delay(int distance, int is_player) { + return (3 + distance) / 6 + 1 + (is_player ? 1 : 0); +} + +static inline int encounter_thrown_hit_delay(int distance, int is_player) { + return distance / 6 + 1 + (is_player ? 1 : 0); +} + +static inline int encounter_blowpipe_hit_delay(int distance, int is_player) { + return encounter_thrown_hit_delay(distance, is_player); +} + +static inline int encounter_ballista_hit_delay(int distance, int is_player) { + return 2 + (1 + distance) / 6 + (is_player ? 1 : 0); +} + +static inline int encounter_dark_bow_second_hit_delay(int distance, int is_player) { + return 1 + (2 + distance) / 3 + (is_player ? 1 : 0); +} + +static inline int encounter_eye_of_ayak_hit_delay(int distance) { + return distance <= 2 ? 1 : 2; +} + +typedef enum { + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE, + ENCOUNTER_PROJECTILE_DISTANCE_TARGET_SW_TILE, +} EncounterProjectileDistanceMode; + +typedef enum { + ENCOUNTER_PROJECTILE_DELAY_MELEE, + ENCOUNTER_PROJECTILE_DELAY_MAGIC, + ENCOUNTER_PROJECTILE_DELAY_RANGED, + ENCOUNTER_PROJECTILE_DELAY_THROWN, + ENCOUNTER_PROJECTILE_DELAY_BALLISTA, + ENCOUNTER_PROJECTILE_DELAY_DARK_BOW_SECOND, + ENCOUNTER_PROJECTILE_DELAY_EYE_OF_AYAK, +} EncounterProjectileDelayKind; + +typedef struct { + int set_delay; + int reduce_delay; + int start_delay; + int visual_delay_ticks; + int visual_hit_early_ticks; +} EncounterProjectileDelayOptions; + +typedef struct { + int damage_delay_ticks; + int visual_start_delay_ticks; + int visual_duration_ticks; +} EncounterProjectileTiming; + +static inline int encounter_rect_distance( + int ax, int ay, int asize, int bx, int by, int bsize +) { + int amax_x = ax + asize - 1; + int amax_y = ay + asize - 1; + int bmax_x = bx + bsize - 1; + int bmax_y = by + bsize - 1; + int dx = 0; + int dy = 0; + if (amax_x < bx) dx = bx - amax_x; + else if (bmax_x < ax) dx = ax - bmax_x; + if (amax_y < by) dy = by - amax_y; + else if (bmax_y < ay) dy = ay - bmax_y; + return dx > dy ? dx : dy; +} + +static inline int encounter_projectile_distance( + int source_x, int source_y, int source_size, + int target_x, int target_y, int target_size, + EncounterProjectileDistanceMode mode +) { + switch (mode) { + case ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE: + return encounter_rect_distance( + source_x, source_y, source_size, target_x, target_y, target_size); + case ENCOUNTER_PROJECTILE_DISTANCE_TARGET_SW_TILE: + return chebyshev_distance(source_x, source_y, target_x, target_y); + } + abort(); +} + +static inline int encounter_projectile_base_hit_delay( + int distance, int is_player, EncounterProjectileDelayKind kind +) { + switch (kind) { + case ENCOUNTER_PROJECTILE_DELAY_MELEE: + return 0; + case ENCOUNTER_PROJECTILE_DELAY_MAGIC: + return encounter_magic_hit_delay(distance, is_player); + case ENCOUNTER_PROJECTILE_DELAY_RANGED: + return encounter_ranged_hit_delay(distance, is_player); + case ENCOUNTER_PROJECTILE_DELAY_THROWN: + return encounter_thrown_hit_delay(distance, is_player); + case ENCOUNTER_PROJECTILE_DELAY_BALLISTA: + return encounter_ballista_hit_delay(distance, is_player); + case ENCOUNTER_PROJECTILE_DELAY_DARK_BOW_SECOND: + return encounter_dark_bow_second_hit_delay(distance, is_player); + case ENCOUNTER_PROJECTILE_DELAY_EYE_OF_AYAK: + return encounter_eye_of_ayak_hit_delay(distance); + } + abort(); +} + +static inline int encounter_projectile_hit_delay( + int distance, int is_player, EncounterProjectileDelayKind kind, + EncounterProjectileDelayOptions options +) { + int delay = encounter_projectile_base_hit_delay(distance, is_player, kind); + if (delay > 0) { + delay -= options.reduce_delay; + if (delay < 1) delay = 1; + } + if (options.set_delay > 0) + delay = options.set_delay; + return delay; +} + +static inline EncounterProjectileTiming encounter_projectile_timing( + int distance, int is_player, EncounterProjectileDelayKind kind, + EncounterProjectileDelayOptions options +) { + int delay = encounter_projectile_hit_delay(distance, is_player, kind, options); + int start_delay = options.start_delay > 0 + ? options.start_delay + : options.visual_delay_ticks; + int duration = delay - start_delay - options.visual_hit_early_ticks; + if (duration < 1) duration = 1; + return (EncounterProjectileTiming){ + .damage_delay_ticks = delay, + .visual_start_delay_ticks = start_delay, + .visual_duration_ticks = duration, + }; +} + +static inline int encounter_dist_to_npc(int px, int py, int nx, int ny, int npc_size) { + return encounter_rect_distance(px, py, 1, nx, ny, npc_size); +} + +static inline void encounter_shuffle(int* arr, int n, uint32_t* rng) { + for (int i = n - 1; i > 0; i--) { + int j = encounter_rand_int(rng, i + 1); + int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; + } +} +/** Standard effective level: floor(base * prayer_mult) + style_bonus + 8. Magic + accuracy is the exception; use osrs_magic_effective_attack_level for it. */ +static inline int osrs_player_eff_level(int base_level, float prayer_mult, int style_bonus) { + return (int)(base_level * prayer_mult) + style_bonus + 8; +} + +static inline int osrs_stance_att_bonus(FightStyle fs, AttackStyle atk) { + switch (fs) { + case FIGHT_STYLE_ACCURATE: return atk == ATTACK_STYLE_MAGIC ? 2 : 3; + case FIGHT_STYLE_CONTROLLED: return atk == ATTACK_STYLE_MELEE ? 1 : 0; + case FIGHT_STYLE_LONGRANGE: return 0; + default: return 0; + } +} + +/** Magic accuracy folds in +9 instead of the melee/ranged +8. */ +static inline int osrs_magic_effective_attack_level( + int magic_level, float prayer_mult, FightStyle fight_style +) { + return (int)(magic_level * prayer_mult) + + osrs_stance_att_bonus(fight_style, ATTACK_STYLE_MAGIC) + 9; +} + +static inline float osrs_offensive_magic_dmg_mult(OffensivePrayer op) { + return (op == OFFENSIVE_PRAYER_AUGURY) ? 1.04f : 1.0f; +} + +static inline int osrs_stance_str_bonus(FightStyle fs) { + switch (fs) { + case FIGHT_STYLE_AGGRESSIVE: return 3; + case FIGHT_STYLE_CONTROLLED: return 1; + default: return 0; + } +} + +static inline int osrs_stance_def_bonus(FightStyle fs) { + switch (fs) { + case FIGHT_STYLE_DEFENSIVE: + case FIGHT_STYLE_LONGRANGE: return 3; + case FIGHT_STYLE_CONTROLLED: return 1; + default: return 0; + } +} + +static inline int osrs_stance_speed_mod(FightStyle fs) { + return fs == FIGHT_STYLE_RAPID ? -1 : 0; +} + +static inline int osrs_stance_range_mod(FightStyle fs) { + return fs == FIGHT_STYLE_LONGRANGE ? 2 : 0; +} + +static inline int osrs_player_att_roll(int eff_level, int equipment_bonus) { + return eff_level * (equipment_bonus + 64); +} + +static inline int osrs_player_melee_max_hit(int eff_str_level, int str_bonus) { + return (eff_str_level * (str_bonus + 64) + 320) / 640; +} + +static inline int osrs_player_ranged_max_hit(int eff_range_level, int ranged_str_bonus) { + return (eff_range_level * (ranged_str_bonus + 64) + 320) / 640; +} + +/** magic_dmg_pct is the total gear bonus in percent (30 = +30%). */ +static inline int osrs_player_magic_max_hit(int spell_base_dmg, int magic_dmg_pct) { + return spell_base_dmg * (100 + magic_dmg_pct) / 100; +} + +/** Correct overhead prayer blocks 100% of damage in PvE but only 40% in PvP. */ +static inline int osrs_prayer_reduce_damage(int damage, int prayer, int attack_style, int is_pvp) { + if (damage <= 0) return 0; + if (!encounter_prayer_correct_for_style(prayer, attack_style)) return damage; + if (is_pvp) return (int)(damage * 0.6f); + return 0; +} + +/** Closed form of rolling accuracy twice and hitting if EITHER succeeds + (osmumten's fang, confliction gauntlets). */ +static inline float osrs_hit_chance_double(int att_roll, int def_roll) { + float fa = (float)att_roll, fd = (float)def_roll; + if (att_roll >= def_roll) { + float num = (fd + 2.0f) * (2.0f * fd + 3.0f); + float den = 6.0f * (fa + 1.0f) * (fa + 1.0f); + return 1.0f - num / den; + } + return fa * (4.0f * fa + 5.0f) / (6.0f * (fa + 1.0f) * (fd + 1.0f)); +} + +typedef struct { + int attack_stab, attack_slash, attack_crush, attack_magic, attack_ranged; + int defence_stab, defence_slash, defence_crush, defence_magic, defence_ranged; + int melee_strength, ranged_strength, magic_damage, prayer; + int attack_speed, attack_range; +} EquipmentBonuses; + +/** Sum ITEM_DATABASE bonuses over a loadout; attack_speed and attack_range come + from the weapon slot only. */ +static inline void osrs_sum_equipment_bonuses(const uint8_t loadout[NUM_GEAR_SLOTS], + EquipmentBonuses* out) { + memset(out, 0, sizeof(*out)); + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + uint8_t idx = loadout[slot]; + if (idx == 255) continue; + const Item* item = &ITEM_DATABASE[idx]; + out->attack_stab += item->attack_stab; + out->attack_slash += item->attack_slash; + out->attack_crush += item->attack_crush; + out->attack_magic += item->attack_magic; + out->attack_ranged += item->attack_ranged; + out->defence_stab += item->defence_stab; + out->defence_slash += item->defence_slash; + out->defence_crush += item->defence_crush; + out->defence_magic += item->defence_magic; + out->defence_ranged += item->defence_ranged; + out->melee_strength += item->melee_strength; + out->ranged_strength += item->ranged_strength; + out->magic_damage += item->magic_damage; + out->prayer += item->prayer; + } + uint8_t weapon = loadout[GEAR_SLOT_WEAPON]; + if (weapon != 255) { + out->attack_speed = ITEM_DATABASE[weapon].attack_speed; + out->attack_range = ITEM_DATABASE[weapon].attack_range; + } +} + +#include "osrs_venator.h" + +#endif /* OSRS_COMBAT_H */ diff --git a/ocean/osrs/osrs_combat_visuals.h b/ocean/osrs/osrs_combat_visuals.h new file mode 100644 index 0000000000..6b31d1939c --- /dev/null +++ b/ocean/osrs/osrs_combat_visuals.h @@ -0,0 +1,880 @@ +#ifndef OSRS_COMBAT_VISUALS_H +#define OSRS_COMBAT_VISUALS_H + +#include "osrs_items.h" +#include "osrs_types.h" +#include "osrs_gfx_ids.h" +#include +#include +#include +#include +#include + +enum { + OSRS_COMBAT_VISUAL_STYLE_ANY = -1, + OSRS_COMBAT_VISUAL_STANCE_ANY = -1, + OSRS_COMBAT_VISUAL_NO_ANIMATION = -1, + OSRS_PLAYER_UNARMED_ATTACK_ANIM = 422, + OSRS_PLAYER_POWERED_STAFF_ATTACK_ANIM = 1167, + OSRS_COMBAT_PROJECTILE_MISSING = -1, +}; + +typedef enum { + OSRS_ITEM_ID_RUNE_ARROW = 892, + OSRS_ITEM_ID_ABYSSAL_WHIP = 4151, + OSRS_ITEM_ID_GRANITE_MAUL = 4153, + OSRS_ITEM_ID_AHRIMS_STAFF = 4710, + OSRS_ITEM_ID_DRAGON_DAGGER = 5698, + OSRS_ITEM_ID_RUNE_CROSSBOW = 9185, + OSRS_ITEM_ID_DIAMOND_BOLTS_E = 9243, + OSRS_ITEM_ID_DARK_BOW = 11235, + OSRS_ITEM_ID_DRAGON_ARROWS = 11212, + OSRS_ITEM_ID_DRAGON_DART = 11230, + OSRS_ITEM_ID_STAFF_OF_THE_DEAD = 11791, + OSRS_ITEM_ID_ARMADYL_CROSSBOW = 11785, + OSRS_ITEM_ID_ARMADYL_GODSWORD = 11802, + OSRS_ITEM_ID_MAGIC_SHORTBOW_I = 12788, + OSRS_ITEM_ID_TRIDENT_OF_THE_SWAMP = 12899, + OSRS_ITEM_ID_TOXIC_BLOWPIPE = 12926, + OSRS_ITEM_ID_DRAGON_CLAWS = 13652, + OSRS_ITEM_ID_ZURIELS_STAFF = 13867, + OSRS_ITEM_ID_HEAVY_BALLISTA = 19481, + OSRS_ITEM_ID_TWISTED_BOW = 20997, + OSRS_ITEM_ID_ELDER_MAUL = 21003, + OSRS_ITEM_ID_KODAI_WAND = 21006, + OSRS_ITEM_ID_GHRAZI_RAPIER = 22324, + OSRS_ITEM_ID_SANGUINESTI_STAFF = 22481, + OSRS_ITEM_ID_VESTAS_LONGSWORD = 22613, + OSRS_ITEM_ID_STATIUSS_WARHAMMER = 22622, + OSRS_ITEM_ID_MORRIGANS_JAVELIN = 22636, + OSRS_ITEM_ID_INQUISITORS_MACE = 24417, + OSRS_ITEM_ID_VOLATILE_NIGHTMARE_STAFF = 24424, + OSRS_ITEM_ID_BOW_OF_FAERDHINEN = 25865, + OSRS_ITEM_ID_ANCIENT_GODSWORD = 26233, + OSRS_ITEM_ID_ZARYTE_CROSSBOW = 26374, + OSRS_ITEM_ID_VOIDWAKER = 27690, + OSRS_ITEM_ID_DRAGON_HUNTER_WAND = 30070, + OSRS_ITEM_ID_EYE_OF_AYAK = 31113, + OSRS_ITEM_ID_TUMEKENS_SHADOW = 27275, +} OsrsCombatVisualItemId; + +typedef enum { + OSRS_COMBAT_VISUAL_KIND_ITEM = 1, + OSRS_COMBAT_VISUAL_KIND_SPELL = 2, + OSRS_COMBAT_VISUAL_KIND_NPC = 3, + OSRS_COMBAT_VISUAL_KIND_SPECIAL = 4, +} OsrsCombatVisualKind; + +typedef enum { + OSRS_COMBAT_PROJECTILE_NONE = 0, + OSRS_COMBAT_PROJECTILE_BOLT, + OSRS_COMBAT_PROJECTILE_RUNE_ARROW, + OSRS_COMBAT_PROJECTILE_DRAGON_ARROW, + OSRS_COMBAT_PROJECTILE_DRAGON_DART, + OSRS_COMBAT_PROJECTILE_TRIDENT, +} OsrsCombatProjectileVisual; + +typedef enum { + OSRS_COMBAT_VISUAL_SPELL_NONE = 0, + OSRS_COMBAT_VISUAL_SPELL_ICE_BARRAGE = 1, + OSRS_COMBAT_VISUAL_SPELL_BLOOD_BARRAGE = 2, +} OsrsCombatVisualSpell; + +typedef struct { + int32_t launch_spotanim_id; + int32_t travel_spotanim_id; + int32_t impact_spotanim_id; + int32_t projectile_model_id; + int32_t projectile_anim_id; + int16_t hit_delay; + int16_t client_delay; + int16_t projectile_start_height; + int16_t projectile_end_height; + int16_t projectile_delay; + int16_t projectile_angle; + int16_t projectile_length_adjustment; + int16_t projectile_progress; + int16_t projectile_step_multiplier; + int16_t projectile_count; +} OsrsCombatProjectileProfile; + +typedef struct { + int16_t projectile_start_height; + int16_t projectile_end_height; + int16_t projectile_delay; + int16_t projectile_angle; + int16_t projectile_length_adjustment; + int16_t projectile_progress; + int16_t projectile_step_multiplier; +} OsrsCombatAltProjectileProfile; + +typedef struct { + uint8_t kind; + int32_t key_id; + const char* key_name; + int8_t style; + int8_t stance_idx; + int16_t attack_anim_id; + OsrsCombatProjectileProfile projectile; + OsrsCombatAltProjectileProfile alt_projectile; + int32_t aux_travel_spotanim_id; + int32_t aux_impact_spotanim_id; + int32_t aux_projectile_model_id; + int32_t aux_projectile_anim_id; + int8_t impact_on_last_only; + int32_t double_launch_spotanim_id; +} OsrsCombatVisualRow; + +enum { + OSRS_PROJECTILE_MODEL_ARROW = 3136, + OSRS_PROJECTILE_MODEL_VENATOR_BOLT = 46993, + OSRS_PROJECTILE_MODEL_TRIDENT = 20825, + OSRS_PROJECTILE_MODEL_DRAGON_ARROW = 26377, + OSRS_PROJECTILE_MODEL_DRAGON_DART = 26379, + OSRS_PROJECTILE_ANIM_TRIDENT = 5462, + OSRS_PROJECTILE_ANIM_DRAGON_ARROW = 6622, + OSRS_PROJECTILE_ANIM_DRAGON_DART = 6622, + OSRS_COMBAT_PROJECTILE_SEQUENCE_MAX = 8, +}; + +#include "osrs_combat_visuals_generated.h" + +#define OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING OSRS_COMBAT_PROJECTILE_MISSING +#define OSRS_COMBAT_VISUAL_COLOSSEUM_ALT_NONE \ + {OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, \ + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, \ + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, \ + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING} +#define OSRS_COMBAT_VISUAL_COLOSSEUM_PROJECTILE(launch, travel, impact, model, anim, hit, client, start, end, delay, angle, len, progress, step, count) \ + {launch, travel, impact, model, anim, hit, client, start, end, delay, angle, \ + len, progress, step, count} +#define OSRS_COMBAT_VISUAL_COLOSSEUM_NPC(npc_id, attack_style, attack_anim, projectile_profile) \ + {(uint8_t)OSRS_COMBAT_VISUAL_KIND_NPC, npc_id, "", (int8_t)attack_style, \ + (int8_t)OSRS_COMBAT_VISUAL_STANCE_ANY, (int16_t)attack_anim, \ + projectile_profile, OSRS_COMBAT_VISUAL_COLOSSEUM_ALT_NONE, \ + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, \ + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, \ + (int8_t)0, OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING} + +static const OsrsCombatVisualRow OSRS_COMBAT_VISUAL_COLOSSEUM_ROWS[] = { + OSRS_COMBAT_VISUAL_COLOSSEUM_NPC( + 12814, ATTACK_STYLE_RANGED, 10850, + OSRS_COMBAT_VISUAL_COLOSSEUM_PROJECTILE( + 24, 15, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + 3136, OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 16, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 1)), + OSRS_COMBAT_VISUAL_COLOSSEUM_NPC( + 12815, ATTACK_STYLE_MAGIC, 10853, + OSRS_COMBAT_VISUAL_COLOSSEUM_PROJECTILE( + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 328, 329, + 5091, 1577, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 16, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 1)), + OSRS_COMBAT_VISUAL_COLOSSEUM_NPC( + 12811, ATTACK_STYLE_MAGIC, 10859, + OSRS_COMBAT_VISUAL_COLOSSEUM_PROJECTILE( + 1458, 1459, 1460, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 16, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 1)), + OSRS_COMBAT_VISUAL_COLOSSEUM_NPC( + 12817, ATTACK_STYLE_RANGED, 10892, + OSRS_COMBAT_VISUAL_COLOSSEUM_PROJECTILE( + + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 2673, 2676, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 16, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 1)), + OSRS_COMBAT_VISUAL_COLOSSEUM_NPC( + 12818, ATTACK_STYLE_MELEE, 10869, + OSRS_COMBAT_VISUAL_COLOSSEUM_PROJECTILE( + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 2685, 2686, + 51213, 10328, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 16, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 1)), + OSRS_COMBAT_VISUAL_COLOSSEUM_NPC( + 12818, ATTACK_STYLE_RANGED, 10869, + OSRS_COMBAT_VISUAL_COLOSSEUM_PROJECTILE( + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 2683, 2684, + 51221, 10327, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 16, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 1)), + OSRS_COMBAT_VISUAL_COLOSSEUM_NPC( + 12818, ATTACK_STYLE_MAGIC, 10869, + OSRS_COMBAT_VISUAL_COLOSSEUM_PROJECTILE( + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 2681, 2682, + 51215, 10329, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 16, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 1)), + OSRS_COMBAT_VISUAL_COLOSSEUM_NPC( + 12819, ATTACK_STYLE_MAGIC, 10903, + OSRS_COMBAT_VISUAL_COLOSSEUM_PROJECTILE( + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 2679, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + 51210, + 10903, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 16, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, + OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING, 1)), +}; + +static const size_t OSRS_COMBAT_VISUAL_COLOSSEUM_ROW_COUNT = + sizeof(OSRS_COMBAT_VISUAL_COLOSSEUM_ROWS) / + sizeof(OSRS_COMBAT_VISUAL_COLOSSEUM_ROWS[0]); + +#undef OSRS_COMBAT_VISUAL_COLOSSEUM_NPC +#undef OSRS_COMBAT_VISUAL_COLOSSEUM_PROJECTILE +#undef OSRS_COMBAT_VISUAL_COLOSSEUM_ALT_NONE +#undef OSRS_COMBAT_VISUAL_COLOSSEUM_MISSING + +typedef struct { + uint16_t item_id; + int16_t special_attack_anim_id; +} OsrsCombatSpecialFallback; + +typedef struct { + uint16_t weapon_item_id; + uint16_t projectile_item_id; + OsrsCombatProjectileVisual visual; +} OsrsCombatWeaponProjectileDefault; + +typedef struct { + OsrsCombatProjectileProfile projectile; + int16_t sequence_index; + int16_t sequence_count; +} OsrsCombatProjectileSequencePart; + +static const OsrsCombatProjectileProfile OSRS_POWERED_STAFF_PROJECTILE_PROFILE = { + GFX_TRIDENT_CAST, GFX_TRIDENT_PROJ, GFX_TRIDENT_IMPACT, + OSRS_PROJECTILE_MODEL_TRIDENT, OSRS_PROJECTILE_ANIM_TRIDENT, + 3, 3, 160, 120, OSRS_COMBAT_PROJECTILE_MISSING, 16, + OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, + OSRS_COMBAT_PROJECTILE_MISSING, 1 +}; + +static const OsrsCombatProjectileProfile OSRS_TUMEKENS_SHADOW_PROJECTILE_PROFILE = { + GFX_TUMEKENS_SHADOW_CAST, GFX_TUMEKENS_SHADOW_PROJ, GFX_TUMEKENS_SHADOW_IMPACT, + OSRS_PROJECTILE_MODEL_TRIDENT, OSRS_PROJECTILE_ANIM_TRIDENT, + 3, 3, 160, 120, OSRS_COMBAT_PROJECTILE_MISSING, 16, + OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, + OSRS_COMBAT_PROJECTILE_MISSING, 1 +}; + +static const OsrsCombatSpecialFallback OSRS_COMBAT_SPECIAL_FALLBACKS[] = { + {OSRS_ITEM_ID_GRANITE_MAUL, 1667}, + {OSRS_ITEM_ID_DRAGON_DAGGER, 1062}, + {OSRS_ITEM_ID_MAGIC_SHORTBOW_I, 1074}, + {OSRS_ITEM_ID_DRAGON_CLAWS, 7514}, + {OSRS_ITEM_ID_VESTAS_LONGSWORD, 7515}, + {OSRS_ITEM_ID_STATIUSS_WARHAMMER, 1378}, + {OSRS_ITEM_ID_INQUISITORS_MACE, 1060}, + {OSRS_ITEM_ID_VOLATILE_NIGHTMARE_STAFF, 8532}, + {OSRS_ITEM_ID_ANCIENT_GODSWORD, 7644}, + {OSRS_ITEM_ID_VOIDWAKER, 1378}, +}; + +static const OsrsCombatWeaponProjectileDefault OSRS_RANGED_PROJECTILE_DEFAULTS[] = { + {OSRS_ITEM_ID_RUNE_CROSSBOW, OSRS_ITEM_ID_DIAMOND_BOLTS_E, OSRS_COMBAT_PROJECTILE_BOLT}, + {OSRS_ITEM_ID_ARMADYL_CROSSBOW, OSRS_ITEM_ID_DIAMOND_BOLTS_E, OSRS_COMBAT_PROJECTILE_BOLT}, + {OSRS_ITEM_ID_HEAVY_BALLISTA, OSRS_ITEM_ID_DIAMOND_BOLTS_E, OSRS_COMBAT_PROJECTILE_BOLT}, + {OSRS_ITEM_ID_ZARYTE_CROSSBOW, OSRS_ITEM_ID_DIAMOND_BOLTS_E, OSRS_COMBAT_PROJECTILE_BOLT}, + {OSRS_ITEM_ID_DARK_BOW, OSRS_ITEM_ID_RUNE_ARROW, OSRS_COMBAT_PROJECTILE_RUNE_ARROW}, + {OSRS_ITEM_ID_MAGIC_SHORTBOW_I, OSRS_ITEM_ID_RUNE_ARROW, OSRS_COMBAT_PROJECTILE_RUNE_ARROW}, + {OSRS_ITEM_ID_BOW_OF_FAERDHINEN, OSRS_ITEM_ID_RUNE_ARROW, OSRS_COMBAT_PROJECTILE_RUNE_ARROW}, + {OSRS_ITEM_ID_TWISTED_BOW, OSRS_ITEM_ID_DRAGON_ARROWS, OSRS_COMBAT_PROJECTILE_DRAGON_ARROW}, + {OSRS_ITEM_ID_TOXIC_BLOWPIPE, OSRS_ITEM_ID_DRAGON_DART, OSRS_COMBAT_PROJECTILE_DRAGON_DART}, +}; + +static const uint16_t OSRS_POWERED_STAFF_ITEMS[] = { + OSRS_ITEM_ID_TRIDENT_OF_THE_SWAMP, + OSRS_ITEM_ID_SANGUINESTI_STAFF, + OSRS_ITEM_ID_EYE_OF_AYAK, + OSRS_ITEM_ID_TUMEKENS_SHADOW, +}; + +static inline int osrs_combat_projectile_value_or(int value, int fallback) { + return value == OSRS_COMBAT_PROJECTILE_MISSING ? fallback : value; +} + +static inline int osrs_combat_visual_row_has_projectile( + const OsrsCombatVisualRow* row +) { + return row && + (row->projectile.launch_spotanim_id != OSRS_COMBAT_PROJECTILE_MISSING || + row->projectile.travel_spotanim_id != OSRS_COMBAT_PROJECTILE_MISSING || + row->projectile.impact_spotanim_id != OSRS_COMBAT_PROJECTILE_MISSING || + row->projectile.projectile_model_id != OSRS_COMBAT_PROJECTILE_MISSING || + row->aux_travel_spotanim_id != OSRS_COMBAT_PROJECTILE_MISSING || + row->aux_projectile_model_id != OSRS_COMBAT_PROJECTILE_MISSING); +} + +static inline int osrs_combat_visual_projectile_count( + const OsrsCombatVisualRow* row +) { + if (!row || row->projectile.projectile_count < 1) return 1; + if (row->projectile.projectile_count > 4) return 4; + return row->projectile.projectile_count; +} + +static inline int osrs_combat_visual_show_impact_for_projectile( + const OsrsCombatVisualRow* effect, + int sequence_index, + int sequence_count +) { + if (!effect || !effect->impact_on_last_only) return 1; + return sequence_index == sequence_count - 1; +} + +static inline int osrs_combat_projectile_profile_has_timing( + const OsrsCombatProjectileProfile* profile +) { + return profile && + profile->projectile_start_height != OSRS_COMBAT_PROJECTILE_MISSING && + profile->projectile_end_height != OSRS_COMBAT_PROJECTILE_MISSING && + profile->projectile_delay != OSRS_COMBAT_PROJECTILE_MISSING && + profile->projectile_angle != OSRS_COMBAT_PROJECTILE_MISSING && + profile->projectile_progress != OSRS_COMBAT_PROJECTILE_MISSING && + profile->projectile_step_multiplier != OSRS_COMBAT_PROJECTILE_MISSING; +} + +static inline int osrs_combat_alt_projectile_profile_has_timing( + const OsrsCombatAltProjectileProfile* profile +) { + return profile && + profile->projectile_start_height != OSRS_COMBAT_PROJECTILE_MISSING && + profile->projectile_end_height != OSRS_COMBAT_PROJECTILE_MISSING && + profile->projectile_delay != OSRS_COMBAT_PROJECTILE_MISSING && + profile->projectile_angle != OSRS_COMBAT_PROJECTILE_MISSING && + profile->projectile_progress != OSRS_COMBAT_PROJECTILE_MISSING && + profile->projectile_step_multiplier != OSRS_COMBAT_PROJECTILE_MISSING; +} + +static inline void osrs_combat_projectile_apply_timing( + OsrsCombatProjectileProfile* out, + const OsrsCombatProjectileProfile* timing +) { + if (!out || !timing) return; + out->hit_delay = timing->hit_delay; + out->client_delay = timing->client_delay; + out->projectile_start_height = timing->projectile_start_height; + out->projectile_end_height = timing->projectile_end_height; + out->projectile_delay = timing->projectile_delay; + out->projectile_angle = timing->projectile_angle; + out->projectile_length_adjustment = timing->projectile_length_adjustment; + out->projectile_progress = timing->projectile_progress; + out->projectile_step_multiplier = timing->projectile_step_multiplier; +} + +static inline void osrs_combat_projectile_apply_alt_timing( + OsrsCombatProjectileProfile* out, + const OsrsCombatAltProjectileProfile* timing +) { + if (!out || !timing) return; + out->projectile_start_height = timing->projectile_start_height; + out->projectile_end_height = timing->projectile_end_height; + out->projectile_delay = timing->projectile_delay; + out->projectile_angle = timing->projectile_angle; + out->projectile_length_adjustment = timing->projectile_length_adjustment; + out->projectile_progress = timing->projectile_progress; + out->projectile_step_multiplier = timing->projectile_step_multiplier; +} + +static inline int osrs_combat_visual_build_projectile_sequence( + const OsrsCombatProjectileProfile* base, + const OsrsCombatVisualRow* effect, + OsrsCombatProjectileSequencePart* out, + int capacity +) { + if (!out || capacity < 0) abort(); + if (!base && !effect) return 0; + + int sequence_count = osrs_combat_visual_projectile_count(effect); + int out_count = 0; + const OsrsCombatProjectileProfile* timing = + effect && osrs_combat_projectile_profile_has_timing(&effect->projectile) + ? &effect->projectile + : base; + + for (int i = 0; i < sequence_count; i++) { + int show_impact = + osrs_combat_visual_show_impact_for_projectile(effect, i, sequence_count); + int use_alt = i > 0 && effect && + osrs_combat_alt_projectile_profile_has_timing(&effect->alt_projectile); + + if (effect && ( + effect->aux_travel_spotanim_id != OSRS_COMBAT_PROJECTILE_MISSING || + effect->aux_projectile_model_id != OSRS_COMBAT_PROJECTILE_MISSING)) { + if (out_count >= capacity) return -1; + OsrsCombatProjectileProfile aux = { + .launch_spotanim_id = i == 0 + ? effect->projectile.launch_spotanim_id + : OSRS_COMBAT_PROJECTILE_MISSING, + .travel_spotanim_id = effect->aux_travel_spotanim_id, + .impact_spotanim_id = show_impact + ? effect->aux_impact_spotanim_id + : OSRS_COMBAT_PROJECTILE_MISSING, + .projectile_model_id = effect->aux_projectile_model_id, + .projectile_anim_id = effect->aux_projectile_anim_id, + .hit_delay = OSRS_COMBAT_PROJECTILE_MISSING, + .client_delay = OSRS_COMBAT_PROJECTILE_MISSING, + .projectile_start_height = OSRS_COMBAT_PROJECTILE_MISSING, + .projectile_end_height = OSRS_COMBAT_PROJECTILE_MISSING, + .projectile_delay = OSRS_COMBAT_PROJECTILE_MISSING, + .projectile_angle = OSRS_COMBAT_PROJECTILE_MISSING, + .projectile_length_adjustment = OSRS_COMBAT_PROJECTILE_MISSING, + .projectile_progress = OSRS_COMBAT_PROJECTILE_MISSING, + .projectile_step_multiplier = OSRS_COMBAT_PROJECTILE_MISSING, + .projectile_count = (int16_t)sequence_count, + }; + osrs_combat_projectile_apply_timing(&aux, timing); + if (use_alt) + osrs_combat_projectile_apply_alt_timing(&aux, &effect->alt_projectile); + out[out_count++] = (OsrsCombatProjectileSequencePart){ + .projectile = aux, + .sequence_index = (int16_t)i, + .sequence_count = (int16_t)sequence_count, + }; + } + + if (!base) continue; + OsrsCombatProjectileProfile primary = *base; + if (timing) + osrs_combat_projectile_apply_timing(&primary, timing); + if (use_alt) + osrs_combat_projectile_apply_alt_timing(&primary, &effect->alt_projectile); + + if (effect && i == 0 && + effect->projectile.launch_spotanim_id != OSRS_COMBAT_PROJECTILE_MISSING && + effect->aux_travel_spotanim_id == OSRS_COMBAT_PROJECTILE_MISSING) { + primary.launch_spotanim_id = effect->projectile.launch_spotanim_id; + } else if (effect && i == 0 && + effect->double_launch_spotanim_id != OSRS_COMBAT_PROJECTILE_MISSING) { + primary.launch_spotanim_id = effect->double_launch_spotanim_id; + } else if (i > 0) { + primary.launch_spotanim_id = OSRS_COMBAT_PROJECTILE_MISSING; + } + if (!show_impact) + primary.impact_spotanim_id = OSRS_COMBAT_PROJECTILE_MISSING; + primary.projectile_count = sequence_count; + + if (out_count >= capacity) return -1; + out[out_count++] = (OsrsCombatProjectileSequencePart){ + .projectile = primary, + .sequence_index = (int16_t)i, + .sequence_count = (int16_t)sequence_count, + }; + } + return out_count; +} + +static inline int osrs_combat_visual_style_matches( + const OsrsCombatVisualRow* row, AttackStyle style +) { + return row->style == OSRS_COMBAT_VISUAL_STYLE_ANY || row->style == (int8_t)style; +} + +static inline int osrs_combat_visual_stance_matches( + const OsrsCombatVisualRow* row, int stance_idx +) { + return row->stance_idx < 0 || row->stance_idx == stance_idx; +} + +static inline int osrs_combat_visual_fight_style_stance_idx( + FightStyle fight_style +) { + switch (fight_style) { + case FIGHT_STYLE_ACCURATE: + case FIGHT_STYLE_AUTOCAST: + return 0; + case FIGHT_STYLE_AGGRESSIVE: + case FIGHT_STYLE_RAPID: + return 1; + case FIGHT_STYLE_CONTROLLED: + return 2; + case FIGHT_STYLE_DEFENSIVE: + case FIGHT_STYLE_LONGRANGE: + case FIGHT_STYLE_DEFENSIVE_AUTOCAST: + return 3; + default: + fprintf(stderr, "unknown fight style: %d\n", fight_style); + abort(); + } +} + +static inline int osrs_combat_visual_key_matches( + const OsrsCombatVisualRow* row, + int kind, + int32_t key_id, + const char* key_name +) { + if (row->kind != kind) return 0; + if (key_id != OSRS_COMBAT_PROJECTILE_MISSING && row->key_id == key_id) return 1; + return key_name && key_name[0] && row->key_name && + strcmp(row->key_name, key_name) == 0; +} + +static inline const OsrsCombatVisualRow* osrs_combat_visual_find_row_in_table( + const OsrsCombatVisualRow* rows, + size_t row_count, + int kind, + int32_t key_id, + const char* key_name, + AttackStyle style, + int stance_idx, + int require_attack_anim, + int require_projectile +) { + const OsrsCombatVisualRow* fallback = NULL; + const OsrsCombatVisualRow* style_fallback = NULL; + const OsrsCombatVisualRow* stance_fallback = NULL; + for (size_t i = 0; i < row_count; i++) { + const OsrsCombatVisualRow* row = &rows[i]; + if (!osrs_combat_visual_key_matches(row, kind, key_id, key_name)) continue; + if (!osrs_combat_visual_style_matches(row, style)) continue; + if (!osrs_combat_visual_stance_matches(row, stance_idx)) continue; + if (require_attack_anim && + row->attack_anim_id == OSRS_COMBAT_VISUAL_NO_ANIMATION) { + continue; + } + if (require_projectile && !osrs_combat_visual_row_has_projectile(row)) { + continue; + } + int exact_style = row->style == (int8_t)style; + int any_style = row->style == OSRS_COMBAT_VISUAL_STYLE_ANY; + int exact_stance = stance_idx >= 0 && row->stance_idx == stance_idx; + int any_stance = row->stance_idx < 0; + if (exact_style && exact_stance) return row; + if (!style_fallback && exact_style && any_stance) + style_fallback = row; + if (!stance_fallback && any_style && exact_stance) + stance_fallback = row; + if (!fallback && any_stance) + fallback = row; + } + return style_fallback ? style_fallback + : (stance_fallback ? stance_fallback : fallback); +} + +static inline const OsrsCombatVisualRow* osrs_combat_visual_find_row( + int kind, + int32_t key_id, + const char* key_name, + AttackStyle style, + int stance_idx, + int require_attack_anim, + int require_projectile +) { + const OsrsCombatVisualRow* colosseum_row = + osrs_combat_visual_find_row_in_table( + OSRS_COMBAT_VISUAL_COLOSSEUM_ROWS, + OSRS_COMBAT_VISUAL_COLOSSEUM_ROW_COUNT, + kind, key_id, key_name, style, stance_idx, + require_attack_anim, require_projectile); + if (colosseum_row) return colosseum_row; + return osrs_combat_visual_find_row_in_table( + OSRS_COMBAT_VISUAL_ROWS, + OSRS_COMBAT_VISUAL_ROW_COUNT, + kind, key_id, key_name, style, stance_idx, + require_attack_anim, require_projectile); +} + +static inline const OsrsCombatVisualRow* osrs_combat_visual_find_item_id_stance( + uint16_t item_id, AttackStyle style, int stance_idx +) { + return osrs_combat_visual_find_row( + OSRS_COMBAT_VISUAL_KIND_ITEM, item_id, NULL, style, stance_idx, 1, 0); +} + +static inline const OsrsCombatVisualRow* osrs_combat_visual_find_item_projectile_id( + uint16_t item_id, AttackStyle style +) { + return osrs_combat_visual_find_row( + OSRS_COMBAT_VISUAL_KIND_ITEM, item_id, NULL, style, + OSRS_COMBAT_VISUAL_STANCE_ANY, 0, 1); +} + +static inline const OsrsCombatVisualRow* osrs_combat_visual_find_special_item_id( + uint16_t item_id, AttackStyle style +) { + return osrs_combat_visual_find_row( + OSRS_COMBAT_VISUAL_KIND_SPECIAL, item_id, NULL, style, + OSRS_COMBAT_VISUAL_STANCE_ANY, 1, 0); +} + +static inline const OsrsCombatVisualRow* osrs_combat_visual_find_special_projectile_item_id( + uint16_t item_id, AttackStyle style +) { + return osrs_combat_visual_find_row( + OSRS_COMBAT_VISUAL_KIND_SPECIAL, item_id, NULL, style, + OSRS_COMBAT_VISUAL_STANCE_ANY, 0, 1); +} + +static inline const OsrsCombatVisualRow* osrs_combat_visual_find_npc_id( + uint16_t npc_id, AttackStyle style +) { + return osrs_combat_visual_find_row( + OSRS_COMBAT_VISUAL_KIND_NPC, npc_id, NULL, style, + OSRS_COMBAT_VISUAL_STANCE_ANY, 0, 0); +} + +static inline const char* osrs_combat_visual_spell_name(int spell_type) { + switch (spell_type) { + case OSRS_COMBAT_VISUAL_SPELL_ICE_BARRAGE: return "Ice Barrage"; + case OSRS_COMBAT_VISUAL_SPELL_BLOOD_BARRAGE: return "Blood Barrage"; + default: return NULL; + } +} + +static inline const OsrsCombatVisualRow* osrs_combat_visual_find_spell( + int spell_type +) { + return osrs_combat_visual_find_row( + OSRS_COMBAT_VISUAL_KIND_SPELL, + spell_type, + osrs_combat_visual_spell_name(spell_type), + ATTACK_STYLE_MAGIC, + OSRS_COMBAT_VISUAL_STANCE_ANY, + 0, + 1); +} + +static inline const OsrsCombatProjectileProfile* osrs_combat_visual_spell_projectile( + int spell_type +) { + const OsrsCombatVisualRow* row = osrs_combat_visual_find_spell(spell_type); + return row ? &row->projectile : NULL; +} + +static inline int osrs_combat_visual_special_fallback_anim(uint16_t item_id) { + for (size_t i = 0; + i < sizeof(OSRS_COMBAT_SPECIAL_FALLBACKS) / + sizeof(OSRS_COMBAT_SPECIAL_FALLBACKS[0]); + i++) { + if (OSRS_COMBAT_SPECIAL_FALLBACKS[i].item_id == item_id) { + return OSRS_COMBAT_SPECIAL_FALLBACKS[i].special_attack_anim_id; + } + } + return OSRS_COMBAT_VISUAL_NO_ANIMATION; +} + +static inline int osrs_combat_visual_weapon_attack_anim_for_stance( + uint8_t item_db_idx, + AttackStyle style, + int stance_idx, + int is_special, + int fallback_anim_id +) { + if (item_db_idx >= NUM_ITEMS) return fallback_anim_id; + uint16_t item_id = ITEM_DATABASE[item_db_idx].item_id; + if (is_special) { + const OsrsCombatVisualRow* special = + osrs_combat_visual_find_special_item_id(item_id, style); + if (special) return special->attack_anim_id; + int fallback_special = osrs_combat_visual_special_fallback_anim(item_id); + if (fallback_special != OSRS_COMBAT_VISUAL_NO_ANIMATION) + return fallback_special; + } + const OsrsCombatVisualRow* row = + osrs_combat_visual_find_item_id_stance(item_id, style, stance_idx); + if (!row) return fallback_anim_id; + return row->attack_anim_id; +} + +static inline int osrs_combat_visual_weapon_attack_anim_for_fight_style( + uint8_t item_db_idx, + AttackStyle style, + FightStyle fight_style, + int is_special, + int fallback_anim_id +) { + return osrs_combat_visual_weapon_attack_anim_for_stance( + item_db_idx, style, osrs_combat_visual_fight_style_stance_idx(fight_style), + is_special, fallback_anim_id); +} + +static inline const OsrsCombatWeaponProjectileDefault* +osrs_combat_visual_default_ranged_projectile(uint16_t weapon_item_id) { + for (size_t i = 0; + i < sizeof(OSRS_RANGED_PROJECTILE_DEFAULTS) / + sizeof(OSRS_RANGED_PROJECTILE_DEFAULTS[0]); + i++) { + if (OSRS_RANGED_PROJECTILE_DEFAULTS[i].weapon_item_id == weapon_item_id) { + return &OSRS_RANGED_PROJECTILE_DEFAULTS[i]; + } + } + return NULL; +} + +static inline const OsrsCombatProjectileProfile* osrs_combat_projectile_profile( + OsrsCombatProjectileVisual visual +) { + const OsrsCombatVisualRow* row = NULL; + switch (visual) { + case OSRS_COMBAT_PROJECTILE_NONE: + return NULL; + case OSRS_COMBAT_PROJECTILE_BOLT: + row = osrs_combat_visual_find_item_projectile_id( + OSRS_ITEM_ID_DIAMOND_BOLTS_E, ATTACK_STYLE_RANGED); + break; + case OSRS_COMBAT_PROJECTILE_RUNE_ARROW: + row = osrs_combat_visual_find_item_projectile_id( + OSRS_ITEM_ID_RUNE_ARROW, ATTACK_STYLE_RANGED); + break; + case OSRS_COMBAT_PROJECTILE_DRAGON_ARROW: + row = osrs_combat_visual_find_item_projectile_id( + OSRS_ITEM_ID_DRAGON_ARROWS, ATTACK_STYLE_RANGED); + break; + case OSRS_COMBAT_PROJECTILE_DRAGON_DART: + row = osrs_combat_visual_find_item_projectile_id( + OSRS_ITEM_ID_DRAGON_DART, ATTACK_STYLE_RANGED); + break; + case OSRS_COMBAT_PROJECTILE_TRIDENT: + return &OSRS_POWERED_STAFF_PROJECTILE_PROFILE; + default: + fprintf(stderr, "unknown combat projectile visual: %d\n", visual); + abort(); + } + if (!row) { + fprintf(stderr, "missing generated combat projectile visual: %d\n", visual); + abort(); + } + return &row->projectile; +} + +static inline const OsrsCombatProjectileProfile* osrs_combat_visual_ranged_projectile_profile( + uint8_t item_db_idx, OsrsCombatProjectileVisual fallback +) { + if (item_db_idx >= NUM_ITEMS) return osrs_combat_projectile_profile(fallback); + uint16_t item_id = ITEM_DATABASE[item_db_idx].item_id; + const OsrsCombatVisualRow* item_projectile = + osrs_combat_visual_find_item_projectile_id(item_id, ATTACK_STYLE_RANGED); + if (item_projectile) return &item_projectile->projectile; + const OsrsCombatWeaponProjectileDefault* default_projectile = + osrs_combat_visual_default_ranged_projectile(item_id); + if (default_projectile) { + const OsrsCombatVisualRow* projectile = + osrs_combat_visual_find_item_projectile_id( + default_projectile->projectile_item_id, ATTACK_STYLE_RANGED); + if (projectile) return &projectile->projectile; + } + return osrs_combat_projectile_profile(fallback); +} + +static inline const OsrsCombatProjectileProfile* osrs_combat_visual_ranged_special_projectile_profile( + uint8_t item_db_idx +) { + if (item_db_idx >= NUM_ITEMS) return NULL; + const OsrsCombatVisualRow* special = + osrs_combat_visual_find_special_projectile_item_id( + ITEM_DATABASE[item_db_idx].item_id, ATTACK_STYLE_RANGED); + return special ? &special->projectile : NULL; +} + +static inline int osrs_combat_visual_item_is_powered_staff(uint16_t item_id) { + for (size_t i = 0; + i < sizeof(OSRS_POWERED_STAFF_ITEMS) / sizeof(OSRS_POWERED_STAFF_ITEMS[0]); + i++) { + if (OSRS_POWERED_STAFF_ITEMS[i] == item_id) return 1; + } + return 0; +} + +static inline OsrsCombatProjectileVisual osrs_combat_visual_magic_projectile( + uint8_t item_db_idx +) { + if (item_db_idx >= NUM_ITEMS) return OSRS_COMBAT_PROJECTILE_NONE; + return osrs_combat_visual_item_is_powered_staff( + ITEM_DATABASE[item_db_idx].item_id) + ? OSRS_COMBAT_PROJECTILE_TRIDENT + : OSRS_COMBAT_PROJECTILE_NONE; +} + +static inline const OsrsCombatProjectileProfile* osrs_combat_visual_magic_projectile_profile( + uint8_t item_db_idx +) { + if (item_db_idx >= NUM_ITEMS) { + return osrs_combat_projectile_profile(OSRS_COMBAT_PROJECTILE_NONE); + } + uint16_t item_id = ITEM_DATABASE[item_db_idx].item_id; + const OsrsCombatVisualRow* item_projectile = + osrs_combat_visual_find_item_projectile_id(item_id, ATTACK_STYLE_MAGIC); + if (item_projectile) return &item_projectile->projectile; + if (item_id == OSRS_ITEM_ID_TUMEKENS_SHADOW) + return &OSRS_TUMEKENS_SHADOW_PROJECTILE_PROFILE; + return osrs_combat_projectile_profile( + osrs_combat_visual_magic_projectile(item_db_idx)); +} + +static inline int osrs_combat_visual_magic_attack_anim_for_fight_style( + uint8_t item_db_idx, FightStyle fight_style, int is_special, int fallback_anim_id +) { + if (item_db_idx >= NUM_ITEMS) return fallback_anim_id; + if (!osrs_combat_visual_item_is_powered_staff( + ITEM_DATABASE[item_db_idx].item_id)) { + return fallback_anim_id; + } + int anim = osrs_combat_visual_weapon_attack_anim_for_fight_style( + item_db_idx, ATTACK_STYLE_MAGIC, fight_style, is_special, + OSRS_COMBAT_VISUAL_NO_ANIMATION); + if (anim != OSRS_COMBAT_VISUAL_NO_ANIMATION) return anim; + if (fight_style != FIGHT_STYLE_AUTOCAST) { + anim = osrs_combat_visual_weapon_attack_anim_for_fight_style( + item_db_idx, ATTACK_STYLE_MAGIC, FIGHT_STYLE_AUTOCAST, is_special, + OSRS_COMBAT_VISUAL_NO_ANIMATION); + if (anim != OSRS_COMBAT_VISUAL_NO_ANIMATION) return anim; + } + return OSRS_PLAYER_POWERED_STAFF_ATTACK_ANIM; +} + +#endif diff --git a/ocean/osrs/osrs_combat_visuals_generated.h b/ocean/osrs/osrs_combat_visuals_generated.h new file mode 100644 index 0000000000..07409c549d --- /dev/null +++ b/ocean/osrs/osrs_combat_visuals_generated.h @@ -0,0 +1,1245 @@ +#ifndef OSRS_COMBAT_VISUALS_GENERATED_H +#define OSRS_COMBAT_VISUALS_GENERATED_H + +static const OsrsCombatVisualRow OSRS_COMBAT_VISUAL_ROWS[] = { + {OSRS_COMBAT_VISUAL_KIND_ITEM, 35, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 78, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {25, 16, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 667, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 746, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 747, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 767, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 772, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 800, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 800, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {43, 36, OSRS_COMBAT_PROJECTILE_MISSING, 3148, 372, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 801, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 801, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {42, 35, OSRS_COMBAT_PROJECTILE_MISSING, 3148, 372, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 802, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 802, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {44, 37, OSRS_COMBAT_PROJECTILE_MISSING, 3148, 372, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 803, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 803, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {45, 38, OSRS_COMBAT_PROJECTILE_MISSING, 3148, 372, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 804, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 804, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {46, 39, OSRS_COMBAT_PROJECTILE_MISSING, 3148, 372, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 805, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 805, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {48, 41, OSRS_COMBAT_PROJECTILE_MISSING, 3148, 372, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 806, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 806, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {232, 226, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 807, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 807, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {233, 227, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 808, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 808, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {234, 228, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 809, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 809, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {235, 229, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 810, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 810, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {236, 230, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 811, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 811, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {237, 231, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 812, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 812, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {232, 226, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 813, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 813, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {233, 227, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 814, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 814, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {234, 228, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 815, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 815, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {235, 229, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 816, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 816, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {236, 230, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 817, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 817, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {237, 231, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 825, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 200, OSRS_COMBAT_PROJECTILE_MISSING, 3144, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 826, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 201, OSRS_COMBAT_PROJECTILE_MISSING, 3144, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 827, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 202, OSRS_COMBAT_PROJECTILE_MISSING, 3144, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 828, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 203, OSRS_COMBAT_PROJECTILE_MISSING, 3144, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 829, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 204, OSRS_COMBAT_PROJECTILE_MISSING, 3144, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 830, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 205, OSRS_COMBAT_PROJECTILE_MISSING, 3144, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 837, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 839, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 841, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 843, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 845, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 847, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 849, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 851, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 853, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 855, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 857, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 859, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 861, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 863, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 863, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {220, 213, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 864, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 864, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {219, 212, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 865, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 865, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {221, 214, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 866, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 866, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {223, 216, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 867, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 867, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {224, 217, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 868, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 868, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {225, 218, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 869, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 869, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {222, 215, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 870, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 870, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {219, 212, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 871, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 871, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {220, 213, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 872, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 872, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {221, 214, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 873, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 873, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {223, 216, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 874, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 874, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {222, 215, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 875, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 875, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {224, 217, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 876, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 876, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {225, 218, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 877, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 878, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 879, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 880, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 882, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {19, 10, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 883, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {19, 10, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 884, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {18, 9, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 885, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {18, 9, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 886, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {20, 11, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 887, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {20, 11, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 888, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {21, 12, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 889, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {21, 12, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 890, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {22, 13, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 891, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {22, 13, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 892, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {24, 15, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 893, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {24, 15, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 975, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1203, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1205, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1207, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1209, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1211, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1213, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1215, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 376, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1217, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1219, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1221, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1223, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1225, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1227, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1229, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1231, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 376, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1233, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1237, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1239, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1241, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1243, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1245, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1247, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1249, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 381, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1251, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1253, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1255, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1257, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1259, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1261, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1263, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 381, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1265, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1267, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1269, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1271, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1273, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1275, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1277, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1279, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1281, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1283, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1285, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1287, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1289, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1291, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1293, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1295, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1297, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1299, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1301, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1303, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1305, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1307, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1309, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1311, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1313, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1315, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1317, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1319, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1321, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1323, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1325, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1327, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1329, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1331, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1333, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1335, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1337, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1339, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1341, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1343, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1345, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1347, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1349, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1351, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1353, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1355, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1357, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1359, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1361, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1363, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1365, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1367, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1369, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1371, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1373, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1375, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1377, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1379, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1381, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1383, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1385, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1387, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1389, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1391, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1393, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1395, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1397, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1399, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1401, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1403, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1405, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1407, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1409, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1419, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 440, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1420, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1422, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1424, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1426, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1428, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1430, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1432, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 1434, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2402, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2415, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2416, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2417, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2460, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 400, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2462, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 400, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2464, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 400, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2466, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 400, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2468, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 400, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2470, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 400, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2472, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 400, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2474, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 400, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2476, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 400, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2866, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {243, 242, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2883, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2952, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 6095, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2961, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 2963, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3053, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3054, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3093, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3093, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {273, 34, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3094, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3094, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {273, 34, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3095, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 393, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3096, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 393, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3097, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 393, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3098, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 393, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3099, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 393, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3100, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 393, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3101, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 393, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3170, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3171, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3172, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3173, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3174, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3175, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3176, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 381, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3190, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3192, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3194, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3196, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3198, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3200, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3202, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3204, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3689, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3691, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3695, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3757, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 3899, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4037, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4039, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4086, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 400, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4151, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1658, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4153, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1665, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4158, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4160, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {325, 326, OSRS_COMBAT_PROJECTILE_MISSING, 5115, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4170, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4212, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4214, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4215, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4216, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4217, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4218, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4219, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4220, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4221, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4222, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4223, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4236, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4565, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4566, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1833, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4580, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4582, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4584, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4587, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4599, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4600, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4675, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4710, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2078, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4718, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2067, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4726, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2080, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4734, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2075, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4740, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 993, OSRS_COMBAT_PROJECTILE_MISSING, 19799, 5477, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4747, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2068, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4755, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2062, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4827, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4862, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2078, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4863, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2078, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4864, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2078, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4865, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2078, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4886, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2067, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4887, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2067, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4888, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2067, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4889, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2067, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4910, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2080, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4911, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2080, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4912, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2080, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4913, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2080, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4934, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2075, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4935, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2075, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4936, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2075, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4937, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2075, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4958, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2068, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4959, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2068, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4960, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2068, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4961, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2068, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4982, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2062, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4983, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2062, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4984, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2062, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 4985, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2062, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5016, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5018, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5616, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {19, 10, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5617, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {18, 9, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5618, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {20, 11, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5619, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {21, 12, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5620, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {22, 13, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5621, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {24, 15, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5622, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {19, 10, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5623, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {18, 9, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5624, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {20, 11, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5625, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {21, 12, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5626, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {22, 13, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5627, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {24, 15, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5628, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5628, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {232, 226, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5629, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5629, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {233, 227, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5630, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5630, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {234, 228, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5631, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5631, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {273, 34, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5632, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5632, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {235, 229, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5633, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5633, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {236, 230, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5634, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5634, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {237, 231, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5635, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5635, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {232, 226, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5636, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5636, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {233, 227, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5637, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5637, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {234, 228, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5638, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5638, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {273, 34, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5639, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5639, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {235, 229, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5640, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5640, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {236, 230, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5641, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5641, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {237, 231, OSRS_COMBAT_PROJECTILE_MISSING, 3131, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5654, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5654, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {219, 212, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5655, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5655, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {220, 213, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5656, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5656, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {221, 214, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5657, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5657, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {223, 216, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5658, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5658, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {222, 215, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5659, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5659, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {224, 217, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5660, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5660, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {225, 218, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5661, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5661, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {219, 212, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5662, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5662, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {220, 213, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5663, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5663, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {221, 214, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5664, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5664, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {223, 216, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5665, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5665, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {222, 215, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5666, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5666, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {224, 217, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5667, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5667, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {225, 218, OSRS_COMBAT_PROJECTILE_MISSING, 3139, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5668, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5670, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5672, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5674, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5676, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5678, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5680, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 376, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5682, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5686, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5688, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5690, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5692, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5694, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5696, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5698, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 376, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5700, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5704, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5706, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5708, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5710, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5712, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5714, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5716, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 381, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5718, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5720, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5722, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5724, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5726, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5728, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5730, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 381, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5734, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 5736, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6082, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2326, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6125, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6126, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6127, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6313, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6315, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6317, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6408, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6410, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6412, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6414, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6416, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6418, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6420, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6522, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7558, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6522, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 442, OSRS_COMBAT_PROJECTILE_MISSING, 14291, 2615, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6523, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6525, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6526, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 429, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6527, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6528, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2661, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6541, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1658, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6562, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6563, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6587, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 393, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6589, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6591, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6593, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6595, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6597, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6599, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6601, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6603, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6605, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6607, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6609, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6611, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6613, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6724, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6739, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6745, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6746, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6760, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6762, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6764, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6773, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6774, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6775, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6776, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6777, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6778, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6779, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6908, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6910, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6912, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 6914, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7140, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7141, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7142, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7158, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7170, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2779, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7409, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7433, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7435, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7437, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7439, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7441, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7443, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7445, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7447, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7449, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7451, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7639, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7640, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7641, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7642, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7643, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7644, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7645, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7646, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7647, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7648, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7668, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1665, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 7675, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8650, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8652, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8654, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8656, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8658, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8660, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8662, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8664, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8666, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8668, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8670, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8672, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8674, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8676, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8678, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8680, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8872, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8874, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8876, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8878, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8880, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8882, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 696, OSRS_COMBAT_PROJECTILE_MISSING, 15569, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8966, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8967, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8968, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8969, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8970, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 8971, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9013, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9044, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9046, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9048, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9050, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9084, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9091, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9092, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9093, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9140, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9141, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9142, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9143, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9144, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9174, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9176, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9177, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9179, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9181, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9183, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9185, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9236, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9237, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9238, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9240, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9241, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9242, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9243, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9244, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9245, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9287, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9288, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9289, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9290, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9291, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9294, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9295, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9296, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9297, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9298, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9301, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9302, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9303, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9304, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9305, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9335, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9337, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9338, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9339, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9340, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9341, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9342, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 27, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9703, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 9705, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10010, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10023, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10024, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10033, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7618, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 21, 15, 11, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10033, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 908, OSRS_COMBAT_PROJECTILE_MISSING, 19461, 5186, 2, 2, 163, 146, 21, 15, 11, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10034, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7618, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 21, 15, 11, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10034, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 909, OSRS_COMBAT_PROJECTILE_MISSING, 19461, 5186, 2, 2, 163, 146, 21, 15, 11, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10129, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10146, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 5247, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10147, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 5247, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10148, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 5247, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10149, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 5247, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10150, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10156, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10280, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10282, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10284, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10440, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10442, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10444, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10487, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 5439, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10491, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10501, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10581, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 376, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10582, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 376, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10583, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 376, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10584, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 376, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10858, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 10887, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 5865, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11037, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11061, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11165, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11167, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11212, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1116, 1120, OSRS_COMBAT_PROJECTILE_MISSING, 26377, 6622, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11227, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1116, 1120, OSRS_COMBAT_PROJECTILE_MISSING, 26377, 6622, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11228, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1116, 1120, OSRS_COMBAT_PROJECTILE_MISSING, 26377, 6622, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11229, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1116, 1120, OSRS_COMBAT_PROJECTILE_MISSING, 26377, 6622, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11230, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11230, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1123, 1122, OSRS_COMBAT_PROJECTILE_MISSING, 26379, 6622, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11231, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11231, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1123, 1122, OSRS_COMBAT_PROJECTILE_MISSING, 26379, 6622, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11233, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11233, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1123, 1122, OSRS_COMBAT_PROJECTILE_MISSING, 26379, 6622, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11234, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7554, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11234, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1123, 1122, OSRS_COMBAT_PROJECTILE_MISSING, 26379, 6622, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11235, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11259, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11367, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11369, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11371, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11373, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11375, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11377, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11379, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11381, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11382, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11384, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11386, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11388, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11389, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11391, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11393, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11395, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11396, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11398, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11400, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11402, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11403, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11405, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11407, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11409, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11410, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11412, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11414, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11416, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11417, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11419, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11705, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 3678, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11706, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 3678, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11707, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11708, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11709, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11711, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11719, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11720, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11721, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11748, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11749, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11750, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11751, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11752, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11753, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11754, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11755, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11756, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11757, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11758, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11785, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11787, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11789, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11791, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11802, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11804, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11806, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11808, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11824, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1711, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11838, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11889, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 381, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11891, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11892, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11902, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11905, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11907, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11908, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11920, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11959, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7618, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 21, 15, 11, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11959, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1272, OSRS_COMBAT_PROJECTILE_MISSING, 19461, 5186, 2, 2, 163, 146, 21, 15, 11, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11998, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12000, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12006, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1658, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12199, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12263, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12275, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12297, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12357, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12373, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12375, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12377, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12379, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12389, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12422, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12424, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12439, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12658, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12727, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2323, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12765, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12766, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12767, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12768, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12773, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1658, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12774, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1658, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12788, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12795, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12796, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12797, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12808, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12809, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12848, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1665, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12899, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12900, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12902, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12904, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 12926, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 5061, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13074, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13075, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13076, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13077, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13078, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13079, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13080, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13081, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13082, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13083, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13084, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13085, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13086, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13087, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13088, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13089, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13090, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13108, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13109, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13110, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13111, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13141, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13142, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13143, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13144, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13241, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13242, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13243, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13244, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13263, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 3298, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13265, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 3297, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13267, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 3297, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13269, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 3297, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13271, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 3297, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13328, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13576, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 13652, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 393, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 19478, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7555, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 19481, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7555, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 19484, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1301, OSRS_COMBAT_PROJECTILE_MISSING, 31497, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 19675, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 19912, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 19918, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2068, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20000, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20011, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20014, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20056, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20155, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20158, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20161, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20243, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20251, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20254, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20257, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20260, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20263, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20368, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20370, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20372, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20374, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20397, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20401, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20402, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20403, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20405, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1658, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20406, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20407, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 376, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20408, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20431, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20552, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20553, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20554, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20555, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20556, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20557, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1665, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20558, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20559, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 407, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20560, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20590, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1833, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20593, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20720, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20727, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7004, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20730, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20733, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20736, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20739, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20756, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2661, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20779, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7328, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20784, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 393, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20785, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20836, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7275, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20849, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20849, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1320, 1319, OSRS_COMBAT_PROJECTILE_MISSING, 32776, 372, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 20997, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21003, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7516, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21006, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21009, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21012, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21015, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7511, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21028, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21031, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21033, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21198, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21200, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21205, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7516, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21206, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21207, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7617, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21209, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7541, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21255, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21276, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21326, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1385, 1384, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21332, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1385, 1384, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21334, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1385, 1384, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21336, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1385, 1384, OSRS_COMBAT_PROJECTILE_MISSING, 3136, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21646, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21742, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21902, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21905, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21924, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21926, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21928, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21932, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21934, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21936, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21938, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21940, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21942, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21944, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21946, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21948, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 21950, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 1468, OSRS_COMBAT_PROJECTILE_MISSING, 3135, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22288, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22290, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22292, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22294, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22296, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22316, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7328, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22323, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22324, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8145, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22325, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8056, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22333, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22335, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22368, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22370, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 419, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22398, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8010, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22433, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22435, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22481, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22486, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8056, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22516, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22542, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 245, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22545, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 245, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22547, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22550, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22552, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1058, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22555, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1058, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22610, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22613, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22622, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22634, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 929, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22634, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1624, 1623, OSRS_COMBAT_PROJECTILE_MISSING, 35977, 372, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22636, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 929, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22636, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {1619, 1620, OSRS_COMBAT_PROJECTILE_MISSING, 35982, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22647, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22719, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7328, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22731, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22734, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22737, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22740, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22743, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22804, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8194, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22804, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 28, OSRS_COMBAT_PROJECTILE_MISSING, 36301, 375, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22806, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8194, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22806, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 697, OSRS_COMBAT_PROJECTILE_MISSING, 36301, 375, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22808, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8194, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22808, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 697, OSRS_COMBAT_PROJECTILE_MISSING, 36301, 375, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22810, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8195, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22810, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 697, OSRS_COMBAT_PROJECTILE_MISSING, 36301, 375, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22812, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8194, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22814, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8194, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22816, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22817, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 22978, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8288, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23206, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23235, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2661, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23276, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23279, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23282, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 406, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23330, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23332, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23334, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23342, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23357, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23360, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23446, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7275, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23458, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23528, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23601, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23611, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23613, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23615, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23617, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23619, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 929, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23620, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23626, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23628, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8145, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23630, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7555, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23653, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2078, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23673, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23675, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23677, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23680, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23682, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23762, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23764, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23821, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23822, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23823, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23849, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23850, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23851, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23852, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23853, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23854, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23855, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23856, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23857, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23862, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23863, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23864, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23895, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23896, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23897, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23898, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23899, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23900, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23901, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23902, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23903, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23983, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23985, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23987, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23989, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23995, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 23997, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24000, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24123, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24125, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24144, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24219, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8288, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24225, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1665, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24227, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1665, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24327, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24372, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24374, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24376, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24395, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24413, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24417, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 4503, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24422, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 4505, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24423, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 4505, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24424, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 4505, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24425, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 4505, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24537, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8640, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24539, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8640, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24551, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24617, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24693, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24695, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24697, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 24699, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8010, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25013, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25042, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25044, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25046, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25059, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25063, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25066, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25110, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25112, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25114, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25314, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7275, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25367, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25369, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25371, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25373, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 386, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25376, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25378, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 395, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25516, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2067, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25517, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 4505, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25604, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7511, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25644, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25721, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7046, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25723, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7046, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25724, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7046, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25725, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7046, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25726, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7046, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25727, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7046, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25728, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7046, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25729, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7046, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25731, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25733, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25734, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8145, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25736, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8056, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25738, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8056, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25739, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8056, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25741, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8056, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25865, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25867, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25870, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25872, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25874, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25876, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25878, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25880, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25882, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25884, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25886, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25888, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25890, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25892, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25894, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25896, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25916, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25918, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25987, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 25989, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26219, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 9471, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26233, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26260, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 422, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26374, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 9168, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26424, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 9270, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26482, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1658, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26484, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1658, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26486, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7552, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26511, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26513, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26515, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26517, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 4505, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26649, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 9347, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26708, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 393, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26710, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26712, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7555, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26945, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26948, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 26950, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27100, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7516, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27157, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 8194, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27184, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27186, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 9168, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27188, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 7555, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27246, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 9471, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27275, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 9493, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27277, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 9493, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27610, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 9858, {2289, 2291, OSRS_COMBAT_PROJECTILE_MISSING, 46993, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27612, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 9858, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27652, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27655, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27657, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 245, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27660, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 245, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27662, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1058, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27665, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1058, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27679, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1058, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27690, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27788, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1058, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27853, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27855, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 5865, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27857, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27859, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27861, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 3297, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27863, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 3297, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27865, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 3297, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27867, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 3297, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27869, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27900, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27904, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 390, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27908, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 401, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27912, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 929, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27916, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 929, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 27920, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 414, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 28338, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 10172, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 28688, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 10656, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 28834, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 5247, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 28919, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 10922, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 28922, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 10922, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 28988, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1711, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 28997, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 10989, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 29000, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 11057, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 29084, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 2068, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 29796, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 428, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 29849, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 1711, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 29851, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 11057, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 15, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 2215, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, 7021, {1203, 1202, 1203, 28083, 7022, 3, 3, 163, 146, 31, 16, 29, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 2217, "", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 7071, {1216, 1217, 1218, 28104, 7069, 3, 3, 144, 124, 30, 16, 6, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 2218, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, 7073, {1219, 1220, OSRS_COMBAT_PROJECTILE_MISSING, 28099, 7074, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 2266, "", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 86, {1, 54, OSRS_COMBAT_PROJECTILE_MISSING, 3154, 373, 3, 3, 172, 124, 41, 16, 0, 64, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 3127, "", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 2656, {439, 445, 446, 9335, 2648, 3, 3, 172, 124, 41, 16, 0, 64, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 3127, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, 2652, {440, 448, 451, 9337, 2659, 2, 2, 163, 146, 32, 15, 0, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 8061, "", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 7952, {OSRS_COMBAT_PROJECTILE_MISSING, 1479, 1480, 35033, 7884, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 8061, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, 7952, {OSRS_COMBAT_PROJECTILE_MISSING, 1477, 1478, 34658, 4500, 3, 3, 155, 146, 41, 5, 5, 11, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPECIAL, 1305, "", ATTACK_STYLE_MELEE, OSRS_COMBAT_PROJECTILE_MISSING, 1058, {248, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPECIAL, 11802, "", ATTACK_STYLE_MELEE, OSRS_COMBAT_PROJECTILE_MISSING, 7644, {1206, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPECIAL, 11804, "", ATTACK_STYLE_MELEE, OSRS_COMBAT_PROJECTILE_MISSING, 7642, {1208, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPECIAL, 11806, "", ATTACK_STYLE_MELEE, OSRS_COMBAT_PROJECTILE_MISSING, 7640, {1207, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPECIAL, 11808, "", ATTACK_STYLE_MELEE, OSRS_COMBAT_PROJECTILE_MISSING, 7638, {1205, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPECIAL, 12926, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, 5061, {1043, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Bind", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1161, {177, 178, 181, 3127, 703, 3, 3, 180, 0, 75, 16, -29, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_VISUAL_SPELL_BLOOD_BARRAGE, "Blood Barrage", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 377, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Blood Blitz", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, 374, 375, 6379, 1969, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Blood Burst", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 376, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Blood Rush", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, 372, 373, 6380, 1968, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Claws of Guthix", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1511, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Confuse", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1163, {102, 103, 104, 3093, 669, 3, 3, 144, 124, 61, 16, -15, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Crumble Undead", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1166, {145, 146, 147, 3109, 685, 3, 3, 124, 124, 46, 16, 0, 64, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Curse", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1165, {108, 109, 110, 3100, 675, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Dark Demonbane", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 8977, {1869, OSRS_COMBAT_PROJECTILE_MISSING, 1870, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Earth Blast", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1162, {138, 139, 140, 3087, 663, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Earth Bolt", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1162, {123, 124, 125, 3084, 661, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Earth Strike", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1162, {96, 97, 98, 3081, 659, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Earth Surge", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 7855, {1461, 1462, 1463, 34618, 7856, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Earth Wave", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {164, 165, 166, 3115, 692, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Enfeeble", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1168, {170, 171, 172, 3120, 697, 3, 3, 144, 124, 48, 16, -2, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Entangle", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1161, {177, 178, 179, 3127, 703, 3, 3, 180, 0, 75, 16, -29, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Fire Blast", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1162, {129, 130, 131, 3087, 663, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Fire Bolt", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1162, {126, 127, 128, 3084, 661, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Fire Strike", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1162, {99, 100, 101, 3081, 659, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Fire Surge", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 7855, {1464, 1465, 1466, 34618, 7856, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Fire Wave", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {155, 156, 157, 3115, 692, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Flames of Zamorak", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 78, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Ghostly Grasp", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 8972, {1856, OSRS_COMBAT_PROJECTILE_MISSING, 1857, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Greater Corruption", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 9158, {1878, OSRS_COMBAT_PROJECTILE_MISSING, 1880, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Iban Blast", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 708, {87, 88, 89, 3077, 656, 3, 3, 144, 124, 60, 16, -14, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_VISUAL_SPELL_ICE_BARRAGE, "Ice Barrage", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, 368, 369, 14215, 1964, 3, 3, 172, 0, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Ice Blitz", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, 362, 363, 6384, 1960, 3, 3, 172, 0, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Ice Burst", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, 366, 367, 1204, 1962, 3, 3, 172, 0, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Ice Rush", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, 360, 361, 6388, 1958, 3, 3, 172, 0, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Inferior Demonbane", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 8977, {1865, OSRS_COMBAT_PROJECTILE_MISSING, 1866, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Lesser Corruption", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 9158, {1877, OSRS_COMBAT_PROJECTILE_MISSING, 1879, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Magic Dart", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1576, {OSRS_COMBAT_PROJECTILE_MISSING, 328, 329, 5091, 1577, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Saradomin Strike", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 76, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Shadow Barrage", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 383, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Shadow Blitz", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, 380, 381, 6395, 1975, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Shadow Burst", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 382, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Shadow Rush", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, 378, 379, 6394, 1974, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Skeletal Grasp", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 8972, {1859, OSRS_COMBAT_PROJECTILE_MISSING, 1860, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Smoke Barrage", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, 390, 391, 6398, 1986, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Smoke Blitz", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, 386, 387, 6402, 1981, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Smoke Burst", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, 388, 389, 6399, 1984, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Smoke Rush", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {OSRS_COMBAT_PROJECTILE_MISSING, 384, 385, 6403, 1980, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Snare", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1161, {177, 178, 180, 3127, 703, 3, 3, 180, 0, 75, 16, -29, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Stun", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1169, {173, 174, 175, 3124, 700, 3, 3, 144, 124, 52, 16, -6, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Superior Demonbane", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 8977, {1867, OSRS_COMBAT_PROJECTILE_MISSING, 1868, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Tele Block", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1820, {OSRS_COMBAT_PROJECTILE_MISSING, 1300, 345, 5800, 1821, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Undead Grasp", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 8972, {1862, OSRS_COMBAT_PROJECTILE_MISSING, 1863, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 2, 2, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Vulnerability", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 811, {167, 168, 169, 3099, 675, 3, 3, 144, 124, 34, 16, 12, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Water Blast", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1162, {135, 136, 137, 3087, 663, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Water Bolt", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1162, {120, 121, 122, 3084, 661, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Water Strike", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1162, {93, 94, 95, 3081, 659, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Water Surge", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 7855, {1458, 1459, 1460, 34618, 7856, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Water Wave", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {161, 162, 163, 3115, 692, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Weaken", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1164, {105, 106, 107, 3096, 672, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Wind Blast", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1162, {132, 133, 134, 3087, 663, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Wind Bolt", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1162, {117, 118, 119, 3084, 661, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Wind Strike", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1162, {90, 91, 92, 3081, 659, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Wind Surge", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 7855, {1455, 1456, 1457, 34618, 7856, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPELL, OSRS_COMBAT_PROJECTILE_MISSING, "Wind Wave", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 1167, {158, 159, 160, 3115, 692, 3, 3, 172, 124, 51, 16, -5, 64, 10, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 7692, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, 7578, {OSRS_COMBAT_PROJECTILE_MISSING, 1374, OSRS_COMBAT_PROJECTILE_MISSING, 853342, 660, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 7693, "", ATTACK_STYLE_MELEE, OSRS_COMBAT_PROJECTILE_MISSING, 7582, {OSRS_COMBAT_PROJECTILE_MISSING, 1382, OSRS_COMBAT_PROJECTILE_MISSING, 33017, 7614, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 7693, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, 7583, {OSRS_COMBAT_PROJECTILE_MISSING, 1383, OSRS_COMBAT_PROJECTILE_MISSING, 853351, 366, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 7693, "", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 7581, {OSRS_COMBAT_PROJECTILE_MISSING, 1384, OSRS_COMBAT_PROJECTILE_MISSING, 853352, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 7698, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, 7605, {OSRS_COMBAT_PROJECTILE_MISSING, 1377, 1378, 33013, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 7699, "", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 7610, {OSRS_COMBAT_PROJECTILE_MISSING, 1376, OSRS_COMBAT_PROJECTILE_MISSING, 33007, 7571, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 7700, "", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 7592, {OSRS_COMBAT_PROJECTILE_MISSING, 448, OSRS_COMBAT_PROJECTILE_MISSING, 9337, 2659, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 7700, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, 7593, {OSRS_COMBAT_PROJECTILE_MISSING, 451, OSRS_COMBAT_PROJECTILE_MISSING, 9342, 2660, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 7706, "", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 7566, {OSRS_COMBAT_PROJECTILE_MISSING, 1375, OSRS_COMBAT_PROJECTILE_MISSING, 33006, 7571, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 7708, "", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, {OSRS_COMBAT_PROJECTILE_MISSING, 660, 659, 14759, 3942, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_NPC, 239, "", OSRS_COMBAT_VISUAL_STYLE_ANY, OSRS_COMBAT_PROJECTILE_MISSING, 86, {1, 54, OSRS_COMBAT_PROJECTILE_MISSING, 3154, 373, 3, 3, 172, 124, 41, 16, 0, 64, 5, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 31113, "", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 12397, {3366, 3367, 3368, 28450, 12398, 3, 3, 160, 120, OSRS_COMBAT_PROJECTILE_MISSING, 16, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPECIAL, 31113, "", ATTACK_STYLE_MAGIC, OSRS_COMBAT_PROJECTILE_MISSING, 12394, {3364, 3367, 3365, 28450, 12398, 3, 3, 160, 120, OSRS_COMBAT_PROJECTILE_MISSING, 16, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_SPECIAL, 11235, "", ATTACK_STYLE_RANGED, OSRS_COMBAT_PROJECTILE_MISSING, 426, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 3, 3, 163, 146, 41, 5, 5, 11, 5, 2}, {163, 146, 41, 25, 14, 11, 10}, 1101, 1103, 26393, 6590, 1, 1109}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11804, "", ATTACK_STYLE_MELEE, 0, 7045, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11804, "", ATTACK_STYLE_MELEE, 2, 7054, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING}, + {OSRS_COMBAT_VISUAL_KIND_ITEM, 11804, "", ATTACK_STYLE_MELEE, 3, 7055, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 1}, {OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING}, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, OSRS_COMBAT_PROJECTILE_MISSING, 0, OSRS_COMBAT_PROJECTILE_MISSING} +}; + +static const size_t OSRS_COMBAT_VISUAL_ROW_COUNT = + sizeof(OSRS_COMBAT_VISUAL_ROWS) / sizeof(OSRS_COMBAT_VISUAL_ROWS[0]); + +#endif diff --git a/ocean/osrs/osrs_consumables.h b/ocean/osrs/osrs_consumables.h new file mode 100644 index 0000000000..a72cde433c --- /dev/null +++ b/ocean/osrs/osrs_consumables.h @@ -0,0 +1,163 @@ +#ifndef OSRS_CONSUMABLES_H +#define OSRS_CONSUMABLES_H + +#include + +typedef enum { + FOOD_SHARK = 0, + FOOD_KARAMBWAN, + FOOD_MANTA_RAY, + FOOD_ANGLERFISH, + NUM_FOOD_TYPES +} FoodType; + +typedef enum { + POTION_PRAYER_RESTORE = 0, + POTION_SUPER_RESTORE, + POTION_ANTIVENOM_PLUS, + POTION_RANGING, + POTION_SUPER_COMBAT, + POTION_SATURATED_HEART, + POTION_SANFEW, + NUM_POTION_TYPES +} PotionType; + +typedef struct { + int hp_healed; + int consumed; +} EatResult; + +typedef struct { + int prayer_restored; + int level_boost; + int venom_cured; + int antivenom_ticks; + int consumed; +} DrinkResult; + +typedef struct { + int hp_healed; + int def_boost; + int att_drain; + int str_drain; + int range_drain; + int magic_drain; +} BrewResult; + +static inline int osrs_food_heal_amount(FoodType type) { + switch (type) { + case FOOD_SHARK: return 20; + case FOOD_KARAMBWAN: return 18; + case FOOD_MANTA_RAY: return 22; + case FOOD_ANGLERFISH: return 22; + default: return 0; + } +} + +static inline int osrs_saturated_heart_magic_boost(int base_magic) { + return 4 + base_magic / 10; +} + +static inline int osrs_super_restore_amount(int level) { + return 8 + level / 4; +} + +static inline int osrs_sanfew_restore_amount(int level) { + return 4 + level * 30 / 100; +} + +static inline int osrs_super_combat_boost_amount(int level) { + return 5 + level * 15 / 100; +} + +static inline int osrs_ranging_boost_amount(int level) { + return 4 + level / 10; +} + +static inline int osrs_brew_heal_amount(int base_hp) { + return base_hp * 15 / 100 + 2; +} + +static inline int osrs_brew_defence_boost_amount(int base_defence) { + return base_defence * 20 / 100 + 2; +} + +static inline EatResult osrs_eat_food(FoodType type, int current_hp, int max_hp, int food_timer) { + EatResult r = {0, 0}; + if (food_timer > 0) return r; + + int heal = osrs_food_heal_amount(type); + if (heal <= 0) return r; + + if (type == FOOD_ANGLERFISH) { + r.consumed = 1; + r.hp_healed = heal; + return r; + } + + if (current_hp >= max_hp) return r; + + r.consumed = 1; + r.hp_healed = heal; + if (current_hp + heal > max_hp) r.hp_healed = max_hp - current_hp; + return r; +} + +static inline DrinkResult osrs_drink_potion(PotionType type, int current_prayer, + int prayer_level, int potion_timer) { + DrinkResult r = {0, 0, 0, 0, 0}; + if (potion_timer > 0) return r; + + switch (type) { + case POTION_PRAYER_RESTORE: + if (current_prayer >= prayer_level) return r; + r.consumed = 1; + r.prayer_restored = 7 + prayer_level / 4; + break; + case POTION_SUPER_RESTORE: + if (current_prayer >= prayer_level) return r; + r.consumed = 1; + r.prayer_restored = osrs_super_restore_amount(prayer_level); + break; + case POTION_SANFEW: + r.consumed = 1; + r.prayer_restored = osrs_sanfew_restore_amount(prayer_level); + r.venom_cured = 1; + break; + case POTION_ANTIVENOM_PLUS: + r.consumed = 1; + r.venom_cured = 1; + r.antivenom_ticks = 300; + break; + case POTION_RANGING: + r.consumed = 1; + r.level_boost = osrs_ranging_boost_amount(prayer_level); + break; + case POTION_SUPER_COMBAT: + r.consumed = 1; + r.level_boost = osrs_super_combat_boost_amount(prayer_level); + break; + case POTION_SATURATED_HEART: + r.consumed = 1; + r.level_boost = osrs_saturated_heart_magic_boost(prayer_level); + break; + default: + break; + } + return r; +} + +static inline BrewResult osrs_brew_effect(int base_hp, int base_def, + int current_att, int current_str, + int current_range, int current_magic) { + BrewResult r; + r.hp_healed = osrs_brew_heal_amount(base_hp); + r.def_boost = osrs_brew_defence_boost_amount(base_def); + r.att_drain = current_att * 10 / 100 + 2; + r.str_drain = current_str * 10 / 100 + 2; + r.range_drain = current_range * 10 / 100 + 2; + r.magic_drain = current_magic * 10 / 100 + 2; + return r; +} + +#endif diff --git a/ocean/osrs/osrs_damage.h b/ocean/osrs/osrs_damage.h new file mode 100644 index 0000000000..4fb3d41b82 --- /dev/null +++ b/ocean/osrs/osrs_damage.h @@ -0,0 +1,43 @@ +#ifndef OSRS_DAMAGE_H +#define OSRS_DAMAGE_H + +#include "osrs_combat.h" +#include "osrs_items.h" + +typedef struct { + int final_damage; + int veng_damage; + int recoil_damage; + int smite_drain; + int prayer_blocked; + int elysian_reduced; +} DamageResult; + +/** Pure damage chain after prayer/mitigation: vengeance reflect, recoil reflect, + smite drain. Computes amounts only; the caller applies them to game state. */ +static inline DamageResult osrs_apply_post_mitigation_pipeline( + int mitigated_damage, + int prayer_blocked, + int target_veng_active, + int target_has_recoil, + int attacker_smite_active +) { + DamageResult r = {0, 0, 0, 0, prayer_blocked, 0}; + r.final_damage = mitigated_damage; + + if (target_veng_active && r.final_damage > 0) { + r.veng_damage = (int)(r.final_damage * 0.75f); + } + + if (target_has_recoil && r.final_damage > 0) { + r.recoil_damage = r.final_damage / 10 + 1; + } + + if (attacker_smite_active && r.final_damage > 0) { + r.smite_drain = r.final_damage / 4; + } + + return r; +} + +#endif /* OSRS_DAMAGE_H */ diff --git a/ocean/osrs/osrs_effects.h b/ocean/osrs/osrs_effects.h new file mode 100644 index 0000000000..4f6b49e56b --- /dev/null +++ b/ocean/osrs/osrs_effects.h @@ -0,0 +1,297 @@ +#ifndef OSRS_PVP_EFFECTS_H +#define OSRS_PVP_EFFECTS_H + +#include "osrs_models.h" +#include "osrs_anim.h" +#include "osrs_spotanims.h" +#include "osrs_gfx_ids.h" +#include + +/* sized for a full Sol Heredit shield slam (~160 one-shot dust puffs); PvP and + inferno use only a handful */ +#define MAX_ACTIVE_EFFECTS 384 + +static const OsrsSpotAnimDef* spotanim_lookup( + const OsrsSpotAnimSet* spotanims, + int gfx_id +) { + return osrs_spotanim_find(spotanims, gfx_id); +} + +static OsrsModel* effect_find_model_in_cache(ModelCache* cache, uint32_t model_id) { + if (!cache) return NULL; + return model_cache_get(cache, model_id); +} + +static OsrsModel* effect_find_model( + const OsrsSpotAnimDef* meta, + ModelCache* model_cache, + ModelCache* secondary_model_cache, + ModelCache* projectile_model_cache +) { + if (!meta || meta->model_id < 0) return NULL; + uint32_t model_ids[3] = { + OSRS_SPOTANIM_MODEL_BASE + meta->id, + OSRS_SPOTANIM_RECOLOR_MODEL_BASE | meta->id, + (uint32_t)meta->model_id, + }; + ModelCache* caches[3] = { + projectile_model_cache, + model_cache, + secondary_model_cache, + }; + for (int m = 0; m < 3; m++) { + for (int c = 0; c < 3; c++) { + OsrsModel* om = effect_find_model_in_cache(caches[c], model_ids[m]); + if (om) return om; + } + } + return NULL; +} + +typedef enum { + EFFECT_NONE = 0, + EFFECT_SPOTANIM, + EFFECT_PROJECTILE, +} EffectType; + +typedef struct { + EffectType type; + int gfx_id; + const OsrsSpotAnimDef* meta; + + /* positions are sub-tile coords, 128 units per tile; ticks are 50 Hz client + ticks, not 600 ms game ticks */ + double src_x, src_y; + double dst_x, dst_y; + double cur_x, cur_y; + double height; + + double x_increment; + double y_increment; + double diagonal_increment; + double height_increment; + double height_accel; + int start_height; + int end_height; + int initial_slope; + + int start_tick; + int stop_tick; + int started; + + AnimPlayback anim_playback; + AnimModelState* anim_state; + + /* 0-2047 OSRS angle units */ + int turn_value; + int tilt_angle; +} ActiveEffect; + +static void effect_free(ActiveEffect* e) { + if (e->anim_state) { + anim_model_state_free(e->anim_state); + e->anim_state = NULL; + } + e->type = EFFECT_NONE; +} + +static int effect_find_slot(ActiveEffect effects[MAX_ACTIVE_EFFECTS]) { + for (int i = 0; i < MAX_ACTIVE_EFFECTS; i++) { + if (effects[i].type == EFFECT_NONE) return i; + } + int oldest = 0; + for (int i = 1; i < MAX_ACTIVE_EFFECTS; i++) { + if (effects[i].start_tick < effects[oldest].start_tick) oldest = i; + } + effect_free(&effects[oldest]); + return oldest; +} + +static void effect_init_anim_state( + ActiveEffect* e, + ModelCache* model_cache, + ModelCache* secondary_model_cache, + ModelCache* projectile_model_cache +) { + if (!e->meta || e->meta->animation_id < 0) return; + + OsrsModel* om = effect_find_model( + e->meta, model_cache, secondary_model_cache, projectile_model_cache); + if (!om || !om->vertex_skins || om->base_vert_count == 0) return; + + e->anim_state = anim_model_state_create_with_face_alpha( + om->vertex_skins, om->base_vert_count, + om->face_alpha_labels, om->base_face_alphas, om->mesh.triangleCount); +} + +static int effect_spawn_spotanim_subtile( + ActiveEffect effects[MAX_ACTIVE_EFFECTS], + int gfx_id, + float subtile_x, float subtile_y, + int current_client_tick, + const OsrsSpotAnimSet* spotanims, + AnimCache* anim_cache, + ModelCache* model_cache, + ModelCache* secondary_model_cache, + ModelCache* projectile_model_cache +) { + const OsrsSpotAnimDef* meta = spotanim_lookup(spotanims, gfx_id); + if (!meta) return -1; + + int slot = effect_find_slot(effects); + ActiveEffect* e = &effects[slot]; + memset(e, 0, sizeof(ActiveEffect)); + e->type = EFFECT_SPOTANIM; + e->gfx_id = gfx_id; + e->meta = meta; + anim_playback_reset(&e->anim_playback); + if (meta->animation_id >= 0) + anim_playback_set_seq(&e->anim_playback, meta->animation_id, ANIM_PLAY_LOOP); + + e->cur_x = subtile_x; + e->cur_y = subtile_y; + e->height = 0; + + e->start_tick = current_client_tick; + + int duration = 30; + if (meta->animation_id >= 0 && anim_cache) { + AnimSequence* seq = anim_get_sequence(anim_cache, meta->animation_id); + if (seq) { + duration = 0; + for (int f = 0; f < seq->frame_count; f++) { + duration += seq->frames[f].delay; + } + } + } + e->stop_tick = current_client_tick + duration; + + effect_init_anim_state(e, model_cache, secondary_model_cache, projectile_model_cache); + return slot; +} + +static int effect_spawn_spotanim( + ActiveEffect effects[MAX_ACTIVE_EFFECTS], + int gfx_id, int world_x, int world_y, + int current_client_tick, const OsrsSpotAnimSet* spotanims, + AnimCache* anim_cache, ModelCache* model_cache, + ModelCache* secondary_model_cache, ModelCache* projectile_model_cache +) { + return effect_spawn_spotanim_subtile(effects, gfx_id, + world_x * 128.0f + 64.0f, world_y * 128.0f + 64.0f, + current_client_tick, spotanims, anim_cache, model_cache, + secondary_model_cache, projectile_model_cache); +} + +static int effect_spawn_projectile( + ActiveEffect effects[MAX_ACTIVE_EFFECTS], + int gfx_id, + int src_world_x, int src_world_y, + int dst_world_x, int dst_world_y, + int delay_client_ticks, + int duration_client_ticks, + int start_height_subtile, + int end_height_subtile, + int slope, + int current_client_tick, + const OsrsSpotAnimSet* spotanims, + ModelCache* model_cache, + ModelCache* secondary_model_cache, + ModelCache* projectile_model_cache +) { + const OsrsSpotAnimDef* meta = spotanim_lookup(spotanims, gfx_id); + if (!meta) return -1; + + int slot = effect_find_slot(effects); + ActiveEffect* e = &effects[slot]; + memset(e, 0, sizeof(ActiveEffect)); + e->type = EFFECT_PROJECTILE; + e->gfx_id = gfx_id; + e->meta = meta; + anim_playback_reset(&e->anim_playback); + if (meta->animation_id >= 0) + anim_playback_set_seq(&e->anim_playback, meta->animation_id, ANIM_PLAY_LOOP); + + e->src_x = src_world_x * 128.0 + 64.0; + e->src_y = src_world_y * 128.0 + 64.0; + e->dst_x = dst_world_x * 128.0 + 64.0; + e->dst_y = dst_world_y * 128.0 + 64.0; + e->cur_x = e->src_x; + e->cur_y = e->src_y; + e->start_height = start_height_subtile; + e->end_height = end_height_subtile; + e->height = start_height_subtile; + e->initial_slope = slope; + e->started = 0; + + e->start_tick = current_client_tick + delay_client_ticks; + e->stop_tick = current_client_tick + delay_client_ticks + duration_client_ticks; + + effect_init_anim_state(e, model_cache, secondary_model_cache, projectile_model_cache); + return slot; +} + +static void effect_client_tick( + ActiveEffect effects[MAX_ACTIVE_EFFECTS], + int current_client_tick +) { + for (int i = 0; i < MAX_ACTIVE_EFFECTS; i++) { + ActiveEffect* e = &effects[i]; + if (e->type == EFFECT_NONE) continue; + + if (current_client_tick >= e->stop_tick) { + effect_free(e); + continue; + } + + if (current_client_tick < e->start_tick) continue; + + if (e->type == EFFECT_PROJECTILE) { + if (!e->started) { + e->cur_x = e->src_x; + e->cur_y = e->src_y; + e->height = e->start_height; + + double cycles_left = (double)(e->stop_tick + 1 - current_client_tick); + e->x_increment = (e->dst_x - e->cur_x) / cycles_left; + e->y_increment = (e->dst_y - e->cur_y) / cycles_left; + e->diagonal_increment = sqrt( + e->x_increment * e->x_increment + + e->y_increment * e->y_increment + ); + + e->height_increment = -e->diagonal_increment * + tan((double)e->initial_slope * 0.02454369); + e->height_accel = 2.0 * ( + (double)e->end_height - e->height - + e->height_increment * cycles_left + ) / (cycles_left * cycles_left); + + e->started = 1; + } + + e->cur_x += e->x_increment; + e->cur_y += e->y_increment; + e->height += e->height_increment + 0.5 * e->height_accel; + e->height_increment += e->height_accel; + + /* 325.949 = 2048 angle units per 2 pi radians, from the deob client */ + e->turn_value = (int)(atan2(e->x_increment, e->y_increment) * + 325.949) + 1024; + e->turn_value &= 0x7FF; + e->tilt_angle = (int)(atan2(e->height_increment, + e->diagonal_increment) * 325.949); + e->tilt_angle &= 0x7FF; + } + + } +} + +static void effect_clear_all(ActiveEffect effects[MAX_ACTIVE_EFFECTS]) { + for (int i = 0; i < MAX_ACTIVE_EFFECTS; i++) { + effect_free(&effects[i]); + } +} + +#endif /* OSRS_PVP_EFFECTS_H */ diff --git a/ocean/osrs/osrs_encounter.h b/ocean/osrs/osrs_encounter.h new file mode 100644 index 0000000000..b6da764efe --- /dev/null +++ b/ocean/osrs/osrs_encounter.h @@ -0,0 +1,2585 @@ +#ifndef OSRS_ENCOUNTER_H +#define OSRS_ENCOUNTER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "osrs_types.h" +#include "osrs_items.h" +#include "osrs_pathfinding.h" +#include "osrs_combat.h" +#include "osrs_consumables.h" +#include "osrs_item_effects.h" +#include "osrs_human_input_types.h" +#include "osrs_render_motion.h" +#include "osrs_lab.h" + +typedef struct EncounterState EncounterState; +typedef struct EncounterContext EncounterContext; + +#define ENCOUNTER_RENDER_HITS_MAX 32 + +typedef struct { + EncounterState* state; + EncounterContext* context; +} EncounterRuntime; + +static inline void encounter_abort_unknown_config( + const char* encounter_name, const char* config_type, const char* key +) { + fprintf(stderr, "%s unknown %s config key: %s\n", + encounter_name, config_type, key); + abort(); +} + +static inline int encounter_require_binary_config( + const char* encounter_name, const char* key, int value +) { + if (value != 0 && value != 1) { + fprintf(stderr, "%s config %s must be 0 or 1, got %d\n", + encounter_name, key, value); + abort(); + } + return value; +} + +static inline int encounter_require_int_range_config( + const char* encounter_name, const char* key, int value, int min_value, int max_value +) { + if (value < min_value || value > max_value) { + fprintf(stderr, "%s config %s must be in [%d, %d], got %d\n", + encounter_name, key, min_value, max_value, value); + abort(); + } + return value; +} + +#define ENCOUNTER_MAX_PENDING_HITS 32 + +typedef struct { + int active; + int damage; + int ticks_remaining; + int attack_style; + int check_prayer; + int prayer_check_delay; + + int spell_type; + int source_npc_type; + int source_npc_slot; + int hit_success; + int elysian_reduced; +} EncounterPendingHit; + +typedef struct { + EncounterPendingHit hits[ENCOUNTER_MAX_PENDING_HITS]; + int count; +} EncounterPendingHitQueue; + +static inline EncounterPendingHit encounter_pending_hit_resolved_at_throw( + int raw_damage, int ticks_remaining, int attack_style, int overhead_prayer, + int source_npc_type, int source_npc_slot, int accuracy_hit, int* out_prayed +) { + EncounterProtectResolve pr = + encounter_resolve_protect_at_throw(raw_damage, overhead_prayer, attack_style); + if (out_prayed) *out_prayed = pr.prayed; + return (EncounterPendingHit){ + .active = 1, + .damage = pr.frozen_damage, + .ticks_remaining = ticks_remaining, + .attack_style = attack_style, + .check_prayer = 0, + .prayer_check_delay = 0, + .spell_type = ENCOUNTER_SPELL_NONE, + .source_npc_type = source_npc_type, + .source_npc_slot = source_npc_slot, + .hit_success = accuracy_hit && !pr.prayed, + .elysian_reduced = 0, + }; +} + +static inline void encounter_pending_hit_queue_clear(EncounterPendingHitQueue* q) { + memset(q, 0, sizeof(*q)); +} + +static inline EncounterPendingHit* encounter_pending_hit_queue_push( + EncounterPendingHitQueue* q, + EncounterPendingHit hit, + const char* owner_label, + int tick, + int slot, + int type +) { + if (q->count < 0 || q->count > ENCOUNTER_MAX_PENDING_HITS) { + fprintf(stderr, + "%s pending-hit queue corrupt tick=%d slot=%d type=%d count=%d\n", + owner_label, tick, slot, type, q->count); + abort(); + } + if (q->count >= ENCOUNTER_MAX_PENDING_HITS) { + fprintf(stderr, + "%s pending-hit queue overflow tick=%d slot=%d type=%d count=%d " + "delay=%d style=%d spell=%d damage=%d\n", + owner_label, tick, slot, type, q->count, + hit.ticks_remaining, hit.attack_style, hit.spell_type, hit.damage); + abort(); + } + + hit.active = 1; + q->hits[q->count] = hit; + return &q->hits[q->count++]; +} + +static inline void encounter_pending_hit_queue_remove( + EncounterPendingHitQueue* q, + int idx, + const char* owner_label +) { + if (q->count < 0 || q->count > ENCOUNTER_MAX_PENDING_HITS) { + fprintf(stderr, "%s pending-hit queue corrupt before remove count=%d\n", + owner_label, q->count); + abort(); + } + if (idx < 0 || idx >= q->count) { + fprintf(stderr, "%s pending-hit queue invalid remove idx=%d count=%d\n", + owner_label, idx, q->count); + abort(); + } + for (int i = idx + 1; i < q->count; i++) + q->hits[i - 1] = q->hits[i]; + q->count--; + memset(&q->hits[q->count], 0, sizeof(q->hits[q->count])); +} + +static inline const EncounterPendingHit* encounter_pending_hit_queue_earliest( + const EncounterPendingHitQueue* q +) { + const EncounterPendingHit* best = NULL; + for (int i = 0; i < q->count; i++) { + const EncounterPendingHit* hit = &q->hits[i]; + if (!hit->active) continue; + if (!best || hit->ticks_remaining < best->ticks_remaining) + best = hit; + } + return best; +} + +static inline int encounter_pending_hit_queue_damage_sum( + const EncounterPendingHitQueue* q +) { + int total = 0; + for (int i = 0; i < q->count; i++) { + if (q->hits[i].active) + total += q->hits[i].damage; + } + return total; +} + +#define ENCOUNTER_MAX_OVERLAY_TILES 16 +#define ENCOUNTER_MAX_OVERLAY_ADDS 4 +#define ENCOUNTER_MAX_OVERLAY_TILE_SHADOWS 48 +#define ENCOUNTER_MAX_OVERLAY_FLOATING_MODELS 16 +#define ENCOUNTER_MAX_ACTIVE_MODIFIERS 16 +#define ENCOUNTER_OVERLAY_STATUS_TEXT_LEN 64 + +#define ENCOUNTER_MAX_OVERLAY_PROJECTILES 48 + +typedef enum { + ENCOUNTER_PROJECTILE_MOTION_OSRS_FLIGHT = 0, + ENCOUNTER_PROJECTILE_MOTION_TARGET_ANCHORED = 1, +} EncounterProjectileMotionMode; + +typedef enum { + ENCOUNTER_PROJECTILE_TARGET_FIXED = 0, + ENCOUNTER_PROJECTILE_TARGET_PLAYER = 1, + ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT = 2, +} EncounterProjectileTargetKind; + +typedef struct { + int active; + int x, y; + float scale; +} EncounterTileShadow; + +typedef struct { + int active; + int anchor_kind; + int npc_slot; + int x, y; + uint32_t model_id; + int anim_id; + float height_offset; + float lateral_offset; + float scale; +} EncounterFloatingModel; + +typedef struct { + int active; + int modifier; + int tier; +} EncounterActiveModifier; + +typedef struct { + struct { int x, y, active; } hazards[ENCOUNTER_MAX_OVERLAY_TILES]; + int hazard_count; + + int boss_x, boss_y, boss_visible; + int boss_form; + int boss_size; + + struct { int x, y, active, variant; } adds[ENCOUNTER_MAX_OVERLAY_ADDS]; + int add_count; + + EncounterTileShadow tile_shadows[ENCOUNTER_MAX_OVERLAY_TILE_SHADOWS]; + int tile_shadow_count; + + EncounterFloatingModel floating_models[ENCOUNTER_MAX_OVERLAY_FLOATING_MODELS]; + int floating_model_count; + + EncounterActiveModifier active_modifiers[ENCOUNTER_MAX_ACTIVE_MODIFIERS]; + int active_modifier_count; + + struct { + int active; + int src_x, src_y; + int dst_x, dst_y; + int style; + int damage; + int duration_ticks; + int start_h; + int end_h; + int curve; + float arc_height; + int tracks_target; + int source_kind; + int source_npc_slot; + int target_kind; + int target_npc_slot; + int start_delay; + int motion_mode; + float offset_x, offset_y, offset_z; + int src_size; + int dst_size; + uint32_t model_id; + int anim_id; + int travel_gfx_id; + int launch_gfx_id; + int impact_gfx_id; + } projectiles[ENCOUNTER_MAX_OVERLAY_PROJECTILES]; + int projectile_count; + + int melee_target_active; + int melee_target_x, melee_target_y; + + int status_text_active; + char status_text[ENCOUNTER_OVERLAY_STATUS_TEXT_LEN]; +} EncounterOverlay; + +static inline int encounter_attack_style_to_proj_style(int attack_style) { + switch (attack_style) { + case ATTACK_STYLE_RANGED: return 0; + case ATTACK_STYLE_MAGIC: return 1; + case ATTACK_STYLE_MELEE: return 2; + default: return 0; + } +} + +static inline EncounterProjectileDelayKind encounter_projectile_delay_kind_for_style( + AttackStyle style +) { + switch (style) { + case ATTACK_STYLE_MELEE: + return ENCOUNTER_PROJECTILE_DELAY_MELEE; + case ATTACK_STYLE_MAGIC: + return ENCOUNTER_PROJECTILE_DELAY_MAGIC; + case ATTACK_STYLE_RANGED: + return ENCOUNTER_PROJECTILE_DELAY_RANGED; + case ATTACK_STYLE_NONE: + return ENCOUNTER_PROJECTILE_DELAY_MELEE; + } + abort(); +} + +static inline OffensivePrayer encounter_offensive_prayer_for_style(AttackStyle style) { + switch (style) { + case ATTACK_STYLE_MELEE: + return OFFENSIVE_PRAYER_PIETY; + case ATTACK_STYLE_RANGED: + return OFFENSIVE_PRAYER_RIGOUR; + case ATTACK_STYLE_MAGIC: + return OFFENSIVE_PRAYER_AUGURY; + case ATTACK_STYLE_NONE: + return OFFENSIVE_PRAYER_NONE; + } + abort(); +} + +static inline int encounter_emit_projectile( + EncounterOverlay* ov, + int src_x, int src_y, int dst_x, int dst_y, + int style, int damage, + int duration_ticks, int start_h, int end_h, int curve, + float arc_height, int tracks_target, int src_size, int dst_size, + uint32_t model_id, int impact_gfx_id +) { + if (ov->projectile_count >= ENCOUNTER_MAX_OVERLAY_PROJECTILES) { + fprintf(stderr, "encounter overlay projectile capacity exceeded: %d\n", + ENCOUNTER_MAX_OVERLAY_PROJECTILES); + abort(); + } + int i = ov->projectile_count++; + ov->projectiles[i].active = 1; + ov->projectiles[i].src_x = src_x; + ov->projectiles[i].src_y = src_y; + ov->projectiles[i].dst_x = dst_x; + ov->projectiles[i].dst_y = dst_y; + ov->projectiles[i].style = style; + ov->projectiles[i].damage = damage; + ov->projectiles[i].duration_ticks = duration_ticks; + ov->projectiles[i].start_h = start_h; + ov->projectiles[i].end_h = end_h; + ov->projectiles[i].curve = curve; + ov->projectiles[i].arc_height = arc_height; + ov->projectiles[i].start_delay = 0; + ov->projectiles[i].motion_mode = ENCOUNTER_PROJECTILE_MOTION_OSRS_FLIGHT; + ov->projectiles[i].offset_x = 0.0f; + ov->projectiles[i].offset_y = 0.0f; + ov->projectiles[i].offset_z = 0.0f; + ov->projectiles[i].tracks_target = tracks_target; + ov->projectiles[i].source_kind = ENCOUNTER_PROJECTILE_TARGET_FIXED; + ov->projectiles[i].source_npc_slot = -1; + ov->projectiles[i].target_kind = tracks_target + ? ENCOUNTER_PROJECTILE_TARGET_PLAYER + : ENCOUNTER_PROJECTILE_TARGET_FIXED; + ov->projectiles[i].target_npc_slot = -1; + ov->projectiles[i].src_size = src_size; + ov->projectiles[i].dst_size = dst_size; + ov->projectiles[i].model_id = model_id; + ov->projectiles[i].anim_id = -1; + ov->projectiles[i].travel_gfx_id = 0; + ov->projectiles[i].launch_gfx_id = 0; + ov->projectiles[i].impact_gfx_id = impact_gfx_id; + return i; +} + +static inline void encounter_emit_tile_shadow( + EncounterOverlay* ov, int x, int y, float scale +) { + if (scale <= 0.0f) { + fprintf(stderr, "encounter tile shadow scale must be positive: %f\n", scale); + abort(); + } + if (ov->tile_shadow_count >= ENCOUNTER_MAX_OVERLAY_TILE_SHADOWS) { + fprintf(stderr, "encounter overlay tile shadow capacity exceeded: %d\n", + ENCOUNTER_MAX_OVERLAY_TILE_SHADOWS); + abort(); + } + int i = ov->tile_shadow_count++; + ov->tile_shadows[i] = (EncounterTileShadow){ + .active = 1, + .x = x, + .y = y, + .scale = scale, + }; +} + +static inline void encounter_emit_floating_model( + EncounterOverlay* ov, + int anchor_kind, + int npc_slot, + int x, + int y, + uint32_t model_id, + int anim_id, + float height_offset, + float lateral_offset, + float scale +) { + if (model_id == 0 || scale <= 0.0f) { + fprintf(stderr, "encounter floating model invalid model=%u scale=%f\n", + model_id, scale); + abort(); + } + if (ov->floating_model_count >= ENCOUNTER_MAX_OVERLAY_FLOATING_MODELS) { + fprintf(stderr, "encounter floating model capacity exceeded: %d\n", + ENCOUNTER_MAX_OVERLAY_FLOATING_MODELS); + abort(); + } + int i = ov->floating_model_count++; + ov->floating_models[i] = (EncounterFloatingModel){ + .active = 1, + .anchor_kind = anchor_kind, + .npc_slot = npc_slot, + .x = x, + .y = y, + .model_id = model_id, + .anim_id = anim_id, + .height_offset = height_offset, + .lateral_offset = lateral_offset, + .scale = scale, + }; +} + +static inline void encounter_emit_active_modifier( + EncounterOverlay* ov, + int modifier, + int tier +) { + if (modifier < 0 || tier <= 0) { + fprintf(stderr, "encounter active modifier invalid modifier=%d tier=%d\n", + modifier, tier); + abort(); + } + if (ov->active_modifier_count >= ENCOUNTER_MAX_ACTIVE_MODIFIERS) { + fprintf(stderr, "encounter active modifier capacity exceeded: %d\n", + ENCOUNTER_MAX_ACTIVE_MODIFIERS); + abort(); + } + int i = ov->active_modifier_count++; + ov->active_modifiers[i] = (EncounterActiveModifier){ + .active = 1, + .modifier = modifier, + .tier = tier, + }; +} + +static inline void encounter_require_projectile_slots(const EncounterOverlay* ov, int slots) { + if (slots < 0 || ov->projectile_count + slots > ENCOUNTER_MAX_OVERLAY_PROJECTILES) { + fprintf(stderr, "encounter overlay projectile capacity exceeded: need %d free from %d/%d\n", + slots, ov->projectile_count, ENCOUNTER_MAX_OVERLAY_PROJECTILES); + abort(); + } +} + +static inline void encounter_require_projectile_index(const EncounterOverlay* ov, int projectile_idx) { + if (projectile_idx < 0 || projectile_idx >= ov->projectile_count) { + fprintf(stderr, "encounter projectile index out of range: %d/%d\n", + projectile_idx, ov->projectile_count); + abort(); + } +} + +static inline void encounter_set_projectile_source_player( + EncounterOverlay* ov, int projectile_idx +) { + encounter_require_projectile_index(ov, projectile_idx); + ov->projectiles[projectile_idx].source_kind = ENCOUNTER_PROJECTILE_TARGET_PLAYER; + ov->projectiles[projectile_idx].source_npc_slot = -1; +} + +static inline void encounter_set_projectile_source_npc_slot( + EncounterOverlay* ov, int projectile_idx, int npc_slot +) { + encounter_require_projectile_index(ov, projectile_idx); + if (npc_slot < 0) { + fprintf(stderr, "encounter projectile source npc slot is invalid: %d\n", npc_slot); + abort(); + } + ov->projectiles[projectile_idx].source_kind = ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT; + ov->projectiles[projectile_idx].source_npc_slot = npc_slot; +} + +static inline void encounter_set_projectile_target_npc_slot( + EncounterOverlay* ov, int projectile_idx, int npc_slot +) { + encounter_require_projectile_index(ov, projectile_idx); + if (npc_slot < 0) { + fprintf(stderr, "encounter projectile target npc slot is invalid: %d\n", npc_slot); + abort(); + } + ov->projectiles[projectile_idx].tracks_target = 1; + ov->projectiles[projectile_idx].target_kind = ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT; + ov->projectiles[projectile_idx].target_npc_slot = npc_slot; +} + +static inline void encounter_set_projectile_motion_mode( + EncounterOverlay* ov, int projectile_idx, int motion_mode +) { + encounter_require_projectile_index(ov, projectile_idx); + ov->projectiles[projectile_idx].motion_mode = motion_mode; +} + +static inline void encounter_set_projectile_animation( + EncounterOverlay* ov, int projectile_idx, int anim_id +) { + encounter_require_projectile_index(ov, projectile_idx); + ov->projectiles[projectile_idx].anim_id = anim_id; +} + +static inline void encounter_set_projectile_launch_gfx( + EncounterOverlay* ov, int projectile_idx, int launch_gfx_id +) { + encounter_require_projectile_index(ov, projectile_idx); + ov->projectiles[projectile_idx].launch_gfx_id = launch_gfx_id; +} + +static inline void encounter_set_projectile_travel_gfx( + EncounterOverlay* ov, int projectile_idx, int travel_gfx_id +) { + encounter_require_projectile_index(ov, projectile_idx); + ov->projectiles[projectile_idx].travel_gfx_id = travel_gfx_id; +} + +static inline void encounter_set_projectile_offset( + EncounterOverlay* ov, int projectile_idx, + float offset_x, float offset_y, float offset_z +) { + encounter_require_projectile_index(ov, projectile_idx); + ov->projectiles[projectile_idx].offset_x = offset_x; + ov->projectiles[projectile_idx].offset_y = offset_y; + ov->projectiles[projectile_idx].offset_z = offset_z; +} + +typedef struct { + EntityType entity_type; + int npc_def_id; + int npc_visible; + int npc_size; + int npc_anim_id; + int x, y; + int dest_x, dest_y; + int current_hitpoints, base_hitpoints; + int special_energy; + OverheadPrayer prayer; + GearSet visible_gear; + int frozen_ticks; + int veng_active; + int is_running; + FightStyle fight_style; + AttackStyle attack_style_this_tick; + int magic_type_this_tick; + int hit_landed_this_tick; + int hit_damage; + int render_hit_count; + int render_hit_damage[ENCOUNTER_RENDER_HITS_MAX]; + int hit_was_successful; + int hit_spell_type; + int elysian_proc_this_tick; + int cast_veng_this_tick; + int ate_food_this_tick; + int ate_karambwan_this_tick; + int used_special_this_tick; + uint8_t equipped[NUM_GEAR_SLOTS]; + int npc_slot; + uint32_t npc_instance_id; + RenderMovementKind render_movement_kind; + int attack_target_entity_idx; + const char* debug_npc_type_name; + int debug_attack_timer; + int debug_attack_style; + int debug_manticore_state_active; + int debug_manticore_cycle_step; + int debug_manticore_orb_style[3]; +} RenderEntity; + +typedef enum { + RENDER_ENTITY_FACE_MOVEMENT = 0, + RENDER_ENTITY_FACE_ATTACK_TARGET = 1, + RENDER_ENTITY_FACE_DEST_TILE = 2, +} RenderEntityFacingMode; + +static inline int render_entity_find_previous_identity_index( + const RenderEntity* previous, + int previous_count, + const int* previous_used, + const RenderEntity* entity +) { + if (entity->entity_type == ENTITY_PLAYER) { + for (int j = 0; j < previous_count; j++) { + if (!previous_used[j] && previous[j].entity_type == ENTITY_PLAYER) { + return j; + } + } + return -1; + } + + if (entity->entity_type != ENTITY_NPC || entity->npc_slot < 0) + return -1; + + for (int i = 0; i < previous_count; i++) { + if (previous_used[i]) continue; + if (previous[i].entity_type == ENTITY_NPC && + previous[i].npc_slot == entity->npc_slot && + previous[i].npc_def_id == entity->npc_def_id) { + if ((previous[i].npc_instance_id || entity->npc_instance_id) && + previous[i].npc_instance_id != entity->npc_instance_id) { + continue; + } + return i; + } + } + return -1; +} + +static inline RenderEntityFacingMode render_entity_select_facing_mode( + const RenderEntity* entity, int moved +) { + if (entity->attack_target_entity_idx >= 0 || entity->current_hitpoints <= 0) + return RENDER_ENTITY_FACE_ATTACK_TARGET; + if (entity->attack_style_this_tick != ATTACK_STYLE_NONE) + return RENDER_ENTITY_FACE_DEST_TILE; + if (moved) + return RENDER_ENTITY_FACE_MOVEMENT; + return RENDER_ENTITY_FACE_DEST_TILE; +} + +static inline void render_entity_from_player(const Player* p, RenderEntity* out) { + memset(out, 0, sizeof(RenderEntity)); + out->entity_type = p->entity_type; + out->npc_def_id = p->npc_def_id; + out->npc_visible = p->npc_visible; + out->npc_size = p->npc_size; + out->npc_anim_id = p->npc_anim_id; + out->x = p->x; + out->y = p->y; + out->dest_x = p->dest_x; + out->dest_y = p->dest_y; + out->current_hitpoints = p->current_hitpoints; + out->base_hitpoints = p->base_hitpoints; + out->special_energy = p->special_energy; + out->prayer = p->prayer_display != PRAYER_NONE ? p->prayer_display : p->prayer; + out->visible_gear = p->visible_gear; + out->frozen_ticks = p->frozen_ticks; + out->veng_active = p->veng_active; + out->is_running = p->is_running; + out->fight_style = p->fight_style; + out->attack_style_this_tick = p->attack_style_this_tick; + out->magic_type_this_tick = p->magic_type_this_tick; + out->hit_landed_this_tick = p->hit_landed_this_tick; + out->hit_damage = p->hit_damage; + if (p->hit_landed_this_tick) { + out->render_hit_count = 1; + out->render_hit_damage[0] = p->hit_damage; + } + out->hit_was_successful = p->hit_was_successful; + out->hit_spell_type = 0; + out->elysian_proc_this_tick = p->elysian_proc_this_tick; + out->cast_veng_this_tick = p->cast_veng_this_tick; + out->ate_food_this_tick = p->ate_food_this_tick; + out->ate_karambwan_this_tick = p->ate_karambwan_this_tick; + out->used_special_this_tick = p->used_special_this_tick; + memcpy(out->equipped, p->equipped, NUM_GEAR_SLOTS); + out->npc_slot = -1; + out->npc_instance_id = 0; + out->render_movement_kind = RENDER_MOVEMENT_NORMAL; + out->attack_target_entity_idx = -1; +} + +static inline void encounter_resolve_attack_target( + RenderEntity* entities, int count, int target_npc_slot +) { + entities[0].attack_target_entity_idx = -1; + if (target_npc_slot < 0) return; + for (int i = 1; i < count; i++) { + if (entities[i].npc_slot == target_npc_slot) { + entities[0].attack_target_entity_idx = i; + return; + } + } +} + +#define ENCOUNTER_OVERHEAD_NO_CHANGE 0 +#define ENCOUNTER_OVERHEAD_OFF 1 +#define ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE 2 +#define ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED 3 +#define ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC 4 +#define ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE 5 +#define ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION 6 +#define ENCOUNTER_OVERHEAD_DIM_PVE 5 +#define ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION 6 + +#define ENCOUNTER_OFFENSIVE_NO_CHANGE 0 +#define ENCOUNTER_OFFENSIVE_OFF 1 +#define ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY 2 +#define ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR 3 +#define ENCOUNTER_OFFENSIVE_SET_REFRESH_AUGURY 4 +#define ENCOUNTER_OFFENSIVE_DIM 5 + +static inline int encounter_apply_overhead_action(OverheadPrayer* overhead, int action) { + OverheadPrayer target; + switch (action) { + case ENCOUNTER_OVERHEAD_NO_CHANGE: + return 0; + case ENCOUNTER_OVERHEAD_OFF: + *overhead = PRAYER_NONE; + return 0; + case ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE: target = PRAYER_PROTECT_MELEE; break; + case ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED: target = PRAYER_PROTECT_RANGED; break; + case ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC: target = PRAYER_PROTECT_MAGIC; break; + case ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE: target = PRAYER_SMITE; break; + case ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION: target = PRAYER_REDEMPTION; break; + default: return 0; + } + *overhead = target; + return 1; +} + +static inline int encounter_apply_offensive_action(OffensivePrayer* offensive, int action) { + OffensivePrayer target; + switch (action) { + case ENCOUNTER_OFFENSIVE_NO_CHANGE: + return 0; + case ENCOUNTER_OFFENSIVE_OFF: + *offensive = OFFENSIVE_PRAYER_NONE; + return 0; + case ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY: target = OFFENSIVE_PRAYER_PIETY; break; + case ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR: target = OFFENSIVE_PRAYER_RIGOUR; break; + case ENCOUNTER_OFFENSIVE_SET_REFRESH_AUGURY: target = OFFENSIVE_PRAYER_AUGURY; break; + default: return 0; + } + *offensive = target; + return 1; +} + +#define ENCOUNTER_MOVE_ACTIONS 25 + +static const int ENCOUNTER_MOVE_TARGET_DX[25] = { + 0, + -1, -1, -1, 0, 0, 1, 1, 1, + -2, -2, -2, -2, -2, + -1, -1, + 0, 0, + 1, 1, + 2, 2, 2, 2, 2 +}; +static const int ENCOUNTER_MOVE_TARGET_DY[25] = { + 0, + -1, 0, 1, -1, 1, -1, 0, 1, + -2, -1, 0, 1, 2, + -2, 2, + -2, 2, + -2, 2, + -2, -1, 0, 1, 2 +}; + +typedef int (*encounter_walkable_fn)(void* ctx, int x, int y); + +static inline int encounter_move_to_target( + Player* p, int target_dx, int target_dy, + encounter_walkable_fn is_walkable, void* ctx +) { + int tx = p->x + target_dx; + int ty = p->y + target_dy; + int dist = abs(target_dx) > abs(target_dy) ? abs(target_dx) : abs(target_dy); + int max_steps = dist; + int steps = 0; + + for (int step = 0; step < max_steps; step++) { + if (p->x == tx && p->y == ty) break; + int dx = 0, dy = 0; + if (tx > p->x) dx = 1; else if (tx < p->x) dx = -1; + if (ty > p->y) dy = 1; else if (ty < p->y) dy = -1; + + int moved = 0; + if (dx != 0 && dy != 0 && + is_walkable(ctx, p->x + dx, p->y + dy) && + is_walkable(ctx, p->x + dx, p->y) && + is_walkable(ctx, p->x, p->y + dy)) { + p->x += dx; p->y += dy; moved = 1; + } else if (dx != 0 && is_walkable(ctx, p->x + dx, p->y)) { + p->x += dx; moved = 1; + } else if (dy != 0 && is_walkable(ctx, p->x, p->y + dy)) { + p->y += dy; moved = 1; + } + if (!moved) break; + steps++; + } + + p->is_running = (steps == 2); + p->dest_x = p->x; + p->dest_y = p->y; + return steps; +} + +static inline PathResult encounter_pathfind( + const CollisionMap* cmap, int world_offset_x, int world_offset_y, + int src_x, int src_y, int dst_x, int dst_y, + pathfind_blocked_fn extra_blocked, void* blocked_ctx +) { + return pathfind_step(cmap, 0, + src_x + world_offset_x, src_y + world_offset_y, + dst_x + world_offset_x, dst_y + world_offset_y, + extra_blocked, blocked_ctx); +} + +static inline PathResult encounter_pathfind_arena( + const CollisionMap* cmap, int world_offset_x, int world_offset_y, + int src_x, int src_y, int dst_x, int dst_y, + pathfind_blocked_fn extra_blocked, void* blocked_ctx, + int arena_base_x, int arena_base_y, int arena_w, int arena_h +) { + return pathfind_step_arena(cmap, 0, + src_x + world_offset_x, src_y + world_offset_y, + dst_x + world_offset_x, dst_y + world_offset_y, + extra_blocked, blocked_ctx, + arena_base_x + world_offset_x, arena_base_y + world_offset_y, + arena_w, arena_h); +} + +static inline int encounter_walk_toward( + Player* p, int tx, int ty, + const CollisionMap* cmap, int world_offset_x, int world_offset_y, + encounter_walkable_fn is_walkable, void* ctx, + pathfind_blocked_fn extra_blocked, void* blocked_ctx, + int arena_base_x, int arena_base_y, int arena_w, int arena_h +) { + int steps = 0; + for (int step = 0; step < 2; step++) { + if (p->x == tx && p->y == ty) break; + PathResult pr = (arena_w > 0) + ? encounter_pathfind_arena(cmap, world_offset_x, world_offset_y, + p->x, p->y, tx, ty, + extra_blocked, blocked_ctx, + arena_base_x, arena_base_y, arena_w, arena_h) + : encounter_pathfind(cmap, world_offset_x, world_offset_y, + p->x, p->y, tx, ty, + extra_blocked, blocked_ctx); + if (!pr.found || (pr.next_dx == 0 && pr.next_dy == 0)) break; + int nx = p->x + pr.next_dx, ny = p->y + pr.next_dy; + if (!is_walkable(ctx, nx, ny)) break; + p->x = nx; p->y = ny; + steps++; + } + p->is_running = (steps == 2); + p->dest_x = p->x; p->dest_y = p->y; + return steps; +} + +static inline int encounter_move_toward_dest( + Player* p, int* dest_x, int* dest_y, + const CollisionMap* cmap, int world_offset_x, int world_offset_y, + encounter_walkable_fn is_walkable, void* ctx, + pathfind_blocked_fn extra_blocked, void* blocked_ctx, + int arena_base_x, int arena_base_y, int arena_w, int arena_h +) { + if (*dest_x < 0 || *dest_y < 0) return 0; + if (p->x == *dest_x && p->y == *dest_y) { + *dest_x = -1; *dest_y = -1; + return 0; + } + return encounter_walk_toward(p, *dest_x, *dest_y, + cmap, world_offset_x, world_offset_y, + is_walkable, ctx, extra_blocked, blocked_ctx, + arena_base_x, arena_base_y, arena_w, arena_h); +} + +static inline int encounter_entity_footprint_cardinal_adjacent( + int ax, int ay, int a_size, + int bx, int by, int b_size +) { + int ax1 = ax + a_size - 1; + int ay1 = ay + a_size - 1; + int bx1 = bx + b_size - 1; + int by1 = by + b_size - 1; + + int dx = 0; + if (ax1 < bx) dx = bx - ax1; + else if (bx1 < ax) dx = ax - bx1; + + int dy = 0; + if (ay1 < by) dy = by - ay1; + else if (by1 < ay) dy = ay - by1; + + return (dx + dy) == 1; +} + +static inline int encounter_entity_footprints_overlap( + int ax, int ay, int a_size, + int bx, int by, int b_size +) { + return !(ax + a_size <= bx || bx + b_size <= ax || + ay + a_size <= by || by + b_size <= ay); +} + +typedef enum { + OSRS_LOS_OPEN = 0, + OSRS_LOS_BLOCKERS, + OSRS_LOS_TILE, +} OsrsLosKind; + +typedef struct { + OsrsLosKind kind; + const LOSBlocker* blockers; + int blocker_count; + int (*tile_blocked)(void* ctx, int x, int y); + void* tile_ctx; +} OsrsLosQuery; + +static inline OsrsLosQuery osrs_los_open(void) { + OsrsLosQuery query; + query.kind = OSRS_LOS_OPEN; + query.blockers = NULL; + query.blocker_count = 0; + query.tile_blocked = NULL; + query.tile_ctx = NULL; + return query; +} + +static inline OsrsLosQuery osrs_los_blockers( + const LOSBlocker* blockers, + int blocker_count +) { + OsrsLosQuery query; + query.kind = OSRS_LOS_BLOCKERS; + query.blockers = blockers; + query.blocker_count = blocker_count; + query.tile_blocked = NULL; + query.tile_ctx = NULL; + return query; +} + +static inline OsrsLosQuery osrs_los_tile( + int (*tile_blocked)(void* ctx, int x, int y), + void* tile_ctx +) { + OsrsLosQuery query; + query.kind = OSRS_LOS_TILE; + query.blockers = NULL; + query.blocker_count = 0; + query.tile_blocked = tile_blocked; + query.tile_ctx = tile_ctx; + return query; +} + +static inline const OsrsLosQuery* osrs_los_open_query(void) { + static const OsrsLosQuery query = { + OSRS_LOS_OPEN, + NULL, + 0, + NULL, + NULL, + }; + return &query; +} + +static inline void osrs_los_require_query( + const OsrsLosQuery* query, + int attack_range +) { + if (attack_range <= 1) return; + if (!query) { + fprintf(stderr, "missing OSRS LoS query for ranged attack\n"); + abort(); + } + if (query->kind < OSRS_LOS_OPEN || query->kind > OSRS_LOS_TILE) { + fprintf(stderr, "invalid OSRS LoS query kind: %d\n", (int)query->kind); + abort(); + } + if (query->kind == OSRS_LOS_BLOCKERS) { + if (query->blocker_count < 0) { + fprintf(stderr, "negative OSRS LoS blocker count: %d\n", + query->blocker_count); + abort(); + } + if (query->blocker_count > 0 && !query->blockers) { + fprintf(stderr, "OSRS LoS blocker query is missing blockers\n"); + abort(); + } + } + if (query->kind == OSRS_LOS_TILE && !query->tile_blocked) { + fprintf(stderr, "OSRS tile LoS query is missing tile_blocked\n"); + abort(); + } +} + +static inline int osrs_los_tile_ray_clear( + const OsrsLosQuery* query, + int x0, int y0, + int x1, int y1 +) { + int dx = x1 - x0; + int dy = y1 - y0; + int adx = dx < 0 ? -dx : dx; + int ady = dy < 0 ? -dy : dy; + if (adx == 0 && ady == 0) return 1; + if (query->tile_blocked(query->tile_ctx, x1, y1)) return 0; + + if (adx > ady) { + int x = x0; + int y_fp = y0 * LOS_FP_SCALE + LOS_FP_HALF; + int slope = (dy * LOS_FP_SCALE) / adx; + int x_inc = dx > 0 ? 1 : -1; + if (dy < 0) y_fp -= 1; + while (x != x1) { + x += x_inc; + int y = y_fp >> 16; + if (query->tile_blocked(query->tile_ctx, x, y)) return 0; + y_fp += slope; + int new_y = y_fp >> 16; + if (new_y != y && + query->tile_blocked(query->tile_ctx, x, new_y)) + return 0; + } + } else { + int y = y0; + int x_fp = x0 * LOS_FP_SCALE + LOS_FP_HALF; + int slope = (dx * LOS_FP_SCALE) / ady; + int y_inc = dy > 0 ? 1 : -1; + if (dx < 0) x_fp -= 1; + while (y != y1) { + y += y_inc; + int x = x_fp >> 16; + if (query->tile_blocked(query->tile_ctx, x, y)) return 0; + x_fp += slope; + int new_x = x_fp >> 16; + if (new_x != x && + query->tile_blocked(query->tile_ctx, new_x, y)) + return 0; + } + } + return 1; +} + +static inline int osrs_los_clear( + const OsrsLosQuery* query, + int px, int py, int psize, + int tx, int ty, int tsize, + int attack_range +) { + osrs_los_require_query(query, attack_range); + if (attack_range <= 1) return 1; + + switch (query->kind) { + case OSRS_LOS_OPEN: + return 1; + + case OSRS_LOS_BLOCKERS: + return entity_has_line_of_sight( + query->blockers, + query->blocker_count, + px, + py, + psize, + tx, + ty, + tsize, + attack_range); + + case OSRS_LOS_TILE: { + int p_los_x = tx; + if (p_los_x < px) p_los_x = px; + if (p_los_x >= px + psize) p_los_x = px + psize - 1; + int p_los_y = ty; + if (p_los_y < py) p_los_y = py; + if (p_los_y >= py + psize) p_los_y = py + psize - 1; + + int t_los_x = px; + if (t_los_x < tx) t_los_x = tx; + if (t_los_x >= tx + tsize) t_los_x = tx + tsize - 1; + int t_los_y = py; + if (t_los_y < ty) t_los_y = ty; + if (t_los_y >= ty + tsize) t_los_y = ty + tsize - 1; + + return osrs_los_tile_ray_clear( + query, t_los_x, t_los_y, p_los_x, p_los_y); + } + } + + fprintf(stderr, "unhandled OSRS LoS query kind: %d\n", (int)query->kind); + abort(); +} + +static inline int encounter_player_can_attack( + int player_x, int player_y, + int target_x, int target_y, int target_size, int attack_range, + const OsrsLosQuery* los_query +) { + int dist = encounter_rect_distance(player_x, player_y, 1, + target_x, target_y, target_size); + if (dist < 1 || dist > attack_range) return 0; + if (attack_range == 1) + return encounter_entity_footprint_cardinal_adjacent( + player_x, player_y, 1, target_x, target_y, target_size); + return osrs_los_clear(los_query, + player_x, player_y, 1, + target_x, target_y, target_size, + attack_range); +} + +#define ENCOUNTER_ATTACK_SEEK_MAX_TILES 128 + +typedef struct { + int x; + int y; +} EncounterAttackSeekTile; + +static inline void encounter_attack_seek_add_tile( + EncounterAttackSeekTile* tiles, int* count, + int x, int y, int world_offset_x, int world_offset_y, + pathfind_blocked_fn extra_blocked, void* blocked_ctx +) { + if (extra_blocked && + extra_blocked(blocked_ctx, x + world_offset_x, y + world_offset_y)) + return; + if (*count >= ENCOUNTER_ATTACK_SEEK_MAX_TILES) { + fprintf(stderr, "attack seek tile capacity exceeded: %d\n", + ENCOUNTER_ATTACK_SEEK_MAX_TILES); + abort(); + } + tiles[*count] = (EncounterAttackSeekTile){x, y}; + (*count)++; +} + +static inline int encounter_attack_seek_nearest_dsq( + int x, int y, const EncounterAttackSeekTile* tiles, int count +) { + int best = 0x3fffffff; + for (int i = 0; i < count; i++) { + int dx = x - tiles[i].x; + int dy = y - tiles[i].y; + int dsq = dx * dx + dy * dy; + if (dsq < best) best = dsq; + } + return best; +} + +static inline int encounter_attack_seek_has_exact_tile( + int x, int y, const EncounterAttackSeekTile* tiles, int count +) { + for (int i = 0; i < count; i++) { + if (tiles[i].x == x && tiles[i].y == y) return 1; + } + return 0; +} + +static inline PathResult encounter_pathfind_arena_attack_approach( + const CollisionMap* cmap, int world_offset_x, int world_offset_y, + int src_x, int src_y, + int target_x, int target_y, int target_size, int attack_range, + encounter_walkable_fn is_walkable, void* ctx, + pathfind_blocked_fn extra_blocked, void* blocked_ctx, + const OsrsLosQuery* los_query, + int arena_base_x, int arena_base_y, int arena_w, int arena_h +) { + PathResult result = {0, 0, 0, src_x, src_y}; + + if (arena_w <= 0 || arena_w > PATHFIND_ARENA_MAX || + arena_h <= 0 || arena_h > PATHFIND_ARENA_MAX) { + fprintf(stderr, "attack approach arena dimensions out of bounds: %dx%d\n", + arena_w, arena_h); + abort(); + } + + int local_src_x = src_x - arena_base_x; + int local_src_y = src_y - arena_base_y; + if (local_src_x < 0 || local_src_x >= arena_w || + local_src_y < 0 || local_src_y >= arena_h) { + return result; + } + + EncounterAttackSeekTile seek_tiles[ENCOUNTER_ATTACK_SEEK_MAX_TILES]; + int seek_count = 0; + for (int xx = 0; xx < target_size; xx++) { + int x = target_x + xx; + encounter_attack_seek_add_tile( + seek_tiles, &seek_count, x, target_y - 1, + world_offset_x, world_offset_y, extra_blocked, blocked_ctx); + encounter_attack_seek_add_tile( + seek_tiles, &seek_count, x, target_y + target_size, + world_offset_x, world_offset_y, extra_blocked, blocked_ctx); + } + for (int yy = 0; yy < target_size; yy++) { + int y = target_y + yy; + encounter_attack_seek_add_tile( + seek_tiles, &seek_count, target_x - 1, y, + world_offset_x, world_offset_y, extra_blocked, blocked_ctx); + encounter_attack_seek_add_tile( + seek_tiles, &seek_count, target_x + target_size, y, + world_offset_x, world_offset_y, extra_blocked, blocked_ctx); + } + static OSRS_THREAD_LOCAL uint16_t approach_gen[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + static OSRS_THREAD_LOCAL int8_t approach_via[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + static OSRS_THREAD_LOCAL int16_t approach_cost[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + static OSRS_THREAD_LOCAL uint16_t approach_gen_counter = 0; + approach_gen_counter++; + if (approach_gen_counter == 0) { + memset(approach_gen, 0, sizeof(approach_gen)); + approach_gen_counter = 1; + } + uint16_t gen = approach_gen_counter; + + #define APPROACH_VISITED(x, y) (approach_gen[(x)][(y)] == gen) + #define APPROACH_VISIT(x, y, v, c) do { \ + approach_gen[(x)][(y)] = gen; \ + approach_via[(x)][(y)] = (v); \ + approach_cost[(x)][(y)] = (c); \ + } while(0) + #define APPROACH_VIA(x, y) approach_via[(x)][(y)] + #define APPROACH_COST(x, y) approach_cost[(x)][(y)] + #define APPROACH_EB(x, y) \ + (extra_blocked && extra_blocked( \ + blocked_ctx, (x) + world_offset_x, (y) + world_offset_y)) + + int queue_x[PATHFIND_MAX_QUEUE_ARENA]; + int queue_y[PATHFIND_MAX_QUEUE_ARENA]; + int head = 0; + int tail = 0; + APPROACH_VISIT(local_src_x, local_src_y, VIA_START, 0); + pathfind_enqueue_or_abort( + queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, + local_src_x, local_src_y); + + int selected_x = -1; + int selected_y = -1; + int min_explored_x = local_src_x; + int min_explored_y = local_src_y; + int max_explored_x = local_src_x; + int max_explored_y = local_src_y; + + static const int dir_dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1}; + static const int dir_dy[8] = {0, 0, -1, 1, -1, -1, 1, 1}; + static const int dir_via[8] = { + VIA_W, VIA_E, VIA_S, VIA_N, VIA_SW, VIA_SE, VIA_NW, VIA_NE + }; + + while (head < tail) { + int cur_x = queue_x[head]; + int cur_y = queue_y[head]; + head++; + + int tile_x = arena_base_x + cur_x; + int tile_y = arena_base_y + cur_y; + if (is_walkable(ctx, tile_x, tile_y) && + !APPROACH_EB(tile_x, tile_y) && + (seek_count > 0 + ? encounter_attack_seek_has_exact_tile( + tile_x, tile_y, seek_tiles, seek_count) + : encounter_player_can_attack( + tile_x, tile_y, target_x, target_y, target_size, + attack_range, los_query))) { + selected_x = cur_x; + selected_y = cur_y; + break; + } + + int abs_x = tile_x + world_offset_x; + int abs_y = tile_y + world_offset_y; + int next_cost = APPROACH_COST(cur_x, cur_y) + 1; + + for (int i = 0; i < 8; i++) { + int dx = dir_dx[i]; + int dy = dir_dy[i]; + int next_x = cur_x + dx; + int next_y = cur_y + dy; + if (next_x < 0 || next_x >= arena_w || + next_y < 0 || next_y >= arena_h) + continue; + if (APPROACH_VISITED(next_x, next_y)) continue; + if (!collision_traversable_step(cmap, 0, abs_x, abs_y, dx, dy)) + continue; + + int next_tile_x = tile_x + dx; + int next_tile_y = tile_y + dy; + if (!is_walkable(ctx, next_tile_x, next_tile_y)) continue; + if (APPROACH_EB(next_tile_x, next_tile_y)) continue; + if (dx != 0 && dy != 0) { + if (APPROACH_EB(tile_x + dx, tile_y)) continue; + if (APPROACH_EB(tile_x, tile_y + dy)) continue; + if (!is_walkable(ctx, tile_x + dx, tile_y)) continue; + if (!is_walkable(ctx, tile_x, tile_y + dy)) continue; + } + + pathfind_enqueue_or_abort( + queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, + next_x, next_y); + APPROACH_VISIT(next_x, next_y, dir_via[i], next_cost); + if (next_x < min_explored_x) min_explored_x = next_x; + if (next_y < min_explored_y) min_explored_y = next_y; + if (next_x > max_explored_x) max_explored_x = next_x; + if (next_y > max_explored_y) max_explored_y = next_y; + } + } + + if (selected_x < 0 && seek_count > 0) { + int first_local_x = seek_tiles[0].x - arena_base_x; + int first_local_y = seek_tiles[0].y - arena_base_y; + int scan_min_x = min_explored_x > first_local_x - PATHFIND_MAX_FALLBACK_RADIUS + ? min_explored_x : first_local_x - PATHFIND_MAX_FALLBACK_RADIUS; + int scan_min_y = min_explored_y > first_local_y - PATHFIND_MAX_FALLBACK_RADIUS + ? min_explored_y : first_local_y - PATHFIND_MAX_FALLBACK_RADIUS; + int scan_max_x = max_explored_x > first_local_x + PATHFIND_MAX_FALLBACK_RADIUS + ? max_explored_x : first_local_x + PATHFIND_MAX_FALLBACK_RADIUS; + int scan_max_y = max_explored_y > first_local_y + PATHFIND_MAX_FALLBACK_RADIUS + ? max_explored_y : first_local_y + PATHFIND_MAX_FALLBACK_RADIUS; + if (scan_min_x < 0) scan_min_x = 0; + if (scan_min_y < 0) scan_min_y = 0; + if (scan_max_x >= arena_w) scan_max_x = arena_w - 1; + if (scan_max_y >= arena_h) scan_max_y = arena_h - 1; + + int best_dsq = 0x3fffffff; + int best_cost = 100; + for (int x = scan_min_x; x <= scan_max_x; x++) { + for (int y = scan_min_y; y <= scan_max_y; y++) { + if (!APPROACH_VISITED(x, y)) continue; + int tile_x = arena_base_x + x; + int tile_y = arena_base_y + y; + if (!is_walkable(ctx, tile_x, tile_y)) continue; + int cost = APPROACH_COST(x, y); + if (cost >= 100) continue; + int dsq = encounter_attack_seek_nearest_dsq( + tile_x, tile_y, seek_tiles, seek_count); + if (dsq < best_dsq || (dsq == best_dsq && cost < best_cost)) { + selected_x = x; + selected_y = y; + best_dsq = dsq; + best_cost = cost; + } + } + } + } + + int cur_x = selected_x; + int cur_y = selected_y; + if (selected_x < 0) goto approach_done; + + result.found = 1; + result.dest_x = arena_base_x + selected_x; + result.dest_y = arena_base_y + selected_y; + if (selected_x == local_src_x && selected_y == local_src_y) + goto approach_done; + + while (1) { + int v = APPROACH_VIA(cur_x, cur_y); + int prev_x = cur_x; + int prev_y = cur_y; + if (v & VIA_W) prev_x++; + else if (v & VIA_E) prev_x--; + if (v & VIA_S) prev_y++; + else if (v & VIA_N) prev_y--; + + if (prev_x == local_src_x && prev_y == local_src_y) { + result.next_dx = cur_x - local_src_x; + result.next_dy = cur_y - local_src_y; + goto approach_done; + } + + cur_x = prev_x; + cur_y = prev_y; + if (APPROACH_VIA(cur_x, cur_y) == VIA_NONE || + APPROACH_VIA(cur_x, cur_y) == VIA_START) { + result.found = 0; + result.next_dx = 0; + result.next_dy = 0; + goto approach_done; + } + } + +approach_done: + #undef APPROACH_VISITED + #undef APPROACH_VISIT + #undef APPROACH_VIA + #undef APPROACH_COST + #undef APPROACH_EB + return result; +} + +static inline int encounter_chase_attack_target( + Player* p, int target_x, int target_y, int target_size, int attack_range, + const CollisionMap* cmap, int world_offset_x, int world_offset_y, + encounter_walkable_fn is_walkable, void* ctx, + pathfind_blocked_fn extra_blocked, void* blocked_ctx, + const OsrsLosQuery* los_query, + int arena_base_x, int arena_base_y, int arena_w, int arena_h +) { + int dist = encounter_rect_distance(p->x, p->y, 1, + target_x, target_y, target_size); + + if (dist == 0) { + int max_r = (target_size + 1) / 2 + 1; + int best_dsq = 9999, bx = -1, by = -1; + for (int dy = -max_r; dy <= max_r; dy++) { + for (int dx = -max_r; dx <= max_r; dx++) { + if (dx == 0 && dy == 0) continue; + int nx = p->x + dx, ny = p->y + dy; + if (!is_walkable(ctx, nx, ny)) continue; + if (encounter_entity_footprints_overlap(nx, ny, 1, + target_x, target_y, target_size)) + continue; + int d = dx * dx + dy * dy; + if (d < best_dsq) { best_dsq = d; bx = nx; by = ny; } + } + } + if (bx < 0) return 0; + int steps = encounter_walk_toward(p, bx, by, + cmap, world_offset_x, world_offset_y, + is_walkable, ctx, extra_blocked, blocked_ctx, + arena_base_x, arena_base_y, arena_w, arena_h); + return steps > 0 ? 1 : 0; + } + + if (encounter_player_can_attack(p->x, p->y, target_x, target_y, + target_size, attack_range, + los_query)) + return 0; + + int cx, cy; + cx = -1; + cy = -1; + + if (arena_w <= 0) { + int scan_min_x = target_x - attack_range; + int scan_max_x = target_x + target_size - 1 + attack_range; + int scan_min_y = target_y - attack_range; + int scan_max_y = target_y + target_size - 1 + attack_range; + + cx = -1; + cy = -1; + int best_player_dsq = 0x3fffffff; + int best_target_dist = 0x3fffffff; + if (scan_min_x <= scan_max_x && scan_min_y <= scan_max_y) { + for (int yy = scan_min_y; yy <= scan_max_y; yy++) { + for (int xx = scan_min_x; xx <= scan_max_x; xx++) { + if (!is_walkable(ctx, xx, yy)) continue; + if (!encounter_player_can_attack(xx, yy, target_x, target_y, + target_size, attack_range, + los_query)) + continue; + int dx = xx - p->x; + int dy = yy - p->y; + int player_dsq = dx * dx + dy * dy; + int target_dist = encounter_rect_distance( + xx, yy, 1, target_x, target_y, target_size); + if (player_dsq < best_player_dsq || + (player_dsq == best_player_dsq && + target_dist < best_target_dist)) { + best_player_dsq = player_dsq; + best_target_dist = target_dist; + cx = xx; + cy = yy; + } + } + } + } + + if (cx < 0) { + cx = p->x < target_x ? target_x : + (p->x > target_x + target_size - 1 ? target_x + target_size - 1 : p->x); + cy = p->y < target_y ? target_y : + (p->y > target_y + target_size - 1 ? target_y + target_size - 1 : p->y); + } + } + + int steps = 0; + for (int step = 0; step < 2; step++) { + if (encounter_player_can_attack(p->x, p->y, target_x, target_y, + target_size, attack_range, + los_query)) + break; + PathResult pr = (arena_w > 0) + ? encounter_pathfind_arena_attack_approach( + cmap, world_offset_x, world_offset_y, + p->x, p->y, + target_x, target_y, target_size, attack_range, + is_walkable, ctx, + extra_blocked, blocked_ctx, + los_query, + arena_base_x, arena_base_y, arena_w, arena_h) + : encounter_pathfind(cmap, world_offset_x, world_offset_y, + p->x, p->y, cx, cy, + extra_blocked, blocked_ctx); + if (!pr.found || (pr.next_dx == 0 && pr.next_dy == 0)) break; + int nx = p->x + pr.next_dx, ny = p->y + pr.next_dy; + if (!is_walkable(ctx, nx, ny)) break; + p->x = nx; p->y = ny; + steps++; + } + p->is_running = (steps == 2); + p->dest_x = p->x; p->dest_y = p->y; + return steps > 0 ? 1 : 0; +} + +typedef int (*encounter_npc_blocked_fn)(void* ctx, int x, int y, int size); +typedef int (*encounter_npc_overlap_hold_fn)(void* ctx); + +typedef enum { + ENCOUNTER_NPC_STEP_TRAVEL_TARGET = 0, + ENCOUNTER_NPC_STEP_STOP_AT_MELEE = 1, + ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET = 2, + ENCOUNTER_NPC_STEP_OSRS_AGGRO_STOP_AT_MELEE = 3, +} EncounterNpcStepPolicy; + +#define ENCOUNTER_NPC_UNDER_PLAYER_NONE 0 +#define ENCOUNTER_NPC_UNDER_PLAYER_MOVED 1 +#define ENCOUNTER_NPC_UNDER_PLAYER_HELD 2 +#define ENCOUNTER_NPC_UNDER_PLAYER_BLOCKED 3 + +static inline int encounter_npc_x_edge_clear( + int x, int y, int size, int dx, int dy, + encounter_npc_blocked_fn is_blocked, void* ctx +) { + if (dx == 0) return 1; + int ex = (dx == 1) ? x + size : x - 1; + int y_start = (dy == -1) ? y - 1 : y; + int y_end = (dy == 1) ? y + size : y + size - 1; + for (int ey = y_start; ey <= y_end; ey++) + if (is_blocked(ctx, ex, ey, 1)) return 0; + return 1; +} + +static inline int encounter_npc_y_edge_clear( + int x, int y, int size, int dx, int dy, + encounter_npc_blocked_fn is_blocked, void* ctx +) { + if (dy == 0) return 1; + int ey = (dy == 1) ? y + size : y - 1; + int x_start = (dx == -1) ? x - 1 : x; + int x_end = (dx == 1) ? x + size : x + size - 1; + for (int ex = x_start; ex <= x_end; ex++) + if (is_blocked(ctx, ex, ey, 1)) return 0; + return 1; +} + +static inline int encounter_npc_axis_gap(int a, int a_size, int b, int b_size) { + int a_max = a + a_size - 1; + int b_max = b + b_size - 1; + if (a_max < b) return b - a_max; + if (b_max < a) return a - b_max; + return 0; +} + +static inline int encounter_npc_try_step( + int* x, int* y, int size, int dx, int dy, + encounter_npc_blocked_fn is_blocked, void* ctx +) { + if (dx == 0 && dy == 0) return 0; + int x_clear = encounter_npc_x_edge_clear(*x, *y, size, dx, dy, is_blocked, ctx); + int y_clear = encounter_npc_y_edge_clear(*x, *y, size, dx, dy, is_blocked, ctx); + if (x_clear && y_clear) { + *x += dx; + *y += dy; + return 1; + } + return 0; +} + +static inline int encounter_npc_step_out_from_under( + int* npc_x, int* npc_y, int npc_size, + int player_x, int player_y, + encounter_npc_blocked_fn is_blocked, void* ctx, + encounter_npc_overlap_hold_fn hold_overlap, + uint32_t* rng +) { + if (!encounter_entity_footprints_overlap( + *npc_x, *npc_y, npc_size, player_x, player_y, 1)) + return ENCOUNTER_NPC_UNDER_PLAYER_NONE; + if (hold_overlap && hold_overlap(ctx)) return ENCOUNTER_NPC_UNDER_PLAYER_HELD; + + int axis = encounter_rand_int(rng, 2); + int sign = encounter_rand_int(rng, 2) == 0 ? 1 : -1; + int dx = axis == 0 ? sign : 0; + int dy = axis == 1 ? sign : 0; + + if (encounter_npc_try_step(npc_x, npc_y, npc_size, dx, dy, is_blocked, ctx)) + return ENCOUNTER_NPC_UNDER_PLAYER_MOVED; + return ENCOUNTER_NPC_UNDER_PLAYER_BLOCKED; +} + +static inline int encounter_npc_step_toward_policy( + int* x, int* y, int tx, int ty, int npc_size, + int target_size, EncounterNpcStepPolicy policy, + encounter_npc_blocked_fn is_blocked, void* ctx, + encounter_npc_overlap_hold_fn hold_overlap, + uint32_t* rng +) { + int size = npc_size; + int is_aggro_policy = + policy == ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET || + policy == ENCOUNTER_NPC_STEP_OSRS_AGGRO_STOP_AT_MELEE; + int stops_at_melee = + policy == ENCOUNTER_NPC_STEP_STOP_AT_MELEE || + policy == ENCOUNTER_NPC_STEP_OSRS_AGGRO_STOP_AT_MELEE; + + if (is_aggro_policy && + encounter_entity_footprints_overlap( + *x, *y, size, tx, ty, target_size)) { + assert(target_size == 1); + assert(rng); + int stepped = encounter_npc_step_out_from_under( + x, y, size, tx, ty, is_blocked, ctx, hold_overlap, rng); + return stepped == ENCOUNTER_NPC_UNDER_PLAYER_MOVED; + } + + int x_gap = encounter_npc_axis_gap(*x, size, tx, target_size); + int y_gap = encounter_npc_axis_gap(*y, size, ty, target_size); + int raw_dx = tx - *x; + int raw_dy = ty - *y; + int dx = (raw_dx > 0) - (raw_dx < 0); + int dy = (raw_dy > 0) - (raw_dy < 0); + + if (x_gap == 0 && y_gap == 0) return 0; + if (stops_at_melee && x_gap + y_gap == 1) return 0; + if (dx == 0 && dy == 0) return 0; + + if (stops_at_melee && x_gap == 1 && y_gap == 1) { + return encounter_npc_try_step(x, y, size, dx, 0, is_blocked, ctx); + } + + if (is_aggro_policy && + dx != 0 && dy != 0 && + encounter_entity_footprints_overlap( + *x + dx, *y + dy, size, tx, ty, target_size)) { + dy = 0; + } + + if (dx != 0 && dy != 0 && + encounter_npc_try_step(x, y, size, dx, dy, is_blocked, ctx)) + return 1; + if (dx != 0 && encounter_npc_try_step(x, y, size, dx, 0, is_blocked, ctx)) + return 1; + if (dy != 0 && encounter_npc_try_step(x, y, size, 0, dy, is_blocked, ctx)) + return 1; + return 0; +} + +static inline void encounter_damage_player( + Player* p, int damage, float* damage_tracker +) { + if (damage > 0) { + p->current_hitpoints -= damage; + if (p->current_hitpoints < 0) p->current_hitpoints = 0; + if (damage_tracker) *damage_tracker += (float)damage; + } + p->hit_landed_this_tick = 1; + p->hit_damage = damage > 0 ? damage : 0; +} + +static inline int encounter_damage_npc( + int* hp, int* hit_landed, int* hit_damage, int damage +) { + int applied = 0; + if (damage > 0) { + applied = damage > *hp ? *hp : damage; + if (applied < 0) applied = 0; + *hp -= applied; + if (*hp < 0) *hp = 0; + } + *hit_landed = 1; + *hit_damage = applied; + return applied; +} + +static inline int encounter_resolve_npc_pending_hit( + EncounterPendingHit* ph, + int* npc_hp, int* hit_landed, int* hit_damage, + int* frozen_ticks, int* blood_heal_acc, float* damage_dealt_acc +) { + (void)frozen_ticks; + if (!ph->active) return 0; + ph->ticks_remaining--; + if (ph->ticks_remaining > 0) return 0; + + int dmg = encounter_damage_npc(npc_hp, hit_landed, hit_damage, ph->damage); + if (damage_dealt_acc) *damage_dealt_acc += dmg; + + if (ph->spell_type == ENCOUNTER_SPELL_BLOOD && blood_heal_acc) + *blood_heal_acc += dmg; + + ph->active = 0; + return 1; +} + +typedef void (*EncounterPendingHitObserver)( + void* user, const EncounterPendingHit* hit, int damage_after_prayer, + int prayer_was_correct, int prayer_was_checked); + +static inline void encounter_resolve_player_pending_hits_observed( + EncounterPendingHitQueue* queue, + Player* player, OverheadPrayer active_prayer, + float* damage_received_acc, int* prayer_correct_count, + int* off_prayer_hit_count, + EncounterPendingHitObserver observer, void* observer_user +) { + for (int i = 0; i < queue->count; i++) { + EncounterPendingHit* hit = &queue->hits[i]; + + if (hit->check_prayer && hit->prayer_check_delay > 0) { + hit->prayer_check_delay--; + if (hit->prayer_check_delay == 0) { + if (encounter_prayer_correct_for_style(active_prayer, hit->attack_style)) { + hit->damage = 0; + if (prayer_correct_count) (*prayer_correct_count)++; + } else if (hit->damage > 0 && hit->attack_style != ATTACK_STYLE_NONE) { + if (off_prayer_hit_count) (*off_prayer_hit_count)++; + } + hit->check_prayer = 0; + } + } + hit->ticks_remaining--; + if (hit->ticks_remaining <= 0) { + int dmg = hit->damage; + int checked = hit->check_prayer; + int prayed = 0; + if (hit->check_prayer) { + prayed = encounter_prayer_correct_for_style(active_prayer, hit->attack_style); + if (prayed) { + dmg = 0; + if (prayer_correct_count) (*prayer_correct_count)++; + } else if (dmg > 0 && hit->attack_style != ATTACK_STYLE_NONE) { + if (off_prayer_hit_count) (*off_prayer_hit_count)++; + } + } else if (dmg > 0 && hit->attack_style != ATTACK_STYLE_NONE) { + if (off_prayer_hit_count) (*off_prayer_hit_count)++; + } + + if (observer && hit->attack_style != ATTACK_STYLE_NONE) + observer(observer_user, hit, dmg, prayed, checked); + encounter_damage_player(player, dmg, damage_received_acc); + encounter_pending_hit_queue_remove(queue, i, "player"); + i--; + } + } +} + +static inline void encounter_clear_tick_flags(Player* p) { + p->attack_style_this_tick = ATTACK_STYLE_NONE; + p->magic_type_this_tick = 0; + p->hit_landed_this_tick = 0; + p->hit_damage = 0; + p->hit_was_successful = 0; + p->elysian_proc_this_tick = 0; + p->cast_veng_this_tick = 0; + p->ate_food_this_tick = 0; + p->ate_karambwan_this_tick = 0; + p->used_special_this_tick = 0; +} + +static inline uint32_t encounter_resolve_seed(uint32_t saved_rng, uint32_t explicit_seed) { + uint32_t rng = 12345; + if (saved_rng != 0) rng = saved_rng; + if (explicit_seed != 0) rng = explicit_seed; + return rng; +} + +static inline int encounter_overhead_drain_effect(OverheadPrayer prayer) { + switch (prayer) { + case PRAYER_PROTECT_MELEE: return 12; + case PRAYER_PROTECT_RANGED: return 12; + case PRAYER_PROTECT_MAGIC: return 12; + case PRAYER_SMITE: return 12; + case PRAYER_REDEMPTION: return 6; + default: return 0; + } +} + +static inline int encounter_offensive_drain_effect(OffensivePrayer prayer) { + switch (prayer) { + case OFFENSIVE_PRAYER_PIETY: return 24; + case OFFENSIVE_PRAYER_RIGOUR: return 24; + case OFFENSIVE_PRAYER_AUGURY: return 24; + case OFFENSIVE_PRAYER_MELEE_LOW: return 6; + case OFFENSIVE_PRAYER_RANGED_LOW: return 6; + case OFFENSIVE_PRAYER_MAGIC_LOW: return 6; + default: return 0; + } +} + +static inline int encounter_player_prayer_bonus(const Player* p) { + EquipmentBonuses bonuses; + osrs_sum_equipment_bonuses(p->equipped, &bonuses); + return bonuses.prayer; +} + +static inline void encounter_drain_all_prayers(Player* p, int prayer_bonus) { + int overhead_effect = p->prayer_just_activated + ? 0 : encounter_overhead_drain_effect(p->prayer); + int offensive_effect = p->offensive_prayer_just_activated + ? 0 : encounter_offensive_drain_effect(p->offensive_prayer); + int total = overhead_effect + offensive_effect; + + p->prayer_just_activated = 0; + p->offensive_prayer_just_activated = 0; + + if (p->current_prayer <= 0) { + p->current_prayer = 0; + p->prayer_drain_counter = 0; + p->prayer = PRAYER_NONE; + p->offensive_prayer = OFFENSIVE_PRAYER_NONE; + return; + } + if (total <= 0) return; + + int drain_resistance = 60 + prayer_bonus * 2; + p->prayer_drain_counter += total; + while (p->prayer_drain_counter > drain_resistance) { + p->current_prayer--; + p->prayer_drain_counter -= drain_resistance; + if (p->current_prayer <= 0) { + p->current_prayer = 0; + p->prayer_drain_counter = 0; + p->prayer = PRAYER_NONE; + p->offensive_prayer = OFFENSIVE_PRAYER_NONE; + break; + } + } +} + +typedef struct { + int attack_bonus; + int strength_bonus; + int eff_level; + int max_hit; + int attack_speed; + int attack_range; + AttackStyle style; + FightStyle fight_style; + int def_stab, def_slash, def_crush, def_magic, def_ranged; + float att_prayer_mult; + float str_prayer_mult; + int spell_base_damage; +} EncounterLoadoutStats; + +static inline void encounter_effective_loadout_for_equipment( + const uint8_t loadout[NUM_GEAR_SLOTS], + uint8_t out[NUM_GEAR_SLOTS] +) { + memcpy(out, loadout, NUM_GEAR_SLOTS); + out[GEAR_SLOT_SHIELD] = osrs_suppress_shield_for_two_handed_weapon( + out[GEAR_SLOT_WEAPON], out[GEAR_SLOT_SHIELD]); +} + +static inline void encounter_derive_loadout_effect_profile( + const uint8_t loadout[NUM_GEAR_SLOTS], + OsrsEquipmentEffectProfile* out +) { + uint8_t effective_loadout[NUM_GEAR_SLOTS]; + encounter_effective_loadout_for_equipment(loadout, effective_loadout); + osrs_derive_equipment_effect_profile(effective_loadout, out); +} + +static inline void encounter_offensive_prayer_mults( + OffensivePrayer op, AttackStyle style, float* att_out, float* str_out +) { + float att = 1.0f, str = 1.0f; + switch (style) { + case ATTACK_STYLE_MELEE: + if (op == OFFENSIVE_PRAYER_PIETY) { + att = 1.20f; + str = 1.23f; + } else if (op == OFFENSIVE_PRAYER_MELEE_LOW) { + att = 1.15f; + str = 1.15f; + } + break; + case ATTACK_STYLE_RANGED: + if (op == OFFENSIVE_PRAYER_RIGOUR) { + att = 1.20f; + str = 1.23f; + } else if (op == OFFENSIVE_PRAYER_RANGED_LOW) { + att = 1.15f; + str = 1.15f; + } + break; + case ATTACK_STYLE_MAGIC: + if (op == OFFENSIVE_PRAYER_AUGURY) { + att = 1.25f; + } else if (op == OFFENSIVE_PRAYER_MAGIC_LOW) { + att = 1.15f; + } + break; + default: break; + } + *att_out = att; + *str_out = str; +} + +static inline void encounter_compute_loadout_stats( + const uint8_t loadout[NUM_GEAR_SLOTS], + AttackStyle style, + OffensivePrayer offensive_prayer, + int base_level, + FightStyle fight_style, + int spell_base_damage, + EncounterLoadoutStats* out +) { + memset(out, 0, sizeof(*out)); + out->style = style; + out->fight_style = fight_style; + + uint8_t effective_loadout[NUM_GEAR_SLOTS]; + encounter_effective_loadout_for_equipment(loadout, effective_loadout); + + EquipmentBonuses eb; + osrs_sum_equipment_bonuses(effective_loadout, &eb); + + const Item* weapon_item = get_item(loadout[GEAR_SLOT_WEAPON]); + if (style == ATTACK_STYLE_MAGIC && weapon_item && + (weapon_item->effect_mask & OSRS_ITEM_EFFECT_TUMEKENS_SHADOW)) { + eb.magic_damage *= 3; + if (eb.magic_damage > 100) eb.magic_damage = 100; + } + + out->def_stab = eb.defence_stab; + out->def_slash = eb.defence_slash; + out->def_crush = eb.defence_crush; + out->def_magic = eb.defence_magic; + out->def_ranged = eb.defence_ranged; + + out->attack_speed = eb.attack_speed + osrs_stance_speed_mod(fight_style); + out->attack_range = eb.attack_range + osrs_stance_range_mod(fight_style); + + if (style == ATTACK_STYLE_MAGIC) { + out->attack_bonus = eb.attack_magic; + } else if (style == ATTACK_STYLE_RANGED) { + out->attack_bonus = eb.attack_ranged; + } else { + out->attack_bonus = eb.attack_stab; + if (eb.attack_slash > out->attack_bonus) out->attack_bonus = eb.attack_slash; + if (eb.attack_crush > out->attack_bonus) out->attack_bonus = eb.attack_crush; + } + + float att_prayer_mult, str_prayer_mult; + encounter_offensive_prayer_mults( + offensive_prayer, style, &att_prayer_mult, &str_prayer_mult); + + out->att_prayer_mult = att_prayer_mult; + out->str_prayer_mult = str_prayer_mult; + out->spell_base_damage = spell_base_damage; + + int att_stance_bonus = osrs_stance_att_bonus(fight_style, style); + int str_stance_bonus = osrs_stance_str_bonus(fight_style); + + if (style == ATTACK_STYLE_MAGIC) { + out->eff_level = osrs_magic_effective_attack_level( + base_level, att_prayer_mult, fight_style); + } else { + out->eff_level = (int)(base_level * att_prayer_mult) + att_stance_bonus + 8; + } + + int eff_str_level = (int)(base_level * str_prayer_mult) + str_stance_bonus + 8; + + float magic_dmg_prayer_mult = osrs_offensive_magic_dmg_mult(offensive_prayer); + + if (style == ATTACK_STYLE_RANGED) { + out->strength_bonus = eb.ranged_strength; + out->max_hit = (int)(0.5 + eff_str_level * (eb.ranged_strength + 64) / 640.0); + } else if (style == ATTACK_STYLE_MAGIC) { + out->strength_bonus = eb.magic_damage; + out->max_hit = (int)(spell_base_damage * (1.0 + eb.magic_damage / 100.0) * magic_dmg_prayer_mult); + } else { + out->strength_bonus = eb.melee_strength; + out->max_hit = (int)(0.5 + eff_str_level * (eb.melee_strength + 64) / 640.0); + } +} + +static inline void encounter_update_loadout_level( + EncounterLoadoutStats* ls, OffensivePrayer offensive_prayer, + int current_att_level, int current_str_level +) { + float att_prayer_mult, str_prayer_mult; + encounter_offensive_prayer_mults( + offensive_prayer, ls->style, &att_prayer_mult, &str_prayer_mult); + ls->att_prayer_mult = att_prayer_mult; + ls->str_prayer_mult = str_prayer_mult; + + int att_stance_bonus = osrs_stance_att_bonus(ls->fight_style, ls->style); + int str_stance_bonus = osrs_stance_str_bonus(ls->fight_style); + if (ls->style == ATTACK_STYLE_MAGIC) { + ls->eff_level = osrs_magic_effective_attack_level( + current_att_level, att_prayer_mult, ls->fight_style); + float magic_dmg_mult = osrs_offensive_magic_dmg_mult(offensive_prayer); + ls->max_hit = (int)(ls->spell_base_damage * (1.0 + ls->strength_bonus / 100.0) * magic_dmg_mult); + } else { + ls->eff_level = (int)(current_att_level * att_prayer_mult) + att_stance_bonus + 8; + int eff_str = (int)(current_str_level * str_prayer_mult) + str_stance_bonus + 8; + ls->max_hit = (int)(0.5 + eff_str * (ls->strength_bonus + 64) / 640.0); + } +} + +static inline void encounter_compute_player_equipped_stats( + Player* p, + AttackStyle style, + FightStyle fight_style, + int spell_base_damage, + EncounterLoadoutStats* out +) { + int current_att = p->current_attack; + int current_str = p->current_strength; + if (style == ATTACK_STYLE_RANGED) { + current_att = p->current_ranged; + current_str = p->current_ranged; + } else if (style == ATTACK_STYLE_MAGIC) { + current_att = p->current_magic; + current_str = p->current_magic; + } + encounter_compute_loadout_stats( + p->equipped, + style, + p->offensive_prayer, + current_att, + fight_style, + spell_base_damage, + out); + encounter_update_loadout_level(out, p->offensive_prayer, current_att, current_str); +} + +static inline void encounter_init_maxed_player_combat_stats( + Player* p, + int prayer_level +) { + p->base_attack = MAXED_BASE_ATTACK; + p->base_strength = MAXED_BASE_STRENGTH; + p->base_defence = MAXED_BASE_DEFENCE; + p->base_ranged = MAXED_BASE_RANGED; + p->base_magic = MAXED_BASE_MAGIC; + p->base_prayer = prayer_level; + p->base_hitpoints = MAXED_BASE_HITPOINTS; + + p->current_attack = p->base_attack; + p->current_strength = p->base_strength; + p->current_defence = p->base_defence; + p->current_ranged = p->base_ranged; + p->current_magic = p->base_magic; + p->current_prayer = p->base_prayer; + p->current_hitpoints = p->base_hitpoints; +} + +static inline void encounter_apply_saturated_heart_boost(Player* p) { + int boost = osrs_saturated_heart_magic_boost(p->base_magic); + int cap = p->base_magic + boost; + p->current_magic += boost; + if (p->current_magic > cap) p->current_magic = cap; + p->saturated_heart_active_ticks = 500; +} + +static inline int encounter_tick_saturated_heart(Player* p) { + if (p->saturated_heart_active_ticks <= 0) return 0; + p->saturated_heart_active_ticks -= 1; + if (p->saturated_heart_active_ticks > 0) return 0; + if (p->current_magic <= p->base_magic) return 0; + p->current_magic = p->base_magic; + return 1; +} + +static inline int encounter_saturated_heart_protects_magic(const Player* p) { + if (p->saturated_heart_active_ticks <= 0) return 0; + int cap = p->base_magic + osrs_saturated_heart_magic_boost(p->base_magic); + return p->current_magic <= cap && p->current_magic > p->base_magic; +} + +static inline int encounter_decay_stat_toward_base(int* current, int base) { + if (*current > base) { + *current -= 1; + return 1; + } + if (*current < base) { + *current += 1; + return 1; + } + return 0; +} + +static inline int encounter_decay_player_combat_stats_toward_base(Player* p) { + int changed = 0; + changed |= encounter_decay_stat_toward_base(&p->current_attack, p->base_attack); + changed |= encounter_decay_stat_toward_base(&p->current_strength, p->base_strength); + changed |= encounter_decay_stat_toward_base(&p->current_defence, p->base_defence); + changed |= encounter_decay_stat_toward_base(&p->current_ranged, p->base_ranged); + if (!encounter_saturated_heart_protects_magic(p)) + changed |= encounter_decay_stat_toward_base(&p->current_magic, p->base_magic); + return changed; +} + +#define ENCOUNTER_STAT_DRIFT_TICKS 100 +#define ENCOUNTER_DIVINE_POTION_TICKS 500 +#define ENCOUNTER_STAT_DRIFT_UNPINNED (-1) + +typedef struct { + int attack_floor; + int strength_floor; + int defence_floor; + int ranged_floor; + int magic_floor; +} EncounterStatDriftPins; + +static inline EncounterStatDriftPins encounter_stat_drift_no_pins(void) { + return (EncounterStatDriftPins){ + .attack_floor = ENCOUNTER_STAT_DRIFT_UNPINNED, + .strength_floor = ENCOUNTER_STAT_DRIFT_UNPINNED, + .defence_floor = ENCOUNTER_STAT_DRIFT_UNPINNED, + .ranged_floor = ENCOUNTER_STAT_DRIFT_UNPINNED, + .magic_floor = ENCOUNTER_STAT_DRIFT_UNPINNED, + }; +} + +static inline int encounter_merge_stat_drift_floor(int a, int b) { + if (a == ENCOUNTER_STAT_DRIFT_UNPINNED) return b; + if (b == ENCOUNTER_STAT_DRIFT_UNPINNED) return a; + return a > b ? a : b; +} + +static inline EncounterStatDriftPins encounter_merge_stat_drift_pins( + EncounterStatDriftPins a, + EncounterStatDriftPins b +) { + return (EncounterStatDriftPins){ + .attack_floor = encounter_merge_stat_drift_floor(a.attack_floor, b.attack_floor), + .strength_floor = encounter_merge_stat_drift_floor(a.strength_floor, b.strength_floor), + .defence_floor = encounter_merge_stat_drift_floor(a.defence_floor, b.defence_floor), + .ranged_floor = encounter_merge_stat_drift_floor(a.ranged_floor, b.ranged_floor), + .magic_floor = encounter_merge_stat_drift_floor(a.magic_floor, b.magic_floor), + }; +} + +static inline EncounterStatDriftPins encounter_divine_super_combat_pins(const Player* p) { + EncounterStatDriftPins pins = encounter_stat_drift_no_pins(); + pins.attack_floor = p->base_attack + osrs_super_combat_boost_amount(p->base_attack); + pins.strength_floor = + p->base_strength + osrs_super_combat_boost_amount(p->base_strength); + pins.defence_floor = p->base_defence + osrs_super_combat_boost_amount(p->base_defence); + return pins; +} + +static inline EncounterStatDriftPins encounter_divine_ranging_pins(const Player* p) { + EncounterStatDriftPins pins = encounter_stat_drift_no_pins(); + pins.ranged_floor = p->base_ranged + osrs_ranging_boost_amount(p->base_ranged); + return pins; +} + +static inline int encounter_enforce_stat_drift_floor(int* current, int base, int floor) { + if (floor <= base) return 0; + if (*current >= floor) return 0; + *current = floor; + return 1; +} + +static inline int encounter_enforce_stat_drift_pins(Player* p, EncounterStatDriftPins pins) { + int changed = 0; + changed |= encounter_enforce_stat_drift_floor( + &p->current_attack, p->base_attack, pins.attack_floor); + changed |= encounter_enforce_stat_drift_floor( + &p->current_strength, p->base_strength, pins.strength_floor); + changed |= encounter_enforce_stat_drift_floor( + &p->current_defence, p->base_defence, pins.defence_floor); + changed |= encounter_enforce_stat_drift_floor( + &p->current_ranged, p->base_ranged, pins.ranged_floor); + changed |= encounter_enforce_stat_drift_floor( + &p->current_magic, p->base_magic, pins.magic_floor); + return changed; +} + +static inline int encounter_drift_stat_toward_base_with_floor( + int* current, + int base, + int floor +) { + if (floor > base && *current <= floor) { + if (*current == floor) return 0; + *current = floor; + return 1; + } + if (*current > base) { + *current -= 1; + if (floor > base && *current < floor) *current = floor; + return 1; + } + if (*current < base) { + *current += 1; + return 1; + } + return 0; +} + +static inline int encounter_tick_stat_drift( + Player* p, + int* stat_drift_timer, + EncounterStatDriftPins pins +) { + assert(p && stat_drift_timer); + assert(*stat_drift_timer >= 0 && *stat_drift_timer < ENCOUNTER_STAT_DRIFT_TICKS); + + int changed = encounter_enforce_stat_drift_pins(p, pins); + *stat_drift_timer += 1; + if (*stat_drift_timer < ENCOUNTER_STAT_DRIFT_TICKS) return changed; + *stat_drift_timer = 0; + + changed |= encounter_drift_stat_toward_base_with_floor( + &p->current_attack, p->base_attack, pins.attack_floor); + changed |= encounter_drift_stat_toward_base_with_floor( + &p->current_strength, p->base_strength, pins.strength_floor); + changed |= encounter_drift_stat_toward_base_with_floor( + &p->current_defence, p->base_defence, pins.defence_floor); + changed |= encounter_drift_stat_toward_base_with_floor( + &p->current_ranged, p->base_ranged, pins.ranged_floor); + changed |= encounter_drift_stat_toward_base_with_floor( + &p->current_magic, p->base_magic, pins.magic_floor); + if (p->current_hitpoints > 0) + changed |= encounter_decay_stat_toward_base(&p->current_hitpoints, p->base_hitpoints); + return changed; +} + +typedef enum { + ENCOUNTER_CONSUMABLE_STAT_EFFECT_NONE = 0, + ENCOUNTER_CONSUMABLE_STAT_EFFECT_BREW_DRAIN, + ENCOUNTER_CONSUMABLE_STAT_EFFECT_RESTORE, + ENCOUNTER_CONSUMABLE_STAT_EFFECT_SANFEW, +} EncounterConsumableStatEffect; + +static inline void encounter_apply_brew_heal(Player* p, int brew_heal) { + p->current_hitpoints += brew_heal; + if (p->current_hitpoints > p->base_hitpoints + brew_heal) + p->current_hitpoints = p->base_hitpoints + brew_heal; + p->ate_food_this_tick = 1; +} + +static inline void encounter_apply_brew_heal_and_timer(Player* p, int brew_heal) { + encounter_apply_brew_heal(p, brew_heal); + p->brew_doses--; + p->potion_timer = 3; +} + +static inline void encounter_add_prayer_restore(Player* p, int restore_amount) { + p->current_prayer += restore_amount; +} + +static inline void encounter_cap_prayer_restore(Player* p) { + if (p->current_prayer > p->base_prayer) + p->current_prayer = p->base_prayer; +} + +static inline void encounter_finish_potion_dose(Player* p, int* doses) { + (*doses)--; + p->potion_timer = 3; +} + +static inline void encounter_brew_drain_stats(Player* p) { + BrewResult brew = osrs_brew_effect(p->base_hitpoints, p->base_defence, + p->current_attack, p->current_strength, + p->current_ranged, p->current_magic); + + p->current_attack -= brew.att_drain; + if (p->current_attack < 0) p->current_attack = 0; + p->current_strength -= brew.str_drain; + if (p->current_strength < 0) p->current_strength = 0; + p->current_ranged -= brew.range_drain; + if (p->current_ranged < 0) p->current_ranged = 0; + p->current_magic -= brew.magic_drain; + if (p->current_magic < 0) p->current_magic = 0; + + p->current_defence += brew.def_boost; + int def_cap = p->base_defence + brew.def_boost; + if (p->current_defence > def_cap) p->current_defence = def_cap; +} + +static inline void encounter_apply_stat_restore(Player* p, int (*amount)(int)) { + int restore = amount(p->base_attack); + p->current_attack += restore; + if (p->current_attack > p->base_attack) p->current_attack = p->base_attack; + restore = amount(p->base_strength); + p->current_strength += restore; + if (p->current_strength > p->base_strength) p->current_strength = p->base_strength; + restore = amount(p->base_defence); + p->current_defence += restore; + if (p->current_defence > p->base_defence) p->current_defence = p->base_defence; + restore = amount(p->base_ranged); + p->current_ranged += restore; + if (p->current_ranged > p->base_ranged) p->current_ranged = p->base_ranged; + restore = amount(p->base_magic); + p->current_magic += restore; + if (p->current_magic > p->base_magic) p->current_magic = p->base_magic; +} + +static inline void encounter_restore_stats(Player* p) { + encounter_apply_stat_restore(p, osrs_super_restore_amount); +} + +static inline void encounter_sanfew_restore_stats(Player* p) { + encounter_apply_stat_restore(p, osrs_sanfew_restore_amount); +} + +static inline void encounter_apply_consumable_stat_effect( + Player* p, + EncounterConsumableStatEffect effect +) { + switch (effect) { + case ENCOUNTER_CONSUMABLE_STAT_EFFECT_NONE: + return; + case ENCOUNTER_CONSUMABLE_STAT_EFFECT_BREW_DRAIN: + encounter_brew_drain_stats(p); + return; + case ENCOUNTER_CONSUMABLE_STAT_EFFECT_RESTORE: + encounter_restore_stats(p); + return; + case ENCOUNTER_CONSUMABLE_STAT_EFFECT_SANFEW: + encounter_sanfew_restore_stats(p); + return; + } + abort(); +} + +static inline void encounter_bastion_boost(Player* p) { + int rng_boost = osrs_ranging_boost_amount(p->base_ranged); + int def_boost = osrs_super_combat_boost_amount(p->base_defence); + p->current_ranged += rng_boost; + int rng_cap = p->base_ranged + rng_boost; + if (p->current_ranged > rng_cap) p->current_ranged = rng_cap; + p->current_defence += def_boost; + int def_cap = p->base_defence + def_boost; + if (p->current_defence > def_cap) p->current_defence = def_cap; +} + +static inline void encounter_super_combat_boost(Player* p) { + int boost = osrs_super_combat_boost_amount(p->base_attack); + p->current_attack += boost; + if (p->current_attack > p->base_attack + boost) p->current_attack = p->base_attack + boost; + boost = osrs_super_combat_boost_amount(p->base_strength); + p->current_strength += boost; + if (p->current_strength > p->base_strength + boost) p->current_strength = p->base_strength + boost; + boost = osrs_super_combat_boost_amount(p->base_defence); + p->current_defence += boost; + if (p->current_defence > p->base_defence + boost) p->current_defence = p->base_defence + boost; +} + +static inline void encounter_ranging_boost(Player* p) { + int boost = osrs_ranging_boost_amount(p->base_ranged); + p->current_ranged += boost; + if (p->current_ranged > p->base_ranged + boost) p->current_ranged = p->base_ranged + boost; +} + +static inline void encounter_recompute_loadout_max_hits( + EncounterLoadoutStats* loadouts, int num_loadouts, Player* p +) { + for (int i = 0; i < num_loadouts; i++) { + EncounterLoadoutStats* ls = &loadouts[i]; + if (ls->style == ATTACK_STYLE_RANGED) { + encounter_update_loadout_level(ls, p->offensive_prayer, p->current_ranged, p->current_ranged); + } else if (ls->style == ATTACK_STYLE_MAGIC) { + encounter_update_loadout_level(ls, p->offensive_prayer, p->current_magic, p->current_magic); + } else { + encounter_update_loadout_level(ls, p->offensive_prayer, p->current_attack, p->current_strength); + } + } +} + +static inline int encounter_use_spec(Player* p, int cost) { + if (p->special_energy < cost) return 0; + p->special_energy -= cost; + return 1; +} + +static inline void encounter_apply_loadout( + Player* p, const uint8_t loadout[NUM_GEAR_SLOTS], GearSet gear_set +) { + encounter_effective_loadout_for_equipment(loadout, p->equipped); + p->current_gear = gear_set; + p->visible_gear = gear_set; + osrs_refresh_player_equipment(p); +} + +static void encounter_populate_inventory( + Player* p, + const uint8_t* const* loadouts, int num_loadouts, + const uint8_t extra_items[NUM_GEAR_SLOTS] +) { + memset(p->inventory, 255 , sizeof(p->inventory)); + memset(p->num_items_in_slot, 0, sizeof(p->num_items_in_slot)); + + for (int s = 0; s < NUM_GEAR_SLOTS; s++) { + int n = 0; + for (int l = 0; l < num_loadouts && n < MAX_ITEMS_PER_SLOT; l++) { + uint8_t item = loadouts[l][s]; + if (item == 255 ) continue; + int dup = 0; + for (int j = 0; j < n; j++) { if (p->inventory[s][j] == item) { dup = 1; break; } } + if (dup) continue; + p->inventory[s][n++] = item; + } + if (extra_items && extra_items[s] != 255 && n < MAX_ITEMS_PER_SLOT) { + int dup = 0; + for (int j = 0; j < n; j++) { if (p->inventory[s][j] == extra_items[s]) { dup = 1; break; } } + if (!dup) p->inventory[s][n++] = extra_items[s]; + } + p->num_items_in_slot[s] = n; + } +} + +static inline void encounter_clear_ammo_inventory_slot(Player* p) { + for (int i = 0; i < MAX_ITEMS_PER_SLOT; i++) + p->inventory[GEAR_SLOT_AMMO][i] = ITEM_NONE; + p->num_items_in_slot[GEAR_SLOT_AMMO] = 0; +} + +static inline void encounter_translate_movement(HumanInput* hi, int* actions, + int head_move, + void* (*get_entity)(void*, int), + void* state) { + if (hi->pending_move_x < 0 || hi->pending_move_y < 0 || head_move < 0) return; + Player* player = (Player*)get_entity(state, 0); + if (!player) return; + int dx = hi->pending_move_x - player->x; + int dy = hi->pending_move_y - player->y; + int sx = (dx > 0) ? 1 : (dx < 0) ? -1 : 0; + int sy = (dy > 0) ? 1 : (dy < 0) ? -1 : 0; + static const int DX8[9] = { 0, 0, 1, 1, 1, 0, -1, -1, -1 }; + static const int DY8[9] = { 0, 1, 1, 0, -1, -1, -1, 0, 1 }; + for (int m = 1; m < 9; m++) { + if (DX8[m] == sx && DY8[m] == sy) { + actions[head_move] = m; + break; + } + } +} + +static inline void encounter_translate_prayer(HumanInput* hi, int* actions, int head_prayer) { + if (hi->pending_prayer < 0 || head_prayer < 0) return; + actions[head_prayer] = hi->pending_prayer; +} + +static inline void encounter_translate_offensive_prayer( + HumanInput* hi, int* actions, int head_offensive +) { + if (hi->pending_offensive_prayer < 0 || head_offensive < 0) return; + actions[head_offensive] = hi->pending_offensive_prayer; +} + +static inline int encounter_find_observed_target_slot( + const int* current_obs_slots, + int observed_slot_count, + int raw_npc_slot +) { + for (int slot = 0; slot < observed_slot_count; slot++) + if (current_obs_slots[slot] == raw_npc_slot) return slot; + return -1; +} + +typedef struct { + const char* name; + + int obs_size; + int num_action_heads; + const int* action_head_dims; + int mask_size; + + size_t state_size; + size_t context_size; + void (*init_context)(EncounterContext* context); + void (*destroy_context)(EncounterContext* context); + void (*init_state)(EncounterState* state, EncounterContext* context); + + EncounterState* (*create)(void); + void (*destroy)(EncounterState* state); + + void (*reset)(EncounterState* state, EncounterContext* context, uint32_t seed); + void (*step)(EncounterState* state, EncounterContext* context, const int* actions); + void (*step_human_commands)( + EncounterState* state, EncounterContext* context, struct HumanInput* hi); + + size_t (*snapshot_size)(EncounterState* state, EncounterContext* context); + void (*snapshot)(EncounterState* state, EncounterContext* context, void* out); + void (*restore)( + EncounterState* state, EncounterContext* context, const void* data, size_t n); + + size_t (*cell_key_size)(EncounterState* state, EncounterContext* context); + void (*write_cell_key)(EncounterState* state, EncounterContext* context, void* out); + float (*progress_score)(EncounterState* state, EncounterContext* context); + + void (*write_obs)(EncounterState* state, EncounterContext* context, float* obs_out); + void (*write_mask)(EncounterState* state, EncounterContext* context, float* mask_out); + float (*get_reward)(EncounterState* state, EncounterContext* context); + int (*is_terminal)(EncounterState* state, EncounterContext* context); + + int (*get_entity_count)(EncounterState* state, EncounterContext* context); + void* (*get_entity)( + EncounterState* state, EncounterContext* context, int index); + + void (*fill_render_entities)( + EncounterState* state, + EncounterContext* context, + RenderEntity* out, + int max_entities, + int* count); + + void (*put_int)(EncounterState* state, EncounterContext* context, const char* key, int value); + void (*put_float)( + EncounterState* state, EncounterContext* context, const char* key, float value); + void (*put_ptr)( + EncounterState* state, EncounterContext* context, const char* key, void* value); + + int arena_base_x, arena_base_y; + int arena_width, arena_height; + + void (*translate_human_input)( + struct HumanInput* hi, int* actions, EncounterState* state, EncounterContext* context); + int (*is_human_targetable_npc_slot)( + EncounterState* state, EncounterContext* context, int npc_slot); + + int (*apply_lab_command)( + EncounterState* state, EncounterContext* context, const char* line); + + int head_move; + int head_prayer; + int head_target; + + void (*render_post_tick)( + EncounterState* state, EncounterContext* context, EncounterOverlay* overlay); + + void* (*get_log)(EncounterState* state, EncounterContext* context); + + int (*get_tick)(EncounterState* state, EncounterContext* context); + int (*get_winner)(EncounterState* state, EncounterContext* context); +} EncounterDef; + +static inline EncounterRuntime encounter_runtime_create(const EncounterDef* def) { + EncounterRuntime runtime = {0}; + if (!def || def->state_size == 0 || def->context_size == 0) { + fprintf(stderr, "encounter_runtime_create: %s has no typed runtime\n", + def ? def->name : "(null)"); + abort(); + } + runtime.state = (EncounterState*)calloc(1, def->state_size); + runtime.context = (EncounterContext*)calloc(1, def->context_size); + if (!runtime.state || !runtime.context) { + fprintf(stderr, "encounter_runtime_create: out of memory for %s\n", def->name); + abort(); + } + if (def->init_context) def->init_context(runtime.context); + if (def->init_state) def->init_state(runtime.state, runtime.context); + return runtime; +} + +static inline void encounter_runtime_destroy(const EncounterDef* def, EncounterRuntime* runtime) { + if (!runtime) return; + if (def && def->destroy_context && runtime->context) { + def->destroy_context(runtime->context); + } + free(runtime->state); + free(runtime->context); + runtime->state = NULL; + runtime->context = NULL; +} + +#define MAX_ENCOUNTERS 32 + +typedef struct { + const EncounterDef* defs[MAX_ENCOUNTERS]; + int count; +} EncounterRegistry; + +/* WARNING: static in header — each TU gets its own copy. only works correctly + when all encounter headers are included from a single compilation unit. */ +static EncounterRegistry g_encounter_registry = { .count = 0 }; + +static inline void encounter_register(const EncounterDef* def) { + if (g_encounter_registry.count >= MAX_ENCOUNTERS) { + fprintf(stderr, "encounter registry capacity exceeded: %d\n", MAX_ENCOUNTERS); + abort(); + } + g_encounter_registry.defs[g_encounter_registry.count++] = def; +} + +static inline const EncounterDef* encounter_find(const char* name) { + for (int i = 0; i < g_encounter_registry.count; i++) { + if (strcmp(g_encounter_registry.defs[i]->name, name) == 0) { + return g_encounter_registry.defs[i]; + } + } + return NULL; +} + +#endif diff --git a/ocean/osrs/osrs_encounter_player.h b/ocean/osrs/osrs_encounter_player.h new file mode 100644 index 0000000000..eebef8b928 --- /dev/null +++ b/ocean/osrs/osrs_encounter_player.h @@ -0,0 +1,230 @@ +#ifndef OSRS_ENCOUNTER_PLAYER_H +#define OSRS_ENCOUNTER_PLAYER_H + +#include "osrs_encounter.h" +#include "osrs_interaction.h" + +typedef enum { + OSRS_PLAYER_MOVE_NONE = 0, + OSRS_PLAYER_MOVE_ACTION, + OSRS_PLAYER_MOVE_DESTINATION, +} OsrsPlayerMoveKind; + +typedef enum { + OSRS_PLAYER_TARGET_MOVE_CHASE = 0, + OSRS_PLAYER_TARGET_MOVE_EXPLICIT_FIRST, +} OsrsPlayerTargetMovePolicy; + +typedef struct { + const CollisionMap* collision_map; + int world_offset_x; + int world_offset_y; + encounter_walkable_fn is_walkable; + void* walkable_ctx; + pathfind_blocked_fn extra_blocked; + void* blocked_ctx; + const OsrsLosQuery* los_query; + int arena_base_x; + int arena_base_y; + int arena_w; + int arena_h; +} OsrsEncounterArena; + +typedef struct { + int slot; + int x; + int y; + int size; + int attack_range; +} OsrsAttackTarget; + +typedef int (*OsrsAttackTargetLookupFn)( + void* ctx, + int target_slot, + OsrsAttackTarget* out); + +typedef struct { + Player* player; + OsrsInteraction* interaction; + OsrsAttackTargetLookupFn target_lookup; + void* target_ctx; + int has_new_target; + int new_target_slot; + OsrsPlayerMoveKind move_kind; + OsrsPlayerTargetMovePolicy target_move_policy; + int move_action; + int* dest_x; + int* dest_y; + int blocked_ticks; + OsrsEncounterArena arena; +} OsrsPlayerStepInput; + +typedef struct { + int moved; + int explicit_moved; + int chased_target; + int interaction_active; + int target_slot; + int can_attack; +} OsrsPlayerStepResult; + +static inline void osrs_player_step_require_input(const OsrsPlayerStepInput* input) { + if (!input || !input->player || !input->interaction || !input->arena.is_walkable) { + fprintf(stderr, "osrs player step input is missing required fields\n"); + abort(); + } + if ((input->move_kind == OSRS_PLAYER_MOVE_DESTINATION || + input->move_kind == OSRS_PLAYER_MOVE_ACTION) && + (!input->dest_x || !input->dest_y)) { + fprintf(stderr, "osrs player step move input is missing destination storage\n"); + abort(); + } +} + +static inline int osrs_player_step_lookup_target( + const OsrsPlayerStepInput* input, + int target_slot, + OsrsAttackTarget* target +) { + if (!input->target_lookup) return 0; + return input->target_lookup(input->target_ctx, target_slot, target); +} + +static inline int osrs_player_step_can_attack_target( + const OsrsPlayerStepInput* input, + const OsrsAttackTarget* target +) { + return encounter_player_can_attack( + input->player->x, + input->player->y, + target->x, + target->y, + target->size, + target->attack_range, + input->arena.los_query); +} + +static inline int osrs_player_step_apply_explicit_move( + const OsrsPlayerStepInput* input +) { + Player* player = input->player; + if (input->move_kind == OSRS_PLAYER_MOVE_ACTION) { + if (input->move_action <= 0 || input->move_action >= ENCOUNTER_MOVE_ACTIONS) + return 0; + return encounter_move_to_target( + player, + ENCOUNTER_MOVE_TARGET_DX[input->move_action], + ENCOUNTER_MOVE_TARGET_DY[input->move_action], + input->arena.is_walkable, + input->arena.walkable_ctx); + } + + if (input->move_kind != OSRS_PLAYER_MOVE_DESTINATION) + return 0; + return encounter_move_toward_dest( + player, + input->dest_x, + input->dest_y, + input->arena.collision_map, + input->arena.world_offset_x, + input->arena.world_offset_y, + input->arena.is_walkable, + input->arena.walkable_ctx, + input->arena.extra_blocked, + input->arena.blocked_ctx, + input->arena.arena_base_x, + input->arena.arena_base_y, + input->arena.arena_w, + input->arena.arena_h); +} + +static inline int osrs_player_step_chase_target( + const OsrsPlayerStepInput* input, + const OsrsAttackTarget* target +) { + return encounter_chase_attack_target( + input->player, + target->x, + target->y, + target->size, + target->attack_range, + input->arena.collision_map, + input->arena.world_offset_x, + input->arena.world_offset_y, + input->arena.is_walkable, + input->arena.walkable_ctx, + input->arena.extra_blocked, + input->arena.blocked_ctx, + input->arena.los_query, + input->arena.arena_base_x, + input->arena.arena_base_y, + input->arena.arena_w, + input->arena.arena_h); +} + +static inline OsrsPlayerStepResult osrs_encounter_player_step( + const OsrsPlayerStepInput* input +) { + osrs_player_step_require_input(input); + + OsrsPlayerStepResult result = { + .target_slot = -1, + }; + Player* player = input->player; + OsrsInteraction* interaction = input->interaction; + + if (input->blocked_ticks > 0) { + result.interaction_active = osrs_interaction_active(interaction); + result.target_slot = result.interaction_active ? interaction->target_slot : -1; + return result; + } + + if (input->has_new_target) { + OsrsAttackTarget target; + if (osrs_player_step_lookup_target(input, input->new_target_slot, &target)) { + osrs_interaction_set(interaction, input->new_target_slot); + } else { + osrs_interaction_clear(interaction); + } + } else if (input->move_kind != OSRS_PLAYER_MOVE_NONE) { + osrs_interaction_check_interrupt(interaction, OSRS_IACT_MOVE); + } + + OsrsAttackTarget target; + int has_target = 0; + if (osrs_interaction_active(interaction)) { + has_target = osrs_player_step_lookup_target( + input, interaction->target_slot, &target); + if (!has_target) { + osrs_interaction_clear(interaction); + } + } + + if (input->move_kind != OSRS_PLAYER_MOVE_NONE && + (!osrs_interaction_active(interaction) || + input->target_move_policy == OSRS_PLAYER_TARGET_MOVE_EXPLICIT_FIRST)) { + result.moved = osrs_player_step_apply_explicit_move(input) > 0; + result.explicit_moved = result.moved; + } else if (osrs_interaction_active(interaction) && has_target) { + result.moved = osrs_player_step_chase_target(input, &target) > 0; + result.chased_target = result.moved; + } + + if (osrs_interaction_active(interaction)) { + has_target = osrs_player_step_lookup_target( + input, interaction->target_slot, &target); + if (has_target) { + result.interaction_active = 1; + result.target_slot = interaction->target_slot; + result.can_attack = osrs_player_step_can_attack_target(input, &target); + } else { + osrs_interaction_clear(interaction); + } + } + + player->dest_x = player->x; + player->dest_y = player->y; + return result; +} + +#endif diff --git a/ocean/osrs/osrs_encounter_visual_events.h b/ocean/osrs/osrs_encounter_visual_events.h new file mode 100644 index 0000000000..18724501a8 --- /dev/null +++ b/ocean/osrs/osrs_encounter_visual_events.h @@ -0,0 +1,347 @@ +#ifndef OSRS_ENCOUNTER_VISUAL_EVENTS_H +#define OSRS_ENCOUNTER_VISUAL_EVENTS_H + +#include "osrs_combat_visuals.h" +#include "osrs_encounter.h" + +typedef struct { + int npc_def_id; + int npc_slot; + uint32_t npc_instance_id; + int npc_visible; + int npc_size; + int npc_anim_id; + int x; + int y; + int dest_x; + int dest_y; + RenderMovementKind render_movement_kind; + int current_hitpoints; + int base_hitpoints; + AttackStyle attack_style_this_tick; + int hit_landed_this_tick; + int hit_damage; + int render_hit_count; + int render_hit_damage[ENCOUNTER_RENDER_HITS_MAX]; + int hit_was_successful; + int hit_spell_type; + int attack_target_entity_idx; +} OsrsNpcRenderEntitySpec; + +typedef struct { + int src_x; + int src_y; + int dst_x; + int dst_y; + int style; + int damage; + int duration_ticks; + int start_h; + int end_h; + int curve; + float arc_height; + int src_size; + int dst_size; + uint32_t model_id; + int anim_id; + int travel_gfx_id; + int launch_gfx_id; + int impact_gfx_id; + int start_delay; +} OsrsProjectileEventSpec; + +typedef struct { + int src_x; + int src_y; + int dst_x; + int dst_y; + int src_size; + int dst_size; + int target_npc_slot; + AttackStyle attack_style; + int damage; + int duration_ticks; + int start_delay; + int fallback_start_h; + int fallback_end_h; + int curve; + int splash_gfx_id; +} OsrsCombatProjectileEmitSpec; + +static inline int osrs_npc_death_linger_start( + int current_hitpoints, + int active, + int* death_ticks, + int linger_ticks +) { + if (!death_ticks || linger_ticks <= 0) { + fprintf(stderr, "invalid npc death linger start input\n"); + abort(); + } + if (current_hitpoints > 0 || !active || *death_ticks > 0) + return 0; + *death_ticks = linger_ticks; + return 1; +} + +static inline int osrs_npc_death_linger_tick(int* death_ticks) { + if (!death_ticks) { + fprintf(stderr, "invalid npc death linger tick input\n"); + abort(); + } + if (*death_ticks <= 0) + return 0; + *death_ticks -= 1; + return *death_ticks == 0; +} + +static inline void osrs_render_entity_from_npc_spec( + const OsrsNpcRenderEntitySpec* spec, + RenderEntity* out +) { + if (!spec || !out || spec->npc_slot < 0 || spec->npc_size <= 0) { + fprintf(stderr, "invalid npc render entity spec\n"); + abort(); + } + memset(out, 0, sizeof(RenderEntity)); + memset(out->equipped, ITEM_NONE, NUM_GEAR_SLOTS); + out->entity_type = ENTITY_NPC; + out->npc_def_id = spec->npc_def_id; + out->npc_slot = spec->npc_slot; + out->npc_instance_id = spec->npc_instance_id; + out->npc_visible = spec->npc_visible; + out->npc_size = spec->npc_size; + out->npc_anim_id = spec->npc_anim_id; + out->x = spec->x; + out->y = spec->y; + out->dest_x = spec->dest_x; + out->dest_y = spec->dest_y; + out->render_movement_kind = spec->render_movement_kind; + out->current_hitpoints = spec->current_hitpoints; + out->base_hitpoints = spec->base_hitpoints; + out->attack_style_this_tick = spec->attack_style_this_tick; + out->hit_landed_this_tick = spec->hit_landed_this_tick; + out->hit_damage = spec->hit_damage; + if (spec->render_hit_count < 0 || + spec->render_hit_count > ENCOUNTER_RENDER_HITS_MAX) { + fprintf(stderr, "invalid npc render hit count\n"); + abort(); + } + out->render_hit_count = spec->render_hit_count; + for (int i = 0; i < spec->render_hit_count; i++) + out->render_hit_damage[i] = spec->render_hit_damage[i]; + if (out->render_hit_count == 0 && out->hit_landed_this_tick) { + out->render_hit_count = 1; + out->render_hit_damage[0] = out->hit_damage; + } + out->hit_was_successful = spec->hit_was_successful; + out->hit_spell_type = spec->hit_spell_type; + out->attack_target_entity_idx = spec->attack_target_entity_idx; +} + +static inline void osrs_npc_primary_anim_event_set( + Player* npc, + int* event_tick, + int* until_tick, + int tick, + int anim_id, + int duration_ticks +) { + if (!npc || !event_tick || !until_tick || anim_id < 0 || duration_ticks <= 0) { + fprintf(stderr, "invalid npc primary animation event\n"); + abort(); + } + npc->npc_anim_id = anim_id; + *event_tick = tick; + *until_tick = tick + duration_ticks; +} + +static inline int osrs_npc_primary_anim_event_should_emit( + const Player* npc, + int event_tick, + int tick +) { + return npc && npc->npc_anim_id >= 0 && event_tick == tick; +} + +static inline void osrs_npc_primary_anim_event_expire( + Player* npc, + int until_tick, + int tick +) { + if (npc && npc->npc_anim_id >= 0 && tick >= until_tick) + npc->npc_anim_id = -1; +} + +static inline void osrs_render_entity_from_npc_player( + const Player* npc, + RenderEntity* out, + int npc_slot, + uint32_t npc_instance_id +) { + if (!npc || !out || npc_slot < 0) { + fprintf(stderr, "invalid npc render entity input\n"); + abort(); + } + render_entity_from_player(npc, out); + out->npc_slot = npc_slot; + out->npc_instance_id = npc_instance_id; +} + +static inline void osrs_render_entity_from_player_entity( + const Player* player, + RenderEntity* out +) { + if (!player || !out) { + fprintf(stderr, "invalid player render entity input\n"); + abort(); + } + render_entity_from_player(player, out); + if (out->entity_type != ENTITY_PLAYER) { + fprintf(stderr, "render entity is not a player\n"); + abort(); + } +} + +static inline void osrs_render_entity_suppress_pose_anims( + RenderEntity* entity, + int idle_anim_id, + int walk_anim_id +) { + if (!entity) { + fprintf(stderr, "missing render entity\n"); + abort(); + } + if (entity->npc_anim_id == idle_anim_id || + entity->npc_anim_id == walk_anim_id) { + entity->npc_anim_id = -1; + } +} + +static inline int osrs_emit_projectile_with_spec( + EncounterOverlay* overlay, + const OsrsProjectileEventSpec* spec, + int tracks_target +) { + if (!overlay || !spec) { + fprintf(stderr, "invalid projectile event input\n"); + abort(); + } + int idx = encounter_emit_projectile( + overlay, + spec->src_x, + spec->src_y, + spec->dst_x, + spec->dst_y, + spec->style, + spec->damage, + spec->duration_ticks, + spec->start_h, + spec->end_h, + spec->curve, + spec->arc_height, + tracks_target, + spec->src_size, + spec->dst_size, + spec->model_id, + spec->impact_gfx_id); + if (spec->anim_id >= 0) + encounter_set_projectile_animation(overlay, idx, spec->anim_id); + if (spec->travel_gfx_id > 0) + encounter_set_projectile_travel_gfx(overlay, idx, spec->travel_gfx_id); + if (spec->launch_gfx_id > 0) + encounter_set_projectile_launch_gfx(overlay, idx, spec->launch_gfx_id); + overlay->projectiles[idx].start_delay = spec->start_delay; + return idx; +} + +static inline float osrs_combat_projectile_profile_arc_height( + const OsrsCombatProjectileProfile* profile, + AttackStyle attack_style +) { + if (attack_style == ATTACK_STYLE_MAGIC) + return 0.0f; + if (profile && profile->travel_spotanim_id == GFX_DRAGON_DART) + return 0.5f; + return 1.0f; +} + +static inline int osrs_emit_combat_projectile_profile_player_to_npc( + EncounterOverlay* overlay, + const OsrsCombatProjectileProfile* profile, + const OsrsCombatProjectileEmitSpec* spec +) { + if (!overlay || !profile || !spec || spec->target_npc_slot < 0) { + fprintf(stderr, "invalid combat projectile profile emit input\n"); + abort(); + } + if (profile->projectile_model_id <= 0) { + fprintf(stderr, "missing combat projectile model for style %d\n", + spec->attack_style); + abort(); + } + + int impact_gfx = osrs_combat_projectile_value_or( + profile->impact_spotanim_id, 0); + if (spec->attack_style == ATTACK_STYLE_MAGIC && + spec->damage <= 0 && spec->splash_gfx_id > 0) { + impact_gfx = spec->splash_gfx_id; + } + + int curve = spec->curve > 0 + ? spec->curve + : osrs_combat_projectile_value_or(profile->projectile_angle, 16); + OsrsProjectileEventSpec event_spec; + event_spec.src_x = spec->src_x; + event_spec.src_y = spec->src_y; + event_spec.dst_x = spec->dst_x; + event_spec.dst_y = spec->dst_y; + event_spec.style = encounter_attack_style_to_proj_style(spec->attack_style); + event_spec.damage = spec->damage; + event_spec.duration_ticks = spec->duration_ticks; + event_spec.start_h = osrs_combat_projectile_value_or( + profile->projectile_start_height, spec->fallback_start_h); + event_spec.end_h = osrs_combat_projectile_value_or( + profile->projectile_end_height, spec->fallback_end_h); + event_spec.curve = curve; + event_spec.arc_height = osrs_combat_projectile_profile_arc_height( + profile, spec->attack_style); + event_spec.src_size = spec->src_size; + event_spec.dst_size = spec->dst_size; + event_spec.model_id = (uint32_t)profile->projectile_model_id; + event_spec.anim_id = profile->projectile_anim_id; + event_spec.travel_gfx_id = 0; + event_spec.launch_gfx_id = osrs_combat_projectile_value_or( + profile->launch_spotanim_id, 0); + event_spec.impact_gfx_id = impact_gfx; + event_spec.start_delay = spec->start_delay; + + int idx = osrs_emit_projectile_with_spec(overlay, &event_spec, 1); + encounter_set_projectile_source_player(overlay, idx); + encounter_set_projectile_target_npc_slot(overlay, idx, spec->target_npc_slot); + return idx; +} + +static inline int osrs_emit_projectile_npc_to_player( + EncounterOverlay* overlay, + const OsrsProjectileEventSpec* spec, + int source_npc_slot +) { + int idx = osrs_emit_projectile_with_spec(overlay, spec, 1); + encounter_set_projectile_source_npc_slot(overlay, idx, source_npc_slot); + return idx; +} + +static inline int osrs_emit_projectile_npc_to_npc( + EncounterOverlay* overlay, + const OsrsProjectileEventSpec* spec, + int source_npc_slot, + int target_npc_slot +) { + int idx = osrs_emit_projectile_with_spec(overlay, spec, 1); + encounter_set_projectile_source_npc_slot(overlay, idx, source_npc_slot); + encounter_set_projectile_target_npc_slot(overlay, idx, target_npc_slot); + return idx; +} + +#endif diff --git a/ocean/osrs/osrs_env.h b/ocean/osrs/osrs_env.h new file mode 100644 index 0000000000..8a48f9fb02 --- /dev/null +++ b/ocean/osrs/osrs_env.h @@ -0,0 +1,20 @@ +#ifndef OSRS_ENV_H +#define OSRS_ENV_H + +#include "osrs_types.h" +#include "osrs_collision.h" +#include "osrs_pathfinding.h" +#include "osrs_encounter.h" +#include "osrs_combat.h" +#include "osrs_special_attacks.h" +#include "osrs_damage.h" +#include "osrs_bolt_procs.h" +#include "osrs_pvp_gear.h" +#include "osrs_pvp_combat.h" +#include "osrs_pvp_movement.h" +#include "osrs_pvp_observations.h" +#include "osrs_pvp_actions.h" +#include "osrs_pvp_opponents.h" +#include "osrs_pvp_api.h" + +#endif diff --git a/ocean/osrs/osrs_env_profile.h b/ocean/osrs/osrs_env_profile.h new file mode 100644 index 0000000000..e74da5f9d2 --- /dev/null +++ b/ocean/osrs/osrs_env_profile.h @@ -0,0 +1,82 @@ +#include +#include + +#define OSRS_ENV_PROFILE_CAT_(a, b) a##b +#define OSRS_ENV_PROFILE_CAT(a, b) OSRS_ENV_PROFILE_CAT_(a, b) +#define OSRS_ENV_PROFILE_FN(suffix) OSRS_ENV_PROFILE_CAT(OSRS_ENV_PROFILE_PREFIX, suffix) +#define OSRS_ENV_PROFILE_G(suffix) \ + OSRS_ENV_PROFILE_CAT(OSRS_ENV_PROFILE_CAT(g_, OSRS_ENV_PROFILE_PREFIX), suffix) + +typedef enum { +#define OSRS_ENV_PROFILE_X(slot, label) slot, + OSRS_ENV_PROFILE_SLOTS(OSRS_ENV_PROFILE_X) +#undef OSRS_ENV_PROFILE_X + OSRS_ENV_PROFILE_COUNT, +} OSRS_ENV_PROFILE_SLOT_TYPE; + +static int OSRS_ENV_PROFILE_G(_profile_enabled) = -1; +static double OSRS_ENV_PROFILE_G(_profile_ms)[OSRS_ENV_PROFILE_COUNT]; + +static const char* OSRS_ENV_PROFILE_G(_profile_names)[OSRS_ENV_PROFILE_COUNT] = { +#define OSRS_ENV_PROFILE_X(slot, label) label, + OSRS_ENV_PROFILE_SLOTS(OSRS_ENV_PROFILE_X) +#undef OSRS_ENV_PROFILE_X +}; + +static int OSRS_ENV_PROFILE_FN(_profile_enabled)(void) { + if (OSRS_ENV_PROFILE_G(_profile_enabled) < 0) { + const char* text = getenv(OSRS_ENV_PROFILE_ENV_VAR); + OSRS_ENV_PROFILE_G(_profile_enabled) = + (text && text[0] && text[0] != '0') ? 1 : 0; + } + return OSRS_ENV_PROFILE_G(_profile_enabled); +} + +static double OSRS_ENV_PROFILE_FN(_profile_now_ms)(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1000000.0; +} + +static void OSRS_ENV_PROFILE_FN(_profile_add)(int slot, double ms) { + if (slot < 0 || slot >= OSRS_ENV_PROFILE_COUNT) abort(); + #pragma omp atomic update + OSRS_ENV_PROFILE_G(_profile_ms)[slot] += ms; +} + +static void OSRS_ENV_PROFILE_FN(_profile_mark)(int enabled, double* last_ms, int slot) { + if (!enabled) return; + double now = OSRS_ENV_PROFILE_FN(_profile_now_ms)(); + OSRS_ENV_PROFILE_FN(_profile_add)(slot, now - *last_ms); + *last_ms = now; +} + +OSRS_ENV_PROFILE_EXPORT int OSRS_ENV_PROFILE_FN(_env_profile_count)(void) { + return OSRS_ENV_PROFILE_FN(_profile_enabled)() ? OSRS_ENV_PROFILE_COUNT : 0; +} + +OSRS_ENV_PROFILE_EXPORT const char* OSRS_ENV_PROFILE_FN(_env_profile_name)(int slot) { + if (slot < 0 || slot >= OSRS_ENV_PROFILE_COUNT) abort(); + return OSRS_ENV_PROFILE_G(_profile_names)[slot]; +} + +OSRS_ENV_PROFILE_EXPORT double OSRS_ENV_PROFILE_FN(_env_profile_read_reset_ms)(int slot) { + if (slot < 0 || slot >= OSRS_ENV_PROFILE_COUNT) abort(); + double value; + #pragma omp atomic read + value = OSRS_ENV_PROFILE_G(_profile_ms)[slot]; + #pragma omp atomic write + OSRS_ENV_PROFILE_G(_profile_ms)[slot] = 0.0; + return value; +} + +#undef OSRS_ENV_PROFILE_CAT_ +#undef OSRS_ENV_PROFILE_CAT +#undef OSRS_ENV_PROFILE_FN +#undef OSRS_ENV_PROFILE_G +#undef OSRS_ENV_PROFILE_PREFIX +#undef OSRS_ENV_PROFILE_COUNT +#undef OSRS_ENV_PROFILE_SLOT_TYPE +#undef OSRS_ENV_PROFILE_ENV_VAR +#undef OSRS_ENV_PROFILE_EXPORT +#undef OSRS_ENV_PROFILE_SLOTS diff --git a/ocean/osrs/osrs_gfx_ids.h b/ocean/osrs/osrs_gfx_ids.h new file mode 100644 index 0000000000..96a27fd6d2 --- /dev/null +++ b/ocean/osrs/osrs_gfx_ids.h @@ -0,0 +1,16 @@ +#ifndef OSRS_GFX_IDS_H +#define OSRS_GFX_IDS_H + +#define GFX_RUNE_ARROW_LAUNCH 24 +#define GFX_SPLASH 85 +#define GFX_TRIDENT_CAST 665 +#define GFX_TRIDENT_PROJ 1040 +#define GFX_TRIDENT_IMPACT 1042 +#define GFX_TUMEKENS_SHADOW_CAST 2125 +#define GFX_TUMEKENS_SHADOW_PROJ 2126 +#define GFX_TUMEKENS_SHADOW_IMPACT 2127 +#define GFX_BLOWPIPE_SPEC 1043 +#define GFX_DRAGON_ARROW_LAUNCH 1116 +#define GFX_DRAGON_DART 1122 + +#endif diff --git a/ocean/osrs/osrs_gui.h b/ocean/osrs/osrs_gui.h new file mode 100644 index 0000000000..c8e5f7a9ea --- /dev/null +++ b/ocean/osrs/osrs_gui.h @@ -0,0 +1,3355 @@ +#ifndef OSRS_GUI_H +#define OSRS_GUI_H + +#include +#include +#include +#include +#include + +#include "osrs_asset_raylib.h" +#include "osrs_human_input_types.h" + +#if __has_include("raylib.h") +#include "raylib.h" +#elif __has_include("raylib-5.5_macos/include/raylib.h") +#include "raylib-5.5_macos/include/raylib.h" +#else +#error "raylib.h not found" +#endif +#include "osrs_types.h" +#include "osrs_items.h" +#include "osrs_pvp_gear.h" +#include "osrs_inventory_clicks.h" +#include "osrs_ui_interfaces.h" + +#define GUI_BG_DARK CLITERAL(Color){ 62, 53, 41, 255 } +#define GUI_BG_SLOT CLITERAL(Color){ 56, 48, 38, 255 } +#define GUI_BORDER CLITERAL(Color){ 42, 36, 28, 255 } +#define GUI_BORDER_LT CLITERAL(Color){ 100, 90, 70, 255 } +#define GUI_TEXT_YELLOW CLITERAL(Color){ 255, 255, 0, 255 } +#define GUI_TEXT_ORANGE CLITERAL(Color){ 255, 152, 31, 255 } +#define GUI_TEXT_WHITE CLITERAL(Color){ 255, 255, 255, 255 } +#define GUI_TEXT_GREEN CLITERAL(Color){ 0, 255, 0, 255 } +#define GUI_SPEC_GREEN CLITERAL(Color){ 0, 180, 0, 255 } + +#define GUI_TEXT_SHADOW CLITERAL(Color){ 0, 0, 0, 255 } + +#define GUI_MAP_CONTAINER_W 211 +#define GUI_MAP_CONTAINER_H 207 +#define GUI_MINIMAP_X 53 +#define GUI_MINIMAP_Y 8 +#define GUI_MINIMAP_W 152 +#define GUI_MINIMAP_H 152 +#define GUI_MINIMAP_MASK_CENTER 75.5f +#define GUI_MINIMAP_MASK_RADIUS 76.0f +#define GUI_MAP_SURROUND_X 29 +#define GUI_MAP_SURROUND_Y 0 +#define GUI_MAP_SURROUND_W 182 +#define GUI_MAP_SURROUND_H 166 +#define GUI_COMPASS_X 34 +#define GUI_COMPASS_Y 5 +#define GUI_COMPASS_W 35 +#define GUI_COMPASS_H 35 + +#define GUI_ORBS_X 0 +#define GUI_ORBS_Y 10 +#define GUI_XP_X 0 +#define GUI_XP_Y 17 +#define GUI_HP_X 0 +#define GUI_HP_Y 37 +#define GUI_PRAYER_X 0 +#define GUI_PRAYER_Y 71 +#define GUI_RUN_X 10 +#define GUI_RUN_Y 103 +#define GUI_SPEC_X 32 +#define GUI_SPEC_Y 128 +#define GUI_WORLDMAP_X 177 +#define GUI_WORLDMAP_Y 137 + +#define GUI_SIDE_MENU_W 241 +#define GUI_SIDE_CONTENT_X 25 +#define GUI_SIDE_CONTENT_Y 37 +#define GUI_SIDE_CONTENT_W 190 +#define GUI_SIDE_CONTENT_H 261 +#define GUI_SIDE_TOP_Y 0 +#define GUI_SIDE_BOTTOM_Y 298 + +#define GUI_TAB_PRESS_TICKS 6 + +typedef enum { + GUI_TAB_COMBAT = 0, + GUI_TAB_STATS = 1, + GUI_TAB_QUESTS = 2, + GUI_TAB_INVENTORY = 3, + GUI_TAB_EQUIPMENT = 4, + GUI_TAB_PRAYER = 5, + GUI_TAB_SPELLBOOK = 6, + GUI_TAB_COUNT = 7 +} GuiTab; + +#define GUI_NUM_SLOT_SPRITES 12 + +typedef enum { + GUI_PRAY_THICK_SKIN = 0, + GUI_PRAY_BURST_STR, + GUI_PRAY_CLARITY, + GUI_PRAY_SHARP_EYE, + GUI_PRAY_MYSTIC_WILL, + GUI_PRAY_ROCK_SKIN, + GUI_PRAY_SUPERHUMAN, + GUI_PRAY_IMPROVED_REFLEX, + GUI_PRAY_RAPID_RESTORE, + GUI_PRAY_RAPID_HEAL, + GUI_PRAY_PROTECT_ITEM, + GUI_PRAY_HAWK_EYE, + GUI_PRAY_MYSTIC_LORE, + GUI_PRAY_STEEL_SKIN, + GUI_PRAY_ULTIMATE_STR, + GUI_PRAY_INCREDIBLE_REFLEX, + GUI_PRAY_PROTECT_MAGIC, + GUI_PRAY_PROTECT_MISSILES, + GUI_PRAY_PROTECT_MELEE, + GUI_PRAY_EAGLE_EYE, + GUI_PRAY_MYSTIC_MIGHT, + GUI_PRAY_RETRIBUTION, + GUI_PRAY_REDEMPTION, + GUI_PRAY_SMITE, + GUI_PRAY_PRESERVE, + GUI_PRAY_CHIVALRY, + GUI_PRAY_PIETY, + GUI_PRAY_RIGOUR, + GUI_PRAY_AUGURY, + GUI_NUM_PRAYERS +} GuiPrayerIdx; + +typedef enum { + GUI_SPELL_SMOKE_RUSH = 0, + GUI_SPELL_SHADOW_RUSH, + GUI_SPELL_BLOOD_RUSH, + GUI_SPELL_ICE_RUSH, + GUI_SPELL_SMOKE_BURST, + GUI_SPELL_SHADOW_BURST, + GUI_SPELL_BLOOD_BURST, + GUI_SPELL_ICE_BURST, + GUI_SPELL_SMOKE_BLITZ, + GUI_SPELL_SHADOW_BLITZ, + GUI_SPELL_BLOOD_BLITZ, + GUI_SPELL_ICE_BLITZ, + GUI_SPELL_SMOKE_BARRAGE, + GUI_SPELL_SHADOW_BARRAGE, + GUI_SPELL_BLOOD_BARRAGE, + GUI_SPELL_ICE_BARRAGE, + GUI_SPELL_PADDEWWA_TELEPORT, + GUI_SPELL_SENNTISTEN_TELEPORT, + GUI_SPELL_KHARYRLL_TELEPORT, + GUI_SPELL_LASSAR_TELEPORT, + GUI_SPELL_DAREEYAK_TELEPORT, + GUI_SPELL_CARRALLANGER_TELEPORT, + GUI_SPELL_ANNAKARL_TELEPORT, + GUI_SPELL_GHORROCK_TELEPORT, + GUI_NUM_SPELLS +} GuiSpellIdx; + +typedef enum { + INV_SLOT_EMPTY = 0, + INV_SLOT_EQUIPMENT, + INV_SLOT_FOOD, + INV_SLOT_KARAMBWAN, + INV_SLOT_BREW, + INV_SLOT_RESTORE, + INV_SLOT_COMBAT_POT, + INV_SLOT_RANGED_POT, + INV_SLOT_ANTIVENOM, + INV_SLOT_PRAYER_POT, + INV_SLOT_BASTION_POT, + INV_SLOT_STAMINA_POT, + INV_SLOT_SATURATED_HEART, +} InvSlotType; + +#define OSRS_ID_SHARK 385 +#define OSRS_ID_KARAMBWAN 3144 +#define OSRS_ID_BREW_4 6685 +#define OSRS_ID_BREW_3 6687 +#define OSRS_ID_BREW_2 6689 +#define OSRS_ID_BREW_1 6691 +#define OSRS_ID_RESTORE_4 3024 +#define OSRS_ID_RESTORE_3 3026 +#define OSRS_ID_RESTORE_2 3028 +#define OSRS_ID_RESTORE_1 3030 +#define OSRS_ID_COMBAT_4 12695 +#define OSRS_ID_COMBAT_3 12697 +#define OSRS_ID_COMBAT_2 12699 +#define OSRS_ID_COMBAT_1 12701 +#define OSRS_ID_RANGED_4 2444 +#define OSRS_ID_RANGED_3 169 +#define OSRS_ID_RANGED_2 171 +#define OSRS_ID_RANGED_1 173 +#define OSRS_ID_ANTIVENOM_4 12913 +#define OSRS_ID_ANTIVENOM_3 12915 +#define OSRS_ID_ANTIVENOM_2 12917 +#define OSRS_ID_ANTIVENOM_1 12919 +#define OSRS_ID_PRAYER_POT_4 2434 +#define OSRS_ID_PRAYER_POT_3 139 +#define OSRS_ID_PRAYER_POT_2 141 +#define OSRS_ID_PRAYER_POT_1 143 +#define OSRS_ID_BASTION_4 22461 +#define OSRS_ID_BASTION_3 22464 +#define OSRS_ID_BASTION_2 22467 +#define OSRS_ID_BASTION_1 22470 +#define OSRS_ID_STAMINA_4 12625 +#define OSRS_ID_STAMINA_3 12627 +#define OSRS_ID_STAMINA_2 12629 +#define OSRS_ID_STAMINA_1 12631 +#define OSRS_ID_SATURATED_HEART 27641 + +#define INV_GRID_SLOTS 28 + +typedef struct { + InvSlotType type; + uint8_t item_db_idx; + int osrs_id; +} InvSlot; + +#define INV_DIM_TICKS 15 +#define INV_DRAG_DEAD_ZONE 5 +#define INV_DRAG_HOLD_SECONDS 0.030 + +typedef enum { + INV_ACTION_NONE = 0, + INV_ACTION_EQUIP, + INV_ACTION_EAT, + INV_ACTION_DRINK, + INV_ACTION_ITEM_ON_ITEM, +} InvAction; + +#define GUI_MAX_NAMED_ASSETS 768 +#define GUI_UI_ITEM_CONTAINER_MAX_SLOTS 64 +#define GUI_UI_MAX_COMPONENT_OVERRIDES 64 +#define GUI_ITEM_STACK_VARIANT_MAX 2048 + +typedef struct { + char name[64]; + Texture2D tex; +} GuiNamedAsset; + +typedef struct { + int base_item_id; + int threshold; + int display_item_id; +} GuiItemStackVariant; + +typedef struct { + int present; + int enabled; + uint8_t item_db_idx; + int osrs_id; + int quantity; + int selected; + unsigned char alpha; + int gear_slot; + const char* empty_asset; +} GuiUiItemSlot; + +typedef struct { + uint32_t component_id; + int hidden; + int force_visible; + const char* text; + const char* sprite_asset; + int sprite_present; + int sprite_id; + GuiUiItemSlot item; +} GuiUiComponentOverride; + +typedef struct { + uint32_t component_id; + int slot_count; + int columns; + float x0; + float y0; + float step_x; + float step_y; + float slot_w; + float slot_h; + GuiUiItemSlot slots[GUI_UI_ITEM_CONTAINER_MAX_SLOTS]; +} GuiUiItemContainerOverride; + +typedef struct { + const GuiUiComponentOverride* components; + int component_count; + const GuiUiItemContainerOverride* item_containers; + int item_container_count; +} GuiUiOverrides; + +typedef struct { + Rectangle current; + int active; +} GuiUiClipState; + +typedef struct { + GuiTab active_tab; + int panel_x, panel_y; + int panel_w, panel_h; + int tab_h; + int status_bar_h; + + float ui_scale; + + int gui_entity_idx; + int gui_entity_count; + + void* encounter_state; + const void* encounter_def; + + int sprites_loaded; + GuiNamedAsset named_assets[GUI_MAX_NAMED_ASSETS]; + int named_asset_count; + OsrsUiInterfaceStore ui_interfaces; + Font font; + Font small_font; + int font_loaded; + int small_font_loaded; + GuiItemStackVariant item_stack_variants[GUI_ITEM_STACK_VARIANT_MAX]; + int item_stack_variant_count; + + Texture2D slot_sprites[GUI_NUM_SLOT_SPRITES]; + + int tab_press_timer[GUI_TAB_COUNT]; + + Texture2D prayer_on[GUI_NUM_PRAYERS]; + + Texture2D spell_on[GUI_NUM_SPELLS]; + Texture2D spell_off[GUI_NUM_SPELLS]; + + Texture2D slot_tile; + + Texture2D minimap_compass; + Texture2D minimap_compass_masked; + Texture2D minimap_alpha_mask; + Texture2D minimap_frame; + Texture2D rm_minimap_alpha_mask; + Texture2D rm_minimap_frame; + Texture2D rm_compass_alpha_mask; + Texture2D rm_tabs_top_row; + Texture2D minimap_dot_player; + Texture2D minimap_dot_npc; + int minimap_chrome_loaded; + + #define GUI_MAX_ITEM_SPRITES 256 + int item_sprite_ids[GUI_MAX_ITEM_SPRITES]; + Texture2D item_sprite_tex[GUI_MAX_ITEM_SPRITES]; + int item_sprite_count; + + InvSlot inv_grid[INV_GRID_SLOTS]; + int inv_grid_dirty; + + int display_inventory_osrs_ids[INV_GRID_SLOTS]; + int display_inventory_count; + + uint8_t inv_prev_equipped[NUM_GEAR_SLOTS]; + int inv_prev_food_count; + int inv_prev_karambwan_count; + int inv_prev_brew_doses; + int inv_prev_restore_doses; + int inv_prev_prayer_pot_doses; + int inv_prev_combat_doses; + int inv_prev_ranged_doses; + int inv_prev_bastion_doses; + int inv_prev_stamina_doses; + int inv_prev_antivenom_doses; + int inv_prev_saturated_heart_count; + + int human_clicked_inv_slot; + + int inv_dim_slot; + int inv_dim_timer; + + int inv_drag_active; + int inv_drag_src_slot; + int inv_drag_start_x; + int inv_drag_start_y; + int inv_drag_mouse_x; + int inv_drag_mouse_y; + double inv_drag_press_time; + + int pending_spell_highlight; + int autocast_selector_open; +} GuiState; + +static inline void gui_mouse_to_panel_space( + const GuiState* gs, int mx, int my, int* out_x, int* out_y +) { + float k = gs->ui_scale > 0.0f ? gs->ui_scale : 1.0f; + float fx = (float)(gs->panel_x + gs->panel_w); + float fy = (float)(gs->panel_y + gs->panel_h); + *out_x = (int)lroundf(fx + ((float)mx - fx) / k); + *out_y = (int)lroundf(fy + ((float)my - fy) / k); +} + +static inline void gui_mouse_to_minimap_space( + const GuiState* gs, int mx, int my, int* out_x, int* out_y +) { + float k = gs->ui_scale > 0.0f ? gs->ui_scale : 1.0f; + float fx = (float)(gs->panel_x + gs->panel_w); + *out_x = (int)lroundf(fx + ((float)mx - fx) / k); + *out_y = (int)lroundf((float)my / k); +} + +static int gui_try_load(Texture2D* tex, const char* path) { + if (osrs_asset_exists(path)) { + *tex = osrs_asset_load_texture(path); + return 1; + } + return 0; +} + +static int gui_try_load_masked_compass(Texture2D* tex, const char* path) { + if (!osrs_asset_exists(path)) return 0; + Image image = osrs_asset_load_image(path); + if (!image.data) return 0; + + ImageFormat(&image, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8); + Color* pixels = (Color*)image.data; + int min_side = image.width < image.height ? image.width : image.height; + float cx = (float)image.width * 0.5f; + float cy = (float)image.height * 0.5f; + float radius = (float)min_side * (19.0f / 51.0f); + float radius_sq = radius * radius; + for (int y = 0; y < image.height; y++) { + for (int x = 0; x < image.width; x++) { + float dx = ((float)x + 0.5f) - cx; + float dy = ((float)y + 0.5f) - cy; + if (dx * dx + dy * dy > radius_sq) { + pixels[x + y * image.width].a = 0; + } + } + } + + *tex = LoadTextureFromImage(image); + UnloadImage(image); + return tex->id != 0; +} + +static int gui_rect_has_area(Rectangle rect) { + return rect.width > 0.0f && rect.height > 0.0f; +} + +static Rectangle gui_rect_intersect(Rectangle a, Rectangle b) { + float x1 = a.x > b.x ? a.x : b.x; + float y1 = a.y > b.y ? a.y : b.y; + float x2 = a.x + a.width < b.x + b.width ? a.x + a.width : b.x + b.width; + float y2 = a.y + a.height < b.y + b.height ? a.y + a.height : b.y + b.height; + if (x2 <= x1 || y2 <= y1) return (Rectangle){0}; + return (Rectangle){x1, y1, x2 - x1, y2 - y1}; +} + +static void gui_apply_scissor(const GuiState* gs, Rectangle rect) { + float k = gs->ui_scale > 0.0f ? gs->ui_scale : 1.0f; + float fx = (float)(gs->panel_x + gs->panel_w); + float fy = (float)(gs->panel_y + gs->panel_h); + int x = (int)floorf(fx + (rect.x - fx) * k); + int y = (int)floorf(fy + (rect.y - fy) * k); + int w = (int)ceilf(rect.width * k); + int h = (int)ceilf(rect.height * k); + if (w < 0) w = 0; + if (h < 0) h = 0; + BeginScissorMode(x, y, w, h); +} + +static void gui_push_clip( + const GuiState* gs, + GuiUiClipState* clip, + Rectangle next, + Rectangle* prev, + int* prev_active +) { + *prev = clip->current; + *prev_active = clip->active; + if (clip->active) next = gui_rect_intersect(next, clip->current); + if (!gui_rect_has_area(next)) next = (Rectangle){0}; + if (clip->active) EndScissorMode(); + gui_apply_scissor(gs, next); + clip->current = next; + clip->active = 1; +} + +static void gui_pop_clip( + const GuiState* gs, GuiUiClipState* clip, Rectangle prev, int prev_active +) { + if (clip->active) EndScissorMode(); + clip->current = prev; + clip->active = prev_active; + if (clip->active) gui_apply_scissor(gs, clip->current); +} + +static Font gui_font_for_size(const GuiState* gs, int size) { + if (gs && size <= 12 && gs->small_font_loaded) return gs->small_font; + if (gs && gs->font_loaded) return gs->font; + return GetFontDefault(); +} + +static int gui_measure_text(const GuiState* gs, const char* text, int size) { + if (!text || !text[0]) return 0; + Font font = gui_font_for_size(gs, size); + if (font.texture.id == 0) return MeasureText(text, size); + Vector2 measured = MeasureTextEx(font, text, (float)size, 0.0f); + return (int)(measured.x + 0.5f); +} + +static void gui_load_fonts(GuiState* gs) { + gs->font = osrs_asset_load_font("fonts/runescape.ttf", 14); + gs->font_loaded = gs->font.texture.id != 0; + if (gs->font_loaded) SetTextureFilter(gs->font.texture, TEXTURE_FILTER_POINT); + gs->small_font = osrs_asset_load_font("fonts/runescape_small.ttf", 12); + gs->small_font_loaded = gs->small_font.texture.id != 0; + if (gs->small_font_loaded) SetTextureFilter(gs->small_font.texture, TEXTURE_FILTER_POINT); +} + +static void gui_load_item_stack_variants(GuiState* gs) { + gs->item_stack_variant_count = 0; + FILE* f = osrs_asset_fopen("sprites/items/item_stack_variants.tsv", "rb"); + if (!f) return; + char line[128]; + while (fgets(line, sizeof(line), f)) { + if (line[0] == '#' || line[0] == '\n' || line[0] == '\0') continue; + GuiItemStackVariant variant = {0}; + if (sscanf(line, "%d\t%d\t%d", + &variant.base_item_id, + &variant.threshold, + &variant.display_item_id) != 3) { + continue; + } + if (gs->item_stack_variant_count >= GUI_ITEM_STACK_VARIANT_MAX) { + fprintf(stderr, "too many item stack variants\n"); + abort(); + } + gs->item_stack_variants[gs->item_stack_variant_count++] = variant; + } + fclose(f); +} + +static Texture2D gui_asset(const GuiState* gs, const char* name) { + for (int i = 0; i < gs->named_asset_count; i++) { + if (strcmp(gs->named_assets[i].name, name) == 0) { + return gs->named_assets[i].tex; + } + } + return (Texture2D){0}; +} + +static Texture2D gui_load_named_asset_path(GuiState* gs, const char* name, const char* path) { + if (gs->named_asset_count >= GUI_MAX_NAMED_ASSETS) { + fprintf(stderr, "GUI named asset capacity exceeded\n"); + abort(); + } + + Texture2D tex = {0}; + if (!gui_try_load(&tex, path)) return tex; + + GuiNamedAsset* asset = &gs->named_assets[gs->named_asset_count++]; + snprintf(asset->name, sizeof(asset->name), "%s", name); + asset->tex = tex; + return tex; +} + +static void gui_load_named_asset(GuiState* gs, const char* name) { + char path[160]; + snprintf(path, sizeof(path), OSRS_ASSET("sprites/gui/%s.png"), name); + gui_load_named_asset_path(gs, name, path); +} + +static Texture2D gui_sprite_asset(GuiState* gs, int sprite_id) { + if (sprite_id < 0) return (Texture2D){0}; + + char name[32]; + snprintf(name, sizeof(name), "#%d", sprite_id); + Texture2D tex = gui_asset(gs, name); + if (tex.id != 0) return tex; + + char path[160]; + snprintf(path, sizeof(path), OSRS_ASSET("sprites/gui/%d.png"), sprite_id); + return gui_load_named_asset_path(gs, name, path); +} + +static void gui_load_named_asset_range(GuiState* gs, const char* prefix, int first, int last) { + char name[64]; + for (int i = first; i <= last; i++) { + snprintf(name, sizeof(name), "%s_%d", prefix, i); + gui_load_named_asset(gs, name); + } +} + +static int gui_load_ui_interfaces(GuiState* gs) { + return osrs_ui_interfaces_load(&gs->ui_interfaces, OSRS_ASSET("ui/interfaces.bin")); +} + +static const int GUI_PRAYER_ON_SPRITE_IDS[GUI_NUM_PRAYERS] = { + 115, 116, 117, 133, 134, + 118, 119, 120, 121, 122, + 123, 502, 503, 124, 125, + 126, 127, 128, 129, 504, + 505, 131, 130, 132, 947, + 945, 946, 1420, 1421, +}; + +static const int GUI_SPELL_ON_SPRITE_IDS[GUI_NUM_SPELLS] = { + 329, 337, 333, 325, + 330, 338, 334, 326, + 331, 339, 335, 327, + 332, 340, 336, 328, + 341, 342, 343, 344, + 345, 346, 347, 348, +}; + +static const int GUI_SPELL_OFF_SPRITE_IDS[GUI_NUM_SPELLS] = { + 379, 387, 383, 375, + 380, 388, 384, 376, + 381, 389, 385, 377, + 382, 390, 386, 378, + 391, 392, 393, 394, + 395, 396, 397, 398, +}; + +static int gui_prayer_on_sprite_id(GuiPrayerIdx idx) { + assert(idx >= 0 && idx < GUI_NUM_PRAYERS); + return GUI_PRAYER_ON_SPRITE_IDS[idx]; +} + +static int gui_spell_on_sprite_id(GuiSpellIdx idx) { + assert(idx >= 0 && idx < GUI_NUM_SPELLS); + return GUI_SPELL_ON_SPRITE_IDS[idx]; +} + +static int gui_spell_off_sprite_id(GuiSpellIdx idx) { + assert(idx >= 0 && idx < GUI_NUM_SPELLS); + return GUI_SPELL_OFF_SPRITE_IDS[idx]; +} + +static int gui_find_sprite_index_by_osrs_id(const GuiState* gs, int osrs_id) { + if (osrs_id <= 0) return -1; + for (int i = 0; i < gs->item_sprite_count; i++) { + if (gs->item_sprite_ids[i] == osrs_id) return i; + } + return -1; +} + +static Texture2D gui_load_sprite_by_osrs_id_if_present(GuiState* gs, int osrs_id) { + Texture2D empty = {0}; + int existing_idx = gui_find_sprite_index_by_osrs_id(gs, osrs_id); + if (existing_idx >= 0) return gs->item_sprite_tex[existing_idx]; + if (osrs_id <= 0 || gs->item_sprite_count >= GUI_MAX_ITEM_SPRITES) return empty; + + const char* path = TextFormat(OSRS_ASSET("sprites/items/%d.png"), osrs_id); + if (!osrs_asset_exists(path)) return empty; + + Texture2D tex = osrs_asset_load_texture(path); + if (tex.id == 0) return empty; + + int idx = gs->item_sprite_count++; + gs->item_sprite_ids[idx] = osrs_id; + gs->item_sprite_tex[idx] = tex; + return tex; +} + +static void gui_require_sprite_by_osrs_id(GuiState* gs, int osrs_id) { + Texture2D tex = gui_load_sprite_by_osrs_id_if_present(gs, osrs_id); + if (tex.id != 0) return; + + fprintf(stderr, "GUI: missing required item sprite raw id %d at %s\n", + osrs_id, + TextFormat(OSRS_ASSET("sprites/items/%d.png"), osrs_id)); + abort(); +} + +static void gui_load_sprites(GuiState* gs) { + gs->sprites_loaded = 1; + int ok = 1; + gs->named_asset_count = 0; + gui_load_ui_interfaces(gs); + gui_load_fonts(gs); + gui_load_item_stack_variants(gs); + + const char* slot_files[] = { + OSRS_ASSET("sprites/gui/slot_head.png"), + OSRS_ASSET("sprites/gui/slot_cape.png"), + OSRS_ASSET("sprites/gui/slot_neck.png"), + OSRS_ASSET("sprites/gui/slot_tile.png"), + OSRS_ASSET("sprites/gui/slot_weapon.png"), + OSRS_ASSET("sprites/gui/slot_shield.png"), + OSRS_ASSET("sprites/gui/slot_body.png"), + OSRS_ASSET("sprites/gui/slot_legs.png"), + OSRS_ASSET("sprites/gui/slot_hands.png"), + OSRS_ASSET("sprites/gui/slot_feet.png"), + OSRS_ASSET("sprites/gui/slot_ring.png"), + OSRS_ASSET("sprites/gui/slot_tile.png"), + }; + for (int i = 0; i < GUI_NUM_SLOT_SPRITES; i++) { + ok &= gui_try_load(&gs->slot_sprites[i], slot_files[i]); + } + + for (int i = 0; i < GUI_NUM_PRAYERS; i++) { + const char* on_path = TextFormat(OSRS_ASSET("sprites/gui/%d.png"), + gui_prayer_on_sprite_id((GuiPrayerIdx)i)); + gui_try_load(&gs->prayer_on[i], on_path); + } + + for (int i = 0; i < GUI_NUM_SPELLS; i++) { + const char* on_path = TextFormat(OSRS_ASSET("sprites/gui/%d.png"), + gui_spell_on_sprite_id((GuiSpellIdx)i)); + const char* off_path = TextFormat(OSRS_ASSET("sprites/gui/%d.png"), + gui_spell_off_sprite_id((GuiSpellIdx)i)); + gui_try_load(&gs->spell_on[i], on_path); + gui_try_load(&gs->spell_off[i], off_path); + } + + gui_try_load(&gs->slot_tile, OSRS_ASSET("sprites/gui/slot_tile.png")); + + gs->minimap_chrome_loaded = 1; + gs->minimap_chrome_loaded &= gui_try_load(&gs->minimap_compass, + OSRS_ASSET("sprites/gui/compass.png")); + gui_try_load_masked_compass(&gs->minimap_compass_masked, + OSRS_ASSET("sprites/gui/compass.png")); + gs->minimap_chrome_loaded &= gui_try_load(&gs->minimap_alpha_mask, + OSRS_ASSET("sprites/gui/minimap_alpha_mask.png")); + gs->minimap_chrome_loaded &= gui_try_load(&gs->minimap_frame, + OSRS_ASSET("sprites/gui/minimap_and_compass_frame.png")); + gui_try_load(&gs->rm_minimap_alpha_mask, + OSRS_ASSET("sprites/gui/rm_minimap_alpha_mask.png")); + gui_try_load(&gs->rm_minimap_frame, + OSRS_ASSET("sprites/gui/rm_minimap_and_compass_frame.png")); + gui_try_load(&gs->rm_compass_alpha_mask, + OSRS_ASSET("sprites/gui/rm_compass_alpha_mask.png")); + gui_try_load(&gs->rm_tabs_top_row, + OSRS_ASSET("sprites/gui/rm_tabs_top_row.png")); + gs->minimap_chrome_loaded &= gui_try_load(&gs->minimap_dot_player, + OSRS_ASSET("sprites/gui/minimap_dot_player.png")); + gs->minimap_chrome_loaded &= gui_try_load(&gs->minimap_dot_npc, + OSRS_ASSET("sprites/gui/minimap_dot_npc.png")); + + static const char* ui_asset_names[] = { + "tradebacking_dark", + "osrs_stretch_side_topbottom_0", + "osrs_stretch_side_topbottom_1", + "osrs_stretch_side_columns_0", + "osrs_stretch_side_columns_1", + "osrs_stretch_mapsurround", + "compass", + "compass_outline", + "resize_map_mask", + "resize_compass_mask", + "tli_button01_orb01_34x34_0", + "ring_34_0", + "orb_xp_0", + "ring_30", + "worldmap_icon_0", + "wiki_icon_0", + "combatboxes_0", + "combatboxes_1", + "combatboxes_special_attack", + "combat_shield", + "miscgraphics_2", + "miscgraphics_3", + "options_icons_16", + "options_icons_18", + "options_icons_28", + "whistle", + "prayerglow_0", + "prayerglow_1", + }; + int ui_asset_count = (int)(sizeof(ui_asset_names) / sizeof(ui_asset_names[0])); + for (int i = 0; i < ui_asset_count; i++) { + gui_load_named_asset(gs, ui_asset_names[i]); + } + gui_load_named_asset_range(gs, "side_stone_highlights", 0, 4); + gui_load_named_asset_range(gs, "sideicons_interface", 0, 16); + gui_load_named_asset_range(gs, "combatboxes", 0, 3); + gui_load_named_asset_range(gs, "combaticons", 0, 19); + gui_load_named_asset_range(gs, "combaticons2", 0, 19); + gui_load_named_asset_range(gs, "combaticons3", 0, 19); + gui_load_named_asset_range(gs, "orb_frame", 0, 2); + gui_load_named_asset_range(gs, "orb_filler", 0, 14); + gui_load_named_asset_range(gs, "orb_icon", 0, 15); + gui_load_named_asset_range(gs, "wornicons", 0, 11); + gui_load_named_asset_range(gs, "skill_icon", 0, 23); + gui_load_named_asset_range(gs, "prayeron", 0, 28); + gui_load_named_asset_range(gs, "prayeroff", 0, 28); + gui_load_named_asset_range(gs, "magicon", 0, 47); + gui_load_named_asset_range(gs, "magicoff", 0, 47); + gui_load_named_asset_range(gs, "standard_spell_on", 0, 79); + + static const char* side_icon_names[] = { + "side_icon_combat", + "side_icon_stats", + "side_icon_quests", + "side_icon_inventory", + "side_icon_equipment", + "side_icon_prayer", + "side_icon_magic", + "side_icon_magic_ancient", + "side_icon_clan", + "side_icon_friends", + "side_icon_grouping", + "side_icon_logout", + "side_icon_options", + "side_icon_emotes", + "side_icon_music", + }; + int side_icon_count = (int)(sizeof(side_icon_names) / sizeof(side_icon_names[0])); + for (int i = 0; i < side_icon_count; i++) { + gui_load_named_asset(gs, side_icon_names[i]); + } + + if (!ok) { + TraceLog(LOG_WARNING, "GUI: some sprites missing from data/sprites/gui/"); + } + + gs->item_sprite_count = 0; + for (int i = 0; i < NUM_ITEMS && gs->item_sprite_count < GUI_MAX_ITEM_SPRITES; i++) { + int item_id = ITEM_DATABASE[i].item_id; + if (item_id <= 0) continue; + gui_load_sprite_by_osrs_id_if_present(gs, item_id); + } + + static const int consumable_ids[] = { + OSRS_ID_SHARK, OSRS_ID_KARAMBWAN, + OSRS_ID_BREW_4, OSRS_ID_BREW_3, OSRS_ID_BREW_2, OSRS_ID_BREW_1, + OSRS_ID_RESTORE_4, OSRS_ID_RESTORE_3, OSRS_ID_RESTORE_2, OSRS_ID_RESTORE_1, + OSRS_ID_COMBAT_4, OSRS_ID_COMBAT_3, OSRS_ID_COMBAT_2, OSRS_ID_COMBAT_1, + OSRS_ID_RANGED_4, OSRS_ID_RANGED_3, OSRS_ID_RANGED_2, OSRS_ID_RANGED_1, + OSRS_ID_ANTIVENOM_4, OSRS_ID_ANTIVENOM_3, OSRS_ID_ANTIVENOM_2, OSRS_ID_ANTIVENOM_1, + OSRS_ID_PRAYER_POT_4, OSRS_ID_PRAYER_POT_3, OSRS_ID_PRAYER_POT_2, OSRS_ID_PRAYER_POT_1, + OSRS_ID_BASTION_4, OSRS_ID_BASTION_3, OSRS_ID_BASTION_2, OSRS_ID_BASTION_1, + OSRS_ID_STAMINA_4, OSRS_ID_STAMINA_3, OSRS_ID_STAMINA_2, OSRS_ID_STAMINA_1, + OSRS_ID_SATURATED_HEART, + }; + for (int i = 0; i < (int)(sizeof(consumable_ids)/sizeof(consumable_ids[0])); i++) { + if (gs->item_sprite_count >= GUI_MAX_ITEM_SPRITES) break; + int cid = consumable_ids[i]; + gui_load_sprite_by_osrs_id_if_present(gs, cid); + } + TraceLog(LOG_INFO, "GUI: loaded %d item sprites (incl consumables)", gs->item_sprite_count); +} + +static Texture2D gui_get_sprite_by_osrs_id(GuiState* gs, int osrs_id); + +static Texture2D gui_get_item_sprite(GuiState* gs, uint8_t item_idx) { + Texture2D empty = { 0 }; + if (item_idx == ITEM_NONE || item_idx >= NUM_ITEMS) return empty; + int item_id = ITEM_DATABASE[item_idx].item_id; + return gui_get_sprite_by_osrs_id(gs, item_id); +} + +static Texture2D gui_get_sprite_by_osrs_id(GuiState* gs, int osrs_id) { + Texture2D empty = { 0 }; + if (osrs_id <= 0) return empty; + return gui_load_sprite_by_osrs_id_if_present(gs, osrs_id); +} + +static int gui_coin_stack_display_id(int quantity) { + if (quantity <= 1) return 995; + if (quantity == 2) return 996; + if (quantity == 3) return 997; + if (quantity == 4) return 998; + if (quantity < 25) return 999; + if (quantity < 100) return 1000; + if (quantity < 250) return 1001; + if (quantity < 1000) return 1002; + return 1003; +} + +static int gui_item_display_id_for_quantity(const GuiState* gs, int item_id, int quantity) { + if (item_id <= 0 || quantity <= 1) return item_id; + if (item_id == 995) return gui_coin_stack_display_id(quantity); + int best_threshold = 0; + int best_display_id = item_id; + for (int i = 0; gs && i < gs->item_stack_variant_count; i++) { + const GuiItemStackVariant* variant = &gs->item_stack_variants[i]; + if (variant->base_item_id != item_id) continue; + if (quantity < variant->threshold) continue; + if (variant->threshold < best_threshold) continue; + best_threshold = variant->threshold; + best_display_id = variant->display_item_id; + } + return best_display_id; +} + +static Texture2D gui_get_item_sprite_for_quantity( + GuiState* gs, + uint8_t item_idx, + int quantity +) { + Texture2D empty = {0}; + if (item_idx == ITEM_NONE || item_idx >= NUM_ITEMS) return empty; + int item_id = ITEM_DATABASE[item_idx].item_id; + int display_id = gui_item_display_id_for_quantity(gs, item_id, quantity); + Texture2D tex = gui_get_sprite_by_osrs_id(gs, display_id); + if (tex.id == 0 && display_id != item_id) { + tex = gui_get_sprite_by_osrs_id(gs, item_id); + } + return tex; +} + +static Texture2D gui_get_sprite_by_osrs_id_for_quantity( + GuiState* gs, + int osrs_id, + int quantity +) { + int display_id = gui_item_display_id_for_quantity(gs, osrs_id, quantity); + Texture2D tex = gui_get_sprite_by_osrs_id(gs, display_id); + if (tex.id == 0 && display_id != osrs_id) { + tex = gui_get_sprite_by_osrs_id(gs, osrs_id); + } + return tex; +} + +static Color gui_stack_text_color(int quantity) { + if (quantity >= 10000000) return GUI_TEXT_GREEN; + if (quantity >= 100000) return GUI_TEXT_WHITE; + return GUI_TEXT_YELLOW; +} + +static void gui_format_stack_quantity(int quantity, char* dst, size_t cap) { + if (!dst || cap == 0) return; + if (quantity >= 10000000) { + snprintf(dst, cap, "%dM", quantity / 1000000); + } else if (quantity >= 100000) { + snprintf(dst, cap, "%dK", quantity / 1000); + } else { + snprintf(dst, cap, "%d", quantity); + } +} + +static void gui_unload_sprites(GuiState* gs) { + osrs_ui_interfaces_unload(&gs->ui_interfaces); + if (!gs->sprites_loaded) return; + for (int i = 0; i < gs->named_asset_count; i++) { + if (gs->named_assets[i].tex.id) UnloadTexture(gs->named_assets[i].tex); + } + gs->named_asset_count = 0; + for (int i = 0; i < GUI_NUM_SLOT_SPRITES; i++) UnloadTexture(gs->slot_sprites[i]); + for (int i = 0; i < GUI_NUM_PRAYERS; i++) { + UnloadTexture(gs->prayer_on[i]); + } + for (int i = 0; i < GUI_NUM_SPELLS; i++) { + UnloadTexture(gs->spell_on[i]); + UnloadTexture(gs->spell_off[i]); + } + if (gs->slot_tile.id) UnloadTexture(gs->slot_tile); + if (gs->minimap_compass.id) UnloadTexture(gs->minimap_compass); + if (gs->minimap_compass_masked.id) UnloadTexture(gs->minimap_compass_masked); + if (gs->minimap_alpha_mask.id) UnloadTexture(gs->minimap_alpha_mask); + if (gs->minimap_frame.id) UnloadTexture(gs->minimap_frame); + if (gs->rm_minimap_alpha_mask.id) UnloadTexture(gs->rm_minimap_alpha_mask); + if (gs->rm_minimap_frame.id) UnloadTexture(gs->rm_minimap_frame); + if (gs->rm_compass_alpha_mask.id) UnloadTexture(gs->rm_compass_alpha_mask); + if (gs->rm_tabs_top_row.id) UnloadTexture(gs->rm_tabs_top_row); + if (gs->minimap_dot_player.id) UnloadTexture(gs->minimap_dot_player); + if (gs->minimap_dot_npc.id) UnloadTexture(gs->minimap_dot_npc); + for (int i = 0; i < gs->item_sprite_count; i++) UnloadTexture(gs->item_sprite_tex[i]); + if (gs->font_loaded) UnloadFont(gs->font); + if (gs->small_font_loaded) UnloadFont(gs->small_font); + gs->item_sprite_count = 0; + gs->font_loaded = 0; + gs->small_font_loaded = 0; + gs->item_stack_variant_count = 0; + gs->sprites_loaded = 0; +} + +static const char* gui_item_short_name(uint8_t item_idx) { + if (item_idx == ITEM_NONE || item_idx >= NUM_ITEMS) return ""; + const char* full = ITEM_DATABASE[item_idx].name; + switch (item_idx) { + case ITEM_HELM_NEITIZNOT: return "Neit helm"; + case ITEM_GOD_CAPE: return "God cape"; + case ITEM_GLORY: return "Glory"; + case ITEM_BLACK_DHIDE_BODY: return "Dhide body"; + case ITEM_MYSTIC_TOP: return "Mystic top"; + case ITEM_RUNE_PLATELEGS: return "Rune legs"; + case ITEM_MYSTIC_BOTTOM: return "Mystic bot"; + case ITEM_WHIP: return "Whip"; + case ITEM_RUNE_CROSSBOW: return "Rune cbow"; + case ITEM_AHRIM_STAFF: return "Ahrim stf"; + case ITEM_DRAGON_DAGGER: return "DDS"; + case ITEM_DRAGON_DEFENDER: return "D defender"; + case ITEM_SPIRIT_SHIELD: return "Spirit sh"; + case ITEM_BARROWS_GLOVES: return "B gloves"; + case ITEM_CLIMBING_BOOTS: return "Climb boot"; + case ITEM_BERSERKER_RING: return "B ring"; + case ITEM_DIAMOND_BOLTS_E: return "D bolts(e)"; + case ITEM_GHRAZI_RAPIER: return "Rapier"; + case ITEM_INQUISITORS_MACE: return "Inq mace"; + case ITEM_STAFF_OF_DEAD: return "SOTD"; + case ITEM_KODAI_WAND: return "Kodai"; + case ITEM_DRAGON_HUNTER_WAND: return "DH wand"; + case ITEM_VOLATILE_STAFF: return "Volatile"; + case ITEM_ZURIELS_STAFF: return "Zuriel stf"; + case ITEM_ARMADYL_CROSSBOW: return "ACB"; + case ITEM_ZARYTE_CROSSBOW: return "ZCB"; + case ITEM_DRAGON_CLAWS: return "D claws"; + case ITEM_AGS: return "AGS"; + case ITEM_ANCIENT_GS: return "Anc GS"; + case ITEM_GRANITE_MAUL: return "G maul"; + case ITEM_ELDER_MAUL: return "Elder maul"; + case ITEM_DARK_BOW: return "Dark bow"; + case ITEM_HEAVY_BALLISTA: return "Ballista"; + case ITEM_VESTAS: return "Vesta's"; + case ITEM_VOIDWAKER: return "Voidwaker"; + case ITEM_STATIUS_WARHAMMER: return "SWH"; + case ITEM_MORRIGANS_JAVELIN: return "Morr jav"; + case ITEM_ANCESTRAL_HAT: return "Anc hat"; + case ITEM_ANCESTRAL_TOP: return "Anc top"; + case ITEM_ANCESTRAL_BOTTOM: return "Anc bot"; + case ITEM_AHRIMS_ROBETOP: return "Ahrim top"; + case ITEM_AHRIMS_ROBESKIRT: return "Ahrim skrt"; + case ITEM_KARILS_TOP: return "Karil top"; + case ITEM_BANDOS_TASSETS: return "Tassets"; + case ITEM_BLESSED_SPIRIT_SHIELD: return "BSS"; + case ITEM_FURY: return "Fury"; + case ITEM_OCCULT_NECKLACE: return "Occult"; + case ITEM_INFERNAL_CAPE: return "Infernal"; + case ITEM_ETERNAL_BOOTS: return "Eternal"; + case ITEM_SEERS_RING_I: return "Seers (i)"; + case ITEM_LIGHTBEARER: return "Lightbear"; + case ITEM_MAGES_BOOK: return "Mage book"; + case ITEM_DRAGON_ARROWS: return "D arrows"; + case ITEM_TORAGS_PLATELEGS: return "Torag legs"; + case ITEM_DHAROKS_PLATELEGS: return "DH legs"; + case ITEM_VERACS_PLATESKIRT: return "Verac skrt"; + case ITEM_TORAGS_HELM: return "Torag helm"; + case ITEM_DHAROKS_HELM: return "DH helm"; + case ITEM_VERACS_HELM: return "Verac helm"; + case ITEM_GUTHANS_HELM: return "Guth helm"; + case ITEM_OPAL_DRAGON_BOLTS: return "Opal bolt"; + case ITEM_IMBUED_SARA_CAPE: return "Sara cape"; + case ITEM_EYE_OF_AYAK: return "Eye Ayak"; + case ITEM_ELIDINIS_WARD_F: return "Eld ward"; + case ITEM_CONFLICTION_GAUNTLETS: return "Confl gnt"; + case ITEM_AVERNIC_TREADS: return "Avernic bt"; + case ITEM_RING_OF_SUFFERING_RI: return "Suff (ri)"; + case ITEM_TWISTED_BOW: return "T bow"; + case ITEM_ELYSIAN_SPIRIT_SHIELD: return "Elysian"; + case ITEM_MASORI_MASK_F: return "Masori msk"; + case ITEM_MASORI_BODY_F: return "Masori bod"; + case ITEM_MASORI_CHAPS_F: return "Masori chp"; + case ITEM_NECKLACE_OF_ANGUISH: return "Anguish"; + case ITEM_DIZANAS_QUIVER: return "Dizana qvr"; + case ITEM_ZARYTE_VAMBRACES: return "Zaryte vam"; + case ITEM_TOXIC_BLOWPIPE: return "Blowpipe"; + case ITEM_AHRIMS_HOOD: return "Ahrim hood"; + case ITEM_TORMENTED_BRACELET: return "Tormented"; + case ITEM_SANGUINESTI_STAFF: return "Sang staff"; + case ITEM_INFINITY_BOOTS: return "Inf boots"; + case ITEM_GOD_BLESSING: return "Blessing"; + case ITEM_RING_OF_RECOIL: return "Recoil"; + case ITEM_VENATOR_RING: return "Venator"; + case ITEM_VIRTUS_MASK: return "Virtus msk"; + case ITEM_VIRTUS_ROBE_TOP: return "Virtus top"; + case ITEM_VIRTUS_ROBE_BOTTOM:return "Virtus bot"; + case ITEM_CRYSTAL_HELM: return "Crystal hm"; + case ITEM_AVAS_ASSEMBLER: return "Assembler"; + case ITEM_CRYSTAL_BODY: return "Crystal bd"; + case ITEM_CRYSTAL_LEGS: return "Crystal lg"; + case ITEM_BOW_OF_FAERDHINEN: return "Fbow"; + case ITEM_CRYSTAL_SHIELD: return "Crystal sh"; + case ITEM_ECHO_BOOTS: return "Echo boots"; + case ITEM_BLESSED_DHIDE_BOOTS: return "Bless boot"; + case ITEM_MYSTIC_HAT: return "Mystic hat"; + case ITEM_TRIDENT_OF_SWAMP: return "Trident"; + case ITEM_BOOK_OF_DARKNESS: return "Book dark"; + case ITEM_AMETHYST_ARROW: return "Ameth arw"; + case ITEM_MYSTIC_BOOTS: return "Myst boots"; + case ITEM_BLESSED_COIF: return "Bless coif"; + case ITEM_BLACK_DHIDE_CHAPS: return "Dhide chap"; + case ITEM_MAGIC_SHORTBOW_I: return "MSB (i)"; + case ITEM_AVAS_ACCUMULATOR: return "Accumulate"; + default: return full; + } +} + +static void gui_text_shadow( + const GuiState* gs, + const char* text, + int x, + int y, + int size, + Color color +) { + if (!text || !text[0]) return; + Font font = gui_font_for_size(gs, size); + Vector2 shadow = {(float)x + 1.0f, (float)y + 1.0f}; + Vector2 pos = {(float)x, (float)y}; + DrawTextEx(font, text, shadow, (float)size, 0.0f, GUI_TEXT_SHADOW); + DrawTextEx(font, text, pos, (float)size, 0.0f, color); +} + +static void gui_draw_slot(int x, int y, int w, int h, Color fill) { + DrawRectangle(x, y, w, h, fill); + DrawRectangleLines(x, y, w, h, GUI_BORDER); + DrawLine(x + 1, y + 1, x + w - 2, y + 1, GUI_BORDER_LT); + DrawLine(x + 1, y + 1, x + 1, y + h - 2, GUI_BORDER_LT); +} + +static void gui_draw_tex_centered(Texture2D tex, int bx, int by, int bw, int bh) { + if (tex.id == 0) return; + float sx = (float)(bw - 4) / (float)tex.width; + float sy = (float)(bh - 4) / (float)tex.height; + float s = (sx < sy) ? sx : sy; + int dw = (int)(tex.width * s); + int dh = (int)(tex.height * s); + int dx = bx + (bw - dw) / 2; + int dy = by + (bh - dh) / 2; + DrawTextureEx(tex, (Vector2){ (float)dx, (float)dy }, 0.0f, s, WHITE); +} + +static void gui_draw_equip_slot(GuiState* gs, int x, int y, int w, int h, + int gear_slot, uint8_t item_idx) { + if (gs->slot_tile.id != 0) { + Rectangle src = { 0, 0, (float)gs->slot_tile.width, (float)gs->slot_tile.height }; + Rectangle dst = { (float)x, (float)y, (float)w, (float)h }; + DrawTexturePro(gs->slot_tile, src, dst, (Vector2){0,0}, 0.0f, WHITE); + } else { + gui_draw_slot(x, y, w, h, GUI_BG_SLOT); + } + + if (item_idx != ITEM_NONE && item_idx < NUM_ITEMS) { + Texture2D item_tex = gui_get_item_sprite(gs, item_idx); + if (item_tex.id != 0) { + gui_draw_tex_centered(item_tex, x, y, w, h); + } else { + const char* name = gui_item_short_name(item_idx); + gui_text_shadow(gs, name, x + 2, y + h / 2 - 4, 7, GUI_TEXT_YELLOW); + } + } else if (gs->sprites_loaded && gear_slot >= 0 && gear_slot < GUI_NUM_SLOT_SPRITES) { + Texture2D bg = gs->slot_sprites[gear_slot]; + if (bg.id != 0) { + gui_draw_tex_centered(bg, x, y, w, h); + } + } +} + +typedef struct { + int logical_tab; + const char* stone_asset; + const char* icon_asset; + Rectangle rect; + Rectangle icon_rect; +} GuiSideStoneRef; + +static const GuiSideStoneRef GUI_SIDE_STONES[] = { + {GUI_TAB_COMBAT, "side_stone_highlights_0", "side_icon_combat", {0, 0, 38, 36}, {4, 0, 33, 36}}, + {GUI_TAB_STATS, "side_stone_highlights_4", "side_icon_stats", {38, 0, 33, 36}, {38, 0, 33, 36}}, + {GUI_TAB_QUESTS, "side_stone_highlights_4", "side_icon_quests", {71, 0, 38, 36}, {71, 0, 33, 36}}, + {GUI_TAB_INVENTORY, "side_stone_highlights_4", "side_icon_inventory", {104, 0, 33, 36}, {104, 0, 33, 36}}, + {GUI_TAB_EQUIPMENT, "side_stone_highlights_4", "side_icon_equipment", {137, 0, 33, 36}, {137, 0, 33, 36}}, + {GUI_TAB_PRAYER, "side_stone_highlights_4", "side_icon_prayer", {170, 0, 33, 36}, {170, 0, 33, 36}}, + {GUI_TAB_SPELLBOOK, "side_stone_highlights_1", "side_icon_magic_ancient", {203, 0, 38, 36}, {204, 0, 33, 36}}, + {-1, "side_stone_highlights_2", "side_icon_clan", {0, 0, 38, 36}, {4, 0, 33, 36}}, + {-1, "side_stone_highlights_4", "side_icon_friends", {38, 0, 33, 36}, {38, 0, 33, 36}}, + {-1, "side_stone_highlights_4", "side_icon_grouping", {71, 0, 33, 36}, {71, 0, 33, 36}}, + {-1, "side_stone_highlights_4", "side_icon_logout", {104, 0, 33, 36}, {104, 0, 33, 36}}, + {-1, "side_stone_highlights_4", "side_icon_options", {137, 0, 33, 36}, {137, 0, 33, 36}}, + {-1, "side_stone_highlights_4", "side_icon_emotes", {170, 0, 33, 36}, {170, 0, 33, 36}}, + {-1, "side_stone_highlights_3", "side_icon_music", {203, 0, 38, 36}, {204, 0, 33, 36}}, +}; + +static Rectangle gui_side_content_rect(GuiState* gs) { + return (Rectangle){ + (float)(gs->panel_x + GUI_SIDE_CONTENT_X), + (float)(gs->panel_y + GUI_SIDE_CONTENT_Y), + (float)GUI_SIDE_CONTENT_W, + (float)GUI_SIDE_CONTENT_H, + }; +} + +static Rectangle gui_side_ref_rect(GuiState* gs, Rectangle ref) { + Rectangle content = gui_side_content_rect(gs); + return (Rectangle){content.x + ref.x, content.y + ref.y, ref.width, ref.height}; +} + +static Rectangle gui_side_component_rect( + GuiState* gs, + const char* group_name, + const char* component_name, + Rectangle fallback +) { + Rectangle out = {0}; + if (osrs_ui_interfaces_component_rect( + &gs->ui_interfaces, + group_name, + component_name, + gui_side_content_rect(gs), + &out)) { + return out; + } + return gui_side_ref_rect(gs, fallback); +} + +static void gui_draw_texture(Texture2D tex, Rectangle dst, Color tint) { + if (tex.id == 0) return; + Rectangle src = {0, 0, (float)tex.width, (float)tex.height}; + DrawTexturePro(tex, src, dst, (Vector2){0, 0}, 0.0f, tint); +} + +static Rectangle gui_texture_fit_rect( + int texture_w, + int texture_h, + Rectangle rect, + float max_w, + float max_h +) { + assert(texture_w > 0); + assert(texture_h > 0); + float scale_x = max_w / (float)texture_w; + float scale_y = max_h / (float)texture_h; + float scale = scale_x < scale_y ? scale_x : scale_y; + if (scale > 1.0f) scale = 1.0f; + float width = (float)texture_w * scale; + float height = (float)texture_h * scale; + return (Rectangle){ + rect.x + (rect.width - width) * 0.5f, + rect.y + (rect.height - height) * 0.5f, + width, + height, + }; +} + +static void gui_draw_texture_centered( + Texture2D tex, + Rectangle rect, + float max_w, + float max_h, + Color tint +) { + if (tex.id == 0) return; + gui_draw_texture(tex, gui_texture_fit_rect(tex.width, tex.height, rect, max_w, max_h), tint); +} + +static void gui_draw_named_asset(GuiState* gs, const char* name, Rectangle dst, Color tint) { + gui_draw_texture(gui_asset(gs, name), dst, tint); +} + +static int gui_draw_named_asset_centered( + GuiState* gs, + const char* name, + Rectangle rect, + float max_w, + float max_h, + Color tint +) { + Texture2D tex = gui_asset(gs, name); + if (tex.id == 0) return 0; + gui_draw_texture_centered(tex, rect, max_w, max_h, tint); + return 1; +} + +static void gui_draw_named_asset_tiled(GuiState* gs, const char* name, Rectangle dst, Color tint) { + Texture2D tex = gui_asset(gs, name); + if (tex.id == 0) { + DrawRectangleRec(dst, GUI_BG_DARK); + return; + } + for (float y = dst.y; y < dst.y + dst.height; y += (float)tex.height) { + for (float x = dst.x; x < dst.x + dst.width; x += (float)tex.width) { + float w = (x + tex.width > dst.x + dst.width) ? dst.x + dst.width - x : (float)tex.width; + float h = (y + tex.height > dst.y + dst.height) ? dst.y + dst.height - y : (float)tex.height; + Rectangle src = {0, 0, w, h}; + Rectangle part = {x, y, w, h}; + DrawTexturePro(tex, src, part, (Vector2){0, 0}, 0.0f, tint); + } + } +} + +static Color gui_ui_color_from_rgb(int rgb, unsigned char opacity) { + return (Color){ + (unsigned char)((rgb >> 16) & 0xff), + (unsigned char)((rgb >> 8) & 0xff), + (unsigned char)(rgb & 0xff), + (unsigned char)(255 - opacity), + }; +} + +static const GuiUiComponentOverride* gui_ui_component_override( + const GuiUiOverrides* overrides, + uint32_t component_id +) { + if (!overrides) return NULL; + for (int i = 0; i < overrides->component_count; i++) { + if (overrides->components[i].component_id == component_id) { + return &overrides->components[i]; + } + } + return NULL; +} + +static GuiUiComponentOverride* gui_ui_push_component_override( + GuiUiComponentOverride* overrides, + int* count, + uint32_t component_id +) { + assert(*count < GUI_UI_MAX_COMPONENT_OVERRIDES); + GuiUiComponentOverride* override = &overrides[(*count)++]; + memset(override, 0, sizeof(*override)); + override->component_id = component_id; + return override; +} + +static const GuiUiItemContainerOverride* gui_ui_item_container_override( + const GuiUiOverrides* overrides, + uint32_t component_id +) { + if (!overrides) return NULL; + for (int i = 0; i < overrides->item_container_count; i++) { + if (overrides->item_containers[i].component_id == component_id) { + return &overrides->item_containers[i]; + } + } + return NULL; +} + +static void gui_draw_ui_item_slot(GuiState* gs, const GuiUiItemSlot* slot, Rectangle rect) { + if (slot->gear_slot >= 0) { + uint8_t item_idx = slot->enabled ? slot->item_db_idx : ITEM_NONE; + gui_draw_equip_slot( + gs, + (int)rect.x, + (int)rect.y, + (int)rect.width, + (int)rect.height, + slot->gear_slot, + item_idx); + if (!slot->enabled && slot->empty_asset) { + gui_draw_named_asset_centered(gs, slot->empty_asset, rect, rect.width, rect.height, WHITE); + } + } else { + if (!slot->enabled) return; + + Texture2D tex = {0}; + if (slot->item_db_idx != ITEM_NONE) { + tex = gui_get_item_sprite_for_quantity(gs, slot->item_db_idx, slot->quantity); + } else if (slot->osrs_id > 0) { + tex = gui_get_sprite_by_osrs_id_for_quantity(gs, slot->osrs_id, slot->quantity); + } + + Color tint = WHITE; + tint.a = slot->alpha == 0 ? 255 : slot->alpha; + if (tex.id != 0) { + Rectangle src = {0, 0, (float)tex.width, (float)tex.height}; + DrawTexturePro(tex, src, rect, (Vector2){0, 0}, 0.0f, tint); + } + + if (slot->quantity > 1) { + char text[16]; + gui_format_stack_quantity(slot->quantity, text, sizeof(text)); + gui_text_shadow(gs, text, (int)rect.x + 1, (int)rect.y - 1, 10, + gui_stack_text_color(slot->quantity)); + } + } + + if (slot->selected) { + DrawRectangleLinesEx((Rectangle){rect.x - 1, rect.y - 1, rect.width + 2, rect.height + 2}, + 2.0f, GUI_TEXT_YELLOW); + } +} + +static void gui_draw_ui_item_container( + GuiState* gs, + const GuiUiItemContainerOverride* container, + Rectangle rect +) { + if (!container || container->columns <= 0) return; + int count = container->slot_count; + if (count > GUI_UI_ITEM_CONTAINER_MAX_SLOTS) count = GUI_UI_ITEM_CONTAINER_MAX_SLOTS; + for (int i = 0; i < count; i++) { + int col = i % container->columns; + int row = i / container->columns; + Rectangle slot = { + rect.x + container->x0 + (float)col * container->step_x, + rect.y + container->y0 + (float)row * container->step_y, + container->slot_w, + container->slot_h, + }; + gui_draw_ui_item_slot(gs, &container->slots[i], slot); + } +} + +static void gui_draw_ui_sprite_component( + GuiState* gs, + const OsrsUiComponent* component, + const GuiUiComponentOverride* override, + Rectangle rect +) { + Texture2D tex = {0}; + if (override && override->sprite_asset) { + tex = gui_asset(gs, override->sprite_asset); + } else { + int sprite_id = override && override->sprite_present ? override->sprite_id : component->sprite_id; + tex = gui_sprite_asset(gs, sprite_id); + } + if (tex.id == 0) return; + + if (rect.width <= 0) rect.width = (float)tex.width; + if (rect.height <= 0) rect.height = (float)tex.height; + Color tint = WHITE; + tint.a = (unsigned char)(255 - component->opacity); + Rectangle src = { + component->flipped_horizontally ? (float)tex.width : 0.0f, + component->flipped_vertically ? (float)tex.height : 0.0f, + component->flipped_horizontally ? -(float)tex.width : (float)tex.width, + component->flipped_vertically ? -(float)tex.height : (float)tex.height, + }; + + if (component->shadow_color != 0) { + Color shadow = gui_ui_color_from_rgb(component->shadow_color, component->opacity); + DrawTexturePro(tex, src, + (Rectangle){rect.x + 1.0f, rect.y + 1.0f, rect.width, rect.height}, + (Vector2){0, 0}, 0.0f, shadow); + } + if (component->border_type > 0) { + Color border = BLACK; + border.a = tint.a; + int layers = component->border_type < 3 ? component->border_type : 3; + for (int layer = 0; layer < layers; layer++) { + float offset = (float)(layer + 1); + DrawTexturePro(tex, src, + (Rectangle){rect.x - offset, rect.y, rect.width, rect.height}, + (Vector2){0, 0}, 0.0f, border); + DrawTexturePro(tex, src, + (Rectangle){rect.x + offset, rect.y, rect.width, rect.height}, + (Vector2){0, 0}, 0.0f, border); + DrawTexturePro(tex, src, + (Rectangle){rect.x, rect.y - offset, rect.width, rect.height}, + (Vector2){0, 0}, 0.0f, border); + DrawTexturePro(tex, src, + (Rectangle){rect.x, rect.y + offset, rect.width, rect.height}, + (Vector2){0, 0}, 0.0f, border); + } + } + + if (component->sprite_tiling) { + for (float y = rect.y; y < rect.y + rect.height; y += (float)tex.height) { + for (float x = rect.x; x < rect.x + rect.width; x += (float)tex.width) { + float w = x + tex.width > rect.x + rect.width + ? rect.x + rect.width - x + : (float)tex.width; + float h = y + tex.height > rect.y + rect.height + ? rect.y + rect.height - y + : (float)tex.height; + Rectangle part_src = src; + part_src.width = component->flipped_horizontally ? -w : w; + part_src.height = component->flipped_vertically ? -h : h; + DrawTexturePro(tex, part_src, (Rectangle){x, y, w, h}, + (Vector2){0, 0}, 0.0f, tint); + } + } + } else { + DrawTexturePro(tex, src, rect, (Vector2){0, 0}, 0.0f, tint); + } +} + +static void gui_draw_ui_text_component( + const GuiState* gs, + const OsrsUiComponent* component, + const GuiUiComponentOverride* override, + Rectangle rect +) { + const char* text = override && override->text ? override->text : component->text; + if (!text || !text[0]) return; + int size = component->line_height > 0 ? component->line_height + 9 : 12; + if (size < 10) size = 10; + Color color = gui_ui_color_from_rgb(component->text_color, component->opacity); + int width = gui_measure_text(gs, text, size); + int x = (int)rect.x; + int y = (int)rect.y; + if (component->x_text_alignment == 1) { + x = (int)(rect.x + (rect.width - width) * 0.5f); + } else if (component->x_text_alignment == 2) { + x = (int)(rect.x + rect.width - width); + } + if (component->y_text_alignment == 1) { + y = (int)(rect.y + (rect.height - size) * 0.5f); + } else if (component->y_text_alignment == 2) { + y = (int)(rect.y + rect.height - size); + } + Font font = gui_font_for_size(gs, size); + if (component->text_shadowed) { + DrawTextEx(font, text, (Vector2){(float)x + 1.0f, (float)y + 1.0f}, + (float)size, 0.0f, BLACK); + } + DrawTextEx(font, text, (Vector2){(float)x, (float)y}, (float)size, 0.0f, color); +} + +static void gui_draw_ui_component( + GuiState* gs, + const OsrsUiInterfaceGroup* group, + const OsrsUiComponent* component, + Rectangle rect, + GuiUiClipState* clip, + const GuiUiOverrides* overrides +) { + const GuiUiComponentOverride* override = gui_ui_component_override(overrides, component->id); + if ((component->hidden && !(override && override->force_visible)) + || (override && override->hidden)) { + return; + } + if (clip && clip->active && !gui_rect_has_area(gui_rect_intersect(rect, clip->current))) { + return; + } + + if (override && (override->sprite_asset || override->sprite_present) && component->type != 5) { + gui_draw_ui_sprite_component(gs, component, override, rect); + } + + if (component->type == 3) { + Color color = gui_ui_color_from_rgb(component->text_color, component->opacity); + if (component->filled) { + DrawRectangleRec(rect, color); + } else { + DrawRectangleLinesEx(rect, 1, color); + } + } else if (component->type == 4) { + gui_draw_ui_text_component(gs, component, override, rect); + } else if (component->type == 5) { + gui_draw_ui_sprite_component(gs, component, override, rect); + } else if (component->type == 6 && component->model_type == 4 && component->model_id > 0) { + GuiUiItemSlot slot = { + .enabled = 1, + .osrs_id = component->model_id, + .quantity = 1, + .alpha = 255, + .gear_slot = -1, + }; + gui_draw_ui_item_slot(gs, &slot, rect); + } else if (component->type == 9) { + Color color = gui_ui_color_from_rgb(component->text_color, component->opacity); + Vector2 a = {rect.x, component->line_direction ? rect.y + rect.height : rect.y}; + Vector2 b = {rect.x + rect.width, component->line_direction ? rect.y : rect.y + rect.height}; + DrawLineEx(a, b, component->line_width > 0 ? (float)component->line_width : 1.0f, color); + } + + Rectangle child_parent = component->type == 0 + ? osrs_ui_rect_expand_to_scroll(rect, component) + : rect; + for (int i = 0; i < group->component_count; i++) { + const OsrsUiComponent* child = &group->components[i]; + if (child->parent_id != (int32_t)component->id) continue; + Rectangle child_rect = osrs_ui_layout_component(child, child_parent, 0); + if (component->type == 0 && gui_rect_has_area(rect)) { + Rectangle prev = {0}; + int prev_active = 0; + gui_push_clip(gs, clip, rect, &prev, &prev_active); + gui_draw_ui_component(gs, group, child, child_rect, clip, overrides); + gui_pop_clip(gs, clip, prev, prev_active); + } else { + gui_draw_ui_component(gs, group, child, child_rect, clip, overrides); + } + } + + const GuiUiItemContainerOverride* container = + gui_ui_item_container_override(overrides, component->id); + if (container) gui_draw_ui_item_container(gs, container, rect); + if (override && override->item.present) { + gui_draw_ui_item_slot(gs, &override->item, rect); + } +} + +static int gui_draw_ui_group( + GuiState* gs, + const char* group_name, + Rectangle mount, + const GuiUiOverrides* overrides +) { + const OsrsUiInterfaceGroup* group = osrs_ui_interface_group(&gs->ui_interfaces, group_name); + if (!group) return 0; + GuiUiClipState clip = {0}; + Rectangle prev = {0}; + int prev_active = 0; + gui_push_clip(gs, &clip, mount, &prev, &prev_active); + for (int i = 0; i < group->component_count; i++) { + const OsrsUiComponent* component = &group->components[i]; + if (component->parent_id != -1) continue; + Rectangle rect = osrs_ui_layout_component( + component, mount, osrs_ui_component_uses_mount_rect(component)); + gui_draw_ui_component(gs, group, component, rect, &clip, overrides); + } + gui_pop_clip(gs, &clip, prev, prev_active); + return 1; +} + +static void gui_draw_side_chrome(GuiState* gs) { + Rectangle backing = {(float)(gs->panel_x + 20), (float)(gs->panel_y + 27), 200.0f, 281.0f}; + gui_draw_named_asset_tiled(gs, "tradebacking_dark", backing, WHITE); + + gui_draw_named_asset(gs, "osrs_stretch_side_topbottom_0", + (Rectangle){(float)gs->panel_x, (float)(gs->panel_y + GUI_SIDE_TOP_Y), GUI_SIDE_MENU_W, 37}, WHITE); + gui_draw_named_asset(gs, "osrs_stretch_side_topbottom_1", + (Rectangle){(float)gs->panel_x, (float)(gs->panel_y + GUI_SIDE_BOTTOM_Y), GUI_SIDE_MENU_W, 37}, WHITE); + gui_draw_named_asset(gs, "osrs_stretch_side_columns_0", + (Rectangle){(float)(gs->panel_x + 2), (float)(gs->panel_y + 37), 26, 261}, WHITE); + gui_draw_named_asset(gs, "osrs_stretch_side_columns_1", + (Rectangle){(float)(gs->panel_x + 212), (float)(gs->panel_y + 37), 26, 261}, WHITE); + + int count = (int)(sizeof(GUI_SIDE_STONES) / sizeof(GUI_SIDE_STONES[0])); + for (int i = 0; i < count; i++) { + const GuiSideStoneRef* ref = &GUI_SIDE_STONES[i]; + if (ref->logical_tab != (int)gs->active_tab) continue; + int row_y = gs->panel_y + (i < 7 ? GUI_SIDE_TOP_Y : GUI_SIDE_BOTTOM_Y); + int pressed = gs->tab_press_timer[gs->active_tab] > 0 ? 1 : 0; + Rectangle stone = { + (float)(gs->panel_x + (int)ref->rect.x), + (float)(row_y + (int)ref->rect.y + pressed), + ref->rect.width, + ref->rect.height, + }; + gui_draw_named_asset_tiled(gs, ref->stone_asset, stone, WHITE); + DrawRectangleRec(stone, (Color){145, 22, 18, (unsigned char)(pressed ? 72 : 44)}); + break; + } + + for (int i = 0; i < count; i++) { + const GuiSideStoneRef* ref = &GUI_SIDE_STONES[i]; + int row_y = gs->panel_y + (i < 7 ? GUI_SIDE_TOP_Y : GUI_SIDE_BOTTOM_Y); + int pressed = ref->logical_tab == (int)gs->active_tab && + gs->tab_press_timer[gs->active_tab] > 0 ? 1 : 0; + Rectangle icon = { + (float)(gs->panel_x + (int)ref->icon_rect.x), + (float)(row_y + (int)ref->icon_rect.y + pressed), + ref->icon_rect.width, + ref->icon_rect.height, + }; + gui_draw_named_asset(gs, ref->icon_asset, icon, WHITE); + } +} + +static void gui_draw_tab_bar(GuiState* gs) { + gui_draw_side_chrome(gs); +} + +static int gui_handle_tab_click(GuiState* gs, int mouse_x, int mouse_y) { + int count = (int)(sizeof(GUI_SIDE_STONES) / sizeof(GUI_SIDE_STONES[0])); + for (int i = 0; i < count; i++) { + const GuiSideStoneRef* ref = &GUI_SIDE_STONES[i]; + if (ref->logical_tab < 0) continue; + int row_y = gs->panel_y + (i < 7 ? GUI_SIDE_TOP_Y : GUI_SIDE_BOTTOM_Y); + int x = gs->panel_x + (int)ref->rect.x; + int y = row_y + (int)ref->rect.y; + if (mouse_x >= x && mouse_x < x + (int)ref->rect.width && + mouse_y >= y && mouse_y < y + (int)ref->rect.height) { + gs->active_tab = (GuiTab)ref->logical_tab; + gs->tab_press_timer[ref->logical_tab] = GUI_TAB_PRESS_TICKS; + return 1; + } + } + return 0; +} + +static int gui_content_y(GuiState* gs) { + return gs->panel_y + gs->status_bar_h + gs->tab_h; +} + +#define INV_COLS 4 +#define INV_PANEL_CONTENT_X GUI_SIDE_CONTENT_X +#define INV_SLOT_X 16 +#define INV_SLOT_Y 8 +#define INV_CELL_W 42 +#define INV_CELL_H 36 +#define INV_SPRITE_W 36 +#define INV_SPRITE_H 32 + +static int gui_consumable_osrs_id(InvSlotType type, int doses) { + switch (type) { + case INV_SLOT_FOOD: return OSRS_ID_SHARK; + case INV_SLOT_KARAMBWAN: return OSRS_ID_KARAMBWAN; + case INV_SLOT_BREW: + if (doses >= 4) return OSRS_ID_BREW_4; + if (doses == 3) return OSRS_ID_BREW_3; + if (doses == 2) return OSRS_ID_BREW_2; + return OSRS_ID_BREW_1; + case INV_SLOT_RESTORE: + if (doses >= 4) return OSRS_ID_RESTORE_4; + if (doses == 3) return OSRS_ID_RESTORE_3; + if (doses == 2) return OSRS_ID_RESTORE_2; + return OSRS_ID_RESTORE_1; + case INV_SLOT_COMBAT_POT: + if (doses >= 4) return OSRS_ID_COMBAT_4; + if (doses == 3) return OSRS_ID_COMBAT_3; + if (doses == 2) return OSRS_ID_COMBAT_2; + return OSRS_ID_COMBAT_1; + case INV_SLOT_RANGED_POT: + if (doses >= 4) return OSRS_ID_RANGED_4; + if (doses == 3) return OSRS_ID_RANGED_3; + if (doses == 2) return OSRS_ID_RANGED_2; + return OSRS_ID_RANGED_1; + case INV_SLOT_ANTIVENOM: + if (doses >= 4) return OSRS_ID_ANTIVENOM_4; + if (doses == 3) return OSRS_ID_ANTIVENOM_3; + if (doses == 2) return OSRS_ID_ANTIVENOM_2; + return OSRS_ID_ANTIVENOM_1; + case INV_SLOT_PRAYER_POT: + if (doses >= 4) return OSRS_ID_PRAYER_POT_4; + if (doses == 3) return OSRS_ID_PRAYER_POT_3; + if (doses == 2) return OSRS_ID_PRAYER_POT_2; + return OSRS_ID_PRAYER_POT_1; + case INV_SLOT_BASTION_POT: + if (doses >= 4) return OSRS_ID_BASTION_4; + if (doses == 3) return OSRS_ID_BASTION_3; + if (doses == 2) return OSRS_ID_BASTION_2; + return OSRS_ID_BASTION_1; + case INV_SLOT_STAMINA_POT: + if (doses >= 4) return OSRS_ID_STAMINA_4; + if (doses == 3) return OSRS_ID_STAMINA_3; + if (doses == 2) return OSRS_ID_STAMINA_2; + return OSRS_ID_STAMINA_1; + case INV_SLOT_SATURATED_HEART: + return OSRS_ID_SATURATED_HEART; + default: return 0; + } +} + +static int gui_inv_first_empty(GuiState* gs) { + for (int i = 0; i < INV_GRID_SLOTS; i++) { + if (gs->inv_grid[i].type == INV_SLOT_EMPTY) return i; + } + return -1; +} + +static int gui_inv_find_equipment(GuiState* gs, uint8_t item_db_idx) { + for (int i = 0; i < INV_GRID_SLOTS; i++) { + if (gs->inv_grid[i].type == INV_SLOT_EQUIPMENT && + gs->inv_grid[i].item_db_idx == item_db_idx) return i; + } + return -1; +} + +static int gui_inv_remove_last_consumable(GuiState* gs, InvSlotType type) { + for (int i = INV_GRID_SLOTS - 1; i >= 0; i--) { + if (gs->inv_grid[i].type == type) { + gs->inv_grid[i].type = INV_SLOT_EMPTY; + gs->inv_grid[i].item_db_idx = 0; + gs->inv_grid[i].osrs_id = 0; + return 1; + } + } + return 0; +} + +static int gui_inv_place_equipment(GuiState* gs, uint8_t item_db_idx) { + int slot = gui_inv_first_empty(gs); + if (slot < 0) return -1; + gs->inv_grid[slot].type = INV_SLOT_EQUIPMENT; + gs->inv_grid[slot].item_db_idx = item_db_idx; + gs->inv_grid[slot].osrs_id = ITEM_DATABASE[item_db_idx].item_id; + return slot; +} + +static void gui_snapshot_inventory_state(GuiState* gs, const Player* p) { + memcpy(gs->inv_prev_equipped, p->equipped, NUM_GEAR_SLOTS); + gs->inv_prev_food_count = p->food_count; + gs->inv_prev_karambwan_count = p->karambwan_count; + gs->inv_prev_brew_doses = p->brew_doses; + gs->inv_prev_restore_doses = p->restore_doses; + gs->inv_prev_prayer_pot_doses = p->prayer_pot_doses; + gs->inv_prev_combat_doses = p->combat_potion_doses; + gs->inv_prev_ranged_doses = p->ranged_potion_doses; + gs->inv_prev_bastion_doses = p->bastion_doses; + gs->inv_prev_stamina_doses = p->stamina_doses; + gs->inv_prev_antivenom_doses = p->antivenom_doses; + gs->inv_prev_saturated_heart_count = p->saturated_heart_count; +} + +static int gui_inventory_consumables_changed(const GuiState* gs, const Player* p) { + return p->food_count != gs->inv_prev_food_count + || p->karambwan_count != gs->inv_prev_karambwan_count + || p->brew_doses != gs->inv_prev_brew_doses + || p->restore_doses != gs->inv_prev_restore_doses + || p->prayer_pot_doses != gs->inv_prev_prayer_pot_doses + || p->combat_potion_doses != gs->inv_prev_combat_doses + || p->ranged_potion_doses != gs->inv_prev_ranged_doses + || p->bastion_doses != gs->inv_prev_bastion_doses + || p->stamina_doses != gs->inv_prev_stamina_doses + || p->antivenom_doses != gs->inv_prev_antivenom_doses + || p->saturated_heart_count != gs->inv_prev_saturated_heart_count; +} + +static void gui_reset_inventory_ui_state(GuiState* gs) { + gs->inv_grid_dirty = 1; + gs->human_clicked_inv_slot = -1; + gs->inv_dim_slot = -1; + gs->inv_dim_timer = 0; + gs->inv_drag_active = 0; + gs->inv_drag_src_slot = -1; + gs->inv_drag_start_x = 0; + gs->inv_drag_start_y = 0; + gs->inv_drag_mouse_x = 0; + gs->inv_drag_mouse_y = 0; + gs->inv_drag_press_time = 0.0; + for (int i = 0; i < GUI_TAB_COUNT; i++) { + gs->tab_press_timer[i] = 0; + } +} + +static void gui_populate_inventory(GuiState* gs, Player* p) { + memset(gs->inv_grid, 0, sizeof(gs->inv_grid)); + int n = 0; + + for (int s = 0; s < NUM_GEAR_SLOTS && n < INV_GRID_SLOTS; s++) { + for (int i = 0; i < p->num_items_in_slot[s] && n < INV_GRID_SLOTS; i++) { + uint8_t item = p->inventory[s][i]; + if (item == ITEM_NONE) continue; + int is_equipped = 0; + for (int e = 0; e < NUM_GEAR_SLOTS; e++) { + if (p->equipped[e] == item) { is_equipped = 1; break; } + } + if (is_equipped) continue; + int dup = 0; + for (int j = 0; j < n; j++) { + if (gs->inv_grid[j].type == INV_SLOT_EQUIPMENT && + gs->inv_grid[j].item_db_idx == item) { dup = 1; break; } + } + if (dup) continue; + gs->inv_grid[n].type = INV_SLOT_EQUIPMENT; + gs->inv_grid[n].item_db_idx = item; + gs->inv_grid[n].osrs_id = ITEM_DATABASE[item].item_id; + n++; + } + } + + for (int i = 0; i < p->food_count && n < INV_GRID_SLOTS; i++) { + gs->inv_grid[n].type = INV_SLOT_FOOD; + gs->inv_grid[n].osrs_id = OSRS_ID_SHARK; + n++; + } + for (int i = 0; i < p->karambwan_count && n < INV_GRID_SLOTS; i++) { + gs->inv_grid[n].type = INV_SLOT_KARAMBWAN; + gs->inv_grid[n].osrs_id = OSRS_ID_KARAMBWAN; + n++; + } + + #define ADD_POTION_VIALS(doses_total, slot_type) do { \ + int _rem = (doses_total); \ + while (_rem > 0 && n < INV_GRID_SLOTS) { \ + int _d = (_rem >= 4) ? 4 : _rem; \ + gs->inv_grid[n].type = (slot_type); \ + gs->inv_grid[n].osrs_id = gui_consumable_osrs_id((slot_type), _d); \ + _rem -= _d; \ + n++; \ + } \ + } while(0) + + ADD_POTION_VIALS(p->brew_doses, INV_SLOT_BREW); + ADD_POTION_VIALS(p->restore_doses, INV_SLOT_RESTORE); + ADD_POTION_VIALS(p->combat_potion_doses, INV_SLOT_COMBAT_POT); + ADD_POTION_VIALS(p->ranged_potion_doses, INV_SLOT_RANGED_POT); + ADD_POTION_VIALS(p->bastion_doses, INV_SLOT_BASTION_POT); + ADD_POTION_VIALS(p->stamina_doses, INV_SLOT_STAMINA_POT); + ADD_POTION_VIALS(p->antivenom_doses, INV_SLOT_ANTIVENOM); + ADD_POTION_VIALS(p->prayer_pot_doses, INV_SLOT_PRAYER_POT); + #undef ADD_POTION_VIALS + + for (int i = 0; i < p->saturated_heart_count && n < INV_GRID_SLOTS; i++) { + gs->inv_grid[n].type = INV_SLOT_SATURATED_HEART; + gs->inv_grid[n].osrs_id = OSRS_ID_SATURATED_HEART; + n++; + } + + gui_snapshot_inventory_state(gs, p); +} + +static void gui_inv_update_potion_doses(GuiState* gs, InvSlotType type, + int total_doses) { + int vial_slots[INV_GRID_SLOTS]; + int vial_count = 0; + for (int i = 0; i < INV_GRID_SLOTS; i++) { + if (gs->inv_grid[i].type == type) { + vial_slots[vial_count++] = i; + } + } + if (vial_count == 0) return; + + int old_total = 0; + for (int v = 0; v < vial_count; v++) { + int oid = gs->inv_grid[vial_slots[v]].osrs_id; + int d4 = gui_consumable_osrs_id(type, 4); + int d3 = gui_consumable_osrs_id(type, 3); + int d2 = gui_consumable_osrs_id(type, 2); + int d1 = gui_consumable_osrs_id(type, 1); + if (oid == d4) old_total += 4; + else if (oid == d3) old_total += 3; + else if (oid == d2) old_total += 2; + else if (oid == d1) old_total += 1; + } + int doses_lost = old_total - total_doses; + + int clicked = gs->human_clicked_inv_slot; + if (doses_lost > 0 && clicked >= 0 && clicked < INV_GRID_SLOTS && + gs->inv_grid[clicked].type == type) { + int oid = gs->inv_grid[clicked].osrs_id; + int cur_dose = 0; + for (int d = 4; d >= 1; d--) { + if (oid == gui_consumable_osrs_id(type, d)) { cur_dose = d; break; } + } + if (cur_dose > 0) { + int take = (doses_lost < cur_dose) ? doses_lost : cur_dose; + cur_dose -= take; + doses_lost -= take; + if (cur_dose <= 0) { + gs->inv_grid[clicked].type = INV_SLOT_EMPTY; + gs->inv_grid[clicked].item_db_idx = 0; + gs->inv_grid[clicked].osrs_id = 0; + } else { + gs->inv_grid[clicked].osrs_id = gui_consumable_osrs_id(type, cur_dose); + } + } + } + + for (int v = vial_count - 1; v >= 0 && doses_lost > 0; v--) { + int slot = vial_slots[v]; + if (slot == clicked) continue; + if (gs->inv_grid[slot].type != type) continue; + int oid = gs->inv_grid[slot].osrs_id; + int cur_dose = 0; + for (int d = 4; d >= 1; d--) { + if (oid == gui_consumable_osrs_id(type, d)) { cur_dose = d; break; } + } + if (cur_dose <= 0) continue; + int take = (doses_lost < cur_dose) ? doses_lost : cur_dose; + cur_dose -= take; + doses_lost -= take; + if (cur_dose <= 0) { + gs->inv_grid[slot].type = INV_SLOT_EMPTY; + gs->inv_grid[slot].item_db_idx = 0; + gs->inv_grid[slot].osrs_id = 0; + } else { + gs->inv_grid[slot].osrs_id = gui_consumable_osrs_id(type, cur_dose); + } + } +} + +static int gui_player_loadout_contains(const Player* p, uint8_t item_db_idx) { + for (int g = 0; g < NUM_GEAR_SLOTS; g++) { + for (int i = 0; i < p->num_items_in_slot[g]; i++) { + if (p->inventory[g][i] == item_db_idx) return 1; + } + } + return 0; +} + +static void gui_update_inventory(GuiState* gs, Player* p) { + for (int s = 0; s < NUM_GEAR_SLOTS; s++) { + uint8_t prev = gs->inv_prev_equipped[s]; + uint8_t curr = p->equipped[s]; + if (prev == curr) continue; + + if (curr != ITEM_NONE && prev != ITEM_NONE) { + int src = gui_inv_find_equipment(gs, curr); + if (src >= 0) { + int in_loadout = gui_player_loadout_contains(p, prev); + if (in_loadout) { + gs->inv_grid[src].type = INV_SLOT_EQUIPMENT; + gs->inv_grid[src].item_db_idx = prev; + gs->inv_grid[src].osrs_id = ITEM_DATABASE[prev].item_id; + } else { + gs->inv_grid[src].type = INV_SLOT_EMPTY; + gs->inv_grid[src].item_db_idx = 0; + gs->inv_grid[src].osrs_id = 0; + } + } + } else if (curr != ITEM_NONE) { + int src = gui_inv_find_equipment(gs, curr); + if (src >= 0) { + gs->inv_grid[src].type = INV_SLOT_EMPTY; + gs->inv_grid[src].item_db_idx = 0; + gs->inv_grid[src].osrs_id = 0; + } + } else if (prev != ITEM_NONE) { + int in_loadout = gui_player_loadout_contains(p, prev); + if (in_loadout && gui_inv_find_equipment(gs, prev) < 0) { + gui_inv_place_equipment(gs, prev); + } + } + } + + int clicked = gs->human_clicked_inv_slot; + int clicked_used = 0; + + int food_diff = gs->inv_prev_food_count - p->food_count; + for (int i = 0; i < food_diff; i++) { + if (!clicked_used && clicked >= 0 && clicked < INV_GRID_SLOTS && + gs->inv_grid[clicked].type == INV_SLOT_FOOD) { + gs->inv_grid[clicked].type = INV_SLOT_EMPTY; + gs->inv_grid[clicked].item_db_idx = 0; + gs->inv_grid[clicked].osrs_id = 0; + clicked_used = 1; + } else { + gui_inv_remove_last_consumable(gs, INV_SLOT_FOOD); + } + } + + int karam_diff = gs->inv_prev_karambwan_count - p->karambwan_count; + for (int i = 0; i < karam_diff; i++) { + if (!clicked_used && clicked >= 0 && clicked < INV_GRID_SLOTS && + gs->inv_grid[clicked].type == INV_SLOT_KARAMBWAN) { + gs->inv_grid[clicked].type = INV_SLOT_EMPTY; + gs->inv_grid[clicked].item_db_idx = 0; + gs->inv_grid[clicked].osrs_id = 0; + clicked_used = 1; + } else { + gui_inv_remove_last_consumable(gs, INV_SLOT_KARAMBWAN); + } + } + + if (p->brew_doses != gs->inv_prev_brew_doses) { + gui_inv_update_potion_doses(gs, INV_SLOT_BREW, p->brew_doses); + } + if (p->restore_doses != gs->inv_prev_restore_doses) { + gui_inv_update_potion_doses(gs, INV_SLOT_RESTORE, p->restore_doses); + } + if (p->combat_potion_doses != gs->inv_prev_combat_doses) { + gui_inv_update_potion_doses(gs, INV_SLOT_COMBAT_POT, p->combat_potion_doses); + } + if (p->ranged_potion_doses != gs->inv_prev_ranged_doses) { + gui_inv_update_potion_doses(gs, INV_SLOT_RANGED_POT, p->ranged_potion_doses); + } + if (p->bastion_doses != gs->inv_prev_bastion_doses) { + gui_inv_update_potion_doses(gs, INV_SLOT_BASTION_POT, p->bastion_doses); + } + if (p->stamina_doses != gs->inv_prev_stamina_doses) { + gui_inv_update_potion_doses(gs, INV_SLOT_STAMINA_POT, p->stamina_doses); + } + if (p->antivenom_doses != gs->inv_prev_antivenom_doses) { + gui_inv_update_potion_doses(gs, INV_SLOT_ANTIVENOM, p->antivenom_doses); + } + if (p->prayer_pot_doses != gs->inv_prev_prayer_pot_doses) { + gui_inv_update_potion_doses(gs, INV_SLOT_PRAYER_POT, p->prayer_pot_doses); + } + int heart_diff = gs->inv_prev_saturated_heart_count - p->saturated_heart_count; + for (int i = 0; i < heart_diff; i++) + gui_inv_remove_last_consumable(gs, INV_SLOT_SATURATED_HEART); + for (int i = 0; i < -heart_diff; i++) { + int slot = gui_inv_first_empty(gs); + if (slot < 0) break; + gs->inv_grid[slot].type = INV_SLOT_SATURATED_HEART; + gs->inv_grid[slot].osrs_id = OSRS_ID_SATURATED_HEART; + } + + if (clicked_used || gui_inventory_consumables_changed(gs, p)) { + gs->human_clicked_inv_slot = -1; + } + + gui_snapshot_inventory_state(gs, p); +} + +static void gui_inv_slot_pos(GuiState* gs, int slot, int* out_x, int* out_y) { + int grid_x = gs->panel_x + INV_PANEL_CONTENT_X + INV_SLOT_X; + int grid_y = gui_content_y(gs) + INV_SLOT_Y; + int col = slot % INV_COLS; + int row = slot / INV_COLS; + *out_x = grid_x + col * INV_CELL_W; + *out_y = grid_y + row * INV_CELL_H; +} + +static int gui_inv_slot_at(GuiState* gs, int mx, int my) { + for (int i = 0; i < INV_GRID_SLOTS; i++) { + int sx, sy; + gui_inv_slot_pos(gs, i, &sx, &sy); + if (mx >= sx && mx < sx + INV_SPRITE_W && my >= sy && my < sy + INV_SPRITE_H) { + return i; + } + } + return -1; +} + +static const char* gui_inv_primary_action_label(const InvSlot* inv) { + uint16_t raw_osrs_id = + inv->osrs_id > 0 && inv->osrs_id <= UINT16_MAX ? (uint16_t)inv->osrs_id : 0; + uint8_t item_idx = inv->type == INV_SLOT_EQUIPMENT + ? inv->item_db_idx + : ITEM_NONE; + OsrsInventoryClickResolution resolution = osrs_inventory_click_interpret( + item_idx, raw_osrs_id, OSRS_CLICK_TICK_FIRST); + switch (resolution.click_action) { + case OSRS_CLICK_EQUIP: { + int gear_slot = item_idx != ITEM_NONE ? item_to_gear_slot(item_idx) : -1; + return gear_slot == GEAR_SLOT_WEAPON || gear_slot == GEAR_SLOT_AMMO + ? "Wield" + : "Wear"; + } + case OSRS_CLICK_EAT: + return "Eat"; + case OSRS_CLICK_DRINK: + return "Drink"; + case OSRS_CLICK_NONE: + break; + default: + fprintf(stderr, "gui inventory: bad click action %d\n", + (int)resolution.click_action); + abort(); + } + switch (inv->type) { + case INV_SLOT_EQUIPMENT: { + if (inv->item_db_idx == ITEM_NONE) return NULL; + int gear_slot = item_to_gear_slot(inv->item_db_idx); + return gear_slot == GEAR_SLOT_WEAPON || gear_slot == GEAR_SLOT_AMMO + ? "Wield" + : "Wear"; + } + case INV_SLOT_FOOD: + case INV_SLOT_KARAMBWAN: + return "Eat"; + case INV_SLOT_BREW: + case INV_SLOT_RESTORE: + case INV_SLOT_COMBAT_POT: + case INV_SLOT_RANGED_POT: + case INV_SLOT_ANTIVENOM: + case INV_SLOT_PRAYER_POT: + case INV_SLOT_BASTION_POT: + case INV_SLOT_STAMINA_POT: + return "Drink"; + case INV_SLOT_SATURATED_HEART: + return NULL; + case INV_SLOT_EMPTY: + default: + return NULL; + } +} + +static const char* gui_inv_raw_osrs_id_display_name(int osrs_id) { + switch (osrs_id) { + case 27610: + return "Venator bow"; + case 12006: + return "Abyssal tentacle"; + case 27281: + return "Divine rune pouch"; + case 23685: + return "Divine super combat"; + case 23733: + return "Divine ranging potion"; + case 10925: + return "Sanfew serum"; + case 4417: + return "Guthix rest"; + case 30875: + return "Surge potion"; + default: + return ""; + } +} + +static const char* gui_inv_slot_display_name(const InvSlot* inv) { + switch (inv->type) { + case INV_SLOT_EQUIPMENT: + if (inv->item_db_idx != ITEM_NONE) return gui_item_short_name(inv->item_db_idx); + return gui_inv_raw_osrs_id_display_name(inv->osrs_id); + case INV_SLOT_FOOD: + return "Shark"; + case INV_SLOT_KARAMBWAN: + return "Karambwan"; + case INV_SLOT_BREW: + return "Saradomin brew"; + case INV_SLOT_RESTORE: + return "Super restore"; + case INV_SLOT_COMBAT_POT: + return "Super combat"; + case INV_SLOT_RANGED_POT: + return "Ranging potion"; + case INV_SLOT_ANTIVENOM: + return "Anti-venom+"; + case INV_SLOT_PRAYER_POT: + return "Prayer potion"; + case INV_SLOT_BASTION_POT: + return "Bastion potion"; + case INV_SLOT_STAMINA_POT: + return "Stamina potion"; + case INV_SLOT_SATURATED_HEART: + return "Saturated heart"; + case INV_SLOT_EMPTY: + default: + return ""; + } +} + +static int gui_inv_select_item(GuiState* gs, HumanInput* hi, int slot) { + if (!hi || !hi->enabled) return 0; + if (slot < 0 || slot >= INV_GRID_SLOTS) return 0; + InvSlot* inv = &gs->inv_grid[slot]; + if (inv->type == INV_SLOT_EMPTY) return 0; + + int item_db_idx = inv->type == INV_SLOT_EQUIPMENT ? inv->item_db_idx : -1; + int osrs_id = inv->osrs_id; + if (osrs_id == 0 && inv->type == INV_SLOT_EQUIPMENT) { + osrs_id = ITEM_DATABASE[inv->item_db_idx].item_id; + } + + gs->inv_dim_slot = slot; + gs->inv_dim_timer = INV_DIM_TICKS; + human_input_apply_ui_intent(hi, + osrs_ui_intent_select_item(slot, item_db_idx, osrs_id)); + return 1; +} + +static InvAction gui_inv_click(GuiState* gs, Player* p, int slot, + HumanInput* hi) { + if (slot < 0 || slot >= INV_GRID_SLOTS) return INV_ACTION_NONE; + InvSlot* inv = &gs->inv_grid[slot]; + if (inv->type == INV_SLOT_EMPTY) return INV_ACTION_NONE; + + gs->inv_dim_slot = slot; + gs->inv_dim_timer = INV_DIM_TICKS; + + int human_active = (hi && hi->enabled); + if (human_active && hi->cursor_mode == CURSOR_ITEM_TARGET) { + OsrsUiIntent intent = osrs_ui_intent_item_on_item( + hi->selected_item_inventory_slot, + slot, + hi->selected_item_db_idx, + hi->selected_item_osrs_id); + human_input_apply_ui_intent(hi, intent); + return INV_ACTION_ITEM_ON_ITEM; + } + + switch (inv->type) { + case INV_SLOT_EQUIPMENT: { + if (human_active && gs->display_inventory_count > 0) { + human_input_queue_inventory_primary_click(hi, slot); + gs->human_clicked_inv_slot = slot; + return INV_ACTION_EQUIP; + } + int gear_slot = item_to_gear_slot(inv->item_db_idx); + if (gear_slot >= 0) { + if (human_active) { + human_input_queue_equip_inventory_item(hi, slot, inv->item_db_idx, gear_slot); + gs->human_clicked_inv_slot = slot; + } else { + slot_equip_item(p, gear_slot, inv->item_db_idx); + } + } + return INV_ACTION_EQUIP; + } + case INV_SLOT_FOOD: + if (human_active) { + hi->pending_food = 1; + human_input_queue_eat(hi, 0, slot); + gs->human_clicked_inv_slot = slot; + } + else { eat_food(p, 0); } + return INV_ACTION_EAT; + case INV_SLOT_KARAMBWAN: + if (human_active) { + hi->pending_karambwan = 1; + human_input_queue_eat(hi, 1, slot); + gs->human_clicked_inv_slot = slot; + } + else { eat_food(p, 1); } + return INV_ACTION_EAT; + case INV_SLOT_BREW: + if (human_active) { + hi->pending_potion = POTION_BREW; + human_input_queue_drink(hi, POTION_BREW, slot); + gs->human_clicked_inv_slot = slot; + } + return INV_ACTION_DRINK; + case INV_SLOT_RESTORE: + if (human_active) { + hi->pending_potion = POTION_RESTORE; + human_input_queue_drink(hi, POTION_RESTORE, slot); + gs->human_clicked_inv_slot = slot; + } + return INV_ACTION_DRINK; + case INV_SLOT_COMBAT_POT: + if (human_active) { + hi->pending_potion = POTION_COMBAT; + human_input_queue_drink(hi, POTION_COMBAT, slot); + gs->human_clicked_inv_slot = slot; + } + return INV_ACTION_DRINK; + case INV_SLOT_RANGED_POT: + if (human_active) { + hi->pending_potion = POTION_RANGED; + human_input_queue_drink(hi, POTION_RANGED, slot); + gs->human_clicked_inv_slot = slot; + } + return INV_ACTION_DRINK; + case INV_SLOT_ANTIVENOM: + if (human_active) { + hi->pending_potion = POTION_ANTIVENOM; + human_input_queue_drink(hi, POTION_ANTIVENOM, slot); + gs->human_clicked_inv_slot = slot; + } + return INV_ACTION_DRINK; + case INV_SLOT_PRAYER_POT: + if (human_active) { + hi->pending_potion = POTION_PRAYER_POT; + human_input_queue_drink(hi, POTION_PRAYER_POT, slot); + gs->human_clicked_inv_slot = slot; + } + return INV_ACTION_DRINK; + case INV_SLOT_BASTION_POT: + if (human_active) { + hi->pending_potion = POTION_BASTION; + human_input_queue_drink(hi, POTION_BASTION, slot); + gs->human_clicked_inv_slot = slot; + } + return INV_ACTION_DRINK; + case INV_SLOT_STAMINA_POT: + if (human_active) { + hi->pending_potion = POTION_STAMINA; + human_input_queue_drink(hi, POTION_STAMINA, slot); + gs->human_clicked_inv_slot = slot; + } + return INV_ACTION_DRINK; + default: + return INV_ACTION_NONE; + } +} + +static void gui_inv_handle_mouse(GuiState* gs, Player* p, HumanInput* hi) { + if (gs->active_tab != GUI_TAB_INVENTORY) return; + + int mx, my; + gui_mouse_to_panel_space(gs, GetMouseX(), GetMouseY(), &mx, &my); + + if (gs->inv_drag_active) { + gs->inv_drag_mouse_x = mx; + gs->inv_drag_mouse_y = my; + + if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) { + int target = gui_inv_slot_at(gs, mx, my); + if (target >= 0 && target != gs->inv_drag_src_slot) { + InvSlot source = gs->inv_grid[gs->inv_drag_src_slot]; + if (hi && hi->enabled) { + human_input_queue_item_on_item( + hi, + gs->inv_drag_src_slot, + target, + source.type == INV_SLOT_EQUIPMENT + ? source.item_db_idx + : ITEM_NONE, + source.osrs_id); + } + InvSlot tmp = gs->inv_grid[target]; + gs->inv_grid[target] = gs->inv_grid[gs->inv_drag_src_slot]; + gs->inv_grid[gs->inv_drag_src_slot] = tmp; + } + gs->inv_drag_active = 0; + gs->inv_drag_src_slot = -1; + } + return; + } + + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + int slot = gui_inv_slot_at(gs, mx, my); + if (slot >= 0 && gs->inv_grid[slot].type != INV_SLOT_EMPTY) { + gs->inv_drag_start_x = mx; + gs->inv_drag_start_y = my; + gs->inv_drag_src_slot = slot; + gs->inv_drag_press_time = GetTime(); + } + } + + if (IsMouseButtonDown(MOUSE_BUTTON_LEFT) && gs->inv_drag_src_slot >= 0 && !gs->inv_drag_active && + GetTime() - gs->inv_drag_press_time >= INV_DRAG_HOLD_SECONDS) { + int dx = mx - gs->inv_drag_start_x; + int dy = my - gs->inv_drag_start_y; + if (dx > INV_DRAG_DEAD_ZONE || dx < -INV_DRAG_DEAD_ZONE || + dy > INV_DRAG_DEAD_ZONE || dy < -INV_DRAG_DEAD_ZONE) { + gs->inv_drag_active = 1; + gs->inv_drag_mouse_x = mx; + gs->inv_drag_mouse_y = my; + gs->inv_dim_slot = gs->inv_drag_src_slot; + gs->inv_dim_timer = 9999; + } + } + + if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT) && gs->inv_drag_src_slot >= 0 && !gs->inv_drag_active) { + gui_inv_click(gs, p, gs->inv_drag_src_slot, hi); + gs->inv_drag_src_slot = -1; + } +} + +static void gui_tick(GuiState* gs) { + for (int i = 0; i < GUI_TAB_COUNT; i++) { + if (gs->tab_press_timer[i] > 0) { + gs->tab_press_timer[i]--; + } + } + if (gs->inv_dim_timer > 0 && !gs->inv_drag_active) { + gs->inv_dim_timer--; + if (gs->inv_dim_timer <= 0) { + gs->inv_dim_slot = -1; + } + } +} + +static GuiUiItemSlot gui_ui_slot_from_inv_slot(const GuiState* gs, const InvSlot* inv, int slot) { + GuiUiItemSlot out = { + .present = 1, + .enabled = inv->type != INV_SLOT_EMPTY, + .item_db_idx = ITEM_NONE, + .osrs_id = inv->osrs_id, + .quantity = 1, + .selected = 0, + .alpha = 255, + .gear_slot = -1, + }; + if (inv->type == INV_SLOT_EQUIPMENT) { + out.item_db_idx = inv->item_db_idx; + } + if (gs->inv_dim_slot == slot && gs->inv_dim_timer > 0) { + out.alpha = 128; + } + if (gs->inv_drag_active && slot == gs->inv_drag_src_slot) { + out.alpha = 80; + } + return out; +} + +static void gui_draw_inventory_drag(GuiState* gs) { + if (!(gs->inv_drag_active && gs->inv_drag_src_slot >= 0)) return; + InvSlot* drag = &gs->inv_grid[gs->inv_drag_src_slot]; + Texture2D tex = {0}; + if (drag->type == INV_SLOT_EQUIPMENT) { + tex = gui_get_item_sprite(gs, drag->item_db_idx); + } else { + tex = gui_get_sprite_by_osrs_id(gs, drag->osrs_id); + } + if (tex.id != 0) { + int dx = gs->inv_drag_mouse_x - INV_SPRITE_W / 2; + int dy = gs->inv_drag_mouse_y - INV_SPRITE_H / 2; + Rectangle src = {0, 0, (float)tex.width, (float)tex.height}; + Rectangle dst = {(float)dx, (float)dy, (float)INV_SPRITE_W, (float)INV_SPRITE_H}; + DrawTexturePro(tex, src, dst, (Vector2){0, 0}, 0.0f, + CLITERAL(Color){255, 255, 255, 200}); + } +} + +static int gui_draw_inventory_decoded(GuiState* gs) { + GuiUiItemContainerOverride container = { + .component_id = OSRS_UI_COMPONENT_ID(OSRS_UI_GROUP_INVENTORY, 0), + .slot_count = INV_GRID_SLOTS, + .columns = 4, + .x0 = INV_SLOT_X, + .y0 = INV_SLOT_Y, + .step_x = 42, + .step_y = 36, + .slot_w = INV_SPRITE_W, + .slot_h = INV_SPRITE_H, + }; + for (int slot = 0; slot < INV_GRID_SLOTS; slot++) { + container.slots[slot] = gui_ui_slot_from_inv_slot(gs, &gs->inv_grid[slot], slot); + } + GuiUiOverrides overrides = { + .item_containers = &container, + .item_container_count = 1, + }; + return gui_draw_ui_group(gs, "inventory", gui_side_content_rect(gs), &overrides); +} + +static void gui_draw_inventory_manual(GuiState* gs) { + for (int slot = 0; slot < INV_GRID_SLOTS; slot++) { + int cx, cy; + gui_inv_slot_pos(gs, slot, &cx, &cy); + InvSlot* inv = &gs->inv_grid[slot]; + + if (inv->type == INV_SLOT_EMPTY) continue; + + Texture2D tex = { 0 }; + if (inv->type == INV_SLOT_EQUIPMENT) { + tex = gui_get_item_sprite(gs, inv->item_db_idx); + } else { + tex = gui_get_sprite_by_osrs_id(gs, inv->osrs_id); + } + + int is_dimmed = (gs->inv_dim_slot == slot && gs->inv_dim_timer > 0); + Color tint = is_dimmed ? CLITERAL(Color){ 255, 255, 255, 128 } : WHITE; + + int dx = cx; + int dy = cy; + + if (gs->inv_drag_active && slot == gs->inv_drag_src_slot) { + if (tex.id != 0) { + Rectangle src = { 0, 0, (float)tex.width, (float)tex.height }; + Rectangle dst = { (float)dx, (float)dy, (float)INV_SPRITE_W, (float)INV_SPRITE_H }; + DrawTexturePro(tex, src, dst, (Vector2){0,0}, 0.0f, + CLITERAL(Color){ 255, 255, 255, 80 }); + } + continue; + } + + if (tex.id != 0) { + Rectangle src = { 0, 0, (float)tex.width, (float)tex.height }; + Rectangle dst = { (float)dx, (float)dy, (float)INV_SPRITE_W, (float)INV_SPRITE_H }; + DrawTexturePro(tex, src, dst, (Vector2){0,0}, 0.0f, tint); + } else { + const char* name = (inv->type == INV_SLOT_EQUIPMENT) + ? gui_item_short_name(inv->item_db_idx) : "???"; + gui_text_shadow(gs, name, cx + 2, cy + 12, 7, GUI_TEXT_YELLOW); + } + } + + gui_draw_inventory_drag(gs); +} + +static void gui_load_display_inventory(GuiState* gs) { + memset(gs->inv_grid, 0, sizeof(gs->inv_grid)); + int count = gs->display_inventory_count; + if (count > INV_GRID_SLOTS) count = INV_GRID_SLOTS; + for (int i = 0; i < count; i++) { + int osrs_id = gs->display_inventory_osrs_ids[i]; + if (osrs_id <= 0) continue; + uint8_t item_idx = osrs_id <= UINT16_MAX + ? osrs_item_index_for_raw_osrs_id((uint16_t)osrs_id) + : ITEM_NONE; + gs->inv_grid[i].type = INV_SLOT_EQUIPMENT; + gs->inv_grid[i].item_db_idx = item_idx; + gs->inv_grid[i].osrs_id = osrs_id; + } +} + +static void gui_draw_inventory(GuiState* gs, Player* p) { + if (gs->display_inventory_count > 0) { + gui_load_display_inventory(gs); + gs->inv_grid_dirty = 0; + if (gui_draw_inventory_decoded(gs)) { + gui_draw_inventory_drag(gs); + return; + } + gui_draw_inventory_manual(gs); + return; + } + if (gs->inv_grid_dirty) { + gui_populate_inventory(gs, p); + gs->inv_grid_dirty = 0; + } else { + gui_update_inventory(gs, p); + } + + if (gui_draw_inventory_decoded(gs)) { + gui_draw_inventory_drag(gs); + return; + } + + gui_draw_inventory_manual(gs); +} + +typedef struct { + int gear_slot; + const char* worn_asset; + const char* component_name; + uint32_t component_file_id; + Rectangle rect; +} GuiWornSlotRef; + +static const GuiWornSlotRef GUI_WORN_SLOT_REFS[] = { + {GEAR_SLOT_HEAD, "wornicons_0", "slot0", 15, {77, 4, 36, 36}}, + {GEAR_SLOT_CAPE, "wornicons_1", "slot1", 16, {36, 43, 36, 36}}, + {GEAR_SLOT_NECK, "wornicons_2", "slot2", 17, {77, 43, 36, 36}}, + {GEAR_SLOT_AMMO, "wornicons_6", "slot13", 25, {133, 43, 36, 36}}, + {GEAR_SLOT_WEAPON, "wornicons_3", "slot3", 18, {21, 82, 36, 36}}, + {GEAR_SLOT_BODY, "wornicons_4", "slot4", 19, {77, 82, 36, 36}}, + {GEAR_SLOT_SHIELD, "wornicons_5", "slot5", 20, {133, 82, 36, 36}}, + {GEAR_SLOT_LEGS, "wornicons_7", "slot7", 21, {77, 122, 36, 36}}, + {GEAR_SLOT_HANDS, "wornicons_9", "slot9", 22, {21, 162, 36, 36}}, + {GEAR_SLOT_FEET, "wornicons_10", "slot10", 23, {77, 162, 36, 36}}, + {GEAR_SLOT_RING, "wornicons_11", "slot12", 24, {133, 162, 36, 36}}, +}; + +typedef struct { + const char* component_name; + const char* icon_component_name; + const char* asset; + Rectangle rect; + Rectangle icon_rect; +} GuiWornButtonRef; + +static const GuiWornButtonRef GUI_WORN_BUTTON_REFS[] = { + {"equipment", "equipment_icon", "options_icons_16", {7, 208, 40, 40}, {10, 210, 32, 32}}, + {"pricechecker", "com_4", "options_icons_28", {52, 208, 40, 40}, {56, 212, 32, 32}}, + {"deathkeep", "com_6", "options_icons_18", {97, 208, 40, 40}, {99, 211, 34, 34}}, + {"call_follower", "com_8", "whistle", {142, 208, 40, 40}, {145, 211, 32, 32}}, +}; + +static int gui_draw_equipment_decoded(GuiState* gs, Player* p) { + GuiUiComponentOverride overrides[GUI_UI_MAX_COMPONENT_OVERRIDES]; + memset(overrides, 0, sizeof(overrides)); + int override_count = 0; + + int slot_count = (int)(sizeof(GUI_WORN_SLOT_REFS) / sizeof(GUI_WORN_SLOT_REFS[0])); + for (int i = 0; i < slot_count; i++) { + const GuiWornSlotRef* ref = &GUI_WORN_SLOT_REFS[i]; + int item_idx = p->equipped[ref->gear_slot]; + GuiUiComponentOverride* override = gui_ui_push_component_override( + overrides, + &override_count, + OSRS_UI_COMPONENT_ID(OSRS_UI_GROUP_WORNITEMS, ref->component_file_id)); + override->item = (GuiUiItemSlot){ + .present = 1, + .enabled = item_idx != ITEM_NONE, + .item_db_idx = item_idx, + .quantity = 1, + .alpha = 255, + .gear_slot = ref->gear_slot, + .empty_asset = ref->worn_asset, + }; + } + + GuiUiOverrides ui_overrides = { + .components = overrides, + .component_count = override_count, + }; + + int button_count = (int)(sizeof(GUI_WORN_BUTTON_REFS) / sizeof(GUI_WORN_BUTTON_REFS[0])); + for (int i = 0; i < button_count; i++) { + const GuiWornButtonRef* ref = &GUI_WORN_BUTTON_REFS[i]; + Rectangle rect = gui_side_component_rect(gs, "wornitems", ref->component_name, ref->rect); + gui_draw_named_asset(gs, "combatboxes_0", rect, WHITE); + if (gui_asset(gs, "combatboxes_0").id == 0) { + gui_draw_slot((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height, GUI_BG_SLOT); + } + } + + return gui_draw_ui_group(gs, "wornitems", gui_side_content_rect(gs), &ui_overrides); +} + +static void gui_draw_equipment(GuiState* gs, Player* p) { + if (gui_draw_equipment_decoded(gs, p)) return; + + gui_draw_named_asset_tiled(gs, "miscgraphics_2", + gui_side_ref_rect(gs, (Rectangle){77, 39, 36, 124}), WHITE); + gui_draw_named_asset_tiled(gs, "miscgraphics_2", + gui_side_ref_rect(gs, (Rectangle){21, 118, 36, 45}), WHITE); + gui_draw_named_asset_tiled(gs, "miscgraphics_2", + gui_side_ref_rect(gs, (Rectangle){133, 118, 36, 45}), WHITE); + gui_draw_named_asset_tiled(gs, "miscgraphics_3", + gui_side_ref_rect(gs, (Rectangle){56, 81, 78, 36}), WHITE); + gui_draw_named_asset_tiled(gs, "miscgraphics_3", + gui_side_ref_rect(gs, (Rectangle){71, 42, 48, 36}), WHITE); + + int slot_count = (int)(sizeof(GUI_WORN_SLOT_REFS) / sizeof(GUI_WORN_SLOT_REFS[0])); + for (int i = 0; i < slot_count; i++) { + const GuiWornSlotRef* ref = &GUI_WORN_SLOT_REFS[i]; + Rectangle dst = gui_side_component_rect(gs, "wornitems", ref->component_name, ref->rect); + int item_idx = p->equipped[ref->gear_slot]; + gui_draw_equip_slot( + gs, + (int)dst.x, + (int)dst.y, + (int)dst.width, + (int)dst.height, + ref->gear_slot, + item_idx); + if (item_idx == ITEM_NONE) { + gui_draw_named_asset_centered(gs, ref->worn_asset, dst, dst.width, dst.height, WHITE); + } + } + + int button_count = (int)(sizeof(GUI_WORN_BUTTON_REFS) / sizeof(GUI_WORN_BUTTON_REFS[0])); + for (int i = 0; i < button_count; i++) { + const GuiWornButtonRef* ref = &GUI_WORN_BUTTON_REFS[i]; + Rectangle rect = gui_side_component_rect(gs, "wornitems", ref->component_name, ref->rect); + Rectangle icon = gui_side_component_rect( + gs, "wornitems", ref->icon_component_name, ref->icon_rect); + gui_draw_named_asset(gs, "combatboxes_0", rect, WHITE); + if (gui_asset(gs, "combatboxes_0").id == 0) { + gui_draw_slot((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height, GUI_BG_SLOT); + } + gui_draw_named_asset_centered(gs, ref->asset, icon, icon.width, icon.height, WHITE); + } +} + +#define GUI_PRAYER_GRID_COUNT GUI_NUM_PRAYERS + +#define GUI_PRAYER_GRID_COLS 5 +#define GUI_PRAYER_CELL_PX 34 +#define GUI_PRAYER_ICON_PX 30 +#define GUI_PRAYER_GRID_X0 4 +#define GUI_PRAYER_GRID_Y0 9 +#define GUI_PRAYER_PITCH 37 + +#define GUI_SPELL_GRID_COLS 4 +#define GUI_SPELL_ICON_PX 24 +#define GUI_SPELL_PITCH_X (24 + 20) +#define GUI_SPELL_PITCH_Y (24 + 4) +#define GUI_SPELL_GRID_X0 ((GUI_SIDE_CONTENT_W - (4 * GUI_SPELL_PITCH_X - 20)) / 2 + 2) +#define GUI_SPELL_GRID_Y0 8 + +static void gui_prayer_grid_metrics(GuiState* gs, int* gx, int* gy, int* cell, int* gap) { + *gap = GUI_PRAYER_PITCH - GUI_PRAYER_CELL_PX; + *cell = GUI_PRAYER_CELL_PX; + *gx = gs->panel_x + GUI_SIDE_CONTENT_X + GUI_PRAYER_GRID_X0; + *gy = gui_content_y(gs) + GUI_PRAYER_GRID_Y0; +} + +static void gui_spell_grid_origin(GuiState* gs, int* gx, int* gy) { + *gx = gs->panel_x + GUI_SIDE_CONTENT_X + GUI_SPELL_GRID_X0; + *gy = gui_content_y(gs) + GUI_SPELL_GRID_Y0; +} + +typedef struct { + const char* names[4]; + FightStyle values[4]; + int count; +} GuiCombatStyleOptions; + +static GuiCombatStyleOptions gui_combat_style_options(uint8_t weapon) { + GuiCombatStyleOptions out = { + .names = { "Accurate", "Aggressive", "Controlled", "Defensive" }, + .values = { + FIGHT_STYLE_ACCURATE, + FIGHT_STYLE_AGGRESSIVE, + FIGHT_STYLE_CONTROLLED, + FIGHT_STYLE_DEFENSIVE, + }, + .count = 4, + }; + + switch (weapon) { + case ITEM_TOXIC_BLOWPIPE: + case ITEM_ZARYTE_CROSSBOW: + case ITEM_BOW_OF_FAERDHINEN: + case ITEM_TWISTED_BOW: + out.names[0] = "Accurate"; + out.names[1] = "Rapid"; + out.names[2] = "Longrange"; + out.values[0] = FIGHT_STYLE_ACCURATE; + out.values[1] = FIGHT_STYLE_RAPID; + out.values[2] = FIGHT_STYLE_LONGRANGE; + out.count = 3; + break; + case ITEM_SCYTHE_OF_VITUR: + out.names[0] = "Chop"; + out.names[1] = "Jab"; + out.names[2] = "Block"; + out.count = 3; + break; + case ITEM_SGS: + out.names[0] = "Chop"; + out.names[1] = "Slash"; + out.names[2] = "Smash"; + out.names[3] = "Block"; + break; + case ITEM_DRAGON_CLAWS: + out.names[0] = "Chop"; + out.names[1] = "Slash"; + out.names[2] = "Lunge"; + out.names[3] = "Block"; + break; + case ITEM_KODAI_WAND: + case ITEM_DRAGON_HUNTER_WAND: + out.names[0] = "Autocast"; + out.names[1] = "Defensive"; + out.values[0] = FIGHT_STYLE_AUTOCAST; + out.values[1] = FIGHT_STYLE_DEFENSIVE_AUTOCAST; + out.count = 2; + break; + default: + break; + } + + return out; +} + +static Rectangle gui_combat_style_fallback_rect(int index) { + static const Rectangle rects[4] = { + {20, 46, 68, 47}, + {102, 46, 68, 47}, + {20, 99, 68, 47}, + {102, 99, 68, 47}, + }; + assert(index >= 0 && index < 4); + return rects[index]; +} + +static Rectangle gui_combat_style_rect(GuiState* gs, int index) { + assert(index >= 0 && index < 4); + char component_name[2] = {(char)('0' + index), '\0'}; + return gui_side_component_rect( + gs, + "combat_interface", + component_name, + gui_combat_style_fallback_rect(index)); +} + +static Rectangle gui_combat_style_icon_rect(int index) { + static const Rectangle rects[4] = { + {37, 51, 34, 24}, + {119, 51, 34, 24}, + {37, 104, 34, 24}, + {119,104, 34, 24}, + }; + assert(index >= 0 && index < 4); + return rects[index]; +} + +static Rectangle gui_combat_style_text_rect(int index) { + static const Rectangle rects[4] = { + {20, 67, 68, 13}, + {102, 76, 68, 13}, + {20, 129, 68, 13}, + {102,129, 68, 13}, + }; + assert(index >= 0 && index < 4); + return rects[index]; +} + +static Rectangle gui_combat_autocast_rect(void) { + return (Rectangle){20, 153, 150, 26}; +} + +static Rectangle gui_combat_autocast_spell_rect(int index) { + static const Rectangle rects[2] = { + {20, 182, 70, 26}, + {100, 182, 70, 26}, + }; + assert(index >= 0 && index < 2); + return rects[index]; +} + +static Rectangle gui_combat_special_rect(void) { + return (Rectangle){20, 200, 150, 26}; +} + +static Rectangle gui_combat_category_rect(void) { + return (Rectangle){0, 233, 190, 28}; +} + +static void gui_draw_combat_box(GuiState* gs, Rectangle rect, int selected) { + gui_draw_named_asset(gs, selected ? "combatboxes_1" : "combatboxes_0", rect, WHITE); + if (gui_asset(gs, selected ? "combatboxes_1" : "combatboxes_0").id == 0) { + DrawRectangleRec(rect, selected ? (Color){83, 61, 43, 245} : (Color){45, 39, 31, 235}); + DrawRectangleLinesEx(rect, 1, selected ? GUI_TEXT_YELLOW : (Color){103, 89, 63, 255}); + } + if (selected) { + DrawRectangleRec(rect, (Color){120, 27, 20, 54}); + } +} + +static const char* gui_combat_icon_asset(uint8_t weapon, FightStyle style, int index) { + switch (weapon) { + case ITEM_TOXIC_BLOWPIPE: + case ITEM_ZARYTE_CROSSBOW: + case ITEM_BOW_OF_FAERDHINEN: + case ITEM_TWISTED_BOW: + switch (style) { + case FIGHT_STYLE_ACCURATE: return "combaticons2_15"; + case FIGHT_STYLE_RAPID: return "combaticons2_16"; + case FIGHT_STYLE_LONGRANGE: return "combaticons2_17"; + default: break; + } + break; + case ITEM_KODAI_WAND: + case ITEM_DRAGON_HUNTER_WAND: + return index == 0 ? "sideicons_interface_14" : "sideicons_interface_15"; + default: + break; + } + + switch (style) { + case FIGHT_STYLE_ACCURATE: return "combaticons_6"; + case FIGHT_STYLE_AGGRESSIVE: return "combaticons_5"; + case FIGHT_STYLE_CONTROLLED: return "combaticons_7"; + case FIGHT_STYLE_DEFENSIVE: return "combaticons_4"; + default: return "sideicons_interface_0"; + } +} + +static int gui_autocast_spell(const Player* p) { + return p->autocast_spell == ENCOUNTER_SPELL_ICE + ? ENCOUNTER_SPELL_ICE + : ENCOUNTER_SPELL_BLOOD; +} + +static const char* gui_autocast_spell_name(int spell) { + return spell == ENCOUNTER_SPELL_ICE ? "Ice Barrage" : "Blood Barrage"; +} + +static uint32_t gui_combat_component_id(uint32_t file_id) { + return OSRS_UI_COMPONENT_ID(OSRS_UI_GROUP_COMBAT_INTERFACE, file_id); +} + +static const char* gui_combat_selected_style_name( + const GuiCombatStyleOptions* styles, + FightStyle fight_style +) { + for (int i = 0; i < styles->count; i++) { + if (fight_style == styles->values[i]) return styles->names[i]; + } + return "Accurate"; +} + +static int gui_draw_combat_decoded( + GuiState* gs, + Player* p, + const char* wpn_name, + const GuiCombatStyleOptions* styles +) { + GuiUiComponentOverride overrides[GUI_UI_MAX_COMPONENT_OVERRIDES]; + int override_count = 0; + + char combat_level[32]; + snprintf(combat_level, sizeof(combat_level), "Combat Lvl: %d", + p->base_attack + p->base_strength + p->base_defence); + + char category_text[64]; + snprintf(category_text, sizeof(category_text), "Attack style: %s", + gui_combat_selected_style_name(styles, p->fight_style)); + + GuiUiComponentOverride* title = gui_ui_push_component_override( + overrides, &override_count, gui_combat_component_id(3)); + title->text = wpn_name; + + GuiUiComponentOverride* level = gui_ui_push_component_override( + overrides, &override_count, gui_combat_component_id(4)); + level->text = combat_level; + + GuiUiComponentOverride* category = gui_ui_push_component_override( + overrides, &override_count, gui_combat_component_id(5)); + category->text = category_text; + + static const uint32_t hidden_file_ids[] = {22, 32, 37, 38, 45, 46, 47, 48, 49}; + int hidden_count = (int)(sizeof(hidden_file_ids) / sizeof(hidden_file_ids[0])); + for (int i = 0; i < hidden_count; i++) { + GuiUiComponentOverride* hidden = gui_ui_push_component_override( + overrides, &override_count, gui_combat_component_id(hidden_file_ids[i])); + hidden->hidden = 1; + } + + static const uint32_t root_file_ids[4] = {6, 10, 14, 18}; + static const uint32_t icon_file_ids[4] = {8, 12, 16, 20}; + static const uint32_t text_file_ids[4] = {9, 13, 17, 21}; + for (int i = 0; i < styles->count; i++) { + int active = p->fight_style == styles->values[i]; + + GuiUiComponentOverride* root = gui_ui_push_component_override( + overrides, &override_count, gui_combat_component_id(root_file_ids[i])); + root->force_visible = 1; + root->sprite_asset = active ? "combatboxes_1" : "combatboxes_0"; + + GuiUiComponentOverride* icon = gui_ui_push_component_override( + overrides, &override_count, gui_combat_component_id(icon_file_ids[i])); + icon->sprite_asset = gui_combat_icon_asset(p->equipped[GEAR_SLOT_WEAPON], + styles->values[i], i); + + GuiUiComponentOverride* text = gui_ui_push_component_override( + overrides, &override_count, gui_combat_component_id(text_file_ids[i])); + text->text = styles->names[i]; + } + + GuiUiOverrides ui_overrides = { + .components = overrides, + .component_count = override_count, + }; + return gui_draw_ui_group(gs, "combat_interface", gui_side_content_rect(gs), &ui_overrides); +} + +static int gui_prayer_is_active(GuiPrayerIdx pidx, Player* p) { + switch (pidx) { + case GUI_PRAY_PROTECT_MAGIC: return p->prayer == PRAYER_PROTECT_MAGIC; + case GUI_PRAY_PROTECT_MISSILES: return p->prayer == PRAYER_PROTECT_RANGED; + case GUI_PRAY_PROTECT_MELEE: return p->prayer == PRAYER_PROTECT_MELEE; + case GUI_PRAY_REDEMPTION: return p->prayer == PRAYER_REDEMPTION; + case GUI_PRAY_SMITE: return p->prayer == PRAYER_SMITE; + case GUI_PRAY_PIETY: return p->offensive_prayer == OFFENSIVE_PRAYER_PIETY; + case GUI_PRAY_RIGOUR: return p->offensive_prayer == OFFENSIVE_PRAYER_RIGOUR; + case GUI_PRAY_AUGURY: return p->offensive_prayer == OFFENSIVE_PRAYER_AUGURY; + default: return 0; + } +} + +static void gui_draw_prayer(GuiState* gs, Player* p) { + int cols = GUI_PRAYER_GRID_COLS; + int gap, icon_sz, gx, gy; + gui_prayer_grid_metrics(gs, &gx, &gy, &icon_sz, &gap); + + for (int i = 0; i < GUI_PRAYER_GRID_COUNT; i++) { + int col = i % cols; + int row = i / cols; + int ix = gx + col * GUI_PRAYER_PITCH; + int iy = gy + row * GUI_PRAYER_PITCH; + + GuiPrayerIdx pidx = (GuiPrayerIdx)i; + int active = gui_prayer_is_active(pidx, p); + Rectangle cell_rect = {(float)ix, (float)iy, (float)icon_sz, (float)icon_sz}; + + if (active) { + gui_draw_named_asset(gs, "prayerglow_0", cell_rect, WHITE); + } + + if (gs->sprites_loaded) { + gui_draw_texture_centered(gs->prayer_on[pidx], cell_rect, + GUI_PRAYER_ICON_PX, GUI_PRAYER_ICON_PX, WHITE); + } + } +} + +static void gui_draw_combat(GuiState* gs, Player* p) { + const char* wpn_name = "Unarmed"; + if (p->equipped[GEAR_SLOT_WEAPON] != ITEM_NONE && + p->equipped[GEAR_SLOT_WEAPON] < NUM_ITEMS) { + wpn_name = gui_item_short_name(p->equipped[GEAR_SLOT_WEAPON]); + } + + GuiCombatStyleOptions styles = gui_combat_style_options(p->equipped[GEAR_SLOT_WEAPON]); + int decoded = gui_draw_combat_decoded(gs, p, wpn_name, &styles); + + if (!decoded) { + Rectangle title = gui_side_ref_rect(gs, (Rectangle){10, 6, 170, 14}); + Rectangle level = gui_side_ref_rect(gs, (Rectangle){10, 26, 170, 12}); + int tw = gui_measure_text(gs, wpn_name, 11); + gui_text_shadow(gs, wpn_name, (int)(title.x + title.width / 2 - tw / 2), + (int)title.y, 11, GUI_TEXT_ORANGE); + const char* combat_level = TextFormat("Combat Lvl: %d", + p->base_attack + p->base_strength + p->base_defence); + int cw = gui_measure_text(gs, combat_level, 10); + gui_text_shadow(gs, combat_level, (int)(level.x + level.width / 2 - cw / 2), + (int)level.y, 10, GUI_TEXT_YELLOW); + + for (int i = 0; i < styles.count; i++) { + Rectangle rect = gui_combat_style_rect(gs, i); + Rectangle icon = gui_side_ref_rect(gs, gui_combat_style_icon_rect(i)); + Rectangle text = gui_side_ref_rect(gs, gui_combat_style_text_rect(i)); + int active = p->fight_style == styles.values[i]; + gui_draw_combat_box(gs, rect, active); + gui_draw_named_asset_centered( + gs, + gui_combat_icon_asset(p->equipped[GEAR_SLOT_WEAPON], styles.values[i], i), + icon, + icon.width, + icon.height, + WHITE); + Color txt_c = active ? GUI_TEXT_YELLOW : GUI_TEXT_WHITE; + int txt_w = gui_measure_text(gs, styles.names[i], 10); + gui_text_shadow( + gs, + styles.names[i], + (int)(text.x + text.width / 2 - txt_w / 2), + (int)(text.y + 1), + 10, + txt_c); + } + } + + if (item_supports_ancient_autocast(p->equipped[GEAR_SLOT_WEAPON])) { + Rectangle ac = gui_side_ref_rect(gs, gui_combat_autocast_rect()); + int spell = gui_autocast_spell(p); + const char* spell_name = gui_autocast_spell_name(spell); + gui_draw_named_asset(gs, "combatboxes_1", ac, WHITE); + DrawRectangleLines((int)ac.x, (int)ac.y, (int)ac.width, (int)ac.height, + p->autocast_enabled ? GUI_TEXT_YELLOW : GUI_BORDER); + gui_text_shadow( + gs, + TextFormat("Autocast: %s", spell_name), + (int)ac.x + 8, + (int)ac.y + 7, + 10, + p->autocast_enabled ? GUI_TEXT_YELLOW : GUI_TEXT_WHITE); + + if (gs->autocast_selector_open) { + const char* names[2] = { "Blood", "Ice" }; + int spells[2] = { ENCOUNTER_SPELL_BLOOD, ENCOUNTER_SPELL_ICE }; + for (int i = 0; i < 2; i++) { + Rectangle rect = gui_side_ref_rect(gs, gui_combat_autocast_spell_rect(i)); + Color c = spells[i] == spell ? GUI_TEXT_YELLOW : GUI_TEXT_WHITE; + gui_draw_named_asset(gs, "combatboxes_1", rect, WHITE); + DrawRectangleLines((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height, + spells[i] == spell ? GUI_TEXT_YELLOW : GUI_BORDER); + int tw = gui_measure_text(gs, names[i], 10); + gui_text_shadow( + gs, + names[i], + (int)(rect.x + rect.width / 2 - tw / 2), + (int)rect.y + 7, + 10, + c); + } + } + } + + Rectangle spec = gui_side_ref_rect(gs, gui_combat_special_rect()); + float spec_pct = (float)p->special_energy / 100.0f; + gui_draw_named_asset(gs, "combatboxes_special_attack", spec, WHITE); + if (gui_asset(gs, "combatboxes_special_attack").id == 0) { + DrawRectangleRec(spec, (Color){32, 28, 22, 235}); + } + Rectangle empty = {spec.x + 2, spec.y + 7, spec.width - 4, 12}; + DrawRectangleRec(empty, (Color){115, 6, 6, 255}); + Rectangle fill = empty; + fill.width *= spec_pct; + DrawRectangleRec(fill, p->spec_armed ? GUI_SPEC_GREEN : (Color){57, 125, 59, 255}); + if (p->spec_armed) { + DrawRectangleRec(spec, CLITERAL(Color){ 200, 200, 50, 60 }); + } + DrawRectangleLinesEx((Rectangle){spec.x + 2, spec.y + 6, spec.width - 4, 14}, 1, + (Color){44, 42, 35, 255}); + const char* spec_text = TextFormat("Special Attack: %d%%", p->special_energy); + int spec_w = gui_measure_text(gs, spec_text, 10); + gui_text_shadow(gs, spec_text, (int)(spec.x + spec.width / 2 - spec_w / 2), + (int)(spec.y + 8), 10, GUI_TEXT_YELLOW); + + if (!decoded) { + Rectangle category = gui_side_ref_rect(gs, gui_combat_category_rect()); + const char* category_text = TextFormat("Attack style: %s", + gui_combat_selected_style_name(&styles, p->fight_style)); + int category_w = gui_measure_text(gs, category_text, 12); + gui_text_shadow(gs, category_text, (int)(category.x + category.width / 2 - category_w / 2), + (int)(category.y + 7), 12, GUI_TEXT_ORANGE); + } +} + +typedef struct { + const char* name; + GuiSpellIdx idx; +} GuiSpellEntry; + +static const GuiSpellEntry GUI_SPELL_GRID[] = { + { "Smoke Rush", GUI_SPELL_SMOKE_RUSH }, + { "Shadow Rush", GUI_SPELL_SHADOW_RUSH }, + { "Paddewwa Teleport", GUI_SPELL_PADDEWWA_TELEPORT }, + { "Blood Rush", GUI_SPELL_BLOOD_RUSH }, + { "Ice Rush", GUI_SPELL_ICE_RUSH }, + { "Senntisten Teleport", GUI_SPELL_SENNTISTEN_TELEPORT }, + { "Smoke Burst", GUI_SPELL_SMOKE_BURST }, + { "Shadow Burst", GUI_SPELL_SHADOW_BURST }, + { "Kharyrll Teleport", GUI_SPELL_KHARYRLL_TELEPORT }, + { "Blood Burst", GUI_SPELL_BLOOD_BURST }, + { "Ice Burst", GUI_SPELL_ICE_BURST }, + { "Lassar Teleport", GUI_SPELL_LASSAR_TELEPORT }, + { "Smoke Blitz", GUI_SPELL_SMOKE_BLITZ }, + { "Shadow Blitz", GUI_SPELL_SHADOW_BLITZ }, + { "Dareeyak Teleport", GUI_SPELL_DAREEYAK_TELEPORT }, + { "Blood Blitz", GUI_SPELL_BLOOD_BLITZ }, + { "Ice Blitz", GUI_SPELL_ICE_BLITZ }, + { "Carrallanger Teleport", GUI_SPELL_CARRALLANGER_TELEPORT }, + { "Smoke Barrage", GUI_SPELL_SMOKE_BARRAGE }, + { "Shadow Barrage",GUI_SPELL_SHADOW_BARRAGE }, + { "Annakarl Teleport", GUI_SPELL_ANNAKARL_TELEPORT }, + { "Blood Barrage", GUI_SPELL_BLOOD_BARRAGE }, + { "Ice Barrage", GUI_SPELL_ICE_BARRAGE }, + { "Ghorrock Teleport", GUI_SPELL_GHORROCK_TELEPORT }, +}; +#define GUI_SPELL_GRID_COUNT ((int)(sizeof(GUI_SPELL_GRID) / sizeof(GUI_SPELL_GRID[0]))) + +static inline int gui_spell_is_ice(GuiSpellIdx s) { + return s == GUI_SPELL_ICE_RUSH + || s == GUI_SPELL_ICE_BURST + || s == GUI_SPELL_ICE_BLITZ + || s == GUI_SPELL_ICE_BARRAGE; +} + +static inline int gui_spell_is_blood(GuiSpellIdx s) { + return s == GUI_SPELL_BLOOD_RUSH + || s == GUI_SPELL_BLOOD_BURST + || s == GUI_SPELL_BLOOD_BLITZ + || s == GUI_SPELL_BLOOD_BARRAGE; +} + +static inline int gui_spell_castable(GuiSpellIdx s) { + return gui_spell_is_ice(s) || gui_spell_is_blood(s); +} + +static void gui_draw_spellbook(GuiState* gs, Player* p) { + (void)p; + int gx, gy; + gui_spell_grid_origin(gs, &gx, &gy); + + for (int i = 0; i < GUI_SPELL_GRID_COUNT; i++) { + int col = i % GUI_SPELL_GRID_COLS; + int row = i / GUI_SPELL_GRID_COLS; + int ix = gx + col * GUI_SPELL_PITCH_X; + int iy = gy + row * GUI_SPELL_PITCH_Y; + + GuiSpellIdx sidx = GUI_SPELL_GRID[i].idx; + int targeting = (gs->pending_spell_highlight >= 0 && + (int)sidx == gs->pending_spell_highlight); + Rectangle cell_rect = {(float)ix, (float)iy, + (float)GUI_SPELL_ICON_PX, (float)GUI_SPELL_ICON_PX}; + + if (gs->sprites_loaded) { + int castable = gui_spell_castable(sidx); + Texture2D tex = castable ? gs->spell_on[sidx] : gs->spell_off[sidx]; + if (tex.id != 0) { + Rectangle src = {0, 0, (float)tex.width, (float)tex.height}; + DrawTexturePro(tex, src, cell_rect, (Vector2){0, 0}, 0.0f, WHITE); + } + } + if (targeting) { + DrawRectangleLinesEx(cell_rect, 2.0f, YELLOW); + } + } +} +typedef enum { + GUI_SKILL_PLAYER_ATTACK = 0, + GUI_SKILL_PLAYER_STRENGTH, + GUI_SKILL_PLAYER_DEFENCE, + GUI_SKILL_PLAYER_RANGED, + GUI_SKILL_PLAYER_PRAYER, + GUI_SKILL_PLAYER_MAGIC, + GUI_SKILL_PLAYER_HITPOINTS, + GUI_SKILL_PLAYER_OTHER, +} GuiSkillValueKind; + +typedef struct { + const char* component_name; + const char* icon_asset; + GuiSkillValueKind value_kind; + Rectangle fallback; +} GuiSkillPanelSlot; + +static const GuiSkillPanelSlot GUI_SKILL_PANEL_SLOTS[] = { + {"attack", "skill_icon_0", GUI_SKILL_PLAYER_ATTACK, {1, 1, 62, 30}}, + {"strength", "skill_icon_1", GUI_SKILL_PLAYER_STRENGTH, {1, 31, 62, 30}}, + {"defence", "skill_icon_2", GUI_SKILL_PLAYER_DEFENCE, {1, 61, 62, 30}}, + {"ranged", "skill_icon_3", GUI_SKILL_PLAYER_RANGED, {1, 91, 62, 30}}, + {"prayer", "skill_icon_4", GUI_SKILL_PLAYER_PRAYER, {1, 121, 62, 30}}, + {"magic", "skill_icon_5", GUI_SKILL_PLAYER_MAGIC, {1, 151, 62, 30}}, + {"runecraft", "skill_icon_18", GUI_SKILL_PLAYER_OTHER, {1, 181, 62, 30}}, + {"construction", "skill_icon_22", GUI_SKILL_PLAYER_OTHER, {1, 211, 62, 32}}, + {"hitpoints", "skill_icon_6", GUI_SKILL_PLAYER_HITPOINTS, {64, 1, 62, 30}}, + {"agility", "skill_icon_7", GUI_SKILL_PLAYER_OTHER, {64, 31, 62, 30}}, + {"herblore", "skill_icon_8", GUI_SKILL_PLAYER_OTHER, {64, 61, 62, 30}}, + {"thieving", "skill_icon_9", GUI_SKILL_PLAYER_OTHER, {64, 91, 62, 30}}, + {"crafting", "skill_icon_10", GUI_SKILL_PLAYER_OTHER, {64, 121, 62, 30}}, + {"fletching", "skill_icon_11", GUI_SKILL_PLAYER_OTHER, {64, 151, 62, 30}}, + {"slayer", "skill_icon_19", GUI_SKILL_PLAYER_OTHER, {64, 181, 62, 30}}, + {"hunter", "skill_icon_21", GUI_SKILL_PLAYER_OTHER, {64, 211, 62, 32}}, + {"mining", "skill_icon_12", GUI_SKILL_PLAYER_OTHER, {127, 1, 62, 30}}, + {"smithing", "skill_icon_13", GUI_SKILL_PLAYER_OTHER, {127, 31, 62, 30}}, + {"fishing", "skill_icon_14", GUI_SKILL_PLAYER_OTHER, {127, 61, 62, 30}}, + {"cooking", "skill_icon_15", GUI_SKILL_PLAYER_OTHER, {127, 91, 62, 30}}, + {"firemaking", "skill_icon_16", GUI_SKILL_PLAYER_OTHER, {127, 121, 62, 30}}, + {"woodcutting", "skill_icon_17", GUI_SKILL_PLAYER_OTHER, {127, 151, 62, 30}}, + {"farming", "skill_icon_20", GUI_SKILL_PLAYER_OTHER, {127, 181, 62, 30}}, + {"sailing", "skill_icon_23", GUI_SKILL_PLAYER_OTHER, {127, 211, 62, 32}}, +}; + +static void gui_skill_values(const Player* p, GuiSkillValueKind kind, int* current, int* base) { + switch (kind) { + case GUI_SKILL_PLAYER_ATTACK: + *current = p->current_attack; + *base = p->base_attack; + break; + case GUI_SKILL_PLAYER_STRENGTH: + *current = p->current_strength; + *base = p->base_strength; + break; + case GUI_SKILL_PLAYER_DEFENCE: + *current = p->current_defence; + *base = p->base_defence; + break; + case GUI_SKILL_PLAYER_RANGED: + *current = p->current_ranged; + *base = p->base_ranged; + break; + case GUI_SKILL_PLAYER_PRAYER: + *current = p->current_prayer; + *base = p->base_prayer; + break; + case GUI_SKILL_PLAYER_MAGIC: + *current = p->current_magic; + *base = p->base_magic; + break; + case GUI_SKILL_PLAYER_HITPOINTS: + *current = p->current_hitpoints; + *base = p->base_hitpoints; + break; + case GUI_SKILL_PLAYER_OTHER: + *current = 99; + *base = 99; + break; + } + if (*base <= 0) *base = 1; + if (*current <= 0) *current = *base; +} + +static Color gui_skill_current_color(int current, int base) { + if (current < base) return (Color){220, 45, 31, 255}; + if (current > base) return GUI_TEXT_GREEN; + return GUI_TEXT_YELLOW; +} + +static void gui_draw_skill_panel_slot( + GuiState* gs, + const GuiSkillPanelSlot* slot, + Rectangle rect, + int current, + int base +) { + DrawRectangleRec(rect, (Color){72, 70, 60, 232}); + DrawLineEx((Vector2){rect.x, rect.y}, (Vector2){rect.x + rect.width - 1, rect.y}, + 1, (Color){139, 130, 104, 255}); + DrawLineEx((Vector2){rect.x, rect.y}, (Vector2){rect.x, rect.y + rect.height - 1}, + 1, (Color){139, 130, 104, 255}); + DrawLineEx((Vector2){rect.x, rect.y + rect.height - 1}, + (Vector2){rect.x + rect.width - 1, rect.y + rect.height - 1}, + 1, (Color){28, 25, 21, 255}); + DrawLineEx((Vector2){rect.x + rect.width - 1, rect.y}, + (Vector2){rect.x + rect.width - 1, rect.y + rect.height - 1}, + 1, (Color){28, 25, 21, 255}); + + Rectangle icon_rect = {rect.x + 3, rect.y + 3, 24, 24}; + gui_draw_named_asset_centered(gs, slot->icon_asset, icon_rect, 24, 24, WHITE); + + char current_text[8]; + char base_text[8]; + snprintf(current_text, sizeof(current_text), "%d", current); + snprintf(base_text, sizeof(base_text), "%d", base); + gui_text_shadow(gs, current_text, (int)rect.x + 39, (int)rect.y + 2, 10, + gui_skill_current_color(current, base)); + gui_text_shadow(gs, base_text, (int)rect.x + 39, (int)rect.y + 17, 9, GUI_TEXT_GREEN); +} + +static void gui_draw_stats(GuiState* gs, Player* p) { + int total_level = 0; + int count = (int)(sizeof(GUI_SKILL_PANEL_SLOTS) / sizeof(GUI_SKILL_PANEL_SLOTS[0])); + + for (int i = 0; i < count; i++) { + const GuiSkillPanelSlot* slot = &GUI_SKILL_PANEL_SLOTS[i]; + int current = 0; + int base = 0; + gui_skill_values(p, slot->value_kind, ¤t, &base); + total_level += base; + + Rectangle rect = gui_side_component_rect(gs, "stats", slot->component_name, slot->fallback); + gui_draw_skill_panel_slot(gs, slot, rect, current, base); + } + + Rectangle total = gui_side_component_rect(gs, "stats", "total", (Rectangle){0, 241, 190, 19}); + DrawRectangleRec(total, (Color){7, 7, 7, 238}); + DrawRectangleLinesEx(total, 1, (Color){99, 91, 68, 255}); + const char* total_text = TextFormat("Total level: %d", total_level); + int width = gui_measure_text(gs, total_text, 10); + gui_text_shadow(gs, total_text, (int)(total.x + (total.width - width) * 0.5f), + (int)(total.y + 5), 10, GUI_TEXT_YELLOW); +} + +static void gui_cycle_entity(GuiState* gs) { + if (gs->gui_entity_count <= 0) return; + gs->gui_entity_idx = (gs->gui_entity_idx + 1) % gs->gui_entity_count; +} + +static void gui_draw(GuiState* gs, Player* p) { + int content_y = gs->panel_y + gs->status_bar_h + gs->tab_h; + gui_draw_tab_bar(gs); + + if (gs->gui_entity_count > 1) { + int hx = gs->panel_x + GUI_SIDE_CONTENT_X + 4; + int hy = content_y + 2; + const char* etype = (p->entity_type == ENTITY_NPC) ? "NPC" : "Player"; + gui_text_shadow(gs, TextFormat("[G] %s %d/%d", etype, + gs->gui_entity_idx + 1, gs->gui_entity_count), + hx, hy, 8, GUI_TEXT_ORANGE); + } + + switch (gs->active_tab) { + case GUI_TAB_COMBAT: gui_draw_combat(gs, p); break; + case GUI_TAB_INVENTORY: gui_draw_inventory(gs, p); break; + case GUI_TAB_EQUIPMENT: gui_draw_equipment(gs, p); break; + case GUI_TAB_PRAYER: gui_draw_prayer(gs, p); break; + case GUI_TAB_SPELLBOOK: gui_draw_spellbook(gs, p); break; + case GUI_TAB_STATS: gui_draw_stats(gs, p); break; + case GUI_TAB_QUESTS: break; + default: break; + } +} + +#endif diff --git a/ocean/osrs/osrs_human_input.h b/ocean/osrs/osrs_human_input.h new file mode 100644 index 0000000000..d3b10ea0ec --- /dev/null +++ b/ocean/osrs/osrs_human_input.h @@ -0,0 +1,441 @@ +#ifndef OSRS_HUMAN_INPUT_H +#define OSRS_HUMAN_INPUT_H + +#include "osrs_types.h" +#include "osrs_items.h" +#include "osrs_human_input_types.h" +#include "osrs_encounter.h" + +struct RenderClient; + +static void human_set_click_cross(HumanInput* hi, int screen_x, int screen_y, int is_attack) { + hi->click_screen_x = screen_x; + hi->click_screen_y = screen_y; + hi->click_cross_timer = 0; + hi->click_cross_active = 1; + hi->click_is_attack = is_attack; +} + +static int human_overhead_click_action(const Player* p, OverheadPrayer target, int set_refresh_action) { + return p->prayer == target ? ENCOUNTER_OVERHEAD_OFF : set_refresh_action; +} + +static int human_offensive_click_action(const Player* p, OffensivePrayer target, int set_refresh_action) { + return p->offensive_prayer == target ? ENCOUNTER_OFFENSIVE_OFF : set_refresh_action; +} + +static int human_gui_rect_contains(Rectangle rect, int mouse_x, int mouse_y) { + return mouse_x >= rect.x && mouse_x < rect.x + rect.width && + mouse_y >= rect.y && mouse_y < rect.y + rect.height; +} + +static int human_gui_prayer_idx_at(GuiState* gs, int mouse_x, int mouse_y) { + int cols = GUI_PRAYER_GRID_COLS; + int gap, icon_sz, gx, gy; + gui_prayer_grid_metrics(gs, &gx, &gy, &icon_sz, &gap); + + if (mouse_x < gx || mouse_y < gy) return -1; + int col = (mouse_x - gx) / (icon_sz + gap); + int row = (mouse_y - gy) / (icon_sz + gap); + if (col < 0 || col >= cols) return -1; + + int idx = row * cols + col; + if (idx < 0 || idx >= GUI_PRAYER_GRID_COUNT) return -1; + + int cell_x = gx + col * (icon_sz + gap); + int cell_y = gy + row * (icon_sz + gap); + if (mouse_x >= cell_x + icon_sz || mouse_y >= cell_y + icon_sz) return -1; + + return idx; +} + +static const char* human_gui_prayer_name(GuiPrayerIdx pidx) { + switch (pidx) { + case GUI_PRAY_PROTECT_MAGIC: return "Protect from Magic"; + case GUI_PRAY_PROTECT_MISSILES: return "Protect from Missiles"; + case GUI_PRAY_PROTECT_MELEE: return "Protect from Melee"; + case GUI_PRAY_SMITE: return "Smite"; + case GUI_PRAY_REDEMPTION: return "Redemption"; + case GUI_PRAY_PIETY: return "Piety"; + case GUI_PRAY_RIGOUR: return "Rigour"; + case GUI_PRAY_AUGURY: return "Augury"; + default: return NULL; + } +} + +static int human_apply_prayer_idx(HumanInput* hi, Player* p, GuiPrayerIdx pidx) { + static const struct { + GuiPrayerIdx idx; + OverheadPrayer target; + int refresh_action; + } overhead_rows[] = { + { GUI_PRAY_PROTECT_MAGIC, PRAYER_PROTECT_MAGIC, ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC }, + { GUI_PRAY_PROTECT_MISSILES, PRAYER_PROTECT_RANGED, ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED }, + { GUI_PRAY_PROTECT_MELEE, PRAYER_PROTECT_MELEE, ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE }, + { GUI_PRAY_SMITE, PRAYER_SMITE, ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE }, + { GUI_PRAY_REDEMPTION, PRAYER_REDEMPTION, ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION }, + }; + static const struct { + GuiPrayerIdx idx; + OffensivePrayer target; + int refresh_action; + } offensive_rows[] = { + { GUI_PRAY_PIETY, OFFENSIVE_PRAYER_PIETY, ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY }, + { GUI_PRAY_RIGOUR, OFFENSIVE_PRAYER_RIGOUR, ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR }, + { GUI_PRAY_AUGURY, OFFENSIVE_PRAYER_AUGURY, ENCOUNTER_OFFENSIVE_SET_REFRESH_AUGURY }, + }; + for (size_t i = 0; i < sizeof(overhead_rows) / sizeof(overhead_rows[0]); i++) { + if (overhead_rows[i].idx != pidx) continue; + hi->pending_prayer = human_overhead_click_action( + p, overhead_rows[i].target, overhead_rows[i].refresh_action); + human_input_queue_overhead_prayer(hi, hi->pending_prayer); + return 1; + } + for (size_t i = 0; i < sizeof(offensive_rows) / sizeof(offensive_rows[0]); i++) { + if (offensive_rows[i].idx != pidx) continue; + hi->pending_offensive_prayer = human_offensive_click_action( + p, offensive_rows[i].target, offensive_rows[i].refresh_action); + human_input_queue_offensive_prayer(hi, hi->pending_offensive_prayer); + return 1; + } + return 0; +} + +static int human_gui_spell_idx_at(GuiState* gs, int mouse_x, int mouse_y) { + int cols = GUI_SPELL_GRID_COLS; + int gx, gy; + gui_spell_grid_origin(gs, &gx, &gy); + + if (mouse_x < gx || mouse_y < gy) return -1; + int col = (mouse_x - gx) / GUI_SPELL_PITCH_X; + int row = (mouse_y - gy) / GUI_SPELL_PITCH_Y; + if (col < 0 || col >= cols) return -1; + + int idx = row * cols + col; + if (idx < 0 || idx >= GUI_SPELL_GRID_COUNT) return -1; + + int cell_x = gx + col * GUI_SPELL_PITCH_X; + int cell_y = gy + row * GUI_SPELL_PITCH_Y; + if (mouse_x >= cell_x + GUI_SPELL_ICON_PX || mouse_y >= cell_y + GUI_SPELL_ICON_PX) + return -1; + + return GUI_SPELL_GRID[idx].idx; +} + +static const char* human_gui_spell_name(GuiSpellIdx sidx) { + for (int i = 0; i < GUI_SPELL_GRID_COUNT; i++) { + if (GUI_SPELL_GRID[i].idx == sidx) return GUI_SPELL_GRID[i].name; + } + return NULL; +} + +static int human_select_spell_idx(HumanInput* hi, GuiSpellIdx sidx) { + if (!gui_spell_castable(sidx)) return 0; + + if (gui_spell_is_ice(sidx)) { + human_input_apply_ui_intent( + hi, osrs_ui_intent_select_spell(ATTACK_ICE, (int)sidx)); + return 1; + } + + if (gui_spell_is_blood(sidx)) { + human_input_apply_ui_intent( + hi, osrs_ui_intent_select_spell(ATTACK_BLOOD, (int)sidx)); + return 1; + } + + return 0; +} + +static int human_apply_selected_target_to_widget(HumanInput* hi, uint32_t widget_component_id) { + if (hi->cursor_mode == CURSOR_ITEM_TARGET) { + OsrsUiIntent intent = osrs_ui_intent_item_on_widget( + hi->selected_item_inventory_slot, + hi->selected_item_db_idx, + hi->selected_item_osrs_id, + widget_component_id); + human_input_apply_ui_intent(hi, intent); + return 1; + } + + if (hi->cursor_mode == CURSOR_SPELL_TARGET) { + human_input_apply_ui_intent(hi, osrs_ui_intent_spell_on_widget( + hi->selected_spell, + hi->selected_spell_gui_idx, + widget_component_id)); + return 1; + } + + return 0; +} + +static int human_gui_combat_style_index_at(GuiState* gs, Player* p, int mouse_x, int mouse_y) { + GuiCombatStyleOptions styles = gui_combat_style_options(p->equipped[GEAR_SLOT_WEAPON]); + for (int i = 0; i < styles.count; i++) { + if (human_gui_rect_contains(gui_combat_style_rect(gs, i), mouse_x, mouse_y)) { + return i; + } + } + return -1; +} + +static int human_apply_combat_style( + HumanInput* hi, + GuiState* gs, + Player* p, + FightStyle fight_style +) { + if (hi->enabled) + human_input_queue_fight_style(hi, fight_style); + else + p->fight_style = fight_style; + + if (fight_style == FIGHT_STYLE_AUTOCAST || + fight_style == FIGHT_STYLE_DEFENSIVE_AUTOCAST) { + int defensive = fight_style == FIGHT_STYLE_DEFENSIVE_AUTOCAST; + if (hi->enabled) { + human_input_queue_set_autocast(hi, gui_autocast_spell(p), defensive); + } else { + p->autocast_enabled = 1; + p->autocast_defensive = defensive; + } + } + + (void)gs; + return 1; +} + +static int human_gui_autocast_button_hit(GuiState* gs, Player* p, int mouse_x, int mouse_y) { + if (!item_supports_ancient_autocast(p->equipped[GEAR_SLOT_WEAPON])) return 0; + return human_gui_rect_contains(gui_side_ref_rect(gs, gui_combat_autocast_rect()), + mouse_x, mouse_y); +} + +static int human_gui_autocast_spell_at(GuiState* gs, Player* p, int mouse_x, int mouse_y) { + if (!item_supports_ancient_autocast(p->equipped[GEAR_SLOT_WEAPON])) return -1; + int spells[2] = { ENCOUNTER_SPELL_BLOOD, ENCOUNTER_SPELL_ICE }; + for (int i = 0; i < 2; i++) { + if (human_gui_rect_contains(gui_side_ref_rect(gs, gui_combat_autocast_spell_rect(i)), + mouse_x, mouse_y)) { + return spells[i]; + } + } + return -1; +} + +static int human_apply_autocast_spell( + HumanInput* hi, + GuiState* gs, + Player* p, + int spell, + int defensive +) { + if (hi->enabled) { + human_input_queue_set_autocast(hi, spell, defensive); + } else { + p->autocast_enabled = 1; + p->autocast_defensive = defensive; + p->autocast_spell = spell; + } + gs->autocast_selector_open = 0; + return 1; +} + +static int human_gui_spec_hit(GuiState* gs, int mouse_x, int mouse_y) { + return human_gui_rect_contains(gui_side_ref_rect(gs, gui_combat_special_rect()), + mouse_x, mouse_y); +} + +static void human_apply_spec_toggle(HumanInput* hi) { + hi->pending_spec = 1; + human_input_queue_spec_toggle(hi); +} + +static void human_handle_prayer_click(HumanInput* hi, GuiState* gs, Player* p, + int mouse_x, int mouse_y) { + int idx = human_gui_prayer_idx_at(gs, mouse_x, mouse_y); + if (idx >= 0) { + if (hi->cursor_mode != CURSOR_NORMAL) { + human_apply_selected_target_to_widget(hi, + osrs_ui_intent_widget_component_id(OSRS_UI_GROUP_PRAYERBOOK, idx)); + return; + } + human_apply_prayer_idx(hi, p, (GuiPrayerIdx)idx); + } +} + +static void human_handle_spell_click(HumanInput* hi, GuiState* gs, + int mouse_x, int mouse_y) { + int idx = human_gui_spell_idx_at(gs, mouse_x, mouse_y); + if (idx >= 0) { + if (hi->cursor_mode != CURSOR_NORMAL) { + human_apply_selected_target_to_widget(hi, + osrs_ui_intent_widget_component_id(OSRS_UI_GROUP_MAGIC_SPELLBOOK, idx)); + return; + } + human_select_spell_idx(hi, (GuiSpellIdx)idx); + } +} + +static void human_handle_combat_click(HumanInput* hi, GuiState* gs, Player* p, + int mouse_x, int mouse_y) { + GuiCombatStyleOptions styles = gui_combat_style_options(p->equipped[GEAR_SLOT_WEAPON]); + + int style_idx = human_gui_combat_style_index_at(gs, p, mouse_x, mouse_y); + if (style_idx >= 0) { + if (hi->cursor_mode != CURSOR_NORMAL) { + human_apply_selected_target_to_widget(hi, + osrs_ui_intent_widget_component_id(OSRS_UI_GROUP_COMBAT_INTERFACE, style_idx)); + return; + } + human_apply_combat_style(hi, gs, p, styles.values[style_idx]); + return; + } + + if (item_supports_ancient_autocast(p->equipped[GEAR_SLOT_WEAPON])) { + if (human_gui_autocast_button_hit(gs, p, mouse_x, mouse_y)) { + if (hi->cursor_mode != CURSOR_NORMAL) { + human_apply_selected_target_to_widget(hi, + osrs_ui_intent_widget_component_id(OSRS_UI_GROUP_COMBAT_INTERFACE, 100)); + return; + } + gs->autocast_selector_open = !gs->autocast_selector_open; + return; + } + + if (gs->autocast_selector_open) { + int spell = human_gui_autocast_spell_at(gs, p, mouse_x, mouse_y); + if (spell >= 0) { + if (hi->cursor_mode != CURSOR_NORMAL) { + human_apply_selected_target_to_widget(hi, + osrs_ui_intent_widget_component_id( + OSRS_UI_GROUP_COMBAT_INTERFACE, 110 + spell)); + return; + } + int defensive = p->fight_style == FIGHT_STYLE_DEFENSIVE_AUTOCAST || + p->autocast_defensive; + human_apply_autocast_spell(hi, gs, p, spell, defensive); + return; + } + } + } + + if (human_gui_spec_hit(gs, mouse_x, mouse_y)) { + if (hi->cursor_mode != CURSOR_NORMAL) { + human_apply_selected_target_to_widget(hi, + osrs_ui_intent_widget_component_id(OSRS_UI_GROUP_COMBAT_INTERFACE, 120)); + return; + } + human_apply_spec_toggle(hi); + } +} + +static void human_to_pvp_actions(HumanInput* hi, int* actions, + Player* agent, Player* target) { + for (int h = 0; h < NUM_ACTION_HEADS; h++) actions[h] = 0; + + actions[HEAD_LOADOUT] = LOADOUT_KEEP; + + if (hi->pending_attack) { + if (hi->pending_spell == ATTACK_ICE) { + actions[HEAD_COMBAT] = ATTACK_ICE; + } else if (hi->pending_spell == ATTACK_BLOOD) { + actions[HEAD_COMBAT] = ATTACK_BLOOD; + } else { + actions[HEAD_COMBAT] = ATTACK_ATK; + } + } else if (hi->pending_move_x >= 0 && hi->pending_move_y >= 0) { + int dx = hi->pending_move_x - target->x; + int dy = hi->pending_move_y - target->y; + int dist = (abs(dx) > abs(dy)) ? abs(dx) : abs(dy); + + if (dist == 0) { + actions[HEAD_COMBAT] = MOVE_UNDER; + } else if (dist == 1) { + if (dx == 0 || dy == 0) { + actions[HEAD_COMBAT] = MOVE_ADJACENT; + } else { + actions[HEAD_COMBAT] = MOVE_DIAGONAL; + } + } else { + int fc = dist; + if (fc < 2) fc = 2; + if (fc > 7) fc = 7; + actions[HEAD_COMBAT] = MOVE_FARCAST_2 + (fc - 2); + } + } + + if (hi->pending_prayer >= 0) { + actions[HEAD_OVERHEAD] = hi->pending_prayer; + } + + if (hi->pending_food) { + actions[HEAD_FOOD] = FOOD_EAT; + } + + if (hi->pending_potion > 0) { + actions[HEAD_POTION] = hi->pending_potion; + } + + if (hi->pending_karambwan) { + actions[HEAD_KARAMBWAN] = KARAM_EAT; + } + + if (hi->pending_veng) { + actions[HEAD_VENG] = VENG_CAST; + } + + if (hi->pending_spec) { + AttackStyle style = (AttackStyle)get_item_attack_style(agent->equipped[GEAR_SLOT_WEAPON]); + switch (style) { + case ATTACK_STYLE_MELEE: actions[HEAD_LOADOUT] = LOADOUT_SPEC_MELEE; break; + case ATTACK_STYLE_RANGED: actions[HEAD_LOADOUT] = LOADOUT_SPEC_RANGE; break; + case ATTACK_STYLE_MAGIC: actions[HEAD_LOADOUT] = LOADOUT_SPEC_MAGIC; break; + default: break; + } + } + + (void)agent; +} + +#define CLICK_CROSS_NUM_FRAMES 4 +#define CLICK_CROSS_ANIM_TICKS 20 + +static void human_draw_click_cross(HumanInput* hi, Texture2D* cross_sprites, int sprites_loaded) { + if (!hi->click_cross_active) return; + if (hi->click_cross_timer >= CLICK_CROSS_ANIM_TICKS) { + hi->click_cross_active = 0; + return; + } + + int frame = hi->click_cross_timer * CLICK_CROSS_NUM_FRAMES / CLICK_CROSS_ANIM_TICKS; + if (frame >= CLICK_CROSS_NUM_FRAMES) frame = CLICK_CROSS_NUM_FRAMES - 1; + int sprite_idx = hi->click_is_attack ? frame + CLICK_CROSS_NUM_FRAMES : frame; + + int cx = hi->click_screen_x; + int cy = hi->click_screen_y; + + if (sprites_loaded && cross_sprites[sprite_idx].id > 0) { + Texture2D tex = cross_sprites[sprite_idx]; + DrawTexture(tex, cx - tex.width / 2, cy - tex.height / 2, WHITE); + } else { + float progress = 1.0f - (float)hi->click_cross_timer / CLICK_CROSS_ANIM_TICKS; + int alpha = (int)(progress * 255); + Color c = hi->click_is_attack + ? CLITERAL(Color){ 255, 50, 50, (unsigned char)alpha } + : CLITERAL(Color){ 255, 255, 0, (unsigned char)alpha }; + DrawLine(cx - 6, cy - 6, cx + 6, cy + 6, c); + DrawLine(cx + 6, cy - 6, cx - 6, cy + 6, c); + } +} + +static void human_tick_visuals(HumanInput* hi) { + if (hi->click_cross_active) { + hi->click_cross_timer++; + if (hi->click_cross_timer >= CLICK_CROSS_ANIM_TICKS) { + hi->click_cross_active = 0; + } + } +} + +#endif diff --git a/ocean/osrs/osrs_human_input_types.h b/ocean/osrs/osrs_human_input_types.h new file mode 100644 index 0000000000..f4d696d2a8 --- /dev/null +++ b/ocean/osrs/osrs_human_input_types.h @@ -0,0 +1,390 @@ +#ifndef OSRS_HUMAN_INPUT_TYPES_H +#define OSRS_HUMAN_INPUT_TYPES_H + +#include +#include +#include + +#include "osrs_ui_intent.h" + +typedef enum { + CURSOR_NORMAL = 0, + CURSOR_ITEM_TARGET, + CURSOR_SPELL_TARGET, +} CursorMode; + +typedef enum { + HUMAN_COMMAND_NONE = 0, + HUMAN_COMMAND_WALK, + HUMAN_COMMAND_ATTACK_NPC, + HUMAN_COMMAND_OVERHEAD_PRAYER, + HUMAN_COMMAND_OFFENSIVE_PRAYER, + HUMAN_COMMAND_EAT, + HUMAN_COMMAND_DRINK, + HUMAN_COMMAND_SPELL_TARGET, + HUMAN_COMMAND_SPEC_TOGGLE, + HUMAN_COMMAND_EQUIP_INVENTORY_ITEM, + HUMAN_COMMAND_FIGHT_STYLE, + HUMAN_COMMAND_SET_AUTOCAST, + HUMAN_COMMAND_ITEM_ON_ITEM, + HUMAN_COMMAND_ITEM_ON_WIDGET, + HUMAN_COMMAND_SPELL_ON_WIDGET, + HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK, +} HumanCommandKind; + +typedef struct { + HumanCommandKind kind; + int world_x, world_y; + int npc_slot; + int overhead_prayer; + int offensive_prayer; + int food; + int potion; + int spell; + int spell_gui_idx; + int inventory_slot; + int target_inventory_slot; + int item_db_idx; + int item_osrs_id; + int gear_slot; + int fight_style; + int autocast_spell; + int autocast_defensive; + uint32_t widget_component_id; +} HumanCommand; + +typedef struct { + HumanCommand* items; + int count; + int capacity; +} HumanCommandQueue; + +typedef struct HumanInput { + int enabled; + + HumanCommandQueue commands; + + int pending_move_x, pending_move_y; + int pending_attack; + int pending_prayer; + int pending_offensive_prayer; + int pending_food; + int pending_karambwan; + int pending_potion; + int pending_veng; + int pending_spec; + int pending_spell; + int pending_target_idx; + int pending_gear; + int pending_modifier_select; + int pending_grapple_slot; + + CursorMode cursor_mode; + int selected_item_inventory_slot; + int selected_item_db_idx; + int selected_item_osrs_id; + int selected_spell; + int selected_spell_gui_idx; + + int click_screen_x, click_screen_y; + int click_cross_timer; + int click_cross_active; + int click_is_attack; +} HumanInput; + +static inline void human_command_queue_reserve(HumanCommandQueue* q, int min_capacity) { + if (q->capacity >= min_capacity) return; + int new_capacity = q->capacity > 0 ? q->capacity : 8; + while (new_capacity < min_capacity) + new_capacity *= 2; + HumanCommand* next = (HumanCommand*)realloc(q->items, (size_t)new_capacity * sizeof(HumanCommand)); + if (!next) { + fprintf(stderr, "human command queue: out of memory\n"); + abort(); + } + q->items = next; + q->capacity = new_capacity; +} + +static inline void human_input_queue_command(HumanInput* hi, HumanCommand cmd) { + human_command_queue_reserve(&hi->commands, hi->commands.count + 1); + hi->commands.items[hi->commands.count++] = cmd; +} + +static inline void human_input_clear_commands(HumanInput* hi) { + hi->commands.count = 0; +} + +static inline void human_input_destroy(HumanInput* hi) { + free(hi->commands.items); + hi->commands.items = NULL; + hi->commands.count = 0; + hi->commands.capacity = 0; +} + +static inline void human_input_init(HumanInput* hi) { + memset(hi, 0, sizeof(*hi)); + hi->pending_move_x = -1; + hi->pending_move_y = -1; + hi->pending_prayer = -1; + hi->pending_offensive_prayer = -1; + hi->pending_target_idx = -1; + hi->selected_item_inventory_slot = -1; + hi->selected_item_db_idx = -1; + hi->selected_spell_gui_idx = -1; + hi->click_cross_active = 0; + human_command_queue_reserve(&hi->commands, 8); +} + +static inline void human_input_clear_pending(HumanInput* hi) { + hi->pending_attack = 0; + hi->pending_prayer = -1; + hi->pending_offensive_prayer = -1; + hi->pending_food = 0; + hi->pending_karambwan = 0; + hi->pending_potion = 0; + hi->pending_veng = 0; + hi->pending_spec = 0; + hi->pending_spell = 0; + hi->pending_target_idx = -1; + hi->pending_gear = 0; + hi->pending_modifier_select = 0; + hi->pending_grapple_slot = 0; + human_input_clear_commands(hi); +} + +static inline void human_input_clear_move(HumanInput* hi) { + hi->pending_move_x = -1; + hi->pending_move_y = -1; +} + +static inline void human_input_clear_selected_ui_target(HumanInput* hi) { + hi->cursor_mode = CURSOR_NORMAL; + hi->selected_item_inventory_slot = -1; + hi->selected_item_db_idx = -1; + hi->selected_item_osrs_id = 0; + hi->selected_spell = 0; + hi->selected_spell_gui_idx = -1; +} + +static inline void human_input_queue_walk(HumanInput* hi, int world_x, int world_y) { + hi->pending_move_x = world_x; + hi->pending_move_y = world_y; + hi->pending_attack = 0; + hi->pending_target_idx = -1; + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_WALK, + .world_x = world_x, + .world_y = world_y, + }); +} + +static inline void human_input_queue_attack_npc(HumanInput* hi, int npc_slot) { + human_input_clear_move(hi); + hi->pending_attack = 1; + hi->pending_target_idx = npc_slot; + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_ATTACK_NPC, + .npc_slot = npc_slot, + }); +} + +static inline void human_input_queue_overhead_prayer(HumanInput* hi, int overhead_prayer) { + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_OVERHEAD_PRAYER, + .overhead_prayer = overhead_prayer, + }); +} + +static inline void human_input_queue_offensive_prayer(HumanInput* hi, int offensive_prayer) { + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_OFFENSIVE_PRAYER, + .offensive_prayer = offensive_prayer, + }); +} + +static inline void human_input_queue_eat(HumanInput* hi, int food, int inventory_slot) { + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_EAT, + .food = food, + .inventory_slot = inventory_slot, + }); +} + +static inline void human_input_queue_drink(HumanInput* hi, int potion, int inventory_slot) { + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_DRINK, + .potion = potion, + .inventory_slot = inventory_slot, + }); +} + +static inline void human_input_queue_spell_target(HumanInput* hi, int spell, int npc_slot) { + human_input_clear_move(hi); + hi->pending_attack = 1; + hi->pending_spell = spell; + hi->pending_target_idx = npc_slot; + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_SPELL_TARGET, + .npc_slot = npc_slot, + .spell = spell, + }); +} + +static inline void human_input_queue_item_on_item( + HumanInput* hi, + int source_inventory_slot, + int target_inventory_slot, + int item_db_idx, + int item_osrs_id +) { + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_ITEM_ON_ITEM, + .inventory_slot = source_inventory_slot, + .target_inventory_slot = target_inventory_slot, + .item_db_idx = item_db_idx, + .item_osrs_id = item_osrs_id, + }); +} + +static inline void human_input_queue_item_on_widget( + HumanInput* hi, + int source_inventory_slot, + int item_db_idx, + int item_osrs_id, + uint32_t widget_component_id +) { + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_ITEM_ON_WIDGET, + .inventory_slot = source_inventory_slot, + .item_db_idx = item_db_idx, + .item_osrs_id = item_osrs_id, + .widget_component_id = widget_component_id, + }); +} + +static inline void human_input_queue_spell_on_widget( + HumanInput* hi, + int spell, + int spell_gui_idx, + uint32_t widget_component_id +) { + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_SPELL_ON_WIDGET, + .spell = spell, + .spell_gui_idx = spell_gui_idx, + .widget_component_id = widget_component_id, + }); +} + +static inline void human_input_queue_spec_toggle(HumanInput* hi) { + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_SPEC_TOGGLE, + }); +} + +static inline void human_input_queue_equip_inventory_item( + HumanInput* hi, int inventory_slot, int item_db_idx, int gear_slot +) { + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_EQUIP_INVENTORY_ITEM, + .inventory_slot = inventory_slot, + .item_db_idx = item_db_idx, + .gear_slot = gear_slot, + }); +} + +static inline void human_input_queue_inventory_primary_click( + HumanInput* hi, + int inventory_slot +) { + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_INVENTORY_PRIMARY_CLICK, + .inventory_slot = inventory_slot, + }); +} + +static inline void human_input_queue_fight_style(HumanInput* hi, int fight_style) { + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_FIGHT_STYLE, + .fight_style = fight_style, + }); +} + +static inline void human_input_queue_set_autocast( + HumanInput* hi, + int autocast_spell, + int autocast_defensive +) { + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_SET_AUTOCAST, + .autocast_spell = autocast_spell, + .autocast_defensive = autocast_defensive, + }); +} + +static inline void human_input_apply_ui_intent(HumanInput* hi, OsrsUiIntent intent) { + switch (intent.kind) { + case OSRS_UI_INTENT_SELECT_ITEM: + human_input_clear_selected_ui_target(hi); + hi->cursor_mode = CURSOR_ITEM_TARGET; + hi->selected_item_inventory_slot = intent.source_inventory_slot; + hi->selected_item_db_idx = intent.item_db_idx; + hi->selected_item_osrs_id = intent.item_osrs_id; + break; + case OSRS_UI_INTENT_SELECT_SPELL: + human_input_clear_selected_ui_target(hi); + hi->cursor_mode = CURSOR_SPELL_TARGET; + hi->selected_spell = intent.spell; + hi->selected_spell_gui_idx = intent.spell_gui_idx; + break; + case OSRS_UI_INTENT_ITEM_ON_ITEM: + human_input_queue_item_on_item( + hi, + intent.source_inventory_slot, + intent.target_inventory_slot, + intent.item_db_idx, + intent.item_osrs_id); + human_input_clear_selected_ui_target(hi); + break; + case OSRS_UI_INTENT_ITEM_ON_WIDGET: + human_input_queue_item_on_widget( + hi, + intent.source_inventory_slot, + intent.item_db_idx, + intent.item_osrs_id, + intent.widget_component_id); + human_input_clear_selected_ui_target(hi); + break; + case OSRS_UI_INTENT_SPELL_ON_TARGET: + hi->pending_attack = 1; + hi->pending_target_idx = intent.npc_slot; + hi->pending_spell = intent.spell; + hi->pending_move_x = -1; + hi->pending_move_y = -1; + human_input_queue_command(hi, (HumanCommand){ + .kind = HUMAN_COMMAND_SPELL_TARGET, + .npc_slot = intent.npc_slot, + .spell = intent.spell, + .spell_gui_idx = intent.spell_gui_idx, + }); + human_input_clear_selected_ui_target(hi); + break; + case OSRS_UI_INTENT_SPELL_ON_WIDGET: + human_input_queue_spell_on_widget( + hi, + intent.spell, + intent.spell_gui_idx, + intent.widget_component_id); + human_input_clear_selected_ui_target(hi); + break; + case OSRS_UI_INTENT_NONE: + break; + default: + fprintf(stderr, "human_input_apply_ui_intent: bad intent kind: %d\n", + (int)intent.kind); + abort(); + } +} + +#endif diff --git a/ocean/osrs/osrs_interaction.h b/ocean/osrs/osrs_interaction.h new file mode 100644 index 0000000000..7c0799dd47 --- /dev/null +++ b/ocean/osrs/osrs_interaction.h @@ -0,0 +1,58 @@ +#ifndef OSRS_INTERACTION_H +#define OSRS_INTERACTION_H + +typedef struct { + int target_slot; +} OsrsInteraction; + +static inline void osrs_interaction_set(OsrsInteraction* ix, int target_slot) { + ix->target_slot = target_slot; +} + +static inline void osrs_interaction_clear(OsrsInteraction* ix) { + ix->target_slot = -1; +} + +static inline int osrs_interaction_active(const OsrsInteraction* ix) { + return ix->target_slot >= 0; +} + +static inline void osrs_interaction_init(OsrsInteraction* ix) { + ix->target_slot = -1; +} + +#define OSRS_IACT_NONE 0 +#define OSRS_IACT_MOVE 1 +#define OSRS_IACT_EAT 2 +#define OSRS_IACT_DRINK 3 +#define OSRS_IACT_EQUIP 4 +#define OSRS_IACT_PRAYER 5 +#define OSRS_IACT_SPEC 6 +#define OSRS_IACT_ATTACK 7 + +static inline int osrs_interaction_check_interrupt(OsrsInteraction* ix, int action_type) { + switch (action_type) { + case OSRS_IACT_MOVE: + case OSRS_IACT_EAT: + case OSRS_IACT_DRINK: + case OSRS_IACT_EQUIP: + osrs_interaction_clear(ix); + return 1; + case OSRS_IACT_NONE: + case OSRS_IACT_PRAYER: + case OSRS_IACT_SPEC: + case OSRS_IACT_ATTACK: + default: + return 0; + } +} + +static inline void osrs_spec_toggle(int* spec_armed) { + *spec_armed = !(*spec_armed); +} + +static inline void osrs_spec_disarm(int* spec_armed) { + *spec_armed = 0; +} + +#endif diff --git a/ocean/osrs/osrs_inventory.h b/ocean/osrs/osrs_inventory.h new file mode 100644 index 0000000000..9ee4cd4eb1 --- /dev/null +++ b/ocean/osrs/osrs_inventory.h @@ -0,0 +1,95 @@ +#ifndef OSRS_INVENTORY_H +#define OSRS_INVENTORY_H + +#include +#include +#include + +#include "osrs_types.h" +#include "osrs_items.h" + +#define OSRS_INVENTORY_SIZE 28 + +typedef struct { + uint8_t item_idx; + uint16_t raw_osrs_id; + uint8_t dose; +} OsrsInventoryCell; + +typedef struct { + OsrsInventoryCell cells[OSRS_INVENTORY_SIZE]; +} OsrsInventorySlotSnapshot; + +static inline int osrs_inventory_slot_valid(int slot) { + return slot >= 0 && slot < OSRS_INVENTORY_SIZE; +} + +static inline OsrsInventoryCell osrs_inventory_cell_empty(void) { + return (OsrsInventoryCell){ + .item_idx = ITEM_NONE, + .raw_osrs_id = 0, + .dose = 0, + }; +} + +static inline int osrs_inventory_cell_is_empty(const OsrsInventoryCell* cell) { + return cell->item_idx == ITEM_NONE && cell->raw_osrs_id == 0; +} + +static inline OsrsInventoryCell osrs_inventory_cell_from_item(uint8_t item_idx) { + if (item_idx == ITEM_NONE) return osrs_inventory_cell_empty(); + if (item_idx >= NUM_ITEMS) { + fprintf(stderr, "inventory cell: invalid item index %u\n", item_idx); + abort(); + } + return (OsrsInventoryCell){ + .item_idx = item_idx, + .raw_osrs_id = ITEM_DATABASE[item_idx].item_id, + .dose = 0, + }; +} + +static inline void osrs_inventory_swap_cells( + OsrsInventoryCell cells[OSRS_INVENTORY_SIZE], + int source_slot, + int target_slot +) { + if (!osrs_inventory_slot_valid(source_slot) || + !osrs_inventory_slot_valid(target_slot)) { + fprintf(stderr, "inventory swap: invalid slots %d -> %d\n", + source_slot, target_slot); + abort(); + } + if (source_slot == target_slot) return; + OsrsInventoryCell tmp = cells[target_slot]; + cells[target_slot] = cells[source_slot]; + cells[source_slot] = tmp; +} + +static inline OsrsInventorySlotSnapshot osrs_inventory_slot_snapshot( + const OsrsInventoryCell cells[OSRS_INVENTORY_SIZE] +) { + OsrsInventorySlotSnapshot snapshot; + memcpy(snapshot.cells, cells, sizeof(snapshot.cells)); + return snapshot; +} + +static inline int osrs_item_gear_slot(uint8_t item_idx) { + if (item_idx >= NUM_ITEMS) return -1; + switch (ITEM_DATABASE[item_idx].slot) { + case SLOT_HEAD: return GEAR_SLOT_HEAD; + case SLOT_CAPE: return GEAR_SLOT_CAPE; + case SLOT_NECK: return GEAR_SLOT_NECK; + case SLOT_WEAPON: return GEAR_SLOT_WEAPON; + case SLOT_BODY: return GEAR_SLOT_BODY; + case SLOT_SHIELD: return GEAR_SLOT_SHIELD; + case SLOT_LEGS: return GEAR_SLOT_LEGS; + case SLOT_HANDS: return GEAR_SLOT_HANDS; + case SLOT_FEET: return GEAR_SLOT_FEET; + case SLOT_RING: return GEAR_SLOT_RING; + case SLOT_AMMO: return GEAR_SLOT_AMMO; + default: return -1; + } +} + +#endif diff --git a/ocean/osrs/osrs_inventory_clicks.h b/ocean/osrs/osrs_inventory_clicks.h new file mode 100644 index 0000000000..0fc81d9744 --- /dev/null +++ b/ocean/osrs/osrs_inventory_clicks.h @@ -0,0 +1,554 @@ +#ifndef OSRS_INVENTORY_CLICKS_H +#define OSRS_INVENTORY_CLICKS_H + +#include "osrs_inventory.h" +#include "osrs_items.h" +#include "osrs_consumables.h" + +typedef enum { + OSRS_CLICK_NONE = 0, + OSRS_CLICK_EQUIP = 1, + OSRS_CLICK_EAT = 2, + OSRS_CLICK_DRINK = 3, +} OsrsClickAction; + +typedef enum { + OSRS_CONSUMABLE_NONE = 0, + OSRS_CONSUMABLE_BREW = 1, + OSRS_CONSUMABLE_SUPER_RESTORE = 2, + OSRS_CONSUMABLE_SANFEW = 3, + OSRS_CONSUMABLE_SUPER_COMBAT = 4, + OSRS_CONSUMABLE_DIVINE_COMBAT = 5, + OSRS_CONSUMABLE_RANGING = 6, + OSRS_CONSUMABLE_DIVINE_RANGING = 7, + OSRS_CONSUMABLE_SURGE = 8, + OSRS_CONSUMABLE_GUTHIX_REST = 9, + OSRS_CONSUMABLE_SATURATED_HEART = 10, + OSRS_CONSUMABLE_ANTIVENOM_PLUS = 11, + OSRS_CONSUMABLE_SHARK_FOOD = 12, + OSRS_CONSUMABLE_KARAMBWAN = 13, +} OsrsConsumableKind; + +typedef enum { + OSRS_CLICK_TICK_FIRST = 0, + OSRS_CLICK_TICK_DUPLICATE = 1, +} OsrsClickTickMultiplicity; + +typedef struct { + uint16_t raw_osrs_id; + OsrsClickAction click_action; + OsrsConsumableKind consumable_kind; + uint8_t dose_count; +} OsrsConsumableClick; + +typedef struct { + OsrsClickAction click_action; + OsrsConsumableKind consumable_kind; + uint8_t dose_count; + uint16_t raw_osrs_id_after_drink; +} OsrsInventoryClickResolution; + +typedef struct { + int consumed; + OsrsConsumableKind consumable_kind; + uint8_t dose_count_before; + uint8_t dose_count_after; + uint16_t raw_osrs_id_before; + uint16_t raw_osrs_id_after_drink; +} OsrsInventoryDrinkConsumeResult; + +typedef void (*OsrsInventoryDrinkOneDoseEffectFn)( + void* ctx, + OsrsConsumableKind kind +); + +#define OSRS_INVENTORY_CELL_OBS_FEATURES 28 +#define OSRS_EQUIPPED_SELF_OBS_FEATURES 18 + +static const OsrsConsumableClick OSRS_CONSUMABLE_CLICK_REGISTRY[] = { + {6685, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BREW, 4}, + {6687, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BREW, 3}, + {6689, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BREW, 2}, + {6691, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_BREW, 1}, + {3024, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_RESTORE, 4}, + {3026, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_RESTORE, 3}, + {3028, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_RESTORE, 2}, + {3030, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_RESTORE, 1}, + {10925, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SANFEW, 4}, + {10927, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SANFEW, 3}, + {10929, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SANFEW, 2}, + {10931, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SANFEW, 1}, + {12695, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_COMBAT, 4}, + {12697, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_COMBAT, 3}, + {12699, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_COMBAT, 2}, + {12701, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SUPER_COMBAT, 1}, + {23685, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_COMBAT, 4}, + {23688, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_COMBAT, 3}, + {23691, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_COMBAT, 2}, + {23694, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_COMBAT, 1}, + {2444, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_RANGING, 4}, + {169, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_RANGING, 3}, + {171, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_RANGING, 2}, + {173, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_RANGING, 1}, + {23733, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_RANGING, 4}, + {23736, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_RANGING, 3}, + {23739, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_RANGING, 2}, + {23742, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_DIVINE_RANGING, 1}, + {30875, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SURGE, 4}, + {30878, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SURGE, 3}, + {30881, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SURGE, 2}, + {30884, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SURGE, 1}, + {4417, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_GUTHIX_REST, 4}, + {4419, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_GUTHIX_REST, 3}, + {4421, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_GUTHIX_REST, 2}, + {4423, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_GUTHIX_REST, 1}, + {27641, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_SATURATED_HEART, 1}, + {12913, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 4}, + {12915, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 3}, + {12917, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 2}, + {12919, OSRS_CLICK_DRINK, OSRS_CONSUMABLE_ANTIVENOM_PLUS, 1}, + {385, OSRS_CLICK_EAT, OSRS_CONSUMABLE_SHARK_FOOD, 0}, + {3144, OSRS_CLICK_EAT, OSRS_CONSUMABLE_KARAMBWAN, 0}, +}; + +static inline OsrsConsumableClick osrs_consumable_click_lookup_raw_osrs_id( + uint16_t raw_osrs_id +); + +static inline void osrs_inventory_clicks_trap(void) { +#if defined(__clang__) || defined(__GNUC__) + __builtin_trap(); +#else + *(volatile int*)0 = 0; +#endif +} + +static inline float osrs_clamp_unit(float v) { + if (v < -1.0f) return -1.0f; + if (v > 1.0f) return 1.0f; + return v; +} + +typedef enum { + COL_CKIND6_NONE = 0, + COL_CKIND6_BREW, + COL_CKIND6_RESTORE, + COL_CKIND6_COMBAT_BOOST, + COL_CKIND6_RANGED_BOOST, + COL_CKIND6_SPECIAL, + COL_CKIND6_FOOD, +} OsrsConsumableKind6; + +static inline OsrsConsumableKind6 col_consumable_kind6(OsrsConsumableKind k) { + switch (k) { + case OSRS_CONSUMABLE_BREW: return COL_CKIND6_BREW; + case OSRS_CONSUMABLE_SUPER_RESTORE: + case OSRS_CONSUMABLE_SANFEW: return COL_CKIND6_RESTORE; + case OSRS_CONSUMABLE_SUPER_COMBAT: + case OSRS_CONSUMABLE_DIVINE_COMBAT: return COL_CKIND6_COMBAT_BOOST; + case OSRS_CONSUMABLE_RANGING: + case OSRS_CONSUMABLE_DIVINE_RANGING: return COL_CKIND6_RANGED_BOOST; + case OSRS_CONSUMABLE_SURGE: + case OSRS_CONSUMABLE_GUTHIX_REST: + case OSRS_CONSUMABLE_SATURATED_HEART: + case OSRS_CONSUMABLE_ANTIVENOM_PLUS: return COL_CKIND6_SPECIAL; + case OSRS_CONSUMABLE_SHARK_FOOD: + case OSRS_CONSUMABLE_KARAMBWAN: return COL_CKIND6_FOOD; + case OSRS_CONSUMABLE_NONE: return COL_CKIND6_NONE; + } + osrs_inventory_clicks_trap(); + return COL_CKIND6_NONE; +} + +static inline int osrs_consumable_hp_heal_amount(OsrsConsumableKind k, int base_hp) { + switch (k) { + case OSRS_CONSUMABLE_BREW: return osrs_brew_heal_amount(base_hp); + case OSRS_CONSUMABLE_SHARK_FOOD: return osrs_food_heal_amount(FOOD_SHARK); + case OSRS_CONSUMABLE_KARAMBWAN: return osrs_food_heal_amount(FOOD_KARAMBWAN); + default: return 0; + } +} + +static inline int osrs_consumable_prayer_restore_amount(OsrsConsumableKind k, int base_prayer) { + switch (k) { + case OSRS_CONSUMABLE_SUPER_RESTORE: return osrs_super_restore_amount(base_prayer); + case OSRS_CONSUMABLE_SANFEW: return osrs_sanfew_restore_amount(base_prayer); + default: return 0; + } +} + +static inline int osrs_consumable_offensive_boost_amount(OsrsConsumableKind k, int base_level) { + switch (k) { + case OSRS_CONSUMABLE_SUPER_COMBAT: + case OSRS_CONSUMABLE_DIVINE_COMBAT: return osrs_super_combat_boost_amount(base_level); + case OSRS_CONSUMABLE_RANGING: + case OSRS_CONSUMABLE_DIVINE_RANGING: return osrs_ranging_boost_amount(base_level); + case OSRS_CONSUMABLE_SATURATED_HEART: return osrs_saturated_heart_magic_boost(base_level); + default: return 0; + } +} + +static inline void osrs_item_effect_class4(uint32_t effect_mask, float out[4]) { + uint32_t lifesteal = OSRS_ITEM_EFFECT_BLOOD_FURY | OSRS_ITEM_EFFECT_SANG_HEAL; + uint32_t damage_amp = OSRS_ITEM_EFFECT_TWISTED_BOW | OSRS_ITEM_EFFECT_FANG | + OSRS_ITEM_EFFECT_TUMEKENS_SHADOW | OSRS_ITEM_EFFECT_DHAROK_PIECE | + OSRS_ITEM_EFFECT_DRAGON_HUNTER_WAND | OSRS_ITEM_EFFECT_VENATOR_BOUNCE; + uint32_t defensive = OSRS_ITEM_EFFECT_ELYSIAN | OSRS_ITEM_EFFECT_CRYSTAL_ARMOUR | + OSRS_ITEM_EFFECT_RECOIL_RING | OSRS_ITEM_EFFECT_VENOM_IMMUNE | + OSRS_ITEM_EFFECT_ECHO_BOOTS | OSRS_ITEM_EFFECT_CONFLICTION | + OSRS_ITEM_EFFECT_VIRTUS_PIECE; + uint32_t util = OSRS_ITEM_EFFECT_LIGHTBEARER; + out[0] = (effect_mask & lifesteal) ? 1.0f : 0.0f; + out[1] = (effect_mask & damage_amp) ? 1.0f : 0.0f; + out[2] = (effect_mask & defensive) ? 1.0f : 0.0f; + out[3] = (effect_mask & util) ? 1.0f : 0.0f; +} + +static inline uint8_t osrs_item_index_for_raw_osrs_id(uint16_t raw_osrs_id) { + for (int i = 0; i < NUM_ITEMS; i++) { + if (ITEM_DATABASE[i].item_id == raw_osrs_id) return (uint8_t)i; + } + return ITEM_NONE; +} + +static inline void osrs_write_inventory_cell_affordance_features( + float* out, + uint8_t item_idx, + uint16_t raw_osrs_id, + uint8_t dose, + int is_equipped, + const float post_use_deltas[6], + int base_hitpoints, + int base_prayer, + int base_level +) { + OsrsConsumableClick consumable = + osrs_consumable_click_lookup_raw_osrs_id(raw_osrs_id); + int present = raw_osrs_id != 0 || item_idx != ITEM_NONE; + int is_gear = item_idx != ITEM_NONE; + int style = is_gear ? get_item_attack_style(item_idx) : 0; + uint32_t effect_mask = is_gear ? ITEM_DATABASE[item_idx].effect_mask : OSRS_ITEM_EFFECT_NONE; + + out[0] = present ? 1.0f : 0.0f; + out[1] = is_equipped ? 1.0f : 0.0f; + out[2] = dose > 0 ? osrs_clamp_unit((float)dose / 4.0f) : 0.0f; + out[3] = style == 1 ? 1.0f : 0.0f; + out[4] = style == 2 ? 1.0f : 0.0f; + out[5] = style == 3 ? 1.0f : 0.0f; + for (int i = 0; i < 6; i++) out[6 + i] = osrs_clamp_unit(post_use_deltas[i]); + + int is_weapon = is_gear && ITEM_DATABASE[item_idx].slot == SLOT_WEAPON; + out[12] = (is_gear && !is_weapon) ? 1.0f : 0.0f; + out[13] = is_weapon ? 1.0f : 0.0f; + OsrsConsumableKind6 k6 = col_consumable_kind6(consumable.consumable_kind); + out[14] = k6 == COL_CKIND6_BREW ? 1.0f : 0.0f; + out[15] = k6 == COL_CKIND6_RESTORE ? 1.0f : 0.0f; + out[16] = k6 == COL_CKIND6_COMBAT_BOOST ? 1.0f : 0.0f; + out[17] = k6 == COL_CKIND6_RANGED_BOOST ? 1.0f : 0.0f; + out[18] = k6 == COL_CKIND6_SPECIAL ? 1.0f : 0.0f; + OsrsConsumableKind ck = consumable.consumable_kind; + int hp_heal = osrs_consumable_hp_heal_amount(ck, base_hitpoints); + int pray_restore = osrs_consumable_prayer_restore_amount(ck, base_prayer); + int off_boost = osrs_consumable_offensive_boost_amount(ck, base_level); + out[19] = base_hitpoints > 0 ? osrs_clamp_unit((float)hp_heal / (float)base_hitpoints) : 0.0f; + out[20] = base_prayer > 0 ? osrs_clamp_unit((float)pray_restore / (float)base_prayer) : 0.0f; + out[21] = osrs_clamp_unit((float)off_boost / STAT_NORM_STRENGTH); + float eff4[4]; + osrs_item_effect_class4(effect_mask, eff4); + out[22] = eff4[0]; + out[23] = eff4[1]; + out[24] = eff4[2]; + out[25] = eff4[3]; + out[26] = is_weapon + ? osrs_clamp_unit((float)ITEM_DATABASE[item_idx].attack_speed / STAT_NORM_SPEED) : 0.0f; + out[27] = is_weapon + ? osrs_clamp_unit((float)ITEM_DATABASE[item_idx].attack_range / STAT_NORM_RANGE) : 0.0f; +} + +static inline void osrs_write_equipped_self_features(float* out, uint8_t item_idx) { + for (int i = 0; i < OSRS_EQUIPPED_SELF_OBS_FEATURES; i++) out[i] = 0.0f; + if (item_idx == ITEM_NONE) return; + if (item_idx >= NUM_ITEMS) osrs_inventory_clicks_trap(); + + const Item* item = &ITEM_DATABASE[item_idx]; + int style = get_item_attack_style(item_idx); + out[0] = 1.0f; + out[1] = style == 1 ? 1.0f : 0.0f; + out[2] = style == 2 ? 1.0f : 0.0f; + out[3] = style == 3 ? 1.0f : 0.0f; + out[4] = osrs_clamp_unit((float)item->attack_slash / STAT_NORM_ATTACK); + out[5] = osrs_clamp_unit((float)item->melee_strength / STAT_NORM_STRENGTH); + out[6] = osrs_clamp_unit((float)item->attack_ranged / STAT_NORM_ATTACK); + out[7] = osrs_clamp_unit((float)item->ranged_strength / STAT_NORM_STRENGTH); + out[8] = osrs_clamp_unit(((float)item->attack_magic / STAT_NORM_ATTACK) + + ((float)item->magic_damage / STAT_NORM_MAGIC_DMG)); + out[9] = osrs_clamp_unit((float)(item->defence_stab + item->defence_slash + + item->defence_crush + item->defence_magic + item->defence_ranged) / + (5.0f * STAT_NORM_DEFENCE)); + out[10] = item->effect_mask != OSRS_ITEM_EFFECT_NONE ? 1.0f : 0.0f; + out[11] = item->slot == SLOT_WEAPON ? 1.0f : 0.0f; + + float eff4[4]; + osrs_item_effect_class4(item->effect_mask, eff4); + out[12] = eff4[0]; + out[13] = eff4[1]; + out[14] = eff4[2]; + out[15] = eff4[3]; + out[16] = item->slot == SLOT_WEAPON + ? osrs_clamp_unit((float)item->attack_speed / STAT_NORM_SPEED) : 0.0f; + out[17] = item->slot == SLOT_WEAPON + ? osrs_clamp_unit((float)item->attack_range / STAT_NORM_RANGE) : 0.0f; +} + +static inline OsrsClickAction osrs_item_click_action(uint8_t item_idx) { + if (item_idx == ITEM_NONE) return OSRS_CLICK_NONE; + if (item_idx >= NUM_ITEMS) osrs_inventory_clicks_trap(); + + switch (ITEM_DATABASE[item_idx].slot) { + case SLOT_HEAD: + case SLOT_CAPE: + case SLOT_NECK: + case SLOT_WEAPON: + case SLOT_BODY: + case SLOT_SHIELD: + case SLOT_LEGS: + case SLOT_HANDS: + case SLOT_FEET: + case SLOT_RING: + case SLOT_AMMO: + return OSRS_CLICK_EQUIP; + default: + osrs_inventory_clicks_trap(); + return OSRS_CLICK_NONE; + } +} + +static inline OsrsConsumableKind osrs_item_click_consumable_kind(uint8_t item_idx) { + if (item_idx == ITEM_NONE) return OSRS_CONSUMABLE_NONE; + if (item_idx >= NUM_ITEMS) osrs_inventory_clicks_trap(); + return OSRS_CONSUMABLE_NONE; +} + +static inline OsrsConsumableClick osrs_consumable_click_lookup_raw_osrs_id( + uint16_t raw_osrs_id +) { + int count = (int)( + sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY) / + sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY[0]) + ); + + for (int i = 0; i < count; i++) { + if (OSRS_CONSUMABLE_CLICK_REGISTRY[i].raw_osrs_id == raw_osrs_id) { + return OSRS_CONSUMABLE_CLICK_REGISTRY[i]; + } + } + + return (OsrsConsumableClick){ + .raw_osrs_id = raw_osrs_id, + .click_action = OSRS_CLICK_NONE, + .consumable_kind = OSRS_CONSUMABLE_NONE, + .dose_count = 0, + }; +} + +static inline uint8_t osrs_consumable_dose_count_after_drink(uint8_t dose_count) { + switch (dose_count) { + case 1: return 0; + case 2: return 1; + case 3: return 2; + case 4: return 3; + default: + osrs_inventory_clicks_trap(); + return 0; + } +} + +static inline uint16_t osrs_consumable_raw_osrs_id_after_drink(uint16_t raw_osrs_id) { + OsrsConsumableClick before = + osrs_consumable_click_lookup_raw_osrs_id(raw_osrs_id); + if (before.click_action != OSRS_CLICK_DRINK || before.dose_count == 0) { + osrs_inventory_clicks_trap(); + } + + uint8_t after_dose = + osrs_consumable_dose_count_after_drink(before.dose_count); + if (after_dose == 0) return 0; + + int count = (int)( + sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY) / + sizeof(OSRS_CONSUMABLE_CLICK_REGISTRY[0]) + ); + + for (int i = 0; i < count; i++) { + OsrsConsumableClick candidate = OSRS_CONSUMABLE_CLICK_REGISTRY[i]; + if (candidate.consumable_kind == before.consumable_kind && + candidate.dose_count == after_dose) { + return candidate.raw_osrs_id; + } + } + + osrs_inventory_clicks_trap(); + return 0; +} + +static inline OsrsInventoryClickResolution osrs_inventory_click_interpret( + uint8_t item_idx, + uint16_t raw_osrs_id, + OsrsClickTickMultiplicity tick_multiplicity +) { + OsrsInventoryClickResolution none_resolution = { + .click_action = OSRS_CLICK_NONE, + .consumable_kind = OSRS_CONSUMABLE_NONE, + .dose_count = 0, + .raw_osrs_id_after_drink = 0, + }; + switch (tick_multiplicity) { + case OSRS_CLICK_TICK_DUPLICATE: + return none_resolution; + case OSRS_CLICK_TICK_FIRST: + break; + default: + osrs_inventory_clicks_trap(); + } + + if (item_idx != ITEM_NONE) { + OsrsClickAction action = osrs_item_click_action(item_idx); + if (raw_osrs_id != 0 && raw_osrs_id != ITEM_DATABASE[item_idx].item_id) { + osrs_inventory_clicks_trap(); + } + return (OsrsInventoryClickResolution){ + .click_action = action, + .consumable_kind = OSRS_CONSUMABLE_NONE, + .dose_count = 0, + .raw_osrs_id_after_drink = 0, + }; + } + + if (raw_osrs_id == 0) { + return none_resolution; + } + + OsrsConsumableClick consumable = + osrs_consumable_click_lookup_raw_osrs_id(raw_osrs_id); + uint16_t after_drink = 0; + if (consumable.click_action == OSRS_CLICK_DRINK && + consumable.dose_count > 0) { + after_drink = osrs_consumable_raw_osrs_id_after_drink(raw_osrs_id); + } + + return (OsrsInventoryClickResolution){ + .click_action = consumable.click_action, + .consumable_kind = consumable.consumable_kind, + .dose_count = consumable.dose_count, + .raw_osrs_id_after_drink = after_drink, + }; +} + +static inline OsrsInventoryClickResolution osrs_inventory_cell_click_interpret( + const OsrsInventoryCell* cell, + OsrsClickTickMultiplicity tick_multiplicity +) { + return osrs_inventory_click_interpret( + cell->item_idx, + cell->raw_osrs_id, + tick_multiplicity); +} + +static inline OsrsInventoryClickResolution osrs_inventory_snapshot_click_interpret( + const OsrsInventorySlotSnapshot* snapshot, + int slot, + OsrsClickTickMultiplicity tick_multiplicity +) { + if (!osrs_inventory_slot_valid(slot)) { + fprintf(stderr, "inventory click: invalid slot %d\n", slot); + abort(); + } + return osrs_inventory_cell_click_interpret( + &snapshot->cells[slot], + tick_multiplicity); +} + +static inline OsrsInventoryCell osrs_inventory_cell_from_raw_osrs_id( + uint16_t raw_osrs_id +) { + if (raw_osrs_id == 0) return osrs_inventory_cell_empty(); + + uint8_t item_idx = osrs_item_index_for_raw_osrs_id(raw_osrs_id); + if (item_idx != ITEM_NONE) return osrs_inventory_cell_from_item(item_idx); + + OsrsConsumableClick consumable = + osrs_consumable_click_lookup_raw_osrs_id(raw_osrs_id); + return (OsrsInventoryCell){ + .item_idx = ITEM_NONE, + .raw_osrs_id = raw_osrs_id, + .dose = consumable.dose_count, + }; +} + +static inline void osrs_inventory_cell_decrement_drink( + OsrsInventoryCell* cell, + OsrsInventoryClickResolution resolution +) { + if (resolution.click_action != OSRS_CLICK_DRINK || + resolution.dose_count == 0 || + cell->dose != resolution.dose_count) { + fprintf(stderr, "inventory drink decrement: invalid cell raw=%u dose=%u action=%d resolved_dose=%u\n", + cell->raw_osrs_id, cell->dose, (int)resolution.click_action, + resolution.dose_count); + abort(); + } + + if (resolution.raw_osrs_id_after_drink == 0) { + *cell = osrs_inventory_cell_empty(); + return; + } + + cell->item_idx = ITEM_NONE; + cell->raw_osrs_id = resolution.raw_osrs_id_after_drink; + cell->dose = osrs_consumable_dose_count_after_drink(resolution.dose_count); +} + +static inline OsrsInventoryDrinkConsumeResult osrs_inventory_cell_consume_drink_one_dose( + OsrsInventoryCell* cell, + OsrsInventoryClickResolution resolution, + int* potion_timer, + OsrsInventoryDrinkOneDoseEffectFn apply_one_dose, + void* ctx +) { + if (cell == NULL || potion_timer == NULL || apply_one_dose == NULL) { + fprintf(stderr, "inventory drink consume: null argument\n"); + abort(); + } + if (resolution.click_action != OSRS_CLICK_DRINK || + resolution.dose_count == 0 || + cell->dose != resolution.dose_count) { + fprintf(stderr, "inventory drink consume: invalid cell raw=%u dose=%u action=%d resolved_dose=%u\n", + cell->raw_osrs_id, cell->dose, (int)resolution.click_action, + resolution.dose_count); + abort(); + } + + uint8_t dose_after = + osrs_consumable_dose_count_after_drink(resolution.dose_count); + OsrsInventoryDrinkConsumeResult result = { + .consumed = 0, + .consumable_kind = resolution.consumable_kind, + .dose_count_before = resolution.dose_count, + .dose_count_after = dose_after, + .raw_osrs_id_before = cell->raw_osrs_id, + .raw_osrs_id_after_drink = resolution.raw_osrs_id_after_drink, + }; + if (*potion_timer > 0) return result; + + osrs_inventory_cell_decrement_drink(cell, resolution); + *potion_timer = 3; + apply_one_dose(ctx, resolution.consumable_kind); + result.consumed = 1; + return result; +} + +static inline void osrs_inventory_cell_consume_eat(OsrsInventoryCell* cell) { + *cell = osrs_inventory_cell_empty(); +} + +#endif diff --git a/ocean/osrs/osrs_item_effects.h b/ocean/osrs/osrs_item_effects.h new file mode 100644 index 0000000000..d2c46e7209 --- /dev/null +++ b/ocean/osrs/osrs_item_effects.h @@ -0,0 +1,521 @@ +#ifndef OSRS_ITEM_EFFECTS_H +#define OSRS_ITEM_EFFECTS_H + +#include +#include + +#include "osrs_damage.h" + +#define OSRS_SPEC_REGEN_INTERVAL 50 +#define OSRS_SPEC_REGEN_LIGHTBEARER 25 +#define OSRS_SPEC_REGEN_AMOUNT 10 +#define OSRS_ECHO_BOOTS_MAX_CHARGES 60000 + +typedef struct { + int attack_roll; + int max_hit; + int min_hit; + int use_double_accuracy; + int use_fang_accuracy; +} OsrsPreparedAttackEffects; + +typedef struct { + int heal_amount; +} OsrsPostAttackEffects; + +static inline OsrsTargetRef osrs_target_ref_none(void) { + OsrsTargetRef target = { .kind = OSRS_TARGET_NONE, .id = -1 }; + return target; +} + +static inline int osrs_target_ref_equal(OsrsTargetRef lhs, OsrsTargetRef rhs) { + return lhs.kind == rhs.kind && lhs.id == rhs.id; +} + +static inline OsrsTargetEffectContext osrs_target_effect_context_none(void) { + return (OsrsTargetEffectContext){0}; +} + +static inline OsrsTargetEffectContext osrs_target_effect_context_magic( + int magic_level, + int magic_attack_bonus +) { + return (OsrsTargetEffectContext){ + .magic_level = magic_level, + .magic_attack_bonus = magic_attack_bonus, + .target_class = OSRS_TARGET_CLASS_STANDARD, + }; +} + +static inline int osrs_target_effect_context_is_dragon( + OsrsTargetEffectContext target_context +) { + return target_context.target_class == OSRS_TARGET_CLASS_DRAGON; +} + +static inline int osrs_magic_attack_is_ancient(OsrsMagicAttackKind kind) { + return kind == OSRS_MAGIC_ATTACK_ANCIENT_ICE || + kind == OSRS_MAGIC_ATTACK_ANCIENT_BLOOD; +} + +static inline int osrs_effect_profile_has( + const OsrsEquipmentEffectProfile* profile, uint32_t effect_mask +) { + return (profile->effect_mask & effect_mask) != 0; +} + +static inline OsrsRecoilSource osrs_recoil_source_from_ring(uint8_t ring_item) { + if (ring_item == ITEM_RING_OF_RECOIL) { + return OSRS_RECOIL_SOURCE_RING_OF_RECOIL; + } + if (ring_item == ITEM_RING_OF_SUFFERING_RI) { + return OSRS_RECOIL_SOURCE_RING_OF_SUFFERING_RI; + } + return OSRS_RECOIL_SOURCE_NONE; +} + +static inline OsrsSpecRegenMode osrs_spec_regen_mode_from_ring(uint8_t ring_item) { + if (ring_item == ITEM_LIGHTBEARER) { + return OSRS_SPEC_REGEN_MODE_LIGHTBEARER; + } + return OSRS_SPEC_REGEN_MODE_NORMAL; +} + +static inline int osrs_scythe_splats_for_target_size(int target_size) { + return target_size >= 3 ? 3 : target_size == 2 ? 2 : 1; +} + +static inline uint8_t osrs_crystal_armour_points(uint8_t item_index) { + switch (item_index) { + case ITEM_CRYSTAL_HELM: return 1; + case ITEM_CRYSTAL_LEGS: return 2; + case ITEM_CRYSTAL_BODY: return 3; + default: return 0; + } +} + +static inline void osrs_item_effect_state_init(OsrsItemEffectState* state) { + memset(state, 0, sizeof(*state)); + state->confliction_weapon_item = ITEM_NONE; + state->confliction_target = osrs_target_ref_none(); +} + +static inline void osrs_clear_confliction_state(OsrsItemEffectState* state) { + state->confliction_is_primed = 0; + state->confliction_weapon_item = ITEM_NONE; + state->confliction_magic_kind = OSRS_MAGIC_ATTACK_NONE; + state->confliction_target = osrs_target_ref_none(); +} + +static inline GearBonuses osrs_gear_bonuses_from_equipment_bonuses( + const EquipmentBonuses* equipment_bonuses +) { + GearBonuses total = {0}; + total.stab_attack = equipment_bonuses->attack_stab; + total.slash_attack = equipment_bonuses->attack_slash; + total.crush_attack = equipment_bonuses->attack_crush; + total.magic_attack = equipment_bonuses->attack_magic; + total.ranged_attack = equipment_bonuses->attack_ranged; + total.stab_defence = equipment_bonuses->defence_stab; + total.slash_defence = equipment_bonuses->defence_slash; + total.crush_defence = equipment_bonuses->defence_crush; + total.magic_defence = equipment_bonuses->defence_magic; + total.ranged_defence = equipment_bonuses->defence_ranged; + total.melee_strength = equipment_bonuses->melee_strength; + total.ranged_strength = equipment_bonuses->ranged_strength; + total.magic_strength = equipment_bonuses->magic_damage; + total.attack_speed = equipment_bonuses->attack_speed; + total.attack_range = equipment_bonuses->attack_range; + return total; +} + +static inline void osrs_derive_equipment_effect_profile( + const uint8_t equipped[NUM_GEAR_SLOTS], + OsrsEquipmentEffectProfile* out +) { + memset(out, 0, sizeof(*out)); + out->weapon_item = equipped[GEAR_SLOT_WEAPON]; + out->ring_item = equipped[GEAR_SLOT_RING]; + out->shield_item = equipped[GEAR_SLOT_SHIELD]; + out->recoil_source = osrs_recoil_source_from_ring(out->ring_item); + out->spec_regen_mode = osrs_spec_regen_mode_from_ring(out->ring_item); + + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + uint8_t item_index = equipped[slot]; + if (item_index >= NUM_ITEMS) { + continue; + } + + uint32_t effect_mask = ITEM_DATABASE[item_index].effect_mask; + out->effect_mask |= effect_mask; + + if (effect_mask & OSRS_ITEM_EFFECT_VIRTUS_PIECE) { + out->virtus_piece_count += 1; + } + if (effect_mask & OSRS_ITEM_EFFECT_DHAROK_PIECE) { + out->dharok_piece_count += 1; + } + if (effect_mask & OSRS_ITEM_EFFECT_CRYSTAL_ARMOUR) { + out->crystal_armour_points += osrs_crystal_armour_points(item_index); + } + } +} + +static inline void osrs_sync_item_effect_state( + Player* player, const OsrsEquipmentEffectProfile* previous_profile +) { + const OsrsEquipmentEffectProfile* current_profile = &player->equipment_effect_profile; + + if (previous_profile->spec_regen_mode != current_profile->spec_regen_mode) { + if (current_profile->spec_regen_mode == OSRS_SPEC_REGEN_MODE_LIGHTBEARER) { + if (player->item_effect_state.special_regen_ticks > OSRS_SPEC_REGEN_LIGHTBEARER) { + player->item_effect_state.special_regen_ticks = 0; + } + } else { + player->item_effect_state.special_regen_ticks = 0; + } + } + + if (previous_profile->recoil_source != current_profile->recoil_source) { + if (current_profile->recoil_source == OSRS_RECOIL_SOURCE_RING_OF_RECOIL) { + player->item_effect_state.recoil_charges = RECOIL_MAX_CHARGES; + } else if (current_profile->recoil_source == OSRS_RECOIL_SOURCE_RING_OF_SUFFERING_RI) { + player->item_effect_state.recoil_charges = RECOIL_MAX_CHARGES; + } else { + player->item_effect_state.recoil_charges = 0; + } + } + + if (!osrs_effect_profile_has(current_profile, OSRS_ITEM_EFFECT_CONFLICTION)) { + osrs_clear_confliction_state(&player->item_effect_state); + } + + int had_echo = osrs_effect_profile_has(previous_profile, OSRS_ITEM_EFFECT_ECHO_BOOTS); + int has_echo = osrs_effect_profile_has(current_profile, OSRS_ITEM_EFFECT_ECHO_BOOTS); + if (has_echo && !had_echo) { + player->item_effect_state.echo_boot_charges = OSRS_ECHO_BOOTS_MAX_CHARGES; + } else if (!has_echo) { + player->item_effect_state.echo_boot_charges = 0; + } +} + +static inline void osrs_refresh_player_equipment(Player* player) { + OsrsEquipmentEffectProfile previous_profile = player->equipment_effect_profile; + EquipmentBonuses equipment_bonuses; + osrs_sum_equipment_bonuses(player->equipped, &equipment_bonuses); + player->slot_cached_bonuses = osrs_gear_bonuses_from_equipment_bonuses(&equipment_bonuses); + osrs_derive_equipment_effect_profile(player->equipped, &player->equipment_effect_profile); + osrs_sync_item_effect_state(player, &previous_profile); + player->slot_gear_dirty = 0; +} + +static inline void osrs_ensure_player_equipment(Player* player) { + if (player->slot_gear_dirty) { + osrs_refresh_player_equipment(player); + } +} + +static inline int osrs_confliction_can_apply( + const OsrsEquipmentEffectProfile* profile, + AttackStyle style, + uint8_t weapon_item, + int is_primary_target +) { + return style == ATTACK_STYLE_MAGIC && + is_primary_target && + osrs_effect_profile_has(profile, OSRS_ITEM_EFFECT_CONFLICTION) && + !item_is_two_handed(weapon_item); +} + +static inline int osrs_confliction_is_match( + const OsrsItemEffectState* state, + uint8_t weapon_item, + OsrsMagicAttackKind magic_kind, + OsrsTargetRef target_ref +) { + return state->confliction_is_primed && + state->confliction_weapon_item == weapon_item && + state->confliction_magic_kind == magic_kind && + osrs_target_ref_equal(state->confliction_target, target_ref); +} + +static inline int osrs_fang_hit_bound_shrink(int max_hit) { + return max_hit * 3 / 20; +} + +static inline OsrsPreparedAttackEffects osrs_prepare_attack_effects_for_melee_style( + const OsrsEquipmentEffectProfile* profile, + const OsrsItemEffectState* state, + uint8_t weapon_item, + AttackStyle style, + MeleeStyle melee_style, + OsrsMagicAttackKind magic_kind, + OsrsTargetRef target_ref, + int is_primary_target, + int base_attack_roll, + int base_max_hit, + OsrsTargetEffectContext target_context, + int attacker_current_hitpoints, + int attacker_base_hitpoints +) { + OsrsPreparedAttackEffects result = { + .attack_roll = base_attack_roll, + .max_hit = base_max_hit, + .min_hit = 0, + .use_double_accuracy = 0, + .use_fang_accuracy = 0, + }; + + if (style == ATTACK_STYLE_RANGED && + weapon_item == ITEM_TWISTED_BOW && + osrs_effect_profile_has(profile, OSRS_ITEM_EFFECT_TWISTED_BOW)) { + int target_magic = max_int( + target_context.magic_level, + target_context.magic_attack_bonus); + result.attack_roll = (int)(result.attack_roll * osrs_tbow_acc_mult(target_magic)); + result.max_hit = (int)(result.max_hit * osrs_tbow_dmg_mult(target_magic)); + } + + if (style == ATTACK_STYLE_RANGED && + weapon_item == ITEM_BOW_OF_FAERDHINEN && + profile->crystal_armour_points > 0) { + result.attack_roll = + result.attack_roll * (20 + profile->crystal_armour_points) / 20; + result.max_hit = + result.max_hit * (40 + profile->crystal_armour_points) / 40; + } + + if (osrs_target_effect_context_is_dragon(target_context) && + weapon_item == ITEM_DRAGON_HUNTER_WAND && + osrs_effect_profile_has(profile, OSRS_ITEM_EFFECT_DRAGON_HUNTER_WAND) && + (style == ATTACK_STYLE_MAGIC || style == ATTACK_STYLE_MELEE)) { + result.attack_roll = result.attack_roll * 7 / 4; + result.max_hit = result.max_hit * 7 / 5; + } + + if (style == ATTACK_STYLE_MAGIC && osrs_magic_attack_is_ancient(magic_kind) && + profile->virtus_piece_count > 0) { + result.max_hit = result.max_hit * (100 + 3 * profile->virtus_piece_count) / 100; + } + + if (style == ATTACK_STYLE_MELEE && profile->dharok_piece_count >= 4) { + float hp_ratio = 1.0f - ((float)attacker_current_hitpoints / (float)attacker_base_hitpoints); + result.max_hit = (int)(result.max_hit * (1.0f + hp_ratio * hp_ratio)); + } + + if (style == ATTACK_STYLE_MELEE && + weapon_item == ITEM_OSMUMTENS_FANG && + osrs_effect_profile_has(profile, OSRS_ITEM_EFFECT_FANG)) { + int fang_shrink = osrs_fang_hit_bound_shrink(result.max_hit); + result.min_hit = fang_shrink; + result.max_hit -= fang_shrink; + if (melee_style == MELEE_STYLE_STAB) { + result.use_fang_accuracy = 1; + } + } + + if (osrs_confliction_can_apply(profile, style, weapon_item, is_primary_target) && + osrs_confliction_is_match(state, weapon_item, magic_kind, target_ref)) { + result.use_double_accuracy = 1; + } + + return result; +} + +static inline OsrsPreparedAttackEffects osrs_prepare_attack_effects( + const OsrsEquipmentEffectProfile* profile, + const OsrsItemEffectState* state, + uint8_t weapon_item, + AttackStyle style, + OsrsMagicAttackKind magic_kind, + OsrsTargetRef target_ref, + int is_primary_target, + int base_attack_roll, + int base_max_hit, + OsrsTargetEffectContext target_context, + int attacker_current_hitpoints, + int attacker_base_hitpoints +) { + return osrs_prepare_attack_effects_for_melee_style( + profile, state, weapon_item, style, MELEE_STYLE_STAB, magic_kind, + target_ref, is_primary_target, base_attack_roll, base_max_hit, + target_context, attacker_current_hitpoints, attacker_base_hitpoints); +} + +static inline int osrs_roll_prepared_attack_damage( + const OsrsPreparedAttackEffects* prepared, + int def_roll, + int splat_max_hit, + uint32_t* rng_state +) { + assert(prepared->min_hit <= splat_max_hit); + int damage = prepared->min_hit + + encounter_rand_int(rng_state, splat_max_hit - prepared->min_hit + 1); + int hit = (prepared->use_fang_accuracy || prepared->use_double_accuracy) + ? encounter_roll_hit_chance_double(rng_state, prepared->attack_roll, def_roll) + : encounter_roll_hit_chance(rng_state, prepared->attack_roll, def_roll); + return hit ? damage : 0; +} + +static inline int osrs_blood_fury_heal_amount( + const OsrsEquipmentEffectProfile* profile, + AttackStyle style, + int damage_dealt, + uint32_t* rng_state +) { + if (damage_dealt > 0 && + style == ATTACK_STYLE_MELEE && + osrs_effect_profile_has(profile, OSRS_ITEM_EFFECT_BLOOD_FURY) && + encounter_rand_int(rng_state, 5) == 0) { + return damage_dealt * 30 / 100; + } + return 0; +} + +static inline OsrsPostAttackEffects osrs_finalize_attack_effects( + const OsrsEquipmentEffectProfile* profile, + OsrsItemEffectState* state, + uint8_t weapon_item, + AttackStyle style, + OsrsMagicAttackKind magic_kind, + OsrsTargetRef target_ref, + int is_primary_target, + int used_double_accuracy, + int hit_landed, + int damage_dealt, + uint32_t* rng_state +) { + OsrsPostAttackEffects result = { .heal_amount = 0 }; + + if (osrs_confliction_can_apply(profile, style, weapon_item, is_primary_target)) { + if (used_double_accuracy) { + osrs_clear_confliction_state(state); + } else if (hit_landed) { + osrs_clear_confliction_state(state); + } else { + state->confliction_is_primed = 1; + state->confliction_weapon_item = weapon_item; + state->confliction_magic_kind = magic_kind; + state->confliction_target = target_ref; + } + } + + if (damage_dealt > 0 && + style == ATTACK_STYLE_MAGIC && + weapon_item == ITEM_SANGUINESTI_STAFF && + osrs_effect_profile_has(profile, OSRS_ITEM_EFFECT_SANG_HEAL) && + encounter_rand_int(rng_state, 6) == 0) { + result.heal_amount = damage_dealt / 2; + } + + return result; +} + +static inline int osrs_has_recoil_available( + const OsrsEquipmentEffectProfile* profile, + const OsrsItemEffectState* state +) { + if (profile->recoil_source == OSRS_RECOIL_SOURCE_RING_OF_SUFFERING_RI) { + return 1; + } + if (profile->recoil_source == OSRS_RECOIL_SOURCE_RING_OF_RECOIL) { + return state->recoil_charges > 0; + } + return 0; +} + +static inline int osrs_has_echo_boots_recoil_available( + const OsrsEquipmentEffectProfile* profile, + const OsrsItemEffectState* state +) { + return osrs_effect_profile_has(profile, OSRS_ITEM_EFFECT_ECHO_BOOTS) && + state->echo_boot_charges > 0; +} + +static inline int osrs_echo_boots_recoil_damage( + const OsrsEquipmentEffectProfile* profile, + const OsrsItemEffectState* state, + int final_damage +) { + if (final_damage <= 0) return 0; + if (!osrs_has_echo_boots_recoil_available(profile, state)) return 0; + return 1; +} + +static inline DamageResult osrs_apply_passive_damage_pipeline( + int raw_damage, + int attack_style, + int target_prayer, + int is_pvp, + int target_veng_active, + int attacker_smite_active, + const OsrsEquipmentEffectProfile* defender_profile, + const OsrsItemEffectState* defender_state, + uint32_t* rng_state +) { + int prayer_correct = encounter_prayer_correct_for_style(target_prayer, attack_style); + int final_damage = osrs_prayer_reduce_damage(raw_damage, target_prayer, attack_style, is_pvp); + int elysian_reduced = 0; + + if (final_damage > 0 && + osrs_effect_profile_has(defender_profile, OSRS_ITEM_EFFECT_ELYSIAN) && + encounter_rand_int(rng_state, 10) < 7) { + final_damage = final_damage * 75 / 100; + elysian_reduced = 1; + } + + DamageResult result = osrs_apply_post_mitigation_pipeline( + final_damage, + prayer_correct, + target_veng_active, + osrs_has_recoil_available(defender_profile, defender_state), + attacker_smite_active + ); + result.elysian_reduced = elysian_reduced; + return result; +} + +static inline void osrs_consume_echo_boots_charge(Player* defender) { + osrs_ensure_player_equipment(defender); + if (!osrs_effect_profile_has( + &defender->equipment_effect_profile, OSRS_ITEM_EFFECT_ECHO_BOOTS)) { + return; + } + if (defender->item_effect_state.echo_boot_charges <= 0) + return; + defender->item_effect_state.echo_boot_charges--; +} + +static inline void osrs_consume_recoil_charges(Player* defender, int recoil_damage) { + osrs_ensure_player_equipment(defender); + if (defender->equipment_effect_profile.recoil_source != OSRS_RECOIL_SOURCE_RING_OF_RECOIL) { + return; + } + + defender->item_effect_state.recoil_charges -= recoil_damage; + if (defender->item_effect_state.recoil_charges > 0) { + return; + } + + defender->item_effect_state.recoil_charges = 0; + defender->equipped[GEAR_SLOT_RING] = ITEM_NONE; + osrs_refresh_player_equipment(defender); +} + +static inline void osrs_tick_special_regen(Player* player) { + osrs_ensure_player_equipment(player); + if (player->special_energy >= 100) { + player->item_effect_state.special_regen_ticks = 0; + return; + } + + int regen_interval = player->equipment_effect_profile.spec_regen_mode == + OSRS_SPEC_REGEN_MODE_LIGHTBEARER ? OSRS_SPEC_REGEN_LIGHTBEARER : OSRS_SPEC_REGEN_INTERVAL; + player->item_effect_state.special_regen_ticks += 1; + if (player->item_effect_state.special_regen_ticks >= regen_interval) { + player->special_energy = clamp(player->special_energy + OSRS_SPEC_REGEN_AMOUNT, 0, 100); + player->item_effect_state.special_regen_ticks = 0; + } +} + +#endif diff --git a/ocean/osrs/osrs_items.h b/ocean/osrs/osrs_items.h new file mode 100644 index 0000000000..6614502d33 --- /dev/null +++ b/ocean/osrs/osrs_items.h @@ -0,0 +1,150 @@ +#ifndef OSRS_ITEMS_H +#define OSRS_ITEMS_H + +#include +#include + +typedef enum { + SLOT_HEAD = 0, + SLOT_CAPE = 1, + SLOT_NECK = 2, + SLOT_WEAPON = 3, + SLOT_BODY = 4, + SLOT_SHIELD = 5, + SLOT_LEGS = 6, + SLOT_HANDS = 7, + SLOT_FEET = 8, + SLOT_RING = 9, + SLOT_AMMO = 10, + NUM_EQUIPMENT_SLOTS = 11 +} EquipmentSlot; + +typedef enum { + OSRS_ITEM_EFFECT_NONE = 0, + OSRS_ITEM_EFFECT_TWISTED_BOW = 1u << 0, + OSRS_ITEM_EFFECT_VIRTUS_PIECE = 1u << 1, + OSRS_ITEM_EFFECT_CONFLICTION = 1u << 2, + OSRS_ITEM_EFFECT_SANG_HEAL = 1u << 3, + OSRS_ITEM_EFFECT_RECOIL_RING = 1u << 4, + OSRS_ITEM_EFFECT_LIGHTBEARER = 1u << 5, + OSRS_ITEM_EFFECT_DHAROK_PIECE = 1u << 6, + OSRS_ITEM_EFFECT_ELYSIAN = 1u << 7, + OSRS_ITEM_EFFECT_CRYSTAL_ARMOUR = 1u << 8, + OSRS_ITEM_EFFECT_DRAGON_HUNTER_WAND = 1u << 9, + OSRS_ITEM_EFFECT_ECHO_BOOTS = 1u << 10, + OSRS_ITEM_EFFECT_BLOOD_FURY = 1u << 11, + OSRS_ITEM_EFFECT_VENOM_IMMUNE = 1u << 12, + OSRS_ITEM_EFFECT_FANG = 1u << 13, + OSRS_ITEM_EFFECT_TUMEKENS_SHADOW = 1u << 14, + OSRS_ITEM_EFFECT_VENATOR_BOUNCE = 1u << 15, +} OsrsItemEffectMask; + +typedef struct { + uint16_t item_id; + char name[32]; + uint8_t slot; + uint8_t two_handed; + uint8_t attack_speed; + uint8_t attack_range; + int16_t attack_stab; + int16_t attack_slash; + int16_t attack_crush; + int16_t attack_magic; + int16_t attack_ranged; + int16_t defence_stab; + int16_t defence_slash; + int16_t defence_crush; + int16_t defence_magic; + int16_t defence_ranged; + int16_t melee_strength; + int16_t ranged_strength; + int16_t magic_damage; + int16_t prayer; + uint32_t effect_mask; +} Item; + +#include "osrs_items_generated.h" + +static inline const Item* get_item(uint8_t item_index) { + if (item_index >= NUM_ITEMS) return NULL; + return &ITEM_DATABASE[item_index]; +} + +static inline int item_supports_ancient_autocast(uint8_t item_index) { + return item_index == ITEM_KODAI_WAND || item_index == ITEM_DRAGON_HUNTER_WAND; +} + +static inline int item_is_weapon(uint8_t item_index) { + if (item_index >= NUM_ITEMS) return 0; + return ITEM_DATABASE[item_index].slot == SLOT_WEAPON; +} + +static inline int get_item_attack_style(uint8_t item_index) { + switch (item_index) { + case ITEM_WHIP: + case ITEM_DRAGON_DAGGER: + case ITEM_GHRAZI_RAPIER: + case ITEM_INQUISITORS_MACE: + case ITEM_DRAGON_CLAWS: + case ITEM_AGS: + case ITEM_ANCIENT_GS: + case ITEM_GRANITE_MAUL: + case ITEM_ELDER_MAUL: + case ITEM_SGS: + case ITEM_SCYTHE_OF_VITUR: + case ITEM_OSMUMTENS_FANG: + case ITEM_ABYSSAL_TENTACLE: + case ITEM_VESTAS: + case ITEM_VOIDWAKER: + case ITEM_STATIUS_WARHAMMER: + return 1; + case ITEM_RUNE_CROSSBOW: + case ITEM_ARMADYL_CROSSBOW: + case ITEM_ZARYTE_CROSSBOW: + case ITEM_DARK_BOW: + case ITEM_HEAVY_BALLISTA: + case ITEM_MORRIGANS_JAVELIN: + case ITEM_MAGIC_SHORTBOW_I: + case ITEM_BOW_OF_FAERDHINEN: + case ITEM_TWISTED_BOW: + case ITEM_TOXIC_BLOWPIPE: + case ITEM_VENATOR_BOW: + return 2; + case ITEM_AHRIM_STAFF: + case ITEM_STAFF_OF_DEAD: + case ITEM_KODAI_WAND: + case ITEM_DRAGON_HUNTER_WAND: + case ITEM_VOLATILE_STAFF: + case ITEM_ZURIELS_STAFF: + case ITEM_TRIDENT_OF_SWAMP: + case ITEM_SANGUINESTI_STAFF: + case ITEM_EYE_OF_AYAK: + case ITEM_TUMEKENS_SHADOW: + return 3; + default: + return 0; + } +} + +static inline int item_is_two_handed(uint8_t item_index) { + if (item_index >= NUM_ITEMS) return 0; + return ITEM_DATABASE[item_index].slot == SLOT_WEAPON && + ITEM_DATABASE[item_index].two_handed; +} + +static inline uint8_t osrs_suppress_shield_for_two_handed_weapon( + uint8_t weapon_item, + uint8_t shield_item +) { + return item_is_two_handed(weapon_item) ? ITEM_NONE : shield_item; +} + +#define STAT_NORM_ATTACK 150.0f +#define STAT_NORM_DEFENCE 100.0f +#define STAT_NORM_STRENGTH 150.0f +#define STAT_NORM_MAGIC_DMG 30.0f +#define STAT_NORM_PRAYER 10.0f +#define STAT_NORM_SPEED 10.0f +#define STAT_NORM_RANGE 15.0f + +#endif diff --git a/ocean/osrs/osrs_items_generated.h b/ocean/osrs/osrs_items_generated.h new file mode 100644 index 0000000000..94bba8e6e5 --- /dev/null +++ b/ocean/osrs/osrs_items_generated.h @@ -0,0 +1,1470 @@ +#ifndef OSRS_ITEMS_GENERATED_H +#define OSRS_ITEMS_GENERATED_H + +typedef enum { + ITEM_HELM_NEITIZNOT = 0, + ITEM_GOD_CAPE = 1, + ITEM_GLORY = 2, + ITEM_BLACK_DHIDE_BODY = 3, + ITEM_MYSTIC_TOP = 4, + ITEM_RUNE_PLATELEGS = 5, + ITEM_MYSTIC_BOTTOM = 6, + ITEM_WHIP = 7, + ITEM_RUNE_CROSSBOW = 8, + ITEM_AHRIM_STAFF = 9, + ITEM_DRAGON_DAGGER = 10, + ITEM_DRAGON_DEFENDER = 11, + ITEM_SPIRIT_SHIELD = 12, + ITEM_BARROWS_GLOVES = 13, + ITEM_CLIMBING_BOOTS = 14, + ITEM_BERSERKER_RING = 15, + ITEM_DIAMOND_BOLTS_E = 16, + ITEM_GHRAZI_RAPIER = 17, + ITEM_INQUISITORS_MACE = 18, + ITEM_STAFF_OF_DEAD = 19, + ITEM_KODAI_WAND = 20, + ITEM_VOLATILE_STAFF = 21, + ITEM_ZURIELS_STAFF = 22, + ITEM_ARMADYL_CROSSBOW = 23, + ITEM_ZARYTE_CROSSBOW = 24, + ITEM_DRAGON_CLAWS = 25, + ITEM_AGS = 26, + ITEM_ANCIENT_GS = 27, + ITEM_GRANITE_MAUL = 28, + ITEM_ELDER_MAUL = 29, + ITEM_DARK_BOW = 30, + ITEM_HEAVY_BALLISTA = 31, + ITEM_VESTAS = 32, + ITEM_VOIDWAKER = 33, + ITEM_STATIUS_WARHAMMER = 34, + ITEM_MORRIGANS_JAVELIN = 35, + ITEM_ANCESTRAL_HAT = 36, + ITEM_ANCESTRAL_TOP = 37, + ITEM_ANCESTRAL_BOTTOM = 38, + ITEM_AHRIMS_ROBETOP = 39, + ITEM_AHRIMS_ROBESKIRT = 40, + ITEM_KARILS_TOP = 41, + ITEM_BANDOS_TASSETS = 42, + ITEM_BLESSED_SPIRIT_SHIELD = 43, + ITEM_FURY = 44, + ITEM_OCCULT_NECKLACE = 45, + ITEM_INFERNAL_CAPE = 46, + ITEM_ETERNAL_BOOTS = 47, + ITEM_SEERS_RING_I = 48, + ITEM_LIGHTBEARER = 49, + ITEM_MAGES_BOOK = 50, + ITEM_DRAGON_ARROWS = 51, + ITEM_TORAGS_PLATELEGS = 52, + ITEM_DHAROKS_PLATELEGS = 53, + ITEM_VERACS_PLATESKIRT = 54, + ITEM_TORAGS_HELM = 55, + ITEM_DHAROKS_HELM = 56, + ITEM_VERACS_HELM = 57, + ITEM_GUTHANS_HELM = 58, + ITEM_OPAL_DRAGON_BOLTS = 59, + ITEM_IMBUED_SARA_CAPE = 60, + ITEM_EYE_OF_AYAK = 61, + ITEM_ELIDINIS_WARD_F = 62, + ITEM_CONFLICTION_GAUNTLETS = 63, + ITEM_AVERNIC_TREADS = 64, + ITEM_RING_OF_SUFFERING_RI = 65, + ITEM_TWISTED_BOW = 66, + ITEM_MASORI_MASK_F = 67, + ITEM_MASORI_BODY_F = 68, + ITEM_MASORI_CHAPS_F = 69, + ITEM_NECKLACE_OF_ANGUISH = 70, + ITEM_DIZANAS_QUIVER = 71, + ITEM_ZARYTE_VAMBRACES = 72, + ITEM_TOXIC_BLOWPIPE = 73, + ITEM_AHRIMS_HOOD = 74, + ITEM_TORMENTED_BRACELET = 75, + ITEM_SANGUINESTI_STAFF = 76, + ITEM_INFINITY_BOOTS = 77, + ITEM_GOD_BLESSING = 78, + ITEM_RING_OF_RECOIL = 79, + ITEM_CRYSTAL_HELM = 80, + ITEM_AVAS_ASSEMBLER = 81, + ITEM_CRYSTAL_BODY = 82, + ITEM_CRYSTAL_LEGS = 83, + ITEM_BOW_OF_FAERDHINEN = 84, + ITEM_BLESSED_DHIDE_BOOTS = 85, + ITEM_MYSTIC_HAT = 86, + ITEM_TRIDENT_OF_SWAMP = 87, + ITEM_BOOK_OF_DARKNESS = 88, + ITEM_AMETHYST_ARROW = 89, + ITEM_MYSTIC_BOOTS = 90, + ITEM_BLESSED_COIF = 91, + ITEM_BLACK_DHIDE_CHAPS = 92, + ITEM_MAGIC_SHORTBOW_I = 93, + ITEM_AVAS_ACCUMULATOR = 94, + ITEM_CRYSTAL_SHIELD = 95, + ITEM_PEGASIAN_BOOTS = 96, + ITEM_JUSTICIAR_FACEGUARD = 97, + ITEM_JUSTICIAR_CHESTGUARD = 98, + ITEM_JUSTICIAR_LEGGUARDS = 99, + ITEM_DRAGON_DART = 100, + ITEM_SCYTHE_OF_VITUR = 101, + ITEM_BLADE_OF_SAELDOR = 102, + ITEM_OSMUMTENS_FANG = 103, + ITEM_SOULREAPER_AXE = 104, + ITEM_TORVA_FULL_HELM = 105, + ITEM_TORVA_PLATEBODY = 106, + ITEM_TORVA_PLATELEGS = 107, + ITEM_BANDOS_CHESTPLATE = 108, + ITEM_BANDOS_BOOTS = 109, + ITEM_PRIMORDIAL_BOOTS = 110, + ITEM_FEROCIOUS_GLOVES = 111, + ITEM_AMULET_OF_TORTURE = 112, + ITEM_BERSERKER_RING_I = 113, + ITEM_ULTOR_RING = 114, + ITEM_AVERNIC_DEFENDER = 115, + ITEM_VENATOR_RING = 116, + ITEM_VIRTUS_MASK = 117, + ITEM_VIRTUS_ROBE_TOP = 118, + ITEM_VIRTUS_ROBE_BOTTOM = 119, + ITEM_MAGUS_RING = 120, + ITEM_TUMEKENS_SHADOW = 121, + ITEM_BGS = 122, + ITEM_SGS = 123, + ITEM_ZGS = 124, + ITEM_CRYSTAL_HALBERD = 125, + ITEM_DRAGON_BATTLEAXE = 126, + ITEM_RUBY_DRAGON_BOLTS_E = 127, + ITEM_DIAMOND_DRAGON_BOLTS_E = 128, + ITEM_RUNE_ARROW = 129, + ITEM_DRAGON_JAVELIN = 130, + ITEM_SPECTRAL_SPIRIT_SHIELD = 131, + ITEM_ELYSIAN_SPIRIT_SHIELD = 132, + ITEM_DRAGONFIRE_SHIELD = 133, + ITEM_DRAGON_HUNTER_WAND = 134, + ITEM_ECHO_BOOTS = 135, + ITEM_SERPENTINE_HELM = 136, + ITEM_AMULET_OF_BLOOD_FURY = 137, + ITEM_FIGHTER_TORSO = 138, + ITEM_AMULET_OF_RANCOUR = 139, + ITEM_OATHPLATE_CHEST = 140, + ITEM_OATHPLATE_LEGS = 141, + ITEM_VENATOR_BOW = 142, + ITEM_ABYSSAL_TENTACLE = 143, + NUM_ITEMS = 144, + ITEM_NONE = 255 +} ItemIndex; + +static const Item ITEM_DATABASE[NUM_ITEMS] = { + [ITEM_HELM_NEITIZNOT] = { + .item_id = 10828, .name = "Helm of neitiznot", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 31, .defence_slash = 29, .defence_crush = 34, + .defence_magic = 3, .defence_ranged = 30, + .melee_strength = 3, .ranged_strength = 0, .magic_damage = 0, .prayer = 3, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_GOD_CAPE] = { + .item_id = 21795, .name = "Imbued zamorak cape", .slot = SLOT_CAPE, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 15, .attack_ranged = 0, + .defence_stab = 3, .defence_slash = 3, .defence_crush = 3, + .defence_magic = 15, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 2, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_GLORY] = { + .item_id = 1712, .name = "Amulet of glory", .slot = SLOT_NECK, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 10, .attack_slash = 10, .attack_crush = 10, + .attack_magic = 10, .attack_ranged = 10, + .defence_stab = 3, .defence_slash = 3, .defence_crush = 3, + .defence_magic = 3, .defence_ranged = 3, + .melee_strength = 6, .ranged_strength = 0, .magic_damage = 0, .prayer = 3, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_BLACK_DHIDE_BODY] = { + .item_id = 2503, .name = "Black d'hide body", .slot = SLOT_BODY, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -15, .attack_ranged = 30, + .defence_stab = 30, .defence_slash = 38, .defence_crush = 45, + .defence_magic = 45, .defence_ranged = 50, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_MYSTIC_TOP] = { + .item_id = 4091, .name = "Mystic robe top", .slot = SLOT_BODY, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 20, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 20, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_RUNE_PLATELEGS] = { + .item_id = 1079, .name = "Rune platelegs", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -21, .attack_ranged = -11, + .defence_stab = 51, .defence_slash = 49, .defence_crush = 47, + .defence_magic = -4, .defence_ranged = 49, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_MYSTIC_BOTTOM] = { + .item_id = 4093, .name = "Mystic robe bottom", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 15, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 15, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_WHIP] = { + .item_id = 4151, .name = "Abyssal whip", .slot = SLOT_WEAPON, + .attack_speed = 4, .attack_range = 1, + .attack_stab = 0, .attack_slash = 82, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 82, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_RUNE_CROSSBOW] = { + .item_id = 9185, .name = "Rune crossbow", .slot = SLOT_WEAPON, + .attack_speed = 6, .attack_range = 7, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 90, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_AHRIM_STAFF] = { + .item_id = 4710, .name = "Ahrim's staff", .slot = SLOT_WEAPON, + .attack_speed = 6, .attack_range = 10, + .attack_stab = 12, .attack_slash = -1, .attack_crush = 65, + .attack_magic = 15, .attack_ranged = 0, + .defence_stab = 3, .defence_slash = 5, .defence_crush = 2, + .defence_magic = 15, .defence_ranged = 0, + .melee_strength = 68, .ranged_strength = 0, .magic_damage = 5, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DRAGON_DAGGER] = { + .item_id = 5698, .name = "Dragon dagger", .slot = SLOT_WEAPON, + .attack_speed = 4, .attack_range = 1, + .attack_stab = 40, .attack_slash = 25, .attack_crush = -4, + .attack_magic = 1, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 1, .defence_ranged = 0, + .melee_strength = 40, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DRAGON_DEFENDER] = { + .item_id = 12954, .name = "Dragon defender", .slot = SLOT_SHIELD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 25, .attack_slash = 24, .attack_crush = 23, + .attack_magic = -3, .attack_ranged = -2, + .defence_stab = 25, .defence_slash = 24, .defence_crush = 23, + .defence_magic = -3, .defence_ranged = -2, + .melee_strength = 6, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_SPIRIT_SHIELD] = { + .item_id = 12829, .name = "Spirit shield", .slot = SLOT_SHIELD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 39, .defence_slash = 41, .defence_crush = 50, + .defence_magic = 1, .defence_ranged = 45, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_BARROWS_GLOVES] = { + .item_id = 7462, .name = "Barrows gloves", .slot = SLOT_HANDS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 12, .attack_slash = 12, .attack_crush = 12, + .attack_magic = 6, .attack_ranged = 12, + .defence_stab = 12, .defence_slash = 12, .defence_crush = 12, + .defence_magic = 6, .defence_ranged = 12, + .melee_strength = 12, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_CLIMBING_BOOTS] = { + .item_id = 3105, .name = "Climbing boots", .slot = SLOT_FEET, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 2, .defence_crush = 2, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 2, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_BERSERKER_RING] = { + .item_id = 6737, .name = "Berserker ring", .slot = SLOT_RING, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 4, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 4, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DIAMOND_BOLTS_E] = { + .item_id = 9243, .name = "Diamond bolts (e)", .slot = SLOT_AMMO, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 105, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_GHRAZI_RAPIER] = { + .item_id = 22324, .name = "Ghrazi rapier", .slot = SLOT_WEAPON, + .attack_speed = 4, .attack_range = 1, + .attack_stab = 94, .attack_slash = 55, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 89, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_INQUISITORS_MACE] = { + .item_id = 24417, .name = "Inquisitor's mace", .slot = SLOT_WEAPON, + .attack_speed = 4, .attack_range = 1, + .attack_stab = 52, .attack_slash = -4, .attack_crush = 95, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 89, .ranged_strength = 0, .magic_damage = 0, .prayer = 2, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_STAFF_OF_DEAD] = { + .item_id = 11791, .name = "Staff of the dead", .slot = SLOT_WEAPON, + .attack_speed = 4, .attack_range = 10, + .attack_stab = 55, .attack_slash = 70, .attack_crush = 0, + .attack_magic = 17, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 3, .defence_crush = 3, + .defence_magic = 17, .defence_ranged = 0, + .melee_strength = 72, .ranged_strength = 0, .magic_damage = 15, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_KODAI_WAND] = { + .item_id = 21006, .name = "Kodai wand", .slot = SLOT_WEAPON, + .attack_speed = 4, .attack_range = 10, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 28, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 3, .defence_crush = 3, + .defence_magic = 20, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 15, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_VOLATILE_STAFF] = { + .item_id = 24424, .name = "Volatile nightmare staff", .slot = SLOT_WEAPON, + .attack_speed = 5, .attack_range = 10, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 16, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 14, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 15, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ZURIELS_STAFF] = { + .item_id = 13867, .name = "Zuriel's staff", .slot = SLOT_WEAPON, + .attack_speed = 5, .attack_range = 10, + .attack_stab = 13, .attack_slash = -1, .attack_crush = 65, + .attack_magic = 18, .attack_ranged = 0, + .defence_stab = 5, .defence_slash = 7, .defence_crush = 4, + .defence_magic = 18, .defence_ranged = 0, + .melee_strength = 72, .ranged_strength = 0, .magic_damage = 10, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ARMADYL_CROSSBOW] = { + .item_id = 11785, .name = "Armadyl crossbow", .slot = SLOT_WEAPON, + .attack_speed = 6, .attack_range = 7, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 100, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ZARYTE_CROSSBOW] = { + .item_id = 26374, .name = "Zaryte crossbow", .slot = SLOT_WEAPON, + .attack_speed = 6, .attack_range = 7, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 110, + .defence_stab = 14, .defence_slash = 14, .defence_crush = 12, + .defence_magic = 15, .defence_ranged = 16, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DRAGON_CLAWS] = { + .item_id = 13652, .name = "Dragon claws", .slot = SLOT_WEAPON, + .attack_speed = 4, .attack_range = 1, + .attack_stab = 41, .attack_slash = 57, .attack_crush = -4, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 13, .defence_slash = 26, .defence_crush = 7, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 56, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_AGS] = { + .item_id = 11802, .name = "Armadyl godsword", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 6, .attack_range = 1, + .attack_stab = 0, .attack_slash = 132, .attack_crush = 80, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 132, .ranged_strength = 0, .magic_damage = 0, .prayer = 8, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ANCIENT_GS] = { + .item_id = 26233, .name = "Ancient godsword", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 6, .attack_range = 1, + .attack_stab = 0, .attack_slash = 132, .attack_crush = 80, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 132, .ranged_strength = 0, .magic_damage = 0, .prayer = 8, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_GRANITE_MAUL] = { + .item_id = 4153, .name = "Granite maul", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 7, .attack_range = 1, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 81, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 79, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ELDER_MAUL] = { + .item_id = 21003, .name = "Elder maul", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 6, .attack_range = 1, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 135, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 147, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DARK_BOW] = { + .item_id = 11235, .name = "Dark bow", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 9, .attack_range = 10, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 95, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_HEAVY_BALLISTA] = { + .item_id = 19481, .name = "Heavy ballista", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 7, .attack_range = 10, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 125, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 15, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_VESTAS] = { + .item_id = 22613, .name = "Vesta's longsword (Deadman Mode", .slot = SLOT_WEAPON, + .attack_speed = 5, .attack_range = 1, + .attack_stab = 106, .attack_slash = 121, .attack_crush = -2, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 1, .defence_slash = 4, .defence_crush = 3, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 118, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_VOIDWAKER] = { + .item_id = 27690, .name = "Voidwaker", .slot = SLOT_WEAPON, + .attack_speed = 4, .attack_range = 1, + .attack_stab = 70, .attack_slash = 80, .attack_crush = -2, + .attack_magic = 5, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 1, .defence_crush = 0, + .defence_magic = 2, .defence_ranged = 0, + .melee_strength = 80, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_STATIUS_WARHAMMER] = { + .item_id = 22622, .name = "Statius's warhammer (Deadman Mo", .slot = SLOT_WEAPON, + .attack_speed = 5, .attack_range = 1, + .attack_stab = -4, .attack_slash = -4, .attack_crush = 123, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 114, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_MORRIGANS_JAVELIN] = { + .item_id = 22636, .name = "Morrigan's javelin (Deadman Mod", .slot = SLOT_WEAPON, + .attack_speed = 6, .attack_range = 5, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 105, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 145, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ANCESTRAL_HAT] = { + .item_id = 21018, .name = "Ancestral hat", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 8, .attack_ranged = -2, + .defence_stab = 12, .defence_slash = 11, .defence_crush = 13, + .defence_magic = 5, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 3, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ANCESTRAL_TOP] = { + .item_id = 21021, .name = "Ancestral robe top", .slot = SLOT_BODY, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 35, .attack_ranged = -8, + .defence_stab = 42, .defence_slash = 31, .defence_crush = 51, + .defence_magic = 28, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 3, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ANCESTRAL_BOTTOM] = { + .item_id = 21024, .name = "Ancestral robe bottom", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 26, .attack_ranged = -7, + .defence_stab = 27, .defence_slash = 24, .defence_crush = 30, + .defence_magic = 20, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 3, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_AHRIMS_ROBETOP] = { + .item_id = 4712, .name = "Ahrim's robetop", .slot = SLOT_BODY, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 30, .attack_ranged = -10, + .defence_stab = 52, .defence_slash = 37, .defence_crush = 63, + .defence_magic = 30, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 1, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_AHRIMS_ROBESKIRT] = { + .item_id = 4714, .name = "Ahrim's robeskirt", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 22, .attack_ranged = -7, + .defence_stab = 33, .defence_slash = 30, .defence_crush = 36, + .defence_magic = 22, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 1, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_KARILS_TOP] = { + .item_id = 4736, .name = "Karil's leathertop", .slot = SLOT_BODY, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -15, .attack_ranged = 30, + .defence_stab = 47, .defence_slash = 42, .defence_crush = 50, + .defence_magic = 65, .defence_ranged = 57, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_BANDOS_TASSETS] = { + .item_id = 11834, .name = "Bandos tassets", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -21, .attack_ranged = -7, + .defence_stab = 71, .defence_slash = 63, .defence_crush = 66, + .defence_magic = -4, .defence_ranged = 93, + .melee_strength = 2, .ranged_strength = 0, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_BLESSED_SPIRIT_SHIELD] = { + .item_id = 12831, .name = "Blessed spirit shield", .slot = SLOT_SHIELD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 53, .defence_slash = 55, .defence_crush = 73, + .defence_magic = 2, .defence_ranged = 52, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 3, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_FURY] = { + .item_id = 6585, .name = "Amulet of fury", .slot = SLOT_NECK, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 10, .attack_slash = 10, .attack_crush = 10, + .attack_magic = 10, .attack_ranged = 10, + .defence_stab = 15, .defence_slash = 15, .defence_crush = 15, + .defence_magic = 15, .defence_ranged = 15, + .melee_strength = 8, .ranged_strength = 0, .magic_damage = 0, .prayer = 5, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_OCCULT_NECKLACE] = { + .item_id = 12002, .name = "Occult necklace", .slot = SLOT_NECK, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 12, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 5, .prayer = 2, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_INFERNAL_CAPE] = { + .item_id = 21295, .name = "Infernal cape", .slot = SLOT_CAPE, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 4, .attack_slash = 4, .attack_crush = 4, + .attack_magic = 1, .attack_ranged = 1, + .defence_stab = 12, .defence_slash = 12, .defence_crush = 12, + .defence_magic = 12, .defence_ranged = 12, + .melee_strength = 8, .ranged_strength = 0, .magic_damage = 0, .prayer = 2, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ETERNAL_BOOTS] = { + .item_id = 13235, .name = "Eternal boots", .slot = SLOT_FEET, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 8, .attack_ranged = 0, + .defence_stab = 5, .defence_slash = 5, .defence_crush = 5, + .defence_magic = 8, .defence_ranged = 5, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 1, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_SEERS_RING_I] = { + .item_id = 11770, .name = "Seers ring (i)", .slot = SLOT_RING, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 12, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 12, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 1, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_LIGHTBEARER] = { + .item_id = 25975, .name = "Lightbearer", .slot = SLOT_RING, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_LIGHTBEARER + }, + [ITEM_MAGES_BOOK] = { + .item_id = 6889, .name = "Mage's book", .slot = SLOT_SHIELD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 15, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 15, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 2, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DRAGON_ARROWS] = { + .item_id = 11212, .name = "Dragon arrow", .slot = SLOT_AMMO, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 60, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_TORAGS_PLATELEGS] = { + .item_id = 4751, .name = "Torag's platelegs", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -21, .attack_ranged = -11, + .defence_stab = 85, .defence_slash = 82, .defence_crush = 83, + .defence_magic = -4, .defence_ranged = 92, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DHAROKS_PLATELEGS] = { + .item_id = 4722, .name = "Dharok's platelegs", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -21, .attack_ranged = -11, + .defence_stab = 85, .defence_slash = 82, .defence_crush = 83, + .defence_magic = -4, .defence_ranged = 92, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_DHAROK_PIECE + }, + [ITEM_VERACS_PLATESKIRT] = { + .item_id = 4759, .name = "Verac's plateskirt", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -21, .attack_ranged = -11, + .defence_stab = 85, .defence_slash = 82, .defence_crush = 83, + .defence_magic = 0, .defence_ranged = 84, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 4, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_TORAGS_HELM] = { + .item_id = 4745, .name = "Torag's helm", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -6, .attack_ranged = -2, + .defence_stab = 55, .defence_slash = 58, .defence_crush = 54, + .defence_magic = -1, .defence_ranged = 62, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DHAROKS_HELM] = { + .item_id = 4716, .name = "Dharok's helm", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -3, .attack_ranged = -1, + .defence_stab = 45, .defence_slash = 48, .defence_crush = 44, + .defence_magic = -1, .defence_ranged = 51, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_DHAROK_PIECE + }, + [ITEM_VERACS_HELM] = { + .item_id = 4753, .name = "Verac's helm", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -6, .attack_ranged = -2, + .defence_stab = 55, .defence_slash = 58, .defence_crush = 54, + .defence_magic = 0, .defence_ranged = 56, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 3, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_GUTHANS_HELM] = { + .item_id = 4724, .name = "Guthan's helm", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -6, .attack_ranged = -2, + .defence_stab = 55, .defence_slash = 58, .defence_crush = 54, + .defence_magic = -1, .defence_ranged = 62, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_OPAL_DRAGON_BOLTS] = { + .item_id = 21932, .name = "Opal dragon bolts (e)", .slot = SLOT_AMMO, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 122, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_IMBUED_SARA_CAPE] = { + .item_id = 21791, .name = "Imbued saradomin cape", .slot = SLOT_CAPE, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 15, .attack_ranged = 0, + .defence_stab = 3, .defence_slash = 3, .defence_crush = 3, + .defence_magic = 15, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 2, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_EYE_OF_AYAK] = { + .item_id = 31113, .name = "Eye of ayak", .slot = SLOT_WEAPON, + .attack_speed = 3, .attack_range = 6, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 30, .attack_ranged = 0, + .defence_stab = 1, .defence_slash = 5, .defence_crush = 5, + .defence_magic = 10, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 2, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ELIDINIS_WARD_F] = { + .item_id = 27251, .name = "Elidinis' ward (f)", .slot = SLOT_SHIELD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 25, .attack_ranged = 0, + .defence_stab = 53, .defence_slash = 55, .defence_crush = 73, + .defence_magic = 2, .defence_ranged = 52, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 5, .prayer = 4, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_CONFLICTION_GAUNTLETS] = { + .item_id = 31106, .name = "Confliction gauntlets", .slot = SLOT_HANDS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 20, .attack_ranged = -4, + .defence_stab = 15, .defence_slash = 18, .defence_crush = 7, + .defence_magic = 5, .defence_ranged = 5, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 7, .prayer = 2, .effect_mask = OSRS_ITEM_EFFECT_CONFLICTION + }, + [ITEM_AVERNIC_TREADS] = { + .item_id = 31097, .name = "Avernic treads (max)", .slot = SLOT_FEET, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 5, .attack_slash = 5, .attack_crush = 5, + .attack_magic = 11, .attack_ranged = 15, + .defence_stab = 21, .defence_slash = 25, .defence_crush = 25, + .defence_magic = 10, .defence_ranged = 10, + .melee_strength = 6, .ranged_strength = 3, .magic_damage = 2, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_RING_OF_SUFFERING_RI] = { + .item_id = 20657, .name = "Ring of suffering (i)", .slot = SLOT_RING, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 20, .defence_slash = 20, .defence_crush = 20, + .defence_magic = 20, .defence_ranged = 20, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 4, .effect_mask = OSRS_ITEM_EFFECT_RECOIL_RING + }, + [ITEM_TWISTED_BOW] = { + .item_id = 20997, .name = "Twisted bow", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 6, .attack_range = 10, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 70, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 20, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_TWISTED_BOW + }, + [ITEM_MASORI_MASK_F] = { + .item_id = 27235, .name = "Masori mask (f)", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -1, .attack_ranged = 12, + .defence_stab = 8, .defence_slash = 10, .defence_crush = 12, + .defence_magic = 12, .defence_ranged = 9, + .melee_strength = 0, .ranged_strength = 2, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_MASORI_BODY_F] = { + .item_id = 27238, .name = "Masori body (f)", .slot = SLOT_BODY, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -4, .attack_ranged = 43, + .defence_stab = 59, .defence_slash = 52, .defence_crush = 64, + .defence_magic = 74, .defence_ranged = 60, + .melee_strength = 0, .ranged_strength = 4, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_MASORI_CHAPS_F] = { + .item_id = 27241, .name = "Masori chaps (f)", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -2, .attack_ranged = 27, + .defence_stab = 35, .defence_slash = 30, .defence_crush = 39, + .defence_magic = 46, .defence_ranged = 37, + .melee_strength = 0, .ranged_strength = 2, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_NECKLACE_OF_ANGUISH] = { + .item_id = 19547, .name = "Necklace of anguish", .slot = SLOT_NECK, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 15, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 5, .magic_damage = 0, .prayer = 2, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DIZANAS_QUIVER] = { + .item_id = 28947, .name = "Dizana's quiver", .slot = SLOT_CAPE, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 18, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 3, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ZARYTE_VAMBRACES] = { + .item_id = 26235, .name = "Zaryte vambraces", .slot = SLOT_HANDS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = -8, .attack_slash = -8, .attack_crush = -8, + .attack_magic = 0, .attack_ranged = 18, + .defence_stab = 8, .defence_slash = 8, .defence_crush = 8, + .defence_magic = 5, .defence_ranged = 8, + .melee_strength = 0, .ranged_strength = 2, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_TOXIC_BLOWPIPE] = { + .item_id = 12926, .name = "Toxic blowpipe", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 3, .attack_range = 5, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 30, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 20, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_AHRIMS_HOOD] = { + .item_id = 4708, .name = "Ahrim's hood", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 6, .attack_ranged = -2, + .defence_stab = 15, .defence_slash = 13, .defence_crush = 16, + .defence_magic = 6, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 1, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_TORMENTED_BRACELET] = { + .item_id = 19544, .name = "Tormented bracelet", .slot = SLOT_HANDS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 10, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 5, .prayer = 2, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_SANGUINESTI_STAFF] = { + .item_id = 22481, .name = "Sanguinesti staff", .slot = SLOT_WEAPON, + .attack_speed = 4, .attack_range = 7, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 25, .attack_ranged = -4, + .defence_stab = 2, .defence_slash = 3, .defence_crush = 1, + .defence_magic = 15, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_SANG_HEAL + }, + [ITEM_INFINITY_BOOTS] = { + .item_id = 6920, .name = "Infinity boots", .slot = SLOT_FEET, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 5, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 5, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_GOD_BLESSING] = { + .item_id = 20220, .name = "Holy blessing", .slot = SLOT_AMMO, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_RING_OF_RECOIL] = { + .item_id = 2550, .name = "Ring of recoil", .slot = SLOT_RING, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_RECOIL_RING + }, + [ITEM_CRYSTAL_HELM] = { + .item_id = 23971, .name = "Crystal helm", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -10, .attack_ranged = 9, + .defence_stab = 12, .defence_slash = 8, .defence_crush = 14, + .defence_magic = 10, .defence_ranged = 18, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 2, .effect_mask = OSRS_ITEM_EFFECT_CRYSTAL_ARMOUR + }, + [ITEM_AVAS_ASSEMBLER] = { + .item_id = 22109, .name = "Ava's assembler", .slot = SLOT_CAPE, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 8, + .defence_stab = 1, .defence_slash = 1, .defence_crush = 1, + .defence_magic = 8, .defence_ranged = 2, + .melee_strength = 0, .ranged_strength = 2, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_CRYSTAL_BODY] = { + .item_id = 23975, .name = "Crystal body", .slot = SLOT_BODY, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -18, .attack_ranged = 31, + .defence_stab = 46, .defence_slash = 38, .defence_crush = 48, + .defence_magic = 44, .defence_ranged = 68, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 3, .effect_mask = OSRS_ITEM_EFFECT_CRYSTAL_ARMOUR + }, + [ITEM_CRYSTAL_LEGS] = { + .item_id = 23979, .name = "Crystal legs", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -12, .attack_ranged = 18, + .defence_stab = 26, .defence_slash = 21, .defence_crush = 30, + .defence_magic = 34, .defence_ranged = 38, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 2, .effect_mask = OSRS_ITEM_EFFECT_CRYSTAL_ARMOUR + }, + [ITEM_BOW_OF_FAERDHINEN] = { + .item_id = 25865, .name = "Bow of faerdhinen", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 5, .attack_range = 10, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 128, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 106, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_BLESSED_DHIDE_BOOTS] = { + .item_id = 19921, .name = "Ancient d'hide boots", .slot = SLOT_FEET, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -10, .attack_ranged = 7, + .defence_stab = 4, .defence_slash = 4, .defence_crush = 4, + .defence_magic = 4, .defence_ranged = 4, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_MYSTIC_HAT] = { + .item_id = 4089, .name = "Mystic hat", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 4, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 4, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_TRIDENT_OF_SWAMP] = { + .item_id = 12899, .name = "Trident of the swamp", .slot = SLOT_WEAPON, + .attack_speed = 4, .attack_range = 7, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 25, .attack_ranged = 0, + .defence_stab = 2, .defence_slash = 3, .defence_crush = 1, + .defence_magic = 15, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_BOOK_OF_DARKNESS] = { + .item_id = 12612, .name = "Book of darkness", .slot = SLOT_SHIELD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 10, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 5, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_AMETHYST_ARROW] = { + .item_id = 21326, .name = "Amethyst arrow", .slot = SLOT_AMMO, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 55, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_MYSTIC_BOOTS] = { + .item_id = 4097, .name = "Mystic boots", .slot = SLOT_FEET, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 3, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 3, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_BLESSED_COIF] = { + .item_id = 10382, .name = "Guthix coif", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -1, .attack_ranged = 7, + .defence_stab = 4, .defence_slash = 7, .defence_crush = 10, + .defence_magic = 4, .defence_ranged = 8, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_BLACK_DHIDE_CHAPS] = { + .item_id = 2497, .name = "Black d'hide chaps", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -10, .attack_ranged = 17, + .defence_stab = 18, .defence_slash = 20, .defence_crush = 26, + .defence_magic = 23, .defence_ranged = 26, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_MAGIC_SHORTBOW_I] = { + .item_id = 12788, .name = "Magic shortbow (i)", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 4, .attack_range = 7, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 75, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_AVAS_ACCUMULATOR] = { + .item_id = 10499, .name = "Ava's accumulator", .slot = SLOT_CAPE, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 4, + .defence_stab = 0, .defence_slash = 1, .defence_crush = 0, + .defence_magic = 4, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_CRYSTAL_SHIELD] = { + .item_id = 4224, .name = "Crystal shield (historical)", .slot = SLOT_SHIELD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -10, .attack_ranged = -10, + .defence_stab = 51, .defence_slash = 54, .defence_crush = 53, + .defence_magic = 0, .defence_ranged = 80, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_PEGASIAN_BOOTS] = { + .item_id = 13237, .name = "Pegasian boots", .slot = SLOT_FEET, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -12, .attack_ranged = 12, + .defence_stab = 5, .defence_slash = 5, .defence_crush = 5, + .defence_magic = 5, .defence_ranged = 5, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_JUSTICIAR_FACEGUARD] = { + .item_id = 22326, .name = "Justiciar faceguard", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -6, .attack_ranged = -2, + .defence_stab = 60, .defence_slash = 63, .defence_crush = 59, + .defence_magic = -6, .defence_ranged = 67, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 2, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_JUSTICIAR_CHESTGUARD] = { + .item_id = 22327, .name = "Justiciar chestguard", .slot = SLOT_BODY, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -40, .attack_ranged = -20, + .defence_stab = 132, .defence_slash = 130, .defence_crush = 117, + .defence_magic = -16, .defence_ranged = 142, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 4, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_JUSTICIAR_LEGGUARDS] = { + .item_id = 22328, .name = "Justiciar legguards", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -31, .attack_ranged = -17, + .defence_stab = 95, .defence_slash = 92, .defence_crush = 93, + .defence_magic = -14, .defence_ranged = 102, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 4, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DRAGON_DART] = { + .item_id = 11230, .name = "Dragon dart", .slot = SLOT_WEAPON, + .attack_speed = 3, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 35, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_SCYTHE_OF_VITUR] = { + .item_id = 22325, .name = "Scythe of vitur", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 5, .attack_range = 1, + .attack_stab = 70, .attack_slash = 125, .attack_crush = 30, + .attack_magic = -6, .attack_ranged = 0, + .defence_stab = -2, .defence_slash = 8, .defence_crush = 10, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 75, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_BLADE_OF_SAELDOR] = { + .item_id = 24551, .name = "Blade of saeldor (c)", .slot = SLOT_WEAPON, + .attack_speed = 4, .attack_range = 1, + .attack_stab = 55, .attack_slash = 94, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 89, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_OSMUMTENS_FANG] = { + .item_id = 26219, .name = "Osmumten's fang", .slot = SLOT_WEAPON, + .attack_speed = 5, .attack_range = 1, + .attack_stab = 105, .attack_slash = 75, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 103, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_FANG + }, + [ITEM_SOULREAPER_AXE] = { + .item_id = 28338, .name = "Soulreaper axe", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 5, .attack_range = 1, + .attack_stab = 28, .attack_slash = 134, .attack_crush = 66, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 121, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_TORVA_FULL_HELM] = { + .item_id = 26382, .name = "Torva full helm", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -5, .attack_ranged = -5, + .defence_stab = 59, .defence_slash = 60, .defence_crush = 62, + .defence_magic = -2, .defence_ranged = 57, + .melee_strength = 8, .ranged_strength = 0, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_TORVA_PLATEBODY] = { + .item_id = 26384, .name = "Torva platebody", .slot = SLOT_BODY, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -18, .attack_ranged = -14, + .defence_stab = 117, .defence_slash = 111, .defence_crush = 117, + .defence_magic = -11, .defence_ranged = 142, + .melee_strength = 6, .ranged_strength = 0, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_TORVA_PLATELEGS] = { + .item_id = 26386, .name = "Torva platelegs", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -24, .attack_ranged = -11, + .defence_stab = 87, .defence_slash = 78, .defence_crush = 79, + .defence_magic = -9, .defence_ranged = 102, + .melee_strength = 4, .ranged_strength = 0, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_BANDOS_CHESTPLATE] = { + .item_id = 11832, .name = "Bandos chestplate", .slot = SLOT_BODY, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -15, .attack_ranged = -10, + .defence_stab = 98, .defence_slash = 93, .defence_crush = 105, + .defence_magic = -6, .defence_ranged = 133, + .melee_strength = 4, .ranged_strength = 0, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_BANDOS_BOOTS] = { + .item_id = 11836, .name = "Bandos boots", .slot = SLOT_FEET, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -5, .attack_ranged = -3, + .defence_stab = 17, .defence_slash = 18, .defence_crush = 19, + .defence_magic = 0, .defence_ranged = 15, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_PRIMORDIAL_BOOTS] = { + .item_id = 13239, .name = "Primordial boots", .slot = SLOT_FEET, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 2, .attack_slash = 2, .attack_crush = 2, + .attack_magic = -4, .attack_ranged = -1, + .defence_stab = 22, .defence_slash = 22, .defence_crush = 22, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 5, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_FEROCIOUS_GLOVES] = { + .item_id = 22981, .name = "Ferocious gloves", .slot = SLOT_HANDS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 16, .attack_slash = 16, .attack_crush = 16, + .attack_magic = -16, .attack_ranged = -16, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 14, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_AMULET_OF_TORTURE] = { + .item_id = 19553, .name = "Amulet of torture", .slot = SLOT_NECK, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 15, .attack_slash = 15, .attack_crush = 15, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 10, .ranged_strength = 0, .magic_damage = 0, .prayer = 2, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_BERSERKER_RING_I] = { + .item_id = 11773, .name = "Berserker ring (i)", .slot = SLOT_RING, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 8, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 8, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ULTOR_RING] = { + .item_id = 28307, .name = "Ultor ring", .slot = SLOT_RING, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 12, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_AVERNIC_DEFENDER] = { + .item_id = 22322, .name = "Avernic defender", .slot = SLOT_SHIELD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 30, .attack_slash = 29, .attack_crush = 28, + .attack_magic = -5, .attack_ranged = -4, + .defence_stab = 30, .defence_slash = 29, .defence_crush = 28, + .defence_magic = -5, .defence_ranged = -4, + .melee_strength = 8, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_VENATOR_RING] = { + .item_id = 28310, .name = "Venator ring", .slot = SLOT_RING, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 10, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 2, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_VIRTUS_MASK] = { + .item_id = 26241, .name = "Virtus mask", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 8, .attack_ranged = -3, + .defence_stab = 15, .defence_slash = 14, .defence_crush = 16, + .defence_magic = 6, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 2, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_VIRTUS_PIECE + }, + [ITEM_VIRTUS_ROBE_TOP] = { + .item_id = 26243, .name = "Virtus robe top", .slot = SLOT_BODY, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 35, .attack_ranged = -11, + .defence_stab = 47, .defence_slash = 36, .defence_crush = 56, + .defence_magic = 31, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 2, .prayer = 2, .effect_mask = OSRS_ITEM_EFFECT_VIRTUS_PIECE + }, + [ITEM_VIRTUS_ROBE_BOTTOM] = { + .item_id = 26245, .name = "Virtus robe bottom", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 26, .attack_ranged = -9, + .defence_stab = 31, .defence_slash = 28, .defence_crush = 34, + .defence_magic = 22, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 2, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_VIRTUS_PIECE + }, + [ITEM_MAGUS_RING] = { + .item_id = 28313, .name = "Magus ring", .slot = SLOT_RING, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 15, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 2, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_TUMEKENS_SHADOW] = { + .item_id = 27275, .name = "Tumeken's shadow", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 5, .attack_range = 10, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 35, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 20, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 1, .effect_mask = OSRS_ITEM_EFFECT_TUMEKENS_SHADOW + }, + [ITEM_BGS] = { + .item_id = 11804, .name = "Bandos godsword", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 6, .attack_range = 1, + .attack_stab = 0, .attack_slash = 132, .attack_crush = 80, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 132, .ranged_strength = 0, .magic_damage = 0, .prayer = 8, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_SGS] = { + .item_id = 11806, .name = "Saradomin godsword", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 6, .attack_range = 1, + .attack_stab = 0, .attack_slash = 132, .attack_crush = 80, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 132, .ranged_strength = 0, .magic_damage = 0, .prayer = 8, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ZGS] = { + .item_id = 11808, .name = "Zamorak godsword", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 6, .attack_range = 1, + .attack_stab = 0, .attack_slash = 132, .attack_crush = 80, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 132, .ranged_strength = 0, .magic_damage = 0, .prayer = 8, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_CRYSTAL_HALBERD] = { + .item_id = 23987, .name = "Crystal halberd", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 7, .attack_range = 2, + .attack_stab = 85, .attack_slash = 110, .attack_crush = 5, + .attack_magic = -4, .attack_ranged = 0, + .defence_stab = -1, .defence_slash = 4, .defence_crush = 5, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 118, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DRAGON_BATTLEAXE] = { + .item_id = 1377, .name = "Dragon battleaxe", .slot = SLOT_WEAPON, + .attack_speed = 6, .attack_range = 1, + .attack_stab = -2, .attack_slash = 70, .attack_crush = 65, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = -1, + .melee_strength = 85, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_RUBY_DRAGON_BOLTS_E] = { + .item_id = 21944, .name = "Ruby dragon bolts (e)", .slot = SLOT_AMMO, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 122, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DIAMOND_DRAGON_BOLTS_E] = { + .item_id = 21946, .name = "Diamond dragon bolts (e)", .slot = SLOT_AMMO, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 122, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_RUNE_ARROW] = { + .item_id = 892, .name = "Rune arrow", .slot = SLOT_AMMO, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 49, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DRAGON_JAVELIN] = { + .item_id = 19484, .name = "Dragon javelin", .slot = SLOT_AMMO, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 150, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_SPECTRAL_SPIRIT_SHIELD] = { + .item_id = 12821, .name = "Spectral spirit shield", .slot = SLOT_SHIELD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 53, .defence_slash = 55, .defence_crush = 73, + .defence_magic = 30, .defence_ranged = 52, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 3, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_ELYSIAN_SPIRIT_SHIELD] = { + .item_id = 12817, .name = "Elysian spirit shield", .slot = SLOT_SHIELD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 63, .defence_slash = 65, .defence_crush = 75, + .defence_magic = 2, .defence_ranged = 57, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 0, .prayer = 3, .effect_mask = OSRS_ITEM_EFFECT_ELYSIAN + }, + [ITEM_DRAGONFIRE_SHIELD] = { + .item_id = 11283, .name = "Dragonfire shield", .slot = SLOT_SHIELD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -10, .attack_ranged = -5, + .defence_stab = 70, .defence_slash = 75, .defence_crush = 72, + .defence_magic = 10, .defence_ranged = 72, + .melee_strength = 7, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_DRAGON_HUNTER_WAND] = { + .item_id = 30070, .name = "Dragon hunter wand", .slot = SLOT_WEAPON, + .attack_speed = 5, .attack_range = 10, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 16, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 16, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 0, .magic_damage = 10, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_DRAGON_HUNTER_WAND + }, + [ITEM_ECHO_BOOTS] = { + .item_id = 28945, .name = "Echo boots", .slot = SLOT_FEET, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -3, .attack_ranged = -1, + .defence_stab = 32, .defence_slash = 32, .defence_crush = 32, + .defence_magic = -3, .defence_ranged = 24, + .melee_strength = 3, .ranged_strength = 0, .magic_damage = 0, .prayer = 4, .effect_mask = OSRS_ITEM_EFFECT_ECHO_BOOTS + }, + [ITEM_SERPENTINE_HELM] = { + .item_id = 12931, .name = "Serpentine helm", .slot = SLOT_HEAD, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -5, .attack_ranged = -5, + .defence_stab = 52, .defence_slash = 55, .defence_crush = 58, + .defence_magic = 0, .defence_ranged = 50, + .melee_strength = 5, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_VENOM_IMMUNE + }, + [ITEM_AMULET_OF_BLOOD_FURY] = { + .item_id = 24780, .name = "Amulet of blood fury", .slot = SLOT_NECK, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 10, .attack_slash = 10, .attack_crush = 10, + .attack_magic = 10, .attack_ranged = 10, + .defence_stab = 15, .defence_slash = 15, .defence_crush = 15, + .defence_magic = 15, .defence_ranged = 15, + .melee_strength = 8, .ranged_strength = 0, .magic_damage = 0, .prayer = 5, .effect_mask = OSRS_ITEM_EFFECT_BLOOD_FURY + }, + [ITEM_FIGHTER_TORSO] = { + .item_id = 10551, .name = "Fighter torso", .slot = SLOT_BODY, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = -40, .attack_ranged = 0, + .defence_stab = 62, .defence_slash = 85, .defence_crush = 62, + .defence_magic = -10, .defence_ranged = 67, + .melee_strength = 4, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_AMULET_OF_RANCOUR] = { + .item_id = 29801, .name = "Amulet of rancour", .slot = SLOT_NECK, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 25, .attack_slash = 25, .attack_crush = 25, + .attack_magic = -6, .attack_ranged = -8, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 12, .ranged_strength = 0, .magic_damage = 0, .prayer = 2, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_OATHPLATE_CHEST] = { + .item_id = 30753, .name = "Oathplate chest", .slot = SLOT_BODY, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 16, .attack_crush = 0, + .attack_magic = -16, .attack_ranged = -18, + .defence_stab = 105, .defence_slash = 128, .defence_crush = 100, + .defence_magic = -5, .defence_ranged = 112, + .melee_strength = 4, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_OATHPLATE_LEGS] = { + .item_id = 30756, .name = "Oathplate legs", .slot = SLOT_LEGS, + .attack_speed = 0, .attack_range = 0, + .attack_stab = 0, .attack_slash = 12, .attack_crush = 0, + .attack_magic = -12, .attack_ranged = -14, + .defence_stab = 75, .defence_slash = 100, .defence_crush = 73, + .defence_magic = -3, .defence_ranged = 81, + .melee_strength = 2, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, + [ITEM_VENATOR_BOW] = { + .item_id = 27610, .name = "Venator bow", .slot = SLOT_WEAPON, + .two_handed = 1, + .attack_speed = 5, .attack_range = 6, + .attack_stab = 0, .attack_slash = 0, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 90, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 0, .ranged_strength = 25, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_VENATOR_BOUNCE + }, + [ITEM_ABYSSAL_TENTACLE] = { + .item_id = 12006, .name = "Abyssal tentacle", .slot = SLOT_WEAPON, + .attack_speed = 4, .attack_range = 1, + .attack_stab = 0, .attack_slash = 90, .attack_crush = 0, + .attack_magic = 0, .attack_ranged = 0, + .defence_stab = 0, .defence_slash = 0, .defence_crush = 0, + .defence_magic = 0, .defence_ranged = 0, + .melee_strength = 86, .ranged_strength = 0, .magic_damage = 0, .prayer = 0, .effect_mask = OSRS_ITEM_EFFECT_NONE + }, +}; + +#endif diff --git a/ocean/osrs/osrs_lab.h b/ocean/osrs/osrs_lab.h new file mode 100644 index 0000000000..e30e9d8b32 --- /dev/null +++ b/ocean/osrs/osrs_lab.h @@ -0,0 +1,299 @@ +#ifndef OSRS_LAB_H +#define OSRS_LAB_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +typedef enum { + ENCOUNTER_LAB_OPTIONAL_INT_UNSET = 0, + ENCOUNTER_LAB_OPTIONAL_INT_SET, +} EncounterLabOptionalIntKind; + +typedef struct { + EncounterLabOptionalIntKind kind; + int value; +} EncounterLabOptionalInt; + +static inline EncounterLabOptionalInt encounter_lab_optional_int_unset(void) { + return (EncounterLabOptionalInt){ .kind = ENCOUNTER_LAB_OPTIONAL_INT_UNSET }; +} + +static inline EncounterLabOptionalInt encounter_lab_optional_int_set(int value) { + return (EncounterLabOptionalInt){ + .kind = ENCOUNTER_LAB_OPTIONAL_INT_SET, + .value = value, + }; +} + +typedef struct { + char* data; + size_t len; + size_t cap; + const char* owner_label; +} EncounterLabString; + +static inline void encounter_lab_abort(const char* owner_label, const char* fmt, ...) { + va_list args; + fprintf(stderr, "%s: ", owner_label); + va_start(args, fmt); + vfprintf(stderr, fmt, args); + va_end(args); + fprintf(stderr, "\n"); + abort(); +} + +static inline void encounter_lab_string_init( + EncounterLabString* out, const char* owner_label +) { + out->len = 0; + out->cap = 4096; + out->owner_label = owner_label; + out->data = (char*)malloc(out->cap); + if (!out->data) encounter_lab_abort(owner_label, "out of memory"); + out->data[0] = '\0'; +} + +static inline void encounter_lab_string_reserve(EncounterLabString* out, size_t need) { + if (need <= out->cap) return; + size_t next = out->cap; + while (next < need) { + if (next > SIZE_MAX / 2) + encounter_lab_abort(out->owner_label, "json output too large"); + next *= 2; + } + char* data = (char*)realloc(out->data, next); + if (!data) encounter_lab_abort(out->owner_label, "out of memory"); + out->data = data; + out->cap = next; +} + +static inline void encounter_lab_string_append(EncounterLabString* out, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + va_list copy; + va_copy(copy, args); + int needed = vsnprintf(NULL, 0, fmt, copy); + va_end(copy); + if (needed < 0) encounter_lab_abort(out->owner_label, "json formatting failed"); + encounter_lab_string_reserve(out, out->len + (size_t)needed + 1); + int written = vsnprintf(out->data + out->len, out->cap - out->len, fmt, args); + va_end(args); + if (written != needed) + encounter_lab_abort(out->owner_label, "json formatting length mismatch"); + out->len += (size_t)written; +} + +static inline int encounter_lab_parse_int_value( + const char* owner_label, const char* value +) { + char* end = NULL; + errno = 0; + long parsed = strtol(value, &end, 10); + if (errno != 0 || !end || *end != '\0' || + parsed < INT32_MIN || parsed > INT32_MAX) { + encounter_lab_abort(owner_label, "invalid integer %s", value); + } + return (int)parsed; +} + +static inline uint32_t encounter_lab_parse_seed_value( + const char* owner_label, const char* value +) { + char* end = NULL; + errno = 0; + unsigned long parsed = strtoul(value, &end, 10); + if (errno != 0 || !end || *end != '\0' || parsed > UINT32_MAX) + encounter_lab_abort(owner_label, "invalid seed %s", value); + return (uint32_t)parsed; +} + +static inline EncounterLabOptionalInt encounter_lab_parse_optional_full_int( + const char* owner_label, const char* value +) { + if (strcmp(value, "full") == 0) return encounter_lab_optional_int_unset(); + return encounter_lab_optional_int_set( + encounter_lab_parse_int_value(owner_label, value)); +} + +static inline char* encounter_lab_trim(char* s) { + while (*s && isspace((unsigned char)*s)) s++; + char* end = s + strlen(s); + while (end > s && isspace((unsigned char)end[-1])) end--; + *end = '\0'; + return s; +} + +static inline void encounter_lab_parse_key_value( + const char* owner_label, char* token, const char** key, const char** value +) { + char* eq = strchr(token, '='); + if (!eq || eq == token || eq[1] == '\0') + encounter_lab_abort(owner_label, "expected key=value token, got %s", token); + *eq = '\0'; + *key = token; + *value = eq + 1; +} + +static inline char* encounter_lab_next_token(const char* owner_label, char** cursor) { + if (!cursor || !*cursor) encounter_lab_abort(owner_label, "null token cursor"); + char* start = *cursor + strspn(*cursor, " \t\r\n"); + if (*start == '\0') { + *cursor = start; + return NULL; + } + char* end = start + strcspn(start, " \t\r\n"); + if (*end != '\0') { + *end = '\0'; + *cursor = end + 1; + } else { + *cursor = end; + } + return start; +} + +typedef struct { + char* buffer; + char* cursor; + const char* command; +} EncounterLabLine; + +static inline EncounterLabLine encounter_lab_line_begin( + const char* owner_label, const char* line +) { + if (!line) encounter_lab_abort(owner_label, "null script line"); + size_t len = strlen(line); + char* buffer = (char*)malloc(len + 1); + if (!buffer) encounter_lab_abort(owner_label, "out of memory"); + memcpy(buffer, line, len + 1); + char* text = encounter_lab_trim(buffer); + EncounterLabLine out = { .buffer = buffer, .cursor = text, .command = NULL }; + if (*text == '\0' || *text == '#') return out; + out.command = encounter_lab_next_token(owner_label, &out.cursor); + return out; +} + +typedef struct { + const char* name; + int kind; +} EncounterLabCommandAlias; + +static inline int encounter_lab_lookup_command_kind( + const char* owner_label, + const char* command, + const EncounterLabCommandAlias* aliases, + size_t alias_count +) { + for (size_t i = 0; i < alias_count; i++) { + if (strcmp(command, aliases[i].name) == 0) return aliases[i].kind; + } + encounter_lab_abort(owner_label, "unknown script command %s", command); + return 0; +} + +typedef struct { + uint32_t magic; + uint32_t version; + uint32_t state_size; + uint32_t reserved; +} EncounterSnapshotFrame; + +static inline void encounter_snapshot_write_frame( + void* snapshot, + size_t snapshot_size, + uint32_t magic, + uint32_t version, + size_t state_size +) { + memset(snapshot, 0, snapshot_size); + EncounterSnapshotFrame* frame = (EncounterSnapshotFrame*)snapshot; + frame->magic = magic; + frame->version = version; + frame->state_size = (uint32_t)state_size; +} + +static inline const EncounterSnapshotFrame* encounter_snapshot_validate_frame( + const char* owner_label, + const void* data, + size_t actual_size, + size_t expected_size, + uint32_t magic, + uint32_t version, + size_t state_size +) { + if (actual_size != expected_size) { + fprintf(stderr, "%s: bad snapshot size %zu (expected %zu)\n", + owner_label, actual_size, expected_size); + abort(); + } + const EncounterSnapshotFrame* frame = (const EncounterSnapshotFrame*)data; + if (frame->magic != magic || frame->version != version) { + fprintf(stderr, "%s: bad magic/version (got 0x%08x v%u, want 0x%08x v%u)\n", + owner_label, frame->magic, frame->version, magic, version); + abort(); + } + if (frame->state_size != state_size) { + fprintf(stderr, "%s: state size mismatch (got %u, want %zu)\n", + owner_label, frame->state_size, state_size); + abort(); + } + return frame; +} + +static inline void encounter_snapshot_copy_state_to( + void* snapshot, size_t state_offset, const void* state, size_t state_size +) { + memcpy((uint8_t*)snapshot + state_offset, state, state_size); +} + +static inline void encounter_snapshot_copy_state_from( + void* state, const void* snapshot, size_t state_offset, size_t state_size +) { + memcpy(state, (const uint8_t*)snapshot + state_offset, state_size); +} + +static inline void encounter_write_terminal_status_text( + int episode_over, + int winner, + int win_outcome, + const char* win_text, + const char* killed_by_name, + char* out, + size_t cap +) { + if (cap == 0) return; + out[0] = '\0'; + if (!episode_over) return; + if (winner == win_outcome) { + snprintf(out, cap, "%s", win_text); + return; + } + snprintf(out, cap, "Killed by %s", killed_by_name); +} + +typedef int (*EncounterLabApplyLineAllocJsonFn)( + void* lab_state, const char* line, char** out_json); + +static inline int encounter_apply_lab_command_dump_wrapper( + void* lab_state, + const char* line, + int dump_result, + EncounterLabApplyLineAllocJsonFn apply_line_alloc_json +) { + char* json = NULL; + int result = apply_line_alloc_json(lab_state, line, &json); + if (result == dump_result && json) { + printf("%s\n", json); + fflush(stdout); + } + free(json); + return result == dump_result ? 1 : 0; +} + +#endif diff --git a/ocean/osrs/osrs_models.h b/ocean/osrs/osrs_models.h new file mode 100644 index 0000000000..f6f57dcf57 --- /dev/null +++ b/ocean/osrs/osrs_models.h @@ -0,0 +1,607 @@ +#ifndef OSRS_MODELS_H +#define OSRS_MODELS_H + +#if __has_include("raylib.h") +#include "raylib.h" +#elif __has_include("raylib-5.5_macos/include/raylib.h") +#include "raylib-5.5_macos/include/raylib.h" +#else +#error "raylib.h not found" +#endif +#include "osrs_assets.h" +#include "osrs_binary_io.h" +#include "osrs_types.h" +#include "osrs_items.h" +#include "data/item_models.h" +#include "data/player_models.h" +#include +#include +#include +#include + +#define MDL4_MAGIC 0x4D444C34 +#define ATLS_MAGIC 0x41544C53 +#define TANM_MAGIC 0x4D4E4154 +#define TANM_VERSION 1 +#define MODEL_CACHE_DENSE_INDEX_LIMIT 0x100000u + +typedef struct { + uint32_t texture_id; + uint16_t x, y, w, h; + uint8_t direction; + uint8_t speed; + uint16_t pad; +} ModelTextureAnimRow; + +typedef struct { + uint32_t model_id; + Mesh mesh; + Model model; + + int16_t* base_vertices; + uint8_t* vertex_skins; + uint16_t* face_indices; + uint8_t* face_priorities; + uint8_t* base_face_alphas; + uint8_t* face_alpha_labels; + uint16_t base_vert_count; + +} OsrsModel; + +typedef struct { + OsrsModel* models; + int* index_by_id; + int count; + size_t index_limit; + Texture2D atlas_texture; + unsigned char* atlas_base_pixels; + unsigned char* atlas_pixels; + ModelTextureAnimRow* texture_anims; + int atlas_width; + int atlas_height; + int texture_anim_count; + float texture_anim_ticks; + int has_atlas; +} ModelCache; + +typedef enum { + OSRS_MODEL_APPEND_OK, + OSRS_MODEL_APPEND_EMPTY, + OSRS_MODEL_APPEND_BASE_VERTEX_OVERFLOW, + OSRS_MODEL_APPEND_FACE_OVERFLOW, +} OsrsModelAppendResult; + +static const char* osrs_model_append_result_name(OsrsModelAppendResult result) { + switch (result) { + case OSRS_MODEL_APPEND_OK: + return "ok"; + case OSRS_MODEL_APPEND_EMPTY: + return "empty model"; + case OSRS_MODEL_APPEND_BASE_VERTEX_OVERFLOW: + return "base vertex overflow"; + case OSRS_MODEL_APPEND_FACE_OVERFLOW: + return "face overflow"; + } + return "unknown append result"; +} + +static OsrsModelAppendResult osrs_model_append_check( + int current_base_vert_count, + int current_face_count, + const OsrsModel* model, + int base_vert_capacity, + int face_capacity +) { + if (!model || !model->base_vertices || model->base_vert_count == 0) { + return OSRS_MODEL_APPEND_EMPTY; + } + if (current_base_vert_count < 0 || current_face_count < 0 || + base_vert_capacity < 0 || face_capacity < 0 || + model->mesh.triangleCount < 0) { + fprintf(stderr, "osrs_model_append_check: invalid composite geometry counts\n"); + abort(); + } + + int model_base_vert_count = (int)model->base_vert_count; + int model_face_count = model->mesh.triangleCount; + if (model_base_vert_count > base_vert_capacity || + current_base_vert_count > base_vert_capacity - model_base_vert_count) { + return OSRS_MODEL_APPEND_BASE_VERTEX_OVERFLOW; + } + if (model_face_count > face_capacity || + current_face_count > face_capacity - model_face_count) { + return OSRS_MODEL_APPEND_FACE_OVERFLOW; + } + return OSRS_MODEL_APPEND_OK; +} + +static int model_cache_companion_path( + char* out, + size_t cap, + const char* path, + const char* suffix +) { + if (!out || cap == 0 || !path || !suffix) return 0; + const char* dot = strrchr(path, '.'); + size_t stem_len = dot ? (size_t)(dot - path) : strlen(path); + int n = snprintf(out, cap, "%.*s%s", (int)stem_len, path, suffix); + return n > 0 && (size_t)n < cap; +} + +static void model_cache_load_texture_anims(ModelCache* cache, const char* model_path) { + if (!cache || !model_path) return; + + char tanm_path[1024]; + if (!model_cache_companion_path(tanm_path, sizeof(tanm_path), model_path, ".tanim")) + return; + + FILE* f = osrs_asset_fopen(tanm_path, "rb"); + if (!f) return; + + uint32_t magic = 0; + uint32_t version = 0; + uint32_t count = 0; + osrs_read_exact(f, &magic, sizeof(magic), 1, tanm_path, "tanim magic"); + osrs_read_exact(f, &version, sizeof(version), 1, tanm_path, "tanim version"); + osrs_read_exact(f, &count, sizeof(count), 1, tanm_path, "tanim count"); + if (magic != TANM_MAGIC || version != TANM_VERSION) { + fprintf(stderr, "model_cache_load: bad texture anim file %s\n", tanm_path); + abort(); + } + + cache->texture_anims = (ModelTextureAnimRow*)osrs_calloc_or_abort( + count, sizeof(ModelTextureAnimRow), "model texture animation rows"); + cache->texture_anim_count = (int)count; + + for (uint32_t i = 0; i < count; i++) { + ModelTextureAnimRow* row = &cache->texture_anims[i]; + osrs_read_exact(f, &row->texture_id, sizeof(row->texture_id), 1, + tanm_path, "tanim texture id"); + osrs_read_exact(f, &row->x, sizeof(row->x), 1, tanm_path, "tanim x"); + osrs_read_exact(f, &row->y, sizeof(row->y), 1, tanm_path, "tanim y"); + osrs_read_exact(f, &row->w, sizeof(row->w), 1, tanm_path, "tanim width"); + osrs_read_exact(f, &row->h, sizeof(row->h), 1, tanm_path, "tanim height"); + osrs_read_exact(f, &row->direction, sizeof(row->direction), 1, + tanm_path, "tanim direction"); + osrs_read_exact(f, &row->speed, sizeof(row->speed), 1, tanm_path, "tanim speed"); + osrs_read_exact(f, &row->pad, sizeof(row->pad), 1, tanm_path, "tanim pad"); + } + fclose(f); + + fprintf(stderr, "model_cache_load: loaded %d texture anim rows from %s\n", + cache->texture_anim_count, tanm_path); +} + +static void model_cache_update_texture_anims(ModelCache* cache, float dt) { + if (!cache || !cache->atlas_pixels || !cache->atlas_base_pixels || + cache->atlas_texture.id <= 0 || cache->texture_anim_count <= 0) { + return; + } + + cache->texture_anim_ticks += dt * 50.0f; + size_t total = (size_t)cache->atlas_width * (size_t)cache->atlas_height * 4; + memcpy(cache->atlas_pixels, cache->atlas_base_pixels, total); + + for (int r = 0; r < cache->texture_anim_count; r++) { + ModelTextureAnimRow* row = &cache->texture_anims[r]; + if (row->w == 0 || row->h == 0 || row->speed == 0) continue; + if ((int)row->x + (int)row->w > cache->atlas_width || + (int)row->y + (int)row->h > cache->atlas_height) { + continue; + } + + int shift = (int)(cache->texture_anim_ticks * (float)row->speed); + if (row->direction == 1 || row->direction == 3) { + int pad = row->pad; + if (pad * 2 >= row->h) pad = 0; + int center_h = row->h - pad * 2; + if (center_h <= 0) center_h = row->h; + shift %= center_h; + if (row->direction == 1) shift = -shift; + for (int y = 0; y < row->h; y++) { + int sy = (y - pad + shift) % center_h; + if (sy < 0) sy += center_h; + sy += pad; + for (int x = 0; x < row->w; x++) { + size_t dst = ((size_t)(row->y + y) * (size_t)cache->atlas_width + + (size_t)(row->x + x)) * 4; + size_t src = ((size_t)(row->y + sy) * (size_t)cache->atlas_width + + (size_t)(row->x + x)) * 4; + memcpy(&cache->atlas_pixels[dst], &cache->atlas_base_pixels[src], 4); + } + } + } else if (row->direction == 2 || row->direction == 4) { + shift %= row->w; + if (row->direction == 2) shift = -shift; + for (int y = 0; y < row->h; y++) { + for (int x = 0; x < row->w; x++) { + int sx = (x + shift) % row->w; + if (sx < 0) sx += row->w; + size_t dst = ((size_t)(row->y + y) * (size_t)cache->atlas_width + + (size_t)(row->x + x)) * 4; + size_t src = ((size_t)(row->y + y) * (size_t)cache->atlas_width + + (size_t)(row->x + sx)) * 4; + memcpy(&cache->atlas_pixels[dst], &cache->atlas_base_pixels[src], 4); + } + } + } + } + + UpdateTexture(cache->atlas_texture, cache->atlas_pixels); +} + +static Texture2D model_cache_load_atlas(ModelCache* cache, const char* model_path) { + char atlas_path[1024]; + if (!model_cache_companion_path(atlas_path, sizeof(atlas_path), model_path, ".atlas")) { + return (Texture2D){0}; + } + + FILE* f = osrs_asset_fopen(atlas_path, "rb"); + if (!f) return (Texture2D){0}; + + uint32_t magic, width, height; + osrs_read_exact(f, &magic, 4, 1, atlas_path, "atlas magic"); + osrs_read_exact(f, &width, 4, 1, atlas_path, "atlas width"); + osrs_read_exact(f, &height, 4, 1, atlas_path, "atlas height"); + if (magic != ATLS_MAGIC || width == 0 || height == 0) { + fprintf(stderr, "model_cache_load: bad atlas %s\n", atlas_path); + abort(); + } + + size_t pixel_count = (size_t)width * (size_t)height * 4; + unsigned char* pixels = (unsigned char*)osrs_malloc_or_abort( + pixel_count, "model atlas pixels"); + osrs_read_exact(f, pixels, 1, pixel_count, atlas_path, "atlas pixels"); + fclose(f); + + Image image = { + .data = pixels, + .width = (int)width, + .height = (int)height, + .mipmaps = 1, + .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, + }; + Texture2D texture = LoadTextureFromImage(image); + if (texture.id > 0) SetTextureFilter(texture, TEXTURE_FILTER_POINT); + if (texture.id > 0 && cache) { + cache->atlas_width = (int)width; + cache->atlas_height = (int)height; + cache->atlas_base_pixels = (unsigned char*)osrs_malloc_or_abort( + pixel_count, "model atlas base pixels"); + cache->atlas_pixels = (unsigned char*)osrs_malloc_or_abort( + pixel_count, "model atlas working pixels"); + memcpy(cache->atlas_base_pixels, pixels, pixel_count); + memcpy(cache->atlas_pixels, pixels, pixel_count); + model_cache_load_texture_anims(cache, model_path); + } + free(pixels); + fprintf(stderr, "model_cache_load: loaded atlas %ux%u from %s\n", width, height, atlas_path); + return texture; +} + +static size_t model_cache_index_limit_or_abort( + FILE* f, + const uint32_t* offsets, + uint32_t count, + const char* path +) { + size_t index_limit = 0; + for (uint32_t i = 0; i < count; i++) { + osrs_seek_or_abort(f, (long)offsets[i], path); + uint32_t model_id = 0; + osrs_read_exact(f, &model_id, sizeof(model_id), 1, path, "model id"); + if (model_id < MODEL_CACHE_DENSE_INDEX_LIMIT) { + size_t candidate = (size_t)model_id + 1; + if (candidate > index_limit) index_limit = candidate; + } + } + return index_limit; +} + +static void model_cache_init_index(ModelCache* cache, size_t limit) { + if (limit == 0) return; + if (limit > SIZE_MAX / sizeof(int)) { + fprintf(stderr, "model_cache_load: model id index too large for %zu slots\n", limit); + abort(); + } + + cache->index_by_id = (int*)osrs_malloc_or_abort( + limit * sizeof(int), "model id index"); + cache->index_limit = limit; + for (size_t i = 0; i < limit; i++) { + cache->index_by_id[i] = -1; + } +} + +static void model_cache_set_index(ModelCache* cache, uint32_t model_id, int index) { + if (!cache->index_by_id || (size_t)model_id >= cache->index_limit) return; + cache->index_by_id[model_id] = index; +} + +static ModelCache* model_cache_load(const char* path) { + FILE* f = osrs_asset_fopen(path, "rb"); + if (!f) { + fprintf(stderr, "model_cache_load: cannot open %s\n", path); + return NULL; + } + + uint32_t magic, count; + osrs_read_exact(f, &magic, 4, 1, path, "magic"); + osrs_read_exact(f, &count, 4, 1, path, "model count"); + + if (magic != MDL4_MAGIC) { + fprintf(stderr, "model_cache_load: bad magic 0x%08X (expected MDL4) in %s\n", + magic, path); + abort(); + } + + uint32_t* offsets = (uint32_t*)osrs_malloc_or_abort( + count * sizeof(uint32_t), "model offsets"); + osrs_read_exact(f, offsets, 4, count, path, "model offsets"); + + if (count > (uint32_t)INT_MAX) { + fprintf(stderr, "model_cache_load: too many models: %u\n", count); + abort(); + } + + ModelCache* cache = (ModelCache*)osrs_calloc_or_abort( + 1, sizeof(ModelCache), "model cache"); + cache->models = (OsrsModel*)osrs_calloc_or_abort( + count, sizeof(OsrsModel), "model entries"); + cache->count = (int)count; + if (count > 0) { + size_t index_limit = model_cache_index_limit_or_abort(f, offsets, count, path); + model_cache_init_index(cache, index_limit); + } + cache->atlas_texture = model_cache_load_atlas(cache, path); + cache->has_atlas = cache->atlas_texture.id > 0; + if (!cache->has_atlas) { + fprintf(stderr, "model_cache_load: MDL4 model set requires a sibling .atlas file: %s\n", path); + abort(); + } + + for (uint32_t i = 0; i < count; i++) { + osrs_seek_or_abort(f, (long)offsets[i], path); + + uint32_t model_id; + uint16_t vert_count, face_count, base_vert_count; + osrs_read_exact(f, &model_id, 4, 1, path, "model id"); + osrs_read_exact(f, &vert_count, 2, 1, path, "expanded vertex count"); + osrs_read_exact(f, &face_count, 2, 1, path, "face count"); + osrs_read_exact(f, &base_vert_count, 2, 1, path, "base vertex count"); + + cache->models[i].model_id = model_id; + cache->models[i].base_vert_count = base_vert_count; + model_cache_set_index(cache, model_id, (int)i); + + Mesh mesh = { 0 }; + mesh.vertexCount = vert_count; + mesh.triangleCount = face_count; + + mesh.vertices = (float*)RL_MALLOC(vert_count * 3 * sizeof(float)); + mesh.colors = (unsigned char*)RL_MALLOC(vert_count * 4); + mesh.texcoords = (float*)RL_MALLOC(vert_count * 2 * sizeof(float)); + if (!mesh.vertices || !mesh.colors || !mesh.texcoords) { + fprintf(stderr, "model_cache_load: raylib mesh allocation failed for model %u\n", + model_id); + abort(); + } + + osrs_read_exact(f, mesh.vertices, sizeof(float), vert_count * 3, path, "expanded vertices"); + osrs_read_exact(f, mesh.colors, 1, vert_count * 4, path, "vertex colors"); + osrs_read_exact(f, mesh.texcoords, sizeof(float), vert_count * 2, path, "texcoords"); + + cache->models[i].base_vertices = (int16_t*)osrs_malloc_or_abort( + base_vert_count * 3 * sizeof(int16_t), "model base vertices"); + osrs_read_exact(f, cache->models[i].base_vertices, sizeof(int16_t), + base_vert_count * 3, path, "base vertices"); + + cache->models[i].vertex_skins = (uint8_t*)osrs_malloc_or_abort( + base_vert_count, "model vertex skins"); + osrs_read_exact(f, cache->models[i].vertex_skins, 1, + base_vert_count, path, "vertex skins"); + + cache->models[i].face_indices = (uint16_t*)osrs_malloc_or_abort( + face_count * 3 * sizeof(uint16_t), "model face indices"); + osrs_read_exact(f, cache->models[i].face_indices, sizeof(uint16_t), + face_count * 3, path, "face indices"); + + cache->models[i].face_priorities = (uint8_t*)osrs_malloc_or_abort( + face_count, "model face priorities"); + osrs_read_exact(f, cache->models[i].face_priorities, 1, + face_count, path, "face priorities"); + + cache->models[i].base_face_alphas = (uint8_t*)osrs_malloc_or_abort( + face_count, "model base face alphas"); + osrs_read_exact(f, cache->models[i].base_face_alphas, 1, + face_count, path, "base face alphas"); + cache->models[i].face_alpha_labels = (uint8_t*)osrs_malloc_or_abort( + face_count, "model face alpha labels"); + osrs_read_exact(f, cache->models[i].face_alpha_labels, 1, + face_count, path, "face alpha labels"); + + for (uint16_t fp = 0; fp < face_count; fp++) { + if (cache->models[i].base_face_alphas[fp] == 0 && + mesh.colors[(fp * 3) * 4 + 3] == 0) + cache->models[i].base_face_alphas[fp] = 255; + } + + UploadMesh(&mesh, false); + cache->models[i].mesh = mesh; + cache->models[i].model = LoadModelFromMesh(mesh); + if (cache->has_atlas) { + cache->models[i].model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = + cache->atlas_texture; + } + } + + free(offsets); + fclose(f); + + fprintf(stderr, "model_cache_load: loaded %d models from %s\n", cache->count, path); + return cache; +} + +static OsrsModel* model_cache_get(ModelCache* cache, uint32_t model_id) { + if (!cache) return NULL; + if (cache->index_by_id && (size_t)model_id < cache->index_limit) { + int idx = cache->index_by_id[model_id]; + if (idx >= 0 && idx < cache->count && cache->models[idx].model_id == model_id) { + return &cache->models[idx]; + } + } + for (int i = 0; i < cache->count; i++) { + if (cache->models[i].model_id == model_id) { + return &cache->models[i]; + } + } + return NULL; +} + +typedef struct { + uint32_t hide_body_mask; + uint32_t body_model_ids[BODY_PART_COUNT]; + uint32_t item_model_ids[NUM_GEAR_SLOTS]; + uint8_t body_visible[BODY_PART_COUNT]; + uint8_t item_visible[NUM_GEAR_SLOTS]; +} OsrsPlayerAppearance; + +#define OSRS_VISIBLE_EQUIP_SLOT_COUNT 9 + +static const int OSRS_VISIBLE_EQUIP_SLOTS[OSRS_VISIBLE_EQUIP_SLOT_COUNT] = { + GEAR_SLOT_HEAD, + GEAR_SLOT_CAPE, + GEAR_SLOT_NECK, + GEAR_SLOT_WEAPON, + GEAR_SLOT_SHIELD, + GEAR_SLOT_BODY, + GEAR_SLOT_LEGS, + GEAR_SLOT_HANDS, + GEAR_SLOT_FEET, +}; + +static const ItemModelMapping* item_model_mapping_for_item(uint16_t item_id) { + for (int i = 0; i < ITEM_MODEL_COUNT; i++) { + if (ITEM_MODEL_MAP[i].item_id == item_id) { + return &ITEM_MODEL_MAP[i]; + } + } + return NULL; +} + +static uint32_t item_to_wield_model(uint16_t item_id) { + const ItemModelMapping* mapping = item_model_mapping_for_item(item_id); + if (!mapping) return ITEM_RENDER_MODEL_MISSING; + return mapping->wield_model; +} + +static uint32_t item_hide_body_mask(uint16_t item_id) { + const ItemModelMapping* mapping = item_model_mapping_for_item(item_id); + if (!mapping) return 0; + return mapping->hide_body_mask; +} + +static inline uint32_t item_render_equip_slot(uint16_t item_id) { + const ItemModelMapping* mapping = item_model_mapping_for_item(item_id); + if (!mapping) return ITEM_RENDER_MODEL_MISSING; + return mapping->equip_slot; +} + +static uint32_t item_render_flags(uint16_t item_id) { + const ItemModelMapping* mapping = item_model_mapping_for_item(item_id); + if (!mapping) return 0; + return mapping->render_flags; +} + +static uint32_t item_render_ready_anim(uint16_t item_id) { + const ItemModelMapping* mapping = item_model_mapping_for_item(item_id); + if (!mapping) return ITEM_RENDER_MODEL_MISSING; + return mapping->ready_anim_id; +} + +static uint32_t item_render_walk_anim(uint16_t item_id) { + const ItemModelMapping* mapping = item_model_mapping_for_item(item_id); + if (!mapping) return ITEM_RENDER_MODEL_MISSING; + return mapping->walk_anim_id; +} + +static uint32_t item_render_run_anim(uint16_t item_id) { + const ItemModelMapping* mapping = item_model_mapping_for_item(item_id); + if (!mapping) return ITEM_RENDER_MODEL_MISSING; + return mapping->run_anim_id; +} + +static int item_render_is_two_handed(uint16_t item_id) { + return (item_render_flags(item_id) & ITEM_RENDER_FLAG_TWO_HANDED) != 0; +} + +static OsrsPlayerAppearance osrs_resolve_player_appearance( + const uint8_t equipped[NUM_GEAR_SLOTS] +) { + OsrsPlayerAppearance out; + memset(&out, 0, sizeof(out)); + + for (int bp = 0; bp < BODY_PART_COUNT; bp++) { + out.body_model_ids[bp] = ITEM_RENDER_MODEL_MISSING; + } + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + out.item_model_ids[slot] = ITEM_RENDER_MODEL_MISSING; + } + + uint8_t weapon_index = equipped[GEAR_SLOT_WEAPON]; + int suppress_shield = 0; + if (weapon_index < NUM_ITEMS) { + uint16_t weapon_item_id = ITEM_DATABASE[weapon_index].item_id; + suppress_shield = item_is_two_handed(weapon_index) || + item_render_is_two_handed(weapon_item_id); + } + + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + if (slot == GEAR_SLOT_SHIELD && suppress_shield) continue; + uint8_t db_idx = equipped[slot]; + if (db_idx >= NUM_ITEMS) continue; + out.hide_body_mask |= item_hide_body_mask(ITEM_DATABASE[db_idx].item_id); + } + + for (int bp = 0; bp < BODY_PART_COUNT; bp++) { + uint32_t model_id = DEFAULT_BODY_MODELS[bp]; + out.body_model_ids[bp] = model_id; + out.body_visible[bp] = ((out.hide_body_mask & (1u << bp)) == 0) && + model_id != ITEM_RENDER_MODEL_MISSING; + } + + for (int i = 0; i < OSRS_VISIBLE_EQUIP_SLOT_COUNT; i++) { + int slot = OSRS_VISIBLE_EQUIP_SLOTS[i]; + if (slot == GEAR_SLOT_SHIELD && suppress_shield) continue; + uint8_t db_idx = equipped[slot]; + if (db_idx >= NUM_ITEMS) continue; + uint16_t item_id = ITEM_DATABASE[db_idx].item_id; + uint32_t model_id = item_to_wield_model(item_id); + out.item_model_ids[slot] = model_id; + out.item_visible[slot] = model_id != ITEM_RENDER_MODEL_MISSING; + } + + return out; +} + +static void model_cache_free(ModelCache* cache) { + if (!cache) return; + for (int i = 0; i < cache->count; i++) { + UnloadModel(cache->models[i].model); + free(cache->models[i].base_vertices); + free(cache->models[i].vertex_skins); + free(cache->models[i].face_indices); + free(cache->models[i].face_priorities); + free(cache->models[i].base_face_alphas); + free(cache->models[i].face_alpha_labels); + } + if (cache->atlas_texture.id > 0) UnloadTexture(cache->atlas_texture); + free(cache->atlas_base_pixels); + free(cache->atlas_pixels); + free(cache->texture_anims); + free(cache->index_by_id); + free(cache->models); + free(cache); +} + +#endif diff --git a/ocean/osrs/osrs_monsters_generated.h b/ocean/osrs/osrs_monsters_generated.h new file mode 100644 index 0000000000..85dc9a17d2 --- /dev/null +++ b/ocean/osrs/osrs_monsters_generated.h @@ -0,0 +1,245 @@ +#ifndef OSRS_MONSTERS_GENERATED_H +#define OSRS_MONSTERS_GENERATED_H + +#include + +typedef enum { + MON_JAL_NIB = 0, + MON_JAL_MEJRAH = 1, + MON_JAL_AK = 2, + MON_JAL_AKREK_MEJ = 3, + MON_JAL_AKREK_XIL = 4, + MON_JAL_AKREK_KET = 5, + MON_JAL_IMKOT = 6, + MON_JAL_XIL = 7, + MON_JAL_ZEK = 8, + MON_JALTOK_JAD = 9, + MON_YT_HURKOT = 10, + MON_TZKAL_ZUK = 11, + MON_ZUK_SHIELD = 12, + MON_JAL_MEJJAK = 13, + MON_ZULRAH_GREEN = 14, + MON_ZULRAH_RED = 15, + MON_ZULRAH_BLUE = 16, + MON_ZULRAH_SNAKELING_MELEE = 17, + MON_ZULRAH_SNAKELING_MAGIC = 18, + NUM_MONSTERS = 19 +} MonsterIndex; + +typedef struct { + uint16_t npc_id; + char name[32]; + int16_t hp; + int16_t att_level; + int16_t str_level; + int16_t def_level; + int16_t magic_level; + int16_t range_level; + uint8_t attack_speed; + uint8_t size; + int16_t max_hit; + int16_t melee_att_bonus; + int16_t melee_str_bonus; + int16_t magic_att_bonus; + int16_t magic_str_bonus; + int16_t range_att_bonus; + int16_t ranged_str_bonus; + int16_t stab_def; + int16_t slash_def; + int16_t crush_def; + int16_t magic_def; + int16_t ranged_def; +} MonsterStats; + +static const MonsterStats MONSTER_DATABASE[NUM_MONSTERS] = { + [MON_JAL_NIB] = { + .npc_id = 7691, .name = "Jal-Nib", + .hp = 10, .att_level = 1, .str_level = 1, .def_level = 15, + .magic_level = 15, .range_level = 1, + .attack_speed = 4, .size = 1, .max_hit = 4, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 0, .magic_str_bonus = 0, + .range_att_bonus = 0, .ranged_str_bonus = 0, + .stab_def = -20, .slash_def = -20, .crush_def = -20, + .magic_def = -20, .ranged_def = -20 + }, + [MON_JAL_MEJRAH] = { + .npc_id = 7692, .name = "Jal-MejRah", + .hp = 25, .att_level = 0, .str_level = 0, .def_level = 55, + .magic_level = 120, .range_level = 120, + .attack_speed = 3, .size = 2, .max_hit = 19, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 0, .magic_str_bonus = 0, + .range_att_bonus = 30, .ranged_str_bonus = 30, + .stab_def = 30, .slash_def = 30, .crush_def = 30, + .magic_def = -20, .ranged_def = 45 + }, + [MON_JAL_AK] = { + .npc_id = 7693, .name = "Jal-Ak", + .hp = 40, .att_level = 160, .str_level = 160, .def_level = 95, + .magic_level = 160, .range_level = 160, + .attack_speed = 6, .size = 3, .max_hit = 29, + .melee_att_bonus = 0, .melee_str_bonus = 45, .magic_att_bonus = 45, .magic_str_bonus = 45, + .range_att_bonus = 45, .ranged_str_bonus = 45, + .stab_def = 25, .slash_def = 25, .crush_def = 25, + .magic_def = 25, .ranged_def = 25 + }, + [MON_JAL_AKREK_MEJ] = { + .npc_id = 7694, .name = "Jal-AkRek-Mej", + .hp = 15, .att_level = 1, .str_level = 1, .def_level = 95, + .magic_level = 120, .range_level = 1, + .attack_speed = 4, .size = 1, .max_hit = 18, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 25, .magic_str_bonus = 25, + .range_att_bonus = 0, .ranged_str_bonus = 0, + .stab_def = 0, .slash_def = 0, .crush_def = 0, + .magic_def = 25, .ranged_def = 0 + }, + [MON_JAL_AKREK_XIL] = { + .npc_id = 7695, .name = "Jal-AkRek-Xil", + .hp = 15, .att_level = 1, .str_level = 1, .def_level = 95, + .magic_level = 1, .range_level = 120, + .attack_speed = 4, .size = 1, .max_hit = 18, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 0, .magic_str_bonus = 0, + .range_att_bonus = 25, .ranged_str_bonus = 25, + .stab_def = 0, .slash_def = 0, .crush_def = 0, + .magic_def = 0, .ranged_def = 25 + }, + [MON_JAL_AKREK_KET] = { + .npc_id = 7696, .name = "Jal-AkRek-Ket", + .hp = 15, .att_level = 120, .str_level = 120, .def_level = 95, + .magic_level = 1, .range_level = 1, + .attack_speed = 4, .size = 1, .max_hit = 18, + .melee_att_bonus = 0, .melee_str_bonus = 25, .magic_att_bonus = 0, .magic_str_bonus = 0, + .range_att_bonus = 0, .ranged_str_bonus = 0, + .stab_def = 25, .slash_def = 25, .crush_def = 25, + .magic_def = 0, .ranged_def = 0 + }, + [MON_JAL_IMKOT] = { + .npc_id = 7697, .name = "Jal-ImKot", + .hp = 75, .att_level = 210, .str_level = 290, .def_level = 120, + .magic_level = 120, .range_level = 220, + .attack_speed = 4, .size = 4, .max_hit = 49, + .melee_att_bonus = 0, .melee_str_bonus = 40, .magic_att_bonus = 0, .magic_str_bonus = 0, + .range_att_bonus = 0, .ranged_str_bonus = 0, + .stab_def = 65, .slash_def = 65, .crush_def = 65, + .magic_def = 30, .ranged_def = 50 + }, + [MON_JAL_XIL] = { + .npc_id = 7698, .name = "Jal-Xil", + .hp = 125, .att_level = 140, .str_level = 180, .def_level = 60, + .magic_level = 90, .range_level = 250, + .attack_speed = 4, .size = 3, .max_hit = 46, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 0, .magic_str_bonus = 0, + .range_att_bonus = 40, .ranged_str_bonus = 50, + .stab_def = 0, .slash_def = 0, .crush_def = 0, + .magic_def = 0, .ranged_def = 0 + }, + [MON_JAL_ZEK] = { + .npc_id = 7699, .name = "Jal-Zek", + .hp = 220, .att_level = 370, .str_level = 510, .def_level = 260, + .magic_level = 300, .range_level = 510, + .attack_speed = 4, .size = 4, .max_hit = 70, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 80, .magic_str_bonus = 0, + .range_att_bonus = 0, .ranged_str_bonus = 0, + .stab_def = 0, .slash_def = 0, .crush_def = 0, + .magic_def = 0, .ranged_def = 0 + }, + [MON_JALTOK_JAD] = { + .npc_id = 7700, .name = "JalTok-Jad", + .hp = 350, .att_level = 750, .str_level = 1020, .def_level = 480, + .magic_level = 510, .range_level = 1020, + .attack_speed = 8, .size = 5, .max_hit = 113, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 100, .magic_str_bonus = 75, + .range_att_bonus = 80, .ranged_str_bonus = 0, + .stab_def = 0, .slash_def = 0, .crush_def = 0, + .magic_def = 0, .ranged_def = 0 + }, + [MON_YT_HURKOT] = { + .npc_id = 7701, .name = "Yt-HurKot", + .hp = 90, .att_level = 165, .str_level = 125, .def_level = 100, + .magic_level = 150, .range_level = 150, + .attack_speed = 4, .size = 1, .max_hit = 18, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 100, .magic_str_bonus = 0, + .range_att_bonus = 80, .ranged_str_bonus = 0, + .stab_def = 0, .slash_def = 0, .crush_def = 0, + .magic_def = 130, .ranged_def = 130 + }, + [MON_TZKAL_ZUK] = { + .npc_id = 7706, .name = "TzKal-Zuk", + .hp = 1200, .att_level = 350, .str_level = 600, .def_level = 260, + .magic_level = 150, .range_level = 400, + .attack_speed = 10, .size = 7, .max_hit = 148, + .melee_att_bonus = 0, .melee_str_bonus = 200, .magic_att_bonus = 550, .magic_str_bonus = 450, + .range_att_bonus = 550, .ranged_str_bonus = 200, + .stab_def = 0, .slash_def = 0, .crush_def = 0, + .magic_def = 350, .ranged_def = 100 + }, + [MON_ZUK_SHIELD] = { + .npc_id = 7707, .name = "Ancestral Glyph", + .hp = 600, .att_level = 0, .str_level = 0, .def_level = 0, + .magic_level = 0, .range_level = 0, + .attack_speed = 0, .size = 5, .max_hit = 0, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 0, .magic_str_bonus = 0, .range_att_bonus = 0, .ranged_str_bonus = 0, + .stab_def = 0, .slash_def = 0, .crush_def = 0, .magic_def = 0, .ranged_def = 0 + }, + [MON_JAL_MEJJAK] = { + .npc_id = 7708, .name = "Jal-MejJak", + .hp = 75, .att_level = 1, .str_level = 1, .def_level = 100, + .magic_level = 1, .range_level = 1, + .attack_speed = 3, .size = 1, .max_hit = 10, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 0, .magic_str_bonus = 0, + .range_att_bonus = 0, .ranged_str_bonus = 0, + .stab_def = 0, .slash_def = 0, .crush_def = 0, + .magic_def = 0, .ranged_def = 0 + }, + [MON_ZULRAH_GREEN] = { + .npc_id = 2042, .name = "Zulrah", + .hp = 500, .att_level = 1, .str_level = 1, .def_level = 300, + .magic_level = 300, .range_level = 300, + .attack_speed = 3, .size = 5, .max_hit = 41, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 50, .magic_str_bonus = 20, + .range_att_bonus = 50, .ranged_str_bonus = 20, + .stab_def = 0, .slash_def = 0, .crush_def = 0, + .magic_def = -45, .ranged_def = 50 + }, + [MON_ZULRAH_RED] = { + .npc_id = 2043, .name = "Zulrah", + .hp = 500, .att_level = 1, .str_level = 1, .def_level = 300, + .magic_level = 300, .range_level = 300, + .attack_speed = 3, .size = 5, .max_hit = 30, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 50, .magic_str_bonus = 20, + .range_att_bonus = 50, .ranged_str_bonus = 20, + .stab_def = 0, .slash_def = 0, .crush_def = 0, + .magic_def = 0, .ranged_def = 300 + }, + [MON_ZULRAH_BLUE] = { + .npc_id = 2044, .name = "Zulrah", + .hp = 500, .att_level = 1, .str_level = 1, .def_level = 300, + .magic_level = 300, .range_level = 300, + .attack_speed = 3, .size = 5, .max_hit = 41, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 50, .magic_str_bonus = 20, + .range_att_bonus = 50, .ranged_str_bonus = 20, + .stab_def = 0, .slash_def = 0, .crush_def = 0, + .magic_def = 300, .ranged_def = 0 + }, + [MON_ZULRAH_SNAKELING_MELEE] = { + .npc_id = 2045, .name = "Snakeling", + .hp = 1, .att_level = 140, .str_level = 138, .def_level = 1, + .magic_level = 1, .range_level = 1, + .attack_speed = 3, .size = 1, .max_hit = 15, + .melee_att_bonus = 120, .melee_str_bonus = 0, .magic_att_bonus = 0, .magic_str_bonus = 0, + .range_att_bonus = 0, .ranged_str_bonus = 0, + .stab_def = -40, .slash_def = -40, .crush_def = -40, + .magic_def = -40, .ranged_def = -40 + }, + [MON_ZULRAH_SNAKELING_MAGIC] = { + .npc_id = 2046, .name = "Snakeling", + .hp = 1, .att_level = 1, .str_level = 1, .def_level = 1, + .magic_level = 185, .range_level = 1, + .attack_speed = 3, .size = 1, .max_hit = 13, + .melee_att_bonus = 0, .melee_str_bonus = 0, .magic_att_bonus = 80, .magic_str_bonus = -20, + .range_att_bonus = 0, .ranged_str_bonus = 0, + .stab_def = -40, .slash_def = -40, .crush_def = -40, + .magic_def = -40, .ranged_def = -40 + }, +}; + +#endif diff --git a/ocean/osrs/osrs_objects.h b/ocean/osrs/osrs_objects.h new file mode 100644 index 0000000000..c172dbf385 --- /dev/null +++ b/ocean/osrs/osrs_objects.h @@ -0,0 +1,199 @@ +#ifndef OSRS_OBJECTS_H +#define OSRS_OBJECTS_H + +#include "raylib.h" +#include "rlgl.h" +#include "osrs_assets.h" +#include "osrs_binary_io.h" +#include +#include +#include +#include + +#define OBJS_MAGIC 0x4F424A53 +#define OBJ2_MAGIC 0x4F424A32 +#define ATLS_MAGIC 0x41544C53 + +typedef struct { + Model model; + Texture2D atlas_texture; + int placement_count; + int total_vertex_count; + int min_world_x; + int min_world_y; + int has_textures; + int loaded; +} ObjectMesh; + +static Texture2D objects_load_atlas(const char* atlas_path) { + Texture2D tex = { 0 }; + FILE* f = osrs_asset_fopen(atlas_path, "rb"); + if (!f) { + fprintf(stderr, "objects_load_atlas: could not open %s\n", atlas_path); + abort(); + } + + uint32_t magic, width, height; + osrs_read_exact(f, &magic, 4, 1, atlas_path, "magic"); + if (magic != ATLS_MAGIC) { + fprintf(stderr, "objects_load_atlas: bad magic %08x (expected ATLS)\n", magic); + abort(); + } + osrs_read_exact(f, &width, 4, 1, atlas_path, "width"); + osrs_read_exact(f, &height, 4, 1, atlas_path, "height"); + + size_t pixel_count = (size_t)width * height; + unsigned char* pixels = (unsigned char*)osrs_calloc_or_abort( + pixel_count, 4, "object atlas pixels"); + osrs_read_exact(f, pixels, 4, pixel_count, atlas_path, "pixels"); + fclose(f); + + Image img = { + .data = pixels, + .width = (int)width, + .height = (int)height, + .mipmaps = 1, + .format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8, + }; + tex = LoadTextureFromImage(img); + SetTextureFilter(tex, TEXTURE_FILTER_POINT); + free(pixels); + + fprintf(stderr, "objects_load_atlas: loaded %ux%u atlas texture\n", width, height); + return tex; +} + +static ObjectMesh* objects_load(const char* path) { + FILE* f = osrs_asset_fopen(path, "rb"); + if (!f) { + fprintf(stderr, "objects_load: could not open %s\n", path); + return NULL; + } + + uint32_t magic, placement_count, total_verts; + int32_t min_wx, min_wy; + osrs_read_exact(f, &magic, 4, 1, path, "magic"); + + int has_textures = 0; + if (magic == OBJ2_MAGIC) { + has_textures = 1; + } else if (magic != OBJS_MAGIC) { + fprintf(stderr, "objects_load: bad magic %08x\n", magic); + abort(); + } + + osrs_read_exact(f, &placement_count, 4, 1, path, "placement count"); + osrs_read_exact(f, &min_wx, 4, 1, path, "min world x"); + osrs_read_exact(f, &min_wy, 4, 1, path, "min world y"); + osrs_read_exact(f, &total_verts, 4, 1, path, "vertex count"); + + fprintf(stderr, "objects_load: %u placements, %u verts, format=%s\n", + placement_count, total_verts, has_textures ? "OBJ2" : "OBJS"); + + float* raw_verts = (float*)osrs_malloc_or_abort( + total_verts * 3 * sizeof(float), "object vertices"); + osrs_read_exact(f, raw_verts, sizeof(float), total_verts * 3, path, "vertices"); + + unsigned char* raw_colors = (unsigned char*)osrs_malloc_or_abort( + total_verts * 4, "object colors"); + osrs_read_exact(f, raw_colors, 1, total_verts * 4, path, "colors"); + + float* raw_texcoords = NULL; + if (has_textures) { + raw_texcoords = (float*)osrs_malloc_or_abort( + total_verts * 2 * sizeof(float), "object texture coordinates"); + osrs_read_exact(f, raw_texcoords, sizeof(float), + total_verts * 2, path, "texture coordinates"); + } + fclose(f); + + Mesh mesh = { 0 }; + mesh.vertexCount = (int)total_verts; + mesh.triangleCount = (int)(total_verts / 3); + mesh.vertices = raw_verts; + mesh.colors = raw_colors; + mesh.texcoords = raw_texcoords; + + mesh.normals = (float*)osrs_calloc_or_abort( + total_verts * 3, sizeof(float), "object normals"); + for (int i = 0; i < mesh.triangleCount; i++) { + int base = i * 9; + float ax = raw_verts[base + 0], ay = raw_verts[base + 1], az = raw_verts[base + 2]; + float bx = raw_verts[base + 3], by = raw_verts[base + 4], bz = raw_verts[base + 5]; + float cx = raw_verts[base + 6], cy = raw_verts[base + 7], cz = raw_verts[base + 8]; + + float e1x = bx - ax, e1y = by - ay, e1z = bz - az; + float e2x = cx - ax, e2y = cy - ay, e2z = cz - az; + float nx = e1y * e2z - e1z * e2y; + float ny = e1z * e2x - e1x * e2z; + float nz = e1x * e2y - e1y * e2x; + float len = sqrtf(nx * nx + ny * ny + nz * nz); + if (len > 0.0001f) { nx /= len; ny /= len; nz /= len; } + + for (int v = 0; v < 3; v++) { + mesh.normals[i * 9 + v * 3 + 0] = nx; + mesh.normals[i * 9 + v * 3 + 1] = ny; + mesh.normals[i * 9 + v * 3 + 2] = nz; + } + } + + UploadMesh(&mesh, false); + + ObjectMesh* om = (ObjectMesh*)osrs_calloc_or_abort( + 1, sizeof(ObjectMesh), "object mesh"); + om->model = LoadModelFromMesh(mesh); + om->placement_count = (int)placement_count; + om->total_vertex_count = (int)total_verts; + om->min_world_x = min_wx; + om->min_world_y = min_wy; + om->has_textures = has_textures; + om->loaded = 1; + + if (has_textures) { + char atlas_path[1024]; + strncpy(atlas_path, path, sizeof(atlas_path) - 1); + atlas_path[sizeof(atlas_path) - 1] = '\0'; + char* dot = strrchr(atlas_path, '.'); + if (dot) { + strcpy(dot, ".atlas"); + } else { + strncat(atlas_path, ".atlas", sizeof(atlas_path) - strlen(atlas_path) - 1); + } + + om->atlas_texture = objects_load_atlas(atlas_path); + if (om->atlas_texture.id > 0) { + om->model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = om->atlas_texture; + } + } + + return om; +} + +static void objects_offset(ObjectMesh* om, int wx, int wy) { + if (!om || !om->loaded) return; + float dx = (float)wx; + float dz = (float)wy; + float* verts = om->model.meshes[0].vertices; + for (int i = 0; i < om->total_vertex_count; i++) { + verts[i * 3 + 0] -= dx; + verts[i * 3 + 2] += dz; + } + UpdateMeshBuffer(om->model.meshes[0], 0, verts, + om->total_vertex_count * 3 * sizeof(float), 0); + om->min_world_x -= wx; + om->min_world_y -= wy; + fprintf(stderr, "objects_offset: shifted by (%d, %d)\n", wx, wy); +} + +static void objects_free(ObjectMesh* om) { + if (!om) return; + if (om->loaded) { + if (om->atlas_texture.id > 0) { + UnloadTexture(om->atlas_texture); + } + UnloadModel(om->model); + } + free(om); +} + +#endif diff --git a/ocean/osrs/osrs_pathfinding.h b/ocean/osrs/osrs_pathfinding.h new file mode 100644 index 0000000000..b41729038e --- /dev/null +++ b/ocean/osrs/osrs_pathfinding.h @@ -0,0 +1,370 @@ +#ifndef OSRS_PATHFINDING_H +#define OSRS_PATHFINDING_H + +#include +#include +#include +#include + +#ifdef __cplusplus +#define OSRS_THREAD_LOCAL thread_local +#else +#define OSRS_THREAD_LOCAL _Thread_local +#endif + +#include "osrs_collision.h" + +#define PATHFIND_GRID_SIZE 104 +#define PATHFIND_ARENA_MAX 48 +#define PATHFIND_MAX_QUEUE_FULL (PATHFIND_GRID_SIZE * PATHFIND_GRID_SIZE) +#define PATHFIND_MAX_QUEUE_ARENA (PATHFIND_ARENA_MAX * PATHFIND_ARENA_MAX) +#define PATHFIND_MAX_FALLBACK_RADIUS 10 + +#define VIA_NONE 0 +#define VIA_S 1 +#define VIA_W 2 +#define VIA_SW 3 +#define VIA_N 4 +#define VIA_NW 6 +#define VIA_E 8 +#define VIA_SE 9 +#define VIA_NE 12 +#define VIA_START 99 + +typedef struct { + int found; + int next_dx; + int next_dy; + int dest_x; + int dest_y; +} PathResult; + +typedef int (*pathfind_blocked_fn)(void* ctx, int abs_x, int abs_y); + +static inline void pathfind_enqueue_or_abort( + int* queue_x, int* queue_y, int* tail, int capacity, int x, int y +) { + if (*tail >= capacity) { + fprintf(stderr, "pathfind queue overflow: capacity=%d\n", capacity); + abort(); + } + + queue_x[*tail] = x; + queue_y[*tail] = y; + (*tail)++; +} + +static const int pathfind_dir_dx[8] = {0, -1, 0, 1, -1, -1, 1, 1}; +static const int pathfind_dir_dy[8] = {-1, 0, 1, 0, -1, 1, -1, 1}; +static const int pathfind_dir_via[8] = { + VIA_S, VIA_W, VIA_N, VIA_E, VIA_SW, VIA_NW, VIA_SE, VIA_NE +}; + +static inline PathResult pathfind_step(const CollisionMap* map, int height, + int src_x, int src_y, int dest_x, int dest_y, + pathfind_blocked_fn extra_blocked, void* blocked_ctx) { + PathResult result = {0, 0, 0, dest_x, dest_y}; + + if (src_x == dest_x && src_y == dest_y) { + result.found = 1; + return result; + } + + int dist = abs(src_x - dest_x); + int dy_abs = abs(src_y - dest_y); + if (dy_abs > dist) dist = dy_abs; + if (dist > 64) { + return result; + } + + int origin_x = ((src_x >> 3) - 6) << 3; + int origin_y = ((src_y >> 3) - 6) << 3; + + int local_src_x = src_x - origin_x; + int local_src_y = src_y - origin_y; + int local_dest_x = dest_x - origin_x; + int local_dest_y = dest_y - origin_y; + + if (local_dest_x < 0 || local_dest_x >= PATHFIND_GRID_SIZE || + local_dest_y < 0 || local_dest_y >= PATHFIND_GRID_SIZE) { + return result; + } + + int via[PATHFIND_GRID_SIZE][PATHFIND_GRID_SIZE]; + int cost[PATHFIND_GRID_SIZE][PATHFIND_GRID_SIZE]; + memset(via, 0, sizeof(via)); + memset(cost, 0, sizeof(cost)); + + int queue_x[PATHFIND_MAX_QUEUE_FULL]; + int queue_y[PATHFIND_MAX_QUEUE_FULL]; + int head = 0; + int tail = 0; + + via[local_src_x][local_src_y] = VIA_START; + cost[local_src_x][local_src_y] = 1; + pathfind_enqueue_or_abort( + queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_FULL, local_src_x, local_src_y); + + int found_path = 0; + int cur_x, cur_y; + + while (head < tail) { + cur_x = queue_x[head]; + cur_y = queue_y[head]; + head++; + + if (cur_x == local_dest_x && cur_y == local_dest_y) { + found_path = 1; + break; + } + + int abs_x = origin_x + cur_x; + int abs_y = origin_y + cur_y; + int next_cost = cost[cur_x][cur_y] + 1; + + #define EB(ax, ay) (extra_blocked && extra_blocked(blocked_ctx, (ax), (ay))) + + for (int i = 0; i < 8; i++) { + int dx = pathfind_dir_dx[i]; + int dy = pathfind_dir_dy[i]; + int next_x = cur_x + dx; + int next_y = cur_y + dy; + if (next_x < 0 || next_x >= PATHFIND_GRID_SIZE || + next_y < 0 || next_y >= PATHFIND_GRID_SIZE) + continue; + if (via[next_x][next_y] != 0) continue; + if (!collision_traversable_step(map, height, abs_x, abs_y, dx, dy)) + continue; + if (dx != 0 && dy != 0) { + if (!collision_traversable_step(map, height, abs_x, abs_y, 0, dy)) + continue; + if (!collision_traversable_step(map, height, abs_x, abs_y, dx, 0)) + continue; + } + if (EB(abs_x + dx, abs_y + dy)) continue; + if (dx != 0 && dy != 0) { + if (EB(abs_x, abs_y + dy)) continue; + if (EB(abs_x + dx, abs_y)) continue; + } + pathfind_enqueue_or_abort( + queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_FULL, next_x, next_y); + via[next_x][next_y] = pathfind_dir_via[i]; + cost[next_x][next_y] = next_cost; + } + + #undef EB + } + + if (!found_path) { + int best_manhattan = PATHFIND_GRID_SIZE * 2; + int best_cost = 999999; + int best_x = -1, best_y = -1; + + for (int fx = 0; fx < PATHFIND_GRID_SIZE; fx++) { + for (int fy = 0; fy < PATHFIND_GRID_SIZE; fy++) { + if (cost[fx][fy] == 0) continue; + + int ddx = fx - local_dest_x; + int ddy = fy - local_dest_y; + int manhattan = (ddx < 0 ? -ddx : ddx) + (ddy < 0 ? -ddy : ddy); + + if (manhattan < best_manhattan || + (manhattan == best_manhattan && cost[fx][fy] < best_cost)) { + best_manhattan = manhattan; + best_cost = cost[fx][fy]; + best_x = fx; + best_y = fy; + } + } + } + + if (best_x == -1) { + return result; + } + + cur_x = best_x; + cur_y = best_y; + found_path = 1; + result.dest_x = origin_x + best_x; + result.dest_y = origin_y + best_y; + } + + while (1) { + int v = via[cur_x][cur_y]; + int prev_x = cur_x; + int prev_y = cur_y; + + if (v & VIA_W) prev_x++; + else if (v & VIA_E) prev_x--; + + if (v & VIA_S) prev_y++; + else if (v & VIA_N) prev_y--; + + if (prev_x == local_src_x && prev_y == local_src_y) { + result.found = 1; + result.next_dx = cur_x - local_src_x; + result.next_dy = cur_y - local_src_y; + return result; + } + + cur_x = prev_x; + cur_y = prev_y; + + if (via[cur_x][cur_y] == VIA_NONE || via[cur_x][cur_y] == VIA_START) { + break; + } + } + + return result; +} + +static inline PathResult pathfind_step_arena( + const CollisionMap* map, int height, + int src_x, int src_y, int dest_x, int dest_y, + pathfind_blocked_fn extra_blocked, void* blocked_ctx, + int arena_origin_x, int arena_origin_y, int arena_w, int arena_h +) { + PathResult result = {0, 0, 0, dest_x, dest_y}; + + if (arena_w <= 0 || arena_w > PATHFIND_ARENA_MAX || + arena_h <= 0 || arena_h > PATHFIND_ARENA_MAX) { + fprintf(stderr, "pathfind arena dimensions out of bounds: %dx%d\n", arena_w, arena_h); + abort(); + } + + if (src_x == dest_x && src_y == dest_y) { + result.found = 1; + return result; + } + + int local_src_x = src_x - arena_origin_x; + int local_src_y = src_y - arena_origin_y; + int local_dest_x = dest_x - arena_origin_x; + int local_dest_y = dest_y - arena_origin_y; + + if (local_src_x < 0 || local_src_x >= arena_w || + local_src_y < 0 || local_src_y >= arena_h || + local_dest_x < 0 || local_dest_x >= arena_w || + local_dest_y < 0 || local_dest_y >= arena_h) { + return result; + } + + static OSRS_THREAD_LOCAL uint16_t bfs_gen[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + static OSRS_THREAD_LOCAL int8_t bfs_via[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + static OSRS_THREAD_LOCAL int16_t bfs_cost[PATHFIND_ARENA_MAX][PATHFIND_ARENA_MAX]; + static OSRS_THREAD_LOCAL uint16_t bfs_gen_counter = 0; + bfs_gen_counter++; + if (bfs_gen_counter == 0) { + memset(bfs_gen, 0, sizeof(bfs_gen)); + bfs_gen_counter = 1; + } + uint16_t gen = bfs_gen_counter; + #define BFS_VISITED(x, y) (bfs_gen[(x)][(y)] == gen) + #define BFS_VISIT(x, y, v, c) do { \ + bfs_gen[(x)][(y)] = gen; bfs_via[(x)][(y)] = (v); bfs_cost[(x)][(y)] = (c); \ + } while(0) + #define BFS_VIA(x, y) bfs_via[(x)][(y)] + #define BFS_COST(x, y) bfs_cost[(x)][(y)] + + int queue_x[PATHFIND_MAX_QUEUE_ARENA]; + int queue_y[PATHFIND_MAX_QUEUE_ARENA]; + int head = 0, tail = 0; + + BFS_VISIT(local_src_x, local_src_y, VIA_START, 1); + pathfind_enqueue_or_abort( + queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, local_src_x, local_src_y); + + int found_path = 0; + int cur_x, cur_y; + + while (head < tail) { + cur_x = queue_x[head]; + cur_y = queue_y[head]; + head++; + + if (cur_x == local_dest_x && cur_y == local_dest_y) { + found_path = 1; + break; + } + + int abs_x = arena_origin_x + cur_x; + int abs_y = arena_origin_y + cur_y; + int next_cost = BFS_COST(cur_x, cur_y) + 1; + + #define EB(ax, ay) (extra_blocked && extra_blocked(blocked_ctx, (ax), (ay))) + + for (int i = 0; i < 8; i++) { + int dx = pathfind_dir_dx[i]; + int dy = pathfind_dir_dy[i]; + int next_x = cur_x + dx; + int next_y = cur_y + dy; + if (next_x < 0 || next_x >= arena_w || + next_y < 0 || next_y >= arena_h) + continue; + if (BFS_VISITED(next_x, next_y)) continue; + if (!collision_traversable_step(map, height, abs_x, abs_y, dx, dy)) + continue; + if (dx != 0 && dy != 0) { + if (!collision_traversable_step(map, height, abs_x, abs_y, 0, dy)) + continue; + if (!collision_traversable_step(map, height, abs_x, abs_y, dx, 0)) + continue; + } + if (EB(abs_x + dx, abs_y + dy)) continue; + if (dx != 0 && dy != 0) { + if (EB(abs_x, abs_y + dy)) continue; + if (EB(abs_x + dx, abs_y)) continue; + } + pathfind_enqueue_or_abort( + queue_x, queue_y, &tail, PATHFIND_MAX_QUEUE_ARENA, next_x, next_y); + BFS_VISIT(next_x, next_y, pathfind_dir_via[i], next_cost); + } + + #undef EB + } + + if (!found_path) { + int best_manhattan = arena_w + arena_h; + int best_cost = 999999; + int best_x = -1, best_y = -1; + + for (int fx = 0; fx < arena_w; fx++) { + for (int fy = 0; fy < arena_h; fy++) { + if (!BFS_VISITED(fx, fy) || BFS_COST(fx, fy) == 0) continue; + int ddx = fx - local_dest_x, ddy = fy - local_dest_y; + int manhattan = (ddx < 0 ? -ddx : ddx) + (ddy < 0 ? -ddy : ddy); + if (manhattan < best_manhattan || + (manhattan == best_manhattan && BFS_COST(fx, fy) < best_cost)) { + best_manhattan = manhattan; + best_cost = BFS_COST(fx, fy); + best_x = fx; best_y = fy; + } + } + } + + if (best_x == -1) return result; + cur_x = best_x; cur_y = best_y; + found_path = 1; + result.dest_x = arena_origin_x + best_x; + result.dest_y = arena_origin_y + best_y; + } + + while (1) { + int v = BFS_VIA(cur_x, cur_y); + int prev_x = cur_x, prev_y = cur_y; + if (v & VIA_W) prev_x++; else if (v & VIA_E) prev_x--; + if (v & VIA_S) prev_y++; else if (v & VIA_N) prev_y--; + if (prev_x == local_src_x && prev_y == local_src_y) { + result.found = 1; + result.next_dx = cur_x - local_src_x; + result.next_dy = cur_y - local_src_y; + return result; + } + cur_x = prev_x; cur_y = prev_y; + if (BFS_VIA(cur_x, cur_y) == VIA_NONE || BFS_VIA(cur_x, cur_y) == VIA_START) break; + } + + return result; +} + +#endif diff --git a/ocean/osrs/osrs_player_consumables.h b/ocean/osrs/osrs_player_consumables.h new file mode 100644 index 0000000000..8068ce361b --- /dev/null +++ b/ocean/osrs/osrs_player_consumables.h @@ -0,0 +1,103 @@ +#ifndef OSRS_PLAYER_CONSUMABLES_H +#define OSRS_PLAYER_CONSUMABLES_H + +#include +#include + +#include "osrs_types.h" +#include "osrs_consumables.h" + +typedef struct { + int hp_healed; + int hp_wasted; + int attack_delay_ticks; + int consumed; +} OsrsPlayerEatResult; + +static inline void osrs_require_player_food_action(FoodType type) { + switch (type) { + case FOOD_SHARK: + case FOOD_KARAMBWAN: + case FOOD_MANTA_RAY: + case FOOD_ANGLERFISH: + return; + default: + fprintf(stderr, "unsupported player food action type: %d\n", (int)type); + abort(); + } +} + +static inline int osrs_player_food_timer(const Player* p, FoodType type) { + osrs_require_player_food_action(type); + return type == FOOD_KARAMBWAN ? p->karambwan_timer : p->food_timer; +} + +static inline int osrs_player_food_count(const Player* p, FoodType type) { + osrs_require_player_food_action(type); + return type == FOOD_KARAMBWAN ? p->karambwan_count : p->food_count; +} + +static inline int osrs_player_can_eat_food_type(const Player* p, FoodType type) { + osrs_require_player_food_action(type); + if (osrs_player_food_count(p, type) <= 0) return 0; + EatResult r = osrs_eat_food(type, p->current_hitpoints, + p->base_hitpoints, osrs_player_food_timer(p, type)); + return r.consumed; +} + +static inline int osrs_player_food_wasted_hp(const Player* p, FoodType type) { + osrs_require_player_food_action(type); + EatResult r = osrs_eat_food(type, p->current_hitpoints, + p->base_hitpoints, osrs_player_food_timer(p, type)); + if (!r.consumed) return 0; + return osrs_food_heal_amount(type) - r.hp_healed; +} + +static inline OsrsPlayerEatResult osrs_player_eat_food_type(Player* p, FoodType type) { + osrs_require_player_food_action(type); + OsrsPlayerEatResult out = {0, 0, 0, 0}; + if (osrs_player_food_count(p, type) <= 0) return out; + + EatResult r = osrs_eat_food(type, p->current_hitpoints, + p->base_hitpoints, osrs_player_food_timer(p, type)); + if (!r.consumed) return out; + + int heal_amount = osrs_food_heal_amount(type); + out.consumed = 1; + out.hp_healed = r.hp_healed; + out.hp_wasted = heal_amount - r.hp_healed; + + if (type == FOOD_KARAMBWAN) { + p->karambwan_count--; + p->karambwan_timer = 2; + p->food_timer = 3; + p->potion_timer = 3; + p->ate_karambwan_this_tick = 1; + p->last_karambwan_heal = out.hp_healed; + p->last_karambwan_waste = out.hp_wasted; + out.attack_delay_ticks = 2; + } else { + p->food_count--; + p->food_timer = 3; + p->ate_food_this_tick = 1; + p->last_food_heal = out.hp_healed; + p->last_food_waste = out.hp_wasted; + out.attack_delay_ticks = 3; + } + + p->current_hitpoints += out.hp_healed; + if (type != FOOD_ANGLERFISH && p->current_hitpoints > p->base_hitpoints) + p->current_hitpoints = p->base_hitpoints; + + int combat_ticks = 0; + if (p->has_attack_timer) { + combat_ticks = p->attack_timer > 0 ? p->attack_timer : 0; + } + p->attack_timer = combat_ticks + out.attack_delay_ticks; + p->attack_timer_uncapped = combat_ticks + out.attack_delay_ticks; + p->has_attack_timer = 1; + + return out; +} + +#endif diff --git a/ocean/osrs/osrs_projectile_orientation.h b/ocean/osrs/osrs_projectile_orientation.h new file mode 100644 index 0000000000..dbba20f31c --- /dev/null +++ b/ocean/osrs/osrs_projectile_orientation.h @@ -0,0 +1,57 @@ +#ifndef OSRS_PROJECTILE_ORIENTATION_H +#define OSRS_PROJECTILE_ORIENTATION_H + +#include + +#define OSRS_PROJECTILE_ORIENTATION_PI 3.14159265358979323846f + +typedef struct { + float yaw; + float pitch; +} OsrsProjectileOrientation; + +static inline float osrs_projectile_clamp_progress(float progress) { + if (progress < 0.0f) return 0.0f; + if (progress > 1.0f) return 1.0f; + return progress; +} + +static inline float osrs_projectile_height_at_progress( + float progress, + float start_height, + float end_height, + float arc_height, + float height_vel, + float height_accel +) { + float t = osrs_projectile_clamp_progress(progress); + if (arc_height > 0.0f) { + return sinf(t * OSRS_PROJECTILE_ORIENTATION_PI) * arc_height + + start_height + (end_height - start_height) * t; + } + return start_height + height_vel * t + 0.5f * height_accel * t * t; +} + +static inline float osrs_projectile_anchor_coord_from_subtile(int subtile_coord) { + return (float)subtile_coord / 128.0f - 0.5f; +} + +static inline float osrs_projectile_subtile_from_anchor_coord(float anchor_coord) { + return (anchor_coord + 0.5f) * 128.0f; +} + +static inline OsrsProjectileOrientation osrs_projectile_orientation_from_step( + float osrs_dx, + float osrs_dy, + float height_delta +) { + float horizontal = sqrtf(osrs_dx * osrs_dx + osrs_dy * osrs_dy); + float pitch_horizontal = horizontal > 0.0001f ? horizontal : 0.0001f; + + OsrsProjectileOrientation orientation; + orientation.yaw = horizontal > 0.0001f ? atan2f(-osrs_dx, osrs_dy) : 0.0f; + orientation.pitch = atan2f(height_delta, pitch_horizontal); + return orientation; +} + +#endif diff --git a/ocean/osrs/osrs_pvp_actions.h b/ocean/osrs/osrs_pvp_actions.h new file mode 100644 index 0000000000..e371ce066e --- /dev/null +++ b/ocean/osrs/osrs_pvp_actions.h @@ -0,0 +1,753 @@ +#ifndef OSRS_PVP_ACTIONS_H +#define OSRS_PVP_ACTIONS_H + +#include "osrs_types.h" +#include "osrs_items.h" +#include "osrs_consumables.h" +#include "osrs_player_consumables.h" +#include "osrs_pvp_gear.h" +#include "osrs_pvp_combat.h" +#include "osrs_pvp_movement.h" +#include "osrs_pvp_observations.h" +#include "osrs_encounter.h" + +/* fury +3 + neitiznot +3, worn in every loadout */ +#define PRAYER_BONUS 6 + +static void eat_food(Player* p, int is_karambwan) { + osrs_player_eat_food_type(p, is_karambwan ? FOOD_KARAMBWAN : FOOD_SHARK); +} + +static void drink_potion(Player* p, int potion_type) { + if (p->potion_timer > 0) return; + + switch (potion_type) { + case POTION_BREW: { + if (p->brew_doses <= 0) return; + p->brew_doses--; + BrewResult br = osrs_brew_effect(p->base_hitpoints, p->base_defence, + p->current_attack, p->current_strength, + p->current_ranged, p->current_magic); + int hp_before = p->current_hitpoints; + int max_hp = p->base_hitpoints + br.hp_healed; + int actual_heal = max_int(0, min_int(br.hp_healed, max_hp - hp_before)); + int waste = br.hp_healed - actual_heal; + int def_before = p->current_defence; + int max_def = p->is_lms ? p->base_defence : p->base_defence + br.def_boost; + p->current_defence = clamp(def_before + br.def_boost, 0, max_def); + p->current_hitpoints = clamp(hp_before + br.hp_healed, 0, max_hp); + p->last_brew_heal = actual_heal; + p->last_brew_waste = waste; + p->current_attack = clamp(p->current_attack - br.att_drain, 0, 255); + p->current_strength = clamp(p->current_strength - br.str_drain, 0, 255); + p->current_magic = clamp(p->current_magic - br.magic_drain, 0, 255); + p->current_ranged = clamp(p->current_ranged - br.range_drain, 0, 255); + p->last_potion_type = potion_type; + p->ate_brew_this_tick = 1; + break; + } + + case POTION_RESTORE: { + if (p->restore_doses <= 0) return; + p->restore_doses--; + int had_restore_need = ( + p->current_attack < p->base_attack || + p->current_strength < p->base_strength || + p->current_defence < p->base_defence || + p->current_ranged < p->base_ranged || + p->current_magic < p->base_magic || + p->current_prayer < p->base_prayer + ); + p->current_prayer = clamp( + p->current_prayer + osrs_super_restore_amount(p->base_prayer), + 0, p->base_prayer); + int atk_restore = osrs_super_restore_amount(p->base_attack); + int str_restore = osrs_super_restore_amount(p->base_strength); + int def_restore = osrs_super_restore_amount(p->base_defence); + int rng_restore = osrs_super_restore_amount(p->base_ranged); + int mag_restore = osrs_super_restore_amount(p->base_magic); + if (p->current_attack < p->base_attack) { + p->current_attack = clamp(p->current_attack + atk_restore, 0, p->base_attack); + } + if (p->current_strength < p->base_strength) { + p->current_strength = clamp(p->current_strength + str_restore, 0, p->base_strength); + } + if (p->current_defence < p->base_defence) { + p->current_defence = clamp(p->current_defence + def_restore, 0, p->base_defence); + } + if (p->current_ranged < p->base_ranged) { + p->current_ranged = clamp(p->current_ranged + rng_restore, 0, p->base_ranged); + } + if (p->current_magic < p->base_magic) { + p->current_magic = clamp(p->current_magic + mag_restore, 0, p->base_magic); + } + p->last_potion_type = potion_type; + p->last_potion_was_waste = had_restore_need ? 0 : 1; + break; + } + + case POTION_COMBAT: { + if (p->combat_potion_doses <= 0) return; + p->combat_potion_doses--; + int atk_boost = osrs_super_combat_boost_amount(p->base_attack); + int str_boost = osrs_super_combat_boost_amount(p->base_strength); + int def_boost = osrs_super_combat_boost_amount(p->base_defence); + int atk_cap = p->base_attack + atk_boost; + int str_cap = p->base_strength + str_boost; + int def_cap = p->is_lms ? p->base_defence : p->base_defence + def_boost; + int had_boost_need = ( + p->current_attack < atk_cap || + p->current_strength < str_cap || + p->current_defence < def_cap + ); + if (p->current_attack < atk_cap) { + p->current_attack = clamp(p->current_attack + atk_boost, 0, atk_cap); + } + if (p->current_strength < str_cap) { + p->current_strength = clamp(p->current_strength + str_boost, 0, str_cap); + } + if (p->current_defence < def_cap) { + p->current_defence = clamp(p->current_defence + def_boost, 0, def_cap); + } + p->last_potion_type = potion_type; + p->last_potion_was_waste = had_boost_need ? 0 : 1; + break; + } + + case POTION_RANGED: { + if (p->ranged_potion_doses <= 0) return; + p->ranged_potion_doses--; + int rng_boost = osrs_ranging_boost_amount(p->base_ranged); + int rng_cap = p->base_ranged + rng_boost; + int had_boost_need = p->current_ranged < rng_cap; + if (p->current_ranged < rng_cap) { + p->current_ranged = clamp(p->current_ranged + rng_boost, 0, rng_cap); + } + p->last_potion_type = potion_type; + p->last_potion_was_waste = had_boost_need ? 0 : 1; + break; + } + } + + p->potion_timer = 3; + p->food_timer = 3; +} + +/* consumable timers are NOT decremented here: they tick after execute_switches + in pvp_step so observations show the post-use countdown */ +static void update_timers(Player* p) { + p->damage_applied_this_tick = 0; + + if (p->has_attack_timer) { + p->attack_timer_uncapped -= 1; + if (p->attack_timer >= 0) { + p->attack_timer -= 1; + } + } + if (p->frozen_ticks > 0) p->frozen_ticks--; + if (p->freeze_immunity_ticks > 0) p->freeze_immunity_ticks--; + if (p->veng_cooldown > 0) p->veng_cooldown--; + + if (!p->is_lms) { + encounter_drain_all_prayers(p, PRAYER_BONUS); + } else { + p->prayer_just_activated = 0; + p->offensive_prayer_just_activated = 0; + } + + if (p->run_energy < OSRS_RUN_ENERGY_FULL && (!p->is_moving || !p->is_running)) { + p->run_recovery_ticks += 1; + if (p->run_recovery_ticks >= RUN_ENERGY_RECOVER_TICKS) { + p->run_energy = clamp( + p->run_energy + OSRS_RUN_ENERGY_UNITS_PER_PERCENT, + 0, + OSRS_RUN_ENERGY_FULL + ); + p->run_recovery_ticks = 0; + } + } else { + p->run_recovery_ticks = 0; + } + + if (p->spec_regen_active && p->special_energy < 100) { + osrs_tick_special_regen(p); + } else if (p->spec_regen_active) { + p->item_effect_state.special_regen_ticks = 0; + } +} + +static void reset_tick_flags(Player* p) { + p->just_attacked = 0; + p->last_queued_hit_damage = 0; + p->attack_was_on_prayer = 0; + p->player_prayed_correct = 0; + p->target_prayed_correct = 0; + p->tick_damage_scale = 0.0f; + p->damage_dealt_scale = 0.0f; + p->damage_received_scale = 0.0f; + p->last_food_heal = 0; + p->last_food_waste = 0; + p->last_karambwan_heal = 0; + p->last_karambwan_waste = 0; + p->last_brew_heal = 0; + p->last_brew_waste = 0; + p->last_potion_type = 0; + p->last_potion_was_waste = 0; + p->attack_click_canceled = 0; + p->attack_click_ready = 0; + p->attack_style_this_tick = ATTACK_STYLE_NONE; + p->magic_type_this_tick = 0; + p->used_special_this_tick = 0; + p->ate_food_this_tick = 0; + p->ate_karambwan_this_tick = 0; + p->ate_brew_this_tick = 0; + p->cast_veng_this_tick = 0; + p->clicks_this_tick = 0; +} + +static void execute_switches(OsrsEnv* env, int agent_idx, int* actions) { + Player* p = &env->players[agent_idx]; + const CollisionMap* cmap = (const CollisionMap*)env->collision_map; + + p->consumable_used_this_tick = 0; + + int overhead_action = actions[HEAD_OVERHEAD]; + int offensive_action = actions[HEAD_OFFENSIVE]; + + if (env->is_lms && + (overhead_action == ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE || + overhead_action == ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION)) { + overhead_action = ENCOUNTER_OVERHEAD_NO_CHANGE; + } + if (p->current_prayer <= 0) { + if (overhead_action >= ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE) + overhead_action = ENCOUNTER_OVERHEAD_NO_CHANGE; + if (offensive_action >= ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY) + offensive_action = ENCOUNTER_OFFENSIVE_NO_CHANGE; + } + + OverheadPrayer prev_prayer = p->prayer; + OffensivePrayer prev_offensive = p->offensive_prayer; + int prayer_commanded = + overhead_action != ENCOUNTER_OVERHEAD_NO_CHANGE || + offensive_action != ENCOUNTER_OFFENSIVE_NO_CHANGE; + if (encounter_apply_overhead_action(&p->prayer, overhead_action)) { + p->prayer_just_activated = 1; + } + if (encounter_apply_offensive_action(&p->offensive_prayer, offensive_action)) { + p->offensive_prayer_just_activated = 1; + } + if (prayer_commanded || p->prayer != prev_prayer || p->offensive_prayer != prev_offensive) + p->clicks_this_tick++; + int loadout_action = actions[HEAD_LOADOUT]; + int loadout_switches = apply_loadout(p, loadout_action); + p->clicks_this_tick += loadout_switches; + if (loadout_switches > 0) + osrs_interaction_check_interrupt(&p->interaction, OSRS_IACT_EQUIP); + + if (loadout_action == LOADOUT_SPEC_MELEE || loadout_action == LOADOUT_SPEC_RANGE || + loadout_action == LOADOUT_SPEC_MAGIC || loadout_action == LOADOUT_GMAUL) { + p->spec_armed = 1; + } + int food_action = actions[HEAD_FOOD]; + if (food_action == FOOD_EAT && can_eat_food(p)) { + eat_food(p, 0); + p->consumable_used_this_tick = 1; + p->clicks_this_tick++; + osrs_interaction_check_interrupt(&p->interaction, OSRS_IACT_EAT); + } + + int potion_action = actions[HEAD_POTION]; + int potion_ok = 0; + switch (potion_action) { + case POTION_BREW: + potion_ok = can_use_potion(p, POTION_BREW) && can_use_brew_boost(p); + break; + case POTION_RESTORE: + potion_ok = can_use_potion(p, POTION_RESTORE) && can_restore_stats(p); + break; + case POTION_COMBAT: + potion_ok = can_use_potion(p, POTION_COMBAT) && can_boost_combat_skills(p); + break; + case POTION_RANGED: + potion_ok = can_use_potion(p, POTION_RANGED) && can_boost_ranged(p); + break; + default: + break; + } + if (potion_ok) { + drink_potion(p, potion_action); + p->consumable_used_this_tick = 1; + p->clicks_this_tick++; + osrs_interaction_check_interrupt(&p->interaction, OSRS_IACT_DRINK); + } + + int karam_action = actions[HEAD_KARAMBWAN]; + if (karam_action == KARAM_EAT && can_eat_karambwan(p)) { + eat_food(p, 1); + p->consumable_used_this_tick = 1; + p->clicks_this_tick++; + osrs_interaction_check_interrupt(&p->interaction, OSRS_IACT_EAT); + } + int combat_action = actions[HEAD_COMBAT]; + int head_move = actions[HEAD_MOVE]; + int is_spec_loadout = (loadout_action == LOADOUT_SPEC_MELEE || + loadout_action == LOADOUT_SPEC_RANGE || + loadout_action == LOADOUT_SPEC_MAGIC || + loadout_action == LOADOUT_GMAUL); + + int command_issued = 0; + if (!is_spec_loadout && head_move > 0 && head_move < MOVE_DIM) { + pvp_set_walk_dest_from_head_move(env, agent_idx, head_move); + command_issued = 1; + } else if (!is_spec_loadout && is_move_action(combat_action)) { + int tx = p->last_obs_target_x; + int ty = p->last_obs_target_y; + int dest_x = -1, dest_y = -1; + switch (combat_action) { + case MOVE_ADJACENT: + if (!select_closest_adjacent_tile(p, tx, ty, &dest_x, &dest_y, cmap)) { + dest_x = -1; dest_y = -1; + } + break; + case MOVE_UNDER: + if (is_in_wilderness(tx, ty) && collision_tile_walkable(cmap, 0, tx, ty)) { + dest_x = tx; dest_y = ty; + } + break; + case MOVE_DIAGONAL: + if (!select_closest_diagonal_tile(p, tx, ty, &dest_x, &dest_y, cmap)) { + dest_x = -1; dest_y = -1; + } + break; + case MOVE_FARCAST_2: + case MOVE_FARCAST_3: + case MOVE_FARCAST_4: + case MOVE_FARCAST_5: + case MOVE_FARCAST_6: + case MOVE_FARCAST_7: { + int fd = combat_action - MOVE_FARCAST_2 + 2; + if (!select_farcast_tile(p, tx, ty, fd, &dest_x, &dest_y, cmap)) { + dest_x = -1; dest_y = -1; + } + break; + } + default: + break; + } + env->pvp_runtime.walk_dest_x[agent_idx] = dest_x; + env->pvp_runtime.walk_dest_y[agent_idx] = dest_y; + command_issued = (dest_x >= 0); + } + /* no clearing else: walk_dest persists until arrival, matching OSRS click + semantics (the SDK sets it back to -1 when the player gets there) */ + if (command_issued) { + p->clicks_this_tick++; + osrs_interaction_check_interrupt(&p->interaction, OSRS_IACT_MOVE); + } + int veng_action = actions[HEAD_VENG]; + if (veng_action == VENG_CAST && p->is_lunar_spellbook && + !p->veng_active && remaining_ticks(p->veng_cooldown) == 0 && + p->current_magic >= 94) { + p->veng_active = 1; + p->veng_cooldown = 50; + p->cast_veng_this_tick = 1; + p->clicks_this_tick++; + } +} + +typedef struct { + int attack_action; + int move_action; + int explicit_move_in_progress; + int is_gmaul; +} PvpAttackDecode; + +static PvpAttackDecode pvp_decode_attack_actions( + OsrsEnv* env, int agent_idx, Player* p, const int* actions +) { + int loadout_action = actions[HEAD_LOADOUT]; + int combat_action = actions[HEAD_COMBAT]; + int attack_action = is_attack_action(combat_action) ? combat_action : ATTACK_NONE; + int move_action = is_move_action(combat_action) ? combat_action : MOVE_NONE; + int explicit_move_in_progress = (actions[HEAD_MOVE] > 0 && actions[HEAD_MOVE] < MOVE_DIM) + || env->pvp_runtime.walk_dest_x[agent_idx] >= 0; + + int is_gmaul = (loadout_action == LOADOUT_GMAUL); + if (is_gmaul) { + attack_action = ATTACK_ATK; + move_action = MOVE_NONE; + } + + int current_loadout = get_current_loadout(p); + int in_mage_loadout = (current_loadout == LOADOUT_MAGE); + int in_tank_loadout = (current_loadout == LOADOUT_TANK); + if (attack_action == ATTACK_ATK && (in_mage_loadout || in_tank_loadout) && !is_gmaul) { + attack_action = ATTACK_NONE; + } + + return (PvpAttackDecode){ + .attack_action = attack_action, + .move_action = move_action, + .explicit_move_in_progress = explicit_move_in_progress, + .is_gmaul = is_gmaul, + }; +} + +static void execute_attack_movement(OsrsEnv* env, int agent_idx, int* actions) { + Player* p = &env->players[agent_idx]; + Player* t = &env->players[1 - agent_idx]; + const CollisionMap* cmap = (const CollisionMap*)env->collision_map; + + PvpAttackDecode decode = pvp_decode_attack_actions(env, agent_idx, p, actions); + + if (decode.attack_action != ATTACK_NONE) + osrs_interaction_set(&p->interaction, 1 - agent_idx); + + int has_attack = (decode.attack_action != ATTACK_NONE) || osrs_interaction_active(&p->interaction); + int dist = chebyshev_distance(p->x, p->y, t->x, t->y); + + AttackStyle attack_style = ATTACK_STYLE_NONE; + if (decode.attack_action != ATTACK_NONE) { + switch (decode.attack_action) { + case ATTACK_ATK: + attack_style = get_slot_weapon_attack_style(p); + break; + case ATTACK_ICE: + attack_style = ATTACK_STYLE_MAGIC; + break; + case ATTACK_BLOOD: + attack_style = ATTACK_STYLE_MAGIC; + break; + default: + break; + } + } else if (osrs_interaction_active(&p->interaction)) { + attack_style = get_slot_weapon_attack_style(p); + } + if (decode.attack_action == ATTACK_ICE && !can_cast_ice_spell(p)) { + attack_style = ATTACK_STYLE_NONE; + } + if (decode.attack_action == ATTACK_BLOOD && !can_cast_blood_spell(p)) { + attack_style = ATTACK_STYLE_NONE; + } + + p->did_attack_auto_move = 0; + + if (has_attack && decode.move_action == MOVE_NONE && !decode.explicit_move_in_progress && can_move(p)) { + if (attack_style == ATTACK_STYLE_MELEE && !is_in_melee_range(p, t)) { + int adj_x, adj_y; + if (select_closest_adjacent_tile(p, t->x, t->y, &adj_x, &adj_y, cmap)) { + set_destination(p, adj_x, adj_y, cmap); + } + p->did_attack_auto_move = 1; + dist = chebyshev_distance(p->x, p->y, t->x, t->y); + } + } + + if (has_attack && dist == 0 && can_move(p)) { + step_out_from_same_tile(p, t, cmap); + } +} + +/* runs after BOTH players' attack movement so range checks use final positions; + checking ranges in the movement phase reintroduces the PID-dependent same-tile bug */ +static void execute_attack_combat(OsrsEnv* env, int agent_idx, int* actions) { + Player* p = &env->players[agent_idx]; + Player* t = &env->players[1 - agent_idx]; + const CollisionMap* cmap = (const CollisionMap*)env->collision_map; + + PvpAttackDecode decode = pvp_decode_attack_actions(env, agent_idx, p, actions); + + if (decode.attack_action == ATTACK_NONE && osrs_interaction_active(&p->interaction)) { + AttackStyle weapon_style = get_slot_weapon_attack_style(p); + if (weapon_style != ATTACK_STYLE_MAGIC) { + decode.attack_action = ATTACK_ATK; + } + } + + int attack_ready = can_attack_now(p); + int has_attack = (decode.attack_action != ATTACK_NONE); + int dist = chebyshev_distance(p->x, p->y, t->x, t->y); + + AttackStyle attack_style = ATTACK_STYLE_NONE; + int magic_type = 0; + + switch (decode.attack_action) { + case ATTACK_ATK: + attack_style = get_slot_weapon_attack_style(p); + break; + case ATTACK_ICE: + attack_style = ATTACK_STYLE_MAGIC; + magic_type = 1; + break; + case ATTACK_BLOOD: + attack_style = ATTACK_STYLE_MAGIC; + magic_type = 2; + break; + default: + break; + } + if (decode.attack_action == ATTACK_ICE && !can_cast_ice_spell(p)) { + attack_style = ATTACK_STYLE_NONE; + } + if (decode.attack_action == ATTACK_BLOOD && !can_cast_blood_spell(p)) { + attack_style = ATTACK_STYLE_NONE; + } + + int can_attack = attack_ready || (decode.is_gmaul && is_granite_maul_attack_available(p)); + + switch (decode.attack_action) { + case ATTACK_ATK: + if (can_attack && attack_style != ATTACK_STYLE_NONE) { + AttackStyle actual_style = (attack_style == ATTACK_STYLE_MAGIC) + ? ATTACK_STYLE_MELEE + : attack_style; + int in_attack_range = 0; + if (actual_style == ATTACK_STYLE_MELEE) { + in_attack_range = is_in_melee_range(p, t); + } else { + int range = get_attack_range(p, actual_style); + in_attack_range = (dist > 0 && dist <= range); + } + if (in_attack_range) { + int is_special = p->spec_armed && is_special_ready(p, actual_style); + perform_attack(env, agent_idx, 1 - agent_idx, actual_style, is_special, 0, dist); + if (is_special) + osrs_spec_disarm(&p->spec_armed); + p->clicks_this_tick++; + } + } + break; + case ATTACK_ICE: + case ATTACK_BLOOD: + if (attack_ready && attack_style == ATTACK_STYLE_MAGIC) { + int range = get_attack_range(p, ATTACK_STYLE_MAGIC); + if (dist > 0 && dist <= range) { + perform_attack(env, agent_idx, 1 - agent_idx, ATTACK_STYLE_MAGIC, 0, magic_type, dist); + p->clicks_this_tick++; + } + } + break; + default: + break; + } + + if (has_attack && decode.move_action == MOVE_NONE && !decode.explicit_move_in_progress + && can_move(p) && !p->did_attack_auto_move) { + int in_range = 0; + int auto_walk_range = 1; + switch (attack_style) { + case ATTACK_STYLE_MELEE: + in_range = is_in_melee_range(p, t); + break; + case ATTACK_STYLE_RANGED: { + int range = get_attack_range(p, ATTACK_STYLE_RANGED); + auto_walk_range = range; + in_range = (dist <= range); + break; + } + case ATTACK_STYLE_MAGIC: { + int range = get_attack_range(p, ATTACK_STYLE_MAGIC); + auto_walk_range = range; + in_range = (dist <= range); + break; + } + default: + in_range = 1; + break; + } + if (!in_range) { + if (attack_style == ATTACK_STYLE_MELEE) { + int adj_x, adj_y; + if (select_closest_adjacent_tile(p, t->x, t->y, &adj_x, &adj_y, cmap)) { + set_destination(p, adj_x, adj_y, cmap); + } + } else { + move_toward_target(p, t, auto_walk_range, cmap); + } + } + } +} + +static float calculate_reward(OsrsEnv* env, int agent_idx) { + float reward = 0.0f; + Player* p = &env->players[agent_idx]; + Player* t = &env->players[1 - agent_idx]; + const RewardShapingConfig* cfg = &env->shaping; + + if (env->episode_over) { + if (env->winner == agent_idx) { + reward += 1.0f; + } + } + + if (cfg->prayer_penalty_enabled && !t->just_attacked) { + int overhead = env->last_executed_actions[agent_idx * NUM_ACTION_HEADS + HEAD_OVERHEAD]; + if (overhead == OVERHEAD_MAGE || overhead == OVERHEAD_RANGED || overhead == OVERHEAD_MELEE) { + reward += cfg->prayer_switch_no_attack_penalty; + } + } + + if (cfg->click_penalty_enabled && p->clicks_this_tick > cfg->click_penalty_threshold) { + int excess = p->clicks_this_tick - cfg->click_penalty_threshold; + reward += cfg->click_penalty_coef * (float)excess; + } + + float base_hp = (float)p->base_hitpoints; + if (p->damage_dealt_scale > 0.0f) { + reward += p->damage_dealt_scale * base_hp * 0.005f; + } + if (t->just_attacked && p->player_prayed_correct) { + reward += 0.01f; + } + + if (!cfg->enabled) { + return reward; + } + + if (env->episode_over) { + if (env->winner == agent_idx) { + if (t->food_count > 0 || t->karambwan_count > 0 || t->brew_doses > 0) { + reward += cfg->ko_bonus; + } + float opp_total = (float)(t->food_count + t->karambwan_count + + t->brew_doses + t->restore_doses + + t->combat_potion_doses + + t->ranged_potion_doses); + float max_total = (float)(MAXED_FOOD_COUNT + MAXED_KARAMBWAN_COUNT + + MAXED_BREW_DOSES + MAXED_RESTORE_DOSES + + MAXED_COMBAT_POTION_DOSES + + MAXED_RANGED_POTION_DOSES); + if (max_total > 0.0f) { + reward += cfg->ko_supplies_bonus_coef * (opp_total / max_total); + } + } else if (env->winner == (1 - agent_idx)) { + if (p->food_count > 0 || p->karambwan_count > 0 || p->brew_doses > 0) { + reward += cfg->wasted_resources_penalty; + } + } + } + float tick_shaping = 0.0f; + + if (p->damage_dealt_scale > 0.0f) { + float damage_hp = p->damage_dealt_scale * base_hp; + tick_shaping += damage_hp * cfg->damage_dealt_coef; + if (damage_hp >= (float)cfg->damage_burst_threshold) { + tick_shaping += (damage_hp - (float)cfg->damage_burst_threshold + 1.0f) + * cfg->damage_burst_bonus; + } + } + + if (p->damage_received_scale > 0.0f) { + tick_shaping += p->damage_received_scale * base_hp * cfg->damage_received_coef; + } + + if (t->just_attacked) { + if (p->player_prayed_correct) { + tick_shaping += cfg->correct_prayer_bonus; + } else { + tick_shaping += cfg->wrong_prayer_penalty; + } + } + + if (p->just_attacked) { + if (!p->target_prayed_correct) { + tick_shaping += cfg->off_prayer_hit_bonus; + } + + if (p->attack_style_this_tick == ATTACK_STYLE_MELEE + && p->frozen_ticks > 0 && !is_in_melee_range(p, t)) { + tick_shaping += cfg->melee_frozen_penalty; + } + + if (p->used_special_this_tick) { + if (t->prayer != PRAYER_PROTECT_MELEE) { + tick_shaping += cfg->spec_off_prayer_bonus; + } + AttackStyle target_style = get_slot_weapon_attack_style(t); + if (target_style == ATTACK_STYLE_MAGIC) { + tick_shaping += cfg->spec_low_defence_bonus; + } + float target_hp_pct = (float)t->current_hitpoints / (float)t->base_hitpoints; + if (target_hp_pct < 0.5f) { + tick_shaping += cfg->spec_low_hp_bonus; + } + } + + if (p->attack_style_this_tick == ATTACK_STYLE_MAGIC) { + AttackStyle weapon_style = get_slot_weapon_attack_style(p); + if (weapon_style != ATTACK_STYLE_MAGIC) { + tick_shaping += cfg->magic_no_staff_penalty; + } + } + + GearBonuses* gear = get_slot_gear_bonuses(p); + int attack_bonus = 0; + switch (p->attack_style_this_tick) { + case ATTACK_STYLE_MAGIC: + attack_bonus = gear->magic_attack; + break; + case ATTACK_STYLE_RANGED: + attack_bonus = gear->ranged_attack; + break; + case ATTACK_STYLE_MELEE: + attack_bonus = gear->slash_attack; + if (gear->stab_attack > attack_bonus) attack_bonus = gear->stab_attack; + if (gear->crush_attack > attack_bonus) attack_bonus = gear->crush_attack; + break; + default: + break; + } + if (attack_bonus < 0) { + tick_shaping += cfg->gear_mismatch_penalty; + } + } + + int ate_food = p->ate_food_this_tick; + int ate_karam = p->ate_karambwan_this_tick; + int ate_brew = p->ate_brew_this_tick; + + if (ate_food || ate_karam) { + float hp_before = p->prev_hp_percent; + if (hp_before > cfg->premature_eat_threshold) { + tick_shaping += cfg->premature_eat_penalty; + } + float max_heal; + if (ate_food) { + max_heal = 20.0f / base_hp; + } else { + max_heal = 18.0f / base_hp; + } + float wasted = hp_before + max_heal - 1.0f; + if (wasted > 0.0f) { + float wasted_hp = wasted * base_hp; + tick_shaping += cfg->wasted_eat_penalty * wasted_hp; + } + } + + if (ate_food && ate_brew && ate_karam) { + float hp_before = p->prev_hp_percent; + float hp_threshold = 45.0f / base_hp; + if (hp_before <= hp_threshold) { + tick_shaping += cfg->smart_triple_eat_bonus; + } else { + float food_brew_heal = (20.0f + 16.0f) / base_hp; + float hp_after_food_brew = hp_before + food_brew_heal; + if (hp_after_food_brew > 1.0f) hp_after_food_brew = 1.0f; + float missing_after = 1.0f - hp_after_food_brew; + float karam_heal_norm = 18.0f / base_hp; + float wasted_karam = karam_heal_norm - missing_after; + if (wasted_karam > 0.0f) { + float wasted_karam_hp = wasted_karam * base_hp; + tick_shaping += cfg->wasted_triple_eat_penalty * wasted_karam_hp; + } + } + } + + reward += tick_shaping * cfg->shaping_scale; + + return reward; +} + +#endif // OSRS_PVP_ACTIONS_H diff --git a/ocean/osrs/osrs_pvp_api.h b/ocean/osrs/osrs_pvp_api.h new file mode 100644 index 0000000000..789ac47af7 --- /dev/null +++ b/ocean/osrs/osrs_pvp_api.h @@ -0,0 +1,598 @@ +#ifndef OSRS_PVP_API_H +#define OSRS_PVP_API_H + +#include "osrs_types.h" +#include "osrs_pvp_gear.h" +#include "osrs_pvp_combat.h" +#include "osrs_pvp_movement.h" +#include "osrs_pvp_observations.h" +#include "osrs_pvp_actions.h" + +static void init_player(Player* p) { + p->base_attack = MAXED_BASE_ATTACK; + p->base_strength = MAXED_BASE_STRENGTH; + p->base_defence = MAXED_BASE_DEFENCE; + p->base_ranged = MAXED_BASE_RANGED; + p->base_magic = MAXED_BASE_MAGIC; + p->base_prayer = MAXED_BASE_PRAYER; + p->base_hitpoints = MAXED_BASE_HITPOINTS; + + p->current_attack = p->base_attack; + p->current_strength = p->base_strength; + p->current_defence = p->base_defence; + p->current_ranged = p->base_ranged; + p->current_magic = p->base_magic; + p->current_prayer = p->base_prayer; + p->current_hitpoints = p->base_hitpoints; + + p->special_energy = 100; + p->spec_regen_active = 0; + p->spec_armed = 0; + osrs_interaction_init(&p->interaction); + osrs_item_effect_state_init(&p->item_effect_state); + + p->current_gear = GEAR_MAGE; + p->visible_gear = GEAR_MAGE; + + p->food_count = MAXED_FOOD_COUNT; + p->karambwan_count = MAXED_KARAMBWAN_COUNT; + p->brew_doses = MAXED_BREW_DOSES; + p->restore_doses = MAXED_RESTORE_DOSES; + p->combat_potion_doses = MAXED_COMBAT_POTION_DOSES; + p->ranged_potion_doses = MAXED_RANGED_POTION_DOSES; + + p->attack_timer = 0; + p->attack_timer_uncapped = 0; + p->has_attack_timer = 0; + p->food_timer = 0; + p->potion_timer = 0; + p->karambwan_timer = 0; + p->consumable_used_this_tick = 0; + p->last_food_heal = 0; + p->last_food_waste = 0; + p->last_karambwan_heal = 0; + p->last_karambwan_waste = 0; + p->last_brew_heal = 0; + p->last_brew_waste = 0; + p->last_potion_type = 0; + p->last_potion_was_waste = 0; + + p->frozen_ticks = 0; + p->freeze_immunity_ticks = 0; + + p->veng_active = 0; + p->veng_cooldown = 0; + + p->prayer = PRAYER_NONE; + p->offensive_prayer = OFFENSIVE_PRAYER_NONE; + p->fight_style = FIGHT_STYLE_ACCURATE; + p->prayer_drain_counter = 0; + p->morr_dot_remaining = 0; + p->morr_dot_tick_counter = 0; + + p->x = 0; + p->y = 0; + p->dest_x = 0; + p->dest_y = 0; + p->is_moving = 0; + p->is_running = 0; + p->run_energy = OSRS_RUN_ENERGY_FULL; + p->run_recovery_ticks = 0; + p->last_obs_target_x = 0; + p->last_obs_target_y = 0; + + p->just_attacked = 0; + p->last_attack_style = ATTACK_STYLE_NONE; + p->attack_was_on_prayer = 0; + p->last_attack_dx = 0; + p->last_attack_dy = 0; + p->last_attack_dist = 0; + p->attack_click_canceled = 0; + p->attack_click_ready = 0; + + memset(p->pending_hits, 0, sizeof(p->pending_hits)); + p->num_pending_hits = 0; + p->damage_applied_this_tick = 0; + p->did_attack_auto_move = 0; + + p->hit_landed_this_tick = 0; + p->hit_was_successful = 0; + p->hit_damage = 0; + p->hit_style = ATTACK_STYLE_NONE; + p->hit_defender_prayer = PRAYER_NONE; + p->hit_was_on_prayer = 0; + p->hit_attacker_idx = -1; + p->freeze_applied_this_tick = 0; + + p->last_target_health_percent = 0.0f; + p->tick_damage_scale = 0.0f; + p->damage_dealt_scale = 0.0f; + p->damage_received_scale = 0.0f; + + p->total_target_hit_count = 0; + p->target_hit_melee_count = 0; + p->target_hit_ranged_count = 0; + p->target_hit_magic_count = 0; + p->target_hit_off_prayer_count = 0; + p->target_hit_correct_count = 0; + + p->total_target_pray_count = 0; + p->target_pray_melee_count = 0; + p->target_pray_ranged_count = 0; + p->target_pray_magic_count = 0; + p->target_pray_correct_count = 0; + + p->player_hit_melee_count = 0; + p->player_hit_ranged_count = 0; + p->player_hit_magic_count = 0; + + p->player_pray_melee_count = 0; + p->player_pray_ranged_count = 0; + p->player_pray_magic_count = 0; + + memset(p->recent_target_attack_styles, 0, sizeof(p->recent_target_attack_styles)); + memset(p->recent_player_attack_styles, 0, sizeof(p->recent_player_attack_styles)); + memset(p->recent_target_prayer_styles, 0, sizeof(p->recent_target_prayer_styles)); + memset(p->recent_player_prayer_styles, 0, sizeof(p->recent_player_prayer_styles)); + memset(p->recent_target_prayer_correct, 0, sizeof(p->recent_target_prayer_correct)); + memset(p->recent_target_hit_correct, 0, sizeof(p->recent_target_hit_correct)); + p->recent_target_attack_index = 0; + p->recent_player_attack_index = 0; + p->recent_target_prayer_index = 0; + p->recent_player_prayer_index = 0; + p->recent_target_prayer_correct_index = 0; + p->recent_target_hit_correct_index = 0; + + p->target_magic_accuracy = -1; + p->target_magic_strength = -1; + p->target_ranged_accuracy = -1; + p->target_ranged_strength = -1; + p->target_melee_accuracy = -1; + p->target_melee_strength = -1; + p->target_magic_gear_magic_defence = -1; + p->target_magic_gear_ranged_defence = -1; + p->target_magic_gear_melee_defence = -1; + p->target_ranged_gear_magic_defence = -1; + p->target_ranged_gear_ranged_defence = -1; + p->target_ranged_gear_melee_defence = -1; + p->target_melee_gear_magic_defence = -1; + p->target_melee_gear_ranged_defence = -1; + p->target_melee_gear_melee_defence = -1; + + p->player_prayed_correct = 0; + p->target_prayed_correct = 0; + + p->total_damage_dealt = 0; + p->total_damage_received = 0; + + p->is_lunar_spellbook = 0; + p->observed_target_lunar_spellbook = 0; + p->has_blood_fury = 1; + + p->melee_spec_weapon = MELEE_SPEC_NONE; + p->ranged_spec_weapon = RANGED_SPEC_NONE; + p->magic_spec_weapon = MAGIC_SPEC_NONE; + + p->bolt_proc_damage = 0.2f; + p->bolt_ignores_defense = 0; + + p->prev_hp_percent = 1.0f; +} + +static void set_fight_positions(OsrsEnv* env) { + int x0, y0, x1, y1; + if (env->has_rng_seed) { + x0 = FIGHT_AREA_BASE_X; + y0 = FIGHT_AREA_BASE_Y; + x1 = x0 + FIGHT_NEARBY_RADIUS; + y1 = y0; + } else { + int base_x = FIGHT_AREA_BASE_X; + int base_y = FIGHT_AREA_BASE_Y; + int max_x = base_x + FIGHT_AREA_WIDTH; + int max_y = base_y + FIGHT_AREA_HEIGHT; + + x0 = base_x + rand_int(env, FIGHT_AREA_WIDTH); + y0 = base_y + rand_int(env, FIGHT_AREA_HEIGHT); + + int near_min_x = max_int(base_x, x0 - FIGHT_NEARBY_RADIUS); + int near_min_y = max_int(base_y, y0 - FIGHT_NEARBY_RADIUS); + int near_max_x = min_int(max_x, x0 + FIGHT_NEARBY_RADIUS); + int near_max_y = min_int(max_y, y0 + FIGHT_NEARBY_RADIUS); + + x1 = near_min_x + rand_int(env, near_max_x - near_min_x); + y1 = near_min_y + rand_int(env, near_max_y - near_min_y); + } + + env->players[0].x = x0; + env->players[0].y = y0; + env->players[0].dest_x = x0; + env->players[0].dest_y = y0; + env->players[0].is_moving = 0; + + env->players[1].x = x1; + env->players[1].y = y1; + env->players[1].dest_x = x1; + env->players[1].dest_y = y1; + env->players[1].is_moving = 0; +} + +/** Point env buffers at internal storage and zero all runtime state. */ +void pvp_init(OsrsEnv* env) { + env->observations = env->_obs_buf; + env->actions = env->_acts_buf; + env->rewards = env->_rews_buf; + env->terminals = env->_terms_buf; + env->action_masks = env->_masks_buf; + env->action_masks_agents = 0x3; + + memset(env->_obs_buf, 0, sizeof(env->_obs_buf)); + memset(env->_acts_buf, 0, sizeof(env->_acts_buf)); + memset(env->_rews_buf, 0, sizeof(env->_rews_buf)); + memset(env->_terms_buf, 0, sizeof(env->_terms_buf)); + memset(env->_masks_buf, 0, sizeof(env->_masks_buf)); + + env->_episode_return = 0.0f; + env->has_rng_seed = 0; + env->is_lms = 1; + env->pvp_runtime.is_pvp_arena = 0; + env->auto_reset = 1; + env->pvp_runtime.use_c_opponent = 0; + env->pvp_runtime.use_c_opponent_p0 = 0; + env->pvp_runtime.use_external_opponent_actions = 0; + env->ocean_io.agent_obs_p1 = NULL; + env->ocean_io.selfplay_mask = NULL; + memset(env->pvp_runtime.external_opponent_actions, 0, sizeof(env->pvp_runtime.external_opponent_actions)); + memset(&env->pvp_runtime.opponent, 0, sizeof(env->pvp_runtime.opponent)); + memset(&env->pvp_runtime.opponent_p0, 0, sizeof(env->pvp_runtime.opponent_p0)); + memset(&env->pvp_runtime.pfsp, 0, sizeof(env->pvp_runtime.pfsp)); + memset(env->pvp_runtime.gear_tier_weights, 0, sizeof(env->pvp_runtime.gear_tier_weights)); + for (int i = 0; i < NUM_AGENTS; i++) { + env->pvp_runtime.walk_dest_x[i] = -1; + env->pvp_runtime.walk_dest_y[i] = -1; + } + memset(&env->shaping, 0, sizeof(env->shaping)); + memset(&env->log, 0, sizeof(env->log)); +} + +void pvp_render(OsrsEnv* env); + +void pvp_reset(OsrsEnv* env) { + if (env->has_rng_seed) { + if (env->rng_seed == 0) { + fprintf(stderr, "Error: seed must be non-zero (use seed=1 or higher in reset())\n"); + abort(); + } + env->rng_state = env->rng_seed + 0x9E3779B9u * env->rng_reset_count; + env->rng_reset_count += 1; + } else { + env->rng_state = (uint32_t)(size_t)env ^ 0xDEADBEEF; + } + + init_player(&env->players[0]); + init_player(&env->players[1]); + + for (int i = 0; i < NUM_AGENTS; i++) { + env->players[i].is_lms = env->is_lms; + if (env->is_lms) { + env->players[i].base_defence = LMS_BASE_DEFENCE; + env->players[i].current_defence = LMS_BASE_DEFENCE; + env->players[i].base_prayer = 99; + env->players[i].current_prayer = 99; + } + } + + set_fight_positions(env); + + env->players[0].last_obs_target_x = env->players[1].x; + env->players[0].last_obs_target_y = env->players[1].y; + env->players[1].last_obs_target_x = env->players[0].x; + env->players[1].last_obs_target_y = env->players[0].y; + + env->tick = 0; + env->episode_over = 0; + env->winner = -1; + if (env->has_rng_seed) { + env->pid_holder = 1 - (int)(env->rng_seed & 1u); + } else { + env->pid_holder = rand_int(env, 2); + } + env->pid_shuffle_countdown = 100 + rand_int(env, 51); + + env->pvp_runtime.is_pvp_arena = 0; + for (int i = 0; i < NUM_AGENTS; i++) { + env->pvp_runtime.walk_dest_x[i] = -1; + env->pvp_runtime.walk_dest_y[i] = -1; + } + + env->_episode_return = 0.0f; + + memset(env->rewards, 0, NUM_AGENTS * sizeof(float)); + memset(env->terminals, 0, NUM_AGENTS); + + memset(env->pending_actions, 0, sizeof(env->pending_actions)); + memset(env->last_executed_actions, 0, sizeof(env->last_executed_actions)); + + int base_tier = sample_gear_tier(env->pvp_runtime.gear_tier_weights, &env->rng_state); + int p1_tier = base_tier; + + float tier_roll = (float)xorshift32(&env->rng_state) / (float)UINT32_MAX; + if (tier_roll >= 0.80f && tier_roll < 0.95f) { + int dir = (xorshift32(&env->rng_state) & 1) ? 1 : -1; + p1_tier = base_tier + dir; + } else if (tier_roll >= 0.95f) { + int dir = (xorshift32(&env->rng_state) & 1) ? 1 : -1; + p1_tier = base_tier + dir * 2; + } + if (p1_tier < 0) p1_tier = 0; + if (p1_tier > 3) p1_tier = 3; + + int tiers[NUM_AGENTS] = { base_tier, p1_tier }; + for (int i = 0; i < NUM_AGENTS; i++) { + init_player_gear_randomized(&env->players[i], tiers[i], &env->rng_state); + env->players[i].food_count = compute_food_count(&env->players[i]); + osrs_refresh_player_equipment(&env->players[i]); + } + + if (env->pvp_runtime.use_c_opponent || env->pvp_runtime.opponent.type == OPP_PFSP) { + opponent_reset(env, &env->pvp_runtime.opponent); + } + if (env->pvp_runtime.use_c_opponent_p0) { + opponent_reset(env, &env->pvp_runtime.opponent_p0); + } + + for (int i = 0; i < NUM_AGENTS; i++) { + generate_slot_observations(env, i); + if (env->action_masks != NULL && (env->action_masks_agents & (1 << i))) { + compute_action_masks(env, i); + } + } +} + +static void pvp_resolve_same_tile(OsrsEnv* env, int first, int second) { + if (env->players[0].x == env->players[1].x && + env->players[0].y == env->players[1].y) { + resolve_same_tile(&env->players[second], &env->players[first], (const CollisionMap*)env->collision_map); + } +} + +/** One game tick: switches for both players, then movement, then attacks, then + pending hits; an action submitted at tick N is visible in state at N+1. */ +void pvp_step(OsrsEnv* env) { + memset(env->rewards, 0, NUM_AGENTS * sizeof(float)); + memset(env->terminals, 0, NUM_AGENTS); + + for (int i = 0; i < NUM_AGENTS; i++) { + env->players[i].hit_landed_this_tick = 0; + env->players[i].hit_was_successful = 0; + env->players[i].hit_damage = 0; + env->players[i].hit_style = ATTACK_STYLE_NONE; + env->players[i].hit_defender_prayer = PRAYER_NONE; + env->players[i].hit_was_on_prayer = 0; + env->players[i].hit_attacker_idx = -1; + env->players[i].freeze_applied_this_tick = 0; + } + reset_tick_flags(&env->players[0]); + reset_tick_flags(&env->players[1]); + + if (env->pvp_runtime.use_c_opponent_p0) { + memset(env->actions, 0, NUM_ACTION_HEADS * sizeof(int)); + } else { + memcpy(env->actions, env->ocean_io.agent_actions, NUM_ACTION_HEADS * sizeof(int)); + } + + if (env->pvp_runtime.use_external_opponent_actions) { + memcpy( + env->actions + NUM_ACTION_HEADS, + env->pvp_runtime.external_opponent_actions, + NUM_ACTION_HEADS * sizeof(int) + ); + } else { + memset(env->actions + NUM_ACTION_HEADS, 0, NUM_ACTION_HEADS * sizeof(int)); + } + + if (env->pvp_runtime.use_c_opponent && !env->pvp_runtime.use_external_opponent_actions) { + generate_opponent_action(env, &env->pvp_runtime.opponent); + memcpy( + env->actions + NUM_ACTION_HEADS, + env->pending_actions + NUM_ACTION_HEADS, + NUM_ACTION_HEADS * sizeof(int) + ); + } + if (env->pvp_runtime.use_c_opponent_p0) { + generate_opponent_action_for_player0(env, &env->pvp_runtime.opponent_p0); + memcpy( + env->actions, + env->pending_actions, + NUM_ACTION_HEADS * sizeof(int) + ); + } + + int first = env->pid_holder; + int second = 1 - env->pid_holder; + + int actions_p0[NUM_ACTION_HEADS]; + int actions_p1[NUM_ACTION_HEADS]; + memcpy(actions_p0, env->actions, NUM_ACTION_HEADS * sizeof(int)); + memcpy(actions_p1, env->actions + NUM_ACTION_HEADS, NUM_ACTION_HEADS * sizeof(int)); + + for (int i = 0; i < NUM_AGENTS; i++) { + int* a = (i == 0) ? actions_p0 : actions_p1; + int lo = a[HEAD_LOADOUT]; + int cv = a[HEAD_COMBAT]; + if (lo == LOADOUT_MAGE || lo == LOADOUT_TANK || lo == LOADOUT_SPEC_MAGIC) { + if (cv == ATTACK_ATK) { + a[HEAD_COMBAT] = ATTACK_NONE; + } + } + } + + memcpy(env->actions, actions_p0, NUM_ACTION_HEADS * sizeof(int)); + memcpy(env->actions + NUM_ACTION_HEADS, actions_p1, NUM_ACTION_HEADS * sizeof(int)); + + memcpy( + env->last_executed_actions, + env->actions, + NUM_AGENTS * NUM_ACTION_HEADS * sizeof(int) + ); + + update_timers(&env->players[0]); + update_timers(&env->players[1]); + + for (int i = 0; i < NUM_AGENTS; i++) { + Player* pi = &env->players[i]; + pi->prev_hp_percent = (float)pi->current_hitpoints / (float)pi->base_hitpoints; + } + + int* agent_actions[NUM_AGENTS]; + agent_actions[0] = actions_p0; + agent_actions[1] = actions_p1; + + int pre_move_x[NUM_AGENTS], pre_move_y[NUM_AGENTS]; + for (int i = 0; i < NUM_AGENTS; i++) { + pre_move_x[i] = env->players[i].x; + pre_move_y[i] = env->players[i].y; + } + + execute_switches(env, first, agent_actions[first]); + execute_switches(env, second, agent_actions[second]); + + for (int i = 0; i < NUM_AGENTS; i++) { + Player* pi = &env->players[i]; + if (pi->food_timer > 0) pi->food_timer--; + if (pi->potion_timer > 0) pi->potion_timer--; + if (pi->karambwan_timer > 0) pi->karambwan_timer--; + } + + pvp_step_player_movement(env, first); + pvp_step_player_movement(env, second); + + pvp_resolve_same_tile(env, first, second); + + execute_attack_movement(env, first, agent_actions[first]); + execute_attack_movement(env, second, agent_actions[second]); + + pvp_resolve_same_tile(env, first, second); + + execute_attack_combat(env, first, agent_actions[first]); + execute_attack_combat(env, second, agent_actions[second]); + + pvp_resolve_same_tile(env, first, second); + + for (int i = 0; i < NUM_AGENTS; i++) { + int dx = abs(env->players[i].x - pre_move_x[i]); + int dy = abs(env->players[i].y - pre_move_y[i]); + int dist = (dx > dy) ? dx : dy; + env->players[i].is_running = (dist >= 2) ? 1 : 0; + } + + process_pending_hits(env, 0, 1); + process_pending_hits(env, 1, 0); + + for (int i = 0; i < NUM_AGENTS; i++) { + Player* p = &env->players[i]; + if (p->morr_dot_remaining > 0) { + p->morr_dot_tick_counter--; + if (p->morr_dot_tick_counter <= 0) { + int dot_dmg = (p->morr_dot_remaining >= 5) ? 5 : p->morr_dot_remaining; + p->current_hitpoints -= dot_dmg; + p->morr_dot_remaining -= dot_dmg; + p->damage_applied_this_tick += dot_dmg; + if (p->current_hitpoints < 0) p->current_hitpoints = 0; + p->morr_dot_tick_counter = 3; + } + } + } + + if (env->players[0].veng_active) { + env->players[1].observed_target_lunar_spellbook = 1; + } + if (env->players[1].veng_active) { + env->players[0].observed_target_lunar_spellbook = 1; + } + env->tick++; + + if (!env->has_rng_seed) { + env->pid_shuffle_countdown--; + if (env->pid_shuffle_countdown <= 0) { + env->pid_holder = 1 - env->pid_holder; + env->pid_shuffle_countdown = 100 + rand_int(env, 51); + } + } + + memcpy(env->pending_actions, env->actions, + NUM_AGENTS * NUM_ACTION_HEADS * sizeof(int)); + for (int i = 0; i < NUM_AGENTS; i++) { + if (env->players[i].current_hitpoints <= 0) { + env->episode_over = 1; + env->winner = 1 - i; + } + } + + if (!env->episode_over && env->tick >= MAX_EPISODE_TICKS) { + env->episode_over = 1; + env->winner = 1; + } + for (int i = 0; i < NUM_AGENTS; i++) { + env->rewards[i] = calculate_reward(env, i); + + if (env->episode_over) { + env->terminals[i] = 1; + } + } + + env->_episode_return += env->rewards[0]; + for (int i = 0; i < NUM_AGENTS; i++) { + generate_slot_observations(env, i); + if (env->action_masks != NULL && (env->action_masks_agents & (1 << i))) { + compute_action_masks(env, i); + } + } + + ocean_write_obs(env); + if (env->ocean_io.agent_obs_p1 != NULL) { + ocean_write_obs_p1(env); + } + env->ocean_io.agent_rewards[0] = env->rewards[0]; + + if (env->episode_over) { + env->ocean_io.agent_terminals[0] = 1; + + if (env->pvp_runtime.opponent.type == OPP_PFSP && env->pvp_runtime.pfsp.active_pool_idx >= 0) { + int idx = env->pvp_runtime.pfsp.active_pool_idx; + env->pvp_runtime.pfsp.episodes[idx] += 1.0f; + if (env->winner == 0) { + env->pvp_runtime.pfsp.wins[idx] += 1.0f; + } + } + + Player* p0 = &env->players[0]; + env->log.episode_return = env->_episode_return; + env->log.episode_length = (float)env->tick; + env->log.damage_dealt = p0->total_damage_dealt; + env->log.damage_received = p0->total_damage_received; + env->log.wins = (env->winner == 0) ? 1.0f : 0.0f; + env->log.prayer_correct = (float)p0->target_pray_correct_count; + env->log.prayer_total = (float)(p0->target_pray_melee_count + + p0->target_pray_ranged_count + p0->target_pray_magic_count); + env->log.food_remaining = (float)p0->food_count; + env->log.karambwan_remaining = (float)p0->karambwan_count; + env->log.brews_remaining = (float)p0->brew_doses; + env->log.spec_energy_remaining = (float)p0->special_energy; + env->log.attacks_landed = (float)p0->total_target_hit_count; + env->log.off_prayer_hits = (float)p0->target_hit_off_prayer_count; + env->log.n = 1.0f; + + if (env->auto_reset) { + pvp_reset(env); + } + } else { + env->ocean_io.agent_terminals[0] = 0; + } +} + +void pvp_close(OsrsEnv* env) { + (void)env; +} + +#endif // OSRS_PVP_API_H diff --git a/ocean/osrs/osrs_pvp_combat.h b/ocean/osrs/osrs_pvp_combat.h new file mode 100644 index 0000000000..58a1344ce2 --- /dev/null +++ b/ocean/osrs/osrs_pvp_combat.h @@ -0,0 +1,1079 @@ +#ifndef OSRS_PVP_COMBAT_H +#define OSRS_PVP_COMBAT_H + +#include "osrs_types.h" +#include "osrs_combat.h" +#include "osrs_special_attacks.h" +#include "osrs_damage.h" +#include "osrs_bolt_procs.h" +#include "osrs_pvp_gear.h" + +static inline int pvp_melee_spec_to_item(MeleeSpecWeapon w) { + switch (w) { + case MELEE_SPEC_AGS: return ITEM_AGS; + case MELEE_SPEC_DRAGON_CLAWS: return ITEM_DRAGON_CLAWS; + case MELEE_SPEC_GRANITE_MAUL: return ITEM_GRANITE_MAUL; + case MELEE_SPEC_DRAGON_DAGGER: return ITEM_DRAGON_DAGGER; + case MELEE_SPEC_VOIDWAKER: return ITEM_VOIDWAKER; + case MELEE_SPEC_DWH: return ITEM_STATIUS_WARHAMMER; + case MELEE_SPEC_BGS: return ITEM_BGS; + case MELEE_SPEC_ZGS: return ITEM_ZGS; + case MELEE_SPEC_SGS: return ITEM_SGS; + case MELEE_SPEC_ANCIENT_GS: return ITEM_ANCIENT_GS; + case MELEE_SPEC_VESTAS: return ITEM_VESTAS; + default: return ITEM_NONE; + } +} + +static inline int pvp_ranged_spec_to_item(RangedSpecWeapon w) { + switch (w) { + case RANGED_SPEC_DARK_BOW: return ITEM_DARK_BOW; + case RANGED_SPEC_BALLISTA: return ITEM_HEAVY_BALLISTA; + case RANGED_SPEC_ACB: return ITEM_ARMADYL_CROSSBOW; + case RANGED_SPEC_ZCB: return ITEM_ZARYTE_CROSSBOW; + case RANGED_SPEC_MSB: return ITEM_MAGIC_SHORTBOW_I; + case RANGED_SPEC_MORRIGANS: return ITEM_MORRIGANS_JAVELIN; + default: return ITEM_NONE; + } +} + +static inline int pvp_magic_spec_to_item(MagicSpecWeapon w) { + switch (w) { + case MAGIC_SPEC_VOLATILE_STAFF: return ITEM_VOLATILE_STAFF; + default: return ITEM_NONE; + } +} + +static int get_melee_spec_cost(MeleeSpecWeapon weapon) { + int cost = osrs_spec_cost(pvp_melee_spec_to_item(weapon)); + return cost > 0 ? cost : 50; +} + +static int get_ranged_spec_cost(RangedSpecWeapon weapon) { + int cost = osrs_spec_cost(pvp_ranged_spec_to_item(weapon)); + return cost > 0 ? cost : 50; +} + +static int get_magic_spec_cost(MagicSpecWeapon weapon) { + int cost = osrs_spec_cost(pvp_magic_spec_to_item(weapon)); + return cost > 0 ? cost : 50; +} + +static float get_melee_spec_str_mult(MeleeSpecWeapon weapon) { + switch (weapon) { + case MELEE_SPEC_AGS: return 1.375f; + case MELEE_SPEC_DRAGON_CLAWS: return 1.0f; + case MELEE_SPEC_GRANITE_MAUL: return 1.0f; + case MELEE_SPEC_DRAGON_DAGGER: return 1.15f; + case MELEE_SPEC_VOIDWAKER: return 1.0f; + case MELEE_SPEC_DWH: return 1.25f; + case MELEE_SPEC_BGS: return 1.21f; + case MELEE_SPEC_ZGS: return 1.1f; + case MELEE_SPEC_SGS: return 1.1f; + case MELEE_SPEC_ANCIENT_GS: return 1.1f; + case MELEE_SPEC_VESTAS: return 1.20f; + case MELEE_SPEC_ABYSSAL_DAGGER: return 0.85f; + case MELEE_SPEC_DRAGON_LONGSWORD:return 1.15f; + case MELEE_SPEC_DRAGON_MACE: return 1.5f; + case MELEE_SPEC_ABYSSAL_BLUDGEON:return 1.20f; + default: return 1.0f; + } +} + +static float get_melee_spec_acc_mult(MeleeSpecWeapon weapon) { + switch (weapon) { + case MELEE_SPEC_AGS: return 2.0f; + case MELEE_SPEC_DRAGON_CLAWS: return 1.0f; + case MELEE_SPEC_GRANITE_MAUL: return 1.0f; + case MELEE_SPEC_DRAGON_DAGGER: return 1.15f; + case MELEE_SPEC_VOIDWAKER: return 1.0f; + case MELEE_SPEC_DWH: return 1.25f; + case MELEE_SPEC_BGS: return 2.0f; + case MELEE_SPEC_ZGS: return 2.0f; + case MELEE_SPEC_SGS: return 2.0f; + case MELEE_SPEC_ANCIENT_GS: return 2.0f; + case MELEE_SPEC_VESTAS: return 1.0f; + case MELEE_SPEC_ABYSSAL_DAGGER: return 1.25f; + case MELEE_SPEC_DRAGON_LONGSWORD:return 1.25f; + case MELEE_SPEC_DRAGON_MACE: return 1.25f; + case MELEE_SPEC_ABYSSAL_BLUDGEON:return 1.0f; + default: return 1.0f; + } +} + +static float get_ranged_spec_str_mult(RangedSpecWeapon weapon) { + switch (weapon) { + case RANGED_SPEC_DARK_BOW: return 1.5f; + case RANGED_SPEC_BALLISTA: return 1.25f; + case RANGED_SPEC_ACB: return 1.0f; + case RANGED_SPEC_ZCB: return 1.0f; + case RANGED_SPEC_DRAGON_KNIFE:return 1.0f; + case RANGED_SPEC_MSB: return 1.0f; + case RANGED_SPEC_MORRIGANS: return 1.0f; + default: return 1.0f; + } +} + +static float get_ranged_spec_acc_mult(RangedSpecWeapon weapon) { + switch (weapon) { + case RANGED_SPEC_DARK_BOW: return 1.0f; + case RANGED_SPEC_BALLISTA: return 1.25f; + case RANGED_SPEC_ACB: return 2.0f; + case RANGED_SPEC_ZCB: return 2.0f; + case RANGED_SPEC_DRAGON_KNIFE:return 1.0f; + case RANGED_SPEC_MSB: return 1.0f; + case RANGED_SPEC_MORRIGANS: return 1.0f; + default: return 1.0f; + } +} + +static inline float get_defence_prayer_mult(Player* p) { + switch (p->offensive_prayer) { + case OFFENSIVE_PRAYER_MELEE_LOW: + case OFFENSIVE_PRAYER_RANGED_LOW: + case OFFENSIVE_PRAYER_MAGIC_LOW: + return 1.15f; + case OFFENSIVE_PRAYER_PIETY: + case OFFENSIVE_PRAYER_RIGOUR: + case OFFENSIVE_PRAYER_AUGURY: + return 1.25f; + default: + return 1.0f; + } +} + +static int calculate_effective_attack(Player* p, AttackStyle style) { + int base_level; + float prayer_mult = 1.0f; + + switch (style) { + case ATTACK_STYLE_MELEE: + base_level = p->current_attack; + if (p->offensive_prayer == OFFENSIVE_PRAYER_PIETY) prayer_mult = 1.20f; + else if (p->offensive_prayer == OFFENSIVE_PRAYER_MELEE_LOW) prayer_mult = 1.15f; + break; + case ATTACK_STYLE_RANGED: + base_level = p->current_ranged; + if (p->offensive_prayer == OFFENSIVE_PRAYER_RIGOUR) prayer_mult = 1.20f; + else if (p->offensive_prayer == OFFENSIVE_PRAYER_RANGED_LOW) prayer_mult = 1.15f; + break; + case ATTACK_STYLE_MAGIC: + base_level = p->current_magic; + if (p->offensive_prayer == OFFENSIVE_PRAYER_AUGURY) prayer_mult = 1.25f; + else if (p->offensive_prayer == OFFENSIVE_PRAYER_MAGIC_LOW) prayer_mult = 1.15f; + break; + default: + return 0; + } + + int style_bonus = osrs_stance_att_bonus(p->fight_style, style); + + if (style == ATTACK_STYLE_MAGIC) + return osrs_magic_effective_attack_level(base_level, prayer_mult, p->fight_style); + return osrs_player_eff_level(base_level, prayer_mult, style_bonus); +} + +static int calculate_effective_strength(Player* p, AttackStyle style) { + int base_level; + float prayer_mult = 1.0f; + + switch (style) { + case ATTACK_STYLE_MELEE: + base_level = p->current_strength; + if (p->offensive_prayer == OFFENSIVE_PRAYER_PIETY) prayer_mult = 1.23f; + else if (p->offensive_prayer == OFFENSIVE_PRAYER_MELEE_LOW) prayer_mult = 1.15f; + break; + case ATTACK_STYLE_RANGED: + base_level = p->current_ranged; + if (p->offensive_prayer == OFFENSIVE_PRAYER_RIGOUR) prayer_mult = 1.23f; + else if (p->offensive_prayer == OFFENSIVE_PRAYER_RANGED_LOW) prayer_mult = 1.15f; + break; + case ATTACK_STYLE_MAGIC: + base_level = p->current_magic; + break; + default: + return 0; + } + + int style_bonus = (style == ATTACK_STYLE_MELEE) ? osrs_stance_str_bonus(p->fight_style) : 0; + + return osrs_player_eff_level(base_level, prayer_mult, style_bonus); +} + +static int calculate_effective_defence(Player* p, AttackStyle incoming_style) { + int base_level = p->current_defence; + float prayer_mult = get_defence_prayer_mult(p); + int style_bonus = osrs_stance_def_bonus(p->fight_style); + + if (incoming_style == ATTACK_STYLE_MAGIC) { + float magic_prayer_mult = 1.0f; + if (p->offensive_prayer == OFFENSIVE_PRAYER_AUGURY) magic_prayer_mult = 1.25f; + else if (p->offensive_prayer == OFFENSIVE_PRAYER_MAGIC_LOW) magic_prayer_mult = 1.15f; + int magic_level = (int)floorf(p->current_magic * magic_prayer_mult); + int def_level = (int)floorf(p->current_defence * prayer_mult); + return (int)(magic_level * 0.7f + def_level * 0.3f) + style_bonus + 8; + } + + return osrs_player_eff_level(base_level, prayer_mult, style_bonus); +} + +static MeleeBonusType get_melee_bonus_type(Player* p) { + if (p->current_gear == GEAR_SPEC) { + return MELEE_SPEC_BONUS_TYPES[p->melee_spec_weapon]; + } + GearBonuses* g = get_slot_gear_bonuses(p); + MeleeBonusType best = MELEE_BONUS_STAB; + int best_val = g->stab_attack; + if (g->slash_attack > best_val) { best = MELEE_BONUS_SLASH; best_val = g->slash_attack; } + if (g->crush_attack > best_val) { best = MELEE_BONUS_CRUSH; } + return best; +} + +static int get_attack_bonus(Player* p, AttackStyle style) { + GearBonuses* g = get_slot_gear_bonuses(p); + switch (style) { + case ATTACK_STYLE_MELEE: { + MeleeBonusType bonus = get_melee_bonus_type(p); + switch (bonus) { + case MELEE_BONUS_STAB: return g->stab_attack; + case MELEE_BONUS_SLASH: return g->slash_attack; + case MELEE_BONUS_CRUSH: return g->crush_attack; + default: return g->slash_attack; + } + } + case ATTACK_STYLE_RANGED: return g->ranged_attack; + case ATTACK_STYLE_MAGIC: return g->magic_attack; + default: return 0; + } +} + +static int get_defence_bonus_for_melee_type(Player* p, MeleeBonusType melee_type) { + GearBonuses* g = get_slot_gear_bonuses(p); + switch (melee_type) { + case MELEE_BONUS_STAB: return g->stab_defence; + case MELEE_BONUS_SLASH: return g->slash_defence; + case MELEE_BONUS_CRUSH: return g->crush_defence; + default: return g->slash_defence; + } +} + +static int get_defence_bonus(Player* defender, AttackStyle style, Player* attacker) { + GearBonuses* g = get_slot_gear_bonuses(defender); + switch (style) { + case ATTACK_STYLE_MELEE: { + MeleeBonusType bonus = get_melee_bonus_type(attacker); + return get_defence_bonus_for_melee_type(defender, bonus); + } + case ATTACK_STYLE_RANGED: return g->ranged_defence; + case ATTACK_STYLE_MAGIC: return g->magic_defence; + default: return 0; + } +} + +static int get_strength_bonus(Player* p, AttackStyle style) { + GearBonuses* g = get_slot_gear_bonuses(p); + switch (style) { + case ATTACK_STYLE_MELEE: return g->melee_strength; + case ATTACK_STYLE_RANGED: return g->ranged_strength; + case ATTACK_STYLE_MAGIC: return g->magic_strength; + default: return 0; + } +} + +static float calculate_hit_chance(Player* attacker, Player* defender, + AttackStyle style, float acc_mult) { + int eff_attack = calculate_effective_attack(attacker, style); + int attack_bonus = get_attack_bonus(attacker, style); + int attack_roll = (int)(eff_attack * (attack_bonus + 64) * acc_mult); + + int eff_defence = calculate_effective_defence(defender, style); + int defence_bonus = get_defence_bonus(defender, style, attacker); + int defence_roll = eff_defence * (defence_bonus + 64); + + return clampf(osrs_hit_chance(attack_roll, defence_roll), 0.0f, 1.0f); +} + +static int calculate_max_hit(Player* p, AttackStyle style, float str_mult, int magic_base_hit) { + int eff_strength = calculate_effective_strength(p, style); + int strength_bonus = get_strength_bonus(p, style); + + int max_hit; + if (style == ATTACK_STYLE_MAGIC) { + int base_damage = magic_base_hit; + float magic_mult = osrs_offensive_magic_dmg_mult(p->offensive_prayer); + max_hit = (int)(base_damage * (1.0f + strength_bonus / 100.0f) * str_mult * magic_mult); + } else if (style == ATTACK_STYLE_RANGED) { + max_hit = (int)(osrs_player_ranged_max_hit(eff_strength, strength_bonus) * str_mult); + } else { + max_hit = (int)(osrs_player_melee_max_hit(eff_strength, strength_bonus) * str_mult); + } + + osrs_ensure_player_equipment(p); + if (p->equipment_effect_profile.dharok_piece_count >= 4 && + style == ATTACK_STYLE_MELEE) { + float hp_ratio = 1.0f - ((float)p->current_hitpoints / p->base_hitpoints); + max_hit = (int)(max_hit * (1.0f + hp_ratio * hp_ratio)); + } + + return max_hit; +} + +static inline int get_ice_freeze_ticks(int current_magic) { + if (current_magic >= ICE_BARRAGE_LEVEL) return 32; + if (current_magic >= ICE_BLITZ_LEVEL) return 24; + if (current_magic >= ICE_BURST_LEVEL) return 16; + return 8; +} + +static inline int get_ice_base_hit(int current_magic) { + if (current_magic >= ICE_BARRAGE_LEVEL) return ICE_BARRAGE_MAX_HIT; + if (current_magic >= ICE_BLITZ_LEVEL) return ICE_BLITZ_MAX_HIT; + if (current_magic >= ICE_BURST_LEVEL) return ICE_BURST_MAX_HIT; + return ICE_RUSH_MAX_HIT; +} + +static inline int get_blood_base_hit(int current_magic) { + if (current_magic >= BLOOD_BARRAGE_LEVEL) return BLOOD_BARRAGE_MAX_HIT; + if (current_magic >= BLOOD_BLITZ_LEVEL) return BLOOD_BLITZ_MAX_HIT; + if (current_magic >= BLOOD_BURST_LEVEL) return BLOOD_BURST_MAX_HIT; + return BLOOD_RUSH_MAX_HIT; +} + +static inline int get_blood_heal_percent(int current_magic) { + if (current_magic >= BLOOD_BARRAGE_LEVEL) return 25; + if (current_magic >= BLOOD_BLITZ_LEVEL) return 20; + if (current_magic >= BLOOD_BURST_LEVEL) return 15; + return 10; +} + +/* pvp hit delays historically excluded the +1 attacker-is-player tick, so every + wrapper passes is_player=0; passing 1 changes all pvp combat timing */ +static inline int pvp_magic_hit_delay(int distance) { + return encounter_projectile_hit_delay( + distance, 0, ENCOUNTER_PROJECTILE_DELAY_MAGIC, + (EncounterProjectileDelayOptions){0}); +} + +static inline int pvp_ranged_hit_delay(int distance) { + return encounter_projectile_hit_delay( + distance, 0, ENCOUNTER_PROJECTILE_DELAY_RANGED, + (EncounterProjectileDelayOptions){0}); +} + +static inline int pvp_ranged_hit_delay_fast(int distance) { + return encounter_projectile_hit_delay( + distance, 0, ENCOUNTER_PROJECTILE_DELAY_THROWN, + (EncounterProjectileDelayOptions){0}); +} + +static inline int pvp_ranged_hit_delay_ballista(int distance) { + return encounter_projectile_hit_delay( + distance, 0, ENCOUNTER_PROJECTILE_DELAY_BALLISTA, + (EncounterProjectileDelayOptions){0}); +} + +static inline int pvp_ranged_hit_delay_dbow_second(int distance) { + return encounter_projectile_hit_delay( + distance, 0, ENCOUNTER_PROJECTILE_DELAY_DARK_BOW_SECOND, + (EncounterProjectileDelayOptions){0}); +} + +static inline int pvp_ranged_hit_delay_for_weapon(int distance, int is_special, RangedSpecWeapon weapon) { + if (!is_special) return pvp_ranged_hit_delay(distance); + switch (weapon) { + case RANGED_SPEC_DRAGON_KNIFE: + case RANGED_SPEC_MORRIGANS: + return pvp_ranged_hit_delay_fast(distance); + case RANGED_SPEC_BALLISTA: + return pvp_ranged_hit_delay_ballista(distance); + default: + return pvp_ranged_hit_delay(distance); + } +} + +typedef enum { + PVP_DEF_DRAIN_NONE = 0, + PVP_DEF_DRAIN_PERCENT_OF_CURRENT = 1, + PVP_DEF_DRAIN_BY_DAMAGE = 2, +} PvpDefDrainType; + +static void pvp_remove_pending_hit(Player* attacker, int idx) { + if (attacker->num_pending_hits < 0 || attacker->num_pending_hits > MAX_PENDING_HITS) { + fprintf(stderr, "pvp pending-hit queue corrupt before remove count=%d\n", + attacker->num_pending_hits); + abort(); + } + if (idx < 0 || idx >= attacker->num_pending_hits) { + fprintf(stderr, "pvp pending-hit queue invalid remove idx=%d count=%d\n", + idx, attacker->num_pending_hits); + abort(); + } + for (int i = idx + 1; i < attacker->num_pending_hits; i++) + attacker->pending_hits[i - 1] = attacker->pending_hits[i]; + attacker->num_pending_hits--; + memset(&attacker->pending_hits[attacker->num_pending_hits], 0, + sizeof(attacker->pending_hits[attacker->num_pending_hits])); +} + +static void queue_hit(int tick, int attacker_idx, int defender_idx, + Player* attacker, Player* defender, int damage, + AttackStyle style, int delay, int is_special, int hit_success, + int freeze_ticks, int heal_percent, int drain_type, int drain_percent, + int flat_heal) { + if (attacker->num_pending_hits < 0 || attacker->num_pending_hits > MAX_PENDING_HITS) { + fprintf(stderr, + "pvp pending-hit queue corrupt tick=%d attacker=%d defender=%d count=%d\n", + tick, attacker_idx, defender_idx, attacker->num_pending_hits); + abort(); + } + if (attacker->num_pending_hits >= MAX_PENDING_HITS) { + fprintf(stderr, + "pvp pending-hit queue overflow tick=%d attacker=%d defender=%d " + "count=%d delay=%d style=%d special=%d damage=%d\n", + tick, attacker_idx, defender_idx, attacker->num_pending_hits, + delay, style, is_special, damage); + abort(); + } + + PendingHit* hit = &attacker->pending_hits[attacker->num_pending_hits++]; + hit->damage = damage; + hit->ticks_until_hit = delay; + hit->attack_type = style; + hit->is_special = is_special; + hit->hit_success = hit_success; + hit->freeze_ticks = freeze_ticks; + hit->heal_percent = heal_percent; + hit->drain_type = drain_type; + hit->drain_percent = drain_percent; + hit->flat_heal = flat_heal; + hit->is_morr_bleed = 0; + hit->defender_prayer_at_attack = defender->prayer; + + int actual_damage = osrs_prayer_reduce_damage(damage, defender->prayer, style, 1); + attacker->last_queued_hit_damage += actual_damage; +} + +static void apply_damage(OsrsEnv* env, int attacker_idx, int defender_idx, + PendingHit* hit) { + Player* attacker = &env->players[attacker_idx]; + Player* defender = &env->players[defender_idx]; + + osrs_ensure_player_equipment(defender); + + DamageResult dr = osrs_apply_passive_damage_pipeline( + hit->damage, hit->attack_type, + hit->defender_prayer_at_attack, + /* is_pvp */ 1, + defender->veng_active, + attacker->prayer == PRAYER_SMITE && !defender->is_lms + , + &defender->equipment_effect_profile, + &defender->item_effect_state, + &env->rng_state + ); + + int damage = dr.final_damage; + + defender->hit_landed_this_tick = 1; + defender->hit_was_successful = hit->hit_success; + defender->hit_damage += damage; + defender->hit_style = hit->attack_type; + defender->hit_defender_prayer = hit->defender_prayer_at_attack; + defender->hit_was_on_prayer = dr.prayer_blocked; + defender->hit_attacker_idx = attacker_idx; + defender->damage_applied_this_tick = damage; + + if (dr.veng_damage > 0) { + attacker->current_hitpoints -= dr.veng_damage; + if (attacker->current_hitpoints < 0) attacker->current_hitpoints = 0; + float reflect_scale = (float)dr.veng_damage / (float)attacker->base_hitpoints; + attacker->total_damage_received += reflect_scale; + defender->total_damage_dealt += reflect_scale; + attacker->damage_received_scale += reflect_scale; + defender->damage_dealt_scale += reflect_scale; + defender->veng_active = 0; + } + + if (dr.recoil_damage > 0) { + int recoil = dr.recoil_damage; + if (defender->equipment_effect_profile.recoil_source == OSRS_RECOIL_SOURCE_RING_OF_RECOIL && + recoil > defender->item_effect_state.recoil_charges) { + recoil = defender->item_effect_state.recoil_charges; + } + attacker->current_hitpoints -= recoil; + if (attacker->current_hitpoints < 0) attacker->current_hitpoints = 0; + float recoil_scale = (float)recoil / (float)attacker->base_hitpoints; + attacker->total_damage_received += recoil_scale; + defender->total_damage_dealt += recoil_scale; + attacker->damage_received_scale += recoil_scale; + defender->damage_dealt_scale += recoil_scale; + osrs_consume_recoil_charges(defender, recoil); + } + + defender->current_hitpoints -= damage; + if (defender->current_hitpoints < 0) defender->current_hitpoints = 0; + float damage_scale = (float)damage / (float)defender->base_hitpoints; + defender->total_damage_received += damage_scale; + attacker->total_damage_dealt += damage_scale; + defender->damage_received_scale += damage_scale; + attacker->damage_dealt_scale += damage_scale; + attacker->last_target_health_percent = + (float)defender->current_hitpoints / (float)defender->base_hitpoints; + + if (hit->hit_success) { + if (hit->drain_type == PVP_DEF_DRAIN_PERCENT_OF_CURRENT && damage > 0) { + int drain = (int)(defender->current_defence * hit->drain_percent / 100.0f); + defender->current_defence = clamp(defender->current_defence - drain, 1, 255); + } else if (hit->drain_type == PVP_DEF_DRAIN_BY_DAMAGE && damage > 0) { + defender->current_defence = clamp(defender->current_defence - damage, 1, 255); + } + + if (hit->freeze_ticks > 0 && defender->freeze_immunity_ticks == 0 && defender->frozen_ticks == 0) { + defender->frozen_ticks = hit->freeze_ticks; + defender->freeze_immunity_ticks = hit->freeze_ticks + 5; + defender->freeze_applied_this_tick = 1; + } + + if (hit->heal_percent > 0) { + int heal = (damage * hit->heal_percent) / 100; + attacker->current_hitpoints = clamp(attacker->current_hitpoints + heal, 0, attacker->base_hitpoints); + } + if (hit->flat_heal > 0) { + attacker->current_hitpoints = clamp(attacker->current_hitpoints + hit->flat_heal, 0, attacker->base_hitpoints); + } + } + + if (hit->is_morr_bleed && hit->hit_success && damage > 0) { + defender->morr_dot_remaining = damage; + } + + if (dr.smite_drain > 0) { + defender->current_prayer = clamp(defender->current_prayer - dr.smite_drain, 0, defender->base_prayer); + } +} + +static void process_pending_hits(OsrsEnv* env, int attacker_idx, int defender_idx) { + Player* attacker = &env->players[attacker_idx]; + + for (int i = 0; i < attacker->num_pending_hits; i++) { + PendingHit* hit = &attacker->pending_hits[i]; + hit->ticks_until_hit--; + + if (hit->ticks_until_hit < 0) { + apply_damage(env, attacker_idx, defender_idx, hit); + + pvp_remove_pending_hit(attacker, i); + i--; + } + } +} + +static inline void push_recent_attack(AttackStyle* buffer, int* index, AttackStyle style) { + buffer[*index] = style; + *index = (*index + 1) % HISTORY_SIZE; +} + +static inline void push_recent_prayer(AttackStyle* buffer, int* index, OverheadPrayer prayer) { + AttackStyle style = ATTACK_STYLE_NONE; + if (prayer == PRAYER_PROTECT_MAGIC) style = ATTACK_STYLE_MAGIC; + else if (prayer == PRAYER_PROTECT_RANGED) style = ATTACK_STYLE_RANGED; + else if (prayer == PRAYER_PROTECT_MELEE) style = ATTACK_STYLE_MELEE; + if (style == ATTACK_STYLE_NONE) return; + buffer[*index] = style; + *index = (*index + 1) % HISTORY_SIZE; +} + +static inline void push_recent_bool(int* buffer, int* index, int value) { + buffer[*index] = value ? 1 : 0; + *index = (*index + 1) % HISTORY_SIZE; +} + +static void register_hit_calculated( + OsrsEnv* env, + int attacker_idx, + int defender_idx, + AttackStyle style, + int total_damage +) { + Player* attacker = &env->players[attacker_idx]; + Player* defender = &env->players[defender_idx]; + GearBonuses* atk_gear = get_slot_gear_bonuses(attacker); + VisibleGearBonuses visible_buf = { + .magic_attack = atk_gear->magic_attack, + .magic_strength = atk_gear->magic_strength, + .ranged_attack = atk_gear->ranged_attack, + .ranged_strength = atk_gear->ranged_strength, + .melee_attack = max_int(atk_gear->stab_attack, max_int(atk_gear->slash_attack, atk_gear->crush_attack)), + .melee_strength = atk_gear->melee_strength, + .magic_defence = atk_gear->magic_defence, + .ranged_defence = atk_gear->ranged_defence, + .melee_defence = max_int(atk_gear->stab_defence, max_int(atk_gear->slash_defence, atk_gear->crush_defence)), + }; + const VisibleGearBonuses* visible = &visible_buf; + + defender->total_target_hit_count += 1; + push_recent_attack(defender->recent_target_attack_styles, &defender->recent_target_attack_index, style); + + if (style == ATTACK_STYLE_MAGIC) { + defender->target_hit_magic_count += 1; + defender->target_magic_accuracy = visible->magic_attack; + defender->target_magic_strength = visible->magic_strength; + defender->target_magic_gear_magic_defence = visible->magic_defence; + defender->target_magic_gear_ranged_defence = visible->ranged_defence; + defender->target_magic_gear_melee_defence = visible->melee_defence; + } else if (style == ATTACK_STYLE_RANGED) { + defender->target_hit_ranged_count += 1; + defender->target_ranged_accuracy = visible->ranged_attack; + defender->target_ranged_strength = visible->ranged_strength; + defender->target_ranged_gear_magic_defence = visible->magic_defence; + defender->target_ranged_gear_ranged_defence = visible->ranged_defence; + defender->target_ranged_gear_melee_defence = visible->melee_defence; + } else if (style == ATTACK_STYLE_MELEE) { + defender->target_hit_melee_count += 1; + if (visible->melee_strength >= defender->target_melee_strength) { + defender->target_melee_accuracy = visible->melee_attack; + defender->target_melee_strength = visible->melee_strength; + defender->target_melee_gear_magic_defence = visible->magic_defence; + defender->target_melee_gear_ranged_defence = visible->ranged_defence; + defender->target_melee_gear_melee_defence = visible->melee_defence; + } + } + + if (defender->prayer == PRAYER_PROTECT_MAGIC) { + defender->player_pray_magic_count += 1; + push_recent_prayer(defender->recent_player_prayer_styles, &defender->recent_player_prayer_index, defender->prayer); + } else if (defender->prayer == PRAYER_PROTECT_RANGED) { + defender->player_pray_ranged_count += 1; + push_recent_prayer(defender->recent_player_prayer_styles, &defender->recent_player_prayer_index, defender->prayer); + } else if (defender->prayer == PRAYER_PROTECT_MELEE) { + defender->player_pray_melee_count += 1; + push_recent_prayer(defender->recent_player_prayer_styles, &defender->recent_player_prayer_index, defender->prayer); + } + + int defender_prayed_correctly = encounter_prayer_correct_for_style(defender->prayer, style); + if (!defender_prayed_correctly) { + defender->target_hit_correct_count += 1; + push_recent_bool(defender->recent_target_hit_correct, &defender->recent_target_hit_correct_index, 1); + } else { + defender->player_prayed_correct = 1; + push_recent_bool(defender->recent_target_hit_correct, &defender->recent_target_hit_correct_index, 0); + } + + attacker->attack_was_on_prayer = defender_prayed_correctly; + + push_recent_attack(attacker->recent_player_attack_styles, &attacker->recent_player_attack_index, style); + if (style == ATTACK_STYLE_MAGIC) attacker->player_hit_magic_count += 1; + else if (style == ATTACK_STYLE_RANGED) attacker->player_hit_ranged_count += 1; + else if (style == ATTACK_STYLE_MELEE) attacker->player_hit_melee_count += 1; + attacker->tick_damage_scale = (float)total_damage / (float)defender->base_hitpoints; + attacker->total_target_pray_count += 1; + + if (defender->prayer == PRAYER_PROTECT_MAGIC) { + attacker->target_pray_magic_count += 1; + push_recent_prayer(attacker->recent_target_prayer_styles, &attacker->recent_target_prayer_index, defender->prayer); + } else if (defender->prayer == PRAYER_PROTECT_RANGED) { + attacker->target_pray_ranged_count += 1; + push_recent_prayer(attacker->recent_target_prayer_styles, &attacker->recent_target_prayer_index, defender->prayer); + } else if (defender->prayer == PRAYER_PROTECT_MELEE) { + attacker->target_pray_melee_count += 1; + push_recent_prayer(attacker->recent_target_prayer_styles, &attacker->recent_target_prayer_index, defender->prayer); + } + + if (encounter_prayer_correct_for_style(defender->prayer, style)) { + attacker->target_pray_correct_count += 1; + attacker->target_prayed_correct = 1; + push_recent_bool(attacker->recent_target_prayer_correct, &attacker->recent_target_prayer_correct_index, 1); + } else { + push_recent_bool(attacker->recent_target_prayer_correct, &attacker->recent_target_prayer_correct_index, 0); + } +} + +static inline int is_attack_available(Player* p) { + if (ONLY_SWITCH_GEAR_WHEN_ATTACK_SOON && remaining_ticks(p->attack_timer) > 0) return 0; + return 1; +} + +static inline int is_melee_weapon_equipped(Player* p) { + return get_slot_weapon_attack_style(p) == ATTACK_STYLE_MELEE; +} + +static inline int is_ranged_weapon_equipped(Player* p) { + return get_slot_weapon_attack_style(p) == ATTACK_STYLE_RANGED; +} + +static inline int is_melee_spec_weapon_equipped(Player* p) { + return p->melee_spec_weapon != MELEE_SPEC_NONE; +} + +static inline int is_ranged_spec_weapon_equipped(Player* p) { + return p->ranged_spec_weapon != RANGED_SPEC_NONE; +} + +static inline int is_magic_spec_weapon_equipped(Player* p) { + return p->magic_spec_weapon != MAGIC_SPEC_NONE; +} + +static inline int can_cast_ice_spell(Player* p) { + if (p->is_lunar_spellbook) return 0; + return p->current_magic >= ICE_RUSH_LEVEL; +} + +static inline int can_cast_blood_spell(Player* p) { + if (p->is_lunar_spellbook) return 0; + return p->current_magic >= BLOOD_RUSH_LEVEL; +} + +static inline int is_ranged_attack_available(Player* p) { + if (!is_attack_available(p)) return 0; + return is_ranged_weapon_equipped(p); +} + +static inline int is_melee_attack_available(Player* p, Player* t) { + if (!is_attack_available(p)) return 0; + (void)t; + return is_melee_weapon_equipped(p); +} + +static inline int is_melee_spec_two_handed(MeleeSpecWeapon weapon) { + switch (weapon) { + case MELEE_SPEC_AGS: + case MELEE_SPEC_DRAGON_CLAWS: + case MELEE_SPEC_BGS: + case MELEE_SPEC_ZGS: + case MELEE_SPEC_SGS: + case MELEE_SPEC_ANCIENT_GS: + case MELEE_SPEC_ABYSSAL_BLUDGEON: + return 1; + default: + return 0; + } +} + +static inline int has_free_inventory_slot(Player* p) { + int food_slots = p->food_count + p->karambwan_count; + int max_food_slots = MAXED_FOOD_COUNT + MAXED_KARAMBWAN_COUNT; + return food_slots < max_food_slots; +} + +static inline int can_equip_two_handed_weapon(Player* p) { + return has_free_inventory_slot(p) || p->equipped[GEAR_SLOT_SHIELD] == ITEM_NONE; +} + +static inline int can_spec(Player* p) { + int cost = get_melee_spec_cost(p->melee_spec_weapon); + return p->melee_spec_weapon != MELEE_SPEC_NONE && p->special_energy >= cost; +} + +static inline int is_granite_maul_attack_available(Player* p) { + if (p->melee_spec_weapon != MELEE_SPEC_GRANITE_MAUL) return 0; + return p->special_energy >= get_melee_spec_cost(MELEE_SPEC_GRANITE_MAUL); +} + +static inline int is_melee_spec_attack_available(Player* p, Player* t) { + (void)t; + if (!is_granite_maul_attack_available(p) && !is_attack_available(p)) return 0; + if (is_melee_spec_two_handed(p->melee_spec_weapon) && !can_equip_two_handed_weapon(p)) return 0; + if (!is_melee_weapon_equipped(p) || !is_melee_spec_weapon_equipped(p)) return 0; + return can_spec(p); +} + +static inline int is_ranged_spec_attack_available(Player* p) { + if (!is_attack_available(p)) return 0; + if (!is_ranged_attack_available(p)) return 0; + if (p->ranged_spec_weapon == RANGED_SPEC_NONE) return 0; + if (!is_ranged_spec_weapon_equipped(p)) return 0; + return p->special_energy >= get_ranged_spec_cost(p->ranged_spec_weapon); +} + +static inline int is_ice_attack_available(Player* p) { + if (p->is_lunar_spellbook) return 0; + return can_cast_ice_spell(p) && is_attack_available(p); +} + +static inline int is_blood_attack_available(Player* p) { + if (p->is_lunar_spellbook) return 0; + return can_cast_blood_spell(p) && is_attack_available(p); +} + +static inline int can_toggle_spec(Player* p) { + if (is_melee_spec_weapon_equipped(p) && p->melee_spec_weapon != MELEE_SPEC_NONE) { + if (is_melee_spec_two_handed(p->melee_spec_weapon) && !can_equip_two_handed_weapon(p)) return 0; + return p->special_energy >= get_melee_spec_cost(p->melee_spec_weapon); + } + if (is_ranged_spec_weapon_equipped(p) && p->ranged_spec_weapon != RANGED_SPEC_NONE) + return p->special_energy >= get_ranged_spec_cost(p->ranged_spec_weapon); + if (is_magic_spec_weapon_equipped(p) && p->magic_spec_weapon != MAGIC_SPEC_NONE) + return p->special_energy >= get_magic_spec_cost(p->magic_spec_weapon); + return 0; +} + +static inline int is_special_ready(Player* p, AttackStyle style) { + switch (style) { + case ATTACK_STYLE_MELEE: + if (!is_melee_spec_weapon_equipped(p) || p->melee_spec_weapon == MELEE_SPEC_NONE) return 0; + if (is_melee_spec_two_handed(p->melee_spec_weapon) && !can_equip_two_handed_weapon(p)) return 0; + return p->special_energy >= get_melee_spec_cost(p->melee_spec_weapon); + case ATTACK_STYLE_RANGED: + if (!is_ranged_spec_weapon_equipped(p) || p->ranged_spec_weapon == RANGED_SPEC_NONE) return 0; + return p->special_energy >= get_ranged_spec_cost(p->ranged_spec_weapon); + case ATTACK_STYLE_MAGIC: + if (!is_magic_spec_weapon_equipped(p) || p->magic_spec_weapon == MAGIC_SPEC_NONE) return 0; + return p->special_energy >= get_magic_spec_cost(p->magic_spec_weapon); + default: + return 0; + } +} + +static inline int get_ticks_until_next_hit(Player* p) { + int min_ticks = -1; + for (int i = 0; i < p->num_pending_hits; i++) { + if (min_ticks < 0 || p->pending_hits[i].ticks_until_hit < min_ticks) { + min_ticks = p->pending_hits[i].ticks_until_hit; + } + } + return min_ticks; +} + +static inline int get_attack_range(Player* p, AttackStyle style) { + switch (style) { + case ATTACK_STYLE_MELEE: + return 1; + case ATTACK_STYLE_RANGED: + case ATTACK_STYLE_MAGIC: + return get_slot_gear_bonuses(p)->attack_range; + default: + return 1; + } +} + +static void perform_attack(OsrsEnv* env, int attacker_idx, int defender_idx, + AttackStyle style, int is_special, int magic_type, int distance) { + Player* attacker = &env->players[attacker_idx]; + Player* defender = &env->players[defender_idx]; + + int dx = abs_int(attacker->x - defender->x); + int dy = abs_int(attacker->y - defender->y); + attacker->last_attack_dx = dx; + attacker->last_attack_dy = dy; + attacker->last_attack_dist = (dx > dy) ? dx : dy; + + if (style == ATTACK_STYLE_MELEE && !is_in_melee_range(attacker, defender)) return; + + float acc_mult = 1.0f; + float str_mult = 1.0f; + int spec_cost = 0; + int was_special_requested = is_special; + int spec_item_idx = ITEM_NONE; + + if (is_special) { + switch (style) { + case ATTACK_STYLE_MELEE: { + MeleeSpecWeapon weapon = attacker->melee_spec_weapon; + spec_cost = get_melee_spec_cost(weapon); + spec_item_idx = pvp_melee_spec_to_item(weapon); + break; + } + case ATTACK_STYLE_RANGED: { + RangedSpecWeapon weapon = attacker->ranged_spec_weapon; + spec_cost = get_ranged_spec_cost(weapon); + spec_item_idx = pvp_ranged_spec_to_item(weapon); + break; + } + case ATTACK_STYLE_MAGIC: { + MagicSpecWeapon weapon = attacker->magic_spec_weapon; + spec_cost = get_magic_spec_cost(weapon); + spec_item_idx = pvp_magic_spec_to_item(weapon); + break; + } + default: + break; + } + + if (attacker->special_energy < spec_cost) { + is_special = 0; + spec_item_idx = ITEM_NONE; + } else { + attacker->special_energy -= spec_cost; + if (!attacker->spec_regen_active && attacker->special_energy < 100) { + attacker->spec_regen_active = 1; + attacker->item_effect_state.special_regen_ticks = 0; + } + } + } + + if (style == ATTACK_STYLE_MELEE) + attacker->current_gear = was_special_requested ? GEAR_SPEC : GEAR_MELEE; + else if (style == ATTACK_STYLE_RANGED) + attacker->current_gear = GEAR_RANGED; + else if (style == ATTACK_STYLE_MAGIC) + attacker->current_gear = GEAR_MAGE; + + if (is_special && spec_item_idx != ITEM_NONE) { + int eff_attack = calculate_effective_attack(attacker, style); + int attack_bonus = get_attack_bonus(attacker, style); + int att_roll = eff_attack * (attack_bonus + 64); + + int eff_defence = calculate_effective_defence(defender, style); + int defence_bonus = get_defence_bonus(defender, style, attacker); + int def_roll = eff_defence * (defence_bonus + 64); + + int magic_base_hit = 30; + int max_hit = calculate_max_hit(attacker, style, 1.0f, magic_base_hit); + + SpecResult sr = osrs_resolve_spec( + spec_item_idx, att_roll, max_hit, def_roll, + defender->current_defence, &env->rng_state + ); + + int total_damage = sr.total_damage; + int hit_delay; + if (style == ATTACK_STYLE_MELEE) + hit_delay = 0; + else if (style == ATTACK_STYLE_RANGED) + hit_delay = pvp_ranged_hit_delay_for_weapon(distance, 1, attacker->ranged_spec_weapon); + else + hit_delay = pvp_magic_hit_delay(distance); + + int drain_type = PVP_DEF_DRAIN_NONE; + int drain_percent = 0; + int freeze_ticks = sr.freeze_ticks; + int heal_percent = 0, flat_heal = 0; + + if (sr.def_drain > 0) { + if (spec_item_idx == ITEM_BGS) { + drain_type = PVP_DEF_DRAIN_BY_DAMAGE; + } else { + drain_type = PVP_DEF_DRAIN_PERCENT_OF_CURRENT; + drain_percent = (spec_item_idx == ITEM_STATIUS_WARHAMMER) ? 30 : + (spec_item_idx == ITEM_ELDER_MAUL) ? 35 : 0; + } + } + if (sr.heal > 0 && spec_item_idx == ITEM_SGS) { + heal_percent = 50; + } + + AttackStyle hit_style = (spec_item_idx == ITEM_VOIDWAKER) ? ATTACK_STYLE_MAGIC : style; + + for (int i = 0; i < sr.num_hits; i++) { + int this_delay = hit_delay; + if (spec_item_idx == ITEM_DARK_BOW && i == 1) + this_delay = pvp_ranged_hit_delay_dbow_second(distance); + queue_hit(env->tick, attacker_idx, defender_idx, + attacker, defender, sr.damage[i], hit_style, this_delay, 1, + sr.damage[i] > 0, freeze_ticks, heal_percent, drain_type, drain_percent, flat_heal); + } + + register_hit_calculated(env, attacker_idx, defender_idx, hit_style, total_damage); + + if (spec_item_idx == ITEM_ANCIENT_GS && total_damage > 0) { + int ags_heal = clamp((int)(defender->base_hitpoints * 0.15f), 0, 15); + queue_hit(env->tick, attacker_idx, defender_idx, + attacker, defender, 25, ATTACK_STYLE_MAGIC, 8, 1, 1, 0, 0, 0, 0, ags_heal); + } + + if (spec_item_idx == ITEM_MORRIGANS_JAVELIN && total_damage > 0) { + attacker->pending_hits[attacker->num_pending_hits - 1].is_morr_bleed = 1; + defender->morr_dot_tick_counter = 3; + } + + goto post_attack; + } + + { + int has_zuriels = (attacker->equipped[GEAR_SLOT_WEAPON] == ITEM_ZURIELS_STAFF); + if (has_zuriels && style == ATTACK_STYLE_MAGIC && magic_type == 1) + acc_mult *= 1.10f; + + float hit_chance = calculate_hit_chance(attacker, defender, style, acc_mult); + int magic_base_hit = 30; + if (style == ATTACK_STYLE_MAGIC) { + if (magic_type == 1) magic_base_hit = get_ice_base_hit(attacker->current_magic); + else if (magic_type == 2) magic_base_hit = get_blood_base_hit(attacker->current_magic); + } + int max_hit = calculate_max_hit(attacker, style, str_mult, magic_base_hit); + + int hit_delay; + if (style == ATTACK_STYLE_MELEE) + hit_delay = 0; + else if (style == ATTACK_STYLE_RANGED) + hit_delay = pvp_ranged_hit_delay(distance); + else + hit_delay = pvp_magic_hit_delay(distance); + + int freeze_ticks = 0, heal_percent = 0; + + if (style == ATTACK_STYLE_MAGIC) { + if (magic_type == 1) { + freeze_ticks = get_ice_freeze_ticks(attacker->current_magic); + if (has_zuriels) freeze_ticks = (int)(freeze_ticks * 1.10f); + } else if (magic_type == 2) { + heal_percent = get_blood_heal_percent(attacker->current_magic); + if (has_zuriels) heal_percent = (int)(heal_percent * 1.50f); + } + } + + int total_damage = 0; + int apply_magic_freeze_on_calc = (style == ATTACK_STYLE_MAGIC && magic_type == 1); + + int ammo_item = attacker->equipped[GEAR_SLOT_AMMO]; + int is_crossbow_ranged = (style == ATTACK_STYLE_RANGED && !is_special); + + int hit_count = 1; + for (int i = 0; i < hit_count; i++) { + int damage = 0; + int hit_success = 0; + + if (rand_float(env) < hit_chance) { + hit_success = 1; + damage = rand_int(env, max_hit + 1); + } + + if (is_crossbow_ranged) { + BoltProcResult bp = osrs_resolve_bolt_proc( + ammo_item, damage, hit_success, max_hit, + attacker->current_ranged, + defender->current_hitpoints, + 0, &env->rng_state + ); + if (bp.proc_triggered) { + damage = bp.modified_damage; + hit_success = 1; + } + } + + total_damage += damage; + + int queued_freeze_ticks = freeze_ticks; + if (apply_magic_freeze_on_calc) { + if (hit_success && defender->freeze_immunity_ticks == 0 && defender->frozen_ticks == 0) { + defender->frozen_ticks = freeze_ticks; + defender->freeze_immunity_ticks = freeze_ticks + 5; + defender->freeze_applied_this_tick = 1; + defender->hit_attacker_idx = attacker_idx; + } + queued_freeze_ticks = 0; + } + queue_hit(env->tick, attacker_idx, defender_idx, + attacker, defender, damage, style, hit_delay, is_special, + hit_success, queued_freeze_ticks, heal_percent, 0, 0, 0); + } + register_hit_calculated(env, attacker_idx, defender_idx, style, total_damage); + } + +post_attack: + attacker->just_attacked = 1; + attacker->last_attack_style = (is_special && spec_item_idx == ITEM_VOIDWAKER) ? ATTACK_STYLE_MAGIC : style; + attacker->attack_style_this_tick = attacker->last_attack_style; + attacker->magic_type_this_tick = magic_type; + attacker->used_special_this_tick = is_special; + + int attack_speed = get_slot_gear_bonuses(attacker)->attack_speed; + int is_instant = (is_special && spec_item_idx == ITEM_GRANITE_MAUL); + if (!is_instant) { + attacker->attack_timer = attack_speed - 1; + attacker->attack_timer_uncapped = attack_speed - 1; + attacker->has_attack_timer = 1; + } +} + +#endif // OSRS_PVP_COMBAT_H diff --git a/ocean/osrs/osrs_pvp_gear.h b/ocean/osrs/osrs_pvp_gear.h new file mode 100644 index 0000000000..d2ec45b86d --- /dev/null +++ b/ocean/osrs/osrs_pvp_gear.h @@ -0,0 +1,755 @@ +#ifndef OSRS_PVP_GEAR_H +#define OSRS_PVP_GEAR_H + +#include "osrs_types.h" +#include "osrs_items.h" +#include "osrs_inventory.h" +#include "osrs_combat.h" +#include "osrs_item_effects.h" + +static const MeleeBonusType MELEE_SPEC_BONUS_TYPES[] = { + [MELEE_SPEC_NONE] = MELEE_BONUS_SLASH, + [MELEE_SPEC_AGS] = MELEE_BONUS_SLASH, + [MELEE_SPEC_DRAGON_CLAWS] = MELEE_BONUS_SLASH, + [MELEE_SPEC_GRANITE_MAUL] = MELEE_BONUS_CRUSH, + [MELEE_SPEC_DRAGON_DAGGER] = MELEE_BONUS_STAB, + [MELEE_SPEC_VOIDWAKER] = MELEE_BONUS_SLASH, + [MELEE_SPEC_DWH] = MELEE_BONUS_CRUSH, + [MELEE_SPEC_BGS] = MELEE_BONUS_SLASH, + [MELEE_SPEC_ZGS] = MELEE_BONUS_SLASH, + [MELEE_SPEC_SGS] = MELEE_BONUS_SLASH, + [MELEE_SPEC_ANCIENT_GS] = MELEE_BONUS_SLASH, + [MELEE_SPEC_VESTAS] = MELEE_BONUS_SLASH, + [MELEE_SPEC_ABYSSAL_DAGGER] = MELEE_BONUS_STAB, + [MELEE_SPEC_DRAGON_LONGSWORD] = MELEE_BONUS_SLASH, + [MELEE_SPEC_DRAGON_MACE] = MELEE_BONUS_CRUSH, + [MELEE_SPEC_ABYSSAL_BLUDGEON] = MELEE_BONUS_CRUSH, +}; + +static const uint8_t MELEE_WEAPON_PRIORITY[] = { + ITEM_VESTAS, ITEM_GHRAZI_RAPIER, ITEM_INQUISITORS_MACE, ITEM_ELDER_MAUL, + ITEM_VOIDWAKER, ITEM_ANCIENT_GS, ITEM_AGS, ITEM_STATIUS_WARHAMMER, ITEM_WHIP +}; + +static const uint8_t RANGE_WEAPON_PRIORITY[] = { + ITEM_MORRIGANS_JAVELIN, ITEM_ZARYTE_CROSSBOW, ITEM_ARMADYL_CROSSBOW, ITEM_RUNE_CROSSBOW +}; + +static const uint8_t MAGE_WEAPON_PRIORITY[] = { + ITEM_ZURIELS_STAFF, ITEM_KODAI_WAND, ITEM_VOLATILE_STAFF, + ITEM_STAFF_OF_DEAD, ITEM_AHRIM_STAFF +}; + +static const uint8_t MELEE_SPEC_PRIORITY[] = { + ITEM_VESTAS, ITEM_ANCIENT_GS, ITEM_AGS, ITEM_DRAGON_CLAWS, + ITEM_VOIDWAKER, ITEM_STATIUS_WARHAMMER, ITEM_DRAGON_DAGGER +}; +#define MELEE_SPEC_PRIORITY_LEN (sizeof(MELEE_SPEC_PRIORITY) / sizeof(MELEE_SPEC_PRIORITY[0])) + +static const uint8_t RANGE_SPEC_PRIORITY[] = { + ITEM_MORRIGANS_JAVELIN, ITEM_ZARYTE_CROSSBOW, ITEM_ARMADYL_CROSSBOW, + ITEM_DARK_BOW, ITEM_HEAVY_BALLISTA +}; +#define RANGE_SPEC_PRIORITY_LEN (sizeof(RANGE_SPEC_PRIORITY) / sizeof(RANGE_SPEC_PRIORITY[0])) + +static const uint8_t MAGIC_SPEC_PRIORITY[] = { + ITEM_VOLATILE_STAFF +}; +#define MAGIC_SPEC_PRIORITY_LEN (sizeof(MAGIC_SPEC_PRIORITY) / sizeof(MAGIC_SPEC_PRIORITY[0])) + +static const uint8_t TANK_BODY_PRIORITY[] = { + ITEM_KARILS_TOP, ITEM_BLACK_DHIDE_BODY +}; + +static const uint8_t MAGE_BODY_PRIORITY[] = { + ITEM_ANCESTRAL_TOP, ITEM_AHRIMS_ROBETOP, ITEM_MYSTIC_TOP +}; + +static const uint8_t TANK_LEGS_PRIORITY[] = { + ITEM_BANDOS_TASSETS, ITEM_TORAGS_PLATELEGS, ITEM_DHAROKS_PLATELEGS, + ITEM_VERACS_PLATESKIRT, ITEM_RUNE_PLATELEGS +}; + +static const uint8_t MAGE_LEGS_PRIORITY[] = { + ITEM_ANCESTRAL_BOTTOM, ITEM_AHRIMS_ROBESKIRT, ITEM_MYSTIC_BOTTOM +}; + +static const uint8_t MELEE_SHIELD_PRIORITY[] = { + ITEM_DRAGON_DEFENDER +}; + +static const uint8_t TANK_SHIELD_PRIORITY[] = { + ITEM_BLESSED_SPIRIT_SHIELD, ITEM_SPIRIT_SHIELD +}; + +static const uint8_t MAGE_SHIELD_PRIORITY[] = { + ITEM_MAGES_BOOK, ITEM_BLESSED_SPIRIT_SHIELD, ITEM_SPIRIT_SHIELD +}; + +static const uint8_t TANK_HEAD_PRIORITY[] = { + ITEM_TORAGS_HELM, ITEM_GUTHANS_HELM, ITEM_VERACS_HELM, + ITEM_DHAROKS_HELM, ITEM_HELM_NEITIZNOT +}; + +static const uint8_t MAGE_HEAD_PRIORITY[] = {ITEM_ANCESTRAL_HAT, ITEM_HELM_NEITIZNOT}; + +static const uint8_t MELEE_CAPE_PRIORITY[] = {ITEM_INFERNAL_CAPE, ITEM_GOD_CAPE}; + +static const uint8_t MAGE_CAPE_PRIORITY[] = {ITEM_GOD_CAPE}; + +static const uint8_t MELEE_NECK_PRIORITY[] = {ITEM_FURY, ITEM_GLORY}; + +static const uint8_t MAGE_NECK_PRIORITY[] = {ITEM_OCCULT_NECKLACE, ITEM_GLORY}; + +static const uint8_t MELEE_RING_PRIORITY[] = {ITEM_BERSERKER_RING}; + +static const uint8_t MAGE_RING_PRIORITY[] = {ITEM_LIGHTBEARER, ITEM_SEERS_RING_I, ITEM_BERSERKER_RING}; + +static inline GearBonuses* get_slot_gear_bonuses(Player* p) { + osrs_ensure_player_equipment(p); + return &p->slot_cached_bonuses; +} + +static inline void update_spec_weapons_for_weapon(Player* p, uint8_t weapon_item) { + p->melee_spec_weapon = MELEE_SPEC_NONE; + p->ranged_spec_weapon = RANGED_SPEC_NONE; + p->magic_spec_weapon = MAGIC_SPEC_NONE; + + switch (weapon_item) { + case ITEM_DRAGON_DAGGER: + p->melee_spec_weapon = MELEE_SPEC_DRAGON_DAGGER; break; + case ITEM_DRAGON_CLAWS: + p->melee_spec_weapon = MELEE_SPEC_DRAGON_CLAWS; break; + case ITEM_AGS: + p->melee_spec_weapon = MELEE_SPEC_AGS; break; + case ITEM_ANCIENT_GS: + p->melee_spec_weapon = MELEE_SPEC_ANCIENT_GS; break; + case ITEM_GRANITE_MAUL: + p->melee_spec_weapon = MELEE_SPEC_GRANITE_MAUL; break; + case ITEM_VESTAS: + p->melee_spec_weapon = MELEE_SPEC_VESTAS; break; + case ITEM_VOIDWAKER: + p->melee_spec_weapon = MELEE_SPEC_VOIDWAKER; break; + case ITEM_STATIUS_WARHAMMER: + p->melee_spec_weapon = MELEE_SPEC_DWH; break; + case ITEM_ELDER_MAUL: + break; + case ITEM_DARK_BOW: + p->ranged_spec_weapon = RANGED_SPEC_DARK_BOW; break; + case ITEM_HEAVY_BALLISTA: + p->ranged_spec_weapon = RANGED_SPEC_BALLISTA; break; + case ITEM_ARMADYL_CROSSBOW: + p->ranged_spec_weapon = RANGED_SPEC_ACB; break; + case ITEM_ZARYTE_CROSSBOW: + p->ranged_spec_weapon = RANGED_SPEC_ZCB; break; + case ITEM_MORRIGANS_JAVELIN: + p->ranged_spec_weapon = RANGED_SPEC_MORRIGANS; break; + case ITEM_VOLATILE_STAFF: + p->magic_spec_weapon = MAGIC_SPEC_VOLATILE_STAFF; break; + default: + break; + } +} + +static inline int item_is_spec_weapon(uint8_t weapon_item) { + switch (weapon_item) { + case ITEM_DRAGON_DAGGER: + case ITEM_DRAGON_CLAWS: + case ITEM_AGS: + case ITEM_ANCIENT_GS: + case ITEM_GRANITE_MAUL: + case ITEM_VESTAS: + case ITEM_VOIDWAKER: + case ITEM_STATIUS_WARHAMMER: + case ITEM_DARK_BOW: + case ITEM_HEAVY_BALLISTA: + case ITEM_ARMADYL_CROSSBOW: + case ITEM_ZARYTE_CROSSBOW: + case ITEM_MORRIGANS_JAVELIN: + case ITEM_VOLATILE_STAFF: + return 1; + default: + return 0; + } +} + +/* PvP inventories are per-slot LMS upgrade pools, not the flat 28-slot bag, so + osrs_equip_from_inventory does not apply here */ +static inline int slot_equip_item(Player* p, int gear_slot, uint8_t item_idx) { + if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) return 0; + if (p->equipped[gear_slot] == item_idx) return 0; + + p->equipped[gear_slot] = item_idx; + p->slot_gear_dirty = 1; + + if (gear_slot == GEAR_SLOT_WEAPON && item_idx < NUM_ITEMS) { + update_spec_weapons_for_weapon(p, item_idx); + int style = get_item_attack_style(item_idx); + + if (item_is_spec_weapon(item_idx)) { + p->current_gear = GEAR_SPEC; + } else if (style == ATTACK_STYLE_MELEE) { + p->current_gear = GEAR_MELEE; + } else if (style == ATTACK_STYLE_RANGED) { + p->current_gear = GEAR_RANGED; + } else if (style == ATTACK_STYLE_MAGIC) { + p->current_gear = GEAR_MAGE; + } + + if (item_idx == ITEM_VOIDWAKER) { + p->visible_gear = GEAR_MAGE; + } else if (style == ATTACK_STYLE_MELEE) { + p->visible_gear = GEAR_MELEE; + } else if (style == ATTACK_STYLE_RANGED) { + p->visible_gear = GEAR_RANGED; + } else if (style == ATTACK_STYLE_MAGIC) { + p->visible_gear = GEAR_MAGE; + } + } + + if (gear_slot == GEAR_SLOT_WEAPON) { + p->equipped[GEAR_SLOT_SHIELD] = osrs_suppress_shield_for_two_handed_weapon( + item_idx, p->equipped[GEAR_SLOT_SHIELD]); + } + + osrs_refresh_player_equipment(p); + return 1; +} + +static inline int player_has_item_in_slot(Player* p, int gear_slot, uint8_t item_idx) { + for (int i = 0; i < p->num_items_in_slot[gear_slot]; i++) { + if (p->inventory[gear_slot][i] == item_idx) return 1; + } + return 0; +} + +static inline uint8_t find_best_available( + Player* p, int gear_slot, + const uint8_t* priority, int priority_len +) { + for (int i = 0; i < priority_len; i++) { + if (player_has_item_in_slot(p, gear_slot, priority[i])) { + return priority[i]; + } + } + return ITEM_NONE; +} + +static inline uint8_t find_best_melee_spec(Player* p) { + return find_best_available(p, GEAR_SLOT_WEAPON, MELEE_SPEC_PRIORITY, MELEE_SPEC_PRIORITY_LEN); +} + +static inline uint8_t find_best_ranged_spec(Player* p) { + return find_best_available(p, GEAR_SLOT_WEAPON, RANGE_SPEC_PRIORITY, RANGE_SPEC_PRIORITY_LEN); +} + +static inline uint8_t find_best_magic_spec(Player* p) { + return find_best_available(p, GEAR_SLOT_WEAPON, MAGIC_SPEC_PRIORITY, MAGIC_SPEC_PRIORITY_LEN); +} + +static inline int player_has_gmaul(Player* p) { + return player_has_item_in_slot(p, GEAR_SLOT_WEAPON, ITEM_GRANITE_MAUL); +} + +typedef struct { + const uint8_t* items; + int len; +} GearPriorityList; + +typedef struct { + GearPriorityList weapon; + GearPriorityList shield; + GearPriorityList body; + GearPriorityList legs; + GearPriorityList head; + GearPriorityList cape; + GearPriorityList neck; + GearPriorityList ring; + int shield_two_handed_aware; +} LoadoutPriorities; + +#define GEAR_LIST(arr) { arr, (int)(sizeof(arr) / sizeof((arr)[0])) } + +/* rows are positional in LOADOUT_* enum order (KEEP hole first, then MELEE, + RANGE, MAGE, TANK, SPEC_MELEE, SPEC_RANGE, SPEC_MAGIC); columns are weapon, + shield, body, legs, head, cape, neck, ring, shield_two_handed_aware. + g++ on the trainer include path cannot compile designated initializers here. */ +static const LoadoutPriorities LOADOUT_PRIORITIES[LOADOUT_GMAUL] = { + { {NULL, 0}, {NULL, 0}, {NULL, 0}, {NULL, 0}, + {NULL, 0}, {NULL, 0}, {NULL, 0}, {NULL, 0}, 0 }, + { GEAR_LIST(MELEE_WEAPON_PRIORITY), GEAR_LIST(MELEE_SHIELD_PRIORITY), + GEAR_LIST(TANK_BODY_PRIORITY), GEAR_LIST(TANK_LEGS_PRIORITY), + GEAR_LIST(TANK_HEAD_PRIORITY), GEAR_LIST(MELEE_CAPE_PRIORITY), + GEAR_LIST(MELEE_NECK_PRIORITY), GEAR_LIST(MELEE_RING_PRIORITY), 1 }, + { GEAR_LIST(RANGE_WEAPON_PRIORITY), GEAR_LIST(TANK_SHIELD_PRIORITY), + GEAR_LIST(TANK_BODY_PRIORITY), GEAR_LIST(TANK_LEGS_PRIORITY), + GEAR_LIST(TANK_HEAD_PRIORITY), GEAR_LIST(MAGE_CAPE_PRIORITY), + GEAR_LIST(MELEE_NECK_PRIORITY), GEAR_LIST(MAGE_RING_PRIORITY), 0 }, + { GEAR_LIST(MAGE_WEAPON_PRIORITY), GEAR_LIST(MAGE_SHIELD_PRIORITY), + GEAR_LIST(MAGE_BODY_PRIORITY), GEAR_LIST(MAGE_LEGS_PRIORITY), + GEAR_LIST(MAGE_HEAD_PRIORITY), GEAR_LIST(MAGE_CAPE_PRIORITY), + GEAR_LIST(MAGE_NECK_PRIORITY), GEAR_LIST(MAGE_RING_PRIORITY), 0 }, + { GEAR_LIST(MAGE_WEAPON_PRIORITY), GEAR_LIST(TANK_SHIELD_PRIORITY), + GEAR_LIST(TANK_BODY_PRIORITY), GEAR_LIST(TANK_LEGS_PRIORITY), + GEAR_LIST(TANK_HEAD_PRIORITY), GEAR_LIST(MAGE_CAPE_PRIORITY), + GEAR_LIST(MELEE_NECK_PRIORITY), GEAR_LIST(MAGE_RING_PRIORITY), 0 }, + { GEAR_LIST(MELEE_SPEC_PRIORITY), GEAR_LIST(MELEE_SHIELD_PRIORITY), + GEAR_LIST(TANK_BODY_PRIORITY), GEAR_LIST(TANK_LEGS_PRIORITY), + GEAR_LIST(TANK_HEAD_PRIORITY), GEAR_LIST(MELEE_CAPE_PRIORITY), + GEAR_LIST(MELEE_NECK_PRIORITY), GEAR_LIST(MELEE_RING_PRIORITY), 1 }, + { GEAR_LIST(RANGE_SPEC_PRIORITY), GEAR_LIST(TANK_SHIELD_PRIORITY), + GEAR_LIST(TANK_BODY_PRIORITY), GEAR_LIST(TANK_LEGS_PRIORITY), + GEAR_LIST(TANK_HEAD_PRIORITY), GEAR_LIST(MAGE_CAPE_PRIORITY), + GEAR_LIST(MELEE_NECK_PRIORITY), GEAR_LIST(MELEE_RING_PRIORITY), 1 }, + { GEAR_LIST(MAGIC_SPEC_PRIORITY), GEAR_LIST(MAGE_SHIELD_PRIORITY), + GEAR_LIST(MAGE_BODY_PRIORITY), GEAR_LIST(MAGE_LEGS_PRIORITY), + GEAR_LIST(MAGE_HEAD_PRIORITY), GEAR_LIST(MAGE_CAPE_PRIORITY), + GEAR_LIST(MAGE_NECK_PRIORITY), GEAR_LIST(MAGE_RING_PRIORITY), 0 }, +}; + +/* out[] order must match DYNAMIC_GEAR_SLOTS: weapon, shield, body, legs, head, + cape, neck, ring */ +static inline void resolve_loadout(Player* p, int loadout, uint8_t out[NUM_DYNAMIC_GEAR_SLOTS]) { + for (int i = 0; i < NUM_DYNAMIC_GEAR_SLOTS; i++) { + out[i] = p->equipped[DYNAMIC_GEAR_SLOTS[i]]; + } + + if (loadout == LOADOUT_GMAUL) { + out[0] = ITEM_GRANITE_MAUL; + out[1] = osrs_suppress_shield_for_two_handed_weapon(out[0], out[1]); + return; + } + if (loadout < LOADOUT_MELEE || loadout > LOADOUT_SPEC_MAGIC) { + return; + } + + const LoadoutPriorities* lp = &LOADOUT_PRIORITIES[loadout]; + + uint8_t weapon = find_best_available(p, GEAR_SLOT_WEAPON, lp->weapon.items, lp->weapon.len); + if (weapon != ITEM_NONE) out[0] = weapon; + + if (lp->shield_two_handed_aware && item_is_two_handed(out[0])) { + out[1] = osrs_suppress_shield_for_two_handed_weapon(out[0], out[1]); + } else { + uint8_t shield = find_best_available(p, GEAR_SLOT_SHIELD, lp->shield.items, lp->shield.len); + if (shield != ITEM_NONE) out[1] = shield; + } + + uint8_t body = find_best_available(p, GEAR_SLOT_BODY, lp->body.items, lp->body.len); + if (body != ITEM_NONE) out[2] = body; + uint8_t legs = find_best_available(p, GEAR_SLOT_LEGS, lp->legs.items, lp->legs.len); + if (legs != ITEM_NONE) out[3] = legs; + uint8_t head = find_best_available(p, GEAR_SLOT_HEAD, lp->head.items, lp->head.len); + if (head != ITEM_NONE) out[4] = head; + uint8_t cape = find_best_available(p, GEAR_SLOT_CAPE, lp->cape.items, lp->cape.len); + if (cape != ITEM_NONE) out[5] = cape; + uint8_t neck = find_best_available(p, GEAR_SLOT_NECK, lp->neck.items, lp->neck.len); + if (neck != ITEM_NONE) out[6] = neck; + uint8_t ring = find_best_available(p, GEAR_SLOT_RING, lp->ring.items, lp->ring.len); + if (ring != ITEM_NONE) out[7] = ring; +} + +static inline int apply_loadout(Player* p, int loadout) { + if (loadout <= LOADOUT_KEEP || loadout > LOADOUT_GMAUL) return 0; + + uint8_t resolved[NUM_DYNAMIC_GEAR_SLOTS]; + resolve_loadout(p, loadout, resolved); + + int changed = 0; + for (int i = 0; i < NUM_DYNAMIC_GEAR_SLOTS; i++) { + int gear_slot = DYNAMIC_GEAR_SLOTS[i]; + changed += slot_equip_item(p, gear_slot, resolved[i]); + } + + return changed; +} + +static inline int is_loadout_active(Player* p, int loadout) { + if (loadout <= LOADOUT_KEEP || loadout > LOADOUT_GMAUL) return 0; + + uint8_t resolved[NUM_DYNAMIC_GEAR_SLOTS]; + resolve_loadout(p, loadout, resolved); + + for (int i = 0; i < NUM_DYNAMIC_GEAR_SLOTS; i++) { + int gear_slot = DYNAMIC_GEAR_SLOTS[i]; + if (p->equipped[gear_slot] != resolved[i]) return 0; + } + return 1; +} + +static inline int get_current_loadout(Player* p) { + for (int l = 1; l <= LOADOUT_GMAUL; l++) { + if (is_loadout_active(p, l)) return l; + } + return 0; +} + +static inline AttackStyle get_slot_weapon_attack_style(Player* p) { + uint8_t weapon = p->equipped[GEAR_SLOT_WEAPON]; + if (weapon >= NUM_ITEMS) return ATTACK_STYLE_NONE; + return (AttackStyle)get_item_attack_style(weapon); +} + +static inline void init_slot_equipment_lms(Player* p) { + memset(p->inventory, ITEM_NONE, sizeof(p->inventory)); + memset(p->num_items_in_slot, 0, sizeof(p->num_items_in_slot)); + + p->equipped[GEAR_SLOT_HEAD] = ITEM_HELM_NEITIZNOT; + p->equipped[GEAR_SLOT_CAPE] = ITEM_GOD_CAPE; + p->equipped[GEAR_SLOT_NECK] = ITEM_GLORY; + p->equipped[GEAR_SLOT_AMMO] = ITEM_DIAMOND_BOLTS_E; + p->equipped[GEAR_SLOT_WEAPON] = ITEM_WHIP; + p->equipped[GEAR_SLOT_SHIELD] = ITEM_DRAGON_DEFENDER; + p->equipped[GEAR_SLOT_BODY] = ITEM_BLACK_DHIDE_BODY; + p->equipped[GEAR_SLOT_LEGS] = ITEM_RUNE_PLATELEGS; + p->equipped[GEAR_SLOT_HANDS] = ITEM_BARROWS_GLOVES; + p->equipped[GEAR_SLOT_FEET] = ITEM_CLIMBING_BOOTS; + p->equipped[GEAR_SLOT_RING] = ITEM_BERSERKER_RING; + update_spec_weapons_for_weapon(p, p->equipped[GEAR_SLOT_WEAPON]); + + p->inventory[GEAR_SLOT_HEAD][0] = ITEM_HELM_NEITIZNOT; + p->num_items_in_slot[GEAR_SLOT_HEAD] = 1; + + p->inventory[GEAR_SLOT_CAPE][0] = ITEM_GOD_CAPE; + p->num_items_in_slot[GEAR_SLOT_CAPE] = 1; + + p->inventory[GEAR_SLOT_NECK][0] = ITEM_GLORY; + p->num_items_in_slot[GEAR_SLOT_NECK] = 1; + + p->inventory[GEAR_SLOT_AMMO][0] = ITEM_DIAMOND_BOLTS_E; + p->num_items_in_slot[GEAR_SLOT_AMMO] = 1; + + p->inventory[GEAR_SLOT_WEAPON][0] = ITEM_WHIP; + p->inventory[GEAR_SLOT_WEAPON][1] = ITEM_RUNE_CROSSBOW; + p->inventory[GEAR_SLOT_WEAPON][2] = ITEM_AHRIM_STAFF; + p->inventory[GEAR_SLOT_WEAPON][3] = ITEM_DRAGON_DAGGER; + p->num_items_in_slot[GEAR_SLOT_WEAPON] = 4; + + p->inventory[GEAR_SLOT_SHIELD][0] = ITEM_DRAGON_DEFENDER; + p->inventory[GEAR_SLOT_SHIELD][1] = ITEM_SPIRIT_SHIELD; + p->num_items_in_slot[GEAR_SLOT_SHIELD] = 2; + + p->inventory[GEAR_SLOT_BODY][0] = ITEM_BLACK_DHIDE_BODY; + p->inventory[GEAR_SLOT_BODY][1] = ITEM_MYSTIC_TOP; + p->num_items_in_slot[GEAR_SLOT_BODY] = 2; + + p->inventory[GEAR_SLOT_LEGS][0] = ITEM_RUNE_PLATELEGS; + p->inventory[GEAR_SLOT_LEGS][1] = ITEM_MYSTIC_BOTTOM; + p->num_items_in_slot[GEAR_SLOT_LEGS] = 2; + + p->inventory[GEAR_SLOT_HANDS][0] = ITEM_BARROWS_GLOVES; + p->num_items_in_slot[GEAR_SLOT_HANDS] = 1; + + p->inventory[GEAR_SLOT_FEET][0] = ITEM_CLIMBING_BOOTS; + p->num_items_in_slot[GEAR_SLOT_FEET] = 1; + + p->inventory[GEAR_SLOT_RING][0] = ITEM_BERSERKER_RING; + p->num_items_in_slot[GEAR_SLOT_RING] = 1; + + osrs_refresh_player_equipment(p); + p->current_gear = GEAR_MELEE; +} + +static inline int add_item_to_inventory(Player* p, int gear_slot, uint8_t item_idx) { + if (gear_slot < 0 || gear_slot >= NUM_GEAR_SLOTS) return 0; + if (p->num_items_in_slot[gear_slot] >= MAX_ITEMS_PER_SLOT) return 0; + + for (int i = 0; i < p->num_items_in_slot[gear_slot]; i++) { + if (p->inventory[gear_slot][i] == item_idx) return 0; + } + + p->inventory[gear_slot][p->num_items_in_slot[gear_slot]] = item_idx; + p->num_items_in_slot[gear_slot]++; + return 1; +} + +static const uint8_t UPGRADE_REPLACES[NUM_ITEMS] = { + [ITEM_HELM_NEITIZNOT] = ITEM_NONE, + [ITEM_GOD_CAPE] = ITEM_NONE, + [ITEM_GLORY] = ITEM_NONE, + [ITEM_BLACK_DHIDE_BODY] = ITEM_NONE, + [ITEM_MYSTIC_TOP] = ITEM_NONE, + [ITEM_RUNE_PLATELEGS] = ITEM_NONE, + [ITEM_MYSTIC_BOTTOM] = ITEM_NONE, + [ITEM_WHIP] = ITEM_NONE, + [ITEM_RUNE_CROSSBOW] = ITEM_NONE, + [ITEM_AHRIM_STAFF] = ITEM_NONE, + [ITEM_DRAGON_DAGGER] = ITEM_NONE, + [ITEM_DRAGON_DEFENDER] = ITEM_NONE, + [ITEM_SPIRIT_SHIELD] = ITEM_NONE, + [ITEM_BARROWS_GLOVES] = ITEM_NONE, + [ITEM_CLIMBING_BOOTS] = ITEM_NONE, + [ITEM_BERSERKER_RING] = ITEM_NONE, + [ITEM_DIAMOND_BOLTS_E] = ITEM_NONE, + [ITEM_GHRAZI_RAPIER] = ITEM_WHIP, + [ITEM_INQUISITORS_MACE] = ITEM_WHIP, + [ITEM_STAFF_OF_DEAD] = ITEM_AHRIM_STAFF, + [ITEM_KODAI_WAND] = ITEM_AHRIM_STAFF, + [ITEM_VOLATILE_STAFF] = ITEM_AHRIM_STAFF, + [ITEM_ZURIELS_STAFF] = ITEM_AHRIM_STAFF, + [ITEM_ARMADYL_CROSSBOW] = ITEM_RUNE_CROSSBOW, + [ITEM_ZARYTE_CROSSBOW] = ITEM_RUNE_CROSSBOW, + [ITEM_DRAGON_CLAWS] = ITEM_DRAGON_DAGGER, + [ITEM_AGS] = ITEM_DRAGON_DAGGER, + [ITEM_ANCIENT_GS] = ITEM_DRAGON_DAGGER, + [ITEM_GRANITE_MAUL] = ITEM_NONE, + [ITEM_ELDER_MAUL] = ITEM_WHIP, + [ITEM_DARK_BOW] = ITEM_NONE, + [ITEM_HEAVY_BALLISTA] = ITEM_NONE, + [ITEM_VESTAS] = ITEM_DRAGON_DAGGER, + [ITEM_VOIDWAKER] = ITEM_DRAGON_DAGGER, + [ITEM_STATIUS_WARHAMMER] = ITEM_DRAGON_DAGGER, + [ITEM_MORRIGANS_JAVELIN] = ITEM_RUNE_CROSSBOW, + [ITEM_ANCESTRAL_HAT] = ITEM_NONE, + [ITEM_ANCESTRAL_TOP] = ITEM_MYSTIC_TOP, + [ITEM_ANCESTRAL_BOTTOM] = ITEM_MYSTIC_BOTTOM, + [ITEM_AHRIMS_ROBETOP] = ITEM_MYSTIC_TOP, + [ITEM_AHRIMS_ROBESKIRT] = ITEM_MYSTIC_BOTTOM, + [ITEM_KARILS_TOP] = ITEM_BLACK_DHIDE_BODY, + [ITEM_BANDOS_TASSETS] = ITEM_RUNE_PLATELEGS, + [ITEM_BLESSED_SPIRIT_SHIELD]= ITEM_SPIRIT_SHIELD, + [ITEM_FURY] = ITEM_GLORY, + [ITEM_OCCULT_NECKLACE] = ITEM_NONE, + [ITEM_INFERNAL_CAPE] = ITEM_NONE, + [ITEM_ETERNAL_BOOTS] = ITEM_CLIMBING_BOOTS, + [ITEM_SEERS_RING_I] = ITEM_NONE, + [ITEM_LIGHTBEARER] = ITEM_NONE, + [ITEM_MAGES_BOOK] = ITEM_NONE, + [ITEM_DRAGON_ARROWS] = ITEM_NONE, + [ITEM_TORAGS_PLATELEGS] = ITEM_RUNE_PLATELEGS, + [ITEM_DHAROKS_PLATELEGS] = ITEM_RUNE_PLATELEGS, + [ITEM_VERACS_PLATESKIRT] = ITEM_RUNE_PLATELEGS, + [ITEM_TORAGS_HELM] = ITEM_HELM_NEITIZNOT, + [ITEM_DHAROKS_HELM] = ITEM_HELM_NEITIZNOT, + [ITEM_VERACS_HELM] = ITEM_HELM_NEITIZNOT, + [ITEM_GUTHANS_HELM] = ITEM_HELM_NEITIZNOT, + [ITEM_OPAL_DRAGON_BOLTS] = ITEM_NONE, +}; + +static inline int remove_item_from_inventory(Player* p, int gear_slot, uint8_t item_idx) { + for (int i = 0; i < p->num_items_in_slot[gear_slot]; i++) { + if (p->inventory[gear_slot][i] == item_idx) { + for (int j = i; j < p->num_items_in_slot[gear_slot] - 1; j++) { + p->inventory[gear_slot][j] = p->inventory[gear_slot][j + 1]; + } + p->num_items_in_slot[gear_slot]--; + p->inventory[gear_slot][p->num_items_in_slot[gear_slot]] = ITEM_NONE; + return 1; + } + } + return 0; +} + +static inline int item_to_gear_slot(uint8_t item_idx) { + return osrs_item_gear_slot(item_idx); +} + +static const uint8_t CHAIN_REPLACES[][2] = { + { ITEM_VESTAS, ITEM_WHIP }, + { ITEM_ZURIELS_STAFF, ITEM_STAFF_OF_DEAD }, + { ITEM_ZURIELS_STAFF, ITEM_VOLATILE_STAFF }, + { ITEM_KODAI_WAND, ITEM_STAFF_OF_DEAD }, + { ITEM_KODAI_WAND, ITEM_VOLATILE_STAFF }, + { ITEM_KODAI_WAND, ITEM_ZURIELS_STAFF }, + { ITEM_VOLATILE_STAFF, ITEM_STAFF_OF_DEAD }, + { ITEM_ZARYTE_CROSSBOW, ITEM_ARMADYL_CROSSBOW }, + { ITEM_MORRIGANS_JAVELIN, ITEM_ZARYTE_CROSSBOW }, + { ITEM_MORRIGANS_JAVELIN, ITEM_ARMADYL_CROSSBOW }, + { ITEM_MORRIGANS_JAVELIN, ITEM_HEAVY_BALLISTA }, + { ITEM_MORRIGANS_JAVELIN, ITEM_DARK_BOW }, + { ITEM_ZARYTE_CROSSBOW, ITEM_HEAVY_BALLISTA }, + { ITEM_ZARYTE_CROSSBOW, ITEM_DARK_BOW }, + { ITEM_ARMADYL_CROSSBOW, ITEM_HEAVY_BALLISTA }, + { ITEM_ARMADYL_CROSSBOW, ITEM_DARK_BOW }, + { ITEM_ANCESTRAL_TOP, ITEM_AHRIMS_ROBETOP }, + { ITEM_ANCESTRAL_BOTTOM, ITEM_AHRIMS_ROBESKIRT }, + { ITEM_BANDOS_TASSETS, ITEM_TORAGS_PLATELEGS }, + { ITEM_BANDOS_TASSETS, ITEM_DHAROKS_PLATELEGS }, + { ITEM_BANDOS_TASSETS, ITEM_VERACS_PLATESKIRT }, + { ITEM_GHRAZI_RAPIER, ITEM_INQUISITORS_MACE }, + { ITEM_GHRAZI_RAPIER, ITEM_WHIP }, + { ITEM_INQUISITORS_MACE, ITEM_WHIP }, + { ITEM_ELDER_MAUL, ITEM_WHIP }, + { ITEM_GHRAZI_RAPIER, ITEM_ELDER_MAUL }, + { ITEM_INQUISITORS_MACE, ITEM_ELDER_MAUL }, + { ITEM_VESTAS, ITEM_ELDER_MAUL }, + { ITEM_VESTAS, ITEM_GHRAZI_RAPIER }, + { ITEM_VESTAS, ITEM_INQUISITORS_MACE }, + { ITEM_VOIDWAKER, ITEM_WHIP }, + { ITEM_VOIDWAKER, ITEM_GHRAZI_RAPIER }, + { ITEM_VOIDWAKER, ITEM_INQUISITORS_MACE }, + { ITEM_VOIDWAKER, ITEM_ELDER_MAUL }, + { ITEM_STATIUS_WARHAMMER, ITEM_WHIP }, + { ITEM_STATIUS_WARHAMMER, ITEM_GHRAZI_RAPIER }, + { ITEM_STATIUS_WARHAMMER, ITEM_INQUISITORS_MACE }, + { ITEM_STATIUS_WARHAMMER, ITEM_ELDER_MAUL }, + { ITEM_STATIUS_WARHAMMER, ITEM_AGS }, + { ITEM_STATIUS_WARHAMMER, ITEM_ANCIENT_GS }, + { ITEM_STATIUS_WARHAMMER, ITEM_DRAGON_CLAWS }, + { ITEM_AGS, ITEM_WHIP }, + { ITEM_ANCIENT_GS, ITEM_WHIP }, + { ITEM_ANCIENT_GS, ITEM_AGS }, + { ITEM_ANCIENT_GS, ITEM_DRAGON_CLAWS }, + { ITEM_AGS, ITEM_DRAGON_CLAWS }, + { ITEM_LIGHTBEARER, ITEM_SEERS_RING_I }, + { ITEM_TORAGS_HELM, ITEM_GUTHANS_HELM }, + { ITEM_TORAGS_HELM, ITEM_VERACS_HELM }, + { ITEM_TORAGS_HELM, ITEM_DHAROKS_HELM }, + { ITEM_GUTHANS_HELM, ITEM_VERACS_HELM }, + { ITEM_GUTHANS_HELM, ITEM_DHAROKS_HELM }, + { ITEM_VERACS_HELM, ITEM_DHAROKS_HELM }, +}; +#define CHAIN_REPLACES_LEN (sizeof(CHAIN_REPLACES) / sizeof(CHAIN_REPLACES[0])) + +static inline void add_loot_item(Player* p, uint8_t item_idx) { + int gear_slot = item_to_gear_slot(item_idx); + if (gear_slot < 0) return; + + for (int i = 0; i < (int)CHAIN_REPLACES_LEN; i++) { + if (CHAIN_REPLACES[i][1] == item_idx) { + uint8_t better = CHAIN_REPLACES[i][0]; + int better_slot = item_to_gear_slot(better); + if (better_slot >= 0 && player_has_item_in_slot(p, better_slot, better)) { + return; + } + } + } + + uint8_t replaces = UPGRADE_REPLACES[item_idx]; + if (replaces != ITEM_NONE) { + int replace_slot = item_to_gear_slot(replaces); + if (replace_slot >= 0) { + remove_item_from_inventory(p, replace_slot, replaces); + } + } + + for (int i = 0; i < (int)CHAIN_REPLACES_LEN; i++) { + if (CHAIN_REPLACES[i][0] == item_idx) { + uint8_t obsolete = CHAIN_REPLACES[i][1]; + int obs_slot = item_to_gear_slot(obsolete); + if (obs_slot >= 0) { + remove_item_from_inventory(p, obs_slot, obsolete); + } + } + } + + add_item_to_inventory(p, gear_slot, item_idx); + + if ((item_idx == ITEM_ARMADYL_CROSSBOW || item_idx == ITEM_ZARYTE_CROSSBOW) + && player_has_item_in_slot(p, GEAR_SLOT_AMMO, ITEM_OPAL_DRAGON_BOLTS)) { + remove_item_from_inventory(p, GEAR_SLOT_AMMO, ITEM_DIAMOND_BOLTS_E); + p->equipped[GEAR_SLOT_AMMO] = ITEM_OPAL_DRAGON_BOLTS; + } + +} + +/* 4 brew + 2 restore + 1 combat + 1 ranged + 2 karambwan + 1 rune pouch */ +#define FIXED_INVENTORY_SLOTS 11 + +static inline int count_switch_items(Player* p) { + int switches = 0; + for (int s = 0; s < NUM_GEAR_SLOTS; s++) { + if (p->num_items_in_slot[s] > 1) { + switches += p->num_items_in_slot[s] - 1; + } + } + return switches; +} + +static inline int compute_food_count(Player* p) { + int switches = count_switch_items(p); + int food = 28 - FIXED_INVENTORY_SLOTS - switches; + return food > 1 ? food : 1; +} + +static const uint8_t CHEST_LOOT[] = { + ITEM_DRAGON_CLAWS, ITEM_AGS, ITEM_ANCIENT_GS, ITEM_GRANITE_MAUL, + ITEM_VOLATILE_STAFF, ITEM_ZARYTE_CROSSBOW, ITEM_ARMADYL_CROSSBOW, + ITEM_DARK_BOW, ITEM_GHRAZI_RAPIER, ITEM_INQUISITORS_MACE, + ITEM_KODAI_WAND, ITEM_STAFF_OF_DEAD, ITEM_ELDER_MAUL, + ITEM_HEAVY_BALLISTA, ITEM_OCCULT_NECKLACE, ITEM_INFERNAL_CAPE, + ITEM_SEERS_RING_I, ITEM_MAGES_BOOK, + ITEM_ANCESTRAL_HAT, ITEM_ANCESTRAL_TOP, ITEM_ANCESTRAL_BOTTOM, + ITEM_AHRIMS_ROBETOP, ITEM_AHRIMS_ROBESKIRT, ITEM_KARILS_TOP, + ITEM_BANDOS_TASSETS, ITEM_BLESSED_SPIRIT_SHIELD, + ITEM_FURY, ITEM_ETERNAL_BOOTS, + ITEM_TORAGS_PLATELEGS, ITEM_DHAROKS_PLATELEGS, ITEM_VERACS_PLATESKIRT, + ITEM_TORAGS_HELM, ITEM_DHAROKS_HELM, ITEM_VERACS_HELM, ITEM_GUTHANS_HELM, + ITEM_OPAL_DRAGON_BOLTS, +}; +#define CHEST_LOOT_LEN (sizeof(CHEST_LOOT) / sizeof(CHEST_LOOT[0])) + +static const uint8_t BLOODIER_LOOT[] = { + ITEM_VESTAS, ITEM_VOIDWAKER, ITEM_STATIUS_WARHAMMER, + ITEM_MORRIGANS_JAVELIN, ITEM_ZURIELS_STAFF, ITEM_LIGHTBEARER +}; +#define BLOODIER_LOOT_LEN (sizeof(BLOODIER_LOOT) / sizeof(BLOODIER_LOOT[0])) + +/* roll counts per tier pin the seeded RNG call order: tier 1 adds 2 chest rolls, + tier 2 adds 4 more, tier 3 adds 2 more plus 1 bloodier roll */ +static inline void init_player_gear_randomized(Player* p, int tier, uint32_t* rng) { + init_slot_equipment_lms(p); + + if (tier <= 0) return; + + #define ADD_RANDOM_LOOT(table, len) do { \ + uint32_t _r = xorshift32(rng); \ + uint8_t _item = (table)[_r % (len)]; \ + add_loot_item(p, _item); \ + } while(0) + + if (tier >= 1) { + ADD_RANDOM_LOOT(CHEST_LOOT, CHEST_LOOT_LEN); + ADD_RANDOM_LOOT(CHEST_LOOT, CHEST_LOOT_LEN); + } + + if (tier >= 2) { + ADD_RANDOM_LOOT(CHEST_LOOT, CHEST_LOOT_LEN); + ADD_RANDOM_LOOT(CHEST_LOOT, CHEST_LOOT_LEN); + ADD_RANDOM_LOOT(CHEST_LOOT, CHEST_LOOT_LEN); + ADD_RANDOM_LOOT(CHEST_LOOT, CHEST_LOOT_LEN); + } + + if (tier >= 3) { + ADD_RANDOM_LOOT(CHEST_LOOT, CHEST_LOOT_LEN); + ADD_RANDOM_LOOT(CHEST_LOOT, CHEST_LOOT_LEN); + ADD_RANDOM_LOOT(BLOODIER_LOOT, BLOODIER_LOOT_LEN); + } + + #undef ADD_RANDOM_LOOT + + if (tier >= 3 && player_has_item_in_slot(p, GEAR_SLOT_SHIELD, ITEM_DRAGON_DEFENDER)) { + int has_1h_melee = 0; + for (int i = 0; i < p->num_items_in_slot[GEAR_SLOT_WEAPON]; i++) { + uint8_t w = p->inventory[GEAR_SLOT_WEAPON][i]; + if (get_item_attack_style(w) == ATTACK_STYLE_MELEE && !item_is_two_handed(w)) { + has_1h_melee = 1; + break; + } + } + if (!has_1h_melee) { + remove_item_from_inventory(p, GEAR_SLOT_SHIELD, ITEM_DRAGON_DEFENDER); + } + } + + uint8_t resolved[NUM_DYNAMIC_GEAR_SLOTS]; + resolve_loadout(p, LOADOUT_MELEE, resolved); + for (int i = 0; i < NUM_DYNAMIC_GEAR_SLOTS; i++) { + slot_equip_item(p, DYNAMIC_GEAR_SLOTS[i], resolved[i]); + } + + osrs_refresh_player_equipment(p); + p->current_gear = GEAR_MELEE; +} + +static inline int sample_gear_tier(float weights[4], uint32_t* rng) { + float r = (float)xorshift32(rng) / (float)UINT32_MAX; + float cumulative = 0.0f; + for (int i = 0; i < 4; i++) { + cumulative += weights[i]; + if (r < cumulative) return i; + } + return 0; +} + +#endif // OSRS_PVP_GEAR_H diff --git a/ocean/osrs/osrs_pvp_movement.h b/ocean/osrs/osrs_pvp_movement.h new file mode 100644 index 0000000000..858f1a5717 --- /dev/null +++ b/ocean/osrs/osrs_pvp_movement.h @@ -0,0 +1,335 @@ +#ifndef OSRS_PVP_MOVEMENT_H +#define OSRS_PVP_MOVEMENT_H + +#include "osrs_types.h" +#include "osrs_collision.h" +#include "osrs_encounter.h" +#include "osrs_encounter_player.h" +#include "osrs_pvp_gear.h" + +static int select_closest_candidate_tile( + Player* p, const int candidates[4][2], int target_x, int target_y, + int* out_x, int* out_y, const CollisionMap* cmap +) { + int has_best = 0; + int best_x = 0; + int best_y = 0; + int best_dist_agent = 0; + int best_dist_target = 0; + int best_hash = 0; + + for (int i = 0; i < 4; i++) { + int cx = candidates[i][0]; + int cy = candidates[i][1]; + if (!is_in_wilderness(cx, cy)) { + continue; + } + if (!collision_tile_walkable(cmap, 0, cx, cy)) { + continue; + } + int dist_agent = chebyshev_distance(p->x, p->y, cx, cy); + int dist_target = chebyshev_distance(cx, cy, target_x, target_y); + int hash = tile_hash(cx, cy); + if (!has_best || + dist_agent < best_dist_agent || + (dist_agent == best_dist_agent && + (dist_target < best_dist_target || + (dist_target == best_dist_target && hash < best_hash)))) { + has_best = 1; + best_x = cx; + best_y = cy; + best_dist_agent = dist_agent; + best_dist_target = dist_target; + best_hash = hash; + } + } + + if (!has_best) { + return 0; + } + *out_x = best_x; + *out_y = best_y; + return 1; +} + +static int select_closest_adjacent_tile(Player* p, int target_x, int target_y, int* out_x, int* out_y, const CollisionMap* cmap) { + const int candidates[4][2] = { + {target_x, target_y + 1}, + {target_x + 1, target_y}, + {target_x, target_y - 1}, + {target_x - 1, target_y} + }; + return select_closest_candidate_tile(p, candidates, target_x, target_y, out_x, out_y, cmap); +} + +static int select_closest_diagonal_tile(Player* p, int target_x, int target_y, int* out_x, int* out_y, const CollisionMap* cmap) { + const int candidates[4][2] = { + {target_x + 1, target_y + 1}, + {target_x + 1, target_y - 1}, + {target_x - 1, target_y - 1}, + {target_x - 1, target_y + 1} + }; + return select_closest_candidate_tile(p, candidates, target_x, target_y, out_x, out_y, cmap); +} + +static int select_farcast_tile(Player* p, int target_x, int target_y, int distance, int* out_x, int* out_y, const CollisionMap* cmap) { + int raw_dx = p->x - target_x; + int raw_dy = p->y - target_y; + int d = distance; + + int dx = raw_dx < -d ? -d : (raw_dx > d ? d : raw_dx); + int dy = raw_dy < -d ? -d : (raw_dy > d ? d : raw_dy); + + int adx = abs_int(dx); + int ady = abs_int(dy); + if (adx < d && ady < d) { + if (adx >= ady) { + dx = (raw_dx >= 0) ? d : -d; + } else { + dy = (raw_dy >= 0) ? d : -d; + } + } + + int cx = target_x + dx; + int cy = target_y + dy; + + if (is_in_wilderness(cx, cy) && collision_tile_walkable(cmap, 0, cx, cy)) { + *out_x = cx; + *out_y = cy; + return 1; + } + + cx = cx < WILD_MIN_X ? WILD_MIN_X : (cx > WILD_MAX_X ? WILD_MAX_X : cx); + cy = cy < WILD_MIN_Y ? WILD_MIN_Y : (cy > WILD_MAX_Y ? WILD_MAX_Y : cy); + if (chebyshev_distance(cx, cy, target_x, target_y) == distance + && collision_tile_walkable(cmap, 0, cx, cy)) { + *out_x = cx; + *out_y = cy; + return 1; + } + + return 0; +} + +static int step_toward_destination(Player* p, const CollisionMap* cmap) { + int dx = p->dest_x - p->x; + int dy = p->dest_y - p->y; + if (dx == 0 && dy == 0) { + return 0; + } + + int step_x = (dx > 0) ? 1 : (dx < 0 ? -1 : 0); + int step_y = (dy > 0) ? 1 : (dy < 0 ? -1 : 0); + + if (step_x != 0 && step_y != 0) { + if (collision_traversable_step(cmap, 0, p->x, p->y, step_x, step_y)) { + p->x += step_x; + p->y += step_y; + return 1; + } + if (collision_traversable_step(cmap, 0, p->x, p->y, step_x, 0)) { + p->x += step_x; + return 1; + } + if (collision_traversable_step(cmap, 0, p->x, p->y, 0, step_y)) { + p->y += step_y; + return 1; + } + return 0; + } + + if (collision_traversable_step(cmap, 0, p->x, p->y, step_x, step_y)) { + p->x += step_x; + p->y += step_y; + return 1; + } + + return 0; +} + +static void set_destination(Player* p, int dest_x, int dest_y, const CollisionMap* cmap) { + p->dest_x = dest_x; + p->dest_y = dest_y; + if (p->x == dest_x && p->y == dest_y) { + p->is_moving = 0; + return; + } + if (!step_toward_destination(p, cmap)) { + p->is_moving = 0; + return; + } + if (p->x != dest_x || p->y != dest_y) { + step_toward_destination(p, cmap); + } + p->is_moving = (p->x != dest_x || p->y != dest_y) ? 1 : 0; +} + +static int pvp_tile_walkable(void* ctx, int x, int y) { + const CollisionMap* cmap = (const CollisionMap*)ctx; + return is_in_wilderness(x, y) && collision_tile_walkable(cmap, 0, x, y); +} + +static void move_toward_target( + Player* p, + Player* target, + int attack_range, + const CollisionMap* cmap +) { + if (p->frozen_ticks > 0) { + return; + } + int moved = encounter_chase_attack_target( + p, + target->x, + target->y, + 1, + attack_range, + cmap, + 0, + 0, + pvp_tile_walkable, + (void*)cmap, + NULL, + NULL, + osrs_los_open_query(), + 0, + 0, + 0, + 0); + p->is_moving = moved; +} + +static void step_out_from_same_tile(Player* p, Player* target, const CollisionMap* cmap) { + if (p->frozen_ticks > 0) { + return; + } + + int dest_x = target->x - 1; + int dest_y = target->y; + if (is_in_wilderness(dest_x, dest_y) && collision_tile_walkable(cmap, 0, dest_x, dest_y)) { + set_destination(p, dest_x, dest_y, cmap); + return; + } + dest_x = target->x + 1; + if (is_in_wilderness(dest_x, dest_y) && collision_tile_walkable(cmap, 0, dest_x, dest_y)) { + set_destination(p, dest_x, dest_y, cmap); + return; + } + dest_x = target->x; + dest_y = target->y - 1; + if (is_in_wilderness(dest_x, dest_y) && collision_tile_walkable(cmap, 0, dest_x, dest_y)) { + set_destination(p, dest_x, dest_y, cmap); + return; + } + dest_y = target->y + 1; + if (is_in_wilderness(dest_x, dest_y) && collision_tile_walkable(cmap, 0, dest_x, dest_y)) { + set_destination(p, dest_x, dest_y, cmap); + return; + } +} + +/* skipped when either player is frozen: walking under a frozen opponent is a + legal, intentional position in OSRS PvP */ +static void resolve_same_tile(Player* mover, Player* blocker, const CollisionMap* cmap) { + if (blocker->frozen_ticks > 0) { + return; + } + if (mover->frozen_ticks > 0) { + return; + } + + static const int OFFSETS[8][2] = { + {-1, 0}, {1, 0}, {0, -1}, {0, 1}, + {-1, -1}, {1, -1}, {-1, 1}, {1, 1} + }; + + for (int i = 0; i < 8; i++) { + int nx = mover->x + OFFSETS[i][0]; + int ny = mover->y + OFFSETS[i][1]; + if (is_in_wilderness(nx, ny) + && collision_tile_walkable(cmap, 0, nx, ny) + && !(nx == blocker->x && ny == blocker->y)) { + mover->x = nx; + mover->y = ny; + mover->dest_x = nx; + mover->dest_y = ny; + mover->is_moving = 0; + return; + } + } +} + +static int pvp_lookup_attack_target(void* ctx, int target_slot, OsrsAttackTarget* out) { + if (target_slot < 0 || target_slot >= NUM_AGENTS) return 0; + OsrsEnv* env = (OsrsEnv*)ctx; + Player* target = &env->players[target_slot]; + Player* self = &env->players[1 - target_slot]; + AttackStyle style = get_slot_weapon_attack_style(self); + int range; + if (style == ATTACK_STYLE_MELEE || style == ATTACK_STYLE_NONE) { + range = 1; + } else { + range = get_attack_range(self, style); + } + out->slot = target_slot; + out->x = target->x; + out->y = target->y; + out->size = 1; + out->attack_range = range; + return 1; +} + +static inline OsrsEncounterArena pvp_build_arena(OsrsEnv* env) { + OsrsEncounterArena arena; + arena.collision_map = (const CollisionMap*)env->collision_map; + arena.world_offset_x = 0; + arena.world_offset_y = 0; + arena.is_walkable = pvp_tile_walkable; + arena.walkable_ctx = (void*)arena.collision_map; + arena.extra_blocked = NULL; + arena.blocked_ctx = NULL; + arena.los_query = osrs_los_open_query(); + arena.arena_base_x = 0; + arena.arena_base_y = 0; + arena.arena_w = 0; + arena.arena_h = 0; + return arena; +} + +static inline OsrsPlayerStepResult pvp_step_player_movement(OsrsEnv* env, int agent_idx) { + OsrsPlayerStepResult result = {.target_slot = -1}; + int* dest_x = &env->pvp_runtime.walk_dest_x[agent_idx]; + int* dest_y = &env->pvp_runtime.walk_dest_y[agent_idx]; + + if (*dest_x < 0 || *dest_y < 0) return result; + + Player* p = &env->players[agent_idx]; + OsrsEncounterArena arena = pvp_build_arena(env); + OsrsPlayerStepInput input = { + .player = p, + .interaction = &p->interaction, + .target_lookup = pvp_lookup_attack_target, + .target_ctx = env, + .has_new_target = 0, + .new_target_slot = -1, + .move_kind = OSRS_PLAYER_MOVE_DESTINATION, + .target_move_policy = OSRS_PLAYER_TARGET_MOVE_EXPLICIT_FIRST, + .move_action = 0, + .dest_x = dest_x, + .dest_y = dest_y, + .blocked_ticks = p->frozen_ticks, + .arena = arena, + }; + result = osrs_encounter_player_step(&input); + p->is_moving = (*dest_x >= 0) ? 1 : 0; + return result; +} + +static inline void pvp_set_walk_dest_from_head_move(OsrsEnv* env, int agent_idx, int move_action) { + Player* p = &env->players[agent_idx]; + if (move_action <= 0 || move_action >= MOVE_DIM) return; + env->pvp_runtime.walk_dest_x[agent_idx] = p->x + ENCOUNTER_MOVE_TARGET_DX[move_action]; + env->pvp_runtime.walk_dest_y[agent_idx] = p->y + ENCOUNTER_MOVE_TARGET_DY[move_action]; +} + +#endif // OSRS_PVP_MOVEMENT_H diff --git a/ocean/osrs/osrs_pvp_observations.h b/ocean/osrs/osrs_pvp_observations.h new file mode 100644 index 0000000000..30f9fc62cb --- /dev/null +++ b/ocean/osrs/osrs_pvp_observations.h @@ -0,0 +1,593 @@ +#ifndef OSRS_PVP_OBSERVATIONS_H +#define OSRS_PVP_OBSERVATIONS_H + +#include +#include "osrs_types.h" +#include "osrs_player_consumables.h" +#include "osrs_pvp_gear.h" +#include "osrs_pvp_combat.h" +#include "osrs_encounter.h" +#include "osrs_pvp_movement.h" + +static inline float get_relative_level_combat(int current, int base) { + int max_level = base + osrs_super_combat_boost_amount(base); + return (float)current / (float)max_level; +} + +static inline float get_relative_level_ranged(int current, int base) { + int max_level = base + osrs_ranging_boost_amount(base); + return (float)current / (float)max_level; +} + +static inline float get_relative_level_magic(int current, int base) { + return (float)current / (float)base; +} + +static inline int can_use_brew_boost(Player* p) { + int def_boost = osrs_brew_defence_boost_amount(p->base_defence); + int def_cap = p->is_lms ? p->base_defence : p->base_defence + def_boost; + if (p->current_defence < def_cap - 1) { + return 1; + } + return p->current_hitpoints <= p->base_hitpoints; +} + +static inline int can_restore_stats(Player* p) { + int stats_drained = p->current_attack < p->base_attack || + p->current_defence < p->base_defence || + p->current_strength < p->base_strength || + p->current_ranged < p->base_ranged || + p->current_magic < p->base_magic; + int prayer_low = p->current_prayer < (int)(p->base_prayer * 0.9f); + return stats_drained || prayer_low; +} + +static inline int can_boost_combat_skills(Player* p) { + int max_att = p->base_attack + osrs_super_combat_boost_amount(p->base_attack); + int max_str = p->base_strength + osrs_super_combat_boost_amount(p->base_strength); + int def_boost = osrs_super_combat_boost_amount(p->base_defence); + int max_def = p->is_lms ? p->base_defence : p->base_defence + def_boost; + return max_att > p->current_attack + 1 || + max_def > p->current_defence + 1 || + max_str > p->current_strength + 1; +} + +static inline int can_boost_ranged(Player* p) { + int max_ranged = p->base_ranged + osrs_ranging_boost_amount(p->base_ranged); + return max_ranged > p->current_ranged + 1; +} + +static inline int can_use_potion(Player* p, int potion_type) { + if (remaining_ticks(p->potion_timer) > 0) { + return 0; + } + switch (potion_type) { + case 1: return p->brew_doses > 0; + case 2: return p->restore_doses > 0; + case 3: return p->combat_potion_doses > 0; + case 4: return p->ranged_potion_doses > 0; + default: return 0; + } +} + +static inline int can_eat_food(Player* p) { + return osrs_player_can_eat_food_type(p, FOOD_SHARK); +} + +static inline int can_eat_karambwan(Player* p) { + return osrs_player_can_eat_food_type(p, FOOD_KARAMBWAN); +} + +static inline int can_move_adjacent(Player* p, const CollisionMap* cmap) { + int dest_x = 0; + int dest_y = 0; + if (!select_closest_adjacent_tile(p, p->last_obs_target_x, p->last_obs_target_y, &dest_x, &dest_y, cmap)) { + return 0; + } + return !(dest_x == p->x && dest_y == p->y); +} + +static inline int can_move_under(Player* p, Player* target) { + int dist = chebyshev_distance(p->x, p->y, p->last_obs_target_x, p->last_obs_target_y); + return remaining_ticks(target->frozen_ticks) > 0 && dist != 0; +} + +static inline int can_move_to_farcast(Player* p, int distance, const CollisionMap* cmap) { + int dest_x = 0; + int dest_y = 0; + if (!select_farcast_tile(p, p->last_obs_target_x, p->last_obs_target_y, distance, &dest_x, &dest_y, cmap)) { + return 0; + } + return !(dest_x == p->x && dest_y == p->y); +} + +static inline int can_move_diagonal(Player* p, const CollisionMap* cmap) { + int dest_x = 0; + int dest_y = 0; + if (!select_closest_diagonal_tile(p, p->last_obs_target_x, p->last_obs_target_y, &dest_x, &dest_y, cmap)) { + return 0; + } + return !(dest_x == p->x && dest_y == p->y); +} + +static void init_obs_norm_divisors(float* d) { + for (int i = 0; i < SLOT_NUM_OBSERVATIONS; i++) d[i] = 1.0f; + + d[4] = 100.0f; d[21] = 100.0f; + d[22] = 10.0f; d[23] = 10.0f; d[24] = 16.0f; d[25] = 20.0f; d[26] = 15.0f; d[27] = 4.0f; + d[29] = 32.0f; d[30] = 32.0f; d[31] = 32.0f; d[32] = 32.0f; + d[39] = 6.0f; d[40] = 3.0f; d[41] = 3.0f; d[42] = 3.0f; d[43] = 4.0f; d[44] = 6.0f; d[45] = 3.0f; + d[46] = 2.0f; + + d[47] = 6.0f; + d[48] = 6.0f; + + d[65] = 2.0f; + + d[60] = 7.0f; + d[61] = 7.0f; + d[62] = 7.0f; + + for (int i = 96; i <= 102; i++) d[i] = 99.0f; + + d[106] = 4.0f; + + for (int i = 119; i <= 132; i++) d[i] = 170.0f; + d[123] = 6.0f; d[124] = 10.0f; + d[139] = 50.0f; + d[140] = 50.0f; +} + +static float OBS_NORM_DIVISORS[SLOT_NUM_OBSERVATIONS]; +static int _obs_norm_initialized = 0; + +static void ensure_obs_norm_initialized(void) { + if (!_obs_norm_initialized) { + init_obs_norm_divisors(OBS_NORM_DIVISORS); + _obs_norm_initialized = 1; + } +} + +static void ocean_write_obs_agent(OsrsEnv* env, float* dst, int agent_idx) { + ensure_obs_norm_initialized(); + float* src = env->observations + agent_idx * SLOT_NUM_OBSERVATIONS; + for (int i = 0; i < SLOT_NUM_OBSERVATIONS; i++) { + dst[i] = src[i] / OBS_NORM_DIVISORS[i]; + } + + unsigned char* mask = env->action_masks + agent_idx * ACTION_MASK_SIZE; + for (int i = 0; i < ACTION_MASK_SIZE; i++) { + dst[SLOT_NUM_OBSERVATIONS + i] = (float)mask[i]; + } +} + +/** Binding-facing output layout: [normalized obs, action mask as float]. */ +static void ocean_write_obs(OsrsEnv* env) { + ocean_write_obs_agent(env, env->ocean_io.agent_obs, 0); +} + +static void ocean_write_obs_p1(OsrsEnv* env) { + ocean_write_obs_agent(env, env->ocean_io.agent_obs_p1, 1); +} + +static void generate_slot_observations(OsrsEnv* env, int agent_idx) { + Player* p = &env->players[agent_idx]; + Player* t = &env->players[1 - agent_idx]; + + float* obs = env->observations + agent_idx * SLOT_NUM_OBSERVATIONS; + + p->last_obs_target_x = t->x; + p->last_obs_target_y = t->y; + + obs[0] = (p->visible_gear == GEAR_MELEE) ? 1.0f : 0.0f; + obs[1] = (p->visible_gear == GEAR_RANGED) ? 1.0f : 0.0f; + obs[2] = (p->visible_gear == GEAR_MAGE) ? 1.0f : 0.0f; + obs[3] = (float)p->spec_armed; + obs[4] = (float)p->special_energy; + + obs[5] = (p->prayer == PRAYER_PROTECT_MELEE) ? 1.0f : 0.0f; + obs[6] = (p->prayer == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; + obs[7] = (p->prayer == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; + obs[8] = (p->prayer == PRAYER_SMITE) ? 1.0f : 0.0f; + obs[9] = (p->prayer == PRAYER_REDEMPTION) ? 1.0f : 0.0f; + + obs[10] = (float)p->current_hitpoints / (float)p->base_hitpoints; + obs[11] = p->last_target_health_percent; + + obs[12] = (t->last_attack_style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; + obs[13] = (t->last_attack_style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; + obs[14] = (t->last_attack_style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; + obs[15] = (t->last_attack_style == ATTACK_STYLE_NONE) ? 1.0f : 0.0f; + + obs[16] = (t->prayer == PRAYER_PROTECT_MELEE) ? 1.0f : 0.0f; + obs[17] = (t->prayer == PRAYER_PROTECT_RANGED) ? 1.0f : 0.0f; + obs[18] = (t->prayer == PRAYER_PROTECT_MAGIC) ? 1.0f : 0.0f; + obs[19] = (t->prayer == PRAYER_SMITE) ? 1.0f : 0.0f; + obs[20] = (t->prayer == PRAYER_REDEMPTION) ? 1.0f : 0.0f; + obs[21] = (float)t->special_energy; + + obs[22] = (float)p->ranged_potion_doses; + obs[23] = (float)p->combat_potion_doses; + obs[24] = (float)p->restore_doses; + obs[25] = (float)p->brew_doses; + obs[26] = (float)p->food_count; + obs[27] = (float)p->karambwan_count; + obs[28] = (float)p->current_prayer / (float)p->base_prayer; + + obs[29] = (float)remaining_ticks(p->frozen_ticks); + obs[30] = (float)remaining_ticks(t->frozen_ticks); + obs[31] = (float)remaining_ticks(p->freeze_immunity_ticks); + obs[32] = (float)remaining_ticks(t->freeze_immunity_ticks); + + obs[33] = is_in_melee_range(p, t) ? 1.0f : 0.0f; + + obs[34] = get_relative_level_combat(p->current_strength, p->base_strength); + obs[35] = get_relative_level_combat(p->current_attack, p->base_attack); + obs[36] = get_relative_level_combat(p->current_defence, p->base_defence); + obs[37] = get_relative_level_ranged(p->current_ranged, p->base_ranged); + obs[38] = get_relative_level_magic(p->current_magic, p->base_magic); + + obs[39] = (float)p->attack_timer; + obs[40] = (float)remaining_ticks(p->food_timer); + obs[41] = (float)remaining_ticks(p->potion_timer); + obs[42] = (float)remaining_ticks(p->karambwan_timer); + + int attack_delay = get_attack_timer_uncapped(p) - 1; + if (attack_delay < -3) attack_delay = -3; + else if (attack_delay > 0) attack_delay = 0; + obs[43] = (float)(attack_delay + 3); + + obs[44] = (float)remaining_ticks(t->attack_timer); + obs[45] = (float)remaining_ticks(t->food_timer); + + int pending_damage = 0; + for (int i = 0; i < p->num_pending_hits; i++) { + pending_damage += p->pending_hits[i].damage; + } + obs[46] = (float)pending_damage / (float)t->base_hitpoints; + + int ticks_until_hit_on_target = get_ticks_until_next_hit(p); + int ticks_until_hit_on_player = get_ticks_until_next_hit(t); + obs[47] = (float)ticks_until_hit_on_target; + obs[48] = (float)ticks_until_hit_on_player; + + obs[49] = p->just_attacked ? 1.0f : 0.0f; + obs[50] = t->just_attacked ? 1.0f : 0.0f; + + obs[51] = p->tick_damage_scale; + obs[52] = p->damage_received_scale; + obs[53] = p->damage_dealt_scale; + + obs[54] = (p->last_attack_style != ATTACK_STYLE_NONE) ? 1.0f : 0.0f; + obs[55] = p->is_moving ? 1.0f : 0.0f; + obs[56] = t->is_moving ? 1.0f : 0.0f; + + obs[57] = (agent_idx == env->pid_holder) ? 1.0f : 0.0f; + + obs[58] = (!p->is_lunar_spellbook && p->current_magic >= 94) ? 1.0f : 0.0f; + obs[59] = (!p->is_lunar_spellbook && p->current_magic >= 92) ? 1.0f : 0.0f; + + int dist = chebyshev_distance(p->x, p->y, t->x, t->y); + int destination_distance = p->is_moving + ? chebyshev_distance(p->dest_x, p->dest_y, t->x, t->y) : dist; + int distance_to_destination = p->is_moving + ? chebyshev_distance(p->x, p->y, p->dest_x, p->dest_y) : 0; + + if (destination_distance > 7) destination_distance = 7; + if (distance_to_destination > 7) distance_to_destination = 7; + if (dist > 7) dist = 7; + + obs[60] = (float)destination_distance; + obs[61] = (float)distance_to_destination; + obs[62] = (float)dist; + + obs[63] = p->player_prayed_correct ? 1.0f : 0.0f; + obs[64] = p->target_prayed_correct ? 1.0f : 0.0f; + + float damage_scale = (p->total_damage_dealt + 1.0f) / (p->total_damage_received + 1.0f); + obs[65] = clampf(damage_scale, 0.5f, 2.0f); + + obs[66] = confidence_scale(p->total_target_hit_count); + obs[67] = ratio_or_zero(p->target_hit_melee_count, p->total_target_hit_count); + obs[68] = ratio_or_zero(p->target_hit_magic_count, p->total_target_hit_count); + obs[69] = ratio_or_zero(p->target_hit_ranged_count, p->total_target_hit_count); + obs[70] = ratio_or_zero(p->player_hit_melee_count, p->total_target_pray_count); + obs[71] = ratio_or_zero(p->player_hit_magic_count, p->total_target_pray_count); + obs[72] = ratio_or_zero(p->player_hit_ranged_count, p->total_target_pray_count); + obs[73] = ratio_or_zero(p->target_hit_correct_count, p->total_target_hit_count); + obs[74] = confidence_scale(p->total_target_pray_count); + obs[75] = ratio_or_zero(p->target_pray_magic_count, p->total_target_pray_count); + obs[76] = ratio_or_zero(p->target_pray_ranged_count, p->total_target_pray_count); + obs[77] = ratio_or_zero(p->target_pray_melee_count, p->total_target_pray_count); + obs[78] = ratio_or_zero(p->player_pray_magic_count, p->total_target_hit_count); + obs[79] = ratio_or_zero(p->player_pray_ranged_count, p->total_target_hit_count); + obs[80] = ratio_or_zero(p->player_pray_melee_count, p->total_target_hit_count); + obs[81] = ratio_or_zero(p->target_pray_correct_count, p->total_target_pray_count); + + int recent_target_hit_melee = 0, recent_target_hit_magic = 0, recent_target_hit_ranged = 0; + int recent_player_hit_melee = 0, recent_player_hit_magic = 0, recent_player_hit_ranged = 0; + int recent_target_pray_magic = 0, recent_target_pray_ranged = 0, recent_target_pray_melee = 0; + int recent_player_pray_magic = 0, recent_player_pray_ranged = 0, recent_player_pray_melee = 0; + int recent_target_hit_correct = 0, recent_target_pray_correct = 0; + + for (int i = 0; i < HISTORY_SIZE; i++) { + if (p->recent_target_attack_styles[i] == ATTACK_STYLE_MELEE) recent_target_hit_melee++; + else if (p->recent_target_attack_styles[i] == ATTACK_STYLE_MAGIC) recent_target_hit_magic++; + else if (p->recent_target_attack_styles[i] == ATTACK_STYLE_RANGED) recent_target_hit_ranged++; + + if (p->recent_player_attack_styles[i] == ATTACK_STYLE_MELEE) recent_player_hit_melee++; + else if (p->recent_player_attack_styles[i] == ATTACK_STYLE_MAGIC) recent_player_hit_magic++; + else if (p->recent_player_attack_styles[i] == ATTACK_STYLE_RANGED) recent_player_hit_ranged++; + + if (p->recent_target_prayer_styles[i] == ATTACK_STYLE_MAGIC) recent_target_pray_magic++; + else if (p->recent_target_prayer_styles[i] == ATTACK_STYLE_RANGED) recent_target_pray_ranged++; + else if (p->recent_target_prayer_styles[i] == ATTACK_STYLE_MELEE) recent_target_pray_melee++; + + if (p->recent_player_prayer_styles[i] == ATTACK_STYLE_MAGIC) recent_player_pray_magic++; + else if (p->recent_player_prayer_styles[i] == ATTACK_STYLE_RANGED) recent_player_pray_ranged++; + else if (p->recent_player_prayer_styles[i] == ATTACK_STYLE_MELEE) recent_player_pray_melee++; + + if (p->recent_target_hit_correct[i]) recent_target_hit_correct++; + if (p->recent_target_prayer_correct[i]) recent_target_pray_correct++; + } + + obs[82] = (float)recent_target_hit_melee / (float)HISTORY_SIZE; + obs[83] = (float)recent_target_hit_magic / (float)HISTORY_SIZE; + obs[84] = (float)recent_target_hit_ranged / (float)HISTORY_SIZE; + obs[85] = (float)recent_player_hit_melee / (float)HISTORY_SIZE; + obs[86] = (float)recent_player_hit_magic / (float)HISTORY_SIZE; + obs[87] = (float)recent_player_hit_ranged / (float)HISTORY_SIZE; + obs[88] = (float)recent_target_hit_correct / (float)HISTORY_SIZE; + obs[89] = (float)recent_target_pray_magic / (float)HISTORY_SIZE; + obs[90] = (float)recent_target_pray_ranged / (float)HISTORY_SIZE; + obs[91] = (float)recent_target_pray_melee / (float)HISTORY_SIZE; + obs[92] = (float)recent_player_pray_magic / (float)HISTORY_SIZE; + obs[93] = (float)recent_player_pray_ranged / (float)HISTORY_SIZE; + obs[94] = (float)recent_player_pray_melee / (float)HISTORY_SIZE; + obs[95] = (float)recent_target_pray_correct / (float)HISTORY_SIZE; + + obs[96] = (float)p->base_attack; + obs[97] = (float)p->base_strength; + obs[98] = (float)p->base_defence; + obs[99] = (float)p->base_ranged; + obs[100] = (float)p->base_magic; + obs[101] = (float)p->base_prayer; + obs[102] = (float)p->base_hitpoints; + + int melee_spec_cost = get_melee_spec_cost(p->melee_spec_weapon); + obs[103] = (p->melee_spec_weapon == MELEE_SPEC_NONE) ? 0.5f : (float)melee_spec_cost / 100.0f; + obs[104] = get_melee_spec_str_mult(p->melee_spec_weapon); + obs[105] = get_melee_spec_acc_mult(p->melee_spec_weapon); + + int melee_hit_count = (p->melee_spec_weapon == MELEE_SPEC_DRAGON_CLAWS) ? 4 : + (p->melee_spec_weapon == MELEE_SPEC_DRAGON_DAGGER || + p->melee_spec_weapon == MELEE_SPEC_ABYSSAL_DAGGER) ? 2 : 1; + obs[106] = (float)melee_hit_count; + obs[107] = (p->melee_spec_weapon == MELEE_SPEC_VOIDWAKER) ? 1.0f : 0.0f; + obs[108] = (p->melee_spec_weapon == MELEE_SPEC_DWH || + p->melee_spec_weapon == MELEE_SPEC_BGS) ? 1.0f : 0.0f; + obs[109] = (p->melee_spec_weapon == MELEE_SPEC_GRANITE_MAUL) ? 1.0f : 0.0f; + + int ranged_spec_cost = get_ranged_spec_cost(p->ranged_spec_weapon); + obs[110] = (p->ranged_spec_weapon == RANGED_SPEC_NONE) ? 0.5f : (float)ranged_spec_cost / 100.0f; + obs[111] = get_ranged_spec_str_mult(p->ranged_spec_weapon); + obs[112] = get_ranged_spec_acc_mult(p->ranged_spec_weapon); + obs[113] = p->bolt_proc_damage; + obs[114] = p->bolt_ignores_defense ? 1.0f : 0.0f; + + obs[115] = (p->magic_spec_weapon != MAGIC_SPEC_NONE) ? 1.0f : 0.0f; + obs[116] = (p->ranged_spec_weapon != RANGED_SPEC_NONE) ? 1.0f : 0.0f; + obs[117] = p->has_blood_fury ? 1.0f : 0.0f; + osrs_ensure_player_equipment(p); + obs[118] = (p->equipment_effect_profile.dharok_piece_count >= 4) ? 1.0f : 0.0f; + + GearBonuses* slot_bonuses = get_slot_gear_bonuses(p); + obs[119] = (float)slot_bonuses->magic_attack; + obs[120] = (float)slot_bonuses->magic_strength; + obs[121] = (float)slot_bonuses->ranged_attack; + obs[122] = (float)slot_bonuses->ranged_strength; + obs[123] = (float)slot_bonuses->attack_speed; + obs[124] = (float)slot_bonuses->attack_range; + obs[125] = (float)slot_bonuses->slash_attack; + obs[126] = (float)slot_bonuses->melee_strength; + obs[127] = (float)slot_bonuses->ranged_defence; + obs[128] = (float)slot_bonuses->magic_defence; + obs[129] = (float)slot_bonuses->slash_defence; + + GearBonuses* target_bonuses = get_slot_gear_bonuses(t); + obs[130] = (float)target_bonuses->ranged_defence; + obs[131] = (float)target_bonuses->magic_defence; + obs[132] = (float)target_bonuses->slash_defence; + + obs[133] = env->is_lms ? 1.0f : 0.0f; + obs[134] = env->pvp_runtime.is_pvp_arena ? 1.0f : 0.0f; + obs[135] = p->veng_active ? 1.0f : 0.0f; + obs[136] = t->veng_active ? 1.0f : 0.0f; + obs[137] = p->is_lunar_spellbook ? 1.0f : 0.0f; + obs[138] = p->observed_target_lunar_spellbook ? 1.0f : 0.0f; + obs[139] = (float)remaining_ticks(p->veng_cooldown); + obs[140] = (float)remaining_ticks(t->veng_cooldown); + obs[141] = is_blood_attack_available(p) ? 1.0f : 0.0f; + obs[142] = is_ice_attack_available(p) ? 1.0f : 0.0f; + obs[143] = can_toggle_spec(p) ? 1.0f : 0.0f; + obs[144] = is_ranged_attack_available(p) ? 1.0f : 0.0f; + obs[145] = is_ranged_spec_attack_available(p) ? 1.0f : 0.0f; + obs[146] = is_melee_attack_available(p, t) ? 1.0f : 0.0f; + obs[147] = is_melee_spec_attack_available(p, t) ? 1.0f : 0.0f; + obs[148] = (p->brew_doses > 0) ? 0.8f : 0.0f; + + obs[149] = (p->attack_timer <= 0) ? 1.0f : 0.0f; + + obs[150] = (float)p->equipped[GEAR_SLOT_WEAPON] / 63.0f; + obs[151] = (p->attack_style_this_tick == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; + obs[152] = (p->attack_style_this_tick == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; + obs[153] = (p->attack_style_this_tick == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; + + AttackStyle target_style = get_slot_weapon_attack_style(t); + obs[154] = (target_style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; + obs[155] = (target_style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; + obs[156] = (target_style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; + + obs[157] = (p->offensive_prayer == OFFENSIVE_PRAYER_PIETY) ? 1.0f : 0.0f; + obs[158] = (p->offensive_prayer == OFFENSIVE_PRAYER_RIGOUR) ? 1.0f : 0.0f; + obs[159] = (p->offensive_prayer == OFFENSIVE_PRAYER_AUGURY) ? 1.0f : 0.0f; + + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + obs[160 + slot] = (float)p->equipped[slot] / 63.0f; + } + + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + obs[171 + slot] = (float)t->equipped[slot] / 63.0f; + } + + uint8_t best_mspec = find_best_melee_spec(p); + obs[182] = (best_mspec == ITEM_VOIDWAKER) ? 1.0f : 0.0f; + + obs[183] = p->used_special_this_tick ? 1.0f : 0.0f; + obs[184] = p->ate_food_this_tick ? 1.0f : 0.0f; + obs[185] = p->ate_karambwan_this_tick ? 1.0f : 0.0f; + AttackStyle current_weapon_style = get_slot_weapon_attack_style(p); + obs[186] = (current_weapon_style == ATTACK_STYLE_MAGIC) ? 1.0f : 0.0f; + obs[187] = (current_weapon_style == ATTACK_STYLE_RANGED) ? 1.0f : 0.0f; + obs[188] = (current_weapon_style == ATTACK_STYLE_MELEE) ? 1.0f : 0.0f; + obs[189] = p->ate_brew_this_tick ? 1.0f : 0.0f; + + float wild_w = (float)(WILD_MAX_X - WILD_MIN_X); + float wild_h = (float)(WILD_MAX_Y - WILD_MIN_Y); + obs[190] = (float)(p->x - WILD_MIN_X) / wild_w; + obs[191] = (float)(WILD_MAX_X - p->x) / wild_w; + obs[192] = (float)(p->y - WILD_MIN_Y) / wild_h; + obs[193] = (float)(WILD_MAX_Y - p->y) / wild_h; + float scale = (wild_w > wild_h ? wild_h : wild_w) * 0.5f; + if (scale < 1.0f) scale = 1.0f; + obs[194] = clampf((float)(t->x - p->x) / scale, -1.0f, 1.0f); + obs[195] = clampf((float)(t->y - p->y) / scale, -1.0f, 1.0f); + const CollisionMap* cmap_obs = (const CollisionMap*)env->collision_map; + for (int m = 0; m < MOVE_DIM; m++) { + if (m == 0) { + obs[196 + m] = 1.0f; + continue; + } + int nx = p->x + ENCOUNTER_MOVE_TARGET_DX[m]; + int ny = p->y + ENCOUNTER_MOVE_TARGET_DY[m]; + obs[196 + m] = pvp_tile_walkable((void*)cmap_obs, nx, ny) ? 1.0f : 0.0f; + } +} + +static void compute_action_masks(OsrsEnv* env, int agent_idx) { + Player* p = &env->players[agent_idx]; + Player* t = &env->players[1 - agent_idx]; + + unsigned char* mask = env->action_masks + agent_idx * ACTION_MASK_SIZE; + int offset = 0; + + mask[offset + LOADOUT_KEEP] = 1; + for (int l = LOADOUT_MELEE; l <= LOADOUT_TANK; l++) { + mask[offset + l] = is_loadout_active(p, l) ? 0 : 1; + } + + int frozen_no_melee = !can_move(p) && !is_in_melee_range(p, t); + + uint8_t best_melee_spec = find_best_melee_spec(p); + int melee_spec_cost = 25; + if (best_melee_spec == ITEM_AGS || best_melee_spec == ITEM_ANCIENT_GS) melee_spec_cost = 50; + if (best_melee_spec == ITEM_STATIUS_WARHAMMER) melee_spec_cost = 35; + mask[offset + LOADOUT_SPEC_MELEE] = (best_melee_spec != ITEM_NONE) && + (p->special_energy >= melee_spec_cost) && !frozen_no_melee; + + uint8_t best_range_spec = find_best_ranged_spec(p); + int range_spec_cost = 50; + mask[offset + LOADOUT_SPEC_RANGE] = (best_range_spec != ITEM_NONE) && + (p->special_energy >= range_spec_cost); + + uint8_t best_magic_spec = find_best_magic_spec(p); + mask[offset + LOADOUT_SPEC_MAGIC] = (best_magic_spec != ITEM_NONE) && + (p->special_energy >= 55); + + mask[offset + LOADOUT_GMAUL] = player_has_gmaul(p) && + (p->special_energy >= 50) && !frozen_no_melee; + + if (frozen_no_melee) { + mask[offset + LOADOUT_MELEE] = 0; + } + offset += LOADOUT_DIM; + + int attack_ready = remaining_ticks(p->attack_timer) == 0; + int current_loadout = get_current_loadout(p); + int in_mage_loadout = (current_loadout == LOADOUT_MAGE); + int in_tank_loadout = (current_loadout == LOADOUT_TANK); + int weapon_style = get_slot_weapon_attack_style(p); + int melee_reachable = (weapon_style == ATTACK_STYLE_MELEE) + ? (is_in_melee_range(p, t) || can_move(p)) + : 1; + int can_move_now = can_move(p); + mask[offset + ATTACK_NONE] = 1; + mask[offset + ATTACK_ATK] = attack_ready && !in_mage_loadout && !in_tank_loadout && + weapon_style != ATTACK_STYLE_NONE && + melee_reachable; + mask[offset + ATTACK_ICE] = attack_ready && can_cast_ice_spell(p); + mask[offset + ATTACK_BLOOD] = attack_ready && can_cast_blood_spell(p); + const CollisionMap* cmap = (const CollisionMap*)env->collision_map; + mask[offset + MOVE_ADJACENT] = can_move_now && can_move_adjacent(p, cmap); + mask[offset + MOVE_UNDER] = can_move_now && can_move_under(p, t); + mask[offset + MOVE_DIAGONAL] = can_move_now && can_move_diagonal(p, cmap); + mask[offset + MOVE_FARCAST_2] = can_move_now && can_move_to_farcast(p, 2, cmap); + mask[offset + MOVE_FARCAST_3] = can_move_now && can_move_to_farcast(p, 3, cmap); + mask[offset + MOVE_FARCAST_4] = can_move_now && can_move_to_farcast(p, 4, cmap); + mask[offset + MOVE_FARCAST_5] = can_move_now && can_move_to_farcast(p, 5, cmap); + mask[offset + MOVE_FARCAST_6] = can_move_now && can_move_to_farcast(p, 6, cmap); + mask[offset + MOVE_FARCAST_7] = can_move_now && can_move_to_farcast(p, 7, cmap); + offset += COMBAT_DIM; + + int has_prayer = p->current_prayer > 0; + mask[offset + ENCOUNTER_OVERHEAD_NO_CHANGE] = 1; + mask[offset + ENCOUNTER_OVERHEAD_OFF] = p->prayer != PRAYER_NONE; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE] = has_prayer; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED] = has_prayer; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC] = has_prayer; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE] = has_prayer && !env->is_lms; + mask[offset + ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION] = has_prayer && !env->is_lms; + offset += OVERHEAD_DIM; + + mask[offset + FOOD_NONE] = 1; + mask[offset + FOOD_EAT] = can_eat_food(p); + offset += FOOD_DIM; + + mask[offset + POTION_NONE] = 1; + mask[offset + POTION_BREW] = can_use_potion(p, 1) && can_use_brew_boost(p); + mask[offset + POTION_RESTORE] = can_use_potion(p, 2) && can_restore_stats(p); + mask[offset + POTION_COMBAT] = can_use_potion(p, 3) && can_boost_combat_skills(p); + mask[offset + POTION_RANGED] = can_use_potion(p, 4) && can_boost_ranged(p); + offset += POTION_DIM; + + mask[offset + KARAM_NONE] = 1; + mask[offset + KARAM_EAT] = can_eat_karambwan(p); + offset += KARAMBWAN_DIM; + + mask[offset + VENG_NONE] = 1; + mask[offset + VENG_CAST] = !env->is_lms && p->is_lunar_spellbook && !p->veng_active && + (remaining_ticks(p->veng_cooldown) == 0) && p->current_magic >= 94; + offset += VENG_DIM; + + mask[offset + ENCOUNTER_OFFENSIVE_NO_CHANGE] = 1; + mask[offset + ENCOUNTER_OFFENSIVE_OFF] = p->offensive_prayer != OFFENSIVE_PRAYER_NONE; + mask[offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY] = has_prayer; + mask[offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR] = has_prayer; + mask[offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_AUGURY] = has_prayer; + offset += OFFENSIVE_DIM; + + mask[offset + 0] = 1; + int can_move_for_move_head = can_move(p); + for (int m = 1; m < MOVE_DIM; m++) { + if (!can_move_for_move_head) { + mask[offset + m] = 0; + continue; + } + int nx = p->x + ENCOUNTER_MOVE_TARGET_DX[m]; + int ny = p->y + ENCOUNTER_MOVE_TARGET_DY[m]; + mask[offset + m] = pvp_tile_walkable((void*)cmap, nx, ny) ? 1 : 0; + } + offset += MOVE_DIM; +} + +#endif // OSRS_PVP_OBSERVATIONS_H diff --git a/ocean/osrs/osrs_pvp_opponents.h b/ocean/osrs/osrs_pvp_opponents.h new file mode 100644 index 0000000000..c7092b53f3 --- /dev/null +++ b/ocean/osrs/osrs_pvp_opponents.h @@ -0,0 +1,2068 @@ +#ifndef OSRS_PVP_OPPONENTS_H +#define OSRS_PVP_OPPONENTS_H + +#define OPP_STYLE_MAGE 0 +#define OPP_STYLE_RANGED 1 +#define OPP_STYLE_MELEE 2 +#define OPP_STYLE_SPEC 3 + +static inline int opp_style_to_loadout(int style) { + switch (style) { + case OPP_STYLE_MAGE: return LOADOUT_MAGE; + case OPP_STYLE_RANGED: return LOADOUT_RANGE; + case OPP_STYLE_MELEE: return LOADOUT_MELEE; + case OPP_STYLE_SPEC: return LOADOUT_SPEC_MELEE; + default: return LOADOUT_KEEP; + } +} + +static inline void opp_apply_gear_switch(int* actions, int style) { + actions[HEAD_LOADOUT] = opp_style_to_loadout(style); +} + +typedef struct { + int can_food; + int can_brew; + int can_karambwan; + int can_restore; +} OppConsumables; + +static inline void opp_tick_cooldowns(OpponentState* opp) { + if (opp->food_cooldown > 0) opp->food_cooldown--; + if (opp->potion_cooldown > 0) opp->potion_cooldown--; + if (opp->karambwan_cooldown > 0) opp->karambwan_cooldown--; +} + +static inline OppConsumables opp_get_consumables(OpponentState* opp, Player* self) { + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + OppConsumables c; + c.can_food = (opp->food_cooldown <= 0 && self->food_count > 0 && hp_pct < 1.0f); + c.can_brew = (opp->potion_cooldown <= 0 && self->brew_doses > 0); + c.can_karambwan = (opp->karambwan_cooldown <= 0 && self->karambwan_count > 0 && hp_pct < 1.0f); + c.can_restore = (opp->potion_cooldown <= 0 && self->restore_doses > 0); + return c; +} + +static inline AttackStyle opp_get_gear_style(Player* p) { + int s = get_item_attack_style(p->equipped[GEAR_SLOT_WEAPON]); + if (s == 3) return ATTACK_STYLE_MAGIC; + if (s == 2) return ATTACK_STYLE_RANGED; + if (s == 1) return ATTACK_STYLE_MELEE; + return ATTACK_STYLE_MAGIC; +} + +static inline int opp_get_defensive_prayer(Player* target) { + AttackStyle target_style = opp_get_gear_style(target); + if (target_style == ATTACK_STYLE_MAGIC) return OVERHEAD_MAGE; + if (target_style == ATTACK_STYLE_RANGED) return OVERHEAD_RANGED; + if (target_style == ATTACK_STYLE_MELEE) return OVERHEAD_MELEE; + return OVERHEAD_MAGE; +} + +static inline int opp_has_prayer_active(Player* self, int prayer_action) { + if (prayer_action == OVERHEAD_MELEE) return self->prayer == PRAYER_PROTECT_MELEE; + if (prayer_action == OVERHEAD_RANGED) return self->prayer == PRAYER_PROTECT_RANGED; + if (prayer_action == OVERHEAD_MAGE) return self->prayer == PRAYER_PROTECT_MAGIC; + return 0; +} + +static inline int opp_attack_ready(Player* self) { + return self->attack_timer <= 0; +} + +static inline int opp_can_reach_melee(Player* self, Player* target) { + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + return dist <= 1 || (self->frozen_ticks == 0 && dist <= 5); +} + +static inline int opp_get_off_prayer_mask(Player* self, Player* target) { + int mask = 0; + if (target->prayer != PRAYER_PROTECT_MAGIC) mask |= (1 << OPP_STYLE_MAGE); + if (target->prayer != PRAYER_PROTECT_RANGED) mask |= (1 << OPP_STYLE_RANGED); + if (target->prayer != PRAYER_PROTECT_MELEE && opp_can_reach_melee(self, target)) + mask |= (1 << OPP_STYLE_MELEE); + if (mask == 0) mask = (1 << OPP_STYLE_MAGE); + return mask; +} + +static inline int opp_pick_from_mask(OsrsEnv* env, int mask) { + int choices[3]; + int count = 0; + for (int i = 0; i < 3; i++) { + if (mask & (1 << i)) choices[count++] = i; + } + return choices[rand_int(env, count)]; +} + +static inline int opp_is_drained(Player* self) { + return self->current_strength < self->base_strength || + self->current_attack < self->base_attack || + self->current_defence < self->base_defence || + self->current_ranged < self->base_ranged || + self->current_magic < self->base_magic; +} + +static inline int opp_should_fc3(Player* self, Player* target) { + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + return target->freeze_immunity_ticks > 1 && + self->frozen_ticks == 0 && + self->attack_timer <= 2 && + dist > 3; +} + +static inline void opp_update_flee_tracking(OpponentState* opp, Player* self, Player* target) { + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + if (dist > opp->prev_dist_to_target && dist > 1) { + opp->target_fleeing_ticks++; + } else { + opp->target_fleeing_ticks = 0; + } + opp->prev_dist_to_target = dist; +} + +typedef struct { float base; float variance; } RandRange; + +typedef struct { + RandRange prayer_accuracy; + RandRange off_prayer_rate; + RandRange offensive_prayer_rate; + RandRange action_delay_chance; + RandRange mistake_rate; + RandRange offensive_prayer_miss; +} OpponentRandRanges; + +#define RR(b, v) {(b), (v)} + +static const OpponentRandRanges OPP_RAND_RANGES[OPP_RANGE_KITER + 1] = { + [OPP_NONE] = { RR(0,0), RR(0,0), RR(0,0), RR(0,0), RR(0,0), RR(0,0) }, + [OPP_TRUE_RANDOM] = { RR(0.33,0), RR(0.33,0), RR(0,0), RR(0,0), RR(0,0), RR(0,0) }, + [OPP_PANICKING] = { RR(0.33,0.1), RR(0.33,0), RR(0,0), RR(0.10,0.05), RR(0,0), RR(0,0) }, + [OPP_WEAK_RANDOM] = { RR(0.40,0.1), RR(0.33,0.1), RR(0,0), RR(0.10,0.05), RR(0.05,0.03), RR(0,0) }, + [OPP_SEMI_RANDOM] = { RR(0.50,0.1), RR(0.40,0.1), RR(0.05,0.03),RR(0.08,0.04), RR(0.05,0.03), RR(0,0) }, + [OPP_STICKY_PRAYER] = { RR(0.33,0), RR(0.33,0), RR(0,0), RR(0.10,0.05), RR(0,0), RR(0,0) }, + [OPP_RANDOM_EATER] = { RR(0.40,0.1), RR(0.33,0.1), RR(0,0), RR(0.08,0.04), RR(0.05,0.03), RR(0,0) }, + [OPP_PRAYER_ROOKIE] = { RR(0.30,0.1), RR(0.20,0.1), RR(0,0), RR(0.12,0.05), RR(0.08,0.04), RR(0,0) }, + [OPP_IMPROVED] = { RR(0.95,0.05),RR(0.95,0.05),RR(0.80,0.10),RR(0.05,0.03), RR(0.03,0.02), RR(0.05,0.03) }, + [OPP_MIXED_EASY] = { RR(0,0), RR(0,0), RR(0,0), RR(0,0), RR(0,0), RR(0,0) }, + [OPP_MIXED_MEDIUM] = { RR(0,0), RR(0,0), RR(0,0), RR(0,0), RR(0,0), RR(0,0) }, + [OPP_ONETICK] = { RR(0.97,0.03),RR(0.97,0.03),RR(0.90,0.05),RR(0.03,0.02), RR(0.02,0.01), RR(0.03,0.02) }, + [OPP_UNPREDICTABLE_IMPROVED]= { RR(0.92,0.05),RR(0.90,0.05),RR(0.75,0.10),RR(0.08,0.04), RR(0.05,0.03), RR(0.08,0.04) }, + [OPP_UNPREDICTABLE_ONETICK] = { RR(0.95,0.03),RR(0.95,0.03),RR(0.85,0.08),RR(0.05,0.03), RR(0.03,0.02), RR(0.05,0.03) }, + [OPP_MIXED_HARD] = { RR(0,0), RR(0,0), RR(0,0), RR(0,0), RR(0,0), RR(0,0) }, + [OPP_MIXED_HARD_BALANCED] = { RR(0,0), RR(0,0), RR(0,0), RR(0,0), RR(0,0), RR(0,0) }, + [OPP_PFSP] = { RR(0,0), RR(0,0), RR(0,0), RR(0,0), RR(0,0), RR(0,0) }, + [OPP_NOVICE_NH] = { RR(0.60,0.10),RR(0.10,0.05),RR(0.10,0.05),RR(0.15,0.05), RR(0.10,0.05), RR(0.30,0.10) }, + [OPP_APPRENTICE_NH] = { RR(0.60,0.10),RR(0.20,0.08),RR(0.20,0.08),RR(0.12,0.05), RR(0.08,0.04), RR(0.30,0.10) }, + [OPP_COMPETENT_NH] = { RR(0.75,0.08),RR(0.25,0.08),RR(0.25,0.08),RR(0.10,0.04), RR(0.06,0.03), RR(0.20,0.08) }, + [OPP_INTERMEDIATE_NH] = { RR(0.85,0.05),RR(0.70,0.08),RR(0.50,0.10),RR(0.08,0.04), RR(0.05,0.03), RR(0.20,0.08) }, + [OPP_ADVANCED_NH] = { RR(0.95,0.05),RR(0.90,0.05),RR(0.75,0.08),RR(0.05,0.03), RR(0.03,0.02), RR(0.10,0.05) }, + [OPP_PROFICIENT_NH] = { RR(0.95,0.03),RR(0.92,0.04),RR(0.80,0.08),RR(0.04,0.02), RR(0.03,0.02), RR(0.10,0.05) }, + [OPP_EXPERT_NH] = { RR(0.97,0.03),RR(0.95,0.03),RR(0.85,0.05),RR(0.03,0.02), RR(0.02,0.01), RR(0.10,0.05) }, + [OPP_MASTER_NH] = { RR(0.98,0.02),RR(0.97,0.03),RR(0.90,0.05),RR(0.02,0.01), RR(0.01,0.01), RR(0.01,0.01) }, + [OPP_SAVANT_NH] = { RR(0.98,0.02),RR(0.97,0.03),RR(0.90,0.05),RR(0.02,0.01), RR(0.01,0.01), RR(0.01,0.01) }, + [OPP_NIGHTMARE_NH] = { RR(0.99,0.01),RR(0.98,0.02),RR(0.95,0.03),RR(0.01,0.01), RR(0.005,0.005),RR(0.01,0.01) }, + [OPP_VENG_FIGHTER] = { RR(0.92,0.05),RR(0.90,0.05),RR(0.85,0.10),RR(0.03,0.02), RR(0.02,0.01), RR(0.05,0.03) }, + [OPP_BLOOD_HEALER] = { RR(0.90,0.05),RR(0.88,0.05),RR(0.80,0.10),RR(0.05,0.03), RR(0.04,0.02), RR(0.05,0.03) }, + [OPP_GMAUL_COMBO] = { RR(0.96,0.03),RR(0.95,0.03),RR(0.90,0.05),RR(0.03,0.02), RR(0.02,0.01), RR(0.02,0.01) }, + [OPP_RANGE_KITER] = { RR(0.93,0.04),RR(0.93,0.04),RR(0.85,0.08),RR(0.04,0.02), RR(0.03,0.02), RR(0.04,0.02) }, +}; + +#undef RR + +static inline float rand_range(OsrsEnv* env, RandRange r) { + float v = r.base + (rand_float(env) * 2.0f - 1.0f) * r.variance; + return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); +} + +static inline int opp_should_skip_offensive(OsrsEnv* env, OpponentState* opp) { + return rand_float(env) < opp->action_delay_chance; +} + +static inline int opp_pick_off_prayer_style_biased(OsrsEnv* env, OpponentState* opp, + Player* self, Player* target) { + int off_mask = opp_get_off_prayer_mask(self, target); + float weights[3] = {0}; + float total = 0; + for (int i = 0; i < 3; i++) { + if (off_mask & (1 << i)) { + weights[i] = opp->style_bias[i]; + total += weights[i]; + } + } + if (total <= 0) return opp_pick_from_mask(env, off_mask); + + float r = rand_float(env) * total; + float cum = 0; + for (int i = 0; i < 3; i++) { + cum += weights[i]; + if (r < cum) return i; + } + return opp_pick_from_mask(env, off_mask); +} + +static inline int opp_apply_prayer_mistake(OsrsEnv* env, OpponentState* opp, int correct_prayer) { + if (rand_float(env) < opp->mistake_rate) { + int prayers[] = {OVERHEAD_MELEE, OVERHEAD_RANGED, OVERHEAD_MAGE}; + return prayers[rand_int(env, 3)]; + } + return correct_prayer; +} + +static const float UNPREDICTABLE_IMP_PRAYER_CUM[] = {0.70f, 0.90f, 0.98f, 1.00f}; +#define UNPREDICTABLE_IMP_PRAYER_CUM_LEN 4 + +static const float UNPREDICTABLE_IMP_ACTION_CUM[] = {0.85f, 0.97f, 1.00f}; +#define UNPREDICTABLE_IMP_ACTION_CUM_LEN 3 + +static const float UNPREDICTABLE_OT_PRAYER_CUM[] = {0.80f, 0.95f, 0.99f, 1.00f}; +#define UNPREDICTABLE_OT_PRAYER_CUM_LEN 4 + +static const float UNPREDICTABLE_OT_ACTION_CUM[] = {0.90f, 0.98f, 1.00f}; +#define UNPREDICTABLE_OT_ACTION_CUM_LEN 3 + +#define UNPREDICTABLE_IMP_WRONG_PRAYER 0.05f +#define UNPREDICTABLE_IMP_SUBOPTIMAL_ATTACK 0.03f +#define UNPREDICTABLE_OT_FAKE_FAIL 0.12f +#define UNPREDICTABLE_OT_WRONG_PREDICT 0.08f + +static inline int opp_sample_delay(OsrsEnv* env, const float* cum_weights, int num_weights) { + float r = rand_float(env); + for (int i = 0; i < num_weights; i++) { + if (r < cum_weights[i]) return i; + } + return num_weights - 1; +} + +static inline int opp_get_defensive_prayer_with_spec(Player* target) { + if (target->visible_gear == GEAR_MELEE) return OVERHEAD_MELEE; + if (target->visible_gear == GEAR_RANGED) return OVERHEAD_RANGED; + if (target->visible_gear == GEAR_MAGE) return OVERHEAD_MAGE; + return opp_get_defensive_prayer(target); +} + +static inline int opp_get_opponent_prayer_style(Player* target) { + if (target->prayer == PRAYER_PROTECT_MAGIC) return OPP_STYLE_MAGE; + if (target->prayer == PRAYER_PROTECT_RANGED) return OPP_STYLE_RANGED; + if (target->prayer == PRAYER_PROTECT_MELEE) return OPP_STYLE_MELEE; + return -1; +} + +static inline int opp_get_target_gear_style(Player* target) { + return (int)target->visible_gear; +} + +static inline int opp_get_mage_attack(Player* self, Player* target) { + int can_freeze = target->freeze_immunity_ticks <= 1 && target->frozen_ticks == 0; + if (can_freeze) return ATTACK_ICE; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + return (hp_pct > 0.98f) ? ATTACK_ICE : ATTACK_BLOOD; +} + +static void opp_apply_boost_potion(OsrsEnv* env, OpponentState* opp, int* actions, + Player* self, int attack_style, int potion_used) { + (void)env; + if (potion_used) return; + if (opp->potion_cooldown > 0) return; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + + if (opp_is_drained(self) && hp_pct > 0.90f && self->restore_doses > 0) { + actions[HEAD_POTION] = POTION_RESTORE; + opp->potion_cooldown = 3; + return; + } + + if (hp_pct <= 0.90f) return; + + if (attack_style == OPP_STYLE_MELEE || attack_style == OPP_STYLE_SPEC) { + if (self->current_strength <= self->base_strength && self->combat_potion_doses > 0) { + actions[HEAD_POTION] = POTION_COMBAT; + opp->potion_cooldown = 3; + } + } else if (attack_style == OPP_STYLE_RANGED) { + if (self->current_ranged <= self->base_ranged && self->ranged_potion_doses > 0) { + actions[HEAD_POTION] = POTION_RANGED; + opp->potion_cooldown = 3; + } + } +} + +static inline int opp_check_eating_queued(int* actions) { + return actions[HEAD_FOOD] != FOOD_NONE || actions[HEAD_KARAMBWAN] != KARAM_NONE; +} + +static int opp_apply_consumables(OsrsEnv* env, OpponentState* opp, int* actions, + Player* self, int include_drained_restore) { + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + float prayer_pct = (float)self->current_prayer / (float)self->base_prayer; + OppConsumables cons = opp_get_consumables(opp, self); + int potion_used = 0; + + if (hp_pct < opp->eat_triple_threshold && cons.can_food && cons.can_brew && cons.can_karambwan) { + actions[HEAD_FOOD] = FOOD_EAT; + actions[HEAD_POTION] = POTION_BREW; + actions[HEAD_KARAMBWAN] = KARAM_EAT; + opp->food_cooldown = 3; + opp->potion_cooldown = 3; + opp->karambwan_cooldown = 2; + potion_used = 1; + } else if (hp_pct < opp->eat_double_threshold && cons.can_food && cons.can_brew) { + actions[HEAD_FOOD] = FOOD_EAT; + actions[HEAD_POTION] = POTION_BREW; + opp->food_cooldown = 3; + opp->potion_cooldown = 3; + potion_used = 1; + } else if (hp_pct < opp->eat_double_threshold && cons.can_food && cons.can_karambwan) { + actions[HEAD_FOOD] = FOOD_EAT; + actions[HEAD_KARAMBWAN] = KARAM_EAT; + opp->food_cooldown = 3; + opp->karambwan_cooldown = 2; + } else if (hp_pct < opp->eat_brew_threshold && cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + potion_used = 1; + } else if (hp_pct < 0.60f && cons.can_food) { + actions[HEAD_FOOD] = FOOD_EAT; + opp->food_cooldown = 3; + } else if (hp_pct < 0.60f && cons.can_karambwan) { + actions[HEAD_KARAMBWAN] = KARAM_EAT; + opp->karambwan_cooldown = 2; + } else if (opp_is_drained(self) && hp_pct < 0.90f && cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + potion_used = 1; + } else if (prayer_pct < 0.30f && cons.can_restore) { + actions[HEAD_POTION] = POTION_RESTORE; + opp->potion_cooldown = 3; + } else if (include_drained_restore && opp_is_drained(self) && cons.can_restore) { + actions[HEAD_POTION] = POTION_RESTORE; + opp->potion_cooldown = 3; + } + + (void)env; + return potion_used; +} + +static inline int opp_set_refresh_for_prayer(OverheadPrayer p) { + switch (p) { + case PRAYER_PROTECT_MAGIC: return ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC; + case PRAYER_PROTECT_RANGED: return ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; + case PRAYER_PROTECT_MELEE: return ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE; + case PRAYER_SMITE: return ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE; + case PRAYER_REDEMPTION: return ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION; + default: return ENCOUNTER_OVERHEAD_NO_CHANGE; + } +} + +static inline void opp_emit_prayer(int* actions, Player* self, int target_overhead_action) { + OverheadPrayer target_prayer; + switch (target_overhead_action) { + case OVERHEAD_NONE: target_prayer = PRAYER_NONE; break; + case OVERHEAD_MAGE: target_prayer = PRAYER_PROTECT_MAGIC; break; + case OVERHEAD_RANGED: target_prayer = PRAYER_PROTECT_RANGED; break; + case OVERHEAD_MELEE: target_prayer = PRAYER_PROTECT_MELEE; break; + case OVERHEAD_SMITE: target_prayer = PRAYER_SMITE; break; + case OVERHEAD_REDEMPTION: target_prayer = PRAYER_REDEMPTION; break; + default: return; + } + if (self->prayer == target_prayer) return; + actions[HEAD_OVERHEAD] = (target_prayer == PRAYER_NONE) + ? ENCOUNTER_OVERHEAD_OFF + : opp_set_refresh_for_prayer(target_prayer); +} + +static inline int opp_process_pending_prayer(OpponentState* opp, int* actions, Player* self) { + if (opp->pending_prayer_value == 0) return 0; + if (opp->pending_prayer_delay > 0) { + opp->pending_prayer_delay--; + if (opp->pending_prayer_delay > 0) return 0; + } + OverheadPrayer target_prayer = PRAYER_NONE; + int action = ENCOUNTER_OVERHEAD_NO_CHANGE; + switch (opp->pending_prayer_value) { + case OVERHEAD_MAGE: target_prayer = PRAYER_PROTECT_MAGIC; action = ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC; break; + case OVERHEAD_RANGED: target_prayer = PRAYER_PROTECT_RANGED; action = ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; break; + case OVERHEAD_MELEE: target_prayer = PRAYER_PROTECT_MELEE; action = ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE; break; + case OVERHEAD_SMITE: target_prayer = PRAYER_SMITE; action = ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE; break; + case OVERHEAD_REDEMPTION: target_prayer = PRAYER_REDEMPTION; action = ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION; break; + default: break; + } + if (self->prayer != target_prayer) actions[HEAD_OVERHEAD] = action; + opp->pending_prayer_value = 0; + return 1; +} + +static void opp_handle_delayed_prayer(OsrsEnv* env, OpponentState* opp, int* actions, + Player* self, Player* target, + const float* cum_weights, int cum_len, + float wrong_prayer_prob, int include_spec) { + int target_style = opp_get_target_gear_style(target); + if (target_style != opp->last_target_gear_style) { + opp->last_target_gear_style = target_style; + + int needed_prayer = include_spec + ? opp_get_defensive_prayer_with_spec(target) + : opp_get_defensive_prayer(target); + + int needs_switch = !opp_has_prayer_active(self, needed_prayer); + + if (needs_switch) { + if (rand_float(env) < wrong_prayer_prob) { + int wrong_options[2]; + int wcount = 0; + int all_prayers[] = {OVERHEAD_MELEE, OVERHEAD_RANGED, OVERHEAD_MAGE}; + for (int i = 0; i < 3; i++) { + if (all_prayers[i] != needed_prayer) + wrong_options[wcount++] = all_prayers[i]; + } + needed_prayer = wrong_options[rand_int(env, wcount)]; + } + + int delay = opp_sample_delay(env, cum_weights, cum_len); + opp->pending_prayer_value = needed_prayer; + opp->pending_prayer_delay = delay; + } + } + + opp_process_pending_prayer(opp, actions, self); +} + +static void opp_apply_defensive_prayer(OsrsEnv* env, OpponentState* opp, int* actions, + Player* self, Player* target, int use_spec_detect) { + int def_prayer; + if (rand_float(env) < opp->prayer_accuracy) { + def_prayer = use_spec_detect + ? opp_get_defensive_prayer_with_spec(target) + : opp_get_defensive_prayer(target); + } else { + int prayers[] = {OVERHEAD_MELEE, OVERHEAD_RANGED, OVERHEAD_MAGE}; + def_prayer = prayers[rand_int(env, 3)]; + } + def_prayer = opp_apply_prayer_mistake(env, opp, def_prayer); + if (!opp_has_prayer_active(self, def_prayer)) { + opp_emit_prayer(actions, self, def_prayer); + } +} + +static inline void opp_offensive_prayer_noop_roll(OsrsEnv* env) { + /* dead roll from an unimplemented offensive-prayer branch: removing it shifts + every later draw in the opponent RNG stream */ + (void)rand_float(env); +} + +static inline void opp_emit_attack(int* actions, int actual_attack) { + if (actual_attack == 0) { + actions[HEAD_COMBAT] = ATTACK_ICE; + } else if (actual_attack == 1) { + actions[HEAD_COMBAT] = ATTACK_BLOOD; + } else { + actions[HEAD_COMBAT] = ATTACK_ATK; + } +} + +static void opp_move_when_waiting(OsrsEnv* env, OpponentState* opp, int* actions, + Player* self, Player* target, float under_prob) { + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + if (target->frozen_ticks > 0 && self->frozen_ticks == 0 && dist > 0 && + (under_prob >= 1.0f || (under_prob > 0.0f && rand_float(env) < under_prob))) { + actions[HEAD_COMBAT] = MOVE_UNDER; + } else if (opp->target_fleeing_ticks >= 2 && dist > 3 && self->frozen_ticks == 0) { + actions[HEAD_COMBAT] = MOVE_FARCAST_3; + } else if (opp_should_fc3(self, target) && target->prayer != PRAYER_PROTECT_MELEE) { + actions[HEAD_COMBAT] = MOVE_FARCAST_3; + } +} + +static void opp_attack_random_style(OsrsEnv* env, int* actions) { + int style = rand_int(env, 3); + opp_apply_gear_switch(actions, style); + if (style == OPP_STYLE_MAGE) { + actions[HEAD_COMBAT] = (rand_int(env, 2) == 0) ? ATTACK_ICE : ATTACK_BLOOD; + } else { + actions[HEAD_COMBAT] = ATTACK_ATK; + } +} + +static void opp_attack_random_style_with_spec(OsrsEnv* env, Player* self, int* actions) { + int style = rand_int(env, 3); + if (self->special_energy >= get_melee_spec_cost(self->melee_spec_weapon) && + rand_float(env) < 0.30f) { + opp_apply_gear_switch(actions, OPP_STYLE_SPEC); + actions[HEAD_COMBAT] = ATTACK_ATK; + } else { + opp_apply_gear_switch(actions, style); + if (style == OPP_STYLE_MAGE) { + actions[HEAD_COMBAT] = (rand_int(env, 2) == 0) ? ATTACK_ICE : ATTACK_BLOOD; + } else { + actions[HEAD_COMBAT] = ATTACK_ATK; + } + } +} + +typedef struct { int melee; int ranged; int magic; } OppSpecPlan; + +static OppSpecPlan opp_plan_specs(Player* self, Player* target, int dist) { + int can_melee_range = (self->frozen_ticks > 0) ? (dist <= 1) : (dist <= 3); + float target_hp_pct = (float)target->current_hitpoints / (float)target->base_hitpoints; + uint8_t ranged_spec = find_best_ranged_spec(self); + uint8_t magic_spec = find_best_magic_spec(self); + int has_ranged_or_magic = (ranged_spec != ITEM_NONE || magic_spec != ITEM_NONE); + + OppSpecPlan plan; + plan.melee = opp_attack_ready(self) && + self->special_energy >= get_melee_spec_cost(self->melee_spec_weapon) && + target->prayer != PRAYER_PROTECT_MELEE && + can_melee_range && + (!has_ranged_or_magic || target_hp_pct < 0.55f); + plan.ranged = opp_attack_ready(self) && ranged_spec != ITEM_NONE && + self->special_energy >= get_ranged_spec_cost(self->ranged_spec_weapon) && + target->prayer != PRAYER_PROTECT_RANGED && + target_hp_pct < 0.55f; + plan.magic = opp_attack_ready(self) && magic_spec != ITEM_NONE && + self->special_energy >= get_magic_spec_cost(self->magic_spec_weapon) && + target->prayer != PRAYER_PROTECT_MAGIC && + target_hp_pct < 0.55f; + return plan; +} + +static int opp_try_fake_switch(OsrsEnv* env, OpponentState* opp, int* actions, + Player* self, Player* target, int off_mask, + float fail_prob) { + if (opp->fake_switch_pending && opp_attack_ready(self)) { + opp->fake_switch_pending = 0; + opp->fake_switch_style = -1; + if (fail_prob >= 0.0f) opp->fake_switch_failed = 0; + return 0; + } + if (opp_attack_ready(self) || opp->fake_switch_pending) return 0; + if (rand_float(env) >= 0.30f) return 0; + + int current_style = (int)self->current_gear; + int can_fake_melee = self->frozen_ticks <= 10 || + chebyshev_distance(self->x, self->y, target->x, target->y) <= 1; + + int fake_options[3]; + int fake_count = 0; + for (int s = 0; s < 3; s++) { + if (!(off_mask & (1 << s))) continue; + if (s == current_style) continue; + if (s == OPP_STYLE_MELEE && !can_fake_melee) continue; + fake_options[fake_count++] = s; + } + + if (fake_count == 0) return 0; + + opp->fake_switch_pending = 1; + opp->fake_switch_style = fake_options[rand_int(env, fake_count)]; + opp->opponent_prayer_at_fake = opp_get_opponent_prayer_style(target); + if (fail_prob >= 0.0f) + opp->fake_switch_failed = (rand_float(env) < fail_prob) ? 1 : 0; + + opp_apply_gear_switch(actions, opp->fake_switch_style); + + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + if (target->frozen_ticks > 0 && self->frozen_ticks == 0 && dist > 0) { + actions[HEAD_COMBAT] = MOVE_UNDER; + } + return 1; +} + +static void opp_true_random(OsrsEnv* env, int* actions) { + for (int i = 0; i < NUM_ACTION_HEADS; i++) { + actions[i] = rand_int(env, ACTION_HEAD_DIMS[i]); + } +} + +static void opp_panicking(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + + opp_tick_cooldowns(opp); + OppConsumables cons = opp_get_consumables(opp, self); + + if (!opp_has_prayer_active(self, opp->chosen_prayer)) { + opp_emit_prayer(actions, self, opp->chosen_prayer); + } + + int eating = 0; + if (hp_pct < 0.25f) { + if (cons.can_food) { + actions[HEAD_FOOD] = FOOD_EAT; + opp->food_cooldown = 3; + eating = 1; + } + if (cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + } + } + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating && rand_float(env) < 0.30f) { + opp_apply_gear_switch(actions, opp->chosen_style); + + if (opp->chosen_style == OPP_STYLE_MAGE) { + int spell = (rand_int(env, 2) == 0) ? ATTACK_ICE : ATTACK_BLOOD; + actions[HEAD_COMBAT] = spell; + } else { + actions[HEAD_COMBAT] = ATTACK_ATK; + } + } +} + +static void opp_weak_random(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + + opp_tick_cooldowns(opp); + OppConsumables cons = opp_get_consumables(opp, self); + + int prayers[] = {OVERHEAD_NONE, OVERHEAD_MELEE, OVERHEAD_RANGED, OVERHEAD_MAGE}; + opp_emit_prayer(actions, self, prayers[rand_int(env, 4)]); + + int eating = 0; + if (hp_pct < 0.30f && rand_float(env) > 0.50f) { + if (cons.can_food) { + actions[HEAD_FOOD] = FOOD_EAT; + opp->food_cooldown = 3; + eating = 1; + } else if (cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + eating = 1; + } + } + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating) { + opp_attack_random_style(env, actions); + } +} + +static void opp_semi_random(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + + opp_tick_cooldowns(opp); + OppConsumables cons = opp_get_consumables(opp, self); + + int prayers[] = {OVERHEAD_MELEE, OVERHEAD_RANGED, OVERHEAD_MAGE}; + opp_emit_prayer(actions, self, prayers[rand_int(env, 3)]); + + int eating = 0; + if (hp_pct < 0.30f) { + if (cons.can_food) { + actions[HEAD_FOOD] = FOOD_EAT; + opp->food_cooldown = 3; + eating = 1; + } else if (cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + eating = 1; + } + } + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating) { + opp_attack_random_style(env, actions); + } +} + +static void opp_sticky_prayer(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + + opp_tick_cooldowns(opp); + OppConsumables cons = opp_get_consumables(opp, self); + + if (!opp->current_prayer_set || rand_float(env) < 0.08f) { + int prayers[] = {OVERHEAD_MELEE, OVERHEAD_RANGED, OVERHEAD_MAGE}; + opp->current_prayer = prayers[rand_int(env, 3)]; + opp->current_prayer_set = 1; + } + if (!opp_has_prayer_active(self, opp->current_prayer)) { + opp_emit_prayer(actions, self, opp->current_prayer); + } + + int eating = 0; + if (hp_pct < 0.30f) { + if (cons.can_food) { + actions[HEAD_FOOD] = FOOD_EAT; + opp->food_cooldown = 3; + eating = 1; + } else if (cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + eating = 1; + } + } + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating) { + opp_attack_random_style(env, actions); + } +} + +static void opp_random_eater(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + float prayer_pct = (float)self->current_prayer / (float)self->base_prayer; + + opp_tick_cooldowns(opp); + OppConsumables cons = opp_get_consumables(opp, self); + + if (!opp->current_prayer_set || rand_float(env) < 0.08f) { + int prayers[] = {OVERHEAD_MELEE, OVERHEAD_RANGED, OVERHEAD_MAGE}; + opp->current_prayer = prayers[rand_int(env, 3)]; + opp->current_prayer_set = 1; + } + if (!opp_has_prayer_active(self, opp->current_prayer)) { + opp_emit_prayer(actions, self, opp->current_prayer); + } + + int potion_used = 0; + if (hp_pct < 0.35f) { + if (cons.can_food) { + actions[HEAD_FOOD] = FOOD_EAT; + opp->food_cooldown = 3; + } + if (cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + potion_used = 1; + } + if (cons.can_karambwan) { + actions[HEAD_KARAMBWAN] = KARAM_EAT; + opp->karambwan_cooldown = 2; + } + } else if (hp_pct < 0.55f) { + if (cons.can_food) { + actions[HEAD_FOOD] = FOOD_EAT; + opp->food_cooldown = 3; + } else if (cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + potion_used = 1; + } + } else if (hp_pct < opp->eat_brew_threshold && cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + potion_used = 1; + } + + if (!potion_used && prayer_pct < 0.30f && cons.can_restore) { + actions[HEAD_POTION] = POTION_RESTORE; + opp->potion_cooldown = 3; + } + + int eating = opp_check_eating_queued(actions); + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating) { + opp_attack_random_style_with_spec(env, self, actions); + } +} + +static void opp_prayer_rookie(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + Player* target = &env->players[0]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + + opp_tick_cooldowns(opp); + OppConsumables cons = opp_get_consumables(opp, self); + + opp_apply_defensive_prayer(env, opp, actions, self, target, 0); + + if (hp_pct < 0.35f) { + if (cons.can_food) { + actions[HEAD_FOOD] = FOOD_EAT; + opp->food_cooldown = 3; + } + if (cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + } + if (cons.can_karambwan) { + actions[HEAD_KARAMBWAN] = KARAM_EAT; + opp->karambwan_cooldown = 2; + } + } else if (hp_pct < 0.55f) { + if (cons.can_food) { + actions[HEAD_FOOD] = FOOD_EAT; + opp->food_cooldown = 3; + } else if (cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + } + } else if (hp_pct < opp->eat_brew_threshold && cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + } + + int eating = opp_check_eating_queued(actions); + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating) { + opp_attack_random_style_with_spec(env, self, actions); + } +} + +typedef struct { + int biased_style_pick; + float spec_target_hp_gate; + float spec_trigger_prob; + int offensive_noop_roll; + int has_move_block; + float move_under_prob; +} OppNhTier; + +static const OppNhTier NH_TIER_IMPROVED = {1, 2.0f, 1.0f, 0, 1, 2.0f}; +static const OppNhTier NH_TIER_COMPETENT = {0, 0.60f, 0.50f, 1, 0, 0.0f}; +static const OppNhTier NH_TIER_INTERMEDIATE = {0, 0.60f, 1.0f, 1, 0, 0.0f}; +static const OppNhTier NH_TIER_ADVANCED = {1, 2.0f, 1.0f, 1, 1, 0.0f}; +static const OppNhTier NH_TIER_PROFICIENT = {1, 2.0f, 1.0f, 1, 1, 0.25f}; +static const OppNhTier NH_TIER_EXPERT = {1, 2.0f, 1.0f, 1, 1, 0.50f}; + +static void opp_nh_tier(OsrsEnv* env, OpponentState* opp, int* actions, const OppNhTier* tier) { + Player* self = &env->players[1]; + Player* target = &env->players[0]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + + opp_tick_cooldowns(opp); + + opp_apply_defensive_prayer(env, opp, actions, self, target, 0); + opp_apply_consumables(env, opp, actions, self, 1); + + int eating = opp_check_eating_queued(actions); + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating) { + int attack_style; + if (rand_float(env) < opp->off_prayer_rate) { + attack_style = tier->biased_style_pick + ? opp_pick_off_prayer_style_biased(env, opp, self, target) + : opp_pick_from_mask(env, opp_get_off_prayer_mask(self, target)); + } else { + attack_style = rand_int(env, 3); + } + + opp_apply_boost_potion(env, opp, actions, self, attack_style, 0); + + float target_hp_pct = (float)target->current_hitpoints / (float)target->base_hitpoints; + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + int can_spec_range = (self->frozen_ticks > 0) ? (dist <= 1) : (dist <= 3); + int should_spec = + self->special_energy >= get_melee_spec_cost(self->melee_spec_weapon) && + target_hp_pct < tier->spec_target_hp_gate && + target->prayer != PRAYER_PROTECT_MELEE && + can_spec_range && + (tier->spec_trigger_prob >= 1.0f || + rand_float(env) < tier->spec_trigger_prob); + + if (should_spec && opp->target_fleeing_ticks >= 2 && dist > 1) { + should_spec = 0; + attack_style = OPP_STYLE_MAGE; + } + + int actual_style; + int actual_attack; + if (should_spec) { + actual_style = OPP_STYLE_SPEC; + actual_attack = 3; + } else if (attack_style == OPP_STYLE_MAGE) { + actual_style = OPP_STYLE_MAGE; + actual_attack = (hp_pct < 0.30f) ? 1 : 0; + } else { + actual_style = attack_style; + actual_attack = 2; + } + + if (actual_attack != 3 && rand_float(env) < opp->offensive_prayer_miss) { + actions[HEAD_LOADOUT] = LOADOUT_KEEP; + } else { + opp_apply_gear_switch(actions, actual_style); + } + + if (tier->offensive_noop_roll) opp_offensive_prayer_noop_roll(env); + + opp_emit_attack(actions, actual_attack); + } else if (tier->has_move_block && !opp_attack_ready(self)) { + opp_move_when_waiting(env, opp, actions, self, target, tier->move_under_prob); + } +} + +static void opp_nh_basic(OsrsEnv* env, OpponentState* opp, int* actions, + float spec_prob, int coin_flip_spell, int drained_restore_tail) { + Player* self = &env->players[1]; + Player* target = &env->players[0]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + + opp_tick_cooldowns(opp); + + opp_apply_defensive_prayer(env, opp, actions, self, target, 0); + opp_apply_consumables(env, opp, actions, self, drained_restore_tail); + + int eating = opp_check_eating_queued(actions); + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating) { + int style; + if (rand_float(env) < opp->off_prayer_rate) { + style = opp_pick_from_mask(env, opp_get_off_prayer_mask(self, target)); + } else { + style = rand_int(env, 3); + } + + opp_apply_boost_potion(env, opp, actions, self, style, 0); + + if (self->special_energy >= get_melee_spec_cost(self->melee_spec_weapon) && + rand_float(env) < spec_prob) { + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + if (opp->target_fleeing_ticks >= 2 && dist > 1) { + opp_apply_gear_switch(actions, OPP_STYLE_MAGE); + actions[HEAD_COMBAT] = ATTACK_ICE; + } else { + opp_apply_gear_switch(actions, OPP_STYLE_SPEC); + actions[HEAD_COMBAT] = ATTACK_ATK; + } + } else { + if (rand_float(env) < opp->offensive_prayer_miss) { + actions[HEAD_LOADOUT] = LOADOUT_KEEP; + } else { + opp_apply_gear_switch(actions, style); + } + + opp_offensive_prayer_noop_roll(env); + + if (style == OPP_STYLE_MAGE) { + actions[HEAD_COMBAT] = coin_flip_spell + ? ((rand_int(env, 2) == 0) ? ATTACK_ICE : ATTACK_BLOOD) + : ((hp_pct < 0.30f) ? ATTACK_BLOOD : ATTACK_ICE); + } else { + actions[HEAD_COMBAT] = ATTACK_ATK; + } + } + } +} + +static void opp_onetick(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + Player* target = &env->players[0]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + + opp_tick_cooldowns(opp); + + if (!opp_attack_ready(self)) { + actions[HEAD_LOADOUT] = LOADOUT_TANK; + } + + opp_apply_defensive_prayer(env, opp, actions, self, target, 1); + + int potion_used = opp_apply_consumables(env, opp, actions, self, 1); + + int eating_queued = opp_check_eating_queued(actions); + + int off_mask = opp_get_off_prayer_mask(self, target); + + if (opp_try_fake_switch(env, opp, actions, self, target, off_mask, -1.0f)) return; + + int preferred_style = -1; + if (opp->opponent_prayer_at_fake >= 0) { + preferred_style = opp->opponent_prayer_at_fake; + opp->opponent_prayer_at_fake = -1; + } + + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + OppSpecPlan spec = opp_plan_specs(self, target, dist); + + if (spec.melee && opp->target_fleeing_ticks >= 2 && dist > 1) { + spec.melee = 0; + } + + int actual_style; + int actual_attack; + int spec_loadout = LOADOUT_SPEC_MELEE; + + if (spec.ranged && (dist >= 3 || target->frozen_ticks > 0)) { + actual_style = OPP_STYLE_RANGED; + actual_attack = 3; + spec_loadout = LOADOUT_SPEC_RANGE; + } else if (spec.magic) { + actual_style = OPP_STYLE_MAGE; + actual_attack = 3; + spec_loadout = LOADOUT_SPEC_MAGIC; + } else if (spec.melee) { + actual_style = OPP_STYLE_SPEC; + actual_attack = 3; + } else if (target->frozen_ticks == 0 && (off_mask & (1 << OPP_STYLE_MAGE))) { + actual_style = OPP_STYLE_MAGE; + actual_attack = opp_get_mage_attack(self, target) == ATTACK_ICE ? 0 : 1; + } else { + int can_use_preferred = preferred_style >= 0 && + (preferred_style != OPP_STYLE_MELEE || self->frozen_ticks <= 10 || dist <= 1); + + if (can_use_preferred) { + actual_style = preferred_style; + if (preferred_style == OPP_STYLE_MAGE) { + actual_attack = (hp_pct < 0.98f) ? 1 : 0; + } else { + actual_attack = 2; + } + } else if (off_mask & (1 << OPP_STYLE_MAGE)) { + actual_style = OPP_STYLE_MAGE; + actual_attack = (hp_pct < 0.98f) ? 1 : 0; + } else { + int non_mage[2]; + int nm_count = 0; + for (int s = 1; s < 3; s++) { + if (off_mask & (1 << s)) non_mage[nm_count++] = s; + } + if (nm_count == 0) { + actual_style = OPP_STYLE_RANGED; + } else { + actual_style = non_mage[rand_int(env, nm_count)]; + } + actual_attack = 2; + } + } + + opp_apply_boost_potion(env, opp, actions, self, actual_style, potion_used); + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating_queued) { + if (actual_attack == 3) { + actions[HEAD_LOADOUT] = spec_loadout; + } else if (rand_float(env) < opp->offensive_prayer_miss) { + actions[HEAD_LOADOUT] = LOADOUT_KEEP; + } else { + opp_apply_gear_switch(actions, actual_style); + } + + opp_emit_attack(actions, actual_attack); + } else if (!opp_attack_ready(self)) { + opp_move_when_waiting(env, opp, actions, self, target, 2.0f); + } +} + +static void opp_unpredictable_improved(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + Player* target = &env->players[0]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + + opp_tick_cooldowns(opp); + + opp_handle_delayed_prayer(env, opp, actions, self, target, + UNPREDICTABLE_IMP_PRAYER_CUM, UNPREDICTABLE_IMP_PRAYER_CUM_LEN, + UNPREDICTABLE_IMP_WRONG_PRAYER, 0); + + int potion_used = opp_apply_consumables(env, opp, actions, self, 1); + + int eating_queued = opp_check_eating_queued(actions); + + if (opp_should_skip_offensive(env, opp)) return; + + int attack_style; + + if (rand_float(env) < UNPREDICTABLE_IMP_SUBOPTIMAL_ATTACK) { + attack_style = rand_int(env, 3); + } else { + attack_style = opp_pick_off_prayer_style_biased(env, opp, self, target); + } + + opp_apply_boost_potion(env, opp, actions, self, attack_style, potion_used); + + if (opp_attack_ready(self) && !eating_queued) { + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + int can_spec_range = (self->frozen_ticks > 0) ? (dist <= 1) : (dist <= 3); + int should_spec = self->special_energy >= get_melee_spec_cost(self->melee_spec_weapon) && + target->prayer != PRAYER_PROTECT_MELEE && + can_spec_range; + + if (should_spec && opp->target_fleeing_ticks >= 2 && dist > 1) { + should_spec = 0; + attack_style = OPP_STYLE_MAGE; + } + + int actual_style; + int actual_attack; + + if (should_spec) { + actual_style = OPP_STYLE_SPEC; + actual_attack = 3; + } else if (attack_style == OPP_STYLE_MAGE) { + actual_style = OPP_STYLE_MAGE; + actual_attack = (hp_pct < 0.30f) ? 1 : 0; + } else { + actual_style = attack_style; + actual_attack = 2; + } + + if (actual_attack != 3 && rand_float(env) < opp->offensive_prayer_miss) { + actions[HEAD_LOADOUT] = LOADOUT_KEEP; + } else { + opp_apply_gear_switch(actions, actual_style); + } + + int action_delay = opp_sample_delay(env, UNPREDICTABLE_IMP_ACTION_CUM, UNPREDICTABLE_IMP_ACTION_CUM_LEN); + if (action_delay == 0) { + opp_emit_attack(actions, actual_attack); + } + } else if (!opp_attack_ready(self)) { + opp_move_when_waiting(env, opp, actions, self, target, 2.0f); + } +} + +static void opp_unpredictable_onetick(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + Player* target = &env->players[0]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + + opp_tick_cooldowns(opp); + + if (!opp_attack_ready(self)) { + actions[HEAD_LOADOUT] = LOADOUT_TANK; + } + + opp_handle_delayed_prayer(env, opp, actions, self, target, + UNPREDICTABLE_OT_PRAYER_CUM, UNPREDICTABLE_OT_PRAYER_CUM_LEN, + 0.0f, 1); + + int potion_used = opp_apply_consumables(env, opp, actions, self, 1); + + int eating_queued = opp_check_eating_queued(actions); + + int off_mask = opp_get_off_prayer_mask(self, target); + + if (opp_try_fake_switch(env, opp, actions, self, target, off_mask, + UNPREDICTABLE_OT_FAKE_FAIL)) return; + + int preferred_style = -1; + + if (opp->opponent_prayer_at_fake >= 0 && !opp->fake_switch_failed) { + if (rand_float(env) < UNPREDICTABLE_OT_WRONG_PREDICT) { + preferred_style = rand_int(env, 3); + } else { + preferred_style = opp->opponent_prayer_at_fake; + } + opp->opponent_prayer_at_fake = -1; + } else if (opp->fake_switch_failed) { + opp->opponent_prayer_at_fake = -1; + opp->fake_switch_failed = 0; + } + + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + OppSpecPlan spec = opp_plan_specs(self, target, dist); + + if (spec.melee && opp->target_fleeing_ticks >= 2 && dist > 1) { + spec.melee = 0; + } + + int actual_style; + int actual_attack; + int spec_loadout = LOADOUT_SPEC_MELEE; + + if (spec.ranged && (dist >= 3 || target->frozen_ticks > 0)) { + actual_style = OPP_STYLE_RANGED; + actual_attack = 3; + spec_loadout = LOADOUT_SPEC_RANGE; + } else if (spec.magic) { + actual_style = OPP_STYLE_MAGE; + actual_attack = 3; + spec_loadout = LOADOUT_SPEC_MAGIC; + } else if (spec.melee) { + actual_style = OPP_STYLE_SPEC; + actual_attack = 3; + } else if (target->frozen_ticks == 0 && (off_mask & (1 << OPP_STYLE_MAGE))) { + actual_style = OPP_STYLE_MAGE; + actual_attack = opp_get_mage_attack(self, target) == ATTACK_ICE ? 0 : 1; + } else { + int can_use_preferred = preferred_style >= 0 && + (preferred_style != OPP_STYLE_MELEE || self->frozen_ticks <= 10 || dist <= 1); + + if (can_use_preferred) { + actual_style = preferred_style; + actual_attack = (preferred_style == OPP_STYLE_MAGE) + ? ((hp_pct < 0.98f) ? 1 : 0) + : 2; + } else if (off_mask & (1 << OPP_STYLE_MAGE)) { + actual_style = OPP_STYLE_MAGE; + actual_attack = (hp_pct < 0.98f) ? 1 : 0; + } else { + int non_mage[2]; + int nm_count = 0; + for (int s = 1; s < 3; s++) { + if (off_mask & (1 << s)) non_mage[nm_count++] = s; + } + actual_style = (nm_count > 0) ? non_mage[rand_int(env, nm_count)] : OPP_STYLE_RANGED; + actual_attack = 2; + } + } + + opp_apply_boost_potion(env, opp, actions, self, actual_style, potion_used); + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating_queued) { + int action_delay = opp_sample_delay(env, UNPREDICTABLE_OT_ACTION_CUM, UNPREDICTABLE_OT_ACTION_CUM_LEN); + if (action_delay == 0) { + if (actual_attack == 3) { + actions[HEAD_LOADOUT] = spec_loadout; + } else if (rand_float(env) < opp->offensive_prayer_miss) { + actions[HEAD_LOADOUT] = LOADOUT_KEEP; + } else { + opp_apply_gear_switch(actions, actual_style); + } + + opp_emit_attack(actions, actual_attack); + } + } else if (!opp_attack_ready(self)) { + opp_move_when_waiting(env, opp, actions, self, target, 2.0f); + } +} + +static void opp_read_agent_action(OsrsEnv* env, OpponentState* opp) { + opp->has_read_this_tick = 0; + opp->read_agent_style = ATTACK_STYLE_NONE; + opp->read_agent_prayer = PRAYER_NONE; + opp->read_agent_moving = 0; + + if (opp->read_chance <= 0.0f || rand_float(env) >= opp->read_chance) { + return; + } + + /* env->actions holds THIS tick's agent actions; pending_actions is last tick's */ + int* agent_actions = &env->actions[0]; + + int loadout = agent_actions[HEAD_LOADOUT]; + int attack = agent_actions[HEAD_COMBAT]; + + if (loadout != LOADOUT_KEEP && loadout != LOADOUT_TANK) { + if (loadout == LOADOUT_MELEE || loadout == LOADOUT_SPEC_MELEE || loadout == LOADOUT_GMAUL) { + opp->read_agent_style = ATTACK_STYLE_MELEE; + } else if (loadout == LOADOUT_RANGE || loadout == LOADOUT_SPEC_RANGE) { + opp->read_agent_style = ATTACK_STYLE_RANGED; + } else if (loadout == LOADOUT_MAGE || loadout == LOADOUT_SPEC_MAGIC) { + opp->read_agent_style = ATTACK_STYLE_MAGIC; + } + opp->has_read_this_tick = 1; + } else if (attack == ATTACK_ICE || attack == ATTACK_BLOOD) { + opp->read_agent_style = ATTACK_STYLE_MAGIC; + opp->has_read_this_tick = 1; + } else if (attack == ATTACK_ATK) { + uint8_t weapon = env->players[0].equipped[GEAR_SLOT_WEAPON]; + int style = get_item_attack_style(weapon); + if (style == 1) opp->read_agent_style = ATTACK_STYLE_MELEE; + else if (style == 2) opp->read_agent_style = ATTACK_STYLE_RANGED; + else if (style == 3) opp->read_agent_style = ATTACK_STYLE_MAGIC; + opp->has_read_this_tick = 1; + } + + int overhead = agent_actions[HEAD_OVERHEAD]; + if (overhead == ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE) opp->read_agent_prayer = PRAYER_PROTECT_MELEE; + else if (overhead == ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED) opp->read_agent_prayer = PRAYER_PROTECT_RANGED; + else if (overhead == ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC) opp->read_agent_prayer = PRAYER_PROTECT_MAGIC; + else if (overhead == ENCOUNTER_OVERHEAD_SET_REFRESH_SMITE) opp->read_agent_prayer = PRAYER_SMITE; + else if (overhead == ENCOUNTER_OVERHEAD_SET_REFRESH_REDEMPTION) opp->read_agent_prayer = PRAYER_REDEMPTION; + + opp->read_agent_moving = is_move_action(attack) ? 1 : 0; +} + +static inline int opp_get_read_defensive_prayer(OpponentState* opp) { + if (opp->read_agent_style == ATTACK_STYLE_MAGIC) return OVERHEAD_MAGE; + if (opp->read_agent_style == ATTACK_STYLE_RANGED) return OVERHEAD_RANGED; + if (opp->read_agent_style == ATTACK_STYLE_MELEE) return OVERHEAD_MELEE; + return -1; +} + +static inline int opp_style_off_read_prayer(OpponentState* opp, int style) { + if (opp->read_agent_prayer == PRAYER_NONE) return 1; + if (style == OPP_STYLE_MAGE && opp->read_agent_prayer != PRAYER_PROTECT_MAGIC) return 1; + if (style == OPP_STYLE_RANGED && opp->read_agent_prayer != PRAYER_PROTECT_RANGED) return 1; + if (style == OPP_STYLE_MELEE && opp->read_agent_prayer != PRAYER_PROTECT_MELEE) return 1; + return 0; +} + +static void opp_master_nh(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + Player* target = &env->players[0]; + + opp_tick_cooldowns(opp); + + opp_read_agent_action(env, opp); + + if (!opp_attack_ready(self)) { + actions[HEAD_LOADOUT] = LOADOUT_TANK; + } + + int def_prayer = -1; + if (opp->has_read_this_tick && opp->read_agent_style != ATTACK_STYLE_NONE) { + def_prayer = opp_get_read_defensive_prayer(opp); + } + if (def_prayer < 0) { + if (rand_float(env) < opp->prayer_accuracy) { + def_prayer = opp_get_defensive_prayer_with_spec(target); + } else { + int prayers[] = {OVERHEAD_MELEE, OVERHEAD_RANGED, OVERHEAD_MAGE}; + def_prayer = prayers[rand_int(env, 3)]; + } + } + def_prayer = opp_apply_prayer_mistake(env, opp, def_prayer); + if (!opp_has_prayer_active(self, def_prayer)) { + opp_emit_prayer(actions, self, def_prayer); + } + + int potion_used = opp_apply_consumables(env, opp, actions, self, 1); + int eating_queued = opp_check_eating_queued(actions); + + int off_mask = opp_get_off_prayer_mask(self, target); + + if (opp_try_fake_switch(env, opp, actions, self, target, off_mask, -1.0f)) return; + + int preferred_style = -1; + if (opp->opponent_prayer_at_fake >= 0) { + preferred_style = opp->opponent_prayer_at_fake; + opp->opponent_prayer_at_fake = -1; + } + + if (opp->has_read_this_tick && opp->read_agent_prayer != PRAYER_NONE) { + int read_off_styles[3]; + int read_off_count = 0; + for (int s = 0; s < 3; s++) { + if (!(off_mask & (1 << s))) continue; + if (opp_style_off_read_prayer(opp, s)) { + read_off_styles[read_off_count++] = s; + } + } + if (read_off_count > 0) { + preferred_style = read_off_styles[rand_int(env, read_off_count)]; + } + } + + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + OppSpecPlan spec = opp_plan_specs(self, target, dist); + + if (opp->has_read_this_tick) { + if (spec.melee && opp->read_agent_prayer == PRAYER_PROTECT_MELEE) + spec.melee = 0; + if (spec.ranged && opp->read_agent_prayer == PRAYER_PROTECT_RANGED) + spec.ranged = 0; + if (spec.magic && opp->read_agent_prayer == PRAYER_PROTECT_MAGIC) + spec.magic = 0; + } + + if (spec.melee && opp->target_fleeing_ticks >= 2 && dist > 1) { + spec.melee = 0; + } + + if (spec.melee && opp->has_read_this_tick && opp->read_agent_moving && dist > 1) { + spec.melee = 0; + } + + int actual_style; + int actual_attack; + int spec_loadout = LOADOUT_SPEC_MELEE; + + if (spec.ranged && (dist >= 3 || target->frozen_ticks > 0)) { + actual_style = OPP_STYLE_RANGED; + actual_attack = 3; + spec_loadout = LOADOUT_SPEC_RANGE; + } else if (spec.magic) { + actual_style = OPP_STYLE_MAGE; + actual_attack = 3; + spec_loadout = LOADOUT_SPEC_MAGIC; + } else if (spec.melee) { + actual_style = OPP_STYLE_SPEC; + actual_attack = 3; + } else if (preferred_style >= 0) { + actual_style = preferred_style; + actual_attack = (preferred_style == OPP_STYLE_MAGE) + ? (opp_get_mage_attack(self, target) == ATTACK_ICE ? 0 : 1) + : 2; + } else if (target->frozen_ticks == 0 && (off_mask & (1 << OPP_STYLE_MAGE))) { + actual_style = OPP_STYLE_MAGE; + actual_attack = opp_get_mage_attack(self, target) == ATTACK_ICE ? 0 : 1; + } else { + actual_style = opp_pick_from_mask(env, off_mask); + actual_attack = (actual_style == OPP_STYLE_MAGE) + ? (opp_get_mage_attack(self, target) == ATTACK_ICE ? 0 : 1) + : 2; + } + + opp_apply_boost_potion(env, opp, actions, self, actual_style, potion_used); + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating_queued) { + if (actual_attack == 3) { + actions[HEAD_LOADOUT] = spec_loadout; + } else if (rand_float(env) < opp->offensive_prayer_miss) { + actions[HEAD_LOADOUT] = LOADOUT_KEEP; + } else { + opp_apply_gear_switch(actions, actual_style); + } + + opp_emit_attack(actions, actual_attack); + } else if (!opp_attack_ready(self)) { + opp_move_when_waiting(env, opp, actions, self, target, 2.0f); + } +} + +static void opp_veng_fighter(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + Player* target = &env->players[0]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + + opp_tick_cooldowns(opp); + OppConsumables cons = opp_get_consumables(opp, self); + + opp_apply_defensive_prayer(env, opp, actions, self, target, 0); + + if (hp_pct < opp->eat_triple_threshold && cons.can_food && cons.can_brew && cons.can_karambwan) { + actions[HEAD_FOOD] = FOOD_EAT; + actions[HEAD_POTION] = POTION_BREW; + actions[HEAD_KARAMBWAN] = KARAM_EAT; + opp->food_cooldown = 3; opp->potion_cooldown = 3; opp->karambwan_cooldown = 2; + } else if (hp_pct < opp->eat_double_threshold && cons.can_food && cons.can_brew) { + actions[HEAD_FOOD] = FOOD_EAT; + actions[HEAD_POTION] = POTION_BREW; + opp->food_cooldown = 3; opp->potion_cooldown = 3; + } else if (hp_pct < opp->eat_double_threshold && cons.can_food && cons.can_karambwan) { + actions[HEAD_FOOD] = FOOD_EAT; + actions[HEAD_KARAMBWAN] = KARAM_EAT; + opp->food_cooldown = 3; opp->karambwan_cooldown = 2; + } else if (hp_pct < opp->eat_brew_threshold && cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + } else if (hp_pct < 0.60f && cons.can_food) { + actions[HEAD_FOOD] = FOOD_EAT; + opp->food_cooldown = 3; + } else if (hp_pct < 0.60f && cons.can_karambwan) { + actions[HEAD_KARAMBWAN] = KARAM_EAT; + opp->karambwan_cooldown = 2; + } else if (opp_is_drained(self) && hp_pct < 0.90f && cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + } else if (opp_is_drained(self) && cons.can_restore) { + actions[HEAD_POTION] = POTION_RESTORE; + opp->potion_cooldown = 3; + } + + int eating = opp_check_eating_queued(actions); + + if (!self->veng_active && remaining_ticks(self->veng_cooldown) == 0) { + actions[HEAD_VENG] = VENG_CAST; + } + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating) { + int attack_style; + if (rand_float(env) < opp->off_prayer_rate) { + int off_mask = opp_get_off_prayer_mask(self, target); + off_mask &= ~(1 << OPP_STYLE_MAGE); + if (off_mask == 0) off_mask = (1 << OPP_STYLE_MELEE) | (1 << OPP_STYLE_RANGED); + attack_style = opp_pick_from_mask(env, off_mask); + } else { + attack_style = rand_int(env, 2) + 1; + } + + opp_apply_boost_potion(env, opp, actions, self, attack_style, 0); + + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + int can_spec_range = (self->frozen_ticks > 0) ? (dist <= 1) : (dist <= 3); + int should_spec = (self->special_energy >= get_melee_spec_cost(self->melee_spec_weapon) && + target->prayer != PRAYER_PROTECT_MELEE && + can_spec_range); + + if (should_spec && opp->target_fleeing_ticks >= 2 && dist > 1) { + should_spec = 0; + } + + if (should_spec) { + opp_apply_gear_switch(actions, OPP_STYLE_SPEC); + actions[HEAD_COMBAT] = ATTACK_ATK; + } else { + if (rand_float(env) < opp->offensive_prayer_miss) { + actions[HEAD_LOADOUT] = LOADOUT_KEEP; + } else { + opp_apply_gear_switch(actions, attack_style); + } + actions[HEAD_COMBAT] = ATTACK_ATK; + } + } else if (!opp_attack_ready(self)) { + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + if (target->frozen_ticks > 0 && self->frozen_ticks == 0 && dist > 0 && + rand_float(env) < 0.40f) { + actions[HEAD_COMBAT] = MOVE_UNDER; + } else if (opp->target_fleeing_ticks >= 2 && dist > 3 && self->frozen_ticks == 0) { + actions[HEAD_COMBAT] = MOVE_FARCAST_3; + } + } +} + +static void opp_blood_healer(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + Player* target = &env->players[0]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + float prayer_pct = (float)self->current_prayer / (float)self->base_prayer; + + opp_tick_cooldowns(opp); + OppConsumables cons = opp_get_consumables(opp, self); + + opp_apply_defensive_prayer(env, opp, actions, self, target, 0); + + if (hp_pct < 0.25f && cons.can_food && cons.can_brew && cons.can_karambwan) { + actions[HEAD_FOOD] = FOOD_EAT; + actions[HEAD_POTION] = POTION_BREW; + actions[HEAD_KARAMBWAN] = KARAM_EAT; + opp->food_cooldown = 3; opp->potion_cooldown = 3; opp->karambwan_cooldown = 2; + } else if (hp_pct < 0.35f && cons.can_food && cons.can_brew) { + actions[HEAD_FOOD] = FOOD_EAT; + actions[HEAD_POTION] = POTION_BREW; + opp->food_cooldown = 3; opp->potion_cooldown = 3; + } else if (hp_pct < 0.35f && cons.can_food && cons.can_karambwan) { + actions[HEAD_FOOD] = FOOD_EAT; + actions[HEAD_KARAMBWAN] = KARAM_EAT; + opp->food_cooldown = 3; opp->karambwan_cooldown = 2; + } else if (hp_pct < 0.35f && cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + } else if (opp_is_drained(self) && hp_pct < 0.50f && cons.can_brew) { + actions[HEAD_POTION] = POTION_BREW; + opp->potion_cooldown = 3; + } else if (prayer_pct < 0.30f && cons.can_restore) { + actions[HEAD_POTION] = POTION_RESTORE; + opp->potion_cooldown = 3; + } else if (opp_is_drained(self) && cons.can_restore) { + actions[HEAD_POTION] = POTION_RESTORE; + opp->potion_cooldown = 3; + } + + int eating = opp_check_eating_queued(actions); + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating) { + int attack_style; + int actual_attack; + + if (hp_pct < 0.40f) { + attack_style = OPP_STYLE_MAGE; + actual_attack = 1; + } else if (hp_pct < 0.70f) { + if (rand_float(env) < 0.80f) { + attack_style = OPP_STYLE_MAGE; + actual_attack = 1; + } else { + if (rand_float(env) < opp->off_prayer_rate) { + attack_style = opp_pick_off_prayer_style_biased(env, opp, self, target); + } else { + attack_style = rand_int(env, 3); + } + if (attack_style == OPP_STYLE_MAGE) { + actual_attack = (target->frozen_ticks == 0 && target->freeze_immunity_ticks == 0) + ? 0 : 1; + } else { + actual_attack = 2; + } + } + } else { + if (rand_float(env) < opp->off_prayer_rate) { + attack_style = opp_pick_off_prayer_style_biased(env, opp, self, target); + } else { + attack_style = rand_int(env, 3); + } + if (attack_style == OPP_STYLE_MAGE) { + actual_attack = (target->frozen_ticks == 0 && target->freeze_immunity_ticks == 0) + ? 0 : 1; + } else { + actual_attack = 2; + } + } + + opp_apply_boost_potion(env, opp, actions, self, attack_style, 0); + + if (rand_float(env) < opp->offensive_prayer_miss) { + actions[HEAD_LOADOUT] = LOADOUT_KEEP; + } else { + opp_apply_gear_switch(actions, attack_style); + } + + if (hp_pct < 0.35f && actual_attack != 1) { + actions[HEAD_LOADOUT] = LOADOUT_TANK; + } + + opp_emit_attack(actions, actual_attack); + } else if (!opp_attack_ready(self)) { + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + if (self->frozen_ticks == 0) { + if (target->frozen_ticks > 0 && dist < 5) { + actions[HEAD_COMBAT] = MOVE_FARCAST_5; + } else if (dist < 4 && target->frozen_ticks == 0) { + actions[HEAD_COMBAT] = MOVE_FARCAST_5; + } else if (opp->target_fleeing_ticks >= 2 && dist > 5) { + actions[HEAD_COMBAT] = MOVE_FARCAST_5; + } + } + } +} + +#define COMBO_IDLE 0 +#define COMBO_SPEC_FIRED 1 + +static void opp_gmaul_combo(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + Player* target = &env->players[0]; + float target_hp_pct = (float)target->current_hitpoints / (float)target->base_hitpoints; + int has_gmaul = player_has_gmaul(self); + + opp_tick_cooldowns(opp); + + opp_apply_defensive_prayer(env, opp, actions, self, target, 0); + opp_apply_consumables(env, opp, actions, self, 1); + + int eating = opp_check_eating_queued(actions); + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp->combo_state == COMBO_SPEC_FIRED && has_gmaul && !eating) { + actions[HEAD_LOADOUT] = LOADOUT_GMAUL; + actions[HEAD_COMBAT] = ATTACK_ATK; + opp->combo_state = COMBO_IDLE; + return; + } + opp->combo_state = COMBO_IDLE; + + if (opp_attack_ready(self) && !eating) { + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + + int melee_spec_cost = get_melee_spec_cost(self->melee_spec_weapon); + int gmaul_cost = 50; + int can_spec_range = (self->frozen_ticks > 0) ? (dist <= 1) : (dist <= 3); + int should_combo = (has_gmaul && + target_hp_pct < opp->ko_threshold && + self->special_energy >= melee_spec_cost + gmaul_cost && + target->prayer != PRAYER_PROTECT_MELEE && + can_spec_range); + + uint8_t ranged_spec = find_best_ranged_spec(self); + int should_ranged_spec = (ranged_spec != 0 && + target_hp_pct < opp->ko_threshold && + self->special_energy >= get_ranged_spec_cost(self->ranged_spec_weapon) && + target->prayer != PRAYER_PROTECT_RANGED && + rand_float(env) < 0.25f); + + if ((should_combo || should_ranged_spec) && + opp->target_fleeing_ticks >= 2 && dist > 1) { + should_combo = 0; + should_ranged_spec = 0; + } + + if (should_combo) { + opp_apply_gear_switch(actions, OPP_STYLE_SPEC); + actions[HEAD_COMBAT] = ATTACK_ATK; + opp->combo_state = COMBO_SPEC_FIRED; + } else if (should_ranged_spec) { + actions[HEAD_LOADOUT] = LOADOUT_SPEC_RANGE; + actions[HEAD_COMBAT] = ATTACK_ATK; + } else { + int attack_style; + if (rand_float(env) < opp->off_prayer_rate) { + attack_style = opp_pick_off_prayer_style_biased(env, opp, self, target); + } else { + attack_style = rand_int(env, 3); + } + + opp_apply_boost_potion(env, opp, actions, self, attack_style, 0); + + int should_regular_spec = (!has_gmaul && + self->special_energy >= melee_spec_cost && + target->prayer != PRAYER_PROTECT_MELEE && + target_hp_pct < 0.50f && + can_spec_range); + if (should_regular_spec && opp->target_fleeing_ticks < 2) { + opp_apply_gear_switch(actions, OPP_STYLE_SPEC); + actions[HEAD_COMBAT] = ATTACK_ATK; + } else { + if (rand_float(env) < opp->offensive_prayer_miss) { + actions[HEAD_LOADOUT] = LOADOUT_KEEP; + } else { + opp_apply_gear_switch(actions, attack_style); + } + + if (attack_style == OPP_STYLE_MAGE) { + actions[HEAD_COMBAT] = (target->frozen_ticks == 0 && + target->freeze_immunity_ticks == 0) + ? ATTACK_ICE : ATTACK_BLOOD; + } else { + actions[HEAD_COMBAT] = ATTACK_ATK; + } + } + } + } else if (!opp_attack_ready(self)) { + opp_move_when_waiting(env, opp, actions, self, target, 2.0f); + } +} + +static void opp_range_kiter(OsrsEnv* env, OpponentState* opp, int* actions) { + Player* self = &env->players[1]; + Player* target = &env->players[0]; + float hp_pct = (float)self->current_hitpoints / (float)self->base_hitpoints; + float target_hp_pct = (float)target->current_hitpoints / (float)target->base_hitpoints; + int dist = chebyshev_distance(self->x, self->y, target->x, target->y); + + opp_tick_cooldowns(opp); + + opp_apply_defensive_prayer(env, opp, actions, self, target, 0); + opp_apply_consumables(env, opp, actions, self, 1); + + int eating = opp_check_eating_queued(actions); + + if (opp_should_skip_offensive(env, opp)) return; + + if (opp_attack_ready(self) && !eating) { + uint8_t ranged_spec = find_best_ranged_spec(self); + int has_ranged_spec = (ranged_spec != 0); + int ranged_spec_cost = has_ranged_spec + ? get_ranged_spec_cost(self->ranged_spec_weapon) : 100; + + int should_ranged_spec = (has_ranged_spec && + self->special_energy >= ranged_spec_cost && + target->prayer != PRAYER_PROTECT_RANGED && + target_hp_pct < 0.55f); + + if (should_ranged_spec && (target->frozen_ticks > 0 || dist >= 3)) { + actions[HEAD_LOADOUT] = LOADOUT_SPEC_RANGE; + actions[HEAD_COMBAT] = ATTACK_ATK; + } else { + int attack_style; + int force_melee = (self->frozen_ticks > 0 && dist <= 1); + int prefer_ranged = (dist >= 3 || target->frozen_ticks > 0); + + if (force_melee) { + attack_style = OPP_STYLE_MELEE; + } else if (prefer_ranged && rand_float(env) < 0.80f) { + attack_style = OPP_STYLE_RANGED; + } else if (rand_float(env) < opp->off_prayer_rate) { + attack_style = opp_pick_off_prayer_style_biased(env, opp, self, target); + } else { + attack_style = rand_int(env, 3); + } + + int actual_attack; + if (hp_pct < 0.30f && attack_style == OPP_STYLE_MAGE) { + actual_attack = 1; + } else if (attack_style == OPP_STYLE_MAGE) { + actual_attack = (target->frozen_ticks == 0 && + target->freeze_immunity_ticks == 0) + ? 0 : 2; + if (actual_attack == 2) attack_style = OPP_STYLE_RANGED; + } else { + actual_attack = 2; + } + + opp_apply_boost_potion(env, opp, actions, self, attack_style, 0); + + int melee_spec_cost = get_melee_spec_cost(self->melee_spec_weapon); + int can_melee_spec = (self->special_energy >= melee_spec_cost && + target->prayer != PRAYER_PROTECT_MELEE && + dist <= 1 && self->frozen_ticks == 0); + if (can_melee_spec && target_hp_pct < 0.40f && !has_ranged_spec) { + opp_apply_gear_switch(actions, OPP_STYLE_SPEC); + actions[HEAD_COMBAT] = ATTACK_ATK; + } else { + if (rand_float(env) < opp->offensive_prayer_miss) { + actions[HEAD_LOADOUT] = LOADOUT_KEEP; + } else { + opp_apply_gear_switch(actions, attack_style); + } + + opp_emit_attack(actions, actual_attack); + } + } + } else if (!opp_attack_ready(self)) { + if (self->frozen_ticks == 0) { + if (target->frozen_ticks > 0 && dist < 5) { + actions[HEAD_COMBAT] = MOVE_FARCAST_5; + } else if (dist < 4) { + actions[HEAD_COMBAT] = MOVE_FARCAST_5; + } else if (dist > 7) { + actions[HEAD_COMBAT] = MOVE_FARCAST_5; + } + } + } +} + +static const OpponentType MIXED_EASY_POOL[] = { + OPP_PANICKING, OPP_TRUE_RANDOM, OPP_WEAK_RANDOM, OPP_SEMI_RANDOM, + OPP_STICKY_PRAYER, OPP_RANDOM_EATER, OPP_PRAYER_ROOKIE, OPP_IMPROVED, +}; +static const int MIXED_EASY_CUM_WEIGHTS[] = {18, 36, 54, 69, 79, 89, 95, 100}; +#define MIXED_EASY_POOL_SIZE 8 + +static const OpponentType MIXED_MEDIUM_POOL[] = { + OPP_RANDOM_EATER, OPP_PRAYER_ROOKIE, OPP_STICKY_PRAYER, + OPP_SEMI_RANDOM, OPP_IMPROVED, +}; +static const int MIXED_MEDIUM_CUM_WEIGHTS[] = {25, 45, 65, 80, 100}; +#define MIXED_MEDIUM_POOL_SIZE 5 + +static const OpponentType MIXED_HARD_POOL[] = { + OPP_IMPROVED, OPP_ONETICK, OPP_UNPREDICTABLE_IMPROVED, + OPP_UNPREDICTABLE_ONETICK, OPP_RANDOM_EATER, +}; +static const int MIXED_HARD_CUM_WEIGHTS[] = {20, 40, 60, 80, 100}; +#define MIXED_HARD_POOL_SIZE 5 + +static const OpponentType MIXED_HARD_BALANCED_POOL[] = { + OPP_RANDOM_EATER, OPP_IMPROVED, OPP_UNPREDICTABLE_IMPROVED, + OPP_ONETICK, OPP_UNPREDICTABLE_ONETICK, +}; +static const int MIXED_HARD_BALANCED_CUM_WEIGHTS[] = {25, 55, 75, 90, 100}; +#define MIXED_HARD_BALANCED_POOL_SIZE 5 + +static OpponentType opp_select_from_pool( + OsrsEnv* env, const OpponentType* pool, const int* cum_weights, int pool_size +) { + int r = rand_int(env, 100); + for (int i = 0; i < pool_size; i++) { + if (r < cum_weights[i]) return pool[i]; + } + return pool[pool_size - 1]; +} + +static void opponent_reset(OsrsEnv* env, OpponentState* opp) { + opp->food_cooldown = 0; + opp->potion_cooldown = 0; + opp->karambwan_cooldown = 0; + opp->current_prayer_set = 0; + + opp->fake_switch_pending = 0; + opp->fake_switch_style = -1; + opp->opponent_prayer_at_fake = -1; + opp->fake_switch_failed = 0; + opp->pending_prayer_value = 0; + opp->pending_prayer_delay = 0; + opp->last_target_gear_style = -1; + + opp->eat_triple_threshold = 0.30f + (rand_float(env) * 0.10f - 0.05f); + opp->eat_double_threshold = 0.50f + (rand_float(env) * 0.10f - 0.05f); + opp->eat_brew_threshold = 0.70f + (rand_float(env) * 0.10f - 0.05f); + + opp->has_read_this_tick = 0; + opp->read_agent_style = ATTACK_STYLE_NONE; + opp->read_agent_prayer = PRAYER_NONE; + opp->read_chance = 0.0f; + opp->read_agent_moving = 0; + opp->prev_dist_to_target = 0; + opp->target_fleeing_ticks = 0; + + if (opp->type == OPP_PANICKING) { + int prayers[] = {OVERHEAD_MELEE, OVERHEAD_RANGED, OVERHEAD_MAGE}; + opp->chosen_prayer = prayers[rand_int(env, 3)]; + opp->chosen_style = rand_int(env, 3); + } + + if (opp->type == OPP_MIXED_EASY) { + opp->active_sub_policy = opp_select_from_pool( + env, MIXED_EASY_POOL, MIXED_EASY_CUM_WEIGHTS, MIXED_EASY_POOL_SIZE); + } else if (opp->type == OPP_MIXED_MEDIUM) { + opp->active_sub_policy = opp_select_from_pool( + env, MIXED_MEDIUM_POOL, MIXED_MEDIUM_CUM_WEIGHTS, MIXED_MEDIUM_POOL_SIZE); + } else if (opp->type == OPP_MIXED_HARD) { + opp->active_sub_policy = opp_select_from_pool( + env, MIXED_HARD_POOL, MIXED_HARD_CUM_WEIGHTS, MIXED_HARD_POOL_SIZE); + } else if (opp->type == OPP_MIXED_HARD_BALANCED) { + opp->active_sub_policy = opp_select_from_pool( + env, MIXED_HARD_BALANCED_POOL, MIXED_HARD_BALANCED_CUM_WEIGHTS, + MIXED_HARD_BALANCED_POOL_SIZE); + } else if (opp->type == OPP_PFSP && env->pvp_runtime.pfsp.pool_size > 0) { + int idx = 0; + int r = rand_int(env, 1000); + for (int i = 0; i < env->pvp_runtime.pfsp.pool_size; i++) { + if (r < env->pvp_runtime.pfsp.cum_weights[i]) { idx = i; break; } + } + env->pvp_runtime.pfsp.active_pool_idx = idx; + opp->active_sub_policy = env->pvp_runtime.pfsp.pool[idx]; + + if (opp->active_sub_policy == OPP_SELFPLAY) { + env->pvp_runtime.use_c_opponent = 0; + env->pvp_runtime.use_external_opponent_actions = 1; + if (env->ocean_io.selfplay_mask) *env->ocean_io.selfplay_mask = 1; + } else { + env->pvp_runtime.use_c_opponent = 1; + env->pvp_runtime.use_external_opponent_actions = 0; + if (env->ocean_io.selfplay_mask) *env->ocean_io.selfplay_mask = 0; + } + } else if (opp->type == OPP_PFSP) { + opp->active_sub_policy = OPP_IMPROVED; + env->pvp_runtime.pfsp.active_pool_idx = -1; + } + + OpponentType resolved = opp->active_sub_policy ? opp->active_sub_policy : opp->type; + if (resolved > 0 && resolved <= OPP_RANGE_KITER) { + const OpponentRandRanges* r = &OPP_RAND_RANGES[resolved]; + opp->prayer_accuracy = rand_range(env, r->prayer_accuracy); + opp->off_prayer_rate = rand_range(env, r->off_prayer_rate); + opp->offensive_prayer_rate = rand_range(env, r->offensive_prayer_rate); + opp->action_delay_chance = rand_range(env, r->action_delay_chance); + opp->mistake_rate = rand_range(env, r->mistake_rate); + opp->offensive_prayer_miss = rand_range(env, r->offensive_prayer_miss); + } + + if (resolved == OPP_MASTER_NH) { + opp->read_chance = 0.10f; + } else if (resolved == OPP_SAVANT_NH) { + opp->read_chance = 0.25f; + } else if (resolved == OPP_NIGHTMARE_NH) { + opp->read_chance = 0.50f; + } + + if (resolved == OPP_VENG_FIGHTER) { + env->players[1].is_lunar_spellbook = 1; + } + + if (resolved == OPP_IMPROVED || resolved == OPP_ONETICK || + resolved == OPP_UNPREDICTABLE_IMPROVED || resolved == OPP_UNPREDICTABLE_ONETICK || + (resolved >= OPP_ADVANCED_NH && resolved <= OPP_NIGHTMARE_NH) || + resolved == OPP_BLOOD_HEALER || resolved == OPP_GMAUL_COMBO || + resolved == OPP_RANGE_KITER) { + float raw[3]; + for (int i = 0; i < 3; i++) raw[i] = 0.33f + (rand_float(env) - 0.5f) * 0.4f; + float sum = raw[0] + raw[1] + raw[2]; + for (int i = 0; i < 3; i++) opp->style_bias[i] = raw[i] / sum; + } else { + opp->style_bias[0] = opp->style_bias[1] = opp->style_bias[2] = 0.333f; + } + + if (resolved == OPP_GMAUL_COMBO) { + opp->combo_state = 0; + opp->ko_threshold = 0.45f + rand_float(env) * 0.15f; + } +} + +static void generate_opponent_action(OsrsEnv* env, OpponentState* opp) { + int* actions = &env->pending_actions[1 * NUM_ACTION_HEADS]; + + memset(actions, 0, NUM_ACTION_HEADS * sizeof(int)); + + opp_update_flee_tracking(opp, &env->players[1], &env->players[0]); + + OpponentType active = opp->type; + if (active == OPP_MIXED_EASY || active == OPP_MIXED_MEDIUM || + active == OPP_MIXED_HARD || active == OPP_MIXED_HARD_BALANCED || + active == OPP_PFSP) { + active = opp->active_sub_policy; + } + + switch (active) { + case OPP_TRUE_RANDOM: + opp_true_random(env, actions); + break; + case OPP_PANICKING: + opp_panicking(env, opp, actions); + break; + case OPP_WEAK_RANDOM: + opp_weak_random(env, opp, actions); + break; + case OPP_SEMI_RANDOM: + opp_semi_random(env, opp, actions); + break; + case OPP_STICKY_PRAYER: + opp_sticky_prayer(env, opp, actions); + break; + case OPP_RANDOM_EATER: + opp_random_eater(env, opp, actions); + break; + case OPP_PRAYER_ROOKIE: + opp_prayer_rookie(env, opp, actions); + break; + case OPP_IMPROVED: + opp_nh_tier(env, opp, actions, &NH_TIER_IMPROVED); + break; + case OPP_ONETICK: + opp_onetick(env, opp, actions); + break; + case OPP_UNPREDICTABLE_IMPROVED: + opp_unpredictable_improved(env, opp, actions); + break; + case OPP_UNPREDICTABLE_ONETICK: + opp_unpredictable_onetick(env, opp, actions); + break; + case OPP_NOVICE_NH: + opp_nh_basic(env, opp, actions, 0.15f, 1, 0); + break; + case OPP_APPRENTICE_NH: + opp_nh_basic(env, opp, actions, 0.30f, 0, 1); + break; + case OPP_COMPETENT_NH: + opp_nh_tier(env, opp, actions, &NH_TIER_COMPETENT); + break; + case OPP_INTERMEDIATE_NH: + opp_nh_tier(env, opp, actions, &NH_TIER_INTERMEDIATE); + break; + case OPP_ADVANCED_NH: + opp_nh_tier(env, opp, actions, &NH_TIER_ADVANCED); + break; + case OPP_PROFICIENT_NH: + opp_nh_tier(env, opp, actions, &NH_TIER_PROFICIENT); + break; + case OPP_EXPERT_NH: + opp_nh_tier(env, opp, actions, &NH_TIER_EXPERT); + break; + case OPP_MASTER_NH: + case OPP_SAVANT_NH: + case OPP_NIGHTMARE_NH: + opp_master_nh(env, opp, actions); + break; + case OPP_VENG_FIGHTER: + opp_veng_fighter(env, opp, actions); + break; + case OPP_BLOOD_HEALER: + opp_blood_healer(env, opp, actions); + break; + case OPP_GMAUL_COMBO: + opp_gmaul_combo(env, opp, actions); + break; + case OPP_RANGE_KITER: + opp_range_kiter(env, opp, actions); + break; + default: + break; + } +} + +static void swap_players_and_pending(OsrsEnv* env) { + Player tmp_player = env->players[0]; + env->players[0] = env->players[1]; + env->players[1] = tmp_player; + + int tmp_actions[NUM_ACTION_HEADS]; + memcpy(tmp_actions, env->pending_actions, NUM_ACTION_HEADS * sizeof(int)); + memcpy( + env->pending_actions, + env->pending_actions + NUM_ACTION_HEADS, + NUM_ACTION_HEADS * sizeof(int) + ); + memcpy( + env->pending_actions + NUM_ACTION_HEADS, + tmp_actions, + NUM_ACTION_HEADS * sizeof(int) + ); +} + +static void generate_opponent_action_for_player0(OsrsEnv* env, OpponentState* opp) { + swap_players_and_pending(env); + generate_opponent_action(env, opp); + swap_players_and_pending(env); +} + +#endif /* OSRS_PVP_OPPONENTS_H */ diff --git a/ocean/osrs/osrs_render.h b/ocean/osrs/osrs_render.h new file mode 100644 index 0000000000..f6755e046d --- /dev/null +++ b/ocean/osrs/osrs_render.h @@ -0,0 +1,6320 @@ +#ifndef OSRS_RENDER_H +#define OSRS_RENDER_H + +#include "raylib.h" +#include "rlgl.h" +#include "raymath.h" +#include "osrs_models.h" +#include "osrs_asset_raylib.h" +#include "osrs_anim.h" +#include "osrs_combat_visuals.h" +#include "osrs_combat.h" +#include "osrs_pvp_combat.h" +#include "osrs_effects.h" +#include "osrs_projectile_orientation.h" +#include "osrs_render_motion.h" +#include "osrs_render_click_hull.h" +#include "data/player_models.h" +#include "data/npc_models.h" +#include "osrs_terrain.h" +#include "osrs_objects.h" +#include "osrs_gui.h" +#include "osrs_human_input.h" +#include "encounters/encounter_colosseum.h" +#include +#include +#include +#include +#include + +#define RENDER_TILE_SIZE 20 +#define RENDER_WINDOW_W 1240 +#define RENDER_WINDOW_H 840 +#define RENDER_UI_SCALE 1.15f +#define RENDER_PANEL_WIDTH GUI_SIDE_MENU_W +#define RENDER_HEADER_HEIGHT 0 +#define RENDER_SPLATS_PER_PLAYER 4 +#define RENDER_HISTORY_INITIAL_CAPACITY 2000 +#define MAX_RENDER_ENTITIES 64 + +#define ANIM_SEQ_IDLE 808 +#define ANIM_SEQ_WALK 819 +#define ANIM_SEQ_RUN 824 +#define ANIM_SEQ_EAT 829 +#define ANIM_SEQ_DEATH 836 +#define ANIM_SEQ_CAST_STANDARD 1162 +#define ANIM_SEQ_CAST_BARRAGE 1979 +#define ANIM_SEQ_CAST_VENG 4410 +#define ANIM_SEQ_BLOCK_SHIELD 1156 +#define ANIM_SEQ_BLOCK_MELEE 424 + +#define RENDER_PANEL_SCREEN_W ((int)(RENDER_PANEL_WIDTH * RENDER_UI_SCALE + 0.5f)) +#define RENDER_GRID_W (RENDER_WINDOW_W - RENDER_PANEL_SCREEN_W) +#define RENDER_GRID_H (RENDER_WINDOW_H - RENDER_HEADER_HEIGHT) + +static inline Camera2D render_chrome_camera(float fixed_x, float fixed_y) { + return (Camera2D){ + .offset = (Vector2){ fixed_x, fixed_y }, + .target = (Vector2){ fixed_x, fixed_y }, + .rotation = 0.0f, + .zoom = RENDER_UI_SCALE, + }; +} + +#define RENDER_MINIMAP_AREA_H GUI_MAP_CONTAINER_H +#define RENDER_MINIMAP_CIRCLE_R (GUI_MINIMAP_W / 2) +#define RENDER_MINIMAP_CIRCLE_CY (GUI_MINIMAP_Y + GUI_MINIMAP_H / 2) +#define RENDER_ORB_R 16 + +#define RENDER_TAB_ROW_H 37 +#define RENDER_PANEL_CONTENT_H GUI_SIDE_CONTENT_H + +#define COLOR_BG CLITERAL(Color){ 20, 20, 25, 255 } +#define COLOR_GRID CLITERAL(Color){ 45, 45, 55, 255 } +#define COLOR_HEADER_BG CLITERAL(Color){ 30, 30, 40, 255 } +#define COLOR_PANEL_BG CLITERAL(Color){ 25, 25, 35, 255 } +#define COLOR_P0 CLITERAL(Color){ 80, 140, 255, 255 } +#define COLOR_P1 CLITERAL(Color){ 255, 90, 90, 255 } +#define COLOR_P0_LIGHT CLITERAL(Color){ 80, 140, 255, 60 } +#define COLOR_P1_LIGHT CLITERAL(Color){ 255, 90, 90, 60 } +#define COLOR_FREEZE CLITERAL(Color){ 100, 170, 255, 90 } +#define COLOR_VENG CLITERAL(Color){ 255, 220, 50, 255 } +#define COLOR_BLOCKED CLITERAL(Color){ 200, 50, 50, 50 } +#define COLOR_WALL CLITERAL(Color){ 220, 150, 40, 50 } +#define COLOR_BRIDGE CLITERAL(Color){ 50, 120, 220, 50 } +#define COLOR_WALL_LINE CLITERAL(Color){ 255, 180, 50, 180 } +#define COLOR_HP_GREEN CLITERAL(Color){ 50, 200, 50, 255 } +#define COLOR_HP_RED CLITERAL(Color){ 200, 50, 50, 255 } +#define COLOR_HP_BG CLITERAL(Color){ 40, 40, 40, 200 } +#define COLOR_SPEC_BAR CLITERAL(Color){ 230, 170, 30, 255 } +#define COLOR_TEXT CLITERAL(Color){ 200, 200, 210, 255 } +#define COLOR_TEXT_DIM CLITERAL(Color){ 130, 130, 140, 255 } +#define COLOR_LABEL CLITERAL(Color){ 170, 170, 180, 255 } + +#define MAX_FLIGHT_PROJECTILES 64 +#define RENDER_CLIENT_TICK_SECONDS 0.020 +#define RENDER_DEFAULT_GAME_TICKS_PER_SECOND (1.0f / 0.6f) +#define RENDER_MAX_VISUAL_GAME_TICKS_PER_SECOND 50.0f +#define PROJ_OSRS_SLOPE_TO_RAD 0.02454369f + +typedef struct { + int active; + float src_x, src_y; + float dst_x, dst_y; + float x, y; + float progress; + float speed; + float start_height; + float end_height; + float curve; + int style; + int damage; + + float vel_x, vel_y; + float height_vel; + float height_accel; + float yaw; + float pitch; + float arc_height; + int tracks_target; + int source_kind; + int source_npc_slot; + int target_kind; + int target_npc_slot; + int start_delay; + int motion_mode; + float offset_x, offset_y, offset_z; + uint32_t model_id; + int anim_id; + int travel_gfx_id; + int travel_gfx_drives_model; + int grow; + AnimPlayback anim_playback; + AnimModelState* anim_state; + int launch_gfx_id; + int launch_spawned; + int impact_gfx_id; +} FlightProjectile; + +#define COMPOSITE_MAX_BASE_VERTS 12000 +#define COMPOSITE_MAX_FACES 8000 +#define COMPOSITE_MAX_EXP_VERTS (COMPOSITE_MAX_FACES * 3) + +typedef struct { + int16_t base_vertices[COMPOSITE_MAX_BASE_VERTS * 3]; + uint8_t vertex_skins[COMPOSITE_MAX_BASE_VERTS]; + uint16_t face_indices[COMPOSITE_MAX_FACES * 3]; + int base_vert_count; + int face_count; + + uint8_t base_face_alphas[COMPOSITE_MAX_FACES]; + uint8_t face_alpha_labels[COMPOSITE_MAX_FACES]; + int has_face_alpha; + + Mesh mesh; + Model model; + int gpu_ready; + + AnimModelState* anim_state; + + uint8_t last_equipped[NUM_GEAR_SLOTS]; + int last_npc_def_id; + int needs_rebuild; +} PlayerComposite; + +#define HULL_MAX_POINTS 512 +#define RENDER_CLICK_HULL_MAX_INPUT_POINTS \ + (COMPOSITE_MAX_EXP_VERTS + RENDER_CLICKBOX_PRISM_POINT_COUNT) + +typedef struct { + int xs[HULL_MAX_POINTS]; + int ys[HULL_MAX_POINTS]; + int count; +} ConvexHull2D; + +static void hull_compute(const int* xs, const int* ys, int n, ConvexHull2D* out) { + out->count = 0; + if (n < 3) return; + + int left = 0; + for (int i = 1; i < n; i++) { + if (xs[i] < xs[left] || (xs[i] == xs[left] && ys[i] < ys[left])) + left = i; + } + + int current = left; + do { + int cx = xs[current], cy = ys[current]; + if (out->count >= HULL_MAX_POINTS) { out->count = 0; return; } + out->xs[out->count] = cx; + out->ys[out->count] = cy; + out->count++; + + if (out->count > n) { out->count = 0; return; } + + int next = 0; + int nx = xs[0], ny = ys[0]; + for (int i = 1; i < n; i++) { + long long cp = (long long)(ys[i] - cy) * (nx - xs[i]) + - (long long)(xs[i] - cx) * (ny - ys[i]); + if (cp > 0) { + next = i; nx = xs[i]; ny = ys[i]; + } else if (cp == 0) { + long long d_i = (long long)(cx - xs[i]) * (cx - xs[i]) + + (long long)(cy - ys[i]) * (cy - ys[i]); + long long d_n = (long long)(cx - nx) * (cx - nx) + + (long long)(cy - ny) * (cy - ny); + if (d_i > d_n) { next = i; nx = xs[i]; ny = ys[i]; } + } + } + current = next; + } while (current != left); +} + +static int hull_contains(const ConvexHull2D* hull, int px, int py) { + if (hull->count < 3) return 0; + int inside = 0; + for (int i = 0, j = hull->count - 1; i < hull->count; j = i++) { + int xi = hull->xs[i], yi = hull->ys[i]; + int xj = hull->xs[j], yj = hull->ys[j]; + if (((yi > py) != (yj > py)) && + (px < (xj - xi) * (py - yi) / (yj - yi) + xi)) + inside = !inside; + } + return inside; +} + +static void hull_append_projected_world_point( + const Camera3D* cam, + Vector3 world_point, + int* xs, + int* ys, + int* count, + int capacity +) { + if (*count >= capacity) return; + Vector2 sv = GetWorldToScreen(world_point, *cam); + if (sv.x < -1000 || sv.x > 5000 || sv.y < -1000 || sv.y > 5000) return; + xs[*count] = (int)sv.x; + ys[*count] = (int)sv.y; + (*count)++; +} + +typedef struct { + int active; + int damage; + int type; + double hitmark_move; + int hitmark_trans; + int ticks_remaining; +} HitSplat; + +typedef struct { + AnimPlayback primary; + AnimPlayback secondary; +} RenderAnimationState; + +typedef struct { + RenderAnimationState anim; + int primary_event_tick; + int last_primary_event_tick; + float sub_x; + float sub_y; + float dest_x; + float dest_y; + OsrsRenderWaypointQueue waypoints; + int visual_moving; + int visual_running; + float visual_effective_speed; + int step_tracker; + float yaw; + float target_yaw; + int facing_opponent; + int hp_bar_visible_until; + HitSplat splats[RENDER_SPLATS_PER_PLAYER]; +} RenderVisualSlotSnapshot; + +#define CONTEXT_MENU_MAX_ITEMS 64 +#define CONTEXT_MENU_ROW_H 20 +#define CONTEXT_MENU_ITEM_TOP 22 +#define CONTEXT_MENU_ITEM_H 18 +#define CONTEXT_MENU_TITLE_H 24 +#define CONTEXT_MENU_PADDING 4 +#define CONTEXT_MENU_MIN_W 158 + +typedef enum { + CMENU_ACTION_NONE = 0, + CMENU_ACTION_WALK_HERE, + CMENU_ACTION_ATTACK, + CMENU_ACTION_LAB_SELECT_NPC, + CMENU_ACTION_LAB_MOVE_SELECTED_NPC, + CMENU_ACTION_LAB_PLACE_PLAYER, + CMENU_ACTION_LAB_SPAWN_NPC, + CMENU_ACTION_LAB_KILL_NPC, + CMENU_ACTION_LAB_DELETE_NPC, + CMENU_ACTION_LAB_TOGGLE_PILLAR, + CMENU_ACTION_GUI_INVENTORY_PRIMARY, + CMENU_ACTION_GUI_INVENTORY_USE, + CMENU_ACTION_GUI_PRAYER, + CMENU_ACTION_GUI_SPELL, + CMENU_ACTION_GUI_COMBAT_STYLE, + CMENU_ACTION_GUI_AUTOCAST, + CMENU_ACTION_GUI_SPEC_TOGGLE, + CMENU_ACTION_CANCEL, +} ContextMenuAction; + +typedef struct { + ContextMenuAction action; + int entity_idx; + int npc_type; + int pillar_idx; + int inventory_slot; + int prayer_idx; + int spell_idx; + int fight_style; + int autocast_spell; + int autocast_defensive; + char label[64]; +} ContextMenuItem; + +typedef struct { + int visible; + int screen_x, screen_y; + int width; + int item_count; + ContextMenuItem items[CONTEXT_MENU_MAX_ITEMS]; + int walk_tile_x, walk_tile_y; + int click_screen_x, click_screen_y; + int hover_idx; +} ContextMenu; + +static int context_menu_height(const ContextMenu* cm) { + return CONTEXT_MENU_TITLE_H + cm->item_count * CONTEXT_MENU_ROW_H; +} + +static int context_menu_row_at(const ContextMenu* cm, int mx, int my) { + int menu_h = context_menu_height(cm); + if (mx < cm->screen_x || mx >= cm->screen_x + cm->width || + my < cm->screen_y || my >= cm->screen_y + menu_h) { + return -1; + } + if (my < cm->screen_y + CONTEXT_MENU_ITEM_TOP) return -1; + int row = (my - cm->screen_y - CONTEXT_MENU_ITEM_TOP) / CONTEXT_MENU_ROW_H; + if (row < 0 || row >= cm->item_count) return -1; + return row; +} + +static void context_menu_draw_text_shadow( + const GuiState* gs, + const char* text, + int x, + int y, + int size, + Color color +) { + gui_text_shadow(gs, text, x, y, size, color); +} + +typedef struct RenderClient { + int is_paused; + float ticks_per_second; + int step_once; + int step_back; + + int show_collision; + int show_pathfinding; + int show_models; + int show_safe_spots; + int show_debug; + int lab_enabled; + int lab_show_forecast; + int lab_selected_npc_slot; + int lab_prev_paused; + int lab_prev_human_enabled; + void (*pre_sim_mutation_hook)(void* ctx); + void* pre_sim_mutation_hook_ctx; + void* lab_entry_snapshot; + size_t lab_entry_snapshot_size; + int lab_restore_requested; + int lab_restore_generation; + + int layout_mode; + + ModelCache* model_cache; + AnimCache* anim_cache; + ModelCache* projectile_model_cache; + AnimCache* projectile_anim_cache; + OsrsSpotAnimSet* spotanims; + ModelCache* npc_model_cache; + AnimCache* npc_anim_cache; + float model_scale; + + Texture2D prayer_icons[6]; + int prayer_icons_loaded; + + Texture2D colosseum_modifier_icons[COLO_NUM_REAL_MODIFIERS][3]; + + Texture2D hitmark_sprites[5]; + int hitmark_sprites_loaded; + + Texture2D click_cross_sprites[8]; + int click_cross_loaded; + + int debug_hit_wx, debug_hit_wy; + float debug_ray_hit_x, debug_ray_hit_y, debug_ray_hit_z; + int debug_plane_wx, debug_plane_wy; + Vector3 debug_ray_origin, debug_ray_dir; + + RenderEntity entities[MAX_RENDER_ENTITIES]; + int entity_count; + + PlayerComposite composites[MAX_RENDER_ENTITIES]; + + ConvexHull2D entity_hulls[MAX_RENDER_ENTITIES]; + float entity_visual_top_y[MAX_RENDER_ENTITIES]; + float entity_visual_mid_y[MAX_RENDER_ENTITIES]; + + RenderAnimationState anim[MAX_RENDER_ENTITIES]; + int primary_event_tick[MAX_RENDER_ENTITIES]; + int last_primary_event_tick[MAX_RENDER_ENTITIES]; + + int prev_npc_slot[MAX_RENDER_ENTITIES]; + int prev_entity_count; + + TerrainMesh* terrain; + + ObjectMesh* objects; + ObjectMesh* objects_zuk; + int zuk_active; + + ObjectMesh* npcs; + + float cam_yaw; + float cam_pitch; + float cam_dist; + float cam_target_x; + float cam_target_z; + + float zoom; + + HitSplat splats[MAX_RENDER_ENTITIES][RENDER_SPLATS_PER_PLAYER]; + + float sub_x[MAX_RENDER_ENTITIES], sub_y[MAX_RENDER_ENTITIES]; + float dest_x[MAX_RENDER_ENTITIES], dest_y[MAX_RENDER_ENTITIES]; + OsrsRenderWaypointQueue waypoints[MAX_RENDER_ENTITIES]; + int visual_moving[MAX_RENDER_ENTITIES]; + int visual_running[MAX_RENDER_ENTITIES]; + float visual_effective_speed[MAX_RENDER_ENTITIES]; + int step_tracker[MAX_RENDER_ENTITIES]; + float yaw[MAX_RENDER_ENTITIES]; + float target_yaw[MAX_RENDER_ENTITIES]; + int facing_opponent[MAX_RENDER_ENTITIES]; + + int hp_bar_visible_until[MAX_RENDER_ENTITIES]; + + ActiveEffect effects[MAX_ACTIVE_EFFECTS]; + int effect_client_tick_counter; + int prev_sol_aoe_age; + + double client_tick_accumulator; + + int arena_base_x, arena_base_y; + int arena_width, arena_height; + + EncounterOverlay encounter_overlay; + + Model cloud_model; int cloud_model_ready; + Model snakeling_model; int snakeling_model_ready; + Model ranged_proj_model; int ranged_proj_model_ready; + Model magic_proj_model; int magic_proj_model_ready; + Model cloud_proj_model; int cloud_proj_model_ready; + Model pillar_models[4]; int pillar_models_ready; + + FlightProjectile flights[MAX_FLIGHT_PROJECTILES]; + +#define MAX_PROJ_MODELS 24 + struct { uint32_t id; Model model; int ready; } proj_models[MAX_PROJ_MODELS]; + int proj_model_count; + +#define MAX_EFFECT_ANIM_STATES 8 + struct { + uint32_t model_id; + int anim_id; + AnimModelState* state; + } effect_anim_states[MAX_EFFECT_ANIM_STATES]; + int effect_anim_state_count; + + const CollisionMap* collision_map; + int collision_world_offset_x; + int collision_world_offset_y; + + double last_tick_time; + + OsrsEnv* history; + int history_count; + int history_capacity; + int history_cursor; + + GuiState gui; + RenderTexture2D minimap_surface; + int minimap_surface_w; + int minimap_surface_h; + + HumanInput human_input; + + int hover_tile_x, hover_tile_y; + + ContextMenu context_menu; +} RenderClient; + +static Camera3D render_build_3d_camera(RenderClient* rc); +static void render_populate_entities(RenderClient* rc, OsrsEnv* env); +static void render_seed_entity_visual_slot(RenderClient* rc, int i); +static void render_reset_episode_visual_state(RenderClient* rc, OsrsEnv* env); +static void context_menu_dismiss(ContextMenu* cm); +static inline int render_world_to_screen_x_rc(RenderClient* rc, int world_x); +static inline int render_world_to_screen_y_rc(RenderClient* rc, int world_y); + +static float render_effective_ticks_per_second(RenderClient* rc) { + if (!rc) return RENDER_DEFAULT_GAME_TICKS_PER_SECOND; + if (rc->ticks_per_second > 0.0f) return rc->ticks_per_second; + return RENDER_MAX_VISUAL_GAME_TICKS_PER_SECOND; +} + +static double render_scaled_frame_dt(RenderClient* rc, double frame_dt) { + float tps = render_effective_ticks_per_second(rc); + return frame_dt * (double)(tps / RENDER_DEFAULT_GAME_TICKS_PER_SECOND); +} + +static int render_offhand_uses_shield_block_anim(uint8_t item_idx) { + switch (item_idx) { + case ITEM_DRAGON_DEFENDER: + case ITEM_AVERNIC_DEFENDER: + case ITEM_MAGES_BOOK: + case ITEM_BOOK_OF_DARKNESS: + case ITEM_ELIDINIS_WARD_F: + return 0; + case ITEM_SPIRIT_SHIELD: + case ITEM_BLESSED_SPIRIT_SHIELD: + case ITEM_SPECTRAL_SPIRIT_SHIELD: + case ITEM_ELYSIAN_SPIRIT_SHIELD: + case ITEM_CRYSTAL_SHIELD: + case ITEM_DRAGONFIRE_SHIELD: + return 1; + default: + return item_idx < NUM_ITEMS && ITEM_DATABASE[item_idx].slot == SLOT_SHIELD; + } +} + +static int render_projectile_profile_value_or(int value, int fallback) { + return osrs_combat_projectile_value_or(value, fallback); +} + +static int render_spawn_profile_projectile( + RenderClient* rc, + const OsrsCombatProjectileProfile* profile, + int src_x, int src_y, int dst_x, int dst_y, + int delay_client_ticks, int duration_client_ticks, + int fallback_start_height, int fallback_end_height, + int fallback_slope +) { + if (!profile) return -1; + if (profile->travel_spotanim_id < 0) return -1; + return effect_spawn_projectile( + rc->effects, profile->travel_spotanim_id, + src_x, src_y, dst_x, dst_y, + delay_client_ticks, duration_client_ticks, + render_projectile_profile_value_or( + profile->projectile_start_height, fallback_start_height), + render_projectile_profile_value_or( + profile->projectile_end_height, fallback_end_height), + render_projectile_profile_value_or( + profile->projectile_angle, fallback_slope), + rc->effect_client_tick_counter, + rc->spotanims, rc->model_cache, rc->npc_model_cache, + rc->projectile_model_cache); +} + +static Player* render_get_player_ptr(OsrsEnv* env, int index) { + if (env->encounter_def && env->encounter_state) { + const EncounterDef* def = (const EncounterDef*)env->encounter_def; + return (Player*)def->get_entity( + (EncounterState*)env->encounter_state, + (EncounterContext*)env->encounter_context, + index); + } + if (index >= 0 && index < NUM_AGENTS) + return &env->players[index]; + return NULL; +} + +static int anim_sequence_is_empty_stub(const AnimSequence* seq) { + if (!seq || seq->frame_count != 1) return 0; + const AnimSequenceFrame* f = &seq->frames[0]; + return f->frame.kind == ANIM_FRAME_LEGACY && f->frame.framebase_id == 0xFFFF; +} + +static int anim_sequence_maya_vert_count(const AnimSequence* seq) { + if (!seq || seq->frame_count == 0) return 0; + if (seq->frames[0].frame.kind != ANIM_FRAME_MAYA_BAKED) return 0; + return seq->frames[0].frame.maya_vertex_count; +} + +static AnimSequence* render_get_anim_sequence(RenderClient* rc, uint16_t seq_id) { + AnimCache* caches[3] = { + rc->anim_cache, rc->npc_anim_cache, rc->projectile_anim_cache + }; + AnimSequence* stub = NULL; + for (int i = 0; i < 3; i++) { + if (!caches[i]) continue; + AnimSequence* seq = anim_get_sequence(caches[i], seq_id); + if (!seq) continue; + if (anim_sequence_is_empty_stub(seq)) { + if (!stub) stub = seq; + continue; + } + return seq; + } + return stub; +} + +static AnimSequence* render_get_anim_sequence_for_model( + RenderClient* rc, uint16_t seq_id, int model_vert_count +) { + AnimCache* caches[3] = { + rc->anim_cache, rc->npc_anim_cache, rc->projectile_anim_cache + }; + AnimSequence* mismatched_maya = NULL; + for (int i = 0; i < 3; i++) { + if (!caches[i]) continue; + AnimSequence* seq = anim_get_sequence(caches[i], seq_id); + if (!seq || anim_sequence_is_empty_stub(seq)) continue; + int maya_vc = anim_sequence_maya_vert_count(seq); + if (maya_vc > 0 && maya_vc != model_vert_count) { + if (!mismatched_maya) mismatched_maya = seq; + continue; + } + return seq; + } + if (mismatched_maya) return mismatched_maya; + return render_get_anim_sequence(rc, seq_id); +} + +static int render_sequence_stalls_movement(const AnimSequence* seq) { + if (!seq) return 0; + if (seq->walk_flag >= 0) return seq->walk_flag == 0; + return seq->interleave_count == 0; +} + +static AnimFrameBase* render_get_framebase(RenderClient* rc, uint16_t base_id) { + AnimFrameBase* fb = NULL; + if (rc->anim_cache) fb = anim_get_framebase(rc->anim_cache, base_id); + if (!fb && rc->npc_anim_cache) fb = anim_get_framebase(rc->npc_anim_cache, base_id); + if (!fb && rc->projectile_anim_cache) + fb = anim_get_framebase(rc->projectile_anim_cache, base_id); + return fb; +} + +typedef struct { + AnimSequenceFrame* sequence_frame; + AnimFrameBase* framebase; +} RenderAnimTrackFrame; + +static RenderAnimTrackFrame render_resolve_anim_track_frame( + RenderClient* rc, + AnimSequence* seq, + int frame_idx +) { + RenderAnimTrackFrame out = {0}; + if (!seq || seq->frame_count <= 0) return out; + int fidx = frame_idx % seq->frame_count; + AnimSequenceFrame* sf = &seq->frames[fidx]; + if (sf->frame.kind == ANIM_FRAME_LEGACY) { + if (sf->frame.framebase_id != 0xFFFF) { + AnimFrameBase* fb = render_get_framebase(rc, sf->frame.framebase_id); + if (fb) { + out.sequence_frame = sf; + out.framebase = fb; + } + } + } else if (sf->frame.kind == ANIM_FRAME_MAYA_BAKED) { + if (!sf->frame.maya_vertices || sf->frame.maya_vertex_count == 0) { + fprintf(stderr, + "render: Maya baked animation frame missing vertices in sequence %u\n", + seq->seq_id); + abort(); + } + out.sequence_frame = sf; + } else { + fprintf(stderr, "render: unknown animation frame kind %u in sequence %u\n", + sf->frame.kind, seq->seq_id); + abort(); + } + return out; +} + +static inline void render_anim_playback_resolve( + RenderClient* rc, AnimPlayback* pb, int model_vert_count +) { + if (pb->seq_id < 0) { pb->sequence = NULL; return; } + if (pb->sequence && pb->model_vert_count == model_vert_count) return; + pb->sequence = render_get_anim_sequence_for_model( + rc, (uint16_t)pb->seq_id, model_vert_count); + pb->model_vert_count = model_vert_count; +} + +static inline RenderAnimTrackFrame render_anim_playback_frame( + RenderClient* rc, const AnimPlayback* pb +) { + return render_resolve_anim_track_frame(rc, pb->sequence, pb->frame_idx); +} + +static void render_apply_anim_sequence_frame_to_model_state( + RenderClient* rc, + AnimModelState* anim_state, + OsrsModel* om, + AnimSequence* seq, + int frame_idx, + const char* context +) { + if (!anim_state || !om || !seq || seq->frame_count <= 0) { + fprintf(stderr, "render: %s animation state is incomplete\n", context); + abort(); + } + AnimSequenceFrame* sf = &seq->frames[frame_idx % seq->frame_count]; + if (sf->frame.kind == ANIM_FRAME_LEGACY) { + if (sf->frame.framebase_id == 0xFFFF) { + fprintf(stderr, "render: %s legacy animation frame has no framebase\n", + context); + abort(); + } + AnimFrameBase* fb = render_get_framebase(rc, sf->frame.framebase_id); + if (!fb) { + fprintf(stderr, "render: %s legacy animation framebase %u is missing\n", + context, sf->frame.framebase_id); + abort(); + } + anim_apply_frame(anim_state, om->base_vertices, &sf->frame, fb); + } else if (sf->frame.kind == ANIM_FRAME_MAYA_BAKED) { + anim_apply_maya_baked_frame(anim_state, &sf->frame); + } else { + fprintf(stderr, "render: %s unknown animation frame kind %u in sequence %u\n", + context, sf->frame.kind, seq->seq_id); + abort(); + } + anim_update_mesh(om->mesh.vertices, anim_state, + om->face_indices, om->mesh.triangleCount); + UpdateMeshBuffer(om->mesh, 0, om->mesh.vertices, + om->mesh.triangleCount * 9 * sizeof(float), 0); + anim_update_mesh_alpha(om->mesh.colors, anim_state, + om->mesh.triangleCount); + if (anim_state->face_alphas) { + UpdateMeshBuffer(om->mesh, 3, om->mesh.colors, + om->mesh.vertexCount * 4, 0); + } +} + +static InfernoState* render_inferno_state_from_env(OsrsEnv* env) { + if (!env || !env->encounter_def || !env->encounter_state) return NULL; + const EncounterDef* def = (const EncounterDef*)env->encounter_def; + if (strcmp(def->name, "inferno") != 0) return NULL; + return (InfernoState*)env->encounter_state; +} + +static InfernoState* render_inferno_state_from_client(RenderClient* rc) { + if (!rc || !rc->gui.encounter_def || !rc->gui.encounter_state) return NULL; + const EncounterDef* def = (const EncounterDef*)rc->gui.encounter_def; + if (strcmp(def->name, "inferno") != 0) return NULL; + return (InfernoState*)rc->gui.encounter_state; +} + +static ColosseumState* render_colosseum_state_from_env(OsrsEnv* env) { + if (!env || !env->encounter_def || !env->encounter_state) return NULL; + const EncounterDef* def = (const EncounterDef*)env->encounter_def; + if (strcmp(def->name, "colosseum") != 0) return NULL; + return (ColosseumState*)env->encounter_state; +} + +static Color render_inferno_lab_forecast_color( + const InfStepOutForecastAction* action +) { + if (!action->valid) return (Color){ 120, 120, 120, 120 }; + if (action->melee_fallback_exposure) return (Color){ 190, 80, 255, 190 }; + if (action->same_tick_mixed_style_conflict) return (Color){ 255, 60, 60, 190 }; + if (action->ranger_mager_offtick_opportunity) return (Color){ 70, 220, 255, 190 }; + for (int t = 0; t < INF_STEP_OUT_FORECAST_HORIZON; t++) { + if (inf_step_out_forecast_tick_has_event(&action->ticks[t])) + return (Color){ 255, 170, 40, 180 }; + } + return (Color){ 60, 220, 80, 170 }; +} + +static void render_inferno_lab_draw_forecast_3d(RenderClient* rc) { + InfernoState* s = render_inferno_state_from_client(rc); + if (!s || !rc->lab_enabled || !rc->lab_show_forecast) return; + + InfStepOutForecast forecast; + inf_build_step_out_forecast(s, &forecast); + int has_terrain = rc->terrain && rc->terrain->loaded; + for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { + const InfStepOutForecastAction* action = &forecast.actions[action_idx]; + float ground = has_terrain + ? terrain_height_avg(rc->terrain, action->land_x, action->land_y) + : 2.0f; + float fx = (float)action->land_x + 0.5f; + float fz = -(float)(action->land_y + 1) + 0.5f; + Color color = render_inferno_lab_forecast_color(action); + DrawCube((Vector3){ fx, ground + 0.06f, fz }, + 0.92f, 0.04f, 0.92f, color); + Color wire = color; + wire.a = 255; + DrawCubeWires((Vector3){ fx, ground + 0.09f, fz }, + 0.94f, 0.04f, 0.94f, wire); + } +} + +static void render_lab_snap_entity_visual(RenderClient* rc, int entity_idx) { + if (entity_idx < 0 || entity_idx >= rc->entity_count) return; + render_seed_entity_visual_slot(rc, entity_idx); + rc->visual_moving[entity_idx] = 0; + rc->visual_running[entity_idx] = 0; + rc->visual_effective_speed[entity_idx] = 0.0f; + rc->step_tracker[entity_idx] = 0; +} + +static void render_lab_snap_all_visuals(RenderClient* rc) { + for (int i = 0; i < rc->entity_count; i++) + render_lab_snap_entity_visual(rc, i); + rc->prev_entity_count = rc->entity_count; +} + +static int render_lab_line_command_is(const char* line, const char* command) { + while (*line && isspace((unsigned char)*line)) line++; + size_t command_len = strlen(command); + return strncmp(line, command, command_len) == 0 && + (line[command_len] == '\0' || + isspace((unsigned char)line[command_len])); +} + +static int render_lab_line_slot_value(const char* line, int* slot) { + const char* token = strstr(line, "slot="); + if (!token) return 0; + char* end = NULL; + long value = strtol(token + 5, &end, 10); + if (end == token + 5 || value < 0 || value > INT_MAX) return 0; + *slot = (int)value; + return 1; +} + +static int render_npc_entity_idx_for_slot(RenderClient* rc, int slot) { + for (int i = 0; i < rc->entity_count; i++) { + if (rc->entities[i].entity_type == ENTITY_NPC && + rc->entities[i].npc_slot == slot) + return i; + } + return -1; +} + +static void render_lab_snap_line_visuals(RenderClient* rc, const char* line) { + if (render_lab_line_command_is(line, "player") || + render_lab_line_command_is(line, "set_player")) { + render_lab_snap_entity_visual(rc, 0); + return; + } + if (render_lab_line_command_is(line, "move_npc")) { + int slot = -1; + if (render_lab_line_slot_value(line, &slot)) { + int entity_idx = render_npc_entity_idx_for_slot(rc, slot); + if (entity_idx >= 0) { + render_lab_snap_entity_visual(rc, entity_idx); + return; + } + } + } + if (render_lab_line_command_is(line, "npc") || + render_lab_line_command_is(line, "spawn_npc") || + render_lab_line_command_is(line, "reset") || + render_lab_line_command_is(line, "wave") || + render_lab_line_command_is(line, "spawn_wave") || + render_lab_line_command_is(line, "delete_npc") || + render_lab_line_command_is(line, "clear_npcs")) { + render_lab_snap_all_visuals(rc); + } +} + +static const EncounterDef* render_lab_def(OsrsEnv* env) { + if (!env || !env->encounter_def || !env->encounter_state) return NULL; + const EncounterDef* def = (const EncounterDef*)env->encounter_def; + if (!def->apply_lab_command || + !def->snapshot_size || !def->snapshot || !def->restore) + return NULL; + return def; +} + +static void render_lab_apply_line(RenderClient* rc, OsrsEnv* env, const char* line) { + const EncounterDef* def = render_lab_def(env); + if (!def) return; + (void)def->apply_lab_command( + (EncounterState*)env->encounter_state, + (EncounterContext*)env->encounter_context, + line); + render_populate_entities(rc, env); + if (rc->lab_selected_npc_slot >= 0) { + int still_active = 0; + for (int i = 0; i < rc->entity_count; i++) { + if (rc->entities[i].entity_type == ENTITY_NPC && + rc->entities[i].npc_slot == rc->lab_selected_npc_slot) { + still_active = 1; + break; + } + } + if (!still_active) rc->lab_selected_npc_slot = -1; + } + render_lab_snap_line_visuals(rc, line); +} + +static void render_lab_clear_entry_snapshot(RenderClient* rc) { + if (!rc) return; + free(rc->lab_entry_snapshot); + rc->lab_entry_snapshot = NULL; + rc->lab_entry_snapshot_size = 0; +} + +static void render_lab_capture_entry_snapshot(RenderClient* rc, OsrsEnv* env) { + const EncounterDef* def = render_lab_def(env); + if (!def) return; + size_t size = def->snapshot_size( + (EncounterState*)env->encounter_state, + (EncounterContext*)env->encounter_context); + void* snapshot = malloc(size); + if (!snapshot) { + fprintf(stderr, "lab: snapshot allocation failed\n"); + abort(); + } + def->snapshot( + (EncounterState*)env->encounter_state, + (EncounterContext*)env->encounter_context, + snapshot); + + render_lab_clear_entry_snapshot(rc); + rc->lab_entry_snapshot = snapshot; + rc->lab_entry_snapshot_size = size; +} + +static void render_lab_restore_controls(RenderClient* rc) { + rc->lab_enabled = 0; + rc->lab_show_forecast = 0; + rc->lab_selected_npc_slot = -1; + rc->is_paused = rc->lab_prev_paused; + rc->human_input.enabled = rc->lab_prev_human_enabled; + human_input_clear_pending(&rc->human_input); + human_input_clear_move(&rc->human_input); + human_input_clear_selected_ui_target(&rc->human_input); + context_menu_dismiss(&rc->context_menu); +} + +static int render_lab_restore_entry_snapshot(RenderClient* rc, OsrsEnv* env) { + if (!rc || !rc->lab_entry_snapshot) return 0; + const EncounterDef* def = render_lab_def(env); + if (!def) return 0; + if (rc->pre_sim_mutation_hook) + rc->pre_sim_mutation_hook(rc->pre_sim_mutation_hook_ctx); + def->restore( + (EncounterState*)env->encounter_state, + (EncounterContext*)env->encounter_context, + rc->lab_entry_snapshot, + rc->lab_entry_snapshot_size); + if (def->get_tick) { + env->tick = def->get_tick( + (EncounterState*)env->encounter_state, + (EncounterContext*)env->encounter_context); + } + rc->lab_restore_requested = 1; + rc->lab_restore_generation++; + render_lab_restore_controls(rc); + render_reset_episode_visual_state(rc, env); + fprintf(stderr, "lab: restored entry snapshot\n"); + return 1; +} + +static inline int render_world_to_screen_x_rc(RenderClient* rc, int world_x) { + return (world_x - rc->arena_base_x) * RENDER_TILE_SIZE; +} + +static inline int render_world_to_screen_y_rc(RenderClient* rc, int world_y) { + int local_y = world_y - rc->arena_base_y; + int flipped = (rc->arena_height - 1) - local_y; + return RENDER_HEADER_HEIGHT + flipped * RENDER_TILE_SIZE; +} + +static void composite_free(PlayerComposite* comp); +static int render_select_secondary(RenderClient* rc, int player_idx); + +static const char* inferno_npc_name(int npc_def_id); + +static const char* colosseum_npc_name(int npc_def_id) { + switch (npc_def_id) { + case 12810: return "Jaguar Warrior"; + case 12811: return "Serpent Shaman"; + case 12812: return "Minotaur"; + case 12813: return "Minotaur"; + case 12814: return "Fremennik Archer"; + case 12815: return "Fremennik Seer"; + case 12816: return "Fremennik Berserker"; + case 12817: return "Javelin Colossus"; + case 12818: return "Manticore"; + case 12819: return "Shockwave Colossus"; + case 12821: return "Sol Heredit"; + case 12823: return "Bee Swarm"; + case 12825: return "Healing Totem"; + default: return NULL; + } +} + +static const char* render_entity_display_name(RenderEntity* ent) { + if (ent->entity_type == ENTITY_PLAYER) return "Player"; + + if (ent->npc_def_id == 2042) return "Zulrah"; + if (ent->npc_def_id == 2043) return "Zulrah"; + if (ent->npc_def_id == 2044) return "Zulrah"; + + const char* inf = inferno_npc_name(ent->npc_def_id); + if (inf) return inf; + + const char* colo = colosseum_npc_name(ent->npc_def_id); + if (colo) return colo; + + return TextFormat("NPC %d", ent->npc_def_id); +} + +typedef struct { + RenderClient* rc; + OsrsEnv* env; +} RenderHumanAttackCtx; + +static int render_can_human_attack_entity( + void* ctx, const RenderEntity* entity, int entity_idx, int gui_entity_idx +) { + RenderHumanAttackCtx* attack_ctx = (RenderHumanAttackCtx*)ctx; + if (entity_idx == gui_entity_idx && entity->entity_type == ENTITY_PLAYER) { + return 0; + } + + if (entity->entity_type == ENTITY_NPC && !entity->npc_visible) { + return 0; + } + + if (attack_ctx->env->encounter_def && attack_ctx->env->encounter_state) { + const EncounterDef* def = (const EncounterDef*)attack_ctx->env->encounter_def; + if (entity->entity_type == ENTITY_NPC && def->is_human_targetable_npc_slot) { + return def->is_human_targetable_npc_slot( + (EncounterState*)attack_ctx->env->encounter_state, + (EncounterContext*)attack_ctx->env->encounter_context, + entity->npc_slot); + } + } + + return 1; +} + +static void context_menu_dismiss(ContextMenu* cm) { + cm->visible = 0; + cm->item_count = 0; + cm->hover_idx = -1; +} + +static ContextMenuItem* context_menu_add( + ContextMenu* cm, ContextMenuAction action, int entity_idx, const char* label +) { + if (cm->item_count >= CONTEXT_MENU_MAX_ITEMS) return NULL; + ContextMenuItem* item = &cm->items[cm->item_count++]; + item->action = action; + item->entity_idx = entity_idx; + item->npc_type = -1; + item->pillar_idx = -1; + item->inventory_slot = -1; + item->prayer_idx = -1; + item->spell_idx = -1; + item->fight_style = -1; + item->autocast_spell = -1; + item->autocast_defensive = 0; + snprintf(item->label, sizeof(item->label), "%s", label); + return item; +} + +static void context_menu_add_lab_npc( + ContextMenu* cm, int npc_type, const char* label +) { + ContextMenuItem* item = context_menu_add( + cm, CMENU_ACTION_LAB_SPAWN_NPC, -1, label); + if (!item) return; + item->npc_type = npc_type; +} + +static int render_lab_selected_npc_entity_idx(RenderClient* rc) { + if (rc->lab_selected_npc_slot < 0) return -1; + for (int i = 0; i < rc->entity_count; i++) { + if (rc->entities[i].entity_type == ENTITY_NPC && + rc->entities[i].npc_slot == rc->lab_selected_npc_slot) + return i; + } + return -1; +} + +static void render_lab_add_spawn_palette(ContextMenu* cm, const char* encounter_name) { + if (strcmp(encounter_name, "inferno") == 0) { + context_menu_add_lab_npc(cm, INF_NPC_RANGER, "Lab spawn ranger"); + context_menu_add_lab_npc(cm, INF_NPC_MAGER, "Lab spawn mager"); + context_menu_add_lab_npc(cm, INF_NPC_JAD, "Lab spawn jad"); + context_menu_add_lab_npc(cm, INF_NPC_ZUK, "Lab spawn Zuk"); + context_menu_add_lab_npc(cm, INF_NPC_BLOB, "Lab spawn blob"); + context_menu_add_lab_npc(cm, INF_NPC_MELEER, "Lab spawn meleer"); + context_menu_add_lab_npc(cm, INF_NPC_BAT, "Lab spawn bat"); + context_menu_add_lab_npc(cm, INF_NPC_NIBBLER, "Lab spawn nibbler"); + context_menu_add_lab_npc(cm, INF_NPC_HEALER_JAD, "Lab spawn Jad healer"); + context_menu_add_lab_npc(cm, INF_NPC_HEALER_ZUK, "Lab spawn Zuk healer"); + context_menu_add_lab_npc(cm, INF_NPC_ZUK_SHIELD, "Lab spawn Zuk shield"); + return; + } + if (strcmp(encounter_name, "colosseum") == 0) { + context_menu_add_lab_npc(cm, COLO_FREMENNIK_BERSERKER, "Lab spawn berserker"); + context_menu_add_lab_npc(cm, COLO_FREMENNIK_ARCHER, "Lab spawn archer"); + context_menu_add_lab_npc(cm, COLO_FREMENNIK_SEER, "Lab spawn seer"); + context_menu_add_lab_npc(cm, COLO_SERPENT_SHAMAN, "Lab spawn shaman"); + context_menu_add_lab_npc(cm, COLO_JAGUAR_WARRIOR, "Lab spawn jaguar"); + context_menu_add_lab_npc(cm, COLO_JAVELIN_COLOSSUS, "Lab spawn javelin"); + context_menu_add_lab_npc(cm, COLO_SHOCKWAVE_COLOSSUS, "Lab spawn shockwave"); + context_menu_add_lab_npc(cm, COLO_MINOTAUR, "Lab spawn minotaur"); + context_menu_add_lab_npc(cm, COLO_MANTICORE, "Lab spawn manticore"); + context_menu_add_lab_npc(cm, COLO_SOL_HEREDIT, "Lab spawn Sol Heredit"); + } +} + +static void context_menu_finish_layout(ContextMenu* cm, int mx, int my) { + int max_w = CONTEXT_MENU_MIN_W; + int title_w = MeasureText("Choose Option", 11) + CONTEXT_MENU_PADDING * 2 + 10; + if (title_w > max_w) max_w = title_w; + for (int i = 0; i < cm->item_count; i++) { + int w = MeasureText(cm->items[i].label, 11) + CONTEXT_MENU_PADDING * 2 + 12; + if (w > max_w) max_w = w; + } + cm->width = max_w; + cm->click_screen_x = mx; + cm->click_screen_y = my; + + int menu_h = context_menu_height(cm); + cm->screen_x = mx; + cm->screen_y = my; + if (cm->screen_x + cm->width > RENDER_WINDOW_W) + cm->screen_x = RENDER_WINDOW_W - cm->width; + if (cm->screen_y + menu_h > RENDER_WINDOW_H) + cm->screen_y = RENDER_WINDOW_H - menu_h; + if (cm->screen_x < 0) cm->screen_x = 0; + if (cm->screen_y < 0) cm->screen_y = 0; + + cm->visible = (cm->item_count > 0); +} + +static int render_pick_ground_tile( + RenderClient* rc, int screen_x, int screen_y, + int* out_wx, int* out_wy, Vector3* out_hit_point, Ray* out_ray +) { + *out_wx = -1; + *out_wy = -1; + Camera3D cam = render_build_3d_camera(rc); + Ray ray = GetScreenToWorldRay((Vector2){ (float)screen_x, (float)screen_y }, cam); + if (out_ray) *out_ray = ray; + float best_dist = 1e30f; + for (int dy = 0; dy < rc->arena_height; dy++) { + for (int dx = 0; dx < rc->arena_width; dx++) { + int wx = rc->arena_base_x + dx; + int wy = rc->arena_base_y + dy; + float tx = (float)wx; + float tz = -(float)(wy + 1); + float ground_y = rc->terrain + ? terrain_height_avg(rc->terrain, wx, wy) : 2.0f; + BoundingBox box = { + .min = { tx, ground_y - 0.1f, tz }, + .max = { tx + 1.0f, ground_y, tz + 1.0f }, + }; + RayCollision col = GetRayCollisionBox(ray, box); + if (col.hit && col.distance < best_dist) { + best_dist = col.distance; + *out_wx = wx; + *out_wy = wy; + if (out_hit_point) *out_hit_point = col.point; + } + } + } + return *out_wx >= 0; +} + +static void context_menu_build(RenderClient* rc, OsrsEnv* env, int mx, int my) { + ContextMenu* cm = &rc->context_menu; + RenderHumanAttackCtx attack_ctx = { .rc = rc, .env = env }; + const EncounterDef* lab_def = rc->lab_enabled ? render_lab_def(env) : NULL; + int lab_on = lab_def != NULL; + cm->item_count = 0; + cm->hover_idx = -1; + cm->walk_tile_x = -1; + cm->walk_tile_y = -1; + + int hit_entities[MAX_RENDER_ENTITIES]; + int hit_count = 0; + + for (int ei = 0; ei < rc->entity_count; ei++) { + RenderEntity* ent = &rc->entities[ei]; + int usable_entity = lab_on + ? (ent->entity_type == ENTITY_NPC && ent->npc_slot >= 0) + : render_can_human_attack_entity( + &attack_ctx, ent, ei, rc->gui.gui_entity_idx); + if (!usable_entity) { + continue; + } + if (hull_contains(&rc->entity_hulls[ei], mx, my)) { + if (hit_count < MAX_RENDER_ENTITIES) + hit_entities[hit_count++] = ei; + } + } + + render_pick_ground_tile(rc, mx, my, &cm->walk_tile_x, &cm->walk_tile_y, NULL, NULL); + + for (int i = 0; i < hit_count; i++) { + int ei = hit_entities[i]; + RenderEntity* ent = &rc->entities[ei]; + const char* name = render_entity_display_name(ent); + char label[64]; + if (lab_on) { + snprintf(label, sizeof(label), "Lab select %s", name); + context_menu_add(cm, CMENU_ACTION_LAB_SELECT_NPC, ei, label); + snprintf(label, sizeof(label), "Lab kill %s", name); + context_menu_add(cm, CMENU_ACTION_LAB_KILL_NPC, ei, label); + snprintf(label, sizeof(label), "Lab delete %s", name); + context_menu_add(cm, CMENU_ACTION_LAB_DELETE_NPC, ei, label); + } + if (!lab_on || render_can_human_attack_entity( + &attack_ctx, ent, ei, rc->gui.gui_entity_idx)) { + snprintf(label, sizeof(label), "Attack %s", name); + context_menu_add(cm, CMENU_ACTION_ATTACK, ei, label); + } + } + + if (lab_on && cm->walk_tile_x >= 0) { + context_menu_add(cm, CMENU_ACTION_LAB_PLACE_PLAYER, -1, "Lab place player"); + if (render_lab_selected_npc_entity_idx(rc) >= 0) + context_menu_add(cm, CMENU_ACTION_LAB_MOVE_SELECTED_NPC, -1, + "Lab move selected NPC"); + render_lab_add_spawn_palette(cm, lab_def->name); + if (strcmp(lab_def->name, "inferno") == 0) { + InfernoState* inf = render_inferno_state_from_env(env); + int pillar_idx = inf + ? inf_lab_nearest_pillar_idx(inf, cm->walk_tile_x, cm->walk_tile_y) : -1; + if (pillar_idx >= 0) { + ContextMenuItem* item = context_menu_add( + cm, CMENU_ACTION_LAB_TOGGLE_PILLAR, -1, + TextFormat("Lab toggle pillar %d", pillar_idx)); + if (item) item->pillar_idx = pillar_idx; + } + } + } + + if (cm->walk_tile_x >= 0) + context_menu_add(cm, CMENU_ACTION_WALK_HERE, -1, "Walk here"); + + context_menu_add(cm, CMENU_ACTION_CANCEL, -1, "Cancel"); + + context_menu_finish_layout(cm, mx, my); +} + +static void context_menu_build_gui( + RenderClient* rc, Player* p, int mx, int my, int layout_mx, int layout_my +) { + ContextMenu* cm = &rc->context_menu; + cm->item_count = 0; + cm->hover_idx = -1; + cm->walk_tile_x = -1; + cm->walk_tile_y = -1; + + switch (rc->gui.active_tab) { + case GUI_TAB_INVENTORY: { + int slot = gui_inv_slot_at(&rc->gui, mx, my); + if (slot >= 0) { + InvSlot* inv = &rc->gui.inv_grid[slot]; + const char* action = gui_inv_primary_action_label(inv); + const char* name = gui_inv_slot_display_name(inv); + if (action && name[0]) { + ContextMenuItem* item = context_menu_add( + cm, + CMENU_ACTION_GUI_INVENTORY_PRIMARY, + -1, + TextFormat("%s %s", action, name)); + if (item) item->inventory_slot = slot; + item = context_menu_add( + cm, + CMENU_ACTION_GUI_INVENTORY_USE, + -1, + TextFormat("Use %s", name)); + if (item) item->inventory_slot = slot; + } + } + break; + } + + case GUI_TAB_PRAYER: { + int idx = human_gui_prayer_idx_at(&rc->gui, mx, my); + if (idx >= 0) { + const char* name = human_gui_prayer_name((GuiPrayerIdx)idx); + if (name) { + int active = gui_prayer_is_active((GuiPrayerIdx)idx, p); + ContextMenuItem* item = context_menu_add( + cm, + CMENU_ACTION_GUI_PRAYER, + -1, + TextFormat("%s %s", active ? "Deactivate" : "Activate", name)); + if (item) item->prayer_idx = idx; + } + } + break; + } + + case GUI_TAB_SPELLBOOK: { + int idx = human_gui_spell_idx_at(&rc->gui, mx, my); + if (idx >= 0 && gui_spell_castable((GuiSpellIdx)idx)) { + const char* name = human_gui_spell_name((GuiSpellIdx)idx); + if (name) { + ContextMenuItem* item = context_menu_add( + cm, + CMENU_ACTION_GUI_SPELL, + -1, + TextFormat("Cast %s", name)); + if (item) item->spell_idx = idx; + } + } + break; + } + + case GUI_TAB_COMBAT: { + GuiCombatStyleOptions styles = gui_combat_style_options( + p->equipped[GEAR_SLOT_WEAPON]); + int style_idx = human_gui_combat_style_index_at(&rc->gui, p, mx, my); + if (style_idx >= 0 && style_idx < styles.count) { + ContextMenuItem* item = context_menu_add( + cm, + CMENU_ACTION_GUI_COMBAT_STYLE, + -1, + TextFormat("Select %s", styles.names[style_idx])); + if (item) item->fight_style = styles.values[style_idx]; + } + + if (human_gui_autocast_button_hit(&rc->gui, p, mx, my)) { + int defensive = p->fight_style == FIGHT_STYLE_DEFENSIVE_AUTOCAST || + p->autocast_defensive; + ContextMenuItem* blood = context_menu_add( + cm, CMENU_ACTION_GUI_AUTOCAST, -1, "Autocast Blood Barrage"); + if (blood) { + blood->autocast_spell = ENCOUNTER_SPELL_BLOOD; + blood->autocast_defensive = defensive; + } + ContextMenuItem* ice = context_menu_add( + cm, CMENU_ACTION_GUI_AUTOCAST, -1, "Autocast Ice Barrage"); + if (ice) { + ice->autocast_spell = ENCOUNTER_SPELL_ICE; + ice->autocast_defensive = defensive; + } + } else if (rc->gui.autocast_selector_open) { + int spell = human_gui_autocast_spell_at(&rc->gui, p, mx, my); + if (spell >= 0) { + int defensive = p->fight_style == FIGHT_STYLE_DEFENSIVE_AUTOCAST || + p->autocast_defensive; + ContextMenuItem* item = context_menu_add( + cm, + CMENU_ACTION_GUI_AUTOCAST, + -1, + TextFormat("Autocast %s", gui_autocast_spell_name(spell))); + if (item) { + item->autocast_spell = spell; + item->autocast_defensive = defensive; + } + } + } + + if (human_gui_spec_hit(&rc->gui, mx, my)) { + context_menu_add(cm, CMENU_ACTION_GUI_SPEC_TOGGLE, -1, + "Use Special Attack"); + } + break; + } + + default: + break; + } + + if (cm->item_count == 0) { + context_menu_dismiss(cm); + return; + } + + context_menu_add(cm, CMENU_ACTION_CANCEL, -1, "Cancel"); + context_menu_finish_layout(cm, layout_mx, layout_my); +} + +static void render_human_attack_npc_slot( + RenderClient* rc, int target_slot, int click_x, int click_y +) { + if (rc->human_input.cursor_mode == CURSOR_ITEM_TARGET) { + human_input_clear_selected_ui_target(&rc->human_input); + } else if (rc->human_input.cursor_mode == CURSOR_SPELL_TARGET) { + human_input_apply_ui_intent( + &rc->human_input, + osrs_ui_intent_spell_on_target( + rc->human_input.selected_spell, + rc->human_input.selected_spell_gui_idx, + target_slot)); + } else { + rc->human_input.pending_attack = 1; + rc->human_input.pending_target_idx = target_slot; + rc->human_input.pending_move_x = -1; + rc->human_input.pending_move_y = -1; + human_input_queue_attack_npc( + &rc->human_input, + rc->human_input.pending_target_idx); + } + human_set_click_cross(&rc->human_input, click_x, click_y, 1); +} + +static void context_menu_execute(RenderClient* rc, OsrsEnv* env, int item_idx) { + ContextMenu* cm = &rc->context_menu; + if (item_idx < 0 || item_idx >= cm->item_count) return; + ContextMenuItem* item = &cm->items[item_idx]; + int lab_on = render_lab_def(env) != NULL; + + switch (item->action) { + case CMENU_ACTION_WALK_HERE: + rc->human_input.pending_move_x = cm->walk_tile_x; + rc->human_input.pending_move_y = cm->walk_tile_y; + rc->human_input.pending_attack = 0; + human_input_queue_walk(&rc->human_input, cm->walk_tile_x, cm->walk_tile_y); + human_set_click_cross(&rc->human_input, cm->click_screen_x, cm->click_screen_y, 0); + break; + + case CMENU_ACTION_ATTACK: { + int ei = item->entity_idx; + if (ei >= 0 && ei < rc->entity_count) { + render_human_attack_npc_slot( + rc, rc->entities[ei].npc_slot, + cm->click_screen_x, cm->click_screen_y); + } + break; + } + + case CMENU_ACTION_GUI_INVENTORY_PRIMARY: { + Player* p = rc->entity_count > 0 && rc->gui.gui_entity_idx < rc->entity_count + ? render_get_player_ptr(env, rc->gui.gui_entity_idx) + : NULL; + if (p) { + gui_inv_click(&rc->gui, p, item->inventory_slot, &rc->human_input); + } + break; + } + + case CMENU_ACTION_GUI_INVENTORY_USE: + gui_inv_select_item(&rc->gui, &rc->human_input, item->inventory_slot); + break; + + case CMENU_ACTION_GUI_PRAYER: { + Player* p = rc->entity_count > 0 && rc->gui.gui_entity_idx < rc->entity_count + ? render_get_player_ptr(env, rc->gui.gui_entity_idx) + : NULL; + if (item->prayer_idx >= 0 && + rc->human_input.cursor_mode != CURSOR_NORMAL) { + human_apply_selected_target_to_widget( + &rc->human_input, + osrs_ui_intent_widget_component_id( + OSRS_UI_GROUP_PRAYERBOOK, item->prayer_idx)); + break; + } + if (p && item->prayer_idx >= 0) { + human_apply_prayer_idx(&rc->human_input, p, (GuiPrayerIdx)item->prayer_idx); + } + break; + } + + case CMENU_ACTION_GUI_SPELL: + if (item->spell_idx >= 0) { + if (rc->human_input.cursor_mode != CURSOR_NORMAL) { + human_apply_selected_target_to_widget( + &rc->human_input, + osrs_ui_intent_widget_component_id( + OSRS_UI_GROUP_MAGIC_SPELLBOOK, item->spell_idx)); + break; + } + human_select_spell_idx(&rc->human_input, (GuiSpellIdx)item->spell_idx); + } + break; + + case CMENU_ACTION_GUI_COMBAT_STYLE: { + Player* p = rc->entity_count > 0 && rc->gui.gui_entity_idx < rc->entity_count + ? render_get_player_ptr(env, rc->gui.gui_entity_idx) + : NULL; + if (p && item->fight_style >= 0) { + if (rc->human_input.cursor_mode != CURSOR_NORMAL) { + human_apply_selected_target_to_widget( + &rc->human_input, + osrs_ui_intent_widget_component_id( + OSRS_UI_GROUP_COMBAT_INTERFACE, item->fight_style)); + break; + } + human_apply_combat_style( + &rc->human_input, + &rc->gui, + p, + (FightStyle)item->fight_style); + } + break; + } + + case CMENU_ACTION_GUI_AUTOCAST: { + Player* p = rc->entity_count > 0 && rc->gui.gui_entity_idx < rc->entity_count + ? render_get_player_ptr(env, rc->gui.gui_entity_idx) + : NULL; + if (p && item->autocast_spell >= 0) { + if (rc->human_input.cursor_mode != CURSOR_NORMAL) { + human_apply_selected_target_to_widget( + &rc->human_input, + osrs_ui_intent_widget_component_id( + OSRS_UI_GROUP_COMBAT_INTERFACE, 110 + item->autocast_spell)); + break; + } + human_apply_autocast_spell( + &rc->human_input, + &rc->gui, + p, + item->autocast_spell, + item->autocast_defensive); + } + break; + } + + case CMENU_ACTION_GUI_SPEC_TOGGLE: + if (rc->human_input.cursor_mode != CURSOR_NORMAL) { + human_apply_selected_target_to_widget( + &rc->human_input, + osrs_ui_intent_widget_component_id(OSRS_UI_GROUP_COMBAT_INTERFACE, 120)); + break; + } + human_apply_spec_toggle(&rc->human_input); + break; + + case CMENU_ACTION_LAB_SELECT_NPC: { + int ei = item->entity_idx; + if (ei >= 0 && ei < rc->entity_count) + rc->lab_selected_npc_slot = rc->entities[ei].npc_slot; + break; + } + + case CMENU_ACTION_LAB_MOVE_SELECTED_NPC: + if (lab_on && rc->lab_selected_npc_slot >= 0) { + char line[96]; + snprintf(line, sizeof(line), "move_npc slot=%d x=%d y=%d", + rc->lab_selected_npc_slot, cm->walk_tile_x, cm->walk_tile_y); + render_lab_apply_line(rc, env, line); + } + break; + + case CMENU_ACTION_LAB_PLACE_PLAYER: { + char line[64]; + snprintf(line, sizeof(line), "player x=%d y=%d", + cm->walk_tile_x, cm->walk_tile_y); + render_lab_apply_line(rc, env, line); + break; + } + + case CMENU_ACTION_LAB_SPAWN_NPC: + if (lab_on) { + char line[96]; + snprintf(line, sizeof(line), "npc type=%d x=%d y=%d", + item->npc_type, cm->walk_tile_x, cm->walk_tile_y); + render_lab_apply_line(rc, env, line); + int newest = -1; + for (int i = 0; i < rc->entity_count; i++) { + if (rc->entities[i].entity_type == ENTITY_NPC && + rc->entities[i].npc_slot > newest) + newest = rc->entities[i].npc_slot; + } + rc->lab_selected_npc_slot = newest; + } + break; + + case CMENU_ACTION_LAB_KILL_NPC: { + int ei = item->entity_idx; + if (lab_on && ei >= 0 && ei < rc->entity_count) { + char line[48]; + snprintf(line, sizeof(line), "kill_npc slot=%d", + rc->entities[ei].npc_slot); + render_lab_apply_line(rc, env, line); + } + break; + } + + case CMENU_ACTION_LAB_DELETE_NPC: { + int ei = item->entity_idx; + if (lab_on && ei >= 0 && ei < rc->entity_count) { + char line[48]; + snprintf(line, sizeof(line), "delete_npc slot=%d", + rc->entities[ei].npc_slot); + render_lab_apply_line(rc, env, line); + } + break; + } + + case CMENU_ACTION_LAB_TOGGLE_PILLAR: { + InfernoState* inf = render_inferno_state_from_env(env); + if (inf && item->pillar_idx >= 0) { + int idx = item->pillar_idx; + int active = inf->pillars[idx].active; + char line[64]; + snprintf(line, sizeof(line), "pillar idx=%d active=%d", + idx, active ? 0 : 1); + render_lab_apply_line(rc, env, line); + } + break; + } + + case CMENU_ACTION_CANCEL: + case CMENU_ACTION_NONE: + break; + } + + context_menu_dismiss(cm); +} + +static void context_menu_draw(RenderClient* rc) { + ContextMenu* cm = &rc->context_menu; + if (!cm->visible || cm->item_count == 0) return; + + int mx = GetMouseX(); + int my = GetMouseY(); + int menu_h = context_menu_height(cm); + + cm->hover_idx = context_menu_row_at(cm, mx, my); + + Color bg = (Color){53, 44, 31, 244}; + Color border = (Color){170, 137, 72, 255}; + Color row_bg = (Color){28, 23, 17, 215}; + Color hover_bg = (Color){74, 60, 38, 235}; + Color title_color = (Color){255, 255, 0, 255}; + Color text_normal = (Color){255, 152, 31, 255}; + Color text_hover = (Color){255, 255, 0, 255}; + + DrawRectangle(cm->screen_x, cm->screen_y, cm->width, menu_h, bg); + DrawRectangleLinesEx( + (Rectangle){ + (float)cm->screen_x, + (float)cm->screen_y, + (float)cm->width, + (float)menu_h, + }, + 1.0f, + border + ); + context_menu_draw_text_shadow( + &rc->gui, + "Choose Option", cm->screen_x + 5, cm->screen_y + 4, 11, title_color); + + for (int i = 0; i < cm->item_count; i++) { + int iy = cm->screen_y + CONTEXT_MENU_ITEM_TOP + i * CONTEXT_MENU_ROW_H; + Color bg_color = (i == cm->hover_idx) ? hover_bg : row_bg; + DrawRectangle( + cm->screen_x + CONTEXT_MENU_PADDING, + iy, + cm->width - CONTEXT_MENU_PADDING * 2, + CONTEXT_MENU_ITEM_H, + bg_color + ); + Color tc = (i == cm->hover_idx) ? text_hover : text_normal; + context_menu_draw_text_shadow( + &rc->gui, + cm->items[i].label, + cm->screen_x + CONTEXT_MENU_PADDING + 4, + iy + 3, + 11, + tc + ); + } +} + +static const EncounterDef* render_lab_def_from_client(RenderClient* rc) { + if (!rc || !rc->gui.encounter_def || !rc->gui.encounter_state) return NULL; + const EncounterDef* def = (const EncounterDef*)rc->gui.encounter_def; + if (!def->apply_lab_command || + !def->snapshot_size || !def->snapshot || !def->restore) + return NULL; + return def; +} + +static void render_lab_draw_hud(RenderClient* rc) { + const EncounterDef* def = render_lab_def_from_client(rc); + if (!def || !rc->lab_enabled) return; + + int selected = rc->lab_selected_npc_slot; + char selected_text[64] = "none"; + for (int i = 0; i < rc->entity_count; i++) { + if (rc->entities[i].entity_type == ENTITY_NPC && + rc->entities[i].npc_slot == selected) { + snprintf(selected_text, sizeof(selected_text), "%d %s", selected, + render_entity_display_name(&rc->entities[i])); + break; + } + } + int is_inferno = strcmp(def->name, "inferno") == 0; + char title[48]; + snprintf(title, sizeof(title), "%s LAB", def->name); + for (char* c = title; *c; c++) *c = (char)toupper((unsigned char)*c); + + DrawRectangle(8, 8, 650, 58, CLITERAL(Color){ 15, 10, 18, 220 }); + DrawRectangleLines(8, 8, 650, 58, CLITERAL(Color){ 190, 80, 255, 255 }); + DrawText(title, 16, 14, 16, CLITERAL(Color){ 230, 210, 255, 255 }); + if (is_inferno) { + DrawText(TextFormat("F8 commit F6 restore F7 forecast %s F9 dump selected: %s", + rc->lab_show_forecast ? "on" : "off", selected_text), + 16, 38, 12, CLITERAL(Color){ 230, 230, 230, 255 }); + } else { + DrawText(TextFormat("F8 commit F6 restore F9 dump selected: %s", + selected_text), + 16, 38, 12, CLITERAL(Color){ 230, 230, 230, 255 }); + } +} + +static void render_draw_colosseum_grapple_banner(RenderClient* rc, OsrsEnv* env) { + (void)rc; + ColosseumState* cs = render_colosseum_state_from_env(env); + if (!cs || !cs->sol.started) return; + const SolHereditState* sol = &cs->sol; + + static const char* CALLOUTS[COLO_NUM_GRAPPLE_SLOTS] = { + "I'LL CRUSH YOUR BODY!", + "I'LL BREAK YOUR BACK!", + "I'LL TWIST YOUR HANDS OFF!", + "I'LL BREAK YOUR LEGS!", + "I'LL CUT YOUR FEET OFF!", + }; + + const char* text = NULL; + Color color = (Color){ 255, 60, 40, 255 }; + if (sol->grapple_active && + sol->grapple_body_slot >= 0 && + sol->grapple_body_slot < COLO_NUM_GRAPPLE_SLOTS) { + text = CALLOUTS[sol->grapple_body_slot]; + } else if (sol->grapple_outcome != COLO_GRAPPLE_OUTCOME_NONE && + cs->tick - sol->grapple_outcome_tick < 4) { + switch (sol->grapple_outcome) { + case COLO_GRAPPLE_OUTCOME_PERFECT: + text = "You perfectly parry Sol Heredit's grapple!"; + color = (Color){ 90, 240, 120, 255 }; + break; + case COLO_GRAPPLE_OUTCOME_DEFENDED: + text = "You successfully defend from Sol Heredit's grapple!"; + color = (Color){ 160, 220, 255, 255 }; + break; + case COLO_GRAPPLE_OUTCOME_FAILED: + text = "You fail to defend the grapple!"; + break; + default: + break; + } + } + if (!text) return; + + int font_size = 24; + int text_w = MeasureText(text, font_size); + int x = (RENDER_GRID_W - text_w) / 2; + int y = RENDER_WINDOW_H / 3; + DrawText(text, x + 2, y + 2, font_size, (Color){ 0, 0, 0, 200 }); + DrawText(text, x, y, font_size, color); +} + +static void render_draw_encounter_status_text(RenderClient* rc) { + EncounterOverlay* ov = &rc->encounter_overlay; + if (!ov->status_text_active || ov->status_text[0] == '\0') return; + + int font_size = 22; + int text_w = MeasureText(ov->status_text, font_size); + int pad_x = 12; + int pad_y = 6; + int box_w = text_w + pad_x * 2; + int box_h = font_size + pad_y * 2; + int x = (RENDER_WINDOW_W - box_w) / 2; + int y = 42; + + DrawRectangle(x, y, box_w, box_h, (Color){ 35, 15, 15, 220 }); + DrawRectangleLines(x, y, box_w, box_h, (Color){ 190, 65, 45, 255 }); + DrawText(ov->status_text, x + pad_x + 1, y + pad_y + 1, + font_size, (Color){ 0, 0, 0, 180 }); + DrawText(ov->status_text, x + pad_x, y + pad_y, + font_size, (Color){ 255, 220, 190, 255 }); +} + +static RenderClient* render_make_client(void) { + osrs_asset_require_group(OSRS_ASSET_GROUP_CORE); + osrs_asset_require_group(OSRS_ASSET_GROUP_GUI); + + RenderClient* rc = (RenderClient*)calloc(1, sizeof(RenderClient)); + rc->ticks_per_second = RENDER_DEFAULT_GAME_TICKS_PER_SECOND; + rc->last_tick_time = 0.0; + rc->model_scale = 0.15f; + rc->zoom = 1.0f; + rc->arena_base_x = FIGHT_AREA_BASE_X; + rc->arena_base_y = FIGHT_AREA_BASE_Y; + rc->arena_width = FIGHT_AREA_WIDTH; + rc->arena_height = FIGHT_AREA_HEIGHT; + rc->show_safe_spots = 0; + rc->show_debug = 0; + rc->lab_enabled = 0; + rc->lab_show_forecast = 0; + rc->lab_selected_npc_slot = -1; + rc->lab_prev_paused = 0; + rc->lab_prev_human_enabled = 0; + rc->lab_entry_snapshot = NULL; + rc->lab_entry_snapshot_size = 0; + rc->lab_restore_requested = 0; + rc->lab_restore_generation = 0; + rc->layout_mode = 1; + rc->cam_yaw = 0.0f; + rc->cam_pitch = 0.6f; + rc->cam_dist = 40.0f; + rc->cam_target_x = (float)rc->arena_base_x + (float)rc->arena_width / 2.0f; + rc->cam_target_z = -((float)rc->arena_base_y + (float)rc->arena_height / 2.0f); + rc->history_capacity = RENDER_HISTORY_INITIAL_CAPACITY; + rc->history = (OsrsEnv*)calloc((size_t)rc->history_capacity, sizeof(OsrsEnv)); + if (!rc->history) { + fprintf(stderr, "render history allocation failed\n"); + abort(); + } + rc->history_count = 0; + rc->history_cursor = -1; + rc->entity_count = 0; + rc->prev_entity_count = 0; + rc->hover_tile_x = -1; + rc->hover_tile_y = -1; + rc->prev_sol_aoe_age = -1; + for (int i = 0; i < MAX_RENDER_ENTITIES; i++) { + anim_playback_reset(&rc->anim[i].primary); + anim_playback_set_seq(&rc->anim[i].secondary, ANIM_SEQ_IDLE, ANIM_PLAY_LOOP); + rc->primary_event_tick[i] = -1; + rc->last_primary_event_tick[i] = -2; + rc->prev_npc_slot[i] = -1; + } + + InitWindow(RENDER_WINDOW_W, RENDER_WINDOW_H, "OSRS PvP Debug Viewer"); + SetTargetFPS(60); + + { + const char* paths[] = { + OSRS_ASSET("sprites/gui/headicons_prayer_0.png"), + OSRS_ASSET("sprites/gui/headicons_prayer_1.png"), + OSRS_ASSET("sprites/gui/headicons_prayer_2.png"), + OSRS_ASSET("sprites/gui/headicons_prayer_3.png"), + OSRS_ASSET("sprites/gui/headicons_prayer_4.png"), + OSRS_ASSET("sprites/gui/headicons_prayer_5.png"), + }; + rc->prayer_icons_loaded = 1; + for (int i = 0; i < 6; i++) { + if (osrs_asset_exists(paths[i])) { + rc->prayer_icons[i] = osrs_asset_load_texture(paths[i]); + } else { + rc->prayer_icons_loaded = 0; + } + } + } + + { + rc->hitmark_sprites_loaded = 1; + for (int i = 0; i < 5; i++) { + const char* path = TextFormat(OSRS_ASSET("sprites/gui/hitmarks_%d.png"), i); + if (osrs_asset_exists(path)) { + rc->hitmark_sprites[i] = osrs_asset_load_texture(path); + } else { + rc->hitmark_sprites_loaded = 0; + } + } + } + + { + static const char* cross_names[8] = { + "cross_yellow_1", "cross_yellow_2", "cross_yellow_3", "cross_yellow_4", + "cross_red_1", "cross_red_2", "cross_red_3", "cross_red_4", + }; + rc->click_cross_loaded = 1; + for (int i = 0; i < 8; i++) { + const char* path = TextFormat(OSRS_ASSET("sprites/gui/%s.png"), cross_names[i]); + if (osrs_asset_exists(path)) { + rc->click_cross_sprites[i] = osrs_asset_load_texture(path); + } else { + rc->click_cross_loaded = 0; + } + } + } + + rc->debug_hit_wx = -1; + rc->debug_hit_wy = -1; + + rc->gui.active_tab = GUI_TAB_INVENTORY; + rc->gui.panel_x = RENDER_WINDOW_W - RENDER_PANEL_WIDTH; + rc->gui.panel_w = RENDER_PANEL_WIDTH; + rc->gui.panel_h = RENDER_TAB_ROW_H + RENDER_PANEL_CONTENT_H + RENDER_TAB_ROW_H; + rc->gui.panel_y = RENDER_WINDOW_H - rc->gui.panel_h; + rc->gui.ui_scale = RENDER_UI_SCALE; + rc->gui.tab_h = RENDER_TAB_ROW_H; + rc->gui.status_bar_h = 0; + rc->gui.gui_entity_idx = 0; + rc->gui.gui_entity_count = 0; + + gui_reset_inventory_ui_state(&rc->gui); + + human_input_init(&rc->human_input); + + rc->context_menu.hover_idx = -1; + + gui_load_sprites(&rc->gui); + + return rc; +} + +static int render_build_static_model(ModelCache* cache, uint32_t model_id, Model* out) { + OsrsModel* om = model_cache_get(cache, model_id); + if (!om || om->mesh.vertexCount == 0) return 0; + + Mesh mesh = { 0 }; + mesh.vertexCount = om->mesh.vertexCount; + mesh.triangleCount = om->mesh.triangleCount; + mesh.vertices = (float*)RL_MALLOC(mesh.vertexCount * 3 * sizeof(float)); + mesh.colors = (unsigned char*)RL_MALLOC(mesh.vertexCount * 4); + memcpy(mesh.vertices, om->mesh.vertices, mesh.vertexCount * 3 * sizeof(float)); + memcpy(mesh.colors, om->mesh.colors, mesh.vertexCount * 4); + + UploadMesh(&mesh, false); + *out = LoadModelFromMesh(mesh); + return 1; +} + +static void render_load_projectile_assets(RenderClient* rc) { + if (!rc->spotanims) { + rc->spotanims = osrs_spotanims_load(OSRS_ASSET("spotanims.bin")); + } + if (!rc->projectile_model_cache) { + rc->projectile_model_cache = model_cache_load(OSRS_ASSET("projectiles.models")); + } + if (!rc->projectile_anim_cache) { + rc->projectile_anim_cache = anim_cache_load(OSRS_ASSET("projectiles.anims")); + } +} + +static Model* render_get_proj_model(RenderClient* rc, uint32_t model_id) { + if (model_id == 0) return NULL; + for (int i = 0; i < rc->proj_model_count; i++) { + if (rc->proj_models[i].id == model_id) + return rc->proj_models[i].ready ? &rc->proj_models[i].model : NULL; + } + if (rc->proj_model_count >= MAX_PROJ_MODELS) { + fprintf(stderr, "render: projectile model cache exhausted while loading model %u\n", + model_id); + abort(); + } + int idx = rc->proj_model_count++; + rc->proj_models[idx].id = model_id; + rc->proj_models[idx].ready = render_build_static_model( + rc->projectile_model_cache, model_id, &rc->proj_models[idx].model); + if (!rc->proj_models[idx].ready) { + rc->proj_models[idx].ready = render_build_static_model( + rc->model_cache, model_id, &rc->proj_models[idx].model); + } + if (!rc->proj_models[idx].ready && rc->npc_model_cache) { + rc->proj_models[idx].ready = render_build_static_model( + rc->npc_model_cache, model_id, &rc->proj_models[idx].model); + } + if (!rc->proj_models[idx].ready) { + fprintf(stderr, "render: explicit projectile model %u is missing from loaded caches\n", + model_id); + abort(); + } + return &rc->proj_models[idx].model; +} + +static OsrsModel* render_get_projectile_osrs_model(RenderClient* rc, uint32_t model_id) { + if (model_id == 0) return NULL; + OsrsModel* om = model_cache_get(rc->projectile_model_cache, model_id); + if (!om) om = model_cache_get(rc->model_cache, model_id); + if (!om && rc->npc_model_cache) + om = model_cache_get(rc->npc_model_cache, model_id); + if (!om) { + fprintf(stderr, "render: explicit projectile model %u is missing from loaded caches\n", + model_id); + abort(); + } + return om; +} + +static const OsrsSpotAnimDef* render_require_travel_spotanim( + RenderClient* rc, + int travel_gfx_id +) { + const OsrsSpotAnimDef* meta = osrs_spotanim_find(rc->spotanims, travel_gfx_id); + if (!meta || meta->model_id < 0) { + fprintf(stderr, "render: travel spotanim %d is missing a model\n", + travel_gfx_id); + abort(); + } + return meta; +} + +static OsrsModel* render_get_travel_spotanim_osrs_model( + RenderClient* rc, + int travel_gfx_id +) { + const OsrsSpotAnimDef* meta = render_require_travel_spotanim(rc, travel_gfx_id); + OsrsModel* om = effect_find_model( + meta, rc->model_cache, rc->npc_model_cache, rc->projectile_model_cache); + if (!om) { + fprintf(stderr, "render: travel spotanim %d model %d is missing from loaded caches\n", + travel_gfx_id, meta->model_id); + abort(); + } + return om; +} + +static OsrsModel* render_get_flight_osrs_model( + RenderClient* rc, + const FlightProjectile* fp +) { + if (fp->travel_gfx_drives_model) + return render_get_travel_spotanim_osrs_model(rc, fp->travel_gfx_id); + return render_get_projectile_osrs_model(rc, fp->model_id); +} + +static int render_projectile_anim_has_dynamic_frames(RenderClient* rc, int anim_id) { + if (anim_id < 0) return 0; + AnimSequence* seq = render_get_anim_sequence(rc, (uint16_t)anim_id); + if (!seq || seq->frame_count <= 0) { + fprintf(stderr, "render: projectile animation %d is missing\n", anim_id); + abort(); + } + return !(seq->frame_count == 1 && + (seq->frames[0].delay == 0 || seq->frames[0].delay >= 0x8000u)); +} + +static AnimModelState* render_create_projectile_anim_state_from_model( + OsrsModel* om, + uint32_t model_id, + int anim_id +) { + if (anim_id < 0 || !om) return NULL; + if (!om->vertex_skins || om->base_vert_count == 0) { + fprintf(stderr, "render: projectile model %u cannot play animation %d\n", + model_id, anim_id); + abort(); + } + return anim_model_state_create_with_face_alpha( + om->vertex_skins, om->base_vert_count, + om->face_alpha_labels, om->base_face_alphas, om->mesh.triangleCount); +} + +static int render_effect_anim_frame_for_tick(const AnimSequence* seq, int tick) { + if (!seq || seq->frame_count <= 0) return 0; + if (seq->frame_count == 1) return 0; + int cycle = 0; + for (int f = 0; f < seq->frame_count; f++) { + int d = seq->frames[f].delay; + if (d <= 0 || d >= 0x8000) d = 1; + cycle += d; + } + if (cycle <= 0) return 0; + int phase = ((tick % cycle) + cycle) % cycle; + int acc = 0; + for (int f = 0; f < seq->frame_count; f++) { + int d = seq->frames[f].delay; + if (d <= 0 || d >= 0x8000) d = 1; + acc += d; + if (phase < acc) return f; + } + return seq->frame_count - 1; +} + +static AnimModelState* render_get_effect_anim_state( + RenderClient* rc, OsrsModel* om, uint32_t model_id, int anim_id +) { + for (int i = 0; i < rc->effect_anim_state_count; i++) { + if (rc->effect_anim_states[i].model_id == model_id && + rc->effect_anim_states[i].anim_id == anim_id) { + return rc->effect_anim_states[i].state; + } + } + if (rc->effect_anim_state_count >= MAX_EFFECT_ANIM_STATES) { + return NULL; + } + AnimModelState* state = + render_create_projectile_anim_state_from_model(om, model_id, anim_id); + if (!state) return NULL; + int idx = rc->effect_anim_state_count++; + rc->effect_anim_states[idx].model_id = model_id; + rc->effect_anim_states[idx].anim_id = anim_id; + rc->effect_anim_states[idx].state = state; + return state; +} + +static OsrsModel* render_animate_effect_model( + RenderClient* rc, uint32_t model_id, int anim_id, int tick +) { + if (anim_id < 0 || model_id == 0) return NULL; + OsrsModel* om = render_get_projectile_osrs_model(rc, model_id); + if (!om || !om->face_indices || !om->vertex_skins || om->base_vert_count == 0) { + return NULL; + } + AnimSequence* seq = render_get_anim_sequence(rc, (uint16_t)anim_id); + if (!seq || seq->frame_count <= 0) return NULL; + AnimModelState* state = render_get_effect_anim_state(rc, om, model_id, anim_id); + if (!state) return NULL; + int frame = render_effect_anim_frame_for_tick(seq, tick); + render_apply_anim_sequence_frame_to_model_state( + rc, state, om, seq, frame, "effect-spotanim"); + return om; +} + +static void render_init_overlay_models(RenderClient* rc) { + if (!rc->model_cache) return; + + rc->cloud_model_ready = render_build_static_model( + rc->model_cache, GFX_TOXIC_CLOUD_MODEL, &rc->cloud_model); + rc->snakeling_model_ready = render_build_static_model( + rc->model_cache, SNAKELING_MODEL_ID, &rc->snakeling_model); + rc->ranged_proj_model_ready = render_build_static_model( + rc->model_cache, GFX_RANGED_PROJ_MODEL, &rc->ranged_proj_model); + rc->magic_proj_model_ready = render_build_static_model( + rc->model_cache, GFX_MAGIC_PROJ_MODEL, &rc->magic_proj_model); + + rc->cloud_proj_model_ready = render_build_static_model( + rc->model_cache, GFX_CLOUD_PROJ_MODEL, &rc->cloud_proj_model); + { + uint32_t pillar_ids[4] = { INF_PILLAR_MODEL_100, INF_PILLAR_MODEL_75, + INF_PILLAR_MODEL_50, INF_PILLAR_MODEL_25 }; + rc->pillar_models_ready = 1; + for (int i = 0; i < 4; i++) { + if (!render_build_static_model(rc->model_cache, pillar_ids[i], &rc->pillar_models[i])) + rc->pillar_models_ready = 0; + } + } + + if (rc->cloud_model_ready) printf("overlay: cloud model loaded\n"); + if (rc->pillar_models_ready) printf("overlay: pillar models loaded (4 HP levels)\n"); + if (rc->snakeling_model_ready) printf("overlay: snakeling model loaded\n"); + if (rc->ranged_proj_model_ready) printf("overlay: ranged projectile model loaded\n"); + if (rc->magic_proj_model_ready) printf("overlay: magic projectile model loaded\n"); + if (rc->cloud_proj_model_ready) printf("overlay: cloud projectile model loaded\n"); +} + +static void flight_deactivate(FlightProjectile* fp) { + if (fp->anim_state) { + anim_model_state_free(fp->anim_state); + fp->anim_state = NULL; + } + fp->active = 0; +} + +static void flight_clear_all(RenderClient* rc) { + for (int i = 0; i < MAX_FLIGHT_PROJECTILES; i++) { + flight_deactivate(&rc->flights[i]); + } +} + +static void flight_finish(RenderClient* rc, FlightProjectile* fp) { + if (fp->impact_gfx_id > 0) { + effect_spawn_spotanim_subtile( + rc->effects, fp->impact_gfx_id, + osrs_projectile_subtile_from_anchor_coord(fp->dst_x), + osrs_projectile_subtile_from_anchor_coord(fp->dst_y), + rc->effect_client_tick_counter + 1, + rc->spotanims, rc->anim_cache, rc->model_cache, + rc->npc_model_cache, rc->projectile_model_cache); + } + flight_deactivate(fp); +} + +static void flight_spawn_launch_gfx(RenderClient* rc, FlightProjectile* fp) { + if (fp->launch_gfx_id <= 0 || fp->launch_spawned) return; + fp->launch_spawned = 1; + effect_spawn_spotanim_subtile( + rc->effects, fp->launch_gfx_id, + osrs_projectile_subtile_from_anchor_coord(fp->src_x), + osrs_projectile_subtile_from_anchor_coord(fp->src_y), + rc->effect_client_tick_counter + 1, + rc->spotanims, rc->anim_cache, rc->model_cache, + rc->npc_model_cache, rc->projectile_model_cache); +} + +static int render_find_npc_entity_idx(const RenderClient* rc, int npc_slot) { + for (int i = 0; i < rc->entity_count; i++) { + const RenderEntity* entity = &rc->entities[i]; + if (entity->entity_type == ENTITY_NPC && + entity->npc_slot == npc_slot && + entity->npc_visible) { + return i; + } + } + return -1; +} + +static int render_resolve_projectile_anchor( + const RenderClient* rc, + int anchor_kind, + int npc_slot, + float fixed_x, + float fixed_y, + float* out_x, + float* out_y +) { + int entity_idx = -1; + switch (anchor_kind) { + case ENCOUNTER_PROJECTILE_TARGET_FIXED: + *out_x = fixed_x; + *out_y = fixed_y; + return 1; + case ENCOUNTER_PROJECTILE_TARGET_PLAYER: + entity_idx = rc->entity_count > 0 ? 0 : -1; + break; + case ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT: + entity_idx = render_find_npc_entity_idx(rc, npc_slot); + break; + default: + fprintf(stderr, "render: unknown projectile anchor kind %d\n", anchor_kind); + abort(); + } + + if (entity_idx < 0) return 0; + *out_x = osrs_projectile_anchor_coord_from_subtile(rc->sub_x[entity_idx]); + *out_y = osrs_projectile_anchor_coord_from_subtile(rc->sub_y[entity_idx]); + return 1; +} + +static int render_resolve_flight_target_anchor( + const RenderClient* rc, + const FlightProjectile* fp, + float* out_x, + float* out_y +) { + return render_resolve_projectile_anchor( + rc, fp->target_kind, fp->target_npc_slot, + fp->dst_x, fp->dst_y, out_x, out_y); +} + +static void flight_update_live_destination(RenderClient* rc, FlightProjectile* fp) { + if (!fp->tracks_target) return; + float x = 0.0f; + float y = 0.0f; + if (render_resolve_flight_target_anchor(rc, fp, &x, &y)) { + fp->dst_x = x; + fp->dst_y = y; + } +} + +static void flight_spawn(RenderClient* rc, + float src_x, float src_y, float dst_x, float dst_y, + int style, int damage, + int duration_ticks, int start_h, int end_h, int curve, + float arc_height, int tracks_target, uint32_t model_id, + int anim_id, int travel_gfx_id, + int launch_gfx_id, int impact_gfx_id, + int start_delay, int motion_mode, + float offset_x, float offset_y, float offset_z, + int source_kind, int source_npc_slot, + int target_kind, int target_npc_slot) { + int slot = -1; + for (int i = 0; i < MAX_FLIGHT_PROJECTILES; i++) { + if (!rc->flights[i].active) { slot = i; break; } + } + if (slot < 0) { + fprintf(stderr, "osrs render: flight projectile overflow at %d slots\n", + MAX_FLIGHT_PROJECTILES); + abort(); + } + + FlightProjectile* fp = &rc->flights[slot]; + memset(fp, 0, sizeof(FlightProjectile)); + fp->active = 1; + fp->src_x = src_x; + fp->src_y = src_y; + fp->dst_x = dst_x; + fp->dst_y = dst_y; + fp->x = src_x; + fp->y = src_y; + fp->progress = 0.0f; + fp->speed = 1.0f / (float)duration_ticks; + fp->start_height = (float)start_h / 128.0f; + fp->end_height = (float)end_h / 128.0f; + fp->curve = (float)curve; + fp->style = style; + fp->damage = damage; + fp->arc_height = arc_height; + fp->tracks_target = tracks_target; + fp->source_kind = source_kind; + fp->source_npc_slot = source_npc_slot; + fp->target_kind = target_kind; + fp->target_npc_slot = target_npc_slot; + fp->model_id = model_id; + fp->anim_id = anim_id; + fp->travel_gfx_id = travel_gfx_id; + fp->travel_gfx_drives_model = (model_id == 0 && travel_gfx_id > 0); + fp->grow = 0; + if (travel_gfx_id > 0) { + const OsrsSpotAnimDef* meta = render_require_travel_spotanim(rc, travel_gfx_id); + if (fp->travel_gfx_drives_model) { + fp->model_id = (uint32_t)meta->model_id; + if (fp->anim_id < 0) + fp->anim_id = meta->animation_id; + } + if (fp->anim_id >= 0) { + OsrsModel* anim_model = render_get_flight_osrs_model(rc, fp); + int mvc = anim_model ? (int)anim_model->base_vert_count : 0; + AnimSequence* seq = + render_get_anim_sequence_for_model(rc, (uint16_t)fp->anim_id, mvc); + int maya_vc = anim_sequence_maya_vert_count(seq); + if (maya_vc > 0 && maya_vc != mvc) { + fp->grow = 1; + fp->anim_id = -1; + } else if (!render_projectile_anim_has_dynamic_frames(rc, fp->anim_id)) { + fp->anim_id = -1; + } + } + } + anim_playback_reset(&fp->anim_playback); + if (fp->anim_id >= 0) { + OsrsModel* pm = render_get_flight_osrs_model(rc, fp); + anim_playback_set_seq(&fp->anim_playback, fp->anim_id, ANIM_PLAY_LOOP); + render_anim_playback_resolve( + rc, &fp->anim_playback, pm ? (int)pm->base_vert_count : 0); + } + fp->anim_state = render_create_projectile_anim_state_from_model( + render_get_flight_osrs_model(rc, fp), fp->model_id, fp->anim_id); + fp->launch_gfx_id = launch_gfx_id; + fp->impact_gfx_id = impact_gfx_id; + fp->start_delay = start_delay; + fp->motion_mode = motion_mode; + fp->offset_x = offset_x; + fp->offset_y = offset_y; + fp->offset_z = offset_z; + flight_update_live_destination(rc, fp); + + float dx = dst_x - src_x, dy = dst_y - src_y; + float dist = sqrtf(dx * dx + dy * dy); + if (dist < 0.01f) dist = 1.0f; + if (arc_height > 0.0f) { + fp->height_vel = 0.0f; + fp->height_accel = 0.0f; + } else { + fp->height_vel = -dist * tanf(curve * PROJ_OSRS_SLOPE_TO_RAD); + fp->height_accel = 2.0f * (fp->end_height - fp->start_height - fp->height_vel); + } + + float h0 = osrs_projectile_height_at_progress(0.0f, + fp->start_height, fp->end_height, fp->arc_height, + fp->height_vel, fp->height_accel); + float h1 = osrs_projectile_height_at_progress(fp->speed, + fp->start_height, fp->end_height, fp->arc_height, + fp->height_vel, fp->height_accel); + OsrsProjectileOrientation orientation = osrs_projectile_orientation_from_step( + dx * fp->speed, dy * fp->speed, h1 - h0); + fp->yaw = orientation.yaw; + fp->pitch = orientation.pitch; + + if (fp->start_delay == 0) flight_spawn_launch_gfx(rc, fp); +} + +static void flight_advance_animation(RenderClient* rc, FlightProjectile* fp) { + (void)rc; + if (fp->anim_id < 0) return; + if (!fp->anim_playback.sequence || fp->anim_playback.sequence->frame_count <= 0) { + fprintf(stderr, "render: projectile animation %d is missing\n", fp->anim_id); + abort(); + } + anim_playback_advance(&fp->anim_playback); +} + +static inline Matrix render_projectile_transform( + float scale_x, float scale_y, float scale_z, + float yaw, float pitch, Vector3 position +) { + Matrix transform = MatrixScale(-scale_x, scale_y, scale_z); + transform = MatrixMultiply( + transform, + MatrixMultiply(MatrixRotateX(pitch), MatrixRotateY(yaw))); + transform = MatrixMultiply(transform, MatrixTranslate(position.x, position.y, position.z)); + return transform; +} + +static inline Matrix render_projectile_transform_offset( + float scale_x, float scale_y, float scale_z, + float yaw, float pitch, Vector3 position, + float offset_x, float offset_y, float offset_z +) { + Matrix transform = MatrixScale(-scale_x, scale_y, scale_z); + transform = MatrixMultiply( + transform, MatrixTranslate(offset_x, offset_z, offset_y)); + transform = MatrixMultiply( + transform, + MatrixMultiply(MatrixRotateX(pitch), MatrixRotateY(yaw))); + transform = MatrixMultiply(transform, MatrixTranslate(position.x, position.y, position.z)); + return transform; +} + +static inline void flight_update_target_anchored_position( + RenderClient* rc, + FlightProjectile* fp +) { + float old_x = fp->x; + float old_y = fp->y; + float target_x = 0.0f; + float target_y = 0.0f; + if (render_resolve_flight_target_anchor(rc, fp, &target_x, &target_y)) { + fp->x = target_x; + fp->y = target_y; + } else { + fp->x = fp->dst_x; + fp->y = fp->dst_y; + } + OsrsProjectileOrientation orientation = + osrs_projectile_orientation_from_step(fp->x - old_x, fp->y - old_y, 0.0f); + fp->yaw = orientation.yaw; + fp->pitch = orientation.pitch; +} + +static inline RangedSpecWeapon render_pvp_ranged_spec_weapon_for_item(uint8_t weapon_db_idx) { + switch (weapon_db_idx) { + case ITEM_DARK_BOW: return RANGED_SPEC_DARK_BOW; + case ITEM_HEAVY_BALLISTA: return RANGED_SPEC_BALLISTA; + case ITEM_ARMADYL_CROSSBOW: return RANGED_SPEC_ACB; + case ITEM_ZARYTE_CROSSBOW: return RANGED_SPEC_ZCB; + case ITEM_MAGIC_SHORTBOW_I: return RANGED_SPEC_MSB; + case ITEM_MORRIGANS_JAVELIN: return RANGED_SPEC_MORRIGANS; + default: return RANGED_SPEC_NONE; + } +} + +static inline int render_pvp_distance_to_target( + const RenderEntity* attacker, const RenderEntity* target +) { + int target_size = target->entity_type == ENTITY_NPC && target->npc_size > 1 + ? target->npc_size : 1; + return encounter_dist_to_npc( + attacker->x, attacker->y, target->x, target->y, target_size); +} + +static void flight_client_tick(RenderClient* rc) { + for (int i = 0; i < MAX_FLIGHT_PROJECTILES; i++) { + FlightProjectile* fp = &rc->flights[i]; + if (!fp->active) continue; + + if (fp->start_delay > 0) { + fp->start_delay--; + if (fp->start_delay == 0) { + if (fp->motion_mode == ENCOUNTER_PROJECTILE_MOTION_TARGET_ANCHORED) { + flight_update_target_anchored_position(rc, fp); + } + flight_spawn_launch_gfx(rc, fp); + } + continue; + } + + if (fp->motion_mode == ENCOUNTER_PROJECTILE_MOTION_TARGET_ANCHORED) { + flight_update_target_anchored_position(rc, fp); + flight_advance_animation(rc, fp); + fp->progress += fp->speed; + if (fp->progress >= 1.0f) { + flight_finish(rc, fp); + } + continue; + } + + float start_progress = fp->progress; + float start_height = osrs_projectile_height_at_progress(start_progress, + fp->start_height, fp->end_height, fp->arc_height, + fp->height_vel, fp->height_accel); + float old_x = fp->x; + float old_y = fp->y; + + float remaining = (1.0f - fp->progress) / fp->speed; + if (remaining < 0.5f) remaining = 0.5f; + + flight_update_live_destination(rc, fp); + + float vx = (fp->dst_x - fp->x) / remaining; + float vy = (fp->dst_y - fp->y) / remaining; + + fp->x += vx; + fp->y += vy; + + float next_progress = start_progress + fp->speed; + float end_height = osrs_projectile_height_at_progress(next_progress, + fp->start_height, fp->end_height, fp->arc_height, + fp->height_vel, fp->height_accel); + OsrsProjectileOrientation orientation = osrs_projectile_orientation_from_step( + fp->x - old_x, fp->y - old_y, end_height - start_height); + fp->yaw = orientation.yaw; + fp->pitch = orientation.pitch; + + flight_advance_animation(rc, fp); + fp->progress += fp->speed; + if (fp->progress >= 1.0f) { + flight_finish(rc, fp); + } + } +} + +static Vector3 flight_get_position(const FlightProjectile* fp, float src_ground, float dst_ground) { + if (fp->motion_mode == ENCOUNTER_PROJECTILE_MOTION_TARGET_ANCHORED) { + return (Vector3){ fp->x + 0.5f, dst_ground + fp->end_height, + -(fp->y + 1.0f) + 0.5f }; + } + + float t = fp->progress; + if (t < 0.0f) t = 0.0f; + if (t > 1.0f) t = 1.0f; + + float ground = src_ground + (dst_ground - src_ground) * t; + float h = osrs_projectile_height_at_progress(t, + fp->start_height, fp->end_height, fp->arc_height, + fp->height_vel, fp->height_accel); + + return (Vector3){ fp->x + 0.5f, ground + h, -(fp->y + 1.0f) + 0.5f }; +} + +static void __attribute__((unused)) render_destroy_client(RenderClient* rc) { + flight_clear_all(rc); + if (rc->minimap_surface.id != 0) { + UnloadRenderTexture(rc->minimap_surface); + } + gui_unload_sprites(&rc->gui); + if (rc->prayer_icons_loaded) { + for (int i = 0; i < 6; i++) { + UnloadTexture(rc->prayer_icons[i]); + } + } + for (int mod = 0; mod < COLO_NUM_REAL_MODIFIERS; mod++) { + for (int tier = 0; tier < 3; tier++) { + if (rc->colosseum_modifier_icons[mod][tier].id != 0) + UnloadTexture(rc->colosseum_modifier_icons[mod][tier]); + } + } + if (rc->hitmark_sprites_loaded) { + for (int i = 0; i < 5; i++) { + UnloadTexture(rc->hitmark_sprites[i]); + } + } + if (rc->click_cross_loaded) { + for (int i = 0; i < 8; i++) { + UnloadTexture(rc->click_cross_sprites[i]); + } + } + if (rc->cloud_model_ready) UnloadModel(rc->cloud_model); + if (rc->snakeling_model_ready) UnloadModel(rc->snakeling_model); + if (rc->ranged_proj_model_ready) UnloadModel(rc->ranged_proj_model); + if (rc->magic_proj_model_ready) UnloadModel(rc->magic_proj_model); + if (rc->cloud_proj_model_ready) UnloadModel(rc->cloud_proj_model); + if (rc->pillar_models_ready) { + for (int i = 0; i < 4; i++) UnloadModel(rc->pillar_models[i]); + } + for (int i = 0; i < rc->proj_model_count; i++) { + if (rc->proj_models[i].ready) UnloadModel(rc->proj_models[i].model); + } + for (int i = 0; i < rc->effect_anim_state_count; i++) { + anim_model_state_free(rc->effect_anim_states[i].state); + } + for (int p = 0; p < MAX_RENDER_ENTITIES; p++) { + composite_free(&rc->composites[p]); + } + if (rc->model_cache) { + model_cache_free(rc->model_cache); + rc->model_cache = NULL; + } + if (rc->projectile_model_cache) { + model_cache_free(rc->projectile_model_cache); + rc->projectile_model_cache = NULL; + } + if (rc->spotanims) { + osrs_spotanims_free(rc->spotanims); + rc->spotanims = NULL; + } + if (rc->anim_cache) { + anim_cache_free(rc->anim_cache); + rc->anim_cache = NULL; + } + if (rc->projectile_anim_cache) { + anim_cache_free(rc->projectile_anim_cache); + rc->projectile_anim_cache = NULL; + } + if (rc->terrain) { + terrain_free(rc->terrain); + rc->terrain = NULL; + } + if (rc->objects) { + objects_free(rc->objects); + rc->objects = NULL; + } + if (rc->objects_zuk) { + objects_free(rc->objects_zuk); + rc->objects_zuk = NULL; + } + if (rc->npcs) { + objects_free(rc->npcs); + rc->npcs = NULL; + } + human_input_destroy(&rc->human_input); + CloseWindow(); + render_lab_clear_entry_snapshot(rc); + free(rc->history); + free(rc); +} + +static Rectangle render_colosseum_draft_card_rect(int option); +static Rectangle render_minimap_spec_orb_rect(void); + +static void render_handle_input(RenderClient* rc, OsrsEnv* env) { + RenderHumanAttackCtx attack_ctx = { .rc = rc, .env = env }; + if (IsKeyPressed(KEY_SPACE)) rc->is_paused = !rc->is_paused; + + if (IsKeyPressed(KEY_RIGHT) && rc->is_paused) { + if (rc->history_cursor >= 0) { + if (rc->history_cursor < rc->history_count - 1) { + rc->history_cursor++; + rc->step_back = 1; + } else { + rc->history_cursor = rc->history_count - 1; + rc->step_back = 1; + } + } else { + rc->step_once = 1; + } + } + + if (IsKeyPressed(KEY_LEFT) && rc->is_paused) { + if (rc->history_cursor == -1 && rc->history_count > 1) { + rc->history_cursor = rc->history_count - 2; + } else if (rc->history_cursor > 0) { + rc->history_cursor--; + } + rc->step_back = 1; + } + + if (IsKeyPressed(KEY_TAB)) ToggleFullscreen(); + if (IsKeyPressed(KEY_C)) rc->show_collision = !rc->show_collision; + if (IsKeyPressed(KEY_P)) rc->show_pathfinding = !rc->show_pathfinding; + if (IsKeyPressed(KEY_M)) rc->show_models = !rc->show_models; + if (IsKeyPressed(KEY_S)) rc->show_safe_spots = !rc->show_safe_spots; + if (IsKeyPressed(KEY_D)) rc->show_debug = !rc->show_debug; + if (IsKeyPressed(KEY_L)) rc->layout_mode = !rc->layout_mode; + if (IsKeyPressed(KEY_F8)) { + const EncounterDef* lab_def = render_lab_def(env); + if (lab_def) { + int enable_lab = !rc->lab_enabled; + if (enable_lab) { + if (rc->pre_sim_mutation_hook) + rc->pre_sim_mutation_hook(rc->pre_sim_mutation_hook_ctx); + rc->lab_prev_paused = rc->is_paused; + rc->lab_prev_human_enabled = rc->human_input.enabled; + render_lab_capture_entry_snapshot(rc, env); + } + rc->lab_enabled = enable_lab; + rc->lab_show_forecast = rc->lab_enabled; + rc->lab_selected_npc_slot = -1; + context_menu_dismiss(&rc->context_menu); + if (rc->lab_enabled) { + rc->is_paused = 1; + rc->human_input.enabled = 1; + human_input_clear_pending(&rc->human_input); + human_input_clear_move(&rc->human_input); + render_populate_entities(rc, env); + render_lab_snap_all_visuals(rc); + } else { + render_lab_restore_controls(rc); + } + fprintf(stderr, "%s lab: %s\n", + lab_def->name, rc->lab_enabled ? "ON" : "OFF"); + } + } + if (IsKeyPressed(KEY_F6)) { + if (render_lab_def(env) && !render_lab_restore_entry_snapshot(rc, env)) { + fprintf(stderr, "lab: no entry snapshot to restore\n"); + } + } + if (IsKeyPressed(KEY_F7) && rc->lab_enabled) { + rc->lab_show_forecast = !rc->lab_show_forecast; + } + if (IsKeyPressed(KEY_F9) && rc->lab_enabled) { + if (render_lab_def(env)) render_lab_apply_line(rc, env, "dump"); + } + + float wheel = GetMouseWheelMove(); + + if (!rc->human_input.enabled && IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) { + Vector2 delta = GetMouseDelta(); + rc->cam_yaw -= delta.x * 0.005f; + rc->cam_pitch += delta.y * 0.005f; + if (rc->cam_pitch < 0.1f) rc->cam_pitch = 0.1f; + if (rc->cam_pitch > 1.4f) rc->cam_pitch = 1.4f; + } + if (IsMouseButtonDown(MOUSE_BUTTON_MIDDLE)) { + Vector2 delta = GetMouseDelta(); + if (rc->human_input.enabled) { + rc->cam_yaw -= delta.x * 0.005f; + rc->cam_pitch += delta.y * 0.005f; + if (rc->cam_pitch < 0.1f) rc->cam_pitch = 0.1f; + if (rc->cam_pitch > 1.4f) rc->cam_pitch = 1.4f; + } else { + float cs = cosf(rc->cam_yaw), sn = sinf(rc->cam_yaw); + rc->cam_target_x += (delta.x * cs + delta.y * sn) * 0.05f; + rc->cam_target_z += (-delta.x * sn + delta.y * cs) * 0.05f; + } + } + if (wheel != 0.0f) { + rc->cam_dist *= (wheel > 0) ? (1.0f / 1.15f) : 1.15f; + if (rc->cam_dist < 5.0f) rc->cam_dist = 5.0f; + if (rc->cam_dist > 200.0f) rc->cam_dist = 200.0f; + } + if (rc->human_input.enabled && rc->entity_count > 0) { + int eidx = rc->gui.gui_entity_idx; + if (eidx < rc->entity_count) { + float tx = (float)rc->sub_x[eidx] / 128.0f; + float tz = -(float)rc->sub_y[eidx] / 128.0f; + float dt = GetFrameTime(); + float lerp = 1.0f - powf(0.85f, dt * 60.0f); + rc->cam_target_x += (tx - rc->cam_target_x) * lerp; + rc->cam_target_z += (tz - rc->cam_target_z) * lerp; + } + } + + if (IsKeyPressed(KEY_ONE)) rc->gui.active_tab = GUI_TAB_INVENTORY; + if (IsKeyPressed(KEY_TWO)) rc->gui.active_tab = GUI_TAB_COMBAT; + if (IsKeyPressed(KEY_THREE)) rc->gui.active_tab = GUI_TAB_PRAYER; + if (IsKeyPressed(KEY_FOUR)) rc->gui.active_tab = GUI_TAB_SPELLBOOK; + if (IsKeyPressed(KEY_FIVE)) rc->gui.active_tab = GUI_TAB_EQUIPMENT; + + { + static const float speed_steps[] = { + 0.5f, + 1.0f, + RENDER_DEFAULT_GAME_TICKS_PER_SECOND, + 5.0f, + 15.0f, + 50.0f, + 0.0f, + }; + static const int num_steps = sizeof(speed_steps) / sizeof(speed_steps[0]); + if (IsKeyPressed(KEY_NINE) || IsKeyPressed(KEY_ZERO)) { + int cur = -1; + for (int i = 0; i < num_steps; i++) { + if (speed_steps[i] == rc->ticks_per_second) { cur = i; break; } + } + if (cur < 0) cur = 2; + if (IsKeyPressed(KEY_NINE) && cur > 0) rc->ticks_per_second = speed_steps[cur - 1]; + if (IsKeyPressed(KEY_ZERO) && cur < num_steps - 1) rc->ticks_per_second = speed_steps[cur + 1]; + } + } + + if (IsKeyPressed(KEY_H)) { + rc->human_input.enabled = !rc->human_input.enabled; + if (!rc->human_input.enabled) { + human_input_clear_pending(&rc->human_input); + human_input_clear_move(&rc->human_input); + human_input_clear_selected_ui_target(&rc->human_input); + context_menu_dismiss(&rc->context_menu); + } + fprintf(stderr, "human control: %s\n", rc->human_input.enabled ? "ON" : "OFF"); + } + + if (rc->human_input.enabled) { + ColosseumState* cs = render_colosseum_state_from_env(env); + if (cs && IsKeyPressed(KEY_B) && cs->sol.grapple_active) + rc->human_input.pending_grapple_slot = cs->sol.grapple_body_slot + 1; + } + + if (IsKeyPressed(KEY_ESCAPE)) { + if (rc->context_menu.visible) { + context_menu_dismiss(&rc->context_menu); + } else if (rc->human_input.cursor_mode != CURSOR_NORMAL) { + human_input_clear_selected_ui_target(&rc->human_input); + } + } + + if (IsKeyPressed(KEY_G)) gui_cycle_entity(&rc->gui); + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + int mx = GetMouseX(); + int my = GetMouseY(); + int pmx, pmy; + gui_mouse_to_panel_space(&rc->gui, mx, my, &pmx, &pmy); + int handled = 0; + + if (!handled && rc->human_input.enabled) { + ColosseumState* cs = render_colosseum_state_from_env(env); + if (cs && cs->modifiers.draft_pending) { + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { + if (cs->modifiers.draft_options[o] < 0) continue; + Rectangle card = render_colosseum_draft_card_rect(o); + if (CheckCollisionPointRec(CLITERAL(Vector2){(float)mx, (float)my}, card)) { + rc->human_input.pending_modifier_select = o + 1; + break; + } + } + handled = 1; + } + } + + if (rc->context_menu.visible) { + ContextMenu* cm = &rc->context_menu; + int menu_h = context_menu_height(cm); + if (mx >= cm->screen_x && mx < cm->screen_x + cm->width && + my >= cm->screen_y && my < cm->screen_y + menu_h) { + int row = context_menu_row_at(cm, mx, my); + if (row >= 0 && row < cm->item_count) + context_menu_execute(rc, env, row); + else + context_menu_dismiss(cm); + } else { + context_menu_dismiss(cm); + } + handled = 1; + } + + if (!handled) + handled = gui_handle_tab_click(&rc->gui, pmx, pmy); + + if (!handled && rc->human_input.enabled && + pmx >= rc->gui.panel_x && pmx < rc->gui.panel_x + rc->gui.panel_w && + pmy >= rc->gui.panel_y && pmy < rc->gui.panel_y + rc->gui.panel_h) { + Player* viewed = (rc->entity_count > 0 && rc->gui.gui_entity_idx < rc->entity_count) + ? render_get_player_ptr(env, rc->gui.gui_entity_idx) : NULL; + + if (viewed) { + switch (rc->gui.active_tab) { + case GUI_TAB_PRAYER: + human_handle_prayer_click(&rc->human_input, &rc->gui, viewed, pmx, pmy); + handled = 1; + break; + case GUI_TAB_SPELLBOOK: + human_handle_spell_click(&rc->human_input, &rc->gui, pmx, pmy); + handled = 1; + break; + case GUI_TAB_COMBAT: + human_handle_combat_click(&rc->human_input, &rc->gui, viewed, pmx, pmy); + handled = 1; + break; + default: + break; + } + } + } + + if (!handled && rc->human_input.enabled) { + int mmx, mmy; + gui_mouse_to_minimap_space(&rc->gui, mx, my, &mmx, &mmy); + if (CheckCollisionPointRec(CLITERAL(Vector2){(float)mmx, (float)mmy}, + render_minimap_spec_orb_rect())) { + human_apply_spec_toggle(&rc->human_input); + handled = 1; + } + } + + if (!handled && rc->human_input.enabled && pmx < rc->gui.panel_x) { + int entity_hit = 0; + for (int ei = 0; ei < rc->entity_count; ei++) { + RenderEntity* ent = &rc->entities[ei]; + if (!render_can_human_attack_entity( + &attack_ctx, ent, ei, rc->gui.gui_entity_idx)) { + continue; + } + if (hull_contains(&rc->entity_hulls[ei], mx, my)) { + render_human_attack_npc_slot( + rc, rc->entities[ei].npc_slot, mx, my); + entity_hit = 1; + break; + } + } + + if (!entity_hit) { + int best_wx, best_wy; + Vector3 hit_point; + Ray ray; + int picked = render_pick_ground_tile( + rc, mx, my, &best_wx, &best_wy, &hit_point, &ray); + rc->debug_ray_origin = ray.position; + rc->debug_ray_dir = ray.direction; + if (picked) { + rc->debug_ray_hit_x = hit_point.x; + rc->debug_ray_hit_y = hit_point.y; + rc->debug_ray_hit_z = hit_point.z; + } + rc->debug_hit_wx = best_wx; + rc->debug_hit_wy = best_wy; + rc->debug_plane_wx = -1; + rc->debug_plane_wy = -1; + if (best_wx >= 0) { + if (rc->human_input.cursor_mode != CURSOR_NORMAL) { + human_input_clear_selected_ui_target(&rc->human_input); + } + rc->human_input.pending_move_x = best_wx; + rc->human_input.pending_move_y = best_wy; + human_input_queue_walk(&rc->human_input, best_wx, best_wy); + human_set_click_cross(&rc->human_input, mx, my, 0); + } + } + } + } + + if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) { + if (rc->human_input.enabled) { + int rmx = GetMouseX(); + int rmy = GetMouseY(); + int prmx, prmy; + gui_mouse_to_panel_space(&rc->gui, rmx, rmy, &prmx, &prmy); + if (prmx < rc->gui.panel_x) { + if (rc->human_input.cursor_mode != CURSOR_NORMAL) + human_input_clear_selected_ui_target(&rc->human_input); + context_menu_build(rc, env, rmx, rmy); + } else if (prmx < rc->gui.panel_x + rc->gui.panel_w && + prmy >= rc->gui.panel_y && + prmy < rc->gui.panel_y + rc->gui.panel_h) { + Player* gui_p = rc->entity_count > 0 && rc->gui.gui_entity_idx < rc->entity_count + ? render_get_player_ptr(env, rc->gui.gui_entity_idx) + : NULL; + if (gui_p) + context_menu_build_gui(rc, gui_p, prmx, prmy, rmx, rmy); + else + context_menu_dismiss(&rc->context_menu); + } else { + context_menu_dismiss(&rc->context_menu); + } + } else if (rc->human_input.cursor_mode != CURSOR_NORMAL) { + human_input_clear_selected_ui_target(&rc->human_input); + } + } + + rc->hover_tile_x = -1; + rc->hover_tile_y = -1; + int hmx = GetMouseX(); + int hmy = GetMouseY(); + int hpx, hpy; + gui_mouse_to_panel_space(&rc->gui, hmx, hmy, &hpx, &hpy); + if (hmx >= 0 && hpx < rc->gui.panel_x && + hmy >= 0 && hmy < RENDER_WINDOW_H) { + render_pick_ground_tile( + rc, hmx, hmy, &rc->hover_tile_x, &rc->hover_tile_y, NULL, NULL); + } +} + +static void render_save_snapshot(RenderClient* rc, OsrsEnv* env) { + if (rc->history_count >= rc->history_capacity) { + int new_capacity = rc->history_capacity * 2; + OsrsEnv* next = (OsrsEnv*)realloc( + rc->history, (size_t)new_capacity * sizeof(OsrsEnv)); + if (!next) { + fprintf(stderr, "render history growth failed at %d snapshots\n", + rc->history_capacity); + abort(); + } + rc->history = next; + rc->history_capacity = new_capacity; + } + rc->history[rc->history_count] = *env; + rc->history_count++; +} + +static void render_restore_snapshot(RenderClient* rc, OsrsEnv* env) { + if (rc->history_cursor < 0 || rc->history_cursor >= rc->history_count) return; + + void* saved_client = env->client; + void* saved_cmap = env->collision_map; + float* saved_ocean_obs = env->ocean_io.agent_obs; + int* saved_ocean_acts = env->ocean_io.agent_actions; + float* saved_ocean_rew = env->ocean_io.agent_rewards; + unsigned char* saved_ocean_term = env->ocean_io.agent_terminals; + + *env = rc->history[rc->history_cursor]; + + env->client = saved_client; + env->collision_map = saved_cmap; + env->ocean_io.agent_obs = saved_ocean_obs; + env->ocean_io.agent_actions = saved_ocean_acts; + env->ocean_io.agent_rewards = saved_ocean_rew; + env->ocean_io.agent_terminals = saved_ocean_term; +} + +static void render_clear_history(RenderClient* rc) { + rc->history_count = 0; + rc->history_cursor = -1; +} + +static void render_push_splat_type(RenderClient* rc, int damage, int pidx, int type); + +static int render_hit_splat_type_for_damage(int damage) { + return damage > 0 ? 1 : 0; +} + +static void render_populate_entities(RenderClient* rc, OsrsEnv* env) { + if (env->encounter_def && env->encounter_state) { + const EncounterDef* def = (const EncounterDef*)env->encounter_def; + if (def->fill_render_entities) { + int count = 0; + def->fill_render_entities( + (EncounterState*)env->encounter_state, + (EncounterContext*)env->encounter_context, + rc->entities, + MAX_RENDER_ENTITIES, + &count); + rc->entity_count = count; + rc->zuk_active = 0; + for (int zi = 0; zi < count; zi++) { + if (rc->entities[zi].npc_def_id == 7706) { rc->zuk_active = 1; break; } + } + } else { + int count = def->get_entity_count( + (EncounterState*)env->encounter_state, + (EncounterContext*)env->encounter_context); + if (count > MAX_RENDER_ENTITIES) count = MAX_RENDER_ENTITIES; + rc->entity_count = count; + for (int i = 0; i < count; i++) { + Player* p = (Player*)def->get_entity( + (EncounterState*)env->encounter_state, + (EncounterContext*)env->encounter_context, + i); + if (p) render_entity_from_player(p, &rc->entities[i]); + } + } + if (def->arena_width > 0 && def->arena_height > 0) { + rc->arena_base_x = def->arena_base_x; + rc->arena_base_y = def->arena_base_y; + rc->arena_width = def->arena_width; + rc->arena_height = def->arena_height; + } + } else { + rc->entity_count = NUM_AGENTS; + for (int i = 0; i < NUM_AGENTS; i++) { + render_entity_from_player(&env->players[i], &rc->entities[i]); + } + } +} + +static int render_default_secondary_for_entity(const RenderEntity* entity) { + if (entity->entity_type != ENTITY_NPC) { + return ANIM_SEQ_IDLE; + } + + const NpcModelMapping* nm = npc_model_lookup((uint16_t)entity->npc_def_id); + return nm ? (int)nm->idle_anim : -1; +} + +static int render_entity_is_visible(const RenderEntity* entity) { + return entity->entity_type != ENTITY_NPC || entity->npc_visible; +} + +static void render_reset_entity_visual_slot(RenderClient* rc, int i) { + anim_playback_reset(&rc->anim[i].primary); + anim_playback_reset(&rc->anim[i].secondary); + rc->primary_event_tick[i] = -1; + rc->last_primary_event_tick[i] = -2; + rc->composites[i].needs_rebuild = 1; + rc->prev_npc_slot[i] = -1; + rc->sub_x[i] = 0; + rc->sub_y[i] = 0; + rc->dest_x[i] = 0; + rc->dest_y[i] = 0; + osrs_render_waypoint_queue_clear(&rc->waypoints[i]); + rc->step_tracker[i] = 0; + rc->visual_moving[i] = 0; + rc->visual_running[i] = 0; + rc->visual_effective_speed[i] = 0.0f; + rc->facing_opponent[i] = 0; + rc->yaw[i] = 0.0f; + rc->target_yaw[i] = 0.0f; + rc->hp_bar_visible_until[i] = 0; + rc->entity_hulls[i].count = 0; + rc->entity_visual_top_y[i] = 0.0f; + rc->entity_visual_mid_y[i] = 0.0f; + for (int s = 0; s < RENDER_SPLATS_PER_PLAYER; s++) + rc->splats[i][s].active = 0; +} + +static RenderVisualSlotSnapshot render_snapshot_entity_visual_slot( + const RenderClient* rc, int i +) { + RenderVisualSlotSnapshot out; + memset(&out, 0, sizeof(out)); + out.anim = rc->anim[i]; + out.primary_event_tick = rc->primary_event_tick[i]; + out.last_primary_event_tick = rc->last_primary_event_tick[i]; + out.sub_x = rc->sub_x[i]; + out.sub_y = rc->sub_y[i]; + out.dest_x = rc->dest_x[i]; + out.dest_y = rc->dest_y[i]; + out.waypoints = rc->waypoints[i]; + out.visual_moving = rc->visual_moving[i]; + out.visual_running = rc->visual_running[i]; + out.visual_effective_speed = rc->visual_effective_speed[i]; + out.step_tracker = rc->step_tracker[i]; + out.yaw = rc->yaw[i]; + out.target_yaw = rc->target_yaw[i]; + out.facing_opponent = rc->facing_opponent[i]; + out.hp_bar_visible_until = rc->hp_bar_visible_until[i]; + memcpy(out.splats, rc->splats[i], sizeof(out.splats)); + return out; +} + +static void render_restore_entity_visual_slot( + RenderClient* rc, int i, const RenderVisualSlotSnapshot* snapshot +) { + rc->anim[i] = snapshot->anim; + rc->primary_event_tick[i] = snapshot->primary_event_tick; + rc->last_primary_event_tick[i] = snapshot->last_primary_event_tick; + rc->sub_x[i] = snapshot->sub_x; + rc->sub_y[i] = snapshot->sub_y; + rc->dest_x[i] = snapshot->dest_x; + rc->dest_y[i] = snapshot->dest_y; + rc->waypoints[i] = snapshot->waypoints; + rc->visual_moving[i] = snapshot->visual_moving; + rc->visual_running[i] = snapshot->visual_running; + rc->visual_effective_speed[i] = snapshot->visual_effective_speed; + rc->step_tracker[i] = snapshot->step_tracker; + rc->yaw[i] = snapshot->yaw; + rc->target_yaw[i] = snapshot->target_yaw; + rc->facing_opponent[i] = snapshot->facing_opponent; + rc->hp_bar_visible_until[i] = snapshot->hp_bar_visible_until; + memcpy(rc->splats[i], snapshot->splats, sizeof(snapshot->splats)); +} + +static void render_seed_entity_visual_slot(RenderClient* rc, int i) { + int size = rc->entities[i].npc_size > 1 ? rc->entities[i].npc_size : 1; + rc->sub_x[i] = rc->entities[i].x * 128 + size * 64; + rc->sub_y[i] = rc->entities[i].y * 128 + size * 64; + rc->dest_x[i] = rc->sub_x[i]; + rc->dest_y[i] = rc->sub_y[i]; + osrs_render_waypoint_queue_clear(&rc->waypoints[i]); + rc->visual_effective_speed[i] = 0.0f; + rc->visual_running[i] = 0; + anim_playback_set_seq(&rc->anim[i].secondary, + render_default_secondary_for_entity(&rc->entities[i]), ANIM_PLAY_LOOP); + rc->prev_npc_slot[i] = rc->entities[i].npc_slot; +} + +static void render_ensure_entity_visual_slots(RenderClient* rc) { + for (int i = 0; i < rc->entity_count; i++) { + if (rc->sub_x[i] == 0 && rc->sub_y[i] == 0) { + render_seed_entity_visual_slot(rc, i); + } + } +} + +static void render_reset_episode_visual_state(RenderClient* rc, OsrsEnv* env) { + render_clear_history(rc); + effect_clear_all(rc->effects); + flight_clear_all(rc); + gui_reset_inventory_ui_state(&rc->gui); + rc->client_tick_accumulator = 0.0; + + render_populate_entities(rc, env); + for (int i = 0; i < MAX_RENDER_ENTITIES; i++) { + render_reset_entity_visual_slot(rc, i); + if (i < rc->entity_count) { + render_seed_entity_visual_slot(rc, i); + } + } + rc->prev_entity_count = rc->entity_count; +} + +static void render_pre_tick(RenderClient* rc, OsrsEnv* env) { + (void)rc; (void)env; +} + +static void render_post_tick(RenderClient* rc, OsrsEnv* env) { + RenderEntity previous_entities[MAX_RENDER_ENTITIES]; + RenderVisualSlotSnapshot previous_visuals[MAX_RENDER_ENTITIES]; + int previous_used[MAX_RENDER_ENTITIES] = {0}; + int new_identity[MAX_RENDER_ENTITIES] = {0}; + int became_visible[MAX_RENDER_ENTITIES] = {0}; + int previous_count = rc->entity_count; + memcpy(previous_entities, rc->entities, + (size_t)previous_count * sizeof(previous_entities[0])); + for (int i = 0; i < previous_count; i++) + previous_visuals[i] = render_snapshot_entity_visual_slot(rc, i); + + render_populate_entities(rc, env); + + for (int i = 0; i < rc->entity_count; i++) { + int previous_idx = render_entity_find_previous_identity_index( + previous_entities, previous_count, previous_used, &rc->entities[i]); + if (previous_idx >= 0) { + previous_used[previous_idx] = 1; + became_visible[i] = + render_entity_is_visible(&rc->entities[i]) && + !render_entity_is_visible(&previous_entities[previous_idx]); + if (previous_idx != i) { + render_restore_entity_visual_slot(rc, i, &previous_visuals[previous_idx]); + rc->composites[i].needs_rebuild = 1; + } + } else { + new_identity[i] = 1; + render_reset_entity_visual_slot(rc, i); + } + rc->prev_npc_slot[i] = rc->entities[i].npc_slot; + } + for (int i = rc->entity_count; i < rc->prev_entity_count; i++) + rc->prev_npc_slot[i] = -1; + rc->prev_entity_count = rc->entity_count; + + for (int i = 0; i < rc->entity_count; i++) { + RenderEntity* p = &rc->entities[i]; + + int size = p->npc_size > 1 ? p->npc_size : 1; + int new_dest_x = p->x * 128 + size * 64; + int new_dest_y = p->y * 128 + size * 64; + + if (osrs_render_should_seed_visual_position( + render_entity_is_visible(p), + new_identity[i], + became_visible[i], + p->render_movement_kind)) { + render_seed_entity_visual_slot(rc, i); + } + + int moved = (new_dest_x != rc->dest_x[i] || new_dest_y != rc->dest_y[i]); + + if (moved) { + osrs_render_waypoint_push( + &rc->waypoints[i], + (float)new_dest_x, (float)new_dest_y, p->is_running); + } + rc->dest_x[i] = new_dest_x; + rc->dest_y[i] = new_dest_y; + + RenderEntityFacingMode facing_mode = render_entity_select_facing_mode(p, moved); + if (facing_mode == RENDER_ENTITY_FACE_ATTACK_TARGET) { + rc->facing_opponent[i] = 1; + } else if (facing_mode == RENDER_ENTITY_FACE_MOVEMENT) { + float dx = (float)(new_dest_x - rc->sub_x[i]); + float dy = (float)(new_dest_y - rc->sub_y[i]); + if (dx != 0.0f || dy != 0.0f) + rc->target_yaw[i] = atan2f(-dx, dy); + rc->facing_opponent[i] = 0; + } else { + int face_x = p->dest_x * 128 + 64; + int face_y = p->dest_y * 128 + 64; + float dx = (float)(face_x - rc->sub_x[i]); + float dy = (float)(face_y - rc->sub_y[i]); + if (dx != 0.0f || dy != 0.0f) + rc->target_yaw[i] = atan2f(-dx, dy); + rc->facing_opponent[i] = 0; + } + + if (p->npc_def_id == 7707) { + rc->target_yaw[i] = 3.14159265f; + rc->facing_opponent[i] = 0; + } + + int render_hit_count = p->render_hit_count; + if (render_hit_count == 0 && p->hit_landed_this_tick) + render_hit_count = 1; + if (render_hit_count < 0 || render_hit_count > ENCOUNTER_RENDER_HITS_MAX) { + fprintf(stderr, "invalid render hit count\n"); + abort(); + } + if (render_hit_count > 0) { + rc->hp_bar_visible_until[i] = env->tick + 10; + for (int h = 0; h < render_hit_count; h++) { + int damage = p->render_hit_count > 0 + ? p->render_hit_damage[h] : p->hit_damage; + render_push_splat_type( + rc, damage, i, render_hit_splat_type_for_damage(damage)); + } + } + if (p->npc_anim_id >= 0 || + p->attack_style_this_tick != ATTACK_STYLE_NONE || + p->cast_veng_this_tick || + p->ate_food_this_tick || + p->ate_karambwan_this_tick || + p->used_special_this_tick || + render_hit_count > 0) { + rc->primary_event_tick[i] = env->tick; + } + } + + int ct = rc->effect_client_tick_counter; + for (int i = 0; i < rc->entity_count; i++) { + RenderEntity* p = &rc->entities[i]; + int target_i; + if (p->attack_target_entity_idx >= 0) { + target_i = p->attack_target_entity_idx; + } else if (rc->entity_count == 2) { + target_i = 1 - i; + } else { + target_i = (i == 0) ? 1 : 0; + } + if (target_i < 0 || target_i >= rc->entity_count) continue; + RenderEntity* t = &rc->entities[target_i]; + + int has_encounter_overlay = (env->encounter_def && + ((const EncounterDef*)env->encounter_def)->render_post_tick); + + if (!has_encounter_overlay) { + if (p->attack_style_this_tick == ATTACK_STYLE_MAGIC) { + uint8_t wpn = p->equipped[GEAR_SLOT_WEAPON]; + int dist = render_pvp_distance_to_target(p, t); + int duration_ticks = pvp_magic_hit_delay(dist) * 30; + const OsrsCombatProjectileProfile* profile = + osrs_combat_visual_magic_projectile_profile(wpn); + if (profile) { + render_spawn_profile_projectile(rc, profile, + p->x, p->y, t->x, t->y, + 0, duration_ticks, 40 * 4, 30 * 4, 16); + } else { + profile = osrs_combat_visual_spell_projectile(p->magic_type_this_tick); + if (profile && profile->travel_spotanim_id >= 0) { + render_spawn_profile_projectile(rc, profile, + t->x, t->y, t->x, t->y, + 0, 56, 43 * 4, 0, 16); + } + } + } + + if (p->attack_style_this_tick == ATTACK_STYLE_RANGED) { + uint8_t wpn = p->equipped[GEAR_SLOT_WEAPON]; + int dist = render_pvp_distance_to_target(p, t); + const OsrsCombatProjectileProfile* special_profile = + p->used_special_this_tick + ? osrs_combat_visual_ranged_special_projectile_profile(wpn) + : NULL; + const OsrsCombatProjectileProfile* base_profile = + osrs_combat_visual_ranged_projectile_profile( + wpn, OSRS_COMBAT_PROJECTILE_BOLT); + int duration_ticks = p->used_special_this_tick + ? pvp_ranged_hit_delay_for_weapon( + dist, 1, render_pvp_ranged_spec_weapon_for_item(wpn)) * 30 + : pvp_ranged_hit_delay(dist) * 30; + if (special_profile) { + render_spawn_profile_projectile(rc, special_profile, + p->x, p->y, t->x, t->y, + 0, duration_ticks, 43 * 4, 31 * 4, 16); + } + if (!special_profile || special_profile->travel_spotanim_id < 0) { + render_spawn_profile_projectile(rc, base_profile, + p->x, p->y, t->x, t->y, + 0, duration_ticks, 43 * 4, 31 * 4, 16); + } + } + } + + if (p->hit_landed_this_tick) { + RenderEntity* att; + if (i == 0) { + att = t; + } else { + att = &rc->entities[0]; + } + + uint8_t att_wpn = att->equipped[GEAR_SLOT_WEAPON]; + const OsrsCombatProjectileProfile* att_magic_profile = + osrs_combat_visual_magic_projectile_profile(att_wpn); + int att_is_powered_staff = att_magic_profile && + att_magic_profile->impact_spotanim_id >= 0; + + int overlay_handles_powered_staff_impact = has_encounter_overlay && + att_is_powered_staff && att->attack_style_this_tick == ATTACK_STYLE_MAGIC; + + if (overlay_handles_powered_staff_impact) continue; + + if (att_is_powered_staff && + att->attack_style_this_tick == ATTACK_STYLE_MAGIC) { + if (p->hit_was_successful) { + effect_spawn_spotanim(rc->effects, att_magic_profile->impact_spotanim_id, + p->x, p->y, ct, rc->spotanims, rc->anim_cache, + rc->model_cache, rc->npc_model_cache, + rc->projectile_model_cache); + } else { + effect_spawn_spotanim(rc->effects, GFX_SPLASH, + p->x, p->y, ct, rc->spotanims, rc->anim_cache, + rc->model_cache, rc->npc_model_cache, + rc->projectile_model_cache); + } + } else { + int spell = p->hit_spell_type; + if (spell > 0) { + const OsrsCombatProjectileProfile* profile = + osrs_combat_visual_spell_projectile(spell); + if (!profile || profile->impact_spotanim_id < 0) { + fprintf(stderr, "render: missing spell impact visual %d\n", spell); + abort(); + } + float fx = (float)p->x * 128.0f + (float)p->npc_size * 64.0f; + float fy = (float)p->y * 128.0f + (float)p->npc_size * 64.0f; + if (p->hit_was_successful) { + effect_spawn_spotanim_subtile(rc->effects, profile->impact_spotanim_id, + fx, fy, ct, rc->spotanims, rc->anim_cache, + rc->model_cache, + rc->npc_model_cache, rc->projectile_model_cache); + } else { + effect_spawn_spotanim_subtile(rc->effects, GFX_SPLASH, + fx, fy, ct, rc->spotanims, rc->anim_cache, + rc->model_cache, + rc->npc_model_cache, rc->projectile_model_cache); + } + } + } + } + } + + if (env->encounter_def && env->encounter_state) { + const EncounterDef* edef = (const EncounterDef*)env->encounter_def; + if (edef->render_post_tick) { + edef->render_post_tick( + (EncounterState*)env->encounter_state, + (EncounterContext*)env->encounter_context, + &rc->encounter_overlay); + + EncounterOverlay* ov = &rc->encounter_overlay; + for (int i = 0; i < ov->projectile_count; i++) { + if (!ov->projectiles[i].active) continue; + int src_sz = ov->projectiles[i].src_size > 0 ? ov->projectiles[i].src_size : ov->boss_size; + int dst_sz = ov->projectiles[i].dst_size > 0 ? ov->projectiles[i].dst_size : 1; + float fixed_sx = (float)ov->projectiles[i].src_x + (float)(src_sz - 1) / 2.0f + 0.5f; + float fixed_sy = (float)ov->projectiles[i].src_y + (float)(src_sz - 1) / 2.0f + 0.5f; + float fixed_dx = (float)ov->projectiles[i].dst_x + (float)(dst_sz - 1) / 2.0f + 0.5f; + float fixed_dy = (float)ov->projectiles[i].dst_y + (float)(dst_sz - 1) / 2.0f + 0.5f; + float sx = fixed_sx; + float sy = fixed_sy; + float dx = fixed_dx; + float dy = fixed_dy; + if (!render_resolve_projectile_anchor( + rc, ov->projectiles[i].source_kind, + ov->projectiles[i].source_npc_slot, + fixed_sx, fixed_sy, &sx, &sy)) { + fprintf(stderr, "render: projectile source anchor missing kind=%d npc_slot=%d\n", + ov->projectiles[i].source_kind, + ov->projectiles[i].source_npc_slot); + abort(); + } + if (!render_resolve_projectile_anchor( + rc, ov->projectiles[i].target_kind, + ov->projectiles[i].target_npc_slot, + fixed_dx, fixed_dy, &dx, &dy)) { + fprintf(stderr, "render: projectile target anchor missing kind=%d npc_slot=%d\n", + ov->projectiles[i].target_kind, + ov->projectiles[i].target_npc_slot); + abort(); + } + + int dur = ov->projectiles[i].duration_ticks > 0 ? ov->projectiles[i].duration_ticks : 35; + int sh = ov->projectiles[i].start_h > 0 ? ov->projectiles[i].start_h : 85; + int eh = ov->projectiles[i].end_h > 0 ? ov->projectiles[i].end_h : 40; + int cv = ov->projectiles[i].curve != 0 ? ov->projectiles[i].curve : 16; + float arc = ov->projectiles[i].arc_height; + int trk = ov->projectiles[i].tracks_target; + + if (ov->projectiles[i].style == 3 || ov->projectiles[i].style == 4) { + dx += 0.5f; + dy += 0.5f; + } + + flight_spawn(rc, sx, sy, dx, dy, + ov->projectiles[i].style, ov->projectiles[i].damage, + dur, sh, eh, cv, arc, trk, + ov->projectiles[i].model_id, + ov->projectiles[i].anim_id, + ov->projectiles[i].travel_gfx_id, + ov->projectiles[i].launch_gfx_id, + ov->projectiles[i].impact_gfx_id, + ov->projectiles[i].start_delay, + ov->projectiles[i].motion_mode, + ov->projectiles[i].offset_x, + ov->projectiles[i].offset_y, + ov->projectiles[i].offset_z, + ov->projectiles[i].source_kind, + ov->projectiles[i].source_npc_slot, + ov->projectiles[i].target_kind, + ov->projectiles[i].target_npc_slot); + } + } + } +} + +static void render_client_tick(RenderClient* rc, int player_idx) { + if (rc->waypoints[player_idx].length == 0) { + rc->visual_moving[player_idx] = 0; + rc->visual_running[player_idx] = 0; + rc->visual_effective_speed[player_idx] = 0.0f; + rc->step_tracker[player_idx] = 0; + } else { + int stall = 0; + if (rc->anim[player_idx].primary.seq_id >= 0 && + rc->anim[player_idx].primary.completed_loops == 0) { + render_anim_playback_resolve(rc, &rc->anim[player_idx].primary, + rc->composites[player_idx].base_vert_count); + stall = render_sequence_stalls_movement( + rc->anim[player_idx].primary.sequence); + } + + if (stall) { + rc->step_tracker[player_idx]++; + rc->visual_moving[player_idx] = 0; + rc->visual_running[player_idx] = 0; + rc->visual_effective_speed[player_idx] = 0.0f; + } else { + int speed = 0; + float dir_dx = 0.0f, dir_dy = 0.0f; + rc->visual_moving[player_idx] = + osrs_render_waypoint_advance_one_client_tick( + &rc->waypoints[player_idx], + &rc->sub_x[player_idx], + &rc->sub_y[player_idx], + &rc->step_tracker[player_idx], + &speed, + &dir_dx, + &dir_dy); + rc->visual_effective_speed[player_idx] = (float)speed; + rc->visual_running[player_idx] = + osrs_render_speed_uses_run_pose((float)speed); + + if (!rc->facing_opponent[player_idx] && rc->entities[player_idx].npc_def_id != 7707) { + if (dir_dx != 0.0f || dir_dy != 0.0f) { + rc->target_yaw[player_idx] = atan2f(-dir_dx, dir_dy); + } + } + } + } + + { + if (rc->facing_opponent[player_idx]) { + int opp; + if (rc->entities[player_idx].attack_target_entity_idx >= 0) { + opp = rc->entities[player_idx].attack_target_entity_idx; + } else { + opp = (rc->entity_count == 2) ? (1 - player_idx) : (player_idx == 0 ? 1 : 0); + } + float dx = (float)(rc->sub_x[opp] - rc->sub_x[player_idx]); + float dy = (float)(rc->sub_y[opp] - rc->sub_y[player_idx]); + if (dx != 0.0f || dy != 0.0f) { + rc->target_yaw[player_idx] = atan2f(-dx, dy); + } + } + + float turn_speed = 32.0f / 2048.0f * 2.0f * 3.14159265f; + float diff = rc->target_yaw[player_idx] - rc->yaw[player_idx]; + + while (diff > 3.14159265f) diff -= 2.0f * 3.14159265f; + while (diff < -3.14159265f) diff += 2.0f * 3.14159265f; + + if (fabsf(diff) <= turn_speed) { + rc->yaw[player_idx] = rc->target_yaw[player_idx]; + } else if (diff > 0.0f) { + rc->yaw[player_idx] += turn_speed; + } else { + rc->yaw[player_idx] -= turn_speed; + } + + while (rc->yaw[player_idx] > 3.14159265f) rc->yaw[player_idx] -= 2.0f * 3.14159265f; + while (rc->yaw[player_idx] < -3.14159265f) rc->yaw[player_idx] += 2.0f * 3.14159265f; + } + + int new_secondary; + if (rc->entities[player_idx].entity_type == ENTITY_NPC) { + const NpcModelMapping* nm = npc_model_lookup( + (uint16_t)rc->entities[player_idx].npc_def_id); + if (nm) { + if (!rc->visual_moving[player_idx]) { + new_secondary = (int)nm->idle_anim; + } else if (osrs_render_speed_uses_run_pose( + rc->visual_effective_speed[player_idx]) && + nm->run_anim != 65535) { + new_secondary = (int)nm->run_anim; + } else { + new_secondary = nm->walk_anim != 65535 + ? (int)nm->walk_anim + : (int)nm->idle_anim; + } + } else { + new_secondary = -1; + } + } else { + new_secondary = render_select_secondary(rc, player_idx); + } + anim_playback_set_seq( + &rc->anim[player_idx].secondary, new_secondary, ANIM_PLAY_LOOP); + + int comp_vc = rc->composites[player_idx].base_vert_count; + if (rc->anim[player_idx].secondary.seq_id >= 0) { + render_anim_playback_resolve(rc, &rc->anim[player_idx].secondary, comp_vc); + anim_playback_advance(&rc->anim[player_idx].secondary); + } + if (rc->anim[player_idx].primary.seq_id >= 0) { + render_anim_playback_resolve(rc, &rc->anim[player_idx].primary, comp_vc); + anim_playback_advance(&rc->anim[player_idx].primary); + } +} + +static void render_get_visual_pos( + RenderClient* rc, int player_idx, + float* out_x, float* out_z, float* out_ground +) { + float tile_x = (float)rc->sub_x[player_idx] / 128.0f; + float tile_y = (float)rc->sub_y[player_idx] / 128.0f; + + *out_x = tile_x; + *out_z = -tile_y; + + if (rc->terrain) { + *out_ground = terrain_height_avg(rc->terrain, + (int)tile_x, (int)tile_y); + } else { + *out_ground = 2.0f; + } +} + +static void render_update_splats_client_tick(RenderClient* rc) { + for (int p = 0; p < rc->entity_count; p++) { + for (int i = 0; i < RENDER_SPLATS_PER_PLAYER; i++) { + HitSplat* s = &rc->splats[p][i]; + if (!s->active) continue; + s->hitmark_move -= 0.25; + if (s->hitmark_move < -5.0) { + s->hitmark_move = -5.0; + } + s->ticks_remaining--; + if (s->ticks_remaining <= 0) { + s->active = 0; + } + } + } +} + +static void render_push_splat_type(RenderClient* rc, int damage, int pidx, int type) { + HitSplat splat = { + .active = 1, + .damage = damage, + .type = type, + .hitmark_move = 5.0, + .hitmark_trans = 230, + .ticks_remaining = 70, + }; + for (int i = 0; i < RENDER_SPLATS_PER_PLAYER; i++) { + if (!rc->splats[pidx][i].active) { + rc->splats[pidx][i] = splat; + return; + } + } + int oldest = 0; + for (int i = 1; i < RENDER_SPLATS_PER_PLAYER; i++) { + if (rc->splats[pidx][i].ticks_remaining < rc->splats[pidx][oldest].ticks_remaining) + oldest = i; + } + rc->splats[pidx][oldest] = splat; +} + +static void render_draw_hitmark(RenderClient* rc, int cx, int cy, int damage, int opacity, int type) { + unsigned char a = (unsigned char)(opacity > 255 ? 255 : (opacity < 0 ? 0 : opacity)); + int sprite_idx = (type >= 0 && type < 5) ? type : ((damage > 0) ? 1 : 0); + + if (rc->hitmark_sprites_loaded) { + Texture2D tex = rc->hitmark_sprites[sprite_idx]; + float draw_x = (float)cx - (float)tex.width / 2.0f; + float draw_y = (float)cy - (float)tex.height / 2.0f; + DrawTexture(tex, (int)draw_x, (int)draw_y, (Color){ 255, 255, 255, a }); + } else { + Color bg = (sprite_idx == 4) ? (Color){ 220, 185, 45, a } : + ((damage > 0) ? (Color){ 175, 25, 25, a } : (Color){ 65, 105, 225, a }); + DrawCircle(cx, cy, 12.0f, bg); + } + + const char* txt = TextFormat("%d", damage); + int tw = MeasureText(txt, 10); + DrawText(txt, cx - tw / 2 + 1, cy - 4, 10, (Color){ 0, 0, 0, a }); + DrawText(txt, cx - tw / 2, cy - 5, 10, (Color){ 255, 255, 255, a }); +} + +static void render_splat_slot_offset(int slot, int* dx, int* dy) { + switch (slot) { + case 0: *dx = 0; *dy = 0; break; + case 1: *dx = 0; *dy = -20; break; + case 2: *dx = -15; *dy = -10; break; + case 3: *dx = 15; *dy = -10; break; + default: *dx = 0; *dy = 0; break; + } +} + +static void render_splat_screen_pos( + int base_x, + int base_y, + int slot, + const HitSplat* s, + int* out_x, + int* out_y +) { + int slot_dx, slot_dy; + render_splat_slot_offset(slot, &slot_dx, &slot_dy); + *out_x = base_x + slot_dx; + *out_y = base_y + slot_dy + (int)s->hitmark_move; +} + +static float render_overhead_anchor_y(float visual_top_y) { + return visual_top_y + 15.0f / 128.0f; +} + +static const char* inferno_npc_name(int npc_def_id) { + switch (npc_def_id) { + case 7691: return "Jal-Nib"; + case 7692: return "Jal-MejRah"; + case 7693: return "Jal-Ak"; + case 7694: return "Jal-AkRek-Ket"; + case 7695: return "Jal-AkRek-Xil"; + case 7696: return "Jal-AkRek-Mej"; + case 7697: return "Jal-ImKot"; + case 7698: return "Jal-Xil"; + case 7699: return "Jal-Zek"; + case 7700: return "JalTok-Jad"; + case 7701: return "Yt-HurKot"; + case 7706: return "TzKal-Zuk"; + case 7707: return "Ancestral Glyph"; + case 7708: return "Jal-MejJak"; + default: return NULL; + } +} + +static void render_draw_panel_npc(int x, int y, RenderEntity* p, OsrsEnv* env) { + int line_h = 14; + + const char* npc_name = NULL; + Color name_color = COLOR_TEXT; + + if (p->npc_def_id == 2042) { npc_name = "Zulrah [GREEN]"; name_color = GREEN; } + else if (p->npc_def_id == 2043) { npc_name = "Zulrah [RED]"; name_color = RED; } + else if (p->npc_def_id == 2044) { npc_name = "Zulrah [BLUE]"; name_color = CLITERAL(Color){ 80, 140, 255, 255 }; } + + if (!npc_name) { + const char* inf_name = inferno_npc_name(p->npc_def_id); + if (inf_name) { + npc_name = inf_name; + name_color = CLITERAL(Color){ 255, 120, 50, 255 }; + } + } + + if (!npc_name) { + const char* colo_name = colosseum_npc_name(p->npc_def_id); + if (colo_name) { + npc_name = colo_name; + name_color = CLITERAL(Color){ 230, 200, 110, 255 }; + } + } + + if (!npc_name) npc_name = TextFormat("NPC %d", p->npc_def_id); + + DrawText(npc_name, x, y, 14, name_color); + y += line_h + 4; + + DrawText(TextFormat("HP: %d / %d", p->current_hitpoints, p->base_hitpoints), x, y, 10, COLOR_TEXT); + y += line_h; + DrawText(TextFormat("Pos: (%d, %d)", p->x, p->y), x, y, 10, COLOR_TEXT_DIM); + y += line_h; + + if (env->encounter_def && env->encounter_state) { + const EncounterDef* edef = (const EncounterDef*)env->encounter_def; + + if (strcmp(edef->name, "zulrah") == 0) { + ZulrahState* zs = (ZulrahState*)env->encounter_state; + DrawText(TextFormat("Visible: %s", zs->zulrah_visible ? "yes" : "no"), x, y, 10, COLOR_TEXT_DIM); + y += line_h; + DrawText(TextFormat("Phase: %d Surface: %d %s", zs->phase_timer, zs->surface_timer, + zs->is_diving ? "DIVING" : ""), x, y, 10, zs->is_diving ? COLOR_FREEZE : COLOR_TEXT_DIM); + y += line_h; + const char* rot_names[] = { "Magma A", "Magma B", "Serp", "Tanz" }; + const char* rot_name = (zs->rotation_index >= 0 && zs->rotation_index < 4) + ? rot_names[zs->rotation_index] : "???"; + DrawText(TextFormat("Rotation: %s (phase %d/%d)", rot_name, + zs->phase_index + 1, + (zs->rotation_index >= 0 && zs->rotation_index < 4) + ? ZUL_ROT_LENGTHS[zs->rotation_index] : 0), + x, y, 10, COLOR_TEXT); + y += line_h; + DrawText(TextFormat("Action: %d/%d (timer %d)", zs->action_index, + zs->action_progress, zs->action_timer), x, y, 10, COLOR_TEXT_DIM); + y += line_h; + + int snakes = 0, clouds = 0; + for (int i = 0; i < ZUL_MAX_SNAKELINGS; i++) + if (zs->snakelings[i].active) snakes++; + for (int i = 0; i < ZUL_MAX_CLOUDS; i++) + if (zs->clouds[i].active) clouds++; + DrawText(TextFormat("Snakelings: %d Clouds: %d", snakes, clouds), x, y, 10, COLOR_TEXT_DIM); + } else if (strcmp(edef->name, "inferno") == 0) { + InfernoState* is = (InfernoState*)env->encounter_state; + DrawText(TextFormat("Wave: %d / %d", is->wave + 1, INF_NUM_WAVES), x, y, 10, COLOR_TEXT); + y += line_h; + + int active_npcs = 0; + for (int i = 0; i < INF_MAX_NPCS; i++) + if (is->npcs[i].active) active_npcs++; + DrawText(TextFormat("NPCs: %d active", active_npcs), x, y, 10, COLOR_TEXT_DIM); + y += line_h; + + int pillars_alive = 0; + for (int i = 0; i < INF_NUM_PILLARS; i++) + if (is->pillars[i].active) pillars_alive++; + DrawText(TextFormat("Pillars: %d / %d", pillars_alive, INF_NUM_PILLARS), x, y, 10, COLOR_TEXT_DIM); + } + } + (void)y; +} + +static Camera3D render_build_3d_camera(RenderClient* rc) { + Camera3D cam = { 0 }; + float cx = rc->cam_target_x; + float cz = rc->cam_target_z; + float cy = (rc->terrain) ? terrain_height_at(rc->terrain, (int)cx, (int)(-cz)) : 2.0f; + + float d = rc->cam_dist; + float px = cx + d * cosf(rc->cam_pitch) * sinf(rc->cam_yaw); + float py = cy + d * sinf(rc->cam_pitch); + float pz = cz + d * cosf(rc->cam_pitch) * cosf(rc->cam_yaw); + + cam.position = (Vector3){ px, py, pz }; + cam.target = (Vector3){ cx, cy, cz }; + cam.up = (Vector3){ 0.0f, 1.0f, 0.0f }; + cam.fovy = 60.0f; + cam.projection = CAMERA_PERSPECTIVE; + return cam; +} + +static const char* render_debug_attack_style_name(int style) { + switch (style) { + case ATTACK_STYLE_NONE: return "NONE"; + case ATTACK_STYLE_MELEE: return "MEL"; + case ATTACK_STYLE_RANGED: return "RNG"; + case ATTACK_STYLE_MAGIC: return "MAG"; + default: return "???"; + } +} + +static Color render_debug_attack_style_color(int style) { + switch (style) { + case ATTACK_STYLE_MELEE: return RED; + case ATTACK_STYLE_RANGED: return GREEN; + case ATTACK_STYLE_MAGIC: return BLUE; + default: return WHITE; + } +} + +static void render_draw_centered_debug_line( + const char* text, int center_x, int* y, int font_size, Color color +) { + int text_width = MeasureText(text, font_size); + DrawText(text, center_x - text_width / 2, *y, font_size, color); + *y += font_size + 1; +} + +static void render_draw_entity_debug_metadata( + const RenderEntity* entity, Vector2 screen_head, + int world_offset_x, int world_offset_y +) { + int y = (int)screen_head.y + 10; + int x = (int)screen_head.x; + int font_size = 10; + + if (entity->debug_npc_type_name) { + render_draw_centered_debug_line( + entity->debug_npc_type_name, x, &y, font_size, COLOR_TEXT); + } + + render_draw_centered_debug_line( + TextFormat("tile (%d,%d) w(%d,%d)", + entity->x, entity->y, + entity->x + world_offset_x, entity->y + world_offset_y), + x, &y, font_size, (Color){ 0, 255, 128, 255 }); + + render_draw_centered_debug_line( + TextFormat("HP:%d/%d", entity->current_hitpoints, entity->base_hitpoints), + x, &y, font_size, COLOR_TEXT); + + int style = entity->debug_attack_style; + render_draw_centered_debug_line( + TextFormat("ATK:%d %s", + entity->debug_attack_timer, + render_debug_attack_style_name(style)), + x, &y, font_size, render_debug_attack_style_color(style)); + + if (entity->frozen_ticks > 0) { + render_draw_centered_debug_line( + TextFormat("FRZ:%d", entity->frozen_ticks), + x, &y, font_size, CLITERAL(Color){100, 200, 255, 255}); + } + + if (entity->debug_manticore_state_active) { + render_draw_centered_debug_line( + TextFormat("MC:%d %s/%s/%s", + entity->debug_manticore_cycle_step, + render_debug_attack_style_name(entity->debug_manticore_orb_style[0]), + render_debug_attack_style_name(entity->debug_manticore_orb_style[1]), + render_debug_attack_style_name(entity->debug_manticore_orb_style[2])), + x, &y, font_size, YELLOW); + } +} + +static int render_select_primary(RenderEntity* p) { + if (p->current_hitpoints <= 0) return ANIM_SEQ_DEATH; + + if (p->attack_style_this_tick != ATTACK_STYLE_NONE) { + if (p->attack_style_this_tick == ATTACK_STYLE_MAGIC) { + uint8_t wpn = p->equipped[GEAR_SLOT_WEAPON]; + return osrs_combat_visual_magic_attack_anim_for_fight_style( + wpn, p->fight_style, p->used_special_this_tick, ANIM_SEQ_CAST_BARRAGE); + } + return osrs_combat_visual_weapon_attack_anim_for_fight_style( + p->equipped[GEAR_SLOT_WEAPON], + (AttackStyle)p->attack_style_this_tick, + p->fight_style, + p->used_special_this_tick, + OSRS_PLAYER_UNARMED_ATTACK_ANIM); + } + + if (p->ate_food_this_tick || p->ate_karambwan_this_tick) { + return ANIM_SEQ_EAT; + } + + if (p->cast_veng_this_tick) { + return ANIM_SEQ_CAST_VENG; + } + + if (p->hit_landed_this_tick && + render_offhand_uses_shield_block_anim(p->equipped[GEAR_SLOT_SHIELD])) { + return ANIM_SEQ_BLOCK_SHIELD; + } + + return -1; +} + +typedef enum { + RENDER_ITEM_READY_ANIM, + RENDER_ITEM_WALK_ANIM, + RENDER_ITEM_RUN_ANIM, +} RenderItemAnimKind; + +static uint32_t render_item_anim_id(uint16_t item_id, RenderItemAnimKind kind) { + switch (kind) { + case RENDER_ITEM_READY_ANIM: + return item_render_ready_anim(item_id); + case RENDER_ITEM_WALK_ANIM: + return item_render_walk_anim(item_id); + case RENDER_ITEM_RUN_ANIM: + return item_render_run_anim(item_id); + } + abort(); +} + +static int render_weapon_anim_or_fallback( + RenderClient* rc, const RenderEntity* p, RenderItemAnimKind kind, int fallback +) { + uint8_t weapon_idx = p->equipped[GEAR_SLOT_WEAPON]; + if (weapon_idx >= NUM_ITEMS) return fallback; + + uint16_t item_id = ITEM_DATABASE[weapon_idx].item_id; + uint32_t anim_id = render_item_anim_id(item_id, kind); + if (anim_id == ITEM_RENDER_MODEL_MISSING || anim_id > UINT16_MAX) return fallback; + if (!render_get_anim_sequence(rc, (uint16_t)anim_id)) return fallback; + return (int)anim_id; +} + +static int render_select_secondary(RenderClient* rc, int player_idx) { + const RenderEntity* p = &rc->entities[player_idx]; + if (!rc->visual_moving[player_idx]) { + return render_weapon_anim_or_fallback(rc, p, RENDER_ITEM_READY_ANIM, ANIM_SEQ_IDLE); + } + if (osrs_render_speed_uses_run_pose(rc->visual_effective_speed[player_idx])) { + return render_weapon_anim_or_fallback(rc, p, RENDER_ITEM_RUN_ANIM, ANIM_SEQ_RUN); + } + return render_weapon_anim_or_fallback(rc, p, RENDER_ITEM_WALK_ANIM, ANIM_SEQ_WALK); +} + +static OsrsModelAppendResult composite_try_add_model(PlayerComposite* comp, OsrsModel* om) { + if (!comp) { + fprintf(stderr, "render: composite_try_add_model got null composite\n"); + abort(); + } + + int bv_off = comp->base_vert_count; + int fc_off = comp->face_count; + + OsrsModelAppendResult append_result = osrs_model_append_check( + bv_off, fc_off, om, COMPOSITE_MAX_BASE_VERTS, COMPOSITE_MAX_FACES); + if (append_result != OSRS_MODEL_APPEND_OK) { + return append_result; + } + if (!comp->mesh.colors || !om->mesh.colors || !om->face_indices || + !om->vertex_skins) { + fprintf(stderr, "render: composite model %u has incomplete geometry buffers\n", + om->model_id); + abort(); + } + + memcpy(comp->base_vertices + bv_off * 3, + om->base_vertices, om->base_vert_count * 3 * sizeof(int16_t)); + + memcpy(comp->vertex_skins + bv_off, + om->vertex_skins, om->base_vert_count); + + int model_faces = om->mesh.triangleCount; + if (om->base_face_alphas) { + memcpy(comp->base_face_alphas + fc_off, om->base_face_alphas, model_faces); + } else { + memset(comp->base_face_alphas + fc_off, 0, model_faces); + } + if (om->face_alpha_labels) { + memcpy(comp->face_alpha_labels + fc_off, om->face_alpha_labels, model_faces); + for (int f = 0; f < model_faces; f++) { + if (om->face_alpha_labels[f] != 255) { + comp->has_face_alpha = 1; + break; + } + } + } else { + memset(comp->face_alpha_labels + fc_off, 255, model_faces); + } + + int nfi = om->mesh.triangleCount * 3; + for (int f = 0; f < nfi; f++) { + comp->face_indices[fc_off * 3 + f] = om->face_indices[f] + (uint16_t)bv_off; + } + + int exp_off = fc_off * 3; + memcpy(comp->mesh.colors + exp_off * 4, + om->mesh.colors, om->mesh.triangleCount * 3 * 4); + if (comp->mesh.texcoords) { + if (om->mesh.texcoords) { + memcpy(comp->mesh.texcoords + exp_off * 2, + om->mesh.texcoords, + om->mesh.triangleCount * 3 * 2 * sizeof(float)); + } else { + memset(comp->mesh.texcoords + exp_off * 2, + 0, + om->mesh.triangleCount * 3 * 2 * sizeof(float)); + } + } + + comp->base_vert_count += om->base_vert_count; + comp->face_count += om->mesh.triangleCount; + return OSRS_MODEL_APPEND_OK; +} + +static void composite_add_model_or_abort( + PlayerComposite* comp, + OsrsModel* om, + uint32_t requested_model_id, + const char* label +) { + OsrsModelAppendResult result = composite_try_add_model(comp, om); + if (result == OSRS_MODEL_APPEND_OK) return; + + int model_faces = om ? om->mesh.triangleCount : 0; + int model_base_verts = om ? (int)om->base_vert_count : 0; + uint32_t actual_model_id = om ? om->model_id : requested_model_id; + fprintf(stderr, + "render: composite append failed for %s model requested=%u actual=%u: %s " + "(base=%d/%d + %d, faces=%d/%d + %d)\n", + label ? label : "unknown", + requested_model_id, + actual_model_id, + osrs_model_append_result_name(result), + comp ? comp->base_vert_count : -1, + COMPOSITE_MAX_BASE_VERTS, + model_base_verts, + comp ? comp->face_count : -1, + COMPOSITE_MAX_FACES, + model_faces); + abort(); +} + +static OsrsModel* composite_get_model_or_abort( + ModelCache* cache, + uint32_t model_id, + const char* label +) { + OsrsModel* model = model_cache_get(cache, model_id); + if (model) return model; + fprintf(stderr, "render: missing %s model %u in model cache\n", + label ? label : "unknown", model_id); + abort(); +} + +static void composite_recreate_anim_state(PlayerComposite* comp) { + if (comp->anim_state) { + anim_model_state_free(comp->anim_state); + comp->anim_state = NULL; + } + if (comp->base_vert_count > 0) { + if (comp->has_face_alpha) { + comp->anim_state = anim_model_state_create_with_face_alpha( + comp->vertex_skins, comp->base_vert_count, + comp->face_alpha_labels, comp->base_face_alphas, + comp->face_count); + } else { + comp->anim_state = anim_model_state_create( + comp->vertex_skins, comp->base_vert_count); + } + } +} + +static void composite_rebuild( + PlayerComposite* comp, ModelCache* cache, RenderEntity* p +) { + comp->base_vert_count = 0; + comp->face_count = 0; + comp->has_face_alpha = 0; + + OsrsPlayerAppearance appearance = osrs_resolve_player_appearance(p->equipped); + + for (int bp = 0; bp < BODY_PART_COUNT; bp++) { + if (!appearance.body_visible[bp]) continue; + uint32_t model_id = appearance.body_model_ids[bp]; + OsrsModel* om = composite_get_model_or_abort(cache, model_id, "body"); + composite_add_model_or_abort(comp, om, model_id, "body"); + } + + for (int s = 0; s < OSRS_VISIBLE_EQUIP_SLOT_COUNT; s++) { + int slot = OSRS_VISIBLE_EQUIP_SLOTS[s]; + if (!appearance.item_visible[slot]) continue; + uint32_t model_id = appearance.item_model_ids[slot]; + OsrsModel* om = composite_get_model_or_abort(cache, model_id, "equipment"); + composite_add_model_or_abort(comp, om, model_id, "equipment"); + } + + composite_recreate_anim_state(comp); + + memcpy(comp->last_equipped, p->equipped, NUM_GEAR_SLOTS); + comp->needs_rebuild = 0; +} + +static void composite_rebuild_npc( + PlayerComposite* comp, ModelCache* cache, ModelCache* npc_cache, int npc_def_id +) { + comp->base_vert_count = 0; + comp->face_count = 0; + comp->has_face_alpha = 0; + + if (comp->mesh.vertices) + memset(comp->mesh.vertices, 0, COMPOSITE_MAX_EXP_VERTS * 3 * sizeof(float)); + if (comp->mesh.colors) + memset(comp->mesh.colors, 0, COMPOSITE_MAX_EXP_VERTS * 4); + if (comp->mesh.texcoords) + memset(comp->mesh.texcoords, 0, COMPOSITE_MAX_EXP_VERTS * 2 * sizeof(float)); + + uint32_t model_id = 0; + const NpcModelMapping* mapping = npc_model_lookup((uint16_t)npc_def_id); + if (!mapping) { + fprintf(stderr, "render: missing NPC model mapping for npc_def_id=%d\n", npc_def_id); + abort(); + } + model_id = mapping->model_id; + + OsrsModel* om = model_cache_get(cache, model_id); + if (!om && npc_cache) + om = model_cache_get(npc_cache, model_id); + if (!om) { + fprintf(stderr, "render: npc_def_id=%d mapped model %u is missing from loaded caches\n", + npc_def_id, model_id); + abort(); + } + composite_add_model_or_abort(comp, om, model_id, "npc"); + + composite_recreate_anim_state(comp); + + comp->last_npc_def_id = npc_def_id; + comp->needs_rebuild = 0; +} + +static void composite_init_gpu(PlayerComposite* comp, ModelCache* cache) { + if (comp->gpu_ready) return; + + comp->mesh.vertexCount = COMPOSITE_MAX_EXP_VERTS; + comp->mesh.triangleCount = COMPOSITE_MAX_FACES; + comp->mesh.vertices = (float*)RL_CALLOC(COMPOSITE_MAX_EXP_VERTS * 3, sizeof(float)); + comp->mesh.colors = (unsigned char*)RL_CALLOC(COMPOSITE_MAX_EXP_VERTS, 4); + if (cache && cache->has_atlas) { + comp->mesh.texcoords = (float*)RL_CALLOC(COMPOSITE_MAX_EXP_VERTS * 2, sizeof(float)); + } + + UploadMesh(&comp->mesh, true); + comp->model = LoadModelFromMesh(comp->mesh); + if (cache && cache->has_atlas) { + comp->model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = + cache->atlas_texture; + } + comp->gpu_ready = 1; +} + +static void composite_animate_and_draw( + PlayerComposite* comp, + const AnimFrameData* secondary_frame, const AnimFrameBase* secondary_fb, + const AnimFrameData* primary_frame, const AnimFrameBase* primary_fb, + const uint8_t* interleave_order, int interleave_count, + Matrix transform +) { + if (!comp->anim_state || comp->face_count == 0) return; + + if (primary_frame && secondary_frame && interleave_order && interleave_count > 0) { + anim_apply_frame_interleaved( + comp->anim_state, comp->base_vertices, + secondary_frame, secondary_fb, + primary_frame, primary_fb, + interleave_order, interleave_count); + } else if (primary_frame) { + if (primary_frame->kind == ANIM_FRAME_LEGACY) { + anim_apply_frame(comp->anim_state, comp->base_vertices, + primary_frame, primary_fb); + } else if (primary_frame->kind == ANIM_FRAME_MAYA_BAKED) { + anim_apply_maya_baked_frame(comp->anim_state, primary_frame); + } else { + fprintf(stderr, "render: unknown primary animation frame kind %u\n", + primary_frame->kind); + abort(); + } + } else if (secondary_frame) { + if (secondary_frame->kind == ANIM_FRAME_LEGACY) { + anim_apply_frame(comp->anim_state, comp->base_vertices, + secondary_frame, secondary_fb); + } else if (secondary_frame->kind == ANIM_FRAME_MAYA_BAKED) { + anim_apply_maya_baked_frame(comp->anim_state, secondary_frame); + } else { + fprintf(stderr, "render: unknown secondary animation frame kind %u\n", + secondary_frame->kind); + abort(); + } + } else { + anim_apply_rest_pose(comp->anim_state, comp->base_vertices); + } + + anim_update_mesh(comp->mesh.vertices, comp->anim_state, + comp->face_indices, comp->face_count); + + anim_update_mesh_alpha(comp->mesh.colors, comp->anim_state, + comp->face_count); + + { + int nv = comp->face_count * 3 * 3; + for (int i = 0; i < nv; i++) { + if (comp->mesh.vertices[i] > 10000.0f) comp->mesh.vertices[i] = 10000.0f; + else if (comp->mesh.vertices[i] < -10000.0f) comp->mesh.vertices[i] = -10000.0f; + } + } + + int exp_verts = comp->face_count * 3; + UpdateMeshBuffer(comp->mesh, 0, comp->mesh.vertices, + exp_verts * 3 * sizeof(float), 0); + if (comp->mesh.texcoords) { + UpdateMeshBuffer(comp->mesh, 1, comp->mesh.texcoords, + exp_verts * 2 * sizeof(float), 0); + } + UpdateMeshBuffer(comp->mesh, 3, comp->mesh.colors, + exp_verts * 4, 0); + + /* LoadModelFromMesh copies the mesh struct by value: DrawModel reads + vertexCount from model.meshes[0], not comp->mesh. */ + comp->model.meshes[0].vertexCount = exp_verts; + comp->model.meshes[0].triangleCount = comp->face_count; + comp->model.transform = transform; + DrawModel(comp->model, (Vector3){ 0, 0, 0 }, 1.0f, WHITE); + + comp->model.meshes[0].vertexCount = COMPOSITE_MAX_EXP_VERTS; + comp->model.meshes[0].triangleCount = COMPOSITE_MAX_FACES; +} + +static void composite_free(PlayerComposite* comp) { + if (comp->gpu_ready) { + UnloadModel(comp->model); + comp->gpu_ready = 0; + } + anim_model_state_free(comp->anim_state); + comp->anim_state = NULL; +} + +static void render_player_composite( + RenderClient* rc, int player_idx, Matrix transform +) { + if (!rc->model_cache) return; + + PlayerComposite* comp = &rc->composites[player_idx]; + RenderEntity* p = &rc->entities[player_idx]; + + composite_init_gpu(comp, rc->model_cache); + + if (p->entity_type == ENTITY_NPC) { + if (comp->needs_rebuild || comp->last_npc_def_id != p->npc_def_id) { + composite_rebuild_npc(comp, rc->model_cache, rc->npc_model_cache, p->npc_def_id); + } + } else { + if (comp->needs_rebuild || + memcmp(comp->last_equipped, p->equipped, NUM_GEAR_SLOTS) != 0) { + composite_rebuild(comp, rc->model_cache, p); + } + } + + if ((!rc->anim_cache && !rc->npc_anim_cache) || !comp->anim_state) { + if (comp->face_count > 0) { + int exp_verts = comp->face_count * 3; + comp->model.meshes[0].vertexCount = exp_verts; + comp->model.meshes[0].triangleCount = comp->face_count; + comp->model.transform = transform; + DrawModel(comp->model, (Vector3){ 0, 0, 0 }, 1.0f, WHITE); + comp->model.meshes[0].vertexCount = COMPOSITE_MAX_EXP_VERTS; + comp->model.meshes[0].triangleCount = COMPOSITE_MAX_FACES; + } + return; + } + + int new_primary; + if (p->entity_type == ENTITY_NPC) { + const NpcModelMapping* nm = npc_model_lookup((uint16_t)p->npc_def_id); + int idle = nm ? (int)nm->idle_anim : -1; + new_primary = (p->npc_anim_id >= 0 && p->npc_anim_id != idle) + ? p->npc_anim_id : -1; + } else { + new_primary = render_select_primary(p); + } + if (new_primary >= 0) { + int event_changed = + rc->primary_event_tick[player_idx] != + rc->last_primary_event_tick[player_idx]; + int need_restart = (rc->anim[player_idx].primary.seq_id != new_primary) || + (rc->anim[player_idx].primary.completed_loops > 0) || + (event_changed && new_primary != ANIM_SEQ_DEATH); + if (need_restart) { + anim_playback_restart( + &rc->anim[player_idx].primary, new_primary, ANIM_PLAY_ONCE); + rc->last_primary_event_tick[player_idx] = + rc->primary_event_tick[player_idx]; + } + } + + if (rc->anim[player_idx].primary.seq_id >= 0 && + rc->anim[player_idx].primary.completed_loops > 0 && + rc->anim[player_idx].primary.seq_id != ANIM_SEQ_DEATH) { + rc->anim[player_idx].primary.seq_id = -1; + } + + RenderAnimTrackFrame sec_track = {0}; + RenderAnimTrackFrame pri_track = {0}; + + if (rc->anim[player_idx].secondary.seq_id >= 0) { + render_anim_playback_resolve( + rc, &rc->anim[player_idx].secondary, comp->base_vert_count); + sec_track = render_anim_playback_frame(rc, &rc->anim[player_idx].secondary); + } + + if (rc->anim[player_idx].primary.seq_id >= 0) { + render_anim_playback_resolve( + rc, &rc->anim[player_idx].primary, comp->base_vert_count); + pri_track = render_anim_playback_frame(rc, &rc->anim[player_idx].primary); + } + + const uint8_t* interleave = NULL; + int interleave_count = 0; + if (pri_track.sequence_frame) { + AnimSequence* prim_seq = rc->anim[player_idx].primary.sequence; + if (prim_seq && prim_seq->interleave_order) { + interleave = prim_seq->interleave_order; + interleave_count = prim_seq->interleave_count; + } + } + + composite_animate_and_draw( + comp, + sec_track.sequence_frame ? &sec_track.sequence_frame->frame : NULL, + sec_track.framebase, + pri_track.sequence_frame ? &pri_track.sequence_frame->frame : NULL, + pri_track.framebase, + interleave, interleave_count, + transform); +} + +static void render_draw_3d_world(RenderClient* rc) { + rlSetClipPlanes(0.5, 500.0); + + Camera3D cam = render_build_3d_camera(rc); + BeginMode3D(cam); + + if (rc->terrain && rc->terrain->loaded) { + DrawModel(rc->terrain->model, (Vector3){ 0, 0, 0 }, 1.0f, WHITE); + + if (rc->show_collision && rc->collision_map) { + for (int dx = 0; dx < rc->arena_width; dx++) { + for (int dy = 0; dy < rc->arena_height; dy++) { + int wx = rc->arena_base_x + dx + rc->collision_world_offset_x; + int wy = rc->arena_base_y + dy + rc->collision_world_offset_y; + int flags = collision_get_flags(rc->collision_map, 0, wx, wy); + + Color col = { 0, 0, 0, 0 }; + if (flags & COLLISION_BLOCKED) { + col = CLITERAL(Color){ 200, 50, 50, 80 }; + } else if (flags & COLLISION_BRIDGE) { + col = CLITERAL(Color){ 50, 120, 220, 80 }; + } else if (flags & 0x0FF) { + col = CLITERAL(Color){ 220, 150, 40, 60 }; + } else { + col = CLITERAL(Color){ 50, 200, 50, 40 }; + } + + float tx = (float)(rc->arena_base_x + dx); + float tz = -(float)(rc->arena_base_y + dy + 1); + float ground = terrain_height_avg(rc->terrain, + rc->arena_base_x + dx, rc->arena_base_y + dy); + DrawCube((Vector3){ tx + 0.5f, ground + 0.05f, tz + 0.5f }, + 1.0f, 0.02f, 1.0f, col); + } + } + } + } else if (rc->npc_model_cache) { + float plat_y = 2.0f; + for (int dx = 0; dx < rc->arena_width; dx++) { + for (int dy = 0; dy < rc->arena_height; dy++) { + float tx = (float)(rc->arena_base_x + dx); + float tz = -(float)(rc->arena_base_y + dy + 1); + + int shade = 45 + ((dx * 7 + dy * 13) % 15); + int r = shade + ((dx * 3 + dy * 11) % 10); + Color c = { (unsigned char)r, (unsigned char)(shade - 3), (unsigned char)(shade - 6), 255 }; + DrawCube((Vector3){ tx + 0.5f, plat_y - 0.05f, tz + 0.5f }, + 1.0f, 0.1f, 1.0f, c); + } + } + } else { + float water_y = 1.5f; + float plat_y = 2.0f; + + for (int dx = 0; dx < rc->arena_width; dx++) { + for (int dy = 0; dy < rc->arena_height; dy++) { + float tx = (float)(rc->arena_base_x + dx); + float tz = -(float)(rc->arena_base_y + dy + 1); + + int on_plat; + if (rc->collision_map) { + int wx = rc->arena_base_x + dx + rc->collision_world_offset_x; + int wy = rc->arena_base_y + dy + rc->collision_world_offset_y; + on_plat = collision_tile_walkable(rc->collision_map, 0, wx, wy); + } else { + on_plat = (dx >= ZUL_PLATFORM_MIN && dx <= ZUL_PLATFORM_MAX && + dy >= ZUL_PLATFORM_MIN && dy <= ZUL_PLATFORM_MAX); + } + + if (on_plat) { + int shade = 35 + ((dx * 7 + dy * 13) % 15); + Color c = { (unsigned char)shade, (unsigned char)(shade * 2), (unsigned char)shade, 255 }; + DrawCube((Vector3){ tx + 0.5f, plat_y - 0.05f, tz + 0.5f }, + 1.0f, 0.1f, 1.0f, c); + } else { + int shade = 15 + ((dx * 3 + dy * 5) % 10); + Color c = { (unsigned char)(shade / 2), (unsigned char)shade, (unsigned char)(shade * 3), 255 }; + DrawCube((Vector3){ tx + 0.5f, water_y - 0.05f, tz + 0.5f }, + 1.0f, 0.1f, 1.0f, c); + } + } + } + } + + InfernoState* pillar_state = render_inferno_state_from_client(rc); + if (rc->npc_model_cache && pillar_state) { + InfernoState* is = pillar_state; + float plat_y = 2.0f; + float ms = 1.0f / 128.0f; + for (int p = 0; p < INF_NUM_PILLARS; p++) { + if (!is->pillars[p].active) continue; + float hp_frac = (float)is->pillars[p].hp / (float)INF_PILLAR_HP; + + float cx = (float)is->pillars[p].x + INF_PILLAR_SIZE / 2.0f; + float cz = -(float)(is->pillars[p].y + INF_PILLAR_SIZE / 2) - 0.5f; + + if (rc->pillar_models_ready) { + int mi = 0; + if (hp_frac <= 0.25f) mi = 3; + else if (hp_frac <= 0.50f) mi = 2; + else if (hp_frac <= 0.75f) mi = 1; + + rlDisableBackfaceCulling(); + rc->pillar_models[mi].transform = MatrixMultiply( + MatrixScale(-ms, ms, ms), + MatrixTranslate(cx, plat_y, cz)); + DrawModel(rc->pillar_models[mi], (Vector3){0,0,0}, 1.0f, WHITE); + rlEnableBackfaceCulling(); + } else { + int base_r = (int)(140 * hp_frac + 180 * (1.0f - hp_frac)); + int base_g = (int)(130 * hp_frac + 40 * (1.0f - hp_frac)); + int base_b = (int)(100 * hp_frac + 20 * (1.0f - hp_frac)); + Color pillar_col = { (unsigned char)base_r, (unsigned char)base_g, (unsigned char)base_b, 240 }; + for (int dx = 0; dx < INF_PILLAR_SIZE; dx++) { + for (int dy = 0; dy < INF_PILLAR_SIZE; dy++) { + float tx = (float)(is->pillars[p].x + dx); + float tz2 = -(float)(is->pillars[p].y + dy + 1); + for (int h = 0; h < 3; h++) { + DrawCube((Vector3){ tx + 0.5f, plat_y + 0.5f + (float)h, tz2 + 0.5f }, + 0.95f, 0.95f, 0.95f, pillar_col); + } + } + } + } + } + } + + if (rc->show_debug && rc->debug_hit_wx >= 0) { + float dtx = (float)rc->debug_hit_wx; + float dtz = -(float)(rc->debug_hit_wy + 1); + float dgy = rc->terrain + ? terrain_height_avg(rc->terrain, rc->debug_hit_wx, rc->debug_hit_wy) + : 2.0f; + DrawCube((Vector3){ dtx + 0.5f, dgy + 0.02f, dtz + 0.5f }, + 1.0f, 0.02f, 1.0f, (Color){ 255, 0, 255, 180 }); + DrawSphere((Vector3){ rc->debug_ray_hit_x, rc->debug_ray_hit_y, rc->debug_ray_hit_z }, + 0.1f, RED); + } + if (rc->show_debug) { + for (int i = 0; i < rc->entity_count; i++) { + RenderEntity* ep = &rc->entities[i]; + if (ep->entity_type == ENTITY_NPC && !ep->npc_visible) continue; + float tx = (float)ep->x; + float ty = (float)ep->y; + float tz = -(ty + 1.0f); + float ground = rc->terrain + ? terrain_height_avg(rc->terrain, ep->x, ep->y) : 2.0f; + int sz = ep->npc_size > 1 ? ep->npc_size : 1; + Color col = (ep->entity_type == ENTITY_PLAYER) + ? CLITERAL(Color){ 0, 255, 0, 100 } + : CLITERAL(Color){ 0, 200, 255, 80 }; + for (int dx = 0; dx < sz; dx++) { + for (int dy = 0; dy < sz; dy++) { + float mx = tx + (float)dx; + float mz = tz - (float)dy; + Color tile_col = col; + if (ep->entity_type == ENTITY_NPC && dx == 0 && dy == 0) + tile_col = CLITERAL(Color){ 255, 230, 40, 150 }; + DrawCube((Vector3){ mx + 0.5f, ground + 0.08f, mz + 0.5f }, + 0.9f, 0.04f, 0.9f, tile_col); + } + } + } + } + + { + EncounterOverlay* ov = &rc->encounter_overlay; + int has_terrain = rc->terrain && rc->terrain->loaded; + + #define OV_GROUND(tile_x, tile_y) \ + (has_terrain ? terrain_height_avg(rc->terrain, (tile_x), (tile_y)) : 2.0f) + + float ms = 1.0f / 128.0f; + for (int i = 0; i < ov->tile_shadow_count; i++) { + EncounterTileShadow* shadow = &ov->tile_shadows[i]; + if (!shadow->active) continue; + float ground = OV_GROUND(shadow->x, shadow->y); + float cx = (float)shadow->x + 0.5f; + float cz = -(float)(shadow->y + 1) + 0.5f; + float radius = 0.45f * shadow->scale; + unsigned char alpha = (unsigned char)(70.0f + 70.0f * shadow->scale); + DrawCylinder( + (Vector3){ cx, ground + 0.045f, cz }, + radius, + radius, + 0.025f, + 36, + CLITERAL(Color){ 8, 6, 4, alpha }); + } + + if (ov->floating_model_count > 0) { + render_load_projectile_assets(rc); + } + int floating_bob_spin = 0; + if (rc->gui.encounter_def) { + const EncounterDef* fm_def = (const EncounterDef*)rc->gui.encounter_def; + floating_bob_spin = (strcmp(fm_def->name, "colosseum") == 0); + } + float floating_anim_t = (float)rc->effect_client_tick_counter; + for (int i = 0; i < ov->floating_model_count; i++) { + EncounterFloatingModel* floating = &ov->floating_models[i]; + if (!floating->active) continue; + float anchor_x = 0.0f; + float anchor_y = 0.0f; + if (!render_resolve_projectile_anchor( + rc, floating->anchor_kind, floating->npc_slot, + (float)floating->x, (float)floating->y, + &anchor_x, &anchor_y)) { + continue; + } + float ground = OV_GROUND((int)anchor_x, (int)anchor_y); + Vector3 pos = { + anchor_x + 0.5f + floating->lateral_offset, + ground + floating->height_offset, + -(anchor_y + 1.0f) + 0.5f + }; + float model_scale = (floating->scale > 0.0f ? floating->scale : 1.0f) / 128.0f; + + float npc_yaw = 0.0f; + if (floating->anchor_kind == ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT) { + int npc_idx = render_find_npc_entity_idx(rc, floating->npc_slot); + if (npc_idx >= 0) npc_yaw = rc->yaw[npc_idx]; + } + Matrix face_rot = MatrixRotateY(npc_yaw); + OsrsModel* animated = render_animate_effect_model( + rc, floating->model_id, floating->anim_id, + rc->effect_client_tick_counter); + if (animated) { + if (floating_bob_spin) { + float bob_phase = floating_anim_t * (2.0f * PI / 100.0f) + + (float)i * 1.7f; + pos.y += 0.08f * sinf(bob_phase); + } + rlDisableBackfaceCulling(); + animated->model.transform = MatrixMultiply( + MatrixMultiply( + MatrixScale(-model_scale, model_scale, model_scale), + face_rot), + MatrixTranslate(pos.x, pos.y, pos.z)); + DrawModel(animated->model, (Vector3){0,0,0}, 1.0f, WHITE); + rlEnableBackfaceCulling(); + continue; + } + + Model* model = render_get_proj_model(rc, floating->model_id); + float spin = 0.0f; + if (floating_bob_spin) { + float bob_phase = floating_anim_t * (2.0f * PI / 100.0f) + + (float)i * 1.7f; + pos.y += 0.08f * sinf(bob_phase); + spin = floating_anim_t * (2.0f * PI / 150.0f); + } + Matrix spin_rot; + switch (floating->model_id) { + case 51213u: spin_rot = MatrixRotateX(spin); break; + case 51221u: spin_rot = MatrixRotateZ(spin); break; + default: spin_rot = MatrixRotateY(spin); break; + } + rlDisableBackfaceCulling(); + model->transform = MatrixMultiply( + MatrixMultiply( + MatrixMultiply( + MatrixScale(-model_scale, model_scale, model_scale), + spin_rot), + face_rot), + MatrixTranslate(pos.x, pos.y, pos.z)); + DrawModel(*model, (Vector3){0,0,0}, 1.0f, WHITE); + rlEnableBackfaceCulling(); + } + + for (int i = 0; i < ov->hazard_count; i++) { + if (!ov->hazards[i].active) continue; + float ground = OV_GROUND(ov->hazards[i].x + 1, ov->hazards[i].y + 1); + + if (rc->cloud_model_ready) { + float cx = (float)ov->hazards[i].x + 1.5f; + float cz = -(float)(ov->hazards[i].y + 2) + 0.5f; + rlDisableBackfaceCulling(); + rc->cloud_model.transform = MatrixMultiply( + MatrixScale(ms, ms, ms), + MatrixTranslate(cx, ground + 0.1f, cz)); + DrawModel(rc->cloud_model, (Vector3){0,0,0}, 1.0f, WHITE); + rlEnableBackfaceCulling(); + } else { + for (int cdx = 0; cdx < 3; cdx++) { + for (int cdy = 0; cdy < 3; cdy++) { + float fx = (float)(ov->hazards[i].x + cdx); + float fz = -(float)(ov->hazards[i].y + cdy + 1); + float tg = OV_GROUND(ov->hazards[i].x + cdx, ov->hazards[i].y + cdy); + DrawCube((Vector3){ fx + 0.5f, tg + 0.08f, fz + 0.5f }, + 0.95f, 0.06f, 0.95f, + CLITERAL(Color){ 80, 180, 50, 100 }); + } + } + } + } + + if (rc->show_debug && ov->boss_visible && ov->boss_size > 0) { + Color form_col; + switch (ov->boss_form) { + case 0: form_col = CLITERAL(Color){ 50, 200, 50, 80 }; break; + case 1: form_col = CLITERAL(Color){ 200, 50, 50, 80 }; break; + case 2: form_col = CLITERAL(Color){ 50, 100, 255, 80 }; break; + default: form_col = CLITERAL(Color){ 200, 200, 200, 80 }; break; + } + Color border_col = form_col; + border_col.a = 200; + int sz = ov->boss_size; + for (int bx = 0; bx < sz; bx++) { + for (int by = 0; by < sz; by++) { + int tx = ov->boss_x + bx; + int ty = ov->boss_y + by; + float ground = OV_GROUND(tx, ty); + float fx = (float)tx; + float fz = -(float)(ty + 1); + DrawCube((Vector3){ fx + 0.5f, ground + 0.04f, fz + 0.5f }, + 1.0f, 0.02f, 1.0f, form_col); + } + } + float x0 = (float)ov->boss_x; + float x1 = (float)(ov->boss_x + sz); + float z0 = -(float)(ov->boss_y + sz); + float z1 = -(float)ov->boss_y; + float border_y = OV_GROUND(ov->boss_x + sz/2, ov->boss_y + sz/2) + 0.06f; + DrawLine3D((Vector3){x0, border_y, z0}, (Vector3){x1, border_y, z0}, border_col); + DrawLine3D((Vector3){x1, border_y, z0}, (Vector3){x1, border_y, z1}, border_col); + DrawLine3D((Vector3){x1, border_y, z1}, (Vector3){x0, border_y, z1}, border_col); + DrawLine3D((Vector3){x0, border_y, z1}, (Vector3){x0, border_y, z0}, border_col); + } + + { + const EncounterDef* edef_molten = + (const EncounterDef*)rc->gui.encounter_def; + if (edef_molten && rc->gui.encounter_state && + strcmp(edef_molten->name, "colosseum") == 0) { + ColosseumState* cs_molten = (ColosseumState*)rc->gui.encounter_state; + const int* molten_xs[2] = { cs_molten->molten_x, cs_molten->sol.hazard_tile_x }; + const int* molten_ys[2] = { cs_molten->molten_y, cs_molten->sol.hazard_tile_y }; + int molten_counts[2] = { cs_molten->molten_count, cs_molten->sol.hazard_tile_count }; + for (int src = 0; src < 2; src++) { + int molten_n = molten_counts[src]; + if (molten_n > COLO_SOL_HAZARD_TILES_MAX) + molten_n = COLO_SOL_HAZARD_TILES_MAX; + for (int mi = 0; mi < molten_n; mi++) { + int tx = molten_xs[src][mi]; + int ty = molten_ys[src][mi]; + float ground = OV_GROUND(tx, ty); + float fx = (float)tx + 0.5f; + float fz = -(float)(ty + 1) + 0.5f; + int simmer = (rc->effect_client_tick_counter + tx * 3 + ty * 7) % 24; + unsigned char glow = (unsigned char)(150 + (simmer < 12 ? simmer : 24 - simmer) * 6); + DrawCylinder((Vector3){ fx, ground + 0.02f, fz }, + 0.42f, 0.46f, 0.05f, 12, + CLITERAL(Color){ 244, 200, 70, glow }); + DrawCylinder((Vector3){ fx, ground + 0.05f, fz }, + 0.26f, 0.30f, 0.05f, 10, + CLITERAL(Color){ 255, 236, 130, glow }); + } + } + + const SolHereditState* sol = &cs_molten->sol; + + for (int ci = 0; ci < COLO_SOL_MAX_CRYSTALS; ci++) { + const ColoSolCrystal* crys = &sol->crystals[ci]; + if (!crys->active || + crys->firing_freeze > COLO_SOL_LASER_BEAM_SHOW_MAX || + crys->firing_freeze < 1) continue; + int step_x = 0, step_y = 0, span = 0; + switch (crys->edge) { + case COLO_SOL_EDGE_NORTH: + step_y = -1; + span = crys->y - (sol->boss_arena_min_y + 1); + break; + case COLO_SOL_EDGE_SOUTH: + step_y = 1; + span = (sol->boss_arena_max_y - 1) - crys->y; + break; + case COLO_SOL_EDGE_EAST: + step_x = -1; + span = crys->x - (sol->boss_arena_min_x + 1); + break; + case COLO_SOL_EDGE_WEST: + step_x = 1; + span = (sol->boss_arena_max_x - 1) - crys->x; + break; + default: break; + } + float grow = (float)(COLO_SOL_LASER_BEAM_SHOW_MAX + 1 + - crys->firing_freeze) + / (float)(COLO_SOL_LASER_BEAM_SHOW_MAX + - COLO_SOL_LASER_BEAM_SHOW_MIN + 1); + if (grow > 1.0f) grow = 1.0f; + int lit = (int)((float)span * grow + 0.5f); + int erase_from = 1; + if (crys->firing_freeze < COLO_SOL_LASER_BEAM_SHOW_MIN) + erase_from = span / 2 + 1; + float imminence = 1.0f + - (float)(crys->firing_freeze - COLO_SOL_LASER_BEAM_SHOW_MIN) + / (float)(COLO_SOL_LASER_BEAM_SHOW_MAX - COLO_SOL_LASER_BEAM_SHOW_MIN); + if (imminence > 1.0f) imminence = 1.0f; + unsigned char a = (unsigned char)(100.0f + 130.0f * imminence); + for (int k = erase_from; k <= lit; k++) { + int tx = crys->x + step_x * k; + int ty = crys->y + step_y * k; + float ground = OV_GROUND(tx, ty); + float fx = (float)tx + 0.5f; + float fz = -(float)(ty + 1) + 0.5f; + DrawCube((Vector3){ fx, ground + 0.25f, fz }, + step_x != 0 ? 1.0f : 0.3f, 0.3f, + step_y != 0 ? 1.0f : 0.3f, + CLITERAL(Color){ 255, 240, 90, a }); + } + } + + for (int bi = 0; bi < COLO_SOL_BEAM_MAX; bi++) { + const ColoSolBeam* beam = &sol->beams[bi]; + if (!beam->active) continue; + float ground = OV_GROUND(beam->x, beam->y); + float fx = (float)beam->x + 0.5f; + float fz = -(float)(beam->y + 1) + 0.5f; + DrawCube((Vector3){ fx, ground + 0.06f, fz }, 0.95f, 0.06f, 0.95f, + CLITERAL(Color){ 255, 255, 210, 170 }); + } + + for (int ci = 0; ci < COLO_SOL_MAX_CRYSTALS; ci++) { + const ColoSolCrystal* crys = &sol->crystals[ci]; + if (!crys->active) continue; + float ground = OV_GROUND(crys->x, crys->y); + float fx = (float)crys->x + 0.5f; + float fz = -(float)(crys->y + 1) + 0.5f; + unsigned char a = crys->firing_freeze > 0 ? 230 : 120; + DrawCube((Vector3){ fx, ground + 0.12f, fz }, 0.4f, 0.4f, 0.4f, + CLITERAL(Color){ 180, 230, 255, a }); + } + + if (col_sol_clamp_active(cs_molten)) { + static const uint32_t GLAD_MODEL[3] = { + 0xC0000u + 12834u, 0xC0000u + 12835u, 0xC0000u + 12836u }; + int amin_x = sol->boss_arena_min_x, amax_x = sol->boss_arena_max_x; + int amin_y = sol->boss_arena_min_y, amax_y = sol->boss_arena_max_y; + float ccx = (float)(amin_x + amax_x) * 0.5f + 0.5f; + float ccz = -((float)(amin_y + amax_y) * 0.5f + 1.0f) + 0.5f; + int gclk = rc->effect_client_tick_counter; + for (int side = 0; side < 4; side++) { + for (int t = amin_x + 2; t <= amax_x - 2; t++) { + int tx, ty; + switch (side) { + case 0: tx = t; ty = amin_y; break; + case 1: tx = t; ty = amax_y; break; + case 2: tx = amin_x; ty = t; break; + default: tx = amax_x; ty = t; break; + } + int variant = (int)(((unsigned)(tx * 3 + ty * 5)) % 3u); + OsrsModel* gom = render_animate_effect_model( + rc, GLAD_MODEL[variant], 10872, gclk + variant * 7); + if (!gom) continue; + float gx = (float)tx + 0.5f; + float gz = -(float)(ty + 1) + 0.5f; + float gground = OV_GROUND(tx, ty); + float gyaw = atan2f(ccx - gx, ccz - gz) + 3.14159265f; + rlDisableBackfaceCulling(); + gom->model.transform = MatrixMultiply( + MatrixMultiply( + MatrixScale(-1.0f/128.0f, 1.0f/128.0f, 1.0f/128.0f), + MatrixRotateY(gyaw)), + MatrixTranslate(gx, gground, gz)); + DrawModel(gom->model, (Vector3){0,0,0}, 1.0f, WHITE); + rlEnableBackfaceCulling(); + } + } + } + } + } + + if (ov->melee_target_active) { + float ground = OV_GROUND(ov->melee_target_x, ov->melee_target_y); + float mx = (float)ov->melee_target_x; + float mz = -(float)(ov->melee_target_y + 1); + DrawCube((Vector3){ mx + 0.5f, ground + 0.06f, mz + 0.5f }, + 1.0f, 0.04f, 1.0f, + CLITERAL(Color){ 255, 50, 50, 150 }); + } + + for (int i = 0; i < ov->add_count; i++) { + if (!ov->adds[i].active) continue; + float ground = OV_GROUND(ov->adds[i].x, ov->adds[i].y); + float sx = (float)ov->adds[i].x + 0.5f; + float sz = -(float)(ov->adds[i].y + 1) + 0.5f; + if (rc->snakeling_model_ready) { + rlDisableBackfaceCulling(); + rc->snakeling_model.transform = MatrixMultiply( + MatrixScale(ms, ms, ms), + MatrixTranslate(sx, ground, sz)); + DrawModel(rc->snakeling_model, (Vector3){0,0,0}, 1.0f, WHITE); + rlEnableBackfaceCulling(); + } else { + Color sc = ov->adds[i].variant + ? CLITERAL(Color){ 100, 150, 255, 200 } + : CLITERAL(Color){ 255, 150, 50, 200 }; + DrawCube((Vector3){ sx, ground + 0.2f, sz }, + 0.6f, 0.3f, 0.6f, sc); + } + } + + for (int i = 0; i < MAX_FLIGHT_PROJECTILES; i++) { + FlightProjectile* fp = &rc->flights[i]; + if (!fp->active || fp->start_delay > 0) continue; + + float src_ground = OV_GROUND((int)fp->src_x, (int)fp->src_y); + float dst_ground = OV_GROUND((int)fp->dst_x, (int)fp->dst_y); + Vector3 pos = flight_get_position(fp, src_ground, dst_ground); + + Model* proj_model = NULL; + if (fp->anim_id >= 0 && (fp->model_id > 0 || fp->travel_gfx_drives_model)) { + OsrsModel* om = render_get_flight_osrs_model(rc, fp); + AnimSequence* seq = fp->anim_playback.sequence; + if (!fp->anim_state || !seq || seq->frame_count <= 0 || !om->face_indices) { + fprintf(stderr, "render: projectile model %u cannot render animation %d\n", + fp->model_id, fp->anim_id); + abort(); + } + int frame_idx = fp->anim_playback.frame_idx % seq->frame_count; + render_apply_anim_sequence_frame_to_model_state( + rc, fp->anim_state, om, seq, frame_idx, + "projectile"); + proj_model = &om->model; + } else if (fp->travel_gfx_drives_model) { + OsrsModel* om = render_get_flight_osrs_model(rc, fp); + proj_model = &om->model; + } else if (fp->model_id > 0) { + proj_model = render_get_proj_model(rc, fp->model_id); + } + if (!proj_model) { + if (fp->style == 0 && rc->ranged_proj_model_ready) + proj_model = &rc->ranged_proj_model; + else if (fp->style == 1 && rc->magic_proj_model_ready) + proj_model = &rc->magic_proj_model; + else if (fp->style == 3 && rc->cloud_proj_model_ready) + proj_model = &rc->cloud_proj_model; + else if (fp->style == 4 && rc->ranged_proj_model_ready) + proj_model = &rc->ranged_proj_model; + } + + if (proj_model) { + rlDisableBackfaceCulling(); + float pms = 1.0f / 128.0f; + if (fp->grow) { + float t = fp->progress; + t = t < 0.0f ? 0.0f : (t > 1.0f ? 1.0f : t); + pms *= 0.15f + 0.85f * (t * t); + } + proj_model->transform = render_projectile_transform_offset( + pms, pms, pms, fp->yaw, fp->pitch, pos, + fp->offset_x, fp->offset_y, fp->offset_z); + DrawModel(*proj_model, (Vector3){0,0,0}, 1.0f, WHITE); + rlEnableBackfaceCulling(); + } + + if (rc->show_debug && + fp->motion_mode != ENCOUNTER_PROJECTILE_MOTION_TARGET_ANCHORED) { + Color pc; + switch (fp->style) { + case 0: pc = CLITERAL(Color){ 80, 220, 80, 150 }; break; + case 1: pc = CLITERAL(Color){ 80, 130, 255, 150 }; break; + case 2: pc = CLITERAL(Color){ 255, 80, 80, 150 }; break; + case 3: pc = CLITERAL(Color){ 50, 180, 50, 150 }; break; + case 4: pc = CLITERAL(Color){ 230, 230, 230, 150 }; break; + default: pc = WHITE; break; + } + Vector3 src_pos = { fp->src_x + 0.5f, src_ground + fp->start_height, + -(fp->src_y + 1.0f) + 0.5f }; + DrawLine3D(src_pos, pos, pc); + DrawSphere(pos, 0.12f, pc); + } + } + + if (rc->show_safe_spots && rc->gui.encounter_state) { + const EncounterDef* edef_ss = (const EncounterDef*)rc->gui.encounter_def; + if (edef_ss && strcmp(edef_ss->name, "zulrah") == 0) { + ZulrahState* zs_ss = (ZulrahState*)rc->gui.encounter_state; + const ZulRotationPhase* phase_ss = zul_current_phase(zs_ss); + int act_stand = phase_ss->stand; + int act_stall = phase_ss->stall; + + for (int si = 0; si < ZUL_NUM_STAND_LOCATIONS; si++) { + int lx = ZUL_STAND_COORDS[si][0]; + int ly = ZUL_STAND_COORDS[si][1]; + float sx = (float)(rc->arena_base_x + lx) + 0.5f; + float sz = -(float)(rc->arena_base_y + ly + 1) + 0.5f; + float gy = OV_GROUND(rc->arena_base_x + lx, rc->arena_base_y + ly); + + Color col; + if (si == act_stand) + col = (Color){0, 255, 0, 160}; + else if (si == act_stall) + col = (Color){255, 255, 0, 160}; + else + col = (Color){0, 180, 180, 80}; + + DrawCube((Vector3){ sx, gy + 0.08f, sz }, + 0.7f, 0.04f, 0.7f, col); + } + } + } + render_inferno_lab_draw_forecast_3d(rc); + + #undef OV_GROUND + } + + { + ObjectMesh* obj = (rc->objects_zuk && rc->objects_zuk->loaded && rc->zuk_active) + ? rc->objects_zuk : rc->objects; + if (obj && obj->loaded) { + rlDisableBackfaceCulling(); + DrawModel(obj->model, (Vector3){ 0, 0, 0 }, 1.0f, WHITE); + rlEnableBackfaceCulling(); + } + } + + if (rc->npcs && rc->npcs->loaded) { + rlDisableBackfaceCulling(); + DrawModel(rc->npcs->model, (Vector3){ 0, 0, 0 }, 1.0f, WHITE); + rlEnableBackfaceCulling(); + } + + if (rc->model_cache) { + float ms = 1.0f / 128.0f; + + rlEnableBackfaceCulling(); + rlSetCullFace(RL_CULL_FACE_BACK); + for (int i = 0; i < rc->entity_count; i++) { + RenderEntity* ep = &rc->entities[i]; + + if (ep->entity_type == ENTITY_NPC && !ep->npc_visible) continue; + + if (ep->entity_type == ENTITY_PLAYER && i != rc->gui.gui_entity_idx) { + int fi = rc->gui.gui_entity_idx; + if (fi >= 0 && fi < rc->entity_count && + ep->x == rc->entities[fi].x && + ep->y == rc->entities[fi].y) { + continue; + } + } + + float px, pz, ground; + render_get_visual_pos(rc, i, &px, &pz, &ground); + float model_ground = osrs_render_entity_model_ground(ground); + + Matrix base = MatrixScale(-ms, ms, ms); + base = MatrixMultiply(base, MatrixRotateY(rc->yaw[i])); + base = MatrixMultiply(base, MatrixTranslate(px, model_ground, pz)); + + render_player_composite(rc, i, base); + + PlayerComposite* comp = &rc->composites[i]; + Camera3D hull_cam = render_build_3d_camera(rc); + int nv = comp->face_count * 3; + int hull_n = 0; + float min_model_y = 1000000.0f; + float max_model_y = -1000000.0f; + int hull_xs[RENDER_CLICK_HULL_MAX_INPUT_POINTS]; + int hull_ys[RENDER_CLICK_HULL_MAX_INPUT_POINTS]; + for (int vi = 0; vi < nv; vi++) { + float vy = comp->mesh.vertices[vi * 3 + 1]; + if (vy < min_model_y) min_model_y = vy; + if (vy > max_model_y) max_model_y = vy; + } + float visual_height_tiles = 0.0f; + if (nv > 0 && min_model_y <= max_model_y) { + visual_height_tiles = (max_model_y - min_model_y) * ms; + rc->entity_visual_mid_y[i] = model_ground + min_model_y * ms + + (max_model_y - min_model_y) * ms * 0.5f; + rc->entity_visual_top_y[i] = model_ground + max_model_y * ms; + } else { + int ent_size = (ep->entity_type == ENTITY_NPC && ep->npc_size > 1) ? ep->npc_size : 1; + rc->entity_visual_mid_y[i] = model_ground + 0.75f + 0.25f * (float)ent_size; + rc->entity_visual_top_y[i] = model_ground + 1.5f + 0.5f * (float)ent_size; + visual_height_tiles = (float)ent_size; + } + for (int vi = 0; vi < nv; vi++) { + float vx = comp->mesh.vertices[vi * 3 + 0]; + float vy = comp->mesh.vertices[vi * 3 + 1]; + float vz = comp->mesh.vertices[vi * 3 + 2]; + Vector3 wv = Vector3Transform((Vector3){ vx, vy, vz }, base); + hull_append_projected_world_point(&hull_cam, wv, + hull_xs, hull_ys, &hull_n, RENDER_CLICK_HULL_MAX_INPUT_POINTS); + } + Vector3 clickbox_points[RENDER_CLICKBOX_PRISM_POINT_COUNT]; + int clickbox_n = render_build_entity_clickbox_prism_points( + ep, px, pz, model_ground, visual_height_tiles, + clickbox_points, RENDER_CLICKBOX_PRISM_POINT_COUNT); + for (int ci = 0; ci < clickbox_n; ci++) { + hull_append_projected_world_point(&hull_cam, clickbox_points[ci], + hull_xs, hull_ys, &hull_n, RENDER_CLICK_HULL_MAX_INPUT_POINTS); + } + hull_compute(hull_xs, hull_ys, hull_n, &rc->entity_hulls[i]); + } + rlEnableBackfaceCulling(); + } + + if (rc->model_cache || rc->projectile_model_cache) { + rlDisableBackfaceCulling(); + float eff_scale = 1.0f / 128.0f; + int eff_ct = rc->effect_client_tick_counter; + + for (int i = 0; i < MAX_ACTIVE_EFFECTS; i++) { + ActiveEffect* e = &rc->effects[i]; + if (e->type == EFFECT_NONE) continue; + if (!e->meta) continue; + + OsrsModel* om = effect_find_model(e->meta, rc->model_cache, + rc->npc_model_cache, rc->projectile_model_cache); + if (!om) continue; + + float ex = (float)(e->cur_x / 128.0); + float ez = -(float)(e->cur_y / 128.0); + float ground = rc->terrain + ? terrain_height_avg(rc->terrain, (int)ex, (int)(e->cur_y / 128.0)) + : 2.0f; + float ey = ground + (float)(e->height / 128.0); + + float scale_xy = eff_scale * (float)e->meta->resize_xy / 128.0f; + float scale_y = eff_scale * (float)e->meta->resize_z / 128.0f; + + if (e->anim_state && e->anim_playback.seq_id >= 0 && om->face_indices) { + render_anim_playback_resolve( + rc, &e->anim_playback, (int)om->base_vert_count); + AnimSequence* seq = e->anim_playback.sequence; + if (seq && seq->frame_count > 0) { + int frame_idx = e->anim_playback.frame_idx % seq->frame_count; + render_apply_anim_sequence_frame_to_model_state( + rc, e->anim_state, om, seq, frame_idx, + "spotanim"); + } + } + + Matrix t; + + if (e->type == EFFECT_PROJECTILE && e->started) { + OsrsProjectileOrientation orientation = + osrs_projectile_orientation_from_step( + (float)e->x_increment, + (float)e->y_increment, + (float)e->height_increment); + t = render_projectile_transform(scale_xy, scale_y, scale_xy, + orientation.yaw, orientation.pitch, + (Vector3){ ex, ey, ez }); + } else { + t = MatrixMultiply( + MatrixScale(-scale_xy, scale_y, scale_xy), + MatrixTranslate(ex, ey, ez)); + } + om->model.transform = t; + + Color tint = WHITE; + if (e->type == EFFECT_SPOTANIM && e->stop_tick > e->start_tick) { + int total = e->stop_tick - e->start_tick; + int elapsed = eff_ct - e->start_tick; + float progress = (float)elapsed / (float)total; + float alpha = 1.0f; + if (progress < 0.2f) alpha = progress / 0.2f; + else if (progress > 0.8f) alpha = (1.0f - progress) / 0.2f; + if (alpha < 0.0f) alpha = 0.0f; + if (alpha > 1.0f) alpha = 1.0f; + tint = (Color){ 255, 255, 255, (unsigned char)(alpha * 255) }; + } + DrawModel(om->model, (Vector3){ 0, 0, 0 }, 1.0f, tint); + } + rlEnableBackfaceCulling(); + } + + float fa_x = (float)rc->arena_base_x; + float fa_z = -(float)rc->arena_base_y; + float fa_w = (float)rc->arena_width; + float fa_h = -(float)rc->arena_height; + float bh = rc->terrain ? terrain_height_at(rc->terrain, rc->arena_base_x, rc->arena_base_y) : 2.0f; + DrawLine3D( + (Vector3){ fa_x, bh, fa_z }, + (Vector3){ fa_x + fa_w, bh, fa_z }, YELLOW); + DrawLine3D( + (Vector3){ fa_x + fa_w, bh, fa_z }, + (Vector3){ fa_x + fa_w, bh, fa_z + fa_h }, YELLOW); + DrawLine3D( + (Vector3){ fa_x + fa_w, bh, fa_z + fa_h }, + (Vector3){ fa_x, bh, fa_z + fa_h }, YELLOW); + DrawLine3D( + (Vector3){ fa_x, bh, fa_z + fa_h }, + (Vector3){ fa_x, bh, fa_z }, YELLOW); + + InfernoState* debug_inferno_state = render_inferno_state_from_client(rc); + if (rc->show_debug && rc->entity_count > 0) { + int player_idx = rc->gui.gui_entity_idx; + if (player_idx < 0 || player_idx >= rc->entity_count || + rc->entities[player_idx].entity_type != ENTITY_PLAYER) { + player_idx = -1; + for (int i = 0; i < rc->entity_count; i++) { + if (rc->entities[i].entity_type == ENTITY_PLAYER) { + player_idx = i; + break; + } + } + } + + if (player_idx >= 0) { + RenderEntity* player = &rc->entities[player_idx]; + float player_x, player_z, player_ground; + render_get_visual_pos(rc, player_idx, &player_x, &player_z, &player_ground); + float player_y = rc->entity_visual_mid_y[player_idx] > player_ground + ? rc->entity_visual_mid_y[player_idx] + : player_ground + 1.0f; + + for (int i = 0; i < rc->entity_count; i++) { + if (i == player_idx) continue; + RenderEntity* target = &rc->entities[i]; + if (target->entity_type == ENTITY_NPC && !target->npc_visible) continue; + + int is_target = target->entity_type == ENTITY_NPC || + player->attack_target_entity_idx == i; + if (!is_target) continue; + + Color lc = GREEN; + if (debug_inferno_state && target->entity_type == ENTITY_NPC) { + int slot = target->npc_slot; + if (slot < 0 || slot >= INF_MAX_NPCS) continue; + InfNPC* npc = &debug_inferno_state->npcs[slot]; + if (!npc->active || npc->death_ticks > 0) continue; + const EncounterLoadoutStats* ls = + &debug_inferno_state->loadout_stats[debug_inferno_state->weapon_set]; + OsrsLosQuery los_query = osrs_los_blockers( + debug_inferno_state->los_blockers, + debug_inferno_state->los_blocker_count); + int can_atk = encounter_player_can_attack( + debug_inferno_state->player.x, + debug_inferno_state->player.y, + npc->x, npc->y, npc->size, + ls->attack_range, + &los_query); + lc = can_atk ? GREEN : RED; + } + + float target_x, target_z, target_ground; + render_get_visual_pos(rc, i, &target_x, &target_z, &target_ground); + float target_y = rc->entity_visual_mid_y[i] > target_ground + ? rc->entity_visual_mid_y[i] + : target_ground + 1.0f; + float line_y = player_y < target_y ? player_y : target_y; + DrawLine3D( + (Vector3){ player_x, line_y, player_z }, + (Vector3){ target_x, line_y, target_z }, + lc); + } + } + } + + if (rc->hover_tile_x >= 0) { + float htx = (float)rc->hover_tile_x; + float htz = -(float)(rc->hover_tile_y + 1); + float hgy = rc->terrain + ? terrain_height_avg(rc->terrain, rc->hover_tile_x, rc->hover_tile_y) + : 2.0f; + float hy = hgy + 0.03f; + Color hcol = CLITERAL(Color){ 0, 220, 220, 180 }; + DrawLine3D((Vector3){ htx, hy, htz }, (Vector3){ htx + 1.0f, hy, htz }, hcol); + DrawLine3D((Vector3){ htx + 1.0f, hy, htz }, (Vector3){ htx + 1.0f, hy, htz + 1.0f }, hcol); + DrawLine3D((Vector3){ htx + 1.0f, hy, htz + 1.0f }, (Vector3){ htx, hy, htz + 1.0f }, hcol); + DrawLine3D((Vector3){ htx, hy, htz + 1.0f }, (Vector3){ htx, hy, htz }, hcol); + } + + EndMode3D(); +} + +static void render_draw_overhead_status(RenderClient* rc, OsrsEnv* env) { + Camera3D cam = render_build_3d_camera(rc); + InfernoState* debug_state = render_inferno_state_from_client(rc); + + static const int prayer_to_headicon[] = { + -1, + 2, + 1, + 0, + 4, + 5, + }; + + for (int i = 0; i < rc->entity_count; i++) { + RenderEntity* p = &rc->entities[i]; + + if (p->entity_type == ENTITY_NPC && !p->npc_visible) continue; + + float px, pz, ground; + render_get_visual_pos(rc, i, &px, &pz, &ground); + int ent_size = (p->entity_type == ENTITY_NPC && p->npc_size > 1) ? p->npc_size : 1; + float head_y = rc->entity_visual_top_y[i]; + float abdomen_y = rc->entity_visual_mid_y[i]; + if (head_y <= abdomen_y) { + head_y = ground + 1.5f + 0.5f * (float)ent_size; + abdomen_y = ground + 0.75f + 0.25f * (float)ent_size; + } + Vector2 screen_head = GetWorldToScreen((Vector3){ px, head_y, pz }, cam); + Vector2 screen_overhead = + GetWorldToScreen((Vector3){ px, render_overhead_anchor_y(head_y), pz }, cam); + Vector2 screen_abdomen = GetWorldToScreen((Vector3){ px, abdomen_y, pz }, cam); + + if (screen_overhead.x < -50 || screen_overhead.x > RENDER_WINDOW_W + 50 || + screen_overhead.y < -50 || screen_overhead.y > RENDER_WINDOW_H + 50) continue; + + for (int si = 0; si < RENDER_SPLATS_PER_PLAYER; si++) { + HitSplat* s = &rc->splats[i][si]; + if (!s->active) continue; + int sx, sy; + render_splat_screen_pos( + (int)screen_abdomen.x, (int)screen_abdomen.y, si, s, &sx, &sy); + render_draw_hitmark(rc, sx, sy, s->damage, s->hitmark_trans, s->type); + } + + float cursor_y = screen_overhead.y; + + if (env->tick < rc->hp_bar_visible_until[i]) { + static const int BAR_WIDTH_BY_SIZE[] = { + 30, 30, 40, 50, 60, 80, 100, 120 + }; + int bw_idx = ent_size; + if (bw_idx > 7) bw_idx = 7; + int bar_w = BAR_WIDTH_BY_SIZE[bw_idx]; + int bar_h = 5; + float hp_frac = (float)p->current_hitpoints / (float)p->base_hitpoints; + if (hp_frac < 0.0f) hp_frac = 0.0f; + if (hp_frac > 1.0f) hp_frac = 1.0f; + int green_w = p->current_hitpoints > 0 ? (int)ceilf(hp_frac * bar_w) : 0; + if (green_w > bar_w) green_w = bar_w; + + int bar_x = (int)screen_overhead.x - bar_w / 2; + int bar_y = (int)cursor_y - bar_h / 2; + DrawRectangle(bar_x, bar_y, green_w, bar_h, COLOR_HP_GREEN); + DrawRectangle(bar_x + green_w, bar_y, bar_w - green_w, bar_h, COLOR_HP_RED); + cursor_y -= (float)(bar_h + 2); + } + + if (rc->prayer_icons_loaded && + p->prayer > PRAYER_NONE && p->prayer <= PRAYER_REDEMPTION) { + int icon_idx = prayer_to_headicon[p->prayer]; + if (icon_idx >= 0 && icon_idx < 6) { + Texture2D tex = rc->prayer_icons[icon_idx]; + float scale = 1.0f; + float draw_x = screen_overhead.x - (float)tex.width * scale / 2.0f; + float draw_y = cursor_y - (float)tex.height * scale; + DrawTextureEx(tex, (Vector2){ draw_x, draw_y }, 0.0f, scale, WHITE); + } + } + + if (rc->show_debug && p->entity_type == ENTITY_NPC && + p->debug_npc_type_name) { + render_draw_entity_debug_metadata(p, screen_head, + rc->collision_world_offset_x, rc->collision_world_offset_y); + } else if (rc->show_debug && p->entity_type == ENTITY_NPC && debug_state) { + InfernoState* is = debug_state; + int slot = p->npc_slot; + if (slot >= 0 && slot < INF_MAX_NPCS && is->npcs[slot].active) { + InfNPC* npc = &is->npcs[slot]; + int dy = (int)screen_head.y + 10; + int dx = (int)screen_head.x; + int fs = 10; + + const char* style_str = "???"; + Color style_col = WHITE; + int style = (npc->type == INF_NPC_JAD) ? inf_npc_jad(npc)->attack_style : npc->attack_style; + if (style == ATTACK_STYLE_MAGIC) { style_str = "MAG"; style_col = BLUE; } + if (style == ATTACK_STYLE_RANGED) { style_str = "RNG"; style_col = GREEN; } + if (style == ATTACK_STYLE_MELEE) { style_str = "MEL"; style_col = RED; } + + const char* atk_txt = TextFormat("ATK:%d %s", npc->attack_timer, style_str); + int tw = MeasureText(atk_txt, fs); + DrawText(atk_txt, dx - tw/2, dy, fs, style_col); + dy += fs + 1; + + if (npc->frozen_ticks > 0) { + const char* frz_txt = TextFormat("FRZ:%d", npc->frozen_ticks); + int fw = MeasureText(frz_txt, fs); + DrawText(frz_txt, dx - fw/2, dy, fs, (Color){100, 200, 255, 255}); + dy += fs + 1; + } + + if (npc->type != INF_NPC_NIBBLER) { + int npc_los = inf_npc_has_los(is, slot); + const char* los_txt = npc_los ? "NPC>P" : "NPC>P X"; + Color los_col = npc_los ? GREEN : RED; + int lw = MeasureText(los_txt, fs); + DrawText(los_txt, dx - lw/2, dy, fs, los_col); + dy += fs + 1; + } + + { + const EncounterLoadoutStats* ls = &is->loadout_stats[is->weapon_set]; + OsrsLosQuery los_query = osrs_los_blockers( + is->los_blockers, + is->los_blocker_count); + int can_atk = encounter_player_can_attack( + is->player.x, is->player.y, + npc->x, npc->y, npc->size, + ls->attack_range, &los_query); + const char* patk_txt = can_atk ? "P>NPC" : "P>NPC X"; + Color patk_col = can_atk ? GREEN : RED; + int pw = MeasureText(patk_txt, fs); + DrawText(patk_txt, dx - pw/2, dy, fs, patk_col); + dy += fs + 1; + } + + if (npc->type == INF_NPC_BLOB && npc->blob_scanned_prayer >= 0) { + const char* scan = "SCAN:???"; + if (npc->blob_scanned_prayer == PRAYER_PROTECT_MAGIC) scan = "SCAN>RNG"; + else if (npc->blob_scanned_prayer == PRAYER_PROTECT_RANGED) scan = "SCAN>MAG"; + int sw = MeasureText(scan, fs); + DrawText(scan, dx - sw/2, dy, fs, YELLOW); + } + } + } + } +} + +static void render_draw_minimap_orb( + GuiState* gs, + Rectangle rect, + const char* filler_asset, + const char* icon_asset, + int value, + int max_value, + Color fallback_fill +) { + gui_draw_named_asset(gs, "orb_frame_0", rect, WHITE); + if (gui_asset(gs, "orb_frame_0").id == 0) { + DrawRectangleRounded((Rectangle){rect.x, rect.y + 7, 34, 20}, 0.22f, 5, + (Color){50, 46, 37, 235}); + } + + Rectangle fill = {rect.x + 27, rect.y + 4, 26, 26}; + gui_draw_named_asset(gs, "orb_filler_0", fill, WHITE); + gui_draw_named_asset(gs, filler_asset, fill, WHITE); + if (gui_asset(gs, filler_asset).id == 0) { + DrawCircle((int)(fill.x + 13), (int)(fill.y + 13), 12, fallback_fill); + } + gui_draw_named_asset_centered(gs, icon_asset, fill, 22, 22, WHITE); + + char text[16]; + snprintf(text, sizeof(text), "%d", value); + Color text_color = max_value > 0 && value < max_value / 3 + ? (Color){255, 80, 70, 255} + : (Color){70, 255, 70, 255}; + Rectangle text_rect = {rect.x + 3, rect.y + 14, 24, 13}; + int tw = MeasureText(text, 12); + int tx = (int)(text_rect.x + text_rect.width / 2 - tw / 2); + int ty = (int)(text_rect.y + 1); + DrawText(text, tx + 1, ty + 1, 12, BLACK); + DrawText(text, tx, ty, 12, text_color); +} + +static int render_minimap_inside(int x, int y) { + float dx = (float)x - GUI_MINIMAP_MASK_CENTER; + float dy = (float)y - GUI_MINIMAP_MASK_CENTER; + return dx * dx + dy * dy <= GUI_MINIMAP_MASK_RADIUS * GUI_MINIMAP_MASK_RADIUS; +} + +static void render_minimap_put_pixel(int x, int y, Color c) { + if (x < 0 || y < 0 || x >= GUI_MINIMAP_W || y >= GUI_MINIMAP_H) return; + if (!render_minimap_inside(x, y)) return; + DrawPixel(x, y, c); +} + +static void render_minimap_fill_rect(int x, int y, int w, int h, Color c) { + for (int yy = 0; yy < h; yy++) { + for (int xx = 0; xx < w; xx++) { + render_minimap_put_pixel(x + xx, y + yy, c); + } + } +} + +static int render_minimap_collision_flags(RenderClient* rc, int wx, int wy) { + if (rc->collision_map == NULL) return COLLISION_NONE; + return collision_get_flags(rc->collision_map, 0, + wx + rc->collision_world_offset_x, + wy + rc->collision_world_offset_y); +} + +static Color render_minimap_tile_color(RenderClient* rc, int wx, int wy) { + if (wx < rc->arena_base_x || wy < rc->arena_base_y || + wx >= rc->arena_base_x + rc->arena_width || + wy >= rc->arena_base_y + rc->arena_height) { + return (Color){10, 8, 6, 255}; + } + + int flags = render_minimap_collision_flags(rc, wx, wy); + if (flags & COLLISION_BLOCKED) return (Color){14, 11, 9, 255}; + if (flags & COLLISION_BRIDGE) return (Color){48, 42, 34, 255}; + return (Color){58, 35, 29, 255}; +} + +static void render_minimap_draw_tile_features(RenderClient* rc, int wx, int wy, int sx, int sy) { + int flags = render_minimap_collision_flags(rc, wx, wy); + if (flags == COLLISION_NONE) return; + + if (flags & COLLISION_BLOCKED) { + render_minimap_fill_rect(sx - 1, sy - 1, 3, 3, (Color){36, 25, 20, 255}); + } + + Color wall = (Color){224, 221, 198, 255}; + if (flags & (COLLISION_WALL_NORTH | COLLISION_IMPENETRABLE_WALL_NORTH)) { + render_minimap_fill_rect(sx - 2, sy - 2, 5, 1, wall); + } + if (flags & (COLLISION_WALL_SOUTH | COLLISION_IMPENETRABLE_WALL_SOUTH)) { + render_minimap_fill_rect(sx - 2, sy + 2, 5, 1, wall); + } + if (flags & (COLLISION_WALL_WEST | COLLISION_IMPENETRABLE_WALL_WEST)) { + render_minimap_fill_rect(sx - 2, sy - 2, 1, 5, wall); + } + if (flags & (COLLISION_WALL_EAST | COLLISION_IMPENETRABLE_WALL_EAST)) { + render_minimap_fill_rect(sx + 2, sy - 2, 1, 5, wall); + } +} + +static float render_minimap_entity_center_x(RenderClient* rc, int entity_idx) { + int sub = rc->sub_x[entity_idx]; + if (sub != 0) return (float)sub / 128.0f; + RenderEntity* ent = &rc->entities[entity_idx]; + int size = ent->npc_size > 1 ? ent->npc_size : 1; + return (float)ent->x + (float)size * 0.5f; +} + +static float render_minimap_entity_center_y(RenderClient* rc, int entity_idx) { + int sub = rc->sub_y[entity_idx]; + if (sub != 0) return (float)sub / 128.0f; + RenderEntity* ent = &rc->entities[entity_idx]; + int size = ent->npc_size > 1 ? ent->npc_size : 1; + return (float)ent->y + (float)size * 0.5f; +} + +static void render_draw_minimap_entity_dot( + GuiState* gs, int sx, int sy, int sz_px, + Texture2D sprite, Color fallback +) { + if (sprite.id != 0) { + Rectangle src = { 0, 0, (float)sprite.width, (float)sprite.height }; + Rectangle dst = { (float)(sx + sz_px / 2 - 2), + (float)(sy + sz_px / 2 - 2), 4.0f, 4.0f }; + DrawTexturePro(sprite, src, dst, (Vector2){0,0}, 0.0f, WHITE); + } else { + DrawRectangle(sx, sy, sz_px, sz_px, fallback); + } + (void)gs; +} + +static void render_ensure_minimap_surface(RenderClient* rc, int w, int h) { + if (rc->minimap_surface.id != 0 && + rc->minimap_surface_w == w && + rc->minimap_surface_h == h) { + return; + } + + if (rc->minimap_surface.id != 0) { + UnloadRenderTexture(rc->minimap_surface); + } + + rc->minimap_surface = LoadRenderTexture(w, h); + if (rc->minimap_surface.id == 0 || rc->minimap_surface.texture.id == 0) { + fprintf(stderr, "minimap render texture allocation failed\n"); + abort(); + } + SetTextureFilter(rc->minimap_surface.texture, TEXTURE_FILTER_POINT); + rc->minimap_surface_w = w; + rc->minimap_surface_h = h; +} + +static void render_draw_minimap_compass(RenderClient* rc, GuiState* gs, Rectangle compass) { + int masked = gs->minimap_compass_masked.id != 0; + Texture2D comp = masked ? gs->minimap_compass_masked : gui_asset(gs, "compass"); + if (comp.id == 0) comp = gs->minimap_compass; + if (comp.id != 0) { + Rectangle src = {0, 0, (float)comp.width, (float)comp.height}; + float draw_w = masked ? (float)comp.width : compass.width; + float draw_h = masked ? (float)comp.height : compass.height; + Rectangle dst = { + compass.x + compass.width * 0.5f, + compass.y + compass.height * 0.5f, + draw_w, + draw_h, + }; + Vector2 origin = {draw_w * 0.5f, draw_h * 0.5f}; + float angle_deg = rc->cam_yaw * (180.0f / 3.14159265f); + DrawTexturePro(comp, src, dst, origin, angle_deg, WHITE); + } else { + gui_draw_named_asset(gs, "resize_compass_mask", compass, WHITE); + gui_text_shadow(gs, "N", (int)compass.x + 16, (int)compass.y + 12, 14, GUI_TEXT_ORANGE); + } +} + +static Rectangle render_minimap_spec_orb_rect(void) { + int orbs_x = GetScreenWidth() - GUI_MAP_CONTAINER_W + GUI_ORBS_X; + int orbs_y = GUI_ORBS_Y; + return (Rectangle){(float)(orbs_x + GUI_SPEC_X), (float)(orbs_y + GUI_SPEC_Y), 57, 34}; +} + +static void render_draw_minimap_area(RenderClient* rc, OsrsEnv* env, Player* p) { + GuiState* gs = &rc->gui; + int map_x = GetScreenWidth() - GUI_MAP_CONTAINER_W; + int map_y = 0; + int mask_w = GUI_MINIMAP_W; + int mask_h = GUI_MINIMAP_H; + int mask_x = map_x + GUI_MINIMAP_X; + int mask_y = map_y + GUI_MINIMAP_Y; + int compass_x = map_x + GUI_COMPASS_X; + int compass_y = map_y + GUI_COMPASS_Y; + + render_ensure_minimap_surface(rc, mask_w, mask_h); + BeginTextureMode(rc->minimap_surface); + ClearBackground(BLANK); + int map_cx = mask_w / 2; + int map_cy = mask_h / 2; + + int player_idx = -1; + for (int e = 0; e < rc->entity_count; e++) { + if (rc->entities[e].entity_type == ENTITY_PLAYER) { player_idx = e; break; } + } + + float player_x = player_idx >= 0 ? render_minimap_entity_center_x(rc, player_idx) : 0.0f; + float player_y = player_idx >= 0 ? render_minimap_entity_center_y(rc, player_idx) : 0.0f; + const float scale = 4.0f; + + for (int y = 0; y < GUI_MINIMAP_H; y++) { + for (int x = 0; x < GUI_MINIMAP_W; x++) { + if (!render_minimap_inside(x, y)) continue; + float sx = (float)x - 76.0f; + float sy = (float)y - 76.0f; + int wx = (int)roundf(player_x + sx / scale); + int wy = (int)roundf(player_y - sy / scale); + DrawPixel(x, y, render_minimap_tile_color(rc, wx, wy)); + } + } + + for (int dy = -20; dy <= 20; dy++) { + for (int dx = -20; dx <= 20; dx++) { + int sx = (int)roundf(76.0f + (float)dx * scale); + int sy = (int)roundf(76.0f - (float)dy * scale); + render_minimap_draw_tile_features(rc, + (int)roundf(player_x) + dx, + (int)roundf(player_y) + dy, + sx, sy); + } + } + + for (int e = 0; e < rc->entity_count; e++) { + RenderEntity* ent = &rc->entities[e]; + if (ent->entity_type == ENTITY_NPC && !ent->npc_visible) continue; + + float dx = render_minimap_entity_center_x(rc, e) - player_x; + float dy = render_minimap_entity_center_y(rc, e) - player_y; + if (dx < -40.0f || dx > 40.0f || dy < -40.0f || dy > 40.0f) continue; + + int dot_cx = (int)roundf((float)map_cx + dx * scale); + int dot_cy = (int)roundf((float)map_cy - dy * scale); + int rel_x = dot_cx - map_cx; + int rel_y = dot_cy - map_cy; + if (rel_x * rel_x + rel_y * rel_y > 68 * 68) continue; + + Texture2D dot_sprite = ent->entity_type == ENTITY_PLAYER + ? gs->minimap_dot_player + : gs->minimap_dot_npc; + Color fallback = ent->entity_type == ENTITY_PLAYER + ? WHITE + : (Color){220, 60, 60, 255}; + int dot_sz = ent->entity_type == ENTITY_PLAYER ? 5 : 4; + render_draw_minimap_entity_dot(gs, + dot_cx - dot_sz / 2, + dot_cy - dot_sz / 2, + dot_sz, + dot_sprite, + fallback); + + if (player_idx >= 0 && e != player_idx && + rc->entities[player_idx].attack_target_entity_idx == e) { + DrawRectangleLines(dot_cx - 3, dot_cy - 3, 7, 7, YELLOW); + } + } + EndTextureMode(); + + BeginMode2D(render_chrome_camera((float)RENDER_WINDOW_W, 0.0f)); + Rectangle surface_src = { 0, 0, (float)mask_w, -(float)mask_h }; + Rectangle surface_dst = { (float)mask_x, (float)mask_y, + (float)mask_w, (float)mask_h }; + DrawTexturePro(rc->minimap_surface.texture, surface_src, surface_dst, + (Vector2){0, 0}, 0.0f, WHITE); + + Rectangle compass = { + (float)compass_x, + (float)compass_y, + (float)GUI_COMPASS_W, + (float)GUI_COMPASS_H, + }; + render_draw_minimap_compass(rc, gs, compass); + + Rectangle cover = { + (float)(map_x + GUI_MAP_SURROUND_X), + (float)(map_y + GUI_MAP_SURROUND_Y), + (float)GUI_MAP_SURROUND_W, + (float)GUI_MAP_SURROUND_H, + }; + if (gui_asset(gs, "osrs_stretch_mapsurround").id != 0) { + gui_draw_named_asset(gs, "osrs_stretch_mapsurround", cover, WHITE); + } else { + Texture2D chrome_sprite = rc->layout_mode == 1 ? gs->rm_minimap_frame : gs->minimap_frame; + if (chrome_sprite.id != 0) { + Rectangle src = {0, 0, (float)chrome_sprite.width, (float)chrome_sprite.height}; + DrawTexturePro(chrome_sprite, src, cover, (Vector2){0, 0}, 0.0f, WHITE); + } + } + + int orbs_x = map_x + GUI_ORBS_X; + int orbs_y = map_y + GUI_ORBS_Y; + Rectangle xp_orb = {(float)(orbs_x + GUI_XP_X), (float)(orbs_y + GUI_XP_Y), 27, 27}; + Rectangle hp_orb = {(float)(orbs_x + GUI_HP_X), (float)(orbs_y + GUI_HP_Y), 57, 34}; + Rectangle prayer_orb = {(float)(orbs_x + GUI_PRAYER_X), (float)(orbs_y + GUI_PRAYER_Y), 57, 34}; + Rectangle run_orb = {(float)(orbs_x + GUI_RUN_X), (float)(orbs_y + GUI_RUN_Y), 57, 34}; + Rectangle spec_orb = render_minimap_spec_orb_rect(); + Rectangle worldmap_button = {(float)(orbs_x + GUI_WORLDMAP_X), (float)(orbs_y + GUI_WORLDMAP_Y), 30, 30}; + Rectangle wiki_button = {(float)(map_x + 166), (float)(map_y + 173), 40, 34}; + + gui_draw_named_asset(gs, "tli_button01_orb01_34x34_0", xp_orb, WHITE); + if (gui_asset(gs, "tli_button01_orb01_34x34_0").id == 0) { + gui_draw_named_asset(gs, "ring_34_0", xp_orb, WHITE); + } + gui_draw_named_asset_centered(gs, "orb_xp_0", xp_orb, 24, 24, WHITE); + + if (p) { + render_draw_minimap_orb(gs, hp_orb, "orb_filler_1", "orb_icon_0", + p->current_hitpoints, p->base_hitpoints, (Color){210, 50, 45, 255}); + render_draw_minimap_orb(gs, prayer_orb, "orb_filler_4", "orb_icon_1", + p->current_prayer, p->base_prayer, (Color){70, 120, 255, 255}); + render_draw_minimap_orb(gs, run_orb, "orb_filler_5", "orb_icon_2", + osrs_run_energy_percent(p->run_energy), 100, (Color){60, 210, 80, 255}); + render_draw_minimap_orb(gs, spec_orb, "orb_filler_9", "orb_icon_6", + p->special_energy, 100, + p->spec_armed ? (Color){80, 220, 90, 255} : (Color){225, 205, 55, 255}); + } + + gui_draw_named_asset(gs, "ring_30", worldmap_button, WHITE); + gui_draw_named_asset_centered(gs, "worldmap_icon_0", worldmap_button, 22, 22, WHITE); + gui_draw_named_asset(gs, "wiki_icon_0", wiki_button, WHITE); + EndMode2D(); + + (void)env; +} + +static int render_target_label_entity_idx(RenderClient* rc) { + if (rc->entity_count <= 0) return -1; + int target = rc->entities[0].attack_target_entity_idx; + if (target >= 0 && target < rc->entity_count) return target; + int gui_idx = rc->gui.gui_entity_idx; + if (gui_idx >= 0 && gui_idx < rc->entity_count) { + target = rc->entities[gui_idx].attack_target_entity_idx; + if (target >= 0 && target < rc->entity_count) return target; + } + for (int ei = 0; ei < rc->entity_count; ei++) { + if (rc->entities[ei].entity_type == ENTITY_NPC) return ei; + } + return rc->entity_count > 1 ? 1 : -1; +} + +static void render_draw_target_label(RenderClient* rc) { + int target = render_target_label_entity_idx(rc); + if (target < 0) return; + const char* label = TextFormat("Target: %s", render_entity_display_name(&rc->entities[target])); + int w = MeasureText(label, 16); + int x = (RENDER_GRID_W - w) / 2; + if (x < 120) x = 120; + DrawText(label, x + 1, 13, 16, BLACK); + DrawText(label, x, 12, 16, COLOR_TEXT); +} + +static int render_display_tick(OsrsEnv* env) { + if (env->encounter_def && env->encounter_state) { + return ((const EncounterDef*)env->encounter_def)->get_tick( + (EncounterState*)env->encounter_state, + (EncounterContext*)env->encounter_context); + } + return env->tick; +} + +static int render_scene_is_pvp(OsrsEnv* env) { + if (!env->encounter_def) return 1; + const EncounterDef* def = (const EncounterDef*)env->encounter_def; + return strcmp(def->name, "nh_pvp") == 0 || strcmp(def->name, "pvp") == 0; +} + +static int render_scene_is_inferno(OsrsEnv* env) { + if (!env->encounter_def) return 0; + const EncounterDef* def = (const EncounterDef*)env->encounter_def; + return strcmp(def->name, "inferno") == 0; +} + +static const char* render_control_hint_text(OsrsEnv* env) { + if (render_scene_is_inferno(env)) { + return "Right-drag: orbit Mid-drag: pan Scroll: zoom D: debug H: human F8: lab"; + } + return "Right-drag: orbit Mid-drag: pan Scroll: zoom SPACE: pause S: safe spots D: debug G: cycle entity H: human"; +} + +static void render_draw_default_top_hud(RenderClient* rc, int display_tick) { + DrawText(TextFormat("Tick: %d", display_tick), 10, 12, 16, COLOR_TEXT); + render_draw_target_label(rc); + + if (rc->entity_count >= 2) { + RenderEntity* p0 = &rc->entities[0]; + RenderEntity* p1 = &rc->entities[1]; + const char* hp_txt = TextFormat("P0: %d/%d P1: %d/%d", + p0->current_hitpoints, p0->base_hitpoints, + p1->current_hitpoints, p1->base_hitpoints); + int hp_w = MeasureText(hp_txt, 16); + DrawText(hp_txt, RENDER_GRID_W - hp_w - 12, 12, 16, COLOR_TEXT); + } +} + +static void render_draw_inferno_top_hud(OsrsEnv* env, int display_tick) { + InfernoState* s = render_inferno_state_from_env(env); + if (!s) { + DrawText(TextFormat("Tick: %d", display_tick), 10, 12, 16, COLOR_TEXT); + return; + } + DrawText(TextFormat("Tick: %d Wave: %d / %d", + display_tick, s->wave + 1, INF_NUM_WAVES), 10, 12, 16, COLOR_TEXT); +} + +static void render_draw_colosseum_top_hud(RenderClient* rc, OsrsEnv* env) { + ColosseumState* s = render_colosseum_state_from_env(env); + if (!s) return; + const char* wave_txt = s->wave == COLO_WAVE_BOSS + ? TextFormat("Wave: %d / %d (Sol Heredit)", s->wave + 1, COLO_NUM_WAVES) + : TextFormat("Wave: %d / %d", s->wave + 1, COLO_NUM_WAVES); + int wave_w = MeasureText(wave_txt, 16); + DrawText(wave_txt, (RENDER_GRID_W - wave_w) / 2, 32, 16, COLOR_TEXT); +} + +static const int COLOSSEUM_MODIFIER_ICON_SPRITE_IDS[COLO_NUM_REAL_MODIFIERS][3] = { + [COLO_MOD_BEES] = {5544, 5559, 5574}, + [COLO_MOD_BLASPHEMY] = {5538, 5553, 5568}, + [COLO_MOD_DOOM] = {5543, 5558, 5573}, + [COLO_MOD_DYNAMIC_DUO] = {5545, 0, 0}, + [COLO_MOD_FRAILTY] = {5541, 5556, 5571}, + [COLO_MOD_MANTIMAYHEM] = {5539, 5554, 5569}, + [COLO_MOD_MYOPIA] = {5547, 5562, 5577}, + [COLO_MOD_REENTRY] = {5536, 5551, 5566}, + [COLO_MOD_RED_FLAG] = {5540, 0, 0}, + [COLO_MOD_RELENTLESS] = {5535, 5550, 5565}, + [COLO_MOD_SOLARFLARE] = {5537, 5552, 5567}, + [COLO_MOD_QUARTET] = {5546, 0, 0}, + [COLO_MOD_TOTEMIC] = {5542, 0, 0}, + [COLO_MOD_VOLATILITY] = {5534, 5549, 5564}, +}; + +static Texture2D* render_require_colosseum_modifier_icon( + RenderClient* rc, + int modifier, + int tier +) { + if (modifier < 0 || modifier >= COLO_NUM_REAL_MODIFIERS || tier < 1 || tier > 3) { + fprintf(stderr, "render: invalid colosseum modifier icon modifier=%d tier=%d\n", + modifier, tier); + abort(); + } + int sprite_id = COLOSSEUM_MODIFIER_ICON_SPRITE_IDS[modifier][tier - 1]; + if (sprite_id <= 0) { + fprintf(stderr, "render: missing colosseum modifier icon mapping modifier=%d tier=%d\n", + modifier, tier); + abort(); + } + Texture2D* texture = &rc->colosseum_modifier_icons[modifier][tier - 1]; + if (texture->id == 0) { + char path[96]; + snprintf(path, sizeof(path), + "sprites/colosseum/modifiers/%d.png", sprite_id); + *texture = osrs_asset_load_texture(OSRS_ASSET(path)); + if (texture->id == 0) { + fprintf(stderr, "render: missing colosseum modifier icon asset %s\n", path); + abort(); + } + SetTextureFilter(*texture, TEXTURE_FILTER_POINT); + } + return texture; +} + +static void render_colosseum_modifier_tooltip_text( + int modifier, const char** out_name, const char** out_desc +) { + switch (modifier) { + case COLO_MOD_BEES: *out_name = "Bees!"; *out_desc = "Roaming poison swarms (1/2/3 per tier)."; break; + case COLO_MOD_BLASPHEMY: *out_name = "Blasphemy"; *out_desc = "Drains prayer for 20/40/60% of damage taken."; break; + case COLO_MOD_DOOM: *out_name = "Doom"; *out_desc = "Stacks on damage; die at 15/10/5 stacks."; break; + case COLO_MOD_DYNAMIC_DUO: *out_name = "Dynamic Duo"; *out_desc = "Shockwave Colossi spawn in pairs."; break; + case COLO_MOD_FRAILTY: *out_name = "Frailty"; *out_desc = "-10/-20/-40% max HP (T1+ disables overheal)."; break; + case COLO_MOD_MANTIMAYHEM: *out_name = "Mantimayhem"; *out_desc = "Manticore: extra orb / venom / unpredictable."; break; + case COLO_MOD_MYOPIA: *out_name = "Myopia"; *out_desc = "Player attack range -2/-4/-6."; break; + case COLO_MOD_REENTRY: *out_name = "Reentry"; *out_desc = "Javelin skyfall leaves molten sand."; break; + case COLO_MOD_RED_FLAG: *out_name = "Red Flag"; *out_desc = "Minotaurs route around obstacles."; break; + case COLO_MOD_RELENTLESS: *out_name = "Relentless"; *out_desc = "Bypass 33/66/100% def, +1/+3/+6 max hit."; break; + case COLO_MOD_SOLARFLARE: *out_name = "Solarflare"; *out_desc = "Orb circling the boss pillars."; break; + case COLO_MOD_QUARTET: *out_name = "Quartet"; *out_desc = "+1 random warbander each wave (incl. W12)."; break; + case COLO_MOD_TOTEMIC: *out_name = "Totemic"; *out_desc = "NPCs at 50% HP spawn a healing totem."; break; + case COLO_MOD_VOLATILITY: *out_name = "Volatility"; *out_desc = "Death explosion / molten pool."; break; + default: *out_name = "Unknown"; *out_desc = ""; break; + } +} + +static void render_draw_colosseum_modifier_hud(RenderClient* rc) { + EncounterOverlay* ov = &rc->encounter_overlay; + if (ov->active_modifier_count <= 0) return; + const int icon_size = 24; + const int gap = 4; + const int x0 = 10; + const int y0 = 36; + Vector2 mouse = GetMousePosition(); + int hover_modifier = -1; + int hover_tier = 0; + int hover_x = 0; + for (int i = 0; i < ov->active_modifier_count; i++) { + EncounterActiveModifier* active = &ov->active_modifiers[i]; + if (!active->active) continue; + Texture2D* texture = render_require_colosseum_modifier_icon( + rc, active->modifier, active->tier); + int x = x0 + i * (icon_size + gap); + DrawRectangle(x - 2, y0 - 2, icon_size + 4, icon_size + 4, + CLITERAL(Color){22, 19, 17, 210}); + DrawRectangleLines(x - 2, y0 - 2, icon_size + 4, icon_size + 4, + CLITERAL(Color){184, 146, 72, 230}); + Rectangle src = {0, 0, (float)texture->width, (float)texture->height}; + Rectangle dst = {(float)x, (float)y0, (float)icon_size, (float)icon_size}; + DrawTexturePro(*texture, src, dst, (Vector2){0, 0}, 0.0f, WHITE); + Rectangle hit = {(float)(x - 2), (float)(y0 - 2), + (float)(icon_size + 4), (float)(icon_size + 4)}; + if (CheckCollisionPointRec(mouse, hit)) { + hover_modifier = active->modifier; + hover_tier = active->tier; + hover_x = x; + } + } + if (hover_modifier >= 0) { + const char* name = NULL; + const char* desc = NULL; + render_colosseum_modifier_tooltip_text(hover_modifier, &name, &desc); + char title[64]; + if (hover_tier >= 1 && hover_tier <= 3) { + static const char* roman[4] = {"", "I", "II", "III"}; + snprintf(title, sizeof(title), "%s %s", name, roman[hover_tier]); + } else { + snprintf(title, sizeof(title), "%s", name); + } + const int title_fs = 12; + const int desc_fs = 10; + const int pad = 6; + int title_w = MeasureText(title, title_fs); + int desc_w = MeasureText(desc, desc_fs); + int box_w = (title_w > desc_w ? title_w : desc_w) + pad * 2; + int box_h = title_fs + desc_fs + pad * 2 + 2; + int box_x = hover_x; + int box_y = y0 + icon_size + 6; + if (box_x + box_w > RENDER_WINDOW_W - 4) box_x = RENDER_WINDOW_W - 4 - box_w; + if (box_x < 4) box_x = 4; + DrawRectangle(box_x, box_y, box_w, box_h, CLITERAL(Color){18, 16, 14, 235}); + DrawRectangleLines(box_x, box_y, box_w, box_h, CLITERAL(Color){184, 146, 72, 230}); + DrawText(title, box_x + pad, box_y + pad, title_fs, CLITERAL(Color){238, 222, 180, 255}); + DrawText(desc, box_x + pad, box_y + pad + title_fs + 2, desc_fs, CLITERAL(Color){200, 200, 200, 255}); + } +} + +static void render_colosseum_draft_wrap_text( + const GuiState* gs, const char* text, int x, int y, int max_w, int fs, int line_h, Color color +) { + char line[160]; + int line_len = 0; + int cur_y = y; + int i = 0; + while (text[i] != '\0') { + int ws = i; + while (text[i] != '\0' && text[i] != ' ') i++; + int word_len = i - ws; + if (word_len > (int)sizeof(line) - 1) word_len = (int)sizeof(line) - 1; + char candidate[160]; + int cand_len = line_len; + memcpy(candidate, line, (size_t)line_len); + if (line_len > 0) candidate[cand_len++] = ' '; + memcpy(candidate + cand_len, text + ws, (size_t)word_len); + cand_len += word_len; + candidate[cand_len] = '\0'; + if (line_len > 0 && MeasureText(candidate, fs) > max_w) { + line[line_len] = '\0'; + context_menu_draw_text_shadow(gs, line, x, cur_y, fs, color); + cur_y += line_h; + memcpy(line, text + ws, (size_t)word_len); + line_len = word_len; + } else { + memcpy(line, candidate, (size_t)cand_len); + line_len = cand_len; + } + while (text[i] == ' ') i++; + } + if (line_len > 0) { + line[line_len] = '\0'; + context_menu_draw_text_shadow(gs, line, x, cur_y, fs, color); + } +} + +static Rectangle render_colosseum_draft_card_rect(int option) { + const int card_w = 176; + const int card_h = 170; + const int gap = 14; + int total_w = COLO_MODIFIER_DRAFT_OPTIONS * card_w + (COLO_MODIFIER_DRAFT_OPTIONS - 1) * gap; + int x0 = (RENDER_GRID_W - total_w) / 2; + int y0 = RENDER_WINDOW_H / 2 - card_h / 2; + return CLITERAL(Rectangle){ + (float)(x0 + option * (card_w + gap)), (float)y0, (float)card_w, (float)card_h }; +} + +static void render_draw_colosseum_modifier_draft(RenderClient* rc, OsrsEnv* env) { + ColosseumState* cs = render_colosseum_state_from_env(env); + if (!cs || !cs->modifiers.draft_pending) return; + + DrawRectangle(0, 0, RENDER_GRID_W, RENDER_WINDOW_H, CLITERAL(Color){0, 0, 0, 150}); + + Rectangle first = render_colosseum_draft_card_rect(0); + const char* banner = "Choose a modifier to start the next wave"; + int banner_fs = 16; + int banner_w = MeasureText(banner, banner_fs); + context_menu_draw_text_shadow(&rc->gui, banner, + (RENDER_GRID_W - banner_w) / 2, (int)first.y - 34, banner_fs, + CLITERAL(Color){255, 255, 0, 255}); + + static const char* roman[4] = {"", "I", "II", "III"}; + Vector2 mouse = GetMousePosition(); + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { + int mod = cs->modifiers.draft_options[o]; + if (mod < 0) continue; + Rectangle card = render_colosseum_draft_card_rect(o); + int hover = CheckCollisionPointRec(mouse, card); + DrawRectangleRec(card, hover ? CLITERAL(Color){74, 60, 38, 245} + : CLITERAL(Color){53, 44, 31, 244}); + DrawRectangleLinesEx(card, hover ? 2.0f : 1.0f, CLITERAL(Color){170, 137, 72, 255}); + + const char* name = NULL; + const char* desc = NULL; + render_colosseum_modifier_tooltip_text(mod, &name, &desc); + + int next_tier = cs->modifiers.tier[mod] + 1; + if (next_tier > COLO_MODIFIER_MAX_TIER[mod]) next_tier = COLO_MODIFIER_MAX_TIER[mod]; + char title[64]; + if (COLO_MODIFIER_MAX_TIER[mod] > 1 && next_tier >= 1 && next_tier <= 3) + snprintf(title, sizeof(title), "%s %s", name, roman[next_tier]); + else + snprintf(title, sizeof(title), "%s", name); + + const int pad = 9; + context_menu_draw_text_shadow(&rc->gui, title, + (int)card.x + pad, (int)card.y + pad, 15, CLITERAL(Color){255, 255, 0, 255}); + render_colosseum_draft_wrap_text(&rc->gui, desc, + (int)card.x + pad, (int)card.y + pad + 28, (int)card.width - pad * 2, 11, 14, + CLITERAL(Color){255, 152, 31, 255}); + context_menu_draw_text_shadow(&rc->gui, hover ? "> click to pick <" : "click to pick", + (int)card.x + pad, (int)card.y + (int)card.height - 22, 11, + hover ? CLITERAL(Color){255, 255, 255, 255} : CLITERAL(Color){170, 170, 170, 255}); + } +} + +static void render_draw_top_hud(RenderClient* rc, OsrsEnv* env) { + int display_tick = render_display_tick(env); + if (render_scene_is_inferno(env)) { + render_draw_inferno_top_hud(env, display_tick); + return; + } + render_draw_default_top_hud(rc, display_tick); + if (env->encounter_def && + strcmp(((const EncounterDef*)env->encounter_def)->name, "colosseum") == 0) { + render_draw_colosseum_top_hud(rc, env); + render_draw_colosseum_modifier_hud(rc); + } +} + +static void render_follow_pvp_fighter_midpoint(RenderClient* rc, OsrsEnv* env, double frame_dt) { + if (!render_scene_is_pvp(env) || rc->human_input.enabled || rc->entity_count < 2) + return; + + float x0 = (float)rc->sub_x[0] / 128.0f; + float y0 = (float)rc->sub_y[0] / 128.0f; + float x1 = (float)rc->sub_x[1] / 128.0f; + float y1 = (float)rc->sub_y[1] / 128.0f; + float target_x = (x0 + x1) * 0.5f; + float target_z = -((y0 + y1) * 0.5f); + float lerp = 1.0f - powf(0.85f, (float)frame_dt * 60.0f); + + rc->cam_target_x += (target_x - rc->cam_target_x) * lerp; + rc->cam_target_z += (target_z - rc->cam_target_z) * lerp; +} + +void pvp_render(OsrsEnv* env) { + RenderClient* rc = (RenderClient*)env->client; + if (rc == NULL) { + rc = render_make_client(); + env->client = rc; + } + + render_populate_entities(rc, env); + render_ensure_entity_visual_slots(rc); + + render_handle_input(rc, env); + double frame_dt = GetFrameTime(); + double visual_dt = render_scaled_frame_dt(rc, frame_dt); + render_follow_pvp_fighter_midpoint(rc, env, frame_dt); + model_cache_update_texture_anims(rc->model_cache, (float)visual_dt); + model_cache_update_texture_anims(rc->npc_model_cache, (float)visual_dt); + model_cache_update_texture_anims(rc->projectile_model_cache, (float)visual_dt); + + if (rc->entity_count > 0 && rc->gui.gui_entity_idx < rc->entity_count) { + Player* gui_p = render_get_player_ptr(env, rc->gui.gui_entity_idx); + if (gui_p) { + if (!rc->human_input.enabled && rc->pre_sim_mutation_hook && + IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) + rc->pre_sim_mutation_hook(rc->pre_sim_mutation_hook_ctx); + gui_inv_handle_mouse(&rc->gui, gui_p, &rc->human_input); + } + } + + { + rc->client_tick_accumulator += visual_dt; + double client_tick = RENDER_CLIENT_TICK_SECONDS; + int steps = (int)(rc->client_tick_accumulator / client_tick); + if (steps > 0) { + rc->client_tick_accumulator -= steps * client_tick; + for (int s = 0; s < steps; s++) { + for (int i = 0; i < rc->entity_count; i++) { + render_client_tick(rc, i); + } + render_update_splats_client_tick(rc); + flight_client_tick(rc); + rc->effect_client_tick_counter++; + effect_client_tick(rc->effects, rc->effect_client_tick_counter); + for (int ei = 0; ei < MAX_ACTIVE_EFFECTS; ei++) { + ActiveEffect* e = &rc->effects[ei]; + if (e->type == EFFECT_NONE || !e->meta || + e->anim_playback.seq_id < 0) continue; + if (rc->effect_client_tick_counter < e->start_tick) continue; + OsrsModel* eom = effect_find_model(e->meta, rc->model_cache, + rc->npc_model_cache, rc->projectile_model_cache); + render_anim_playback_resolve(rc, &e->anim_playback, + eom ? (int)eom->base_vert_count : 0); + anim_playback_advance(&e->anim_playback); + } + { + const EncounterDef* edef_dust = + (const EncounterDef*)rc->gui.encounter_def; + if (edef_dust && rc->gui.encounter_state && + strcmp(edef_dust->name, "colosseum") == 0) { + const SolHereditState* sol = + &((ColosseumState*)rc->gui.encounter_state)->sol; + int age = sol->aoe_attack != COLO_SOL_AOE_NONE + ? sol->aoe_age : -1; + if (age == COLO_SOL_AOE_DAMAGE_AGE && + rc->prev_sol_aoe_age != COLO_SOL_AOE_DAMAGE_AGE) { + for (int ty = sol->boss_arena_min_y; + ty <= sol->boss_arena_max_y; ty++) { + for (int tx = sol->boss_arena_min_x; + tx <= sol->boss_arena_max_x; tx++) { + if (!col_sol_aoe_tile_is_hazard(sol, tx, ty)) + continue; + int dust_gfx = 2699 + + (((tx * 7 + ty * 13) & 0x7fffffff) % 8); + effect_spawn_spotanim_subtile( + rc->effects, dust_gfx, + tx * 128.0f + 64.0f, ty * 128.0f + 64.0f, + rc->effect_client_tick_counter + 1, + rc->spotanims, rc->anim_cache, + rc->model_cache, rc->npc_model_cache, + rc->projectile_model_cache); + } + } + } + rc->prev_sol_aoe_age = age; + } + } + gui_tick(&rc->gui); + human_tick_visuals(&rc->human_input); + } + } + } + + BeginDrawing(); + ClearBackground(COLOR_BG); + + render_draw_3d_world(rc); + + render_draw_overhead_status(rc, env); + + if (rc->show_debug && env->encounter_state && env->encounter_def && + strcmp(((const EncounterDef*)env->encounter_def)->name, "inferno") == 0) { + InfernoState* is = (InfernoState*)env->encounter_state; + int dy = RENDER_WINDOW_H - 160; + int dx = 10; + int fs = 12; + Color dc = (Color){220, 220, 220, 255}; + + int tgt = is->interaction.target_slot; + if (tgt >= 0 && tgt < INF_MAX_NPCS) { + InfNPC* tn = &is->npcs[tgt]; + const char* tname = inferno_npc_name(INF_NPC_DEF_IDS[tn->type]); + DrawText(TextFormat("TARGET: %s [#%d]", tname, tgt), dx, dy, fs, dc); + } else { + DrawText("TARGET: none", dx, dy, fs, (Color){180, 80, 80, 255}); + } + dy += fs + 2; + + const char* gear = is->weapon_set == INF_GEAR_MAGE ? "mage" : + is->weapon_set == INF_GEAR_LONG_RANGE ? "long" : "bp"; + const char* spell = "none"; + if (inf_autocast_is_active(is)) + spell = inf_player_autocast_spell(&is->player) == ENCOUNTER_SPELL_ICE + ? "ice" + : "blood"; + DrawText(TextFormat("GEAR: %s ATK: %d/%d SPELL: %s", gear, + is->player.attack_timer, is->loadout_stats[is->weapon_set].attack_speed, spell), + dx, dy, fs, dc); + dy += fs + 2; + + DrawText(TextFormat("RNG:%d MAG:%d DEF:%d", + is->player.current_ranged, is->player.current_magic, is->player.current_defence), + dx, dy, fs, dc); + dy += fs + 2; + + DrawText(TextFormat("BREW:%d REST:%d BAST:%d STAM:%d", + is->player.brew_doses, is->player.restore_doses, + is->player.bastion_doses, is->player.stamina_doses), + dx, dy, fs, dc); + dy += fs + 2; + + int mag_hits = 0, rng_hits = 0; + for (int h = 0; h < is->player_pending_hits.count; h++) { + if (is->player_pending_hits.hits[h].attack_style == ATTACK_STYLE_MAGIC) mag_hits++; + else rng_hits++; + } + if (is->player_pending_hits.count > 0) { + DrawText(TextFormat("INCOMING: %d (%dM %dR)", + is->player_pending_hits.count, mag_hits, rng_hits), + dx, dy, fs, (Color){255, 150, 150, 255}); + } + } + + if (rc->show_debug) { + for (int ei = 0; ei < rc->entity_count; ei++) { + ConvexHull2D* h = &rc->entity_hulls[ei]; + if (h->count < 3) continue; + Color col = (ei == rc->gui.gui_entity_idx) + ? (Color){ 0, 255, 0, 180 } : (Color){ 255, 0, 0, 180 }; + for (int hi = 0; hi < h->count; hi++) { + int ni = (hi + 1) % h->count; + DrawLine(h->xs[hi], h->ys[hi], h->xs[ni], h->ys[ni], col); + } + } + } + + render_draw_top_hud(rc, env); + DrawText(render_control_hint_text(env), 10, RENDER_WINDOW_H - 20, 10, COLOR_TEXT_DIM); + + rc->gui.gui_entity_count = rc->entity_count; + rc->gui.encounter_state = env->encounter_state; + rc->gui.encounter_def = env->encounter_def; + if (rc->gui.gui_entity_idx >= rc->entity_count) + rc->gui.gui_entity_idx = 0; + human_draw_click_cross(&rc->human_input, + rc->click_cross_sprites, + rc->click_cross_loaded); + + if (rc->show_debug) { + char dbg[256]; + snprintf(dbg, sizeof(dbg), "box: (%d,%d) world (%d,%d) plane: (%d,%d) hit3d: (%.1f,%.1f,%.1f)", + rc->debug_hit_wx, rc->debug_hit_wy, + rc->debug_hit_wx >= 0 ? rc->debug_hit_wx + rc->collision_world_offset_x : -1, + rc->debug_hit_wy >= 0 ? rc->debug_hit_wy + rc->collision_world_offset_y : -1, + rc->debug_plane_wx, rc->debug_plane_wy, + rc->debug_ray_hit_x, rc->debug_ray_hit_y, rc->debug_ray_hit_z); + DrawText(dbg, 10, 30, 16, MAGENTA); + snprintf(dbg, sizeof(dbg), "ray org: (%.1f,%.1f,%.1f) dir: (%.3f,%.3f,%.3f)", + rc->debug_ray_origin.x, rc->debug_ray_origin.y, rc->debug_ray_origin.z, + rc->debug_ray_dir.x, rc->debug_ray_dir.y, rc->debug_ray_dir.z); + DrawText(dbg, 10, 48, 16, MAGENTA); + for (int ei = 0; ei < rc->entity_count; ei++) { + RenderEntity* ent = &rc->entities[ei]; + if (ent->entity_type != ENTITY_PLAYER) continue; + snprintf(dbg, sizeof(dbg), "player tile: local (%d,%d) world (%d,%d)", + ent->x, ent->y, + ent->x + rc->collision_world_offset_x, + ent->y + rc->collision_world_offset_y); + DrawText(dbg, 10, 66, 16, (Color){ 0, 255, 128, 255 }); + break; + } + } + + if (rc->entity_count > 0) { + Player* gui_player = render_get_player_ptr(env, rc->gui.gui_entity_idx); + rc->gui.pending_spell_highlight = -1; + if (rc->human_input.cursor_mode == CURSOR_SPELL_TARGET) { + rc->gui.pending_spell_highlight = rc->human_input.selected_spell_gui_idx; + } + rc->gui.display_inventory_count = 0; + { + ColosseumState* colo_inv = render_colosseum_state_from_env(env); + if (colo_inv && colo_inv->active_loadout_profile >= 0 && + colo_inv->active_loadout_profile < COLO_NUM_LOADOUT_PROFILES) { + int live_kit[COLO_INVENTORY_DISPLAY_SLOTS]; + col_build_live_inventory_display(colo_inv, live_kit); + for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS && + i < INV_GRID_SLOTS; i++) + rc->gui.display_inventory_osrs_ids[i] = live_kit[i]; + rc->gui.display_inventory_count = COLO_INVENTORY_DISPLAY_SLOTS; + } + } + if (gui_player) { + BeginMode2D(render_chrome_camera( + (float)RENDER_WINDOW_W, (float)RENDER_WINDOW_H)); + gui_draw(&rc->gui, gui_player); + EndMode2D(); + } + + render_draw_minimap_area(rc, env, gui_player); + + if (rc->show_debug) { + int target_idx = render_target_label_entity_idx(rc); + if (target_idx >= 0 && rc->entities[target_idx].entity_type == ENTITY_NPC) { + render_draw_panel_npc(10, 70, &rc->entities[target_idx], env); + } + } + } + + render_draw_encounter_status_text(rc); + render_draw_colosseum_grapple_banner(rc, env); + render_lab_draw_hud(rc); + + render_draw_colosseum_modifier_draft(rc, env); + + context_menu_draw(rc); + + EndDrawing(); +} + +#endif diff --git a/ocean/osrs/osrs_render_click_hull.h b/ocean/osrs/osrs_render_click_hull.h new file mode 100644 index 0000000000..9937fd06f5 --- /dev/null +++ b/ocean/osrs/osrs_render_click_hull.h @@ -0,0 +1,65 @@ +#ifndef OSRS_RENDER_CLICK_HULL_H +#define OSRS_RENDER_CLICK_HULL_H + +#if __has_include("raylib.h") +#include "raylib.h" +#elif __has_include("raylib-5.5_macos/include/raylib.h") +#include "raylib-5.5_macos/include/raylib.h" +#else +#error "raylib.h not found" +#endif +#include "osrs_encounter.h" + +#define RENDER_CLICKBOX_PRISM_SIDE_COUNT 6 +#define RENDER_CLICKBOX_PRISM_POINT_COUNT (RENDER_CLICKBOX_PRISM_SIDE_COUNT * 2) + +static inline int render_entity_clickbox_size(const RenderEntity* entity) { + return entity->npc_size > 1 ? entity->npc_size : 1; +} + +static inline float render_entity_clickbox_radius_tiles(const RenderEntity* entity) { + return (float)render_entity_clickbox_size(entity) * 0.4f; +} + +static inline float render_entity_clickbox_height_tiles( + const RenderEntity* entity, float visual_height_tiles +) { + float min_height = (float)render_entity_clickbox_size(entity); + float model_height = visual_height_tiles * 0.4f; + return model_height > min_height ? model_height : min_height; +} + +static inline int render_build_entity_clickbox_prism_points( + const RenderEntity* entity, + float center_x, + float center_z, + float ground_y, + float visual_height_tiles, + Vector3* out, + int out_capacity +) { + if (entity->entity_type != ENTITY_NPC) return 0; + if (out_capacity < RENDER_CLICKBOX_PRISM_POINT_COUNT) return 0; + + float radius = render_entity_clickbox_radius_tiles(entity); + float height = render_entity_clickbox_height_tiles(entity, visual_height_tiles); + static const float hex[RENDER_CLICKBOX_PRISM_SIDE_COUNT][2] = { + { 1.0f, 0.0f }, + { 0.5f, 0.8660254038f }, + { -0.5f, 0.8660254038f }, + { -1.0f, 0.0f }, + { -0.5f, -0.8660254038f }, + { 0.5f, -0.8660254038f }, + }; + + int n = 0; + for (int i = 0; i < RENDER_CLICKBOX_PRISM_SIDE_COUNT; i++) { + float x = center_x + hex[i][0] * radius; + float z = center_z + hex[i][1] * radius; + out[n++] = (Vector3){ x, ground_y, z }; + out[n++] = (Vector3){ x, ground_y + height, z }; + } + return n; +} + +#endif diff --git a/ocean/osrs/osrs_render_motion.h b/ocean/osrs/osrs_render_motion.h new file mode 100644 index 0000000000..a3336a9d28 --- /dev/null +++ b/ocean/osrs/osrs_render_motion.h @@ -0,0 +1,132 @@ +#ifndef OSRS_RENDER_MOTION_H +#define OSRS_RENDER_MOTION_H + +#include +#include + +#define OSRS_RENDER_SUB_UNITS_PER_TILE 128.0f +#define OSRS_RENDER_CLIENT_TICKS_PER_GAME_TICK 30.0f +#define OSRS_RENDER_ENTITY_GROUND_LIFT_OSRS_UNITS 8.0f +#define OSRS_RENDER_ENTITY_GROUND_LIFT \ + (OSRS_RENDER_ENTITY_GROUND_LIFT_OSRS_UNITS / OSRS_RENDER_SUB_UNITS_PER_TILE) +#define OSRS_RENDER_RUN_SPEED_THRESHOLD 8.0f +#define OSRS_RENDER_WAYPOINT_QUEUE_DEPTH 10 +#define OSRS_RENDER_AXIS_SNAP_SUB_UNITS 256.0f + +typedef enum { + RENDER_MOVEMENT_NORMAL = 0, + RENDER_MOVEMENT_TELEPORT = 1, +} RenderMovementKind; + +typedef struct { + float x[OSRS_RENDER_WAYPOINT_QUEUE_DEPTH]; + float y[OSRS_RENDER_WAYPOINT_QUEUE_DEPTH]; + int running[OSRS_RENDER_WAYPOINT_QUEUE_DEPTH]; + int length; +} OsrsRenderWaypointQueue; + +static inline float osrs_render_entity_model_ground(float ground) { + return ground + OSRS_RENDER_ENTITY_GROUND_LIFT; +} + +static inline int osrs_render_speed_one_client_tick( + int path_length, + int waypoint_running, + int* stall_debt +) { + int speed = 4; + if (path_length > 2) speed = 6; + if (path_length > 3) speed = 8; + if (stall_debt != NULL && *stall_debt > 0 && path_length > 1) { + speed = 8; + (*stall_debt)--; + } + if (waypoint_running) speed <<= 1; + return speed; +} + +static inline int osrs_render_speed_uses_run_pose(float effective_speed) { + return effective_speed >= OSRS_RENDER_RUN_SPEED_THRESHOLD; +} + +static inline int osrs_render_should_seed_visual_position( + int visible, + int new_identity, + int became_visible, + RenderMovementKind movement_kind +) { + if (!visible) return 0; + return new_identity || became_visible || + movement_kind == RENDER_MOVEMENT_TELEPORT; +} + +static inline float osrs_render_advance_axis_toward( + float sub, float dest, float speed +) { + float dx = dest - sub; + if (dx == 0.0f) return sub; + if (dx > 0.0f) return sub + (dx > speed ? speed : dx); + return sub + (dx < -speed ? -speed : dx); +} + +static inline void osrs_render_waypoint_queue_clear(OsrsRenderWaypointQueue* q) { + q->length = 0; +} + +static inline void osrs_render_waypoint_push( + OsrsRenderWaypointQueue* q, float x, float y, int running +) { + int n = q->length < OSRS_RENDER_WAYPOINT_QUEUE_DEPTH - 1 + ? q->length + : OSRS_RENDER_WAYPOINT_QUEUE_DEPTH - 1; + for (int i = n; i > 0; i--) { + q->x[i] = q->x[i - 1]; + q->y[i] = q->y[i - 1]; + q->running[i] = q->running[i - 1]; + } + q->x[0] = x; + q->y[0] = y; + q->running[0] = running; + q->length = n + 1; +} + +static inline int osrs_render_waypoint_advance_one_client_tick( + OsrsRenderWaypointQueue* q, + float* sub_x, + float* sub_y, + int* stall_debt, + int* speed_out, + float* dir_dx, + float* dir_dy +) { + if (q->length == 0) { + *speed_out = 0; + *dir_dx = 0.0f; + *dir_dy = 0.0f; + return 0; + } + int wi = q->length - 1; + float wx = q->x[wi]; + float wy = q->y[wi]; + *dir_dx = wx - *sub_x; + *dir_dy = wy - *sub_y; + if (*dir_dx > OSRS_RENDER_AXIS_SNAP_SUB_UNITS || + *dir_dx < -OSRS_RENDER_AXIS_SNAP_SUB_UNITS || + *dir_dy > OSRS_RENDER_AXIS_SNAP_SUB_UNITS || + *dir_dy < -OSRS_RENDER_AXIS_SNAP_SUB_UNITS) { + *sub_x = wx; + *sub_y = wy; + q->length--; + *speed_out = 0; + return 1; + } + int speed = osrs_render_speed_one_client_tick( + q->length, q->running[wi], stall_debt); + *sub_x = osrs_render_advance_axis_toward(*sub_x, wx, (float)speed); + *sub_y = osrs_render_advance_axis_toward(*sub_y, wy, (float)speed); + if (*sub_x == wx && *sub_y == wy) q->length--; + *speed_out = speed; + return 1; +} + +#endif diff --git a/ocean/osrs/osrs_scene_assets.h b/ocean/osrs/osrs_scene_assets.h new file mode 100644 index 0000000000..ada3351459 --- /dev/null +++ b/ocean/osrs/osrs_scene_assets.h @@ -0,0 +1,80 @@ +#ifndef OSRS_SCENE_ASSETS_H +#define OSRS_SCENE_ASSETS_H + +#include "osrs_assets.h" +#include "osrs_collision.h" +#include "osrs_objects.h" +#include "osrs_render.h" +#include "osrs_terrain.h" + +typedef struct { + OsrsAssetGroupKind required_groups[4]; + + const char* terrain_path; + const char* objects_path; + const char* objects_secondary_path; + const char* cmap_path; + const char* npc_models_path; + const char* npc_anims_path; + + int world_origin_x; + int world_origin_y; +} EncounterSceneConfig; + +static inline CollisionMap* encounter_load_scene_assets( + RenderClient* rc, const EncounterSceneConfig* cfg +) { + if (!rc || !cfg) { + fprintf(stderr, "encounter_load_scene_assets: null rc or cfg\n"); + abort(); + } + + for (int i = 0; i < (int)(sizeof(cfg->required_groups) / sizeof(cfg->required_groups[0])); i++) { + OsrsAssetGroupKind group = cfg->required_groups[i]; + if ((int)group < 0) break; + osrs_asset_require_group(group); + } + + rc->model_cache = model_cache_load(OSRS_ASSET("equipment.models")); + if (rc->model_cache) rc->show_models = 1; + rc->anim_cache = anim_cache_load(OSRS_ASSET("equipment.anims")); + render_load_projectile_assets(rc); + render_init_overlay_models(rc); + + if (cfg->terrain_path) { + rc->terrain = terrain_load(cfg->terrain_path); + if (rc->terrain && (cfg->world_origin_x || cfg->world_origin_y)) + terrain_offset(rc->terrain, cfg->world_origin_x, cfg->world_origin_y); + } + + if (cfg->objects_path) { + rc->objects = objects_load(cfg->objects_path); + if (rc->objects && (cfg->world_origin_x || cfg->world_origin_y)) + objects_offset(rc->objects, cfg->world_origin_x, cfg->world_origin_y); + } + + if (cfg->objects_secondary_path) { + rc->objects_zuk = objects_load(cfg->objects_secondary_path); + if (rc->objects_zuk && (cfg->world_origin_x || cfg->world_origin_y)) + objects_offset(rc->objects_zuk, cfg->world_origin_x, cfg->world_origin_y); + } + + if (cfg->npc_models_path) + rc->npc_model_cache = model_cache_load(cfg->npc_models_path); + if (cfg->npc_anims_path) + rc->npc_anim_cache = anim_cache_load(cfg->npc_anims_path); + + CollisionMap* cmap = NULL; + if (cfg->cmap_path) { + cmap = collision_map_load(cfg->cmap_path); + if (cmap) { + rc->collision_map = cmap; + rc->collision_world_offset_x = cfg->world_origin_x; + rc->collision_world_offset_y = cfg->world_origin_y; + } + } + + return cmap; +} + +#endif diff --git a/ocean/osrs/osrs_special_attacks.h b/ocean/osrs/osrs_special_attacks.h new file mode 100644 index 0000000000..f3be724b22 --- /dev/null +++ b/ocean/osrs/osrs_special_attacks.h @@ -0,0 +1,542 @@ +#ifndef OSRS_SPECIAL_ATTACKS_H +#define OSRS_SPECIAL_ATTACKS_H + +#include + +#include "osrs_combat.h" +#include "osrs_items.h" + +#define BLOWPIPE_SPEC_ACC_MULT 2 +#define BLOWPIPE_SPEC_DMG_NUM 3 +#define BLOWPIPE_SPEC_DMG_DEN 2 +#define BLOWPIPE_SPEC_HEAL_PCT 50 +#define BLOWPIPE_SPEC_COST 50 + +/** Blowpipe special: accuracy and damage roll only; the caller applies the heal. */ +static inline int osrs_blowpipe_spec_resolve( + int base_att_roll, int base_max_hit, + int target_def_level, int target_ranged_def_bonus, + uint32_t* rng_state +) { + int att_roll = base_att_roll * BLOWPIPE_SPEC_ACC_MULT; + int def_roll = (target_def_level + 9) * (target_ranged_def_bonus + 64); + int spec_max = base_max_hit * BLOWPIPE_SPEC_DMG_NUM / BLOWPIPE_SPEC_DMG_DEN; + if (encounter_roll_hit_chance(rng_state, att_roll, def_roll)) + return encounter_rand_int(rng_state, spec_max + 1); + return 0; +} + +typedef struct { + int num_hits; + int damage[4]; + int total_damage; + int heal; + int def_drain; + int magic_def_drain; + int prayer_restore; + int freeze_ticks; + int spec_cost; + int attack_speed_override; /* 0 = use weapon speed */ +} SpecResult; + +/** Spec energy cost by weapon item index; 0 = the weapon has no special. */ +static inline int osrs_spec_cost(int weapon_item_idx) { + switch (weapon_item_idx) { + case ITEM_AGS: return 50; + case ITEM_DRAGON_CLAWS: return 50; + case ITEM_STATIUS_WARHAMMER: return 35; + case ITEM_BGS: return 50; + case ITEM_ZGS: return 50; + case ITEM_SGS: return 50; + case ITEM_ANCIENT_GS: return 50; + case ITEM_VESTAS: return 25; + case ITEM_VOIDWAKER: return 50; + case ITEM_GRANITE_MAUL: return 50; + case ITEM_DRAGON_DAGGER: return 25; + case ITEM_ELDER_MAUL: return 50; + case ITEM_TOXIC_BLOWPIPE: return 50; + case ITEM_MAGIC_SHORTBOW_I: return 50; + case ITEM_DARK_BOW: return 55; + case ITEM_ZARYTE_CROSSBOW: return 75; + case ITEM_HEAVY_BALLISTA: return 65; + case ITEM_MORRIGANS_JAVELIN: return 50; + case ITEM_ARMADYL_CROSSBOW: return 50; + case ITEM_VOLATILE_STAFF: return 55; + case ITEM_EYE_OF_AYAK: return 50; + default: return 0; + } +} +/** Resolve a special attack by weapon item index. */ +static inline SpecResult osrs_resolve_spec( + int weapon_item_idx, int att_roll, int max_hit, + int def_roll, int target_def_level, uint32_t* rng_state +) { + SpecResult r = {0, {0, 0, 0, 0}, 0, 0, 0, 0, 0, 0, 0, 0}; + + switch (weapon_item_idx) { + + case ITEM_AGS: { + int spec_att = att_roll * 2; + int spec_max = max_hit * 11 / 8; + r.spec_cost = 50; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) + r.damage[0] = encounter_rand_int(rng_state, spec_max + 1); + r.total_damage = r.damage[0]; + break; + } + + case ITEM_DRAGON_CLAWS: { + r.spec_cost = 50; + r.num_hits = 4; + + int roll1 = encounter_roll_hit_chance(rng_state, att_roll, def_roll); + int roll2 = encounter_roll_hit_chance(rng_state, att_roll, def_roll); + int roll3 = encounter_roll_hit_chance(rng_state, att_roll, def_roll); + int roll4 = encounter_roll_hit_chance(rng_state, att_roll, def_roll); + + if (roll1) { + int low = max_hit; + int high = max_hit + low - 1; + int total = low + encounter_rand_int(rng_state, high - low + 1); + r.damage[0] = total / 2; + r.damage[1] = total / 4; + r.damage[2] = total / 8; + r.damage[3] = total / 8 + 1; + } else if (roll2) { + int low = max_hit * 3 / 4; + int high = max_hit + low - 1; + int total = low + encounter_rand_int(rng_state, high - low + 1); + r.damage[0] = total / 2; + r.damage[1] = total / 4; + r.damage[2] = total / 4 + 1; + r.damage[3] = 0; + } else if (roll3) { + int low = max_hit / 2; + int high = max_hit + low - 1; + int total = low + encounter_rand_int(rng_state, high - low + 1); + r.damage[0] = total / 2; + r.damage[1] = total / 2 + 1; + r.damage[2] = 0; + r.damage[3] = 0; + } else if (roll4) { + int low = max_hit / 4; + int high = max_hit + low - 1; + int total = low + encounter_rand_int(rng_state, high - low + 1); + r.damage[0] = total + 1; + r.damage[1] = 0; + r.damage[2] = 0; + r.damage[3] = 0; + } else { + if (encounter_rand_int(rng_state, 3) < 2) { + r.damage[0] = 1; r.damage[1] = 1; + } + r.damage[2] = 0; r.damage[3] = 0; + } + r.total_damage = r.damage[0] + r.damage[1] + r.damage[2] + r.damage[3]; + break; + } + + case ITEM_STATIUS_WARHAMMER: { + int spec_att = att_roll * 5 / 4; + int spec_max = max_hit * 5 / 4; + r.spec_cost = 35; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) { + r.damage[0] = encounter_rand_int(rng_state, spec_max + 1); + r.def_drain = target_def_level * 30 / 100; + } + r.total_damage = r.damage[0]; + break; + } + + case ITEM_BGS: { + int spec_att = att_roll * 2; + int spec_max = max_hit * 121 / 100; + r.spec_cost = 50; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) { + r.damage[0] = encounter_rand_int(rng_state, spec_max + 1); + r.def_drain = r.damage[0]; + } + r.total_damage = r.damage[0]; + break; + } + + case ITEM_ZGS: { + int spec_att = att_roll * 2; + int spec_max = max_hit * 11 / 10; + r.spec_cost = 50; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) { + r.damage[0] = encounter_rand_int(rng_state, spec_max + 1); + if (r.damage[0] > 0) r.freeze_ticks = 32; + } + r.total_damage = r.damage[0]; + break; + } + + case ITEM_SGS: { + int spec_att = att_roll * 2; + int spec_max = max_hit * 11 / 10; + r.spec_cost = 50; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) + r.damage[0] = encounter_rand_int(rng_state, spec_max + 1); + r.total_damage = r.damage[0]; + if (r.total_damage > 0) { + r.heal = r.total_damage / 2; + if (r.heal < 10) r.heal = 10; + r.prayer_restore = r.total_damage / 4; + if (r.prayer_restore < 5) r.prayer_restore = 5; + } + break; + } + + /* ancient godsword blood-prison delayed damage is NOT modeled here; the pvp + layer approximates it with its own delayed hit */ + case ITEM_ANCIENT_GS: { + int spec_att = att_roll * 2; + int spec_max = max_hit * 11 / 10; + r.spec_cost = 50; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) + r.damage[0] = encounter_rand_int(rng_state, spec_max + 1); + r.total_damage = r.damage[0]; + break; + } + + case ITEM_VESTAS: { + int vls_max = max_hit * 6 / 5; + int vls_min = max_hit / 5; + int reduced_def = def_roll / 4; + r.spec_cost = 25; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, att_roll, reduced_def)) + r.damage[0] = vls_min + encounter_rand_int(rng_state, vls_max - vls_min + 1); + r.total_damage = r.damage[0]; + break; + } + + case ITEM_VOIDWAKER: { + int vw_min = max_hit / 2; + int vw_max = max_hit * 3 / 2; + int reduced_def = def_roll / 4; + r.spec_cost = 50; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, att_roll, reduced_def)) + r.damage[0] = vw_min + encounter_rand_int(rng_state, vw_max - vw_min + 1); + r.total_damage = r.damage[0]; + break; + } + + case ITEM_GRANITE_MAUL: { + r.spec_cost = 50; + r.num_hits = 1; + r.attack_speed_override = 1; + if (encounter_roll_hit_chance(rng_state, att_roll, def_roll)) + r.damage[0] = encounter_rand_int(rng_state, max_hit + 1); + r.total_damage = r.damage[0]; + break; + } + + case ITEM_DRAGON_DAGGER: { + int spec_att = att_roll * 23 / 20; + int spec_max = max_hit * 23 / 20; + r.spec_cost = 25; + r.num_hits = 2; + for (int i = 0; i < 2; i++) { + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) + r.damage[i] = encounter_rand_int(rng_state, spec_max + 1); + } + r.total_damage = r.damage[0] + r.damage[1]; + break; + } + + case ITEM_ELDER_MAUL: { + int spec_att = att_roll * 5 / 4; + r.spec_cost = 50; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) { + r.damage[0] = encounter_rand_int(rng_state, max_hit + 1); + r.def_drain = target_def_level * 35 / 100; + } + r.total_damage = r.damage[0]; + break; + } + + case ITEM_TOXIC_BLOWPIPE: { + int spec_att = att_roll * 2; + int spec_max = max_hit * 3 / 2; + r.spec_cost = 50; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) + r.damage[0] = encounter_rand_int(rng_state, spec_max + 1); + r.total_damage = r.damage[0]; + r.heal = r.total_damage / 2; + break; + } + + case ITEM_MAGIC_SHORTBOW_I: { + int spec_att = att_roll * 10 / 7; + r.spec_cost = 50; + r.num_hits = 2; + for (int i = 0; i < 2; i++) { + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) + r.damage[i] = encounter_rand_int(rng_state, max_hit + 1); + } + r.total_damage = r.damage[0] + r.damage[1]; + break; + } + + case ITEM_DARK_BOW: { + int spec_max = max_hit * 3 / 2; + if (spec_max > 48) spec_max = 48; + r.spec_cost = 55; + r.num_hits = 2; + for (int i = 0; i < 2; i++) { + if (encounter_roll_hit_chance(rng_state, att_roll, def_roll)) { + int dmg = encounter_rand_int(rng_state, spec_max + 1); + r.damage[i] = dmg < 8 ? 8 : dmg; + } else { + r.damage[i] = 8; + } + } + r.total_damage = r.damage[0] + r.damage[1]; + break; + } + + case ITEM_HEAVY_BALLISTA: { + int spec_att = att_roll * 5 / 4; + int spec_max = max_hit * 5 / 4; + r.spec_cost = 65; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) + r.damage[0] = encounter_rand_int(rng_state, spec_max + 1); + r.total_damage = r.damage[0]; + break; + } + + /* the ZCB guaranteed bolt proc is NOT rolled here: the caller must pass + is_zcb_spec=1 to osrs_resolve_bolt_proc after this */ + case ITEM_ZARYTE_CROSSBOW: { + int spec_att = att_roll * 2; + r.spec_cost = 75; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) + r.damage[0] = encounter_rand_int(rng_state, max_hit + 1); + r.total_damage = r.damage[0]; + break; + } + + /* approximation: modeled VLS-like; the real javelin is an initial hit plus a + bleed, which the pvp layer adds separately */ + case ITEM_MORRIGANS_JAVELIN: { + int morr_max = max_hit * 6 / 5; + int morr_min = max_hit / 5; + int reduced_def = def_roll / 4; + r.spec_cost = 50; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, att_roll, reduced_def)) + r.damage[0] = morr_min + encounter_rand_int(rng_state, morr_max - morr_min + 1); + r.total_damage = r.damage[0]; + break; + } + + case ITEM_ARMADYL_CROSSBOW: { + int spec_att = att_roll * 2; + r.spec_cost = 50; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) + r.damage[0] = encounter_rand_int(rng_state, max_hit + 1); + r.total_damage = r.damage[0]; + break; + } + + /* volatile max hit scales with magic level; the sim assumes 99 magic = 58 */ + case ITEM_VOLATILE_STAFF: { + int spec_att = att_roll * 3 / 2; + int vol_max = 58; + r.spec_cost = 55; + r.num_hits = 1; + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) + r.damage[0] = encounter_rand_int(rng_state, vol_max + 1); + r.total_damage = r.damage[0]; + break; + } + + case ITEM_EYE_OF_AYAK: { + int spec_att = att_roll * 2; + int spec_max = max_hit * 13 / 10; + r.spec_cost = 50; + r.num_hits = 1; + r.attack_speed_override = 5; + if (encounter_roll_hit_chance(rng_state, spec_att, def_roll)) { + r.damage[0] = encounter_rand_int(rng_state, spec_max + 1); + r.magic_def_drain = r.damage[0]; + } + r.total_damage = r.damage[0]; + break; + } + + default: + break; + } + + return r; +} + +/** Rewrite an already-resolved special attack to the weapon's deterministic + best outcome (guaranteed-max path). */ +static inline void osrs_spec_result_force_max( + SpecResult* r, int weapon_item_idx, int max_hit, int target_def_level +) { + assert(r != NULL); + SpecResult forced = {0, {0, 0, 0, 0}, 0, 0, 0, 0, 0, 0, 0, 0}; + forced.spec_cost = osrs_spec_cost(weapon_item_idx); + + switch (weapon_item_idx) { + case ITEM_AGS: + forced.num_hits = 1; + forced.damage[0] = max_hit * 11 / 8; + break; + + case ITEM_DRAGON_CLAWS: { + int total = 2 * max_hit - 1; + forced.num_hits = 4; + forced.damage[0] = total / 2; + forced.damage[1] = total / 4; + forced.damage[2] = total / 8; + forced.damage[3] = total / 8 + 1; + break; + } + + case ITEM_STATIUS_WARHAMMER: + forced.num_hits = 1; + forced.damage[0] = max_hit * 5 / 4; + forced.def_drain = target_def_level * 30 / 100; + break; + + case ITEM_BGS: + forced.num_hits = 1; + forced.damage[0] = max_hit * 121 / 100; + forced.def_drain = forced.damage[0]; + break; + + case ITEM_ZGS: + forced.num_hits = 1; + forced.damage[0] = max_hit * 11 / 10; + if (forced.damage[0] > 0) forced.freeze_ticks = 32; + break; + + case ITEM_SGS: + forced.num_hits = 1; + forced.damage[0] = max_hit * 11 / 10; + if (forced.damage[0] > 0) { + forced.heal = forced.damage[0] / 2; + if (forced.heal < 10) forced.heal = 10; + forced.prayer_restore = forced.damage[0] / 4; + if (forced.prayer_restore < 5) forced.prayer_restore = 5; + } + break; + + case ITEM_ANCIENT_GS: + forced.num_hits = 1; + forced.damage[0] = max_hit * 11 / 10; + break; + + case ITEM_VESTAS: + forced.num_hits = 1; + forced.damage[0] = max_hit * 6 / 5; + break; + + case ITEM_VOIDWAKER: + forced.num_hits = 1; + forced.damage[0] = max_hit * 3 / 2; + break; + + case ITEM_GRANITE_MAUL: + forced.num_hits = 1; + forced.damage[0] = max_hit; + forced.attack_speed_override = 1; + break; + + case ITEM_DRAGON_DAGGER: { + int spec_max = max_hit * 23 / 20; + forced.num_hits = 2; + forced.damage[0] = spec_max; + forced.damage[1] = spec_max; + break; + } + + case ITEM_ELDER_MAUL: + forced.num_hits = 1; + forced.damage[0] = max_hit; + forced.def_drain = target_def_level * 35 / 100; + break; + + case ITEM_TOXIC_BLOWPIPE: + forced.num_hits = 1; + forced.damage[0] = max_hit * 3 / 2; + forced.heal = forced.damage[0] / 2; + break; + + case ITEM_MAGIC_SHORTBOW_I: + forced.num_hits = 2; + forced.damage[0] = max_hit; + forced.damage[1] = max_hit; + break; + + case ITEM_DARK_BOW: { + int spec_max = max_hit * 3 / 2; + if (spec_max > 48) spec_max = 48; + if (spec_max < 8) spec_max = 8; + forced.num_hits = 2; + forced.damage[0] = spec_max; + forced.damage[1] = spec_max; + break; + } + + case ITEM_HEAVY_BALLISTA: + forced.num_hits = 1; + forced.damage[0] = max_hit * 5 / 4; + break; + + case ITEM_ZARYTE_CROSSBOW: + forced.num_hits = 1; + forced.damage[0] = max_hit; + break; + + case ITEM_MORRIGANS_JAVELIN: + forced.num_hits = 1; + forced.damage[0] = max_hit * 6 / 5; + break; + + case ITEM_ARMADYL_CROSSBOW: + forced.num_hits = 1; + forced.damage[0] = max_hit; + break; + + case ITEM_VOLATILE_STAFF: + forced.num_hits = 1; + forced.damage[0] = 58; + break; + + case ITEM_EYE_OF_AYAK: + forced.num_hits = 1; + forced.damage[0] = max_hit * 13 / 10; + forced.magic_def_drain = forced.damage[0]; + forced.attack_speed_override = 5; + break; + + default: + assert(!"osrs_spec_result_force_max called for a non-special weapon"); + break; + } + + for (int i = 0; i < forced.num_hits && i < 4; i++) + forced.total_damage += forced.damage[i]; + *r = forced; +} + +#endif /* OSRS_SPECIAL_ATTACKS_H */ diff --git a/ocean/osrs/osrs_spotanims.h b/ocean/osrs/osrs_spotanims.h new file mode 100644 index 0000000000..5367cff315 --- /dev/null +++ b/ocean/osrs/osrs_spotanims.h @@ -0,0 +1,89 @@ +#ifndef OSRS_SPOTANIMS_H +#define OSRS_SPOTANIMS_H + +#include "osrs_assets.h" +#include "osrs_binary_io.h" +#include +#include + +#define OSRS_SPOTANIM_MAGIC 0x544F5053u +#define OSRS_SPOTANIM_VERSION 1u +#define OSRS_SPOTANIM_MODEL_BASE 0xA2000000u +#define OSRS_SPOTANIM_RECOLOR_MODEL_BASE 0x000D0000u + +typedef struct { + uint32_t id; + int32_t model_id; + int32_t animation_id; + uint32_t resize_xy; + uint32_t resize_z; + uint32_t rotation; + int32_t brightness; + int32_t shadow; +} OsrsSpotAnimDef; + +typedef struct { + OsrsSpotAnimDef* defs; + int count; + int loaded; +} OsrsSpotAnimSet; + +static void osrs_spotanims_free(OsrsSpotAnimSet* set); + +static OsrsSpotAnimSet* osrs_spotanims_load(const char* path) { + FILE* f = osrs_asset_fopen(path, "rb"); + if (!f) { + fprintf(stderr, "osrs_spotanims_load: cannot open %s\n", path); + return NULL; + } + + uint32_t magic = 0; + uint32_t version = 0; + uint32_t count = 0; + osrs_read_exact(f, &magic, sizeof(magic), 1, path, "spotanim magic"); + osrs_read_exact(f, &version, sizeof(version), 1, path, "spotanim version"); + osrs_read_exact(f, &count, sizeof(count), 1, path, "spotanim count"); + if (magic != OSRS_SPOTANIM_MAGIC || version != OSRS_SPOTANIM_VERSION) { + fprintf(stderr, "%s: bad spotanim header magic=0x%08X version=%u\n", + path, magic, version); + abort(); + } + if (count > (uint32_t)INT_MAX) { + fprintf(stderr, "%s: spotanim count too large: %u\n", path, count); + abort(); + } + + OsrsSpotAnimSet* set = (OsrsSpotAnimSet*)osrs_calloc_or_abort( + 1, sizeof(*set), "spotanims"); + set->defs = (OsrsSpotAnimDef*)osrs_calloc_or_abort( + count, sizeof(*set->defs), "spotanim rows"); + set->count = (int)count; + for (uint32_t i = 0; i < count; i++) { + osrs_read_exact(f, &set->defs[i], sizeof(set->defs[i]), 1, + path, "spotanim row"); + } + fclose(f); + set->loaded = 1; + fprintf(stderr, "osrs_spotanims_load: loaded %d from %s\n", + set->count, path); + return set; +} + +static const OsrsSpotAnimDef* osrs_spotanim_find( + const OsrsSpotAnimSet* set, + int id +) { + if (!set || !set->loaded || id < 0) return NULL; + for (int i = 0; i < set->count; i++) { + if ((int)set->defs[i].id == id) return &set->defs[i]; + } + return NULL; +} + +static void osrs_spotanims_free(OsrsSpotAnimSet* set) { + if (!set) return; + free(set->defs); + free(set); +} + +#endif diff --git a/ocean/osrs/osrs_terrain.h b/ocean/osrs/osrs_terrain.h new file mode 100644 index 0000000000..e15c8be551 --- /dev/null +++ b/ocean/osrs/osrs_terrain.h @@ -0,0 +1,176 @@ +#ifndef OSRS_TERRAIN_H +#define OSRS_TERRAIN_H + +#include "raylib.h" +#include "osrs_assets.h" +#include "osrs_binary_io.h" +#include +#include +#include +#include + +#define TERR_MAGIC 0x54455252 + +typedef struct { + Model model; + int vertex_count; + int region_count; + int min_world_x; + int min_world_y; + int loaded; + float* heightmap; + int hm_min_x; + int hm_min_y; + int hm_width; + int hm_height; +} TerrainMesh; + +static TerrainMesh* terrain_load(const char* path) { + FILE* f = osrs_asset_fopen(path, "rb"); + if (!f) { + fprintf(stderr, "terrain_load: could not open %s\n", path); + return NULL; + } + + uint32_t magic, vert_count, region_count; + int32_t min_wx, min_wy; + osrs_read_exact(f, &magic, 4, 1, path, "magic"); + if (magic != TERR_MAGIC) { + fprintf(stderr, "terrain_load: bad magic %08x\n", magic); + abort(); + } + osrs_read_exact(f, &vert_count, 4, 1, path, "vertex count"); + osrs_read_exact(f, ®ion_count, 4, 1, path, "region count"); + osrs_read_exact(f, &min_wx, 4, 1, path, "min world x"); + osrs_read_exact(f, &min_wy, 4, 1, path, "min world y"); + + fprintf(stderr, "terrain_load: %u verts, %u regions, origin (%d, %d)\n", + vert_count, region_count, min_wx, min_wy); + + float* raw_verts = (float*)osrs_malloc_or_abort( + vert_count * 3 * sizeof(float), "terrain vertices"); + osrs_read_exact(f, raw_verts, sizeof(float), vert_count * 3, path, "vertices"); + + unsigned char* raw_colors = (unsigned char*)osrs_malloc_or_abort( + vert_count * 4, "terrain colors"); + osrs_read_exact(f, raw_colors, 1, vert_count * 4, path, "colors"); + + Mesh mesh = { 0 }; + mesh.vertexCount = (int)vert_count; + mesh.triangleCount = (int)(vert_count / 3); + mesh.vertices = raw_verts; + mesh.colors = raw_colors; + + mesh.normals = (float*)osrs_calloc_or_abort( + vert_count * 3, sizeof(float), "terrain normals"); + for (int i = 0; i < mesh.triangleCount; i++) { + int base = i * 9; + float ax = raw_verts[base + 0], ay = raw_verts[base + 1], az = raw_verts[base + 2]; + float bx = raw_verts[base + 3], by = raw_verts[base + 4], bz = raw_verts[base + 5]; + float cx = raw_verts[base + 6], cy = raw_verts[base + 7], cz = raw_verts[base + 8]; + + float e1x = bx - ax, e1y = by - ay, e1z = bz - az; + float e2x = cx - ax, e2y = cy - ay, e2z = cz - az; + float nx = e1y * e2z - e1z * e2y; + float ny = e1z * e2x - e1x * e2z; + float nz = e1x * e2y - e1y * e2x; + float len = sqrtf(nx * nx + ny * ny + nz * nz); + if (len > 0.0001f) { nx /= len; ny /= len; nz /= len; } + + for (int v = 0; v < 3; v++) { + mesh.normals[i * 9 + v * 3 + 0] = nx; + mesh.normals[i * 9 + v * 3 + 1] = ny; + mesh.normals[i * 9 + v * 3 + 2] = nz; + } + } + + UploadMesh(&mesh, false); + + TerrainMesh* tm = (TerrainMesh*)osrs_calloc_or_abort( + 1, sizeof(TerrainMesh), "terrain mesh"); + tm->model = LoadModelFromMesh(mesh); + tm->vertex_count = (int)vert_count; + tm->region_count = (int)region_count; + tm->min_world_x = min_wx; + tm->min_world_y = min_wy; + tm->loaded = 1; + + int32_t hm_min_x, hm_min_y; + uint32_t hm_w, hm_h; + size_t has_heightmap = fread(&hm_min_x, 4, 1, f); + if (has_heightmap == 1) { + osrs_read_exact(f, &hm_min_y, 4, 1, path, "heightmap min y"); + osrs_read_exact(f, &hm_w, 4, 1, path, "heightmap width"); + osrs_read_exact(f, &hm_h, 4, 1, path, "heightmap height"); + if (hm_w == 0 || hm_h == 0 || hm_w > 4096 || hm_h > 4096) { + fprintf(stderr, "terrain_load: invalid heightmap dimensions %ux%u\n", + hm_w, hm_h); + abort(); + } + tm->hm_min_x = hm_min_x; + tm->hm_min_y = hm_min_y; + tm->hm_width = (int)hm_w; + tm->hm_height = (int)hm_h; + tm->heightmap = (float*)osrs_malloc_or_abort( + hm_w * hm_h * sizeof(float), "terrain heightmap"); + osrs_read_exact(f, tm->heightmap, sizeof(float), + hm_w * hm_h, path, "heightmap values"); + fprintf(stderr, "terrain heightmap: %dx%d, origin (%d, %d)\n", + tm->hm_width, tm->hm_height, tm->hm_min_x, tm->hm_min_y); + } else if (!feof(f)) { + fprintf(stderr, "terrain_load: failed probing heightmap header\n"); + abort(); + } + + fclose(f); + return tm; +} + +static void terrain_offset(TerrainMesh* tm, int wx, int wy) { + if (!tm || !tm->loaded) return; + float dx = (float)wx; + float dz = (float)wy; + float* verts = tm->model.meshes[0].vertices; + for (int i = 0; i < tm->vertex_count; i++) { + verts[i * 3 + 0] -= dx; + verts[i * 3 + 2] += dz; + } + UpdateMeshBuffer(tm->model.meshes[0], 0, verts, + tm->vertex_count * 3 * sizeof(float), 0); + tm->min_world_x -= wx; + tm->min_world_y -= wy; + if (tm->heightmap) { + tm->hm_min_x -= wx; + tm->hm_min_y -= wy; + } + fprintf(stderr, "terrain_offset: shifted by (%d, %d), new origin (%d, %d)\n", + wx, wy, tm->min_world_x, tm->min_world_y); +} + +static float terrain_height_at(TerrainMesh* tm, int world_x, int world_y) { + if (!tm || !tm->heightmap) return -2.0f; + int lx = world_x - tm->hm_min_x; + int ly = world_y - tm->hm_min_y; + if (lx < 0 || lx >= tm->hm_width || ly < 0 || ly >= tm->hm_height) + return -2.0f; + return tm->heightmap[lx + ly * tm->hm_width]; +} + +static float terrain_height_avg(TerrainMesh* tm, int world_x, int world_y) { + float h00 = terrain_height_at(tm, world_x, world_y); + float h10 = terrain_height_at(tm, world_x + 1, world_y); + float h01 = terrain_height_at(tm, world_x, world_y + 1); + float h11 = terrain_height_at(tm, world_x + 1, world_y + 1); + return (h00 + h10 + h01 + h11) * 0.25f; +} + +static void terrain_free(TerrainMesh* tm) { + if (!tm) return; + if (tm->loaded) { + UnloadModel(tm->model); + } + free(tm->heightmap); + free(tm); +} + +#endif diff --git a/ocean/osrs/osrs_types.h b/ocean/osrs/osrs_types.h new file mode 100644 index 0000000000..3252d3f455 --- /dev/null +++ b/ocean/osrs/osrs_types.h @@ -0,0 +1,1039 @@ +#ifndef OSRS_TYPES_H +#define OSRS_TYPES_H + +#include +#include +#include +#include +#include +#include "osrs_interaction.h" + +#define NUM_AGENTS 2 +#define MAX_PENDING_HITS 8 +#define HISTORY_SIZE 5 + +#define TICK_DURATION_MS 600 +#define MAX_EPISODE_TICKS 300 + +#define WILD_MIN_X 2940 +#define WILD_MAX_X 3392 +#define WILD_MIN_Y 3525 +#define WILD_MAX_Y 3968 +#define FIGHT_AREA_BASE_X 3041 +#define FIGHT_AREA_BASE_Y 3530 +#define FIGHT_AREA_WIDTH 61 +#define FIGHT_AREA_HEIGHT 28 +#define FIGHT_NEARBY_RADIUS 5 + +#define ONLY_SWITCH_GEAR_WHEN_ATTACK_SOON 1 + +#define ICE_RUSH_LEVEL 58 +#define ICE_BURST_LEVEL 70 +#define ICE_BLITZ_LEVEL 82 +#define ICE_BARRAGE_LEVEL 94 + +#define BLOOD_RUSH_LEVEL 56 +#define BLOOD_BURST_LEVEL 68 +#define BLOOD_BLITZ_LEVEL 80 +#define BLOOD_BARRAGE_LEVEL 92 + +#define ICE_RUSH_MAX_HIT 18 +#define ICE_BURST_MAX_HIT 22 +#define ICE_BLITZ_MAX_HIT 26 +#define ICE_BARRAGE_MAX_HIT 30 + +#define BLOOD_RUSH_MAX_HIT 15 +#define BLOOD_BURST_MAX_HIT 21 +#define BLOOD_BLITZ_MAX_HIT 25 +#define BLOOD_BARRAGE_MAX_HIT 29 + +#define NUM_GEAR_SLOTS 11 + +#define NUM_ACTION_HEADS 9 + +#define HEAD_LOADOUT 0 +#define HEAD_COMBAT 1 +#define HEAD_OVERHEAD 2 +#define HEAD_FOOD 3 +#define HEAD_POTION 4 +#define HEAD_KARAMBWAN 5 +#define HEAD_VENG 6 +#define HEAD_OFFENSIVE 7 +#define HEAD_MOVE 8 + +#define LOADOUT_DIM 9 +#define COMBAT_DIM 13 +#define OVERHEAD_DIM 7 +#define FOOD_DIM 2 +#define POTION_DIM 5 +#define KARAMBWAN_DIM 2 +#define VENG_DIM 2 +#define OFFENSIVE_DIM 5 +#define MOVE_DIM 25 + +#define ACTION_MASK_SIZE (LOADOUT_DIM + COMBAT_DIM + OVERHEAD_DIM + \ + FOOD_DIM + POTION_DIM + KARAMBWAN_DIM + VENG_DIM + OFFENSIVE_DIM + MOVE_DIM) + +static const int ACTION_HEAD_DIMS[NUM_ACTION_HEADS] = { + LOADOUT_DIM, + COMBAT_DIM, + OVERHEAD_DIM, + FOOD_DIM, + POTION_DIM, + KARAMBWAN_DIM, + VENG_DIM, + OFFENSIVE_DIM, + MOVE_DIM, +}; + +#define NUM_ITEM_STATS 18 + +#define MAX_ITEMS_PER_SLOT 10 + +#define NUM_DYNAMIC_GEAR_SLOTS 8 + +#define SLOT_NUM_OBSERVATIONS 221 + +#define MAXED_BASE_ATTACK 99 +#define MAXED_BASE_STRENGTH 99 +#define MAXED_BASE_DEFENCE 99 +#define LMS_BASE_DEFENCE 75 +#define MAXED_BASE_RANGED 99 +#define MAXED_BASE_MAGIC 99 +#define MAXED_BASE_PRAYER 77 +#define MAXED_BASE_HITPOINTS 99 + +#define MAXED_FOOD_COUNT 11 +#define MAXED_KARAMBWAN_COUNT 2 +#define MAXED_BREW_DOSES 4 +#define MAXED_RESTORE_DOSES 8 +#define MAXED_COMBAT_POTION_DOSES 4 +#define MAXED_RANGED_POTION_DOSES 4 + +#define RUN_ENERGY_RECOVER_TICKS 3 +#define OSRS_RUN_ENERGY_UNITS_PER_PERCENT 100 +#define OSRS_RUN_ENERGY_FULL 10000 + +typedef enum { + ATTACK_STYLE_NONE = 0, + ATTACK_STYLE_MELEE, + ATTACK_STYLE_RANGED, + ATTACK_STYLE_MAGIC +} AttackStyle; + +typedef enum { + MELEE_STYLE_STAB = 0, + MELEE_STYLE_SLASH, + MELEE_STYLE_CRUSH, +} MeleeStyle; + +typedef enum { + PRAYER_NONE = 0, + PRAYER_PROTECT_MAGIC, + PRAYER_PROTECT_RANGED, + PRAYER_PROTECT_MELEE, + PRAYER_SMITE, + PRAYER_REDEMPTION +} OverheadPrayer; + +typedef enum { + GEAR_MAGE = 0, + GEAR_RANGED, + GEAR_MELEE, + GEAR_SPEC, + GEAR_TANK +} GearSet; + +typedef enum { + OFFENSIVE_PRAYER_NONE = 0, + OFFENSIVE_PRAYER_MELEE_LOW, + OFFENSIVE_PRAYER_RANGED_LOW, + OFFENSIVE_PRAYER_MAGIC_LOW, + OFFENSIVE_PRAYER_PIETY, + OFFENSIVE_PRAYER_RIGOUR, + OFFENSIVE_PRAYER_AUGURY +} OffensivePrayer; + +typedef enum { + FIGHT_STYLE_ACCURATE = 0, + FIGHT_STYLE_AGGRESSIVE, + FIGHT_STYLE_CONTROLLED, + FIGHT_STYLE_DEFENSIVE, + FIGHT_STYLE_RAPID, + FIGHT_STYLE_LONGRANGE, + FIGHT_STYLE_AUTOCAST, + FIGHT_STYLE_DEFENSIVE_AUTOCAST, +} FightStyle; + +typedef enum { + MELEE_BONUS_STAB = 0, + MELEE_BONUS_SLASH, + MELEE_BONUS_CRUSH +} MeleeBonusType; + +typedef enum { + MELEE_SPEC_NONE = 0, + MELEE_SPEC_AGS, + MELEE_SPEC_DRAGON_CLAWS, + MELEE_SPEC_GRANITE_MAUL, + MELEE_SPEC_DRAGON_DAGGER, + MELEE_SPEC_VOIDWAKER, + MELEE_SPEC_DWH, + MELEE_SPEC_BGS, + MELEE_SPEC_ZGS, + MELEE_SPEC_SGS, + MELEE_SPEC_ANCIENT_GS, + MELEE_SPEC_VESTAS, + MELEE_SPEC_ABYSSAL_DAGGER, + MELEE_SPEC_DRAGON_LONGSWORD, + MELEE_SPEC_DRAGON_MACE, + MELEE_SPEC_ABYSSAL_BLUDGEON +} MeleeSpecWeapon; + +typedef enum { + RANGED_SPEC_NONE = 0, + RANGED_SPEC_DARK_BOW, + RANGED_SPEC_BALLISTA, + RANGED_SPEC_ACB, + RANGED_SPEC_ZCB, + RANGED_SPEC_DRAGON_KNIFE, + RANGED_SPEC_MSB, + RANGED_SPEC_MORRIGANS +} RangedSpecWeapon; + +typedef enum { + MAGIC_SPEC_NONE = 0, + MAGIC_SPEC_VOLATILE_STAFF +} MagicSpecWeapon; + +typedef enum { + GEAR_SLOT_HEAD = 0, + GEAR_SLOT_CAPE, + GEAR_SLOT_NECK, + GEAR_SLOT_AMMO, + GEAR_SLOT_WEAPON, + GEAR_SLOT_SHIELD, + GEAR_SLOT_BODY, + GEAR_SLOT_LEGS, + GEAR_SLOT_HANDS, + GEAR_SLOT_FEET, + GEAR_SLOT_RING, +} GearSlotIndex; + +static const int DYNAMIC_GEAR_SLOTS[NUM_DYNAMIC_GEAR_SLOTS] = { + GEAR_SLOT_WEAPON, GEAR_SLOT_SHIELD, GEAR_SLOT_BODY, GEAR_SLOT_LEGS, + GEAR_SLOT_HEAD, GEAR_SLOT_CAPE, GEAR_SLOT_NECK, GEAR_SLOT_RING +}; + +typedef enum { + LOADOUT_KEEP = 0, + LOADOUT_MELEE, + LOADOUT_RANGE, + LOADOUT_MAGE, + LOADOUT_TANK, + LOADOUT_SPEC_MELEE, + LOADOUT_SPEC_RANGE, + LOADOUT_SPEC_MAGIC, + LOADOUT_GMAUL, +} LoadoutAction; + +#define ATTACK_NONE 0 +#define ATTACK_ATK 1 +#define ATTACK_ICE 2 +#define ATTACK_BLOOD 3 +#define MOVE_ADJACENT 4 +#define MOVE_UNDER 5 +#define MOVE_DIAGONAL 6 +#define MOVE_FARCAST_2 7 +#define MOVE_FARCAST_3 8 +#define MOVE_FARCAST_4 9 +#define MOVE_FARCAST_5 10 +#define MOVE_FARCAST_6 11 +#define MOVE_FARCAST_7 12 +#define MOVE_NONE ATTACK_NONE + +static inline int is_attack_action(int v) { return v >= ATTACK_ATK && v <= ATTACK_BLOOD; } +static inline int is_move_action(int v) { return v >= MOVE_ADJACENT && v <= MOVE_FARCAST_7; } + +typedef enum { + OVERHEAD_NONE = 0, + OVERHEAD_MAGE, + OVERHEAD_RANGED, + OVERHEAD_MELEE, + OVERHEAD_SMITE, + OVERHEAD_REDEMPTION, +} OverheadAction; + +typedef enum { + FOOD_NONE = 0, + FOOD_EAT, +} FoodAction; + +typedef enum { + POTION_NONE = 0, + POTION_BREW, + POTION_RESTORE, + POTION_COMBAT, + POTION_RANGED, + POTION_ANTIVENOM, + POTION_BASTION, + POTION_STAMINA, + POTION_PRAYER_POT, + POTION_SURGE, +} PotionAction; + +typedef enum { + KARAM_NONE = 0, + KARAM_EAT, +} KaramAction; + +typedef enum { + VENG_NONE = 0, + VENG_CAST, +} VengAction; + +#define OSRS_INFERNO_IDLE_PHASE_COUNT 6 + +typedef struct { + int stab_attack; + int slash_attack; + int crush_attack; + int magic_attack; + int ranged_attack; + int stab_defence; + int slash_defence; + int crush_defence; + int magic_defence; + int ranged_defence; + int melee_strength; + int ranged_strength; + int magic_strength; + int attack_speed; + int attack_range; +} GearBonuses; + +typedef struct { + int magic_attack; + int magic_strength; + int ranged_attack; + int ranged_strength; + int melee_attack; + int melee_strength; + int magic_defence; + int ranged_defence; + int melee_defence; +} VisibleGearBonuses; + +typedef struct { + int damage; + int ticks_until_hit; + AttackStyle attack_type; + int is_special; + int hit_success; + int freeze_ticks; + int heal_percent; + int drain_type; + int drain_percent; + int flat_heal; + int is_morr_bleed; + OverheadPrayer defender_prayer_at_attack; +} PendingHit; + +typedef enum { + ENTITY_PLAYER = 0, + ENTITY_NPC = 1, +} EntityType; + +typedef enum { + OSRS_MAGIC_ATTACK_NONE = 0, + OSRS_MAGIC_ATTACK_ANCIENT_ICE, + OSRS_MAGIC_ATTACK_ANCIENT_BLOOD, + OSRS_MAGIC_ATTACK_STANDARD_SPELL, + OSRS_MAGIC_ATTACK_POWERED_STAFF, +} OsrsMagicAttackKind; + +typedef enum { + OSRS_TARGET_NONE = 0, + OSRS_TARGET_PLAYER, + OSRS_TARGET_NPC, +} OsrsTargetKind; + +typedef struct { + OsrsTargetKind kind; + int id; +} OsrsTargetRef; + +typedef enum { + OSRS_TARGET_CLASS_STANDARD = 0, + OSRS_TARGET_CLASS_DRAGON, +} OsrsTargetClass; + +typedef struct { + int magic_level; + int magic_attack_bonus; + OsrsTargetClass target_class; +} OsrsTargetEffectContext; + +typedef enum { + OSRS_RECOIL_SOURCE_NONE = 0, + OSRS_RECOIL_SOURCE_RING_OF_RECOIL, + OSRS_RECOIL_SOURCE_RING_OF_SUFFERING_RI, +} OsrsRecoilSource; + +typedef enum { + OSRS_SPEC_REGEN_MODE_NORMAL = 0, + OSRS_SPEC_REGEN_MODE_LIGHTBEARER, +} OsrsSpecRegenMode; + +typedef struct { + uint32_t effect_mask; + uint8_t weapon_item; + uint8_t ring_item; + uint8_t shield_item; + uint8_t virtus_piece_count; + uint8_t dharok_piece_count; + uint8_t crystal_armour_points; + OsrsRecoilSource recoil_source; + OsrsSpecRegenMode spec_regen_mode; +} OsrsEquipmentEffectProfile; + +typedef struct { + int special_regen_ticks; + int recoil_charges; + int echo_boot_charges; + uint8_t confliction_is_primed; + uint8_t confliction_weapon_item; + OsrsMagicAttackKind confliction_magic_kind; + OsrsTargetRef confliction_target; +} OsrsItemEffectState; + +typedef struct { + EntityType entity_type; + int npc_def_id; + int npc_visible; + int npc_size; + int npc_anim_id; + + int is_lms; + + int base_attack; + int base_strength; + int base_defence; + int base_ranged; + int base_magic; + int base_prayer; + int base_hitpoints; + + int current_attack; + int current_strength; + int current_defence; + int current_ranged; + int current_magic; + int current_prayer; + int current_hitpoints; + + int special_energy; + int spec_regen_active; + int spec_armed; + OsrsInteraction interaction; + OsrsItemEffectState item_effect_state; + + GearSet current_gear; + GearSet visible_gear; + + int food_count; + int karambwan_count; + int brew_doses; + int restore_doses; + int prayer_pot_doses; + int combat_potion_doses; + int ranged_potion_doses; + int bastion_doses; + int stamina_doses; + int antivenom_doses; + int saturated_heart_count; + int saturated_heart_active_ticks; + + int attack_timer; + int attack_timer_uncapped; + int has_attack_timer; + int food_timer; + int potion_timer; + int karambwan_timer; + + uint8_t consumable_used_this_tick; + int last_food_heal; + int last_food_waste; + int last_karambwan_heal; + int last_karambwan_waste; + int last_brew_heal; + int last_brew_waste; + int last_potion_type; + int last_potion_was_waste; + + int frozen_ticks; + int freeze_immunity_ticks; + + int veng_active; + int veng_cooldown; + + OverheadPrayer prayer; + + OverheadPrayer prayer_display; + OffensivePrayer offensive_prayer; + FightStyle fight_style; + int autocast_enabled; + int autocast_defensive; + int autocast_spell; + int prayer_drain_counter; + + uint8_t prayer_just_activated; + uint8_t offensive_prayer_just_activated; + + int x, y; + int dest_x, dest_y; + int is_moving; + int is_running; + int run_energy; + int run_recovery_ticks; + int last_obs_target_x; + int last_obs_target_y; + + int just_attacked; + AttackStyle last_attack_style; + int last_queued_hit_damage; + int attack_was_on_prayer; + int attack_click_canceled; + int attack_click_ready; + int last_attack_dx; + int last_attack_dy; + int last_attack_dist; + + PendingHit pending_hits[MAX_PENDING_HITS]; + int num_pending_hits; + int damage_applied_this_tick; + int did_attack_auto_move; + + int hit_landed_this_tick; + int hit_was_successful; + int hit_damage; + AttackStyle hit_style; + OverheadPrayer hit_defender_prayer; + int hit_was_on_prayer; + int hit_attacker_idx; + int freeze_applied_this_tick; + int elysian_proc_this_tick; + + int morr_dot_remaining; + int morr_dot_tick_counter; + + float last_target_health_percent; + float tick_damage_scale; + float damage_dealt_scale; + float damage_received_scale; + + int total_target_hit_count; + int target_hit_melee_count; + int target_hit_ranged_count; + int target_hit_magic_count; + int target_hit_off_prayer_count; + int target_hit_correct_count; + + int total_target_pray_count; + int target_pray_melee_count; + int target_pray_ranged_count; + int target_pray_magic_count; + int target_pray_correct_count; + + int player_hit_melee_count; + int player_hit_ranged_count; + int player_hit_magic_count; + + int player_pray_melee_count; + int player_pray_ranged_count; + int player_pray_magic_count; + + AttackStyle recent_target_attack_styles[HISTORY_SIZE]; + AttackStyle recent_player_attack_styles[HISTORY_SIZE]; + AttackStyle recent_target_prayer_styles[HISTORY_SIZE]; + AttackStyle recent_player_prayer_styles[HISTORY_SIZE]; + int recent_target_prayer_correct[HISTORY_SIZE]; + int recent_target_hit_correct[HISTORY_SIZE]; + int recent_target_attack_index; + int recent_player_attack_index; + int recent_target_prayer_index; + int recent_player_prayer_index; + int recent_target_prayer_correct_index; + int recent_target_hit_correct_index; + + int target_magic_accuracy; + int target_magic_strength; + int target_ranged_accuracy; + int target_ranged_strength; + int target_melee_accuracy; + int target_melee_strength; + int target_magic_gear_magic_defence; + int target_magic_gear_ranged_defence; + int target_magic_gear_melee_defence; + int target_ranged_gear_magic_defence; + int target_ranged_gear_ranged_defence; + int target_ranged_gear_melee_defence; + int target_melee_gear_magic_defence; + int target_melee_gear_ranged_defence; + int target_melee_gear_melee_defence; + + int player_prayed_correct; + int target_prayed_correct; + + float total_damage_dealt; + float total_damage_received; + + int is_lunar_spellbook; + int observed_target_lunar_spellbook; + int has_blood_fury; + + MeleeSpecWeapon melee_spec_weapon; + RangedSpecWeapon ranged_spec_weapon; + MagicSpecWeapon magic_spec_weapon; + + float bolt_proc_damage; + int bolt_ignores_defense; + + uint8_t equipped[NUM_GEAR_SLOTS]; + + uint8_t inventory[NUM_GEAR_SLOTS][MAX_ITEMS_PER_SLOT]; + + uint8_t num_items_in_slot[NUM_GEAR_SLOTS]; + + GearBonuses slot_cached_bonuses; + OsrsEquipmentEffectProfile equipment_effect_profile; + int slot_gear_dirty; + + AttackStyle attack_style_this_tick; + int magic_type_this_tick; + int used_special_this_tick; + int ate_food_this_tick; + int ate_karambwan_this_tick; + int ate_brew_this_tick; + int cast_veng_this_tick; + int clicks_this_tick; + + float prev_hp_percent; + + int gui_max_hit; + int gui_attack_speed; + int gui_attack_range; + int gui_strength_bonus; +} Player; + +typedef struct Log { + float episode_return; + float episode_length; + float wins; + float damage_dealt; + float damage_received; + float wave; + float prayer_correct; + float prayer_total; + float idle_ticks; + float attack_ready_no_attack_ticks; + float target_available_no_attack_ticks; + float safe_attack_opportunity_missed_ticks; + float progressless_ticks; + float npc_pressure_if_ready_count; + float npc_pressure_this_tick_count; + float npc_pressure_max_incoming_hit; + float attack_ready_no_attack_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; + float target_available_no_attack_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; + float safe_attack_opportunity_missed_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; + float progressless_ticks_by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT]; + float brews_used; + float npc_kills; + float zulrah_tier_n[3]; + float zulrah_tier_wins[3]; + float zulrah_tier_score_sum[3]; + float zulrah_tier_damage_received[3]; + float zulrah_tier_episode_length[3]; + float zulrah_tier_cloud_occupancy_ticks[3]; + float zulrah_tier_cloud_damage_received[3]; + float cloud_occupancy_ticks; + float cloud_occupancy_frac; + float cloud_damage_received; + float active_cloud_count_ticks; + float pending_cloud_count_ticks; + float zulrah_kills; + float offensive_prayer_attacks; + float offensive_prayer_correct; + float offensive_prayer_attacks_by_style[4]; + float offensive_prayer_correct_by_style[4]; + float brews_remaining; + float restores_remaining; + float food_remaining; + float karambwan_remaining; + float spec_energy_remaining; + float attacks_landed; + float off_prayer_hits; + float behind_shield_pct; + float min_zuk_hp_seen; + float hp_restored; + float zuk_healer_damage; + float min_zuk_hp_normal; + float n_normal; + + float phase_reached_normal_sum; + + float count_min_hp_le_240_normal; + + float count_all_zuk_healers_dead_normal; + float count_healer_resolved_20_normal; + float damage_after_all_zuk_healers_dead_normal_sum; + float hp_restored_after_240_normal_sum; + float spark_damage_after_240_normal_sum; + + float count_died_with_zuk_healer_alive_normal; + float count_died_after_240_normal; + float start_wave; + float n; + float post_healer_set_damage_reward_coeff_normal_sum; + float post_healer_set_kill_bonus_coeff_normal_sum; + float post_healer_set_alive_penalty_coeff_normal_sum; + float post_healer_set_alive_penalty_cap_normal_sum; + float post_healer_set_damage_reward_normal_sum; + float post_healer_set_kill_bonus_reward_normal_sum; + float post_healer_set_alive_penalty_normal_sum; + float post_healer_set_pressure_normal_sum; + float action_mask_checks_normal_sum; + + float hist_score_bank[8]; + float hist_n_bank[8]; + + float colo_pray_faced_by_type[12]; + float colo_pray_correct_by_type[12]; + float colo_offpray_damage_by_type[12]; + float colo_total_damage_by_type[12]; + + float colo_death_by_type[12]; + float colo_death_fatal_damage; + + float colo_offpray_damage_conflict; + float colo_offpray_damage_solo; + float colo_death_on_conflict_tick; + + float colo_death_dmg_unprayable; + float colo_death_dmg_offpray; + float colo_death_dmg_prayed; + float colo_death_dmg_self; + float colo_death_heal_remaining; + float colo_farm_damage; + float colo_typeless_damage_by_type[12]; + float colo_outcome_score; + float colo_min_sol_hp; + + float colo_max_depth_reached; +} Log; + +typedef struct { + float damage_dealt_coef; + float damage_received_coef; + float correct_prayer_bonus; + float wrong_prayer_penalty; + float prayer_switch_no_attack_penalty; + float off_prayer_hit_bonus; + float melee_frozen_penalty; + float wasted_eat_penalty; + float premature_eat_penalty; + float magic_no_staff_penalty; + float gear_mismatch_penalty; + float spec_off_prayer_bonus; + float spec_low_defence_bonus; + float spec_low_hp_bonus; + float smart_triple_eat_bonus; + float wasted_triple_eat_penalty; + float damage_burst_bonus; + int damage_burst_threshold; + float premature_eat_threshold; + float ko_bonus; + float ko_supplies_bonus_coef; + float wasted_resources_penalty; + float shaping_scale; + int enabled; + int prayer_penalty_enabled; + int click_penalty_enabled; + int click_penalty_threshold; + float click_penalty_coef; +} RewardShapingConfig; + +typedef enum { + OPP_NONE = 0, + OPP_TRUE_RANDOM, + OPP_PANICKING, + OPP_WEAK_RANDOM, + OPP_SEMI_RANDOM, + OPP_STICKY_PRAYER, + OPP_RANDOM_EATER, + OPP_PRAYER_ROOKIE, + OPP_IMPROVED, + OPP_MIXED_EASY, + OPP_MIXED_MEDIUM, + OPP_ONETICK, + OPP_UNPREDICTABLE_IMPROVED, + OPP_UNPREDICTABLE_ONETICK, + OPP_MIXED_HARD, + OPP_MIXED_HARD_BALANCED, + OPP_PFSP, + OPP_NOVICE_NH, + OPP_APPRENTICE_NH, + OPP_COMPETENT_NH, + OPP_INTERMEDIATE_NH, + OPP_ADVANCED_NH, + OPP_PROFICIENT_NH, + OPP_EXPERT_NH, + OPP_MASTER_NH, + OPP_SAVANT_NH, + OPP_NIGHTMARE_NH, + OPP_VENG_FIGHTER, + OPP_BLOOD_HEALER, + OPP_GMAUL_COMBO, + OPP_RANGE_KITER, + OPP_SELFPLAY, +} OpponentType; + +#define MAX_OPPONENT_POOL 32 + +typedef struct { + OpponentType pool[MAX_OPPONENT_POOL]; + int cum_weights[MAX_OPPONENT_POOL]; + int pool_size; + int active_pool_idx; + float wins[MAX_OPPONENT_POOL]; + float episodes[MAX_OPPONENT_POOL]; +} PFSPState; + +typedef struct { + OpponentType type; + OpponentType active_sub_policy; + int chosen_prayer; + int chosen_style; + int current_prayer; + int current_prayer_set; + int food_cooldown; + int potion_cooldown; + int karambwan_cooldown; + + int fake_switch_pending; + int fake_switch_style; + int opponent_prayer_at_fake; + int fake_switch_failed; + int pending_prayer_value; + int pending_prayer_delay; + int last_target_gear_style; + + float eat_triple_threshold; + float eat_double_threshold; + float eat_brew_threshold; + + float prayer_accuracy; + float off_prayer_rate; + float offensive_prayer_rate; + float action_delay_chance; + float mistake_rate; + + float read_chance; + int has_read_this_tick; + AttackStyle read_agent_style; + OverheadPrayer read_agent_prayer; + int read_agent_moving; + + int prev_dist_to_target; + int target_fleeing_ticks; + + int combo_state; + float ko_threshold; + + float offensive_prayer_miss; + + float style_bias[3]; +} OpponentState; + +typedef struct { + int is_pvp_arena; + int use_c_opponent; + int use_c_opponent_p0; + int use_external_opponent_actions; + int external_opponent_actions[NUM_ACTION_HEADS]; + OpponentState opponent; + OpponentState opponent_p0; + PFSPState pfsp; + float gear_tier_weights[4]; + + int walk_dest_x[NUM_AGENTS]; + int walk_dest_y[NUM_AGENTS]; +} OsrsPvpRuntime; + +typedef struct { + float* agent_obs; + float* agent_obs_p1; + unsigned char* selfplay_mask; + int* agent_actions; + float* agent_rewards; + unsigned char* agent_terminals; +} OsrsOceanBuffers; + +#define OCEAN_OBS_SIZE (SLOT_NUM_OBSERVATIONS + ACTION_MASK_SIZE) + +typedef struct { + Log log; + + float* observations; + int* actions; + float* rewards; + unsigned char* terminals; + unsigned char* action_masks; + unsigned char action_masks_agents; + int num_agents; + + Player players[NUM_AGENTS]; + + int tick; + int episode_over; + int winner; + int auto_reset; + int pid_holder; + int pid_shuffle_countdown; + + int is_lms; + + uint32_t rng_state; + uint32_t rng_seed; + uint32_t rng_reset_count; + int has_rng_seed; + + int pending_actions[NUM_AGENTS * NUM_ACTION_HEADS]; + int last_executed_actions[NUM_AGENTS * NUM_ACTION_HEADS]; + + RewardShapingConfig shaping; + + OsrsPvpRuntime pvp_runtime; + + const void* encounter_def; + void* encounter_state; + void* encounter_context; + + void* collision_map; + + void* client; + + OsrsOceanBuffers ocean_io; + float _episode_return; + + float _obs_buf[NUM_AGENTS * SLOT_NUM_OBSERVATIONS]; + int _acts_buf[NUM_AGENTS * NUM_ACTION_HEADS]; + float _rews_buf[NUM_AGENTS]; + unsigned char _terms_buf[NUM_AGENTS]; + unsigned char _masks_buf[NUM_AGENTS * ACTION_MASK_SIZE]; + +} OsrsEnv; + +static inline int abs_int(int val) { + return val < 0 ? -val : val; +} + +static inline int min_int(int a, int b) { + return a < b ? a : b; +} + +static inline int max_int(int a, int b) { + return a > b ? a : b; +} + +static inline int clamp(int val, int min, int max) { + if (val < min) return min; + if (val > max) return max; + return val; +} + +static inline int osrs_run_energy_percent(int run_energy) { + return clamp(run_energy / OSRS_RUN_ENERGY_UNITS_PER_PERCENT, 0, 100); +} + +static inline float clampf(float val, float min, float max) { + if (val < min) return min; + if (val > max) return max; + return val; +} + +static inline int chebyshev_distance(int x1, int y1, int x2, int y2) { + int dx = x1 - x2; + int dy = y1 - y2; + if (dx < 0) dx = -dx; + if (dy < 0) dy = -dy; + return (dx > dy) ? dx : dy; +} + +static inline int is_in_melee_range(Player* p, Player* t) { + int dx = abs_int(p->x - t->x); + int dy = abs_int(p->y - t->y); + return (dx == 1 && dy == 0) || (dx == 0 && dy == 1); +} + +/** state must be non-zero or the stream sticks at zero. */ +static inline uint32_t xorshift32(uint32_t* state) { + uint32_t x = *state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *state = x; + return x; +} + +static inline int rand_int(OsrsEnv* env, int max) { + if (max <= 0) return 0; + return xorshift32(&env->rng_state) % max; +} + +static inline float rand_float(OsrsEnv* env) { + return (float)xorshift32(&env->rng_state) / (float)UINT32_MAX; +} + +static inline int is_in_wilderness(int x, int y) { + return x >= WILD_MIN_X && x <= WILD_MAX_X && y >= WILD_MIN_Y && y <= WILD_MAX_Y; +} + +static inline int tile_hash(int x, int y) { + return (x << 15) | y; +} + +static inline int remaining_ticks(int ticks) { + return ticks > 0 ? ticks : 0; +} + +static inline int get_attack_timer_uncapped(Player* p) { + return p->has_attack_timer ? p->attack_timer_uncapped : -100; +} + +static inline int can_attack_now(Player* p) { + if (!p->has_attack_timer) return 1; + return p->attack_timer < 0; +} + +static inline int can_move(Player* p) { + return p->frozen_ticks <= 0; +} + +static inline float ratio_or_zero(int numerator, int denominator) { + if (denominator == 0) { + return 0.0f; + } + return (float)numerator / (float)denominator; +} + +static inline float confidence_scale(int count) { + if (count >= 10) { + return 1.0f; + } + return (float)count / 10.0f; +} + +#define RECOIL_MAX_CHARGES 40 + +#endif diff --git a/ocean/osrs/osrs_ui_intent.h b/ocean/osrs/osrs_ui_intent.h new file mode 100644 index 0000000000..6618615a32 --- /dev/null +++ b/ocean/osrs/osrs_ui_intent.h @@ -0,0 +1,117 @@ +#ifndef OSRS_UI_INTENT_H +#define OSRS_UI_INTENT_H + +#include + +typedef enum { + OSRS_UI_INTENT_NONE = 0, + OSRS_UI_INTENT_SELECT_ITEM, + OSRS_UI_INTENT_SELECT_SPELL, + OSRS_UI_INTENT_ITEM_ON_ITEM, + OSRS_UI_INTENT_ITEM_ON_WIDGET, + OSRS_UI_INTENT_SPELL_ON_TARGET, + OSRS_UI_INTENT_SPELL_ON_WIDGET, +} OsrsUiIntentKind; + +typedef struct { + OsrsUiIntentKind kind; + int source_inventory_slot; + int target_inventory_slot; + int item_db_idx; + int item_osrs_id; + int spell; + int spell_gui_idx; + int npc_slot; + uint32_t widget_component_id; +} OsrsUiIntent; + +static inline uint32_t osrs_ui_intent_widget_component_id(int group_id, int child_id) { + return ((uint32_t)group_id << 16) | ((uint32_t)child_id & 0xffffu); +} + +static inline int osrs_ui_intent_widget_group_id(uint32_t component_id) { + return (int)(component_id >> 16); +} + +static inline int osrs_ui_intent_widget_child_id(uint32_t component_id) { + return (int)(component_id & 0xffffu); +} + +static inline OsrsUiIntent osrs_ui_intent_select_item( + int inventory_slot, + int item_db_idx, + int item_osrs_id +) { + return (OsrsUiIntent){ + .kind = OSRS_UI_INTENT_SELECT_ITEM, + .source_inventory_slot = inventory_slot, + .item_db_idx = item_db_idx, + .item_osrs_id = item_osrs_id, + }; +} + +static inline OsrsUiIntent osrs_ui_intent_select_spell(int spell, int spell_gui_idx) { + return (OsrsUiIntent){ + .kind = OSRS_UI_INTENT_SELECT_SPELL, + .spell = spell, + .spell_gui_idx = spell_gui_idx, + }; +} + +static inline OsrsUiIntent osrs_ui_intent_item_on_item( + int source_inventory_slot, + int target_inventory_slot, + int item_db_idx, + int item_osrs_id +) { + return (OsrsUiIntent){ + .kind = OSRS_UI_INTENT_ITEM_ON_ITEM, + .source_inventory_slot = source_inventory_slot, + .target_inventory_slot = target_inventory_slot, + .item_db_idx = item_db_idx, + .item_osrs_id = item_osrs_id, + }; +} + +static inline OsrsUiIntent osrs_ui_intent_item_on_widget( + int source_inventory_slot, + int item_db_idx, + int item_osrs_id, + uint32_t widget_component_id +) { + return (OsrsUiIntent){ + .kind = OSRS_UI_INTENT_ITEM_ON_WIDGET, + .source_inventory_slot = source_inventory_slot, + .item_db_idx = item_db_idx, + .item_osrs_id = item_osrs_id, + .widget_component_id = widget_component_id, + }; +} + +static inline OsrsUiIntent osrs_ui_intent_spell_on_target( + int spell, + int spell_gui_idx, + int npc_slot +) { + return (OsrsUiIntent){ + .kind = OSRS_UI_INTENT_SPELL_ON_TARGET, + .spell = spell, + .spell_gui_idx = spell_gui_idx, + .npc_slot = npc_slot, + }; +} + +static inline OsrsUiIntent osrs_ui_intent_spell_on_widget( + int spell, + int spell_gui_idx, + uint32_t widget_component_id +) { + return (OsrsUiIntent){ + .kind = OSRS_UI_INTENT_SPELL_ON_WIDGET, + .spell = spell, + .spell_gui_idx = spell_gui_idx, + .widget_component_id = widget_component_id, + }; +} + +#endif diff --git a/ocean/osrs/osrs_ui_interfaces.h b/ocean/osrs/osrs_ui_interfaces.h new file mode 100644 index 0000000000..68f7a2eea7 --- /dev/null +++ b/ocean/osrs/osrs_ui_interfaces.h @@ -0,0 +1,474 @@ +#ifndef OSRS_UI_INTERFACES_H +#define OSRS_UI_INTERFACES_H + +#if __has_include("raylib.h") +#include "raylib.h" +#elif __has_include("raylib-5.5_macos/include/raylib.h") +#include "raylib-5.5_macos/include/raylib.h" +#else +#error "raylib.h not found" +#endif + +#include +#include +#include +#include + +#include "osrs_assets.h" + +#define OSRS_UI_BIN_MAGIC "RCUIBIN2" +#define OSRS_UI_BIN_VERSION_MIN 1u +#define OSRS_UI_BIN_VERSION_MAX 2u +#define OSRS_UI_INTERFACE_MAX_ACTIONS 10 + +#define OSRS_UI_COMPONENT_ID(group, file) (((uint32_t)(group) << 16) | (uint32_t)(file)) +#define OSRS_UI_GROUP_INVENTORY 149u +#define OSRS_UI_GROUP_WORNITEMS 387u +#define OSRS_UI_GROUP_PRAYERBOOK 541u +#define OSRS_UI_GROUP_MAGIC_SPELLBOOK 218u +#define OSRS_UI_GROUP_COMBAT_INTERFACE 593u + +typedef enum { + OSRS_UI_LISTENER_VALUE_INT = 0, + OSRS_UI_LISTENER_VALUE_STRING +} OsrsUiListenerValueKind; + +typedef struct { + uint32_t id; + int32_t parent_id; + uint32_t group_id; + uint32_t file_id; + unsigned char is_if3; + unsigned char type; + unsigned char hidden; + unsigned char sprite_tiling; + unsigned char filled; + unsigned char line_direction; + unsigned char text_shadowed; + unsigned char flipped_vertically; + unsigned char flipped_horizontally; + unsigned char no_click_through; + unsigned char opacity; + unsigned char border_type; + unsigned char line_width; + unsigned char line_height; + int32_t content_type; + int32_t x; + int32_t y; + int32_t width; + int32_t height; + int32_t width_mode; + int32_t height_mode; + int32_t x_position_mode; + int32_t y_position_mode; + int32_t scroll_width; + int32_t scroll_height; + int32_t sprite_id; + int32_t texture_id; + int32_t shadow_color; + int32_t model_id; + int32_t model_type; + int32_t font_id; + int32_t text_color; + uint32_t click_mask; + int32_t x_text_alignment; + int32_t y_text_alignment; + char* name; + char* text; + char* target_verb; + char* actions[OSRS_UI_INTERFACE_MAX_ACTIONS]; + int action_count; +} OsrsUiComponent; + +typedef struct { + uint32_t id; + char* name; + OsrsUiComponent* components; + int component_count; +} OsrsUiInterfaceGroup; + +typedef struct { + OsrsUiInterfaceGroup* groups; + int group_count; + int loaded; +} OsrsUiInterfaceStore; + +typedef struct { + const unsigned char* data; + size_t size; + size_t pos; + int failed; +} OsrsUiReader; + +static uint8_t osrs_ui_read_u8(OsrsUiReader* r) { + if (r->pos + 1 > r->size) { + r->failed = 1; + return 0; + } + return r->data[r->pos++]; +} + +static uint16_t osrs_ui_read_u16(OsrsUiReader* r) { + if (r->pos + 2 > r->size) { + r->failed = 1; + return 0; + } + uint16_t value = (uint16_t)r->data[r->pos] + | ((uint16_t)r->data[r->pos + 1] << 8); + r->pos += 2; + return value; +} + +static uint32_t osrs_ui_read_u32(OsrsUiReader* r) { + if (r->pos + 4 > r->size) { + r->failed = 1; + return 0; + } + uint32_t value = (uint32_t)r->data[r->pos] + | ((uint32_t)r->data[r->pos + 1] << 8) + | ((uint32_t)r->data[r->pos + 2] << 16) + | ((uint32_t)r->data[r->pos + 3] << 24); + r->pos += 4; + return value; +} + +static int32_t osrs_ui_read_i32(OsrsUiReader* r) { + return (int32_t)osrs_ui_read_u32(r); +} + +static char* osrs_ui_read_string(OsrsUiReader* r) { + uint16_t len = osrs_ui_read_u16(r); + if (r->failed || r->pos + len > r->size) { + r->failed = 1; + return NULL; + } + char* out = (char*)calloc((size_t)len + 1, 1); + if (!out) { + fprintf(stderr, "osrs ui string allocation failed\n"); + abort(); + } + memcpy(out, r->data + r->pos, len); + r->pos += len; + return out; +} + +static unsigned char* osrs_ui_read_file_bytes(const char* path, size_t* out_size) { + FILE* file = osrs_asset_fopen(path, "rb"); + if (!file) return NULL; + fseek(file, 0, SEEK_END); + long size = ftell(file); + fseek(file, 0, SEEK_SET); + if (size <= 0) { + fclose(file); + return NULL; + } + unsigned char* data = (unsigned char*)malloc((size_t)size); + if (!data) { + fclose(file); + fprintf(stderr, "osrs ui file allocation failed\n"); + abort(); + } + if (fread(data, 1, (size_t)size, file) != (size_t)size) { + free(data); + fclose(file); + return NULL; + } + fclose(file); + *out_size = (size_t)size; + return data; +} + +static void osrs_ui_free_component(OsrsUiComponent* component) { + free(component->name); + free(component->text); + free(component->target_verb); + for (int i = 0; i < component->action_count; i++) { + free(component->actions[i]); + } +} + +static void osrs_ui_interfaces_unload(OsrsUiInterfaceStore* store) { + if (!store) return; + for (int g = 0; g < store->group_count; g++) { + OsrsUiInterfaceGroup* group = &store->groups[g]; + free(group->name); + for (int c = 0; c < group->component_count; c++) { + osrs_ui_free_component(&group->components[c]); + } + free(group->components); + } + free(store->groups); + memset(store, 0, sizeof(*store)); +} + +static int osrs_ui_interfaces_load(OsrsUiInterfaceStore* store, const char* path) { + if (!store || !path) return 0; + osrs_ui_interfaces_unload(store); + + size_t size = 0; + unsigned char* bytes = osrs_ui_read_file_bytes(path, &size); + if (!bytes) return 0; + if (size < 16 || memcmp(bytes, OSRS_UI_BIN_MAGIC, 8) != 0) { + free(bytes); + return 0; + } + + OsrsUiReader r = {.data = bytes, .size = size, .pos = 8}; + uint32_t version = osrs_ui_read_u32(&r); + uint32_t group_count = osrs_ui_read_u32(&r); + if (version < OSRS_UI_BIN_VERSION_MIN || version > OSRS_UI_BIN_VERSION_MAX + || group_count > 4096) { + free(bytes); + return 0; + } + + store->groups = (OsrsUiInterfaceGroup*)calloc(group_count, sizeof(*store->groups)); + if (!store->groups) { + free(bytes); + fprintf(stderr, "osrs ui group allocation failed\n"); + abort(); + } + store->group_count = (int)group_count; + + for (uint32_t g = 0; g < group_count && !r.failed; g++) { + OsrsUiInterfaceGroup* group = &store->groups[g]; + group->id = osrs_ui_read_u32(&r); + group->name = osrs_ui_read_string(&r); + uint32_t component_count = osrs_ui_read_u32(&r); + if (component_count > 65535) { + r.failed = 1; + break; + } + group->component_count = (int)component_count; + group->components = (OsrsUiComponent*)calloc(component_count, sizeof(*group->components)); + if (!group->components) { + free(bytes); + fprintf(stderr, "osrs ui component allocation failed\n"); + abort(); + } + for (uint32_t c = 0; c < component_count && !r.failed; c++) { + OsrsUiComponent* component = &group->components[c]; + component->id = osrs_ui_read_u32(&r); + component->parent_id = osrs_ui_read_i32(&r); + component->group_id = osrs_ui_read_u32(&r); + component->file_id = osrs_ui_read_u32(&r); + component->is_if3 = osrs_ui_read_u8(&r); + component->type = osrs_ui_read_u8(&r); + component->hidden = osrs_ui_read_u8(&r); + component->sprite_tiling = osrs_ui_read_u8(&r); + component->filled = osrs_ui_read_u8(&r); + component->line_direction = osrs_ui_read_u8(&r); + component->text_shadowed = osrs_ui_read_u8(&r); + component->flipped_vertically = osrs_ui_read_u8(&r); + component->flipped_horizontally = osrs_ui_read_u8(&r); + component->no_click_through = osrs_ui_read_u8(&r); + component->opacity = osrs_ui_read_u8(&r); + component->border_type = osrs_ui_read_u8(&r); + component->line_width = osrs_ui_read_u8(&r); + component->line_height = osrs_ui_read_u8(&r); + component->content_type = osrs_ui_read_i32(&r); + component->x = osrs_ui_read_i32(&r); + component->y = osrs_ui_read_i32(&r); + component->width = osrs_ui_read_i32(&r); + component->height = osrs_ui_read_i32(&r); + component->width_mode = osrs_ui_read_i32(&r); + component->height_mode = osrs_ui_read_i32(&r); + component->x_position_mode = osrs_ui_read_i32(&r); + component->y_position_mode = osrs_ui_read_i32(&r); + component->scroll_width = osrs_ui_read_i32(&r); + component->scroll_height = osrs_ui_read_i32(&r); + component->sprite_id = osrs_ui_read_i32(&r); + component->texture_id = osrs_ui_read_i32(&r); + component->shadow_color = osrs_ui_read_i32(&r); + component->model_id = osrs_ui_read_i32(&r); + component->model_type = osrs_ui_read_i32(&r); + component->font_id = osrs_ui_read_i32(&r); + component->text_color = osrs_ui_read_i32(&r); + component->click_mask = osrs_ui_read_u32(&r); + component->x_text_alignment = osrs_ui_read_i32(&r); + component->y_text_alignment = osrs_ui_read_i32(&r); + component->name = osrs_ui_read_string(&r); + component->text = osrs_ui_read_string(&r); + component->target_verb = osrs_ui_read_string(&r); + int encoded_action_count = (int)osrs_ui_read_u8(&r); + component->action_count = encoded_action_count; + if (component->action_count > OSRS_UI_INTERFACE_MAX_ACTIONS) { + component->action_count = OSRS_UI_INTERFACE_MAX_ACTIONS; + } + for (int a = 0; a < encoded_action_count; a++) { + char* action = osrs_ui_read_string(&r); + if (a < OSRS_UI_INTERFACE_MAX_ACTIONS) { + component->actions[a] = action; + } else { + free(action); + } + } + if (version >= 2) { + int encoded_listener_count = (int)osrs_ui_read_u8(&r); + for (int l = 0; l < encoded_listener_count; l++) { + osrs_ui_read_u8(&r); + int encoded_value_count = (int)osrs_ui_read_u8(&r); + for (int v = 0; v < encoded_value_count; v++) { + int value_type = (int)osrs_ui_read_u8(&r); + if (value_type == OSRS_UI_LISTENER_VALUE_STRING) { + free(osrs_ui_read_string(&r)); + } else { + osrs_ui_read_i32(&r); + } + } + } + + int encoded_trigger_count = (int)osrs_ui_read_u8(&r); + for (int t = 0; t < encoded_trigger_count; t++) { + osrs_ui_read_u8(&r); + int encoded_value_count = (int)osrs_ui_read_u8(&r); + for (int v = 0; v < encoded_value_count; v++) { + osrs_ui_read_i32(&r); + } + } + } + } + } + + free(bytes); + if (r.failed) { + osrs_ui_interfaces_unload(store); + return 0; + } + store->loaded = 1; + return 1; +} + +static const OsrsUiInterfaceGroup* osrs_ui_interface_group( + const OsrsUiInterfaceStore* store, + const char* name +) { + if (!store || !store->loaded || !name) return NULL; + for (int i = 0; i < store->group_count; i++) { + if (store->groups[i].name && strcmp(store->groups[i].name, name) == 0) { + return &store->groups[i]; + } + } + return NULL; +} + +static Rectangle osrs_ui_rect_expand_to_scroll(Rectangle rect, const OsrsUiComponent* component) { + Rectangle out = rect; + if (component->scroll_width > 0 && (float)component->scroll_width > out.width) { + out.width = (float)component->scroll_width; + } + if (component->scroll_height > 0 && (float)component->scroll_height > out.height) { + out.height = (float)component->scroll_height; + } + return out; +} + +static Rectangle osrs_ui_layout_component( + const OsrsUiComponent* component, + Rectangle parent, + int root +) { + if (root) return parent; + + float w = (float)component->width; + float h = (float)component->height; + if (component->width_mode == 1) { + w = parent.width - (float)component->width; + } else if (component->width_mode == 2) { + w = parent.width * (float)component->width / 16384.0f; + } + if (component->height_mode == 1) { + h = parent.height - (float)component->height; + } else if (component->height_mode == 2) { + h = parent.height * (float)component->height / 16384.0f; + } + if (w < 0) w = 0; + if (h < 0) h = 0; + + float x = parent.x + (float)component->x; + float y = parent.y + (float)component->y; + if (component->x_position_mode == 1) { + x = parent.x + (parent.width - w) * 0.5f + (float)component->x; + } else if (component->x_position_mode == 2) { + x = parent.x + parent.width - w - (float)component->x; + } else if (component->x_position_mode == 3) { + x = parent.x + parent.width * (float)component->x / 16384.0f; + } else if (component->x_position_mode == 4) { + x = parent.x + (parent.width - w) * 0.5f + + parent.width * (float)component->x / 16384.0f; + } else if (component->x_position_mode == 5) { + x = parent.x + parent.width - w + - parent.width * (float)component->x / 16384.0f; + } + + if (component->y_position_mode == 1) { + y = parent.y + (parent.height - h) * 0.5f + (float)component->y; + } else if (component->y_position_mode == 2) { + y = parent.y + parent.height - h - (float)component->y; + } else if (component->y_position_mode == 3) { + y = parent.y + parent.height * (float)component->y / 16384.0f; + } else if (component->y_position_mode == 4) { + y = parent.y + (parent.height - h) * 0.5f + + parent.height * (float)component->y / 16384.0f; + } else if (component->y_position_mode == 5) { + y = parent.y + parent.height - h + - parent.height * (float)component->y / 16384.0f; + } + + return (Rectangle){x, y, w, h}; +} + +static int osrs_ui_component_uses_mount_rect(const OsrsUiComponent* component) { + return component->file_id == 0 && component->parent_id == -1; +} + +static int osrs_ui_find_component_rect_recursive( + const OsrsUiInterfaceGroup* group, + const OsrsUiComponent* component, + const char* component_name, + Rectangle rect, + Rectangle* out_rect +) { + if (component->name && strcmp(component->name, component_name) == 0) { + *out_rect = rect; + return 1; + } + + Rectangle child_parent = component->type == 0 + ? osrs_ui_rect_expand_to_scroll(rect, component) + : rect; + for (int i = 0; i < group->component_count; i++) { + const OsrsUiComponent* child = &group->components[i]; + if (child->parent_id != (int32_t)component->id) continue; + Rectangle child_rect = osrs_ui_layout_component(child, child_parent, 0); + if (osrs_ui_find_component_rect_recursive( + group, child, component_name, child_rect, out_rect)) { + return 1; + } + } + return 0; +} + +static int osrs_ui_interfaces_component_rect( + const OsrsUiInterfaceStore* store, + const char* group_name, + const char* component_name, + Rectangle mount, + Rectangle* out_rect +) { + const OsrsUiInterfaceGroup* group = osrs_ui_interface_group(store, group_name); + if (!group || !component_name || !out_rect) return 0; + for (int i = 0; i < group->component_count; i++) { + const OsrsUiComponent* component = &group->components[i]; + if (component->parent_id != -1) continue; + Rectangle rect = osrs_ui_layout_component( + component, mount, osrs_ui_component_uses_mount_rect(component)); + if (osrs_ui_find_component_rect_recursive( + group, component, component_name, rect, out_rect)) { + return 1; + } + } + return 0; +} + +#endif diff --git a/ocean/osrs/osrs_venator.h b/ocean/osrs/osrs_venator.h new file mode 100644 index 0000000000..94d634afd0 --- /dev/null +++ b/ocean/osrs/osrs_venator.h @@ -0,0 +1,576 @@ +#ifndef OSRS_VENATOR_H +#define OSRS_VENATOR_H + +#include +#include + +#ifndef OSRS_COMBAT_H +#error "Include osrs_combat.h instead of osrs_venator.h" +#endif + +#define OSRS_VENATOR_BOUNCE_RADIUS 2 +#define OSRS_VENATOR_MAX_CHAIN_HITS 3 + +typedef enum { + OSRS_VENATOR_SIZE_ODD, + OSRS_VENATOR_SIZE_2, + OSRS_VENATOR_SIZE_4, +} OsrsVenatorSizeClass; + +typedef enum { + OSRS_VENATOR_MONSTER_DEAD = 0, + OSRS_VENATOR_MONSTER_ALIVE = 1, +} OsrsVenatorMonsterLife; + +typedef enum { + OSRS_VENATOR_HIT_PRIMARY = 0, + OSRS_VENATOR_HIT_BOUNCE = 1, +} OsrsVenatorHitKind; + +typedef enum { + OSRS_VENATOR_CHAIN_LENGTH_ONE = 1, + OSRS_VENATOR_CHAIN_LENGTH_TWO = 2, + OSRS_VENATOR_CHAIN_LENGTH_THREE = 3, +} OsrsVenatorChainLength; + +typedef enum { + OSRS_VENATOR_CANDIDATE_MISSING = 0, + OSRS_VENATOR_CANDIDATE_FOUND = 1, +} OsrsVenatorCandidateSearchKind; + +typedef enum { + OSRS_VENATOR_PRIMARY_CANDIDATE_EXCLUDED = 0, + OSRS_VENATOR_PRIMARY_CANDIDATE_INCLUDED = 1, +} OsrsVenatorPrimaryCandidateMode; + +typedef enum { + OSRS_VENATOR_DAMAGE_NOT_ROLLED = 0, + OSRS_VENATOR_DAMAGE_ROLLED = 1, +} OsrsVenatorDamageRollState; + +typedef enum { + OSRS_VENATOR_ACCURACY_MISS = 0, + OSRS_VENATOR_ACCURACY_HIT = 1, +} OsrsVenatorAccuracyOutcome; + +typedef struct { + int x; + int y; +} OsrsVenatorTile; + +typedef struct { + int sw_x; + int sw_y; + int size; +} OsrsVenatorFootprint; + +typedef struct { + int slot; + OsrsVenatorFootprint footprint; + OsrsVenatorMonsterLife life; +} OsrsVenatorMonster; + +typedef struct { + int count; + OsrsVenatorTile tiles[4]; +} OsrsVenatorTileSet; + +typedef struct { + int count; + OsrsVenatorTile tiles[2]; +} OsrsVenatorRequiredTiles; + +typedef struct { + int slot; + OsrsVenatorFootprint footprint; + OsrsVenatorHitKind kind; +} OsrsVenatorChainHit; + +typedef struct { + OsrsVenatorChainLength length; + OsrsVenatorChainHit hits[OSRS_VENATOR_MAX_CHAIN_HITS]; +} OsrsVenatorChain; + +typedef struct { + OsrsVenatorCandidateSearchKind kind; + OsrsVenatorMonster monster; + int distance; +} OsrsVenatorCandidateSearch; + +typedef struct { + OsrsVenatorDamageRollState roll_state; + OsrsVenatorAccuracyOutcome accuracy; + int damage; + int max_hit; +} OsrsVenatorResolvedDamageHit; + +typedef struct { + OsrsVenatorChainLength length; + int total_damage; + OsrsVenatorResolvedDamageHit hits[OSRS_VENATOR_MAX_CHAIN_HITS]; +} OsrsVenatorDamageResult; + +static inline OsrsVenatorTile osrs_venator_tile(int x, int y) { + return (OsrsVenatorTile){ .x = x, .y = y }; +} + +static inline OsrsVenatorFootprint osrs_venator_footprint( + int sw_x, + int sw_y, + int size +) { + return (OsrsVenatorFootprint){ .sw_x = sw_x, .sw_y = sw_y, .size = size }; +} + +static inline OsrsVenatorMonster osrs_venator_monster( + int slot, + int sw_x, + int sw_y, + int size, + OsrsVenatorMonsterLife life +) { + return (OsrsVenatorMonster){ + .slot = slot, + .footprint = osrs_venator_footprint(sw_x, sw_y, size), + .life = life, + }; +} + +static inline OsrsVenatorSizeClass osrs_venator_size_class(int size) { + switch (size) { + case 1: + case 3: + case 5: + return OSRS_VENATOR_SIZE_ODD; + case 2: + return OSRS_VENATOR_SIZE_2; + case 4: + return OSRS_VENATOR_SIZE_4; + } + abort(); +} + +static inline OsrsVenatorTile osrs_venator_sw_tile(OsrsVenatorFootprint f) { + return osrs_venator_tile(f.sw_x, f.sw_y); +} + +static inline OsrsVenatorTile osrs_venator_centre_odd_tile(OsrsVenatorFootprint f) { + if (osrs_venator_size_class(f.size) != OSRS_VENATOR_SIZE_ODD) abort(); + int offset = (f.size - 1) / 2; + return osrs_venator_tile(f.sw_x + offset, f.sw_y + offset); +} + +static inline OsrsVenatorTile osrs_venator_centre_reference_tile( + OsrsVenatorFootprint f +) { + OsrsVenatorSizeClass size_class = osrs_venator_size_class(f.size); + if (size_class == OSRS_VENATOR_SIZE_ODD) { + return osrs_venator_centre_odd_tile(f); + } + int offset = f.size / 2; + return osrs_venator_tile(f.sw_x + offset, f.sw_y + offset); +} + +static inline OsrsVenatorTile osrs_venator_centre_sw_reference_tile( + OsrsVenatorFootprint f +) { + switch (f.size) { + case 1: + case 2: + return osrs_venator_sw_tile(f); + case 3: + return osrs_venator_centre_odd_tile(f); + case 4: + case 5: + return osrs_venator_tile(f.sw_x + 1, f.sw_y + 1); + } + abort(); +} + +static inline OsrsVenatorTile osrs_venator_selection_anchor_tile( + OsrsVenatorFootprint f +) { + if (osrs_venator_size_class(f.size) == OSRS_VENATOR_SIZE_ODD) { + return osrs_venator_centre_odd_tile(f); + } + return osrs_venator_sw_tile(f); +} + +static inline void osrs_venator_required_tiles_push_unique( + OsrsVenatorRequiredTiles* required, + OsrsVenatorTile tile +) { + for (int i = 0; i < required->count; i++) { + if (required->tiles[i].x == tile.x && required->tiles[i].y == tile.y) { + return; + } + } + if (required->count >= 2) abort(); + required->tiles[required->count++] = tile; +} + +static inline OsrsVenatorTileSet osrs_venator_sender_origin_tiles( + OsrsVenatorFootprint sender +) { + OsrsVenatorTileSet origins = {0}; + switch (osrs_venator_size_class(sender.size)) { + case OSRS_VENATOR_SIZE_ODD: + origins.tiles[origins.count++] = osrs_venator_centre_odd_tile(sender); + return origins; + case OSRS_VENATOR_SIZE_2: + for (int dx = 0; dx < 2; dx++) { + for (int dy = 0; dy < 2; dy++) { + origins.tiles[origins.count++] = + osrs_venator_tile(sender.sw_x + dx, sender.sw_y + dy); + } + } + return origins; + case OSRS_VENATOR_SIZE_4: + for (int dx = 1; dx <= 2; dx++) { + for (int dy = 1; dy <= 2; dy++) { + origins.tiles[origins.count++] = + osrs_venator_tile(sender.sw_x + dx, sender.sw_y + dy); + } + } + return origins; + } + abort(); +} + +static inline OsrsVenatorRequiredTiles osrs_venator_accept_required_tiles( + OsrsVenatorFootprint sender, + OsrsVenatorFootprint target +) { + OsrsVenatorRequiredTiles required = {0}; + OsrsVenatorSizeClass sender_class = osrs_venator_size_class(sender.size); + + switch (target.size) { + case 1: + case 2: + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_sw_tile(target)); + return required; + case 3: + if (sender_class == OSRS_VENATOR_SIZE_ODD) { + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_centre_reference_tile(target)); + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_sw_tile(target)); + } else if (sender_class == OSRS_VENATOR_SIZE_2) { + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_centre_reference_tile(target)); + } else { + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_sw_tile(target)); + } + return required; + case 4: + case 5: + if (sender_class == OSRS_VENATOR_SIZE_ODD) { + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_centre_reference_tile(target)); + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_sw_tile(target)); + } else if (sender_class == OSRS_VENATOR_SIZE_2) { + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_centre_reference_tile(target)); + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_centre_sw_reference_tile(target)); + } else { + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_sw_tile(target)); + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_centre_sw_reference_tile(target)); + } + return required; + } + abort(); +} + +static inline OsrsVenatorRequiredTiles osrs_venator_send_required_tiles( + OsrsVenatorFootprint sender, + OsrsVenatorFootprint target +) { + OsrsVenatorRequiredTiles required = {0}; + switch (osrs_venator_size_class(sender.size)) { + case OSRS_VENATOR_SIZE_ODD: + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_sw_tile(target)); + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_centre_reference_tile(target)); + return required; + case OSRS_VENATOR_SIZE_2: + if (target.size <= 3) { + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_centre_reference_tile(target)); + } else { + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_centre_reference_tile(target)); + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_centre_sw_reference_tile(target)); + } + return required; + case OSRS_VENATOR_SIZE_4: + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_sw_tile(target)); + osrs_venator_required_tiles_push_unique( + &required, osrs_venator_centre_sw_reference_tile(target)); + return required; + } + abort(); +} + +static inline int osrs_venator_origins_find_required_tiles( + OsrsVenatorTileSet origins, + OsrsVenatorRequiredTiles required +) { + for (int origin_idx = 0; origin_idx < origins.count; origin_idx++) { + int found_all = 1; + for (int tile_idx = 0; tile_idx < required.count; tile_idx++) { + int dist = chebyshev_distance( + origins.tiles[origin_idx].x, + origins.tiles[origin_idx].y, + required.tiles[tile_idx].x, + required.tiles[tile_idx].y); + if (dist > OSRS_VENATOR_BOUNCE_RADIUS) { + found_all = 0; + break; + } + } + if (found_all) return 1; + } + return 0; +} + +static inline int osrs_venator_accepts_bounce( + OsrsVenatorFootprint sender, + OsrsVenatorFootprint target +) { + return osrs_venator_origins_find_required_tiles( + osrs_venator_sender_origin_tiles(sender), + osrs_venator_accept_required_tiles(sender, target)); +} + +static inline int osrs_venator_sends_bounce( + OsrsVenatorFootprint sender, + OsrsVenatorFootprint target +) { + return osrs_venator_origins_find_required_tiles( + osrs_venator_sender_origin_tiles(sender), + osrs_venator_send_required_tiles(sender, target)); +} + +static inline int osrs_venator_can_bounce( + OsrsVenatorFootprint sender, + OsrsVenatorFootprint target +) { + return osrs_venator_accepts_bounce(sender, target) && + osrs_venator_sends_bounce(sender, target); +} + +static inline int osrs_venator_selection_distance( + OsrsVenatorFootprint sender, + OsrsVenatorFootprint candidate +) { + OsrsVenatorTile sender_anchor = osrs_venator_selection_anchor_tile(sender); + OsrsVenatorTile candidate_anchor = osrs_venator_selection_anchor_tile(candidate); + return chebyshev_distance( + sender_anchor.x, + sender_anchor.y, + candidate_anchor.x, + candidate_anchor.y); +} + +static inline int osrs_venator_is_better_candidate( + int candidate_distance, + int candidate_slot, + int best_distance, + int best_slot +) { + if (candidate_distance < best_distance) return 1; + return candidate_distance == best_distance && candidate_slot < best_slot; +} + +static inline void osrs_venator_consider_candidate( + OsrsVenatorCandidateSearch* search, + OsrsVenatorFootprint range_sender, + OsrsVenatorFootprint selection_sender, + OsrsVenatorMonster candidate, + int forbidden_slot +) { + if (candidate.life != OSRS_VENATOR_MONSTER_ALIVE) return; + if (candidate.slot == forbidden_slot) return; + + if (!osrs_venator_can_bounce(range_sender, candidate.footprint)) return; + + int distance = osrs_venator_selection_distance(selection_sender, candidate.footprint); + if (search->kind == OSRS_VENATOR_CANDIDATE_MISSING || + osrs_venator_is_better_candidate( + distance, + candidate.slot, + search->distance, + search->monster.slot)) { + search->kind = OSRS_VENATOR_CANDIDATE_FOUND; + search->monster = candidate; + search->distance = distance; + } +} + +static inline int osrs_venator_live_non_primary_count( + const OsrsVenatorMonster* candidates, + int candidate_count, + int primary_slot +) { + if (candidate_count < 0) abort(); + if (candidate_count > 0 && candidates == NULL) abort(); + + int count = 0; + for (int i = 0; i < candidate_count; i++) { + if (candidates[i].life == OSRS_VENATOR_MONSTER_ALIVE && + candidates[i].slot != primary_slot) { + count++; + } + } + return count; +} + +static inline OsrsVenatorCandidateSearch osrs_venator_find_next_candidate( + OsrsVenatorFootprint range_sender, + OsrsVenatorFootprint selection_sender, + OsrsVenatorMonster primary, + const OsrsVenatorMonster* candidates, + int candidate_count, + int forbidden_slot, + OsrsVenatorPrimaryCandidateMode primary_candidate_mode +) { + if (candidate_count < 0) abort(); + if (candidate_count > 0 && candidates == NULL) abort(); + + OsrsVenatorCandidateSearch search = { OSRS_VENATOR_CANDIDATE_MISSING }; + if (primary_candidate_mode == OSRS_VENATOR_PRIMARY_CANDIDATE_INCLUDED && + primary.life == OSRS_VENATOR_MONSTER_ALIVE) { + osrs_venator_consider_candidate( + &search, range_sender, selection_sender, primary, forbidden_slot); + } + + for (int i = 0; i < candidate_count; i++) { + if (candidates[i].slot == primary.slot) continue; + osrs_venator_consider_candidate( + &search, range_sender, selection_sender, candidates[i], forbidden_slot); + } + + return search; +} + +static inline OsrsVenatorChain osrs_venator_resolve_chain( + OsrsVenatorMonster primary, + const OsrsVenatorMonster* candidates, + int candidate_count +) { + if (candidate_count < 0) abort(); + if (candidate_count > 0 && candidates == NULL) abort(); + + OsrsVenatorChain chain = { + .length = OSRS_VENATOR_CHAIN_LENGTH_ONE, + .hits = { + { + .slot = primary.slot, + .footprint = primary.footprint, + .kind = OSRS_VENATOR_HIT_PRIMARY, + }, + }, + }; + + OsrsVenatorCandidateSearch hit2 = osrs_venator_find_next_candidate( + primary.footprint, + primary.footprint, + primary, + candidates, + candidate_count, + primary.slot, + OSRS_VENATOR_PRIMARY_CANDIDATE_EXCLUDED); + if (hit2.kind == OSRS_VENATOR_CANDIDATE_MISSING) { + return chain; + } + + chain.length = OSRS_VENATOR_CHAIN_LENGTH_TWO; + chain.hits[1] = (OsrsVenatorChainHit){ + .slot = hit2.monster.slot, + .footprint = hit2.monster.footprint, + .kind = OSRS_VENATOR_HIT_BOUNCE, + }; + + if (osrs_venator_live_non_primary_count( + candidates, candidate_count, primary.slot) < 2) { + return chain; + } + + OsrsVenatorCandidateSearch hit3 = osrs_venator_find_next_candidate( + primary.footprint, + hit2.monster.footprint, + primary, + candidates, + candidate_count, + hit2.monster.slot, + OSRS_VENATOR_PRIMARY_CANDIDATE_INCLUDED); + if (hit3.kind == OSRS_VENATOR_CANDIDATE_MISSING) { + return chain; + } + + chain.length = OSRS_VENATOR_CHAIN_LENGTH_THREE; + chain.hits[2] = (OsrsVenatorChainHit){ + .slot = hit3.monster.slot, + .footprint = hit3.monster.footprint, + .kind = OSRS_VENATOR_HIT_BOUNCE, + }; + return chain; +} + +static inline int osrs_venator_bounce_max_hit(int original_max_hit) { + if (original_max_hit < 0) abort(); + return original_max_hit * 2 / 3; +} + +static inline void osrs_venator_validate_chain_length(OsrsVenatorChainLength length) { + switch (length) { + case OSRS_VENATOR_CHAIN_LENGTH_ONE: + case OSRS_VENATOR_CHAIN_LENGTH_TWO: + case OSRS_VENATOR_CHAIN_LENGTH_THREE: + return; + } + abort(); +} + +static inline OsrsVenatorDamageResult osrs_venator_roll_chain_damage( + const OsrsVenatorChain* chain, + const int target_defence_rolls[OSRS_VENATOR_MAX_CHAIN_HITS], + int attack_roll, + int original_max_hit, + uint32_t* rng_state +) { + if (chain == NULL || target_defence_rolls == NULL || rng_state == NULL) abort(); + if (attack_roll < 0) abort(); + if (original_max_hit < 0) abort(); + osrs_venator_validate_chain_length(chain->length); + + OsrsVenatorDamageResult result = { .length = chain->length }; + int bounce_max_hit = osrs_venator_bounce_max_hit(original_max_hit); + for (int i = 0; i < (int)chain->length; i++) { + int max_hit = i == 0 ? original_max_hit : bounce_max_hit; + int hit = encounter_roll_hit_chance( + rng_state, attack_roll, target_defence_rolls[i]); + int damage = hit ? encounter_rand_int(rng_state, max_hit + 1) : 0; + result.hits[i] = (OsrsVenatorResolvedDamageHit){ + .roll_state = OSRS_VENATOR_DAMAGE_ROLLED, + .accuracy = hit + ? OSRS_VENATOR_ACCURACY_HIT + : OSRS_VENATOR_ACCURACY_MISS, + .damage = damage, + .max_hit = max_hit, + }; + result.total_damage += damage; + } + return result; +} + +#endif diff --git a/ocean/osrs/osrs_visual.c b/ocean/osrs/osrs_visual.c new file mode 100644 index 0000000000..86b7b97512 --- /dev/null +++ b/ocean/osrs/osrs_visual.c @@ -0,0 +1,2054 @@ +#include +#include +#include +#include +#include +#include +#include +#include "osrs_env.h" +#include "osrs_assets.h" +#include "osrs_encounter.h" +#include "osrs_binary_io.h" +#include "encounters/encounter_nh_pvp.h" +#include "encounters/encounter_zulrah.h" +#if defined(__clang__) || defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#endif +#include "encounters/encounter_inferno.h" +#include "encounters/encounter_colosseum.h" +#if defined(__clang__) || defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +#ifdef OSRS_VISUAL +#include "osrs_render.h" +#include "puffercpu.h" +#include "osrs_visual_net.h" + +static void visual_require_gui_item_sprite(int raw_osrs_id, void* ctx) { + gui_require_sprite_by_osrs_id((GuiState*)ctx, raw_osrs_id); +} +#endif + +#ifdef __EMSCRIPTEN__ +#include +#else +#include +#endif + +static int encounter_name_is_pvp(const char* encounter_name) { + return encounter_name && + (strcmp(encounter_name, "pvp") == 0 || + strcmp(encounter_name, "nh_pvp") == 0); +} + +static void print_player_state(Player* p, int idx) { + printf("Player %d: HP=%d/%d Prayer=%d Gear=%d Pos=(%d,%d) Frozen=%d\n", + idx, p->current_hitpoints, p->base_hitpoints, + p->current_prayer, p->current_gear, p->x, p->y, p->frozen_ticks); +} + +static void print_env_state(OsrsEnv* env) { + printf("\n=== Tick %d ===\n", env->tick); + print_player_state(&env->players[0], 0); + print_player_state(&env->players[1], 1); + printf("PID holder: %d\n", env->pid_holder); +} + +static void run_random_episode(OsrsEnv* env, int verbose) { + pvp_reset(env); + + while (!env->episode_over) { + for (int agent = 0; agent < NUM_AGENTS; agent++) { + int* actions = env->actions + agent * NUM_ACTION_HEADS; + for (int h = 0; h < NUM_ACTION_HEADS; h++) { + actions[h] = rand() % ACTION_HEAD_DIMS[h]; + } + } + + pvp_step(env); + + if (verbose && env->tick % 50 == 0) { + print_env_state(env); + } + } + + if (verbose) { + printf("\n=== Episode End ===\n"); + printf("Winner: Player %d\n", env->winner); + printf("Length: %d ticks\n", env->tick); + printf("P0 damage dealt: %.0f\n", env->players[0].total_damage_dealt); + printf("P1 damage dealt: %.0f\n", env->players[1].total_damage_dealt); + } +} + +static void benchmark(OsrsEnv* env, int num_steps) { + printf("Benchmarking %d steps...\n", num_steps); + + clock_t start = clock(); + int episodes = 0; + int total_steps = 0; + + while (total_steps < num_steps) { + pvp_reset(env); + episodes++; + + while (!env->episode_over && total_steps < num_steps) { + for (int agent = 0; agent < NUM_AGENTS; agent++) { + int* actions = env->actions + agent * NUM_ACTION_HEADS; + for (int h = 0; h < NUM_ACTION_HEADS; h++) { + actions[h] = rand() % ACTION_HEAD_DIMS[h]; + } + } + + pvp_step(env); + total_steps++; + } + } + + clock_t end = clock(); + double elapsed = (double)(end - start) / CLOCKS_PER_SEC; + + printf("Results:\n"); + printf(" Total steps: %d\n", total_steps); + printf(" Episodes: %d\n", episodes); + printf(" Time: %.3f seconds\n", elapsed); + printf(" Steps/sec: %.0f\n", total_steps / elapsed); + printf(" Avg episode length: %.1f ticks\n", (float)total_steps / episodes); +} + +static EncounterContext* visual_create_encounter_context(const EncounterDef* edef) { + if (!edef || edef->context_size == 0) + return NULL; + EncounterContext* context = (EncounterContext*)calloc(1, edef->context_size); + if (!context) abort(); + if (edef->init_context) + edef->init_context(context); + return context; +} + +static void visual_destroy_encounter_context( + const EncounterDef* edef, + EncounterContext** context +) { + if (!context || !*context) + return; + if (edef && edef->destroy_context) + edef->destroy_context(*context); + free(*context); + *context = NULL; +} + +static double osrs_profile_now_seconds(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0; +} + +#ifdef COLO_PROFILE_ENABLED +static void osrs_print_colosseum_profile_results(void) { + int count = colosseum_env_profile_count(); + if (count <= 0) return; + double values[COLO_PROF_COUNT]; + int order[COLO_PROF_COUNT]; + for (int i = 0; i < count; i++) { + values[i] = colosseum_env_profile_read_reset_ms(i); + order[i] = i; + } + for (int i = 0; i < count; i++) { + int best = i; + for (int j = i + 1; j < count; j++) { + if (values[order[j]] > values[order[best]]) best = j; + } + int tmp = order[i]; + order[i] = order[best]; + order[best] = tmp; + } + double total = values[COLO_PROF_C_STEP_TOTAL]; + printf("Colosseum profile buckets:\n"); + for (int r = 0; r < count; r++) { + int slot = order[r]; + double pct = total > 0.0 ? 100.0 * values[slot] / total : 0.0; + printf(" %-28s %.3f ms %.2f%%\n", + colosseum_env_profile_name(slot), values[slot], pct); + } +} +#endif + +#ifdef INF_PROFILE_ENABLED +static int osrs_inferno_profile_slot_is_counter(int slot) { + return slot == INF_PROF_FORECAST_CALLS || + slot == INF_PROF_FORECAST_VALID_ACTIONS || + slot == INF_PROF_FORECAST_DISTINCT_LANDINGS; +} + +static void osrs_print_inferno_profile_results(int total_steps) { + int count = inferno_env_profile_count(); + if (count <= 0) return; + double values[INF_PROF_COUNT]; + int order[INF_PROF_COUNT]; + int order_count = 0; + for (int i = 0; i < count; i++) { + values[i] = inferno_env_profile_read_reset_ms(i); + if (!osrs_inferno_profile_slot_is_counter(i)) + order[order_count++] = i; + } + for (int i = 0; i < order_count; i++) { + int best = i; + for (int j = i + 1; j < order_count; j++) { + if (values[order[j]] > values[order[best]]) best = j; + } + int tmp = order[i]; + order[i] = order[best]; + order[best] = tmp; + } + double total = values[INF_PROF_C_STEP_TOTAL]; + printf("Inferno profile buckets:\n"); + for (int r = 0; r < order_count; r++) { + int slot = order[r]; + double pct = total > 0.0 ? 100.0 * values[slot] / total : 0.0; + printf(" %-28s %.3f ms %.2f%%\n", + inferno_env_profile_name(slot), values[slot], pct); + } + + double calls = values[INF_PROF_FORECAST_CALLS]; + double valid_actions = values[INF_PROF_FORECAST_VALID_ACTIONS]; + double distinct_landings = values[INF_PROF_FORECAST_DISTINCT_LANDINGS]; + double steps = total_steps > 0 ? (double)total_steps : 1.0; + double call_divisor = calls > 0.0 ? calls : 1.0; + printf("Inferno forecast counters:\n"); + printf(" %-28s %.0f total %.3f per step\n", + inferno_env_profile_name(INF_PROF_FORECAST_CALLS), + calls, + calls / steps); + printf(" %-28s %.0f total %.3f per step %.3f per forecast\n", + inferno_env_profile_name(INF_PROF_FORECAST_VALID_ACTIONS), + valid_actions, + valid_actions / steps, + valid_actions / call_divisor); + printf(" %-28s %.0f total %.3f per step %.3f per forecast\n", + inferno_env_profile_name(INF_PROF_FORECAST_DISTINCT_LANDINGS), + distinct_landings, + distinct_landings / steps, + distinct_landings / call_divisor); +} +#endif + +typedef struct { + CollisionMap* cmap; + int offset_x; + int offset_y; +} VisualCollisionLoad; + +static VisualCollisionLoad visual_load_encounter_collision_map( + const EncounterDef* edef, OsrsEnv* env, const char* encounter_name +) { + CollisionMap* cmap = NULL; + int offset_x = 0, offset_y = 0; + if (strcmp(encounter_name, "zulrah") == 0) { + cmap = collision_map_load(OSRS_ASSET("zulrah.cmap")); + offset_x = 2256; offset_y = 3061; + } else if (strcmp(encounter_name, "inferno") == 0) { + cmap = collision_map_load(OSRS_ASSET("inferno.cmap")); + offset_x = 2246; offset_y = 5315; + } else if (strcmp(encounter_name, "colosseum") == 0) { + cmap = collision_map_load(OSRS_ASSET("colosseum.cmap")); + offset_x = 1808; offset_y = 3090; + } + VisualCollisionLoad result = { NULL, offset_x, offset_y }; + if (cmap) { + edef->put_ptr(env->encounter_state, env->encounter_context, "collision_map", cmap); + edef->put_int(env->encounter_state, env->encounter_context, "world_offset_x", offset_x); + edef->put_int(env->encounter_state, env->encounter_context, "world_offset_y", offset_y); + env->collision_map = cmap; + result.cmap = cmap; + } + return result; +} + +static const EncounterDef* visual_open_encounter(OsrsEnv* env, const char* encounter_name) { + const EncounterDef* edef = encounter_find(encounter_name); + if (!edef) { + fprintf(stderr, "unknown encounter: %s\n", encounter_name); + return NULL; + } + env->encounter_def = (void*)edef; + env->encounter_state = edef->create(); + env->encounter_context = visual_create_encounter_context(edef); + return edef; +} + +static void run_profile( + OsrsEnv* env, + const char* encounter_name, + int start_wave, + int profile_steps +) { + if (profile_steps > 0) { + printf("Profiling %s for %d steps...\n", + encounter_name ? encounter_name : "pvp", + profile_steps); + } else { + printf("Profiling %s for 10 seconds...\n", encounter_name ? encounter_name : "pvp"); + } + + if (encounter_name) { + const EncounterDef* edef = visual_open_encounter(env, encounter_name); + if (!edef) return; + + visual_load_encounter_collision_map(edef, env, encounter_name); + if (start_wave >= 0 && edef->put_int) { + edef->put_int( + env->encounter_state, + env->encounter_context, + "start_wave", + start_wave); + fprintf(stderr, "start_wave: %d\n", start_wave); + } + edef->reset(env->encounter_state, env->encounter_context, 0); + } else { + env->pvp_runtime.use_c_opponent = 1; + env->pvp_runtime.opponent.type = OPP_IMPROVED; + env->is_lms = 1; + pvp_reset(env); + } + + const EncounterDef* profile_edef = (const EncounterDef*)env->encounter_def; + float* encounter_obs = NULL; + if (profile_edef) { + encounter_obs = (float*)calloc( + (size_t)(profile_edef->obs_size + profile_edef->mask_size), + sizeof(float)); + if (!encounter_obs) abort(); + profile_edef->write_obs( + env->encounter_state, + (EncounterContext*)env->encounter_context, + encounter_obs); + profile_edef->write_mask( + env->encounter_state, + (EncounterContext*)env->encounter_context, + encounter_obs + profile_edef->obs_size); +#ifdef COLO_PROFILE_ENABLED + if (strcmp(profile_edef->name, "colosseum") == 0) { + int count = colosseum_env_profile_count(); + for (int i = 0; i < count; i++) + (void)colosseum_env_profile_read_reset_ms(i); + } +#endif +#ifdef INF_PROFILE_ENABLED + if (strcmp(profile_edef->name, "inferno") == 0) { + int count = inferno_env_profile_count(); + for (int i = 0; i < count; i++) + (void)inferno_env_profile_read_reset_ms(i); + } +#endif + } + + double start = osrs_profile_now_seconds(); + double elapsed = 0; + int total_steps = 0; + int enc_actions[64] = {0}; + + while ((profile_steps > 0 && total_steps < profile_steps) || + (profile_steps <= 0 && elapsed < 10.0)) { + if (env->encounter_def && env->encounter_state) { + const EncounterDef* edef = (const EncounterDef*)env->encounter_def; +#ifdef COLO_PROFILE_ENABLED + int col_profile_this_step = strcmp(edef->name, "colosseum") == 0; + int col_prof_enabled = col_profile_this_step ? COLO_PROFILE_ENABLED() : 0; + double col_prof_total_t0 = col_prof_enabled ? COLO_PROFILE_NOW_MS() : 0.0; + double col_prof_t0 = col_prof_total_t0; +#endif +#ifdef INF_PROFILE_ENABLED + int inf_profile_this_step = strcmp(edef->name, "inferno") == 0; + int inf_prof_enabled = inf_profile_this_step ? INF_PROFILE_ENABLED() : 0; + double inf_prof_total_t0 = inf_prof_enabled ? INF_PROFILE_NOW_MS() : 0.0; + double inf_prof_t0 = inf_prof_total_t0; +#endif + for (int h = 0; h < edef->num_action_heads; h++) { + enc_actions[h] = rand() % edef->action_head_dims[h]; + } + if (getenv("OSRS_PROFILE_PIN_INV")) { + for (int h = 2; h < 15 && h < edef->num_action_heads; h++) + enc_actions[h] = 0; + } +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_ACTIONS); +#endif +#ifdef INF_PROFILE_ENABLED + INF_PROFILE_MARK(INF_PROF_C_ACTIONS); +#endif + edef->step(env->encounter_state, env->encounter_context, enc_actions); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_ENCOUNTER_STEP); +#endif +#ifdef INF_PROFILE_ENABLED + INF_PROFILE_MARK(INF_PROF_C_ENCOUNTER_STEP); +#endif + edef->write_obs( + env->encounter_state, + (EncounterContext*)env->encounter_context, + encounter_obs); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_WRITE_OBS); +#endif +#ifdef INF_PROFILE_ENABLED + INF_PROFILE_MARK(INF_PROF_C_WRITE_OBS); +#endif + edef->write_mask( + env->encounter_state, + (EncounterContext*)env->encounter_context, + encounter_obs + edef->obs_size); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_WRITE_MASK); +#endif +#ifdef INF_PROFILE_ENABLED + INF_PROFILE_MARK(INF_PROF_C_WRITE_MASK); +#endif + (void)edef->get_reward( + env->encounter_state, + (EncounterContext*)env->encounter_context); + if (edef->is_terminal(env->encounter_state, env->encounter_context)) { +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_REWARD_TERMINAL); + COLO_PROFILE_MARK(COLO_PROF_C_TERMINAL_LOG); +#endif +#ifdef INF_PROFILE_ENABLED + INF_PROFILE_MARK(INF_PROF_C_REWARD_TERMINAL); + INF_PROFILE_MARK(INF_PROF_C_TERMINAL_LOG); +#endif + edef->reset( + env->encounter_state, env->encounter_context, (uint32_t)rand()); + edef->write_obs( + env->encounter_state, + (EncounterContext*)env->encounter_context, + encounter_obs); + edef->write_mask( + env->encounter_state, + (EncounterContext*)env->encounter_context, + encounter_obs + edef->obs_size); +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_RESET); +#endif +#ifdef INF_PROFILE_ENABLED + INF_PROFILE_MARK(INF_PROF_C_RESET); +#endif + } else { +#ifdef COLO_PROFILE_ENABLED + COLO_PROFILE_MARK(COLO_PROF_C_REWARD_TERMINAL); +#endif +#ifdef INF_PROFILE_ENABLED + INF_PROFILE_MARK(INF_PROF_C_REWARD_TERMINAL); +#endif + } +#ifdef COLO_PROFILE_ENABLED + if (col_prof_enabled) + COLO_PROFILE_ADD( + COLO_PROF_C_STEP_TOTAL, + COLO_PROFILE_NOW_MS() - col_prof_total_t0); +#endif +#ifdef INF_PROFILE_ENABLED + if (inf_prof_enabled) + INF_PROFILE_ADD( + INF_PROF_C_STEP_TOTAL, + INF_PROFILE_NOW_MS() - inf_prof_total_t0); +#endif + } else { + for (int agent = 0; agent < NUM_AGENTS; agent++) { + int* actions = env->actions + agent * NUM_ACTION_HEADS; + for (int h = 0; h < NUM_ACTION_HEADS; h++) { + actions[h] = rand() % ACTION_HEAD_DIMS[h]; + } + } + pvp_step(env); + if (env->episode_over) { + pvp_reset(env); + } + } + + total_steps++; + if (total_steps % 1000 == 0) { + elapsed = osrs_profile_now_seconds() - start; + } + } + elapsed = osrs_profile_now_seconds() - start; + + printf("Results:\n"); + printf(" Total steps: %d\n", total_steps); + printf(" Time: %.3f seconds\n", elapsed); + printf(" Steps/sec: %.0f\n", total_steps / elapsed); +#ifdef COLO_PROFILE_ENABLED + if (encounter_name && strcmp(encounter_name, "colosseum") == 0) + osrs_print_colosseum_profile_results(); +#endif +#ifdef INF_PROFILE_ENABLED + if (encounter_name && strcmp(encounter_name, "inferno") == 0) + osrs_print_inferno_profile_results(total_steps); +#endif + + if (env->encounter_def && env->encounter_state) { + ((const EncounterDef*)env->encounter_def)->destroy(env->encounter_state); + env->encounter_state = NULL; + visual_destroy_encounter_context( + (const EncounterDef*)env->encounter_def, + (EncounterContext**)&env->encounter_context); + } + free(encounter_obs); +} + +#ifdef OSRS_VISUAL +/* replay file: binary format for pre-recorded actions. + header: [int32 num_ticks] [uint32 rng_state], then num_ticks * num_heads int32 values. */ +typedef struct { + int* actions; + int num_ticks; + int num_heads; + int current_tick; + uint32_t rng_seed; + void* initial_snapshot; + size_t initial_snapshot_size; +} ReplayFile; + +static ReplayFile* replay_load(const char* path, int num_heads, size_t snapshot_size) { + FILE* f = fopen(path, "rb"); + if (!f) { + fprintf(stderr, "replay: can't open %s\n", path); + abort(); + } + int num_ticks = 0; + uint32_t rng_seed = 12345; + osrs_read_exact(f, &num_ticks, 4, 1, path, "replay tick count"); + osrs_read_exact(f, &rng_seed, 4, 1, path, "replay rng seed"); + if (num_ticks < 0 || num_heads <= 0) { + fprintf(stderr, "replay: invalid shape ticks=%d heads=%d\n", + num_ticks, num_heads); + abort(); + } + if ((size_t)num_ticks > SIZE_MAX / (size_t)num_heads) { + fprintf(stderr, "replay: action count overflow ticks=%d heads=%d\n", + num_ticks, num_heads); + abort(); + } + ReplayFile* rf = (ReplayFile*)osrs_calloc_or_abort( + 1, sizeof(ReplayFile), "replay file"); + rf->num_ticks = num_ticks; + rf->num_heads = num_heads; + rf->current_tick = 0; + rf->rng_seed = rng_seed; + size_t action_count = (size_t)num_ticks * (size_t)num_heads; + rf->actions = (int*)osrs_malloc_or_abort( + action_count * sizeof(int), "replay actions"); + osrs_read_exact(f, rf->actions, sizeof(int), action_count, path, "replay actions"); + long payload_end = ftell(f); + if (payload_end < 0) { + fprintf(stderr, "replay: ftell failed for %s\n", path); + abort(); + } + if (fseek(f, 0, SEEK_END) != 0) { + fprintf(stderr, "replay: seek failed for %s\n", path); + abort(); + } + long file_end = ftell(f); + if (file_end < 0 || fseek(f, payload_end, SEEK_SET) != 0) { + fprintf(stderr, "replay: seek failed for %s\n", path); + abort(); + } + long remaining = file_end - payload_end; + if (remaining > 0) { + if (snapshot_size == 0 || remaining != (long)snapshot_size) { + fprintf(stderr, + "replay: unexpected trailing bytes in %s: got %ld, expected %zu\n", + path, remaining, snapshot_size); + abort(); + } + rf->initial_snapshot = osrs_malloc_or_abort(snapshot_size, "replay snapshot"); + rf->initial_snapshot_size = snapshot_size; + osrs_read_exact(f, rf->initial_snapshot, 1, snapshot_size, path, "replay snapshot"); + } + fclose(f); + fprintf(stderr, "replay loaded: %d ticks, rng=%u from %s\n", num_ticks, rng_seed, path); + return rf; +} + +static int replay_get_actions(ReplayFile* rf, int* out) { + if (rf->current_tick >= rf->num_ticks) return 0; + int base = rf->current_tick * rf->num_heads; + for (int h = 0; h < rf->num_heads; h++) out[h] = rf->actions[base + h]; + rf->current_tick++; + return 1; +} + +static void __attribute__((unused)) replay_free(ReplayFile* rf) { + if (rf) { free(rf->actions); free(rf->initial_snapshot); free(rf); } +} + +#define VISUAL_POLICY_MAX_ACTION_HEADS 64 + +typedef enum { + VISUAL_POLICY_NONE = 0, + VISUAL_POLICY_SAMPLE = 1, + VISUAL_POLICY_ARGMAX = 2, +} VisualPolicyMode; + +typedef struct { + int input_size; + int decoder_value_heads; + int entity_encoder; + int hidden_size; + int num_layers; +} VisualPolicyModelShape; + +typedef struct { + int enabled; + VisualPolicyMode mode; + uint32_t rng_state; + Weights* weights; + VisualNet* net; + float* obs; + int obs_size; + int mask_size; + int action_dims[VISUAL_POLICY_MAX_ACTION_HEADS]; + int num_action_heads; +} VisualPolicy; + +static uint32_t visual_policy_parse_seed(const char* value) { + errno = 0; + char* end = NULL; + unsigned long parsed = strtoul(value, &end, 10); + if (errno || !end || *end != '\0' || parsed > UINT32_MAX) { + fprintf(stderr, "policy: invalid policy seed: %s\n", value); + abort(); + } + return (uint32_t)parsed; +} + +static VisualPolicyMode visual_policy_parse_mode(const char* value) { + if (!value || strcmp(value, "sample") == 0) return VISUAL_POLICY_SAMPLE; + if (strcmp(value, "argmax") == 0) return VISUAL_POLICY_ARGMAX; + fprintf(stderr, "policy: invalid policy mode: %s\n", value); + abort(); +} + +static int visual_policy_is_continuous( + const int* action_dims, + int num_action_heads +) { + for (int h = 0; h < num_action_heads; h++) { + if (action_dims[h] != 1) return 0; + } + return 1; +} + +#define VISUAL_POLICY_ENTITY_FEATS 37 +#define VISUAL_POLICY_ENTITY_BOTTLENECK 16 +#define VISUAL_POLICY_INV_FEATS 28 +#define VISUAL_POLICY_INV_BOTTLENECK 16 + +static int64_t visual_policy_expected_weight_count( + int input_size, + int hidden_size, + int num_layers, + const int* action_dims, + int num_action_heads, + int decoder_value_heads, + int entity_encoder +) { + int action_sum = 0; + for (int h = 0; h < num_action_heads; h++) { + action_sum += action_dims[h]; + } + + int64_t total = 0; + total += (int64_t)hidden_size * input_size; + if (entity_encoder >= 1) { + total += (int64_t)VISUAL_POLICY_ENTITY_BOTTLENECK * VISUAL_POLICY_ENTITY_FEATS; + total += (int64_t)hidden_size * VISUAL_POLICY_ENTITY_BOTTLENECK; + } + if (entity_encoder >= 2) { + total += (int64_t)VISUAL_POLICY_INV_BOTTLENECK * VISUAL_POLICY_INV_FEATS; + total += (int64_t)hidden_size * VISUAL_POLICY_INV_BOTTLENECK; + } + total += (int64_t)(action_sum + decoder_value_heads) * hidden_size; + if (visual_policy_is_continuous(action_dims, num_action_heads)) { + total += num_action_heads; + } + total += (int64_t)num_layers * 3 * hidden_size * hidden_size; + return total; +} + +static int64_t visual_policy_file_weight_count(const Weights* weights) { + return weights->size - 7; +} + +static VisualPolicyModelShape visual_policy_select_model_shape( + const VisualPolicy* policy, + const EncounterDef* edef, + int cli_hidden_size, + int cli_num_layers, + int cli_entity_encoder +) { + static const int HS_GRID[] = {128, 256, 512, 1024, 2048, 4096}; + int obs_input_size = policy->obs_size; + int full_input_size = policy->obs_size + policy->mask_size; + int64_t file_weights = visual_policy_file_weight_count(policy->weights); + + VisualPolicyModelShape match = {0}; + int matches = 0; + for (int hi = 0; hi < (int)(sizeof(HS_GRID) / sizeof(HS_GRID[0])); hi++) { + int hs = HS_GRID[hi]; + if (cli_hidden_size > 0 && hs != cli_hidden_size) continue; + for (int layers = 1; layers <= 8; layers++) { + if (cli_num_layers > 0 && layers != cli_num_layers) continue; + for (int enc = 0; enc <= 2; enc++) { + if (cli_entity_encoder > 0 && enc != cli_entity_encoder) continue; + for (int value_heads = 0; value_heads <= 1; value_heads++) { + for (int variant = 0; variant <= 1; variant++) { + int input_size = variant ? full_input_size : obs_input_size; + int64_t expected = visual_policy_expected_weight_count( + input_size, hs, layers, policy->action_dims, + policy->num_action_heads, value_heads, enc); + if (expected != file_weights) continue; + match = (VisualPolicyModelShape){ + input_size, value_heads, enc, hs, layers}; + matches++; + if (matches <= 8) { + fprintf(stderr, + "policy: %s arch candidate hs=%d layers=%d entity=%d input=%s value_heads=%d\n", + edef->name, hs, layers, enc, + variant ? "obs+mask" : "obs", value_heads); + } + } + } + } + } + } + + if (matches == 0) { + fprintf(stderr, + "policy: %s model shape mismatch: file=%lld floats matches no architecture" + " (obs=%d mask=%d, scanned hs 128..4096, layers 1..8, entity 0..2%s)\n", + edef->name, (long long)file_weights, policy->obs_size, policy->mask_size, + (cli_hidden_size > 0 || cli_num_layers > 0 || cli_entity_encoder > 0) + ? " within the given CLI constraints" : ""); + abort(); + } + if (matches > 1) { + fprintf(stderr, + "policy: %s model shape ambiguous: %d architectures match %lld floats" + " (candidates above) -- pin --hidden-size/--num-layers/--entity-encoder\n", + edef->name, matches, (long long)file_weights); + abort(); + } + return match; +} + +static int64_t visual_policy_layout_tensor(const char* name, int64_t off, int64_t count) { + int64_t start = off; + int64_t end = (off + count + 7) & ~(int64_t)7; + fprintf(stderr, "policy: tensor %-16s [%lld, %lld) (%lld floats)\n", + name, (long long)start, (long long)end, (long long)count); + return end; +} + +static void visual_policy_assert_offset( + const char* group, const Weights* weights, int64_t expect +) { + if ((int64_t)weights->idx != expect) { + fprintf(stderr, "policy: %s weight offset mismatch: cursor=%d expected=%lld\n", + group, weights->idx, (long long)expect); + abort(); + } +} + +static VisualNet* visual_policy_make_puffernet( + Weights* weights, + int input_dim, + int hidden_dim, + int num_layers, + int action_dims[], + int num_action_heads, + int decoder_value_heads, + int entity_encoder +) { + VisualNet* net = (VisualNet*)calloc(1, sizeof(VisualNet)); + if (!net) { + fprintf(stderr, "policy: failed to allocate puffer net\n"); + abort(); + } + net->num_agents = 1; + net->obs = (float*)calloc((size_t)input_dim, sizeof(float)); + if (!net->obs) { + fprintf(stderr, "policy: failed to allocate puffer net obs\n"); + abort(); + } + + int action_sum = 0; + int is_continuous = visual_policy_is_continuous(action_dims, num_action_heads); + for (int h = 0; h < num_action_heads; h++) { + action_sum += action_dims[h]; + } + if (is_continuous && decoder_value_heads == 0) { + fprintf(stderr, "policy: continuous policy-only decoder is unsupported\n"); + abort(); + } + + net->is_continuous = is_continuous; + net->num_actions = num_action_heads; + + /* Expected .bin layout in policy_weights_create order (src/algo.cu): encoder + tensor(s), decoder weight, optional logstd, then the MinGRU projections. */ + int64_t off = 0; + if (entity_encoder) { + off = visual_policy_layout_tensor("enc.global_w", off, (int64_t)hidden_dim * input_dim); + off = visual_policy_layout_tensor("enc.entity_l1_w", off, + (int64_t)COLO_ENT_INF_BOTTLENECK * COLO_ENT_INF_FEATS); + off = visual_policy_layout_tensor("enc.entity_l2_w", off, + (int64_t)hidden_dim * COLO_ENT_INF_BOTTLENECK); + if (entity_encoder >= 2) { + off = visual_policy_layout_tensor("enc.inv_l1_w", off, + (int64_t)COLO_ENT_INF_INV_BOTTLENECK * COLO_ENT_INF_INV_FEATS); + off = visual_policy_layout_tensor("enc.inv_l2_w", off, + (int64_t)hidden_dim * COLO_ENT_INF_INV_BOTTLENECK); + } + } else { + off = visual_policy_layout_tensor("enc.weight", off, (int64_t)hidden_dim * input_dim); + } + int64_t off_after_encoder = off; + off = visual_policy_layout_tensor("decoder.weight", off, + (int64_t)(action_sum + decoder_value_heads) * hidden_dim); + int64_t off_after_decoder = off; + if (is_continuous) { + off = visual_policy_layout_tensor("decoder.logstd", off, (int64_t)num_action_heads); + } + int64_t off_after_logstd = off; + for (int l = 0; l < num_layers; l++) { + char name[32]; + snprintf(name, sizeof(name), "mingru.proj[%d]", l); + off = visual_policy_layout_tensor(name, off, (int64_t)3 * hidden_dim * hidden_dim); + } + int64_t off_total = off; + + if (entity_encoder) { + net->entity_encoder = make_colosseum_entity_encoder( + weights, 1, input_dim, hidden_dim, entity_encoder); + } else { + net->encoder = make_linear(weights, 1, input_dim, hidden_dim); + } + visual_policy_assert_offset("encoder", weights, off_after_encoder); + + net->decoder = make_linear(weights, 1, hidden_dim, action_sum + decoder_value_heads); + visual_policy_assert_offset("decoder", weights, off_after_decoder); + + if (net->is_continuous) { + net->log_std = get_weights_aligned(weights, num_action_heads); + visual_policy_assert_offset("logstd", weights, off_after_logstd); + } + + net->mingru = make_mingru(weights, 1, hidden_dim, num_layers); + visual_policy_assert_offset("mingru", weights, off_total); + + if (!net->is_continuous) { + net->multidiscrete = make_multidiscrete(1, action_dims, num_action_heads); + } + return net; +} + +static uint32_t visual_policy_next_u32(VisualPolicy* policy) { + policy->rng_state = policy->rng_state * 1664525u + 1013904223u; + return policy->rng_state; +} + +static float visual_policy_next_uniform(VisualPolicy* policy) { + return (float)((visual_policy_next_u32(policy) >> 8) * (1.0 / 16777216.0)); +} + +static int g_cli_hidden_size = -1; +static int g_cli_num_layers = -1; +static int g_cli_entity_encoder = 0; +static const char* g_cli_screenshot_path = NULL; +static int g_cli_screenshot_frame = 0; +static int g_cli_gui_tab = -1; +static float g_cli_tps = 0.0f; +static float g_cli_camera_dist = -1.0f; +static float g_cli_camera_yaw = -1000.0f; +static float g_cli_camera_pitch = -1000.0f; +static int g_cli_visual_loadout_mode = -1; +static int g_cli_prayer_oracle = 0; +static void visual_policy_init( + VisualPolicy* policy, + const EncounterDef* edef, + const char* model_path, + VisualPolicyMode mode, + uint32_t seed, + int cli_hidden_size, + int cli_num_layers, + int cli_entity_encoder +) { + memset(policy, 0, sizeof(*policy)); + if (!model_path || !model_path[0]) return; + if (!edef) { + fprintf(stderr, "policy: missing encounter definition\n"); + abort(); + } + if (edef->num_action_heads > VISUAL_POLICY_MAX_ACTION_HEADS) { + fprintf(stderr, "policy: too many action heads: %d\n", edef->num_action_heads); + abort(); + } + int action_mask_size = 0; + for (int h = 0; h < edef->num_action_heads; h++) { + action_mask_size += edef->action_head_dims[h]; + } + if (action_mask_size != edef->mask_size) { + fprintf(stderr, "policy: %s mask mismatch heads=%d mask=%d\n", + edef->name, action_mask_size, edef->mask_size); + abort(); + } + policy->obs_size = edef->obs_size; + policy->mask_size = edef->mask_size; + policy->num_action_heads = edef->num_action_heads; + for (int h = 0; h < edef->num_action_heads; h++) { + policy->action_dims[h] = edef->action_head_dims[h]; + } + policy->weights = load_weights(model_path); + if (!policy->weights) { + fprintf(stderr, "policy: failed to load model: %s\n", model_path); + abort(); + } + VisualPolicyModelShape model_shape = visual_policy_select_model_shape( + policy, edef, cli_hidden_size, cli_num_layers, cli_entity_encoder); + fprintf(stderr, + "policy: %s arch resolved hs=%d layers=%d entity=%d input=%d value_heads=%d\n", + edef->name, model_shape.hidden_size, model_shape.num_layers, + model_shape.entity_encoder, model_shape.input_size, + model_shape.decoder_value_heads); + policy->net = visual_policy_make_puffernet( + policy->weights, + model_shape.input_size, + model_shape.hidden_size, + model_shape.num_layers, + policy->action_dims, + policy->num_action_heads, + model_shape.decoder_value_heads, + model_shape.entity_encoder); + int64_t file_weights = visual_policy_file_weight_count(policy->weights); + if (policy->weights->idx != file_weights) { + fprintf(stderr, + "policy: model shape mismatch consumed=%d floats file=%lld floats\n", + policy->weights->idx, (long long)file_weights); + abort(); + } + policy->obs = (float*)osrs_calloc_or_abort( + (size_t)(policy->obs_size + policy->mask_size), + sizeof(float), + "visual policy obs"); + policy->mode = mode; + policy->rng_state = seed; + policy->enabled = 1; + fprintf(stderr, "policy: loaded %s mode=%s seed=%u\n", + model_path, mode == VISUAL_POLICY_ARGMAX ? "argmax" : "sample", seed); +} + +static void __attribute__((unused)) visual_policy_destroy(VisualPolicy* policy) { + if (!policy) return; + if (policy->net) visual_net_free(policy->net); + free(policy->weights); + free(policy->obs); + memset(policy, 0, sizeof(*policy)); +} + +static void visual_policy_reset_recurrent(VisualPolicy* policy) { + if (!policy || !policy->net || !policy->net->mingru) return; + memset(policy->net->mingru->state, 0, + (size_t)policy->net->mingru->num_layers * + (size_t)policy->net->mingru->batch_size * + (size_t)policy->net->mingru->hidden_size * + sizeof(float)); +} + +static int visual_policy_argmax_masked(const float* logits, const float* mask, int dim) { + int best_action = -1; + float best_logit = -INFINITY; + for (int a = 0; a < dim; a++) { + if (mask[a] <= 0.5f) continue; + if (best_action < 0 || logits[a] > best_logit) { + best_action = a; + best_logit = logits[a]; + } + } + return best_action; +} + +static int visual_policy_sample_masked( + VisualPolicy* policy, + const float* logits, + const float* mask, + int dim +) { + int best_action = visual_policy_argmax_masked(logits, mask, dim); + if (best_action < 0) return -1; + float max_logit = logits[best_action]; + float sum = 0.0f; + for (int a = 0; a < dim; a++) { + if (mask[a] <= 0.5f) continue; + sum += expf(logits[a] - max_logit); + } + if (!(sum > 0.0f) || !isfinite(sum)) { + fprintf(stderr, "policy: invalid masked softmax sum %f\n", sum); + abort(); + } + float threshold = visual_policy_next_uniform(policy) * sum; + float acc = 0.0f; + for (int a = 0; a < dim; a++) { + if (mask[a] <= 0.5f) continue; + acc += expf(logits[a] - max_logit); + if (threshold <= acc) return a; + } + return best_action; +} + +static void visual_policy_actions( + VisualPolicy* policy, + const EncounterDef* edef, + EncounterState* state, + EncounterContext* context, + int* actions +) { + if (!policy || !policy->enabled) return; + edef->write_obs(state, context, policy->obs); + edef->write_mask(state, context, policy->obs + policy->obs_size); + float* encoded; + if (policy->net->entity_encoder) { + colosseum_entity_encoder(policy->net->entity_encoder, policy->obs); + encoded = policy->net->entity_encoder->output; + } else { + linear(policy->net->encoder, policy->obs); + encoded = policy->net->encoder->output; + } + mingru(policy->net->mingru, encoded); + linear(policy->net->decoder, policy->net->mingru->output); + + const float* logits = policy->net->decoder->output; + const float* mask = policy->obs + policy->obs_size; + int logit_offset = 0; + int mask_offset = 0; + for (int h = 0; h < policy->num_action_heads; h++) { + int dim = policy->action_dims[h]; + int action = policy->mode == VISUAL_POLICY_ARGMAX + ? visual_policy_argmax_masked(logits + logit_offset, mask + mask_offset, dim) + : visual_policy_sample_masked( + policy, logits + logit_offset, mask + mask_offset, dim); + if (action < 0) { + fprintf(stderr, "policy: action head %d has no valid mask entry\n", h); + abort(); + } + actions[h] = action; + logit_offset += dim; + mask_offset += dim; + } +} + +typedef struct { + VisualPolicy* policy; + const EncounterDef* edef; + EncounterState* state; + EncounterContext* context; + int actions[64]; +#ifndef __EMSCRIPTEN__ + pthread_t thread; +#endif + int in_flight; + int has_actions; +} AsyncPolicy; + +#ifndef __EMSCRIPTEN__ +static void* async_policy_worker(void* arg) { + AsyncPolicy* ap = (AsyncPolicy*)arg; + visual_policy_actions(ap->policy, ap->edef, ap->state, ap->context, ap->actions); + return NULL; +} +#endif + +static void async_policy_join(AsyncPolicy* ap) { +#ifndef __EMSCRIPTEN__ + if (!ap->in_flight) return; + if (pthread_join(ap->thread, NULL) != 0) { + fprintf(stderr, "async policy: pthread_join failed\n"); + abort(); + } + ap->in_flight = 0; + ap->has_actions = 1; +#else + (void)ap; +#endif +} + +static void async_policy_spawn( + AsyncPolicy* ap, + VisualPolicy* policy, + const EncounterDef* edef, + OsrsEnv* env +) { +#ifndef __EMSCRIPTEN__ + if (ap->in_flight) { + fprintf(stderr, "async policy: spawn while in flight\n"); + abort(); + } + ap->policy = policy; + ap->edef = edef; + ap->state = env->encounter_state; + ap->context = (EncounterContext*)env->encounter_context; + ap->has_actions = 0; + if (pthread_create(&ap->thread, NULL, async_policy_worker, ap) != 0) { + fprintf(stderr, "async policy: pthread_create failed\n"); + abort(); + } + ap->in_flight = 1; +#else + (void)ap; (void)policy; (void)edef; (void)env; +#endif +} + +typedef struct { + OsrsEnv* env; + const char* encounter_name; + ReplayFile* replay; + VisualPolicy policy; + AsyncPolicy async_policy; + int start_wave; + double episode_end_time; + int episode_ended; + int seen_lab_restore_generation; +} VisualState; + +static void visual_async_policy_guard(void* ctx) { + VisualState* vs = (VisualState*)ctx; + async_policy_join(&vs->async_policy); + vs->async_policy.has_actions = 0; +} + +static void visual_frame(void* arg) { + VisualState* vs = (VisualState*)arg; + OsrsEnv* env = vs->env; + RenderClient* rc = (RenderClient*)env->client; + if (rc->lab_restore_generation != vs->seen_lab_restore_generation) { + vs->seen_lab_restore_generation = rc->lab_restore_generation; + vs->episode_ended = 0; + async_policy_join(&vs->async_policy); + vs->async_policy.has_actions = 0; + visual_policy_reset_recurrent(&vs->policy); + } + + if (rc->step_back) { + rc->step_back = 0; + async_policy_join(&vs->async_policy); + vs->async_policy.has_actions = 0; + render_restore_snapshot(rc, env); + if (rc->history_cursor >= rc->history_count - 1) { + rc->history_cursor = -1; + } + pvp_render(env); + return; + } + + if (rc->history_cursor >= 0) { + pvp_render(env); + return; + } + + if (vs->episode_ended) { + pvp_render(env); + if (GetTime() - vs->episode_end_time >= 2.0) { + vs->episode_ended = 0; + async_policy_join(&vs->async_policy); + vs->async_policy.has_actions = 0; + if (env->encounter_def) { + ((const EncounterDef*)env->encounter_def)->reset( + env->encounter_state, + (EncounterContext*)env->encounter_context, + (uint32_t)rand()); + } else { + pvp_reset(env); + } + render_reset_episode_visual_state(rc, env); + visual_policy_reset_recurrent(&vs->policy); + render_save_snapshot(rc, env); + } + return; + } + + if (rc->is_paused && !rc->step_once) { + pvp_render(env); + return; + } + rc->step_once = 0; + + if (rc->ticks_per_second > 0.0f) { + double interval = 1.0 / (double)rc->ticks_per_second; + double now = GetTime(); + if (now - rc->last_tick_time < interval) { + pvp_render(env); + return; + } + rc->last_tick_time += interval; + if (now - rc->last_tick_time >= interval) + rc->last_tick_time = now; + } + + async_policy_join(&vs->async_policy); + + render_pre_tick(rc, env); + + if (env->encounter_def && env->encounter_state) { + const EncounterDef* edef = (const EncounterDef*)env->encounter_def; + int enc_actions[64] = {0}; + int used_human_step = 0; + + if (rc->human_input.enabled && edef->step_human_commands) { + edef->step_human_commands( + env->encounter_state, + (EncounterContext*)env->encounter_context, + &rc->human_input); + used_human_step = 1; + } else if (rc->human_input.enabled) { + if (edef->translate_human_input) + edef->translate_human_input(&rc->human_input, enc_actions, + env->encounter_state, + (EncounterContext*)env->encounter_context); + if (rc->human_input.pending_move_x >= 0 && edef->put_int) { + edef->put_int(env->encounter_state, + (EncounterContext*)env->encounter_context, + "player_dest_x", + rc->human_input.pending_move_x); + edef->put_int(env->encounter_state, + (EncounterContext*)env->encounter_context, + "player_dest_y", + rc->human_input.pending_move_y); + } else if (rc->human_input.pending_attack && edef->put_int) { + edef->put_int( + env->encounter_state, + (EncounterContext*)env->encounter_context, + "player_dest_x", + -1); + edef->put_int( + env->encounter_state, + (EncounterContext*)env->encounter_context, + "player_dest_y", + -1); + } + human_input_clear_pending(&rc->human_input); + } else if (vs->replay && replay_get_actions(vs->replay, enc_actions)) { + } else if (vs->policy.enabled) { + if (vs->async_policy.has_actions) { + memcpy(enc_actions, vs->async_policy.actions, + sizeof(enc_actions)); + } else { + visual_policy_actions( + &vs->policy, + edef, + env->encounter_state, + (EncounterContext*)env->encounter_context, + enc_actions); + } + } else if (strcmp(edef->name, "zulrah") == 0) { + zul_heuristic_actions((ZulrahState*)env->encounter_state, enc_actions); + } else { + for (int h = 0; h < edef->num_action_heads; h++) { + enc_actions[h] = rand() % edef->action_head_dims[h]; + } + } + vs->async_policy.has_actions = 0; + + if (!used_human_step) { + edef->step( + env->encounter_state, + (EncounterContext*)env->encounter_context, + enc_actions); + } + env->tick = edef->get_tick( + env->encounter_state, (EncounterContext*)env->encounter_context); + + if (rc->human_input.enabled && rc->human_input.pending_move_x >= 0) { + Player* ply = edef->get_entity( + env->encounter_state, (EncounterContext*)env->encounter_context, 0); + if (ply && ply->x == rc->human_input.pending_move_x && + ply->y == rc->human_input.pending_move_y) { + human_input_clear_move(&rc->human_input); + } + } + + } else { + if (rc->human_input.enabled) { + human_to_pvp_actions(&rc->human_input, + env->actions, &env->players[0], &env->players[1]); + int* opp = env->actions + NUM_ACTION_HEADS; + for (int h = 0; h < NUM_ACTION_HEADS; h++) { + opp[h] = rand() % ACTION_HEAD_DIMS[h]; + } + human_input_clear_pending(&rc->human_input); + } else { + for (int agent = 0; agent < NUM_AGENTS; agent++) { + int* actions = env->actions + agent * NUM_ACTION_HEADS; + for (int h = 0; h < NUM_ACTION_HEADS; h++) { + actions[h] = rand() % ACTION_HEAD_DIMS[h]; + } + } + } + pvp_step(env); + + if (rc->human_input.enabled && rc->human_input.pending_move_x >= 0) { + Player* p0 = &env->players[0]; + if (p0->x == rc->human_input.pending_move_x && + p0->y == rc->human_input.pending_move_y) { + human_input_clear_move(&rc->human_input); + } + } + } + + render_post_tick(rc, env); + render_save_snapshot(rc, env); + pvp_render(env); + + int is_over = env->encounter_def + ? ((const EncounterDef*)env->encounter_def)->is_terminal( + env->encounter_state, (EncounterContext*)env->encounter_context) + : env->episode_over; + if (is_over) { + vs->episode_ended = 1; + vs->episode_end_time = GetTime(); + } else if (env->encounter_def && vs->policy.enabled && + !rc->human_input.enabled && !vs->replay) { + async_policy_spawn( + &vs->async_policy, + &vs->policy, + (const EncounterDef*)env->encounter_def, + env); + } +} + +static void run_metrics( + OsrsEnv* env, + const char* encounter_name, + const char* model_path, + VisualPolicyMode policy_mode, + uint32_t policy_seed, + int num_episodes, + int loadout_mode, + int bis_oracle, + int start_wave +) { + if (!encounter_name || strcmp(encounter_name, "colosseum") != 0) { + fprintf(stderr, "metrics mode requires --encounter colosseum\n"); + return; + } + const EncounterDef* edef = visual_open_encounter(env, encounter_name); + if (!edef) return; + visual_load_encounter_collision_map(edef, env, encounter_name); + edef->put_int(env->encounter_state, env->encounter_context, "loadout_profile_mode", loadout_mode); + edef->put_float(env->encounter_state, env->encounter_context, "beginner_loadout_fraction", 0.5f); + edef->put_int(env->encounter_state, env->encounter_context, "start_wave", + start_wave >= 0 ? start_wave : 1); + if (bis_oracle) + edef->put_int(env->encounter_state, env->encounter_context, + "bis_gear_oracle_mode", 1); + edef->reset(env->encounter_state, env->encounter_context, policy_seed); + + VisualPolicy policy; + visual_policy_init(&policy, edef, model_path, policy_mode, policy_seed, + g_cli_hidden_size, g_cli_num_layers, g_cli_entity_encoder); + if (!policy.enabled) { + fprintf(stderr, "metrics mode: failed to load model (pass --model )\n"); + return; + } + + static const char* npc_names[COLO_NUM_NPC_TYPES] = { + "berserker", "archer", "seer", "serpent", "jaguar", "javelin", + "shockwave", "minotaur", "manticore", "sol", "totem", "bees" + }; + static uint64_t wpn_npc[256][COLO_NUM_NPC_TYPES]; + static uint64_t wpn_total[256]; + static uint64_t wpn_spec[256]; + static double wpn_eff_sum[256]; + static uint64_t wpn_eff_n[256]; + static double npc_eff_sum[COLO_NUM_NPC_TYPES]; + static uint64_t npc_eff_n[COLO_NUM_NPC_TYPES]; + static uint64_t wave_ticks[12], wave_visits[12], wave_reinforced[12]; + static uint64_t wave_attacks_post_reinforce[12]; + memset(wpn_npc, 0, sizeof(wpn_npc)); + memset(wpn_total, 0, sizeof(wpn_total)); + memset(wpn_spec, 0, sizeof(wpn_spec)); + memset(wpn_eff_sum, 0, sizeof(wpn_eff_sum)); + memset(wpn_eff_n, 0, sizeof(wpn_eff_n)); + memset(npc_eff_sum, 0, sizeof(npc_eff_sum)); + memset(npc_eff_n, 0, sizeof(npc_eff_n)); + memset(wave_ticks, 0, sizeof(wave_ticks)); + memset(wave_visits, 0, sizeof(wave_visits)); + memset(wave_reinforced, 0, sizeof(wave_reinforced)); + memset(wave_attacks_post_reinforce, 0, sizeof(wave_attacks_post_reinforce)); + uint64_t total_attacks = 0, argmax_set_attacks = 0, argmax_evals = 0; + long total_ticks = 0; + int episodes = 0; + int prev_wave = 0, prev_reinf_timer = 0, wave_seen = 0; + int enc_actions[64] = {0}; + + enum { METRICS_MAX_EPISODES = 512 }; + if (num_episodes > METRICS_MAX_EPISODES) { + fprintf(stderr, "metrics mode caps at %d episodes\n", METRICS_MAX_EPISODES); + return; + } + static float ep_scores[METRICS_MAX_EPISODES]; + static int ep_winners[METRICS_MAX_EPISODES]; + + while (episodes < num_episodes) { + visual_policy_actions(&policy, edef, env->encounter_state, + (EncounterContext*)env->encounter_context, enc_actions); + edef->step(env->encounter_state, + (EncounterContext*)env->encounter_context, enc_actions); + total_ticks++; + ColosseumState* cs = (ColosseumState*)env->encounter_state; + + int w_now = cs->wave >= 0 && cs->wave < 12 ? cs->wave : 11; + if (!wave_seen || w_now != prev_wave) { + wave_visits[w_now]++; + wave_seen = 1; + } + wave_ticks[w_now]++; + if (prev_reinf_timer > 0 && + cs->reinforcement_timer == COLO_REINFORCE_FIRED) + wave_reinforced[w_now]++; + prev_wave = w_now; + prev_reinf_timer = cs->reinforcement_timer; + + if (cs->tick_scratch.player_attacked) { + int slot = cs->player_attack_npc_idx; + uint8_t w = cs->player.equipped[GEAR_SLOT_WEAPON]; + if (slot >= 0 && slot < COLO_MAX_NPCS) { + int t = (int)cs->npcs[slot].type; + if (t >= 0 && t < COLO_NUM_NPC_TYPES) { + wpn_npc[w][t]++; + wpn_total[w]++; + total_attacks++; + if (cs->player.used_special_this_tick) wpn_spec[w]++; + + if (cs->reinforcement_timer == COLO_REINFORCE_FIRED) + wave_attacks_post_reinforce[w_now]++; + + int am = col_attacked_with_argmax_set(cs); + if (am >= 0) { + argmax_evals++; + argmax_set_attacks += (uint64_t)am; + } + const ColoNPC* npc = &cs->npcs[slot]; + if (col_npc_is_live_target(npc) && + !col_type_is_hazard_entity(npc->type)) { + const ColoBestGear (*best)[COLO_NUM_NPC_TYPES] = + col_get_best_gear_table(cs); + float best_dpt = 0.0f; + for (int st = 0; st < COLO_NUM_WEAPON_SETS; st++) + if (best[st][t].dpt > best_dpt) + best_dpt = best[st][t].dpt; + float cur_dpt = col_expected_dpt_for_equipment_vs_npc( + cs, cs->player.equipped, npc, 1); + if (best_dpt > 0.0f) { + double eff = (double)(cur_dpt / best_dpt); + wpn_eff_sum[w] += eff; + wpn_eff_n[w]++; + npc_eff_sum[t] += eff; + npc_eff_n[t]++; + } + } + } + } + } + if (edef->is_terminal(env->encounter_state, + (EncounterContext*)env->encounter_context)) { + ep_scores[episodes] = cs->log.outcome_score; + ep_winners[episodes] = cs->winner; + episodes++; + edef->reset(env->encounter_state, + (EncounterContext*)env->encounter_context, + policy_seed + (uint32_t)episodes); + visual_policy_reset_recurrent(&policy); + wave_seen = 0; + prev_reinf_timer = 0; + } + } + + printf("# colosseum weapon behavioral metrics\n"); + printf("# episodes=%d ticks=%ld total_attacks=%llu mode=%s bis_oracle=%d\n", + num_episodes, total_ticks, (unsigned long long)total_attacks, + policy_mode == VISUAL_POLICY_ARGMAX ? "argmax" : "sample", bis_oracle); + { + double score_sum = 0.0; + int wins = 0; + for (int e = 0; e < episodes; e++) { + score_sum += (double)ep_scores[e]; + if (ep_winners[e] == COLO_OUTCOME_PLAYER_WON) wins++; + } + printf("# outcome scores: mean %.4f, wins %d/%d, per-episode:", + episodes ? score_sum / (double)episodes : 0.0, wins, episodes); + for (int e = 0; e < episodes; e++) printf(" %.3f", ep_scores[e]); + printf("\n"); + } + printf("# argmax-style attacks: %llu/%llu (%.1f%%)\n", + (unsigned long long)argmax_set_attacks, + (unsigned long long)argmax_evals, + argmax_evals ? 100.0 * (double)argmax_set_attacks / (double)argmax_evals : 0.0); + printf("weapon,total_attacks,per_episode,pct,spec_pct,mean_dpt_eff\n"); + for (int w = 0; w < 256; w++) { + if (wpn_total[w] == 0) continue; + const Item* it = get_item((uint8_t)w); + const char* wn = (it && it->name[0]) ? it->name : "unknown"; + printf("%s,%llu,%.1f,%.1f%%,%.1f%%,%.2f\n", wn, + (unsigned long long)wpn_total[w], + (double)wpn_total[w] / (double)num_episodes, + total_attacks ? 100.0 * (double)wpn_total[w] / (double)total_attacks : 0.0, + 100.0 * (double)wpn_spec[w] / (double)wpn_total[w], + wpn_eff_n[w] ? wpn_eff_sum[w] / (double)wpn_eff_n[w] : 0.0); + } + printf("\nnpc,attacks,mean_dpt_eff\n"); + for (int t = 0; t < COLO_NUM_NPC_TYPES; t++) { + if (npc_eff_n[t] == 0) continue; + printf("%s,%llu,%.2f\n", npc_names[t], + (unsigned long long)npc_eff_n[t], + npc_eff_sum[t] / (double)npc_eff_n[t]); + } + printf("\nwave,visits,mean_ticks,reinforced_pct,attacks_post_reinforce\n"); + for (int wv = 0; wv < 12; wv++) { + if (wave_visits[wv] == 0) continue; + printf("%d,%llu,%.0f,%.0f%%,%llu\n", wv + 1, + (unsigned long long)wave_visits[wv], + (double)wave_ticks[wv] / (double)wave_visits[wv], + 100.0 * (double)wave_reinforced[wv] / (double)wave_visits[wv], + (unsigned long long)wave_attacks_post_reinforce[wv]); + } + printf("\nweapon\\npc"); + for (int t = 0; t < COLO_NUM_NPC_TYPES; t++) printf(",%s", npc_names[t]); + printf("\n"); + for (int w = 0; w < 256; w++) { + if (wpn_total[w] == 0) continue; + const Item* it = get_item((uint8_t)w); + const char* wn = (it && it->name[0]) ? it->name : "unknown"; + printf("%s", wn); + for (int t = 0; t < COLO_NUM_NPC_TYPES; t++) + printf(",%llu", (unsigned long long)wpn_npc[w][t]); + printf("\n"); + } + visual_policy_destroy(&policy); +} + +static void run_visual( + OsrsEnv* env, + const char* encounter_name, + const char* replay_path, + int start_wave, + int gear_tier, + const char* model_path, + VisualPolicyMode policy_mode, + uint32_t policy_seed +) { + env->client = NULL; + + if (encounter_name) { + const EncounterDef* edef = visual_open_encounter(env, encounter_name); + if (!edef) return; + if (encounter_name_is_pvp(encounter_name) && edef->put_int) { + edef->put_int(env->encounter_state, env->encounter_context, "use_c_opponent", 1); + edef->put_int(env->encounter_state, env->encounter_context, "opponent_type", OPP_IMPROVED); +#ifdef __EMSCRIPTEN__ + edef->put_int(env->encounter_state, env->encounter_context, "use_c_opponent_p0", 0); +#else + edef->put_int(env->encounter_state, env->encounter_context, "use_c_opponent_p0", 1); + edef->put_int(env->encounter_state, env->encounter_context, "opponent_p0_type", OPP_IMPROVED); +#endif + edef->put_int(env->encounter_state, env->encounter_context, "is_lms", 1); + edef->put_int(env->encounter_state, env->encounter_context, "gear_tier", gear_tier); + } + if (strcmp(encounter_name, "colosseum") == 0 && edef->put_int && + g_cli_visual_loadout_mode >= 0) { + edef->put_int(env->encounter_state, env->encounter_context, + "loadout_profile_mode", g_cli_visual_loadout_mode); + } + if (strcmp(encounter_name, "colosseum") == 0 && edef->put_int && + g_cli_prayer_oracle) { + edef->put_int(env->encounter_state, env->encounter_context, + "prayer_oracle_mode", 1); + } + + VisualCollisionLoad cload = visual_load_encounter_collision_map(edef, env, encounter_name); + if (cload.cmap) { + fprintf(stderr, "%s collision map: %d regions, offset (%d, %d)\n", + encounter_name, cload.cmap->count, cload.offset_x, cload.offset_y); + } + + if (start_wave >= 0 && edef->put_int) { + edef->put_int( + env->encounter_state, + env->encounter_context, + "start_wave", + start_wave); + } + edef->reset(env->encounter_state, env->encounter_context, 0); + fprintf(stderr, "encounter: %s (obs=%d, heads=%d)\n", + edef->name, edef->obs_size, edef->num_action_heads); + if (start_wave >= 0) + fprintf(stderr, "start_wave: %d\n", start_wave); + } else { + env->pvp_runtime.use_c_opponent = 1; + env->pvp_runtime.opponent.type = OPP_IMPROVED; + env->is_lms = 1; + pvp_reset(env); + } + + const char* cmap_path = getenv("OSRS_COLLISION_MAP"); + if (cmap_path && cmap_path[0]) { + env->collision_map = collision_map_load(cmap_path); + if (env->collision_map) { + fprintf(stderr, "collision map loaded: %d regions\n", + ((CollisionMap*)env->collision_map)->count); + } + } + + pvp_render(env); + RenderClient* rc = (RenderClient*)env->client; +#ifdef __EMSCRIPTEN__ + if (!encounter_name || encounter_name_is_pvp(encounter_name)) { + rc->ticks_per_second = 15.0f; + } +#endif + + if (!encounter_name || encounter_name_is_pvp(encounter_name)) { + osrs_asset_require_group(OSRS_ASSET_GROUP_PVP); + } else if (strcmp(encounter_name, "zulrah") == 0) { + osrs_asset_require_group(OSRS_ASSET_GROUP_ZULRAH); + osrs_asset_require_group(OSRS_ASSET_GROUP_COMBAT_VISUALS); + } else if (strcmp(encounter_name, "inferno") == 0) { + osrs_asset_require_group(OSRS_ASSET_GROUP_INFERNO); + osrs_asset_require_group(OSRS_ASSET_GROUP_COMBAT_VISUALS); + } else if (strcmp(encounter_name, "colosseum") == 0) { + osrs_asset_require_group(OSRS_ASSET_GROUP_COLOSSEUM); + osrs_asset_require_group(OSRS_ASSET_GROUP_COMBAT_VISUALS); + col_for_each_display_inventory_sprite_raw_osrs_id( + visual_require_gui_item_sprite, + &rc->gui); + } + + if (env->collision_map) { + rc->collision_map = (const CollisionMap*)env->collision_map; + } + + rc->model_cache = model_cache_load(OSRS_ASSET("equipment.models")); + if (rc->model_cache) { + rc->show_models = 1; + } + rc->anim_cache = anim_cache_load(OSRS_ASSET("equipment.anims")); + render_load_projectile_assets(rc); + render_init_overlay_models(rc); + if (!encounter_name || encounter_name_is_pvp(encounter_name)) { + rc->terrain = terrain_load(OSRS_ASSET("wilderness.terrain")); + rc->objects = NULL; + rc->npcs = NULL; + } else if (strcmp(encounter_name, "zulrah") == 0) { + rc->terrain = terrain_load(OSRS_ASSET("zulrah.terrain")); + rc->objects = objects_load(OSRS_ASSET("zulrah.objects")); + + int zul_off_x = 2240 + 16; + int zul_off_y = 3008 + 53; + if (rc->terrain) + terrain_offset(rc->terrain, zul_off_x, zul_off_y); + if (rc->objects) + objects_offset(rc->objects, zul_off_x, zul_off_y); + + rc->collision_map = (const CollisionMap*)env->collision_map; + rc->collision_world_offset_x = 2256; + rc->collision_world_offset_y = 3061; + + rc->npc_model_cache = model_cache_load(OSRS_ASSET("zulrah.models")); + rc->npc_anim_cache = anim_cache_load(OSRS_ASSET("zulrah.anims")); + fprintf(stderr, "zulrah: npc_models=%d, npc_anims=%d seqs\n", + rc->npc_model_cache ? rc->npc_model_cache->count : 0, + rc->npc_anim_cache ? rc->npc_anim_cache->seq_count : 0); + } else if (encounter_name && strcmp(encounter_name, "inferno") == 0) { + rc->terrain = terrain_load(OSRS_ASSET("inferno.terrain")); + rc->objects = objects_load(OSRS_ASSET("inferno.objects")); + rc->objects_zuk = objects_load(OSRS_ASSET("inferno_zuk.objects")); + if (rc->terrain) + terrain_offset(rc->terrain, 2246, 5315); + if (rc->objects) + objects_offset(rc->objects, 2246, 5315); + if (rc->objects_zuk) + objects_offset(rc->objects_zuk, 2246, 5315); + + rc->npc_model_cache = model_cache_load(OSRS_ASSET("inferno.models")); + rc->npc_anim_cache = anim_cache_load(OSRS_ASSET("inferno.anims")); + + if (env->collision_map) { + rc->collision_map = (const CollisionMap*)env->collision_map; + rc->collision_world_offset_x = 2246; + rc->collision_world_offset_y = 5315; + } + + fprintf(stderr, "inferno: terrain=%s, cmap=%s, npc_models=%d, npc_anims=%d seqs\n", + rc->terrain ? "loaded" : "MISSING", + rc->collision_map ? "loaded" : "MISSING", + rc->npc_model_cache ? rc->npc_model_cache->count : 0, + rc->npc_anim_cache ? rc->npc_anim_cache->seq_count : 0); + } else if (encounter_name && strcmp(encounter_name, "colosseum") == 0) { + rc->terrain = terrain_load(OSRS_ASSET("colosseum.terrain")); + rc->objects = objects_load(OSRS_ASSET("colosseum.objects")); + if (rc->terrain) + terrain_offset(rc->terrain, 1808, 3090); + if (rc->objects) + objects_offset(rc->objects, 1808, 3090); + rc->npc_model_cache = model_cache_load(OSRS_ASSET("colosseum_npcs.models")); + rc->npc_anim_cache = anim_cache_load(OSRS_ASSET("colosseum_npcs.anims")); + if (env->collision_map) { + rc->collision_map = (const CollisionMap*)env->collision_map; + rc->collision_world_offset_x = 1808; + rc->collision_world_offset_y = 3090; + } + fprintf(stderr, "colosseum: terrain=%s, cmap=%s, npc_models=%d, npc_anims=%d seqs\n", + rc->terrain ? "loaded" : "MISSING", + rc->collision_map ? "loaded" : "MISSING", + rc->npc_model_cache ? rc->npc_model_cache->count : 0, + rc->npc_anim_cache ? rc->npc_anim_cache->seq_count : 0); + } + + render_populate_entities(rc, env); + + rc->cam_target_x = (float)rc->arena_base_x + (float)rc->arena_width / 2.0f; + rc->cam_target_z = -((float)rc->arena_base_y + (float)rc->arena_height / 2.0f); + + for (int i = 0; i < rc->entity_count; i++) { + int size = rc->entities[i].npc_size > 1 ? rc->entities[i].npc_size : 1; + rc->sub_x[i] = rc->entities[i].x * 128 + size * 64; + rc->sub_y[i] = rc->entities[i].y * 128 + size * 64; + rc->dest_x[i] = rc->sub_x[i]; + rc->dest_y[i] = rc->sub_y[i]; + } + + ReplayFile* replay = NULL; + if (replay_path && env->encounter_def) { + const EncounterDef* edef = (const EncounterDef*)env->encounter_def; + size_t snapshot_size = 0; + if (edef->snapshot_size) + snapshot_size = edef->snapshot_size( + env->encounter_state, + env->encounter_context); + replay = replay_load(replay_path, edef->num_action_heads, snapshot_size); + if (replay && replay->initial_snapshot) { + if (!edef->restore) { + fprintf(stderr, "replay: encounter has snapshot data but no restore hook\n"); + abort(); + } + edef->restore( + env->encounter_state, + env->encounter_context, + replay->initial_snapshot, + replay->initial_snapshot_size); + } else if (replay && edef->put_int) { + edef->reset(env->encounter_state, env->encounter_context, 0); + edef->put_int( + env->encounter_state, + env->encounter_context, + "seed", + (int)replay->rng_seed); + } + if (replay) { + render_populate_entities(rc, env); + for (int i = 0; i < rc->entity_count; i++) { + int size = rc->entities[i].npc_size > 1 ? rc->entities[i].npc_size : 1; + rc->sub_x[i] = rc->entities[i].x * 128 + size * 64; + rc->sub_y[i] = rc->entities[i].y * 128 + size * 64; + rc->dest_x[i] = rc->sub_x[i]; + rc->dest_y[i] = rc->sub_y[i]; + } + } + } + + VisualPolicy policy; + visual_policy_init( + &policy, + (const EncounterDef*)env->encounter_def, + model_path, + policy_mode, + policy_seed, + g_cli_hidden_size, + g_cli_num_layers, + g_cli_entity_encoder); + + render_save_snapshot(rc, env); + +#ifdef __EMSCRIPTEN__ + static VisualState web_visual_state; + web_visual_state = (VisualState){ + .env = env, + .encounter_name = encounter_name, + .replay = replay, + .policy = policy, + .start_wave = start_wave, + .episode_end_time = 0, + .episode_ended = 0, + .seen_lab_restore_generation = rc->lab_restore_generation, + }; + rc->pre_sim_mutation_hook = visual_async_policy_guard; + rc->pre_sim_mutation_hook_ctx = &web_visual_state; + emscripten_set_main_loop_arg(visual_frame, &web_visual_state, 0, 1); +#else + VisualState vs = { + .env = env, + .encounter_name = encounter_name, + .replay = replay, + .policy = policy, + .start_wave = start_wave, + .episode_end_time = 0, + .episode_ended = 0, + .seen_lab_restore_generation = rc->lab_restore_generation, + }; + rc->pre_sim_mutation_hook = visual_async_policy_guard; + rc->pre_sim_mutation_hook_ctx = &vs; + + if (g_cli_camera_dist > 0.0f) rc->cam_dist = g_cli_camera_dist; + if (g_cli_camera_yaw > -999.0f) rc->cam_yaw = g_cli_camera_yaw; + if (g_cli_camera_pitch > -999.0f) rc->cam_pitch = g_cli_camera_pitch; + if (g_cli_gui_tab >= 0 && g_cli_gui_tab < GUI_TAB_COUNT) + rc->gui.active_tab = (GuiTab)g_cli_gui_tab; + if (g_cli_tps > 0.0f) + rc->ticks_per_second = g_cli_tps; + + int frame_counter = 0; + while (!WindowShouldClose()) { + if (g_cli_screenshot_path && rc->entity_count > 0) { + int eidx = rc->gui.gui_entity_idx; + if (eidx >= 0 && eidx < rc->entity_count) { + rc->cam_target_x = (float)rc->sub_x[eidx] / 128.0f; + rc->cam_target_z = -(float)rc->sub_y[eidx] / 128.0f; + } + } + visual_frame(&vs); + frame_counter++; + if (g_cli_screenshot_path && frame_counter >= g_cli_screenshot_frame) { + TakeScreenshot(g_cli_screenshot_path); + fprintf(stderr, "screenshot: wrote %s at frame %d\n", + g_cli_screenshot_path, frame_counter); + break; + } + } + + async_policy_join(&vs.async_policy); + + replay_free(replay); + visual_policy_destroy(&vs.policy); + + if (env->client) { + render_destroy_client((RenderClient*)env->client); + env->client = NULL; + } + if (env->encounter_def && env->encounter_state) { + ((const EncounterDef*)env->encounter_def)->destroy(env->encounter_state); + env->encounter_state = NULL; + visual_destroy_encounter_context( + (const EncounterDef*)env->encounter_def, + (EncounterContext**)&env->encounter_context); + } +#endif +} +#endif + +static void visual_alloc_env_buffers(OsrsEnv* env) { + env->observations = (float*)calloc(NUM_AGENTS * SLOT_NUM_OBSERVATIONS, sizeof(float)); + env->actions = (int*)calloc(NUM_AGENTS * NUM_ACTION_HEADS, sizeof(int)); + env->rewards = (float*)calloc(NUM_AGENTS, sizeof(float)); + env->terminals = (unsigned char*)calloc(NUM_AGENTS, sizeof(unsigned char)); + env->action_masks = (unsigned char*)calloc(NUM_AGENTS * ACTION_MASK_SIZE, sizeof(unsigned char)); + env->action_masks_agents = (1 << NUM_AGENTS) - 1; + env->ocean_io.agent_actions = env->actions; + env->ocean_io.agent_obs = (float*)calloc(OCEAN_OBS_SIZE, sizeof(float)); + env->ocean_io.agent_rewards = env->rewards; + env->ocean_io.agent_terminals = env->terminals; +} + +static void visual_free_env_buffers(OsrsEnv* env) { + free(env->observations); + free(env->actions); + free(env->rewards); + free(env->terminals); + free(env->action_masks); + free(env->ocean_io.agent_obs); +} + +int main(int argc, char** argv) { + int use_visual = 1; + int use_profile = 0; + int gear_tier = -1; + int start_wave = -1; + int profile_steps = 0; + int metrics_episodes = 0; + int metrics_bis_oracle = 0; + int loadout_mode = 2; + const char* encounter_name __attribute__((unused)) = NULL; + const char* replay_path __attribute__((unused)) = NULL; + const char* model_path __attribute__((unused)) = NULL; + const char* policy_mode_name __attribute__((unused)) = "sample"; + uint32_t policy_seed __attribute__((unused)) = 1; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--visual") == 0) use_visual = 1; + else if (strcmp(argv[i], "--profile") == 0) { use_profile = 1; use_visual = 0; } + else if (strcmp(argv[i], "--encounter") == 0 && i + 1 < argc) + encounter_name = argv[++i]; + else if (strcmp(argv[i], "--replay") == 0 && i + 1 < argc) + replay_path = argv[++i]; + else if (strcmp(argv[i], "--model") == 0 && i + 1 < argc) + model_path = argv[++i]; + else if (strcmp(argv[i], "--policy-mode") == 0 && i + 1 < argc) + policy_mode_name = argv[++i]; + else if (strcmp(argv[i], "--policy-seed") == 0 && i + 1 < argc) + policy_seed = visual_policy_parse_seed(argv[++i]); + else if (strcmp(argv[i], "--hidden-size") == 0 && i + 1 < argc) + g_cli_hidden_size = atoi(argv[++i]); + else if (strcmp(argv[i], "--num-layers") == 0 && i + 1 < argc) + g_cli_num_layers = atoi(argv[++i]); + else if (strcmp(argv[i], "--entity-encoder") == 0) + g_cli_entity_encoder = (i + 1 < argc && argv[i + 1][0] != '-') ? atoi(argv[++i]) : 1; + else if (strcmp(argv[i], "--screenshot") == 0 && i + 1 < argc) + g_cli_screenshot_path = argv[++i]; + else if (strcmp(argv[i], "--screenshot-frame") == 0 && i + 1 < argc) + g_cli_screenshot_frame = atoi(argv[++i]); + else if (strcmp(argv[i], "--tab") == 0 && i + 1 < argc) + g_cli_gui_tab = atoi(argv[++i]); + else if (strcmp(argv[i], "--tps") == 0 && i + 1 < argc) + g_cli_tps = (float)atof(argv[++i]); + else if (strcmp(argv[i], "--camera-dist") == 0 && i + 1 < argc) + g_cli_camera_dist = (float)atof(argv[++i]); + else if (strcmp(argv[i], "--camera-yaw") == 0 && i + 1 < argc) + g_cli_camera_yaw = (float)atof(argv[++i]); + else if (strcmp(argv[i], "--camera-pitch") == 0 && i + 1 < argc) + g_cli_camera_pitch = (float)atof(argv[++i]); + else if (strcmp(argv[i], "--tier") == 0 && i + 1 < argc) + gear_tier = atoi(argv[++i]); + else if (strcmp(argv[i], "--wave") == 0 && i + 1 < argc) + start_wave = atoi(argv[++i]); + else if ((strcmp(argv[i], "--start-wave") == 0 || + strcmp(argv[i], "--start_wave") == 0) && i + 1 < argc) + start_wave = atoi(argv[++i]); + else if (strcmp(argv[i], "--profile-steps") == 0 && i + 1 < argc) + profile_steps = atoi(argv[++i]); + else if (strcmp(argv[i], "--metrics") == 0 && i + 1 < argc) { + metrics_episodes = atoi(argv[++i]); + use_visual = 0; + } + else if (strcmp(argv[i], "--bis-oracle") == 0) + metrics_bis_oracle = 1; + else if (strcmp(argv[i], "--loadout-mode") == 0 && i + 1 < argc) { + loadout_mode = atoi(argv[++i]); + g_cli_visual_loadout_mode = loadout_mode; + } + else if (strcmp(argv[i], "--prayer-oracle") == 0) + g_cli_prayer_oracle = 1; + } + +#ifdef __EMSCRIPTEN__ + if (!encounter_name) encounter_name = "inferno"; + if (encounter_name && strcmp(encounter_name, "pvp") == 0) encounter_name = "nh_pvp"; +#else +#ifdef OSRS_VISUAL_DEFAULT_ENCOUNTER + if (!encounter_name) encounter_name = OSRS_VISUAL_DEFAULT_ENCOUNTER; +#endif + if (encounter_name && strcmp(encounter_name, "pvp") == 0) encounter_name = "nh_pvp"; +#endif + VisualPolicyMode policy_mode __attribute__((unused)) = + visual_policy_parse_mode(policy_mode_name); + + srand((unsigned int)time(NULL)); + +#ifdef __EMSCRIPTEN__ + static OsrsEnv env; +#else + OsrsEnv env; +#endif + memset(&env, 0, sizeof(OsrsEnv)); + + if (metrics_episodes > 0) { + run_metrics(&env, encounter_name, model_path, policy_mode, policy_seed, + metrics_episodes, loadout_mode, metrics_bis_oracle, start_wave); + return 0; + } + + if (use_profile) { + visual_alloc_env_buffers(&env); + + run_profile(&env, encounter_name, start_wave, profile_steps); + + visual_free_env_buffers(&env); + pvp_close(&env); + return 0; + } + + if (use_visual) { +#ifdef OSRS_VISUAL + pvp_init(&env); + if (gear_tier >= 0 && gear_tier <= 3) { + for (int t = 0; t < 4; t++) env.pvp_runtime.gear_tier_weights[t] = 0.0f; + env.pvp_runtime.gear_tier_weights[gear_tier] = 1.0f; + } else { + env.pvp_runtime.gear_tier_weights[0] = 0.60f; + env.pvp_runtime.gear_tier_weights[1] = 0.25f; + env.pvp_runtime.gear_tier_weights[2] = 0.10f; + env.pvp_runtime.gear_tier_weights[3] = 0.05f; + } + env.ocean_io.agent_actions = env.actions; + env.ocean_io.agent_obs = env._obs_buf; + env.ocean_io.agent_rewards = env.rewards; + env.ocean_io.agent_terminals = env.terminals; + run_visual( + &env, + encounter_name, + replay_path, + start_wave, + gear_tier, + model_path, + policy_mode, + policy_seed); + pvp_close(&env); +#else + fprintf(stderr, "not compiled with visual support (use: make visual)\n"); + return 1; +#endif + } else { + visual_alloc_env_buffers(&env); + + printf("OSRS PvP C Environment Demo\n\n"); + + printf("Running single verbose episode...\n"); + run_random_episode(&env, 1); + + printf("\n"); + benchmark(&env, 100000); + + printf("\nVerifying observations...\n"); + pvp_reset(&env); + printf("Observation count per agent: %d\n", SLOT_NUM_OBSERVATIONS); + printf("First 10 observations (agent 0): "); + for (int i = 0; i < 10; i++) { + printf("%.2f ", env.observations[i]); + } + printf("\n"); + + printf("\nAction heads: %d\n", NUM_ACTION_HEADS); + printf("Action dims: ["); + for (int i = 0; i < NUM_ACTION_HEADS; i++) { + printf("%d", ACTION_HEAD_DIMS[i]); + if (i < NUM_ACTION_HEADS - 1) { + printf(", "); + } + } + printf("]\n"); + + printf("\nDemo complete.\n"); + + visual_free_env_buffers(&env); + pvp_close(&env); + } + + return 0; +} diff --git a/ocean/osrs/osrs_visual_net.h b/ocean/osrs/osrs_visual_net.h new file mode 100644 index 0000000000..3e6487bce6 --- /dev/null +++ b/ocean/osrs/osrs_visual_net.h @@ -0,0 +1,185 @@ +#pragma once + +#define COLO_ENT_INF_NPC_START 1030 +#define COLO_ENT_INF_NUM_NPCS 24 +#define COLO_ENT_INF_FEATS 37 +#define COLO_ENT_INF_TYPE_ONEHOT 12 +#define COLO_ENT_INF_BOTTLENECK 16 +#define COLO_ENT_INF_INV_START 48 +#define COLO_ENT_INF_INV_NUM_CELLS 28 +#define COLO_ENT_INF_INV_FEATS 28 +#define COLO_ENT_INF_INV_PRESENT 0 +#define COLO_ENT_INF_INV_BOTTLENECK 16 + +typedef struct ColosseumEntityEncoder ColosseumEntityEncoder; +struct ColosseumEntityEncoder { + float* output; + float* global_w; + float* entity_l1_w; + float* entity_l2_w; + float* z1; + float* h1; + float* entity_e; + int batch_size; + int input_dim; + int hidden_dim; + int mode; + float* inv_l1_w; + float* inv_l2_w; + float* inv_z1; + float* inv_h1; + float* inv_e; +}; + +ColosseumEntityEncoder* make_colosseum_entity_encoder( + Weights* weights, int batch_size, int input_dim, int hidden_dim, int mode) { + size_t out_size = (size_t)batch_size * hidden_dim * sizeof(float); + ColosseumEntityEncoder* layer = + (ColosseumEntityEncoder*)calloc(1, sizeof(ColosseumEntityEncoder) + out_size); + *layer = (ColosseumEntityEncoder){ + .output = (float*)(layer + 1), + .global_w = get_weights_aligned(weights, hidden_dim * input_dim), + .entity_l1_w = get_weights_aligned(weights, COLO_ENT_INF_BOTTLENECK * COLO_ENT_INF_FEATS), + .entity_l2_w = get_weights_aligned(weights, hidden_dim * COLO_ENT_INF_BOTTLENECK), + .z1 = (float*)calloc((size_t)COLO_ENT_INF_NUM_NPCS * COLO_ENT_INF_BOTTLENECK, sizeof(float)), + .h1 = (float*)calloc((size_t)COLO_ENT_INF_NUM_NPCS * COLO_ENT_INF_BOTTLENECK, sizeof(float)), + .entity_e = (float*)calloc((size_t)COLO_ENT_INF_NUM_NPCS * hidden_dim, sizeof(float)), + .batch_size = batch_size, + .input_dim = input_dim, + .hidden_dim = hidden_dim, + .mode = mode, + }; + // mode 2: the inventory-pool weights follow entity_l2 in the .bin (reg_params order). + // Read them as sequenced statements AFTER the initializer above (C does not specify the + // initializer's internal evaluation order, so the global/entity reads must complete first). + if (mode >= 2) { + layer->inv_l1_w = get_weights_aligned(weights, COLO_ENT_INF_INV_BOTTLENECK * COLO_ENT_INF_INV_FEATS); + layer->inv_l2_w = get_weights_aligned(weights, hidden_dim * COLO_ENT_INF_INV_BOTTLENECK); + layer->inv_z1 = (float*)calloc((size_t)COLO_ENT_INF_INV_NUM_CELLS * COLO_ENT_INF_INV_BOTTLENECK, sizeof(float)); + layer->inv_h1 = (float*)calloc((size_t)COLO_ENT_INF_INV_NUM_CELLS * COLO_ENT_INF_INV_BOTTLENECK, sizeof(float)); + layer->inv_e = (float*)calloc((size_t)COLO_ENT_INF_INV_NUM_CELLS * hidden_dim, sizeof(float)); + } + return layer; +} + +void colosseum_entity_encoder(ColosseumEntityEncoder* layer, float* observations) { + int H = layer->hidden_dim; + int IN = layer->input_dim; + for (int b = 0; b < layer->batch_size; b++) { + float* obs = observations + (size_t)b * IN; + float* out = layer->output + (size_t)b * H; + + for (int o = 0; o < H; o++) { + float sum = 0.0f; + for (int i = 0; i < IN; i++) sum += obs[i] * layer->global_w[o * IN + i]; + out[o] = sum; + } + + float* npcs = obs + COLO_ENT_INF_NPC_START; + for (int n = 0; n < COLO_ENT_INF_NUM_NPCS; n++) { + float* rec = npcs + n * COLO_ENT_INF_FEATS; + float* z1n = layer->z1 + n * COLO_ENT_INF_BOTTLENECK; + for (int k = 0; k < COLO_ENT_INF_BOTTLENECK; k++) { + float sum = 0.0f; + for (int i = 0; i < COLO_ENT_INF_FEATS; i++) + sum += rec[i] * layer->entity_l1_w[k * COLO_ENT_INF_FEATS + i]; + z1n[k] = sum; + } + } + _gelu(layer->z1, layer->h1, COLO_ENT_INF_NUM_NPCS * COLO_ENT_INF_BOTTLENECK); + for (int n = 0; n < COLO_ENT_INF_NUM_NPCS; n++) { + float* h1n = layer->h1 + n * COLO_ENT_INF_BOTTLENECK; + float* en = layer->entity_e + (size_t)n * H; + for (int o = 0; o < H; o++) { + float sum = 0.0f; + for (int k = 0; k < COLO_ENT_INF_BOTTLENECK; k++) + sum += h1n[k] * layer->entity_l2_w[o * COLO_ENT_INF_BOTTLENECK + k]; + en[o] = sum; + } + } + + for (int o = 0; o < H; o++) { + float best = -INFINITY; + int best_n = -1; + for (int n = 0; n < COLO_ENT_INF_NUM_NPCS; n++) { + float* rec = npcs + n * COLO_ENT_INF_FEATS; + float type_sum = 0.0f; + for (int t = 0; t < COLO_ENT_INF_TYPE_ONEHOT; t++) type_sum += rec[t]; + if (type_sum <= 0.0f) continue; + float v = layer->entity_e[(size_t)n * H + o]; + if (v > best) { best = v; best_n = n; } + } + out[o] += (best_n < 0) ? 0.0f : best; + } + + if (layer->mode >= 2) { + float* cells = obs + COLO_ENT_INF_INV_START; + for (int n = 0; n < COLO_ENT_INF_INV_NUM_CELLS; n++) { + float* rec = cells + n * COLO_ENT_INF_INV_FEATS; + float* z1n = layer->inv_z1 + n * COLO_ENT_INF_INV_BOTTLENECK; + for (int k = 0; k < COLO_ENT_INF_INV_BOTTLENECK; k++) { + float sum = 0.0f; + for (int i = 0; i < COLO_ENT_INF_INV_FEATS; i++) + sum += rec[i] * layer->inv_l1_w[k * COLO_ENT_INF_INV_FEATS + i]; + z1n[k] = sum; + } + } + _gelu(layer->inv_z1, layer->inv_h1, COLO_ENT_INF_INV_NUM_CELLS * COLO_ENT_INF_INV_BOTTLENECK); + for (int n = 0; n < COLO_ENT_INF_INV_NUM_CELLS; n++) { + float* h1n = layer->inv_h1 + n * COLO_ENT_INF_INV_BOTTLENECK; + float* en = layer->inv_e + (size_t)n * H; + for (int o = 0; o < H; o++) { + float sum = 0.0f; + for (int k = 0; k < COLO_ENT_INF_INV_BOTTLENECK; k++) + sum += h1n[k] * layer->inv_l2_w[o * COLO_ENT_INF_INV_BOTTLENECK + k]; + en[o] = sum; + } + } + for (int o = 0; o < H; o++) { + float best = -INFINITY; + int best_n = -1; + for (int n = 0; n < COLO_ENT_INF_INV_NUM_CELLS; n++) { + float* rec = cells + n * COLO_ENT_INF_INV_FEATS; + if (rec[COLO_ENT_INF_INV_PRESENT] <= 0.0f) continue; + float v = layer->inv_e[(size_t)n * H + o]; + if (v > best) { best = v; best_n = n; } + } + out[o] += (best_n < 0) ? 0.0f : best; + } + } + } +} + +void free_colosseum_entity_encoder(ColosseumEntityEncoder* layer) { + free(layer->z1); + free(layer->h1); + free(layer->entity_e); + free(layer->inv_z1); + free(layer->inv_h1); + free(layer->inv_e); + free(layer); +} + +typedef struct VisualNet VisualNet; +struct VisualNet { + int num_agents; + float* obs; + Linear* encoder; + ColosseumEntityEncoder* entity_encoder; + MinGRU* mingru; + Linear* decoder; + float* log_std; + int is_continuous; + int num_actions; + Multidiscrete* multidiscrete; +}; + +void visual_net_free(VisualNet* net) { + free(net->obs); + if (net->encoder) free(net->encoder); + if (net->entity_encoder) free_colosseum_entity_encoder(net->entity_encoder); + free(net->decoder); + free_mingru(net->mingru); + if (net->multidiscrete) free(net->multidiscrete); + free(net); +} diff --git a/ocean/osrs/scripts/osrs_asset_manifest.py b/ocean/osrs/scripts/osrs_asset_manifest.py new file mode 100755 index 0000000000..86db01e6bb --- /dev/null +++ b/ocean/osrs/scripts/osrs_asset_manifest.py @@ -0,0 +1,392 @@ +#!/usr/bin/env python3 +"""Validate, package, and generate runtime metadata for OSRS assets.""" + +from __future__ import annotations + +import argparse +from dataclasses import dataclass +import gzip +import hashlib +import json +from pathlib import Path +import re +import sys +import tarfile + + +EXPECTED_FORMAT = "puffer-osrs-asset-manifest-v1" +SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$") +IGNORED_ARCHIVE_NAMES = {".DS_Store"} +IGNORED_ARCHIVE_PREFIXES = ("._",) +IGNORED_ARCHIVE_DIRS = {"__MACOSX"} + + +@dataclass(frozen=True) +class AssetArchive: + name: str + url: str + sha256: str + strip_components: int + + +@dataclass(frozen=True) +class RequiredGroup: + name: str + files: tuple[str, ...] + + +@dataclass(frozen=True) +class AssetManifest: + format: str + asset_version: str + archive: AssetArchive + required_groups: tuple[RequiredGroup, ...] + + +def path_is_safe(path: str | None) -> bool: + if not path: + return False + if path.startswith("/") or "\\" in path: + return False + return all(part not in ("", ".", "..") for part in path.split("/")) + + +def expect_str(raw: object, key: str, context: str) -> str: + if not isinstance(raw, dict): + raise SystemExit(f"osrs-assets: {context} must be an object") + value = raw.get(key) + if not isinstance(value, str) or not value: + raise SystemExit(f"osrs-assets: {context}.{key} must be a string") + return value + + +def expect_int(raw: object, key: str, context: str) -> int: + if not isinstance(raw, dict): + raise SystemExit(f"osrs-assets: {context} must be an object") + value = raw.get(key) + if not isinstance(value, int): + raise SystemExit(f"osrs-assets: {context}.{key} must be an integer") + return value + + +def load_manifest(path: Path) -> AssetManifest: + raw = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(raw, dict): + raise SystemExit("osrs-assets: manifest must be an object") + + manifest_format = expect_str(raw, "format", "manifest") + if manifest_format != EXPECTED_FORMAT: + raise SystemExit(f"osrs-assets: unsupported manifest format: {manifest_format}") + + asset_version = expect_str(raw, "asset_version", "manifest") + if not re.fullmatch(r"[a-z0-9][a-z0-9._-]*", asset_version): + raise SystemExit(f"osrs-assets: unsafe asset version: {asset_version}") + + archive_raw = raw.get("archive") + archive = AssetArchive( + name=expect_str(archive_raw, "name", "archive"), + url=expect_str(archive_raw, "url", "archive"), + sha256=expect_str(archive_raw, "sha256", "archive"), + strip_components=expect_int(archive_raw, "strip_components", "archive"), + ) + if archive.name != f"{asset_version}.tar.gz": + raise SystemExit( + "osrs-assets: archive.name must match asset_version plus .tar.gz" + ) + if not archive.url.startswith(("https://", "http://", "file://")): + raise SystemExit(f"osrs-assets: unsupported archive URL: {archive.url}") + if not SHA256_PATTERN.fullmatch(archive.sha256): + raise SystemExit(f"osrs-assets: bad archive SHA256: {archive.sha256}") + if archive.strip_components < 0: + raise SystemExit("osrs-assets: archive strip_components must be nonnegative") + + groups_raw = raw.get("required_groups") + if not isinstance(groups_raw, list) or not groups_raw: + raise SystemExit("osrs-assets: required_groups must be a nonempty list") + + groups: list[RequiredGroup] = [] + seen_group_names: set[str] = set() + for group_raw in groups_raw: + name = expect_str(group_raw, "name", "required_group") + if not re.fullmatch(r"[a-z][a-z0-9_]*", name): + raise SystemExit(f"osrs-assets: unsafe required group name: {name}") + if name in seen_group_names: + raise SystemExit(f"osrs-assets: repeated required group: {name}") + seen_group_names.add(name) + + files_raw = group_raw.get("files") if isinstance(group_raw, dict) else None + if not isinstance(files_raw, list) or not files_raw: + raise SystemExit(f"osrs-assets: group {name} has no files") + files: list[str] = [] + seen_files: set[str] = set() + for file_raw in files_raw: + if not isinstance(file_raw, str) or not path_is_safe(file_raw): + raise SystemExit(f"osrs-assets: unsafe asset path in group {name}: {file_raw}") + if file_raw in seen_files: + raise SystemExit(f"osrs-assets: repeated asset path in group {name}: {file_raw}") + seen_files.add(file_raw) + files.append(file_raw) + groups.append(RequiredGroup(name=name, files=tuple(files))) + + return AssetManifest( + format=manifest_format, + asset_version=asset_version, + archive=archive, + required_groups=tuple(groups), + ) + + +def missing_required_files(manifest: AssetManifest, data_dir: Path) -> list[tuple[str, str]]: + missing: list[tuple[str, str]] = [] + for group in manifest.required_groups: + for asset_path in group.files: + if not (data_dir / asset_path).is_file(): + missing.append((group.name, asset_path)) + return missing + + +def c_identifier_group_name(name: str) -> str: + return "OSRS_ASSET_GROUP_" + name.upper() + + +def c_path_array_name(name: str) -> str: + return "OSRS_ASSET_" + name.upper() + "_PATHS" + + +def c_string(value: str) -> str: + return json.dumps(value) + + +def generated_header_text(manifest: AssetManifest) -> str: + lines: list[str] = [ + "/* Generated by ocean/osrs/scripts/osrs_asset_manifest.py. */", + "#ifndef OSRS_ASSETS_GENERATED_H", + "#define OSRS_ASSETS_GENERATED_H", + "", + "#include ", + "", + "typedef enum {", + ] + for idx, group in enumerate(manifest.required_groups): + lines.append(f" {c_identifier_group_name(group.name)} = {idx},") + lines.extend([ + " OSRS_ASSET_GROUP_COUNT,", + "} OsrsAssetGroupKind;", + "", + "typedef struct {", + " const char* name;", + " const char* const* paths;", + " size_t path_count;", + "} OsrsAssetGroup;", + "", + ]) + + for group in manifest.required_groups: + lines.append(f"static const char* const {c_path_array_name(group.name)}[] = {{") + for asset_path in group.files: + lines.append(f" {c_string(asset_path)},") + lines.extend(["};", ""]) + + lines.extend([ + "static const OsrsAssetGroup OSRS_ASSET_GROUPS[OSRS_ASSET_GROUP_COUNT] = {", + ]) + for group in manifest.required_groups: + ident = c_identifier_group_name(group.name) + array_name = c_path_array_name(group.name) + lines.extend([ + f" [{ident}] = {{", + f" .name = {c_string(group.name)},", + f" .paths = {array_name},", + f" .path_count = sizeof({array_name}) / sizeof({array_name}[0]),", + " },", + ]) + lines.extend([ + "};", + "", + "#endif", + "", + ]) + return "\n".join(lines) + + +def write_if_changed(path: Path, text: str) -> bool: + if path.exists() and path.read_text(encoding="utf-8") == text: + return False + path.write_text(text, encoding="utf-8") + return True + + +def archive_member_allowed(path: Path) -> bool: + if path.name in IGNORED_ARCHIVE_NAMES: + return False + if any(part in IGNORED_ARCHIVE_DIRS for part in path.parts): + return False + return not any(part.startswith(IGNORED_ARCHIVE_PREFIXES) for part in path.parts) + + +def archive_relpaths(data_dir: Path) -> list[Path]: + relpaths: list[Path] = [] + for path in data_dir.rglob("*"): + relpath = path.relative_to(data_dir) + if ".download" in relpath.parts: + continue + if not archive_member_allowed(relpath): + continue + if path.is_file(): + relpaths.append(relpath) + return sorted(relpaths, key=lambda p: p.as_posix()) + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as file: + for chunk in iter(lambda: file.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def package_archive(manifest: AssetManifest, data_dir: Path, dist_dir: Path) -> Path: + missing = missing_required_files(manifest, data_dir) + if missing: + for group_name, asset_path in missing: + print(f"{group_name}\t{asset_path}", file=sys.stderr) + raise SystemExit("osrs-assets: cannot package archive with missing required files") + + dist_dir.mkdir(parents=True, exist_ok=True) + archive_path = dist_dir / manifest.archive.name + top_level = manifest.asset_version + with archive_path.open("wb") as raw: + with gzip.GzipFile(fileobj=raw, mode="wb", mtime=0) as gzip_file: + with tarfile.open( + fileobj=gzip_file, + mode="w", + format=tarfile.PAX_FORMAT, + ) as tar: + for relpath in archive_relpaths(data_dir): + tar.add( + data_dir / relpath, + arcname=f"{top_level}/{relpath.as_posix()}", + filter=normalize_tarinfo, + recursive=False, + ) + return archive_path + + +def normalize_tarinfo(info: tarfile.TarInfo) -> tarfile.TarInfo: + info.uid = 0 + info.gid = 0 + info.uname = "" + info.gname = "" + info.mtime = 0 + return info + + +def print_group_files(manifest: AssetManifest, group_names: list[str]) -> None: + requested = set(group_names) + manifest_names = {group.name for group in manifest.required_groups} + unknown = requested - manifest_names + if unknown: + raise SystemExit(f"osrs-assets: unknown groups: {', '.join(sorted(unknown))}") + for group in manifest.required_groups: + if requested and group.name not in requested: + continue + for asset_path in group.files: + print(f"{group.name}\t{asset_path}") + + +def print_emcc_preload_args(manifest: AssetManifest, group_names: list[str]) -> None: + requested = set(group_names) + manifest_names = {group.name for group in manifest.required_groups} + unknown = requested - manifest_names + if unknown: + raise SystemExit(f"osrs-assets: unknown groups: {', '.join(sorted(unknown))}") + for group in manifest.required_groups: + if requested and group.name not in requested: + continue + for asset_path in group.files: + print(f"--preload-file ocean/osrs/data/{asset_path}@{asset_path}") + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(description="manage OSRS asset manifests") + subparsers = parser.add_subparsers(dest="command", required=True) + + archive_parser = subparsers.add_parser("archive-tsv") + archive_parser.add_argument("manifest", type=Path) + + missing_parser = subparsers.add_parser("missing-required") + missing_parser.add_argument("manifest", type=Path) + missing_parser.add_argument("data_dir", type=Path) + + validate_parser = subparsers.add_parser("validate") + validate_parser.add_argument("manifest", type=Path) + + generate_parser = subparsers.add_parser("generate-c-header") + generate_parser.add_argument("manifest", type=Path) + generate_parser.add_argument("--output", required=True, type=Path) + + check_parser = subparsers.add_parser("check-c-header") + check_parser.add_argument("manifest", type=Path) + check_parser.add_argument("--output", required=True, type=Path) + + group_files_parser = subparsers.add_parser("group-files") + group_files_parser.add_argument("manifest", type=Path) + group_files_parser.add_argument("--group", action="append", default=[]) + + emcc_parser = subparsers.add_parser("emcc-preload-args") + emcc_parser.add_argument("manifest", type=Path) + emcc_parser.add_argument("--group", action="append", default=[]) + + package_parser = subparsers.add_parser("package-archive") + package_parser.add_argument("manifest", type=Path) + package_parser.add_argument("--data-dir", required=True, type=Path) + package_parser.add_argument("--dist-dir", required=True, type=Path) + + args = parser.parse_args(argv) + manifest = load_manifest(args.manifest) + + if args.command == "archive-tsv": + archive = manifest.archive + print(f"{archive.name}\t{archive.url}\t{archive.sha256}\t{archive.strip_components}") + return 0 + + if args.command == "missing-required": + missing = missing_required_files(manifest, args.data_dir) + for group_name, asset_path in missing: + print(f"{group_name}\t{asset_path}") + return 1 if missing else 0 + + if args.command == "validate": + return 0 + + if args.command == "generate-c-header": + write_if_changed(args.output, generated_header_text(manifest)) + return 0 + + if args.command == "check-c-header": + expected = generated_header_text(manifest) + if not args.output.exists(): + raise SystemExit(f"osrs-assets: generated header is missing: {args.output}") + actual = args.output.read_text(encoding="utf-8") + if actual != expected: + raise SystemExit(f"osrs-assets: generated header is stale: {args.output}") + return 0 + + if args.command == "group-files": + print_group_files(manifest, args.group) + return 0 + + if args.command == "emcc-preload-args": + print_emcc_preload_args(manifest, args.group) + return 0 + + if args.command == "package-archive": + archive_path = package_archive(manifest, args.data_dir, args.dist_dir) + print(f"{sha256_file(archive_path)}\t{archive_path}") + return 0 + + raise AssertionError(args.command) + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/ocean/osrs/scripts/setup-data.sh b/ocean/osrs/scripts/setup-data.sh new file mode 100644 index 0000000000..445a631275 --- /dev/null +++ b/ocean/osrs/scripts/setup-data.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +OSRS_ROOT="${ROOT}/ocean/osrs" +MANIFEST="${OSRS_ASSET_MANIFEST:-${OSRS_ROOT}/asset_manifest.json}" +DATA_DIR="${OSRS_DATA_DIR:-${OSRS_ROOT}/data}" +DOWNLOAD_DIR="${OSRS_ASSET_DOWNLOAD_DIR:-${DATA_DIR}/.download}" +MANIFEST_TOOL="${OSRS_ROOT}/scripts/osrs_asset_manifest.py" +ARCHIVE_TSV="${DOWNLOAD_DIR}/archive.tsv" +MISSING_TSV="${DOWNLOAD_DIR}/missing-required.tsv" + +need() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "setup-osrs-data: missing required command: $1" >&2 + exit 1 + fi +} + +verify_sha256() { + local expected="$1" + local path="$2" + if command -v sha256sum >/dev/null 2>&1; then + echo "${expected} ${path}" | sha256sum -c - + return + fi + if command -v shasum >/dev/null 2>&1; then + echo "${expected} ${path}" | shasum -a 256 -c - + return + fi + echo "setup-osrs-data: missing required command: sha256sum or shasum" >&2 + exit 1 +} + +download() { + local url="$1" + local out="$2" + echo "setup-osrs-data: downloading ${url}" + curl -fL --retry 3 --retry-delay 2 -o "${out}" "${url}" +} + +need curl +need python3 +need tar + +mkdir -p "${DOWNLOAD_DIR}" "${DATA_DIR}" + +python3 "${MANIFEST_TOOL}" archive-tsv "${MANIFEST}" > "${ARCHIVE_TSV}" +IFS=$'\t' read -r archive_name archive_url archive_sha strip_components < "${ARCHIVE_TSV}" + +if [ "${OSRS_ASSET_SETUP_FORCE:-0}" != "1" ]; then + if python3 "${MANIFEST_TOOL}" missing-required "${MANIFEST}" "${DATA_DIR}" > "${MISSING_TSV}"; then + echo "setup-osrs-data: loose assets satisfy ${archive_name}" + exit 0 + fi + echo "setup-osrs-data: missing required loose assets" + while IFS=$'\t' read -r group_name asset_path; do + [ -n "${asset_path}" ] || continue + echo "setup-osrs-data: missing ${group_name}: ${asset_path}" + done < "${MISSING_TSV}" +fi + +archive_path="${DOWNLOAD_DIR}/${archive_name}" +tmp_archive="${DOWNLOAD_DIR}/.${archive_name}.tmp" + +if [ ! -f "${archive_path}" ] || ! verify_sha256 "${archive_sha}" "${archive_path}" >/dev/null 2>&1; then + rm -f "${tmp_archive}" + download "${archive_url}" "${tmp_archive}" + verify_sha256 "${archive_sha}" "${tmp_archive}" + mv "${tmp_archive}" "${archive_path}" +fi + +echo "setup-osrs-data: extracting ${archive_name} into ${DATA_DIR}" +tar_args=(-xzf "${archive_path}" --exclude='._*' --exclude='*/._*') +if [ "$(uname -s)" = "Linux" ]; then + tar_args+=(--warning=no-unknown-keyword) +fi +tar_args+=(--strip-components="${strip_components}" -C "${DATA_DIR}") +tar "${tar_args[@]}" +find "${DATA_DIR}" -name '._*' -delete + +if ! python3 "${MANIFEST_TOOL}" missing-required "${MANIFEST}" "${DATA_DIR}" > "${MISSING_TSV}"; then + echo "setup-osrs-data: install did not satisfy manifest" >&2 + while IFS=$'\t' read -r group_name asset_path; do + [ -n "${asset_path}" ] || continue + echo "setup-osrs-data: missing ${group_name}: ${asset_path}" >&2 + done < "${MISSING_TSV}" + exit 1 +fi + +echo "setup-osrs-data: ready" diff --git a/ocean/osrs/tests/bench_colosseum_forecast_profile.c b/ocean/osrs/tests/bench_colosseum_forecast_profile.c new file mode 100644 index 0000000000..50dce74465 --- /dev/null +++ b/ocean/osrs/tests/bench_colosseum_forecast_profile.c @@ -0,0 +1,616 @@ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#include +#endif + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +#define BENCH_CORPUS_STATES 512 + +static volatile double bench_sink = 0.0; + +typedef enum { + COLO_BENCH_SETUP_CLEAR = 0, + COLO_BENCH_SETUP_COLLECT, + COLO_BENCH_SETUP_BASE_COPY, + COLO_BENCH_SETUP_FLAGS, + COLO_BENCH_SETUP_PRECOMP, + COLO_BENCH_ACTION_PREP, + COLO_BENCH_RESTORE_NPCS, + COLO_BENCH_RESTORE_FLAGS, + COLO_BENCH_TICK_ADMIN, + COLO_BENCH_MOVEMENT, + COLO_BENCH_ATTACK_AI, + COLO_BENCH_FINALIZE, + COLO_BENCH_OBS_SCORE, + COLO_BENCH_COUNT, +} ColoBenchBucket; + +typedef struct { + uint64_t ns[COLO_BENCH_COUNT]; + uint64_t total_ns; + uint64_t fanout_ns; + uint64_t forecasts; + uint64_t actions_seen; + uint64_t valid_actions; + uint64_t rollout_actions; + uint64_t invalid_or_duplicate_actions; + uint64_t static_threat_actions; + uint64_t rollout_ticks; + uint64_t npc_tick_visits; + uint64_t slot_count_sum; + uint64_t forecast_obs_checksum; +} ColoForecastBenchProfile; + +typedef struct { + ColosseumState states[BENCH_CORPUS_STATES]; + int count; +} ColoBenchCorpus; + +static const char* const COLO_BENCH_BUCKET_NAMES[COLO_BENCH_COUNT] = { + "setup_clear", + "setup_collect_slots", + "setup_base_npc_copy", + "setup_base_flags", + "setup_precompute", + "action_landing_dedup", + "restore_npc_slots", + "restore_npc_flags", + "rollout_tick_admin", + "rollout_movement", + "rollout_attack_ai", + "action_finalize", + "obs_score_encode", +}; + +static uint64_t now_ns(void) { +#ifdef __APPLE__ + static mach_timebase_info_data_t timebase; + if (timebase.denom == 0) { + mach_timebase_info(&timebase); + } + uint64_t ticks = mach_absolute_time(); + return ticks * (uint64_t)timebase.numer / (uint64_t)timebase.denom; +#else + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +#endif +} + +static void bench_add_ns(ColoForecastBenchProfile* profile, int bucket, uint64_t start_ns) { + profile->ns[bucket] += now_ns() - start_ns; +} + +static uint64_t bench_hash_bytes(uint64_t h, const void* data, size_t size) { + const uint8_t* bytes = (const uint8_t*)data; + for (size_t i = 0; i < size; i++) { + h ^= bytes[i]; + h *= 1099511628211ULL; + } + return h; +} + +static uint64_t bench_splitmix64(uint64_t* state) { + uint64_t z = (*state += 0x9E3779B97F4A7C15ULL); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + return z ^ (z >> 31); +} + +static void bench_trace_actions( + ColosseumState* s, + uint64_t* rng, + int actions[COLO_NUM_ACTION_HEADS] +) { + for (int head = 0; head < COLO_NUM_ACTION_HEADS; head++) { + actions[head] = (int)(bench_splitmix64(rng) % (uint64_t)COLO_ACTION_DIMS[head]); + } + if (s->modifiers.draft_pending) { + actions[COLO_HEAD_PRIMARY] = 0; + actions[COLO_HEAD_MODIFIER_SELECT] = 1 + + (int)(bench_splitmix64(rng) % COLO_MODIFIER_DRAFT_OPTIONS); + } +} + +static void bench_init_context(ColosseumContext* ctx, int start_wave) { + col_init_context_typed(ctx); + ctx->config.start_wave = start_wave; + ctx->config.step_out_forecast_obs_enabled = 1; + ctx->config.forecast_horizon = COLO_STEP_OUT_FORECAST_HORIZON; + ctx->config.forecast_run_tile_mode = COLO_FORECAST_RUN_TILE_FULL; +} + +static void bench_capture_state( + ColoBenchCorpus* corpus, + const ColosseumState* s +) { + if (corpus->count >= BENCH_CORPUS_STATES) return; + corpus->states[corpus->count++] = *s; +} + +static void bench_add_wave_rollout( + ColoBenchCorpus* corpus, + int start_wave, + uint32_t seed, + uint64_t action_seed +) { + ColosseumContext ctx; + ColosseumState s; + int actions[COLO_NUM_ACTION_HEADS]; + uint64_t rng = action_seed; + bench_init_context(&ctx, start_wave); + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); + for (int step = 0; step < 160 && corpus->count < BENCH_CORPUS_STATES; step++) { + bench_capture_state(corpus, &s); + bench_trace_actions(&s, &rng, actions); + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + if (s.episode_over) { + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed + (uint32_t)step + 1u); + } + } +} + +static void bench_prepare_custom( + ColosseumState* s, + ColosseumContext* ctx, + uint32_t seed, + int player_x, + int player_y +) { + bench_init_context(ctx, 1); + memset(s, 0, sizeof(*s)); + col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); + memset(s->npcs, 0, sizeof(s->npcs)); + memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); + memset(s->totems, 0, sizeof(s->totems)); + memset(s->bees, 0, sizeof(s->bees)); + s->modifiers.draft_pending = 0; + s->modifiers.draft_gates_spawn = 0; + s->wave_ready_delay = 0; + s->wave_spawn_delay = 0; + s->reinforcement_timer = COLO_REINFORCE_FIRED; + s->warband_cycle_anchor = s->tick; + s->player.x = player_x; + s->player.y = player_y; + col_rebuild_player_collision_flags(s); + col_refresh_current_obs_slots_ctx(s, ctx); +} + +static void bench_add_custom_dense(ColoBenchCorpus* corpus) { + ColosseumState s; + ColosseumContext ctx; + bench_prepare_custom(&s, &ctx, 0xC010D375u, 17, 16); + int slot = col_spawn_npc_at(&s, COLO_MANTICORE, 16, 20); + ColoManticoreState* mc = colo_npc_manticore(&s.npcs[slot]); + mc->cycle_step = 1; + mc->orb_style[0] = ATTACK_STYLE_MAGIC; + mc->orb_style[1] = ATTACK_STYLE_RANGED; + mc->orb_style[2] = ATTACK_STYLE_MELEE; + s.npcs[slot].attack_timer = 0; + slot = col_spawn_npc_at(&s, COLO_JAVELIN_COLOSSUS, 21, 17); + s.npcs[slot].attack_timer = 0; + colo_npc_javelin(&s.npcs[slot])->attack_count = 4; + slot = col_spawn_npc_at(&s, COLO_SERPENT_SHAMAN, 12, 16); + s.npcs[slot].attack_timer = 0; + slot = col_spawn_npc_at(&s, COLO_SHOCKWAVE_COLOSSUS, 19, 12); + s.npcs[slot].attack_timer = 0; + slot = col_spawn_npc_at(&s, COLO_MINOTAUR, 14, 16); + s.npcs[slot].attack_timer = 0; + slot = col_spawn_npc_at(&s, COLO_JAGUAR_WARRIOR, 17, 19); + s.npcs[slot].attack_timer = 0; + slot = col_spawn_npc_at(&s, COLO_FREMENNIK_BERSERKER, 17, 17); + colo_npc_warband(&s.npcs[slot])->formation_dir = COLO_WARBAND_FORM_NORTH; + slot = col_spawn_npc_at(&s, COLO_FREMENNIK_ARCHER, 18, 17); + colo_npc_warband(&s.npcs[slot])->formation_dir = COLO_WARBAND_FORM_EAST; + col_rebuild_player_collision_flags(&s); + col_refresh_current_obs_slots_ctx(&s, &ctx); + bench_capture_state(corpus, &s); +} + +static void bench_build_corpus(ColoBenchCorpus* corpus) { + memset(corpus, 0, sizeof(*corpus)); + bench_add_custom_dense(corpus); + bench_add_wave_rollout(corpus, 0, 0xC010001u, 0xA1001u); + bench_add_wave_rollout(corpus, 3, 0xC010004u, 0xA1004u); + bench_add_wave_rollout(corpus, 7, 0xC010008u, 0xA1008u); + bench_add_wave_rollout(corpus, 11, 0xC010012u, 0xA1012u); + if (corpus->count <= 0) abort(); +} + +static void bench_encode_step_out_forecast_obs( + const ColoStepOutForecast* forecast, + int horizon, + float out[COLO_STEP_OUT_FORECAST_OBS_SIZE] +) { + int i = 0; + for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { + const ColoStepOutForecastAction* action = &forecast->actions[action_idx]; + int first_attack_tick = 0; + int first_style_mask = 0; + int max_hit = 0; + int ranged_magic_same_tick = 0; + for (int tick_idx = 0; tick_idx < horizon; tick_idx++) { + const ColoStepOutForecastTick* tick = &action->ticks[tick_idx]; + int style_mask = col_step_out_forecast_tick_style_mask(tick); + if (first_attack_tick == 0 && + col_step_out_forecast_tick_has_event(tick)) { + first_attack_tick = tick_idx + 1; + first_style_mask = style_mask; + } + if (tick->max_hit > max_hit) max_hit = tick->max_hit; + if (tick->ranged_count > 0 && tick->magic_count > 0) + ranged_magic_same_tick = 1; + } + out[i++] = action->valid ? 1.0f : 0.0f; + out[i++] = (float)first_attack_tick / (float)horizon; + out[i++] = (float)first_style_mask / 7.0f; + out[i++] = (float)max_hit / 150.0f; + out[i++] = action->same_tick_mixed_style_conflict ? 1.0f : 0.0f; + out[i++] = ranged_magic_same_tick ? 1.0f : 0.0f; + out[i++] = action->ranged_magic_offtick_opportunity ? 1.0f : 0.0f; + out[i++] = action->melee_fallback_exposure ? 1.0f : 0.0f; + } + assert(i == COLO_STEP_OUT_FORECAST_OBS_SIZE); +} + +static void bench_profiled_tick( + const ColosseumState* s, + ColoForecastNpcLocal npcs[COLO_MAX_NPCS], + uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT], + ColoStepOutForecastAction* action, + ColoForecastObsSummary* summary, + int tick_idx, + const int slots[COLO_MAX_NPCS], + int slot_count, + const ColoForecastPrecomp* pre, + ColoForecastBenchProfile* profile +) { + for (int slot_idx = 0; slot_idx < slot_count; slot_idx++) { + int i = slots[slot_idx]; + ColoForecastNpcLocal* npc = &npcs[i]; + uint64_t start = now_ns(); + if (npc->stun_timer > 0) npc->stun_timer--; + if (npc->frozen_ticks > 0) npc->frozen_ticks--; + if (npc->type == COLO_SOL_HEREDIT) { + if (npc->sol_immobile_ticks > 0) npc->sol_immobile_ticks--; + if (npc->sol_attack_delay > 0) npc->sol_attack_delay--; + } + bench_add_ns(profile, COLO_BENCH_TICK_ADMIN, start); + + start = now_ns(); + ColoForecastMoveResult move_result = + col_forecast_local_move_npc( + s, npcs, npc_flags, i, action, pre->sol_clamp_active); + bench_add_ns(profile, COLO_BENCH_MOVEMENT, start); + + start = now_ns(); + col_forecast_local_attack_npc( + s, npcs, slots, slot_count, i, action, summary, tick_idx, + &move_result, pre); + bench_add_ns(profile, COLO_BENCH_ATTACK_AI, start); + profile->npc_tick_visits++; + } +} + +static void bench_profiled_forecast_build( + const ColosseumState* s, + ColoStepOutForecast* out, + ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS], + int horizon, + int run_tile_mode, + ColoForecastBenchProfile* profile +) { + if (horizon < 1) horizon = 1; + if (horizon > COLO_STEP_OUT_FORECAST_HORIZON) + horizon = COLO_STEP_OUT_FORECAST_HORIZON; + + uint64_t total_start = now_ns(); + uint64_t start = now_ns(); + memset(out, 0, sizeof(*out)); + if (summaries) + memset(summaries, 0, sizeof(ColoForecastObsSummary) * ENCOUNTER_MOVE_ACTIONS); + bench_add_ns(profile, COLO_BENCH_SETUP_CLEAR, start); + + int forecast_slots[COLO_MAX_NPCS]; + start = now_ns(); + int forecast_slot_count = col_collect_step_out_forecast_slots(s, forecast_slots); + bench_add_ns(profile, COLO_BENCH_SETUP_COLLECT, start); + profile->slot_count_sum += (uint64_t)forecast_slot_count; + + ColoForecastNpcLocal base_npcs[COLO_MAX_NPCS]; + uint8_t base_npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; + ColoForecastPrecomp pre = {0}; + if (forecast_slot_count > 0) { + start = now_ns(); + col_forecast_local_copy_npc_slots( + s, base_npcs, forecast_slots, forecast_slot_count); + bench_add_ns(profile, COLO_BENCH_SETUP_BASE_COPY, start); + + start = now_ns(); + col_forecast_local_rebuild_npc_flags( + base_npcs, base_npc_flags, forecast_slots, forecast_slot_count); + bench_add_ns(profile, COLO_BENCH_SETUP_FLAGS, start); + + start = now_ns(); + pre = col_forecast_precompute( + s, base_npcs, forecast_slots, forecast_slot_count); + bench_add_ns(profile, COLO_BENCH_SETUP_PRECOMP, start); + } + + uint64_t fanout_start = now_ns(); + for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { + profile->actions_seen++; + start = now_ns(); + int unique_action = col_step_out_forecast_prepare_unique_action_ctx( + s, out, summaries, action_idx, forecast_slot_count); + bench_add_ns(profile, COLO_BENCH_ACTION_PREP, start); + if (!unique_action) { + profile->invalid_or_duplicate_actions++; + continue; + } + profile->valid_actions++; + ColoStepOutForecastAction* action = &out->actions[action_idx]; + ColoForecastObsSummary* summary = summaries ? &summaries[action_idx] : NULL; + if (run_tile_mode == COLO_FORECAST_RUN_TILE_STATIC_THREAT && + col_forecast_action_is_run_tile(action_idx)) { + col_forecast_static_threat_action( + s, base_npcs, forecast_slots, forecast_slot_count, action, summary); + profile->static_threat_actions++; + start = now_ns(); + col_step_out_forecast_finalize_action(action, horizon); + bench_add_ns(profile, COLO_BENCH_FINALIZE, start); + continue; + } + + ColoForecastNpcLocal npcs[COLO_MAX_NPCS]; + uint8_t npc_flags[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT]; + start = now_ns(); + col_forecast_local_copy_slot_set( + npcs, base_npcs, forecast_slots, forecast_slot_count); + bench_add_ns(profile, COLO_BENCH_RESTORE_NPCS, start); + + start = now_ns(); + memcpy(npc_flags, base_npc_flags, sizeof(base_npc_flags)); + bench_add_ns(profile, COLO_BENCH_RESTORE_FLAGS, start); + + profile->rollout_actions++; + for (int tick_idx = 0; tick_idx < horizon; tick_idx++) { + bench_profiled_tick( + s, npcs, npc_flags, action, summary, tick_idx, + forecast_slots, forecast_slot_count, &pre, profile); + profile->rollout_ticks++; + } + + start = now_ns(); + col_step_out_forecast_finalize_action(action, horizon); + bench_add_ns(profile, COLO_BENCH_FINALIZE, start); + } + profile->fanout_ns += now_ns() - fanout_start; + profile->total_ns += now_ns() - total_start; + profile->forecasts++; +} + +static void bench_profiled_forecast_obs( + const ColosseumState* s, + float out[COLO_STEP_OUT_FORECAST_OBS_SIZE], + ColoForecastBenchProfile* profile +) { + ColoStepOutForecast forecast; + ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS]; + bench_profiled_forecast_build( + s, &forecast, summaries, COLO_STEP_OUT_FORECAST_HORIZON, + COLO_FORECAST_RUN_TILE_FULL, profile); + uint64_t start = now_ns(); + col_write_step_out_forecast_obs_summary( + &forecast, summaries, COLO_STEP_OUT_FORECAST_HORIZON, out, 0); + profile->forecast_obs_checksum = bench_hash_bytes( + profile->forecast_obs_checksum, out, sizeof(float) * COLO_STEP_OUT_FORECAST_OBS_SIZE); + bench_add_ns(profile, COLO_BENCH_OBS_SCORE, start); +} + +static void bench_raw_forecast_obs( + const ColosseumState* s, + float out[COLO_STEP_OUT_FORECAST_OBS_SIZE] +) { + ColoStepOutForecast forecast; + ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS]; + col_build_step_out_forecast_horizon_mode_summary( + s, &forecast, summaries, COLO_STEP_OUT_FORECAST_HORIZON, + COLO_FORECAST_RUN_TILE_FULL); + col_write_step_out_forecast_obs_summary( + &forecast, summaries, COLO_STEP_OUT_FORECAST_HORIZON, out, 0); + bench_sink += out[0]; +} + +static void bench_check_profiled_exact(const ColoBenchCorpus* corpus) { + float obs_a[COLO_STEP_OUT_FORECAST_OBS_SIZE]; + float obs_b[COLO_STEP_OUT_FORECAST_OBS_SIZE]; + for (int i = 0; i < corpus->count; i++) { + ColoStepOutForecast expected; + ColoStepOutForecast actual; + ColoForecastBenchProfile profile = {0}; + col_build_step_out_forecast_horizon_mode( + &corpus->states[i], &expected, COLO_STEP_OUT_FORECAST_HORIZON, + COLO_FORECAST_RUN_TILE_FULL); + bench_profiled_forecast_build( + &corpus->states[i], &actual, NULL, COLO_STEP_OUT_FORECAST_HORIZON, + COLO_FORECAST_RUN_TILE_FULL, &profile); + if (memcmp(&expected, &actual, sizeof(expected)) != 0) { + fprintf(stderr, "profiled forecast mismatch on corpus state %d\n", i); + abort(); + } + bench_encode_step_out_forecast_obs( + &expected, COLO_STEP_OUT_FORECAST_HORIZON, obs_a); + bench_encode_step_out_forecast_obs( + &actual, COLO_STEP_OUT_FORECAST_HORIZON, obs_b); + if (memcmp(obs_a, obs_b, sizeof(obs_a)) != 0) { + fprintf(stderr, "profiled obs mismatch on corpus state %d\n", i); + abort(); + } + } +} + +static double bench_raw_forecast_seconds( + const ColoBenchCorpus* corpus, + int iters +) { + float obs[COLO_STEP_OUT_FORECAST_OBS_SIZE]; + uint64_t start = now_ns(); + for (int i = 0; i < iters; i++) { + bench_raw_forecast_obs(&corpus->states[i % corpus->count], obs); + } + return (double)(now_ns() - start) / 1000000000.0; +} + +static double bench_state_copy_seconds( + const ColoBenchCorpus* corpus, + int iters +) { + uint64_t start = now_ns(); + for (int i = 0; i < iters; i++) { + ColosseumState s = corpus->states[i % corpus->count]; + bench_sink += (double)s.player.x; + } + return (double)(now_ns() - start) / 1000000000.0; +} + +static double bench_env_step_seconds( + const ColoBenchCorpus* corpus, + const ColosseumContext* template_ctx, + int iters +) { + float obs[COLO_NUM_OBS + COLO_ACTION_MASK_SIZE]; + int actions[COLO_NUM_ACTION_HEADS] = {0}; + uint64_t start = now_ns(); + for (int i = 0; i < iters; i++) { + ColosseumState s = corpus->states[i % corpus->count]; + ColosseumContext ctx = *template_ctx; + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs + COLO_NUM_OBS); + bench_sink += obs[0]; + bench_sink += col_get_reward_ctx((EncounterState*)&s, (EncounterContext*)&ctx); + bench_sink += col_is_terminal_ctx((EncounterState*)&s, (EncounterContext*)&ctx); + } + return (double)(now_ns() - start) / 1000000000.0; +} + +static void bench_collect_profile( + const ColoBenchCorpus* corpus, + int iters, + ColoForecastBenchProfile* profile +) { + float obs[COLO_STEP_OUT_FORECAST_OBS_SIZE]; + memset(profile, 0, sizeof(*profile)); + for (int i = 0; i < iters; i++) { + bench_profiled_forecast_obs(&corpus->states[i % corpus->count], obs, profile); + } +} + +static uint64_t bench_profile_exclusive_sum(const ColoForecastBenchProfile* profile) { + uint64_t sum = 0; + for (int i = 0; i < COLO_BENCH_COUNT; i++) sum += profile->ns[i]; + return sum; +} + +static void bench_print_profile( + const ColoForecastBenchProfile* profile, + double raw_us_per_forecast, + double env_us_per_step, + double env_no_forecast_us_per_step +) { + double measured_us = (double)profile->total_ns / (double)profile->forecasts / 1000.0; + double obs_score_us = + (double)profile->ns[COLO_BENCH_OBS_SCORE] / (double)profile->forecasts / 1000.0; + uint64_t exclusive_sum = bench_profile_exclusive_sum(profile); + uint64_t unattributed_ns = + profile->total_ns + profile->ns[COLO_BENCH_OBS_SCORE] > exclusive_sum + ? profile->total_ns + profile->ns[COLO_BENCH_OBS_SCORE] - exclusive_sum + : 0; + double total_for_pct_us = measured_us + obs_score_us; + + printf("corpus_forecasts=%llu actions=%llu valid=%llu rollout_actions=%llu skipped=%llu avg_slots=%.2f\n", + (unsigned long long)profile->forecasts, + (unsigned long long)profile->actions_seen, + (unsigned long long)profile->valid_actions, + (unsigned long long)profile->rollout_actions, + (unsigned long long)profile->invalid_or_duplicate_actions, + (double)profile->slot_count_sum / (double)profile->forecasts); + double accounted_us = (double)exclusive_sum / (double)profile->forecasts / 1000.0; + double forecast_delta_us = env_us_per_step - env_no_forecast_us_per_step; + + printf("rollout_ticks=%llu npc_tick_visits=%llu fanout_us=%.3f measured_forecast_plus_obs_us=%.3f raw_forecast_plus_obs_us=%.3f env_step_us=%.3f env_step_no_forecast_us=%.3f forecast_delta_us=%.3f forecast_pct_of_env_step=%.2f delta_pct_of_env_step=%.2f\n", + (unsigned long long)profile->rollout_ticks, + (unsigned long long)profile->npc_tick_visits, + (double)profile->fanout_ns / (double)profile->forecasts / 1000.0, + total_for_pct_us, + raw_us_per_forecast, + env_us_per_step, + env_no_forecast_us_per_step, + forecast_delta_us, + raw_us_per_forecast * 100.0 / env_us_per_step, + forecast_delta_us * 100.0 / env_us_per_step); + printf("internal bucket breakdown, accounted-normalized and raw-scaled:\n"); + for (int i = 0; i < COLO_BENCH_COUNT; i++) { + double us = (double)profile->ns[i] / (double)profile->forecasts / 1000.0; + double pct = accounted_us > 0.0 ? us * 100.0 / accounted_us : 0.0; + printf(" %-24s %9.3f measured_us %9.3f raw_scaled_us %6.2f%%\n", + COLO_BENCH_BUCKET_NAMES[i], us, raw_us_per_forecast * pct / 100.0, pct); + } + printf("timer_unattributed_loop_overhead %.3f us\n", + (double)unattributed_ns / (double)profile->forecasts / 1000.0); + printf("forecast_obs_checksum=%llu bench_sink=%.3f\n", + (unsigned long long)profile->forecast_obs_checksum, bench_sink); +} + +int main(void) { + ColosseumContext ctx; + ColosseumContext no_forecast_ctx; + ColoForecastBenchProfile profile; + ColoBenchCorpus* corpus = calloc(1, sizeof(*corpus)); + if (!corpus) abort(); + bench_init_context(&ctx, 0); + bench_init_context(&no_forecast_ctx, 0); + no_forecast_ctx.config.step_out_forecast_obs_enabled = 0; + bench_build_corpus(corpus); + bench_check_profiled_exact(corpus); + + int raw_iters = 50000; + int env_iters = 20000; + int profiled_iters = 3000; + double raw_seconds = bench_raw_forecast_seconds(corpus, raw_iters); + double copy_seconds = bench_state_copy_seconds(corpus, env_iters); + double env_seconds = bench_env_step_seconds(corpus, &ctx, env_iters); + double env_no_forecast_seconds = + bench_env_step_seconds(corpus, &no_forecast_ctx, env_iters); + bench_collect_profile(corpus, profiled_iters, &profile); + + double raw_us = raw_seconds * 1000000.0 / (double)raw_iters; + double copy_us = copy_seconds * 1000000.0 / (double)env_iters; + double env_us = env_seconds * 1000000.0 / (double)env_iters - copy_us; + double env_no_forecast_us = + env_no_forecast_seconds * 1000000.0 / (double)env_iters - copy_us; + + printf("sizeof(ColosseumState)=%zu COLO_NUM_OBS=%d COLO_STEP_OUT_FORECAST_OBS_SIZE=%d actions=%d horizon=%d corpus_states=%d\n", + sizeof(ColosseumState), + COLO_NUM_OBS, + COLO_STEP_OUT_FORECAST_OBS_SIZE, + ENCOUNTER_MOVE_ACTIONS, + COLO_STEP_OUT_FORECAST_HORIZON, + corpus->count); + printf("raw_iters=%d env_iters=%d profiled_iters=%d state_copy_us=%.3f\n", + raw_iters, env_iters, profiled_iters, copy_us); + bench_print_profile(&profile, raw_us, env_us, env_no_forecast_us); + free(corpus); + return 0; +} diff --git a/ocean/osrs/tests/bench_inferno_forecast.c b/ocean/osrs/tests/bench_inferno_forecast.c new file mode 100644 index 0000000000..c507dac97a --- /dev/null +++ b/ocean/osrs/tests/bench_inferno_forecast.c @@ -0,0 +1,346 @@ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_inferno.h" + +static volatile double bench_sink = 0.0; + +static double now_seconds(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec / 1000000000.0; +} + +static void init_bench_state(InfernoState* state, int player_x, int player_y) { + inf_legacy_context()->config = inf_default_config(); + inf_legacy_context()->config.step_out_forecast_obs_enabled = 1; + inf_legacy_context()->config.step_out_forecast_obs_mode = + INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT; + inf_build_npc_stats(); + memset(state, 0, sizeof(*state)); + memset(state->npc_los_cache, -1, sizeof(state->npc_los_cache)); + state->rng_state = 20260516u; + state->wave = 63; + state->player.entity_type = ENTITY_PLAYER; + state->player.x = player_x; + state->player.y = player_y; + state->player.base_hitpoints = 99; + state->player.current_hitpoints = 99; + state->player.current_defence = 99; + state->player.current_magic = 99; + state->player.current_prayer = 99; + state->player_last_interaction_target_slot = -1; + state->player_last_interaction_age = 1; + state->player_dest_x = -1; + state->player_dest_y = -1; + state->weapon_set = INF_GEAR_LONG_RANGE; + osrs_interaction_init(&state->interaction); + for (int p = 0; p < INF_NUM_PILLARS; p++) { + state->pillars[p].x = INF_PILLAR_POS[p][0]; + state->pillars[p].y = INF_PILLAR_POS[p][1]; + state->pillars[p].hp = INF_PILLAR_HP; + state->pillars[p].active = 1; + } + inf_rebuild_los(state); + inf_rebuild_player_collision_flags(state); +} + +static void add_bench_npc( + InfernoState* state, int slot, InfNPCType type, int x, int y, int timer +) { + inf_init_npc(state, slot, type, x, y); + state->npcs[slot].attack_timer = timer; + state->npcs[slot].stun_timer = 0; + state->npcs[slot].frozen_ticks = 0; +} + +static void init_empty_state(InfernoState* state) { + init_bench_state(state, 29, 39); +} + +static void init_pillar_stack_state(InfernoState* state) { + init_bench_state(state, 29, 39); + add_bench_npc(state, 0, INF_NPC_RANGER, 24, 31, 0); + add_bench_npc(state, 1, INF_NPC_MAGER, 29, 30, 0); + inf_rebuild_entity_collision_flags(state); +} + +static void init_dense_wave_state(InfernoState* state) { + init_bench_state(state, 29, 39); + add_bench_npc(state, 0, INF_NPC_NIBBLER, 15, 39, 0); + add_bench_npc(state, 1, INF_NPC_NIBBLER, 16, 39, 1); + add_bench_npc(state, 2, INF_NPC_NIBBLER, 17, 39, 2); + add_bench_npc(state, 3, INF_NPC_MAGER, 24, 31, 0); + add_bench_npc(state, 4, INF_NPC_RANGER, 29, 30, 0); + add_bench_npc(state, 5, INF_NPC_MELEER, 23, 34, 0); + add_bench_npc(state, 6, INF_NPC_BLOB, 20, 32, 1); + add_bench_npc(state, 7, INF_NPC_BLOB, 26, 35, 2); + add_bench_npc(state, 8, INF_NPC_BAT, 18, 30, 3); + add_bench_npc(state, 9, INF_NPC_BAT, 19, 30, 4); + inf_rebuild_entity_collision_flags(state); +} + +static void init_pillar_stack_no_forecast_state(InfernoState* state) { + init_pillar_stack_state(state); + inf_legacy_context()->config.step_out_forecast_obs_enabled = 0; + inf_legacy_context()->config.step_out_forecast_obs_mode = + INF_STEP_OUT_FORECAST_MODE_OFF; +} + +static void init_dense_wave_no_forecast_state(InfernoState* state) { + init_dense_wave_state(state); + inf_legacy_context()->config.step_out_forecast_obs_enabled = 0; + inf_legacy_context()->config.step_out_forecast_obs_mode = + INF_STEP_OUT_FORECAST_MODE_OFF; +} + +typedef void (*BenchInit)(InfernoState*); +typedef void (*BenchFn)(InfernoState*, float*); +typedef void (*FixedBenchFn)(const InfernoState*, float*); +typedef void (*ForecastBuilder)( + const InfernoState*, const InfernoContext*, InfStepOutForecast*); + +static void bench_forecast_exact(InfernoState* state, float* obs) { + (void)obs; + InfStepOutForecast forecast; + inf_build_step_out_forecast_exact_ctx(state, inf_legacy_context(), &forecast); + bench_sink += forecast.actions[0].valid; + bench_sink += forecast.actions[ENCOUNTER_MOVE_ACTIONS - 1].ticks[0].max_hit; +} + +static void bench_forecast_fast_static(InfernoState* state, float* obs) { + (void)obs; + InfStepOutForecast forecast; + inf_build_step_out_forecast_fast_static_ctx( + state, inf_legacy_context(), &forecast); + bench_sink += forecast.actions[0].valid; + bench_sink += forecast.actions[ENCOUNTER_MOVE_ACTIONS - 1].ticks[0].max_hit; +} + +static void bench_forecast_fast_readonly(InfernoState* state, float* obs) { + (void)obs; + InfStepOutForecast forecast; + inf_build_step_out_forecast_fast_readonly_ctx( + state, inf_legacy_context(), &forecast); + bench_sink += forecast.actions[0].valid; + bench_sink += forecast.actions[ENCOUNTER_MOVE_ACTIONS - 1].ticks[0].max_hit; +} + +static void bench_obs(InfernoState* state, float* obs) { + inf_write_obs((EncounterState*)state, obs); + bench_sink += obs[0]; + bench_sink += obs[INF_NUM_OBS - 1]; +} + +static void bench_mask(InfernoState* state, float* obs) { + inf_write_mask((EncounterState*)state, obs); + bench_sink += obs[0]; + bench_sink += obs[INF_ACTION_MASK_SIZE - 1]; +} + +static void bench_copy_fixed(const InfernoState* template, float* obs) { + (void)obs; + InfernoState state; + memcpy(&state, template, sizeof(state)); + bench_sink += state.player.x; +} + +static void bench_step_fixed(const InfernoState* template, float* obs) { + (void)obs; + InfernoState state; + memcpy(&state, template, sizeof(state)); + int actions[INF_NUM_ACTION_HEADS] = {0}; + inf_step((EncounterState*)&state, actions); + bench_sink += state.player.current_hitpoints; +} + +static void bench_step_obs_mask_fixed(const InfernoState* template, float* obs) { + InfernoState state; + memcpy(&state, template, sizeof(state)); + int actions[INF_NUM_ACTION_HEADS] = {0}; + inf_step((EncounterState*)&state, actions); + inf_write_obs((EncounterState*)&state, obs); + inf_write_mask((EncounterState*)&state, obs + INF_NUM_OBS); + bench_sink += state.player.current_hitpoints; + bench_sink += obs[0]; +} + +static void run_bench(const char* label, BenchInit init, BenchFn fn, int iters) { + InfernoState state; + float obs[INF_NUM_OBS + INF_ACTION_MASK_SIZE]; + init(&state); + fn(&state, obs); + double start = now_seconds(); + for (int i = 0; i < iters; i++) { + fn(&state, obs); + } + double elapsed = now_seconds() - start; + printf("%-24s %9d calls %9.3f ms %9.3f us/call\n", + label, iters, elapsed * 1000.0, elapsed * 1000000.0 / (double)iters); +} + +static void run_fixed_bench(const char* label, BenchInit init, FixedBenchFn fn, int iters) { + InfernoState template; + float obs[INF_NUM_OBS + INF_ACTION_MASK_SIZE]; + init(&template); + fn(&template, obs); + double start = now_seconds(); + for (int i = 0; i < iters; i++) { + fn(&template, obs); + } + double elapsed = now_seconds() - start; + printf("%-24s %9d calls %9.3f ms %9.3f us/call\n", + label, iters, elapsed * 1000.0, elapsed * 1000000.0 / (double)iters); +} + +static void report_forecast_diff( + const char* label, + BenchInit init, + ForecastBuilder fast_builder +) { + InfernoState state; + init(&state); + InfStepOutForecast exact; + InfStepOutForecast fast; + InfStepOutForecastOracleDiff diff; + inf_build_step_out_forecast_exact_ctx(&state, inf_legacy_context(), &exact); + fast_builder(&state, inf_legacy_context(), &fast); + inf_compare_step_out_forecasts(&exact, &fast, &diff); + double fn_rate = diff.exact_dangerous_actions > 0 ? + (double)diff.dangerous_false_negatives / + (double)diff.exact_dangerous_actions : 0.0; + int exact_safe_actions = diff.sampled_actions - diff.exact_dangerous_actions; + double fp_rate = exact_safe_actions > 0 ? + (double)diff.dangerous_false_positives / + (double)exact_safe_actions : 0.0; + printf("%-24s actions=%d action_mismatch=%d tick_mismatch=%d dangerous_fn=%d dangerous_fp=%d fn_rate=%.4f fp_rate=%.4f exact_danger=%d fast_danger=%d max_hit_err_sum=%d max_hit_err_max=%d\n", + label, + diff.sampled_actions, + diff.action_feature_mismatches, + diff.tick_feature_mismatches, + diff.dangerous_false_negatives, + diff.dangerous_false_positives, + fn_rate, + fp_rate, + diff.exact_dangerous_actions, + diff.fast_dangerous_actions, + diff.max_hit_abs_error_sum, + diff.max_hit_abs_error_max); +} + +static void add_forecast_diff( + InfStepOutForecastOracleDiff* total, + const InfStepOutForecastOracleDiff* diff +) { + total->action_feature_mismatches += diff->action_feature_mismatches; + total->tick_feature_mismatches += diff->tick_feature_mismatches; + total->dangerous_false_negatives += diff->dangerous_false_negatives; + total->dangerous_false_positives += diff->dangerous_false_positives; + total->exact_safe_fast_dangerous += diff->exact_safe_fast_dangerous; + total->exact_dangerous_actions += diff->exact_dangerous_actions; + total->fast_dangerous_actions += diff->fast_dangerous_actions; + total->sampled_actions += diff->sampled_actions; + total->max_hit_abs_error_sum += diff->max_hit_abs_error_sum; + if (diff->max_hit_abs_error_max > total->max_hit_abs_error_max) + total->max_hit_abs_error_max = diff->max_hit_abs_error_max; +} + +static void report_sampled_forecast_diff( + const char* label, + BenchInit init, + ForecastBuilder fast_builder, + int samples +) { + InfernoState state; + init(&state); + InfStepOutForecastOracleDiff total = {0}; + int resets = 0; + for (int sample = 0; sample < samples; sample++) { + InfStepOutForecast exact; + InfStepOutForecast fast; + InfStepOutForecastOracleDiff diff; + inf_build_step_out_forecast_exact_ctx(&state, inf_legacy_context(), &exact); + fast_builder(&state, inf_legacy_context(), &fast); + inf_compare_step_out_forecasts(&exact, &fast, &diff); + add_forecast_diff(&total, &diff); + + int actions[INF_NUM_ACTION_HEADS] = {0}; + actions[INF_HEAD_MOVE] = sample % ENCOUNTER_MOVE_ACTIONS; + inf_step((EncounterState*)&state, actions); + if (state.episode_over) { + init(&state); + resets++; + } + } + + double fn_rate = total.exact_dangerous_actions > 0 ? + (double)total.dangerous_false_negatives / + (double)total.exact_dangerous_actions : 0.0; + int exact_safe_actions = total.sampled_actions - total.exact_dangerous_actions; + double fp_rate = exact_safe_actions > 0 ? + (double)total.dangerous_false_positives / + (double)exact_safe_actions : 0.0; + printf("%-24s samples=%d sampled_actions=%d resets=%d action_mismatch=%d tick_mismatch=%d dangerous_fn=%d dangerous_fp=%d fn_rate=%.4f fp_rate=%.4f exact_danger=%d fast_danger=%d max_hit_err_sum=%d max_hit_err_max=%d\n", + label, + samples, + total.sampled_actions, + resets, + total.action_feature_mismatches, + total.tick_feature_mismatches, + total.dangerous_false_negatives, + total.dangerous_false_positives, + fn_rate, + fp_rate, + total.exact_dangerous_actions, + total.fast_dangerous_actions, + total.max_hit_abs_error_sum, + total.max_hit_abs_error_max); +} + +int main(void) { + printf("sizeof(InfernoState) = %zu\n", sizeof(InfernoState)); + printf("INF_NUM_OBS = %d\n", INF_NUM_OBS); + printf("INF_STEP_OUT_FORECAST_OBS_SIZE = %d\n", INF_STEP_OUT_FORECAST_OBS_SIZE); + run_bench("empty exact", init_empty_state, bench_forecast_exact, 200000); + run_bench("empty static", init_empty_state, bench_forecast_fast_static, 200000); + run_bench("empty readonly", init_empty_state, bench_forecast_fast_readonly, 200000); + run_bench("empty obs", init_empty_state, bench_obs, 200000); + run_bench("empty mask", init_empty_state, bench_mask, 200000); + report_forecast_diff("empty exact vs static", + init_empty_state, inf_build_step_out_forecast_fast_static_ctx); + report_forecast_diff("empty exact vs readonly", + init_empty_state, inf_build_step_out_forecast_fast_readonly_ctx); + run_bench("stack exact", init_pillar_stack_state, bench_forecast_exact, 100000); + run_bench("stack static", init_pillar_stack_state, bench_forecast_fast_static, 100000); + run_bench("stack readonly", init_pillar_stack_state, bench_forecast_fast_readonly, 100000); + run_bench("stack obs", init_pillar_stack_state, bench_obs, 100000); + run_bench("stack obs no forecast", init_pillar_stack_no_forecast_state, bench_obs, 100000); + run_bench("stack mask", init_pillar_stack_state, bench_mask, 100000); + report_forecast_diff("stack exact vs static", + init_pillar_stack_state, inf_build_step_out_forecast_fast_static_ctx); + report_forecast_diff("stack exact vs readonly", + init_pillar_stack_state, inf_build_step_out_forecast_fast_readonly_ctx); + run_bench("dense exact", init_dense_wave_state, bench_forecast_exact, 50000); + run_bench("dense static", init_dense_wave_state, bench_forecast_fast_static, 50000); + run_bench("dense readonly", init_dense_wave_state, bench_forecast_fast_readonly, 50000); + run_bench("dense obs", init_dense_wave_state, bench_obs, 50000); + run_bench("dense obs no forecast", init_dense_wave_no_forecast_state, bench_obs, 50000); + run_bench("dense mask", init_dense_wave_state, bench_mask, 50000); + report_forecast_diff("dense exact vs static", + init_dense_wave_state, inf_build_step_out_forecast_fast_static_ctx); + report_forecast_diff("dense exact vs readonly", + init_dense_wave_state, inf_build_step_out_forecast_fast_readonly_ctx); + run_fixed_bench("dense copy fixed", init_dense_wave_state, bench_copy_fixed, 50000); + run_fixed_bench("dense step fixed", init_dense_wave_state, bench_step_fixed, 50000); + run_fixed_bench("dense step+obs+mask", init_dense_wave_state, bench_step_obs_mask_fixed, 50000); + run_fixed_bench("step+obs+mask no fc", init_dense_wave_no_forecast_state, bench_step_obs_mask_fixed, 50000); + report_sampled_forecast_diff("dense sampled static", + init_dense_wave_state, inf_build_step_out_forecast_fast_static_ctx, 256); + report_sampled_forecast_diff("dense sampled readonly", + init_dense_wave_state, inf_build_step_out_forecast_fast_readonly_ctx, 256); + printf("bench_sink = %.3f\n", bench_sink); + return 0; +} diff --git a/ocean/osrs/tests/inferno_lab_cli.c b/ocean/osrs/tests/inferno_lab_cli.c new file mode 100644 index 0000000000..09bc6a8931 --- /dev/null +++ b/ocean/osrs/tests/inferno_lab_cli.c @@ -0,0 +1,86 @@ +#include +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_inferno.h" + +static char* read_script_line(FILE* file) { + size_t cap = 256; + size_t len = 0; + char* line = (char*)malloc(cap); + if (!line) { + fprintf(stderr, "inferno lab: out of memory\n"); + abort(); + } + + int ch; + while ((ch = fgetc(file)) != EOF) { + if (len + 2 > cap) { + if (cap > SIZE_MAX / 2) { + fprintf(stderr, "inferno lab: script line too large\n"); + abort(); + } + cap *= 2; + char* next = (char*)realloc(line, cap); + if (!next) { + fprintf(stderr, "inferno lab: out of memory\n"); + abort(); + } + line = next; + } + line[len++] = (char)ch; + if (ch == '\n') break; + } + + if (len == 0 && ch == EOF) { + free(line); + return NULL; + } + line[len] = '\0'; + return line; +} + +static void run_script(FILE* file, InfernoState* state) { + for (;;) { + char* line = read_script_line(file); + if (!line) break; + + char* dump = NULL; + InfLabLineResult result = inf_lab_apply_script_line_alloc_json( + state, line, &dump); + if (result == INF_LAB_LINE_DUMP) { + printf("%s\n", dump); + free(dump); + } + free(line); + } +} + +int main(int argc, char** argv) { + InfernoState* state = (InfernoState*)inf_create(); + inf_put_float((EncounterState*)state, "late_start_supply_profile_scale", 1.0f); + inf_reset((EncounterState*)state, 20260515u); + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_CLEAR_NPCS, + }); + + if (argc == 1) { + run_script(stdin, state); + } else if (argc == 2) { + FILE* file = fopen(argv[1], "r"); + if (!file) { + fprintf(stderr, "inferno lab: cannot open %s\n", argv[1]); + abort(); + } + run_script(file, state); + fclose(file); + } else { + fprintf(stderr, "usage: inferno_lab [script]\n"); + inf_destroy((EncounterState*)state); + return 2; + } + + inf_destroy((EncounterState*)state); + return 0; +} diff --git a/ocean/osrs/tests/osrs_test_check.h b/ocean/osrs/tests/osrs_test_check.h new file mode 100644 index 0000000000..d002bfe467 --- /dev/null +++ b/ocean/osrs/tests/osrs_test_check.h @@ -0,0 +1,50 @@ +#ifndef OSRS_TEST_CHECK_H +#define OSRS_TEST_CHECK_H + +#include + +static int tests_run = 0; +static int tests_passed = 0; +static int tests_failed = 0; + +#define CHECK(label, cond) do { \ + tests_run++; \ + if (cond) { tests_passed++; } \ + else { tests_failed++; printf(" FAIL: %s\n", (label)); } \ +} while (0) + +#define ASSERT_INT_EQ(label, actual, expected) do { \ + tests_run++; \ + if ((actual) == (expected)) { \ + tests_passed++; \ + } else { \ + tests_failed++; \ + printf(" FAIL: %s - got %d, expected %d\n", \ + (label), (actual), (expected)); \ + } \ +} while (0) + +#define ASSERT_FLOAT_NEAR(label, actual, expected, tol) do { \ + tests_run++; \ + float diff = (float)((actual) - (expected)); \ + if (diff < 0.0f) diff = -diff; \ + if (diff <= (tol)) { \ + tests_passed++; \ + } else { \ + tests_failed++; \ + printf(" FAIL: %s - got %.6f, expected %.6f (tol %.6f)\n", \ + (label), (float)(actual), (float)(expected), (float)(tol)); \ + } \ +} while (0) + +static int osrs_test_summary(void) { + printf("\n%d/%d tests passed", tests_passed, tests_run); + if (tests_failed > 0) { + printf(" (%d failed)\n", tests_failed); + return 1; + } + printf("\n"); + return 0; +} + +#endif diff --git a/ocean/osrs/tests/probe_colo_best_gear_dpt.c b/ocean/osrs/tests/probe_colo_best_gear_dpt.c new file mode 100644 index 0000000000..7d8690d14f --- /dev/null +++ b/ocean/osrs/tests/probe_colo_best_gear_dpt.c @@ -0,0 +1,451 @@ +#include +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +#include "ocean/osrs/tests/osrs_test_check.h" + +static void loadout_reset(ColosseumState* s, ColosseumContext* ctx, int mode, + float frac, uint32_t seed) { + col_init_context_typed(ctx); + ctx->config.loadout_profile_mode = mode; + ctx->config.beginner_loadout_fraction = frac; + memset(s, 0, sizeof(*s)); + col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); +} + +static int setup_contains(const uint8_t setup[NUM_GEAR_SLOTS], uint8_t item) { + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + if (setup[slot] == item) return 1; + return 0; +} + +static void test_argmax_setup_and_style(void) { + printf("test_argmax_setup_and_style (T1, T3)\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 771); + col_build_npc_stats(); + s.player.current_attack = 118; + s.player.current_strength = 118; + s.player.current_ranged = 112; + s.player.current_magic = 99; + col_mark_live_loadout_dirty(&s); + + ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; + col_build_best_gear_table(&s, best); + + const ColoBestGear* bm = &best[COLO_GEAR_MAGIC][COLO_FREMENNIK_BERSERKER]; + CHECK("T1 magic best weapon is Tumeken's shadow", + bm->setup[GEAR_SLOT_WEAPON] == ITEM_TUMEKENS_SHADOW); + CHECK("T1 magic best setup includes Occult necklace", + setup_contains(bm->setup, ITEM_OCCULT_NECKLACE)); + CHECK("T1 magic best setup includes Confliction gauntlets", + setup_contains(bm->setup, ITEM_CONFLICTION_GAUNTLETS)); + CHECK("T1 magic best setup includes Avernic treads", + setup_contains(bm->setup, ITEM_AVERNIC_TREADS)); + + const ColoBestGear* jm = &best[COLO_GEAR_MAGIC][COLO_JAGUAR_WARRIOR]; + ColoNPC jnpc = (ColoNPC){ + .type = COLO_JAGUAR_WARRIOR, + .hp = COLO_NPC_STATS[COLO_JAGUAR_WARRIOR].hp, + .max_hp = COLO_NPC_STATS[COLO_JAGUAR_WARRIOR].hp, + .size = COLO_NPC_STATS[COLO_JAGUAR_WARRIOR].size, + .active = 1, .death_ticks = 0, + }; + float ref = col_expected_dpt_for_equipment_vs_npc(&s, jm->setup, &jnpc, 1); + CHECK("T1 jaguar magic argmax DPT reproducible through the leaf (accuracy-weighted)", + fabsf(ref - jm->dpt) < 1e-3f); + CHECK("T1 jaguar magic argmax setup is the shadow kit", + jm->setup[GEAR_SLOT_WEAPON] == ITEM_TUMEKENS_SHADOW && + setup_contains(jm->setup, ITEM_OCCULT_NECKLACE) && + setup_contains(jm->setup, ITEM_CONFLICTION_GAUNTLETS)); + + struct { ColoNpcType type; int want_style; const char* name; } spec[] = { + { COLO_FREMENNIK_BERSERKER, COLO_GEAR_MAGIC, "berserker -> magic" }, + { COLO_FREMENNIK_ARCHER, COLO_GEAR_MELEE, "archer -> melee" }, + { COLO_FREMENNIK_SEER, COLO_GEAR_RANGED, "seer -> ranged" }, + { COLO_SERPENT_SHAMAN, COLO_GEAR_RANGED, "serpent -> ranged" }, + }; + for (int k = 0; k < (int)(sizeof(spec) / sizeof(spec[0])); k++) { + int argmax = -1; + float best_dpt = -1.0f; + for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) + if (best[style][spec[k].type].dpt > best_dpt) { + best_dpt = best[style][spec[k].type].dpt; + argmax = style; + } + char label[96]; + snprintf(label, sizeof(label), "T3 argmax style: %s", spec[k].name); + CHECK(label, argmax == spec[k].want_style); + } +} + +static void test_beats_worn_single_swap(void) { + printf("test_beats_worn_single_swap (T2)\n"); + int modes[] = { COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, + COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY }; + for (int m = 0; m < 2; m++) { + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, modes[m], 0.0f, 401 + m); + col_build_npc_stats(); + s.player.current_attack = 118; + s.player.current_strength = 118; + s.player.current_ranged = 112; + s.player.current_magic = 99; + col_mark_live_loadout_dirty(&s); + + ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; + col_build_best_gear_table(&s, best); + + uint8_t weapons[64]; + int nweap = 0; + uint8_t add_seen[256] = {0}; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + uint8_t w = s.player.equipped[slot]; + if (w != ITEM_NONE && item_is_weapon(w) && !add_seen[w]) { + add_seen[w] = 1; weapons[nweap++] = w; + } + } + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { + uint8_t w = s.inventory_cells[cell].item_idx; + if (w != ITEM_NONE && item_is_weapon(w) && !add_seen[w]) { + add_seen[w] = 1; weapons[nweap++] = w; + } + } + + int violations = 0; + for (int wi = 0; wi < nweap; wi++) { + uint8_t w = weapons[wi]; + int wstyle = get_item_attack_style(w); + if (wstyle < 1 || wstyle > 3) continue; + ColoWeaponSet style_set = (ColoWeaponSet)(wstyle - 1); + uint8_t worn[NUM_GEAR_SLOTS]; + memcpy(worn, s.player.equipped, NUM_GEAR_SLOTS); + worn[GEAR_SLOT_WEAPON] = w; + if (item_is_two_handed(w)) worn[GEAR_SLOT_SHIELD] = ITEM_NONE; + for (int type = 0; type < COLO_NUM_NPC_TYPES; type++) { + if (col_type_is_hazard_entity((ColoNpcType)type)) continue; + ColoNPC npc = (ColoNPC){ + .type = (ColoNpcType)type, + .hp = COLO_NPC_STATS[type].hp, + .max_hp = COLO_NPC_STATS[type].hp, + .size = COLO_NPC_STATS[type].size, + .active = 1, .death_ticks = 0, + }; + float worn_dpt = col_expected_dpt_for_equipment_vs_npc(&s, worn, &npc, 1); + if (best[style_set][type].dpt < worn_dpt - 1e-3f) violations++; + } + } + char label[96]; + snprintf(label, sizeof(label), "T2 best-gear >= worn single-swap (profile %d)", m); + CHECK(label, violations == 0); + } +} + +static void test_equip_and_augury_same_tick(void) { + printf("test_equip_and_augury_same_tick (T4)\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 99); + col_build_npc_stats(); + + int shadow_cell = -1; + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) + if (s.inventory_cells[cell].item_idx == ITEM_TUMEKENS_SHADOW) { shadow_cell = cell; break; } + CHECK("T4 shadow is in the bag", shadow_cell >= 0); + if (shadow_cell < 0) return; + + CHECK("T4 starts on a non-magic weapon", + col_equipped_weapon_attack_style(&s.player) != ATTACK_STYLE_MAGIC); + s.player.current_prayer = 99; + + static float mask[COLO_ACTION_MASK_SIZE]; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + int off_base = -1; + { + int offset = 0; + for (int h = 0; h < COLO_HEAD_OFFENSIVE; h++) offset += COLO_ACTION_DIMS[h]; + off_base = offset; + } + int augury_bit = off_base + 4; + CHECK("T4 mask allows Augury on a non-magic weapon (points-only)", + mask[augury_bit] == 1.0f); + + int act[COLO_NUM_ACTION_HEADS] = {0}; + act[COLO_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = shadow_cell + 1; + act[COLO_HEAD_OFFENSIVE] = 4; + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, act); + + CHECK("T4 weapon equipped to shadow on the swap tick", + s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_TUMEKENS_SHADOW); + CHECK("T4 offensive prayer is Augury after the swap tick", + s.player.offensive_prayer == OFFENSIVE_PRAYER_AUGURY); + + const EncounterLoadoutStats* live = col_live_loadout_stats(&s); + EncounterLoadoutStats no_aug; + int cur_magic = s.player.current_magic; + encounter_compute_loadout_stats( + s.player.equipped, ATTACK_STYLE_MAGIC, OFFENSIVE_PRAYER_NONE, + cur_magic, FIGHT_STYLE_ACCURATE, + col_weapon_set_spell_base_damage(&s, COLO_GEAR_MAGIC), &no_aug); + encounter_update_loadout_level(&no_aug, OFFENSIVE_PRAYER_NONE, cur_magic, cur_magic); + CHECK("T4 live stats are the magic style (shadow equipped)", + live->style == ATTACK_STYLE_MAGIC); + CHECK("T4 Augury raises effective level over no-prayer", + live->eff_level > no_aug.eff_level); +} + +static void test_per_cell_marginal_bit(void) { + printf("test_per_cell_marginal_bit (T5)\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 55); + col_build_npc_stats(); + s.player.current_attack = 118; + s.player.current_strength = 118; + s.player.current_ranged = 112; + s.player.current_magic = 99; + col_mark_live_loadout_dirty(&s); + + memset(s.npcs, 0, sizeof(s.npcs)); + s.npcs[0] = (ColoNPC){ + .type = COLO_FREMENNIK_BERSERKER, + .hp = COLO_NPC_STATS[COLO_FREMENNIK_BERSERKER].hp, + .max_hp = COLO_NPC_STATS[COLO_FREMENNIK_BERSERKER].hp, + .size = COLO_NPC_STATS[COLO_FREMENNIK_BERSERKER].size, + .x = 18, .y = 18, .active = 1, .death_ticks = 0, + }; + s.player.x = 18; s.player.y = 20; + osrs_interaction_set(&s.interaction, 0); + + ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; + col_build_best_gear_table(&s, best); + + int magic_was_argmax = -1; + { + float bd = -1.0f; + for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) + if (best[style][COLO_FREMENNIK_BERSERKER].dpt > bd) { + bd = best[style][COLO_FREMENNIK_BERSERKER].dpt; + magic_was_argmax = style; + } + } + CHECK("T5 magic is the argmax with shadow present", magic_was_argmax == COLO_GEAR_MAGIC); + float magic_dpt_with = best[COLO_GEAR_MAGIC][COLO_FREMENNIK_BERSERKER].dpt; + + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) + if (s.inventory_cells[cell].item_idx == ITEM_TUMEKENS_SHADOW) + s.inventory_cells[cell].item_idx = ITEM_NONE; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + if (s.player.equipped[slot] == ITEM_TUMEKENS_SHADOW) + s.player.equipped[slot] = ITEM_NONE; + + ColoBestGear best2[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; + col_build_best_gear_table(&s, best2); + float magic_dpt_without = best2[COLO_GEAR_MAGIC][COLO_FREMENNIK_BERSERKER].dpt; + CHECK("T5 removing shadow drops the magic best DPT", + magic_dpt_without < magic_dpt_with - 1e-3f); + int argmax_without = -1; + { + float bd = -1.0f; + for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) + if (best2[style][COLO_FREMENNIK_BERSERKER].dpt > bd) { + bd = best2[style][COLO_FREMENNIK_BERSERKER].dpt; + argmax_without = style; + } + } + CHECK("T5 removing shadow flips the berserker argmax away from magic", + argmax_without != COLO_GEAR_MAGIC); +} + +static void test_memo_result_preserving(void) { + printf("test_memo_result_preserving (T6)\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 313); + col_build_npc_stats(); + s.player.current_attack = 118; + s.player.current_strength = 118; + s.player.current_ranged = 112; + s.player.current_magic = 99; + col_mark_live_loadout_dirty(&s); + + ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; + col_build_best_gear_table(&s, best); + + int mismatches = 0; + for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) { + for (int type = 0; type < COLO_NUM_NPC_TYPES; type++) { + if (col_type_is_hazard_entity((ColoNpcType)type)) continue; + const ColoBestGear* bg = &best[style][type]; + if (bg->dpt < 0.0f) continue; + ColoNPC npc = (ColoNPC){ + .type = (ColoNpcType)type, + .hp = COLO_NPC_STATS[type].hp, + .max_hp = COLO_NPC_STATS[type].hp, + .size = COLO_NPC_STATS[type].size, + .active = 1, .death_ticks = 0, + }; + float direct = col_expected_dpt_for_equipment_vs_npc(&s, bg->setup, &npc, 1); + if (direct != bg->dpt) mismatches++; + } + } + CHECK("T6 memoized oracle DPT bit-identical to un-memoized leaf", mismatches == 0); +} + +static void test_confliction_reference(void) { + printf("test_confliction_reference (T7)\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 717); + col_build_npc_stats(); + s.player.current_magic = 99; + col_mark_live_loadout_dirty(&s); + + ColoNPC npc = (ColoNPC){ + .type = COLO_JAGUAR_WARRIOR, + .hp = COLO_NPC_STATS[COLO_JAGUAR_WARRIOR].hp, + .max_hp = COLO_NPC_STATS[COLO_JAGUAR_WARRIOR].hp, + .size = COLO_NPC_STATS[COLO_JAGUAR_WARRIOR].size, + .active = 1, .death_ticks = 0, + }; + + uint8_t setup_1h[NUM_GEAR_SLOTS]; + memset(setup_1h, ITEM_NONE, NUM_GEAR_SLOTS); + setup_1h[GEAR_SLOT_WEAPON] = ITEM_TRIDENT_OF_SWAMP; + setup_1h[GEAR_SLOT_HANDS] = ITEM_CONFLICTION_GAUNTLETS; + setup_1h[GEAR_SLOT_NECK] = ITEM_OCCULT_NECKLACE; + + uint8_t setup_1h_nocon[NUM_GEAR_SLOTS]; + memcpy(setup_1h_nocon, setup_1h, NUM_GEAR_SLOTS); + setup_1h_nocon[GEAR_SLOT_HANDS] = ITEM_NONE; + + float dpt_con = col_expected_dpt_for_equipment_vs_npc(&s, setup_1h, &npc, 1); + float dpt_nocon = col_expected_dpt_for_equipment_vs_npc(&s, setup_1h_nocon, &npc, 1); + + EncounterLoadoutStats stats; + encounter_compute_loadout_stats( + setup_1h, ATTACK_STYLE_MAGIC, OFFENSIVE_PRAYER_AUGURY, 99, FIGHT_STYLE_ACCURATE, + col_weapon_set_spell_base_damage(&s, COLO_GEAR_MAGIC), &stats); + encounter_update_loadout_level(&stats, OFFENSIVE_PRAYER_AUGURY, 99, 99); + OsrsEquipmentEffectProfile effects; + encounter_derive_loadout_effect_profile(setup_1h, &effects); + CHECK("T7 confliction applies for a 1h magic weapon", + osrs_confliction_can_apply(&effects, ATTACK_STYLE_MAGIC, ITEM_TRIDENT_OF_SWAMP, 1)); + + int att_roll = osrs_player_att_roll(stats.eff_level, stats.attack_bonus); + const ColoNpcStats* ns = &COLO_NPC_STATS[COLO_JAGUAR_WARRIOR]; + int def_roll = col_npc_target_def_roll( + &npc, ns, ATTACK_STYLE_MAGIC, MELEE_STYLE_STAB); + float single = osrs_hit_chance(att_roll, def_roll); + float dbl = osrs_hit_chance_double(att_roll, def_roll); + float ref_hit = dbl / (1.0f + dbl - single); + float ref_dpt = ref_hit * (0.0f + (float)stats.max_hit) * 0.5f / (float)stats.attack_speed; + CHECK("T7 confliction DPT matches the reference steady-state formula", + fabsf(dpt_con - ref_dpt) < 1e-2f); + CHECK("T7 confliction (charged double-acc) beats no-confliction single roll", + dpt_con > dpt_nocon - 1e-6f && ref_hit > single); + + uint8_t setup_2h[NUM_GEAR_SLOTS]; + memset(setup_2h, ITEM_NONE, NUM_GEAR_SLOTS); + setup_2h[GEAR_SLOT_WEAPON] = ITEM_TUMEKENS_SHADOW; + setup_2h[GEAR_SLOT_HANDS] = ITEM_CONFLICTION_GAUNTLETS; + OsrsEquipmentEffectProfile effects_2h; + encounter_derive_loadout_effect_profile(setup_2h, &effects_2h); + CHECK("T7 confliction DISABLED with a 2h weapon (shadow)", + !osrs_confliction_can_apply(&effects_2h, ATTACK_STYLE_MAGIC, ITEM_TUMEKENS_SHADOW, 1)); +} + +static void test_best_is_locally_optimal(void) { + printf("test_best_is_locally_optimal (T8)\n"); + int modes[] = { COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, + COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY }; + int violations = 0; + for (int m = 0; m < 2; m++) { + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, modes[m], 0.0f, 808 + m); + col_build_npc_stats(); + s.player.current_attack = 118; + s.player.current_strength = 118; + s.player.current_ranged = 112; + s.player.current_magic = 99; + col_mark_live_loadout_dirty(&s); + ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; + col_build_best_gear_table(&s, best); + + for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) { + for (int type = 0; type < COLO_NUM_NPC_TYPES; type++) { + if (col_type_is_hazard_entity((ColoNpcType)type)) continue; + const ColoBestGear* bg = &best[style][type]; + if (bg->dpt < 0.0f) continue; + ColoNPC npc = col_matchup_representative_npc((ColoNpcType)type); + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + if (slot == GEAR_SLOT_WEAPON) continue; + if (bg->setup[slot] == ITEM_NONE) continue; + uint8_t variant[NUM_GEAR_SLOTS]; + memcpy(variant, bg->setup, NUM_GEAR_SLOTS); + variant[slot] = ITEM_NONE; + float v = col_expected_dpt_for_equipment_vs_npc(&s, variant, &npc, 1); + if (v > bg->dpt + 1e-4f) violations++; + } + } + } + } + CHECK("T8 argmax setup is locally optimal under single-slot empties", violations == 0); +} + +static void calibrate_norm(void) { + printf("NORM calibration sweep (max best-gear DPT over profile x weapon x type)\n"); + int modes[] = { COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, + COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY }; + const char* names[] = { "speedrun", "beginner" }; + float global_max = 0.0f; + for (int m = 0; m < 2; m++) { + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, modes[m], 0.0f, 1000 + m); + col_build_npc_stats(); + s.player.current_attack = 118; + s.player.current_strength = 118; + s.player.current_ranged = 112; + s.player.current_magic = 99; + col_mark_live_loadout_dirty(&s); + ColoBestGear best[COLO_NUM_WEAPON_SETS][COLO_NUM_NPC_TYPES]; + col_build_best_gear_table(&s, best); + float pmax = 0.0f; + int pmax_style = -1, pmax_type = -1; + for (int style = 0; style < COLO_NUM_WEAPON_SETS; style++) + for (int type = 0; type < COLO_NUM_NPC_TYPES; type++) + if (best[style][type].dpt > pmax) { + pmax = best[style][type].dpt; + pmax_style = style; pmax_type = type; + } + printf(" %-9s max best-gear DPT = %.3f (style %d, type %s)\n", + names[m], pmax, pmax_style, colo_npc_type_name(pmax_type)); + if (pmax > global_max) global_max = pmax; + } + printf(" GLOBAL max best-gear DPT = %.3f\n", global_max); + printf(" NORM for top setup at ~0.9 -> %.1f ; current COLO_EXPECTED_DPT_NORM = %.1f\n", + global_max / 0.9f, (double)COLO_EXPECTED_DPT_NORM); +} + +int main(int argc, char** argv) { + if (argc > 1 && strcmp(argv[1], "--calibrate") == 0) { + calibrate_norm(); + return 0; + } + printf("colosseum best-gear DPT oracle probe\n\n"); + test_argmax_setup_and_style(); + test_beats_worn_single_swap(); + test_equip_and_augury_same_tick(); + test_per_cell_marginal_bit(); + test_memo_result_preserving(); + test_confliction_reference(); + test_best_is_locally_optimal(); + return osrs_test_summary(); +} diff --git a/ocean/osrs/tests/probe_colo_bis_gear_prayer.c b/ocean/osrs/tests/probe_colo_bis_gear_prayer.c new file mode 100644 index 0000000000..7ceea62b81 --- /dev/null +++ b/ocean/osrs/tests/probe_colo_bis_gear_prayer.c @@ -0,0 +1,49 @@ +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +static const OffensivePrayer STALE_PRAYERS[3] = { + OFFENSIVE_PRAYER_PIETY, OFFENSIVE_PRAYER_RIGOUR, OFFENSIVE_PRAYER_AUGURY +}; + +int main(void) { + int checks = 0, fails = 0; + for (uint32_t i = 1; i <= 60; i++) { + uint32_t seed = i * 0x9E3779B1u; + ColosseumContext ctx; + ColosseumState s; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + ctx.config.bis_gear_oracle_mode = 1; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); + + int target = -1; + for (int n = 0; n < COLO_MAX_NPCS; n++) { + if (col_npc_is_live_enemy(&s.npcs[n])) { target = n; break; } + } + if (target < 0) continue; + osrs_interaction_set(&s.interaction, target); + + for (int p = 0; p < 3; p++) { + s.player.offensive_prayer = STALE_PRAYERS[p]; + col_apply_bis_gear_oracle(&s); + OffensivePrayer expected = encounter_offensive_prayer_for_style( + col_weapon_set_attack_style(s.weapon_set)); + checks++; + if (s.player.offensive_prayer != expected) { + fails++; + printf("seed=%u stale=%d set=%d style=%d off=%d expected=%d\n", + seed, STALE_PRAYERS[p], (int)s.weapon_set, + (int)col_weapon_set_attack_style(s.weapon_set), + (int)s.player.offensive_prayer, (int)expected); + } + } + } + printf("checked %d oracle commits, %d mismatched prayers\n", checks, fails); + if (checks > 0 && fails == 0) { printf("PASS\n"); return 0; } + printf("FAIL\n"); + return 1; +} diff --git a/ocean/osrs/tests/probe_colo_damage_scale.c b/ocean/osrs/tests/probe_colo_damage_scale.c new file mode 100644 index 0000000000..ccb4f90ac0 --- /dev/null +++ b/ocean/osrs/tests/probe_colo_damage_scale.c @@ -0,0 +1,118 @@ +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +static int failures = 0; + +static void check(const char* name, int got, int want) { + if (got != want) { + printf("FAIL %s: got %d want %d\n", name, got, want); + failures++; + } else { + printf("ok %s: %d\n", name, got); + } +} + +static int direct_hp_loss(uint32_t seed, int dmg, float scale) { + static ColosseumContext ctx; + static ColosseumState s; + col_init_context_typed(&ctx); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); + s.active_player_damage_received_scale = scale; + int hp0 = s.player.current_hitpoints; + col_damage_player_from(&s, dmg, COLO_MANTICORE, COLO_DMG_UNPRAYABLE); + return hp0 - s.player.current_hitpoints; +} + +static int queued_hp_loss(uint32_t seed, int dmg, float scale) { + static ColosseumContext ctx; + static ColosseumState s; + col_init_context_typed(&ctx); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); + s.active_player_damage_received_scale = scale; + int hp0 = s.player.current_hitpoints; + int prayed = 0; + + s.player.prayer = PRAYER_NONE; + EncounterPendingHit hit = encounter_pending_hit_resolved_at_throw( + dmg, 1, ATTACK_STYLE_RANGED, s.player.prayer, COLO_SERPENT_SHAMAN, 0, 1, &prayed); + int landed_raw = hit.damage; + col_push_player_pending_hit(&s, hit); + + s.tick++; + col_resolve_player_pending_hits(&s); + (void)landed_raw; + return hp0 - s.player.current_hitpoints; +} + +static int doom_stacks_after_melee(uint32_t seed, int dmg, float scale) { + static ColosseumContext ctx; + static ColosseumState s; + col_init_context_typed(&ctx); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); + s.active_player_damage_received_scale = scale; + s.modifiers.active_mask |= (1u << COLO_MOD_DOOM); + s.modifiers.tier[COLO_MOD_DOOM] = 1; + s.player.prayer = PRAYER_NONE; + s.doom_stacks = 0; + col_apply_instant_melee_hit(&s, &ctx, 0, COLO_JAGUAR_WARRIOR, dmg, 1); + return s.doom_stacks; +} + +static int round_half_up(int dmg, float scale) { + if (scale >= 1.0f) return dmg; + if (scale <= 0.0f) return 0; + int v = (int)((float)dmg * scale + 0.5f); + return v < 0 ? 0 : v; +} + +int main(void) { + const uint32_t SEED = 0xC0DEu; + const int DMG = 20; + + int d_full = direct_hp_loss(SEED, DMG, 1.0f); + int d_half = direct_hp_loss(SEED, DMG, 0.5f); + int d_zero = direct_hp_loss(SEED, DMG, 0.0f); + check("direct scale=1.0 == raw dmg", d_full, DMG); + check("direct scale=0.5 == round_half_up", d_half, round_half_up(DMG, 0.5f)); + check("direct scale=0.0 == 0 (invuln)", d_zero, 0); + + int q_full = queued_hp_loss(SEED, DMG, 1.0f); + int q_half = queued_hp_loss(SEED, DMG, 0.5f); + int q_zero = queued_hp_loss(SEED, DMG, 0.0f); + check("queued scale=1.0 landed > 0 (sanity)", q_full > 0 ? 1 : 0, 1); + check("queued scale=0.5 == round_half_up(landed)", q_half, round_half_up(q_full, 0.5f)); + check("queued scale=0.0 == 0 (invuln)", q_zero, 0); + + int doom_full = doom_stacks_after_melee(SEED, DMG, 1.0f); + int doom_zero = doom_stacks_after_melee(SEED, DMG, 0.0f); + check("doom scale=1.0 accrues one stack", doom_full, 1); + check("doom scale=0.0 accrues NO stack (invuln-equivalent)", doom_zero, 0); + + int identity_ok = 1; + for (int dmg = 0; dmg <= 255; dmg++) { + ColosseumState s; + memset(&s, 0, sizeof(s)); + s.active_player_damage_received_scale = 1.0f; + if (col_scale_incoming_damage(&s, dmg) != dmg) { identity_ok = 0; break; } + } + check("scale=1.0 identity over dmg[0,255]", identity_ok, 1); + + int zero_ok = 1; + for (int dmg = 0; dmg <= 255; dmg++) { + ColosseumState s; + memset(&s, 0, sizeof(s)); + s.active_player_damage_received_scale = 0.0f; + if (col_scale_incoming_damage(&s, dmg) != 0) { zero_ok = 0; break; } + } + check("scale=0.0 zeros dmg[0,255]", zero_ok, 1); + + if (failures) { + printf("\n%d FAILURE(S)\n", failures); + return 1; + } + printf("\nALL PASS\n"); + return 0; +} diff --git a/ocean/osrs/tests/probe_colo_prayer_switch_slip.c b/ocean/osrs/tests/probe_colo_prayer_switch_slip.c new file mode 100644 index 0000000000..a450d17460 --- /dev/null +++ b/ocean/osrs/tests/probe_colo_prayer_switch_slip.c @@ -0,0 +1,45 @@ +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +static void prep(ColosseumContext* ctx, ColosseumState* s, float fail_prob) { + col_init_context_typed(ctx); + memset(s, 0, sizeof(*s)); + s->rng_state = 0xC0FFEEu; + + ENCOUNTER_COLOSSEUM.put_float((EncounterState*)s, (EncounterContext*)ctx, + "prayer_switch_fail_prob", fail_prob); + s->player.prayer = PRAYER_PROTECT_RANGED; + s->player.current_prayer = 990; + s->player.prayer_just_activated = 0; +} + +int main(void) { + int actions[COLO_NUM_ACTION_HEADS] = {0}; + actions[COLO_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC; + + ColosseumContext ctx_off; ColosseumState s_off; + prep(&ctx_off, &s_off, 0.0f); + col_player_pretick(&s_off, &ctx_off, actions); + int applied = (s_off.player.prayer == PRAYER_PROTECT_MAGIC); + + ColosseumContext ctx_on; ColosseumState s_on; + prep(&ctx_on, &s_on, 1.0f); + col_player_pretick(&s_on, &ctx_on, actions); + int reverted = (s_on.player.prayer == PRAYER_PROTECT_RANGED); + int flag_clear = (s_on.player.prayer_just_activated == 0); + + printf("p=0.0: prayer=%d (expect MAGIC=%d) applied=%d\n", + s_off.player.prayer, PRAYER_PROTECT_MAGIC, applied); + printf("p=1.0: prayer=%d (expect RANGED=%d) reverted=%d just_activated=%d\n", + s_on.player.prayer, PRAYER_PROTECT_RANGED, reverted, + s_on.player.prayer_just_activated); + if (applied && reverted && flag_clear) { + printf("PASS\n"); + return 0; + } + printf("FAIL\n"); + return 1; +} diff --git a/ocean/osrs/tests/probe_colo_remove_brews.c b/ocean/osrs/tests/probe_colo_remove_brews.c new file mode 100644 index 0000000000..cc9adf3d0f --- /dev/null +++ b/ocean/osrs/tests/probe_colo_remove_brews.c @@ -0,0 +1,42 @@ +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +static int reset_brew_doses(int remove_brews, uint32_t seed) { + ColosseumContext ctx; + ColosseumState s; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + ctx.config.loadout_profile_mode = 2; + ctx.config.beginner_loadout_fraction = 0.5f; + memset(&s, 0, sizeof(s)); + + ENCOUNTER_COLOSSEUM.put_int((EncounterState*)&s, (EncounterContext*)&ctx, + "remove_brews", remove_brews); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); + return s.player.brew_doses; +} + +int main(void) { + int off_min = 1 << 30, off_max = -1, on_min = 1 << 30, on_max = -1; + for (uint32_t i = 1; i <= 60; i++) { + uint32_t seed = i * 0x9E3779B1u; + int off = reset_brew_doses(0, seed); + int on = reset_brew_doses(1, seed); + if (off < off_min) off_min = off; + if (off > off_max) off_max = off; + if (on < on_min) on_min = on; + if (on > on_max) on_max = on; + } + printf("remove_brews=0: brew_doses min=%d max=%d (expect both loadouts, 4 and 24)\n", + off_min, off_max); + printf("remove_brews=1: brew_doses min=%d max=%d (expect 0 and 0)\n", on_min, on_max); + if (on_max == 0 && off_min > 0) { + printf("PASS\n"); + return 0; + } + printf("FAIL\n"); + return 1; +} diff --git a/ocean/osrs/tests/probe_colo_rng_diversity.c b/ocean/osrs/tests/probe_colo_rng_diversity.c new file mode 100644 index 0000000000..7a3a94e795 --- /dev/null +++ b/ocean/osrs/tests/probe_colo_rng_diversity.c @@ -0,0 +1,85 @@ +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +#define FNV_OFFSET 1469598103934665603ULL +#define FNV_PRIME 1099511628211ULL + +static uint32_t lowbias32_ref(uint32_t x) { + x ^= x >> 16; + x *= 0x7feb352dU; + x ^= x >> 15; + x *= 0x846ca68bU; + x ^= x >> 16; + return x; +} + +static uint64_t splitmix64(uint64_t* s) { + uint64_t z = (*s += 0x9E3779B97F4A7C15ULL); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + return z ^ (z >> 31); +} + +static uint64_t spawn_digest(const ColosseumState* s) { + uint64_t h = FNV_OFFSET; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + int32_t v[3] = { (int32_t)s->npcs[i].type, s->npcs[i].x, s->npcs[i].y }; + for (int k = 0; k < 3; k++) { h ^= (uint64_t)(uint32_t)v[k]; h *= FNV_PRIME; } + } + h ^= (uint64_t)(uint32_t)s->wave; h *= FNV_PRIME; + h ^= (uint64_t)s->rng_state; h *= FNV_PRIME; + return h; +} + +static void fill_actions(const ColosseumState* s, uint64_t* rng, int acts[COLO_NUM_ACTION_HEADS]) { + for (int head = 0; head < COLO_NUM_ACTION_HEADS; head++) + acts[head] = (int)(splitmix64(rng) % (uint64_t)COLO_ACTION_DIMS[head]); + if (s->modifiers.draft_pending) { + acts[COLO_HEAD_PRIMARY] = 0; + acts[COLO_HEAD_MODIFIER_SELECT] = 1 + (int)(splitmix64(rng) % COLO_MODIFIER_DRAFT_OPTIONS); + } +} + +static uint64_t reset_env_seed(ColosseumState* s, ColosseumContext* ctx, uint32_t index) { + memset(s, 0, sizeof(*s)); + s->rng_state = lowbias32_ref(index); + col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, 0); + return spawn_digest(s); +} + +int main(void) { + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + + enum { N = 8 }; + uint64_t dig[N]; + ColosseumState s; + for (uint32_t i = 0; i < N; i++) dig[i] = reset_env_seed(&s, &ctx, i); + int distinct = 1; + for (int i = 0; i < N; i++) + for (int j = i + 1; j < N; j++) + if (dig[i] == dig[j]) distinct = 0; + printf("per-env: %d indices, %s\n", N, distinct ? "all distinct" : "COLLISION"); + + uint64_t ep1 = reset_env_seed(&s, &ctx, 0); + uint64_t arng = 0xABCDEF01u; + int acts[COLO_NUM_ACTION_HEADS]; + for (int t = 0; t < 120 && !s.episode_over; t++) { + fill_actions(&s, &arng, acts); + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, acts); + } + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 0); + uint64_t ep2 = spawn_digest(&s); + int episode_varies = (ep1 != ep2); + printf("per-episode: ep1=%016llx ep2=%016llx -> %s\n", + (unsigned long long)ep1, (unsigned long long)ep2, + episode_varies ? "varies" : "IDENTICAL"); + + if (distinct && episode_varies) { printf("PASS\n"); return 0; } + printf("FAIL\n"); + return 1; +} diff --git a/ocean/osrs/tests/probe_colo_skyfall_dodge.c b/ocean/osrs/tests/probe_colo_skyfall_dodge.c new file mode 100644 index 0000000000..c7805fad41 --- /dev/null +++ b/ocean/osrs/tests/probe_colo_skyfall_dodge.c @@ -0,0 +1,562 @@ +#include +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +#define PROBE_NPC_TELLS_OFFSET 37 +#define PROBE_SKYFALL_DAMAGE 38 +#define PROBE_PLAYER_X 17 +#define PROBE_PLAYER_Y 16 +#define PROBE_JAVELIN_X 20 +#define PROBE_JAVELIN_Y 16 + +typedef enum { + PROBE_TARGET_AFTER_MOVE_NONE = 0, + PROBE_TARGET_AFTER_MOVE_SAME_TICK, + PROBE_TARGET_AFTER_MOVE_NEXT_TICK, +} ProbeTargetAfterMoveMode; + +typedef struct { + int visible_tick; + int visible_timer; + int marked_x; + int marked_y; + int obs_slot; + float obs_pending; + float obs_timer; + float obs_dx; + float obs_dy; +} ProbeSkyfallObs; + +typedef struct { + int damage_taken; + int first_visible_tick; + int first_visible_timer; + int move_action; + int move_tick; + int landing_tick; + int player_x_after_move; + int player_y_after_move; + int interaction_after_move; + int player_x_before_landing; + int player_y_before_landing; +} ProbeStepDodgeResult; + +static void probe_fail(const char* label) { + fprintf(stderr, "FAIL %s\n", label); + abort(); +} + +static void probe_check(const char* label, int ok) { + if (!ok) probe_fail(label); + printf("PASS %s\n", label); +} + +static void probe_check_float(const char* label, float got, float expected) { + float delta = fabsf(got - expected); + if (delta > 0.000001f) { + fprintf(stderr, "FAIL %s got=%.9f expected=%.9f delta=%.9f\n", + label, got, expected, delta); + abort(); + } + printf("PASS %s got=%.6f expected=%.6f\n", label, got, expected); +} + +static void probe_clear_npcs(ColosseumState* s) { + memset(s->npcs, 0, sizeof(s->npcs)); + memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); + memset(s->totems, 0, sizeof(s->totems)); + memset(s->bees, 0, sizeof(s->bees)); + col_rebuild_player_collision_flags(s); +} + +static void probe_init_context(ColosseumContext* ctx) { + col_init_context_typed(ctx); + ctx->config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY; + ctx->config.beginner_loadout_fraction = 0.0f; + ctx->config.step_out_forecast_obs_enabled = 1; + ctx->config.action_debug_log = 0; +} + +static void probe_init_empty_state( + ColosseumState* s, + ColosseumContext* ctx, + uint32_t seed +) { + probe_init_context(ctx); + memset(s, 0, sizeof(*s)); + col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); + probe_clear_npcs(s); + s->wave_spawn_delay = 0; + s->wave_ready_delay = 0; + s->modifiers.draft_pending = 0; + s->warband_cycle_anchor = s->tick; + s->player.x = PROBE_PLAYER_X; + s->player.y = PROBE_PLAYER_Y; + s->player.current_hitpoints = 99; + s->player.attack_timer = 99; + s->player_dest_x = -1; + s->player_dest_y = -1; + col_apply_weapon_set(s, COLO_GEAR_RANGED); + encounter_pending_hit_queue_clear(&s->player_pending_hits); + col_rebuild_player_collision_flags(s); +} + +static ColoJavelinState* probe_spawn_javelin(ColosseumState* s) { + col_init_npc(s, 0, COLO_JAVELIN_COLOSSUS, PROBE_JAVELIN_X, PROBE_JAVELIN_Y); + ColoNPC* npc = &s->npcs[0]; + npc->stun_timer = 0; + npc->frozen_ticks = 0; + return colo_npc_javelin(npc); +} + +static int probe_obs_slot_for_npc(const ColosseumState* s, int npc_idx) { + for (int slot = 0; slot < COLO_OBS_NPCS; slot++) { + if (s->current_obs_slots[slot] == npc_idx) return slot; + } + probe_fail("javelin obs slot found"); + return -1; +} + +static float* probe_npc_tells(float* obs, int obs_slot) { + int base = COLO_OBS_AFTER_EQUIPPED_SELF + + obs_slot * COLO_FEATURES_PER_NPC + + PROBE_NPC_TELLS_OFFSET; + return &obs[base]; +} + +static ProbeSkyfallObs probe_read_skyfall_obs( + ColosseumState* s, + ColosseumContext* ctx, + float* obs +) { + col_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); + int obs_slot = probe_obs_slot_for_npc(s, 0); + float* tells = probe_npc_tells(obs, obs_slot); + ColoJavelinState* jv = colo_npc_javelin(&s->npcs[0]); + ProbeSkyfallObs out = { + .visible_tick = s->tick, + .visible_timer = jv->skyfall_timer, + .marked_x = jv->skyfall_tile_x, + .marked_y = jv->skyfall_tile_y, + .obs_slot = obs_slot, + .obs_pending = tells[0], + .obs_timer = tells[1], + .obs_dx = tells[2], + .obs_dy = tells[3], + }; + return out; +} + +static int probe_move_action_ending_off_tile(const ColosseumState* s, int x, int y) { + for (int action = 1; action < ENCOUNTER_MOVE_ACTIONS; action++) { + ColosseumState tmp = *s; + int moved = encounter_move_to_target( + &tmp.player, + ENCOUNTER_MOVE_TARGET_DX[action], + ENCOUNTER_MOVE_TARGET_DY[action], + col_player_walkable, + &tmp); + if (moved > 0 && (tmp.player.x != x || tmp.player.y != y)) return action; + } + probe_fail("move action ending off marked tile found"); + return 0; +} + +static int probe_primary_attack_action_for_slot(int obs_slot) { + if (obs_slot < 0 || obs_slot >= COLO_OBS_NPCS) probe_fail("valid obs target slot"); + return col_primary_attack_action_for_obs_slot(obs_slot); +} + +static void probe_zero_actions(int* actions) { + for (int head = 0; head < COLO_NUM_ACTION_HEADS; head++) actions[head] = 0; +} + +static void probe_step(ColosseumState* s, ColosseumContext* ctx, int* actions) { + col_step_ctx((EncounterState*)s, (EncounterContext*)ctx, actions); +} + +static void probe_establish_attack_lock(ColosseumState* s, ColosseumContext* ctx) { + int actions[COLO_NUM_ACTION_HEADS]; + float obs[COLO_NUM_OBS]; + probe_zero_actions(actions); + col_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); + actions[COLO_HEAD_PRIMARY] = + probe_primary_attack_action_for_slot(probe_obs_slot_for_npc(s, 0)); + probe_step(s, ctx, actions); + probe_check("attack lock active before skyfall fire", + osrs_interaction_active(&s->interaction) && + s->interaction.target_slot == 0); +} + +static ProbeSkyfallObs probe_fire_skyfall_visible( + ColosseumState* s, + ColosseumContext* ctx, + int attack_timer_before_fire +) { + int actions[COLO_NUM_ACTION_HEADS]; + static float obs[COLO_NUM_OBS]; + ColoJavelinState* jv = colo_npc_javelin(&s->npcs[0]); + jv->attack_count = 4; + s->npcs[0].attack_timer = attack_timer_before_fire; + probe_zero_actions(actions); + probe_step(s, ctx, actions); + probe_check("real step fired skyfall", + jv->skyfall_pending == 1 && + jv->skyfall_timer == COLO_JAVELIN_SKYFALL_DELAY && + jv->skyfall_tile_x == s->player.x && + jv->skyfall_tile_y == s->player.y); + int rolled_damage = jv->skyfall_damage; + jv->skyfall_damage = PROBE_SKYFALL_DAMAGE; + ProbeSkyfallObs seen = probe_read_skyfall_obs(s, ctx, obs); + printf( + "FULL_STEP fire tick=%d marked=(%d,%d) timer=%d rolled_damage=%d forced_damage=%d obs_slot=%d obs=(pending %.1f timer %.3f dx %.3f dy %.3f)\n", + seen.visible_tick, + seen.marked_x, + seen.marked_y, + seen.visible_timer, + rolled_damage, + jv->skyfall_damage, + seen.obs_slot, + seen.obs_pending, + seen.obs_timer, + seen.obs_dx, + seen.obs_dy); + return seen; +} + +static ProbeStepDodgeResult probe_run_no_lock_wait_case(int wait_visible_ticks) { + ColosseumContext ctx; + ColosseumState s; + int actions[COLO_NUM_ACTION_HEADS]; + probe_init_empty_state(&s, &ctx, 0x5100u + (uint32_t)wait_visible_ticks); + probe_spawn_javelin(&s); + ProbeSkyfallObs first = probe_fire_skyfall_visible(&s, &ctx, 0); + int move_action = probe_move_action_ending_off_tile(&s, first.marked_x, first.marked_y); + int hp_before = s.player.current_hitpoints; + int move_tick = -1; + int player_x_after_move = s.player.x; + int player_y_after_move = s.player.y; + int interaction_after_move = osrs_interaction_active(&s.interaction) + ? s.interaction.target_slot : -1; + + for (int i = 0; i < wait_visible_ticks; i++) { + probe_zero_actions(actions); + probe_step(&s, &ctx, actions); + } + + if (colo_npc_javelin(&s.npcs[0])->skyfall_pending) { + probe_zero_actions(actions); + actions[COLO_HEAD_PRIMARY] = move_action; + move_tick = s.tick + 1; + probe_step(&s, &ctx, actions); + player_x_after_move = s.player.x; + player_y_after_move = s.player.y; + interaction_after_move = osrs_interaction_active(&s.interaction) + ? s.interaction.target_slot : -1; + } + + int player_x_before_landing = s.player.x; + int player_y_before_landing = s.player.y; + while (colo_npc_javelin(&s.npcs[0])->skyfall_pending) { + player_x_before_landing = s.player.x; + player_y_before_landing = s.player.y; + probe_zero_actions(actions); + probe_step(&s, &ctx, actions); + } + + ProbeStepDodgeResult result = { + .damage_taken = hp_before - s.player.current_hitpoints, + .first_visible_tick = first.visible_tick, + .first_visible_timer = first.visible_timer, + .move_action = move_action, + .move_tick = move_tick, + .landing_tick = s.tick, + .player_x_after_move = player_x_after_move, + .player_y_after_move = player_y_after_move, + .interaction_after_move = interaction_after_move, + .player_x_before_landing = player_x_before_landing, + .player_y_before_landing = player_y_before_landing, + }; + return result; +} + +static ProbeStepDodgeResult probe_run_attack_lock_case( + const char* label, + ProbeTargetAfterMoveMode target_mode +) { + ColosseumContext ctx; + ColosseumState s; + int actions[COLO_NUM_ACTION_HEADS]; + static float obs[COLO_NUM_OBS]; + probe_init_empty_state(&s, &ctx, 0x6200u + (uint32_t)target_mode); + probe_spawn_javelin(&s); + ColoJavelinState* jv = colo_npc_javelin(&s.npcs[0]); + jv->attack_count = 4; + s.npcs[0].attack_timer = 2; + int player_range = col_player_attack_range(&s); + int npc_dist = col_npc_dist_to_player(&s, &s.npcs[0]); + printf( + "LOCK_SETUP %s player_range=%d npc_dist=%d player=(%d,%d) javelin_sw=(%d,%d)\n", + label, + player_range, + npc_dist, + s.player.x, + s.player.y, + s.npcs[0].x, + s.npcs[0].y); + probe_establish_attack_lock(&s, &ctx); + ProbeSkyfallObs first = probe_fire_skyfall_visible(&s, &ctx, 1); + int move_action = probe_move_action_ending_off_tile(&s, first.marked_x, first.marked_y); + int hp_before = s.player.current_hitpoints; + + probe_zero_actions(actions); + actions[COLO_HEAD_PRIMARY] = move_action; + if (target_mode == PROBE_TARGET_AFTER_MOVE_SAME_TICK) { + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + actions[COLO_HEAD_PRIMARY] = + probe_primary_attack_action_for_slot(probe_obs_slot_for_npc(&s, 0)); + } + int move_tick = s.tick + 1; + probe_step(&s, &ctx, actions); + int player_x_after_move = s.player.x; + int player_y_after_move = s.player.y; + int interaction_after_move = osrs_interaction_active(&s.interaction) + ? s.interaction.target_slot : -1; + + if (target_mode == PROBE_TARGET_AFTER_MOVE_NEXT_TICK && + colo_npc_javelin(&s.npcs[0])->skyfall_pending) { + probe_zero_actions(actions); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + actions[COLO_HEAD_PRIMARY] = + probe_primary_attack_action_for_slot(probe_obs_slot_for_npc(&s, 0)); + probe_step(&s, &ctx, actions); + } + + int player_x_before_landing = s.player.x; + int player_y_before_landing = s.player.y; + while (colo_npc_javelin(&s.npcs[0])->skyfall_pending) { + player_x_before_landing = s.player.x; + player_y_before_landing = s.player.y; + probe_zero_actions(actions); + probe_step(&s, &ctx, actions); + } + + ProbeStepDodgeResult result = { + .damage_taken = hp_before - s.player.current_hitpoints, + .first_visible_tick = first.visible_tick, + .first_visible_timer = first.visible_timer, + .move_action = move_action, + .move_tick = move_tick, + .landing_tick = s.tick, + .player_x_after_move = player_x_after_move, + .player_y_after_move = player_y_after_move, + .interaction_after_move = interaction_after_move, + .player_x_before_landing = player_x_before_landing, + .player_y_before_landing = player_y_before_landing, + }; + return result; +} + +static ProbeStepDodgeResult probe_run_attack_lock_idle_case(const char* label) { + ColosseumContext ctx; + ColosseumState s; + int actions[COLO_NUM_ACTION_HEADS]; + probe_init_empty_state(&s, &ctx, 0x6300u); + probe_spawn_javelin(&s); + ColoJavelinState* jv = colo_npc_javelin(&s.npcs[0]); + jv->attack_count = 4; + s.npcs[0].attack_timer = 2; + printf("LOCK_SETUP %s player_range=%d npc_dist=%d\n", + label, + col_player_attack_range(&s), + col_npc_dist_to_player(&s, &s.npcs[0])); + probe_establish_attack_lock(&s, &ctx); + ProbeSkyfallObs first = probe_fire_skyfall_visible(&s, &ctx, 1); + int hp_before = s.player.current_hitpoints; + int player_x_before_landing = s.player.x; + int player_y_before_landing = s.player.y; + + while (colo_npc_javelin(&s.npcs[0])->skyfall_pending) { + player_x_before_landing = s.player.x; + player_y_before_landing = s.player.y; + probe_zero_actions(actions); + probe_step(&s, &ctx, actions); + } + + ProbeStepDodgeResult result = { + .damage_taken = hp_before - s.player.current_hitpoints, + .first_visible_tick = first.visible_tick, + .first_visible_timer = first.visible_timer, + .move_action = 0, + .move_tick = -1, + .landing_tick = s.tick, + .player_x_after_move = s.player.x, + .player_y_after_move = s.player.y, + .interaction_after_move = osrs_interaction_active(&s.interaction) + ? s.interaction.target_slot : -1, + .player_x_before_landing = player_x_before_landing, + .player_y_before_landing = player_y_before_landing, + }; + return result; +} + +static void probe_print_step_result(const char* label, ProbeStepDodgeResult r) { + printf( + "%s first_visible_tick=%d first_timer=%d move_action=%d move_tick=%d landing_tick=%d after_move=(%d,%d) before_landing=(%d,%d) interaction_after_move=%d damage=%d\n", + label, + r.first_visible_tick, + r.first_visible_timer, + r.move_action, + r.move_tick, + r.landing_tick, + r.player_x_after_move, + r.player_y_after_move, + r.player_x_before_landing, + r.player_y_before_landing, + r.interaction_after_move, + r.damage_taken); +} + +static void probe_obs_honesty(void) { + printf("\n== OBS HONESTY ==\n"); + ColosseumContext ctx; + ColosseumState s; + static float obs[COLO_NUM_OBS]; + probe_init_empty_state(&s, &ctx, 0x1001u); + ColoJavelinState* jv = probe_spawn_javelin(&s); + jv->skyfall_pending = 1; + jv->skyfall_tile_x = PROBE_PLAYER_X; + jv->skyfall_tile_y = PROBE_PLAYER_Y; + jv->skyfall_timer = COLO_JAVELIN_SKYFALL_DELAY; + jv->skyfall_damage = PROBE_SKYFALL_DAMAGE; + + const int player_positions[][2] = { + {PROBE_PLAYER_X, PROBE_PLAYER_Y}, + {PROBE_PLAYER_X - 1, PROBE_PLAYER_Y}, + {PROBE_PLAYER_X + 1, PROBE_PLAYER_Y + 1}, + {PROBE_PLAYER_X - 2, PROBE_PLAYER_Y - 2}, + }; + int count = (int)(sizeof(player_positions) / sizeof(player_positions[0])); + for (int i = 0; i < count; i++) { + s.player.x = player_positions[i][0]; + s.player.y = player_positions[i][1]; + col_rebuild_player_collision_flags(&s); + ProbeSkyfallObs seen = probe_read_skyfall_obs(&s, &ctx, obs); + float expected_dx = col_obs_rel_x(jv->skyfall_tile_x, s.player.x); + float expected_dy = col_obs_rel_y(jv->skyfall_tile_y, s.player.y); + printf( + "OBS player=(%d,%d) marked=(%d,%d) raw_delta=(%d,%d) obs_slot=%d pending=%.1f timer=%.3f dx=%.6f dy=%.6f\n", + s.player.x, + s.player.y, + jv->skyfall_tile_x, + jv->skyfall_tile_y, + jv->skyfall_tile_x - s.player.x, + jv->skyfall_tile_y - s.player.y, + seen.obs_slot, + seen.obs_pending, + seen.obs_timer, + seen.obs_dx, + seen.obs_dy); + probe_check_float("skyfall tell dx matches marked tile", seen.obs_dx, expected_dx); + probe_check_float("skyfall tell dy matches marked tile", seen.obs_dy, expected_dy); + } +} + +static void probe_direct_resolve(void) { + printf("\n== SIM DODGE LOGIC ==\n"); + ColosseumContext ctx; + ColosseumState s; + probe_init_empty_state(&s, &ctx, 0x2002u); + ColoJavelinState* jv = probe_spawn_javelin(&s); + jv->skyfall_pending = 1; + jv->skyfall_timer = 1; + jv->skyfall_damage = PROBE_SKYFALL_DAMAGE; + jv->skyfall_tile_x = s.player.x; + jv->skyfall_tile_y = s.player.y; + int hp_before = s.player.current_hitpoints; + col_npc_resolve_javelin_skyfall(&s, &ctx, 0); + printf("DIRECT on_tile hp=%d->%d damage=%d pending=%d\n", + hp_before, + s.player.current_hitpoints, + hp_before - s.player.current_hitpoints, + jv->skyfall_pending); + probe_check("direct on-tile skyfall applies damage", + hp_before - s.player.current_hitpoints == PROBE_SKYFALL_DAMAGE && + jv->skyfall_pending == 0); + + probe_init_empty_state(&s, &ctx, 0x2003u); + jv = probe_spawn_javelin(&s); + jv->skyfall_pending = 1; + jv->skyfall_timer = 1; + jv->skyfall_damage = PROBE_SKYFALL_DAMAGE; + jv->skyfall_tile_x = s.player.x; + jv->skyfall_tile_y = s.player.y; + s.player.x += 1; + col_rebuild_player_collision_flags(&s); + hp_before = s.player.current_hitpoints; + col_npc_resolve_javelin_skyfall(&s, &ctx, 0); + printf("DIRECT off_tile player=(%d,%d) marked=(%d,%d) hp=%d->%d damage=%d pending=%d\n", + s.player.x, + s.player.y, + jv->skyfall_tile_x, + jv->skyfall_tile_y, + hp_before, + s.player.current_hitpoints, + hp_before - s.player.current_hitpoints, + jv->skyfall_pending); + probe_check("direct off-tile skyfall applies zero damage", + hp_before - s.player.current_hitpoints == 0 && + jv->skyfall_pending == 0); +} + +static void probe_full_step_dodgeability(void) { + printf("\n== FULL-STEP DODGEABILITY ==\n"); + ProbeStepDodgeResult wait0 = probe_run_no_lock_wait_case(0); + ProbeStepDodgeResult wait1 = probe_run_no_lock_wait_case(1); + ProbeStepDodgeResult wait2 = probe_run_no_lock_wait_case(2); + probe_print_step_result("FULL_STEP wait0_move_from_timer3", wait0); + probe_print_step_result("FULL_STEP wait1_move_from_timer2", wait1); + probe_print_step_result("FULL_STEP wait2_move_from_timer1", wait2); + probe_check("timer 3 visible action dodges skyfall", wait0.damage_taken == 0); + probe_check("timer 2 visible action dodges skyfall", wait1.damage_taken == 0); + probe_check("timer 1 visible action is too late", wait2.damage_taken == PROBE_SKYFALL_DAMAGE); + printf("ACTIONABLE_LEAD move_actions=2 visible_timers=3,2 too_late_timer=1\n"); +} + +static void probe_attack_lock_interaction(void) { + printf("\n== ATTACK-LOCK INTERACTION ==\n"); + ProbeStepDodgeResult idle = probe_run_attack_lock_idle_case("locked_idle"); + ProbeStepDodgeResult move_only = probe_run_attack_lock_case( + "locked_move_only", + PROBE_TARGET_AFTER_MOVE_NONE); + ProbeStepDodgeResult target_move = probe_run_attack_lock_case( + "locked_target_and_move_same_tick", + PROBE_TARGET_AFTER_MOVE_SAME_TICK); + ProbeStepDodgeResult move_then_target = probe_run_attack_lock_case( + "locked_move_then_retarget_next_tick", + PROBE_TARGET_AFTER_MOVE_NEXT_TICK); + probe_print_step_result("LOCK idle", idle); + probe_print_step_result("LOCK move_only", move_only); + probe_print_step_result("LOCK target_and_move_same_tick", target_move); + probe_print_step_result("LOCK move_then_retarget_next_tick", move_then_target); + probe_check("locked idle eats skyfall", idle.damage_taken == PROBE_SKYFALL_DAMAGE); + probe_check("MOVE-only while locked breaks lock and dodges", move_only.damage_taken == 0); + probe_check("TARGET+MOVE same tick stays on tile and eats skyfall", + target_move.damage_taken == PROBE_SKYFALL_DAMAGE); + probe_check("retargeting one tick after a successful dodge stays safe", + move_then_target.damage_taken == 0); +} + +int main(void) { + printf("=== Colosseum javelin skyfall dodge probe ===\n"); + printf("step order under test: action pretick, NPC skyfall resolve, NPC attack fire, player movement, obs write\n"); + probe_obs_honesty(); + probe_direct_resolve(); + probe_full_step_dodgeability(); + probe_attack_lock_interaction(); + printf("\nVERDICT_DATA obs_honest=1 off_tile_dodges=1 real_step_actionable_lead=2 move_only_lock_dodges=1 target_plus_move_blocks_dodge=1\n"); + return 0; +} diff --git a/ocean/osrs/tests/probe_colo_wave_advance.c b/ocean/osrs/tests/probe_colo_wave_advance.c new file mode 100644 index 0000000000..fe5ade1c25 --- /dev/null +++ b/ocean/osrs/tests/probe_colo_wave_advance.c @@ -0,0 +1,218 @@ +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +static int scripted_overhead(const ColosseumState* s) { + int best_style = ATTACK_STYLE_NONE, best_ticks = 1 << 30, best_dmg = -1; + const EncounterPendingHitQueue* q = &s->player_pending_hits; + for (int i = 0; i < q->count; i++) { + const EncounterPendingHit* h = &q->hits[i]; + if (!h->active || !h->check_prayer) continue; + if (h->ticks_remaining < best_ticks || + (h->ticks_remaining == best_ticks && h->damage > best_dmg)) { + best_ticks = h->ticks_remaining; best_dmg = h->damage; best_style = h->attack_style; + } + } + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s->npcs[n]; + if (npc->type != COLO_MANTICORE || !col_npc_is_live_enemy(npc)) continue; + const ColoManticoreState* mc = &npc->type_state.manticore; + if (mc->cycle_step < 0) continue; + int orb = mc->cycle_step; + if (orb >= 3) continue; + int ticks = (mc->cycle_step == 0) ? npc->attack_timer : 0; + int dmg = 36; + if (ticks < best_ticks || (ticks == best_ticks && dmg > best_dmg)) { + best_ticks = ticks; best_dmg = dmg; best_style = mc->orb_style[orb]; + } + } + switch (best_style) { + case ATTACK_STYLE_MAGIC: return COLO_OVERHEAD_MAGIC; + case ATTACK_STYLE_RANGED: return COLO_OVERHEAD_RANGED; + case ATTACK_STYLE_MELEE: return COLO_OVERHEAD_MELEE; + default: return COLO_OVERHEAD_NO_CHANGE; + } +} + +static int scripted_target(const ColosseumState* s) { + int best_slot = -1, best_dist = 1 << 30; + for (int slot = 0; slot < COLO_OBS_NPCS; slot++) { + int idx = s->current_obs_slots[slot]; + if (idx < 0 || idx >= COLO_MAX_NPCS) continue; + const ColoNPC* npc = &s->npcs[idx]; + if (!col_npc_is_live_target(npc)) continue; + int d = col_npc_dist_to_player(s, npc); + if (d < best_dist) { best_dist = d; best_slot = slot; } + } + return best_slot < 0 ? 0 : col_primary_attack_action_for_obs_slot(best_slot); +} + +static int scripted_first_modifier(const ColosseumState* s) { + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + if (s->modifiers.draft_options[o] >= 0) return o; + return 0; +} + +static int scripted_heal_cell(const ColosseumState* s) { + for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { + const ColoInvCell* cell = &s->inventory_cells[i]; + OsrsInventoryClickResolution r = osrs_inventory_click_interpret( + cell->item_idx, cell->raw_osrs_id, OSRS_CLICK_TICK_FIRST); + if ((r.consumable_kind == OSRS_CONSUMABLE_BREW || + r.consumable_kind == OSRS_CONSUMABLE_GUTHIX_REST || + r.consumable_kind == OSRS_CONSUMABLE_SHARK_FOOD || + r.consumable_kind == OSRS_CONSUMABLE_KARAMBWAN) && + col_inventory_cell_actionable(s, i)) return i; + } + return -1; +} + +static void aggressive_policy(ColosseumState* s, int* actions) { + for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) actions[h] = 0; + if (s->modifiers.draft_pending) { + actions[COLO_HEAD_MODIFIER_SELECT] = scripted_first_modifier(s) + 1; + return; + } + actions[COLO_HEAD_PRAYER] = scripted_overhead(s); + actions[COLO_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR; + actions[COLO_HEAD_PRIMARY] = scripted_target(s); + if (s->player.current_hitpoints < 55) { + int heal_cell = scripted_heal_cell(s); + if (heal_cell >= 0) { + OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret( + &s->inventory_cells[heal_cell], OSRS_CLICK_TICK_FIRST); + int head = r.click_action == OSRS_CLICK_DRINK ? COLO_HEAD_DRINK : COLO_HEAD_EAT; + actions[head] = heal_cell + 1; + } + } +} + +static int count_live_enemies(const ColosseumState* s) { + int n = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (col_npc_is_live_enemy(&s->npcs[i])) n++; + return n; +} + +static int forcekill_live_enemies(ColosseumState* s) { + int killed = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s->npcs[i]; + if (!col_npc_is_live_enemy(npc)) continue; + npc->hp = 0; + killed++; + } + return killed; +} + +enum { MODE_AGGRESSIVE, MODE_FORCEKILL }; + +static void run_episodes(const char* label, int mode, int n_eps) { + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY; + ctx.config.beginner_loadout_fraction = 0.0f; + ctx.config.step_out_forecast_obs_enabled = 1; + ctx.config.wave_clear_bonus = 1.0f; + + static float obs[COLO_NUM_OBS]; + ColosseumState s; + unsigned int rng = 0x9e3779b9u; + + int advanced_eps = 0; + int max_wave_overall = 0; + int total_clear_bonus_fires = 0; + int total_waves_cleared = 0; + int wins = 0; + long total_spawned_w1 = 0, total_killed_w1 = 0; + int wave_ended_eps = 0; + double dmg_dealt = 0, dmg_recv = 0; + + for (int ep = 0; ep < n_eps; ep++) { + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, (uint32_t)(rng | 1u)); + + int actions[COLO_NUM_ACTION_HEADS] = {0}; + long guard = 0; + int wave1_spawn_count = -1; + int max_wave_this_ep = s.wave; + float clear_bonus_seen = 0.0f; + int wave_completed_seen = 0; + int prev_waves_cleared = s.log.waves_cleared; + + while (!s.episode_over && guard++ < COLO_MAX_TICKS + 16) { + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + + if (wave1_spawn_count < 0 && s.wave == 0 && !s.modifiers.draft_pending && + s.wave_spawn_delay == 0 && s.wave_ready_delay == 0) { + int live = count_live_enemies(&s); + if (live > 0) wave1_spawn_count = s.current_wave_total_killable; + } + + if (s.modifiers.draft_pending) { + aggressive_policy(&s, actions); + } else if (mode == MODE_FORCEKILL) { + + for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) actions[h] = 0; + forcekill_live_enemies(&s); + } else { + aggressive_policy(&s, actions); + } + + float r_before = s.reward; + (void)r_before; + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + + if (s.tick_scratch.wave_completed) { + wave_completed_seen++; + + clear_bonus_seen += ctx.config.wave_clear_bonus; + } + if (s.wave > max_wave_this_ep) max_wave_this_ep = s.wave; + } + + int waves_cleared = s.log.waves_cleared - prev_waves_cleared; + total_waves_cleared += waves_cleared; + total_clear_bonus_fires += wave_completed_seen; + if (max_wave_this_ep >= 1) advanced_eps++; + if (max_wave_this_ep > max_wave_overall) max_wave_overall = max_wave_this_ep; + if (wave_completed_seen > 0) wave_ended_eps++; + if (s.winner == COLO_OUTCOME_PLAYER_WON) wins++; + if (wave1_spawn_count > 0) { total_spawned_w1 += wave1_spawn_count; } + total_killed_w1 += s.log.total_npc_kills; + dmg_dealt += s.log.total_damage_dealt; + dmg_recv += s.log.total_damage_received; + + rng = rng * 1103515245u + 12345u; + } + + printf("%-14s eps=%d\n", label, n_eps); + printf(" wave1 spawns (killable)/ep = %.2f kills/ep = %.2f\n", + (double)total_spawned_w1 / n_eps, (double)total_killed_w1 / n_eps); + printf(" episodes that reached wave>=1 (0->1 advance) = %d / %d\n", + advanced_eps, n_eps); + printf(" episodes where the wave ever ENDED (wave_completed fired) = %d / %d\n", + wave_ended_eps, n_eps); + printf(" total wave_completed fires = %d total log.waves_cleared = %d\n", + total_clear_bonus_fires, total_waves_cleared); + printf(" clear-bonus fires (== wave_completed) = %d wins = %d\n", + total_clear_bonus_fires, wins); + printf(" max wave reached overall = %d\n", max_wave_overall); + printf(" dmg_dealt/ep = %.0f dmg_recv/ep = %.0f\n", + dmg_dealt / n_eps, dmg_recv / n_eps); + printf("\n"); +} + +int main(void) { + printf("=== Colosseum wave-1 -> wave-2 advance probe (CPU, speedrun loadout) ===\n"); + printf("wave index 0 == \"wave 1\"; advance means s->wave reaches 1.\n"); + printf("COLO_NUM_WAVES=%d COLO_WAVE_BOSS=%d SPEEDRUN_MAX_TICKS=%d REINFORCE_TICKS=%d\n\n", + COLO_NUM_WAVES, COLO_WAVE_BOSS, COLO_SPEEDRUN_MAX_TICKS, COLO_REINFORCEMENT_TICKS); + + run_episodes("AGGRESSIVE", MODE_AGGRESSIVE, 32); + run_episodes("FORCEKILL", MODE_FORCEKILL, 32); + return 0; +} diff --git a/ocean/osrs/tests/probe_colosseum_winnability.c b/ocean/osrs/tests/probe_colosseum_winnability.c new file mode 100644 index 0000000000..53c599bb65 --- /dev/null +++ b/ocean/osrs/tests/probe_colosseum_winnability.c @@ -0,0 +1,185 @@ +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +static int scripted_overhead(const ColosseumState* s) { + int best_style = ATTACK_STYLE_NONE, best_ticks = 1 << 30, best_dmg = -1; + const EncounterPendingHitQueue* q = &s->player_pending_hits; + for (int i = 0; i < q->count; i++) { + const EncounterPendingHit* h = &q->hits[i]; + if (!h->active || !h->check_prayer) continue; + if (h->ticks_remaining < best_ticks || + (h->ticks_remaining == best_ticks && h->damage > best_dmg)) { + best_ticks = h->ticks_remaining; best_dmg = h->damage; best_style = h->attack_style; + } + } + + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s->npcs[n]; + if (npc->type != COLO_MANTICORE || !col_npc_is_live_enemy(npc)) continue; + const ColoManticoreState* mc = &npc->type_state.manticore; + if (mc->cycle_step < 0) continue; + int orb = mc->cycle_step; + if (orb >= 3) continue; + int ticks = (mc->cycle_step == 0) ? npc->attack_timer : 0; + int dmg = 36; + if (ticks < best_ticks || (ticks == best_ticks && dmg > best_dmg)) { + best_ticks = ticks; best_dmg = dmg; best_style = mc->orb_style[orb]; + } + } + switch (best_style) { + case ATTACK_STYLE_MAGIC: return COLO_OVERHEAD_MAGIC; + case ATTACK_STYLE_RANGED: return COLO_OVERHEAD_RANGED; + case ATTACK_STYLE_MELEE: return COLO_OVERHEAD_MELEE; + default: return COLO_OVERHEAD_NO_CHANGE; + } +} + +static int forecast_action_danger(const ColoStepOutForecastAction* a, int horizon) { + if (!a->valid) return 1 << 30; + int danger = a->same_tick_mixed_style_conflict ? 100000 : 0; + int worst = 0; + for (int t = 0; t < horizon; t++) + if (a->ticks[t].max_hit > worst) worst = a->ticks[t].max_hit; + return danger + worst; +} + +static int g_move_mode = 0; + +static int scripted_move(ColosseumState* s) { + if (g_move_mode == 1) return 0; + ColoStepOutForecast f; + col_build_step_out_forecast_horizon_mode(s, &f, COLO_STEP_OUT_FORECAST_HORIZON, 0); + int horizon = COLO_STEP_OUT_FORECAST_HORIZON; + const ColoStepOutForecastAction* idle = &f.actions[0]; + if (idle->valid && !idle->same_tick_mixed_style_conflict) return 0; + int best = 0, best_danger = forecast_action_danger(idle, horizon); + for (int a = 1; a < ENCOUNTER_MOVE_ACTIONS; a++) { + int d = forecast_action_danger(&f.actions[a], horizon); + if (d < best_danger) { best_danger = d; best = a; } + } + return best; +} + +static int scripted_target(const ColosseumState* s) { + int best_slot = -1, best_dist = 1 << 30; + for (int slot = 0; slot < COLO_OBS_NPCS; slot++) { + int idx = s->current_obs_slots[slot]; + if (idx < 0 || idx >= COLO_MAX_NPCS) continue; + const ColoNPC* npc = &s->npcs[idx]; + if (!col_npc_is_live_target(npc)) continue; + int d = col_npc_dist_to_player(s, npc); + if (d < best_dist) { best_dist = d; best_slot = slot; } + } + return best_slot < 0 ? 0 : col_primary_attack_action_for_obs_slot(best_slot); +} + +static int scripted_least_bad_modifier(const ColosseumState* s) { + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + if (s->modifiers.draft_options[o] >= 0) return o; + return 0; +} + +static int scripted_heal_cell(const ColosseumState* s) { + for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { + const ColoInvCell* cell = &s->inventory_cells[i]; + OsrsInventoryClickResolution r = osrs_inventory_click_interpret( + cell->item_idx, cell->raw_osrs_id, OSRS_CLICK_TICK_FIRST); + if ((r.consumable_kind == OSRS_CONSUMABLE_BREW || + r.consumable_kind == OSRS_CONSUMABLE_GUTHIX_REST || + r.consumable_kind == OSRS_CONSUMABLE_SHARK_FOOD || + r.consumable_kind == OSRS_CONSUMABLE_KARAMBWAN) && + col_inventory_cell_actionable(s, i)) return i; + } + return -1; +} + +static void scripted_policy(ColosseumState* s, int* actions) { + for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) actions[h] = 0; + if (s->modifiers.draft_pending) { + actions[COLO_HEAD_MODIFIER_SELECT] = scripted_least_bad_modifier(s) + 1; + return; + } + actions[COLO_HEAD_PRAYER] = scripted_overhead(s); + actions[COLO_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR; + int move = scripted_move(s); + actions[COLO_HEAD_PRIMARY] = move > 0 ? move : scripted_target(s); + if (s->player.current_hitpoints < 55) { + int heal_cell = scripted_heal_cell(s); + if (heal_cell >= 0) { + OsrsInventoryClickResolution r = osrs_inventory_cell_click_interpret( + &s->inventory_cells[heal_cell], OSRS_CLICK_TICK_FIRST); + int head = r.click_action == OSRS_CLICK_DRINK ? COLO_HEAD_DRINK : COLO_HEAD_EAT; + actions[head] = heal_cell + 1; + } + } +} + +typedef int (*PolicyFn)(ColosseumState*, int*, unsigned int*); + +static void run_episodes(const char* label, int scripted, int start_wave, int n_eps) { + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = start_wave; + ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_MIXED; + ctx.config.beginner_loadout_fraction = 0.5f; + ctx.config.step_out_forecast_obs_enabled = 1; + + static float obs[COLO_NUM_OBS]; + ColosseumState s; + int wins = 0, wave_sum = 0, wave_max = 0; + int hist[16] = {0}; + double pray_correct = 0, npc_attacks = 0, dmg_recv = 0, dmg_dealt = 0; + unsigned int rng = 0x9e3779b9u ^ (unsigned)(start_wave * 2654435761u); + + for (int ep = 0; ep < n_eps; ep++) { + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, (uint32_t)(rng | 1u)); + int actions[COLO_NUM_ACTION_HEADS] = {0}; + long guard = 0; + while (!s.episode_over && guard++ < COLO_MAX_TICKS + 16) { + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + if (scripted) { + scripted_policy(&s, actions); + } else { + for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) { + rng = rng * 1103515245u + 12345u; + actions[h] = (int)((rng >> 16) % (unsigned)COLO_ACTION_DIMS[h]); + } + } + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + } + int wave = s.wave; + if (s.winner == COLO_OUTCOME_PLAYER_WON) wins++; + wave_sum += wave; + if (wave > wave_max) wave_max = wave; + if (wave < 16) hist[wave]++; + pray_correct += s.log.total_prayer_correct; + npc_attacks += s.log.total_npc_attacks; + dmg_recv += s.log.total_damage_received; + dmg_dealt += s.log.total_damage_dealt; + rng = rng * 1103515245u + 12345u; + } + double pray_rate = npc_attacks > 0 ? pray_correct / npc_attacks : 0.0; + printf("%-22s start_wave=%d eps=%d mean_wave=%.2f max_wave=%d wins=%d pray_rate=%.2f dmg_recv/ep=%.0f dmg_dealt/ep=%.0f\n", + label, start_wave, n_eps, (double)wave_sum / n_eps, wave_max, wins, + pray_rate, dmg_recv / n_eps, dmg_dealt / n_eps); + printf(" wave histogram (0-based): "); + for (int w = 0; w < 13; w++) if (hist[w]) printf("w%d:%d ", w, hist[w]); + printf("\n"); +} + +int main(void) { + printf("=== Colosseum winnability probe (scripted vs random, CPU) ===\n"); + printf("(RL baseline for reference: mean_wave ~4.5 0-based, prayer ~0.62)\n"); + run_episodes("RANDOM (sanity)", 0, 0, 64); + g_move_mode = 1; + run_episodes("SCRIPTED-stand", 1, 0, 64); + run_episodes("SCRIPTED-stand@w4", 1, 3, 64); + g_move_mode = 0; + run_episodes("SCRIPTED-deconflict", 1, 0, 64); + run_episodes("SCRIPTED-deconflict@w4", 1, 3, 64); + return 0; +} diff --git a/ocean/osrs/tests/test_colo_prayer_wiring.c b/ocean/osrs/tests/test_colo_prayer_wiring.c new file mode 100644 index 0000000000..0f069c9db7 --- /dev/null +++ b/ocean/osrs/tests/test_colo_prayer_wiring.c @@ -0,0 +1,120 @@ +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +#define col_init_context_typed(ctx_ptr) do { \ + col_init_context_typed(ctx_ptr); \ + (ctx_ptr)->config.late_start_state_mode = 0; \ +} while (0) + +typedef enum { POLICY_HOLD_MELEE, POLICY_OBS_TELEGRAPH } PrayerPolicy; + +static int telegraph_overhead(const ColosseumState* s) { + AttackStyle best_style = ATTACK_STYLE_NONE; + int best_ticks = 1 << 30; + for (int slot = 0; slot < COLO_OBS_NPCS; slot++) { + int idx = s->current_obs_slots[slot]; + if (idx < 0 || idx >= COLO_MAX_NPCS) continue; + const ColoNPC* npc = &s->npcs[idx]; + if (!col_npc_is_live_enemy(npc)) continue; + ColoNpcNextPrayerObs t = col_npc_next_prayer_obs(s, npc, idx); + if (!t.active) continue; + if (t.ticks < best_ticks) { best_ticks = t.ticks; best_style = t.style; } + } + switch (best_style) { + case ATTACK_STYLE_MAGIC: return COLO_OVERHEAD_MAGIC; + case ATTACK_STYLE_RANGED: return COLO_OVERHEAD_RANGED; + default: return COLO_OVERHEAD_MELEE; + } +} + +static int nearest_target(const ColosseumState* s) { + int best_slot = -1, best_dist = 1 << 30; + for (int slot = 0; slot < COLO_OBS_NPCS; slot++) { + int idx = s->current_obs_slots[slot]; + if (idx < 0 || idx >= COLO_MAX_NPCS) continue; + const ColoNPC* npc = &s->npcs[idx]; + if (!col_npc_is_live_target(npc)) continue; + int d = col_npc_dist_to_player(s, npc); + if (d < best_dist) { best_dist = d; best_slot = slot; } + } + return best_slot < 0 ? 0 : col_primary_attack_action_for_obs_slot(best_slot); +} + +static void run_test(const char* label, PrayerPolicy policy, int start_wave, int n_eps) { + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = start_wave; + ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_MIXED; + ctx.config.beginner_loadout_fraction = 0.5f; + ctx.config.step_out_forecast_obs_enabled = 1; + + static float obs[COLO_NUM_OBS]; + ColosseumState s; + int wave_sum = 0; + double pray_correct = 0, npc_attacks = 0, dmg_recv = 0; + long tele_total = 0, tele_match = 0; + AttackStyle predicted[COLO_MAX_NPCS]; + unsigned int rng = 0x51ed270bu ^ (unsigned)(start_wave * 2654435761u + policy); + + for (int ep = 0; ep < n_eps; ep++) { + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, (uint32_t)(rng | 1u)); + int actions[COLO_NUM_ACTION_HEADS] = {0}; + long guard = 0; + while (!s.episode_over && guard++ < COLO_MAX_TICKS + 16) { + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) actions[h] = 0; + if (s.modifiers.draft_pending) { + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + if (s.modifiers.draft_options[o] >= 0) { actions[COLO_HEAD_MODIFIER_SELECT] = o + 1; break; } + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + continue; + } + actions[COLO_HEAD_PRAYER] = (policy == POLICY_HOLD_MELEE) + ? COLO_OVERHEAD_MELEE : telegraph_overhead(&s); + actions[COLO_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR; + actions[COLO_HEAD_PRIMARY] = nearest_target(&s); + + for (int n = 0; n < COLO_MAX_NPCS; n++) predicted[n] = ATTACK_STYLE_NONE; + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s.npcs[n]; + if (!col_npc_is_live_enemy(npc)) continue; + ColoNpcNextPrayerObs t = col_npc_next_prayer_obs(&s, npc, n); + if (t.active && t.ticks <= 1) predicted[n] = t.style; + } + + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + + for (int n = 0; n < COLO_MAX_NPCS; n++) { + const ColoNPC* npc = &s.npcs[n]; + if (!npc->attacked_this_tick) continue; + AttackStyle actual = (AttackStyle)npc->attack_style_this_tick; + if (actual != ATTACK_STYLE_MELEE && actual != ATTACK_STYLE_RANGED && + actual != ATTACK_STYLE_MAGIC) continue; + tele_total++; + if (predicted[n] == actual) tele_match++; + } + } + wave_sum += s.wave; + pray_correct += s.log.total_prayer_correct; + npc_attacks += s.log.total_npc_attacks; + dmg_recv += s.log.total_damage_received; + rng = rng * 1103515245u + 12345u; + } + double pray_rate = npc_attacks > 0 ? pray_correct / npc_attacks : 0.0; + double tele_acc = tele_total > 0 ? (double)tele_match / (double)tele_total : 0.0; + printf("%-26s wave=%d eps=%d mean_wave=%.2f pray_correct=%.2f offpray_dmg/ep=%.0f telegraph_acc=%.2f (%ld/%ld)\n", + label, start_wave, n_eps, (double)wave_sum / n_eps, pray_rate, + dmg_recv / n_eps, tele_acc, tele_match, tele_total); +} + +int main(void) { + printf("=== Colosseum prayer-wiring probe (stand + attack, prayer policy varies) ===\n"); + run_test("HOLD_MELEE @w1", POLICY_HOLD_MELEE, 0, 128); + run_test("OBS_TELEGRAPH@w1", POLICY_OBS_TELEGRAPH, 0, 128); + run_test("HOLD_MELEE @w4", POLICY_HOLD_MELEE, 3, 128); + run_test("OBS_TELEGRAPH@w4", POLICY_OBS_TELEGRAPH, 3, 128); + return 0; +} diff --git a/ocean/osrs/tests/test_colosseum_consumable_sprite_assets.c b/ocean/osrs/tests/test_colosseum_consumable_sprite_assets.c new file mode 100644 index 0000000000..6df17149bc --- /dev/null +++ b/ocean/osrs/tests/test_colosseum_consumable_sprite_assets.c @@ -0,0 +1,56 @@ +#include +#include + +#include "ocean/osrs/osrs_assets.h" +#include "ocean/osrs/encounters/encounter_colosseum.h" + +typedef struct { + int checked; + int failures; +} SpriteAssetCheck; + +static int path_has_png_signature(const char* path) { + unsigned char signature[8]; + FILE* f = fopen(path, "rb"); + if (!f) return 0; + size_t n = fread(signature, 1, sizeof(signature), f); + fclose(f); + static const unsigned char png_signature[8] = { + 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' + }; + return n == sizeof(signature) && + memcmp(signature, png_signature, sizeof(signature)) == 0; +} + +static void check_colosseum_consumable_sprite(int raw_osrs_id, void* ctx) { + SpriteAssetCheck* check = (SpriteAssetCheck*)ctx; + char logical_path[64]; + snprintf(logical_path, sizeof(logical_path), "sprites/items/%d.png", raw_osrs_id); + const char* path = osrs_asset_path(logical_path); + check->checked++; + if (path_has_png_signature(path)) return; + + check->failures++; + fprintf(stderr, "missing or invalid sprite for colosseum raw id %d: %s\n", + raw_osrs_id, + path); +} + +int main(void) { + SpriteAssetCheck check = {0}; + col_for_each_reachable_consumable_dose_raw_osrs_id( + check_colosseum_consumable_sprite, + &check); + if (check.checked == 0) { + fprintf(stderr, "no colosseum consumable dose sprites checked\n"); + return 1; + } + if (check.failures != 0) { + fprintf(stderr, "test_colosseum_consumable_sprite_assets: %d failure(s)\n", + check.failures); + return 1; + } + printf("test_colosseum_consumable_sprite_assets: OK (%d sprites)\n", + check.checked); + return 0; +} diff --git a/ocean/osrs/tests/test_colosseum_forecast_exact.c b/ocean/osrs/tests/test_colosseum_forecast_exact.c new file mode 100644 index 0000000000..9a2f66085b --- /dev/null +++ b/ocean/osrs/tests/test_colosseum_forecast_exact.c @@ -0,0 +1,584 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +static void col_step_out_forecast_landing_selftest_one_state(ColosseumState* s) { + for (int x = COLO_ARENA_MIN_X - 2; x <= COLO_ARENA_MAX_X + 2; x++) { + for (int y = COLO_ARENA_MIN_Y - 2; y <= COLO_ARENA_MAX_Y + 2; y++) { + s->player.x = x; + s->player.y = y; + for (int action_idx = 0; action_idx < ENCOUNTER_MOVE_ACTIONS; action_idx++) { + int valid = col_step_out_forecast_action_valid(s, action_idx); + Player moved = s->player; + if (action_idx != 0) { + encounter_move_to_target( + &moved, + ENCOUNTER_MOVE_TARGET_DX[action_idx], + ENCOUNTER_MOVE_TARGET_DY[action_idx], + col_player_walkable, + (void*)s); + } + ColoForecastLanding landing = + col_step_out_forecast_action_landing_ctx(s, action_idx); + if (landing.valid != valid || + landing.land_x != moved.x || + landing.land_y != moved.y) { + fprintf(stderr, + "colosseum landing mismatch player=(%d,%d) action=%d: helper=(%d,%d,%d) full=(%d,%d,%d)\n", + x, y, action_idx, + landing.valid, landing.land_x, landing.land_y, + valid, moved.x, moved.y); + abort(); + } + } + } + } +} + +static void col_step_out_forecast_landing_selftest(void) { + ColosseumContext ctx; + ColosseumState s; + col_init_context_typed(&ctx); + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 0x51A7u); + memset(s.npcs, 0, sizeof(s.npcs)); + memset(s.npc_collision_flags, 0, sizeof(s.npc_collision_flags)); + col_rebuild_player_collision_flags(&s); + col_step_out_forecast_landing_selftest_one_state(&s); + + col_spawn_npc_at(&s, COLO_SERPENT_SHAMAN, 12, 9); + col_spawn_npc_at(&s, COLO_JAVELIN_COLOSSUS, 18, 18); + col_rebuild_player_collision_flags(&s); + col_step_out_forecast_landing_selftest_one_state(&s); + + s.wave = COLO_WAVE_BOSS; + s.sol.started = 1; + s.sol.boss_arena_min_x = COLO_BOSS_ARENA_MIN_X; + s.sol.boss_arena_min_y = COLO_BOSS_ARENA_MIN_Y; + s.sol.boss_arena_max_x = COLO_BOSS_ARENA_MAX_X; + s.sol.boss_arena_max_y = COLO_BOSS_ARENA_MAX_Y; + col_step_out_forecast_landing_selftest_one_state(&s); + + printf("colosseum landing helper selftest PASS: %d actions across 3 states\n", + ENCOUNTER_MOVE_ACTIONS); +} + +#define col_init_context_typed(ctx_ptr) do { \ + col_init_context_typed(ctx_ptr); \ + (ctx_ptr)->config.late_start_state_mode = 0; \ +} while (0) + +#define EXACT_MAGIC "COLOEXACTv1" +#define EXACT_VERSION 2u +#define EXACT_CHUNK_BYTES 65536 + +typedef struct { + char magic[16]; + uint32_t version; + uint32_t state_size; + uint32_t forecast_size; + uint32_t forecast_obs_size; + uint32_t obs_size; + uint32_t action_mask_size; + uint32_t action_features; + uint32_t record_count; +} ColoExactFileHeader; + +typedef struct { + uint32_t scenario_id; + uint32_t step_index; + uint32_t tick; + uint32_t wave; + uint32_t terminal; + uint32_t winner; + uint32_t state_size; + uint32_t obs_size; + uint32_t action_mask_size; + uint32_t forecast_size; + uint64_t state_hash; + uint64_t forecast_hash; + uint64_t forecast_obs_hash; + uint64_t obs_hash; + uint64_t action_mask_hash; + float reward; +} ColoExactRecordHeader; + +typedef struct { + FILE* file; + uint32_t record_count; +} ColoExactWriter; + +static uint64_t exact_fnv_bytes(uint64_t h, const void* data, size_t size) { + const uint8_t* bytes = (const uint8_t*)data; + for (size_t i = 0; i < size; i++) { + h ^= bytes[i]; + h *= 1099511628211ULL; + } + return h; +} + +static uint64_t exact_hash_bytes(const void* data, size_t size) { + return exact_fnv_bytes(1469598103934665603ULL, data, size); +} + +static void exact_write_all(FILE* file, const void* data, size_t size) { + if (fwrite(data, 1, size, file) != size) { + perror("write colosseum exact fixture"); + abort(); + } +} + +static void exact_readable_path( + char* out, + size_t out_size, + const char* dir, + const char* file_name +) { + int n = snprintf(out, out_size, "%s/%s", dir, file_name); + if (n < 0 || (size_t)n >= out_size) { + fprintf(stderr, "fixture path too long: %s/%s\n", dir, file_name); + abort(); + } +} + +static void exact_mkdir_if_needed(const char* dir) { + if (mkdir(dir, 0777) == 0) return; + if (errno == EEXIST) return; + perror("mkdir colosseum exact fixture dir"); + abort(); +} + +static void exact_writer_open(ColoExactWriter* writer, const char* path) { + memset(writer, 0, sizeof(*writer)); + writer->file = fopen(path, "wb"); + if (!writer->file) { + perror("open colosseum exact fixture"); + abort(); + } + + ColoExactFileHeader header = {0}; + memcpy(header.magic, EXACT_MAGIC, sizeof(EXACT_MAGIC)); + header.version = EXACT_VERSION; + header.state_size = (uint32_t)sizeof(ColosseumState); + header.forecast_size = (uint32_t)sizeof(ColoStepOutForecast); + header.forecast_obs_size = COLO_STEP_OUT_FORECAST_OBS_SIZE; + header.obs_size = COLO_NUM_OBS; + header.action_mask_size = COLO_ACTION_MASK_SIZE; + header.action_features = COLO_STEP_OUT_FORECAST_ACTION_FEATURES; + exact_write_all(writer->file, &header, sizeof(header)); +} + +static void exact_writer_close(ColoExactWriter* writer) { + ColoExactFileHeader header = {0}; + memcpy(header.magic, EXACT_MAGIC, sizeof(EXACT_MAGIC)); + header.version = EXACT_VERSION; + header.state_size = (uint32_t)sizeof(ColosseumState); + header.forecast_size = (uint32_t)sizeof(ColoStepOutForecast); + header.forecast_obs_size = COLO_STEP_OUT_FORECAST_OBS_SIZE; + header.obs_size = COLO_NUM_OBS; + header.action_mask_size = COLO_ACTION_MASK_SIZE; + header.action_features = COLO_STEP_OUT_FORECAST_ACTION_FEATURES; + header.record_count = writer->record_count; + if (fseek(writer->file, 0, SEEK_SET) != 0) { + perror("seek colosseum exact fixture"); + abort(); + } + exact_write_all(writer->file, &header, sizeof(header)); + if (fclose(writer->file) != 0) { + perror("close colosseum exact fixture"); + abort(); + } + writer->file = NULL; +} + +static void exact_capture( + ColoExactWriter* writer, + uint32_t scenario_id, + uint32_t step_index, + ColosseumState* s, + ColosseumContext* ctx +) { + ColoStepOutForecast forecast; + float forecast_obs[COLO_STEP_OUT_FORECAST_OBS_SIZE]; + float obs[COLO_NUM_OBS]; + float action_mask[COLO_ACTION_MASK_SIZE]; + + ColoForecastObsSummary summaries[ENCOUNTER_MOVE_ACTIONS]; + col_build_step_out_forecast_horizon_mode_summary( + s, &forecast, summaries, COLO_STEP_OUT_FORECAST_HORIZON, + ctx->config.forecast_run_tile_mode); + int fi = col_write_step_out_forecast_obs_summary( + &forecast, summaries, COLO_STEP_OUT_FORECAST_HORIZON, forecast_obs, 0); + assert(fi == COLO_STEP_OUT_FORECAST_OBS_SIZE); + col_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); + col_write_mask_ctx((EncounterState*)s, (EncounterContext*)ctx, action_mask); + + ColoExactRecordHeader record = {0}; + record.scenario_id = scenario_id; + record.step_index = step_index; + record.tick = (uint32_t)s->tick; + record.wave = (uint32_t)s->wave; + record.terminal = (uint32_t)col_is_terminal_ctx( + (EncounterState*)s, (EncounterContext*)ctx); + record.winner = (uint32_t)s->winner; + record.state_size = (uint32_t)sizeof(*s); + record.obs_size = COLO_NUM_OBS; + record.action_mask_size = COLO_ACTION_MASK_SIZE; + record.forecast_size = (uint32_t)sizeof(forecast); + record.state_hash = exact_hash_bytes(s, sizeof(*s)); + record.forecast_hash = exact_hash_bytes(&forecast, sizeof(forecast)); + record.forecast_obs_hash = exact_hash_bytes(forecast_obs, sizeof(forecast_obs)); + record.obs_hash = exact_hash_bytes(obs, sizeof(obs)); + record.action_mask_hash = exact_hash_bytes(action_mask, sizeof(action_mask)); + record.reward = col_get_reward_ctx((EncounterState*)s, (EncounterContext*)ctx); + + exact_write_all(writer->file, &record, sizeof(record)); + exact_write_all(writer->file, &forecast, sizeof(forecast)); + exact_write_all(writer->file, forecast_obs, sizeof(forecast_obs)); + exact_write_all(writer->file, obs, sizeof(obs)); + exact_write_all(writer->file, action_mask, sizeof(action_mask)); + exact_write_all(writer->file, s, sizeof(*s)); + writer->record_count++; +} + +static uint64_t exact_splitmix64(uint64_t* state) { + uint64_t z = (*state += 0x9E3779B97F4A7C15ULL); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + return z ^ (z >> 31); +} + +static void exact_zero_actions(int actions[COLO_NUM_ACTION_HEADS]) { + memset(actions, 0, sizeof(int) * COLO_NUM_ACTION_HEADS); +} + +static void exact_trace_actions( + ColosseumState* s, + uint64_t* rng, + int actions[COLO_NUM_ACTION_HEADS] +) { + for (int head = 0; head < COLO_NUM_ACTION_HEADS; head++) { + actions[head] = (int)(exact_splitmix64(rng) % (uint64_t)COLO_ACTION_DIMS[head]); + } + if (s->modifiers.draft_pending) { + actions[COLO_HEAD_PRIMARY] = 0; + actions[COLO_HEAD_MODIFIER_SELECT] = 1 + + (int)(exact_splitmix64(rng) % COLO_MODIFIER_DRAFT_OPTIONS); + } +} + +static void exact_init_state( + ColosseumState* s, + ColosseumContext* ctx, + int start_wave, + uint32_t seed +) { + col_init_context_typed(ctx); + ctx->config.start_wave = start_wave; + ctx->config.step_out_forecast_obs_enabled = 1; + memset(s, 0, sizeof(*s)); + col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); +} + +static void exact_refresh_geometry(ColosseumState* s, ColosseumContext* ctx) { + col_rebuild_player_collision_flags(s); + col_refresh_current_obs_slots_ctx(s, ctx); +} + +static void exact_clear_npcs(ColosseumState* s, ColosseumContext* ctx) { + memset(s->npcs, 0, sizeof(s->npcs)); + memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); + memset(s->totems, 0, sizeof(s->totems)); + memset(s->bees, 0, sizeof(s->bees)); + exact_refresh_geometry(s, ctx); +} + +static void exact_prepare_custom( + ColosseumState* s, + ColosseumContext* ctx, + uint32_t seed, + int player_x, + int player_y +) { + exact_init_state(s, ctx, 1, seed); + exact_clear_npcs(s, ctx); + s->modifiers.draft_pending = 0; + s->modifiers.draft_gates_spawn = 0; + s->wave_ready_delay = 0; + s->wave_spawn_delay = 0; + s->reinforcement_timer = COLO_REINFORCE_FIRED; + s->warband_cycle_anchor = s->tick; + s->player.x = player_x; + s->player.y = player_y; + exact_refresh_geometry(s, ctx); +} + +static void exact_run_steps( + ColoExactWriter* writer, + uint32_t scenario_id, + ColosseumState* s, + ColosseumContext* ctx, + int steps, + uint64_t action_seed +) { + uint32_t capture_idx = 0; + int actions[COLO_NUM_ACTION_HEADS]; + uint64_t rng = action_seed; + exact_capture(writer, scenario_id, capture_idx++, s, ctx); + for (int step = 0; step < steps; step++) { + exact_trace_actions(s, &rng, actions); + col_step_ctx((EncounterState*)s, (EncounterContext*)ctx, actions); + exact_capture(writer, scenario_id, capture_idx++, s, ctx); + if (s->episode_over) break; + } +} + +static void exact_run_idle_steps( + ColoExactWriter* writer, + uint32_t scenario_id, + ColosseumState* s, + ColosseumContext* ctx, + int steps +) { + uint32_t capture_idx = 0; + int actions[COLO_NUM_ACTION_HEADS]; + exact_zero_actions(actions); + exact_capture(writer, scenario_id, capture_idx++, s, ctx); + for (int step = 0; step < steps; step++) { + col_step_ctx((EncounterState*)s, (EncounterContext*)ctx, actions); + exact_capture(writer, scenario_id, capture_idx++, s, ctx); + if (s->episode_over) break; + } +} + +static void exact_scenario_wave_rollout( + ColoExactWriter* writer, + uint32_t scenario_id, + int start_wave, + uint32_t seed, + int steps +) { + ColosseumState s; + ColosseumContext ctx; + exact_init_state(&s, &ctx, start_wave, seed); + exact_run_steps(writer, scenario_id, &s, &ctx, steps, + ((uint64_t)scenario_id << 40) ^ seed ^ 0x53A91C4D12ULL); +} + +static void exact_scenario_pillar_safespot(ColoExactWriter* writer) { + ColosseumState s; + ColosseumContext ctx; + exact_prepare_custom(&s, &ctx, 0x1101u, 7, 9); + col_spawn_npc_at(&s, COLO_SERPENT_SHAMAN, 12, 9); + exact_refresh_geometry(&s, &ctx); + exact_run_steps(writer, 100u, &s, &ctx, 8, 0x991001u); +} + +static void exact_scenario_manticore_mid_barrage(ColoExactWriter* writer) { + ColosseumState s; + ColosseumContext ctx; + exact_prepare_custom(&s, &ctx, 0x1102u, 17, 16); + int slot = col_spawn_npc_at(&s, COLO_MANTICORE, 16, 19); + ColoManticoreState* mc = colo_npc_manticore(&s.npcs[slot]); + mc->cycle_step = 1; + mc->orb_style[0] = ATTACK_STYLE_MAGIC; + mc->orb_style[1] = ATTACK_STYLE_RANGED; + mc->orb_style[2] = ATTACK_STYLE_MELEE; + s.npcs[slot].attack_timer = 0; + exact_refresh_geometry(&s, &ctx); + exact_run_idle_steps(writer, 101u, &s, &ctx, 5); +} + +static void exact_scenario_javelin_boundary(ColoExactWriter* writer) { + ColosseumState s; + ColosseumContext ctx; + exact_prepare_custom(&s, &ctx, 0x1103u, 16, 16); + int slot = col_spawn_npc_at(&s, COLO_JAVELIN_COLOSSUS, 18, 18); + s.npcs[slot].attack_timer = 0; + colo_npc_javelin(&s.npcs[slot])->attack_count = 4; + exact_refresh_geometry(&s, &ctx); + exact_run_idle_steps(writer, 102u, &s, &ctx, 6); +} + +static void exact_scenario_warband_phase(ColoExactWriter* writer) { + ColosseumState s; + ColosseumContext ctx; + exact_prepare_custom(&s, &ctx, 0x1104u, 17, 16); + int slot = col_spawn_npc_at(&s, COLO_FREMENNIK_BERSERKER, 17, 17); + colo_npc_warband(&s.npcs[slot])->formation_dir = COLO_WARBAND_FORM_NORTH; + s.warband_cycle_anchor = s.tick; + exact_refresh_geometry(&s, &ctx); + exact_run_idle_steps(writer, 103u, &s, &ctx, 8); +} + +static void exact_scenario_red_flag_minotaur(ColoExactWriter* writer) { + ColosseumState s; + ColosseumContext ctx; + exact_prepare_custom(&s, &ctx, 0x1105u, 7, 9); + s.modifiers.active_mask |= 1u << COLO_MOD_RED_FLAG; + s.modifiers.tier[COLO_MOD_RED_FLAG] = 1; + col_spawn_npc_at(&s, COLO_MINOTAUR, 12, 9); + exact_refresh_geometry(&s, &ctx); + exact_run_idle_steps(writer, 104u, &s, &ctx, 10); +} + +static void exact_scenario_frozen_npc(ColoExactWriter* writer) { + ColosseumState s; + ColosseumContext ctx; + exact_prepare_custom(&s, &ctx, 0x1106u, 17, 16); + int slot = col_spawn_npc_at(&s, COLO_SERPENT_SHAMAN, 20, 16); + s.npcs[slot].attack_timer = 0; + s.npcs[slot].frozen_ticks = 3; + exact_refresh_geometry(&s, &ctx); + exact_run_idle_steps(writer, 105u, &s, &ctx, 6); +} + +static void exact_scenario_stunned_npc(ColoExactWriter* writer) { + ColosseumState s; + ColosseumContext ctx; + exact_prepare_custom(&s, &ctx, 0x1107u, 17, 16); + int slot = col_spawn_npc_at(&s, COLO_SHOCKWAVE_COLOSSUS, 20, 16); + s.npcs[slot].attack_timer = 0; + s.npcs[slot].stun_timer = 3; + exact_refresh_geometry(&s, &ctx); + exact_run_idle_steps(writer, 106u, &s, &ctx, 6); +} + +static void exact_scenario_perimeter_los(ColoExactWriter* writer) { + ColosseumState s; + ColosseumContext ctx; + exact_prepare_custom(&s, &ctx, 0x1108u, 0, 15); + int edge = col_spawn_npc_at(&s, COLO_SERPENT_SHAMAN, COLO_ARENA_MIN_X, 15); + s.npcs[edge].attack_timer = 0; + int sol = col_spawn_npc_at(&s, COLO_SOL_HEREDIT, COLO_SOL_SPAWN_X, COLO_SOL_SPAWN_Y); + s.wave = COLO_WAVE_BOSS; + s.sol.started = 1; + s.sol.boss_idx = sol; + s.sol.boss_arena_min_x = COLO_BOSS_ARENA_MIN_X; + s.sol.boss_arena_min_y = COLO_BOSS_ARENA_MIN_Y; + s.sol.boss_arena_max_x = COLO_BOSS_ARENA_MAX_X; + s.sol.boss_arena_max_y = COLO_BOSS_ARENA_MAX_Y; + s.sol.phase = 1; + s.sol.crystal_count = 1; + s.sol.crystals[0].active = 1; + s.sol.crystals[0].edge = COLO_SOL_EDGE_NORTH; + s.sol.crystals[0].x = 16; + s.sol.crystals[0].y = s.sol.boss_arena_max_y - 1; + s.sol.crystals[0].dir = 1; + s.sol.crystals[0].move_timer = COLO_SOL_CRYSTAL_MOVE_TICKS; + s.sol.crystals[0].firing_freeze = COLO_SOL_LASER_FREEZE; + s.sol.laser_cooldown = COLO_SOL_CRYSTAL_COOLDOWN_MIN; + s.player.x = 16; + s.player.y = 14; + exact_refresh_geometry(&s, &ctx); + exact_run_idle_steps(writer, 107u, &s, &ctx, 5); +} + +static void exact_generate_fixture(const char* path) { + ColoExactWriter writer; + exact_writer_open(&writer, path); + + exact_scenario_wave_rollout(&writer, 1u, 0, 0xC010001u, 28); + exact_scenario_wave_rollout(&writer, 4u, 3, 0xC010004u, 32); + exact_scenario_wave_rollout(&writer, 8u, 7, 0xC010008u, 34); + exact_scenario_wave_rollout(&writer, 12u, 11, 0xC010012u, 34); + exact_scenario_pillar_safespot(&writer); + exact_scenario_manticore_mid_barrage(&writer); + exact_scenario_javelin_boundary(&writer); + exact_scenario_warband_phase(&writer); + exact_scenario_red_flag_minotaur(&writer); + exact_scenario_frozen_npc(&writer); + exact_scenario_stunned_npc(&writer); + exact_scenario_perimeter_los(&writer); + + exact_writer_close(&writer); +} + +static int exact_compare_files(const char* expected_path, const char* actual_path) { + FILE* expected = fopen(expected_path, "rb"); + if (!expected) { + perror("open expected colosseum exact fixture"); + abort(); + } + FILE* actual = fopen(actual_path, "rb"); + if (!actual) { + perror("open actual colosseum exact fixture"); + abort(); + } + + uint8_t expected_buf[EXACT_CHUNK_BYTES]; + uint8_t actual_buf[EXACT_CHUNK_BYTES]; + uint64_t offset = 0; + for (;;) { + size_t ne = fread(expected_buf, 1, sizeof(expected_buf), expected); + size_t na = fread(actual_buf, 1, sizeof(actual_buf), actual); + if (ne != na) { + printf("colosseum exact mismatch: size differs at byte %llu\n", + (unsigned long long)offset); + fclose(expected); + fclose(actual); + return 1; + } + if (ne == 0) break; + if (memcmp(expected_buf, actual_buf, ne) != 0) { + for (size_t i = 0; i < ne; i++) { + if (expected_buf[i] == actual_buf[i]) continue; + printf("colosseum exact mismatch at byte %llu: expected %u got %u\n", + (unsigned long long)(offset + i), + (unsigned)expected_buf[i], + (unsigned)actual_buf[i]); + fclose(expected); + fclose(actual); + return 1; + } + } + offset += (uint64_t)ne; + } + + fclose(expected); + fclose(actual); + return 0; +} + +int main(int argc, char** argv) { + if (argc != 3 || + (strcmp(argv[1], "--write-golden") != 0 && + strcmp(argv[1], "--compare") != 0)) { + fprintf(stderr, + "usage: %s --write-golden DIR | --compare DIR\n", argv[0]); + return 2; + } + + col_static_los_table_selftest(); + col_static_footprint_table_selftest(); + col_step_out_forecast_landing_selftest(); + + char fixture_path[1024]; + char current_path[1024]; + exact_mkdir_if_needed(argv[2]); + exact_readable_path( + fixture_path, sizeof(fixture_path), argv[2], + "colosseum_forecast_exact.bin"); + + if (strcmp(argv[1], "--write-golden") == 0) { + exact_generate_fixture(fixture_path); + printf("colosseum exact golden wrote %s\n", fixture_path); + return 0; + } + + exact_readable_path( + current_path, sizeof(current_path), argv[2], + "colosseum_forecast_exact.current.bin"); + exact_generate_fixture(current_path); + int failed = exact_compare_files(fixture_path, current_path); + if (failed) return 1; + printf("colosseum exact golden compare PASS: %s\n", fixture_path); + return 0; +} diff --git a/ocean/osrs/tests/test_colosseum_golden.c b/ocean/osrs/tests/test_colosseum_golden.c new file mode 100644 index 0000000000..8ef21ba404 --- /dev/null +++ b/ocean/osrs/tests/test_colosseum_golden.c @@ -0,0 +1,237 @@ +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +#define FNV_OFFSET 1469598103934665603ULL +#define FNV_PRIME 1099511628211ULL + +static inline uint64_t fnv_bytes(uint64_t h, const void* p, size_t n) { + const uint8_t* b = (const uint8_t*)p; + for (size_t i = 0; i < n; i++) { + h ^= b[i]; + h *= FNV_PRIME; + } + return h; +} + +static inline uint64_t fnv_f32(uint64_t h, float v) { + uint32_t bits; + memcpy(&bits, &v, sizeof(bits)); + return fnv_bytes(h, &bits, sizeof(bits)); +} + +static inline uint64_t fnv_i32(uint64_t h, int v) { + int32_t w = (int32_t)v; + return fnv_bytes(h, &w, sizeof(w)); +} + +static inline uint64_t splitmix64(uint64_t* s) { + uint64_t z = (*s += 0x9E3779B97F4A7C15ULL); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + return z ^ (z >> 31); +} + +typedef struct { + const char* name; + const char* npc_path; + int public_start_wave; + uint32_t env_seed; + uint64_t action_seed; +} GoldenConfig; + +static void fill_actions( + const ColosseumState* s, + uint64_t* action_rng, + int actions[COLO_NUM_ACTION_HEADS] +) { + for (int head = 0; head < COLO_NUM_ACTION_HEADS; head++) { + actions[head] = + (int)(splitmix64(action_rng) % (uint64_t)COLO_ACTION_DIMS[head]); + } + if (s->modifiers.draft_pending) { + actions[COLO_HEAD_PRIMARY] = 0; + actions[COLO_HEAD_MODIFIER_SELECT] = + 1 + (int)(splitmix64(action_rng) % COLO_MODIFIER_DRAFT_OPTIONS); + } +} + +static uint64_t run_episode(const GoldenConfig* cfg, int max_ticks) { + ColosseumContext ctx; + ColosseumState s; + static float obs[COLO_NUM_OBS]; + int actions[COLO_NUM_ACTION_HEADS]; + + col_init_context_typed(&ctx); + ctx.config.start_wave = cfg->public_start_wave - 1; + ctx.config.step_out_forecast_obs_enabled = 1; + ctx.config.forecast_horizon = 4; + + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, cfg->env_seed); + + uint64_t action_rng = cfg->action_seed; + uint64_t h = FNV_OFFSET; + + for (int t = 0; t < max_ticks; t++) { + fill_actions(&s, &action_rng, actions); + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + + for (int i = 0; i < COLO_NUM_OBS; i++) h = fnv_f32(h, obs[i]); + h = fnv_f32(h, s.reward); + h = fnv_i32(h, s.wave); + h = fnv_i32(h, s.tick); + h = fnv_i32(h, s.episode_over); + h = fnv_i32(h, s.winner); + + if (s.episode_over) break; + } + + return h; +} + +static const GoldenConfig CONFIGS[] = { + { + "w01_shaman_jaguar", + "wave 1 opens a mandatory draft, then spawns warband + serpent shaman, overrun reinforces jaguar", + 1, + 0x00010001u, + 0x6a09e667f3bcc909ULL, + }, + { + "w02_javelin", + "wave 2 starts warband + serpent shaman + javelin colossus, overrun reinforces jaguar", + 2, + 0x00020003u, + 0xbb67ae8584caa73bULL, + }, + { + "w03_double_javelin", + "wave 3 starts warband + serpent shaman + two javelin colossi, overrun reinforces jaguar", + 3, + 0x00030005u, + 0x3c6ef372fe94f82bULL, + }, + { + "w04_manticore", + "wave 4 starts warband + serpent shaman + manticore, overrun reinforces jaguar + serpent shaman", + 4, + 0x00040007u, + 0xa54ff53a5f1d36f1ULL, + }, + { + "w05_javelin_manticore", + "wave 5 starts warband + serpent shaman + javelin colossus + manticore, overrun reinforces jaguar + serpent shaman", + 5, + 0x0005000bu, + 0x510e527fade682d1ULL, + }, + { + "w06_double_javelin_manticore", + "wave 6 starts warband + serpent shaman + two javelin colossi + manticore, overrun reinforces jaguar + serpent shaman", + 6, + 0x0006000du, + 0x9b05688c2b3e6c1fULL, + }, + { + "w07_shockwave", + "wave 7 starts warband + javelin colossus + manticore + shockwave colossus, overrun reinforces minotaur", + 7, + 0x00070011u, + 0x1f83d9abfb41bd6bULL, + }, + { + "w08_double_javelin_shockwave", + "wave 8 starts warband + two javelin colossi + manticore + shockwave colossus, overrun reinforces minotaur", + 8, + 0x00080013u, + 0x5be0cd19137e2179ULL, + }, + { + "w09_double_manticore", + "wave 9 starts warband + javelin colossus + two manticores, overrun reinforces minotaur", + 9, + 0x00090017u, + 0xcbbb9d5dc1059ed8ULL, + }, + { + "w10_heavy_mix", + "wave 10 starts warband + two javelin colossi + two manticores, overrun reinforces minotaur + serpent shaman", + 10, + 0x000a001du, + 0x629a292a367cd507ULL, + }, + { + "w11_shockwave_heavy_mix", + "wave 11 starts warband + javelin colossus + two manticores + shockwave colossus, overrun reinforces minotaur + serpent shaman", + 11, + 0x000b001fu, + 0x9159015a3070dd17ULL, + }, + { + "w12_sol", + "wave 12 starts Sol Heredit in the boss arena", + 12, + 0x000c0025u, + 0x152fecd8f70e5939ULL, + }, +}; + +#define NUM_CONFIGS ((int)(sizeof(CONFIGS) / sizeof(CONFIGS[0]))) +#define EPISODE_TICKS 4000 + +static const uint64_t BASELINE[NUM_CONFIGS] = { + 0x6b648dbd26450b82ULL, + 0xd25dd5f73aea2df6ULL, + 0x137d8011443b61f8ULL, + 0xeb82de91da34947dULL, + 0xf10a60721a18d0ecULL, + 0xef3182c4eebbaeb5ULL, + 0x63f92c48e77deeecULL, + 0x4e15c78ede861b01ULL, + 0x4b420b3e18d846ebULL, + 0x054f146530962087ULL, + 0x4b5835b98fc7d4b0ULL, + 0xae22f0ac054585aaULL, +}; + +int main(int argc, char** argv) { + int print_mode = (argc > 1 && strcmp(argv[1], "--print") == 0); + + col_build_npc_stats(); + + printf("colosseum golden-master (%d configs, <=%d ticks each)\n\n", + NUM_CONFIGS, EPISODE_TICKS); + + int failed = 0; + for (int c = 0; c < NUM_CONFIGS; c++) { + uint64_t h = run_episode(&CONFIGS[c], EPISODE_TICKS); + if (print_mode) { + printf(" 0x%016llxULL,\n", (unsigned long long)h); + } else { + int ok = h == BASELINE[c]; + printf(" %-28s 0x%016llx %s\n", + CONFIGS[c].name, (unsigned long long)h, ok ? "PASS" : "FAIL"); + if (!ok) { + printf(" expected 0x%016llx\n", + (unsigned long long)BASELINE[c]); + failed++; + } + } + } + + if (print_mode) { + printf("\nconfig coverage:\n"); + for (int c = 0; c < NUM_CONFIGS; c++) { + printf(" %-28s %s\n", CONFIGS[c].name, CONFIGS[c].npc_path); + } + printf("\npaste the array above into BASELINE[].\n"); + return 0; + } + + printf("\n%d/%d configs match baseline\n", NUM_CONFIGS - failed, NUM_CONFIGS); + return failed > 0 ? 1 : 0; +} diff --git a/ocean/osrs/tests/test_colosseum_modifiers.c b/ocean/osrs/tests/test_colosseum_modifiers.c new file mode 100644 index 0000000000..cfe0404587 --- /dev/null +++ b/ocean/osrs/tests/test_colosseum_modifiers.c @@ -0,0 +1,7719 @@ +#include +#include +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +#define col_init_context_typed(ctx_ptr) do { \ + col_init_context_typed(ctx_ptr); \ + (ctx_ptr)->config.late_start_state_mode = 0; \ +} while (0) + +#include "ocean/osrs/tests/osrs_test_check.h" + +#define TEST_NPC_TELLS_OFFSET 26 +#define TEST_MOD_HAZARD_BASE (COLO_OBS_AFTER_NPCS + COLO_MODIFIER_FLAGS_OBS_SIZE) +#define TEST_MOD_OBS_DOOM_LETHAL (TEST_MOD_HAZARD_BASE + 2) +#define TEST_MOD_OBS_VENOM_TIMER (TEST_MOD_HAZARD_BASE + 6) +#define TEST_MOD_OBS_SOLARFLARE (TEST_MOD_HAZARD_BASE + 10) +#define TEST_MOD_OBS_MOLTEN (TEST_MOD_HAZARD_BASE + 18) +#define TEST_MOD_OBS_VOLATILITY (TEST_MOD_HAZARD_BASE + 30) + +static EncounterLoadoutStats test_col_live_stats_for_set( + const ColosseumState* s, + ColoWeaponSet set +); +static OsrsEquipmentEffectProfile test_col_live_effects_for_set( + const ColosseumState* s, + ColoWeaponSet set +); +static EncounterLoadoutStats test_col_spec_stats_for_kind( + const ColosseumState* s, + int kind +); +static void venator_spawn_enemy( + ColosseumState* s, + int slot, + ColoNpcType type, + int x, + int y, + int size +); + +static void step_and_observe(ColosseumState* s, ColosseumContext* ctx, const int* actions) { + static float obs[COLO_NUM_OBS]; + static float mask[COLO_ACTION_MASK_SIZE]; + col_step_ctx((EncounterState*)s, (EncounterContext*)ctx, actions); + col_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); + col_write_mask_ctx((EncounterState*)s, (EncounterContext*)ctx, mask); +} + +static void advance_to_wave_spawn(ColosseumState* s, ColosseumContext* ctx) { + int idle[COLO_NUM_ACTION_HEADS] = {0}; + + if (s->modifiers.draft_pending) { + s->modifiers.draft_pending = 0; + s->modifiers.draft_gates_spawn = 0; + s->modifiers.draft_free_movement = 0; + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + s->modifiers.draft_options[o] = -1; + s->wave_spawn_delay = col_wave_entry_delay_ticks(s->wave_spawn_target); + } + while (s->wave_spawn_delay > 0) + step_and_observe(s, ctx, idle); +} + +static int draft_is_open(const ColosseumState* s) { + if (!s->modifiers.draft_pending) return 0; + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + if (s->modifiers.draft_options[o] >= 0) return 1; + return 0; +} + +static void complete_open_draft(ColosseumState* s, ColosseumContext* ctx, int option) { + if (!draft_is_open(s)) return; + int pick[COLO_NUM_ACTION_HEADS] = {0}; + pick[COLO_HEAD_MODIFIER_SELECT] = option + 1; + step_and_observe(s, ctx, pick); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + while (s->wave_spawn_delay > 0) step_and_observe(s, ctx, idle); +} + +static void force_clear_wave(ColosseumState* s) { + for (int i = 0; i < COLO_MAX_NPCS; i++) { + if (!s->npcs[i].active) continue; + if (col_type_is_hazard_entity(s->npcs[i].type)) continue; + s->npcs[i].hp = 0; + s->npcs[i].active = 0; + } +} + +static int first_live_score_enemy(const ColosseumState* s) { + for (int i = 0; i < COLO_MAX_NPCS; i++) { + if (col_npc_is_live_enemy(&s->npcs[i])) return i; + } + return -1; +} + +static void kill_first_live_score_enemy(ColosseumState* s) { + int slot = first_live_score_enemy(s); + CHECK("a live score enemy exists", slot >= 0); + if (slot < 0) return; + s->npcs[slot].hp = 0; + col_apply_npc_death(s, slot); +} + +static float score_for_depth(float depth) { + float ratio = depth / (float)COLO_NUM_WAVES; + return 0.99f * ratio * ratio; +} + +static void land_pending_player_hits(ColosseumState* s) { + for (int t = 0; t < 4; t++) col_resolve_player_projectiles_on_npcs(s); +} + +static void geo_clear_npcs(ColosseumState* s) { + memset(s->npcs, 0, sizeof(s->npcs)); + memset(s->npc_collision_flags, 0, sizeof(s->npc_collision_flags)); + memset(s->totems, 0, sizeof(s->totems)); + memset(s->bees, 0, sizeof(s->bees)); + col_rebuild_player_collision_flags(s); +} + +static void init_forecast_test_state( + ColosseumState* s, + ColosseumContext* ctx, + uint32_t seed, + int player_x, + int player_y +) { + col_init_context_typed(ctx); + memset(s, 0, sizeof(*s)); + col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); + geo_clear_npcs(s); + s->modifiers.draft_pending = 0; + s->player.x = player_x; + s->player.y = player_y; + col_rebuild_player_collision_flags(s); +} + +static int forecast_move_action_for_delta(int dx, int dy) { + for (int action = 0; action < ENCOUNTER_MOVE_ACTIONS; action++) + if (ENCOUNTER_MOVE_TARGET_DX[action] == dx && + ENCOUNTER_MOVE_TARGET_DY[action] == dy) return action; + assert(0 && "missing movement action delta"); + return 0; +} + +static int forecast_action_has_event(const ColoStepOutForecastAction* action) { + for (int tick = 0; tick < COLO_STEP_OUT_FORECAST_HORIZON; tick++) + if (col_step_out_forecast_tick_has_event(&action->ticks[tick])) return 1; + return 0; +} + +static int test_find_inventory_cell_with_item(const ColosseumState* s, uint8_t item_idx) { + for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) + if (s->inventory_cells[i].item_idx == item_idx) return i; + return -1; +} + +static int test_find_inventory_cell_with_consumable( + const ColosseumState* s, + OsrsConsumableKind kind +) { + for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { + const ColoInvCell* cell = &s->inventory_cells[i]; + OsrsInventoryClickResolution r = + osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); + if (r.consumable_kind == kind) return i; + } + return -1; +} + +static void test_click_inventory_cell_action_s( + const ColosseumState* s, int actions[COLO_NUM_ACTION_HEADS], int cell +) { + assert(cell >= 0 && cell < COLO_INVENTORY_DISPLAY_SLOTS); + OsrsInventoryClickResolution r = + osrs_inventory_cell_click_interpret(&s->inventory_cells[cell], OSRS_CLICK_TICK_FIRST); + if (r.click_action == OSRS_CLICK_EQUIP) { + int slot = osrs_item_gear_slot(s->inventory_cells[cell].item_idx); + assert(slot >= 0 && slot < NUM_GEAR_SLOTS); + actions[COLO_HEAD_EQUIP_SLOT(slot)] = cell + 1; + } else if (r.click_action == OSRS_CLICK_EAT) { + actions[COLO_HEAD_EAT] = cell + 1; + } else if (r.click_action == OSRS_CLICK_DRINK) { + actions[COLO_HEAD_DRINK] = cell + 1; + } else { + assert(0 && "test clicked a non-actionable cell"); + } +} + +static void test_click_consumable_action( + const ColosseumState* s, + int actions[COLO_NUM_ACTION_HEADS], + OsrsConsumableKind kind +) { + test_click_inventory_cell_action_s(s, actions, test_find_inventory_cell_with_consumable(s, kind)); +} + +static float test_click_mask_for_cell_s( + const ColosseumState* s, const float mask[COLO_ACTION_MASK_SIZE], int cell +) { + OsrsInventoryClickResolution r = + osrs_inventory_cell_click_interpret(&s->inventory_cells[cell], OSRS_CLICK_TICK_FIRST); + int head; + if (r.click_action == OSRS_CLICK_EQUIP) { + int slot = osrs_item_gear_slot(s->inventory_cells[cell].item_idx); + head = (slot >= 0 && slot < NUM_GEAR_SLOTS) ? COLO_HEAD_EQUIP_SLOT(slot) + : COLO_HEAD_EQUIP_SLOT(0); + } else if (r.click_action == OSRS_CLICK_EAT) { + head = COLO_HEAD_EAT; + } else if (r.click_action == OSRS_CLICK_DRINK) { + head = COLO_HEAD_DRINK; + } else { + head = COLO_HEAD_EQUIP_SLOT(0); + } + return mask[col_action_head_mask_offset(head) + 1 + cell]; +} + +static int test_sum_inventory_doses_for_kind( + const ColosseumState* s, + OsrsConsumableKind kind +) { + int doses = 0; + for (int i = 0; i < COLO_INVENTORY_DISPLAY_SLOTS; i++) { + const ColoInvCell* cell = &s->inventory_cells[i]; + OsrsInventoryClickResolution r = + osrs_inventory_cell_click_interpret(cell, OSRS_CLICK_TICK_FIRST); + if (r.consumable_kind == kind) doses += cell->dose; + } + return doses; +} + +static int test_aggregate_doses_for_kind( + const ColosseumState* s, + OsrsConsumableKind kind +) { + switch (kind) { + case OSRS_CONSUMABLE_BREW: + return s->player.brew_doses; + case OSRS_CONSUMABLE_SUPER_RESTORE: + case OSRS_CONSUMABLE_SANFEW: + return s->player.restore_doses; + case OSRS_CONSUMABLE_SUPER_COMBAT: + case OSRS_CONSUMABLE_DIVINE_COMBAT: + return s->player.combat_potion_doses; + case OSRS_CONSUMABLE_RANGING: + case OSRS_CONSUMABLE_DIVINE_RANGING: + return s->player.ranged_potion_doses; + case OSRS_CONSUMABLE_SURGE: + return s->surge_doses; + case OSRS_CONSUMABLE_ANTIVENOM_PLUS: + return s->player.antivenom_doses; + case OSRS_CONSUMABLE_GUTHIX_REST: + case OSRS_CONSUMABLE_SATURATED_HEART: + case OSRS_CONSUMABLE_NONE: + case OSRS_CONSUMABLE_SHARK_FOOD: + case OSRS_CONSUMABLE_KARAMBWAN: + return -1; + } + abort(); +} + +static void test_prepare_for_drink_kind( + ColosseumState* s, + OsrsConsumableKind kind +) { + s->player.potion_timer = 0; + s->player.current_hitpoints = 50; + s->player.current_prayer = 40; + s->player.special_energy = 50; + s->surge_cooldown = 0; + s->player_venom = COLO_VENOM_START; + s->player_venom_timer = 17; + s->player_poison = COLO_POISON_BEE_CONTACT_SEVERITY; + s->player_poison_timer = 11; + if (kind == OSRS_CONSUMABLE_DIVINE_COMBAT || + kind == OSRS_CONSUMABLE_DIVINE_RANGING) { + s->player.current_hitpoints = 99; + } +} + +typedef enum { + TEST_INV_OBS_ROLE_ARMOR = 12, + TEST_INV_OBS_ROLE_WEAPON = 13, + TEST_INV_OBS_KIND_BREW = 14, + TEST_INV_OBS_KIND_RESTORE = 15, + TEST_INV_OBS_KIND_COMBAT_BOOST = 16, + TEST_INV_OBS_KIND_RANGED_BOOST = 17, + TEST_INV_OBS_KIND_SPECIAL = 18, + TEST_INV_OBS_EFFECT_LIFESTEAL = 22, + TEST_INV_OBS_EFFECT_DAMAGE_AMP = 23, + TEST_INV_OBS_EFFECT_DEFENSIVE = 24, + TEST_INV_OBS_EFFECT_UTIL = 25, +} TestInventoryObsFeature; + +typedef struct { + float is_gear; + float is_consumable; + float can_use; + float has_effect; + float role_food; + float role_potion_family; + float kind_food; +} TestDroppedInventoryFields; + +static float test_binary_float(int value) { + return value ? 1.0f : 0.0f; +} + +static float test_any_inventory_kind_bit(const float* cell_obs) { + return test_binary_float( + cell_obs[TEST_INV_OBS_KIND_BREW] != 0.0f || + cell_obs[TEST_INV_OBS_KIND_RESTORE] != 0.0f || + cell_obs[TEST_INV_OBS_KIND_COMBAT_BOOST] != 0.0f || + cell_obs[TEST_INV_OBS_KIND_RANGED_BOOST] != 0.0f || + cell_obs[TEST_INV_OBS_KIND_SPECIAL] != 0.0f); +} + +static TestDroppedInventoryFields test_expected_dropped_inventory_fields( + const ColosseumState* s, + int cell_idx +) { + const ColoInvCell* cell = &s->inventory_cells[cell_idx]; + OsrsConsumableClick consumable = + osrs_consumable_click_lookup_raw_osrs_id(cell->raw_osrs_id); + uint32_t effect_mask = OSRS_ITEM_EFFECT_NONE; + if (cell->item_idx != ITEM_NONE) { + if (cell->item_idx >= NUM_ITEMS) abort(); + effect_mask = ITEM_DATABASE[cell->item_idx].effect_mask; + } + OsrsConsumableKind6 k6 = col_consumable_kind6(consumable.consumable_kind); + + return (TestDroppedInventoryFields){ + .is_gear = test_binary_float(cell->item_idx != ITEM_NONE), + .is_consumable = test_binary_float(consumable.click_action != OSRS_CLICK_NONE), + .can_use = test_binary_float(col_inventory_cell_actionable(s, cell_idx)), + .has_effect = test_binary_float(effect_mask != OSRS_ITEM_EFFECT_NONE), + .role_food = test_binary_float(consumable.click_action == OSRS_CLICK_EAT), + .role_potion_family = test_binary_float(consumable.click_action == OSRS_CLICK_DRINK), + .kind_food = test_binary_float(k6 == COL_CKIND6_FOOD), + }; +} + +static TestDroppedInventoryFields test_reconstructed_dropped_inventory_fields( + const ColosseumState* s, + const float obs[COLO_NUM_OBS], + const float mask[COLO_ACTION_MASK_SIZE], + int cell_idx +) { + int base = COLO_OBS_AFTER_PILLARS + + cell_idx * COLO_INVENTORY_CELL_OBS_FEATURES; + const float* cell_obs = &obs[base]; + float kind = test_any_inventory_kind_bit(cell_obs); + float effect = test_binary_float( + cell_obs[TEST_INV_OBS_EFFECT_LIFESTEAL] != 0.0f || + cell_obs[TEST_INV_OBS_EFFECT_DAMAGE_AMP] != 0.0f || + cell_obs[TEST_INV_OBS_EFFECT_DEFENSIVE] != 0.0f || + cell_obs[TEST_INV_OBS_EFFECT_UTIL] != 0.0f); + + return (TestDroppedInventoryFields){ + .is_gear = test_binary_float( + cell_obs[TEST_INV_OBS_ROLE_ARMOR] != 0.0f || + cell_obs[TEST_INV_OBS_ROLE_WEAPON] != 0.0f), + .is_consumable = kind, + .can_use = test_click_mask_for_cell_s(s, mask, cell_idx), + .has_effect = effect, + .role_food = 0.0f, + .role_potion_family = kind, + .kind_food = 0.0f, + }; +} + +static void test_check_inventory_dropped_field( + const char* scenario, + int cell_idx, + const char* field, + float expected, + float reconstructed +) { + char label[240]; + snprintf(label, sizeof(label), "%s cell %d reconstructs %s", scenario, cell_idx, field); + CHECK(label, reconstructed == expected); +} + +static void test_check_inventory_cut_equivalence_state( + ColosseumState* s, + ColosseumContext* ctx, + const char* scenario +) { + ctx->config.mask_inventory_heads = 0; + static float obs[COLO_NUM_OBS]; + static float mask[COLO_ACTION_MASK_SIZE]; + col_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); + col_write_mask_ctx((EncounterState*)s, (EncounterContext*)ctx, mask); + + for (int cell = 0; cell < COLO_INVENTORY_DISPLAY_SLOTS; cell++) { + TestDroppedInventoryFields expected = + test_expected_dropped_inventory_fields(s, cell); + TestDroppedInventoryFields reconstructed = + test_reconstructed_dropped_inventory_fields(s, obs, mask, cell); + test_check_inventory_dropped_field( + scenario, cell, "is_gear", expected.is_gear, reconstructed.is_gear); + test_check_inventory_dropped_field( + scenario, cell, "is_consumable", + expected.is_consumable, reconstructed.is_consumable); + test_check_inventory_dropped_field( + scenario, cell, "can_use", expected.can_use, reconstructed.can_use); + test_check_inventory_dropped_field( + scenario, cell, "has_effect", expected.has_effect, reconstructed.has_effect); + test_check_inventory_dropped_field( + scenario, cell, "role_food", expected.role_food, reconstructed.role_food); + test_check_inventory_dropped_field( + scenario, cell, "role_potion_family", + expected.role_potion_family, reconstructed.role_potion_family); + test_check_inventory_dropped_field( + scenario, cell, "kind_food", expected.kind_food, reconstructed.kind_food); + } +} + +static int test_count_item_in_equipment_and_inventory( + const ColosseumState* s, + uint8_t item_idx +) { + int count = 0; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) + if (s->player.equipped[slot] == item_idx) count++; + for (int slot = 0; slot < COLO_INVENTORY_DISPLAY_SLOTS; slot++) + if (s->inventory_cells[slot].item_idx == item_idx) count++; + return count; +} + +static int test_npc_covers_player(const ColosseumState* s, const ColoNPC* npc) { + int size = col_npc_effective_size(npc); + return s->player.x >= npc->x && s->player.x < npc->x + size && + s->player.y >= npc->y && s->player.y < npc->y + size; +} + +static uint8_t test_profile_spec_item(ColoLoadoutProfile profile, int kind) { + if (profile == COLO_LOADOUT_PROFILE_SPEEDRUN && kind == 1) return ITEM_DRAGON_CLAWS; + if (profile == COLO_LOADOUT_PROFILE_SPEEDRUN && kind == 2) return ITEM_ELDER_MAUL; + if (profile == COLO_LOADOUT_PROFILE_BEGINNER && kind == 1) return ITEM_SGS; + if (profile == COLO_LOADOUT_PROFILE_BEGINNER && kind == 2) return ITEM_DRAGON_CLAWS; + abort(); +} + +static void test_fuzz_obs_mask(void) { + printf("test_fuzz_obs_mask\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + ctx.world_offset_x = 1808; + ctx.world_offset_y = 3090; + ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_MIXED; + ctx.config.beginner_loadout_fraction = 0.5f; + + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 12345); + + int episodes = 0; + int actions[COLO_NUM_ACTION_HEADS]; + unsigned int rng = 99; + for (int start = 1; start <= COLO_NUM_WAVES; start++) { + ctx.config.start_wave = start - 1; + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, (uint32_t)(start * 7 + 1)); + for (long t = 0; t < 120000 && episodes < 600; t++) { + for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) { + rng = rng * 1103515245u + 12345u; + actions[h] = (int)((rng >> 16) % (unsigned)COLO_ACTION_DIMS[h]); + } + step_and_observe(&s, &ctx, actions); + if (s.episode_over) { + episodes++; + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, (uint32_t)(rng | 1)); + } + } + } + + CHECK("fuzz ran full episodes with obs/mask asserts holding", episodes > 0); + printf(" episodes=%d (obs+mask running-index asserts held every tick)\n", episodes); +} + +static void test_zero_actions_hit_timeout(void) { + printf("test_zero_actions_hit_timeout\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 12345); + CHECK("the challenge-start draft is open at reset", s.modifiers.draft_pending == 1); + advance_to_wave_spawn(&s, &ctx); + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + force_clear_wave(&s); + step_and_observe(&s, &ctx, idle); + CHECK("clearing wave 1 opened the wave-2 draft", draft_is_open(&s)); + + long t = 0; + for (; t < s.episode_max_ticks + 10 && !s.episode_over; t++) + step_and_observe(&s, &ctx, idle); + + CHECK("all-none actions terminate at the tick cap", s.episode_over == 1); + CHECK("timeout fires exactly at the episode cap", s.tick == s.episode_max_ticks); + CHECK("timeout counts as a loss", s.winner == COLO_OUTCOME_PLAYER_DIED); + CHECK("timeout is marked as a truncation", s.time_limit_truncated == 1); + CHECK("the draft was still pending when time ran out", s.modifiers.draft_pending == 1); +} + +static void test_offpray_attribution_log(void) { + printf("test_offpray_attribution_log\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 777); + + col_log_prayer_event(&s, COLO_SERPENT_SHAMAN, 0 , 11); + CHECK("off-prayer hit counted as faced", s.log.pray_faced_by_type[COLO_SERPENT_SHAMAN] == 1.0f); + CHECK("off-prayer hit not counted correct", s.log.pray_correct_by_type[COLO_SERPENT_SHAMAN] == 0.0f); + CHECK("off-prayer damage attributed to the shaman", s.log.offpray_damage_by_type[COLO_SERPENT_SHAMAN] == 11.0f); + + col_log_prayer_event(&s, COLO_SERPENT_SHAMAN, 1 , 0); + CHECK("prayed hit counted as faced", s.log.pray_faced_by_type[COLO_SERPENT_SHAMAN] == 2.0f); + CHECK("prayed hit counted correct", s.log.pray_correct_by_type[COLO_SERPENT_SHAMAN] == 1.0f); + CHECK("prayed hit adds no off-prayer damage", s.log.offpray_damage_by_type[COLO_SERPENT_SHAMAN] == 11.0f); + + CHECK("a type that never threw stays out of the prayer log", + s.log.pray_faced_by_type[COLO_JAVELIN_COLOSSUS] == 0.0f); +} + +static void test_step_loop_draft(void) { + printf("test_step_loop_draft\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 42); + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int walk_east[COLO_NUM_ACTION_HEADS] = {0}; + walk_east[COLO_HEAD_PRIMARY] = 7; + + CHECK("the challenge-start draft is open at reset", draft_is_open(&s)); + CHECK("the start draft allows movement", s.modifiers.draft_free_movement == 1); + int spawned = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) if (s.npcs[i].active) spawned = 1; + CHECK("no NPCs exist while the start draft pends", !spawned); + int x0 = s.player.x; + step_and_observe(&s, &ctx, walk_east); + CHECK("the player roams during the start draft", s.player.x == x0 + 1); + + int pick0[COLO_NUM_ACTION_HEADS] = {0}; + pick0[COLO_HEAD_MODIFIER_SELECT] = 1; + step_and_observe(&s, &ctx, pick0); + CHECK("the start pick activated a modifier", s.modifiers.active_mask != 0); + CHECK("the start pick armed the 5-tick spawn delay", + s.wave_spawn_delay == COLO_WAVE_SPAWN_DELAY_TICKS); + + int x1 = s.player.x; + for (int t = 0; t < 4; t++) step_and_observe(&s, &ctx, walk_east); + CHECK("player movement is free before the spawn resolves", + s.player.x == x1 + 4); + spawned = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) if (s.npcs[i].active) spawned = 1; + CHECK("arena still empty through t4", !spawned); + + int t5_x = s.player.x, t5_y = s.player.y; + step_and_observe(&s, &ctx, walk_east); + spawned = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) if (s.npcs[i].active) spawned = 1; + CHECK("the wave resolved at t5 (visible from t6)", spawned); + CHECK("the t5 queued move still landed", s.player.x == t5_x + 1); + CHECK("the spawn armed the movement gate", s.wave_ready_delay == COLO_WAVE_READY_TICKS); + CHECK("the spawn armed the attack gate", + s.wave_attack_delay == COLO_WAVE_ATTACK_GATE_TICKS); + + int exclusion_ok = 1; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + const ColoNPC* npc = &s.npcs[i]; + if (!npc->active || col_type_is_warbander(npc->type)) continue; + if (col_type_is_hazard_entity(npc->type)) continue; + if (encounter_rect_distance(t5_x, t5_y, 1, + npc->x, npc->y, col_npc_effective_size(npc)) <= COLO_SPAWN_EXCLUSION_CHEB) + exclusion_ok = 0; + } + CHECK("no primary spawned within Chebyshev 4 of the pre-move t5 tile", exclusion_ok); + + int ax = -1, ay = -1, sx = -1, sy = -1, bx = -1, by = -1; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + const ColoNPC* npc = &s.npcs[i]; + if (!npc->active) continue; + if (npc->type == COLO_FREMENNIK_ARCHER) { ax = npc->x; ay = npc->y; } + if (npc->type == COLO_FREMENNIK_SEER) { sx = npc->x; sy = npc->y; } + if (npc->type == COLO_FREMENNIK_BERSERKER) { bx = npc->x; by = npc->y; } + } + CHECK("warband trio spawned", ax >= 0 && sx >= 0 && bx >= 0); + CHECK("seer spawned 2E of the archer", sx == ax + 2 && sy == ay); + CHECK("berserker spawned 1E+1N of the archer", bx == ax + 1 && by == ay + 1); + + int pos_sum_spawn = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (s.npcs[i].active) pos_sum_spawn += s.npcs[i].x * 64 + s.npcs[i].y; + step_and_observe(&s, &ctx, idle); + int pos_sum_t6 = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (s.npcs[i].active) pos_sum_t6 += s.npcs[i].x * 64 + s.npcs[i].y; + CHECK("NPCs frozen at t6", pos_sum_t6 == pos_sum_spawn); + step_and_observe(&s, &ctx, idle); + int pos_sum_t7 = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (s.npcs[i].active) pos_sum_t7 += s.npcs[i].x * 64 + s.npcs[i].y; + CHECK("NPCs started moving at t7 (warband darts to the player)", + pos_sum_t7 != pos_sum_t6); + step_and_observe(&s, &ctx, idle); + CHECK("no damage taken through t8 (attacks open t9)", + s.player.current_hitpoints == s.player.base_hitpoints); + CHECK("attack gate open from t9", s.wave_attack_delay == 1); + + force_clear_wave(&s); + step_and_observe(&s, &ctx, idle); + CHECK("clearing wave 1 opened the wave-2 draft", draft_is_open(&s)); + CHECK("the wave-2 draft gates the wave-2 spawn", s.wave_spawn_target == 1); + + int px = s.player.x; + int py = s.player.y; + for (int t = 0; t < 12; t++) step_and_observe(&s, &ctx, walk_east); + CHECK("movement is ignored while the draft is open", + s.player.x == px && s.player.y == py); + CHECK("the draft never auto-closes", draft_is_open(&s)); + float mask[COLO_ACTION_MASK_SIZE]; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + int any_walk_valid = 0; + for (int d = 1; d < ENCOUNTER_MOVE_ACTIONS; d++) + if (mask[d] > 0.0f) any_walk_valid = 1; + CHECK("the mask offers only idle movement while frozen", !any_walk_valid); + + int chosen = s.modifiers.draft_options[0]; + int pick[COLO_NUM_ACTION_HEADS] = {0}; + pick[COLO_HEAD_MODIFIER_SELECT] = 1; + step_and_observe(&s, &ctx, pick); + CHECK("the pick activated the chosen modifier", + chosen >= 0 && col_mod_active(&s, (ColoModifier)chosen)); + CHECK("draft closed after the pick", !s.modifiers.draft_pending); + CHECK("the pick armed the 5-tick spawn delay", + s.wave_spawn_delay == COLO_WAVE_SPAWN_DELAY_TICKS); + for (int t = 0; t < 4; t++) step_and_observe(&s, &ctx, idle); + spawned = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) if (s.npcs[i].active) spawned = 1; + CHECK("no wave-2 NPCs through the 4 post-pick ticks", !spawned); + step_and_observe(&s, &ctx, idle); + spawned = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) if (s.npcs[i].active) spawned = 1; + CHECK("wave 2 resolved on the 5th post-pick tick", spawned); + CHECK("advanced to wave 2 after the pick", s.wave == 1); + CHECK("the spawn re-armed the movement + attack gates", + s.wave_ready_delay == COLO_WAVE_READY_TICKS && + s.wave_attack_delay == COLO_WAVE_ATTACK_GATE_TICKS); +} + +static void test_eleven_drafts_per_run(void) { + printf("test_eleven_drafts_per_run\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 4242); + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int picks = 0; + int draft_waves_ok = 1; + for (long t = 0; t < 4000 && !s.episode_over; t++) { + if (draft_is_open(&s)) { + + if (s.wave_spawn_target != picks) draft_waves_ok = 0; + complete_open_draft(&s, &ctx, 0); + picks++; + continue; + } + force_clear_wave(&s); + + s.player.current_hitpoints = s.player.base_hitpoints; + s.doom_stacks = 0; + step_and_observe(&s, &ctx, idle); + } + CHECK("the run ended in victory", s.episode_over && s.winner == COLO_OUTCOME_PLAYER_WON); + CHECK("exactly 12 drafts were offered and picked", picks == 12); + CHECK("the log counted all 12 mandatory picks", s.log.modifiers_picked == 12); + CHECK("draft k gated wave index k for every k", draft_waves_ok); +} + +static void test_draft_offer_and_select(void) { + printf("test_draft_offer_and_select\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 1; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 7); + + CHECK("fresh run has no active modifiers", s.modifiers.active_mask == 0); + CHECK("start_wave>1 runs skip the prior drafts (no draft open)", + s.modifiers.draft_pending == 0); + + col_modifier_open_draft(&s, 4); + CHECK("draft opened with options", draft_is_open(&s)); + int distinct = 1; + int a = s.modifiers.draft_options[0], b = s.modifiers.draft_options[1], c = s.modifiers.draft_options[2]; + if (a == b || a == c || (b == c && b >= 0)) distinct = 0; + CHECK("draft options are distinct", distinct); + + int chosen = s.modifiers.draft_options[0]; + col_modifier_apply_selection(&s, 0); + CHECK("selection set the active bit", col_mod_active(&s, (ColoModifier)chosen)); + CHECK("selection set tier >= 1", col_mod_tier(&s, (ColoModifier)chosen) >= 1); + CHECK("draft closed after selection", s.modifiers.draft_pending == 0); + CHECK("selection logged", s.log.modifiers_picked == 1); + + s.wave = 1; + col_spawn_wave(&s); + CHECK("modifier persists across waves", col_mod_active(&s, (ColoModifier)chosen)); + + int rfdd_late = 0, rfdd_window = 0, boss_excluded_seen = 0; + for (int rep = 0; rep < 400; rep++) { + int late_wave = 7 + rep % 4; + col_modifier_open_draft(&s, late_wave); + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { + int m = s.modifiers.draft_options[o]; + if (m == COLO_MOD_RED_FLAG || m == COLO_MOD_DYNAMIC_DUO) rfdd_late = 1; + } + col_modifier_open_draft(&s, 2 + rep % 5); + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { + int m = s.modifiers.draft_options[o]; + if (m == COLO_MOD_RED_FLAG || m == COLO_MOD_DYNAMIC_DUO) rfdd_window = 1; + } + col_modifier_open_draft(&s, COLO_WAVE_BOSS); + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { + int m = s.modifiers.draft_options[o]; + if (m >= 0 && COLO_MODIFIER_PRE_BOSS_ONLY[m]) boss_excluded_seen = 1; + } + } + s.modifiers.draft_pending = 0; + CHECK("Red Flag / Dynamic Duo never offered into wave 8+", rfdd_late == 0); + CHECK("Red Flag / Dynamic Duo do appear in drafts before wave 7", rfdd_window == 1); + CHECK("the wave-12 draft excludes RF/DD/Mantimayhem/Reentry", boss_excluded_seen == 0); +} + +static void test_draft_upgrade_bias(void) { + printf("test_draft_upgrade_bias\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 1; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 11); + + s.modifiers.active_mask |= (1u << COLO_MOD_RELENTLESS); + s.modifiers.tier[COLO_MOD_RELENTLESS] = 1; + + const int N = 4000; + int owned_offers = 0, unowned_offers = 0; + for (int rep = 0; rep < N; rep++) { + col_modifier_open_draft(&s, 4); + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) { + int m = s.modifiers.draft_options[o]; + if (m == COLO_MOD_RELENTLESS) owned_offers++; + if (m == COLO_MOD_DOOM) unowned_offers++; + } + s.modifiers.draft_pending = 0; + } + CHECK("both modifiers appear across the sample", owned_offers > 0 && unowned_offers > 0); + CHECK("the owned T1 modifier is offered with clearly elevated frequency (~2x weight)", + owned_offers * 2 > unowned_offers * 3); + printf(" owned=%d unowned=%d over %d drafts\n", owned_offers, unowned_offers, N); + + s.modifiers.tier[COLO_MOD_RELENTLESS] = 3; + int maxed_seen = 0; + for (int rep = 0; rep < 200; rep++) { + col_modifier_open_draft(&s, 4); + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + if (s.modifiers.draft_options[o] == COLO_MOD_RELENTLESS) maxed_seen = 1; + s.modifiers.draft_pending = 0; + } + CHECK("a maxed modifier is never offered again", maxed_seen == 0); +} + +static void test_frailty_hp(void) { + printf("test_frailty_hp\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 1); + CHECK("base HP is 99 with no Frailty", s.player.base_hitpoints == 99); + + s.modifiers.active_mask |= (1u << COLO_MOD_FRAILTY); + s.modifiers.tier[COLO_MOD_FRAILTY] = 3; + col_mod_apply_frailty_hp(&s); + CHECK("Frailty III cuts max HP to 60", s.player.base_hitpoints == 60); + CHECK("current HP clamped to new max", s.player.current_hitpoints <= 60); + + s.modifiers.tier[COLO_MOD_FRAILTY] = 1; + col_mod_apply_frailty_hp(&s); + CHECK("Frailty I cuts max HP to 90", s.player.base_hitpoints == 90); +} + +static void test_relentless_damage(void) { + printf("test_relentless_damage\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 3); + + const ColoNpcStats* berserker = &COLO_NPC_STATS[COLO_FREMENNIK_BERSERKER]; + long base_total = 0, relentless_total = 0; + int base_hits = 0, relentless_hits = 0; + const int N = 20000; + + s.modifiers.active_mask = 0; + s.modifiers.tier[COLO_MOD_RELENTLESS] = 0; + for (int i = 0; i < N; i++) { + int hit = 0; + int dmg = col_npc_roll_vs_player(&s, berserker, ATTACK_STYLE_MELEE, berserker->max_hit, &hit); + base_total += dmg; + base_hits += hit; + } + + s.modifiers.active_mask |= (1u << COLO_MOD_RELENTLESS); + s.modifiers.tier[COLO_MOD_RELENTLESS] = 3; + for (int i = 0; i < N; i++) { + int hit = 0; + int dmg = col_npc_roll_vs_player(&s, berserker, ATTACK_STYLE_MELEE, berserker->max_hit, &hit); + relentless_total += dmg; + relentless_hits += hit; + } + + CHECK("Relentless III forces every hit to land", relentless_hits == N); + CHECK("Relentless III lands more often than baseline", relentless_hits > base_hits); + CHECK("Relentless raises mean incoming damage", relentless_total > base_total); + printf(" base_hits=%d/%d relentless_hits=%d/%d base_dmg=%ld relentless_dmg=%ld\n", + base_hits, N, relentless_hits, N, base_total, relentless_total); +} + +static void test_quartet_extra_spawn(void) { + printf("test_quartet_extra_spawn\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + + ColosseumState base; + memset(&base, 0, sizeof(base)); + col_reset_ctx((EncounterState*)&base, (EncounterContext*)&ctx, 5); + base.wave = 0; + col_spawn_wave(&base); + int base_count = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) if (base.npcs[i].active) base_count++; + + ColosseumState q; + memset(&q, 0, sizeof(q)); + col_reset_ctx((EncounterState*)&q, (EncounterContext*)&ctx, 5); + q.modifiers.active_mask |= (1u << COLO_MOD_QUARTET); + q.modifiers.tier[COLO_MOD_QUARTET] = 1; + q.wave = 0; + col_spawn_wave(&q); + int q_count = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) if (q.npcs[i].active) q_count++; + + CHECK("Quartet spawns one extra NPC at wave start", q_count == base_count + 1); + + ColosseumState q12; + memset(&q12, 0, sizeof(q12)); + col_reset_ctx((EncounterState*)&q12, (EncounterContext*)&ctx, 5); + q12.modifiers.active_mask |= (1u << COLO_MOD_QUARTET); + q12.modifiers.tier[COLO_MOD_QUARTET] = 1; + q12.wave = COLO_WAVE_BOSS; + col_spawn_wave(&q12); + int sol = 0, warband = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + if (!q12.npcs[i].active) continue; + if (q12.npcs[i].type == COLO_SOL_HEREDIT) sol++; + else warband++; + } + CHECK("wave 12 has Sol", sol == 1); + CHECK("Quartet adds a warbander on wave 12", warband == 1); +} + +static void test_bees_hazard(void) { + printf("test_bees_hazard\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 11); + advance_to_wave_spawn(&s, &ctx); + s.modifiers.active_mask |= (1u << COLO_MOD_BEES); + s.modifiers.tier[COLO_MOD_BEES] = 2; + s.wave = 0; + col_spawn_wave(&s); + + int bee_npcs = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (s.npcs[i].active && s.npcs[i].type == COLO_BEE_SWARM) bee_npcs++; + CHECK("Bees II fields two 1-HP bee NPCs", bee_npcs == 2 && + s.bees[0].phase == COLO_HAZARD_ALIVE && s.bees[1].phase == COLO_HAZARD_ALIVE); + CHECK("a bee NPC has exactly 1 HP", s.npcs[s.bees[0].npc_slot].hp == 1); + CHECK("a bee swarm uses its cache 2x2 footprint", s.npcs[s.bees[0].npc_slot].size == 2); + + ColoNPC* bee_npc = &s.npcs[s.bees[0].npc_slot]; + int bx = bee_npc->x, by = bee_npc->y; + for (int t = 0; t < COLO_BEE_MOVE_INTERVAL - 1; t++) col_mod_tick_bees(&s); + CHECK("the swarm holds for 11 ticks", bee_npc->x == bx && bee_npc->y == by); + col_mod_tick_bees(&s); + int stepped = abs(bee_npc->x - bx) <= 1 && abs(bee_npc->y - by) <= 1 && + (bee_npc->x != bx || bee_npc->y != by); + CHECK("the 12th tick steps one tile (diagonal allowed) toward the player", stepped); + + int all_tiles_poison = 1; + for (int dx = 0; dx < 2; dx++) { + for (int dy = 0; dy < 2; dy++) { + bee_npc->x = 10; + bee_npc->y = 10; + s.player.x = 10 + dx; + s.player.y = 10 + dy; + s.player_poison = 0; + s.player_poison_timer = 0; + s.bees[0].move_timer = COLO_BEE_MOVE_INTERVAL; + col_mod_tick_bees(&s); + if (s.player_poison != COLO_POISON_BEE_CONTACT_SEVERITY || + s.player_poison_timer != COLO_POISON_INTERVAL) + all_tiles_poison = 0; + } + } + CHECK("bee contact applies poison from all four footprint tiles", all_tiles_poison); + + bee_npc->x = s.player.x; + bee_npc->y = s.player.y; + s.player.prayer = PRAYER_PROTECT_MELEE; + int damaged = 0; + for (int t = 0; t < 64 && !damaged; t++) { + bee_npc->x = s.player.x; + bee_npc->y = s.player.y; + s.player.current_hitpoints = 99; + col_mod_tick_bees(&s); + if (s.player.current_hitpoints < 99) damaged = 1; + } + CHECK("a swarm beneath the player deals unblockable damage", damaged); + + s.wave = COLO_WAVE_BOSS; + col_sol_begin_boss_arena(&s); + bee_npc->x = COLO_BOSS_ARENA_MIN_X; + bee_npc->y = COLO_BOSS_ARENA_MIN_Y + 2; + s.player.x = COLO_BOSS_ARENA_MIN_X - 3; + s.player.y = bee_npc->y; + s.bees[0].move_timer = 1; + col_mod_tick_bees(&s); + CHECK("bee movement ignores the wave-12 boss-box clamp", + bee_npc->x == COLO_BOSS_ARENA_MIN_X - 1); + s.wave = 0; + s.sol = (SolHereditState){0}; + s.player.x = bee_npc->x; + s.player.y = bee_npc->y; + + int slot = s.bees[0].npc_slot; + col_player_attack_target(&s, slot); + land_pending_player_hits(&s); + CHECK("a single hit kills the swarm", !s.npcs[slot].active); + CHECK("the killed swarm enters its 50-tick respawn", + s.bees[0].phase == COLO_HAZARD_RESPAWNING && + s.bees[0].respawn_timer == COLO_BEE_RESPAWN_TICKS); + for (int t = 0; t < COLO_BEE_RESPAWN_TICKS - 1; t++) col_mod_tick_bees(&s); + CHECK("still respawning one tick early", s.bees[0].phase == COLO_HAZARD_RESPAWNING); + col_mod_tick_bees(&s); + CHECK("the swarm respawns exactly 50 ticks after death", + s.bees[0].phase == COLO_HAZARD_ALIVE && + s.npcs[s.bees[0].npc_slot].active && + s.npcs[s.bees[0].npc_slot].type == COLO_BEE_SWARM); + + ColosseumState sc; + memset(&sc, 0, sizeof(sc)); + ctx.config.start_wave = 0; + col_reset_ctx((EncounterState*)&sc, (EncounterContext*)&ctx, 13); + sc.modifiers.draft_pending = 0; + sc.modifiers.draft_gates_spawn = 0; + sc.modifiers.draft_free_movement = 0; + sc.modifiers.active_mask |= (1u << COLO_MOD_BEES); + sc.modifiers.tier[COLO_MOD_BEES] = 1; + sc.wave = 0; + col_spawn_wave(&sc); + sc.wave_spawn_delay = 0; + int live_bee = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (sc.npcs[i].active && sc.npcs[i].type == COLO_BEE_SWARM) live_bee = 1; + CHECK("a bee NPC is live going into the clear check", live_bee); + force_clear_wave(&sc); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + step_and_observe(&sc, &ctx, idle); + CHECK("the wave clears (next draft opens) with the bee swarm still alive", + draft_is_open(&sc) && sc.wave_spawn_target == 1); +} + +static void test_totem_lifecycle(void) { + printf("test_totem_lifecycle\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 1; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 211); + geo_clear_npcs(&s); + s.modifiers.active_mask |= (1u << COLO_MOD_TOTEMIC); + s.modifiers.tier[COLO_MOD_TOTEMIC] = 1; + s.player.x = 25; s.player.y = 18; + col_rebuild_player_collision_flags(&s); + + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 12, 16); + s.npcs[0].hp = 70; + col_mod_on_npc_hp_changed(&s, 0); + CHECK("no totem above 50% HP", s.totems[0].phase == COLO_HAZARD_NONE); + s.npcs[0].hp = 60; + col_mod_on_npc_hp_changed(&s, 0); + CHECK("crossing <=50% spawns a totem", s.totems[0].phase == COLO_HAZARD_ALIVE); + int tslot = s.totems[0].npc_slot; + CHECK("the totem is a live 1-HP NPC beside its owner", + tslot >= 0 && s.npcs[tslot].active && + s.npcs[tslot].type == COLO_HEALING_TOTEM && s.npcs[tslot].hp == 1); + col_mod_on_npc_hp_changed(&s, 0); + CHECK("no duplicate totem for the same owner", s.totems[0].phase == COLO_HAZARD_ALIVE); + + for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL - 1; t++) col_mod_tick_totems(&s); + CHECK("no heal before the 7th tick", s.npcs[0].hp == 60); + col_mod_tick_totems(&s); + CHECK("the 7th tick heals 30% of the owner's max HP", s.npcs[0].hp == 60 + 37); + for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); + CHECK("the pulse is gated while the owner is above 50%", s.npcs[0].hp == 97); + s.npcs[0].hp = 50; + for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); + CHECK("the pulse resumes once the owner re-crosses 50%", s.npcs[0].hp == 87); + + col_player_attack_target(&s, tslot); + land_pending_player_hits(&s); + CHECK("a single attack destroys the totem", !s.npcs[tslot].active); + CHECK("destruction arms the 200-tick respawn", + s.totems[0].phase == COLO_HAZARD_RESPAWNING && + s.totems[0].respawn_timer == COLO_TOTEM_RESPAWN_TICKS); + for (int t = 0; t < COLO_TOTEM_RESPAWN_TICKS - 1; t++) col_mod_tick_totems(&s); + CHECK("still down one tick early", s.totems[0].phase == COLO_HAZARD_RESPAWNING); + col_mod_tick_totems(&s); + CHECK("the totem respawns exactly 200 ticks after destruction", + s.totems[0].phase == COLO_HAZARD_ALIVE && + s.npcs[s.totems[0].npc_slot].active && + s.npcs[s.totems[0].npc_slot].type == COLO_HEALING_TOTEM); + + int tslot2 = s.totems[0].npc_slot; + s.npcs[0].hp = 0; + col_apply_npc_death(&s, 0); + CHECK("the owner's death despawns its totem", + !s.npcs[tslot2].active && s.totems[0].phase == COLO_HAZARD_NONE); + + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 12, 16); + s.npcs[0].hp = 60; + col_mod_on_npc_hp_changed(&s, 0); + int tslot3 = s.totems[0].npc_slot; + col_player_attack_target(&s, tslot3); + land_pending_player_hits(&s); + CHECK("second totem down and respawning", s.totems[0].phase == COLO_HAZARD_RESPAWNING); + s.npcs[0].hp = 0; + col_apply_npc_death(&s, 0); + CHECK("the owner's death cancels a pending totem respawn", + s.totems[0].phase == COLO_HAZARD_NONE); + +} + +static void test_totemic_sol_wave12(void) { + printf("test_totemic_sol_wave12\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 11; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 223); + advance_to_wave_spawn(&s, &ctx); + s.modifiers.active_mask |= (1u << COLO_MOD_TOTEMIC); + s.modifiers.tier[COLO_MOD_TOTEMIC] = 1; + int sol = col_sol_find_idx(&s); + CHECK("Sol is live", sol >= 0); + + s.npcs[sol].hp = COLO_SOL_HP_MAX * 60 / 100; + col_mod_on_npc_hp_changed(&s, sol); + CHECK("no totem while Sol is above 50%", s.totems[sol].phase == COLO_HAZARD_NONE); + s.npcs[sol].hp = COLO_SOL_HP_MAX / 2; + col_mod_on_npc_hp_changed(&s, sol); + CHECK("Sol at 50% spawns a totem", s.totems[sol].phase == COLO_HAZARD_ALIVE); + int tslot = s.totems[sol].npc_slot; + CHECK("Sol's totem is an attackable 1-HP NPC inside the boss arena", + s.npcs[tslot].type == COLO_HEALING_TOTEM && s.npcs[tslot].hp == 1 && + col_in_boss_arena(&s, s.npcs[tslot].x, s.npcs[tslot].y)); + + int hp0 = s.npcs[sol].hp; + for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL - 1; t++) col_mod_tick_totems(&s); + CHECK("no Sol heal before the 7th tick", s.npcs[sol].hp == hp0); + col_mod_tick_totems(&s); + CHECK("the pulse heals Sol exactly 75", s.npcs[sol].hp == hp0 + COLO_TOTEM_SOL_HEAL); + for (int t = 0; t < COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); + CHECK("Sol keeps healing 75/7t even above 50% (until destroyed)", + s.npcs[sol].hp == hp0 + 2 * COLO_TOTEM_SOL_HEAL); + + col_player_attack_target(&s, tslot); + land_pending_player_hits(&s); + int hp1 = s.npcs[sol].hp; + for (int t = 0; t < 3 * COLO_TOTEM_HEAL_INTERVAL; t++) col_mod_tick_totems(&s); + CHECK("a destroyed totem stops the Sol heal (until the 200t respawn)", + !s.npcs[tslot].active && s.npcs[sol].hp == hp1 && + s.totems[sol].phase == COLO_HAZARD_RESPAWNING); +} + +static void test_reentry_sand_tiles(void) { + printf("test_reentry_sand_tiles\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 1; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 227); + geo_clear_npcs(&s); + + s.modifiers.active_mask |= (1u << COLO_MOD_REENTRY); + s.modifiers.tier[COLO_MOD_REENTRY] = 1; + col_mod_reentry_on_skyfall(&s, 20, 12); + CHECK("T1 leaves one pool on the targeted tile", + s.molten_count == 1 && s.molten_x[0] == 20 && s.molten_y[0] == 12); + CHECK("the T1 pool is the stronger Reentry kind", + s.molten_kind[0] == COLO_POOL_REENTRY); + + s.player.x = 20; s.player.y = 12; + int burns = 0, burn_ok = 1, off_cadence_seen = 0; + for (int t = 0; t < 24; t++) { + s.player.current_hitpoints = 99; + col_mod_tick_molten_pools(&s); + int dmg = 99 - s.player.current_hitpoints; + if (dmg > 0) { + burns++; + if (dmg < 1 || dmg > COLO_REENTRY_MOLTEN_MAX_HIT) burn_ok = 0; + } else { + off_cadence_seen = 1; + } + } + CHECK("Reentry burn is 1-15 and always positive when it fires", burn_ok); + CHECK("Reentry fires every other tick (~half of 24)", burns >= 11 && burns <= 13); + CHECK("Reentry has off-cadence no-damage ticks", off_cadence_seen); + + for (int t = 0; t < 500; t++) col_mod_tick_molten_pools(&s); + CHECK("the T1 pool persists all wave", s.molten_count == 1); + col_modifiers_on_wave_spawn(&s); + CHECK("T1 (temporary) clears at wave end", s.molten_count == 0); + + s.modifiers.tier[COLO_MOD_REENTRY] = 2; + col_mod_reentry_on_skyfall(&s, 20, 12); + int has_target = 0, has_sw = 0, has_w = 0, all_reentry = 1; + for (int i = 0; i < s.molten_count; i++) { + if (s.molten_x[i] == 20 && s.molten_y[i] == 12) has_target = 1; + if (s.molten_x[i] == 19 && s.molten_y[i] == 11) has_sw = 1; + if (s.molten_x[i] == 19 && s.molten_y[i] == 12) has_w = 1; + if (s.molten_kind[i] != COLO_POOL_REENTRY) all_reentry = 0; + } + CHECK("T2 covers the targeted tile + the tile SOUTH-WEST of it", + s.molten_count == 2 && has_target && has_sw && !has_w); + CHECK("T2 pools are Reentry kind", all_reentry); + col_modifiers_on_wave_spawn(&s); + CHECK("Reentry T2 pools are PERMANENT (survive wave end)", s.molten_count == 2); + + s.molten_count = 0; + s.modifiers.tier[COLO_MOD_REENTRY] = 3; + col_mod_reentry_on_skyfall(&s, 20, 12); + has_target = has_sw = has_w = 0; + for (int i = 0; i < s.molten_count; i++) { + if (s.molten_x[i] == 20 && s.molten_y[i] == 12) has_target = 1; + if (s.molten_x[i] == 19 && s.molten_y[i] == 11) has_sw = 1; + if (s.molten_x[i] == 19 && s.molten_y[i] == 12) has_w = 1; + } + CHECK("T3 additionally covers the WEST tile", s.molten_count == 3 && + has_target && has_sw && has_w); + col_modifiers_on_wave_spawn(&s); + CHECK("Reentry T3 pools are PERMANENT (survive wave end)", s.molten_count == 3); + + s.molten_count = 0; + s.modifiers.active_mask |= (1u << COLO_MOD_VOLATILITY); + s.modifiers.tier[COLO_MOD_VOLATILITY] = 3; + s.player.x = 5; s.player.y = 18; + col_mod_volatility_on_death(&s, 20, 16, 1); + CHECK("Volatility T3 leaves a temporary Volatility pool at the centre", + s.molten_count == 1 && s.molten_kind[0] == COLO_POOL_VOLATILITY); + col_modifiers_on_wave_spawn(&s); + CHECK("the Volatility (temporary) pool clears at wave end", s.molten_count == 0); +} + +static void test_venom_escalation(void) { + printf("test_venom_escalation\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 1; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 229); + s.modifiers.active_mask |= (1u << COLO_MOD_MANTIMAYHEM); + s.modifiers.tier[COLO_MOD_MANTIMAYHEM] = 2; + + col_mod_manticore_apply_venom(&s, 1); + CHECK("the first proc arms 6 damage on the 30-tick clock", + s.player_venom == COLO_VENOM_START && s.player_venom_timer == COLO_VENOM_INTERVAL); + + static const int EXPECT[9] = { 6, 8, 10, 12, 14, 16, 18, 20, 20 }; + int seq_ok = 1, cadence_ok = 1; + for (int k = 0; k < 9; k++) { + for (int t = 0; t < COLO_VENOM_INTERVAL - 1; t++) { + s.player.current_hitpoints = 99; + col_mod_tick_venom(&s); + if (s.player.current_hitpoints != 99) cadence_ok = 0; + } + s.player.current_hitpoints = 99; + col_mod_tick_venom(&s); + if (99 - s.player.current_hitpoints != EXPECT[k]) seq_ok = 0; + } + CHECK("venom deals 6,8,10..20 then holds the cap", seq_ok); + CHECK("venom damage lands exactly every 30 ticks", cadence_ok); + + s.player_venom = COLO_VENOM_START; + s.player_venom_timer = 17; + col_mod_manticore_apply_venom(&s, 1); + CHECK("reapplication bumps the next damage +2 without resetting the clock", + s.player_venom == COLO_VENOM_START + COLO_VENOM_STEP && + s.player_venom_timer == 17); + s.player_venom = COLO_VENOM_CAP; + col_mod_manticore_apply_venom(&s, 1); + CHECK("reapplication never exceeds the 20 cap", s.player_venom == COLO_VENOM_CAP); + + s.player_venom = COLO_VENOM_START; + s.player_venom_timer = 17; + s.modifiers.draft_pending = 1; + int idle[COLO_NUM_ACTION_HEADS] = {0}; + step_and_observe(&s, &ctx, idle); + CHECK("venom timer freezes during the draft gap", s.player_venom_timer == 17); + s.modifiers.draft_pending = 0; + + s.player_venom = COLO_VENOM_START; + s.player_venom_timer = 2; + col_modifiers_on_wave_spawn(&s); + CHECK("venom survives the wave boundary", + s.player_venom == COLO_VENOM_START && s.player_venom_timer == 2); + s.player.current_hitpoints = 99; + col_mod_tick_venom(&s); + CHECK("venom does not tick early after the wave boundary", s.player.current_hitpoints == 99); + col_mod_tick_venom(&s); + CHECK("venom still ticks on the next wave", + s.player.current_hitpoints == 99 - COLO_VENOM_START && + s.player_venom == COLO_VENOM_START + COLO_VENOM_STEP); +} + +static void test_bee_poison_status(void) { + printf("test_bee_poison_status\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 230); + + col_mod_apply_bee_poison(&s); + CHECK("bee poison starts at severity 5", + s.player_poison == COLO_POISON_BEE_CONTACT_SEVERITY && + s.player_poison_timer == COLO_POISON_INTERVAL); + + int cadence_ok = 1; + int hits_ok = 1; + for (int hit = 0; hit < COLO_POISON_BEE_CONTACT_SEVERITY; hit++) { + int severity_before = s.player_poison; + for (int t = 0; t < COLO_POISON_INTERVAL - 1; t++) { + s.player.current_hitpoints = 99; + col_mod_tick_poison(&s); + if (s.player.current_hitpoints != 99) cadence_ok = 0; + } + s.player.current_hitpoints = 99; + col_mod_tick_poison(&s); + if (99 - s.player.current_hitpoints != 1 || + s.player_poison != severity_before - 1) + hits_ok = 0; + } + CHECK("bee poison deals exactly five 1-damage hits", hits_ok); + CHECK("bee poison hits exactly 30 ticks apart", cadence_ok); + CHECK("bee poison expires at severity 0", + s.player_poison == 0 && s.player_poison_timer == 0); +} + +static void test_mantimayhem_t3_shuffle(void) { + printf("test_mantimayhem_t3_shuffle\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 1; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 239); + geo_clear_npcs(&s); + s.modifiers.active_mask |= (1u << COLO_MOD_MANTIMAYHEM); + s.modifiers.tier[COLO_MOD_MANTIMAYHEM] = 3; + s.player.x = 17; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + int one_each_ok = 1; + int arm_copies_fixed_ok = 1; + int melee_slot_seen[3] = { 0, 0, 0 }; + for (int rep = 0; rep < 300; rep++) { + geo_clear_npcs(&s); + s.wave_manticore_pattern_rolled = 0; + col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); + ColoManticoreState* mc = colo_npc_manticore(&s.npcs[0]); + if (mc->orb_style[0] != ATTACK_STYLE_NONE || + mc->orb_style[1] != ATTACK_STYLE_NONE || + mc->orb_style[2] != ATTACK_STYLE_NONE) { + arm_copies_fixed_ok = 0; + } + s.npcs[0].attack_timer = 0; + s.player.current_hitpoints = 99; + col_npc_manticore_arm(&s, 0); + if (mc->cycle_step != 0) arm_copies_fixed_ok = 0; + int counts[3] = { 0, 0, 0 }; + for (int o = 0; o < 3; o++) { + if (mc->fixed_orb_style[o] == ATTACK_STYLE_RANGED) counts[0]++; + if (mc->fixed_orb_style[o] == ATTACK_STYLE_MAGIC) counts[1]++; + if (mc->fixed_orb_style[o] == ATTACK_STYLE_MELEE) { + counts[2]++; + melee_slot_seen[o] = 1; + } + if (mc->orb_style[o] != mc->fixed_orb_style[o]) arm_copies_fixed_ok = 0; + } + if (counts[0] != 1 || counts[1] != 1 || counts[2] != 1) one_each_ok = 0; + } + CHECK("T3 fixed cycles always carry exactly one of each style", + one_each_ok); + CHECK("arming copies the fixed cycle into the active telegraph", + arm_copies_fixed_ok); + CHECK("the melee orb appears in every position across the sample", + melee_slot_seen[0] && melee_slot_seen[1] && melee_slot_seen[2]); +} + +static void test_relentless_def_level_bypass(void) { + printf("test_relentless_def_level_bypass\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 1; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 233); + + const EncounterLoadoutStats* ls = col_live_loadout_stats(&s); + int def_bonus = encounter_player_def_bonus( + ls->def_stab, ls->def_slash, ls->def_crush, ls->def_magic, ls->def_ranged, + ATTACK_STYLE_MELEE, MELEE_STYLE_STAB); + CHECK("rig sanity: the geared player has a positive melee defence bonus", def_bonus > 0); + + int t0 = col_player_def_roll(&s, ATTACK_STYLE_MELEE, MELEE_STYLE_STAB); + CHECK("tier 0 uses the full 99 defence level", t0 == (99 + 8) * (def_bonus + 64)); + + s.modifiers.active_mask |= (1u << COLO_MOD_RELENTLESS); + s.modifiers.tier[COLO_MOD_RELENTLESS] = 1; + int t1 = col_player_def_roll(&s, ATTACK_STYLE_MELEE, MELEE_STYLE_STAB); + CHECK("tier I keeps exactly 67% of the level (99 -> 66), bonus intact", + t1 == (66 + 8) * (def_bonus + 64)); + + s.modifiers.tier[COLO_MOD_RELENTLESS] = 2; + int t2 = col_player_def_roll(&s, ATTACK_STYLE_MELEE, MELEE_STYLE_STAB); + CHECK("tier II keeps exactly 34% of the level (99 -> 33), bonus intact", + t2 == (33 + 8) * (def_bonus + 64)); + + s.modifiers.tier[COLO_MOD_RELENTLESS] = 3; + int t3 = col_player_def_roll(&s, ATTACK_STYLE_MELEE, MELEE_STYLE_STAB); + CHECK("tier III zeroes the level share; the gear term alone remains", + t3 == 8 * (def_bonus + 64)); +} + +static void test_mantimayhem_stress(void) { + printf("test_mantimayhem_stress\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 8; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 17); + s.modifiers.active_mask |= (1u << COLO_MOD_MANTIMAYHEM); + s.modifiers.tier[COLO_MOD_MANTIMAYHEM] = 2; + s.wave = 8; + col_spawn_wave(&s); + + int manticores = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (s.npcs[i].active && s.npcs[i].type == COLO_MANTICORE) manticores++; + CHECK("wave 9 spawns two manticores", manticores == 2); + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int venom_seen = 0; + for (int t = 0; t < 2000 && !s.episode_over; t++) { + s.player.current_hitpoints = 9999; + step_and_observe(&s, &ctx, idle); + if (s.player_venom > 0) venom_seen = 1; + } + CHECK("Mantimayhem T2 survived sustained barrages without queue overflow", 1); + CHECK("Mantimayhem T2 inflicted venom on the player", venom_seen); +} + +static int sf_collect_move_gaps(ColosseumState* s, int ticks, int* gaps, int max_gaps) { + int n = 0; + int last_move_t = -1; + int previous_step = s->solarflare.step; + for (int t = 1; t <= ticks; t++) { + col_mod_tick_solarflare(s); + if (s->solarflare.step != previous_step) { + if (last_move_t >= 0 && n < max_gaps) gaps[n++] = t - last_move_t; + last_move_t = t; + previous_step = s->solarflare.step; + } + } + return n; +} + +static int sf_tile_on_pillar_perimeter(int pillar_index, int x, int y) { + int px = COLO_PILLARS[pillar_index][0]; + int py = COLO_PILLARS[pillar_index][1]; + int in_ring_box = x >= px - 1 && x <= px + COLO_PILLAR_SIZE && + y >= py - 1 && y <= py + COLO_PILLAR_SIZE; + int in_pillar = x >= px && x < px + COLO_PILLAR_SIZE && + y >= py && y < py + COLO_PILLAR_SIZE; + return in_ring_box && !in_pillar; +} + +static int sf_advance_to_step(ColosseumState* s, int target_step, int max_ticks) { + for (int t = 0; t < max_ticks; t++) { + col_mod_tick_solarflare(s); + if (s->solarflare.step == target_step) return 1; + } + return 0; +} + +static void sf_reset_tier(ColosseumState* s, int tier) { + s->modifiers.tier[COLO_MOD_SOLARFLARE] = tier; + col_mod_sync_solarflare(s); + s->player.x = 16; + s->player.y = 16; +} + +static void test_solarflare_orb(void) { + printf("test_solarflare_orb\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 23); + s.modifiers.active_mask |= (1u << COLO_MOD_SOLARFLARE); + s.modifiers.tier[COLO_MOD_SOLARFLARE] = 2; + s.wave = 0; + col_spawn_wave(&s); + CHECK("Solarflare orb is active", s.solarflare.active); + + int geometry_ok = 1; + int corner_tiles_ok = 1; + for (int p = 0; p < COLO_NUM_PILLARS; p++) { + int px = COLO_PILLARS[p][0]; + int py = COLO_PILLARS[p][1]; + const int corners[4][3] = { + {0, px - 1, py - 1}, + {4, px + COLO_PILLAR_SIZE, py - 1}, + {8, px + COLO_PILLAR_SIZE, py + COLO_PILLAR_SIZE}, + {12, px - 1, py + COLO_PILLAR_SIZE}, + }; + for (int step = 0; step < COLO_SOLARFLARE_RING_STEPS; step++) { + int x, y; + col_solarflare_tile(&s, p, step, &x, &y); + if (!sf_tile_on_pillar_perimeter(p, x, y)) geometry_ok = 0; + if (col_static_blocked(x, y)) geometry_ok = 0; + } + for (int c = 0; c < 4; c++) { + int x, y; + col_solarflare_tile(&s, p, corners[c][0], &x, &y); + if (x != corners[c][1] || y != corners[c][2]) corner_tiles_ok = 0; + } + } + CHECK("Solarflare has one distance-1 perimeter orb per pillar", geometry_ok); + CHECK("Solarflare visits the four specified pillar-ring corners", corner_tiles_ok); + + RenderEntity entities[COLO_MAX_NPCS + COLO_NUM_PILLARS + 2]; + int entity_count = 0; + col_fill_render_entities_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, + entities, COLO_MAX_NPCS + COLO_NUM_PILLARS + 2, &entity_count); + int render_orbs = 0; + int render_slots_ok = 1; + for (int e = 0; e < entity_count; e++) { + if (entities[e].npc_def_id != COLO_NPC_DEF_ID_SOLARFLARE) continue; + int p = entities[e].npc_slot - COLO_NPC_SLOT_SOLARFLARE; + if (p < 0 || p >= COLO_NUM_PILLARS) { + render_slots_ok = 0; + } else { + int x, y; + col_solarflare_tile(&s, p, s.solarflare.step, &x, &y); + if (entities[e].npc_instance_id != COLO_SOLARFLARE_RENDER_INSTANCE_ID + (uint32_t)p) + render_slots_ok = 0; + if (entities[e].x != x || entities[e].y != y) render_slots_ok = 0; + } + render_orbs++; + } + CHECK("Solarflare renders four per-pillar orb entities", + render_orbs == COLO_NUM_PILLARS && render_slots_ok); + + int obs_pillar = 2; + int obs_x, obs_y; + col_solarflare_tile(&s, obs_pillar, s.solarflare.step, &obs_x, &obs_y); + s.player.x = obs_x; + s.player.y = obs_y; + static float obs[COLO_NUM_OBS]; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + int solarflare_obs_idx = TEST_MOD_OBS_SOLARFLARE; + CHECK("Solarflare modifier obs writes nearest of four orbs", + obs[solarflare_obs_idx] == 0.0f && + obs[solarflare_obs_idx + 1] == 0.0f && + obs[solarflare_obs_idx + 2] == 1.0f); + + int gaps[32]; + s.player.x = 16; + s.player.y = 16; + int n = sf_collect_move_gaps(&s, 40, gaps, 32); + int t2_ok = n >= 8; + for (int g = 0; g < n; g++) if (gaps[g] != 2) t2_ok = 0; + CHECK("T2 moves every 2 ticks with no corner pause", t2_ok); + sf_reset_tier(&s, 2); + int t2_corner_reached = sf_advance_to_step(&s, 4, 16); + CHECK("T2 does not pause at Solarflare corners", + t2_corner_reached && s.solarflare.pause_timer == 0); + + sf_reset_tier(&s, 1); + n = sf_collect_move_gaps(&s, 60, gaps, 32); + int t1_ok = n >= 6; + for (int g = 0; g < n; g++) + if (gaps[g] != 2 && gaps[g] != 2 + COLO_SOLARFLARE_CORNER_PAUSE) t1_ok = 0; + int t1_paused = 0; + for (int g = 0; g < n; g++) + if (gaps[g] == 2 + COLO_SOLARFLARE_CORNER_PAUSE) t1_paused = 1; + CHECK("T1 moves every 2 ticks and pauses 7 at each corner", t1_ok && t1_paused); + sf_reset_tier(&s, 1); + int t1_corner_reached = sf_advance_to_step(&s, 4, 16); + CHECK("T1 sets a 7 tick Solarflare corner pause", + t1_corner_reached && s.solarflare.pause_timer == COLO_SOLARFLARE_CORNER_PAUSE); + + sf_reset_tier(&s, 3); + n = sf_collect_move_gaps(&s, 40, gaps, 32); + int t3_ok = n >= 8, t3_paused = 0, t3_fast = 0; + for (int g = 0; g < n; g++) { + if (gaps[g] != 1 && gaps[g] != 1 + COLO_SOLARFLARE_CORNER_PAUSE_T3) t3_ok = 0; + if (gaps[g] == 1 + COLO_SOLARFLARE_CORNER_PAUSE_T3) t3_paused = 1; + if (gaps[g] == 1) t3_fast = 1; + } + CHECK("T3 moves every tick AND stops 2 ticks at each corner (A27)", + t3_ok && t3_paused && t3_fast); + sf_reset_tier(&s, 3); + int t3_corner_reached = sf_advance_to_step(&s, 4, 16); + CHECK("T3 sets a 2 tick Solarflare corner pause", + t3_corner_reached && s.solarflare.pause_timer == COLO_SOLARFLARE_CORNER_PAUSE_T3); + + CHECK("Solarflare max-hit constants remain tiered", + col_mod_solarflare_max_hit(1) == COLO_SOLARFLARE_MAX_HIT_T1 && + col_mod_solarflare_max_hit(2) == COLO_SOLARFLARE_MAX_HIT_T2 && + col_mod_solarflare_max_hit(3) == COLO_SOLARFLARE_MAX_HIT_T3); + + sf_reset_tier(&s, 3); + int hit_x, hit_y; + col_solarflare_tile(&s, 0, s.solarflare.step, &hit_x, &hit_y); + s.player.x = hit_x; + s.player.y = hit_y; + s.player.prayer = PRAYER_PROTECT_MAGIC; + s.player.current_hitpoints = 9999; + int damaged = 0; + for (int t = 0; t < 80 && !damaged; t++) { + int hp = s.player.current_hitpoints; + s.solarflare.pause_timer = 1; + col_mod_tick_solarflare(&s); + if (s.player.current_hitpoints < hp) damaged = 1; + } + CHECK("Solarflare orb deals contact damage", damaged); + CHECK("Solarflare III disables prayer on hit", s.player.prayer == PRAYER_NONE); +} + +static void test_volatility_explosion(void) { + printf("test_volatility_explosion\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 29); + s.modifiers.active_mask |= (1u << COLO_MOD_VOLATILITY); + s.modifiers.tier[COLO_MOD_VOLATILITY] = 1; + + s.player.x = 17; s.player.y = 17; + s.player.current_hitpoints = 99; + int idx = 0; + col_init_npc(&s, idx, COLO_FREMENNIK_BERSERKER, 18, 17); + int hp_before = s.player.current_hitpoints; + s.npcs[idx].hp = 0; + col_apply_npc_death(&s, idx); + CHECK("Volatility explosion hits an adjacent player", s.player.current_hitpoints < hp_before); +} + +static void test_modifier_hazard_obs_fixes(void) { + printf("test_modifier_hazard_obs_fixes\n"); + ColosseumContext ctx; + ColosseumState s; + static float obs[COLO_NUM_OBS]; + + init_forecast_test_state(&s, &ctx, 301, 17, 16); + col_init_npc(&s, 0, COLO_JAVELIN_COLOSSUS, 20, 16); + ColoJavelinState* jv = colo_npc_javelin(&s.npcs[0]); + jv->skyfall_pending = 1; + jv->skyfall_tile_x = 19; + jv->skyfall_tile_y = 14; + jv->skyfall_timer = 2; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + int tells = COLO_OBS_AFTER_EQUIPPED_SELF + TEST_NPC_TELLS_OFFSET; + CHECK("javelin skyfall tells expose landing dx while pending", + fabsf(obs[tells + 2] - col_obs_rel_x(19, s.player.x)) < 0.000001f); + CHECK("javelin skyfall tells expose landing dy while pending", + fabsf(obs[tells + 3] - col_obs_rel_y(14, s.player.y)) < 0.000001f); + jv->skyfall_pending = 0; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("javelin skyfall landing tells clear when not pending", + obs[tells + 2] == 0.0f && obs[tells + 3] == 0.0f); + + init_forecast_test_state(&s, &ctx, 302, 12, 10); + col_init_npc(&s, 0, COLO_BEE_SWARM, 10, 10); + s.bees[0] = (ColoBeeSwarm){ + .phase = COLO_HAZARD_ALIVE, + .npc_slot = 0, + .respawn_timer = 0, + .move_timer = 1, + }; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + tells = COLO_OBS_AFTER_EQUIPPED_SELF + TEST_NPC_TELLS_OFFSET; + CHECK("bee tells expose a nonzero move timer", + obs[tells] > 0.0f && obs[tells] <= 1.0f); + CHECK("bee tells expose next-step contact", + obs[tells + 1] == 0.0f && + fabsf(obs[tells + 2] - col_obs_rel_x(11, s.player.x)) < 0.000001f && + fabsf(obs[tells + 3] - col_obs_rel_y(10, s.player.y)) < 0.000001f && + obs[tells + 4] == 1.0f); + + init_forecast_test_state(&s, &ctx, 303, 10, 10); + s.molten_count = 6; + s.molten_x[0] = 20; s.molten_y[0] = 20; + s.molten_x[1] = 12; s.molten_y[1] = 10; + s.molten_x[2] = 9; s.molten_y[2] = 10; + s.molten_x[3] = 10; s.molten_y[3] = 13; + s.molten_x[4] = 8; s.molten_y[4] = 8; + s.molten_x[5] = 10; s.molten_y[5] = 14; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + int molten = TEST_MOD_OBS_MOLTEN; + CHECK("modifier molten obs lists the nearest pool first", + fabsf(obs[molten] - col_obs_rel_x(9, s.player.x)) < 0.000001f && + fabsf(obs[molten + 1] - col_obs_rel_y(10, s.player.y)) < 0.000001f && + obs[molten + 2] == 1.0f); + CHECK("modifier molten obs keeps the four nearest pools ordered", + fabsf(obs[molten + 3] - col_obs_rel_x(12, s.player.x)) < 0.000001f && + fabsf(obs[molten + 6] - col_obs_rel_x(8, s.player.x)) < 0.000001f && + fabsf(obs[molten + 9] - col_obs_rel_x(10, s.player.x)) < 0.000001f && + obs[molten + 5] == 1.0f && + obs[molten + 8] == 1.0f && + obs[molten + 11] == 1.0f); + + init_forecast_test_state(&s, &ctx, 304, 0, 0); + s.modifiers.active_mask |= (1u << COLO_MOD_SOLARFLARE); + s.modifiers.tier[COLO_MOD_SOLARFLARE] = 2; + s.solarflare = (ColoSolarflareOrb){ + .active = 1, + .step = 3, + .move_timer = 1, + .pause_timer = 0, + }; + int current_x, current_y, next_x, next_y; + col_solarflare_tile(&s, 0, s.solarflare.step, ¤t_x, ¤t_y); + col_solarflare_tile(&s, 0, s.solarflare.step + 1, &next_x, &next_y); + s.player.x = current_x; + s.player.y = current_y; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + int solarflare = TEST_MOD_OBS_SOLARFLARE; + CHECK("Solarflare obs exposes the nearest orb next tile", + fabsf(obs[solarflare + 6] - col_obs_rel_x(next_x, s.player.x)) < 0.000001f && + fabsf(obs[solarflare + 7] - col_obs_rel_y(next_y, s.player.y)) < 0.000001f); + + init_forecast_test_state(&s, &ctx, 305, 17, 16); + s.modifiers.active_mask |= (1u << COLO_MOD_DOOM); + s.modifiers.tier[COLO_MOD_DOOM] = 3; + s.doom_stacks = COLO_DOOM_CAP[3] - 2; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("Doom lethality obs is clear before cap minus one", + obs[TEST_MOD_OBS_DOOM_LETHAL] == 0.0f); + s.doom_stacks = COLO_DOOM_CAP[3] - 1; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("Doom lethality obs flips at cap minus one", + obs[TEST_MOD_OBS_DOOM_LETHAL] == 1.0f); + + s.player_venom = COLO_VENOM_START; + s.player_venom_timer = COLO_VENOM_INTERVAL / 2; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("venom obs exposes the next tick timer", + fabsf(obs[TEST_MOD_OBS_VENOM_TIMER] - 0.5f) < 0.000001f); + + init_forecast_test_state(&s, &ctx, 306, 17, 17); + s.modifiers.active_mask |= (1u << COLO_MOD_VOLATILITY); + s.modifiers.tier[COLO_MOD_VOLATILITY] = 1; + col_init_npc(&s, 0, COLO_FREMENNIK_BERSERKER, 18, 17); + osrs_interaction_set(&s.interaction, 0); + s.npcs[0].hp = 5; + col_queue_npc_pending_hit(&s, 0, 5, 1, ATTACK_STYLE_MELEE, ENCOUNTER_SPELL_NONE); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + int volatility = TEST_MOD_OBS_VOLATILITY; + CHECK("Volatility current-target blast marks player in footprint", + obs[volatility + 3] == 1.0f); + CHECK("Volatility queued-kill blast marks player in footprint", + obs[volatility + 7] == 1.0f); +} + +static void test_death_linger_wave_clear_and_render(void) { + printf("test_death_linger_wave_clear_and_render\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 31); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.wave = 0; + s.wave_spawn_delay = 0; + s.wave_ready_delay = 0; + s.reinforcement_timer = COLO_REINFORCEMENT_TICKS; + s.player.x = 12; + s.player.y = 16; + + int idx = 0; + col_init_npc(&s, idx, COLO_FREMENNIK_BERSERKER, 14, 16); + s.npcs[idx].hp = 1; + int dealt = encounter_damage_npc( + &s.npcs[idx].hp, &s.npcs[idx].hit_landed_this_tick, + &s.npcs[idx].hit_damage, 1); + s.npcs[idx].hit_was_successful_this_tick = dealt > 0; + col_apply_npc_death(&s, idx); + + int linger_ticks = col_npc_death_linger_ticks(COLO_FREMENNIK_BERSERKER); + CHECK("lethal hit starts NPC death linger", s.npcs[idx].active && + s.npcs[idx].death_ticks == linger_ticks); + + RenderEntity entities[4]; + int entity_count = 0; + col_fill_render_entities_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, entities, 4, &entity_count); + CHECK("dying NPC is still rendered", entity_count == 2 && entities[1].npc_slot == idx); + CHECK("dying NPC uses death animation", + entities[1].npc_anim_id == col_npc_death_anim_id(COLO_FREMENNIK_BERSERKER)); + CHECK("lethal hitsplat remains on death frame", + entities[1].hit_landed_this_tick == 1 && entities[1].hit_damage == 1); + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + step_and_observe(&s, &ctx, idle); + CHECK("wave clears while corpse is dying", s.tick_scratch.wave_completed == 1); + CHECK("dying corpse remains active after wave clear", + s.npcs[idx].active && s.npcs[idx].death_ticks == linger_ticks - 1); + + for (int t = 0; t < linger_ticks; t++) step_and_observe(&s, &ctx, idle); + CHECK("dying corpse despawns after linger", !s.npcs[idx].active); +} + +static void test_static_arena_mask(void) { + printf("test_static_arena_mask\n"); + col_build_npc_stats(); + + int gate_rows_ok = 1; + for (int x = 0; x <= 33; x++) { + int walkable = (x == 13 || x == 14 || x == 19 || x == 20); + if (col_static_blocked(x, 0) != !walkable) gate_rows_ok = 0; + if (col_static_blocked(x, 33) != !walkable) gate_rows_ok = 0; + } + CHECK("south+north inner rows walkable exactly at the gate flanks {13,14,19,20}", + gate_rows_ok); + + int west_ok = 1; + for (int y = 0; y <= 33; y++) { + int walkable = (y == 13 || y == 14 || y == 19 || y == 20); + if (col_static_blocked(0, y) != !walkable) west_ok = 0; + } + CHECK("west col 0 open exactly at the entrance rows {13,14,19,20}", west_ok); + + int east_ok = 1; + for (int y = 0; y <= 33; y++) + if (!col_static_blocked(33, y)) east_ok = 0; + CHECK("east col 33 fully walled", east_ok); + + CHECK("row 3 west extent [0,5)", col_static_blocked(4, 3) && !col_static_blocked(5, 3)); + CHECK("row 30 west extent [0,6)", col_static_blocked(5, 30) && !col_static_blocked(6, 30)); + CHECK("row 29 east extent [29,34)", !col_static_blocked(28, 29) && col_static_blocked(29, 29)); + + int pillars_ok = 1, rim_ok = 1; + for (int p = 0; p < COLO_NUM_PILLARS; p++) { + int px = COLO_PILLARS[p][0], py = COLO_PILLARS[p][1]; + for (int dx = 0; dx < 3; dx++) + for (int dy = 0; dy < 3; dy++) + if (!col_static_blocked(px + dx, py + dy)) pillars_ok = 0; + if (col_static_blocked(px - 1, py + 1)) rim_ok = 0; + if (col_static_blocked(px + 3, py + 1)) rim_ok = 0; + } + CHECK("all 36 pillar tiles blocked on every wave", pillars_ok); + CHECK("tiles flanking each pillar stay walkable", rim_ok); + + int zones_ok = 1; + for (int a = 0; a < COLO_NUM_SPAWN_ANCHORS; a++) + for (int dx = 0; dx < COLO_SPAWN_ZONE_SIZE; dx++) + for (int dy = 0; dy < COLO_SPAWN_ZONE_SIZE; dy++) + if (col_static_blocked(COLO_SPAWN_ANCHORS[a][0] + dx, + COLO_SPAWN_ANCHORS[a][1] + dy)) zones_ok = 0; + CHECK("every 3x3 spawn-anchor zone fully walkable on the static mask", zones_ok); + + CHECK("wave start (7,18) walkable", + !col_static_blocked(COLO_PLAYER_START_X, COLO_PLAYER_START_Y)); + CHECK("boss start (16,10) walkable", + !col_static_blocked(COLO_BOSS_PLAYER_START_X, COLO_BOSS_PLAYER_START_Y)); + int sol_ok = 1; + for (int dx = 0; dx < 5; dx++) + for (int dy = 0; dy < 5; dy++) + if (col_static_blocked(COLO_SOL_SPAWN_X + dx, COLO_SOL_SPAWN_Y + dy)) sol_ok = 0; + CHECK("Sol's 5x5 footprint at (16,19) unblocked", sol_ok); +} + +static void test_static_los_and_attack_gate(void) { + printf("test_static_los_and_attack_gate\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 31); + geo_clear_npcs(&s); + + CHECK("SW pillar blocks a ray along row 9", !col_tiles_have_los(&s, 7, 9, 12, 9)); + CHECK("pillar block is symmetric", !col_tiles_have_los(&s, 12, 9, 7, 9)); + CHECK("ray one row north of the pillar is clear", col_tiles_have_los(&s, 7, 12, 12, 12)); + CHECK("north gate doors block along the inner row", !col_tiles_have_los(&s, 14, 33, 19, 33)); + CHECK("row 32 inside the north gate is clear", col_tiles_have_los(&s, 14, 32, 19, 32)); + + s.player.x = 5; s.player.y = 9; + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 13, 9); + s.npcs[0].attack_timer = 0; + CHECK("shaman behind the pillar has no LoS", !col_npc_has_los_to_player(&s, &s.npcs[0])); + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("no-LoS shaman holds fire", s.npcs[0].attacked_this_tick == 0); + col_npc_move_ctx(&s, &ctx, 0); + CHECK("no-LoS shaman steps toward the player instead", s.npcs[0].moved_this_tick == 1); + + geo_clear_npcs(&s); + s.player.x = 5; s.player.y = 12; + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 13, 12); + s.npcs[0].attack_timer = 0; + CHECK("clear-row shaman has LoS", col_npc_has_los_to_player(&s, &s.npcs[0])); + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("clear-row shaman attacks", s.npcs[0].attacked_this_tick == 1); +} + +static void test_spawn_anchor_exclusion(void) { + printf("test_spawn_anchor_exclusion\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 37); + advance_to_wave_spawn(&s, &ctx); + + geo_clear_npcs(&s); + s.player.x = 5; s.player.y = 18; + int cand[COLO_NUM_SPAWN_ANCHORS]; + int n = col_spawn_anchor_candidates(&s, cand); + CHECK("b5 spawn-fix tile leaves exactly 10 candidate anchors", n == 10); + int suppressed_ok = 1; + for (int i = 0; i < n; i++) + if (cand[i] == 0 || cand[i] == 1 || cand[i] == 2) suppressed_ok = 0; + CHECK("the 3 suppressed anchors are (3,14),(9,16),(3,19)", suppressed_ok); + + int on_anchor_ok = 1, excluded_ok = 1, distinct_ok = 1, unblocked_ok = 1; + int warband_ok = 1; + for (int rep = 0; rep < 30; rep++) { + s.player.x = 5; s.player.y = 18; + s.wave = 4; + col_spawn_wave(&s); + int used[COLO_NUM_SPAWN_ANCHORS] = {0}; + int archer_x = -1, archer_y = -1; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + if (!s.npcs[i].active || s.npcs[i].type != COLO_FREMENNIK_ARCHER) continue; + archer_x = s.npcs[i].x; archer_y = s.npcs[i].y; + } + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s.npcs[i]; + if (!npc->active) continue; + int size = col_npc_effective_size(npc); + for (int dx = 0; dx < size; dx++) + for (int dy = 0; dy < size; dy++) + if (col_static_blocked(npc->x + dx, npc->y + dy)) unblocked_ok = 0; + if (col_type_is_warbander(npc->type)) { + + if (npc->type == COLO_FREMENNIK_ARCHER) { + if (npc->x < COLO_WARBAND_BOX_MIN_X || npc->x > COLO_WARBAND_BOX_MAX_X || + npc->y < COLO_WARBAND_BOX_MIN_Y || npc->y > COLO_WARBAND_BOX_MAX_Y) + warband_ok = 0; + } else if (npc->type == COLO_FREMENNIK_SEER) { + + if (npc->x != archer_x + 2 || npc->y != archer_y) warband_ok = 0; + } else { + + if (npc->x != archer_x + 1 || npc->y != archer_y + 1) warband_ok = 0; + } + continue; + } + int anchor = -1; + for (int a = 0; a < COLO_NUM_SPAWN_ANCHORS; a++) + if (COLO_SPAWN_ANCHORS[a][0] == npc->x && + COLO_SPAWN_ANCHORS[a][1] == npc->y) anchor = a; + if (anchor < 0) { on_anchor_ok = 0; continue; } + if (used[anchor]) distinct_ok = 0; + used[anchor] = 1; + if (col_spawn_excluded_near_player(&s, npc->x, npc->y, COLO_SPAWN_ZONE_SIZE)) + excluded_ok = 0; + } + } + CHECK("every primary spawns with its SW tile ON one of the 12 anchors", on_anchor_ok); + CHECK("no primary ever lands within Chebyshev 4 of the player", excluded_ok); + CHECK("anchors draw without replacement (no double-booking)", distinct_ok); + CHECK("no spawned footprint touches a blocked tile", unblocked_ok); + CHECK("warband trio spawns centre-box in the fixed diamond formation", warband_ok); +} + +static void test_reinforcement_gates(void) { + printf("test_reinforcement_gates\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 10; + ColosseumState s; + + for (int north = 0; north <= 1; north++) { + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 41); + geo_clear_npcs(&s); + s.player.x = 16; + s.player.y = north ? 16 : 15; + col_spawn_reinforcements(&s); + + int count = 0, in_gap_ok = 1, row_ok = 1; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s.npcs[i]; + if (!npc->active) continue; + count++; + int size = col_npc_effective_size(npc); + if (npc->x < COLO_GATE_MIN_X || npc->x + size - 1 > COLO_GATE_MAX_X) + in_gap_ok = 0; + int inner_row = north ? npc->y + size - 1 : npc->y; + if (inner_row != (north ? COLO_GATE_NORTH_SPAWN_ROW : COLO_GATE_SOUTH_SPAWN_ROW)) + row_ok = 0; + for (int dx = 0; dx < size; dx++) + for (int dy = 0; dy < size; dy++) + if (col_static_blocked(npc->x + dx, npc->y + dy)) row_ok = 0; + } + CHECK("reinforcement set spawned (minotaur + shaman)", count == 2); + CHECK("reinforcements land inside the gate gap x 15-18", in_gap_ok); + CHECK(north ? "player y=16 -> north gate row (yellow line, not nearest)" + : "player y=15 -> south gate row", row_ok); + } +} + +static void test_outcome_score_uses_fresh_damage(void) { + printf("test_outcome_score_uses_fresh_damage\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 101); + advance_to_wave_spawn(&s, &ctx); + complete_open_draft(&s, &ctx, 1); + + CHECK("wave 1 spawned a positive HP pool", s.current_wave_hp_pool > 0); + int pool = s.current_wave_hp_pool; + + s.current_wave_fresh_damage = 0.0f; + CHECK("zero fresh damage gives zero within-wave progress", + col_current_wave_score_progress(&s) == 0.0f); + + s.current_wave_fresh_damage = 0.25f * (float)pool; + CHECK("a quarter of the HP pool is quarter-wave depth", + fabsf(col_episode_outcome_score(&s) - score_for_depth(0.25f)) < 0.0001f); + + s.current_wave_fresh_damage = 0.5f * (float)pool; + CHECK("half the HP pool is half-wave depth and climbs", + fabsf(col_episode_outcome_score(&s) - score_for_depth(0.5f)) < 0.0001f); + + s.current_wave_fresh_damage = (float)pool; + CHECK("a full pool caps within-wave progress at zero", + col_current_wave_score_progress(&s) == 0.0f); +} + +static void test_outcome_score_reinforcement_grows_denominator(void) { + printf("test_outcome_score_reinforcement_grows_denominator\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 10; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 202); + advance_to_wave_spawn(&s, &ctx); + + CHECK("wave 11 starts with seven score enemies", s.current_wave_total_killable == 7); + int pool_before = s.current_wave_hp_pool; + CHECK("the HP pool is positive", pool_before > 0); + + s.current_wave_fresh_damage = 0.3f * (float)pool_before; + float score_before = col_episode_outcome_score(&s); + + col_spawn_reinforcements(&s); + CHECK("reinforcements enter the score denominator", s.current_wave_total_killable == 9); + CHECK("reinforcements grow the HP pool", s.current_wave_hp_pool > pool_before); + + CHECK("a larger pool lowers the fresh fraction for fixed fresh damage", + col_episode_outcome_score(&s) < score_before); + + s.current_wave_fresh_damage = 0.6f * (float)s.current_wave_hp_pool; + CHECK("more fresh damage over the larger pool climbs the score again", + col_episode_outcome_score(&s) > score_before); +} + +static void test_fresh_damage_not_farmable_via_healing(void) { + printf("test_fresh_damage_not_farmable_via_healing\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 1; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 711); + geo_clear_npcs(&s); + col_reset_current_wave_score_progress(&s); + + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 12, 16); + s.npcs[0].hp = 100; + s.npcs[0].max_hp = 100; + s.npcs[0].min_hp_seen = 100; + s.player.x = 13; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + + col_queue_npc_pending_hit(&s, 0, 70, 1, ATTACK_STYLE_MELEE, ENCOUNTER_SPELL_NONE); + land_pending_player_hits(&s); + CHECK("first hit lands for 70", s.npcs[0].hp == 30); + CHECK("fresh damage tracks the first hit", + fabsf(s.current_wave_fresh_damage - 70.0f) < 0.001f); + CHECK("min_hp_seen follows the new low", s.npcs[0].min_hp_seen == 30); + + s.npcs[0].hp = 60; + CHECK("healing does not touch min_hp_seen", s.npcs[0].min_hp_seen == 30); + + col_queue_npc_pending_hit(&s, 0, 30, 1, ATTACK_STYLE_MELEE, ENCOUNTER_SPELL_NONE); + land_pending_player_hits(&s); + CHECK("re-damaging restored HP credits no fresh damage", + fabsf(s.current_wave_fresh_damage - 70.0f) < 0.001f); + + col_queue_npc_pending_hit(&s, 0, 20, 1, ATTACK_STYLE_MELEE, ENCOUNTER_SPELL_NONE); + land_pending_player_hits(&s); + CHECK("a new low credits only the fresh portion (20)", + fabsf(s.current_wave_fresh_damage - 90.0f) < 0.001f); + CHECK("min_hp_seen follows the deeper low", s.npcs[0].min_hp_seen == 10); + + CHECK("gross damage double-counts the healed HP, fresh does not", + fabsf(s.tick_scratch.damage_dealt - 120.0f) < 0.001f); +} + +static void test_outcome_score_wave_clear_has_no_double_count(void) { + printf("test_outcome_score_wave_clear_has_no_double_count\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 303); + complete_open_draft(&s, &ctx, 1); + + while (first_live_score_enemy(&s) >= 0) kill_first_live_score_enemy(&s); + s.log.waves_cleared = 1; + CHECK("within-wave progress is zero after all wave enemies are killed", + col_current_wave_score_progress(&s) == 0.0f); + CHECK("cleared-wave score comes only from waves_cleared", + fabsf(col_episode_outcome_score(&s) - score_for_depth(1.0f)) < 0.000001f); +} + +static void test_outcome_score_sol_uses_boss_progress_only(void) { + printf("test_outcome_score_sol_uses_boss_progress_only\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = COLO_WAVE_BOSS; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 404); + + s.min_sol_hp_seen = 750; + float expected = score_for_depth(col_sol_score_progress(&s)); + CHECK("boss wave has no within-wave kill denominator", + s.current_wave_total_killable == 0 && col_current_wave_score_progress(&s) == 0.0f); + CHECK("Sol score is boss progress only", + fabsf(col_episode_outcome_score(&s) - expected) < 0.000001f); +} + +static void test_roster_cap_nine(void) { + printf("test_roster_cap_nine\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 47); + s.modifiers.active_mask |= (1u << COLO_MOD_QUARTET) | (1u << COLO_MOD_DYNAMIC_DUO); + s.modifiers.tier[COLO_MOD_QUARTET] = 1; + s.modifiers.tier[COLO_MOD_DYNAMIC_DUO] = 1; + s.wave = 7; + col_spawn_wave(&s); + int count = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) if (s.npcs[i].active) count++; + CHECK("wave 8 + Quartet + Dynamic Duo spawns all 9 NPCs", count == 9); +} + +static void test_wave12_quartet_and_win(void) { + printf("test_wave12_quartet_and_win\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 11; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 43); + advance_to_wave_spawn(&s, &ctx); + + CHECK("boss-wave player start (16,10)", + s.player.x == COLO_BOSS_PLAYER_START_X && s.player.y == COLO_BOSS_PLAYER_START_Y); + CHECK("boss arena clamp is (9,9)-(24,24)", + s.sol.boss_arena_min_x == 9 && s.sol.boss_arena_min_y == 9 && + s.sol.boss_arena_max_x == 24 && s.sol.boss_arena_max_y == 24); + int sol_idx = col_sol_find_idx(&s); + CHECK("Sol spawned at SW (16,19)", sol_idx >= 0 && + s.npcs[sol_idx].x == COLO_SOL_SPAWN_X && s.npcs[sol_idx].y == COLO_SOL_SPAWN_Y); + + int placement_ok = 1, reachable_ok = 1, win_ok = 1, survivor_ok = 1; + for (int rep = 0; rep < 10; rep++) { + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 43 + (uint32_t)rep); + s.modifiers.active_mask |= (1u << COLO_MOD_QUARTET); + s.modifiers.tier[COLO_MOD_QUARTET] = 1; + s.wave = COLO_WAVE_BOSS; + col_spawn_wave(&s); + s.wave_spawn_delay = 0; + + int wb = -1; + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (s.npcs[i].active && col_type_is_warbander(s.npcs[i].type)) wb = i; + if (wb < 0) { placement_ok = 0; continue; } + int wx = s.npcs[wb].x, wy = s.npcs[wb].y; + int cheb_dx = abs(wx - s.player.x), cheb_dy = abs(wy - s.player.y); + if (wx < 9 || wx > 24 || wy < 9 || wy > 24) placement_ok = 0; + if (col_static_blocked(wx, wy)) placement_ok = 0; + if ((cheb_dx > cheb_dy ? cheb_dx : cheb_dy) <= COLO_SPAWN_EXCLUSION_CHEB) + placement_ok = 0; + + int seen[COLO_ARENA_WIDTH][COLO_ARENA_HEIGHT] = {{0}}; + int qx[34 * 34], qy[34 * 34], head = 0, tail = 0; + qx[tail] = s.player.x; qy[tail] = s.player.y; tail++; + seen[s.player.x][s.player.y] = 1; + int reached = 0; + while (head < tail && !reached) { + int cx = qx[head], cy = qy[head]; head++; + static const int D[4][2] = { {1,0}, {-1,0}, {0,1}, {0,-1} }; + for (int d = 0; d < 4; d++) { + int nx = cx + D[d][0], ny = cy + D[d][1]; + if (nx == wx && ny == wy) { reached = 1; break; } + if (nx < 9 || nx > 24 || ny < 9 || ny > 24) continue; + if (seen[nx][ny] || col_static_blocked(nx, ny)) continue; + int gx, gy; + if (!col_grid_index(nx, ny, &gx, &gy)) continue; + if (s.npc_collision_flags[gx][gy]) continue; + seen[nx][ny] = 1; + qx[tail] = nx; qy[tail] = ny; tail++; + } + } + if (!reached) reachable_ok = 0; + + int sol = col_sol_find_idx(&s); + if (sol < 0) { win_ok = 0; continue; } + s.npcs[sol].hp = 0; + col_apply_npc_death(&s, sol); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + step_and_observe(&s, &ctx, idle); + if (!(s.episode_over && s.winner == COLO_OUTCOME_PLAYER_WON)) win_ok = 0; + if (!s.npcs[wb].active) survivor_ok = 0; + } + CHECK("Quartet warbander spawns on a walkable interior tile outside the exclusion", + placement_ok); + CHECK("Quartet warbander is pathable from the player", reachable_ok); + CHECK("Sol's death wins the wave-12 run (A6)", win_ok); + CHECK("the surviving warbander does not block the win", survivor_ok); +} + +static void test_player_walks_through_npc_footprint(void) { + printf("test_player_walks_through_npc_footprint\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 510, 16, 16); + col_init_npc(&s, 0, COLO_SOL_HEREDIT, 17, 16); + col_rebuild_player_collision_flags(&s); + int gx, gy; + int npc_flag = col_grid_index(17, 16, &gx, &gy) && s.npc_collision_flags[gx][gy]; + int player_flag = col_grid_index(17, 16, &gx, &gy) && s.player_collision_flags[gx][gy]; + ColoWalkCtx wc = { .s = &s, .ctx = &ctx }; + CHECK("NPC footprint remains stamped for NPC systems", npc_flag != 0); + CHECK("NPC footprint is not stamped as player collision", player_flag == 0); + CHECK("player walkability ignores NPC footprint", + col_player_walkable(&s, 17, 16) == 1); + CHECK("player pathfinding extra block ignores NPC footprint", + col_pathfind_blocked(&wc, 17 + ctx.world_offset_x, 16 + ctx.world_offset_y) == 0); + int actions[COLO_NUM_ACTION_HEADS] = {0}; + actions[COLO_HEAD_PRIMARY] = forecast_move_action_for_delta(1, 0); + step_and_observe(&s, &ctx, actions); + CHECK("explicit movement can step onto Sol footprint", + s.player.x == 17 && s.player.y == 16); +} + +static int wb_find_npc(const ColosseumState* s, ColoNpcType type) { + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (s->npcs[i].active && s->npcs[i].type == type) return i; + return -1; +} + +static void wb_isolate_warband(ColosseumState* s) { + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (s->npcs[i].active && !col_type_is_warbander(s->npcs[i].type)) + col_deactivate_npc(s, i); +} + +static void wb_move_npc(ColosseumState* s, int slot, int x, int y) { + int size = col_npc_effective_size(&s->npcs[slot]); + col_stamp_npc_collision_footprint(s, s->npcs[slot].x, s->npcs[slot].y, size, 0); + s->npcs[slot].x = x; + s->npcs[slot].y = y; + col_stamp_npc_collision_footprint(s, x, y, size, 1); +} + +static int wb_attacks_this_tick(const ColosseumState* s, ColoNpcType type) { + int n = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (s->npcs[i].active && s->npcs[i].type == type && + s->npcs[i].attacked_this_tick) n++; + return n; +} + +static void test_warband_cycle_offsets(void) { + printf("test_warband_cycle_offsets\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 51); + advance_to_wave_spawn(&s, &ctx); + complete_open_draft(&s, &ctx, 1); + wb_isolate_warband(&s); + + wb_move_npc(&s, wb_find_npc(&s, COLO_FREMENNIK_BERSERKER), s.player.x, s.player.y + 1); + wb_move_npc(&s, wb_find_npc(&s, COLO_FREMENNIK_SEER), s.player.x + 1, s.player.y); + wb_move_npc(&s, wb_find_npc(&s, COLO_FREMENNIK_ARCHER), s.player.x - 1, s.player.y); + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int anchor = -1; + int first_tick[3] = { -1, -1, -1 }; + int count[3] = { 0, 0, 0 }; + int offsets_ok = 1; + static const ColoNpcType SPECIES[3] = { + COLO_FREMENNIK_BERSERKER, COLO_FREMENNIK_SEER, COLO_FREMENNIK_ARCHER }; + + for (int t = 0; t < 46 && !s.episode_over; t++) { + s.player.current_hitpoints = 9999; + step_and_observe(&s, &ctx, idle); + if (anchor < 0) anchor = s.warband_cycle_anchor; + for (int sp = 0; sp < 3; sp++) { + if (!wb_attacks_this_tick(&s, SPECIES[sp])) continue; + count[sp]++; + if (first_tick[sp] < 0) first_tick[sp] = s.tick; + if (anchor < 0 || (s.tick - anchor) % COLO_WARBAND_CYCLE_TICKS != sp + 1) + offsets_ok = 0; + } + } + CHECK("cycle anchored at the wave's first actionable tick", anchor >= 0); + CHECK("standing player eats repeated full cycles (berserker)", count[0] >= 4); + CHECK("standing player eats repeated full cycles (seer)", count[1] >= 4); + CHECK("standing player eats repeated full cycles (archer)", count[2] >= 4); + CHECK("berserker only lands on ticks = N+1 mod 6; seer +2; archer +3", offsets_ok); + CHECK("first berserker window is exactly N+1 (wave-anchored, no spawn timer)", + first_tick[0] == anchor + 1); + CHECK("seer first window N+2", first_tick[1] == anchor + 2); + CHECK("archer first window N+3", first_tick[2] == anchor + 3); +} + +static void test_warband_move_skip(void) { + printf("test_warband_move_skip\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 53); + advance_to_wave_spawn(&s, &ctx); + complete_open_draft(&s, &ctx, 1); + wb_isolate_warband(&s); + wb_move_npc(&s, wb_find_npc(&s, COLO_FREMENNIK_BERSERKER), s.player.x, s.player.y + 1); + wb_move_npc(&s, wb_find_npc(&s, COLO_FREMENNIK_SEER), s.player.x + 1, s.player.y); + wb_move_npc(&s, wb_find_npc(&s, COLO_FREMENNIK_ARCHER), s.player.x - 1, s.player.y); + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int walk_south[COLO_NUM_ACTION_HEADS] = {0}; + walk_south[COLO_HEAD_PRIMARY] = 4; + + while ((s.wave_ready_delay > 0 || s.wave_attack_delay > 1) && !s.episode_over) + step_and_observe(&s, &ctx, idle); + + int attacks = 0; + int moved_every_tick = 1; + for (int t = 0; t < 14 && !s.episode_over; t++) { + s.player.current_hitpoints = 9999; + step_and_observe(&s, &ctx, walk_south); + if (!s.tick_scratch.player_moved) moved_every_tick = 0; + for (int sp = 0; sp < COLO_MAX_NPCS; sp++) + if (s.npcs[sp].active && col_type_is_warbander(s.npcs[sp].type) && + s.npcs[sp].attacked_this_tick) attacks++; + } + CHECK("the scripted stutter-step actually moved every tick", moved_every_tick); + CHECK("warband fired zero attacks across the stutter-step run", attacks == 0); + CHECK("zero warband damage across the stutter-step run", + s.log.total_damage_received == 0.0f); +} + +static uint64_t wb_trajectory_hash(ColosseumState* s, ColosseumContext* ctx, + int seed, const int* actions, int ticks) { + memset(s, 0, sizeof(*s)); + col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); + advance_to_wave_spawn(s, ctx); + complete_open_draft(s, ctx, 1); + wb_isolate_warband(s); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + while ((s->wave_ready_delay > 0 || s->wave_attack_delay > 1) && !s->episode_over) + step_and_observe(s, ctx, idle); + uint64_t h = 1469598103934665603ULL; + for (int t = 0; t < ticks && !s->episode_over; t++) { + s->player.current_hitpoints = 9999; + step_and_observe(s, ctx, actions); + h = (h ^ (uint64_t)(uint32_t)s->player.x) * 1099511628211ULL; + h = (h ^ (uint64_t)(uint32_t)s->player.y) * 1099511628211ULL; + for (int n = 0; n < COLO_MAX_NPCS; n++) { + if (!s->npcs[n].active || !col_type_is_warbander(s->npcs[n].type)) + continue; + uint64_t p = (uint64_t)(uint32_t)s->npcs[n].x + | ((uint64_t)(uint32_t)s->npcs[n].y << 8) + | ((uint64_t)n << 16); + h = (h ^ p) * 1099511628211ULL; + } + } + return h; +} + +static void test_warband_bfs_memo_bit_identity(void) { + printf("test_warband_bfs_memo_bit_identity\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + ColosseumState s; + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int walk_south[COLO_NUM_ACTION_HEADS] = {0}; + walk_south[COLO_HEAD_PRIMARY] = 4; + + memset(col_warband_bfs_memo_key, 0, sizeof(col_warband_bfs_memo_key)); + uint64_t idle_cold = wb_trajectory_hash(&s, &ctx, 51, idle, 40); + uint64_t idle_warm = wb_trajectory_hash(&s, &ctx, 51, idle, 40); + CHECK("memo-served warband trajectory == fresh BFS (idle player)", + idle_warm == idle_cold); + + uint64_t walk_polluted = wb_trajectory_hash(&s, &ctx, 53, walk_south, 40); + memset(col_warband_bfs_memo_key, 0, sizeof(col_warband_bfs_memo_key)); + uint64_t walk_cold = wb_trajectory_hash(&s, &ctx, 53, walk_south, 40); + CHECK("polluted-table episode == its fresh-memo reference (walking player)", + walk_polluted == walk_cold); +} + +static void test_warband_melee_distance_gate(void) { + printf("test_warband_melee_distance_gate\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 57); + geo_clear_npcs(&s); + s.player.x = 7; s.player.y = 18; + s.tick_scratch.player_moved = 0; + s.tick = 100; + + col_init_npc(&s, 0, COLO_FREMENNIK_ARCHER, 12, 18); + CHECK("rig sanity: the ranged archer has clear LoS", + col_npc_has_los_to_player(&s, &s.npcs[0])); + s.warband_cycle_anchor = s.tick - 3; + col_warband_attack_phase(&s, &ctx); + CHECK("archer at distance never attacks, even with LoS on its window", + s.npcs[0].attacked_this_tick == 0); + + wb_move_npc(&s, 0, 8, 18); + col_warband_attack_phase(&s, &ctx); + CHECK("cardinally adjacent archer attacks on its window", + s.npcs[0].attacked_this_tick == 1); + + s.npcs[0].attacked_this_tick = 0; + wb_move_npc(&s, 0, 8, 19); + col_warband_attack_phase(&s, &ctx); + CHECK("diagonally adjacent archer does not attack (cardinal-only)", + s.npcs[0].attacked_this_tick == 0); +} + +static void test_warband_two_tick_stationary_gate(void) { + printf("test_warband_two_tick_stationary_gate\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 57); + geo_clear_npcs(&s); + s.player.x = 7; s.player.y = 18; + s.tick = 100; + + col_init_npc(&s, 0, COLO_FREMENNIK_BERSERKER, 8, 18); + s.warband_cycle_anchor = s.tick - col_warband_window_offset(COLO_FREMENNIK_BERSERKER); + + s.npcs[0].moved_this_tick = 0; + s.npcs[0].moved_last_tick = 1; + col_warband_attack_phase(&s, &ctx); + CHECK("a member with only one stationary tick holds its attack", + s.npcs[0].attacked_this_tick == 0); + + s.npcs[0].moved_this_tick = 0; + s.npcs[0].moved_last_tick = 0; + col_warband_attack_phase(&s, &ctx); + CHECK("a member stationary two ticks attacks on its window", + s.npcs[0].attacked_this_tick == 1); + + s.npcs[0].attacked_this_tick = 0; + s.npcs[0].moved_this_tick = 1; + s.npcs[0].moved_last_tick = 0; + col_warband_attack_phase(&s, &ctx); + CHECK("a member that moved this tick cannot attack", + s.npcs[0].attacked_this_tick == 0); +} + +static void test_warband_formation_convergence(void) { + printf("test_warband_formation_convergence\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + int idle[COLO_NUM_ACTION_HEADS] = {0}; + + for (int quartet = 0; quartet <= 1; quartet++) { + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 59 + (uint32_t)quartet); + complete_open_draft(&s, &ctx, 1); + if (quartet) { + s.modifiers.active_mask |= (1u << COLO_MOD_QUARTET); + s.modifiers.tier[COLO_MOD_QUARTET] = 1; + s.wave = 0; + col_spawn_wave(&s); + } + wb_isolate_warband(&s); + + for (int t = 0; t < 30 && !s.episode_over; t++) { + s.player.current_hitpoints = 9999; + step_and_observe(&s, &ctx, idle); + } + + int formed_ok = 1, members = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + ColoNPC* npc = &s.npcs[i]; + if (!npc->active || !col_type_is_warbander(npc->type)) continue; + members++; + int dir = colo_npc_warband(npc)->formation_dir; + int ex = s.player.x + COLO_WARBAND_FORM_OFFSET[dir][0]; + int ey = s.player.y + COLO_WARBAND_FORM_OFFSET[dir][1]; + if (npc->x != ex || npc->y != ey) formed_ok = 0; + } + if (quartet) { + CHECK("Quartet diamond: 4 members each on their N/E/W/S slot", + formed_ok && members == 4); + } else { + CHECK("trio converges to exactly N/E/W of a stationary player", + formed_ok && members == 3); + } + } +} + +static void test_warband_two_tile_speed(void) { + printf("test_warband_two_tile_speed\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 61); + geo_clear_npcs(&s); + s.player.x = 7; s.player.y = 18; + + col_init_npc(&s, 0, COLO_FREMENNIK_ARCHER, 20, 18); + col_npc_move_ctx(&s, &ctx, 0); + CHECK("warbander closes 2 tiles in one tick on open ground", + s.npcs[0].x == 18 && s.npcs[0].y == 18 && s.npcs[0].moved_this_tick == 1); + col_npc_move_ctx(&s, &ctx, 0); + CHECK("second tick closes 2 more", s.npcs[0].x == 16 && s.npcs[0].y == 18); +} + +static void test_warband_pillar_routefind_vs_shaman_safespot(void) { + printf("test_warband_pillar_routefind_vs_shaman_safespot\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 67); + advance_to_wave_spawn(&s, &ctx); + complete_open_draft(&s, &ctx, 1); + geo_clear_npcs(&s); + s.player.x = 7; s.player.y = 9; + col_rebuild_player_collision_flags(&s); + + col_init_npc(&s, 0, COLO_FREMENNIK_ARCHER, 13, 9); + int adjacent_by = -1; + int archer_attacks = 0; + for (int t = 0; t < 40 && !s.episode_over; t++) { + s.player.current_hitpoints = 9999; + step_and_observe(&s, &ctx, idle); + int dx = abs(s.npcs[0].x - s.player.x), dy = abs(s.npcs[0].y - s.player.y); + if (dx + dy == 1 && adjacent_by < 0) adjacent_by = t; + archer_attacks += wb_attacks_this_tick(&s, COLO_FREMENNIK_ARCHER); + } + CHECK("archer routefinds around the pillar into melee contact", + adjacent_by >= 0 && adjacent_by <= 14); + CHECK("the routefinding archer then lands cycle attacks", archer_attacks > 0); + + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 67); + s.wave_spawn_delay = 0; + complete_open_draft(&s, &ctx, 1); + geo_clear_npcs(&s); + s.player.x = 7; s.player.y = 9; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 13, 9); + int shaman_attacks = 0; + for (int t = 0; t < 40 && !s.episode_over; t++) { + s.player.current_hitpoints = 9999; + step_and_observe(&s, &ctx, idle); + shaman_attacks += s.npcs[0].attacked_this_tick; + } + CHECK("greedy shaman wedges against the pillar (safespot holds)", + s.npcs[0].x == 11 && s.npcs[0].y == 9); + CHECK("safespotted shaman never attacks", shaman_attacks == 0); +} + +static void test_red_flag_minotaur_routefind(void) { + printf("test_red_flag_minotaur_routefind\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + + for (int red_flag = 0; red_flag <= 1; red_flag++) { + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 71); + geo_clear_npcs(&s); + if (red_flag) { + s.modifiers.active_mask |= (1u << COLO_MOD_RED_FLAG); + s.modifiers.tier[COLO_MOD_RED_FLAG] = 1; + } + s.player.x = 7; s.player.y = 9; + col_rebuild_player_collision_flags(&s); + + col_init_npc(&s, 0, COLO_MINOTAUR, 11, 9); + int min_dist = 99; + for (int t = 0; t < 40; t++) { + col_npc_move_ctx(&s, &ctx, 0); + int d = encounter_dist_to_npc( + s.player.x, s.player.y, s.npcs[0].x, s.npcs[0].y, 3); + if (d < min_dist) min_dist = d; + } + if (red_flag) { + CHECK("Red Flag minotaur routefinds into melee contact", min_dist == 1); + } else { + CHECK("plain minotaur stays wedged on the pillar (safespot holds)", + s.npcs[0].x == 11 && s.npcs[0].y == 9 && min_dist == 4); + } + } +} + +static void test_minotaur_heal_semantics(void) { + printf("test_minotaur_heal_semantics\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 73); + + geo_clear_npcs(&s); + s.player.x = 30; s.player.y = 18; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MINOTAUR, 12, 12); + col_init_npc(&s, 1, COLO_SERPENT_SHAMAN, 16, 13); + col_init_npc(&s, 2, COLO_SERPENT_SHAMAN, 17, 17); + col_init_npc(&s, 3, COLO_SERPENT_SHAMAN, 13, 17); + col_init_npc(&s, 4, COLO_MINOTAUR, 18, 12); + s.npcs[1].hp = 62; + s.npcs[2].hp = 30; + s.npcs[3].hp = 100; + s.npcs[4].hp = 100; + s.npcs[0].attack_timer = 0; + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("the lowest-HP-fraction eligible ally heals to FULL", s.npcs[2].hp == 125); + CHECK("exactly one ally healed per action", s.npcs[1].hp == 62); + CHECK("an ally at/above 75% max HP is not eligible", s.npcs[3].hp == 100); + CHECK("another minotaur is never healed", s.npcs[4].hp == 100); + CHECK("healing is not an attack", s.npcs[0].attacked_this_tick == 0); + CHECK("the heal action re-arms the 5-tick timer (D9)", + s.npcs[0].attack_timer == COLO_NPC_STATS[COLO_MINOTAUR].attack_speed); + + geo_clear_npcs(&s); + col_init_npc(&s, 0, COLO_MINOTAUR, 12, 12); + col_init_npc(&s, 1, COLO_SERPENT_SHAMAN, 20, 13); + s.npcs[1].hp = 30; + s.npcs[0].attack_timer = 0; + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("centre distance 7 is in heal reach", s.npcs[1].hp == 125); + geo_clear_npcs(&s); + col_init_npc(&s, 0, COLO_MINOTAUR, 12, 12); + col_init_npc(&s, 1, COLO_SERPENT_SHAMAN, 21, 13); + s.npcs[1].hp = 30; + s.npcs[0].attack_timer = 0; + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("centre distance 8 is out of heal reach", s.npcs[1].hp == 30); + + geo_clear_npcs(&s); + s.player.x = 5; s.player.y = 14; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MINOTAUR, 4, 8); + col_init_npc(&s, 1, COLO_SERPENT_SHAMAN, 12, 9); + col_init_npc(&s, 2, COLO_SERPENT_SHAMAN, 5, 16); + s.npcs[1].hp = 10; + s.npcs[2].hp = 60; + s.npcs[0].attack_timer = 0; + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("pillars block the heal (blocked ally skipped)", s.npcs[1].hp == 10); + CHECK("the clear-LoS ally heals instead", s.npcs[2].hp == 125); + + geo_clear_npcs(&s); + s.player.x = 11; s.player.y = 11; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MINOTAUR, 12, 12); + col_init_npc(&s, 1, COLO_SERPENT_SHAMAN, 16, 13); + s.npcs[1].hp = 30; + s.npcs[0].attack_timer = 0; + s.player.current_hitpoints = 99; + int queue_before = s.player_pending_hits.count; + float mino_faced_before = s.log.pray_faced_by_type[COLO_MINOTAUR]; + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("player in melee distance: the minotaur attacks instead of healing", + s.npcs[0].attacked_this_tick == 1 && s.npcs[1].hp == 30); + CHECK("the crush resolves instantly, queuing nothing for a later tick", + s.player_pending_hits.count == queue_before && + s.log.pray_faced_by_type[COLO_MINOTAUR] > mino_faced_before); +} + +static void test_manticore_barrage_period(void) { + printf("test_manticore_barrage_period\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 79); + geo_clear_npcs(&s); + s.player.x = 17; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); + s.npcs[0].attack_timer = 2; + + ColoManticoreState* mc = colo_npc_manticore(&s.npcs[0]); + int starts[8]; + int nstarts = 0; + for (int t = 0; t < 36; t++) { + s.player.current_hitpoints = 99; + int prev = mc->cycle_step; + col_npc_attack_ctx(&s, &ctx, 0); + if (prev == 0 && mc->cycle_step == 1 && nstarts < 8) starts[nstarts++] = t; + } + CHECK("4 barrage starts inside 36 ticks", nstarts == 4); + int period_ok = nstarts >= 4; + for (int b = 1; b < nstarts; b++) + if (starts[b] - starts[b - 1] != 10) period_ok = 0; + CHECK("barrage-to-barrage period is exactly 10 ticks across 3 gaps", period_ok); +} + +static void test_manticore_telegraph_during_windup(void) { + printf("test_manticore_telegraph_during_windup\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 131); + geo_clear_npcs(&s); + s.player.x = 17; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); + s.npcs[0].attack_timer = 6; + ColoManticoreState* mc = colo_npc_manticore(&s.npcs[0]); + CHECK("spawn rolls a hidden fixed cycle", + mc->fixed_orb_style[0] != ATTACK_STYLE_NONE && + mc->fixed_orb_style[1] != ATTACK_STYLE_NONE && + mc->fixed_orb_style[2] != ATTACK_STYLE_NONE && + mc->orb_style[0] == ATTACK_STYLE_NONE && + mc->orb_style[1] == ATTACK_STYLE_NONE && + mc->orb_style[2] == ATTACK_STYLE_NONE); + + s.player.current_hitpoints = 99; + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("manticore arms during the charge-up at orb 0", mc->cycle_step == 0); + CHECK("the active telegraph matches the fixed cycle before any orb fires", + mc->orb_style[0] == mc->fixed_orb_style[0] && + mc->orb_style[1] == mc->fixed_orb_style[1] && + mc->orb_style[2] == mc->fixed_orb_style[2]); + CHECK("orb 2 is melee (the range+magic pair leads, melee last)", + mc->orb_style[2] == ATTACK_STYLE_MELEE); + + AttackStyle locked0 = mc->orb_style[0]; + for (int t = 0; t < 4 && mc->cycle_step == 0; t++) { + s.player.current_hitpoints = 99; + col_npc_attack_ctx(&s, &ctx, 0); + } + CHECK("the charge pattern stays stable until orb 0 fires (no per-tick re-roll)", + mc->orb_style[0] == locked0); + + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 131); + geo_clear_npcs(&s); + s.player.x = 17; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); + s.npcs[0].attack_timer = 3; + mc = colo_npc_manticore(&s.npcs[0]); + int orb0_blocked = 0; + for (int t = 0; t < 12; t++) { + if (mc->cycle_step == 0 && mc->orb_style[0] != ATTACK_STYLE_NONE) { + AttackStyle s0 = mc->orb_style[0]; + s.player.prayer = s0 == ATTACK_STYLE_MAGIC ? PRAYER_PROTECT_MAGIC : + s0 == ATTACK_STYLE_RANGED ? PRAYER_PROTECT_RANGED : + PRAYER_PROTECT_MELEE; + } + s.player.current_hitpoints = 99; + int step_before = mc->cycle_step; + col_npc_attack_ctx(&s, &ctx, 0); + if (step_before == 0 && mc->cycle_step == 1) { + orb0_blocked = (s.player.current_hitpoints == 99); + break; + } + } + CHECK("a pre-prayed telegraphed orb 0 is blocked on its fire tick", orb0_blocked); + + while (mc->cycle_step >= 0) { + s.player.current_hitpoints = 99; + col_npc_attack_ctx(&s, &ctx, 0); + } + CHECK("disarm clears only the active telegraph", + mc->orb_style[0] == ATTACK_STYLE_NONE && + mc->fixed_orb_style[0] != ATTACK_STYLE_NONE); +} + +static void test_prayer_oracle_manticore_orbs(void) { + printf("test_prayer_oracle_manticore_orbs\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 79); + advance_to_wave_spawn(&s, &ctx); + geo_clear_npcs(&s); + s.wave_ready_delay = 0; + s.wave_attack_delay = 0; + s.player.x = 17; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); + ColoManticoreState* mc = colo_npc_manticore(&s.npcs[0]); + + s.npcs[0].attack_timer = 1; + mc->cycle_step = -1; + mc->fixed_orb_style[0] = ATTACK_STYLE_MAGIC; + mc->fixed_orb_style[1] = ATTACK_STYLE_RANGED; + mc->fixed_orb_style[2] = ATTACK_STYLE_MELEE; + s.player.prayer = PRAYER_NONE; + col_apply_prayer_oracle(&s); + CHECK("oracle prays orb 0's style on the charge-complete tick (magic, not default ranged)", + s.player.prayer == PRAYER_PROTECT_MAGIC); + + for (int o = 0; o < 3; o++) mc->orb_style[o] = mc->fixed_orb_style[o]; + mc->cycle_step = 1; + s.player.prayer = PRAYER_NONE; + col_apply_prayer_oracle(&s); + CHECK("oracle prays the in-flight orb 1 style (ranged)", + s.player.prayer == PRAYER_PROTECT_RANGED); + mc->cycle_step = 2; + s.player.prayer = PRAYER_NONE; + col_apply_prayer_oracle(&s); + CHECK("oracle prays the in-flight orb 2 style (melee, not default ranged)", + s.player.prayer == PRAYER_PROTECT_MELEE); + + mc->cycle_step = -1; + for (int o = 0; o < 3; o++) mc->orb_style[o] = ATTACK_STYLE_NONE; + s.npcs[0].attack_timer = 5; + s.player.prayer = PRAYER_PROTECT_MELEE; + col_apply_prayer_oracle(&s); + CHECK("oracle leaves prayer untouched with no thrower this tick", + s.player.prayer == PRAYER_PROTECT_MELEE); + + s.npcs[0].attack_timer = 2; + for (int t = 0; t < 36; t++) { + s.player.current_hitpoints = 99; + col_apply_prayer_oracle(&s); + col_npc_attack_ctx(&s, &ctx, 0); + } + float faced = s.log.pray_faced_by_type[COLO_MANTICORE]; + float correct = s.log.pray_correct_by_type[COLO_MANTICORE]; + CHECK("solo-manticore barrages under the oracle: every orb faced", + faced >= 9.0f); + CHECK("solo-manticore barrages under the oracle: every orb prayed", + correct == faced); +} + +static int late_start_total_doses(const ColosseumState* s) { + int total = 0; + for (int c = 0; c < COLO_INVENTORY_DISPLAY_SLOTS; c++) + total += s->inventory_cells[c].dose; + return total; +} + +static int late_start_total_picks(const ColosseumState* s) { + int picks = 0; + for (int m = 0; m < COLO_NUM_REAL_MODIFIERS; m++) + picks += s->modifiers.tier[m]; + return picks; +} + +static void test_late_start_entry_state(void) { + printf("test_late_start_entry_state\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 7; + ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY; + ColosseumState s; + + ctx.config.late_start_state_mode = 0; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 4242); + int bare_doses = late_start_total_doses(&s); + CHECK("mode 0 keeps the bare start: no picks", late_start_total_picks(&s) == 0); + CHECK("mode 0 keeps the bare start: no live draft, spawn armed", + !s.modifiers.draft_pending && s.wave_spawn_delay > 0); + + ctx.config.late_start_state_mode = 1; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 4242); + CHECK("wave-8 start carries exactly 7 synthesized picks", + late_start_total_picks(&s) == 7); + CHECK("the start wave's own draft opens LIVE", s.modifiers.draft_pending == 1); + CHECK("the live draft freezes movement like every mid-run draft", + s.modifiers.draft_free_movement == 0); + CHECK("the live pick gates the spawn", + s.modifiers.draft_gates_spawn == 1 && s.wave_spawn_delay == 0); + int caps_ok = 1; + for (int m = 0; m < COLO_NUM_REAL_MODIFIERS; m++) + if (s.modifiers.tier[m] > COLO_MODIFIER_MAX_TIER[m]) caps_ok = 0; + CHECK("synthesized tiers respect per-modifier caps", caps_ok); + int prior_doses = late_start_total_doses(&s); + float kept = (float)prior_doses / (float)bare_doses; + CHECK("supplies drained to the wave-8 prior (~51% +- noise/rounding)", + kept > 0.40f && kept < 0.62f); + + uint32_t mask_a = s.modifiers.active_mask; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 4242); + CHECK("synthesis is deterministic per env seed", + s.modifiers.active_mask == mask_a && + late_start_total_doses(&s) == prior_doses); + + ColosseumContext organic_ctx; + col_init_context_typed(&organic_ctx); + organic_ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY; + ColosseumState org; + memset(&org, 0, sizeof(org)); + col_reset_ctx((EncounterState*)&org, (EncounterContext*)&organic_ctx, 99); + org.modifiers.active_mask = (1u << COLO_MOD_BEES); + org.modifiers.tier[COLO_MOD_BEES] = 3; + org.inventory_cells[27] = org.inventory_cells[26]; + org.player.current_hitpoints = 50; + col_record_wave_entry(&org, 7); + int org_doses = late_start_total_doses(&org); + + ctx.config.late_start_state_mode = 2; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 4242); + CHECK("mode 2 replays the recorded organic modifier stack", + s.modifiers.active_mask == (1u << COLO_MOD_BEES) && + s.modifiers.tier[COLO_MOD_BEES] == 3); + CHECK("mode 2 replays the recorded bag and vitals", + late_start_total_doses(&s) == org_doses && + s.player.current_hitpoints == 50); + CHECK("mode 2 still opens the live pre-wave draft", s.modifiers.draft_pending == 1); +} + +static void test_manticore_orb_same_tick_flick(void) { + printf("test_manticore_orb_same_tick_flick\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 83); + advance_to_wave_spawn(&s, &ctx); + geo_clear_npcs(&s); + s.wave_ready_delay = 0; + s.wave_attack_delay = 0; + s.player.x = 17; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); + ColoManticoreState* mc = colo_npc_manticore(&s.npcs[0]); + + int no_fire_damage = 1, prayed_one_tick = 1, prayer_counted = 0, any_queued = 0; + for (int rep = 0; rep < 16; rep++) { + s.player_pending_hits.count = 0; + mc->cycle_step = 1; + mc->orb_style[1] = ATTACK_STYLE_MAGIC; + s.player.prayer = PRAYER_PROTECT_MAGIC; + s.player.current_hitpoints = 99; + int pc_before = s.tick_scratch.prayer_correct; + col_npc_attack_ctx(&s, &ctx, 0); + if (s.player.current_hitpoints != 99) no_fire_damage = 0; + if (s.tick_scratch.prayer_correct > pc_before) prayer_counted = 1; + for (int h = 0; h < s.player_pending_hits.count; h++) { + const EncounterPendingHit* ph = &s.player_pending_hits.hits[h]; + if (!ph->active) continue; + any_queued = 1; + if (ph->ticks_remaining != 1 || ph->damage != 0) prayed_one_tick = 0; + } + } + CHECK("a prayed orb queues a 1-tick landing for 0 damage", any_queued && prayed_one_tick); + CHECK("no orb damage lands on the fire tick", no_fire_damage); + CHECK("the orb's prayer is checked on its fire tick", prayer_counted); + + s.modifiers.active_mask |= (1u << COLO_MOD_RELENTLESS); + s.modifiers.tier[COLO_MOD_RELENTLESS] = 3; + int queued_damage = 0, no_fire_damage2 = 1; + for (int rep = 0; rep < 32 && !queued_damage; rep++) { + s.player_pending_hits.count = 0; + mc->cycle_step = 1; + mc->orb_style[1] = ATTACK_STYLE_MAGIC; + s.player.prayer = PRAYER_PROTECT_RANGED; + s.player.current_hitpoints = 99; + col_npc_attack_ctx(&s, &ctx, 0); + if (s.player.current_hitpoints != 99) no_fire_damage2 = 0; + for (int h = 0; h < s.player_pending_hits.count; h++) { + const EncounterPendingHit* ph = &s.player_pending_hits.hits[h]; + if (ph->active && ph->ticks_remaining == 1 && ph->damage > 0) queued_damage = 1; + } + } + CHECK("an unprayed orb queues damage for a 1-tick-later landing", queued_damage); + CHECK("an unprayed orb lands nothing on the fire tick", no_fire_damage2); + + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 83); + s.wave_spawn_delay = 0; + complete_open_draft(&s, &ctx, 1); + geo_clear_npcs(&s); + s.player.x = 17; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); + s.npcs[0].attack_timer = 4; + mc = colo_npc_manticore(&s.npcs[0]); + + int actions[COLO_NUM_ACTION_HEADS] = {0}; + int protected_ticks = 0, flicked_damage = 0; + for (int t = 0; t < 40 && !s.episode_over; t++) { + int expect_orb = mc->cycle_step >= 0 && mc->cycle_step < 3; + if (expect_orb) { + AttackStyle next = mc->orb_style[mc->cycle_step]; + actions[COLO_HEAD_PRAYER] = + next == ATTACK_STYLE_MAGIC ? COLO_OVERHEAD_MAGIC : + next == ATTACK_STYLE_RANGED ? COLO_OVERHEAD_RANGED : + COLO_OVERHEAD_MELEE; + } else { + actions[COLO_HEAD_PRAYER] = COLO_OVERHEAD_NO_CHANGE; + } + int hp_before = s.player.current_hitpoints; + step_and_observe(&s, &ctx, actions); + if (expect_orb) { + protected_ticks++; + if (s.player.current_hitpoints < hp_before) flicked_damage = 1; + } + s.player.current_hitpoints = 99; + s.player.current_prayer = 99; + } + CHECK("multiple telegraphed orb ticks observed through the step loop", + protected_ticks >= 6); + CHECK("flicking the telegraphed style each fire tick blocks every such orb", + !flicked_damage); +} + +static void test_manticore_shared_wave_cycle(void) { + printf("test_manticore_shared_wave_cycle\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + + int all_shared = 1; + int all_valid = 1; + int saw_double = 0; + + for (int wave = 8; wave <= 10; wave++) { + for (uint32_t seed = 1; seed <= 40; seed++) { + ColosseumState s; + memset(&s, 0, sizeof(s)); + ctx.config.start_wave = wave; + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, seed); + advance_to_wave_spawn(&s, &ctx); + + const AttackStyle* first = NULL; + int n_mc = 0; + for (int i = 0; i < COLO_MAX_NPCS; i++) { + if (!s.npcs[i].active || s.npcs[i].type != COLO_MANTICORE) continue; + ColoManticoreState* mc = colo_npc_manticore(&s.npcs[i]); + n_mc++; + + int counts[3] = { 0, 0, 0 }; + for (int o = 0; o < 3; o++) { + if (mc->fixed_orb_style[o] == ATTACK_STYLE_RANGED) counts[0]++; + else if (mc->fixed_orb_style[o] == ATTACK_STYLE_MAGIC) counts[1]++; + else if (mc->fixed_orb_style[o] == ATTACK_STYLE_MELEE) counts[2]++; + } + if (counts[0] != 1 || counts[1] != 1 || counts[2] != 1 || + mc->fixed_orb_style[2] != ATTACK_STYLE_MELEE) all_valid = 0; + if (first == NULL) first = mc->fixed_orb_style; + else if (first[0] != mc->fixed_orb_style[0] || + first[1] != mc->fixed_orb_style[1] || + first[2] != mc->fixed_orb_style[2]) all_shared = 0; + } + if (n_mc >= 2) saw_double = 1; + } + } + CHECK("double-manticore waves actually spawn two manticores", saw_double); + CHECK("every manticore in a wave shares one fixed orb cycle", all_shared); + CHECK("the wave cycle is valid (one of each style, melee last)", all_valid); + + ColosseumState s; + memset(&s, 0, sizeof(s)); + ctx.config.start_wave = 1; + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 89); + geo_clear_npcs(&s); + s.wave = 8; + s.wave_manticore_pattern_rolled = 0; + s.player.x = 13; s.player.y = 12; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 12, 16); + col_init_npc(&s, 1, COLO_MANTICORE, 18, 16); + ColoManticoreState* amc = colo_npc_manticore(&s.npcs[0]); + ColoManticoreState* bmc = colo_npc_manticore(&s.npcs[1]); + CHECK("manually-spawned peers also share the wave pattern", + amc->fixed_orb_style[0] == bmc->fixed_orb_style[0] && + amc->fixed_orb_style[1] == bmc->fixed_orb_style[1] && + amc->fixed_orb_style[2] == bmc->fixed_orb_style[2]); + + s.npcs[0].attack_timer = 0; + s.npcs[1].attack_timer = 0; + s.player.current_hitpoints = 99; + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("A committed and fired orb 0", amc->cycle_step == 1); + col_npc_attack_ctx(&s, &ctx, 1); + CHECK("a peer ready at A's barrage start is delayed, holding the shared pattern", + s.npcs[1].attack_timer == COLO_MANTICORE_STAGGER_TICKS && + bmc->cycle_step == 0 && + bmc->orb_style[0] == bmc->fixed_orb_style[0]); + for (int t = 0; t < 4; t++) col_npc_attack_ctx(&s, &ctx, 1); + CHECK("the delayed peer is still holding 4 ticks after A fired", + bmc->cycle_step == 0 && s.npcs[1].attack_timer == 1); + col_npc_attack_ctx(&s, &ctx, 1); + CHECK("the delayed peer fires exactly 5 ticks after A's barrage started", + bmc->cycle_step == 1); +} + +static void test_manticore_stagger_overlap_fidelity(void) { + printf("test_manticore_stagger_overlap_fidelity\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + + ColosseumState s; + memset(&s, 0, sizeof(s)); + ctx.config.start_wave = 1; + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 89); + geo_clear_npcs(&s); + s.wave = 8; + s.wave_manticore_pattern_rolled = 0; + s.player.x = 13; s.player.y = 12; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 12, 16); + col_init_npc(&s, 1, COLO_MANTICORE, 18, 16); + ColoManticoreState* amc = colo_npc_manticore(&s.npcs[0]); + ColoManticoreState* bmc = colo_npc_manticore(&s.npcs[1]); + s.player.current_hitpoints = 99; + s.npcs[0].attack_timer = 0; + s.npcs[1].attack_timer = 2; + col_npc_attack_ctx(&s, &ctx, 0); + col_npc_attack_ctx(&s, &ctx, 1); + CHECK("a peer still charging at A's barrage start is not delayed", + s.npcs[1].attack_timer == 1 && bmc->cycle_step == 0); + col_npc_attack_ctx(&s, &ctx, 0); + col_npc_attack_ctx(&s, &ctx, 1); + CHECK("a peer whose charge completes mid-barrage overlaps immediately", + amc->cycle_step == 2 && bmc->cycle_step == 1); + + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 89); + geo_clear_npcs(&s); + s.wave = 8; + s.wave_manticore_pattern_rolled = 0; + s.player.x = 5; s.player.y = 9; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 5, 14); + col_init_npc(&s, 1, COLO_MANTICORE, 11, 8); + amc = colo_npc_manticore(&s.npcs[0]); + bmc = colo_npc_manticore(&s.npcs[1]); + s.player.current_hitpoints = 99; + s.npcs[0].attack_timer = 0; + s.npcs[1].attack_timer = 0; + CHECK("fixture: A sees the player", col_npc_has_los_to_player(&s, &s.npcs[0])); + CHECK("fixture: the pillar blocks B's LoS", !col_npc_has_los_to_player(&s, &s.npcs[1])); + col_npc_attack_ctx(&s, &ctx, 0); + col_npc_attack_ctx(&s, &ctx, 1); + CHECK("a ready but LoS-blocked peer is not delayed at A's barrage start", + amc->cycle_step == 1 && s.npcs[1].attack_timer == 0 && bmc->cycle_step < 0); + s.player.x = 12; s.player.y = 12; + col_rebuild_player_collision_flags(&s); + CHECK("fixture: B sees the player after the step", + col_npc_has_los_to_player(&s, &s.npcs[1])); + col_npc_attack_ctx(&s, &ctx, 0); + col_npc_attack_ctx(&s, &ctx, 1); + CHECK("entering a second manticore's LoS mid-barrage eats the overlap", + amc->cycle_step == 2 && bmc->cycle_step == 1); + + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 89); + geo_clear_npcs(&s); + s.wave = 8; + s.wave_manticore_pattern_rolled = 0; + s.player.x = 13; s.player.y = 12; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MANTICORE, 12, 16); + col_init_npc(&s, 1, COLO_MANTICORE, 18, 16); + amc = colo_npc_manticore(&s.npcs[0]); + bmc = colo_npc_manticore(&s.npcs[1]); + s.player.current_hitpoints = 99; + s.npcs[0].attack_timer = 3; + s.npcs[1].attack_timer = 3; + for (int t = 0; t < 3; t++) { + col_npc_attack_ctx(&s, &ctx, 0); + col_npc_attack_ctx(&s, &ctx, 1); + } + CHECK("a synced pair alternates: first fires, second is delayed 5", + amc->cycle_step == 1 && + s.npcs[1].attack_timer == COLO_MANTICORE_STAGGER_TICKS && + bmc->cycle_step == 0); + col_npc_attack_ctx(&s, &ctx, 0); + col_npc_attack_ctx(&s, &ctx, 1); + CHECK("the delayed half of the pair keeps holding through A's barrage", + amc->cycle_step == 2 && bmc->cycle_step == 0 && + s.npcs[1].attack_timer == COLO_MANTICORE_STAGGER_TICKS - 1); +} + +static void test_projectile_prayer_locks_at_throw(void) { + printf("test_projectile_prayer_locks_at_throw\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 83); + geo_clear_npcs(&s); + s.player.x = 17; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 10, 16); + + int any_queued = 0, all_frozen = 1, no_throw_damage = 1, prayer_counted = 0, flick_safe = 1; + for (int rep = 0; rep < 24; rep++) { + s.player_pending_hits.count = 0; + s.npcs[0].attack_timer = 0; + s.player.prayer = PRAYER_PROTECT_MAGIC; + s.player.current_hitpoints = 99; + int pc_before = s.tick_scratch.prayer_correct; + col_npc_attack_ctx(&s, &ctx, 0); + if (s.player.current_hitpoints != 99) no_throw_damage = 0; + if (s.tick_scratch.prayer_correct > pc_before) prayer_counted = 1; + s.player.prayer = PRAYER_NONE; + for (int h = 0; h < s.player_pending_hits.count; h++) { + const EncounterPendingHit* ph = &s.player_pending_hits.hits[h]; + if (!ph->active || ph->attack_style != ATTACK_STYLE_MAGIC) continue; + any_queued = 1; + if (ph->damage != 0) all_frozen = 0; + if (ph->check_prayer != 0) flick_safe = 0; + } + } + CHECK("a prayed projectile freezes to 0 damage at throw", any_queued && all_frozen); + CHECK("the projectile flies (nothing lands on the throw tick)", no_throw_damage); + CHECK("the projectile's prayer is counted on its throw tick", prayer_counted); + CHECK("the frozen hit is flick-resistant (check_prayer=0)", flick_safe); + CHECK("the throw path attributes to the per-type prayer log", + s.log.pray_faced_by_type[COLO_SERPENT_SHAMAN] > 0.0f); + + s.modifiers.active_mask |= (1u << COLO_MOD_RELENTLESS); + s.modifiers.tier[COLO_MOD_RELENTLESS] = 3; + int kept_damage = 0; + for (int rep = 0; rep < 32 && !kept_damage; rep++) { + s.player_pending_hits.count = 0; + s.npcs[0].attack_timer = 0; + s.player.prayer = PRAYER_PROTECT_MELEE; + s.player.current_hitpoints = 99; + col_npc_attack_ctx(&s, &ctx, 0); + s.player.prayer = PRAYER_PROTECT_MAGIC; + for (int h = 0; h < s.player_pending_hits.count; h++) { + const EncounterPendingHit* ph = &s.player_pending_hits.hits[h]; + if (ph->active && ph->attack_style == ATTACK_STYLE_MAGIC && ph->damage > 0) kept_damage = 1; + } + } + CHECK("a wrong-prayer projectile keeps its damage (locked at throw)", kept_damage); +} + +static void test_javelin_skyfall_no_defence_gate(void) { + printf("test_javelin_skyfall_no_defence_gate\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 97); + geo_clear_npcs(&s); + s.player.x = 17; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_JAVELIN_COLOSSUS, 16, 12); + ColoJavelinState* jv = colo_npc_javelin(&s.npcs[0]); + const ColoNpcStats* stats = &COLO_NPC_STATS[COLO_JAVELIN_COLOSSUS]; + + int queue_before = s.player_pending_hits.count; + for (int a = 0; a < 4; a++) col_npc_attack_javelin(&s, &ctx, 0, stats); + CHECK("attacks 1-4 are normal queued throws", + s.player_pending_hits.count == queue_before + 4 && jv->skyfall_pending == 0); + col_npc_attack_javelin(&s, &ctx, 0, stats); + CHECK("the 5th attack marks the player's tile with the 6-tick delay", + jv->skyfall_pending == 1 && jv->skyfall_timer == COLO_JAVELIN_SKYFALL_DELAY && + jv->skyfall_tile_x == s.player.x && jv->skyfall_tile_y == s.player.y); + + int nonzero = 0, in_range_ok = 1, high_roll = 0; + for (int rep = 0; rep < 300; rep++) { + jv->attack_count = 4; + jv->skyfall_pending = 0; + col_npc_attack_javelin(&s, &ctx, 0, stats); + if (jv->skyfall_damage > 0) nonzero++; + if (jv->skyfall_damage < 0 || jv->skyfall_damage > COLO_JAVELIN_SKYFALL_MAX_HIT) in_range_ok = 0; + if (jv->skyfall_damage >= 37) high_roll = 1; + } + CHECK("skyfall damage ignores defence (>=80% of 300 marks nonzero)", nonzero >= 240); + CHECK("rolls span the raw 0..40 typeless band up to the top", in_range_ok && high_roll); + + s.player.prayer = PRAYER_PROTECT_RANGED; + s.player.current_hitpoints = 99; + jv->skyfall_pending = 1; + jv->skyfall_timer = 1; + jv->skyfall_damage = 37; + jv->skyfall_tile_x = s.player.x; + jv->skyfall_tile_y = s.player.y; + col_npc_resolve_javelin_skyfall(&s, &ctx, 0); + CHECK("on the marked tile the skyfall lands through Protect-from-Missiles", + s.player.current_hitpoints == 99 - 37 && jv->skyfall_pending == 0); + + s.player.current_hitpoints = 99; + jv->skyfall_pending = 1; + jv->skyfall_timer = 1; + jv->skyfall_damage = 37; + jv->skyfall_tile_x = s.player.x + 1; + jv->skyfall_tile_y = s.player.y; + col_npc_resolve_javelin_skyfall(&s, &ctx, 0); + CHECK("off the marked tile the skyfall misses entirely", + s.player.current_hitpoints == 99 && jv->skyfall_pending == 0); +} + +static int sol_setup(ColosseumState* s, ColosseumContext* ctx, uint32_t seed) { + col_init_context_typed(ctx); + ctx->config.start_wave = 11; + memset(s, 0, sizeof(*s)); + col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + while (s->wave_spawn_delay > 0 || s->wave_ready_delay > 0 || + s->wave_attack_delay > 0) + step_and_observe(s, ctx, idle); + return col_sol_find_idx(s); +} + +static int sol_setup_speedrun(ColosseumState* s, ColosseumContext* ctx, uint32_t seed) { + col_init_context_typed(ctx); + ctx->config.start_wave = 11; + ctx->config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY; + memset(s, 0, sizeof(*s)); + col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + while (s->wave_spawn_delay > 0 || s->wave_ready_delay > 0 || + s->wave_attack_delay > 0) + step_and_observe(s, ctx, idle); + return col_sol_find_idx(s); +} + +static void sol_move_player(ColosseumState* s, int x, int y) { + s->player.x = x; + s->player.y = y; + s->player_dest_x = -1; + s->player_dest_y = -1; + osrs_interaction_clear(&s->interaction); + col_rebuild_player_collision_flags(s); +} + +static int sol_count_active_beams(const ColosseumState* s) { + int n = 0; + for (int b = 0; b < COLO_SOL_BEAM_MAX; b++) + if (s->sol.beams[b].active) n++; + return n; +} + +static void sol_clear_beams_and_sand(ColosseumState* s) { + memset(s->sol.beams, 0, sizeof(s->sol.beams)); + s->sol.hazard_tile_count = 0; +} + +static int sol_phase_sand_invariants_hold(const ColosseumState* s, int expected_count) { + if (s->sol.hazard_tile_count != expected_count) return 0; + int player_tile_seen = 0; + for (int i = 0; i < s->sol.hazard_tile_count; i++) { + int x = s->sol.hazard_tile_x[i]; + int y = s->sol.hazard_tile_y[i]; + if (!col_in_boss_arena(s, x, y)) return 0; + if (col_static_blocked(x, y)) return 0; + if (x == s->player.x && y == s->player.y) player_tile_seen = 1; + for (int j = 0; j < i; j++) + if (x == s->sol.hazard_tile_x[j] && y == s->sol.hazard_tile_y[j]) + return 0; + } + return player_tile_seen; +} + +static void sol_pin(ColosseumState* s, int idx, int x, int y) { + wb_move_npc(s, idx, x, y); + s->sol.attack_delay = 30000; + s->sol.immobile_ticks = 30000; +} + +static void test_sol_adjacency_gate_and_kiting(void) { + printf("test_sol_adjacency_gate_and_kiting\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup(&s, &ctx, 101); + CHECK("Sol spawned on wave 12", idx >= 0); + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int moved = 0; + int first_attack_tick = -1; + int dist_at_first_attack = -1; + for (int t = 0; t < 30 && first_attack_tick < 0; t++) { + s.player.current_hitpoints = 99; + int pre_dist = encounter_dist_to_npc( + s.player.x, s.player.y, s.npcs[idx].x, s.npcs[idx].y, 5); + step_and_observe(&s, &ctx, idle); + if (s.npcs[idx].moved_this_tick) moved++; + if (s.npcs[idx].attacked_this_tick) { + first_attack_tick = s.tick; + dist_at_first_attack = pre_dist; + } + } + CHECK("Sol chases the player across the arena", moved >= 5); + CHECK("Sol initiates only when adjacent at the start of a tick", + first_attack_tick > 0 && dist_at_first_attack == 1); + CHECK("the fight opener is a forced Spear (variant 1)", + s.sol.last_attack_kind == COLO_SOL_ATTACK_SPEAR && + s.sol.aoe_attack == COLO_SOL_AOE_SPEAR1); + + int second_attack_tick = -1; + for (int t = 0; t < 12 && second_attack_tick < 0; t++) { + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + if (s.npcs[idx].attacked_this_tick) second_attack_tick = s.tick; + } + CHECK("a stationary player eats the next attack exactly 7 ticks later", + second_attack_tick == first_attack_tick + COLO_SOL_SPEAR_DELAY); + + int walk_east[COLO_NUM_ACTION_HEADS] = {0}; + walk_east[COLO_HEAD_PRIMARY] = 7; + int third_attack_tick = -1; + for (int t = 0; t < 40 && third_attack_tick < 0; t++) { + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, t < 8 ? walk_east : idle); + if (s.npcs[idx].attacked_this_tick) third_attack_tick = s.tick; + } + CHECK("kiting on the cooldown delays the next attack beyond its delay", + third_attack_tick > second_attack_tick + COLO_SOL_SPEAR_DELAY); +} + +static void test_sol_attack_selection_invariants(void) { + printf("test_sol_attack_selection_invariants\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup(&s, &ctx, 103); + CHECK("the fight opens with the forced-spear flag armed", s.sol.force_spear == 1); + CHECK("a forced draw is a Spear", col_sol_select_attack(&s) == COLO_SOL_ATTACK_SPEAR); + + s.sol.phase = 3; + s.sol.special_cooldown = 0; + int normals_since_special = 99; + int specials = 0, normals = 0, violations = 0; + for (int n = 0; n < 100; n++) { + int kind = col_sol_select_attack(&s); + if (kind == COLO_SOL_ATTACK_TRIPLE || kind == COLO_SOL_ATTACK_GRAPPLE) { + if (normals_since_special < COLO_SOL_SPECIAL_COOLDOWN) violations++; + specials++; + normals_since_special = 0; + } else { + normals++; + normals_since_special++; + } + } + CHECK("specials appear in the 100-draw mix", specials > 0); + CHECK("the double-weighted normals dominate", normals > specials); + CHECK("every special has >= 2 normals since the previous special", violations == 0); + + s.sol.phase = 0; + s.sol.special_cooldown = 0; + int early_specials = 0; + for (int n = 0; n < 50; n++) { + int kind = col_sol_select_attack(&s); + if (kind != COLO_SOL_ATTACK_SPEAR && kind != COLO_SOL_ATTACK_SHIELD) + early_specials++; + } + CHECK("above 90% HP only spear/shield are drawn", early_specials == 0); + + s.sol.last_attack_kind = COLO_SOL_ATTACK_NONE; + s.sol.last_variant = 0; + int v1 = col_sol_pick_variant(&s.sol, COLO_SOL_ATTACK_SPEAR); + int v2 = col_sol_pick_variant(&s.sol, COLO_SOL_ATTACK_SPEAR); + int v3 = col_sol_pick_variant(&s.sol, COLO_SOL_ATTACK_SPEAR); + int v4 = col_sol_pick_variant(&s.sol, COLO_SOL_ATTACK_SHIELD); + int v5 = col_sol_pick_variant(&s.sol, COLO_SOL_ATTACK_SPEAR); + CHECK("consecutive same-type casts alternate 1 -> 2 -> 1", v1 == 1 && v2 == 2 && v3 == 1); + CHECK("a type switch resets the variant to 1", v4 == 1 && v5 == 1); + + idx = sol_setup(&s, &ctx, 107); + sol_move_player(&s, s.npcs[idx].x + 2, s.npcs[idx].y - 1); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int opener_tick = -1; + for (int t = 0; t < 10 && opener_tick < 0; t++) { + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + if (s.npcs[idx].attacked_this_tick) opener_tick = s.tick; + } + CHECK("opener landed against the adjacent player", opener_tick > 0); + + s.npcs[idx].hp = (COLO_SOL_HP_MAX * 89) / 100; + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + CHECK("the 90% crossing enters phase 1", s.sol.phase == 1); + CHECK("the transition spawns the phase crystal", s.sol.crystal_count == 1); + CHECK("the transition drops 6 beams around the player", + sol_count_active_beams(&s) == 6); + CHECK("Sol is frozen through the transition", s.sol.immobile_ticks > 0); + CHECK("the post-transition attack is forced to Spear", s.sol.force_spear == 1); + int next_kind = -1; + for (int t = 0; t < 20 && next_kind < 0; t++) { + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + if (s.npcs[idx].attacked_this_tick) next_kind = s.sol.last_attack_kind; + } + CHECK("the first attack after the transition is a Spear", + next_kind == COLO_SOL_ATTACK_SPEAR); +} + +static void test_sol_parry_schedule_and_damage(void) { + printf("test_sol_parry_schedule_and_damage\n"); + ColosseumContext ctx; + ColosseumState s; + int idle[COLO_NUM_ACTION_HEADS] = {0}; + + for (int low = 0; low <= 1; low++) { + int idx = sol_setup(&s, &ctx, 109 + (uint32_t)low); + s.npcs[idx].hp = low ? (COLO_SOL_HP_MAX * 40) / 100 : (COLO_SOL_HP_MAX * 80) / 100; + s.sol.phase = low ? 3 : 1; + s.sol.attack_delay = 1000; + sol_move_player(&s, 12, 12); + col_sol_start_triple_parry(&s, idx); + s.player.current_hitpoints = 99; + + int dmg_at[13] = {0}; + int hp_prev = 99; + for (int t = 1; t <= 12; t++) { + step_and_observe(&s, &ctx, idle); + dmg_at[t] = hp_prev - s.player.current_hitpoints; + hp_prev = s.player.current_hitpoints; + } + int h3 = low ? 10 : 9; + int d2 = low ? 30 : 25; + int d3 = low ? 45 : 35; + CHECK(low ? "low band: 15/30/45 land at +3/+6/+10" + : "high band: 15/25/35 land at +3/+6/+9", + dmg_at[3] == 15 && dmg_at[6] == d2 && dmg_at[h3] == d3); + int clean = 1; + for (int t = 1; t <= 12; t++) + if (t != 3 && t != 6 && t != h3 && dmg_at[t] != 0) clean = 0; + CHECK("no parry damage lands off-schedule", clean); + CHECK("the combo retires after the third hit", s.sol.parry_hits_left == 0); + } +} + +static void test_sol_parry_prayer_punish(void) { + printf("test_sol_parry_prayer_punish\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup(&s, &ctx, 113); + s.npcs[idx].hp = (COLO_SOL_HP_MAX * 80) / 100; + s.sol.phase = 1; + s.sol.attack_delay = 1000; + sol_move_player(&s, 12, 12); + + col_sol_start_triple_parry(&s, idx); + s.player.current_hitpoints = 99; + int actions[COLO_NUM_ACTION_HEADS] = {0}; + int deactivated_after_each = 1; + int prayer_correct_before = s.log.total_prayer_correct; + for (int t = 1; t <= 9; t++) { + actions[COLO_HEAD_PRAYER] = (t == 3 || t == 6 || t == 9) + ? COLO_OVERHEAD_MELEE : COLO_OVERHEAD_NO_CHANGE; + step_and_observe(&s, &ctx, actions); + if ((t == 3 || t == 6 || t == 9) && s.player.prayer != PRAYER_NONE) + deactivated_after_each = 0; + } + CHECK("flicking Protect from Melee exactly at land blocks all three hits", + s.player.current_hitpoints == 99); + CHECK("every parry hit force-deactivates the overhead prayers", + deactivated_after_each); + CHECK("blocked parry hits count prayer_correct", + s.log.total_prayer_correct >= prayer_correct_before + 3); + + col_sol_start_triple_parry(&s, idx); + s.player.current_hitpoints = 99; + for (int t = 1; t <= 9; t++) { + actions[COLO_HEAD_PRAYER] = COLO_OVERHEAD_MELEE; + step_and_observe(&s, &ctx, actions); + } + CHECK("camping the prayer early makes every hit unblockable (75 total)", + s.player.current_hitpoints == 99 - (15 + 25 + 35)); +} + +static void test_sol_grapple_perfect_parry(void) { + printf("test_sol_grapple_perfect_parry\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup(&s, &ctx, 127); + s.sol.attack_delay = 1000; + sol_move_player(&s, 18, 18); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int actions[COLO_NUM_ACTION_HEADS] = {0}; + + col_sol_start_grapple(&s); + CHECK("the called slot is inside the 5-slot A12 domain", + s.sol.grapple_body_slot >= 0 && s.sol.grapple_body_slot < COLO_NUM_GRAPPLE_SLOTS); + s.player.current_hitpoints = 99; + for (int t = 0; t < COLO_SOL_GRAPPLE_WINDOW; t++) step_and_observe(&s, &ctx, idle); + int fail_dmg = 99 - s.player.current_hitpoints; + CHECK("an unanswered grapple lands 20-44", fail_dmg >= 20 && fail_dmg <= 44); + + col_sol_start_grapple(&s); + s.player.current_hitpoints = 99; + actions[COLO_HEAD_GRAPPLE_PARRY] = s.sol.grapple_body_slot + 1; + step_and_observe(&s, &ctx, actions); + CHECK("an early correct click parries without the perfect bonus", + !s.sol.grapple_active && s.player.current_hitpoints == 99 && + s.sol.next_attack_guaranteed_max == 0); + actions[COLO_HEAD_GRAPPLE_PARRY] = 0; + + col_sol_start_grapple(&s); + s.player.current_hitpoints = 99; + int slot = s.sol.grapple_body_slot; + while (s.sol.grapple_timer > 2) step_and_observe(&s, &ctx, idle); + actions[COLO_HEAD_GRAPPLE_PARRY] = slot + 1; + step_and_observe(&s, &ctx, actions); + actions[COLO_HEAD_GRAPPLE_PARRY] = 0; + CHECK("a last-tick click is a perfect parry: no damage, max armed", + s.player.current_hitpoints == 99 && s.sol.next_attack_guaranteed_max == 1); + + int max_hit = col_live_loadout_stats(&s)->max_hit; + CHECK("rig sanity: the melee loadout has a positive max hit", max_hit > 0); + col_player_attack_target(&s, idx); + CHECK("the guaranteed max is consumed at no less than the loadout max hit", + s.player_attack_dmg >= max_hit && s.sol.next_attack_guaranteed_max == 0 && + s.sol.guaranteed_max_ticks == 0); + + col_sol_start_grapple(&s); + s.player.current_hitpoints = 99; + while (s.sol.grapple_timer > 2) step_and_observe(&s, &ctx, idle); + actions[COLO_HEAD_GRAPPLE_PARRY] = s.sol.grapple_body_slot + 1; + step_and_observe(&s, &ctx, actions); + actions[COLO_HEAD_GRAPPLE_PARRY] = 0; + CHECK("second perfect parry armed", s.sol.next_attack_guaranteed_max == 1); + for (int t = 0; t < COLO_SOL_PERFECT_MAX_TICKS; t++) step_and_observe(&s, &ctx, idle); + CHECK("an unconsumed guaranteed max expires after 5 ticks", + s.sol.next_attack_guaranteed_max == 0); +} + +static void test_sol_perfect_parry_forces_spec_attack(void) { + printf("test_sol_perfect_parry_forces_spec_attack\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup_speedrun(&s, &ctx, 128); + CHECK("speedrun Sol setup succeeded", idx >= 0); + CHECK("speedrun inventory carries dragon claws", + test_find_inventory_cell_with_item(&s, ITEM_DRAGON_CLAWS) >= 0); + + int max_hit = test_col_spec_stats_for_kind(&s, 1).max_hit; + int claws_total = 2 * max_hit - 1; + int expected_total = claws_total / 2 + claws_total / 4 + + claws_total / 8 + claws_total / 8 + 1; + CHECK("rig sanity: claws max hit is positive", max_hit > 0); + + s.sol.attack_delay = 1000; + s.player.special_energy = 100; + col_equip_from_cell(&s, test_find_inventory_cell_with_item(&s, ITEM_DRAGON_CLAWS)); + s.player.spec_armed = 1; + s.sol.next_attack_guaranteed_max = 1; + s.sol.guaranteed_max_ticks = COLO_SOL_PERFECT_MAX_TICKS; + col_player_attack_target(&s, idx); + + CHECK("perfect-parry claws uses the forced first-success best total", + s.player_attack_dmg == expected_total && + s.npcs[idx].pending_hits.count == 4); + CHECK("perfect-parry spec consumes and clears the max flag", + s.sol.next_attack_guaranteed_max == 0 && + s.sol.guaranteed_max_ticks == 0); + CHECK("perfect-parry spec still spends claws energy", + s.player.special_energy == 50 && s.player.spec_armed == 0); +} + +static void test_sol_shield_safe_rings(void) { + printf("test_sol_shield_safe_rings\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup(&s, &ctx, 131); + sol_pin(&s, idx, COLO_SOL_SPAWN_X, COLO_SOL_SPAWN_Y); + ColoNPC* boss = &s.npcs[idx]; + int cx = boss->x + 2, cy = boss->y + 2; + s.sol.aoe_x = boss->x; + s.sol.aoe_y = boss->y; + + s.sol.aoe_attack = COLO_SOL_AOE_SHIELD1; + CHECK("shield1: the inner 7x7 burns", + col_sol_aoe_tile_is_hazard(&s.sol, cx, cy - 3)); + CHECK("shield1: the Chebyshev-4 ring face is safe", + !col_sol_aoe_tile_is_hazard(&s.sol, cx, cy - 4)); + CHECK("shield1: the ring corner is safe", + !col_sol_aoe_tile_is_hazard(&s.sol, cx - 4, cy - 4)); + CHECK("shield1: one past the ring burns", + col_sol_aoe_tile_is_hazard(&s.sol, cx, cy - 5)); + CHECK("shield1: the far arena burns", + col_sol_aoe_tile_is_hazard(&s.sol, cx - 8, cy - 8)); + + s.sol.aoe_attack = COLO_SOL_AOE_SHIELD2; + CHECK("shield2: Chebyshev 4 is inside the 9x9 block", + col_sol_aoe_tile_is_hazard(&s.sol, cx, cy - 4)); + CHECK("shield2: the Chebyshev-5 ring is safe", + !col_sol_aoe_tile_is_hazard(&s.sol, cx, cy - 5) && + !col_sol_aoe_tile_is_hazard(&s.sol, cx - 5, cy - 5)); + CHECK("shield2: one past the ring burns", + col_sol_aoe_tile_is_hazard(&s.sol, cx, cy - 6)); + + s.sol.aoe_attack = COLO_SOL_AOE_NONE; + s.sol.attack_delay = 1000; + int idle[COLO_NUM_ACTION_HEADS] = {0}; + for (int variant = 1; variant <= 2; variant++) { + int ring = variant == 1 ? COLO_SOL_SHIELD1_RING : COLO_SOL_SHIELD2_RING; + for (int spot = 0; spot < 3; spot++) { + int off = spot == 0 ? ring : (spot == 1 ? ring - 1 : ring + 1); + sol_move_player(&s, cx, cy - off); + col_sol_cast_aoe(&s, idx, COLO_SOL_ATTACK_SHIELD, variant); + s.player.current_hitpoints = 99; + for (int t = 0; t < COLO_SOL_AOE_DAMAGE_AGE; t++) + step_and_observe(&s, &ctx, idle); + int dmg = 99 - s.player.current_hitpoints; + if (spot == 0) + CHECK(variant == 1 ? "shield1 bite: the ring tile is safe" + : "shield2 bite: the ring tile is safe", dmg == 0); + else + CHECK(spot == 1 ? "shield bite: inside the block lands 20-44" + : "shield bite: outside the ring lands 20-44", + dmg >= 20 && dmg <= 44); + s.sol.aoe_attack = COLO_SOL_AOE_NONE; + } + } +} + +static void test_sol_spear_lines(void) { + printf("test_sol_spear_lines\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup(&s, &ctx, 137); + sol_pin(&s, idx, COLO_SOL_SPAWN_X, COLO_SOL_SPAWN_Y); + + sol_move_player(&s, 18, 18); + col_sol_cast_aoe(&s, idx, COLO_SOL_ATTACK_SPEAR, 1); + CHECK("the cast leaves the spear direction unresolved", + s.sol.aoe_dir_x == 0 && s.sol.aoe_dir_y == 0); + int front_ok = 1; + for (int x = 16; x <= 20; x++) + if (!col_sol_aoe_tile_is_hazard(&s.sol, x, 18)) front_ok = 0; + CHECK("spear1 front row covers all 5 columns", front_ok); + CHECK("spear1 runs TWO lines at the off-centre columns", + col_sol_aoe_tile_is_hazard(&s.sol, 17, 17) && + col_sol_aoe_tile_is_hazard(&s.sol, 19, 17)); + CHECK("spear1 centre + corner columns are safe 1 back (the dodge)", + !col_sol_aoe_tile_is_hazard(&s.sol, 16, 17) && + !col_sol_aoe_tile_is_hazard(&s.sol, 18, 17) && + !col_sol_aoe_tile_is_hazard(&s.sol, 20, 17)); + CHECK("spear lines cover 8 tiles from forward 4 (colosim LINE_LENGTH 7)", + col_sol_aoe_tile_is_hazard(&s.sol, 17, 10) && + !col_sol_aoe_tile_is_hazard(&s.sol, 17, 9)); + + col_sol_cast_aoe(&s, idx, COLO_SOL_ATTACK_SPEAR, 2); + CHECK("spear2's 7x7 slam covers the full flush ring row", + col_sol_aoe_tile_is_hazard(&s.sol, 15, 18) && + col_sol_aoe_tile_is_hazard(&s.sol, 17, 18) && + col_sol_aoe_tile_is_hazard(&s.sol, 21, 18)); + CHECK("spear2 runs THREE lines at the corner + centre columns past the slam", + col_sol_aoe_tile_is_hazard(&s.sol, 16, 17) && + col_sol_aoe_tile_is_hazard(&s.sol, 18, 17) && + col_sol_aoe_tile_is_hazard(&s.sol, 20, 17)); + CHECK("spear2 off-centre columns are safe past the slam (the dodge)", + !col_sol_aoe_tile_is_hazard(&s.sol, 17, 17) && + !col_sol_aoe_tile_is_hazard(&s.sol, 19, 17)); + + sol_move_player(&s, 21, 21); + col_sol_cast_aoe(&s, idx, COLO_SOL_ATTACK_SPEAR, 1); + CHECK("east cast: the front column burns and lines run east", + col_sol_aoe_tile_is_hazard(&s.sol, 21, 21) && + col_sol_aoe_tile_is_hazard(&s.sol, 22, 20) && + col_sol_aoe_tile_is_hazard(&s.sol, 22, 22) && + !col_sol_aoe_tile_is_hazard(&s.sol, 22, 21)); + + s.sol.aoe_attack = COLO_SOL_AOE_NONE; + s.sol.attack_delay = 1000; + int idle[COLO_NUM_ACTION_HEADS] = {0}; + sol_move_player(&s, 18, 18); + col_sol_cast_aoe(&s, idx, COLO_SOL_ATTACK_SPEAR, 1); + s.player.current_hitpoints = 99; + for (int t = 0; t < COLO_SOL_AOE_DAMAGE_AGE; t++) + step_and_observe(&s, &ctx, idle); + int dmg = 99 - s.player.current_hitpoints; + CHECK("standing on a spear tile at the bite tick lands 20-44", + dmg >= 20 && dmg <= 44); + s.sol.aoe_attack = COLO_SOL_AOE_NONE; + sol_move_player(&s, 18, 18); + col_sol_cast_aoe(&s, idx, COLO_SOL_ATTACK_SPEAR, 1); + step_and_observe(&s, &ctx, idle); + sol_move_player(&s, 18, 17); + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + CHECK("the telegraph-tick dodge to the centre column avoids spear1 fully", + s.player.current_hitpoints == 99); + + s.sol.aoe_attack = COLO_SOL_AOE_NONE; + sol_move_player(&s, 18, 18); + col_sol_cast_aoe(&s, idx, COLO_SOL_ATTACK_SPEAR, 1); + step_and_observe(&s, &ctx, idle); + CHECK("the direction is still unresolved on the telegraph tick", + s.sol.aoe_dir_x == 0 && s.sol.aoe_dir_y == 0); + sol_move_player(&s, 22, 16); + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + CHECK("the bite locks the diagonal from the fleeing player's tile", + s.sol.aoe_dir_x == 1 && s.sol.aoe_dir_y == -1); + CHECK("running away laterally past the corner is NOT a free dodge", + s.player.current_hitpoints < 99 && + 99 - s.player.current_hitpoints >= 20 && + 99 - s.player.current_hitpoints <= 44); +} + +static void test_sol_crystal_lifecycle(void) { + printf("test_sol_crystal_lifecycle\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup(&s, &ctx, 139); + (void)idx; + s.sol.attack_delay = 1000; + sol_move_player(&s, 16, 14); + + int accumulates = 1, edges_ok = 1; + for (int p = 1; p <= 4; p++) { + col_sol_enter_phase(&s, p); + if (s.sol.crystal_count != p) accumulates = 0; + if (s.sol.crystals[p - 1].edge != p - 1) edges_ok = 0; + } + CHECK("one crystal spawns at each transition (4 by 25%)", accumulates); + CHECK("crystals take their own edges in N/E/S/W order", edges_ok); + col_sol_enter_phase(&s, 5); + CHECK("the enrage transition adds no fifth crystal", s.sol.crystal_count == 4); + s.sol.phase = 4; + sol_clear_beams_and_sand(&s); + + int on_segments = 1; + const ColoSolCrystal* cn = &s.sol.crystals[0]; + const ColoSolCrystal* ce = &s.sol.crystals[1]; + const ColoSolCrystal* cs2 = &s.sol.crystals[2]; + const ColoSolCrystal* cw = &s.sol.crystals[3]; + if (cn->y != 23 || cn->x < 11 || cn->x > 22) on_segments = 0; + if (ce->x != 23 || ce->y < 11 || ce->y > 22) on_segments = 0; + if (cs2->y != 10 || cs2->x < 11 || cs2->x > 22) on_segments = 0; + if (cw->x != 10 || cw->y < 11 || cw->y > 22) on_segments = 0; + CHECK("each crystal patrols one tile inside its boundary, endpoints inset 2" + " (clear of the corner pillars)", on_segments); + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int x0 = s.sol.crystals[0].x; + for (int t = 0; t < 5; t++) { + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + } + CHECK("the crystal takes its first patrol step on the 5th tick", + abs(s.sol.crystals[0].x - x0) == 1); + int x1 = s.sol.crystals[0].x; + for (int t = 0; t < 3; t++) { + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + } + CHECK("then one patrol step every 3 ticks", + abs(s.sol.crystals[0].x - x1) == 1); + int positions_hold = 1; + + sol_move_player(&s, s.sol.crystals[0].x, 14); + col_sol_fire_lasers(&s); + CHECK("the volley arms every crystal at once", + s.sol.crystals[0].firing_freeze == COLO_SOL_LASER_FREEZE && + s.sol.crystals[3].firing_freeze == COLO_SOL_LASER_FREEZE); + CHECK("the volley rerolls the shared cooldown 25-34 before enrage", + s.sol.laser_cooldown >= 25 && s.sol.laser_cooldown <= 34); + int cx_before = s.sol.crystals[0].x; + s.player.current_hitpoints = 99; + for (int t = 0; t < 6; t++) { + step_and_observe(&s, &ctx, idle); + if (s.sol.crystals[0].x != cx_before) positions_hold = 0; + } + CHECK("crystals hold position while firing", positions_hold); + CHECK("no laser damage before the freeze-3 tick", + s.player.current_hitpoints == 99); + step_and_observe(&s, &ctx, idle); + int laser_dmg = 99 - s.player.current_hitpoints; + CHECK("an aligned player eats 60-75 at firing_freeze == 3", + laser_dmg >= 60 && laser_dmg <= 75); + + for (int t = 0; t < 8; t++) step_and_observe(&s, &ctx, idle); + sol_move_player(&s, s.sol.crystals[0].x, 14); + col_sol_fire_lasers(&s); + for (int t = 0; t < 5; t++) step_and_observe(&s, &ctx, idle); + sol_move_player(&s, s.sol.crystals[0].x + 1, 14); + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + CHECK("stepping off the line before the damage tick dodges the laser", + s.player.current_hitpoints == 99); + + s.sol.phase = 5; + col_sol_fire_lasers(&s); + CHECK("at enrage the volley cooldown is 12", + s.sol.laser_cooldown == COLO_SOL_CRYSTAL_COOLDOWN_ENRAGE); +} + +static void test_sol_aoe_reaction_window(void) { + printf("test_sol_aoe_reaction_window\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup(&s, &ctx, 149); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + + int cast_seen = 0; + for (int t = 0; t < 30 && !cast_seen; t++) { + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + if (s.npcs[idx].attacked_this_tick) cast_seen = 1; + } + CHECK("the opener spear casts with the player on a hazard tile", + cast_seen && s.sol.aoe_attack == COLO_SOL_AOE_SPEAR1 && + col_sol_aoe_tile_is_hazard(&s.sol, s.player.x, s.player.y)); + + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + CHECK("no AoE damage on the telegraph tick (cast + 1)", + s.player.current_hitpoints == 99); + step_and_observe(&s, &ctx, idle); + CHECK("a stationary player is hit exactly 2 ticks after the cast", + s.player.current_hitpoints < 99); + + sol_move_player(&s, 16, 16); + cast_seen = 0; + for (int t = 0; t < 20 && !cast_seen; t++) { + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + if (s.npcs[idx].attacked_this_tick) cast_seen = 1; + } + CHECK("a second AoE cast follows on the spear/shield rotation", + cast_seen && s.sol.aoe_attack != COLO_SOL_AOE_NONE && + col_sol_aoe_tile_is_hazard(&s.sol, s.player.x, s.player.y)); + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + int safe_x = -1, safe_y = -1; + for (int x = s.player.x - 3; x <= s.player.x + 3 && safe_x < 0; x++) + for (int y = s.player.y - 3; y <= s.player.y + 3 && safe_x < 0; y++) + if (col_in_boss_arena(&s, x, y) && !col_static_blocked(x, y) && + !col_sol_aoe_tile_is_hazard(&s.sol, x, y)) { + safe_x = x; safe_y = y; + } + CHECK("a safe tile exists within reach of the dodge", safe_x >= 0); + sol_move_player(&s, safe_x, safe_y); + step_and_observe(&s, &ctx, idle); + CHECK("moving off on the telegraph tick dodges the slam", + s.player.current_hitpoints == 99); +} + +static void test_sol_laser_react_window(void) { + printf("test_sol_laser_react_window\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup(&s, &ctx, 151); + (void)idx; + s.sol.attack_delay = 1000; + int idle[COLO_NUM_ACTION_HEADS] = {0}; + + col_sol_spawn_crystal(&s); + sol_move_player(&s, s.sol.crystals[0].x, 14); + col_sol_fire_lasers(&s); + s.player.current_hitpoints = 99; + for (int t = 0; t < 3; t++) step_and_observe(&s, &ctx, idle); + CHECK("the beam telegraph opens 3 ticks after the volley (freeze 6)", + s.sol.crystals[0].firing_freeze == COLO_SOL_LASER_BEAM_SHOW_MAX); + for (int t = 0; t < 3; t++) step_and_observe(&s, &ctx, idle); + CHECK("the 3 reaction ticks pass without damage", + s.player.current_hitpoints == 99); + step_and_observe(&s, &ctx, idle); + CHECK("the aligned hit lands on the tick after the reaction window", + s.player.current_hitpoints < 99); +} + +static void test_sol_phase_transition_sand_guarantees(void) { + printf("test_sol_phase_transition_sand_guarantees\n"); + ColosseumContext ctx; + ColosseumState s; + int seeded_ok = 1; + + for (uint32_t seed = 150; seed < 230; seed++) { + int idx = sol_setup(&s, &ctx, seed); + (void)idx; + s.sol.attack_delay = 1000; + sol_move_player(&s, 17, 14); + col_sol_enter_phase(&s, 1); + for (int t = 0; t < COLO_SOL_BEAM_TO_POOL_TICKS; t++) + col_sol_tick_molten(&s); + if (!sol_phase_sand_invariants_hold(&s, COLO_SOL_BEAM_COUNT)) { + seeded_ok = 0; + break; + } + } + CHECK("seeded phase transitions place exactly 6 unique in-arena sand tiles with one under player", + seeded_ok); + + int idx = sol_setup(&s, &ctx, 231); + (void)idx; + s.sol.attack_delay = 1000; + + int corner_x = COLO_BOSS_ARENA_MIN_X + 2; + int corner_y = COLO_BOSS_ARENA_MIN_Y + 1; + sol_move_player(&s, corner_x, corner_y); + CHECK("rig sanity: corner-edge player tile is walkable", + col_in_boss_arena(&s, s.player.x, s.player.y) && + !col_static_blocked(s.player.x, s.player.y)); + col_sol_enter_phase(&s, 1); + for (int t = 0; t < COLO_SOL_BEAM_TO_POOL_TICKS; t++) + col_sol_tick_molten(&s); + CHECK("corner-edge phase transition still places 6 in-arena sand tiles including player", + sol_phase_sand_invariants_hold(&s, COLO_SOL_BEAM_COUNT)); +} + +static void test_sol_beams_become_pools(void) { + printf("test_sol_beams_become_pools\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup(&s, &ctx, 149); + (void)idx; + s.sol.attack_delay = 1000; + sol_move_player(&s, 17, 14); + + col_sol_drop_beams(&s); + int beams = sol_count_active_beams(&s); + int in_box = 1; + for (int b = 0; b < COLO_SOL_BEAM_MAX; b++) { + if (!s.sol.beams[b].active) continue; + int dx = abs(s.sol.beams[b].x - s.player.x); + int dy = abs(s.sol.beams[b].y - s.player.y); + if (dx > COLO_SOL_BEAM_SPREAD || dy > COLO_SOL_BEAM_SPREAD) in_box = 0; + if (col_static_blocked(s.sol.beams[b].x, s.sol.beams[b].y)) in_box = 0; + } + CHECK("6 beams drop inside the 9x9 around the player", beams == 6 && in_box); + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + for (int t = 0; t < COLO_SOL_BEAM_TO_POOL_TICKS; t++) { + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + } + CHECK("every beam becomes a molten pool when its strike lands", + s.sol.hazard_tile_count == 6 && sol_count_active_beams(&s) == 0); + + sol_move_player(&s, s.sol.hazard_tile_x[0], s.sol.hazard_tile_y[0]); + int burns_ok = 1; + for (int t = 0; t < 30; t++) { + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + int dmg = 99 - s.player.current_hitpoints; + if (dmg < COLO_MOLTEN_SAND_MIN_HIT || + dmg > COLO_MOLTEN_SAND_MIN_HIT + COLO_MOLTEN_SAND_RAND - 1) burns_ok = 0; + } + CHECK("standing on a pool burns 5-9 every tick", burns_ok); + CHECK("pools persist for the rest of the fight", s.sol.hazard_tile_count == 6); +} + +static void test_sol_beam_strike_reaction_window(void) { + printf("test_sol_beam_strike_reaction_window\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup(&s, &ctx, 157); + s.sol.attack_delay = 1000; + sol_move_player(&s, 16, 16); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + + s.npcs[idx].hp = COLO_SOL_HP_MAX * 89 / 100; + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + int player_beam = 0; + for (int b = 0; b < COLO_SOL_BEAM_MAX; b++) + if (s.sol.beams[b].active && s.sol.beams[b].x == s.player.x && + s.sol.beams[b].y == s.player.y) player_beam = 1; + CHECK("the transition telegraphs a beam on the player's tile", + player_beam && s.sol.hazard_tile_count == 0); + CHECK("no strike damage on the telegraph tick", + s.player.current_hitpoints == 99); + step_and_observe(&s, &ctx, idle); + CHECK("no strike damage 1 tick after the telegraph", + s.player.current_hitpoints == 99 && s.sol.hazard_tile_count == 0); + step_and_observe(&s, &ctx, idle); + CHECK("the pillars strike 2 ticks after the telegraph and burn the camper", + s.sol.hazard_tile_count == 6 && s.player.current_hitpoints < 99); + + int start_x = -1, start_y = -1; + for (int x = 12; x <= 21 && start_x < 0; x++) + for (int y = 12; y <= 21 && start_x < 0; y++) { + if (!col_in_boss_arena(&s, x, y) || col_static_blocked(x, y)) continue; + int pooled = 0; + for (int p = 0; p < s.sol.hazard_tile_count; p++) + if (s.sol.hazard_tile_x[p] == x && s.sol.hazard_tile_y[p] == y) + pooled = 1; + if (!pooled) { start_x = x; start_y = y; } + } + CHECK("a pool-free start tile exists for the dodge rig", start_x >= 0); + sol_move_player(&s, start_x, start_y); + s.npcs[idx].hp = COLO_SOL_HP_MAX * 74 / 100; + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + int safe_x = -1, safe_y = -1; + for (int x = s.player.x - 2; x <= s.player.x + 2 && safe_x < 0; x++) + for (int y = s.player.y - 2; y <= s.player.y + 2 && safe_x < 0; y++) { + if (!col_in_boss_arena(&s, x, y) || col_static_blocked(x, y)) continue; + int marked = 0; + for (int b = 0; b < COLO_SOL_BEAM_MAX; b++) + if (s.sol.beams[b].active && s.sol.beams[b].x == x && + s.sol.beams[b].y == y) marked = 1; + for (int p = 0; p < s.sol.hazard_tile_count; p++) + if (s.sol.hazard_tile_x[p] == x && s.sol.hazard_tile_y[p] == y) + marked = 1; + if (!marked) { safe_x = x; safe_y = y; } + } + CHECK("an unmarked tile exists within reach of the beam dodge", safe_x >= 0); + sol_move_player(&s, safe_x, safe_y); + step_and_observe(&s, &ctx, idle); + step_and_observe(&s, &ctx, idle); + CHECK("moving off the marked tile on the telegraph tick dodges the strike", + s.player.current_hitpoints == 99); +} + +static void test_sol_enrage_sand_telegraphs(void) { + printf("test_sol_enrage_sand_telegraphs\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup(&s, &ctx, 163); + s.sol.attack_delay = 30000; + sol_move_player(&s, 16, 16); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + + s.npcs[idx].hp = COLO_SOL_HP_MAX * 89 / 100; + for (int t = 0; t < 3; t++) { + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + } + int pools_before = s.sol.hazard_tile_count; + CHECK("the phase-1 sands have pooled before the enrage rig", + pools_before >= 1 && sol_count_active_beams(&s) == 0); + + int before = s.sol.hazard_tile_count; + col_sol_add_pool(&s, s.sol.hazard_tile_x[0], s.sol.hazard_tile_y[0]); + CHECK("re-covering a pooled tile has no additional effect", + s.sol.hazard_tile_count == before); + + s.npcs[idx].hp = COLO_SOL_HP_MAX * 9 / 100; + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + CHECK("enrage entry creates NO instant pools (every sand telegraphs)", + s.sol.hazard_tile_count == pools_before && + sol_count_active_beams(&s) >= COLO_SOL_ENRAGE_OPEN_POOLS); + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + CHECK("no enrage sand strikes 1 tick after the telegraphs", + s.sol.hazard_tile_count == pools_before); + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + int pools_at_strike = s.sol.hazard_tile_count; + CHECK("the enrage sands strike 2 ticks after their telegraphs", + pools_at_strike > pools_before); + CHECK("the enrage spam keeps telegraphing (no instant spam pools)", + sol_count_active_beams(&s) >= 1); + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + CHECK("spam sand does not strike 1 tick after its telegraph", + s.sol.hazard_tile_count == pools_at_strike); + s.player.current_hitpoints = 99; + step_and_observe(&s, &ctx, idle); + CHECK("spam sand strikes 2 ticks after its telegraph", + s.sol.hazard_tile_count == pools_at_strike + 1); +} + +static void test_solarflare_sol_orbit_boxes(void) { + printf("test_solarflare_sol_orbit_boxes\n"); + ColosseumContext ctx; + ColosseumState s; + int idx = sol_setup(&s, &ctx, 167); + (void)idx; + s.modifiers.active_mask |= (1u << COLO_MOD_SOLARFLARE); + s.modifiers.tier[COLO_MOD_SOLARFLARE] = 2; + col_mod_sync_solarflare(&s); + CHECK("Solarflare orb is active on the Sol wave", s.solarflare.active); + + int inside = 1; + int anchors_distinct = 1; + int ax[COLO_NUM_PILLARS], ay[COLO_NUM_PILLARS]; + for (int p = 0; p < COLO_NUM_PILLARS; p++) { + col_solarflare_tile(&s, p, 0, &ax[p], &ay[p]); + for (int q = 0; q < p; q++) + if (ax[p] == ax[q] && ay[p] == ay[q]) anchors_distinct = 0; + for (int step = 0; step < COLO_SOLARFLARE_RING_STEPS; step++) { + int x, y; + col_solarflare_tile(&s, p, step, &x, &y); + if (x <= s.sol.boss_arena_min_x || x >= s.sol.boss_arena_max_x || + y <= s.sol.boss_arena_min_y || y >= s.sol.boss_arena_max_y) + inside = 0; + } + } + CHECK("all four Sol orbit boxes sit inside the improvised arena", inside); + CHECK("the four Sol orbit boxes are distinct corners", anchors_distinct); + + ColosseumContext ctx1; + ColosseumState s1; + col_init_context_typed(&ctx1); + memset(&s1, 0, sizeof(s1)); + col_reset_ctx((EncounterState*)&s1, (EncounterContext*)&ctx1, 29); + int x0, y0; + col_solarflare_tile(&s1, 0, 0, &x0, &y0); + CHECK("waves 1-11 keep the pillar orbit geometry", + x0 == COLO_PILLARS[0][0] - 1 && y0 == COLO_PILLARS[0][1] - 1); +} + +static void loadout_reset(ColosseumState* s, ColosseumContext* ctx, int mode, + float frac, uint32_t seed) { + col_init_context_typed(ctx); + ctx->config.loadout_profile_mode = mode; + ctx->config.beginner_loadout_fraction = frac; + memset(s, 0, sizeof(*s)); + col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); + + s->modifiers.draft_pending = 0; + s->modifiers.draft_gates_spawn = 0; + s->modifiers.draft_free_movement = 0; + s->wave_spawn_delay = col_wave_entry_delay_ticks(s->wave_spawn_target); +} + +static int col_loadout_stats_equal( + const EncounterLoadoutStats* a, + const EncounterLoadoutStats* b +) { + return a->attack_bonus == b->attack_bonus && + a->strength_bonus == b->strength_bonus && + a->eff_level == b->eff_level && + a->max_hit == b->max_hit && + a->attack_speed == b->attack_speed && + a->attack_range == b->attack_range && + a->style == b->style && + a->fight_style == b->fight_style && + a->def_stab == b->def_stab && + a->def_slash == b->def_slash && + a->def_crush == b->def_crush && + a->def_magic == b->def_magic && + a->def_ranged == b->def_ranged && + a->att_prayer_mult == b->att_prayer_mult && + a->str_prayer_mult == b->str_prayer_mult && + a->spell_base_damage == b->spell_base_damage; +} + +static EncounterLoadoutStats test_col_live_stats_for_set( + const ColosseumState* s, + ColoWeaponSet set +) { + ColosseumState copy = *s; + col_apply_weapon_set(©, set); + return *col_live_loadout_stats(©); +} + +static OsrsEquipmentEffectProfile test_col_live_effects_for_set( + const ColosseumState* s, + ColoWeaponSet set +) { + ColosseumState copy = *s; + col_apply_weapon_set(©, set); + return *col_live_effects(©); +} + +static EncounterLoadoutStats test_col_spec_stats_for_kind( + const ColosseumState* s, + int kind +) { + ColosseumState copy = *s; + copy.player.equipped[GEAR_SLOT_WEAPON] = + test_profile_spec_item(copy.active_loadout_profile, kind); + copy.player.equipped[GEAR_SLOT_SHIELD] = osrs_suppress_shield_for_two_handed_weapon( + copy.player.equipped[GEAR_SLOT_WEAPON], + copy.player.equipped[GEAR_SLOT_SHIELD]); + col_sync_weapon_set_from_equipped_weapon(©); + col_mark_live_loadout_dirty(©); + return *col_live_loadout_stats(©); +} + +static void test_loadout_profiles_and_supplies(void) { + printf("test_loadout_profiles_and_supplies\n"); + ColosseumContext ctx; + ColosseumState s; + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 11); + CHECK("beginner mode pins the beginner profile", + s.active_loadout_profile == COLO_LOADOUT_PROFILE_BEGINNER); + CHECK("beginner melee weapon is the fang", + s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_OSMUMTENS_FANG); + CHECK("beginner supplies match the budget example", + s.player.brew_doses == 24 && s.player.restore_doses == 32 && + s.player.combat_potion_doses == 8 && s.player.ranged_potion_doses == 8 && + s.surge_doses == 0); + CHECK("beginner restore kind is super restore", + s.full_supplies.restore_kind == COLO_RESTORE_SUPER_RESTORE); + OsrsEquipmentEffectProfile beginner_ranged_effects = + test_col_live_effects_for_set(&s, COLO_GEAR_RANGED); + OsrsEquipmentEffectProfile beginner_melee_effects = + test_col_live_effects_for_set(&s, COLO_GEAR_MELEE); + CHECK("beginner bowfa set carries full crystal points (1+2+3)", + beginner_ranged_effects.crystal_armour_points == 6); + CHECK("beginner melee neck carries blood fury", + osrs_effect_profile_has(&beginner_melee_effects, OSRS_ITEM_EFFECT_BLOOD_FURY)); + CHECK("beginner melee head carries venom immunity", + osrs_effect_profile_has(&beginner_melee_effects, OSRS_ITEM_EFFECT_VENOM_IMMUNE)); + int beginner_melee_max = col_live_loadout_stats(&s)->max_hit; + CHECK("beginner spec weapons are SGS then claws", + test_find_inventory_cell_with_item(&s, ITEM_SGS) >= 0 && + test_find_inventory_cell_with_item(&s, ITEM_DRAGON_CLAWS) >= 0); + + uint8_t beginner_sgs_without_defender[NUM_GEAR_SLOTS]; + memcpy(beginner_sgs_without_defender, COLO_BEGINNER_MELEE_LOADOUT, NUM_GEAR_SLOTS); + beginner_sgs_without_defender[GEAR_SLOT_WEAPON] = ITEM_SGS; + beginner_sgs_without_defender[GEAR_SLOT_SHIELD] = ITEM_NONE; + EncounterLoadoutStats beginner_sgs_expected; + encounter_compute_loadout_stats(beginner_sgs_without_defender, ATTACK_STYLE_MELEE, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AGGRESSIVE, 0, &beginner_sgs_expected); + EncounterLoadoutStats beginner_sgs_spec = test_col_spec_stats_for_kind(&s, 1); + CHECK("beginner SGS spec stats exclude dragon defender bonuses", + col_loadout_stats_equal(&beginner_sgs_spec, &beginner_sgs_expected)); + + uint8_t beginner_claws_without_defender[NUM_GEAR_SLOTS]; + memcpy(beginner_claws_without_defender, COLO_BEGINNER_MELEE_LOADOUT, NUM_GEAR_SLOTS); + beginner_claws_without_defender[GEAR_SLOT_WEAPON] = ITEM_DRAGON_CLAWS; + beginner_claws_without_defender[GEAR_SLOT_SHIELD] = ITEM_NONE; + EncounterLoadoutStats beginner_claws_without; + encounter_compute_loadout_stats(beginner_claws_without_defender, ATTACK_STYLE_MELEE, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AGGRESSIVE, 0, &beginner_claws_without); + EncounterLoadoutStats beginner_claws_spec = test_col_spec_stats_for_kind(&s, 2); + CHECK("beginner claws spec stats keep dragon defender strength", + beginner_claws_spec.strength_bonus == + beginner_claws_without.strength_bonus + + ITEM_DATABASE[ITEM_DRAGON_DEFENDER].melee_strength); + CHECK("beginner claws spec stats keep dragon defender accuracy", + beginner_claws_spec.attack_bonus > beginner_claws_without.attack_bonus); + + uint8_t beginner_bowfa_without_defender[NUM_GEAR_SLOTS]; + memcpy(beginner_bowfa_without_defender, COLO_BEGINNER_RANGED_LOADOUT, NUM_GEAR_SLOTS); + beginner_bowfa_without_defender[GEAR_SLOT_SHIELD] = ITEM_NONE; + EncounterLoadoutStats beginner_bowfa_legal_2h; + encounter_compute_loadout_stats(beginner_bowfa_without_defender, ATTACK_STYLE_RANGED, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, &beginner_bowfa_legal_2h); + EncounterLoadoutStats beginner_ranged_stats = + test_col_live_stats_for_set(&s, COLO_GEAR_RANGED); + CHECK("beginner bowfa ranged stats suppress the shield slot", + col_loadout_stats_equal( + &beginner_ranged_stats, &beginner_bowfa_legal_2h)); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 1.0f, 12); + CHECK("speedrun mode pins the speedrun profile", + s.active_loadout_profile == COLO_LOADOUT_PROFILE_SPEEDRUN); + CHECK("speedrun melee weapon is the scythe", + s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_SCYTHE_OF_VITUR); + CHECK("speedrun loadout has exactly one scythe", + test_count_item_in_equipment_and_inventory(&s, ITEM_SCYTHE_OF_VITUR) == 1); + CHECK("speedrun supplies match the high-efficiency kit", + s.player.brew_doses == 4 && s.player.restore_doses == 28 && + s.player.combat_potion_doses == 4 && s.player.ranged_potion_doses == 4 && + s.surge_doses == 4); + CHECK("speedrun restore kind is sanfew", + s.full_supplies.restore_kind == COLO_RESTORE_SANFEW); + OsrsEquipmentEffectProfile speedrun_ranged_effects = + test_col_live_effects_for_set(&s, COLO_GEAR_RANGED); + CHECK("speedrun ranged set has the tbow effect", + osrs_effect_profile_has(&speedrun_ranged_effects, OSRS_ITEM_EFFECT_TWISTED_BOW)); + + EncounterLoadoutStats speedrun_melee_stats = *col_live_loadout_stats(&s); + EncounterLoadoutStats speedrun_ranged_stats = + test_col_live_stats_for_set(&s, COLO_GEAR_RANGED); + CHECK("speedrun scythe total (7/4 splats) out-hits the beginner fang", + speedrun_melee_stats.max_hit * 7 / 4 > beginner_melee_max); + CHECK("both loadouts melee-style on the melee set", + speedrun_melee_stats.style == ATTACK_STYLE_MELEE && + speedrun_ranged_stats.style == ATTACK_STYLE_RANGED); + CHECK("spec stats computed for both spec weapons", + test_col_spec_stats_for_kind(&s, 1).max_hit > 0 && + test_col_spec_stats_for_kind(&s, 2).max_hit > 0); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_MIXED, 1.0f, 13); + CHECK("mixed fraction 1.0 always samples beginner", + s.active_loadout_profile == COLO_LOADOUT_PROFILE_BEGINNER); + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_MIXED, 0.0f, 14); + CHECK("mixed fraction 0.0 always samples speedrun", + s.active_loadout_profile == COLO_LOADOUT_PROFILE_SPEEDRUN); +} + +static void test_loadout_consumables(void) { + printf("test_loadout_consumables\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 21); + complete_open_draft(&s, &ctx, 1); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int base_max_hit = col_live_loadout_stats(&s)->max_hit; + + s.player.current_hitpoints = 50; + int brew[COLO_NUM_ACTION_HEADS] = {0}; + test_click_consumable_action(&s, brew, OSRS_CONSUMABLE_BREW); + step_and_observe(&s, &ctx, brew); + CHECK("brew heals 16", s.player.current_hitpoints == 66); + CHECK("brew consumes a dose and starts the timer", + s.player.brew_doses == 23 && s.player.potion_timer == 3); + CHECK("brew drains attack below base", s.player.current_attack < 99); + CHECK("brew drain lowers the melee max hit", + col_live_loadout_stats(&s)->max_hit < base_max_hit); + + for (int t = 0; t < 3; t++) step_and_observe(&s, &ctx, idle); + int restore[COLO_NUM_ACTION_HEADS] = {0}; + test_click_consumable_action(&s, restore, OSRS_CONSUMABLE_SUPER_RESTORE); + step_and_observe(&s, &ctx, restore); + CHECK("super restore returns attack to base", s.player.current_attack == 99); + CHECK("restore recovers the melee max hit", + col_live_loadout_stats(&s)->max_hit == base_max_hit); + CHECK("restore consumed a dose", s.player.restore_doses == 31); + + for (int t = 0; t < 3; t++) step_and_observe(&s, &ctx, idle); + int combat[COLO_NUM_ACTION_HEADS] = {0}; + test_click_consumable_action(&s, combat, OSRS_CONSUMABLE_SUPER_COMBAT); + step_and_observe(&s, &ctx, combat); + CHECK("super combat boosts attack to 118", s.player.current_attack == 118); + CHECK("super combat boosts strength to 118", s.player.current_strength == 118); + CHECK("super combat raises the melee max hit", + col_live_loadout_stats(&s)->max_hit > base_max_hit); + CHECK("combat pot consumed a dose", s.player.combat_potion_doses == 7); + + float mask[COLO_ACTION_MASK_SIZE]; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + int combat_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_SUPER_COMBAT); + int surge_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_SURGE); + CHECK("combat pot masked while potion timer is live", + test_click_mask_for_cell_s(&s, mask, combat_cell) == 0.0f); + CHECK("surge masked for the beginner (no doses)", + surge_cell < 0); + + for (int t = 0; t < 3; t++) step_and_observe(&s, &ctx, idle); + int rng_pot[COLO_NUM_ACTION_HEADS] = {0}; + test_click_consumable_action(&s, rng_pot, OSRS_CONSUMABLE_RANGING); + step_and_observe(&s, &ctx, rng_pot); + CHECK("ranging potion boosts ranged to 112", s.player.current_ranged == 112); + + for (int t = 0; t < 3; t++) step_and_observe(&s, &ctx, idle); + s.player.current_prayer = 40; + step_and_observe(&s, &ctx, restore); + CHECK("super restore gives +32 prayer", s.player.current_prayer == 72); +} + +static void test_loadout_divine_potions_and_stat_drift(void) { + printf("test_loadout_divine_potions_and_stat_drift\n"); + ColosseumContext ctx; + ColosseumState s; + int idle[COLO_NUM_ACTION_HEADS] = {0}; + float mask[COLO_ACTION_MASK_SIZE]; + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 81); + CHECK("speedrun combat and ranging potions are divine", + test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_DIVINE_COMBAT) >= 0 && + test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_DIVINE_RANGING) >= 0); + int divine_combat_cell = + test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_DIVINE_COMBAT); + int divine_ranged_cell = + test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_DIVINE_RANGING); + s.player.current_hitpoints = 10; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + CHECK("divine boost potions are still clickable at 10 HP", + test_click_mask_for_cell_s(&s, mask, divine_combat_cell) == 1.0f && + test_click_mask_for_cell_s(&s, mask, divine_ranged_cell) == 1.0f); + s.player.current_hitpoints = 11; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + CHECK("divine boost potions unmask above 10 HP", + test_click_mask_for_cell_s(&s, mask, divine_combat_cell) == 1.0f && + test_click_mask_for_cell_s(&s, mask, divine_ranged_cell) == 1.0f); + + s.player.current_hitpoints = 50; + int combat[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, combat, divine_combat_cell); + step_and_observe(&s, &ctx, combat); + CHECK("divine combat chunks 10 HP and starts a 500-tick hold", + s.player.current_hitpoints == 40 && + s.divine_combat_timer == ENCOUNTER_DIVINE_POTION_TICKS && + s.player.combat_potion_doses == 3); + CHECK("divine combat boosts to the held floor", + s.player.current_attack == 118 && s.player.current_strength == 118 && + s.player.current_defence == 118); + for (int t = 0; t < ENCOUNTER_DIVINE_POTION_TICKS - 1; t++) + col_tick_live_stat_drift_and_divines(&s); + CHECK("divine combat stats survive through tick 499", + s.player.current_attack == 118 && s.player.current_strength == 118 && + s.player.current_defence == 118 && s.divine_combat_timer == 1); + col_tick_live_stat_drift_and_divines(&s); + CHECK("divine combat expiry drops its stats to base instantly", + s.player.current_attack == 99 && s.player.current_strength == 99 && + s.player.current_defence == 99 && s.divine_combat_timer == 0); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 82); + s.player.current_hitpoints = 50; + int ranged[COLO_NUM_ACTION_HEADS] = {0}; + test_click_consumable_action(&s, ranged, OSRS_CONSUMABLE_DIVINE_RANGING); + step_and_observe(&s, &ctx, ranged); + CHECK("divine ranging chunks 10 HP and starts a 500-tick hold", + s.player.current_hitpoints == 40 && + s.divine_ranged_timer == ENCOUNTER_DIVINE_POTION_TICKS && + s.player.ranged_potion_doses == 3); + for (int t = 0; t < ENCOUNTER_DIVINE_POTION_TICKS; t++) + col_tick_live_stat_drift_and_divines(&s); + CHECK("divine ranging expiry drops Ranged to base instantly", + s.player.current_ranged == 99 && s.divine_ranged_timer == 0); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 83); + CHECK("beginner boost potions are regular", + test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_SUPER_COMBAT) >= 0 && + test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_RANGING) >= 0); + int regular_combat[COLO_NUM_ACTION_HEADS] = {0}; + test_click_consumable_action(&s, regular_combat, OSRS_CONSUMABLE_SUPER_COMBAT); + step_and_observe(&s, &ctx, regular_combat); + CHECK("regular beginner combat boost does not arm a divine timer", + s.player.current_hitpoints == 99 && s.divine_combat_timer == 0 && + s.player.current_attack == 118); + for (int t = 0; t < ENCOUNTER_STAT_DRIFT_TICKS; t++) + col_tick_live_stat_drift_and_divines(&s); + CHECK("regular beginner combat boost decays one level after 100 live ticks", + s.player.current_attack == 117 && s.player.current_strength == 117 && + s.player.current_defence == 117); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 84); + s.stat_drift_timer = ENCOUNTER_STAT_DRIFT_TICKS - 1; + s.divine_combat_timer = 1; + s.player.current_attack = 118; + s.player.current_strength = 118; + s.player.current_defence = 118; + step_and_observe(&s, &ctx, idle); + CHECK("stat drift and divine timers freeze during the draft gap", + s.stat_drift_timer == ENCOUNTER_STAT_DRIFT_TICKS - 1 && + s.divine_combat_timer == 1 && s.player.current_attack == 118); + + s.stat_drift_timer = 37; + s.divine_combat_timer = 123; + s.divine_ranged_timer = 234; + ColoSnapshot snap; + col_snapshot_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &snap); + CHECK("snapshot version is v20 for Solarflare shared cadence", + snap.version == COLO_SNAPSHOT_VERSION && COLO_SNAPSHOT_VERSION == 20u); + ColosseumState restored; + memset(&restored, 0, sizeof(restored)); + col_restore_ctx((EncounterState*)&restored, (EncounterContext*)&ctx, &snap, sizeof(snap)); + CHECK("snapshot round-trips stat drift and divine timers", + restored.stat_drift_timer == 37 && + restored.divine_combat_timer == 123 && + restored.divine_ranged_timer == 234); +} + +static void test_loadout_sanfew_and_serp_helm(void) { + printf("test_loadout_sanfew_and_serp_helm\n"); + ColosseumContext ctx; + ColosseumState s; + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 31); + complete_open_draft(&s, &ctx, 1); + s.player_venom = 8; + s.player_venom_timer = 12; + s.player_poison = COLO_POISON_BEE_CONTACT_SEVERITY; + s.player_poison_timer = 9; + s.player.current_prayer = 40; + int restore[COLO_NUM_ACTION_HEADS] = {0}; + test_click_consumable_action(&s, restore, OSRS_CONSUMABLE_SANFEW); + step_and_observe(&s, &ctx, restore); + CHECK("sanfew clears venom", s.player_venom == 0 && s.player_venom_timer == 0); + CHECK("sanfew clears poison", s.player_poison == 0 && s.player_poison_timer == 0); + CHECK("sanfew gives +33 prayer", s.player.current_prayer == 73); + CHECK("sanfew consumed a restore dose", s.player.restore_doses == 27); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 32); + s.modifiers.active_mask |= (1u << COLO_MOD_MANTIMAYHEM); + s.modifiers.tier[COLO_MOD_MANTIMAYHEM] = 2; + CHECK("rig sanity: beginner starts in the melee set", + s.weapon_set == COLO_GEAR_MELEE); + col_mod_manticore_apply_venom(&s, 1); + CHECK("serp helm blocks venom in the melee set", s.player_venom == 0); + col_apply_weapon_set(&s, COLO_GEAR_RANGED); + col_mod_manticore_apply_venom(&s, 1); + CHECK("venom applies in the ranged set", s.player_venom == COLO_VENOM_START); + col_apply_weapon_set(&s, COLO_GEAR_MELEE); + int venom_before = s.player_venom; + col_mod_manticore_apply_venom(&s, 1); + CHECK("serp helm does not cure or escalate an existing stack", + s.player_venom == venom_before); + + s.player_poison = 0; + s.player_poison_timer = 0; + col_mod_apply_bee_poison(&s); + CHECK("serp helm blocks bee poison in the melee set", s.player_poison == 0); + col_apply_weapon_set(&s, COLO_GEAR_RANGED); + col_mod_apply_bee_poison(&s); + CHECK("a venomed player cannot also be poisoned", s.player_poison == 0); + s.player_venom = 0; + s.player_venom_timer = 0; + col_mod_apply_bee_poison(&s); + CHECK("bee poison applies in the ranged set once venom is gone", + s.player_poison == COLO_POISON_BEE_CONTACT_SEVERITY); + col_mod_manticore_apply_venom(&s, 1); + CHECK("venom application replaces an active poison", + s.player_venom == COLO_VENOM_START && s.player_poison == 0 && + s.player_poison_timer == 0); +} + +static void test_consumable_overdrink_mask(void) { + printf("test_consumable_overdrink_mask\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 11); + s.player.potion_timer = 0; + + int brew = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_BREW); + int combat = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_DIVINE_COMBAT); + int sanfew = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_SANFEW); + CHECK("speedrun kit exposes brew/combat/sanfew cells", + brew >= 0 && combat >= 0 && sanfew >= 0); + + s.player.current_hitpoints = s.player.base_hitpoints; + CHECK("brew masked at full HP", !col_inventory_cell_actionable(&s, brew)); + s.player.current_hitpoints = s.player.base_hitpoints - 10; + CHECK("brew valid below max HP", col_inventory_cell_actionable(&s, brew)); + + s.player.current_attack = s.player.base_attack; + s.player.current_strength = s.player.base_strength; + s.player.current_defence = s.player.base_defence; + CHECK("combat valid at unboosted stats", col_inventory_cell_actionable(&s, combat)); + s.player.current_attack = 105; + s.player.current_strength = 112; + s.player.current_defence = 118; + CHECK("combat masked once all combat stats >= 105", + !col_inventory_cell_actionable(&s, combat)); + s.player.current_strength = 104; + CHECK("combat valid again when one stat dips below 105", + col_inventory_cell_actionable(&s, combat)); + + s.player.current_attack = s.player.base_attack; + s.player.current_strength = s.player.base_strength; + s.player.current_defence = s.player.base_defence; + s.player.current_ranged = s.player.base_ranged; + s.player.current_magic = s.player.base_magic; + s.player.current_prayer = s.player.base_prayer; + s.player_venom = 0; + s.player_poison = 0; + CHECK("sanfew masked with full stats/prayer and no venom", + !col_inventory_cell_actionable(&s, sanfew)); + s.player_venom = 4; + CHECK("sanfew valid while venomed (it cures)", col_inventory_cell_actionable(&s, sanfew)); + s.player_venom = 0; + s.player.current_prayer = s.player.base_prayer - 60; + CHECK("sanfew valid when prayer is well down", col_inventory_cell_actionable(&s, sanfew)); +} + +static void test_loadout_surge_potion(void) { + printf("test_loadout_surge_potion\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 41); + int idle[COLO_NUM_ACTION_HEADS] = {0}; + + s.player.special_energy = 40; + int surge[COLO_NUM_ACTION_HEADS] = {0}; + test_click_consumable_action(&s, surge, OSRS_CONSUMABLE_SURGE); + step_and_observe(&s, &ctx, surge); + CHECK("surge restores 25 energy", s.player.special_energy == 65); + CHECK("surge consumes a dose and arms the cooldown", + s.surge_doses == 3 && s.surge_cooldown == COLO_SURGE_COOLDOWN_TICKS); + step_and_observe(&s, &ctx, idle); + CHECK("surge cooldown frozen during the draft gap", + s.surge_cooldown == COLO_SURGE_COOLDOWN_TICKS); + complete_open_draft(&s, &ctx, 1); + while (s.wave_spawn_delay > 0 || s.wave_ready_delay > 0 || + s.wave_attack_delay > 0) + step_and_observe(&s, &ctx, idle); + int cd_before = s.surge_cooldown; + step_and_observe(&s, &ctx, idle); + CHECK("surge cooldown ticks during live gameplay", s.surge_cooldown == cd_before - 1); + + s.player.special_energy = 100; + s.surge_cooldown = 0; + s.player.potion_timer = 0; + float mask[COLO_ACTION_MASK_SIZE]; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + int surge_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_SURGE); + CHECK("surge remains clickable at full special energy", + test_click_mask_for_cell_s(&s, mask, surge_cell) == 1.0f); + int surge_doses_before = s.surge_doses; + int full_energy_surge[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, full_energy_surge, surge_cell); + step_and_observe(&s, &ctx, full_energy_surge); + CHECK("full-energy surge burns one dose and caps energy", + s.surge_doses == surge_doses_before - 1 && s.player.special_energy == 100); +} + +static void test_loadout_spec_weapons(void) { + printf("test_loadout_spec_weapons\n"); + ColosseumContext ctx; + ColosseumState s; + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 51); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.wave_ready_delay = 0; + s.player.x = 16; s.player.y = 16; + col_init_npc(&s, 0, COLO_FREMENNIK_BERSERKER, 16, 17); + s.player.special_energy = 100; + col_equip_from_cell(&s, test_find_inventory_cell_with_item(&s, ITEM_DRAGON_CLAWS)); + s.player.spec_armed = 1; + osrs_interaction_set(&s.interaction, 0); + s.player.attack_timer = 0; + col_player_attack_target(&s, 0); + CHECK("claws spec drains 50 energy", s.player.special_energy == 50); + CHECK("claws spec disarms after firing", s.player.spec_armed == 0); + CHECK("claws spec queues the 4-splat cascade", + s.npcs[0].pending_hits.count == 4); + CHECK("spec sets the claws attack speed", s.player.attack_timer == + get_item(ITEM_DRAGON_CLAWS)->attack_speed); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 54); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.wave_ready_delay = 0; + s.player.x = 10; s.player.y = 16; + col_init_npc(&s, 0, COLO_SOL_HEREDIT, 11, 14); + s.player.attack_timer = 0; + col_player_attack_target(&s, 0); + CHECK("scythe queues 3 splats into the 5x5 boss", + s.npcs[0].pending_hits.count == 3); + geo_clear_npcs(&s); + col_init_npc(&s, 0, COLO_FREMENNIK_BERSERKER, 10, 17); + s.player.attack_timer = 0; + col_player_attack_target(&s, 0); + CHECK("scythe queues 1 splat into a 1x1 warbander", + s.npcs[0].pending_hits.count == 1); + + geo_clear_npcs(&s); + s.player.x = 16; s.player.y = 16; + col_init_npc(&s, 0, COLO_FREMENNIK_ARCHER, 15, 16); + col_init_npc(&s, 1, COLO_FREMENNIK_ARCHER, 15, 15); + col_init_npc(&s, 2, COLO_FREMENNIK_ARCHER, 15, 17); + + ColScytheResolvedHit arc_hits[COLO_SCYTHE_MAX_HITS]; + int arc_count = col_resolve_scythe_hits(&s, 0, arc_hits); + int all_rank0 = 1; + for (int h = 0; h < arc_count; h++) + if (arc_hits[h].splat_rank != 0) all_rank0 = 0; + CHECK("scythe arc resolves three distinct full-100% hits", + arc_count == 3 && all_rank0 && + arc_hits[0].npc_slot != arc_hits[1].npc_slot && + arc_hits[1].npc_slot != arc_hits[2].npc_slot && + arc_hits[0].npc_slot != arc_hits[2].npc_slot); + s.player.attack_timer = 0; + col_player_attack_target(&s, 0); + CHECK("scythe arc queues one hit per distinct arc target", + s.npcs[0].pending_hits.count == 1 && + s.npcs[1].pending_hits.count == 1 && + s.npcs[2].pending_hits.count == 1); + for (int t = 0; t < 4; t++) + col_resolve_player_projectiles_on_npcs_ctx(&s, &ctx); + CHECK("scythe landed hits create one render hit per target", + ctx.npc_render_hit_count[0] == 1 && + ctx.npc_render_hit_count[1] == 1 && + ctx.npc_render_hit_count[2] == 1); + + geo_clear_npcs(&s); + s.player.x = 16; s.player.y = 16; + col_init_npc(&s, 0, COLO_FREMENNIK_ARCHER, 15, 16); + col_init_npc(&s, 1, COLO_HEALING_TOTEM, 15, 15); + col_init_npc(&s, 2, COLO_FREMENNIK_ARCHER, 15, 17); + s.player.attack_timer = 0; + col_player_attack_target(&s, 0); + CHECK("scythe arc skips incidental hazard entities", + s.npcs[0].pending_hits.count == 1 && + s.npcs[1].pending_hits.count == 0 && + s.npcs[2].pending_hits.count == 1); + + int per_target_roll_seen = 0; + for (uint32_t seed = 90; seed < 240 && !per_target_roll_seen; seed++) { + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, seed); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.wave_ready_delay = 0; + s.player.x = 16; s.player.y = 16; + col_init_npc(&s, 0, COLO_FREMENNIK_ARCHER, 15, 16); + col_init_npc(&s, 1, COLO_FREMENNIK_BERSERKER, 15, 15); + s.player.attack_timer = 0; + col_player_attack_target(&s, 0); + int scythe_max = col_live_loadout_stats(&s)->max_hit; + if (s.npcs[0].pending_hits.count == 1 && + s.npcs[1].pending_hits.count == 1 && + s.npcs[0].pending_hits.hits[0].damage == scythe_max && + s.npcs[1].pending_hits.hits[0].damage != (scythe_max >> 1)) { + per_target_roll_seen = 1; + } + } + CHECK("scythe rolls incidental targets against their own defence", + per_target_roll_seen); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 55); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.wave_ready_delay = 0; + s.player.x = 16; s.player.y = 16; + col_init_npc(&s, 0, COLO_JAGUAR_WARRIOR, 15, 16); + col_npc_attack_jaguar(&s, &ctx, 0, &COLO_NPC_STATS[COLO_JAGUAR_WARRIOR]); + CHECK("jaguar multi-hit records three player render splats", + ctx.player_render_hit_count == 3); + RenderEntity scythe_entities[4]; + int scythe_entity_count = 0; + col_fill_render_entities_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, + scythe_entities, 4, &scythe_entity_count); + CHECK("render entity carries jaguar multi-hit splats", + scythe_entity_count >= 1 && + scythe_entities[0].render_hit_count == 3); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 52); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.wave_ready_delay = 0; + s.player.x = 16; s.player.y = 16; + col_init_npc(&s, 0, COLO_FREMENNIK_BERSERKER, 16, 17); + const ColoNpcStats* zerk = &COLO_NPC_STATS[COLO_FREMENNIK_BERSERKER]; + col_equip_from_cell(&s, test_find_inventory_cell_with_item(&s, ITEM_ELDER_MAUL)); + int tries = 0; + while (s.npcs[0].def_drained == 0 && tries < 200) { + s.player.special_energy = 100; + s.player.spec_armed = 1; + s.player.attack_timer = 0; + s.npcs[0].hp = zerk->hp; + col_player_attack_target(&s, 0); + tries++; + } + CHECK("elder maul spec eventually lands", s.npcs[0].def_drained > 0); + CHECK("elder maul drains 35% of current defence", + s.npcs[0].def_drained == zerk->def_level * 35 / 100); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 53); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.wave_ready_delay = 0; + s.player.x = 16; s.player.y = 16; + col_init_npc(&s, 0, COLO_FREMENNIK_SEER, 16, 17); + const ColoNpcStats* seer = &COLO_NPC_STATS[COLO_FREMENNIK_SEER]; + s.player.equipped[GEAR_SLOT_SHIELD] = ITEM_NONE; + col_equip_from_cell(&s, test_find_inventory_cell_with_item(&s, ITEM_SGS)); + int healed = 0; + tries = 0; + while (!healed && tries < 200) { + s.player.current_hitpoints = 20; + s.player.current_prayer = 10; + s.player.special_energy = 100; + s.player.spec_armed = 1; + s.player.attack_timer = 0; + s.npcs[0].hp = seer->hp; + col_player_attack_target(&s, 0); + if (s.player.current_hitpoints > 20) healed = 1; + tries++; + } + CHECK("SGS spec eventually lands", healed); + CHECK("SGS heal honors the wiki minimum (>= 10)", + s.player.current_hitpoints >= 30); + CHECK("SGS prayer restore honors the wiki minimum (>= 5)", + s.player.current_prayer >= 15); + + col_equip_from_cell(&s, test_find_inventory_cell_with_item(&s, ITEM_DRAGON_CLAWS)); + s.player.special_energy = 10; + s.player.spec_armed = 0; + int arm[COLO_NUM_ACTION_HEADS] = {0}; + arm[COLO_HEAD_SPEC] = 1; + col_tick_player_ctx(&s, &ctx, arm, 0); + CHECK("arming is refused without the energy", s.player.spec_armed == 0); + s.player.special_energy = 100; + col_tick_player_ctx(&s, &ctx, arm, 0); + CHECK("arming succeeds with the energy", s.player.spec_armed == 1); + CHECK("equipped claws reach is melee range", + col_player_attack_range(&s) == 1); + arm[COLO_HEAD_SPEC] = 2; + col_tick_player_ctx(&s, &ctx, arm, 0); + CHECK("disarm action clears the armed spec", s.player.spec_armed == 0); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 54); + s.modifiers.draft_pending = 0; + s.wave_ready_delay = 0; + s.inventory_cells[26] = osrs_inventory_cell_empty(); + s.inventory_cells[27] = osrs_inventory_cell_empty(); + col_equip_from_cell(&s, test_find_inventory_cell_with_item(&s, ITEM_SGS)); + s.player.special_energy = 100; + s.player.spec_armed = 1; + col_equip_from_cell(&s, + test_find_inventory_cell_with_item(&s, ITEM_DRAGON_DEFENDER)); + CHECK("shield equip displaces the 2H spec weapon", + s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_NONE && + s.player.equipped[GEAR_SLOT_SHIELD] == ITEM_DRAGON_DEFENDER); + CHECK("shield equip displacing the weapon disarms the spec", + s.player.spec_armed == 0); +} + +static void test_loadout_item_effects(void) { + printf("test_loadout_item_effects\n"); + ColosseumContext ctx; + ColosseumState s; + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 61); + col_apply_weapon_set(&s, COLO_GEAR_RANGED); + const EncounterLoadoutStats* rls = col_live_loadout_stats(&s); + const OsrsEquipmentEffectProfile* ranged_effects = col_live_effects(&s); + int base_att = osrs_player_att_roll(rls->eff_level, rls->attack_bonus); + const ColoNpcStats* sol = &COLO_NPC_STATS[COLO_SOL_HEREDIT]; + const ColoNpcStats* jag = &COLO_NPC_STATS[COLO_JAGUAR_WARRIOR]; + OsrsPreparedAttackEffects vs_sol = osrs_prepare_attack_effects( + ranged_effects, &s.player.item_effect_state, + ITEM_TWISTED_BOW, ATTACK_STYLE_RANGED, OSRS_MAGIC_ATTACK_NONE, + osrs_target_ref_none(), 1, base_att, rls->max_hit, + osrs_target_effect_context_magic(sol->magic_level, sol->magic_att_bonus), + s.player.current_hitpoints, s.player.base_hitpoints); + OsrsPreparedAttackEffects vs_jag = osrs_prepare_attack_effects( + ranged_effects, &s.player.item_effect_state, + ITEM_TWISTED_BOW, ATTACK_STYLE_RANGED, OSRS_MAGIC_ATTACK_NONE, + osrs_target_ref_none(), 1, base_att, rls->max_hit, + osrs_target_effect_context_magic(jag->magic_level, jag->magic_att_bonus), + s.player.current_hitpoints, s.player.base_hitpoints); + CHECK("tbow hits harder into Sol's 300 magic than the jaguar's 100", + vs_sol.max_hit > vs_jag.max_hit && vs_sol.attack_roll > vs_jag.attack_roll); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 62); + col_apply_weapon_set(&s, COLO_GEAR_RANGED); + const EncounterLoadoutStats* bls = col_live_loadout_stats(&s); + ranged_effects = col_live_effects(&s); + int bowfa_att = osrs_player_att_roll(bls->eff_level, bls->attack_bonus); + OsrsPreparedAttackEffects bowfa = osrs_prepare_attack_effects( + ranged_effects, &s.player.item_effect_state, + ITEM_BOW_OF_FAERDHINEN, ATTACK_STYLE_RANGED, OSRS_MAGIC_ATTACK_NONE, + osrs_target_ref_none(), 1, bowfa_att, bls->max_hit, + osrs_target_effect_context_magic(100, 0), + s.player.current_hitpoints, s.player.base_hitpoints); + CHECK("crystal armour scales the bowfa damage by 46/40", + bowfa.max_hit == bls->max_hit * 46 / 40); + CHECK("crystal armour scales the bowfa accuracy by 26/20", + bowfa.attack_roll == bowfa_att * 26 / 20); + + int procs = 0; + uint32_t rng = 777; + col_apply_weapon_set(&s, COLO_GEAR_MELEE); + const OsrsEquipmentEffectProfile* melee_effects = col_live_effects(&s); + for (int i = 0; i < 400; i++) { + int heal = osrs_blood_fury_heal_amount( + melee_effects, ATTACK_STYLE_MELEE, 30, &rng); + if (heal > 0) { + procs++; + CHECK("blood fury heals 30% of the damage", heal == 9); + if (heal != 9) break; + } + } + CHECK("blood fury procs at a plausible 20% rate", procs > 40 && procs < 130); + + col_apply_weapon_set(&s, COLO_GEAR_RANGED); + ranged_effects = col_live_effects(&s); + int ranged_heal = osrs_blood_fury_heal_amount( + ranged_effects, ATTACK_STYLE_RANGED, 30, &rng); + CHECK("no blood fury heal on the ranged set", ranged_heal == 0); +} + +static void test_loadout_offensive_prayers(void) { + printf("test_loadout_offensive_prayers\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); + complete_open_draft(&s, &ctx, 1); + int base_max_hit = col_live_loadout_stats(&s)->max_hit; + + int piety[COLO_NUM_ACTION_HEADS] = {0}; + piety[COLO_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY; + step_and_observe(&s, &ctx, piety); + CHECK("piety activates", s.player.offensive_prayer == OFFENSIVE_PRAYER_PIETY); + CHECK("piety raises the melee max hit (L12)", + col_live_loadout_stats(&s)->max_hit > base_max_hit); + CHECK("piety raises the spec max hits too", + test_col_spec_stats_for_kind(&s, 1).max_hit > 0 && + test_col_spec_stats_for_kind(&s, 2).max_hit > 0); + + int off[COLO_NUM_ACTION_HEADS] = {0}; + off[COLO_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_OFF; + step_and_observe(&s, &ctx, off); + CHECK("offensive off restores the base max hit", + s.player.offensive_prayer == OFFENSIVE_PRAYER_NONE && + col_live_loadout_stats(&s)->max_hit == base_max_hit); + + float mask[COLO_ACTION_MASK_SIZE]; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + int off_offset = col_action_head_mask_offset(COLO_HEAD_OFFENSIVE); + CHECK("augury is offered points-only even on a non-magic weapon (equip+pray fix)", + mask[off_offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_AUGURY] == 1.0f); + CHECK("piety and rigour are offered", + mask[off_offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY] == 1.0f && + mask[off_offset + ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR] == 1.0f); + + int overhead[COLO_NUM_ACTION_HEADS] = {0}; + overhead[COLO_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE; + step_and_observe(&s, &ctx, overhead); + CHECK("shared overhead melee activates protect melee", + s.player.prayer == PRAYER_PROTECT_MELEE); + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + int prayer_offset = col_action_head_mask_offset(COLO_HEAD_PRAYER); + CHECK("shared overhead off is valid while overhead prayer is active", + mask[prayer_offset + ENCOUNTER_OVERHEAD_OFF] == 1.0f); + int overhead_off[COLO_NUM_ACTION_HEADS] = {0}; + overhead_off[COLO_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_OFF; + step_and_observe(&s, &ctx, overhead_off); + CHECK("shared overhead off clears active overhead prayer", + s.player.prayer == PRAYER_NONE); + + piety[COLO_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY; + col_player_pretick(&s, &ctx, piety); + CHECK("piety reactivates before drain-zero regression", + s.player.offensive_prayer == OFFENSIVE_PRAYER_PIETY && + col_live_loadout_stats(&s)->max_hit > base_max_hit); + s.player.current_prayer = 0; + int idle[COLO_NUM_ACTION_HEADS] = {0}; + col_player_pretick(&s, &ctx, idle); + CHECK("prayer drain auto-clear recomputes offensive loadout stats", + s.player.offensive_prayer == OFFENSIVE_PRAYER_NONE && + col_live_loadout_stats(&s)->max_hit == base_max_hit); +} + +static void test_total_damage_by_type_captures_typeless(void) { + printf("test_total_damage_by_type_captures_typeless\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 2323); + int hp0 = s.player.current_hitpoints; + float total_before = s.log.total_damage_by_type[COLO_JAVELIN_COLOSSUS]; + float offpray_before = s.log.offpray_damage_by_type[COLO_JAVELIN_COLOSSUS]; + + float typeless_before = s.log.typeless_damage_by_type[COLO_JAVELIN_COLOSSUS]; + float unprayable_before = s.tick_scratch.landed_unprayable_damage; + col_damage_player_from(&s, 17, COLO_JAVELIN_COLOSSUS, COLO_DMG_UNPRAYABLE); + + CHECK("typeless NPC damage lands on the player", s.player.current_hitpoints == hp0 - 17); + CHECK("typeless NPC damage is attributed to total damage", + s.log.total_damage_by_type[COLO_JAVELIN_COLOSSUS] == total_before + 17.0f); + CHECK("typeless NPC damage stays out of off-prayer damage", + s.log.offpray_damage_by_type[COLO_JAVELIN_COLOSSUS] == offpray_before); + CHECK("typeless NPC damage books the unprayable forensics channel", + s.tick_scratch.landed_unprayable_damage == unprayable_before + 17.0f); + CHECK("typeless NPC damage books the per-type typeless total", + s.log.typeless_damage_by_type[COLO_JAVELIN_COLOSSUS] == typeless_before + 17.0f); +} + +static void test_npc_magic_defence_rolls_off_magic_level(void) { + printf("test_npc_magic_defence_rolls_off_magic_level\n"); + col_build_npc_stats(); + + ColoNPC shaman; + memset(&shaman, 0, sizeof(shaman)); + shaman.type = COLO_SERPENT_SHAMAN; + const ColoNpcStats* shaman_ns = &COLO_NPC_STATS[COLO_SERPENT_SHAMAN]; + int sh_magic = col_npc_target_def_roll(&shaman, shaman_ns, ATTACK_STYLE_MAGIC, MELEE_STYLE_SLASH); + int sh_ranged = col_npc_target_def_roll(&shaman, shaman_ns, ATTACK_STYLE_RANGED, MELEE_STYLE_SLASH); + int sh_melee = col_npc_target_def_roll(&shaman, shaman_ns, ATTACK_STYLE_MELEE, MELEE_STYLE_SLASH); + CHECK("shaman magic def rolls off Magic level not Defence", + sh_magic == (shaman_ns->magic_level + 9) * (shaman_ns->magic_def_bonus + 64)); + CHECK("shaman is most magic-resistant (magic > ranged > melee)", + sh_magic > sh_ranged && sh_ranged > sh_melee); + + ColoNPC manticore; + memset(&manticore, 0, sizeof(manticore)); + manticore.type = COLO_MANTICORE; + const ColoNpcStats* mant_ns = &COLO_NPC_STATS[COLO_MANTICORE]; + int mt_magic = col_npc_target_def_roll(&manticore, mant_ns, ATTACK_STYLE_MAGIC, MELEE_STYLE_SLASH); + int mt_ranged = col_npc_target_def_roll(&manticore, mant_ns, ATTACK_STYLE_RANGED, MELEE_STYLE_SLASH); + int mt_melee = col_npc_target_def_roll(&manticore, mant_ns, ATTACK_STYLE_MELEE, MELEE_STYLE_SLASH); + CHECK("manticore magic def rolls off Magic level not Defence", + mt_magic == (mant_ns->magic_level + 9) * (mant_ns->magic_def_bonus + 64)); + CHECK("manticore is easiest to melee (scythe): melee is the lowest def roll", + mt_melee < mt_ranged && mt_melee < mt_magic); +} + +static void test_matchup_dpt_obs_ranking(void) { + printf("test_matchup_dpt_obs_ranking\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 771); + col_build_npc_stats(); + + geo_clear_npcs(&s); + s.player.x = 12; + s.player.y = 16; + venator_spawn_enemy(&s, 0, COLO_FREMENNIK_BERSERKER, 16, 16, 1); + ColoVenatorPreviewTargets targets; + col_collect_venator_preview_targets(&s, &targets); + int isolated_extra = col_venator_extra_bounce_if_shot(&s, &targets, 0); + CHECK("isolated Venator preview has no extra bounces", isolated_extra == 0); + + venator_spawn_enemy(&s, 1, COLO_FREMENNIK_ARCHER, 18, 16, 1); + venator_spawn_enemy(&s, 2, COLO_FREMENNIK_BERSERKER, 18, 17, 1); + col_collect_venator_preview_targets(&s, &targets); + int clustered_extra = col_venator_extra_bounce_if_shot(&s, &targets, 0); + CHECK("clustered Venator preview sees extra bounces", clustered_extra >= 1); +} + +static void test_primary_head_resolution(void) { + printf("test_primary_head_resolution\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 881, 16, 16); + s.wave_ready_delay = 0; + s.wave_spawn_delay = 0; + col_init_npc(&s, 0, COLO_FREMENNIK_BERSERKER, 18, 16); + s.npcs[0].hp = 200; + s.npcs[0].max_hp = 200; + col_rebuild_player_collision_flags(&s); + col_refresh_current_obs_slots_ctx(&s, &ctx); + int obs_slot = col_find_target_obs_slot(&s, 0); + int attack_action = col_primary_attack_action_for_obs_slot(obs_slot); + + int attack[COLO_NUM_ACTION_HEADS] = {0}; + attack[COLO_HEAD_PRIMARY] = attack_action; + col_tick_player_ctx(&s, &ctx, attack, 1); + CHECK("PRIMARY attack action sets the mapped NPC interaction", + osrs_interaction_active(&s.interaction) && s.interaction.target_slot == 0); + + int idle[COLO_NUM_ACTION_HEADS] = {0}; + col_tick_player_ctx(&s, &ctx, idle, 1); + CHECK("PRIMARY noop holds the existing interaction", + osrs_interaction_active(&s.interaction) && s.interaction.target_slot == 0); + + float mask[COLO_ACTION_MASK_SIZE]; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + int primary_offset = col_action_head_mask_offset(COLO_HEAD_PRIMARY); + CHECK("held PRIMARY target stays valid for inventory cancel and re-attack", + mask[primary_offset + attack_action] == 1.0f); + + int move_action = forecast_move_action_for_delta(1, 0); + int move[COLO_NUM_ACTION_HEADS] = {0}; + move[COLO_HEAD_PRIMARY] = move_action; + int x_before = s.player.x; + col_tick_player_ctx(&s, &ctx, move, 1); + CHECK("PRIMARY move cancels the held interaction", + !osrs_interaction_active(&s.interaction)); + CHECK("PRIMARY move walks using the old movement action mapping", + s.player.x == x_before + 1 && s.player.y == 16); + + col_tick_player_ctx(&s, &ctx, attack, 1); + CHECK("PRIMARY attack reacquires the mapped NPC after movement", + osrs_interaction_active(&s.interaction) && s.interaction.target_slot == 0); +} + +static void test_combat_fidelity_contract_sizes(void) { + printf("test_combat_fidelity_contract_sizes\n"); + CHECK("three weapon sets (melee/ranged/magic)", COLO_NUM_WEAPON_SETS == 3); + CHECK("twenty action heads (per-category inventory heads)", COLO_NUM_ACTION_HEADS == 20); + CHECK("first equip head follows PRIMARY and PRAYER", COLO_HEAD_EQUIP_BASE == 2); + CHECK("equip heads cover every gear slot", + COLO_HEAD_EAT == COLO_HEAD_EQUIP_BASE + NUM_GEAR_SLOTS && COLO_HEAD_DRINK == COLO_HEAD_EAT + 1); + CHECK("equip head dim is 29", COLO_ACTION_DIMS[COLO_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] == 29); + CHECK("eat and drink heads are 29-way", + COLO_ACTION_DIMS[COLO_HEAD_EAT] == 29 && COLO_ACTION_DIMS[COLO_HEAD_DRINK] == 29); + CHECK("prayer head uses shared PVE overhead dim", + COLO_ACTION_DIMS[COLO_HEAD_PRAYER] == ENCOUNTER_OVERHEAD_DIM_PVE); + CHECK("spell head dim is 3 (none/summon-thrall/death-charge)", COLO_SPELL_DIM == 3); + CHECK("obs width is 3044", COLO_NUM_OBS == 3044); + CHECK("weapon-choice tail has 58 features (28 cell DPT + 28 spec + 2 wielded)", + COLO_WEAPON_CHOICE_OBS_SIZE == 58); + CHECK("inventory block has 784 features", COLO_INVENTORY_OBS_SIZE == 784); + CHECK("equipped-self block has 198 features", COLO_EQUIPPED_SELF_OBS_SIZE == 198); + CHECK("modifier hazard tail has 38 features", COLO_MODIFIER_HAZARD_OBS_SIZE == 38); + CHECK("modifier block has 74 features", COLO_MODIFIER_OBS_SIZE == 74); + CHECK("NPC slots have 37 features (DPT obs removed, B0 neutral)", COLO_FEATURES_PER_NPC == 37); + CHECK("snapshot version is v20", COLO_SNAPSHOT_VERSION == 20u); + CHECK("every active NPC gets an obs slot (no busy-wave drop)", + COLO_OBS_NPCS == 24 && COLO_OBS_NPCS == COLO_MAX_NPCS); + CHECK("PRIMARY head covers noop, movement, and NPC obs slots", + COLO_ACTION_DIMS[COLO_HEAD_PRIMARY] == COLO_PRIMARY_DIM && + COLO_ACTION_DIMS[COLO_HEAD_PRIMARY] == 49); + CHECK("player block remains 36", COLO_PLAYER_OBS_SIZE == 36); + + int mask_sum = 0; + for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) mask_sum += COLO_ACTION_DIMS[h]; + CHECK("mask size equals the summed action-head dims", + COLO_ACTION_MASK_SIZE == mask_sum && COLO_ACTION_MASK_SIZE == 452); + + int obs_sum = COLO_PLAYER_OBS_SIZE + COLO_PILLAR_OBS_SIZE + + COLO_INVENTORY_OBS_SIZE + COLO_EQUIPPED_SELF_OBS_SIZE + COLO_NPC_OBS_SIZE + + COLO_MODIFIER_OBS_SIZE + COLO_WAVE_OBS_SIZE + COLO_BOSS_OBS_SIZE + + COLO_PENDING_HIT_OBS_SIZE + COLO_STEP_OUT_FORECAST_OBS_SIZE + + COLO_THREAT_LOS_OBS_SIZE + COLO_THRALL_DC_OBS_SIZE + + COLO_WEAPON_CHOICE_OBS_SIZE + COLO_SPAWN_OBS_SIZE + + COLO_THREAT_FIELD_OBS_SIZE; + CHECK("obs width equals the summed section sizes", COLO_NUM_OBS == obs_sum); + + float opa, ops; + encounter_offensive_prayer_mults(OFFENSIVE_PRAYER_PIETY, ATTACK_STYLE_MELEE, &opa, &ops); + CHECK("Piety boosts melee att+str", opa > 1.0f && ops > 1.0f); + encounter_offensive_prayer_mults(OFFENSIVE_PRAYER_PIETY, ATTACK_STYLE_RANGED, &opa, &ops); + CHECK("Piety is inert off-style (ranged)", opa == 1.0f && ops == 1.0f); + encounter_offensive_prayer_mults(OFFENSIVE_PRAYER_RIGOUR, ATTACK_STYLE_RANGED, &opa, &ops); + CHECK("Rigour boosts ranged att+str", opa > 1.0f && ops > 1.0f); + encounter_offensive_prayer_mults(OFFENSIVE_PRAYER_RIGOUR, ATTACK_STYLE_MELEE, &opa, &ops); + CHECK("Rigour is inert off-style (melee)", opa == 1.0f && ops == 1.0f); + encounter_offensive_prayer_mults(OFFENSIVE_PRAYER_AUGURY, ATTACK_STYLE_MAGIC, &opa, &ops); + CHECK("Augury boosts magic att", opa > 1.0f); + encounter_offensive_prayer_mults(OFFENSIVE_PRAYER_AUGURY, ATTACK_STYLE_MELEE, &opa, &ops); + CHECK("Augury is inert off-style (melee)", opa == 1.0f && ops == 1.0f); +} + +static void scythe_spawn_enemy(ColosseumState* s, int slot, int x, int y, int size) { + ColoNPC* npc = &s->npcs[slot]; + memset(npc, 0, sizeof(*npc)); + npc->active = 1; + npc->type = COLO_FREMENNIK_BERSERKER; + npc->x = x; + npc->y = y; + npc->size = size; + npc->hp = 200; + npc->death_ticks = 0; +} + +static void venator_spawn_enemy( + ColosseumState* s, + int slot, + ColoNpcType type, + int x, + int y, + int size +) { + ColoNPC* npc = &s->npcs[slot]; + memset(npc, 0, sizeof(*npc)); + npc->active = 1; + npc->type = type; + npc->x = x; + npc->y = y; + npc->size = size; + npc->hp = 200; + npc->death_ticks = 0; +} + +static void test_scythe_multihit_per_size(void) { + printf("test_scythe_multihit_per_size\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 909, 15, 15); + + ColScytheResolvedHit out[COLO_SCYTHE_MAX_HITS]; + + geo_clear_npcs(&s); + scythe_spawn_enemy(&s, 0, 14, 14, 3); + int n3 = col_resolve_scythe_hits(&s, 0, out); + CHECK("3x3 primary yields exactly 3 hits", n3 == 3); + CHECK("3x3 hits are all on the primary with ranks 0/1/2", + out[0].npc_slot == 0 && out[0].splat_rank == 0 && + out[1].npc_slot == 0 && out[1].splat_rank == 1 && + out[2].npc_slot == 0 && out[2].splat_rank == 2); + + geo_clear_npcs(&s); + scythe_spawn_enemy(&s, 0, 15, 15, 2); + int n2 = col_resolve_scythe_hits(&s, 0, out); + CHECK("2x2 primary yields exactly 2 hits", n2 == 2); + CHECK("2x2 hits are on the primary at ranks 0/1", + out[0].npc_slot == 0 && out[0].splat_rank == 0 && + out[1].npc_slot == 0 && out[1].splat_rank == 1); + + geo_clear_npcs(&s); + scythe_spawn_enemy(&s, 0, 16, 15, 1); + scythe_spawn_enemy(&s, 1, 16, 14, 1); + scythe_spawn_enemy(&s, 2, 16, 16, 1); + int narc = col_resolve_scythe_hits(&s, 0, out); + CHECK("three separate 1x1s in the arc yield exactly 3 hits", narc == 3); + int distinct_full = 1; + int seen_slot[3] = {0, 0, 0}; + for (int h = 0; h < narc; h++) { + if (out[h].splat_rank != 0) distinct_full = 0; + if (out[h].npc_slot >= 0 && out[h].npc_slot < 3) seen_slot[out[h].npc_slot]++; + } + CHECK("each separate 1x1 takes a full-100% (rank 0) hit", distinct_full); + CHECK("the three separate enemies are distinct targets", + seen_slot[0] == 1 && seen_slot[1] == 1 && seen_slot[2] == 1); + + CHECK("scythe never exceeds the resolved-hit buffer", + n3 <= COLO_SCYTHE_MAX_HITS && n2 <= COLO_SCYTHE_MAX_HITS && + narc <= COLO_SCYTHE_MAX_HITS); +} + +static void test_venator_bow_bounce_colosseum_integration(void) { + printf("test_venator_bow_bounce_colosseum_integration\n"); + ColosseumContext ctx; + ColosseumState s; + int expected_damage[OSRS_VENATOR_MAX_CHAIN_HITS] = {0}; + int expected_total = 0; + int original_max = 0; + int bounce_max = 0; + int base_ticks = 0; + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 8207); + int venator_cell = test_find_inventory_cell_with_item(&s, ITEM_VENATOR_BOW); + if (venator_cell < 0) { + CHECK("speedrun inventory carries venator bow", 0); + return; + } + col_equip_from_cell(&s, venator_cell); + s.player.current_ranged = 40; + col_mark_live_loadout_dirty(&s); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.wave_ready_delay = 0; + s.player.x = 12; + s.player.y = 16; + venator_spawn_enemy(&s, 0, COLO_FREMENNIK_BERSERKER, 16, 16, 1); + venator_spawn_enemy(&s, 1, COLO_FREMENNIK_ARCHER, 18, 16, 1); + venator_spawn_enemy(&s, 2, COLO_FREMENNIK_BERSERKER, 18, 17, 1); + + OsrsVenatorChain chain = col_resolve_venator_chain(&s, 0); + if (chain.length != OSRS_VENATOR_CHAIN_LENGTH_THREE || + chain.hits[0].slot != 0 || + chain.hits[1].slot != 1 || + chain.hits[2].slot != 2) { + CHECK("venator resolves a three-hop clustered chain", 0); + return; + } + + uint8_t weapon_item = s.player.equipped[GEAR_SLOT_WEAPON]; + const EncounterLoadoutStats* ls = col_live_loadout_stats(&s); + const OsrsEquipmentEffectProfile* effects = col_live_effects(&s); + const ColoNPC* primary = &s.npcs[0]; + const ColoNpcStats* primary_stats = &COLO_NPC_STATS[primary->type]; + int base_att_roll = osrs_player_att_roll(ls->eff_level, ls->attack_bonus); + MeleeStyle weapon_melee_style = col_item_melee_style(weapon_item); + OsrsPreparedAttackEffects prepared = + osrs_prepare_attack_effects_for_melee_style( + effects, + &s.player.item_effect_state, + weapon_item, + ls->style, + weapon_melee_style, + OSRS_MAGIC_ATTACK_NONE, + osrs_target_ref_none(), + 1, + base_att_roll, + ls->max_hit, + osrs_target_effect_context_magic( + primary_stats->magic_level, + primary_stats->magic_att_bonus), + s.player.current_hitpoints, + s.player.base_hitpoints); + original_max = prepared.max_hit; + bounce_max = osrs_venator_bounce_max_hit(original_max); + base_ticks = ls->attack_range > 1 ? 3 : 1; + + uint32_t rng = s.rng_state; + int primary_def_roll = col_npc_target_def_roll( + &s.npcs[0], &COLO_NPC_STATS[s.npcs[0].type], + ls->style, weapon_melee_style); + int bounce_def_roll = col_npc_target_def_roll( + &s.npcs[1], &COLO_NPC_STATS[s.npcs[1].type], + ls->style, weapon_melee_style); + expected_total = 0; + for (int hop = 0; hop < (int)chain.length; hop++) { + int slot = chain.hits[hop].slot; + int splat_max = hop == 0 ? original_max : bounce_max; + int target_def_roll = col_npc_target_def_roll( + &s.npcs[slot], &COLO_NPC_STATS[s.npcs[slot].type], + ls->style, weapon_melee_style); + expected_damage[hop] = osrs_roll_prepared_attack_damage( + &prepared, target_def_roll, splat_max, &rng); + expected_total += expected_damage[hop]; + } + + CHECK("venator fixture is sensitive to per-target defence", + bounce_def_roll != primary_def_roll); + CHECK("speedrun inventory carries venator bow", 1); + CHECK("venator resolves a three-hop clustered chain", 1); + CHECK("venator bow is equipped for the integration attack", + s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_VENATOR_BOW); + CHECK("venator bounce max is floor two thirds of the original", + bounce_max == original_max * 2 / 3 && bounce_max < original_max); + + col_player_attack_target_ctx(&s, &ctx, 0); + + CHECK("venator records render chain slots", + ctx.player_venator_chain_count == 3 && + ctx.player_venator_chain_slots[0] == 0 && + ctx.player_venator_chain_slots[1] == 1 && + ctx.player_venator_chain_slots[2] == 2); + CHECK("venator queues one pending hit on each chain target", + s.npcs[0].pending_hits.count == 1 && + s.npcs[1].pending_hits.count == 1 && + s.npcs[2].pending_hits.count == 1); + CHECK("venator queues staggered bounce delays", + s.npcs[0].pending_hits.hits[0].ticks_remaining == base_ticks && + s.npcs[1].pending_hits.hits[0].ticks_remaining == base_ticks + 1 && + s.npcs[2].pending_hits.hits[0].ticks_remaining == base_ticks + 2); + CHECK("venator primary damage matches the independent roll", + s.npcs[0].pending_hits.hits[0].damage == expected_damage[0]); + CHECK("venator bounce damage matches capped independent rolls", + s.npcs[1].pending_hits.hits[0].damage == expected_damage[1] && + s.npcs[2].pending_hits.hits[0].damage == expected_damage[2] && + expected_damage[1] <= bounce_max && + expected_damage[2] <= bounce_max); + CHECK("venator attack damage sums every queued splat", + s.player_attack_dmg == expected_total); + + EncounterOverlay ov = {0}; + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("venator emits primary plus two bounce projectiles", + ov.projectile_count == 3); + CHECK("venator primary projectile targets the attacked NPC", + ov.projectiles[0].source_kind == ENCOUNTER_PROJECTILE_TARGET_PLAYER && + ov.projectiles[0].target_kind == ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT && + ov.projectiles[0].target_npc_slot == 0); + CHECK("venator bounce projectiles chain through NPC slots", + ov.projectiles[1].source_kind == ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT && + ov.projectiles[1].source_npc_slot == 0 && + ov.projectiles[1].target_kind == ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT && + ov.projectiles[1].target_npc_slot == 1 && + ov.projectiles[2].source_kind == ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT && + ov.projectiles[2].source_npc_slot == 1 && + ov.projectiles[2].target_kind == ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT && + ov.projectiles[2].target_npc_slot == 2); + CHECK("venator projectile hops use the venator bolt model", + ov.projectiles[0].model_id == OSRS_PROJECTILE_MODEL_VENATOR_BOLT && + ov.projectiles[1].model_id == OSRS_PROJECTILE_MODEL_VENATOR_BOLT && + ov.projectiles[2].model_id == OSRS_PROJECTILE_MODEL_VENATOR_BOLT); + CHECK("venator bounce projectile delays increase by hop", + ov.projectiles[1].start_delay > ov.projectiles[0].start_delay && + ov.projectiles[2].start_delay > ov.projectiles[1].start_delay); +} + +static void test_bee_contact_damage_band(void) { + printf("test_bee_contact_damage_band\n"); + ColosseumContext ctx; + ColosseumState s; + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 4242); + col_apply_weapon_set(&s, COLO_GEAR_RANGED); + geo_clear_npcs(&s); + s.modifiers.active_mask |= (1u << COLO_MOD_BEES); + s.modifiers.tier[COLO_MOD_BEES] = 1; + col_mod_sync_bees(&s); + ColoNPC* bee = &s.npcs[s.bees[0].npc_slot]; + + int in_band = 1, any_zero = 0; + for (int t = 0; t < 200; t++) { + bee->x = s.player.x; + bee->y = s.player.y; + s.player.current_hitpoints = 99; + int hp0 = s.player.current_hitpoints; + col_mod_tick_bees(&s); + int dmg = hp0 - s.player.current_hitpoints; + if (dmg < COLO_BEE_MIN_DAMAGE || dmg > COLO_BEE_MAX_DAMAGE) in_band = 0; + if (dmg == 0) any_zero = 1; + } + CHECK("bee contact damage stays inside the 15-20 band", in_band); + CHECK("bee contact never deals a zero-damage tick while overlapping", !any_zero); + + col_apply_weapon_set(&s, COLO_GEAR_MELEE); + CHECK("rig sanity: the melee set is venom-immune", + osrs_effect_profile_has(col_live_effects(&s), OSRS_ITEM_EFFECT_VENOM_IMMUNE)); + bee->x = s.player.x; + bee->y = s.player.y; + s.player.current_hitpoints = 99; + col_mod_tick_bees(&s); + CHECK("serpentine-helm immunity zeroes bee contact damage", + s.player.current_hitpoints == 99); +} + +static void test_divine_state_obs_presence(void) { + printf("test_divine_state_obs_presence\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); + s.modifiers.draft_pending = 0; + + static float obs_base[COLO_NUM_OBS]; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs_base); + CHECK("player block has no divine timer tail", COLO_PLAYER_OBS_SIZE == 36); + + col_apply_divine_combat_potion_effect(&s); + s.divine_ranged_timer = ENCOUNTER_DIVINE_POTION_TICKS; + col_enforce_divine_stat_floors(&s); + col_mark_live_loadout_dirty(&s); + static float obs_boost[COLO_NUM_OBS]; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs_boost); + CHECK("divine boosts still surface through live max-hit scalar", + obs_boost[20] > obs_base[20]); +} + +static void test_magic_set_max_hit_math(void) { + printf("test_magic_set_max_hit_math\n"); + + EncounterLoadoutStats budget; + encounter_compute_loadout_stats(COLO_BEGINNER_MAGIC_LOADOUT, ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_ACCURATE, 31, &budget); + CHECK("budget magic gear magic_damage% is 0 (no Occult)", budget.strength_bonus == 0); + CHECK("budget Trident max hit == floor(31*(1+0/100)) == 31", budget.max_hit == 31); + CHECK("budget magic eff level == floor(99)+2(accurate)+9 == 110", budget.eff_level == 110); + CHECK("budget magic style is magic", budget.style == ATTACK_STYLE_MAGIC); + CHECK("budget Trident (1h) keeps a shield bonus from the Dragon defender", + budget.attack_range == 7); + + EncounterLoadoutStats budget_aug; + encounter_compute_loadout_stats(COLO_BEGINNER_MAGIC_LOADOUT, ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_AUGURY, 99, FIGHT_STYLE_ACCURATE, 31, &budget_aug); + CHECK("budget Trident augury max hit == floor(31*1.04) == 32", budget_aug.max_hit == 32); + + EncounterLoadoutStats untripled; + { + uint8_t no_shadow[NUM_GEAR_SLOTS]; + memcpy(no_shadow, COLO_SPEEDRUN_MAGIC_LOADOUT, NUM_GEAR_SLOTS); + no_shadow[GEAR_SLOT_WEAPON] = ITEM_TRIDENT_OF_SWAMP; + encounter_compute_loadout_stats(no_shadow, ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_ACCURATE, 34, &untripled); + } + CHECK("high-eff magic gear magic_damage% before Shadow == 14", + untripled.strength_bonus == 14); + + EncounterLoadoutStats hieff; + encounter_compute_loadout_stats(COLO_SPEEDRUN_MAGIC_LOADOUT, ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_ACCURATE, 34, &hieff); + CHECK("Shadow triples gear magic_damage% 14 -> 42", hieff.strength_bonus == 42); + CHECK("high-eff Shadow max hit == floor(34*(1+42/100)) == 48", hieff.max_hit == 48); + CHECK("the tripled 42% is what feeds the max hit (untripled 14% would give 38)", + (int)(34 * (1.0 + 14 / 100.0)) == 38 && hieff.max_hit == 48); + CHECK("high-eff magic eff level == 110", hieff.eff_level == 110); + CHECK("Shadow (2h) suppresses the shield (range 10, no shield bonus)", + hieff.attack_range == 10); + + EncounterLoadoutStats hieff_aug; + encounter_compute_loadout_stats(COLO_SPEEDRUN_MAGIC_LOADOUT, ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_AUGURY, 99, FIGHT_STYLE_ACCURATE, 34, &hieff_aug); + CHECK("high-eff Shadow augury max hit == floor(34*1.42*1.04) == 50", + hieff_aug.max_hit == 50); + + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 91); + col_apply_weapon_set(&s, COLO_GEAR_MAGIC); + CHECK("env budget magic set max hit == 31", col_live_loadout_stats(&s)->max_hit == 31); + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 92); + col_apply_weapon_set(&s, COLO_GEAR_MAGIC); + CHECK("env high-eff magic set max hit == 48", col_live_loadout_stats(&s)->max_hit == 48); + + col_apply_weapon_set(&s, COLO_GEAR_MELEE); + int gear_magic[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, + gear_magic, test_find_inventory_cell_with_item(&s, ITEM_TUMEKENS_SHADOW)); + col_tick_player_ctx(&s, &ctx, gear_magic, 1); + CHECK("clicking the magic weapon switches the player to the magic style", + s.weapon_set == COLO_GEAR_MAGIC); +} + +static int thrall_scenario(ColosseumState* s, ColosseumContext* ctx, int mode, uint32_t seed) { + col_init_context_typed(ctx); + ctx->config.loadout_profile_mode = mode; + memset(s, 0, sizeof(*s)); + col_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); + geo_clear_npcs(s); + s->modifiers.draft_pending = 0; + s->player.x = 12; + s->player.y = 18; + col_rebuild_player_collision_flags(s); + int slot = col_spawn_npc_at(s, COLO_FREMENNIK_BERSERKER, 16, 18); + s->npcs[slot].hp = 200; + s->npcs[slot].max_hp = 200; + osrs_interaction_set(&s->interaction, slot); + return slot; +} + +static void test_thrall_regression(void) { + printf("test_thrall_regression\n"); + ColosseumContext ctx; + ColosseumState s; + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int summon[COLO_NUM_ACTION_HEADS] = {0}; + summon[COLO_HEAD_SPELL] = COLO_SPELL_SUMMON_THRALL; + + int slot = thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 201); + col_tick_player_ctx(&s, &ctx, summon, 1); + CHECK("summon activates the thrall on the targeted NPC", + s.thrall_active && s.thrall_target_slot == slot); + CHECK("budget thrall lifetime starts at 99 (decremented this tick)", + s.thrall_lifetime_left == 98); + CHECK("thrall recast gate is 17 (decremented this tick)", s.thrall_recast_cd == 16); + + osrs_interaction_clear(&s.interaction); + encounter_pending_hit_queue_clear(&s.npcs[slot].pending_hits); + + for (int t = 0; t < 3; t++) col_tick_player_ctx(&s, &ctx, idle, 1); + CHECK("thrall fires exactly once per 4 ticks (timer back to 4)", + s.thrall_attack_timer == COLO_THRALL_TICK); + CHECK("exactly one thrall hit is queued in the cadence window", + s.npcs[slot].pending_hits.count == 1 && + s.npcs[slot].pending_hits.hits[0].source_npc_slot == -1 && + s.npcs[slot].pending_hits.hits[0].attack_style == ATTACK_STYLE_MAGIC); + float dmg_before = s.tick_scratch.damage_dealt; + int npc_hp_before = s.npcs[slot].hp; + land_pending_player_hits(&s); + int thrall_dmg = npc_hp_before - s.npcs[slot].hp; + CHECK("a single thrall hit lands player-credited damage in [0,3]", + thrall_dmg >= 0 && thrall_dmg <= COLO_THRALL_MAX_HIT); + CHECK("the thrall damage is credited to the player accumulator", + s.tick_scratch.damage_dealt >= dmg_before); + + osrs_interaction_set(&s.interaction, slot); + + int life_now = s.thrall_lifetime_left; + col_tick_player_ctx(&s, &ctx, summon, 1); + CHECK("summon during the recast gate does not reset lifetime", + s.thrall_lifetime_left == life_now - 1); + + while (s.thrall_recast_cd > 0) col_tick_player_ctx(&s, &ctx, idle, 1); + col_tick_player_ctx(&s, &ctx, summon, 1); + CHECK("re-summon after the gate replaces with a fresh 99-tick thrall", + s.thrall_active && s.thrall_lifetime_left == 98); + + s.thrall_lifetime_left = 1; + col_tick_player_ctx(&s, &ctx, idle, 1); + CHECK("budget thrall despawns when lifetime reaches 0", + !s.thrall_active && s.thrall_target_slot == -1); + + thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 202); + col_tick_player_ctx(&s, &ctx, summon, 1); + CHECK("high-eff thrall lifetime starts at 198 (decremented this tick)", + s.thrall_lifetime_left == 197); + + col_init_context_typed(&ctx); + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 203); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.player.x = 12; s.player.y = 18; + col_rebuild_player_collision_flags(&s); + int sol_slot = col_spawn_npc_at(&s, COLO_SOL_HEREDIT, 18, 16); + s.npcs[sol_slot].hp = 1500; + s.npcs[sol_slot].max_hp = 1500; + osrs_interaction_set(&s.interaction, sol_slot); + col_tick_player_ctx(&s, &ctx, summon, 1); + CHECK("summon vs Sol Heredit is a no-op (Sol is thrall-immune)", !s.thrall_active); + float mask[COLO_ACTION_MASK_SIZE]; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + int spell_off = col_action_head_mask_offset(COLO_HEAD_SPELL); + CHECK("summon-thrall is masked illegal while targeting Sol", + mask[spell_off + COLO_SPELL_SUMMON_THRALL] == 0.0f); + + s.thrall_active = 1; + s.thrall_target_slot = sol_slot; + s.thrall_attack_timer = 1; + s.thrall_lifetime_left = 50; + int sol_hp = s.npcs[sol_slot].hp; + col_tick_player_ctx(&s, &ctx, idle, 1); + land_pending_player_hits(&s); + CHECK("thrall never damages Sol", s.npcs[sol_slot].hp == sol_hp); +} + +static void test_death_charge_regression(void) { + printf("test_death_charge_regression\n"); + ColosseumContext ctx; + ColosseumState s; + int idle[COLO_NUM_ACTION_HEADS] = {0}; + int cast_dc[COLO_NUM_ACTION_HEADS] = {0}; + cast_dc[COLO_HEAD_SPELL] = COLO_SPELL_DEATH_CHARGE; + + int slot = thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 211); + s.player.special_energy = 50; + col_tick_player_ctx(&s, &ctx, cast_dc, 1); + CHECK("Death Charge arms a 100-tick window (decremented this tick)", + s.death_charge_window_left == 99 && s.death_charge_cd == 0); + s.npcs[slot].hp = 0; + col_apply_npc_death(&s, slot); + CHECK("a player-credited kill in the window grants +15 spec", + s.player.special_energy == 65); + CHECK("the kill closes the window and starts the 100-tick cooldown", + s.death_charge_window_left == 0 && s.death_charge_cd == 100); + + slot = thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 212); + s.player.special_energy = 95; + col_tick_player_ctx(&s, &ctx, cast_dc, 1); + s.npcs[slot].hp = 0; + col_apply_npc_death(&s, slot); + CHECK("Death Charge spec gain clamps at 100", s.player.special_energy == 100); + + slot = thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 213); + s.player.special_energy = 40; + s.npcs[slot].hp = 0; + col_apply_npc_death(&s, slot); + CHECK("a kill outside an armed window grants no spec and starts no cooldown", + s.player.special_energy == 40 && s.death_charge_cd == 0); + + slot = thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 214); + col_tick_player_ctx(&s, &ctx, cast_dc, 1); + while (s.death_charge_window_left > 0) col_tick_player_ctx(&s, &ctx, idle, 1); + CHECK("an unused window closes without starting the cooldown", + s.death_charge_window_left == 0 && s.death_charge_cd == 0); + + int slot_b; + { + col_init_context_typed(&ctx); + ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 215); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.player.x = 12; s.player.y = 18; + col_rebuild_player_collision_flags(&s); + slot = col_spawn_npc_at(&s, COLO_FREMENNIK_BERSERKER, 16, 18); + slot_b = col_spawn_npc_at(&s, COLO_FREMENNIK_BERSERKER, 16, 19); + s.npcs[slot].hp = 50; s.npcs[slot].max_hp = 50; + s.npcs[slot_b].hp = 50; s.npcs[slot_b].max_hp = 50; + osrs_interaction_set(&s.interaction, slot); + } + s.player.special_energy = 50; + col_tick_player_ctx(&s, &ctx, cast_dc, 1); + s.npcs[slot].hp = 0; + s.npcs[slot_b].hp = 0; + col_apply_npc_death(&s, slot); + col_apply_npc_death(&s, slot_b); + CHECK("two same-tick kills consume the charge exactly once (+15)", + s.player.special_energy == 65 && s.death_charge_window_left == 0); + + slot = thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 216); + s.player.special_energy = 50; + s.npcs[slot].hp = 1; + int summon[COLO_NUM_ACTION_HEADS] = {0}; + summon[COLO_HEAD_SPELL] = COLO_SPELL_SUMMON_THRALL; + col_tick_player_ctx(&s, &ctx, summon, 1); + col_tick_player_ctx(&s, &ctx, cast_dc, 1); + s.thrall_attack_timer = 1; + col_tick_player_ctx(&s, &ctx, idle, 1); + int spec_before = s.player.special_energy; + + for (int h = 0; h < s.npcs[slot].pending_hits.count; h++) + if (s.npcs[slot].pending_hits.hits[h].active && + s.npcs[slot].pending_hits.hits[h].source_npc_slot == -1) + s.npcs[slot].pending_hits.hits[h].damage = 5; + land_pending_player_hits(&s); + CHECK("a thrall-credited kill procs Death Charge (+15)", + s.npcs[slot].hp <= 0 && s.player.special_energy == spec_before + 15); + + slot = thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 217); + s.death_charge_cd = 50; + float mask[COLO_ACTION_MASK_SIZE]; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + int spell_off = col_action_head_mask_offset(COLO_HEAD_SPELL); + CHECK("death-charge is masked illegal while the cooldown is up", + mask[spell_off + COLO_SPELL_DEATH_CHARGE] == 0.0f); + s.death_charge_cd = 0; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + CHECK("death-charge is legal when ready", mask[spell_off + COLO_SPELL_DEATH_CHARGE] == 1.0f); +} + +static void test_combat_fidelity_snapshot_roundtrip(void) { + printf("test_combat_fidelity_snapshot_roundtrip\n"); + ColosseumContext ctx; + ColosseumState s; + int slot = thrall_scenario(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 221); + col_apply_weapon_set(&s, COLO_GEAR_MAGIC); + s.thrall_active = 1; + s.thrall_target_slot = slot; + s.thrall_lifetime_left = 123; + s.thrall_attack_timer = 2; + s.thrall_recast_cd = 9; + s.death_charge_window_left = 44; + s.death_charge_cd = 0; + + ColoSnapshot snap; + col_snapshot_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &snap); + CHECK("snapshot frame is v20", snap.version == 20u); + + ColosseumState restored; + memset(&restored, 0, sizeof(restored)); + col_restore_ctx((EncounterState*)&restored, (EncounterContext*)&ctx, &snap, sizeof(snap)); + CHECK("magic weapon set survives the round-trip", + restored.weapon_set == COLO_GEAR_MAGIC); + CHECK("the recomputed magic set max hit matches the live high-eff value (48)", + col_live_loadout_stats(&restored)->max_hit == 48); + CHECK("thrall fields round-trip bit-identically", + restored.thrall_active == 1 && restored.thrall_target_slot == slot && + restored.thrall_lifetime_left == 123 && restored.thrall_attack_timer == 2 && + restored.thrall_recast_cd == 9); + CHECK("Death-Charge fields round-trip bit-identically", + restored.death_charge_window_left == 44 && restored.death_charge_cd == 0); +} + +static void test_step_out_forecast_manticore_armed_pattern(void) { + printf("test_step_out_forecast_manticore_armed_pattern\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 401, 17, 16); + col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); + s.npcs[0].attack_timer = 1; + ColoManticoreState* mc = colo_npc_manticore(&s.npcs[0]); + mc->cycle_step = 0; + mc->orb_style[0] = ATTACK_STYLE_MAGIC; + mc->orb_style[1] = ATTACK_STYLE_RANGED; + mc->orb_style[2] = ATTACK_STYLE_MELEE; + + ColoStepOutForecast forecast; + col_build_step_out_forecast_ctx(&s, &forecast); + const ColoStepOutForecastAction* idle = &forecast.actions[0]; + CHECK("armed manticore idle forecast is valid", idle->valid == 1); + CHECK("armed manticore orb 0 records magic on tick 1", + idle->ticks[0].magic_count == 1 && idle->ticks[0].max_hit == COLO_MANTICORE_MAX_HIT_MAGIC); + CHECK("armed manticore orb 1 records ranged on tick 2", + idle->ticks[1].ranged_count == 1 && idle->ticks[1].max_hit == COLO_MANTICORE_MAX_HIT_RANGED); + CHECK("armed manticore orb 2 records melee on tick 3", + idle->ticks[2].melee_count == 1 && idle->melee_fallback_exposure == 1); +} + +static void test_step_out_forecast_manticore_pair_stagger(void) { + printf("test_step_out_forecast_manticore_pair_stagger\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 406, 17, 16); + col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); + col_init_npc(&s, 1, COLO_MANTICORE, 12, 12); + s.npcs[0].attack_timer = 1; + s.npcs[1].attack_timer = 1; + ColoManticoreState* amc = colo_npc_manticore(&s.npcs[0]); + ColoManticoreState* bmc = colo_npc_manticore(&s.npcs[1]); + amc->cycle_step = 0; + amc->orb_style[0] = ATTACK_STYLE_MAGIC; + amc->orb_style[1] = ATTACK_STYLE_RANGED; + amc->orb_style[2] = ATTACK_STYLE_MELEE; + bmc->cycle_step = 0; + bmc->orb_style[0] = ATTACK_STYLE_MAGIC; + bmc->orb_style[1] = ATTACK_STYLE_RANGED; + bmc->orb_style[2] = ATTACK_STYLE_MELEE; + + ColoStepOutForecast forecast; + col_build_step_out_forecast_ctx(&s, &forecast); + const ColoStepOutForecastAction* idle = &forecast.actions[0]; + CHECK("synced-pair forecast predicts ONE orb per tick, not two", + idle->ticks[0].magic_count == 1 && + idle->ticks[1].ranged_count == 1 && + idle->ticks[2].melee_count == 1); + + s.npcs[1].attack_timer = 3; + bmc->orb_style[0] = ATTACK_STYLE_RANGED; + col_build_step_out_forecast_ctx(&s, &forecast); + idle = &forecast.actions[0]; + CHECK("still-charging peer forecast overlaps mid-barrage", + idle->ticks[2].melee_count == 1 && idle->ticks[2].ranged_count == 1); +} + +static void test_step_out_forecast_warband_window_and_break(void) { + printf("test_step_out_forecast_warband_window_and_break\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 402, 7, 18); + s.tick = 100; + s.warband_cycle_anchor = 100; + col_init_npc(&s, 0, COLO_FREMENNIK_BERSERKER, 8, 18); + + ColoStepOutForecast forecast; + col_build_step_out_forecast_ctx(&s, &forecast); + int run_west = forecast_move_action_for_delta(-2, 0); + CHECK("adjacent berserker records melee on its next window", + forecast.actions[0].ticks[0].melee_count == 1); + CHECK("running west breaks the berserker forecast adjacency", + !forecast_action_has_event(&forecast.actions[run_west])); +} + +static void test_step_out_forecast_ranged_los_candidate_tiles(void) { + printf("test_step_out_forecast_ranged_los_candidate_tiles\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 403, 7, 9); + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 12, 12); + s.npcs[0].attack_timer = 1; + + ColoStepOutForecast forecast; + col_build_step_out_forecast_ctx(&s, &forecast); + int run_north = forecast_move_action_for_delta(0, 2); + CHECK("pillar-blocked idle tile records no shaman forecast", + !forecast_action_has_event(&forecast.actions[0])); + CHECK("clear run-north tile records the shaman magic forecast", + forecast.actions[run_north].ticks[0].magic_count == 1); +} + +static void test_step_out_forecast_valid_flags(void) { + printf("test_step_out_forecast_valid_flags\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 404, 7, 9); + + ColoStepOutForecast forecast; + col_build_step_out_forecast_ctx(&s, &forecast); + int walk_east = forecast_move_action_for_delta(1, 0); + int walk_west = forecast_move_action_for_delta(-1, 0); + CHECK("pillar move has invalid step-out forecast flag", + forecast.actions[walk_east].valid == 0); + CHECK("clear move has valid step-out forecast flag", + forecast.actions[walk_west].valid == 1); +} + +static void test_step_out_forecast_same_tick_mixed_styles(void) { + printf("test_step_out_forecast_same_tick_mixed_styles\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 405, 17, 16); + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 13, 16); + col_init_npc(&s, 1, COLO_JAVELIN_COLOSSUS, 20, 15); + s.npcs[0].attack_timer = 1; + s.npcs[1].attack_timer = 1; + + ColoStepOutForecast forecast; + col_build_step_out_forecast_ctx(&s, &forecast); + const ColoStepOutForecastAction* idle = &forecast.actions[0]; + CHECK("same tick magic and ranged forecast conflict is flagged", + idle->same_tick_mixed_style_conflict == 1); + CHECK("same tick magic and ranged counts are both recorded", + idle->ticks[0].magic_count == 1 && idle->ticks[0].ranged_count == 1); +} + +static void test_render_bridge_combat_visuals_and_loadout(void) { + printf("test_render_bridge_combat_visuals_and_loadout\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 501, 17, 16); + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 13, 16); + s.npcs[0].attack_timer = 0; + col_npc_attack_ctx(&s, &ctx, 0); + EncounterOverlay ov = {0}; + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("magic NPC attack emits a render projectile", ov.projectile_count > 0); + CHECK("magic NPC projectile tracks the player", + ov.projectiles[0].source_npc_slot == 0 && + ov.projectiles[0].target_kind == ENCOUNTER_PROJECTILE_TARGET_PLAYER); + CHECK("serpent shaman uses Water Surge projectile ids", + ov.projectiles[0].launch_gfx_id == 1458 && + ov.projectiles[0].travel_gfx_id == 1459 && + ov.projectiles[0].impact_gfx_id == 1460); + RenderEntity npc_anim_entities[4]; + int npc_anim_count = 0; + col_fill_render_entities_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, + npc_anim_entities, 4, &npc_anim_count); + CHECK("serpent shaman attack drives body attack animation", + npc_anim_count >= 2 && npc_anim_entities[1].npc_anim_id == 10859); + + init_forecast_test_state(&s, &ctx, 502, 17, 16); + col_init_npc(&s, 0, COLO_JAGUAR_WARRIOR, 18, 16); + s.npcs[0].attack_timer = 0; + col_npc_attack_ctx(&s, &ctx, 0); + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("melee NPC attack emits no projectile", ov.projectile_count == 0); + + init_forecast_test_state(&s, &ctx, 503, 17, 16); + col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); + ColoManticoreState* mc = colo_npc_manticore(&s.npcs[0]); + mc->fixed_orb_style[0] = ATTACK_STYLE_MAGIC; + mc->fixed_orb_style[1] = ATTACK_STYLE_RANGED; + mc->fixed_orb_style[2] = ATTACK_STYLE_MELEE; + s.npcs[0].attack_timer = 2; + col_npc_attack_ctx(&s, &ctx, 0); + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + memset(npc_anim_entities, 0, sizeof(npc_anim_entities)); + npc_anim_count = 0; + col_fill_render_entities_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, + npc_anim_entities, 4, &npc_anim_count); + CHECK("manticore arm tick drives charge animation once", + s.npcs[0].attacked_this_tick == 0 && + npc_anim_count >= 2 && + npc_anim_entities[1].npc_anim_id == 10868); + CHECK("manticore windup renders stacked remaining orbs bottom first", + ov.projectile_count == 0 && + ov.floating_model_count == 3 && + ov.floating_models[0].model_id == 51215u && + ov.floating_models[0].anim_id == 10329 && + ov.floating_models[1].model_id == 51221u && + ov.floating_models[1].anim_id == 10327 && + ov.floating_models[2].model_id == 51213u && + ov.floating_models[2].anim_id == 10328 && + ov.floating_models[0].height_offset < ov.floating_models[1].height_offset && + ov.floating_models[1].height_offset < ov.floating_models[2].height_offset); + + init_forecast_test_state(&s, &ctx, 503, 17, 16); + col_init_npc(&s, 0, COLO_MANTICORE, 16, 12); + mc = colo_npc_manticore(&s.npcs[0]); + mc->fixed_orb_style[0] = ATTACK_STYLE_MELEE; + mc->fixed_orb_style[1] = ATTACK_STYLE_RANGED; + mc->fixed_orb_style[2] = ATTACK_STYLE_MAGIC; + s.npcs[0].attack_timer = 0; + col_npc_attack_ctx(&s, &ctx, 0); + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + memset(npc_anim_entities, 0, sizeof(npc_anim_entities)); + npc_anim_count = 0; + col_fill_render_entities_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, + npc_anim_entities, 4, &npc_anim_count); + CHECK("manticore attack drives body attack animation", + npc_anim_count >= 2 && npc_anim_entities[1].npc_anim_id == 10869); + CHECK("alive manticore attack does not select death animation", + npc_anim_count >= 2 && + npc_anim_entities[1].npc_anim_id != col_npc_death_anim_id(COLO_MANTICORE)); + int manticore_dist = encounter_projectile_distance( + s.npcs[0].x, s.npcs[0].y, col_npc_effective_size(&s.npcs[0]), + s.player.x, s.player.y, 1, ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + EncounterProjectileTiming manticore_timing = + col_npc_projectile_timing(COLO_MANTICORE, ATTACK_STYLE_MELEE, manticore_dist); + CHECK("manticore melee orb emits the 51213 projectile with scaled duration", + ov.projectile_count == 1 && + ov.projectiles[0].model_id == 51213u && + ov.projectiles[0].anim_id == 10328 && + ov.projectiles[0].travel_gfx_id == 2685 && + ov.projectiles[0].impact_gfx_id == 2686 && + ov.projectiles[0].duration_ticks == manticore_timing.visual_duration_ticks * 30 && + ov.projectiles[0].duration_ticks > 1); + + init_forecast_test_state(&s, &ctx, 503, 17, 16); + col_init_npc(&s, 0, COLO_JAVELIN_COLOSSUS, 20, 16); + s.npcs[0].attack_timer = 0; + col_npc_attack_ctx(&s, &ctx, 0); + memset(npc_anim_entities, 0, sizeof(npc_anim_entities)); + npc_anim_count = 0; + col_fill_render_entities_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, + npc_anim_entities, 4, &npc_anim_count); + CHECK("javelin colossus attack drives body attack animation", + npc_anim_count >= 2 && npc_anim_entities[1].npc_anim_id == 10892); + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("javelin normal ranged throw waits one tick before visual release", + ov.projectile_count == 1 && + ov.projectiles[0].start_delay == + COLO_JAVELIN_PROJECTILE_RELEASE_DELAY_TICKS * 30); + + init_forecast_test_state(&s, &ctx, 503, 17, 16); + col_init_npc(&s, 0, COLO_JAVELIN_COLOSSUS, 20, 16); + ColoJavelinState* jv = colo_npc_javelin(&s.npcs[0]); + jv->attack_count = 4; + s.npcs[0].attack_timer = 0; + col_npc_attack_ctx(&s, &ctx, 0); + CHECK("javelin skyfall launch marks the attack tick", + s.npcs[0].attacked_this_tick == 1); + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("javelin skyfall launch lobs straight up out of the colossus", + ov.projectile_count == 1 && + ov.projectiles[0].source_kind == ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT && + ov.projectiles[0].source_npc_slot == 0 && + ov.projectiles[0].target_kind == ENCOUNTER_PROJECTILE_TARGET_FIXED && + ov.projectiles[0].src_x == s.npcs[0].x && + ov.projectiles[0].src_y == s.npcs[0].y && + ov.projectiles[0].dst_x == ov.projectiles[0].src_x && + ov.projectiles[0].dst_y == ov.projectiles[0].src_y && + ov.projectiles[0].travel_gfx_id == COLO_JAVELIN_SKYFALL_LAUNCH_TRAVEL_GFX_ID && + ov.projectiles[0].travel_gfx_id != 2673 && + ov.projectiles[0].impact_gfx_id == 0 && + ov.projectiles[0].start_h < ov.projectiles[0].end_h && + ov.projectiles[0].curve == COLO_JAVELIN_SKYFALL_LAUNCH_CURVE && + ov.projectiles[0].start_delay == 0 && + ov.projectiles[0].duration_ticks == + (COLO_JAVELIN_SKYFALL_DELAY - + COLO_JAVELIN_SKYFALL_DROP_GAME_TICKS - + COLO_JAVELIN_SKYFALL_APEX_GAME_TICKS) * 30); + CHECK("javelin skyfall launch emits growing target shadow", + ov.tile_shadow_count == 1 && + ov.tile_shadows[0].active == 1 && + ov.tile_shadows[0].x == jv->skyfall_tile_x && + ov.tile_shadows[0].y == jv->skyfall_tile_y && + ov.tile_shadows[0].scale > 0.0f && + ov.tile_shadows[0].scale < 1.0f); + float initial_shadow_scale = ov.tile_shadows[0].scale; + int saved_skyfall_timer = jv->skyfall_timer; + int saved_attacked_this_tick = s.npcs[0].attacked_this_tick; + jv->skyfall_timer = 1; + s.npcs[0].attacked_this_tick = 0; + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("javelin skyfall target shadow grows to full before landing", + ov.tile_shadow_count == 1 && + ov.tile_shadows[0].x == jv->skyfall_tile_x && + ov.tile_shadows[0].y == jv->skyfall_tile_y && + ov.tile_shadows[0].scale > initial_shadow_scale && + ov.tile_shadows[0].scale >= 0.99f); + jv->skyfall_timer = saved_skyfall_timer; + s.npcs[0].attacked_this_tick = saved_attacked_this_tick; + memset(npc_anim_entities, 0, sizeof(npc_anim_entities)); + npc_anim_count = 0; + col_fill_render_entities_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, + npc_anim_entities, 4, &npc_anim_count); + CHECK("javelin skyfall launch drives lob body animation", + npc_anim_count >= 2 && + npc_anim_entities[1].npc_anim_id == COLO_JAVELIN_SKYFALL_ANIM_ID); + int clear_anim[COLO_NUM_ACTION_HEADS] = {0}; + step_and_observe(&s, &ctx, clear_anim); + jv->attack_count = 5; + s.npcs[0].attack_timer = 0; + col_npc_attack_ctx(&s, &ctx, 0); + memset(npc_anim_entities, 0, sizeof(npc_anim_entities)); + npc_anim_count = 0; + col_fill_render_entities_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, + npc_anim_entities, 4, &npc_anim_count); + CHECK("javelin normal attack after skyfall uses throw body animation", + npc_anim_count >= 2 && npc_anim_entities[1].npc_anim_id == 10892); + + init_forecast_test_state(&s, &ctx, 504, 17, 16); + col_init_npc(&s, 0, COLO_JAVELIN_COLOSSUS, 20, 16); + jv = colo_npc_javelin(&s.npcs[0]); + jv->skyfall_pending = 1; + jv->skyfall_tile_x = 21; + jv->skyfall_tile_y = 15; + jv->skyfall_damage = 37; + + jv->skyfall_timer = COLO_JAVELIN_SKYFALL_DROP_GAME_TICKS + 1; + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("javelin skyfall descent holds until the telegraph midpoint", + ov.tile_shadow_count == 1 && ov.projectile_count == 0); + + jv->skyfall_timer = COLO_JAVELIN_SKYFALL_DROP_GAME_TICKS; + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("javelin skyfall descent falls straight down with fiery impact at the midpoint", + ov.projectile_count == 1 && + ov.projectiles[0].source_kind == ENCOUNTER_PROJECTILE_TARGET_FIXED && + ov.projectiles[0].target_kind == ENCOUNTER_PROJECTILE_TARGET_FIXED && + ov.projectiles[0].src_x == jv->skyfall_tile_x && + ov.projectiles[0].src_y == jv->skyfall_tile_y && + ov.projectiles[0].dst_x == jv->skyfall_tile_x && + ov.projectiles[0].dst_y == jv->skyfall_tile_y && + ov.projectiles[0].travel_gfx_id == COLO_JAVELIN_SKYFALL_DROP_TRAVEL_GFX_ID && + ov.projectiles[0].travel_gfx_id != 2673 && + ov.projectiles[0].impact_gfx_id == COLO_JAVELIN_SKYFALL_IMPACT_GFX_ID && + ov.projectiles[0].start_h > ov.projectiles[0].end_h && + ov.projectiles[0].curve == COLO_JAVELIN_SKYFALL_DROP_CURVE && + ov.projectiles[0].duration_ticks == COLO_JAVELIN_SKYFALL_DROP_GAME_TICKS * 30); + + jv->skyfall_timer = COLO_JAVELIN_SKYFALL_DROP_GAME_TICKS - 1; + CHECK("javelin skyfall drop point leaves room below it", jv->skyfall_timer >= 1); + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("javelin skyfall descent is not re-emitted after the midpoint", + ov.projectile_count == 0); + + jv->skyfall_timer = 1; + col_npc_resolve_javelin_skyfall(&s, &ctx, 0); + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("javelin skyfall landing clears the shadow and emits no late descent", + ov.tile_shadow_count == 0 && ov.projectile_count == 0 && + jv->skyfall_pending == 0); + + init_forecast_test_state(&s, &ctx, 503, 17, 16); + col_apply_weapon_set(&s, COLO_GEAR_RANGED); + int target_slot = col_spawn_npc_at(&s, COLO_JAVELIN_COLOSSUS, 22, 15); + s.interaction.target_slot = target_slot; + int idle[COLO_NUM_ACTION_HEADS] = {0}; + col_tick_player_ctx(&s, &ctx, idle, 1); + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("player ranged attack emits a render projectile", ov.projectile_count > 0); + CHECK("player projectile targets the attacked NPC", + ov.projectiles[0].source_kind == ENCOUNTER_PROJECTILE_TARGET_PLAYER && + ov.projectiles[0].target_npc_slot == target_slot); + + RenderEntity entities[4]; + int count = 0; + col_fill_render_entities_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, entities, 4, &count); + const uint8_t* const* loadouts = col_loadouts_for_profile(s.active_loadout_profile); + CHECK("render player uses the active loadout weapon", + count > 0 && + entities[0].equipped[GEAR_SLOT_WEAPON] == + loadouts[s.weapon_set][GEAR_SLOT_WEAPON]); + CHECK("player inventory cells are populated for the GUI", + test_find_inventory_cell_with_item( + &s, loadouts[s.weapon_set][GEAR_SLOT_WEAPON]) >= 0); + + s.modifiers.active_mask = + (1u << (unsigned)COLO_MOD_RELENTLESS) | + (1u << (unsigned)COLO_MOD_FRAILTY); + s.modifiers.tier[COLO_MOD_RELENTLESS] = 2; + s.modifiers.tier[COLO_MOD_FRAILTY] = 3; + memset(&ov, 0, sizeof(ov)); + col_render_post_tick_ctx((EncounterState*)&s, (EncounterContext*)&ctx, &ov); + CHECK("render overlay exposes active modifier tiers", + ov.active_modifier_count == 2 && + ov.active_modifiers[0].modifier == COLO_MOD_FRAILTY && + ov.active_modifiers[0].tier == 3 && + ov.active_modifiers[1].modifier == COLO_MOD_RELENTLESS && + ov.active_modifiers[1].tier == 2); +} + +static void test_render_bridge_npc_debug_and_warband_motion(void) { + printf("test_render_bridge_npc_debug_and_warband_motion\n"); + ColosseumContext ctx; + ColosseumState s; + init_forecast_test_state(&s, &ctx, 504, 17, 16); + col_init_npc(&s, 0, COLO_FREMENNIK_ARCHER, 20, 18); + col_init_npc(&s, 1, COLO_MANTICORE, 20, 12); + s.npcs[0].attack_timer = 5; + s.npcs[1].attack_timer = 3; + s.npcs[1].type_state.manticore.cycle_step = 1; + s.npcs[1].type_state.manticore.orb_style[0] = ATTACK_STYLE_RANGED; + s.npcs[1].type_state.manticore.orb_style[1] = ATTACK_STYLE_MAGIC; + s.npcs[1].type_state.manticore.orb_style[2] = ATTACK_STYLE_MELEE; + + RenderEntity entities[4]; + int count = 0; + col_fill_render_entities_ctx( + (EncounterState*)&s, (EncounterContext*)&ctx, entities, 4, &count); + + CHECK("warband render entity uses run-speed interpolation", + count >= 3 && entities[1].npc_slot == 0 && entities[1].is_running == 1); + CHECK("warband render entity carries debug stats", + count >= 3 && + strcmp(entities[1].debug_npc_type_name, "Fremennik Archer") == 0 && + entities[1].debug_attack_timer == 5 && + entities[1].debug_attack_style == ATTACK_STYLE_RANGED); + CHECK("manticore render entity carries cycle and orb debug state", + count >= 3 && + entities[2].debug_manticore_state_active == 1 && + entities[2].debug_manticore_cycle_step == 1 && + entities[2].debug_manticore_orb_style[0] == ATTACK_STYLE_RANGED && + entities[2].debug_manticore_orb_style[1] == ATTACK_STYLE_MAGIC && + entities[2].debug_manticore_orb_style[2] == ATTACK_STYLE_MELEE); +} + +static int test_los_every_tile_blocked(void* ctx, int x, int y) { + (void)ctx; + (void)x; + (void)y; + return 1; +} + +static void test_osrs_los_query_contracts(void) { + printf("test_osrs_los_query_contracts\n"); + OsrsLosQuery open_query = osrs_los_open(); + CHECK("explicit open LoS permits a ranged attack", + encounter_player_can_attack(0, 0, 4, 0, 1, 10, &open_query) == 1); + + OsrsLosQuery tile_query = osrs_los_tile(test_los_every_tile_blocked, NULL); + CHECK("tile LoS refuses when every tile blocks", + encounter_player_can_attack(0, 0, 4, 0, 1, 10, &tile_query) == 0); +} + +static void test_player_ranged_los_blocked_by_pillar(void) { + printf("test_player_ranged_los_blocked_by_pillar\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 4242); + geo_clear_npcs(&s); + col_apply_weapon_set(&s, COLO_GEAR_RANGED); + CHECK("ranged loadout reaches past 1 tile", col_player_attack_range(&s) > 1); + + s.player.x = 5; s.player.y = 9; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_JAGUAR_WARRIOR, 13, 9); + ColoNPC* npc = &s.npcs[0]; + CHECK("player + target tiles are clear of static blockers", + !col_static_blocked(5, 9) && !col_static_blocked(13, 9)); + CHECK("pillar 0 sits on the line between them", col_static_blocked(9, 9)); + CHECK("no LoS through the pillar", col_npc_has_los_to_player(&s, npc) == 0); + OsrsLosQuery los_query = col_player_los_query(&s); + CHECK("shared tile LoS blocks the same pillar line", + encounter_player_can_attack(s.player.x, s.player.y, + npc->x, npc->y, col_npc_effective_size(npc), + col_player_attack_range(&s), &los_query) == 0); + + s.player.x = 13; s.player.y = 4; + col_rebuild_player_collision_flags(&s); + CHECK("the clear column tile is walkable", !col_static_blocked(13, 4)); + CHECK("LoS is clear down the column", col_npc_has_los_to_player(&s, npc) == 1); + int actions[COLO_NUM_ACTION_HEADS] = {0}; + osrs_interaction_set(&s.interaction, 0); + s.player.attack_timer = 0; + s.player_dest_x = -1; s.player_dest_y = -1; + s.tick_scratch.player_attacked = 0; + col_tick_player_ctx(&s, &ctx, actions, 1); + CHECK("a ranged attack with clear LoS fires", + s.tick_scratch.player_attacked == 1); +} + +static void test_player_chase_routes_around_pillar_for_los(void) { + printf("test_player_chase_routes_around_pillar_for_los\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + ctx.world_offset_x = 1808; + ctx.world_offset_y = 3090; + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 5151); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + col_apply_weapon_set(&s, COLO_GEAR_RANGED); + + s.player.x = 5; + s.player.y = 9; + s.player_dest_x = -1; + s.player_dest_y = -1; + col_init_npc(&s, 0, COLO_JAGUAR_WARRIOR, 13, 9); + col_rebuild_player_collision_flags(&s); + ColoNPC* npc = &s.npcs[0]; + OsrsLosQuery los_query = col_player_los_query(&s); + int attack_range = col_player_attack_range(&s); + CHECK("start tile is range-valid and LoS-blocked", + encounter_player_can_attack(s.player.x, s.player.y, + npc->x, npc->y, col_npc_effective_size(npc), + attack_range, &los_query) == 0); + + int actions[COLO_NUM_ACTION_HEADS] = {0}; + int attacked_tick = -1; + int moved_from_start = 0; + osrs_interaction_set(&s.interaction, 0); + for (int tick = 0; tick < 12; tick++) { + s.tick_scratch.player_attacked = 0; + col_tick_player_ctx(&s, &ctx, actions, 1); + if (s.player.x != 5 || s.player.y != 9) moved_from_start = 1; + if (s.tick_scratch.player_attacked) { + attacked_tick = tick; + break; + } + } + + CHECK("pillar-blocked interaction makes the player chase", + moved_from_start == 1); + CHECK("chase reaches LoS and fires within twelve ticks", + attacked_tick >= 0); + CHECK("attack fires from a LoS-valid tile", + col_npc_has_los_to_player(&s, npc) == 1 && + encounter_rect_distance(s.player.x, s.player.y, 1, + npc->x, npc->y, col_npc_effective_size(npc)) <= attack_range); +} + +static void test_colosseum_npc_movement_player_tile_guards(void) { + printf("test_colosseum_npc_movement_player_tile_guards\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 6161); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.player.x = 16; + s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 16, 16); + col_tick_npcs_ctx(&s, &ctx); + CHECK("overlapped ranged NPC shuffles off the player tile", + !test_npc_covers_player(&s, &s.npcs[0]) && s.npcs[0].moved_this_tick == 1); + + geo_clear_npcs(&s); + s.player.x = 16; + s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 16, 16); + s.player_last_interaction_target_slot = 0; + s.player_last_interaction_age = 0; + col_npc_move_ctx(&s, &ctx, 0); + CHECK("overlapped just-clicked ranged NPC holds current tile", + test_npc_covers_player(&s, &s.npcs[0]) && s.npcs[0].moved_this_tick == 0); + + geo_clear_npcs(&s); + s.player.x = 16; + s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 7, 16); + int shaman_x = s.npcs[0].x; + int shaman_y = s.npcs[0].y; + col_npc_move_ctx(&s, &ctx, 0); + CHECK("ranged NPC already in range and LoS holds its attack tile", + s.npcs[0].x == shaman_x && s.npcs[0].y == shaman_y && + encounter_dist_to_npc(s.player.x, s.player.y, + s.npcs[0].x, s.npcs[0].y, col_npc_effective_size(&s.npcs[0])) <= + COLO_NPC_STATS[COLO_SERPENT_SHAMAN].attack_range && + col_npc_has_los_to_player(&s, &s.npcs[0])); + + geo_clear_npcs(&s); + s.player.x = 6; + s.player.y = 11; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 13, 11); + shaman_x = s.npcs[0].x; + shaman_y = s.npcs[0].y; + CHECK("named regression start is range plus LoS", + encounter_dist_to_npc(s.player.x, s.player.y, + s.npcs[0].x, s.npcs[0].y, col_npc_effective_size(&s.npcs[0])) <= + COLO_NPC_STATS[COLO_SERPENT_SHAMAN].attack_range && + col_npc_has_los_to_player(&s, &s.npcs[0])); + col_npc_move_ctx(&s, &ctx, 0); + CHECK("ranged shaman range+LoS player one-step around pillar holds OSRS tile", + s.npcs[0].x == shaman_x && s.npcs[0].y == shaman_y && + s.npcs[0].moved_this_tick == 0); + CHECK("ranged shaman never takes the over-closed player-adjacent tile", + encounter_dist_to_npc(s.player.x, s.player.y, + s.npcs[0].x, s.npcs[0].y, col_npc_effective_size(&s.npcs[0])) > 1); + + geo_clear_npcs(&s); + s.player.x = 5; + s.player.y = 9; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_SERPENT_SHAMAN, 13, 9); + col_npc_move_ctx(&s, &ctx, 0); + CHECK("LoS-broken shaman takes one SDK legal greedy step", + s.npcs[0].x == 12 && s.npcs[0].y == 9 && + s.npcs[0].moved_this_tick == 1 && + !test_npc_covers_player(&s, &s.npcs[0])); + + const ColoNpcType large_types[3] = { + COLO_JAVELIN_COLOSSUS, + COLO_SHOCKWAVE_COLOSSUS, + COLO_MANTICORE, + }; + for (int i = 0; i < 3; i++) { + geo_clear_npcs(&s); + s.player.x = 12; + s.player.y = 12; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, large_types[i], 5, 5); + col_npc_move_ctx(&s, &ctx, 0); + CHECK("size-3 ranged NPC uses leading-edge clearance around pillar", + s.npcs[0].x == 6 && s.npcs[0].y == 5 && + s.npcs[0].moved_this_tick == 1); + } + + geo_clear_npcs(&s); + s.player.x = 18; + s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_JAGUAR_WARRIOR, 14, 16); + int jaguar_covered_player = 0; + int jaguar_reached_melee = 0; + for (int tick = 0; tick < 4; tick++) { + col_npc_move_ctx(&s, &ctx, 0); + if (test_npc_covers_player(&s, &s.npcs[0])) jaguar_covered_player = 1; + if (encounter_dist_to_npc(s.player.x, s.player.y, + s.npcs[0].x, s.npcs[0].y, col_npc_effective_size(&s.npcs[0])) == 1) { + jaguar_reached_melee = 1; + break; + } + } + CHECK("melee NPC stops adjacent and never covers the player", + jaguar_reached_melee == 1 && jaguar_covered_player == 0); + + geo_clear_npcs(&s); + s.modifiers.active_mask |= (1u << COLO_MOD_RED_FLAG); + s.modifiers.tier[COLO_MOD_RED_FLAG] = 1; + s.player.x = 7; + s.player.y = 9; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_MINOTAUR, 11, 9); + int covered_player = 0; + int reached_melee = 0; + for (int tick = 0; tick < 32; tick++) { + col_npc_move_ctx(&s, &ctx, 0); + if (test_npc_covers_player(&s, &s.npcs[0])) covered_player = 1; + if (encounter_dist_to_npc(s.player.x, s.player.y, + s.npcs[0].x, s.npcs[0].y, col_npc_effective_size(&s.npcs[0])) == 1) { + reached_melee = 1; + break; + } + } + CHECK("routefinding minotaur paths around the pillar to melee", + reached_melee == 1); + CHECK("routefinding minotaur never covers the player tile", + covered_player == 0); +} + +static void test_npc_melee_instant_unprayable(void) { + printf("test_npc_melee_instant_unprayable\n"); + ColosseumContext ctx; + col_init_context_typed(&ctx); + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 91); + geo_clear_npcs(&s); + + s.player.x = 18; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_JAGUAR_WARRIOR, 16, 16); + + int blocked_ok = 1, queued = 0; + float correct_before = s.log.pray_correct_by_type[COLO_JAGUAR_WARRIOR]; + for (int rep = 0; rep < 8; rep++) { + ctx.player_render_hit_count = 0; + s.npcs[0].attack_timer = 0; + s.player.prayer = PRAYER_PROTECT_MELEE; + s.player.current_hitpoints = 99; + col_npc_attack_ctx(&s, &ctx, 0); + if (s.player.current_hitpoints != 99) blocked_ok = 0; + queued += s.player_pending_hits.count; + } + CHECK("Protect-from-Melee on the swing tick blocks every jaguar hit", blocked_ok); + CHECK("instant melee queues nothing for a later landing (prayed)", queued == 0); + CHECK("a blocked melee hit still counts prayer_correct", + s.log.pray_correct_by_type[COLO_JAGUAR_WARRIOR] > correct_before); + + float faced_before = s.log.pray_faced_by_type[COLO_JAGUAR_WARRIOR]; + float wrong_correct_before = s.log.pray_correct_by_type[COLO_JAGUAR_WARRIOR]; + float offpray_before = s.log.offpray_damage_by_type[COLO_JAGUAR_WARRIOR]; + int wrong_faced = 0, wrong_queued = 0; + for (int rep = 0; rep < 64; rep++) { + ctx.player_render_hit_count = 0; + s.npcs[0].attack_timer = 0; + s.player.prayer = PRAYER_PROTECT_MAGIC; + s.player.current_hitpoints = 99; + col_npc_attack_ctx(&s, &ctx, 0); + wrong_queued += s.player_pending_hits.count; + wrong_faced += 3; + } + CHECK("each off-prayer jaguar swing is faced on the same call", + s.log.pray_faced_by_type[COLO_JAGUAR_WARRIOR] - faced_before == (float)wrong_faced); + CHECK("an off-prayer melee swing is never counted correct", + s.log.pray_correct_by_type[COLO_JAGUAR_WARRIOR] == wrong_correct_before); + CHECK("instant melee queues nothing for a later landing (off-prayer)", + wrong_queued == 0); + CHECK("an unprayed melee hit deals damage on the same call", + s.log.offpray_damage_by_type[COLO_JAGUAR_WARRIOR] > offpray_before); +} + +static void test_player_melee_lands_at_delay_zero(void) { + printf("test_player_melee_lands_at_delay_zero\n"); + ColosseumContext ctx; + ColosseumState s; + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 1.0f, 91); + geo_clear_npcs(&s); + s.player.x = 18; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_JAGUAR_WARRIOR, 16, 16); + + s.player.attack_timer = 0; + col_player_attack_target(&s, 0); + CHECK("a melee swing queues at least one pending hit on the target", + s.npcs[0].pending_hits.count >= 1); + int all_delay_zero = s.npcs[0].pending_hits.count >= 1; + for (int h = 0; h < s.npcs[0].pending_hits.count; h++) + if (s.npcs[0].pending_hits.hits[h].ticks_remaining != 0) all_delay_zero = 0; + CHECK("Q6: melee pending hits land at delay 0 (shared OSRS melee delay)", + all_delay_zero); + + int hp_before = s.npcs[0].hp; + int resolved_same_pass = 1; + for (int swing = 0; swing < 32 && s.npcs[0].hp == hp_before; swing++) { + col_resolve_player_projectiles_on_npcs(&s); + if (s.npcs[0].pending_hits.count != 0) resolved_same_pass = 0; + if (s.npcs[0].hp < hp_before) break; + s.player.attack_timer = 0; + col_player_attack_target(&s, 0); + } + col_resolve_player_projectiles_on_npcs(&s); + CHECK("a delay-0 melee hit lands on the first resolver pass", + s.npcs[0].hp < hp_before && resolved_same_pass && + s.npcs[0].pending_hits.count == 0); + + ColosseumState r; + ColosseumContext rctx; + loadout_reset(&r, &rctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 1.0f, 91); + geo_clear_npcs(&r); + col_apply_weapon_set(&r, COLO_GEAR_RANGED); + r.player.x = 10; r.player.y = 16; + col_rebuild_player_collision_flags(&r); + col_init_npc(&r, 0, COLO_JAGUAR_WARRIOR, 16, 16); + r.player.attack_timer = 0; + col_player_attack_target(&r, 0); + int ranged_delay_positive = r.npcs[0].pending_hits.count >= 1; + for (int h = 0; h < r.npcs[0].pending_hits.count; h++) + if (r.npcs[0].pending_hits.hits[h].ticks_remaining <= 0) ranged_delay_positive = 0; + CHECK("a ranged swing keeps its flight delay (>0), so Q6 stays melee-scoped", + ranged_delay_positive); +} + +static void test_echo_boots_recoil_reflects_to_attacker(void) { + printf("test_echo_boots_recoil_reflects_to_attacker\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 1.0f, 91); + geo_clear_npcs(&s); + CHECK("beginner kit equips echo boots", + s.player.equipped[GEAR_SLOT_FEET] == ITEM_ECHO_BOOTS && + osrs_effect_profile_has(col_live_effects(&s), OSRS_ITEM_EFFECT_ECHO_BOOTS)); + CHECK("echo boots start charged", + s.player.item_effect_state.echo_boot_charges == OSRS_ECHO_BOOTS_MAX_CHARGES); + + s.player.x = 18; s.player.y = 16; + col_rebuild_player_collision_flags(&s); + col_init_npc(&s, 0, COLO_JAGUAR_WARRIOR, 16, 16); + + int recoiled = 0; + long charges_before = s.player.item_effect_state.echo_boot_charges; + for (int rep = 0; rep < 64 && !recoiled; rep++) { + ctx.player_render_hit_count = 0; + s.npcs[0].attack_timer = 0; + s.npcs[0].hit_damage = 0; + s.player.prayer = PRAYER_PROTECT_MAGIC; + s.player.current_hitpoints = 99; + int hp_before = s.npcs[0].hp; + long ch_before = s.player.item_effect_state.echo_boot_charges; + col_npc_attack_ctx(&s, &ctx, 0); + if (s.player.current_hitpoints < 99) { + + CHECK("a connecting melee reflects exactly one recoil point per hit", + hp_before - s.npcs[0].hp >= 1); + CHECK("recoil consumes one echo-boots charge per reflected hit", + ch_before - s.player.item_effect_state.echo_boot_charges == + (long)(hp_before - s.npcs[0].hp)); + recoiled = 1; + } + } + CHECK("an unprayed jaguar eventually lands and recoils", recoiled); + CHECK("recoil drew down the echo-boots charge pool", + s.player.item_effect_state.echo_boot_charges < charges_before); + + ColosseumState sol; + ColosseumContext solctx; + loadout_reset(&sol, &solctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 1.0f, 91); + geo_clear_npcs(&sol); + col_init_npc(&sol, 0, COLO_SOL_HEREDIT, 16, 16); + int sol_hp_before = sol.npcs[0].hp; + col_apply_echo_boots_recoil(&sol, &solctx, 0, 30); + CHECK("echo-boots recoil never reflects onto Sol Heredit", + sol.npcs[0].hp == sol_hp_before && + sol.player.item_effect_state.echo_boot_charges == OSRS_ECHO_BOOTS_MAX_CHARGES); +} + +static void test_colosseum_live_inventory_display(void) { + printf("test_colosseum_live_inventory_display\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 7); + + int kit[COLO_INVENTORY_DISPLAY_SLOTS]; + col_build_live_inventory_display(&s, kit); + CHECK("abyssal tentacle is the melee switch carried in the bag", kit[14] == 12006); + CHECK("worn scythe is not duplicated in the starting bag (worn only)", + test_count_item_in_equipment_and_inventory(&s, ITEM_SCYTHE_OF_VITUR) == 1); + CHECK("tbow switch stays in grid", kit[0] == 20997); + CHECK("brew vial full at start", kit[18] == 6685); + CHECK("divine combat vial full at start", kit[16] == 23685); + CHECK("surge vial full at start", kit[12] == 30875); + + int brew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_BREW); + s.player.current_hitpoints = 50; + int brew[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, brew, brew_cell); + step_and_observe(&s, &ctx, brew); + col_build_live_inventory_display(&s, kit); + CHECK("brew vial shows 3-dose after a drink", kit[18] == 6687); + + s.player.current_hitpoints = 99; + s.player.current_attack = s.player.base_attack; + s.player.current_strength = s.player.base_strength; + s.player.current_defence = s.player.base_defence; + s.player.potion_timer = 0; + int divine_combat_cell = + test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_DIVINE_COMBAT); + int combat[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, combat, divine_combat_cell); + step_and_observe(&s, &ctx, combat); + col_build_live_inventory_display(&s, kit); + CHECK("divine combat vial shows 3-dose after a drink", kit[16] == 23688); + + s.player.special_energy = 50; + s.surge_cooldown = 0; + s.player.potion_timer = 0; + int surge_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_SURGE); + int surge[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, surge, surge_cell); + step_and_observe(&s, &ctx, surge); + col_build_live_inventory_display(&s, kit); + CHECK("surge vial shows 3-dose after a drink", kit[12] == 30878); + + s.inventory_cells[brew_cell] = (ColoInvCell){ + .raw_osrs_id = 6691, + .item_idx = ITEM_NONE, + .dose = 1, + }; + s.player.potion_timer = 0; + s.player.current_hitpoints = 50; + int last_brew[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, last_brew, brew_cell); + step_and_observe(&s, &ctx, last_brew); + col_build_live_inventory_display(&s, kit); + CHECK("emptied brew slot clears", kit[18] == 0); + + int tbow_cell = test_find_inventory_cell_with_item(&s, ITEM_TWISTED_BOW); + int ranged[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, ranged, tbow_cell); + step_and_observe(&s, &ctx, ranged); + col_build_live_inventory_display(&s, kit); + CHECK("ranged weapon click swaps scythe into the tbow cell", kit[0] == 22325); +} + +static void test_stage3_t1_inventory_ranged_weapon_swap(void) { + printf("test_stage3_t1_inventory_ranged_weapon_swap\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 501); + uint8_t melee_weapon = s.player.equipped[GEAR_SLOT_WEAPON]; + int bow_cell = test_find_inventory_cell_with_item(&s, ITEM_TWISTED_BOW); + int actions[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, actions, bow_cell); + step_and_observe(&s, &ctx, actions); + CHECK("T1 ranged weapon click equips tbow", + s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_TWISTED_BOW); + CHECK("T1 displaced melee weapon returns to clicked cell", + s.inventory_cells[bow_cell].item_idx == melee_weapon); +} + +static void test_stage3_t1_inventory_weapon_slot_last_click_wins(void) { + printf("test_stage3_t1_inventory_weapon_slot_last_click_wins\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 508); + int bow_cell = test_find_inventory_cell_with_item(&s, ITEM_TWISTED_BOW); + int claws_cell = test_find_inventory_cell_with_item(&s, ITEM_DRAGON_CLAWS); + int actions[COLO_NUM_ACTION_HEADS] = {0}; + + actions[COLO_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] = claws_cell + 1; + step_and_observe(&s, &ctx, actions); + CHECK("weapon equip head equips the named weapon", + s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_DRAGON_CLAWS); + CHECK("unclicked weapon stays in its inventory cell", + s.inventory_cells[bow_cell].item_idx == ITEM_TWISTED_BOW); +} + +static void test_stage3_t1_human_inventory_primary_click_uses_resolver(void) { + printf("test_stage3_t1_human_inventory_primary_click_uses_resolver\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 509); + int bow_cell = test_find_inventory_cell_with_item(&s, ITEM_TWISTED_BOW); + int claws_cell = test_find_inventory_cell_with_item(&s, ITEM_DRAGON_CLAWS); + HumanInput hi; + human_input_init(&hi); + hi.enabled = 1; + human_input_queue_inventory_primary_click(&hi, bow_cell); + human_input_queue_inventory_primary_click(&hi, claws_cell); + int actions[COLO_NUM_ACTION_HEADS] = {0}; + col_translate_human_commands_ctx(&hi, actions, &s, &ctx); + + CHECK("human weapon clicks collapse to the weapon equip head", + actions[COLO_HEAD_EQUIP_SLOT(GEAR_SLOT_WEAPON)] == claws_cell + 1); + step_and_observe(&s, &ctx, actions); + CHECK("human inventory clicks use last-click-wins semantics", + s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_DRAGON_CLAWS); + CHECK("human earlier same-slot click is ignored", + s.inventory_cells[bow_cell].item_idx == ITEM_TWISTED_BOW); + human_input_destroy(&hi); +} + +static void test_stage3_t1_human_rearrange_swaps_inventory_slots(void) { + printf("test_stage3_t1_human_rearrange_swaps_inventory_slots\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 516); + int bow_cell = test_find_inventory_cell_with_item(&s, ITEM_TWISTED_BOW); + int claws_cell = test_find_inventory_cell_with_item(&s, ITEM_DRAGON_CLAWS); + + HumanInput hi; + human_input_init(&hi); + hi.enabled = 1; + human_input_queue_item_on_item( + &hi, + bow_cell, + claws_cell, + s.inventory_cells[bow_cell].item_idx, + s.inventory_cells[bow_cell].raw_osrs_id); + int actions[COLO_NUM_ACTION_HEADS] = {0}; + col_translate_human_commands_ctx(&hi, actions, &s, &ctx); + + CHECK("human rearrange moves bow to target slot", + s.inventory_cells[claws_cell].item_idx == ITEM_TWISTED_BOW); + CHECK("human rearrange moves claws to source slot", + s.inventory_cells[bow_cell].item_idx == ITEM_DRAGON_CLAWS); + CHECK("human rearrange leaves action space heads unchanged", + COLO_NUM_ACTION_HEADS == 20 && COLO_ACTION_DIMS[COLO_HEAD_EQUIP_BASE] == 29); + human_input_destroy(&hi); +} + +static void test_stage3_t2_brew_click_decrements_dose(void) { + printf("test_stage3_t2_brew_click_decrements_dose\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 502); + int brew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_BREW); + s.player.current_hitpoints = 50; + int actions[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, actions, brew_cell); + step_and_observe(&s, &ctx, actions); + CHECK("T2 brew raises HP", s.player.current_hitpoints > 50); + CHECK("T2 brew cell dose drops 4 to 3", + s.inventory_cells[brew_cell].dose == 3 && + s.inventory_cells[brew_cell].raw_osrs_id == 6687); + CHECK("T2 brew starts potion timer", s.player.potion_timer == 3); +} + +static void test_stage3_t3_one_dose_vial_empties(void) { + printf("test_stage3_t3_one_dose_vial_empties\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 503); + int brew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_BREW); + s.inventory_cells[brew_cell] = (ColoInvCell){ + .raw_osrs_id = 6691, + .item_idx = ITEM_NONE, + .dose = 1, + }; + s.player.current_hitpoints = 50; + int actions[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, actions, brew_cell); + step_and_observe(&s, &ctx, actions); + CHECK("T3 one-dose vial cell becomes empty", + s.inventory_cells[brew_cell].raw_osrs_id == 0 && + s.inventory_cells[brew_cell].item_idx == ITEM_NONE && + s.inventory_cells[brew_cell].dose == 0); +} + +static void test_colosseum_potion_click_source_of_truth(void) { + printf("test_colosseum_potion_click_source_of_truth\n"); + ColosseumContext ctx; + ColosseumState s; + int actions[COLO_NUM_ACTION_HEADS] = {0}; + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 510); + int restore_cell = + test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_SUPER_RESTORE); + int restore_sum_before = + test_sum_inventory_doses_for_kind(&s, OSRS_CONSUMABLE_SUPER_RESTORE); + s.player.current_prayer = 40; + s.player.restore_doses = 99; + test_click_inventory_cell_action_s(&s, actions, restore_cell); + step_and_observe(&s, &ctx, actions); + CHECK("super restore consumes exactly one clicked-cell dose", + s.inventory_cells[restore_cell].dose == 3); + CHECK("super restore aggregate is rebuilt from cells", + s.player.restore_doses == restore_sum_before - 1); + + memset(actions, 0, sizeof(actions)); + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 511); + int sanfew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_SANFEW); + int sanfew_sum_before = test_sum_inventory_doses_for_kind(&s, OSRS_CONSUMABLE_SANFEW); + s.player.current_prayer = 40; + s.player.restore_doses = 99; + test_click_inventory_cell_action_s(&s, actions, sanfew_cell); + step_and_observe(&s, &ctx, actions); + CHECK("sanfew consumes exactly one clicked-cell dose", + s.inventory_cells[sanfew_cell].dose == 3); + CHECK("sanfew aggregate is rebuilt from cells", + s.player.restore_doses == sanfew_sum_before - 1); + + memset(actions, 0, sizeof(actions)); + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 512); + int divine_cell = + test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_DIVINE_COMBAT); + int divine_sum_before = + test_sum_inventory_doses_for_kind(&s, OSRS_CONSUMABLE_DIVINE_COMBAT); + s.player.current_hitpoints = 99; + s.player.combat_potion_doses = 99; + test_click_inventory_cell_action_s(&s, actions, divine_cell); + step_and_observe(&s, &ctx, actions); + CHECK("divine combat consumes exactly one clicked-cell dose", + s.inventory_cells[divine_cell].dose == 3); + CHECK("divine combat aggregate is rebuilt from cells", + s.player.combat_potion_doses == divine_sum_before - 1); + CHECK("divine combat drink starts the potion timer", s.player.potion_timer == 3); + + memset(actions, 0, sizeof(actions)); + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 517); + restore_cell = + test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_SUPER_RESTORE); + restore_sum_before = + test_sum_inventory_doses_for_kind(&s, OSRS_CONSUMABLE_SUPER_RESTORE); + s.player.current_prayer = s.player.base_prayer - 1; + test_click_inventory_cell_action_s(&s, actions, restore_cell); + step_and_observe(&s, &ctx, actions); + CHECK("low-missing-prayer restore click still consumes one dose", + s.inventory_cells[restore_cell].dose == 3); + CHECK("low-missing-prayer restore rebuilds aggregate from cells", + s.player.restore_doses == restore_sum_before - 1); +} + +static void test_colosseum_potion_timer_and_same_tick_gate(void) { + printf("test_colosseum_potion_timer_and_same_tick_gate\n"); + ColosseumContext ctx; + ColosseumState s; + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 513); + int brew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_BREW); + s.player.current_hitpoints = 50; + int brew[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, brew, brew_cell); + step_and_observe(&s, &ctx, brew); + int brew_dose_after_first = s.inventory_cells[brew_cell].dose; + int brew_aggregate_after_first = s.player.brew_doses; + s.player.current_hitpoints = 50; + step_and_observe(&s, &ctx, brew); + CHECK("second potion click before timer expiry is blocked", + s.inventory_cells[brew_cell].dose == brew_dose_after_first && + s.player.brew_doses == brew_aggregate_after_first && + s.player.potion_timer == 2); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 514); + int sanfew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_SANFEW); + int divine_cell = + test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_DIVINE_COMBAT); + s.player.current_prayer = 40; + + int drink_one[COLO_NUM_ACTION_HEADS] = {0}; + drink_one[COLO_HEAD_DRINK] = divine_cell + 1; + step_and_observe(&s, &ctx, drink_one); + CHECK("the drink head consumes exactly one potion per tick", + s.inventory_cells[divine_cell].dose == 3 && + s.inventory_cells[sanfew_cell].dose == 4); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 515); + sanfew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_SANFEW); + s.player.current_prayer = 40; + + int drink_again[COLO_NUM_ACTION_HEADS] = {0}; + drink_again[COLO_HEAD_DRINK] = sanfew_cell + 1; + step_and_observe(&s, &ctx, drink_again); + CHECK("first drink consumes one dose", s.inventory_cells[sanfew_cell].dose == 3); + step_and_observe(&s, &ctx, drink_again); + CHECK("second drink before potion timer expiry is blocked", + s.inventory_cells[sanfew_cell].dose == 3); +} + +typedef struct { + const char* label; + OsrsConsumableKind kind; + uint16_t raw4; + uint16_t raw3; + uint16_t raw2; + uint16_t raw1; +} TestDrinkKindDoseChain; + +static const TestDrinkKindDoseChain TEST_DRINK_KIND_DOSE_CHAINS[] = { + {"brew", OSRS_CONSUMABLE_BREW, 6685, 6687, 6689, 6691}, + {"super restore", OSRS_CONSUMABLE_SUPER_RESTORE, 3024, 3026, 3028, 3030}, + {"sanfew", OSRS_CONSUMABLE_SANFEW, 10925, 10927, 10929, 10931}, + {"super combat", OSRS_CONSUMABLE_SUPER_COMBAT, 12695, 12697, 12699, 12701}, + {"divine combat", OSRS_CONSUMABLE_DIVINE_COMBAT, 23685, 23688, 23691, 23694}, + {"ranging", OSRS_CONSUMABLE_RANGING, 2444, 169, 171, 173}, + {"divine ranging", OSRS_CONSUMABLE_DIVINE_RANGING, 23733, 23736, 23739, 23742}, + {"surge", OSRS_CONSUMABLE_SURGE, 30875, 30878, 30881, 30884}, + {"guthix rest", OSRS_CONSUMABLE_GUTHIX_REST, 4417, 4419, 4421, 4423}, + {"anti-venom+", OSRS_CONSUMABLE_ANTIVENOM_PLUS, 12913, 12915, 12917, 12919}, + {"saturated heart", OSRS_CONSUMABLE_SATURATED_HEART, 0, 0, 0, 27641}, +}; + +static void test_colosseum_all_drink_kinds_shared_one_dose_path(void) { + printf("test_colosseum_all_drink_kinds_shared_one_dose_path\n"); + for (int i = 0; i < (int)(sizeof(TEST_DRINK_KIND_DOSE_CHAINS) / + sizeof(TEST_DRINK_KIND_DOSE_CHAINS[0])); i++) { + const TestDrinkKindDoseChain* c = &TEST_DRINK_KIND_DOSE_CHAINS[i]; + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, + (uint32_t)(700 + i)); + int cell = 0; + uint16_t start_raw = c->raw4 ? c->raw4 : c->raw1; + col_init_empty_inventory_cells(&s); + s.inventory_cells[cell] = osrs_inventory_cell_from_raw_osrs_id(start_raw); + col_sync_consumable_counters_from_inventory(&s); + test_prepare_for_drink_kind(&s, c->kind); + + int actions[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, actions, cell); + step_and_observe(&s, &ctx, actions); + uint16_t expected_after_first = c->raw4 ? c->raw3 : 0; + uint8_t expected_dose_after_first = c->raw4 ? 3 : 0; + char label[160]; + snprintf(label, sizeof(label), "%s first click decrements one dose", c->label); + CHECK(label, + s.inventory_cells[cell].raw_osrs_id == expected_after_first && + s.inventory_cells[cell].dose == expected_dose_after_first); + snprintf(label, sizeof(label), "%s first click arms potion timer", c->label); + CHECK(label, s.player.potion_timer == 3); + int aggregate = test_aggregate_doses_for_kind(&s, c->kind); + if (aggregate >= 0) { + snprintf(label, sizeof(label), "%s aggregate rebuilds from clicked cell", + c->label); + CHECK(label, aggregate == test_sum_inventory_doses_for_kind(&s, c->kind)); + } + + if (!c->raw4) { + s.inventory_cells[cell] = osrs_inventory_cell_from_raw_osrs_id(c->raw1); + } + uint16_t raw_before_gate = s.inventory_cells[cell].raw_osrs_id; + uint8_t dose_before_gate = s.inventory_cells[cell].dose; + step_and_observe(&s, &ctx, actions); + snprintf(label, sizeof(label), "%s timer gate blocks next click", c->label); + CHECK(label, + s.inventory_cells[cell].raw_osrs_id == raw_before_gate && + s.inventory_cells[cell].dose == dose_before_gate); + + const uint16_t chain[] = {c->raw4, c->raw3, c->raw2, c->raw1, 0}; + int start = c->raw4 ? 0 : 3; + s.inventory_cells[cell] = osrs_inventory_cell_from_raw_osrs_id(chain[start]); + col_sync_consumable_counters_from_inventory(&s); + for (int step = start; step < 4; step++) { + test_prepare_for_drink_kind(&s, c->kind); + memset(actions, 0, sizeof(actions)); + test_click_inventory_cell_action_s(&s, actions, cell); + step_and_observe(&s, &ctx, actions); + snprintf(label, sizeof(label), "%s chain step %d raw id", c->label, step); + CHECK(label, s.inventory_cells[cell].raw_osrs_id == chain[step + 1]); + uint8_t expected_dose = chain[step + 1] == 0 ? 0 : (uint8_t)(3 - step); + snprintf(label, sizeof(label), "%s chain step %d dose", c->label, step); + CHECK(label, s.inventory_cells[cell].dose == expected_dose); + } + } +} + +static void test_inventory_pure_cut_reconstruction(void) { + printf("test_inventory_pure_cut_reconstruction\n"); + ColosseumContext ctx; + ColosseumState s; + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 900); + test_check_inventory_cut_equivalence_state(&s, &ctx, "speedrun loadout"); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 901); + test_check_inventory_cut_equivalence_state(&s, &ctx, "beginner loadout"); + + for (int i = 0; i < (int)(sizeof(TEST_DRINK_KIND_DOSE_CHAINS) / + sizeof(TEST_DRINK_KIND_DOSE_CHAINS[0])); i++) { + const TestDrinkKindDoseChain* c = &TEST_DRINK_KIND_DOSE_CHAINS[i]; + const uint16_t chain[] = {c->raw4, c->raw3, c->raw2, c->raw1}; + for (int dose = 0; dose < 4; dose++) { + if (chain[dose] == 0) continue; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, + (uint32_t)(920 + i * 4 + dose)); + col_init_empty_inventory_cells(&s); + s.inventory_cells[0] = osrs_inventory_cell_from_raw_osrs_id(chain[dose]); + col_sync_consumable_counters_from_inventory(&s); + test_prepare_for_drink_kind(&s, c->kind); + char label[160]; + snprintf(label, sizeof(label), "%s dose chain raw %u", + c->label, (unsigned)chain[dose]); + test_check_inventory_cut_equivalence_state(&s, &ctx, label); + } + } + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 902); + int bow_cell = test_find_inventory_cell_with_item(&s, ITEM_TWISTED_BOW); + assert(bow_cell >= 0); + int bow_actions[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, bow_actions, bow_cell); + step_and_observe(&s, &ctx, bow_actions); + CHECK("inventory pure-cut gear swap equipped tbow", + s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_TWISTED_BOW); + test_check_inventory_cut_equivalence_state(&s, &ctx, "gear swap after tbow click"); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 903); + int bowfa_cell = test_find_inventory_cell_with_item(&s, ITEM_BOW_OF_FAERDHINEN); + assert(bowfa_cell >= 0); + CHECK("full inventory two-handed equip is denied", + s.player.equipped[GEAR_SLOT_WEAPON] != ITEM_NONE && + s.player.equipped[GEAR_SLOT_SHIELD] != ITEM_NONE && + !col_inventory_cell_actionable(&s, bowfa_cell)); + test_check_inventory_cut_equivalence_state( + &s, &ctx, "full inventory two-handed equip denial"); + + s.inventory_cells[27] = osrs_inventory_cell_empty(); + int bowfa_actions[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, bowfa_actions, bowfa_cell); + step_and_observe(&s, &ctx, bowfa_actions); + CHECK("two-handed bowfa suppresses shield when a spare cell exists", + s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_BOW_OF_FAERDHINEN && + s.player.equipped[GEAR_SLOT_SHIELD] == ITEM_NONE); + test_check_inventory_cut_equivalence_state( + &s, &ctx, "two-handed shield suppression after bowfa click"); + + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 904); + col_init_empty_inventory_cells(&s); + s.inventory_cells[0] = osrs_inventory_cell_from_raw_osrs_id(6685); + s.inventory_cells[1] = osrs_inventory_cell_from_raw_osrs_id(6685); + col_sync_consumable_counters_from_inventory(&s); + s.player.current_hitpoints = 50; + s.player.potion_timer = 0; + test_check_inventory_cut_equivalence_state(&s, &ctx, "duplicate brew stacks"); +} + +static void test_stage3_t4_click_mask_bits(void) { + printf("test_stage3_t4_click_mask_bits\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 504); + int equipped_scythe_cell = test_find_inventory_cell_with_item(&s, ITEM_SCYTHE_OF_VITUR); + int empty_cell = 27; + s.inventory_cells[empty_cell] = (ColoInvCell){ .item_idx = ITEM_NONE }; + int brew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_BREW); + s.player.current_hitpoints = 50; + float mask[COLO_ACTION_MASK_SIZE]; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + int spec_off = col_action_head_mask_offset(COLO_HEAD_SPEC); + CHECK("T4 worn scythe is not present as a clickable inventory cell", + equipped_scythe_cell < 0); + CHECK("T4 empty cell click bit is 0", + test_click_mask_for_cell_s(&s, mask, empty_cell) == 0.0f); + CHECK("T4 beneficial full-dose brew cell click bit is 1", + test_click_mask_for_cell_s(&s, mask, brew_cell) == 1.0f); + CHECK("T4 spec arm bit is 0 for non-spec equipped weapon", + mask[spec_off + 1] == 0.0f); +} + +static void test_stage3_t4_mask_inventory_heads_flag(void) { + printf("test_stage3_t4_mask_inventory_heads_flag\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_BEGINNER_ONLY, 0.0f, 507); + ctx.config.mask_inventory_heads = 1; + + float mask[COLO_ACTION_MASK_SIZE]; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + int all_inventory_heads_pinned_to_noop = 1; + int inv_heads[COLO_INV_CLICK_HEADS]; + int nh = 0; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) inv_heads[nh++] = COLO_HEAD_EQUIP_SLOT(slot); + inv_heads[nh++] = COLO_HEAD_EAT; + inv_heads[nh++] = COLO_HEAD_DRINK; + for (int i = 0; i < nh; i++) { + int offset = col_action_head_mask_offset(inv_heads[i]); + if (mask[offset] != 1.0f) all_inventory_heads_pinned_to_noop = 0; + for (int action = 1; action < COLO_INV_CLICK_DIM; action++) + if (mask[offset + action] != 0.0f) all_inventory_heads_pinned_to_noop = 0; + } + CHECK("mask_inventory_heads pins every inventory head to noop only", + all_inventory_heads_pinned_to_noop); + CHECK("mask_inventory_heads leaves the action-mask size unchanged", + COLO_ACTION_MASK_SIZE == 452); + + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.wave_ready_delay = 0; + s.player.current_hitpoints = 50; + int brew_cell = test_find_inventory_cell_with_consumable(&s, OSRS_CONSUMABLE_BREW); + int brew_doses_before = s.player.brew_doses; + int actions[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, actions, brew_cell); + step_and_observe(&s, &ctx, actions); + CHECK("mask_inventory_heads makes sampled inventory clicks no-op in dispatch", + s.player.current_hitpoints == 50 && s.player.brew_doses == brew_doses_before); +} + +static void test_stage3_t5_claws_click_spec_fires(void) { + printf("test_stage3_t5_claws_click_spec_fires\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 505); + geo_clear_npcs(&s); + s.modifiers.draft_pending = 0; + s.wave_ready_delay = 0; + s.player.x = 16; + s.player.y = 16; + col_init_npc(&s, 0, COLO_FREMENNIK_BERSERKER, 16, 17); + int actions[COLO_NUM_ACTION_HEADS] = {0}; + test_click_inventory_cell_action_s(&s, actions, test_find_inventory_cell_with_item(&s, ITEM_DRAGON_CLAWS)); + actions[COLO_HEAD_SPEC] = 1; + step_and_observe(&s, &ctx, actions); + CHECK("T5 claws click equips claws", + s.player.equipped[GEAR_SLOT_WEAPON] == ITEM_DRAGON_CLAWS); + CHECK("T5 SPEC arms equipped claws", s.player.spec_armed == 1); + s.player.attack_timer = 0; + col_player_attack_target(&s, 0); + CHECK("T5 claws special fires four splats", + s.npcs[0].pending_hits.count == 4 && s.player.special_energy == 50); +} + +static void test_stage3_t6_obs_mask_fuzz_contract(void) { + printf("test_stage3_t6_obs_mask_fuzz_contract\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_MIXED, 0.5f, 506); + int actions[COLO_NUM_ACTION_HEADS] = {0}; + uint32_t rng = 506; + for (int t = 0; t < 256 && !s.episode_over; t++) { + for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) { + rng = rng * 1664525u + 1013904223u; + actions[h] = (int)((rng >> 16) % (uint32_t)COLO_ACTION_DIMS[h]); + } + step_and_observe(&s, &ctx, actions); + } + CHECK("T6 obs running-index assert reached COLO_NUM_OBS", COLO_NUM_OBS == 3044); + CHECK("T6 mask running-index assert reached 452", COLO_ACTION_MASK_SIZE == 452); +} + +static void test_death_attribution_credits_actual_source(void) { + printf("test_death_attribution_credits_actual_source\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 7); + ColPendingHitObserverContext obs = { &s, &ctx }; + + EncounterPendingHit manticore = {0}; + manticore.source_npc_type = COLO_MANTICORE; + manticore.source_npc_slot = -1; + manticore.attack_style = ATTACK_STYLE_MAGIC; + col_pending_hit_prayer_observer(&obs, &manticore, 10, 0, 0); + CHECK("manticore landing credits the manticore", s.last_hit_by_type == COLO_MANTICORE); + + EncounterPendingHit shockwave = {0}; + shockwave.source_npc_type = COLO_SHOCKWAVE_COLOSSUS; + shockwave.source_npc_slot = -1; + shockwave.attack_style = ATTACK_STYLE_RANGED; + col_pending_hit_prayer_observer(&obs, &shockwave, 12, 0, 0); + CHECK("a non-manticore landing re-credits the actual source", + s.last_hit_by_type == COLO_SHOCKWAVE_COLOSSUS); + + col_pending_hit_prayer_observer(&obs, &manticore, 0, 1, 0); + CHECK("a 0-damage splash does not change attribution", + s.last_hit_by_type == COLO_SHOCKWAVE_COLOSSUS); +} + +static int test_walkable_block_corner(void* ctx, int x, int y) { + (void)ctx; + return !(x == 1 && y == 1); +} + +static int test_walkable_open(void* ctx, int x, int y) { + (void)ctx; (void)x; (void)y; + return 1; +} + +static void test_move_action_no_corner_cut(void) { + printf("test_move_action_no_corner_cut\n"); + Player p; + memset(&p, 0, sizeof(p)); + p.x = 1; p.y = 0; + encounter_move_to_target(&p, -1, 1, test_walkable_block_corner, NULL); + CHECK("a blocked corner is never cut", !(p.x == 0 && p.y == 1)); + CHECK("a blocked-corner diagonal degrades to the open cardinal", p.x == 0 && p.y == 0); + + Player q; + memset(&q, 0, sizeof(q)); + q.x = 1; q.y = 0; + encounter_move_to_target(&q, -1, 1, test_walkable_open, NULL); + CHECK("an unobstructed diagonal still moves diagonally", q.x == 0 && q.y == 1); +} + +static void test_melee_reach_cardinal_vs_diagonal(void) { + printf("test_melee_reach_cardinal_vs_diagonal\n"); + const OsrsLosQuery* open = osrs_los_open_query(); + const int tx = 10, ty = 10; + for (int tsize = 1; tsize <= 3; tsize++) { + + const int corners[4][2] = { + {tx - 1, ty - 1}, {tx + tsize, ty - 1}, + {tx - 1, ty + tsize}, {tx + tsize, ty + tsize}}; + for (int i = 0; i < 4; i++) { + int cx = corners[i][0], cy = corners[i][1]; + CHECK("reach-1 helper rejects a diagonal corner", + encounter_entity_footprint_cardinal_adjacent(cx, cy, 1, tx, ty, tsize) == 0); + CHECK("range-1 gate rejects a diagonal corner", + encounter_player_can_attack(cx, cy, tx, ty, tsize, 1, open) == 0); + CHECK("range-2 (halberd) gate allows a diagonal corner", + encounter_player_can_attack(cx, cy, tx, ty, tsize, 2, open) == 1); + } + + for (int k = 0; k < tsize; k++) { + CHECK("range-1 gate allows a west cardinal-edge tile", + encounter_player_can_attack(tx - 1, ty + k, tx, ty, tsize, 1, open) == 1); + CHECK("range-1 gate allows an east cardinal-edge tile", + encounter_player_can_attack(tx + tsize, ty + k, tx, ty, tsize, 1, open) == 1); + CHECK("range-1 gate allows a south cardinal-edge tile", + encounter_player_can_attack(tx + k, ty - 1, tx, ty, tsize, 1, open) == 1); + CHECK("range-1 gate allows a north cardinal-edge tile", + encounter_player_can_attack(tx + k, ty + tsize, tx, ty, tsize, 1, open) == 1); + } + + CHECK("overlap is never meleeable", + encounter_player_can_attack(tx, ty, tx, ty, tsize, 1, open) == 0); + } +} + +static void test_modifier_draft_forces_pick(void) { + printf("test_modifier_draft_forces_pick\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); + col_modifier_open_draft(&s, 2); + CHECK("a draft is open after col_modifier_open_draft", draft_is_open(&s)); + + static float mask[COLO_ACTION_MASK_SIZE]; + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + int base = col_action_head_mask_offset(COLO_HEAD_MODIFIER_SELECT); + CHECK("no-op masked off while a draft is pending", mask[base] == 0.0f); + int selectable = 0; + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + if (mask[base + 1 + o] == 1.0f) selectable = 1; + CHECK("at least one draft option is selectable (forced pick is possible)", selectable); + + complete_open_draft(&s, &ctx, 0); + CHECK("draft closed after the pick", !s.modifiers.draft_pending); + col_write_mask_ctx((EncounterState*)&s, (EncounterContext*)&ctx, mask); + CHECK("no-op valid again once no draft is pending", mask[base] == 1.0f); +} + +static void test_gear_and_boost_reward_signals(void) { + printf("test_gear_and_boost_reward_signals\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); + advance_to_wave_spawn(&s, &ctx); + int slot = -1; + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (col_npc_is_live_target(&s.npcs[i]) && !col_type_is_hazard_entity(s.npcs[i].type)) { + slot = i; + break; + } + CHECK("a live target exists at reset", slot >= 0); + + s.tick_scratch.player_attacked = 1; + s.player_attack_npc_idx = slot; + + float q_attack = col_attacked_gear_quality_ratio(&s); + CHECK("gear-quality signal fires in [0,1] when attacking", q_attack >= 0.0f && q_attack <= 1.0f); + + const ColoNPC* tnpc = &s.npcs[slot]; + const ColoBestGear (*best)[COLO_NUM_NPC_TYPES] = col_get_best_gear_table(&s); + int argmax_set = 0; + float argmax_dpt = -1.0f; + for (int set = 0; set < COLO_NUM_WEAPON_SETS; set++) + if (best[set][tnpc->type].dpt > argmax_dpt) { + argmax_dpt = best[set][tnpc->type].dpt; + argmax_set = set; + } + memcpy(s.player.equipped, best[argmax_set][tnpc->type].setup, sizeof(s.player.equipped)); + CHECK("oracle's argmax-best kit yields ~max gear quality", + col_attacked_gear_quality_ratio(&s) > 0.99f); + + s.player_attack_style_id = ATTACK_STYLE_RANGED; + s.player.current_ranged = s.player.base_ranged; + CHECK("no boost reward when ranged is at base", + col_attacked_with_offensive_boost(&s) == 0); + s.player.current_ranged = s.player.base_ranged + 5; + CHECK("boost reward when ranged is above base", + col_attacked_with_offensive_boost(&s) == 1); + + s.tick_scratch.player_attacked = 0; + CHECK("gear-quality is the no-attack sentinel", col_attacked_gear_quality_ratio(&s) < 0.0f); + CHECK("boost signal is the no-attack sentinel", col_attacked_with_offensive_boost(&s) < 0); +} + +static int colo_test_cell_of_named_item(const ColosseumState* s, const char* name) { + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) { + uint8_t item = s->inventory_cells[c].item_idx; + if (item == ITEM_NONE) continue; + const Item* meta = get_item(item); + if (meta && strcmp(meta->name, name) == 0) return c; + } + return -1; +} + +static void test_threat_field_obs(void) { + printf("test_threat_field_obs\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); + + s.player.x = 5; s.player.y = 9; + col_rebuild_player_collision_flags(&s); + int manti = col_spawn_npc_at(&s, COLO_MANTICORE, 11, 8); + CHECK("fixture: shooter spawned", manti >= 0); + CHECK("fixture: the pillar blocks LoS to the player's tile", + !col_npc_has_los_to_player(&s, &s.npcs[manti])); + + static float obs[COLO_NUM_OBS]; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + + const int f0 = COLO_OBS_AFTER_SPAWN; + const int f1 = f0 + COLO_THREAT_FIELD_TILES; +#define FIELD_CELL(dx, dy) \ + (((dy) + COLO_THREAT_FIELD_RADIUS) * COLO_THREAT_FIELD_DIM + \ + ((dx) + COLO_THREAT_FIELD_RADIUS)) + + CHECK("pillar shadow: the player's tile reads zero shooters", + obs[f0 + FIELD_CELL(0, 0)] == 0.0f); + CHECK("exposed tile east of the pillar reads the shooter", + obs[f0 + FIELD_CELL(7, 3)] > 0.0f); + CHECK("pillar tile is unstandable", obs[f1 + FIELD_CELL(3, -1)] == 1.0f); + CHECK("NPC body tile is unstandable", obs[f1 + FIELD_CELL(6, -1)] == 1.0f); + CHECK("the player's own tile is standable", obs[f1 + FIELD_CELL(0, 0)] == 0.0f); + CHECK("out-of-arena tile is unstandable", obs[f1 + FIELD_CELL(-8, 0)] == 1.0f); + + s.player.x = 12; s.player.y = 12; + col_rebuild_player_collision_flags(&s); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("center cell reads one shooter after stepping into LoS", + obs[f0 + FIELD_CELL(0, 0)] == 0.25f); + + int jag = col_spawn_npc_at(&s, COLO_JAGUAR_WARRIOR, 16, 12); + CHECK("fixture: melee NPC spawned", jag >= 0); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("jaguar body tile is unstandable", obs[f1 + FIELD_CELL(4, 0)] == 1.0f); + int jag_size = col_npc_effective_size(&s.npcs[jag]); + col_stamp_npc_collision_footprint(&s, s.npcs[jag].x, s.npcs[jag].y, jag_size, 0); + s.npcs[jag].x += 1; + col_stamp_npc_collision_footprint(&s, s.npcs[jag].x, s.npcs[jag].y, jag_size, 1); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("vacated tile frees after the melee body moves (not a stale memo)", + obs[f1 + FIELD_CELL(4, 0)] == 0.0f); + CHECK("the melee body's new tile is unstandable", + obs[f1 + FIELD_CELL(6, 0)] == 1.0f); + + col_deactivate_npc(&s, manti); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("center cell reads zero shooters after the manticore dies", + obs[f0 + FIELD_CELL(0, 0)] == 0.0f); + CHECK("dead manticore's body tile frees in channel 1", + obs[f1 + FIELD_CELL(-1, -4)] == 0.0f); + + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + float served_field[COLO_THREAT_FIELD_OBS_CACHE_FLOATS]; + memcpy(served_field, &obs[f0], sizeof(served_field)); + memset(&s.obs_memos, 0, sizeof(s.obs_memos)); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("memo-served threat field == fresh recompute", + memcmp(served_field, &obs[f0], sizeof(served_field)) == 0); + + ctx.config.threat_field_obs_enabled = 0; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + int all_zero = 1; + for (int k = 0; k < COLO_THREAT_FIELD_OBS_SIZE; k++) + if (obs[f0 + k] != 0.0f) all_zero = 0; + CHECK("disabled threat field leaves the block zeroed", all_zero); +#undef FIELD_CELL +} + +static void test_inventory_obs_memo(void) { + printf("test_inventory_obs_memo\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); + + static float obs[COLO_NUM_OBS]; + int cell_brew = -1; + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) + if (s.inventory_cells[c].item_idx == ITEM_NONE) { cell_brew = c; break; } + CHECK("a gear-free cell exists to hold the brew", cell_brew >= 0); + s.inventory_cells[cell_brew] = osrs_inventory_cell_from_raw_osrs_id(6685); + CHECK("4-dose brew seeded", s.inventory_cells[cell_brew].dose == 4); + + const int cell_base = COLO_OBS_AFTER_PILLARS + + cell_brew * COLO_INVENTORY_CELL_OBS_FEATURES; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("4-dose brew renders the full dose feature", obs[cell_base + 2] == 1.0f); + + s.inventory_cells[cell_brew] = osrs_inventory_cell_from_raw_osrs_id(6687); + CHECK("sip took a dose", s.inventory_cells[cell_brew].dose == 3); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("sip moves the dose feature (dose is in the memo key, not a stale block)", + obs[cell_base + 2] == 0.75f); + + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + float served_block[COLO_INVENTORY_OBS_CACHE_FLOATS]; + memcpy(served_block, &obs[COLO_OBS_AFTER_PILLARS], sizeof(served_block)); + memset(&s.obs_memos, 0, sizeof(s.obs_memos)); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("memo-served inventory block == fresh recompute after the sip", + memcmp(served_block, &obs[COLO_OBS_AFTER_PILLARS], sizeof(served_block)) == 0); +} + +static void test_weapon_choice_obs_rank_and_farm_cap(void) { + printf("test_weapon_choice_obs_rank_and_farm_cap\n"); + ColosseumContext ctx; + ColosseumState s; + loadout_reset(&s, &ctx, COLO_LOADOUT_PROFILE_MODE_SPEEDRUN_ONLY, 0.0f, 71); + + static float obs[COLO_NUM_OBS]; + const int dpt_base = COLO_OBS_AFTER_THRALL_DC; + const int spec_base = dpt_base + COLO_CELL_WEAPON_DPT_OBS_SIZE; + const int wielded_base = spec_base + COLO_CELL_SPEC_OBS_SIZE; + + int cell_tentacle = colo_test_cell_of_named_item(&s, "Abyssal tentacle"); + int cell_claws = colo_test_cell_of_named_item(&s, "Dragon claws"); + int cell_tbow = colo_test_cell_of_named_item(&s, "Twisted bow"); + CHECK("speedrun kit carries tentacle+claws+tbow in cells", + cell_tentacle >= 0 && cell_claws >= 0 && cell_tbow >= 0); + + int manti = col_spawn_npc_at(&s, COLO_MANTICORE, 10, 10); + osrs_interaction_set(&s.interaction, manti); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("3x3: wielded scythe DPT outranks the tentacle cell", + obs[wielded_base] > obs[dpt_base + cell_tentacle]); + CHECK("3x3: claws cell ranks below tentacle cell", + obs[dpt_base + cell_claws] < obs[dpt_base + cell_tentacle]); + CHECK("3x3: wielded scythe is ~the best achievable (ratio ~1)", + obs[wielded_base + 1] > 0.95f); + CHECK("claws cell carries the spec bit", obs[spec_base + cell_claws] == 1.0f); + CHECK("tentacle cell carries no spec bit", obs[spec_base + cell_tentacle] == 0.0f); + + int serpent = col_spawn_npc_at(&s, COLO_SERPENT_SHAMAN, 20, 10); + osrs_interaction_set(&s.interaction, serpent); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("1x1: tbow cell outranks the wielded scythe", + obs[dpt_base + cell_tbow] > obs[wielded_base]); + CHECK("1x1: wielded-vs-best ratio exposes the scythe gap", + obs[wielded_base + 1] < 0.85f); + + osrs_interaction_clear(&s.interaction); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("no target: cell DPT + wielded floats are zero", + obs[dpt_base + cell_tentacle] == 0.0f && obs[wielded_base] == 0.0f && + obs[wielded_base + 1] == 0.0f); + CHECK("no target: spec bits stay up (target-independent)", + obs[spec_base + cell_claws] == 1.0f); + + osrs_interaction_set(&s.interaction, manti); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + float pre_drain_tentacle = obs[dpt_base + cell_tentacle]; + float pre_drain_tbow = obs[dpt_base + cell_tbow]; + float pre_drain_wielded = obs[wielded_base]; + s.npcs[manti].def_drained = COLO_NPC_STATS[COLO_MANTICORE].def_level; + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("def drain raises the melee tentacle cell DPT (not a stale block)", + obs[dpt_base + cell_tentacle] > pre_drain_tentacle); + CHECK("def drain raises the ranged tbow cell DPT (not a stale block)", + obs[dpt_base + cell_tbow] > pre_drain_tbow); + CHECK("def drain raises the wielded DPT (not a stale block)", + obs[wielded_base] > pre_drain_wielded); + + int cell_brew = -1; + for (int c = 0; c < OSRS_INVENTORY_SIZE; c++) + if (s.inventory_cells[c].item_idx == ITEM_NONE) { cell_brew = c; break; } + CHECK("a gear-free cell exists to hold the brew", cell_brew >= 0); + s.inventory_cells[cell_brew] = osrs_inventory_cell_from_raw_osrs_id(6685); + CHECK("4-dose brew seeded", s.inventory_cells[cell_brew].dose == 4); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + s.inventory_cells[cell_brew] = osrs_inventory_cell_from_raw_osrs_id(6687); + CHECK("sip took a dose", s.inventory_cells[cell_brew].dose == 3); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + float served_block[COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS]; + memcpy(served_block, &obs[dpt_base], + sizeof(float) * COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS); + memset(&s.obs_memos, 0, sizeof(s.obs_memos)); + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + CHECK("memo-served weapon-choice block == fresh recompute after the sip", + memcmp(served_block, &obs[dpt_base], + sizeof(float) * COLO_WEAPON_CHOICE_OBS_CACHE_FLOATS) == 0); + s.inventory_cells[cell_brew] = osrs_inventory_cell_empty(); + + col_spawn_reinforcements(&s); + int jaguar = -1; + for (int i = 0; i < COLO_MAX_NPCS; i++) + if (s.npcs[i].active && s.npcs[i].spawned_as_reinforcement) { jaguar = i; break; } + CHECK("reinforcement spawns are tagged", jaguar >= 0); + + ctx.config.damage_reward_coeff = 1.0f; + ctx.config.wave_clear_bonus = 0.0f; + ctx.config.farm_safe_damage_cap = 1; + s.wave = 0; + s.tick_scratch = (ColoTickScratch){0}; + s.tick_scratch.fresh_damage_dealt = 100.0f; + s.tick_scratch.fresh_damage_reinforcement = 30.0f; + float r_capped = col_compute_reward_ctx(&s, &ctx); + CHECK("cap on, wave 1: reinforcement damage pays nothing", + fabsf(r_capped - 70.0f) < 1e-3f); + CHECK("farm damage is logged", fabsf(s.log.farm_damage - 30.0f) < 1e-3f); + + ctx.config.farm_safe_damage_cap = 0; + s.tick_scratch.fresh_damage_dealt = 100.0f; + s.tick_scratch.fresh_damage_reinforcement = 30.0f; + float r_uncapped = col_compute_reward_ctx(&s, &ctx); + CHECK("cap off: full damage pays", fabsf(r_uncapped - 100.0f) < 1e-3f); + + ctx.config.farm_safe_damage_cap = 1; + s.wave = COLO_FARM_CAP_WAVES; + s.tick_scratch.fresh_damage_dealt = 100.0f; + s.tick_scratch.fresh_damage_reinforcement = 30.0f; + float r_late = col_compute_reward_ctx(&s, &ctx); + CHECK("cap on, wave 5+: reinforcements stay full-value at the default window", + fabsf(r_late - 100.0f) < 1e-3f); + + ctx.config.farm_cap_waves = COLO_FARM_CAP_WAVES + 1; + s.tick_scratch.fresh_damage_dealt = 100.0f; + s.tick_scratch.fresh_damage_reinforcement = 30.0f; + float r_widened = col_compute_reward_ctx(&s, &ctx); + CHECK("widened farm_cap_waves caps the same wave-5 reinforcement damage", + fabsf(r_widened - 70.0f) < 1e-3f); + ctx.config.farm_cap_waves = COLO_FARM_CAP_WAVES; +} + +int main(void) { + test_stage3_t1_inventory_ranged_weapon_swap(); + test_stage3_t1_inventory_weapon_slot_last_click_wins(); + test_stage3_t1_human_inventory_primary_click_uses_resolver(); + test_stage3_t1_human_rearrange_swaps_inventory_slots(); + test_stage3_t2_brew_click_decrements_dose(); + test_stage3_t3_one_dose_vial_empties(); + test_colosseum_potion_click_source_of_truth(); + test_colosseum_potion_timer_and_same_tick_gate(); + test_colosseum_all_drink_kinds_shared_one_dose_path(); + test_inventory_pure_cut_reconstruction(); + test_stage3_t4_click_mask_bits(); + test_stage3_t4_mask_inventory_heads_flag(); + test_stage3_t5_claws_click_spec_fires(); + test_stage3_t6_obs_mask_fuzz_contract(); + test_fuzz_obs_mask(); + test_osrs_los_query_contracts(); + test_player_ranged_los_blocked_by_pillar(); + test_player_chase_routes_around_pillar_for_los(); + test_colosseum_npc_movement_player_tile_guards(); + test_zero_actions_hit_timeout(); + test_offpray_attribution_log(); + test_step_loop_draft(); + test_eleven_drafts_per_run(); + test_solarflare_orb(); + test_volatility_explosion(); + test_modifier_hazard_obs_fixes(); + test_death_linger_wave_clear_and_render(); + test_draft_offer_and_select(); + test_draft_upgrade_bias(); + test_mantimayhem_stress(); + test_frailty_hp(); + test_relentless_damage(); + test_relentless_def_level_bypass(); + test_quartet_extra_spawn(); + test_bees_hazard(); + test_totem_lifecycle(); + test_totemic_sol_wave12(); + test_reentry_sand_tiles(); + test_venom_escalation(); + test_bee_poison_status(); + test_mantimayhem_t3_shuffle(); + test_static_arena_mask(); + test_static_los_and_attack_gate(); + test_spawn_anchor_exclusion(); + test_reinforcement_gates(); + test_outcome_score_uses_fresh_damage(); + test_outcome_score_reinforcement_grows_denominator(); + test_fresh_damage_not_farmable_via_healing(); + test_outcome_score_wave_clear_has_no_double_count(); + test_outcome_score_sol_uses_boss_progress_only(); + test_roster_cap_nine(); + test_wave12_quartet_and_win(); + test_player_walks_through_npc_footprint(); + test_warband_cycle_offsets(); + test_warband_move_skip(); + test_warband_bfs_memo_bit_identity(); + test_warband_melee_distance_gate(); + test_warband_two_tick_stationary_gate(); + test_warband_formation_convergence(); + test_warband_two_tile_speed(); + test_warband_pillar_routefind_vs_shaman_safespot(); + test_red_flag_minotaur_routefind(); + test_minotaur_heal_semantics(); + test_manticore_barrage_period(); + test_manticore_telegraph_during_windup(); + test_prayer_oracle_manticore_orbs(); + test_late_start_entry_state(); + test_manticore_orb_same_tick_flick(); + test_projectile_prayer_locks_at_throw(); + test_npc_melee_instant_unprayable(); + test_player_melee_lands_at_delay_zero(); + test_echo_boots_recoil_reflects_to_attacker(); + test_manticore_shared_wave_cycle(); + test_manticore_stagger_overlap_fidelity(); + test_javelin_skyfall_no_defence_gate(); + test_sol_adjacency_gate_and_kiting(); + test_sol_attack_selection_invariants(); + test_sol_parry_schedule_and_damage(); + test_sol_parry_prayer_punish(); + test_sol_grapple_perfect_parry(); + test_sol_perfect_parry_forces_spec_attack(); + test_sol_shield_safe_rings(); + test_sol_spear_lines(); + test_sol_crystal_lifecycle(); + test_sol_aoe_reaction_window(); + test_sol_laser_react_window(); + test_sol_phase_transition_sand_guarantees(); + test_sol_beams_become_pools(); + test_sol_beam_strike_reaction_window(); + test_sol_enrage_sand_telegraphs(); + test_solarflare_sol_orbit_boxes(); + test_loadout_profiles_and_supplies(); + test_loadout_consumables(); + test_loadout_divine_potions_and_stat_drift(); + test_loadout_sanfew_and_serp_helm(); + test_consumable_overdrink_mask(); + test_loadout_surge_potion(); + test_loadout_spec_weapons(); + test_colosseum_live_inventory_display(); + test_loadout_item_effects(); + test_loadout_offensive_prayers(); + test_npc_magic_defence_rolls_off_magic_level(); + test_total_damage_by_type_captures_typeless(); + test_matchup_dpt_obs_ranking(); + test_primary_head_resolution(); + test_combat_fidelity_contract_sizes(); + test_scythe_multihit_per_size(); + test_venator_bow_bounce_colosseum_integration(); + test_bee_contact_damage_band(); + test_divine_state_obs_presence(); + test_magic_set_max_hit_math(); + test_thrall_regression(); + test_death_charge_regression(); + test_combat_fidelity_snapshot_roundtrip(); + test_step_out_forecast_manticore_armed_pattern(); + test_step_out_forecast_manticore_pair_stagger(); + test_step_out_forecast_warband_window_and_break(); + test_step_out_forecast_ranged_los_candidate_tiles(); + test_step_out_forecast_valid_flags(); + test_step_out_forecast_same_tick_mixed_styles(); + test_render_bridge_combat_visuals_and_loadout(); + test_render_bridge_npc_debug_and_warband_motion(); + test_melee_reach_cardinal_vs_diagonal(); + test_death_attribution_credits_actual_source(); + test_move_action_no_corner_cut(); + test_modifier_draft_forces_pick(); + test_gear_and_boost_reward_signals(); + test_weapon_choice_obs_rank_and_farm_cap(); + test_threat_field_obs(); + test_inventory_obs_memo(); + + return osrs_test_summary(); +} diff --git a/ocean/osrs/tests/test_colosseum_sol_spear_shapes.c b/ocean/osrs/tests/test_colosseum_sol_spear_shapes.c new file mode 100644 index 0000000000..74e7fe8e21 --- /dev/null +++ b/ocean/osrs/tests/test_colosseum_sol_spear_shapes.c @@ -0,0 +1,216 @@ +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +#define WIN 48 +#define BX 100 +#define BY 100 +#define SIZE 5 +#define LINE_LENGTH 7 + +static uint8_t ref[WIN][WIN]; + +static void ref_mark(int x_col, int y_col) { + int x = x_col - BX + WIN / 2; + int y = -y_col - BY + WIN / 2; + if (x < 0 || x >= WIN || y < 0 || y >= WIN) { + fprintf(stderr, "reference stamp out of window (%d, %d)\n", x, y); + assert(0); + } + ref[x][y] = 1; +} + +static void ref_fill_rect(int fx, int fy, int tx, int ty) { + for (int xx = fx; xx < tx; xx++) + for (int yy = ty; yy > fy; yy--) + ref_mark(xx, yy); +} + +typedef enum { + DIR_WEST, DIR_EAST, DIR_NORTH, DIR_SOUTH, + DIR_NORTHEAST, DIR_NORTHWEST, DIR_SOUTHEAST, DIR_SOUTHWEST, +} ColosimDir; + +static const int COLOSIM_DIRS[8][2] = { + { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 }, + { 1, -1 }, { -1, -1 }, { 1, 1 }, { -1, 1 }, +}; + +static void ref_fill_line(int fx, int fy, ColosimDir dir, int length) { + for (int n = 0; n <= length; n++) + ref_mark(fx + COLOSIM_DIRS[dir][0] * n, fy + COLOSIM_DIRS[dir][1] * n); +} + +static void ref_stamp_spear1(ColosimDir dir) { + int lx = BX, ly = -BY; + memset(ref, 0, sizeof(ref)); + ref_fill_rect(lx, ly - SIZE, lx + SIZE, ly); + switch (dir) { + case DIR_WEST: + ref_fill_rect(lx - 1, ly - SIZE, lx, ly); + ref_fill_line(lx - 2, ly - 1, dir, LINE_LENGTH); + ref_fill_line(lx - 2, ly - 3, dir, LINE_LENGTH); + break; + case DIR_EAST: + ref_fill_rect(lx + SIZE, ly - SIZE, lx + SIZE + 1, ly); + ref_fill_line(lx + SIZE + 1, ly - 1, dir, LINE_LENGTH); + ref_fill_line(lx + SIZE + 1, ly - 3, dir, LINE_LENGTH); + break; + case DIR_NORTH: + ref_fill_rect(lx, ly - SIZE - 1, lx + SIZE, ly - SIZE); + ref_fill_line(lx + 1, ly - SIZE - 1, dir, LINE_LENGTH); + ref_fill_line(lx + 3, ly - SIZE - 1, dir, LINE_LENGTH); + break; + case DIR_SOUTH: + ref_fill_rect(lx, ly, lx + SIZE, ly + 1); + ref_fill_line(lx + 1, ly + 2, dir, LINE_LENGTH); + ref_fill_line(lx + 3, ly + 2, dir, LINE_LENGTH); + break; + case DIR_NORTHEAST: + ref_fill_line(lx + SIZE - 1, ly - SIZE, dir, LINE_LENGTH); + ref_fill_line(lx + SIZE, ly - SIZE + 1, dir, LINE_LENGTH); + break; + case DIR_SOUTHEAST: + ref_fill_line(lx + SIZE, ly, dir, LINE_LENGTH); + ref_fill_line(lx + SIZE - 1, ly + 1, dir, LINE_LENGTH); + break; + case DIR_SOUTHWEST: + ref_fill_line(lx - 1, ly, dir, LINE_LENGTH); + ref_fill_line(lx, ly + 1, dir, LINE_LENGTH); + break; + case DIR_NORTHWEST: + ref_fill_line(lx - 1, ly - SIZE + 1, dir, LINE_LENGTH); + ref_fill_line(lx, ly - SIZE, dir, LINE_LENGTH); + break; + } +} + +static void ref_stamp_spear2(ColosimDir dir) { + int lx = BX, ly = -BY; + memset(ref, 0, sizeof(ref)); + ref_fill_rect(lx - 1, ly - SIZE - 1, lx + SIZE + 1, ly + 1); + switch (dir) { + case DIR_WEST: + ref_fill_rect(lx - 1, ly - SIZE, lx, ly); + ref_fill_line(lx - 2, ly, dir, LINE_LENGTH); + ref_fill_line(lx - 2, ly - 2, dir, LINE_LENGTH); + ref_fill_line(lx - 2, ly - 4, dir, LINE_LENGTH); + break; + case DIR_EAST: + ref_fill_line(lx + SIZE + 1, ly, dir, LINE_LENGTH); + ref_fill_line(lx + SIZE + 1, ly - 2, dir, LINE_LENGTH); + ref_fill_line(lx + SIZE + 1, ly - 4, dir, LINE_LENGTH); + break; + case DIR_NORTH: + ref_fill_line(lx, ly - SIZE - 1, dir, LINE_LENGTH); + ref_fill_line(lx + 2, ly - SIZE - 1, dir, LINE_LENGTH); + ref_fill_line(lx + 4, ly - SIZE - 1, dir, LINE_LENGTH); + break; + case DIR_SOUTH: + ref_fill_line(lx, ly + 2, dir, LINE_LENGTH); + ref_fill_line(lx + 2, ly + 2, dir, LINE_LENGTH); + ref_fill_line(lx + 4, ly + 2, dir, LINE_LENGTH); + break; + case DIR_NORTHEAST: + ref_fill_line(lx + SIZE + 1, ly - SIZE - 1, dir, LINE_LENGTH); + ref_fill_line(lx + SIZE - 2, ly - SIZE - 1, dir, LINE_LENGTH); + ref_fill_line(lx + SIZE + 1, ly - SIZE + 2, dir, LINE_LENGTH); + break; + case DIR_SOUTHEAST: + ref_fill_line(lx + SIZE + 1, ly - 1, dir, LINE_LENGTH); + ref_fill_line(lx + SIZE + 1, ly + 2, dir, LINE_LENGTH); + ref_fill_line(lx + SIZE - 2, ly + 2, dir, LINE_LENGTH); + break; + case DIR_SOUTHWEST: + ref_fill_line(lx - 2, ly + 2, dir, LINE_LENGTH); + ref_fill_line(lx - 2, ly - 1, dir, LINE_LENGTH); + ref_fill_line(lx + 1, ly + 2, dir, LINE_LENGTH); + break; + case DIR_NORTHWEST: + ref_fill_line(lx - 2, ly - SIZE + 2, dir, LINE_LENGTH); + ref_fill_line(lx - 2, ly - SIZE - 1, dir, LINE_LENGTH); + ref_fill_line(lx + 1, ly - SIZE - 1, dir, LINE_LENGTH); + break; + } +} + +static const int OUR_DIR[8][2] = { + { -1, 0 }, { 1, 0 }, { 0, 1 }, { 0, -1 }, + { 1, 1 }, { -1, 1 }, { 1, -1 }, { -1, -1 }, +}; + +static const char* DIR_NAME[8] = { + "W", "E", "N", "S", "NE", "NW", "SE", "SW", +}; + +static int compare_shape(int variant, ColosimDir dir) { + SolHereditState sol; + memset(&sol, 0, sizeof(sol)); + sol.aoe_attack = variant == 1 ? COLO_SOL_AOE_SPEAR1 : COLO_SOL_AOE_SPEAR2; + sol.aoe_x = BX; + sol.aoe_y = BY; + sol.aoe_dir_x = OUR_DIR[dir][0]; + sol.aoe_dir_y = OUR_DIR[dir][1]; + + int mismatches = 0; + for (int x = 0; x < WIN; x++) { + for (int y = 0; y < WIN; y++) { + int wx = BX + x - WIN / 2; + int wy = BY + y - WIN / 2; + int got = col_sol_aoe_tile_is_hazard(&sol, wx, wy); + int want = ref[x][y]; + if (got != want) { + if (mismatches < 6) + fprintf(stderr, + "spear%d %s mismatch at rel (%d, %d): sim=%d colosim=%d\n", + variant, DIR_NAME[dir], + wx - BX, wy - BY, got, want); + mismatches++; + } + } + } + return mismatches; +} + +static void test_direction_rule(void) { + struct { int px, py, dx, dy; } cases[] = { + { BX - 3, BY + 2, -1, 0 }, + { BX + 7, BY, 1, 0 }, + { BX + 2, BY + 9, 0, 1 }, + { BX, BY - 1, 0, -1 }, + { BX + 6, BY + 5, 1, 1 }, + { BX - 1, BY + 5, -1, 1 }, + { BX + 5, BY - 2, 1, -1 }, + { BX - 4, BY - 4, -1, -1 }, + { BX + 2, BY + 2, -1, -1 }, + }; + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + int dx = 0, dy = 0; + col_sol_aoe_direction(cases[i].px, cases[i].py, BX, BY, &dx, &dy); + if (dx != cases[i].dx || dy != cases[i].dy) { + fprintf(stderr, + "direction case %zu: player (%d, %d) -> got (%d, %d) want (%d, %d)\n", + i, cases[i].px, cases[i].py, dx, dy, cases[i].dx, cases[i].dy); + assert(0); + } + } +} + +int main(void) { + int total_mismatches = 0; + for (int dir = 0; dir < 8; dir++) { + ref_stamp_spear1((ColosimDir)dir); + total_mismatches += compare_shape(1, (ColosimDir)dir); + ref_stamp_spear2((ColosimDir)dir); + total_mismatches += compare_shape(2, (ColosimDir)dir); + } + if (total_mismatches) { + fprintf(stderr, "%d hazard tile mismatches vs colosim\n", total_mismatches); + return 1; + } + test_direction_rule(); + printf("sol spear shapes: 16/16 direction x variant sets match colosim\n"); + return 0; +} diff --git a/ocean/osrs/tests/test_inferno_attack_styles.c b/ocean/osrs/tests/test_inferno_attack_styles.c new file mode 100644 index 0000000000..7d8d12da4d --- /dev/null +++ b/ocean/osrs/tests/test_inferno_attack_styles.c @@ -0,0 +1,9601 @@ +#include +#include +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_inferno.h" +#include "ocean/osrs/osrs_anim.h" +#include "ocean/osrs/osrs_effects.h" +#include "ocean/osrs/osrs_projectile_orientation.h" +#include "ocean/osrs/osrs_render_motion.h" +#include + +#include "ocean/osrs/tests/osrs_test_check.h" + +#define ASSERT_INT_LE(label, actual, expected) do { \ + tests_run++; \ + if ((actual) <= (expected)) { \ + tests_passed++; \ + } else { \ + tests_failed++; \ + printf(" FAIL: %s - got %d, expected <= %d\n", \ + (label), (actual), (expected)); \ + } \ +} while (0) + +static void assert_child_aborts(const char* label, void (*fn)(void)) { + fflush(NULL); + pid_t pid = fork(); + if (pid == 0) { + fn(); + _exit(0); + } + + int status = 0; + waitpid(pid, &status, 0); + tests_run++; + if (WIFSIGNALED(status) || (WIFEXITED(status) && WEXITSTATUS(status) != 0)) { + tests_passed++; + } else { + tests_failed++; + printf(" FAIL: %s - child returned successfully\n", label); + } +} + +static char* read_source_file(const char* path) { + FILE* fp = fopen(path, "rb"); + if (!fp) return NULL; + if (fseek(fp, 0, SEEK_END) != 0) { + fclose(fp); + return NULL; + } + long len = ftell(fp); + if (len < 0) { + fclose(fp); + return NULL; + } + if (fseek(fp, 0, SEEK_SET) != 0) { + fclose(fp); + return NULL; + } + char* data = (char*)malloc((size_t)len + 1); + if (!data) { + fclose(fp); + return NULL; + } + size_t got = fread(data, 1, (size_t)len, fp); + fclose(fp); + if (got != (size_t)len) { + free(data); + return NULL; + } + data[len] = '\0'; + return data; +} + +static int source_block_contains( + const char* path, + const char* block_start, + const char* block_end, + const char* needle +) { + char* source = read_source_file(path); + if (!source) return 0; + char* start = strstr(source, block_start); + if (!start) { + free(source); + return 0; + } + char* end = strstr(start, block_end); + if (!end) { + free(source); + return 0; + } + int found = 0; + char saved = *end; + *end = '\0'; + found = strstr(start, needle) != NULL; + *end = saved; + free(source); + return found; +} + +static int source_count_token(const char* start, const char* token) { + int count = 0; + size_t token_len = strlen(token); + const char* p = start; + while ((p = strstr(p, token)) != NULL) { + count++; + p += token_len; + } + return count; +} + +static int source_seen_key(char keys[128][96], int key_count, const char* key) { + for (int i = 0; i < key_count; i++) { + if (strcmp(keys[i], key) == 0) return 1; + } + return 0; +} + +static int inferno_my_log_metric_key_count(void) { + char* source = read_source_file("ocean/osrs_inferno/osrs_inferno.h"); + if (!source) return -1; + char* start = strstr(source, "void puf_log"); + if (!start) { + free(source); + return -1; + } + + char keys[128][96] = {{0}}; + int key_count = 0; + const char* prefix = "dict_set(out, \""; + size_t prefix_len = strlen(prefix); + char* p = start; + while ((p = strstr(p, prefix)) != NULL) { + p += prefix_len; + char* end = strchr(p, '"'); + if (!end) break; + size_t len = (size_t)(end - p); + if (len >= sizeof(keys[0])) len = sizeof(keys[0]) - 1; + char key[96] = {0}; + memcpy(key, p, len); + if (!source_seen_key(keys, key_count, key)) { + if (key_count >= 128) { + free(source); + return 10000; + } + memcpy(keys[key_count], key, len + 1); + key_count++; + } + p = end + 1; + } + + int idle_metric_calls = source_count_token(start, "inferno_log_idle_metric("); + free(source); + return key_count + + idle_metric_calls * (1 + OSRS_INFERNO_IDLE_PHASE_COUNT); +} + +#define ASSERT_SOURCE_BLOCK_CONTAINS(label, path, block_start, block_end, needle) do { \ + tests_run++; \ + if (source_block_contains((path), (block_start), (block_end), (needle))) { \ + tests_passed++; \ + } else { \ + tests_failed++; \ + printf(" FAIL: %s - missing \"%s\" in %s\n", \ + (label), (needle), (path)); \ + } \ +} while (0) + +#define ASSERT_SOURCE_BLOCK_NOT_CONTAINS(label, path, block_start, block_end, needle) do { \ + tests_run++; \ + if (!source_block_contains((path), (block_start), (block_end), (needle))) { \ + tests_passed++; \ + } else { \ + tests_failed++; \ + printf(" FAIL: %s - unexpected \"%s\" in %s\n", \ + (label), (needle), (path)); \ + } \ +} while (0) + +#define ASSERT_SOURCE_NOT_CONTAINS(label, path, needle) do { \ + char* source = read_source_file((path)); \ + tests_run++; \ + if (source && strstr(source, (needle)) == NULL) { \ + tests_passed++; \ + } else { \ + tests_failed++; \ + printf(" FAIL: %s - unexpected \"%s\" in %s\n", \ + (label), (needle), (path)); \ + } \ + free(source); \ +} while (0) + +static int inferno_source_bundle_contains(const char* needle) { + static const char* paths[] = { + "ocean/osrs/encounters/encounter_inferno.h", + "ocean/osrs/encounters/inferno/encounter_inferno_model.inc", + "ocean/osrs/encounters/inferno/encounter_inferno_helpers.inc", + "ocean/osrs/encounters/inferno/encounter_inferno_reset_spawn.inc", + "ocean/osrs/encounters/inferno/encounter_inferno_movement.inc", + "ocean/osrs/encounters/inferno/encounter_inferno_combat.inc", + "ocean/osrs/encounters/inferno/encounter_inferno_player_actions.inc", + "ocean/osrs/encounters/inferno/encounter_inferno_reward_step.inc", + "ocean/osrs/encounters/inferno/encounter_inferno_forecast.inc", + "ocean/osrs/encounters/inferno/encounter_inferno_lab.inc", + "ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc", + "ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc", + }; + + for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) { + char* source = read_source_file(paths[i]); + if (!source) return 1; + int found = strstr(source, needle) != NULL; + free(source); + if (found) return 1; + } + return 0; +} + +#define ASSERT_INFERNO_SOURCE_NOT_CONTAINS(label, needle) do { \ + tests_run++; \ + if (!inferno_source_bundle_contains((needle))) { \ + tests_passed++; \ + } else { \ + tests_failed++; \ + printf(" FAIL: %s - unexpected \"%s\" in Inferno source bundle\n", \ + (label), (needle)); \ + } \ +} while (0) + +#define ASSERT_FLOAT_GT(label, actual, threshold) do { \ + tests_run++; \ + if ((actual) > (threshold)) { \ + tests_passed++; \ + } else { \ + tests_failed++; \ + printf(" FAIL: %s - got %.6f, expected > %.6f\n", \ + (label), (float)(actual), (float)(threshold)); \ + } \ +} while (0) + +#define ASSERT_STR_EQ(label, actual, expected) do { \ + tests_run++; \ + if (strcmp((actual), (expected)) == 0) { \ + tests_passed++; \ + } else { \ + tests_failed++; \ + printf(" FAIL: %s - got \"%s\", expected \"%s\"\n", \ + (label), (actual), (expected)); \ + } \ +} while (0) + +static void reset_test_context(void) { + InfernoContext* ctx = inf_legacy_context(); + ctx->config = inf_default_config(); + ctx->collision_map = NULL; + ctx->world_offset_x = 0; + ctx->world_offset_y = 0; + ctx->human_commands = NULL; + ctx->human_command_count = 0; + ctx->human_command_mode = 0; +} + +static InfernoState make_test_state(int player_x, int player_y) { + reset_test_context(); + InfernoState state; + memset(&state, 0, sizeof(state)); + memset(state.npc_los_cache, -1, sizeof(state.npc_los_cache)); + state.player.x = player_x; + state.player.y = player_y; + state.player_last_interaction_target_slot = -1; + state.player_last_interaction_age = 1; + return state; +} + +static InfConfig* test_config(void) { + return &inf_legacy_context()->config; +} + +static float test_supply_milestone_surplus_reward( + InfernoState* state, + int public_wave +) { + return inf_supply_milestone_surplus_reward( + state, inf_legacy_context(), public_wave); +} + +static InfNPC make_test_npc(InfNPCType type, int x, int y, int size) { + InfNPC npc; + memset(&npc, 0, sizeof(npc)); + npc.type = type; + npc.x = x; + npc.y = y; + npc.size = size; + npc.aggro_target = -1; + npc.attack_visual_target = -1; + npc.resurrection_visual_target = -1; + npc.attack_style = INF_NPC_STATS[type].default_style; + npc.blob_scanned_prayer = -1; + inf_npc_init_type_state(&npc); + return npc; +} + +static InfNPCStats make_test_stats(int default_style) { + InfNPCStats stats; + memset(&stats, 0, sizeof(stats)); + stats.default_style = default_style; + stats.can_melee = 1; + return stats; +} + +static HumanInput make_human_input(void) { + HumanInput input; + memset(&input, 0, sizeof(input)); + input.pending_move_x = -1; + input.pending_move_y = -1; + input.pending_prayer = -1; + input.pending_offensive_prayer = -1; + input.pending_target_idx = -1; + return input; +} + +static void init_spell_cast_test_state(InfernoState* state, InfNPCType target_type) { + *state = make_test_state(10, 10); + state->rng_state = 1; + state->weapon_set = INF_GEAR_MAGE; + state->player.entity_type = ENTITY_PLAYER; + state->player.base_hitpoints = 99; + state->player.current_hitpoints = 99; + state->player.base_attack = 99; + state->player.base_strength = 99; + state->player.base_defence = 99; + state->player.base_ranged = 99; + state->player.base_magic = 99; + state->player.current_attack = 99; + state->player.current_strength = 99; + state->player.current_defence = 99; + state->player.current_ranged = 99; + state->player.current_magic = 99; + state->player.autocast_enabled = 1; + state->player.autocast_defensive = 0; + state->player.autocast_spell = ENCOUNTER_SPELL_BLOOD; + state->player_dest_x = -1; + state->player_dest_y = -1; + osrs_interaction_init(&state->interaction); + encounter_apply_loadout(&state->player, INF_MAX_MAGE_LOADOUT, GEAR_MAGE); + encounter_compute_loadout_stats(INF_MAX_MAGE_LOADOUT, ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, 30, + &state->loadout_stats[INF_GEAR_MAGE]); + encounter_compute_loadout_stats(INF_MAX_RANGE_LONG_LOADOUT, ATTACK_STYLE_RANGED, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, + &state->loadout_stats[INF_GEAR_LONG_RANGE]); + encounter_compute_loadout_stats(INF_MAX_RANGE_FAST_LOADOUT, ATTACK_STYLE_RANGED, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, + &state->loadout_stats[INF_GEAR_BP]); + + state->npcs[0] = make_test_npc( + target_type, 16, 10, INF_NPC_STATS[target_type].size); + state->npcs[0].active = 1; + state->npcs[0].hp = state->npcs[0].max_hp = INF_NPC_STATS[target_type].hp; + inf_refresh_current_obs_slots(state); +} + +static int inf_action_target_for_npc(InfernoState* state, int npc_slot) { + inf_refresh_current_obs_slots(state); + int target_slot = inf_find_target_obs_slot(state, npc_slot); + ASSERT_INT_EQ("target NPC has observation slot", target_slot >= 0, 1); + if (target_slot < 0) return 0; + return target_slot + 1; +} + +static int inferno_action_head_mask_offset(int head) { + int offset = 0; + for (int h = 0; h < head; h++) + offset += INF_ACTION_DIMS[h]; + return offset; +} + +static void fire_player_action_at_slot_zero( + InfernoState* state, + int spell_action +) { + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_TARGET] = inf_action_target_for_npc(state, 0); + actions[INF_HEAD_SPELL] = spell_action; + state->player.attack_timer = 0; + encounter_pending_hit_queue_clear(&state->npcs[0].pending_hits); + inf_tick_player(state, actions, 1); +} + +static int inferno_pending_hit_obs_start(void); +static int inferno_pillar_obs_start(int pillar_idx); +static int inferno_obs_slot_dig_index(int slot_idx); +static void init_zuk_timing_state(InfernoState* state); + +enum { + INF_OBS_WAVE_NORM = 14, + INF_OBS_WAVE_PHASE_START = 15, + INF_OBS_ZUK_ATTACK_TIMER = 21, + INF_OBS_PRAYER_TIMER = 42, + INF_OBS_PRAYER_MELEE = 43, + INF_OBS_PRAYER_RANGED = 44, + INF_OBS_PRAYER_MAGIC = 45, + INF_OBS_PRAYER_CONFLICT = 46, + INF_OBS_PRESSURE_START = 47, + INF_OBS_PRESSURE_IF_READY_TOTAL = 47, + INF_OBS_PRESSURE_IF_READY_MELEE = 48, + INF_OBS_PRESSURE_IF_READY_RANGED = 49, + INF_OBS_PRESSURE_IF_READY_MAGIC = 50, + INF_OBS_PRESSURE_IF_READY_MAX_HIT = 51, + INF_OBS_PRESSURE_IF_READY_SUM_MAX_HIT = 52, + INF_OBS_PRESSURE_THIS_TICK_TOTAL = 53, + INF_OBS_PRESSURE_THIS_TICK_MELEE = 54, + INF_OBS_PRESSURE_THIS_TICK_RANGED = 55, + INF_OBS_PRESSURE_THIS_TICK_MAGIC = 56, + INF_OBS_PRESSURE_THIS_TICK_MAX_HIT = 57, + INF_OBS_PRESSURE_THIS_TICK_SUM_MAX_HIT = 58, + INF_OBS_PRESSURE_TIMER_LTE_1 = 59, + INF_OBS_PRESSURE_TIMER_LTE_2 = 60, + INF_OBS_PRESSURE_TIMER_LTE_4 = 61, + INF_OBS_PRESSURE_IF_READY_STYLE_COUNT = 62, + INF_OBS_PRESSURE_THIS_TICK_STYLE_COUNT = 63, + INF_OBS_PRESSURE_NEAREST_TIMER = 64, + INF_OBS_ZUK_PHASE_START = 65, + INF_OBS_ZUK_SHIELD_DIR = 66, + INF_OBS_ZUK_SHIELD_FREEZE = 67, +}; + +static void init_jad_timing_test_state(InfernoState* state, int player_x, int player_y, int jad_x, int jad_y) { + reset_test_context(); + memset(state, 0, sizeof(*state)); + memset(state->npc_los_cache, -1, sizeof(state->npc_los_cache)); + state->rng_state = 12345; + state->wave = 66; + state->player.entity_type = ENTITY_PLAYER; + state->player.x = player_x; + state->player.y = player_y; + state->player.base_hitpoints = 99; + state->player.current_hitpoints = 99; + state->player.base_prayer = 99; + state->player.current_prayer = 99; + state->player.base_attack = 99; + state->player.base_strength = 99; + state->player.base_defence = 99; + state->player.base_ranged = 99; + state->player.base_magic = 99; + state->player.current_attack = 99; + state->player.current_strength = 99; + state->player.current_defence = 99; + state->player.current_ranged = 99; + state->player.current_magic = 99; + state->player.prayer = PRAYER_NONE; + state->weapon_set = INF_GEAR_MAGE; + state->player_last_interaction_target_slot = -1; + state->player_last_interaction_age = 1; + state->player_dest_x = -1; + state->player_dest_y = -1; + osrs_interaction_init(&state->interaction); + encounter_compute_loadout_stats(INF_MAX_MAGE_LOADOUT, ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, 30, + &state->loadout_stats[INF_GEAR_MAGE]); + + state->npcs[0] = make_test_npc( + INF_NPC_JAD, jad_x, jad_y, INF_NPC_STATS[INF_NPC_JAD].size); + state->npcs[0].active = 1; + state->npcs[0].attack_timer = 0; + inf_npc_jad(&state->npcs[0])->attack_style = ATTACK_STYLE_MAGIC; + state->npcs[0].attack_style = ATTACK_STYLE_RANGED; +} + +static void step_inferno_with_prayer(InfernoState* state, int prayer_action) { + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_PRAYER] = prayer_action; + inf_step((EncounterState*)state, actions); +} + +static void step_inferno_noop(InfernoState* state) { + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + inf_step((EncounterState*)state, actions); +} + +static int find_active_npc_type(const InfernoState* state, InfNPCType type) { + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (state->npcs[i].active && state->npcs[i].type == type) + return i; + } + return -1; +} + +static int count_active_npcs(const InfernoState* state) { + int count = 0; + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (state->npcs[i].active) count++; + } + return count; +} + +static int count_active_npc_type(const InfernoState* state, InfNPCType type) { + int count = 0; + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (state->npcs[i].active && state->npcs[i].type == type) + count++; + } + return count; +} + +static int find_active_npc_type_at( + const InfernoState* state, InfNPCType type, int x, int y +) { + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (state->npcs[i].active && state->npcs[i].type == type && + state->npcs[i].x == x && state->npcs[i].y == y) + return i; + } + return -1; +} + +static int force_mager_resurrect(InfernoState* s, int idx) { + for (uint32_t seed = 1; seed < 100000; seed++) { + InfernoState probe = *s; + probe.rng_state = seed; + if (inf_mager_resurrect(&probe, idx)) { + s->rng_state = seed; + return inf_mager_resurrect(s, idx); + } + } + return 0; +} + +static int force_mager_attack_resurrection(InfernoState* s, int idx) { + for (uint32_t seed = 1; seed < 100000; seed++) { + InfernoState probe = *s; + probe.rng_state = seed; + inf_npc_attack(&probe, idx); + if (probe.dead_mob_count == 0 && + count_active_npc_type(&probe, INF_NPC_RANGER) > 0) { + s->rng_state = seed; + inf_npc_attack(s, idx); + return 1; + } + } + return 0; +} + +static int distance_to_player(const InfernoState* state, const InfNPC* npc) { + return encounter_dist_to_npc( + state->player.x, state->player.y, npc->x, npc->y, npc->size); +} + +static int test_profiled_supply_count(int full_doses, float profile_fraction, float scale) { + float effective_fraction = 1.0f - scale * (1.0f - profile_fraction); + int doses = (int)((float)full_doses * effective_fraction + 0.5f); + if (doses < 0) doses = 0; + if (doses > full_doses) doses = full_doses; + return doses; +} + +static void reset_inferno_at_public_wave(EncounterState* raw_state, + int public_wave, + float supply_profile_scale) { + inf_put_int(raw_state, "start_wave", public_wave); + inf_put_float(raw_state, "damage_reward_coeff", 0.01f); + inf_put_float(raw_state, "shield_penalty_coeff", 0.01f); + inf_put_float(raw_state, "tag_reward_coeff", 0.25f); + inf_put_float(raw_state, "late_start_supply_profile_scale", supply_profile_scale); + inf_reset(raw_state, 123); +} + +static void assert_supply_doses(const char* label, + const Player* player, + InfSupplyDoses expected) { + char buf[128]; + snprintf(buf, sizeof(buf), "%s brew doses", label); + ASSERT_INT_EQ(buf, player->brew_doses, expected.brew_doses); + snprintf(buf, sizeof(buf), "%s restore doses", label); + ASSERT_INT_EQ(buf, player->restore_doses, expected.restore_doses); + snprintf(buf, sizeof(buf), "%s bastion doses", label); + ASSERT_INT_EQ(buf, player->bastion_doses, expected.bastion_doses); + snprintf(buf, sizeof(buf), "%s stamina doses", label); + ASSERT_INT_EQ(buf, player->stamina_doses, expected.stamina_doses); +} + +static void test_final_wave_reward_applies_healer_tags_and_heal_cost(void) { + printf("--- final-wave reward applies healer tags and heal cost ---\n"); + + InfernoState healing_state = make_test_state(24, 24); + InfernoState damage_state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&healing_state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&healing_state, "shield_penalty_coeff", 0.01f); + inf_put_float((EncounterState*)&healing_state, "tag_reward_coeff", 0.25f); + healing_state.wave = INF_NUM_WAVES - 1; + healing_state.tick_scratch.damage_dealt = 50.0f; + healing_state.tick_scratch.hp_restored = 10.0f; + healing_state.tick_scratch.shield_damage = 7.0f; + healing_state.tick_scratch.healer_tags = 2; + healing_state.npcs[0] = make_test_npc(INF_NPC_HEALER_ZUK, 26, 24, 1); + healing_state.npcs[0].active = 1; + healing_state.npcs[0].aggro_target = 1; + healing_state.npcs[1] = make_test_npc(INF_NPC_ZUK, 28, 24, 5); + healing_state.npcs[1].active = 1; + healing_state.npcs[1].hp = 1150; + healing_state.min_zuk_hp_seen = 1200.0f; + + damage_state = healing_state; + damage_state.wave = 0; + damage_state.npcs[0].aggro_target = -1; + damage_state.tick_scratch.healer_tags = 0; + + ASSERT_FLOAT_NEAR("active healer reward includes tags, heal cost, and shield penalty", + inf_compute_reward(&healing_state), 0.33f, 0.0001f); + ASSERT_FLOAT_NEAR("active healer reward updates zuk low watermark", + healing_state.min_zuk_hp_seen, 1150.0f, 0.0001f); + ASSERT_FLOAT_NEAR("non-final-wave reward still uses damage path", + inf_compute_reward(&damage_state), 0.33f, 0.0001f); +} + +static void test_final_wave_reward_uses_zuk_low_watermark_progress(void) { + printf("--- final-wave reward uses zuk low-watermark progress ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "shield_penalty_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.25f); + state.wave = INF_NUM_WAVES - 1; + state.min_zuk_hp_seen = 1200.0f; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 1150; + state.npcs[0].max_hp = 1200; + state.npcs[1] = make_test_npc(INF_NPC_JAD, 24, 32, 5); + state.npcs[1].active = 1; + + state.tick_scratch.damage_dealt = 250.0f; + state.tick_scratch.hp_restored = 100.0f; + state.tick_scratch.shield_damage = 7.0f; + ASSERT_FLOAT_NEAR("first zuk low watermark pays progress minus shield penalty", + inf_compute_reward(&state), 0.43f, 0.0001f); + ASSERT_FLOAT_NEAR("first zuk low watermark updates state", + state.min_zuk_hp_seen, 1150.0f, 0.0001f); + + state.tick_scratch.damage_dealt = 400.0f; + state.tick_scratch.hp_restored = 0.0f; + state.tick_scratch.shield_damage = 0.0f; + ASSERT_FLOAT_NEAR("repeated hits at same zuk hp give zero reward", + inf_compute_reward(&state), 0.0f, 0.0001f); + ASSERT_FLOAT_NEAR("same-hp hits keep low watermark", + state.min_zuk_hp_seen, 1150.0f, 0.0001f); + + state.tick_scratch.damage_dealt = 600.0f; + state.npcs[0].hp = 1180; + ASSERT_FLOAT_NEAR("healed zuk above low watermark gives zero reward", + inf_compute_reward(&state), 0.0f, 0.0001f); + ASSERT_FLOAT_NEAR("healed zuk does not revoke low watermark", + state.min_zuk_hp_seen, 1150.0f, 0.0001f); + + state.tick_scratch.damage_dealt = 900.0f; + ASSERT_FLOAT_NEAR("non-zuk damage without new low watermark gives zero reward", + inf_compute_reward(&state), 0.0f, 0.0001f); + ASSERT_FLOAT_NEAR("non-zuk damage leaves low watermark unchanged", + state.min_zuk_hp_seen, 1150.0f, 0.0001f); + + state.npcs[0].hp = 1140; + state.tick_scratch.damage_dealt = 50.0f; + ASSERT_FLOAT_NEAR("new lower zuk hp pays only incremental progress", + inf_compute_reward(&state), 0.10f, 0.0001f); + ASSERT_FLOAT_NEAR("new lower zuk hp refreshes low watermark", + state.min_zuk_hp_seen, 1140.0f, 0.0001f); +} + +static void test_final_wave_reward_blocks_zuk_damage_while_healers_heal(void) { + printf("--- final-wave reward blocks zuk damage while healers heal ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "shield_penalty_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.25f); + state.wave = INF_NUM_WAVES - 1; + state.min_zuk_hp_seen = 240.0f; + state.zuk.healer_spawned = 1; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 220; + state.npcs[0].max_hp = 1200; + state.npcs[1] = make_test_npc(INF_NPC_HEALER_ZUK, 20, 34, 1); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = + INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[1].aggro_target = 0; + + state.min_zuk_hp_seen = 245.0f; + state.npcs[0].hp = 235; + ASSERT_FLOAT_NEAR("zuk threshold-crossing hit pays once after healer spawn", + inf_compute_reward(&state), 0.10f, 0.0001f); + ASSERT_FLOAT_NEAR("threshold-crossing hit updates low watermark", + state.min_zuk_hp_seen, 235.0f, 0.0001f); + + state.min_zuk_hp_seen = 240.0f; + state.npcs[0].hp = 220; + + ASSERT_FLOAT_NEAR("zuk progress pays nothing while a healer heals zuk", + inf_compute_reward(&state), 0.0f, 0.0001f); + ASSERT_FLOAT_NEAR("low watermark still tracks observed zuk hp", + state.min_zuk_hp_seen, 220.0f, 0.0001f); + + state.min_zuk_hp_seen = 240.0f; + state.npcs[1].aggro_target = -1; + + ASSERT_FLOAT_NEAR("zuk progress resumes after healers are tagged", + inf_compute_reward(&state), 0.20f, 0.0001f); +} + +static void test_final_wave_reward_pays_zuk_healer_damage(void) { + printf("--- final-wave reward pays zuk healer damage ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "shield_penalty_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.25f); + state.wave = INF_NUM_WAVES - 1; + state.min_zuk_hp_seen = 240.0f; + state.zuk.healer_spawned = 1; + state.tick_scratch.damage_dealt = 31.0f; + state.tick_scratch.damage_zuk_healers = 31.0f; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 240; + state.npcs[0].max_hp = 1200; + state.npcs[1] = make_test_npc(INF_NPC_HEALER_ZUK, 20, 34, 1); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = + INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[1].aggro_target = -1; + + ASSERT_FLOAT_NEAR("zuk healer damage uses base damage reward", + inf_compute_reward(&state), 0.31f, 0.0001f); +} + +static void test_post_healer_zuk_damage_reward_is_after_clear_only(void) { + printf("--- post-healer Zuk damage reward is after clear only ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "post_healer_zuk_damage_coeff", 0.005f); + state.wave = INF_NUM_WAVES - 1; + state.min_zuk_hp_seen = 180.0f; + state.zuk.healer_spawned = 1; + state.tick_at_all_zuk_healers_dead = -1; + state.tick_scratch.damage_dealt = 50.0f; + state.tick_scratch.damage_zuk = 50.0f; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 220; + state.npcs[0].max_hp = 1200; + + ASSERT_FLOAT_NEAR("post-healer coeff does not pay before healer clear", + inf_compute_reward(&state), 0.0f, 0.0001f); + + state.tick_at_all_zuk_healers_dead = 321; + state.min_zuk_hp_seen = 180.0f; + ASSERT_FLOAT_NEAR("post-healer coeff pays healed-back Zuk damage after clear", + inf_compute_reward(&state), 0.25f, 0.0001f); + ASSERT_FLOAT_NEAR("post-healer damage reward does not revoke low watermark", + state.min_zuk_hp_seen, 180.0f, 0.0001f); +} + +static void test_zuk_healer_phase_hp_delta_default_preserves_low_watermark(void) { + printf("--- Zuk healer phase HP delta reward defaults off ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + state.wave = INF_NUM_WAVES - 1; + state.zuk.healer_spawned = 1; + state.min_zuk_hp_seen = 180.0f; + state.tick_scratch.damage_zuk = 50.0f; + state.tick_scratch.damage_dealt = 50.0f; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 220; + state.npcs[0].max_hp = 1200; + + ASSERT_FLOAT_NEAR("default coefficient preserves old no-pay healed-back Zuk hit", + inf_compute_reward(&state), 0.0f, 0.0001f); + ASSERT_FLOAT_NEAR("default coefficient preserves low watermark", + state.min_zuk_hp_seen, 180.0f, 0.0001f); +} + +static void test_zuk_healer_phase_hp_delta_pays_healed_back_zuk_damage(void) { + printf("--- Zuk healer phase HP delta pays healed-back Zuk damage ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "zuk_healer_phase_hp_delta_coeff", 0.005f); + state.wave = INF_NUM_WAVES - 1; + state.zuk.healer_spawned = 1; + state.min_zuk_hp_seen = 180.0f; + state.tick_scratch.damage_zuk = 50.0f; + state.tick_scratch.damage_dealt = 50.0f; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 220; + state.npcs[0].max_hp = 1200; + + ASSERT_FLOAT_NEAR("healer-phase HP delta pays current Zuk damage", + inf_compute_reward(&state), 0.25f, 0.0001f); + ASSERT_FLOAT_NEAR("healed-back Zuk hit does not change low watermark", + state.min_zuk_hp_seen, 180.0f, 0.0001f); +} + +static void test_zuk_healer_phase_hp_delta_avoids_double_pay_below_low_watermark(void) { + printf("--- Zuk healer phase HP delta avoids double pay below low watermark ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "zuk_healer_phase_hp_delta_coeff", 0.005f); + state.wave = INF_NUM_WAVES - 1; + state.zuk.healer_spawned = 1; + state.min_zuk_hp_seen = 245.0f; + state.tick_scratch.damage_zuk = 10.0f; + state.tick_scratch.damage_dealt = 10.0f; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 235; + state.npcs[0].max_hp = 1200; + + ASSERT_FLOAT_NEAR("post-spawn low-watermark damage is paid once through HP delta", + inf_compute_reward(&state), 0.05f, 0.0001f); + ASSERT_FLOAT_NEAR("low watermark still tracks metrics", + state.min_zuk_hp_seen, 235.0f, 0.0001f); +} + +static void test_zuk_healer_phase_hp_delta_penalizes_zuk_healing_once(void) { + printf("--- Zuk healer phase HP delta penalizes Zuk healing once ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "zuk_healer_phase_hp_delta_coeff", 0.005f); + state.wave = INF_NUM_WAVES - 1; + state.zuk.healer_spawned = 1; + state.min_zuk_hp_seen = 180.0f; + state.tick_scratch.hp_restored = 40.0f; + state.tick_scratch.hp_restored_zuk = 40.0f; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 220; + state.npcs[0].max_hp = 1200; + state.npcs[1] = make_test_npc(INF_NPC_HEALER_ZUK, 20, 34, 1); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = + INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[1].aggro_target = 0; + + ASSERT_FLOAT_NEAR("Zuk heal penalty is not double-counted", + inf_compute_reward(&state), -0.20f, 0.0001f); +} + +static void test_zuk_healer_phase_hp_delta_pays_net_same_tick_delta(void) { + printf("--- Zuk healer phase HP delta pays net same-tick delta ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "zuk_healer_phase_hp_delta_coeff", 0.005f); + state.wave = INF_NUM_WAVES - 1; + state.zuk.healer_spawned = 1; + state.min_zuk_hp_seen = 180.0f; + state.tick_scratch.damage_zuk = 50.0f; + state.tick_scratch.damage_dealt = 50.0f; + state.tick_scratch.hp_restored = 20.0f; + state.tick_scratch.hp_restored_zuk = 20.0f; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 220; + state.npcs[0].max_hp = 1200; + state.npcs[1] = make_test_npc(INF_NPC_HEALER_ZUK, 20, 34, 1); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = + INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[1].aggro_target = 0; + + ASSERT_FLOAT_NEAR("same tick damage and healing use net Zuk HP delta", + inf_compute_reward(&state), 0.15f, 0.0001f); +} + +static void test_zuk_healer_phase_hp_delta_keeps_non_zuk_heal_cost(void) { + printf("--- Zuk healer phase HP delta keeps non-Zuk heal cost ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "zuk_healer_phase_hp_delta_coeff", 0.005f); + state.wave = INF_NUM_WAVES - 1; + state.zuk.healer_spawned = 1; + state.min_zuk_hp_seen = 180.0f; + state.tick_scratch.hp_restored = 30.0f; + state.tick_scratch.hp_restored_zuk = 0.0f; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 220; + state.npcs[0].max_hp = 1200; + state.npcs[1] = make_test_npc(INF_NPC_JAD, 24, 32, 5); + state.npcs[1].active = 1; + state.npcs[1].hp = 200; + state.npcs[1].max_hp = INF_NPC_STATS[INF_NPC_JAD].hp; + state.npcs[2] = make_test_npc(INF_NPC_HEALER_JAD, 20, 34, 1); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = + INF_NPC_STATS[INF_NPC_HEALER_JAD].hp; + state.npcs[2].aggro_target = 1; + + ASSERT_FLOAT_NEAR("non-Zuk healing still uses generic heal cost", + inf_compute_reward(&state), -0.30f, 0.0001f); +} + +static void init_post_healer_set_reward_state(InfernoState* state) { + *state = make_test_state(24, 24); + state->wave = INF_NUM_WAVES - 1; + state->tick = 200; + state->tick_at_all_zuk_healers_dead = 100; + state->min_zuk_hp_seen = 500.0f; + state->npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state->npcs[0].active = 1; + state->npcs[0].hp = 500; + state->npcs[0].max_hp = 1200; +} + +static void test_post_healer_set_damage_reward_defaults_off(void) { + printf("--- post-healer set damage reward defaults off ---\n"); + + InfernoState state; + init_post_healer_set_reward_state(&state); + state.tick_scratch.damage_set = 50.0f; + + ASSERT_FLOAT_NEAR("default post-healer set damage reward is off", + inf_compute_reward(&state), 0.0f, 0.0001f); +} + +static void test_post_healer_set_damage_reward_pays_after_healer_clear(void) { + printf("--- post-healer set damage reward pays after healer clear ---\n"); + + InfernoState state; + init_post_healer_set_reward_state(&state); + inf_put_float((EncounterState*)&state, + "post_healer_set_damage_reward_coeff", 0.002f); + state.tick_scratch.damage_set = 50.0f; + + ASSERT_FLOAT_NEAR("post-healer set damage is rewarded", + inf_compute_reward(&state), 0.10f, 0.0001f); +} + +static void test_post_healer_set_kill_bonus_uses_existing_emitter(void) { + printf("--- post-healer set kill bonus uses existing emitter ---\n"); + + InfernoState state; + init_post_healer_set_reward_state(&state); + inf_put_float((EncounterState*)&state, "post_healer_set_kill_bonus", 0.09f); + state.tick_scratch.kill_set = 1; + + ASSERT_FLOAT_NEAR("post-healer set kill bonus emits through set channel", + inf_compute_reward(&state), 0.03f, 0.0001f); + ASSERT_FLOAT_NEAR("remaining post-healer set kill bonus is pending", + state.pending_set_kill_bonus, 0.06f, 0.0001f); +} + +static void test_post_healer_set_alive_penalty_caps_per_episode(void) { + printf("--- post-healer set alive penalty caps per episode ---\n"); + + InfernoState state; + init_post_healer_set_reward_state(&state); + inf_put_float((EncounterState*)&state, + "post_healer_set_alive_tick_penalty_coeff", 0.01f); + inf_put_float((EncounterState*)&state, + "post_healer_set_alive_penalty_cap", 0.03f); + state.post_healer_set_alive_penalty_total = 0.025f; + state.npcs[1] = make_test_npc(INF_NPC_RANGER, 29, 36, 1); + state.npcs[1].active = 1; + state.npcs[1].hp = 46; + state.npcs[2] = make_test_npc(INF_NPC_MAGER, 20, 36, 1); + state.npcs[2].active = 1; + state.npcs[2].hp = 70; + + ASSERT_FLOAT_NEAR("post-healer set alive penalty respects cap", + inf_compute_reward(&state), -0.005f, 0.0001f); + ASSERT_FLOAT_NEAR("post-healer set alive penalty total reaches cap", + state.post_healer_set_alive_penalty_total, 0.03f, 0.0001f); +} + +static void test_zuk_untagged_healer_tick_penalty_defaults_off(void) { + printf("--- Zuk untagged healer tick penalty defaults off ---\n"); + + InfernoState state = make_test_state(24, 24); + + state.wave = INF_NUM_WAVES - 1; + state.zuk.healer_spawned = 1; + state.min_zuk_hp_seen = 180.0f; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 220; + state.npcs[0].max_hp = 1200; + state.npcs[1] = make_test_npc(INF_NPC_HEALER_ZUK, 20, 34, 1); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = + INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[1].aggro_target = 0; + + ASSERT_FLOAT_NEAR("default untagged healer pressure is off", + inf_compute_reward(&state), 0.0f, 0.0001f); +} + +static void test_zuk_untagged_healer_tick_penalty_counts_only_untagged_zuk_healers(void) { + printf("--- Zuk untagged healer tick penalty counts only untagged Zuk healers ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, + "zuk_untagged_healer_tick_penalty_coeff", 0.01f); + state.wave = INF_NUM_WAVES - 1; + state.zuk.healer_spawned = 1; + state.min_zuk_hp_seen = 180.0f; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 220; + state.npcs[0].max_hp = 1200; + state.npcs[1] = make_test_npc(INF_NPC_HEALER_ZUK, 20, 34, 1); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = + INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[1].aggro_target = 0; + state.npcs[2] = make_test_npc(INF_NPC_HEALER_ZUK, 21, 34, 1); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = + INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[2].aggro_target = -1; + state.npcs[3] = make_test_npc(INF_NPC_JAD, 24, 32, 5); + state.npcs[3].active = 1; + state.npcs[3].hp = 200; + state.npcs[3].max_hp = INF_NPC_STATS[INF_NPC_JAD].hp; + state.npcs[4] = make_test_npc(INF_NPC_HEALER_JAD, 20, 35, 1); + state.npcs[4].active = 1; + state.npcs[4].hp = state.npcs[4].max_hp = + INF_NPC_STATS[INF_NPC_HEALER_JAD].hp; + state.npcs[4].aggro_target = 3; + + ASSERT_FLOAT_NEAR("only the untagged Zuk healer creates pressure", + inf_compute_reward(&state), -0.01f, 0.0001f); +} + +static void test_zuk_untagged_healer_target_bonus_defaults_off(void) { + printf("--- Zuk untagged healer target bonus defaults off ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + state.zuk.healer_spawned = 1; + state.min_zuk_hp_seen = 180.0f; + state.npcs[2] = make_test_npc( + INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[2].aggro_target = 0; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_TARGET] = 34; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("default target bonus pays no per-tick target reward", + state.tick_scratch.zuk_untagged_healer_targets, 0); + ASSERT_INT_EQ("default target bonus still records target attempts", + state.total_zuk_untagged_healer_targets, 1); + ASSERT_FLOAT_NEAR("default target bonus pays nothing", + inf_compute_reward(&state), 0.0f, 0.0001f); +} + +static void test_zuk_untagged_healer_target_bonus_rewards_distinct_healers(void) { + printf("--- Zuk untagged healer target bonus rewards distinct healers ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + state.zuk.healer_spawned = 1; + state.min_zuk_hp_seen = 180.0f; + inf_put_float((EncounterState*)&state, + "zuk_untagged_healer_target_bonus_coeff", 0.07f); + + state.npcs[2] = make_test_npc( + INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[2].aggro_target = 0; + state.npcs[3] = make_test_npc( + INF_NPC_HEALER_ZUK, 21, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + state.npcs[3].active = 1; + state.npcs[3].hp = state.npcs[3].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[3].aggro_target = 0; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + ASSERT_INT_EQ("first healer slot maps to npc 2", + state.current_obs_slots[33], 2); + ASSERT_INT_EQ("second healer slot maps to npc 3", + state.current_obs_slots[34], 3); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_TARGET] = 34; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("first untagged healer target rewarded", + state.tick_scratch.zuk_untagged_healer_targets, 1); + ASSERT_INT_EQ("first untagged healer target reward count", + state.total_zuk_untagged_healer_target_rewards, 1); + ASSERT_FLOAT_NEAR("first untagged healer target reward", + inf_compute_reward(&state), 0.07f, 0.0001f); + + state.tick_scratch.zuk_untagged_healer_targets = 0; + actions[INF_HEAD_TARGET] = 34; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("repeat target does not reward twice", + state.tick_scratch.zuk_untagged_healer_targets, 0); + + actions[INF_HEAD_TARGET] = 35; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("second distinct untagged healer target rewarded", + state.tick_scratch.zuk_untagged_healer_targets, 1); + ASSERT_INT_EQ("second distinct untagged healer target reward count", + state.total_zuk_untagged_healer_target_rewards, 2); +} + +static void test_zuk_safe_untagged_healer_target_bonus_records_safe_subset(void) { + printf("--- Zuk safe untagged healer target bonus records safe subset ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + state.zuk.healer_spawned = 1; + state.min_zuk_hp_seen = 180.0f; + inf_put_float((EncounterState*)&state, + "zuk_safe_untagged_healer_target_bonus_coeff", 0.11f); + + state.npcs[2] = make_test_npc( + INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[2].aggro_target = 0; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_TARGET] = 34; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("safe untagged healer target attempt", + state.total_zuk_untagged_healer_targets, 1); + ASSERT_INT_EQ("safe untagged healer target subset count", + state.total_zuk_safe_untagged_healer_targets, 1); + ASSERT_INT_EQ("unsafe untagged healer target subset count", + state.total_zuk_unsafe_untagged_healer_targets, 0); + ASSERT_INT_EQ("safe untagged healer target reward count", + state.total_zuk_safe_untagged_healer_target_rewards, 1); + ASSERT_INT_EQ("safe untagged healer per-tick reward event", + state.tick_scratch.zuk_safe_untagged_healer_targets, 1); + ASSERT_FLOAT_NEAR("safe untagged healer target reward", + inf_compute_reward(&state), 0.11f, 0.0001f); +} + +static void test_zuk_untagged_healer_target_bonus_excludes_tagged_healers(void) { + printf("--- Zuk untagged healer target bonus excludes tagged healers ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + state.zuk.healer_spawned = 1; + inf_put_float((EncounterState*)&state, + "zuk_untagged_healer_target_bonus_coeff", 0.07f); + + state.npcs[2] = make_test_npc( + INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[2].aggro_target = -1; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_TARGET] = 34; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("already tagged healer target gets no bonus", + state.tick_scratch.zuk_untagged_healer_targets, 0); +} + +static void test_zuk_healer_tags_first_reward_mode_blocks_pre_tag_damage(void) { + printf("--- Zuk healer tags-first reward mode blocks pre-tag damage ---\n"); + + InfernoState state = make_test_state(24, 24); + state.wave = INF_NUM_WAVES - 1; + state.zuk.healer_spawned = 1; + test_config()->zuk_healer_reward_mode = 1; + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.25f); + inf_put_float((EncounterState*)&state, "zuk_healer_damage_reward_coeff", 0.02f); + inf_put_float((EncounterState*)&state, "zuk_healer_kill_bonus", 0.30f); + state.min_zuk_hp_seen = 300.0f; + state.total_zuk_healer_tags = 2; + state.tick_scratch.healer_tags = 1; + state.tick_scratch.zuk_healer_tags = 1; + state.tick_scratch.damage_zuk = 80.0f; + state.tick_scratch.damage_zuk_healers = 10.0f; + state.tick_scratch.kill_zuk_healer = 1; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 220; + state.npcs[0].max_hp = 1200; + + ASSERT_FLOAT_NEAR("only healer tag reward is paid before all tags", + inf_compute_reward(&state), 0.25f, 0.0001f); + ASSERT_FLOAT_NEAR("pre-tag kill bonus is not delayed", + state.pending_zuk_healer_kill_bonus, 0.0f, 0.0001f); + ASSERT_FLOAT_NEAR("low watermark still tracks for metrics", + state.min_zuk_hp_seen, 220.0f, 0.0001f); +} + +static void test_zuk_healer_tags_first_reward_mode_resumes_after_all_tags(void) { + printf("--- Zuk healer tags-first reward mode resumes after all tags ---\n"); + + InfernoState state = make_test_state(24, 24); + state.wave = INF_NUM_WAVES - 1; + state.zuk.healer_spawned = 1; + test_config()->zuk_healer_reward_mode = 1; + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "zuk_healer_damage_reward_coeff", 0.02f); + inf_put_float((EncounterState*)&state, "zuk_healer_kill_bonus", 0.30f); + state.min_zuk_hp_seen = 300.0f; + state.total_zuk_healer_tags = 4; + state.tick_scratch.damage_zuk = 80.0f; + state.tick_scratch.damage_zuk_healers = 10.0f; + state.tick_scratch.kill_zuk_healer = 1; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 220; + state.npcs[0].max_hp = 1200; + + ASSERT_FLOAT_NEAR("damage and kill rewards resume after all tags", + inf_compute_reward(&state), 1.15f, 0.0001f); +} + +static void test_joseph_reward_mode_pays_tags_while_healers_heal(void) { + printf("--- Joseph reward mode pays tags while healers heal ---\n"); + + InfernoState state = make_test_state(24, 24); + state.wave = INF_NUM_WAVES - 1; + test_config()->joseph_reward_mode = 1; + state.min_zuk_hp_seen = 300.0f; + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.50f); + state.tick_scratch.damage_dealt = 70.0f; + state.tick_scratch.damage_zuk = 70.0f; + state.tick_scratch.healer_tags = 1; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 220; + state.npcs[0].max_hp = 1200; + state.npcs[1] = make_test_npc(INF_NPC_HEALER_ZUK, 20, 48, 1); + state.npcs[1].active = 1; + state.npcs[1].hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[1].aggro_target = 0; + + ASSERT_FLOAT_NEAR("only tag reward is paid while a Zuk healer heals", + inf_compute_reward(&state), 0.50f, 0.0001f); + ASSERT_FLOAT_NEAR("Joseph mode still tracks Zuk low watermark", + state.min_zuk_hp_seen, 220.0f, 0.0001f); +} + +static void test_zuk_healer_attack_shape_reward_applies_in_joseph_mode(void) { + printf("--- Zuk healer attack shape reward applies in Joseph mode ---\n"); + + InfernoState state = make_test_state(24, 24); + state.wave = INF_NUM_WAVES - 1; + test_config()->joseph_reward_mode = 1; + inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.50f); + inf_put_float((EncounterState*)&state, + "zuk_untagged_healer_nonmagic_attack_bonus_coeff", 0.07f); + inf_put_float((EncounterState*)&state, "zuk_healer_mage_attack_penalty_coeff", 0.04f); + state.tick_scratch.healer_tags = 1; + state.tick_scratch.zuk_untagged_healer_nonmagic_attacks = 2; + state.tick_scratch.zuk_healer_mage_attack_fires = 1; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 220; + state.npcs[0].max_hp = 1200; + state.npcs[1] = make_test_npc(INF_NPC_HEALER_ZUK, 20, 48, 1); + state.npcs[1].active = 1; + state.npcs[1].hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[1].aggro_target = 0; + + ASSERT_FLOAT_NEAR("tag reward is shaped by healer attack style", + inf_compute_reward(&state), 0.60f, 0.0001f); +} + +static void test_offensive_prayer_reward_shapes_normal_and_joseph_mode(void) { + printf("--- offensive prayer reward shapes normal and Joseph mode ---\n"); + + InfernoState normal = make_test_state(24, 24); + inf_put_float((EncounterState*)&normal, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&normal, "offensive_prayer_reward_coeff", 0.25f); + normal.tick_scratch.damage_dealt = 40.0f; + normal.tick_scratch.offensive_prayer_correct_damage_roll = 40.0f; + + ASSERT_FLOAT_NEAR("normal reward multiplies correct offensive prayer damage", + inf_compute_reward(&normal), 0.50f, 0.0001f); + + InfernoState wrong = make_test_state(24, 24); + inf_put_float((EncounterState*)&wrong, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&wrong, "offensive_prayer_reward_coeff", 0.25f); + wrong.tick_scratch.damage_dealt = 40.0f; + + ASSERT_FLOAT_NEAR("wrong offensive prayer receives base damage reward only", + inf_compute_reward(&wrong), 0.40f, 0.0001f); + + InfernoState zero = make_test_state(24, 24); + inf_put_float((EncounterState*)&zero, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&zero, "offensive_prayer_reward_coeff", 0.25f); + zero.tick_scratch.offensive_prayer_correct = 1; + + ASSERT_FLOAT_NEAR("correct offensive prayer without damage receives no shape", + inf_compute_reward(&zero), 0.0f, 0.0001f); + + InfernoState joseph = make_test_state(24, 24); + test_config()->joseph_reward_mode = 1; + inf_put_float((EncounterState*)&joseph, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&joseph, "offensive_prayer_reward_coeff", 0.25f); + joseph.tick_scratch.damage_dealt = 40.0f; + joseph.tick_scratch.offensive_prayer_correct_damage_roll = 40.0f; + + ASSERT_FLOAT_NEAR("Joseph reward multiplies correct offensive prayer damage", + inf_compute_reward(&joseph), 0.50f, 0.0001f); +} + +static void init_ranged_offensive_prayer_test_state(InfernoState* state) { + init_spell_cast_test_state(state, INF_NPC_NIBBLER); + state->weapon_set = INF_GEAR_BP; + state->player.autocast_enabled = 0; + state->npcs[0].x = 13; + state->npcs[0].y = 10; + encounter_apply_loadout(&state->player, INF_MAX_RANGE_FAST_LOADOUT, GEAR_RANGED); + encounter_compute_loadout_stats(INF_MAX_RANGE_FAST_LOADOUT, ATTACK_STYLE_RANGED, + state->player.offensive_prayer, 99, FIGHT_STYLE_RAPID, 0, + &state->loadout_stats[INF_GEAR_BP]); + inf_refresh_current_obs_slots(state); +} + +static void test_offensive_prayer_attack_events_count_real_attacks(void) { + printf("--- offensive prayer attack events count real attacks ---\n"); + + InfernoState ranged = make_test_state(10, 10); + init_ranged_offensive_prayer_test_state(&ranged); + ranged.player.offensive_prayer = OFFENSIVE_PRAYER_RIGOUR; + fire_player_action_at_slot_zero(&ranged, 0); + + ASSERT_INT_EQ("ranged attack fires", ranged.tick_scratch.player_attacked, 1); + ASSERT_INT_EQ("ranged attack counted", ranged.total_offensive_prayer_attacks, 1); + ASSERT_INT_EQ("ranged Rigour counted correct", ranged.total_offensive_prayer_correct, 1); + ASSERT_INT_EQ("ranged style total counted", + ranged.offensive_prayer_attacks_by_style[ATTACK_STYLE_RANGED], 1); + ASSERT_INT_EQ("ranged style correct counted", + ranged.offensive_prayer_correct_by_style[ATTACK_STYLE_RANGED], 1); + + InfernoState wrong_ranged = make_test_state(10, 10); + init_ranged_offensive_prayer_test_state(&wrong_ranged); + wrong_ranged.player.offensive_prayer = OFFENSIVE_PRAYER_PIETY; + fire_player_action_at_slot_zero(&wrong_ranged, 0); + + ASSERT_INT_EQ("wrong ranged attack counted", + wrong_ranged.total_offensive_prayer_attacks, 1); + ASSERT_INT_EQ("ranged with Piety counted wrong", + wrong_ranged.total_offensive_prayer_correct, 0); + + InfernoState magic = make_test_state(10, 10); + init_spell_cast_test_state(&magic, INF_NPC_NIBBLER); + magic.player.offensive_prayer = OFFENSIVE_PRAYER_AUGURY; + fire_player_action_at_slot_zero(&magic, 1); + + ASSERT_INT_EQ("magic attack counted", magic.total_offensive_prayer_attacks, 1); + ASSERT_INT_EQ("magic Augury counted correct", magic.total_offensive_prayer_correct, 1); + ASSERT_INT_EQ("magic style total counted", + magic.offensive_prayer_attacks_by_style[ATTACK_STYLE_MAGIC], 1); + ASSERT_INT_EQ("magic style correct counted", + magic.offensive_prayer_correct_by_style[ATTACK_STYLE_MAGIC], 1); + + InfernoState wrong_magic = make_test_state(10, 10); + init_spell_cast_test_state(&wrong_magic, INF_NPC_NIBBLER); + wrong_magic.player.offensive_prayer = OFFENSIVE_PRAYER_RIGOUR; + fire_player_action_at_slot_zero(&wrong_magic, 2); + + ASSERT_INT_EQ("magic with Rigour counted wrong", + wrong_magic.total_offensive_prayer_correct, 0); +} + +static void test_offensive_prayer_barrage_aoe_counts_once(void) { + printf("--- offensive prayer barrage AoE counts once ---\n"); + + InfernoState state = make_test_state(10, 10); + init_spell_cast_test_state(&state, INF_NPC_NIBBLER); + state.player.offensive_prayer = OFFENSIVE_PRAYER_AUGURY; + state.npcs[1] = make_test_npc(INF_NPC_NIBBLER, 17, 10, INF_NPC_STATS[INF_NPC_NIBBLER].size); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = INF_NPC_STATS[INF_NPC_NIBBLER].hp; + inf_refresh_current_obs_slots(&state); + + fire_player_action_at_slot_zero(&state, 1); + + ASSERT_INT_EQ("barrage attack fires", state.tick_scratch.player_attacked, 1); + ASSERT_INT_EQ("barrage counts one offensive prayer event", + state.total_offensive_prayer_attacks, 1); + ASSERT_INT_EQ("barrage with Augury counts correct", + state.total_offensive_prayer_correct, 1); +} + +static void test_offensive_prayer_no_attack_no_event(void) { + printf("--- offensive prayer no attack no event ---\n"); + + InfernoState state = make_test_state(10, 10); + init_spell_cast_test_state(&state, INF_NPC_NIBBLER); + state.player.offensive_prayer = OFFENSIVE_PRAYER_AUGURY; + state.player.attack_timer = 3; + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_TARGET] = inf_action_target_for_npc(&state, 0); + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("cooldown prevents attack", state.tick_scratch.player_attacked, 0); + ASSERT_INT_EQ("cooldown produces no offensive prayer event", + state.total_offensive_prayer_attacks, 0); + ASSERT_INT_EQ("cooldown produces no correct event", + state.tick_scratch.offensive_prayer_correct, 0); +} + +static void test_offensive_prayer_melee_maps_to_piety(void) { + printf("--- offensive prayer melee maps to Piety ---\n"); + + InfernoState state = make_test_state(10, 10); + state.player.offensive_prayer = OFFENSIVE_PRAYER_PIETY; + inf_record_offensive_prayer_attack(&state, ATTACK_STYLE_MELEE, 7.0f); + + ASSERT_INT_EQ("melee requires Piety", + encounter_offensive_prayer_for_style(ATTACK_STYLE_MELEE), + OFFENSIVE_PRAYER_PIETY); + ASSERT_INT_EQ("melee Piety counted correct", + state.total_offensive_prayer_correct, 1); + ASSERT_INT_EQ("melee style counted", + state.offensive_prayer_attacks_by_style[ATTACK_STYLE_MELEE], 1); + ASSERT_FLOAT_NEAR("melee correct prayer records damage roll", + state.tick_scratch.offensive_prayer_correct_damage_roll, 7.0f, 1e-6f); +} + +static void test_player_reward_damage_uses_xp_drop_tick(void) { + printf("--- player reward damage uses XP-drop tick ---\n"); + + InfernoState state = make_test_state(10, 10); + state.npcs[0] = make_test_npc( + INF_NPC_RANGER, 16, 10, INF_NPC_STATS[INF_NPC_RANGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = 15; + state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + + float reward_damage = inf_record_player_reward_damage(&state, 0, 50); + + ASSERT_FLOAT_NEAR("reward damage caps to current hp", + reward_damage, 15.0f, 1e-6f); + ASSERT_FLOAT_NEAR("damage dealt stat records on fire tick", + state.tick_scratch.damage_dealt, 15.0f, 1e-6f); + ASSERT_FLOAT_NEAR("set damage stat records on fire tick", + state.tick_scratch.damage_set, 15.0f, 1e-6f); + ASSERT_INT_EQ("reward damage does not apply hp before hitsplat", + state.npcs[0].hp, 15); +} + +static void test_idle_diagnostics_count_missed_attack_opportunities(void) { + printf("--- idle diagnostics count missed attack opportunities ---\n"); + + InfernoState state = make_test_state(10, 10); + state.weapon_set = INF_GEAR_BP; + state.loadout_stats[INF_GEAR_BP].attack_range = 7; + state.npcs[0] = make_test_npc( + INF_NPC_RANGER, 14, 10, INF_NPC_STATS[INF_NPC_RANGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + state.npcs[0].attack_timer = 5; + + ASSERT_INT_EQ("target exists", + inf_has_live_player_target(&state), 1); + ASSERT_INT_EQ("target can be attacked", + inf_has_attackable_player_target(&state), 1); + ASSERT_INT_EQ("no immediate threat", + inf_player_has_immediate_threat(&state), 0); + + inf_record_idle_diagnostics(&state, 1, 1, 1, 1); + + ASSERT_INT_EQ("attack ready no attack total", + state.total_attack_ready_no_attack_ticks, 1); + ASSERT_INT_EQ("target available no attack total", + state.total_target_available_no_attack_ticks, 1); + ASSERT_INT_EQ("safe opportunity missed total", + state.total_safe_attack_opportunity_missed_ticks, 1); + ASSERT_INT_EQ("progressless total", + state.total_progressless_ticks, 1); + ASSERT_INT_EQ("set phase attack ready counter", + state.attack_ready_no_attack_ticks_by_phase[INF_IDLE_PHASE_SET], 1); + ASSERT_INT_EQ("set phase safe opportunity counter", + state.safe_attack_opportunity_missed_ticks_by_phase[INF_IDLE_PHASE_SET], 1); + ASSERT_INT_EQ("set phase progressless counter", + state.progressless_ticks_by_phase[INF_IDLE_PHASE_SET], 1); +} + +static void test_idle_diagnostics_phase_split(void) { + printf("--- idle diagnostics phase split ---\n"); + + InfernoState set = make_test_state(10, 10); + set.wave = 20; + ASSERT_INT_EQ("ordinary waves use set phase", + inf_idle_diagnostic_phase(&set), INF_IDLE_PHASE_SET); + + InfernoState jad = make_test_state(10, 10); + jad.wave = 66; + jad.npcs[0] = make_test_npc( + INF_NPC_JAD, 14, 10, INF_NPC_STATS[INF_NPC_JAD].size); + jad.npcs[0].active = 1; + jad.npcs[0].hp = INF_NPC_STATS[INF_NPC_JAD].hp; + ASSERT_INT_EQ("non-final live jad uses jad phase", + inf_idle_diagnostic_phase(&jad), INF_IDLE_PHASE_JAD); + + InfernoState zuk = make_test_state(25, 42); + zuk.wave = INF_WAVE_ZUK; + zuk.tick_at_all_zuk_healers_dead = -1; + ASSERT_INT_EQ("final wave before jad uses zuk pre-jad phase", + inf_idle_diagnostic_phase(&zuk), INF_IDLE_PHASE_ZUK_PRE_JAD); + + zuk.npcs[0] = make_test_npc( + INF_NPC_JAD, 24, 44, INF_NPC_STATS[INF_NPC_JAD].size); + zuk.npcs[0].active = 1; + zuk.npcs[0].hp = INF_NPC_STATS[INF_NPC_JAD].hp; + ASSERT_INT_EQ("final wave live jad uses zuk jad phase", + inf_idle_diagnostic_phase(&zuk), INF_IDLE_PHASE_ZUK_JAD); + + zuk.npcs[1] = make_test_npc( + INF_NPC_HEALER_ZUK, 22, 44, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + zuk.npcs[1].active = 1; + zuk.npcs[1].hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + ASSERT_INT_EQ("live zuk healer uses zuk healer phase", + inf_idle_diagnostic_phase(&zuk), INF_IDLE_PHASE_ZUK_HEALERS); + + zuk.npcs[0].active = 0; + zuk.npcs[1].active = 0; + zuk.tick_at_all_zuk_healers_dead = 500; + ASSERT_INT_EQ("after healers dead uses post-healer phase", + inf_idle_diagnostic_phase(&zuk), INF_IDLE_PHASE_ZUK_POST_HEALERS); +} + +static void test_joseph_reward_mode_damps_healed_zuk_damage(void) { + printf("--- Joseph reward mode damps healed Zuk damage ---\n"); + + InfernoState state = make_test_state(24, 24); + state.wave = INF_NUM_WAVES - 1; + test_config()->joseph_reward_mode = 1; + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + state.tick_scratch.damage_dealt = 100.0f; + state.tick_scratch.damage_zuk = 100.0f; + state.total_hp_restored_zuk = 1200.0f; + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 22, 50, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 200; + state.npcs[0].max_hp = 1200; + + ASSERT_FLOAT_NEAR("Zuk damage is downweighted by prior healing", + inf_compute_reward(&state), 0.20f, 0.0001f); +} + +static void test_jad_damage_reward_pauses_while_jad_healers_heal(void) { + printf("--- jad damage reward pauses while jad healers heal ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "shield_penalty_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.25f); + state.wave = 66; + state.tick_scratch.damage_dealt = 40.0f; + state.tick_scratch.damage_jad = 40.0f; + state.npcs[0] = make_test_npc(INF_NPC_JAD, 24, 32, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 200; + state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_JAD].hp; + state.npcs[1] = make_test_npc(INF_NPC_HEALER_JAD, 20, 34, 1); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = + INF_NPC_STATS[INF_NPC_HEALER_JAD].hp; + inf_npc_healer(&state.npcs[1])->owner_idx = 0; + state.npcs[1].aggro_target = 0; + + ASSERT_FLOAT_NEAR("jad damage pays nothing while a healer heals jad", + inf_compute_reward(&state), 0.0f, 0.0001f); + + state.npcs[1].aggro_target = -1; + + ASSERT_FLOAT_NEAR("jad damage resumes after the healer is tagged", + inf_compute_reward(&state), 0.40f, 0.0001f); +} + +static void test_jad_healer_damage_never_gets_damage_reward(void) { + printf("--- jad healer damage never gets damage reward ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, "damage_reward_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "shield_penalty_coeff", 0.01f); + inf_put_float((EncounterState*)&state, "tag_reward_coeff", 0.25f); + state.wave = 66; + state.tick_scratch.damage_dealt = 40.0f; + state.tick_scratch.damage_jad_healers = 40.0f; + state.npcs[0] = make_test_npc(INF_NPC_HEALER_JAD, 20, 34, 1); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = + INF_NPC_STATS[INF_NPC_HEALER_JAD].hp; + state.npcs[0].aggro_target = -1; + + ASSERT_FLOAT_NEAR("jad healer damage is not rewarded", + inf_compute_reward(&state), 0.0f, 0.0001f); +} + +static void test_shield_tag_reward_excludes_zuk(void) { + printf("--- shield tag reward excludes zuk ---\n"); + + InfernoState state = make_test_state(24, 24); + + inf_put_float((EncounterState*)&state, "shield_tag_reward_coeff", 0.20f); + state.npcs[0] = make_test_npc(INF_NPC_ZUK_SHIELD, 23, 44, 1); + state.npcs[0].active = 1; + state.npcs[0].hp = 100; + + state.npcs[1] = make_test_npc(INF_NPC_MAGER, 20, 44, 1); + state.npcs[1].active = 1; + state.npcs[1].hp = 100; + state.npcs[1].aggro_target = 0; + + state.npcs[2] = make_test_npc(INF_NPC_RANGER, 22, 44, 1); + state.npcs[2].active = 1; + state.npcs[2].hp = 100; + state.npcs[2].aggro_target = 0; + + state.npcs[3] = make_test_npc(INF_NPC_JAD, 24, 44, 1); + state.npcs[3].active = 1; + state.npcs[3].hp = 100; + state.npcs[3].aggro_target = 0; + + state.npcs[4] = make_test_npc(INF_NPC_ZUK, 26, 44, 5); + state.npcs[4].active = 1; + state.npcs[4].hp = 1000; + state.npcs[4].aggro_target = 0; + + state.npcs[5] = make_test_npc(INF_NPC_HEALER_ZUK, 28, 44, 1); + state.npcs[5].active = 1; + state.npcs[5].hp = 100; + state.npcs[5].aggro_target = 0; + + ASSERT_INT_EQ("mager can be tagged off shield", + inf_is_shield_taggable_slot(&state, 1), 1); + ASSERT_INT_EQ("ranger can be tagged off shield", + inf_is_shield_taggable_slot(&state, 2), 1); + ASSERT_INT_EQ("jad can be tagged off shield", + inf_is_shield_taggable_slot(&state, 3), 1); + ASSERT_INT_EQ("zuk cannot be tagged off shield", + inf_is_shield_taggable_slot(&state, 4), 0); + ASSERT_INT_EQ("zuk healer cannot be tagged off shield", + inf_is_shield_taggable_slot(&state, 5), 0); + ASSERT_INT_EQ("shield cannot be tagged off itself", + inf_is_shield_taggable_slot(&state, 0), 0); + + state.tick_scratch.shield_tags = 3; + ASSERT_FLOAT_NEAR("shield tag reward pays per valid shield tag", + inf_compute_reward(&state), 0.60f, 0.0001f); +} + +static void test_inferno_reset_supplies_match_current_inventory(void) { + printf("--- inferno reset supplies match current inventory ---\n"); + + EncounterState* raw_state = inf_create(); + InfernoState* state = (InfernoState*)raw_state; + InfSupplyDoses full = inf_full_starting_supplies(); + + reset_inferno_at_public_wave(raw_state, 1, 1.0f); + + assert_supply_doses("wave 1", &state->player, full); + + inf_destroy(raw_state); +} + +static int test_inventory_potion_vials(int doses) { + return (doses + 3) / 4; +} + +static int test_player_inventory_occupied_slots(Player* p) { + int occupied = 0; + + for (int s = 0; s < NUM_GEAR_SLOTS; s++) { + for (int i = 0; i < p->num_items_in_slot[s]; i++) { + uint8_t item = p->inventory[s][i]; + if (item == ITEM_NONE) continue; + + int is_equipped = 0; + for (int e = 0; e < NUM_GEAR_SLOTS; e++) { + if (p->equipped[e] == item) { + is_equipped = 1; + break; + } + } + if (!is_equipped) + occupied++; + } + } + + occupied += p->food_count; + occupied += p->karambwan_count; + occupied += test_inventory_potion_vials(p->brew_doses); + occupied += test_inventory_potion_vials(p->restore_doses); + occupied += test_inventory_potion_vials(p->combat_potion_doses); + occupied += test_inventory_potion_vials(p->ranged_potion_doses); + occupied += test_inventory_potion_vials(p->bastion_doses); + occupied += test_inventory_potion_vials(p->stamina_doses); + occupied += test_inventory_potion_vials(p->antivenom_doses); + occupied += test_inventory_potion_vials(p->prayer_pot_doses); + return occupied; +} + +static int test_player_slot_inventory_contains( + const Player* p, + int gear_slot, + uint8_t item +) { + for (int i = 0; i < p->num_items_in_slot[gear_slot]; i++) { + if (p->inventory[gear_slot][i] == item) + return 1; + } + return 0; +} + +static void test_inferno_reset_inventory_leaves_one_empty_slot(void) { + printf("--- inferno reset inventory leaves one empty slot ---\n"); + + EncounterState* raw_state = inf_create(); + InfernoState* state = (InfernoState*)raw_state; + + reset_inferno_at_public_wave(raw_state, 1, 1.0f); + + ASSERT_INT_EQ("full kit bastion doses", state->player.bastion_doses, 16); + ASSERT_INT_EQ("full kit bastion vials", + test_inventory_potion_vials(state->player.bastion_doses), 4); + ASSERT_INT_EQ("full kit occupied inventory slots", + test_player_inventory_occupied_slots(&state->player), 27); + + inf_destroy(raw_state); +} + +static void test_inferno_max_profile_reset_uses_existing_gear(void) { + printf("--- inferno max profile reset uses existing gear ---\n"); + + EncounterState* raw_state = inf_create(); + InfernoState* state = (InfernoState*)raw_state; + + reset_inferno_at_public_wave(raw_state, 1, 1.0f); + + ASSERT_INT_EQ("default loadout profile mode", + test_config()->loadout_profile_mode, INF_LOADOUT_PROFILE_MODE_MAX_ONLY); + ASSERT_INT_EQ("default active profile", + state->active_loadout_profile, INF_LOADOUT_PROFILE_MAX); + ASSERT_INT_EQ("max mage weapon", + state->player.equipped[GEAR_SLOT_WEAPON], ITEM_KODAI_WAND); + ASSERT_INT_EQ("max mage shield", + state->player.equipped[GEAR_SLOT_SHIELD], ITEM_ELYSIAN_SPIRIT_SHIELD); + ASSERT_INT_EQ("max long-range weapon in inventory", + test_player_slot_inventory_contains( + &state->player, GEAR_SLOT_WEAPON, ITEM_TWISTED_BOW), 1); + ASSERT_INT_EQ("max fast-range weapon in inventory", + test_player_slot_inventory_contains( + &state->player, GEAR_SLOT_WEAPON, ITEM_TOXIC_BLOWPIPE), 1); + + inf_destroy(raw_state); +} + +static void test_inferno_budget_profile_reset_uses_budget_gear(void) { + printf("--- inferno budget profile reset uses budget gear ---\n"); + + EncounterState* raw_state = inf_create(); + InfernoState* state = (InfernoState*)raw_state; + + inf_put_int(raw_state, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); + reset_inferno_at_public_wave(raw_state, 1, 1.0f); + + ASSERT_INT_EQ("budget active profile", + state->active_loadout_profile, INF_LOADOUT_PROFILE_BUDGET); + ASSERT_INT_EQ("budget mage head", + state->player.equipped[GEAR_SLOT_HEAD], ITEM_CRYSTAL_HELM); + ASSERT_INT_EQ("budget mage cape", + state->player.equipped[GEAR_SLOT_CAPE], ITEM_DIZANAS_QUIVER); + ASSERT_INT_EQ("budget mage neck", + state->player.equipped[GEAR_SLOT_NECK], ITEM_OCCULT_NECKLACE); + ASSERT_INT_EQ("budget mage ammo", + state->player.equipped[GEAR_SLOT_AMMO], ITEM_GOD_BLESSING); + ASSERT_INT_EQ("budget mage weapon", + state->player.equipped[GEAR_SLOT_WEAPON], ITEM_DRAGON_HUNTER_WAND); + ASSERT_INT_EQ("budget mage shield", + state->player.equipped[GEAR_SLOT_SHIELD], ITEM_CRYSTAL_SHIELD); + ASSERT_INT_EQ("budget mage body", + state->player.equipped[GEAR_SLOT_BODY], ITEM_AHRIMS_ROBETOP); + ASSERT_INT_EQ("budget mage legs", + state->player.equipped[GEAR_SLOT_LEGS], ITEM_AHRIMS_ROBESKIRT); + ASSERT_INT_EQ("budget mage hands", + state->player.equipped[GEAR_SLOT_HANDS], ITEM_CONFLICTION_GAUNTLETS); + ASSERT_INT_EQ("budget mage feet", + state->player.equipped[GEAR_SLOT_FEET], ITEM_ECHO_BOOTS); + ASSERT_INT_EQ("budget mage ring", + state->player.equipped[GEAR_SLOT_RING], ITEM_VENATOR_RING); + ASSERT_INT_EQ("budget bowfa available", + test_player_slot_inventory_contains( + &state->player, GEAR_SLOT_WEAPON, ITEM_BOW_OF_FAERDHINEN), 1); + ASSERT_INT_EQ("budget blowpipe available", + test_player_slot_inventory_contains( + &state->player, GEAR_SLOT_WEAPON, ITEM_TOXIC_BLOWPIPE), 1); + ASSERT_INT_EQ("budget range body available", + test_player_slot_inventory_contains( + &state->player, GEAR_SLOT_BODY, ITEM_CRYSTAL_BODY), 1); + ASSERT_INT_EQ("budget range legs available", + test_player_slot_inventory_contains( + &state->player, GEAR_SLOT_LEGS, ITEM_CRYSTAL_LEGS), 1); + ASSERT_INT_EQ("budget inventory leaves one empty slot", + test_player_inventory_occupied_slots(&state->player), 27); + + inf_destroy(raw_state); +} + +static void test_inferno_mixed_profile_sampling_respects_fraction(void) { + printf("--- inferno mixed profile sampling respects fraction ---\n"); + + EncounterState* raw_a = inf_create(); + EncounterState* raw_b = inf_create(); + EncounterState* raw_c = inf_create(); + InfernoState* a = (InfernoState*)raw_a; + InfernoState* b = (InfernoState*)raw_b; + InfernoState* c = (InfernoState*)raw_c; + + inf_put_int(raw_a, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_MIXED_MAX_BUDGET); + inf_put_float(raw_a, "budget_loadout_fraction", 0.0f); + inf_reset(raw_a, 456u); + ASSERT_INT_EQ("zero fraction samples max", + a->active_loadout_profile, INF_LOADOUT_PROFILE_MAX); + + inf_put_int(raw_b, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_MIXED_MAX_BUDGET); + inf_put_float(raw_b, "budget_loadout_fraction", 1.0f); + inf_reset(raw_b, 456u); + ASSERT_INT_EQ("one fraction samples budget", + b->active_loadout_profile, INF_LOADOUT_PROFILE_BUDGET); + + inf_put_int(raw_c, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_MIXED_MAX_BUDGET); + inf_put_float(raw_c, "budget_loadout_fraction", 1.0f); + inf_reset(raw_c, 456u); + ASSERT_INT_EQ("same seed and fraction are deterministic", + c->active_loadout_profile, b->active_loadout_profile); + + inf_destroy(raw_a); + inf_destroy(raw_b); + inf_destroy(raw_c); +} + +static void test_inferno_gear_actions_map_to_active_profile_slots(void) { + printf("--- inferno gear actions map to active profile slots ---\n"); + + EncounterState* raw_state = inf_create(); + InfernoState* state = (InfernoState*)raw_state; + int actions[INF_NUM_ACTION_HEADS]; + + inf_put_int(raw_state, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); + inf_reset(raw_state, 789u); + + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_GEAR] = 2; + inf_tick_player(state, actions, 1); + ASSERT_INT_EQ("gear action 2 selects long range", + state->weapon_set, INF_GEAR_LONG_RANGE); + ASSERT_INT_EQ("long range budget weapon", + state->player.equipped[GEAR_SLOT_WEAPON], ITEM_BOW_OF_FAERDHINEN); + + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_GEAR] = 3; + inf_tick_player(state, actions, 1); + ASSERT_INT_EQ("gear action 3 selects fast range", + state->weapon_set, INF_GEAR_BP); + ASSERT_INT_EQ("fast range budget weapon", + state->player.equipped[GEAR_SLOT_WEAPON], ITEM_TOXIC_BLOWPIPE); + + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_GEAR] = 1; + inf_tick_player(state, actions, 1); + ASSERT_INT_EQ("gear action 1 selects mage", + state->weapon_set, INF_GEAR_MAGE); + ASSERT_INT_EQ("mage budget weapon", + state->player.equipped[GEAR_SLOT_WEAPON], ITEM_DRAGON_HUNTER_WAND); + + inf_destroy(raw_state); +} + +static void test_inferno_reset_preserves_reward_config(void) { + printf("--- inferno reset preserves reward config ---\n"); + + EncounterState* raw_state = inf_create(); + InfernoState* state = (InfernoState*)raw_state; + + inf_put_float(raw_state, "supply_milestone_brew_reward_coeff", 0.001f); + inf_put_float(raw_state, "supply_milestone_restore_reward_coeff", 0.002f); + inf_put_float(raw_state, "offensive_prayer_reward_coeff", 0.009f); + inf_put_float(raw_state, "post_healer_zuk_damage_coeff", 0.003f); + inf_put_float(raw_state, "zuk_healer_phase_hp_delta_coeff", 0.004f); + inf_put_float(raw_state, "zuk_untagged_healer_tick_penalty_coeff", 0.005f); + inf_put_float(raw_state, "zuk_untagged_healer_target_bonus_coeff", 0.006f); + inf_put_float(raw_state, + "zuk_untagged_healer_nonmagic_attack_bonus_coeff", 0.007f); + inf_put_float(raw_state, "zuk_healer_mage_attack_penalty_coeff", 0.008f); + inf_put_int(raw_state, "zuk_safe_untagged_healer_target_mask", 1); + inf_put_int(raw_state, "zuk_force_safe_untagged_healer_target_mask", 1); + inf_put_int(raw_state, "zuk_healer_reward_mode", 1); + inf_put_int(raw_state, "joseph_reward_mode", 1); + inf_put_int(raw_state, "terminal_penalty_enabled", 1); + inf_put_int(raw_state, "step_out_forecast_obs_enabled", 0); + inf_put_int(raw_state, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); + inf_put_float(raw_state, "budget_loadout_fraction", 1.0f); + inf_reset(raw_state, 123u); + + ASSERT_FLOAT_NEAR("supply milestone brew reward coefficient", + test_config()->supply_milestone_brew_reward_coeff, 0.001f, 1e-6f); + ASSERT_FLOAT_NEAR("supply milestone restore reward coefficient", + test_config()->supply_milestone_restore_reward_coeff, 0.002f, 1e-6f); + ASSERT_FLOAT_NEAR("offensive prayer reward coefficient", + test_config()->offensive_prayer_reward_coeff, 0.009f, 1e-6f); + ASSERT_FLOAT_NEAR("post-healer Zuk damage coefficient", + test_config()->post_healer_zuk_damage_coeff, 0.003f, 1e-6f); + ASSERT_FLOAT_NEAR("Zuk healer-phase HP delta coefficient", + test_config()->zuk_healer_phase_hp_delta_coeff, 0.004f, 1e-6f); + ASSERT_FLOAT_NEAR("Zuk untagged healer tick penalty coefficient", + test_config()->zuk_untagged_healer_tick_penalty_coeff, 0.005f, 1e-6f); + ASSERT_FLOAT_NEAR("Zuk untagged healer target bonus coefficient", + test_config()->zuk_untagged_healer_target_bonus_coeff, 0.006f, 1e-6f); + ASSERT_FLOAT_NEAR("Zuk untagged healer non-magic attack bonus coefficient", + test_config()->zuk_untagged_healer_nonmagic_attack_bonus_coeff, 0.007f, 1e-6f); + ASSERT_FLOAT_NEAR("Zuk healer mage attack penalty coefficient", + test_config()->zuk_healer_mage_attack_penalty_coeff, 0.008f, 1e-6f); + ASSERT_INT_EQ("safe untagged healer target mask", + test_config()->zuk_safe_untagged_healer_target_mask, 1); + ASSERT_INT_EQ("force safe untagged healer target mask", + test_config()->zuk_force_safe_untagged_healer_target_mask, 1); + ASSERT_INT_EQ("Zuk healer reward mode", test_config()->zuk_healer_reward_mode, 1); + ASSERT_INT_EQ("Joseph reward mode", test_config()->joseph_reward_mode, 1); + ASSERT_INT_EQ("terminal penalty enabled", test_config()->terminal_penalty_enabled, 1); + ASSERT_INT_EQ("step-out forecast obs disabled", + test_config()->step_out_forecast_obs_enabled, 0); + ASSERT_INT_EQ("step-out forecast obs mode disabled", + test_config()->step_out_forecast_obs_mode, INF_STEP_OUT_FORECAST_MODE_OFF); + ASSERT_INT_EQ("loadout profile mode preserved", + test_config()->loadout_profile_mode, INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); + ASSERT_FLOAT_NEAR("budget loadout fraction preserved", + test_config()->budget_loadout_fraction, 1.0f, 1e-6f); + ASSERT_INT_EQ("budget-only reset selects budget profile", + state->active_loadout_profile, INF_LOADOUT_PROFILE_BUDGET); + + inf_destroy(raw_state); +} + +static void test_supply_milestone_reward_defaults_off(void) { + printf("--- supply milestone reward defaults off ---\n"); + + InfernoState state = make_test_state(24, 24); + test_config()->late_start_supply_profile_scale = 1.0f; + state.player.brew_doses = 24; + state.player.restore_doses = 40; + + ASSERT_FLOAT_NEAR("default supply milestone reward", + test_supply_milestone_surplus_reward(&state, 64), 0.0f, 0.0001f); + ASSERT_INT_EQ("default supply milestone does not consume anchor", + (int)state.supply_milestone_rewarded_mask, 0); +} + +static void test_supply_milestone_reward_pays_surplus_at_anchor_once(void) { + printf("--- supply milestone reward pays surplus at anchor once ---\n"); + + InfernoState state = make_test_state(24, 24); + test_config()->late_start_supply_profile_scale = 1.0f; + test_config()->supply_milestone_brew_reward_coeff = 0.24f; + test_config()->supply_milestone_restore_reward_coeff = 0.20f; + state.player.brew_doses = 18; + state.player.restore_doses = 24; + + ASSERT_FLOAT_NEAR("wave 64 supply surplus reward", + test_supply_milestone_surplus_reward(&state, 64), 0.06f, 0.0001f); + ASSERT_FLOAT_NEAR("wave 64 supply surplus pays once", + test_supply_milestone_surplus_reward(&state, 64), 0.0f, 0.0001f); +} + +static void test_supply_milestone_reward_never_penalizes_shortage(void) { + printf("--- supply milestone reward never penalizes shortage ---\n"); + + InfernoState state = make_test_state(24, 24); + test_config()->late_start_supply_profile_scale = 1.0f; + test_config()->supply_milestone_brew_reward_coeff = 0.24f; + test_config()->supply_milestone_restore_reward_coeff = 0.20f; + + ASSERT_FLOAT_NEAR("shortage reward is zero", + test_supply_milestone_surplus_reward(&state, 64), 0.0f, 0.0001f); + + InfernoState non_anchor = make_test_state(24, 24); + test_config()->late_start_supply_profile_scale = 1.0f; + test_config()->supply_milestone_brew_reward_coeff = 0.24f; + non_anchor.player.brew_doses = 24; + non_anchor.player.restore_doses = 40; + ASSERT_FLOAT_NEAR("non-anchor reward is zero", + test_supply_milestone_surplus_reward(&non_anchor, 63), 0.0f, 0.0001f); +} + +static void test_late_start_supply_profile_anchor_waves(void) { + printf("--- inferno late-start supply profile anchor waves ---\n"); + + struct { + int public_wave; + float brew_fraction; + float restore_fraction; + float bastion_fraction; + float stamina_fraction; + } anchors[] = { + { 20, 1.0000f, 0.9500f, 1.0000f, 1.0000f }, + { 40, 0.9167f, 0.8750f, 1.0000f, 1.0000f }, + { 61, 0.8333f, 0.7500f, 1.0000f, 1.0000f }, + { 64, 0.5833f, 0.5000f, 0.7500f, 1.0000f }, + { 68, 0.5833f, 0.4250f, 0.6250f, 1.0000f }, + { 69, 0.5000f, 0.3000f, 0.3750f, 1.0000f }, + { 70, 0.4375f, 0.2250f, 0.3750f, 1.0000f }, + { 71, 0.3750f, 0.1500f, 0.3750f, 1.0000f }, + }; + + EncounterState* raw_state = inf_create(); + InfernoState* state = (InfernoState*)raw_state; + InfSupplyDoses full = inf_full_starting_supplies(); + + for (int i = 0; i < (int)(sizeof(anchors) / sizeof(anchors[0])); i++) { + reset_inferno_at_public_wave(raw_state, anchors[i].public_wave, 1.0f); + InfSupplyDoses expected = { + .brew_doses = test_profiled_supply_count(full.brew_doses, + anchors[i].brew_fraction, 1.0f), + .restore_doses = test_profiled_supply_count(full.restore_doses, + anchors[i].restore_fraction, 1.0f), + .bastion_doses = test_profiled_supply_count(full.bastion_doses, + anchors[i].bastion_fraction, 1.0f), + .stamina_doses = test_profiled_supply_count(full.stamina_doses, + anchors[i].stamina_fraction, 1.0f), + }; + char label[64]; + snprintf(label, sizeof(label), "wave %d", anchors[i].public_wave); + assert_supply_doses(label, &state->player, expected); + } + + inf_destroy(raw_state); +} + +static void test_late_start_supply_profile_interpolation_and_scale(void) { + printf("--- inferno late-start supply profile interpolation and scale ---\n"); + + EncounterState* raw_state = inf_create(); + InfernoState* state = (InfernoState*)raw_state; + InfSupplyDoses full = inf_full_starting_supplies(); + float t = 1.0f / 3.0f; + float brew_fraction = 0.8333f + (0.5833f - 0.8333f) * t; + float restore_fraction = 0.7500f + (0.5000f - 0.7500f) * t; + float bastion_fraction = 1.0000f + (0.7500f - 1.0000f) * t; + + reset_inferno_at_public_wave(raw_state, 62, 1.0f); + InfSupplyDoses interpolated = { + .brew_doses = test_profiled_supply_count(full.brew_doses, brew_fraction, 1.0f), + .restore_doses = test_profiled_supply_count(full.restore_doses, restore_fraction, 1.0f), + .bastion_doses = test_profiled_supply_count(full.bastion_doses, bastion_fraction, 1.0f), + .stamina_doses = full.stamina_doses, + }; + assert_supply_doses("wave 62", &state->player, interpolated); + + reset_inferno_at_public_wave(raw_state, 69, 0.0f); + assert_supply_doses("wave 69 scale 0", &state->player, full); + + reset_inferno_at_public_wave(raw_state, 69, 0.5f); + InfSupplyDoses half_scale = { + .brew_doses = test_profiled_supply_count(full.brew_doses, 0.5000f, 0.5f), + .restore_doses = test_profiled_supply_count(full.restore_doses, 0.3000f, 0.5f), + .bastion_doses = test_profiled_supply_count(full.bastion_doses, 0.3750f, 0.5f), + .stamina_doses = test_profiled_supply_count(full.stamina_doses, 1.0000f, 0.5f), + }; + assert_supply_doses("wave 69 scale 0.5", &state->player, half_scale); + + inf_destroy(raw_state); +} + +static void test_curriculum_supply_no_brew_is_curriculum_only(void) { + printf("--- curriculum no-brew starts are curriculum-only ---\n"); + + EncounterState* raw_state = inf_create(); + InfernoState* state = (InfernoState*)raw_state; + + inf_put_int(raw_state, "curriculum_no_brew_mode", + INF_CURRICULUM_SUPPLY_MODE_ALL); + inf_put_float(raw_state, "curriculum_no_brew_frac", 1.0f); + reset_inferno_at_public_wave(raw_state, 71, 1.0f); + ASSERT_INT_EQ("normal start ignores curriculum no-brew", + state->player.brew_doses, 9); + + inf_put_int(raw_state, "curriculum_agent", 1); + reset_inferno_at_public_wave(raw_state, 71, 1.0f); + ASSERT_INT_EQ("curriculum start applies no-brew", + state->player.brew_doses, 0); + ASSERT_INT_EQ("curriculum no-brew leaves restores alone", + state->player.restore_doses, 6); + + inf_put_int(raw_state, "curriculum_agent", 0); + inf_put_int(raw_state, "curriculum_no_brew_mode", + INF_CURRICULUM_SUPPLY_MODE_OFF); + inf_put_float(raw_state, "curriculum_no_brew_frac", 0.0f); + inf_destroy(raw_state); +} + +static void test_curriculum_supply_modes_gate_zuk_and_pre_zuk(void) { + printf("--- curriculum supply modes gate Zuk and pre-Zuk starts ---\n"); + + ASSERT_INT_EQ("off mode does not apply", + inf_curriculum_supply_mode_applies(INF_CURRICULUM_SUPPLY_MODE_OFF, 69), 0); + ASSERT_INT_EQ("all mode applies to Zuk", + inf_curriculum_supply_mode_applies(INF_CURRICULUM_SUPPLY_MODE_ALL, 69), 1); + ASSERT_INT_EQ("Zuk mode applies to wave 69", + inf_curriculum_supply_mode_applies(INF_CURRICULUM_SUPPLY_MODE_ZUK, 69), 1); + ASSERT_INT_EQ("Zuk mode applies to wave 71", + inf_curriculum_supply_mode_applies(INF_CURRICULUM_SUPPLY_MODE_ZUK, 71), 1); + ASSERT_INT_EQ("Zuk mode skips wave 54", + inf_curriculum_supply_mode_applies(INF_CURRICULUM_SUPPLY_MODE_ZUK, 54), 0); + ASSERT_INT_EQ("pre-Zuk mode applies to wave 54", + inf_curriculum_supply_mode_applies(INF_CURRICULUM_SUPPLY_MODE_PRE_ZUK, 54), 1); + ASSERT_INT_EQ("pre-Zuk mode skips wave 69", + inf_curriculum_supply_mode_applies(INF_CURRICULUM_SUPPLY_MODE_PRE_ZUK, 69), 0); +} + +static void test_curriculum_supply_jitter_clamps_to_inventory_bounds(void) { + printf("--- curriculum supply jitter clamps to inventory bounds ---\n"); + + EncounterState* raw_state = inf_create(); + InfernoState* state = (InfernoState*)raw_state; + + inf_put_int(raw_state, "curriculum_agent", 1); + inf_put_int(raw_state, "curriculum_supply_jitter_mode", + INF_CURRICULUM_SUPPLY_MODE_ALL); + inf_put_float(raw_state, "curriculum_supply_shared_jitter", 1.0f); + inf_put_float(raw_state, "curriculum_supply_brew_jitter", 1.0f); + inf_put_float(raw_state, "curriculum_supply_restore_jitter", 1.0f); + reset_inferno_at_public_wave(raw_state, 71, 1.0f); + + ASSERT_INT_EQ("jitter keeps brew nonnegative", + state->player.brew_doses >= 0, 1); + ASSERT_INT_EQ("jitter keeps brew within full supplies", + state->player.brew_doses <= 24, 1); + ASSERT_INT_EQ("jitter keeps restore nonnegative", + state->player.restore_doses >= 0, 1); + ASSERT_INT_EQ("jitter keeps restore within full supplies", + state->player.restore_doses <= 40, 1); + + inf_put_int(raw_state, "curriculum_agent", 0); + inf_put_int(raw_state, "curriculum_supply_jitter_mode", + INF_CURRICULUM_SUPPLY_MODE_OFF); + inf_put_float(raw_state, "curriculum_supply_shared_jitter", 0.0f); + inf_put_float(raw_state, "curriculum_supply_brew_jitter", 0.0f); + inf_put_float(raw_state, "curriculum_supply_restore_jitter", 0.0f); + inf_destroy(raw_state); +} + +static void test_late_start_supply_observations(void) { + printf("--- inferno late-start supply observations ---\n"); + + EncounterState* raw_state = inf_create(); + InfernoState* state = (InfernoState*)raw_state; + InfSupplyDoses full = inf_full_starting_supplies(); + float obs[INF_NUM_OBS]; + + reset_inferno_at_public_wave(raw_state, 69, 1.0f); + inf_write_obs(raw_state, obs); + + enum { + INF_OBS_BREW_DOSES = 11, + INF_OBS_RESTORE_DOSES = 12, + INF_OBS_BASTION_DOSES = 25, + INF_OBS_STAMINA_DOSES = 26, + }; + ASSERT_FLOAT_NEAR("brew obs uses full-kit denominator", + obs[INF_OBS_BREW_DOSES], + (float)state->player.brew_doses / (float)full.brew_doses, 0.0001f); + ASSERT_FLOAT_NEAR("restore obs uses full-kit denominator", + obs[INF_OBS_RESTORE_DOSES], + (float)state->player.restore_doses / (float)full.restore_doses, 0.0001f); + ASSERT_FLOAT_NEAR("bastion obs uses full-kit denominator", + obs[INF_OBS_BASTION_DOSES], + (float)state->player.bastion_doses / (float)full.bastion_doses, 0.0001f); + ASSERT_FLOAT_NEAR("stamina obs uses full-kit denominator", + obs[INF_OBS_STAMINA_DOSES], + (float)state->player.stamina_doses / (float)full.stamina_doses, 0.0001f); + + inf_destroy(raw_state); +} + +static void test_tagged_jad_healer_melee_geometry(void) { + printf("--- tagged jad healer melee geometry ---\n"); + + InfernoState diagonal_state = make_test_state(5, 5); + InfernoState cardinal_state = make_test_state(5, 5); + InfernoState meleer_diagonal_state = make_test_state(5, 5); + + diagonal_state.player.current_defence = 99; + diagonal_state.player.current_magic = 99; + diagonal_state.player.prayer = PRAYER_NONE; + diagonal_state.weapon_set = INF_GEAR_MAGE; + + cardinal_state.player.current_defence = 99; + cardinal_state.player.current_magic = 99; + cardinal_state.player.prayer = PRAYER_NONE; + cardinal_state.weapon_set = INF_GEAR_MAGE; + + meleer_diagonal_state.player.current_defence = 99; + meleer_diagonal_state.player.current_magic = 99; + meleer_diagonal_state.player.prayer = PRAYER_NONE; + meleer_diagonal_state.weapon_set = INF_GEAR_MAGE; + + diagonal_state.npcs[0] = make_test_npc(INF_NPC_HEALER_JAD, 6, 6, 1); + diagonal_state.npcs[0].active = 1; + diagonal_state.npcs[0].aggro_target = -1; + + cardinal_state.npcs[0] = make_test_npc(INF_NPC_HEALER_JAD, 6, 5, 1); + cardinal_state.npcs[0].active = 1; + cardinal_state.npcs[0].aggro_target = -1; + + meleer_diagonal_state.npcs[0] = make_test_npc(INF_NPC_MELEER, 6, 6, 1); + meleer_diagonal_state.npcs[0].active = 1; + meleer_diagonal_state.npcs[0].aggro_target = -1; + + inf_npc_attack(&diagonal_state, 0); + inf_npc_attack(&cardinal_state, 0); + inf_npc_attack(&meleer_diagonal_state, 0); + + ASSERT_INT_EQ("diagonal healer does not attack", diagonal_state.npcs[0].attacked_this_tick, 0); + ASSERT_INT_EQ("diagonal healer keeps attack style none", + diagonal_state.npcs[0].attack_style_this_tick, ATTACK_STYLE_NONE); + ASSERT_INT_EQ("cardinal healer attacks", cardinal_state.npcs[0].attacked_this_tick, 1); + ASSERT_INT_EQ("cardinal healer uses melee", + cardinal_state.npcs[0].attack_style_this_tick, ATTACK_STYLE_MELEE); + ASSERT_INT_EQ("diagonal pure meleer does not attack", + meleer_diagonal_state.npcs[0].attacked_this_tick, 0); + ASSERT_INT_EQ("diagonal pure meleer keeps attack style none", + meleer_diagonal_state.npcs[0].attack_style_this_tick, ATTACK_STYLE_NONE); +} + +static void test_overlap_shuffle_hold_after_recent_target_click(void) { + printf("--- overlap shuffle held after recent target click ---\n"); + + InfernoState state = make_test_state(20, 20); + state.player_last_interaction_target_slot = 0; + state.player_last_interaction_age = 0; + + state.npcs[0] = make_test_npc( + INF_NPC_RANGER, 20, 20, INF_NPC_STATS[INF_NPC_RANGER].size); + state.npcs[0].active = 1; + + inf_rebuild_entity_collision_flags(&state); + inf_npc_move(&state, 0); + + ASSERT_INT_EQ("held overlap keeps x", state.npcs[0].x, 20); + ASSERT_INT_EQ("held overlap keeps y", state.npcs[0].y, 20); + ASSERT_INT_EQ("held overlap does not mark moved", state.npcs[0].moved_this_tick, 0); +} + +static void test_overlap_shuffle_respects_npc_collision_flags(void) { + printf("--- overlap shuffle respects npc collision flags ---\n"); + + const uint32_t west_shuffle_seed = 12345; + + InfernoState clear_state = make_test_state(20, 20); + clear_state.rng_state = west_shuffle_seed; + clear_state.npcs[0] = make_test_npc(INF_NPC_HEALER_JAD, 20, 20, 1); + clear_state.npcs[0].active = 1; + clear_state.npcs[1] = make_test_npc(INF_NPC_HEALER_JAD, 21, 20, 1); + clear_state.npcs[1].active = 1; + clear_state.npcs[2] = make_test_npc(INF_NPC_HEALER_JAD, 20, 21, 1); + clear_state.npcs[2].active = 1; + clear_state.npcs[3] = make_test_npc(INF_NPC_HEALER_JAD, 20, 19, 1); + clear_state.npcs[3].active = 1; + + inf_rebuild_entity_collision_flags(&clear_state); + inf_npc_move(&clear_state, 0); + + ASSERT_INT_EQ("clear sampled overlap shuffle moves west x", clear_state.npcs[0].x, 19); + ASSERT_INT_EQ("clear sampled overlap shuffle moves west y", clear_state.npcs[0].y, 20); + ASSERT_INT_EQ("clear sampled overlap shuffle marks moved", clear_state.npcs[0].moved_this_tick, 1); + + InfernoState blocked_state = make_test_state(20, 20); + blocked_state.rng_state = west_shuffle_seed; + blocked_state.npcs[0] = make_test_npc(INF_NPC_HEALER_JAD, 20, 20, 1); + blocked_state.npcs[0].active = 1; + blocked_state.npcs[1] = make_test_npc(INF_NPC_HEALER_JAD, 21, 20, 1); + blocked_state.npcs[1].active = 1; + blocked_state.npcs[2] = make_test_npc(INF_NPC_HEALER_JAD, 19, 20, 1); + blocked_state.npcs[2].active = 1; + blocked_state.npcs[3] = make_test_npc(INF_NPC_HEALER_JAD, 20, 21, 1); + blocked_state.npcs[3].active = 1; + + inf_rebuild_entity_collision_flags(&blocked_state); + inf_npc_move(&blocked_state, 0); + + ASSERT_INT_EQ("blocked sampled overlap shuffle does not fallback x", blocked_state.npcs[0].x, 20); + ASSERT_INT_EQ("blocked sampled overlap shuffle does not fallback y", blocked_state.npcs[0].y, 20); + ASSERT_INT_EQ("blocked sampled overlap shuffle does not mark moved", + blocked_state.npcs[0].moved_this_tick, 0); +} + +static void test_large_npc_overlap_shuffle_can_partially_unclip(void) { + printf("--- large npc overlap shuffle can partially unclip ---\n"); + + InfernoState state = make_test_state(21, 21); + state.rng_state = 12345; + state.npcs[0] = make_test_npc( + INF_NPC_MAGER, 20, 20, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[0].active = 1; + + inf_rebuild_entity_collision_flags(&state); + inf_npc_move(&state, 0); + + int dx = abs(state.npcs[0].x - 20); + int dy = abs(state.npcs[0].y - 20); + ASSERT_INT_EQ("large npc takes one shuffle step", dx + dy, 1); + ASSERT_INT_EQ("large npc marks moved", state.npcs[0].moved_this_tick, 1); +} + +static void test_player_movement_ignores_npc_collision_flags(void) { + printf("--- player movement ignores npc collision flags ---\n"); + + InfernoState state = make_test_state(18, 30); + state.player_dest_x = 26; + state.player_dest_y = 30; + + state.npcs[0] = make_test_npc(INF_NPC_HEALER_JAD, 19, 30, 1); + state.npcs[0].active = 1; + state.npcs[1] = make_test_npc( + INF_NPC_JAD, 20, 30, INF_NPC_STATS[INF_NPC_JAD].size); + state.npcs[1].active = 1; + + inf_rebuild_entity_collision_flags(&state); + + ASSERT_INT_EQ("healer tile has npc collision flag", + state.npc_collision_flags[19 - INF_ARENA_MIN_X][30 - INF_ARENA_MIN_Y], 1); + ASSERT_INT_EQ("jad tile has npc collision flag", + state.npc_collision_flags[20 - INF_ARENA_MIN_X][30 - INF_ARENA_MIN_Y], 1); + InfWalkCtx walk_ctx = { &state, inf_legacy_context() }; + ASSERT_INT_EQ("player can walk through jad healer tile", + inf_tile_walkable(&walk_ctx, 19, 30), 1); + ASSERT_INT_EQ("player can walk through jad footprint", + inf_tile_walkable(&walk_ctx, 20, 30), 1); + + int steps = encounter_move_toward_dest( + &state.player, &state.player_dest_x, &state.player_dest_y, + inf_legacy_context()->collision_map, + inf_legacy_context()->world_offset_x, + inf_legacy_context()->world_offset_y, + inf_tile_walkable, &walk_ctx, inf_pathfind_blocked, &walk_ctx, + INF_ARENA_MIN_X, INF_ARENA_MIN_Y, INF_ARENA_WIDTH, INF_ARENA_HEIGHT); + + ASSERT_INT_EQ("player runs through npc footprints", steps, 2); + ASSERT_INT_EQ("player reaches jad footprint x", state.player.x, 20); + ASSERT_INT_EQ("player reaches jad footprint y", state.player.y, 30); + + InfMoveCtx move_ctx = { &state, inf_legacy_context(), 0 }; + ASSERT_INT_EQ("npc movement still treats npc collision as blocked", + inf_npc_blocked(&move_ctx, 20, 30, 1), 1); +} + +static void test_tagged_jad_healer_stops_at_melee_contact(void) { + printf("--- tagged jad healer stops at melee contact ---\n"); + + InfernoState state = make_test_state(20, 20); + state.npcs[0] = make_test_npc(INF_NPC_HEALER_JAD, 19, 20, 1); + state.npcs[0].active = 1; + state.npcs[0].aggro_target = -1; + + inf_rebuild_entity_collision_flags(&state); + inf_npc_move(&state, 0); + + ASSERT_INT_EQ("healer keeps melee contact x", state.npcs[0].x, 19); + ASSERT_INT_EQ("healer keeps melee contact y", state.npcs[0].y, 20); + ASSERT_INT_EQ("healer does not mark moved", state.npcs[0].moved_this_tick, 0); +} + +static void test_tagged_jad_healers_queue_behind_front_healer(void) { + printf("--- tagged jad healers queue behind front healer ---\n"); + + InfernoState state = make_test_state(20, 20); + state.player.current_defence = 99; + state.player.current_magic = 99; + state.player.prayer = PRAYER_PROTECT_MAGIC; + state.weapon_set = INF_GEAR_MAGE; + + for (int i = 0; i < 5; i++) { + state.npcs[i] = make_test_npc(INF_NPC_HEALER_JAD, 19 - i, 20, 1); + state.npcs[i].active = 1; + state.npcs[i].aggro_target = -1; + state.npcs[i].attack_timer = 0; + } + + inf_rebuild_entity_collision_flags(&state); + inf_tick_npcs(&state); + + int attacks = 0; + int on_player = 0; + for (int i = 0; i < 5; i++) { + if (state.npcs[i].attacked_this_tick) attacks++; + if (state.npcs[i].x == state.player.x && state.npcs[i].y == state.player.y) + on_player++; + } + + ASSERT_INT_EQ("only front healer attacks", attacks, 1); + ASSERT_INT_EQ("no healer steps onto player", on_player, 0); + ASSERT_INT_EQ("front healer remains first in queue", state.npcs[0].x, 19); + ASSERT_INT_EQ("second healer remains blocked behind front", state.npcs[1].x, 18); +} + +static void test_meleer_dig_can_stack_without_losing_collision_flag(void) { + printf("--- meleer dig can stack without losing collision flag ---\n"); + + InfernoState state = make_test_state(20, 20); + state.npcs[0] = make_test_npc( + INF_NPC_MELEER, 5, 5, INF_NPC_STATS[INF_NPC_MELEER].size); + state.npcs[0].active = 1; + state.npcs[0].dig_freeze_timer = 1; + int dig_x = state.player.x - state.npcs[0].size + 1; + int dig_y = state.player.y - state.npcs[0].size + 1; + state.npcs[1] = make_test_npc( + INF_NPC_RANGER, dig_x, dig_y, INF_NPC_STATS[INF_NPC_RANGER].size); + state.npcs[1].active = 1; + + inf_rebuild_entity_collision_flags(&state); + inf_meleer_dig_check(&state, 0); + + ASSERT_INT_EQ("dig lands on first candidate x", state.npcs[0].x, dig_x); + ASSERT_INT_EQ("dig lands on first candidate y", state.npcs[0].y, dig_y); + ASSERT_INT_EQ("stacked landing tile has both NPCs", + state.npc_collision_flags[dig_x - INF_ARENA_MIN_X][dig_y - INF_ARENA_MIN_Y], 2); +} + +static void test_jad_healer_spawn_offsets_match_wave_67_reference(void) { + printf("--- jad healer spawn offsets match wave 67 reference ---\n"); + + InfernoState state = make_test_state(18, 32); + state.rng_state = 12345; + state.wave = 66; + state.npcs[0] = make_test_npc(INF_NPC_JAD, 23, 30, INF_NPC_STATS[INF_NPC_JAD].size); + state.npcs[0].active = 1; + state.npcs[0].hp = 100; + state.npcs[0].max_hp = 300; + + inf_rebuild_entity_collision_flags(&state); + inf_jad_check_healers(&state, 0); + + int healers = 0; + for (int i = 1; i < INF_MAX_NPCS; i++) { + if (!state.npcs[i].active || state.npcs[i].type != INF_NPC_HEALER_JAD) continue; + healers++; + int dx = state.npcs[i].x - state.npcs[0].x; + int dy = state.npcs[i].y - state.npcs[0].y; + ASSERT_INT_EQ("wave 67 healer owner", inf_npc_healer(&state.npcs[i])->owner_idx, 0); + ASSERT_INT_EQ("wave 67 healer aggro", state.npcs[i].aggro_target, 0); + ASSERT_INT_EQ("wave 67 healer x min", dx >= -5, 1); + ASSERT_INT_EQ("wave 67 healer x max", dx <= 5, 1); + ASSERT_INT_EQ("wave 67 healer y min", dy >= -4, 1); + ASSERT_INT_EQ("wave 67 healer y max", dy <= 10, 1); + ASSERT_INT_EQ("wave 67 healer outside jad footprint", + encounter_entity_footprints_overlap( + state.npcs[i].x, state.npcs[i].y, 1, + state.npcs[0].x, state.npcs[0].y, state.npcs[0].size), + 0); + } + ASSERT_INT_EQ("wave 67 healer count", healers, 5); +} + +static void test_jad_healer_spawn_offsets_match_zuk_reference(void) { + printf("--- jad healer spawn offsets match zuk reference ---\n"); + + InfernoState state = make_test_state(INF_ZUK_PLAYER_START_X, INF_ZUK_PLAYER_START_Y); + state.rng_state = 67890; + state.wave = 68; + state.npcs[0] = make_test_npc(INF_NPC_JAD, 24, 32, INF_NPC_STATS[INF_NPC_JAD].size); + state.npcs[0].active = 1; + state.npcs[0].hp = 100; + state.npcs[0].max_hp = 300; + + inf_rebuild_entity_collision_flags(&state); + inf_jad_check_healers(&state, 0); + + int healers = 0; + for (int i = 1; i < INF_MAX_NPCS; i++) { + if (!state.npcs[i].active || state.npcs[i].type != INF_NPC_HEALER_JAD) continue; + healers++; + int dx = state.npcs[i].x - state.npcs[0].x; + int dy = state.npcs[i].y - state.npcs[0].y; + ASSERT_INT_EQ("zuk healer x min", dx >= 0, 1); + ASSERT_INT_EQ("zuk healer x max", dx <= 5, 1); + ASSERT_INT_EQ("zuk healer y min", dy >= 5, 1); + ASSERT_INT_EQ("zuk healer y max", dy <= 8, 1); + ASSERT_INT_EQ("zuk healer outside jad footprint", + encounter_entity_footprints_overlap( + state.npcs[i].x, state.npcs[i].y, 1, + state.npcs[0].x, state.npcs[0].y, state.npcs[0].size), + 0); + } + ASSERT_INT_EQ("zuk healer count", healers, 3); +} + +static void test_npc_terrain_blocks_full_footprint_lava_shelf(void) { + printf("--- npc terrain blocks full footprint lava shelf ---\n"); + + InfernoState state = make_test_state(INF_ZUK_PLAYER_START_X, INF_ZUK_PLAYER_START_Y); + state.wave = 68; + + ASSERT_INT_EQ("jad footprint y39 fits player arena", + inf_npc_terrain_blocked(&state, 24, 39, INF_NPC_STATS[INF_NPC_JAD].size), 0); + ASSERT_INT_EQ("jad footprint y40 enters lava shelf", + inf_npc_terrain_blocked(&state, 24, 40, INF_NPC_STATS[INF_NPC_JAD].size), 1); + ASSERT_INT_EQ("jad movement y40 is blocked", + inf_npc_environment_blocked(&state, 24, 40, INF_NPC_STATS[INF_NPC_JAD].size), 1); +} + +static void test_zuk_jad_healer_spawn_falls_back_to_passable_arena_tiles(void) { + printf("--- zuk jad healer spawn falls back to passable arena tiles ---\n"); + + InfernoState state = make_test_state(INF_ZUK_PLAYER_START_X, INF_ZUK_PLAYER_START_Y); + state.rng_state = 96969; + state.wave = 68; + state.npcs[0] = make_test_npc(INF_NPC_JAD, 24, 39, INF_NPC_STATS[INF_NPC_JAD].size); + state.npcs[0].active = 1; + state.npcs[0].hp = 100; + state.npcs[0].max_hp = 300; + + inf_rebuild_entity_collision_flags(&state); + inf_jad_check_healers(&state, 0); + + int healers = 0; + for (int i = 1; i < INF_MAX_NPCS; i++) { + if (!state.npcs[i].active || state.npcs[i].type != INF_NPC_HEALER_JAD) continue; + healers++; + int dx = state.npcs[i].x - state.npcs[0].x; + int dy = state.npcs[i].y - state.npcs[0].y; + ASSERT_INT_EQ("fallback healer x min", dx >= -5, 1); + ASSERT_INT_EQ("fallback healer x max", dx <= 5, 1); + ASSERT_INT_EQ("fallback healer y min", dy >= -4, 1); + ASSERT_INT_EQ("fallback healer y max", dy <= 10, 1); + ASSERT_INT_EQ("fallback healer stays in arena", state.npcs[i].y <= INF_ARENA_MAX_Y, 1); + ASSERT_INT_EQ("fallback healer terrain valid", + inf_npc_terrain_blocked(&state, state.npcs[i].x, state.npcs[i].y, 1), 0); + ASSERT_INT_EQ("fallback healer outside jad footprint", + encounter_entity_footprints_overlap( + state.npcs[i].x, state.npcs[i].y, 1, + state.npcs[0].x, state.npcs[0].y, state.npcs[0].size), + 0); + } + ASSERT_INT_EQ("fallback healer count", healers, 3); +} + +static void test_meleer_dig_landing_order(void) { + printf("--- meleer dig landing order ---\n"); + + InfernoState state = make_test_state(20, 20); + state.npcs[0] = make_test_npc( + INF_NPC_MELEER, 5, 5, INF_NPC_STATS[INF_NPC_MELEER].size); + state.npcs[0].active = 1; + state.npcs[0].dig_freeze_timer = 1; + + state.pillars[0].active = 1; + state.pillars[0].x = 17; + state.pillars[0].y = 17; + + inf_meleer_dig_check(&state, 0); + + ASSERT_INT_EQ("blocked first landing candidate falls through to player tile x", state.npcs[0].x, 20); + ASSERT_INT_EQ("blocked first landing candidate falls through to player tile y", state.npcs[0].y, 20); + ASSERT_INT_EQ("dig freeze consumed", state.npcs[0].dig_freeze_timer, 0); + ASSERT_INT_EQ("post-dig stun applied", state.npcs[0].stun_timer, 2); + ASSERT_INT_EQ("post-dig attack delay applied", state.npcs[0].dig_attack_delay, 6); +} + +typedef struct { + int xs[32]; + int ys[32]; + int count; +} ChaseBlockTest; + +static int chase_block_test_has_block(const ChaseBlockTest* t, int x, int y) { + for (int i = 0; i < t->count; i++) { + if (t->xs[i] == x && t->ys[i] == y) return 1; + } + return 0; +} + +static int chase_block_test_walkable(void* ctx, int x, int y) { + const ChaseBlockTest* t = (const ChaseBlockTest*)ctx; + return x >= 0 && x <= 10 && y >= 0 && y <= 10 && + !chase_block_test_has_block(t, x, y); +} + +static int chase_block_test_path_blocked(void* ctx, int x, int y) { + const ChaseBlockTest* t = (const ChaseBlockTest*)ctx; + return chase_block_test_has_block(t, x, y); +} + +static void chase_block_test_run_until_attackable( + Player* player, const ChaseBlockTest* blocks, + int target_x, int target_y, int target_size, int attack_range, + const OsrsLosQuery* los_query +) { + for (int i = 0; i < 12 && !encounter_player_can_attack( + player->x, player->y, + target_x, target_y, target_size, attack_range, + los_query); i++) { + int moved = encounter_chase_attack_target( + player, + target_x, target_y, target_size, attack_range, + NULL, 0, 0, + chase_block_test_walkable, (void*)blocks, + chase_block_test_path_blocked, (void*)blocks, + los_query, + 0, 0, 11, 11); + ASSERT_INT_EQ("continued chase avoids blocked tiles", + chase_block_test_has_block(blocks, player->x, player->y), 0); + if (!moved) break; + } +} + +static void test_attack_chase_uses_reachable_approach_tile(void) { + printf("--- attack chase uses reachable approach tile ---\n"); + + ChaseBlockTest blocks = { + .xs = {1, 1, 1, 2, 2, 3, 3, 3}, + .ys = {4, 5, 6, 4, 6, 4, 5, 6}, + .count = 8, + }; + Player player; + memset(&player, 0, sizeof(player)); + player.x = 0; + player.y = 5; + OsrsLosQuery los_query = osrs_los_open(); + + int moved = encounter_chase_attack_target( + &player, + 5, 5, 1, 3, + NULL, 0, 0, + chase_block_test_walkable, &blocks, + chase_block_test_path_blocked, &blocks, + &los_query, + 0, 0, 11, 11); + + ASSERT_INT_EQ("chase moves despite sealed nearest attack tile", moved, 1); + ASSERT_INT_EQ("chase does not stay on start x", player.x == 0 && player.y == 5, 0); + ASSERT_INT_EQ("chase avoids blocked tiles", + chase_block_test_has_block(&blocks, player.x, player.y), 0); + + chase_block_test_run_until_attackable(&player, &blocks, 5, 5, 1, 3, &los_query); + ASSERT_INT_EQ("chase reaches a reachable attack tile", + encounter_player_can_attack(player.x, player.y, 5, 5, 1, 3, &los_query), 1); +} + +static void test_attack_chase_routes_around_los_blocker_while_in_range(void) { + printf("--- attack chase routes around LOS blocker while in range ---\n"); + + ChaseBlockTest blocks = { + .xs = {2}, + .ys = {0}, + .count = 1, + }; + LOSBlocker pillar = {2, 0, 1, LOS_FULL_MASK}; + OsrsLosQuery los_query = osrs_los_blockers(&pillar, 1); + Player player; + memset(&player, 0, sizeof(player)); + player.x = 0; + player.y = 0; + + ASSERT_INT_EQ("starting tile is range-valid but LOS-blocked", + encounter_player_can_attack(player.x, player.y, 5, 0, 1, 10, &los_query), 0); + + int moved = encounter_chase_attack_target( + &player, + 5, 0, 1, 10, + NULL, 0, 0, + chase_block_test_walkable, &blocks, + chase_block_test_path_blocked, &blocks, + &los_query, + 0, 0, 11, 11); + + ASSERT_INT_EQ("LOS-blocked in-range target causes movement", moved, 1); + ASSERT_INT_EQ("chase avoids LOS blocker tile", + chase_block_test_has_block(&blocks, player.x, player.y), 0); + chase_block_test_run_until_attackable(&player, &blocks, 5, 0, 1, 10, &los_query); + ASSERT_INT_EQ("chase reaches a clear long-range attack tile", + encounter_player_can_attack(player.x, player.y, 5, 0, 1, 10, &los_query), 1); +} + +static void test_attack_chase_routes_around_los_blocker_for_short_range(void) { + printf("--- attack chase routes around LOS blocker for short range ---\n"); + + ChaseBlockTest blocks = { + .xs = {2}, + .ys = {0}, + .count = 1, + }; + LOSBlocker pillar = {2, 0, 1, LOS_FULL_MASK}; + OsrsLosQuery los_query = osrs_los_blockers(&pillar, 1); + Player player; + memset(&player, 0, sizeof(player)); + player.x = 0; + player.y = 0; + + int moved = encounter_chase_attack_target( + &player, + 5, 0, 1, 3, + NULL, 0, 0, + chase_block_test_walkable, &blocks, + chase_block_test_path_blocked, &blocks, + &los_query, + 0, 0, 11, 11); + + ASSERT_INT_EQ("short-range LOS-blocked target causes movement", moved, 1); + ASSERT_INT_EQ("short-range chase avoids LOS blocker tile", + chase_block_test_has_block(&blocks, player.x, player.y), 0); + chase_block_test_run_until_attackable(&player, &blocks, 5, 0, 1, 3, &los_query); + ASSERT_INT_EQ("chase reaches a clear short-range attack tile", + encounter_player_can_attack(player.x, player.y, 5, 0, 1, 3, &los_query), 1); +} + +static void test_melee_fallback_geometry(void) { + printf("--- inferno melee fallback geometry ---\n"); + + InfernoState diagonal_state = make_test_state(5, 5); + InfernoState cardinal_state = make_test_state(5, 5); + InfernoState distant_state = make_test_state(5, 5); + + InfNPC ranger_diagonal = make_test_npc(INF_NPC_RANGER, 6, 6, 1); + InfNPC mager_diagonal = make_test_npc(INF_NPC_MAGER, 6, 6, 1); + InfNPC blob_diagonal = make_test_npc(INF_NPC_BLOB, 6, 6, 1); + InfNPC blob_cardinal = make_test_npc(INF_NPC_BLOB, 6, 5, 1); + InfNPC jad_diagonal = make_test_npc(INF_NPC_JAD, 6, 6, 1); + InfNPC jad_cardinal = make_test_npc(INF_NPC_JAD, 6, 5, 1); + InfNPC blob_distant = make_test_npc(INF_NPC_BLOB, 7, 5, 1); + + InfNPCStats ranged_stats = make_test_stats(ATTACK_STYLE_RANGED); + InfNPCStats magic_stats = make_test_stats(ATTACK_STYLE_MAGIC); + + ASSERT_INT_EQ( + "ranger diagonal melee fallback", + inf_melee_fallback_possible( + &diagonal_state, &ranger_diagonal, &ranged_stats, + ATTACK_STYLE_RANGED, distance_to_player(&diagonal_state, &ranger_diagonal)), + 1); + ASSERT_INT_EQ( + "mager diagonal melee fallback", + inf_melee_fallback_possible( + &diagonal_state, &mager_diagonal, &magic_stats, + ATTACK_STYLE_MAGIC, distance_to_player(&diagonal_state, &mager_diagonal)), + 1); + ASSERT_INT_EQ( + "blob diagonal melee fallback blocked", + inf_melee_fallback_possible( + &diagonal_state, &blob_diagonal, &magic_stats, + ATTACK_STYLE_MAGIC, distance_to_player(&diagonal_state, &blob_diagonal)), + 0); + ASSERT_INT_EQ( + "blob cardinal melee fallback", + inf_melee_fallback_possible( + &cardinal_state, &blob_cardinal, &magic_stats, + ATTACK_STYLE_MAGIC, distance_to_player(&cardinal_state, &blob_cardinal)), + 1); + ASSERT_INT_EQ( + "jad diagonal melee fallback blocked", + inf_melee_fallback_possible( + &diagonal_state, &jad_diagonal, &ranged_stats, + ATTACK_STYLE_RANGED, distance_to_player(&diagonal_state, &jad_diagonal)), + 0); + ASSERT_INT_EQ( + "jad cardinal melee fallback", + inf_melee_fallback_possible( + &cardinal_state, &jad_cardinal, &ranged_stats, + ATTACK_STYLE_RANGED, distance_to_player(&cardinal_state, &jad_cardinal)), + 1); + ASSERT_INT_EQ( + "fallback blocked outside melee distance", + inf_melee_fallback_possible( + &distant_state, &blob_distant, &magic_stats, + ATTACK_STYLE_MAGIC, distance_to_player(&distant_state, &blob_distant)), + 0); + ASSERT_INT_EQ( + "fallback blocked when planned style already melee", + inf_melee_fallback_possible( + &cardinal_state, &blob_cardinal, &magic_stats, + ATTACK_STYLE_MELEE, distance_to_player(&cardinal_state, &blob_cardinal)), + 0); +} + +static void test_style_mask_preview(void) { + printf("--- inferno style mask preview ---\n"); + + InfernoState diagonal_state = make_test_state(5, 5); + InfernoState cardinal_state = make_test_state(5, 5); + InfNPC ranger_diagonal = make_test_npc(INF_NPC_RANGER, 6, 6, 1); + InfNPC blob_cardinal = make_test_npc(INF_NPC_BLOB, 6, 5, 1); + InfNPC blob_diagonal = make_test_npc(INF_NPC_BLOB, 6, 6, 1); + InfNPCStats ranged_stats = make_test_stats(ATTACK_STYLE_RANGED); + InfNPCStats magic_stats = make_test_stats(ATTACK_STYLE_MAGIC); + + int ranger_mask = inf_attack_style_options_mask( + &diagonal_state, &ranger_diagonal, &ranged_stats, + ATTACK_STYLE_RANGED, distance_to_player(&diagonal_state, &ranger_diagonal)); + int blob_cardinal_mask = inf_attack_style_options_mask( + &cardinal_state, &blob_cardinal, &magic_stats, + ATTACK_STYLE_MAGIC, distance_to_player(&cardinal_state, &blob_cardinal)); + int blob_diagonal_mask = inf_attack_style_options_mask( + &diagonal_state, &blob_diagonal, &magic_stats, + ATTACK_STYLE_MAGIC, distance_to_player(&diagonal_state, &blob_diagonal)); + + ASSERT_INT_EQ( + "ranger diagonal preview mask", + ranger_mask, + INF_STYLE_MASK_MELEE | INF_STYLE_MASK_RANGED); + ASSERT_INT_EQ( + "ranger diagonal preview style is uncertain", + inf_attack_style_from_mask(ranger_mask), + ATTACK_STYLE_NONE); + ASSERT_INT_EQ( + "ranger diagonal obs preview keeps ranged primary", + inf_attack_style_obs_preview(ranger_mask), + ATTACK_STYLE_RANGED); + ASSERT_INT_EQ( + "blob cardinal preview mask", + blob_cardinal_mask, + INF_STYLE_MASK_MELEE | INF_STYLE_MASK_MAGIC); + ASSERT_INT_EQ( + "blob cardinal obs preview keeps magic primary", + inf_attack_style_obs_preview(blob_cardinal_mask), + ATTACK_STYLE_MAGIC); + ASSERT_INT_EQ( + "blob diagonal preview keeps magic only", + blob_diagonal_mask, + INF_STYLE_MASK_MAGIC); + ASSERT_INT_EQ( + "blob diagonal preview style is magic", + inf_attack_style_from_mask(blob_diagonal_mask), + ATTACK_STYLE_MAGIC); +} + +static void test_style_choice_sampling(void) { + printf("--- inferno style choice sampling ---\n"); + + uint32_t rng_state = 12345; + int saw_melee = 0; + int saw_ranged = 0; + + for (int i = 0; i < 128; i++) { + int style = inf_choose_attack_style_for_tick( + &rng_state, INF_STYLE_MASK_MELEE | INF_STYLE_MASK_RANGED); + if (style == ATTACK_STYLE_MELEE) saw_melee = 1; + if (style == ATTACK_STYLE_RANGED) saw_ranged = 1; + } + + ASSERT_INT_EQ("50-50 branch can emit melee", saw_melee, 1); + ASSERT_INT_EQ("50-50 branch can emit primary style", saw_ranged, 1); + ASSERT_INT_EQ( + "single-style mask stays deterministic", + inf_choose_attack_style_for_tick(&rng_state, INF_STYLE_MASK_MAGIC), + ATTACK_STYLE_MAGIC); +} + +static void test_dead_mob_store_eligibility(void) { + printf("--- inferno dead mob resurrection eligibility ---\n"); + + ASSERT_INT_EQ("bat resurrectable", inf_dead_mob_is_resurrectable(INF_NPC_BAT), 1); + ASSERT_INT_EQ("blob parent resurrectable", inf_dead_mob_is_resurrectable(INF_NPC_BLOB), 1); + ASSERT_INT_EQ("meleer resurrectable", inf_dead_mob_is_resurrectable(INF_NPC_MELEER), 1); + ASSERT_INT_EQ("ranger resurrectable", inf_dead_mob_is_resurrectable(INF_NPC_RANGER), 1); + ASSERT_INT_EQ("mager resurrectable", inf_dead_mob_is_resurrectable(INF_NPC_MAGER), 1); + + ASSERT_INT_EQ("nibbler not resurrectable", inf_dead_mob_is_resurrectable(INF_NPC_NIBBLER), 0); + ASSERT_INT_EQ("blob melee split not resurrectable", inf_dead_mob_is_resurrectable(INF_NPC_BLOB_MELEE), 0); + ASSERT_INT_EQ("blob range split not resurrectable", inf_dead_mob_is_resurrectable(INF_NPC_BLOB_RANGE), 0); + ASSERT_INT_EQ("blob mage split not resurrectable", inf_dead_mob_is_resurrectable(INF_NPC_BLOB_MAGE), 0); + ASSERT_INT_EQ("jad not resurrectable", inf_dead_mob_is_resurrectable(INF_NPC_JAD), 0); +} + +static void test_resurrected_mob_does_not_reenter_dead_store(void) { + printf("--- resurrected mob does not reenter dead store ---\n"); + + InfernoState state = make_test_state(25, 16); + state.wave = 35; + + state.npcs[0] = make_test_npc(INF_NPC_MAGER, 20, 20, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + + state.dead_mobs[0].type = INF_NPC_RANGER; + state.dead_mobs[0].x = 18; + state.dead_mobs[0].y = 18; + state.dead_mobs[0].hp = INF_NPC_STATS[INF_NPC_RANGER].hp / 2; + state.dead_mobs[0].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + state.dead_mob_count = 1; + + ASSERT_INT_EQ("resurrection succeeds", force_mager_resurrect(&state, 0), 1); + ASSERT_INT_EQ("dead store consumed", state.dead_mob_count, 0); + + int resurrected_slot = -1; + for (int i = 1; i < INF_MAX_NPCS; i++) { + if (state.npcs[i].active && state.npcs[i].type == INF_NPC_RANGER) { + resurrected_slot = i; + break; + } + } + ASSERT_INT_EQ("resurrected ranger spawned", resurrected_slot >= 0, 1); + ASSERT_INT_EQ("respawned ranger marked resurrected", + state.npcs[resurrected_slot].resurrection_count, 1); + + inf_store_dead_mob(&state, &state.npcs[resurrected_slot]); + ASSERT_INT_EQ("resurrected ranger not re-added", state.dead_mob_count, 0); +} + +static void test_blob_split_waits_for_death_removal(void) { + printf("--- blob split waits for death removal ---\n"); + + InfernoState state = make_test_state(25, 16); + state.wave = 30; + state.npcs[0] = make_test_npc( + INF_NPC_BLOB, 10, 10, INF_NPC_STATS[INF_NPC_BLOB].size); + state.npcs[0].active = 1; + state.npcs[0].hp = 0; + state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_BLOB].hp; + + inf_apply_npc_death(&state, 0); + + ASSERT_INT_EQ("dead blob remains during death linger", state.npcs[0].active, 1); + ASSERT_INT_EQ("dead blob death linger starts", state.npcs[0].death_ticks, INF_NPC_DEATH_LINGER_TICKS); + ASSERT_INT_EQ("blob parent enters mager resurrection store", state.dead_mob_count, 1); + ASSERT_INT_EQ("blob parent store type", state.dead_mobs[0].type, INF_NPC_BLOB); + ASSERT_INT_EQ("blob split melee not spawned before removal", + count_active_npc_type(&state, INF_NPC_BLOB_MELEE), 0); + ASSERT_INT_EQ("blob split range not spawned before removal", + count_active_npc_type(&state, INF_NPC_BLOB_RANGE), 0); + ASSERT_INT_EQ("blob split mage not spawned before removal", + count_active_npc_type(&state, INF_NPC_BLOB_MAGE), 0); + + for (int t = 0; t < INF_NPC_DEATH_LINGER_TICKS; t++) + inf_tick_npcs(&state); + + ASSERT_INT_EQ("blob parent removed after death linger", state.npcs[0].active, 0); + ASSERT_INT_EQ("blob split melee spawned after removal", + find_active_npc_type_at(&state, INF_NPC_BLOB_MELEE, 10, 10) >= 0, 1); + ASSERT_INT_EQ("blob split range spawned after removal", + find_active_npc_type_at(&state, INF_NPC_BLOB_RANGE, 11, 11) >= 0, 1); + ASSERT_INT_EQ("blob split mage spawned after removal", + find_active_npc_type_at(&state, INF_NPC_BLOB_MAGE, 12, 12) >= 0, 1); + ASSERT_INT_EQ("blob split melee not resurrectable", + state.dead_mob_count, 1); + + int range_idx = find_active_npc_type_at(&state, INF_NPC_BLOB_RANGE, 11, 11); + int melee_idx = find_active_npc_type_at(&state, INF_NPC_BLOB_MELEE, 10, 10); + int mage_idx = find_active_npc_type_at(&state, INF_NPC_BLOB_MAGE, 12, 12); + ASSERT_INT_EQ("split range cooldown", state.npcs[range_idx].attack_timer, 4); + ASSERT_INT_EQ("split melee cooldown", state.npcs[melee_idx].attack_timer, 4); + ASSERT_INT_EQ("split mage cooldown", state.npcs[mage_idx].attack_timer, 4); +} + +static void test_mager_resurrection_render_event_is_not_magic_projectile(void) { + printf("--- mager resurrection render event is not magic projectile ---\n"); + + InfernoState state = make_test_state(25, 16); + state.wave = 35; + state.player.current_defence = 99; + state.player.current_magic = 99; + state.weapon_set = INF_GEAR_MAGE; + state.npcs[0] = make_test_npc( + INF_NPC_MAGER, 30, 20, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + state.npcs[0].attack_timer = 0; + state.dead_mobs[0].type = INF_NPC_RANGER; + state.dead_mobs[0].x = 18; + state.dead_mobs[0].y = 18; + state.dead_mobs[0].hp = INF_NPC_STATS[INF_NPC_RANGER].hp / 2; + state.dead_mobs[0].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + state.dead_mob_count = 1; + + ASSERT_INT_EQ("mager attack converted into resurrection", + force_mager_attack_resurrection(&state, 0), 1); + ASSERT_INT_EQ("resurrection is marked for render", + state.npcs[0].resurrecting_this_tick, 1); + ASSERT_INT_EQ("resurrection does not expose a magic attack style", + state.npcs[0].attack_style_this_tick, ATTACK_STYLE_NONE); + ASSERT_INT_EQ("resurrected ranger attack delay", + state.npcs[state.npcs[0].resurrection_visual_target].attack_timer, + INF_NPC_STATS[INF_NPC_RANGER].attack_speed); + + RenderEntity entities[4]; + int count = 0; + inf_fill_render_entities((EncounterState*)&state, entities, 4, &count); + ASSERT_INT_EQ("resurrection render has mager and resurrected mob", count >= 3, 1); + ASSERT_INT_EQ("mager uses resurrection animation", + entities[1].npc_anim_id, INF_GEN_ANIM_MAGER_RESURRECT); + ASSERT_INT_EQ("mager faces resurrected mob", + entities[1].dest_x, + state.npcs[state.npcs[0].resurrection_visual_target].x + + state.npcs[state.npcs[0].resurrection_visual_target].size / 2); + + EncounterOverlay ov; + memset(&ov, 0, sizeof(ov)); + inf_render_post_tick((EncounterState*)&state, &ov); + ASSERT_INT_EQ("resurrection does not emit magic projectile", ov.projectile_count, 0); +} + +static void test_double_mager_wave_resurrection_limit(void) { + printf("--- double mager wave respects once-only resurrection ---\n"); + + InfernoState state = make_test_state(25, 16); + state.wave = 65; + + state.npcs[0] = make_test_npc(INF_NPC_MAGER, 18, 18, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + + state.dead_mobs[0].type = INF_NPC_MAGER; + state.dead_mobs[0].x = 22; + state.dead_mobs[0].y = 22; + state.dead_mobs[0].hp = INF_NPC_STATS[INF_NPC_MAGER].hp / 2; + state.dead_mobs[0].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + state.dead_mob_count = 1; + + ASSERT_INT_EQ("first mage resurrection succeeds", force_mager_resurrect(&state, 0), 1); + + int resurrected_slot = -1; + for (int i = 1; i < INF_MAX_NPCS; i++) { + if (state.npcs[i].active && state.npcs[i].type == INF_NPC_MAGER) { + resurrected_slot = i; + break; + } + } + ASSERT_INT_EQ("second mager spawned", resurrected_slot >= 0, 1); + ASSERT_INT_EQ("respawned mager marked resurrected", + state.npcs[resurrected_slot].resurrection_count, 1); + + inf_store_dead_mob(&state, &state.npcs[0]); + ASSERT_INT_EQ("original mage can still enter dead store once", state.dead_mob_count, 1); + + ASSERT_INT_EQ("resurrected mage can resurrect the original once", + force_mager_resurrect(&state, resurrected_slot), 1); + + int original_respawn_slot = -1; + for (int i = 1; i < INF_MAX_NPCS; i++) { + if (i == resurrected_slot) continue; + if (state.npcs[i].active && state.npcs[i].type == INF_NPC_MAGER && + state.npcs[i].resurrection_count == 1) { + original_respawn_slot = i; + break; + } + } + ASSERT_INT_EQ("original mage respawned once", original_respawn_slot >= 0, 1); + + inf_store_dead_mob(&state, &state.npcs[resurrected_slot]); + ASSERT_INT_EQ("already-resurrected mage stays out of store", state.dead_mob_count, 0); + inf_store_dead_mob(&state, &state.npcs[original_respawn_slot]); + ASSERT_INT_EQ("re-resurrected original mage stays out of store", state.dead_mob_count, 0); +} + +static void test_pending_hit_obs_timer_prefers_prayer_window(void) { + printf("--- pending hit obs timer prefers prayer window ---\n"); + + EncounterPendingHit jad_hit = {0}; + jad_hit.check_prayer = 1; + jad_hit.prayer_check_delay = 3; + jad_hit.ticks_remaining = 4; + + EncounterPendingHit normal_hit = {0}; + normal_hit.check_prayer = 0; + normal_hit.prayer_check_delay = 0; + normal_hit.ticks_remaining = 2; + + ASSERT_INT_EQ("jad timer uses prayer window", inf_pending_hit_obs_timer(&jad_hit), 3); + ASSERT_INT_EQ("normal timer uses travel time", inf_pending_hit_obs_timer(&normal_hit), 2); +} + +static void test_blob_attacks_player_on_six_tick_cadence(void) { + printf("--- blob attacks the player on a 6-tick cadence ---\n"); + + InfernoState state = make_test_state(10, 10); + state.player.current_defence = 99; + state.player.current_magic = 99; + state.player.prayer = PRAYER_NONE; + state.weapon_set = INF_GEAR_MAGE; + + state.npcs[0] = make_test_npc( + INF_NPC_BLOB, 20, 10, INF_NPC_STATS[INF_NPC_BLOB].size); + state.npcs[0].active = 1; + state.npcs[0].hp = INF_NPC_STATS[INF_NPC_BLOB].hp; + + int prev_scanned = state.npcs[0].blob_scanned_prayer; + int last_fire = -1, gap_a = -1, gap_b = -1; + for (int tick = 0; tick < 40; tick++) { + inf_npc_attack(&state, 0); + int cur_scanned = state.npcs[0].blob_scanned_prayer; + if (prev_scanned >= 0 && cur_scanned < 0) { + if (last_fire >= 0) { + if (gap_a < 0) gap_a = tick - last_fire; + else if (gap_b < 0) gap_b = tick - last_fire; + } + last_fire = tick; + } + prev_scanned = cur_scanned; + } + + ASSERT_INT_EQ("blob fire-to-fire cadence is 6 ticks", gap_a, 6); + ASSERT_INT_EQ("blob cadence stays 6 across cycles", gap_b, 6); +} + +static void test_jad_has_no_pre_fire_style_preview(void) { + printf("--- jad has no pre-fire style preview ---\n"); + + InfernoState state = make_test_state(10, 10); + state.player.current_defence = 99; + state.player.current_magic = 99; + state.player.prayer = PRAYER_NONE; + state.weapon_set = INF_GEAR_MAGE; + state.wave = 66; + + state.npcs[0] = make_test_npc( + INF_NPC_JAD, 20, 10, INF_NPC_STATS[INF_NPC_JAD].size); + state.npcs[0].active = 1; + state.npcs[0].attack_timer = 2; + + inf_npc_attack(&state, 0); + + ASSERT_INT_EQ("jad timer decrements without preview", state.npcs[0].attack_timer, 1); + ASSERT_INT_EQ("jad style stays hidden before fire", inf_npc_jad(&state.npcs[0])->attack_style, ATTACK_STYLE_NONE); + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + ASSERT_FLOAT_NEAR("prayer-critical timer ignores hidden jad style", obs[INF_OBS_PRAYER_TIMER], 1.0f, 1e-6f); + ASSERT_INT_EQ("prayer-critical style stays zero before fire", + (int)(obs[INF_OBS_PRAYER_MELEE] + obs[INF_OBS_PRAYER_RANGED] + obs[INF_OBS_PRAYER_MAGIC]), 0); +} + +static void test_jad_fire_tick_exposes_three_tick_prayer_deadline(void) { + printf("--- jad fire tick exposes three tick prayer deadline ---\n"); + + InfernoState state; + init_jad_timing_test_state(&state, 10, 10, 16, 10); + + step_inferno_with_prayer(&state, 0); + + ASSERT_INT_EQ("jad attack queued one pending hit", state.player_pending_hits.count, 1); + ASSERT_INT_EQ("jad style resets after firing", inf_npc_jad(&state.npcs[0])->attack_style, ATTACK_STYLE_NONE); + ASSERT_INT_EQ("jad pending hit shows three tick prayer delay after fire", state.player_pending_hits.hits[0].prayer_check_delay, 3); + ASSERT_INT_EQ("jad close-range hit lands four ticks after fire", state.player_pending_hits.hits[0].ticks_remaining, 4); + + float obs[INF_NUM_OBS]; + memset(obs, 0, sizeof(obs)); + inf_write_obs((EncounterState*)&state, obs); + ASSERT_FLOAT_NEAR("prayer-critical timer exposes jad fire deadline", obs[INF_OBS_PRAYER_TIMER], 0.3f, 1e-6f); + ASSERT_FLOAT_NEAR("prayer-critical magic style exposed after fire", obs[INF_OBS_PRAYER_MAGIC], 1.0f, 1e-6f); + int pending_start = inferno_pending_hit_obs_start(); + ASSERT_FLOAT_NEAR("pending hit obs timer uses prayer window", obs[pending_start + 3], 0.3f, 1e-6f); + ASSERT_FLOAT_NEAR("pending hit pre-check damage exposes max threat", obs[pending_start + 4], 113.0f / 150.0f, 1e-6f); +} + +static void test_jad_prayer_on_third_tick_blocks(void) { + printf("--- jad prayer on third tick blocks ---\n"); + + InfernoState state; + init_jad_timing_test_state(&state, 10, 10, 16, 10); + + step_inferno_with_prayer(&state, 0); + step_inferno_with_prayer(&state, 0); + step_inferno_with_prayer(&state, 0); + step_inferno_with_prayer(&state, ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC); + + ASSERT_INT_EQ("jad prayer check consumed pending protection", state.player_pending_hits.hits[0].check_prayer, 0); + ASSERT_INT_EQ("jad protected damage is frozen at zero", state.player_pending_hits.hits[0].damage, 0); + ASSERT_INT_EQ("jad prayer check counted correct prayer", state.tick_scratch.prayer_correct, 1); + + step_inferno_with_prayer(&state, 0); + ASSERT_INT_EQ("jad protected hit removed after landing", state.player_pending_hits.count, 0); + ASSERT_INT_EQ("jad protected hit leaves player hp unchanged", state.player.current_hitpoints, 99); +} + +static void test_jad_prayer_first_on_fourth_tick_does_not_block(void) { + printf("--- jad prayer first on fourth tick does not block ---\n"); + + int saw_late_damage = 0; + for (uint32_t seed = 1; seed < 10000 && !saw_late_damage; seed++) { + InfernoState state; + init_jad_timing_test_state(&state, 10, 10, 16, 10); + state.rng_state = seed; + + step_inferno_with_prayer(&state, 0); + step_inferno_with_prayer(&state, 0); + step_inferno_with_prayer(&state, 0); + step_inferno_with_prayer(&state, 0); + ASSERT_INT_EQ("late-prayer test reaches checked pending hit", state.player_pending_hits.hits[0].check_prayer, 0); + + step_inferno_with_prayer(&state, ENCOUNTER_OVERHEAD_SET_REFRESH_MAGIC); + if (state.tick_scratch.damage_received > 0.0f) { + saw_late_damage = 1; + ASSERT_INT_EQ("late prayer did not block queued jad damage", state.player.current_hitpoints < 99, 1); + } + } + ASSERT_INT_EQ("found a seed where late jad prayer takes damage", saw_late_damage, 1); +} + +static void test_jad_long_distance_damage_uses_delayed_projectile_landing(void) { + printf("--- jad long distance damage uses delayed projectile landing ---\n"); + + int saw_expected_landing = 0; + for (uint32_t seed = 1; seed < 10000 && !saw_expected_landing; seed++) { + InfernoState state; + init_jad_timing_test_state(&state, 10, 10, 36, 10); + state.rng_state = seed; + + int dist = encounter_projectile_distance( + state.npcs[0].x, state.npcs[0].y, state.npcs[0].size, + state.player.x, state.player.y, 1, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + EncounterProjectileTiming timing = + inf_npc_projectile_timing(INF_NPC_JAD, ATTACK_STYLE_MAGIC, dist); + int expected_landing_after_fire = + INF_JAD_PROJECTILE_DELAY + timing.damage_delay_ticks; + + step_inferno_with_prayer(&state, 0); + for (int t = 1; t < expected_landing_after_fire; t++) { + step_inferno_with_prayer(&state, 0); + ASSERT_FLOAT_NEAR("jad long-distance hit has not landed early", state.tick_scratch.damage_received, 0.0f, 1e-6f); + } + step_inferno_with_prayer(&state, 0); + if (state.tick_scratch.damage_received > 0.0f) { + saw_expected_landing = 1; + } + } + ASSERT_INT_EQ("found a seed where long-distance jad damage lands on expected tick", saw_expected_landing, 1); +} + +static void test_triple_jad_pending_threats_fit_obs_layout(void) { + printf("--- triple jad pending threats fit obs layout ---\n"); + + InfernoState state; + init_jad_timing_test_state(&state, 25, 30, 18, 33); + state.wave = 67; + state.npcs[1] = make_test_npc(INF_NPC_JAD, 28, 33, INF_NPC_STATS[INF_NPC_JAD].size); + state.npcs[1].active = 1; + state.npcs[1].attack_timer = 0; + inf_npc_jad(&state.npcs[1])->attack_style = ATTACK_STYLE_RANGED; + state.npcs[2] = make_test_npc(INF_NPC_JAD, 23, 22, INF_NPC_STATS[INF_NPC_JAD].size); + state.npcs[2].active = 1; + state.npcs[2].attack_timer = 0; + inf_npc_jad(&state.npcs[2])->attack_style = ATTACK_STYLE_MAGIC; + + step_inferno_with_prayer(&state, 0); + + ASSERT_INT_EQ("triple jad queues three pending threats", state.player_pending_hits.count, 3); + for (int h = 0; h < state.player_pending_hits.count; h++) { + ASSERT_INT_EQ("each jad threat keeps three tick prayer deadline", state.player_pending_hits.hits[h].prayer_check_delay, 3); + } + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + ASSERT_INT_EQ("inferno obs shape includes exact spark slots", INF_NUM_OBS, 1570); +} + +static void test_inferno_obs_shape_includes_step_out_forecast_features(void) { + printf("--- inferno obs shape includes step-out forecast features ---\n"); + + ASSERT_INT_EQ("gear action head removed tank slot", + INF_ACTION_DIMS[INF_HEAD_GEAR], 4); + ASSERT_INT_EQ("prayer action head includes redemption", + INF_ACTION_DIMS[INF_HEAD_PRAYER], ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION); + ASSERT_INT_EQ("action mask includes redemption", + INF_ACTION_MASK_SIZE, 89); + ASSERT_SOURCE_BLOCK_CONTAINS("native binding reuses inferno action dims", + "ocean/osrs_inferno/osrs_inferno.h", + "#define OBS_SIZE INF_NUM_OBS", + "typedef float obs_t;", + "#define ACT_SIZES INF_ACTION_DIMS_INIT"); + ASSERT_INT_EQ("player obs includes NPC pressure summary", + INF_PLAYER_OBS_SIZE, 75); + ASSERT_INT_EQ("pillar obs includes footprint size", + INF_PILLAR_OBS_SIZE, 15); + ASSERT_INT_EQ("npc obs includes loadout reachability signals", + INF_TOTAL_NPC_OBS_SIZE, 896); + ASSERT_INT_EQ("step-out forecast covers every movement action", + INF_STEP_OUT_FORECAST_OBS_SIZE, 200); + ASSERT_INT_EQ("inferno obs shape includes exact spark landings", + INF_PENDING_SPARK_OBS_SIZE, 224); + ASSERT_INT_EQ("inferno obs shape includes reachability pass", + INF_NUM_OBS, 1570); + ASSERT_INFERNO_SOURCE_NOT_CONTAINS("armor_tank state is removed", + "armor_tank"); + ASSERT_INFERNO_SOURCE_NOT_CONTAINS("extra npc obs scaffold is removed", + "INF_EXTRA_NPC_OBS_FEATURES"); + ASSERT_INFERNO_SOURCE_NOT_CONTAINS("legacy npc pending-hit count is removed", + "pending_hit_count"); + ASSERT_INFERNO_SOURCE_NOT_CONTAINS("legacy player pending-hit count is removed", + "player_pending_hit_count"); + ASSERT_SOURCE_NOT_CONTAINS("binding legacy npc pending-hit count is removed", + "ocean/osrs_inferno/osrs_inferno.h", "pending_hit_count"); + ASSERT_SOURCE_NOT_CONTAINS("binding legacy player pending-hit count is removed", + "ocean/osrs_inferno/osrs_inferno.h", "player_pending_hit_count"); +} + +static void test_inferno_obs_wave_phase_one_hot(void) { + printf("--- inferno obs wave phase one hot ---\n"); + + int waves[6] = {1, 18, 35, 50, 67, 69}; + for (int phase = 0; phase < 6; phase++) { + InfernoState state = make_test_state(20, 20); + state.wave = waves[phase] - 1; + state.player.current_hitpoints = 99; + state.player.base_hitpoints = 99; + state.player.base_prayer = 99; + state.player.current_prayer = 99; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + for (int i = 0; i < 6; i++) { + float expected = (i == phase) ? 1.0f : 0.0f; + ASSERT_FLOAT_NEAR("wave phase one hot", obs[INF_OBS_WAVE_PHASE_START + i], + expected, 1e-6f); + } + } + + InfernoState triple_jad = make_test_state(20, 20); + triple_jad.wave = 67; + triple_jad.player.current_hitpoints = 99; + triple_jad.player.base_hitpoints = 99; + triple_jad.player.base_prayer = 99; + triple_jad.player.current_prayer = 99; + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&triple_jad, obs); + ASSERT_FLOAT_NEAR("wave 68 stays in Jad phase", + obs[INF_OBS_WAVE_PHASE_START + 4], 1.0f, 1e-6f); +} + +static void test_inferno_obs_exposes_pillar_footprint_size(void) { + printf("--- inferno obs exposes pillar footprint size ---\n"); + + InfernoState state = make_test_state(20, 20); + state.player.current_hitpoints = 99; + state.player.base_hitpoints = 99; + state.player.base_prayer = 99; + state.player.current_prayer = 99; + state.pillars[0] = (InfPillar){ + .x = INF_PILLAR_POS[0][0], + .y = INF_PILLAR_POS[0][1], + .hp = INF_PILLAR_HP, + .active = 1, + }; + state.pillars[1] = (InfPillar){ + .x = INF_PILLAR_POS[1][0], + .y = INF_PILLAR_POS[1][1], + .hp = 0, + .active = 0, + }; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int active_start = inferno_pillar_obs_start(0); + ASSERT_FLOAT_NEAR("active pillar active bit", + obs[active_start], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("active pillar hp", + obs[active_start + 1], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("active pillar relative x", + obs[active_start + 2], + (float)(INF_PILLAR_POS[0][0] - state.player.x) / (float)INF_ARENA_WIDTH, + 1e-6f); + ASSERT_FLOAT_NEAR("active pillar relative y", + obs[active_start + 3], + (float)(INF_PILLAR_POS[0][1] - state.player.y) / (float)INF_ARENA_HEIGHT, + 1e-6f); + ASSERT_FLOAT_NEAR("active pillar footprint size", + obs[active_start + 4], (float)INF_PILLAR_SIZE / 7.0f, 1e-6f); + + int inactive_start = inferno_pillar_obs_start(1); + ASSERT_FLOAT_NEAR("inactive pillar footprint size", + obs[inactive_start + 4], 0.0f, 1e-6f); +} + +static void test_inferno_obs_exposes_meleer_dig_state(void) { + printf("--- inferno obs exposes meleer dig state ---\n"); + + InfernoState state = make_test_state(20, 20); + state.player.current_hitpoints = 99; + state.player.base_hitpoints = 99; + state.player.base_prayer = 99; + state.player.current_prayer = 99; + state.npcs[0] = make_test_npc( + INF_NPC_MELEER, 24, 20, INF_NPC_STATS[INF_NPC_MELEER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_MELEER].hp; + state.npcs[0].no_los_ticks = 25; + state.npcs[0].dig_freeze_timer = 3; + state.npcs[0].dig_attack_delay = 6; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int meleer_slot = 4; + int dig_start = inferno_obs_slot_dig_index(meleer_slot); + ASSERT_INT_EQ("meleer occupies first meleer slot", + state.current_obs_slots[meleer_slot], 0); + ASSERT_FLOAT_NEAR("meleer no-los dig progress", + obs[dig_start], 0.5f, 1e-6f); + ASSERT_FLOAT_NEAR("meleer emerge timer", + obs[dig_start + 1], 0.5f, 1e-6f); + ASSERT_FLOAT_NEAR("meleer post-dig attack delay", + obs[dig_start + 2], 1.0f, 1e-6f); + +} + +static void test_jad_special_wave_spawn_cadence_matches_reference(void) { + printf("--- jad special wave spawn cadence matches reference ---\n"); + + InfernoState single = make_test_state(0, 0); + single.wave = 66; + inf_spawn_wave(&single); + + int single_jad = -1; + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (single.npcs[i].active && single.npcs[i].type == INF_NPC_JAD) { + single_jad = i; + break; + } + } + ASSERT_INT_EQ("wave 67 spawns one jad", single_jad >= 0, 1); + ASSERT_INT_EQ("wave 67 jad stun", single.npcs[single_jad].stun_timer, 1); + ASSERT_INT_EQ("wave 67 jad attack speed timer", single.npcs[single_jad].attack_timer, 8); + + InfernoState triple = make_test_state(0, 0); + triple.wave = 67; + triple.rng_state = 12345; + inf_spawn_wave(&triple); + + int num_jads = 0; + int stun_sum = 0; + int stun_product = 1; + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (!triple.npcs[i].active || triple.npcs[i].type != INF_NPC_JAD) + continue; + num_jads++; + stun_sum += triple.npcs[i].stun_timer; + stun_product *= triple.npcs[i].stun_timer; + ASSERT_INT_EQ("wave 68 jad attack timer includes stun offset", + triple.npcs[i].attack_timer, 9 + triple.npcs[i].stun_timer); + } + ASSERT_INT_EQ("wave 68 spawns three jads", num_jads, 3); + ASSERT_INT_EQ("wave 68 shuffled stun sum", stun_sum, 12); + ASSERT_INT_EQ("wave 68 shuffled stun product", stun_product, 28); +} + +static void test_triple_jad_first_attacks_are_staggered(void) { + printf("--- triple jad first attacks are staggered ---\n"); + + InfernoState state; + memset(&state, 0, sizeof(state)); + test_config()->start_wave = 67; + test_config()->late_start_supply_profile_scale = 1.0f; + inf_reset((EncounterState*)&state, 12345); + state.wave_ready_delay = 0; + + int jad_slots[3] = { -1, -1, -1 }; + int first_attack_ticks[3] = { 0, 0, 0 }; + int num_jads = 0; + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (!state.npcs[i].active || state.npcs[i].type != INF_NPC_JAD) + continue; + if (num_jads < 3) jad_slots[num_jads] = i; + num_jads++; + } + ASSERT_INT_EQ("wave 68 first-attack test has three jads", num_jads, 3); + + for (int tick = 1; tick <= 32; tick++) { + step_inferno_noop(&state); + for (int j = 0; j < 3; j++) { + int slot = jad_slots[j]; + if (slot < 0) continue; + if (first_attack_ticks[j] == 0 && state.npcs[slot].attacked_this_tick) + first_attack_ticks[j] = tick; + } + } + + for (int a = 0; a < 2; a++) { + for (int b = a + 1; b < 3; b++) { + if (first_attack_ticks[b] < first_attack_ticks[a]) { + int tmp = first_attack_ticks[a]; + first_attack_ticks[a] = first_attack_ticks[b]; + first_attack_ticks[b] = tmp; + } + } + } + + ASSERT_INT_EQ("wave 68 first jad attacks", first_attack_ticks[0] > 0, 1); + ASSERT_INT_EQ("wave 68 second jad attacks", first_attack_ticks[1] > 0, 1); + ASSERT_INT_EQ("wave 68 third jad attacks", first_attack_ticks[2] > 0, 1); + ASSERT_INT_EQ("wave 68 first attack gap 1", first_attack_ticks[1] - first_attack_ticks[0], 3); + ASSERT_INT_EQ("wave 68 first attack gap 2", first_attack_ticks[2] - first_attack_ticks[1], 3); +} + +static void test_jad_melee_stays_instant_and_untelegraphed(void) { + printf("--- jad melee stays instant and untelegraphed ---\n"); + + InfernoState preview_state = make_test_state(5, 5); + preview_state.player.current_defence = 99; + preview_state.player.current_magic = 99; + preview_state.player.prayer = PRAYER_NONE; + preview_state.weapon_set = INF_GEAR_MAGE; + preview_state.wave = 66; + + preview_state.npcs[0] = make_test_npc( + INF_NPC_JAD, 6, 5, INF_NPC_STATS[INF_NPC_JAD].size); + preview_state.npcs[0].active = 1; + preview_state.npcs[0].attack_timer = 1; + inf_npc_jad(&preview_state.npcs[0])->attack_style = ATTACK_STYLE_RANGED; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&preview_state, obs); + + ASSERT_FLOAT_NEAR( + "jad prayer-critical preview does not advertise melee fallback", + obs[INF_OBS_PRAYER_CONFLICT], 1.0f / 3.0f, 1e-6f); + ASSERT_INT_EQ( + "jad prayer-critical preview keeps ranged one-hot", + (int)(obs[INF_OBS_PRAYER_MELEE] + obs[INF_OBS_PRAYER_RANGED] + obs[INF_OBS_PRAYER_MAGIC]), + 1); + ASSERT_FLOAT_NEAR( + "jad preview keeps ranged as the visible style", + obs[INF_OBS_PRAYER_RANGED], 1.0f, 1e-6f); + + int saw_melee = 0; + for (uint32_t seed = 0; seed < 256; seed++) { + InfernoState attack_state = make_test_state(5, 5); + attack_state.rng_state = seed; + attack_state.player.current_defence = 99; + attack_state.player.current_magic = 99; + attack_state.player.prayer = PRAYER_NONE; + attack_state.weapon_set = INF_GEAR_MAGE; + attack_state.wave = 66; + + attack_state.npcs[0] = make_test_npc( + INF_NPC_JAD, 6, 5, INF_NPC_STATS[INF_NPC_JAD].size); + attack_state.npcs[0].active = 1; + attack_state.npcs[0].attack_timer = 0; + inf_npc_jad(&attack_state.npcs[0])->attack_style = ATTACK_STYLE_RANGED; + + inf_npc_attack(&attack_state, 0); + + if (attack_state.npcs[0].attack_style_this_tick == ATTACK_STYLE_MELEE) { + saw_melee = 1; + ASSERT_INT_EQ( + "jad melee fallback does not queue a pending hit", + attack_state.player_pending_hits.count, 0); + break; + } + } + + ASSERT_INT_EQ("jad can still choose melee instantly at fire time", saw_melee, 1); +} + +static int inferno_obs_slot_type(int slot_idx) { + if (slot_idx >= 0 && slot_idx < 2) return INF_NPC_MAGER; + if (slot_idx >= 2 && slot_idx < 4) return INF_NPC_RANGER; + if (slot_idx >= 4 && slot_idx < 6) return INF_NPC_MELEER; + if (slot_idx >= 6 && slot_idx < 8) return INF_NPC_BLOB; + if (slot_idx >= 8 && slot_idx < 10) return INF_NPC_BAT; + if (slot_idx >= 10 && slot_idx < 12) return INF_NPC_BLOB_MAGE; + if (slot_idx >= 12 && slot_idx < 14) return INF_NPC_BLOB_RANGE; + if (slot_idx >= 14 && slot_idx < 16) return INF_NPC_BLOB_MELEE; + if (slot_idx >= 16 && slot_idx < 22) return INF_NPC_NIBBLER; + if (slot_idx >= 22 && slot_idx < 25) return INF_NPC_JAD; + if (slot_idx == 25) return INF_NPC_ZUK; + if (slot_idx == 26) return INF_NPC_ZUK_SHIELD; + if (slot_idx >= 27 && slot_idx < 33) return INF_NPC_HEALER_JAD; + if (slot_idx >= 33 && slot_idx < 37) return INF_NPC_HEALER_ZUK; + return -1; +} + +static int inferno_obs_slot_feature_count(int slot_idx) { + int type = inferno_obs_slot_type(slot_idx); + int has_style = (type == INF_NPC_BLOB || type == INF_NPC_JAD); + int has_scan = (type == INF_NPC_BLOB); + int has_los = (type != INF_NPC_NIBBLER && type != INF_NPC_MELEER && + type != INF_NPC_HEALER_JAD && type != INF_NPC_ZUK_SHIELD); + int has_target_category = (type != INF_NPC_NIBBLER && + type != INF_NPC_ZUK_SHIELD); + int has_timer = (type != INF_NPC_NIBBLER && type != INF_NPC_HEALER_JAD && + type != INF_NPC_ZUK_SHIELD); + int has_targeted = 1; + int has_meleer_dig = (type == INF_NPC_MELEER); + + return 11 + has_timer + 3 * has_style + has_los + 3 * has_scan + + 4 * has_target_category + has_targeted + 1 + 6 + 3 * has_meleer_dig; +} + +static int inferno_obs_slot_start(int slot_idx) { + int start = INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE; + for (int i = 0; i < slot_idx; i++) { + start += inferno_obs_slot_feature_count(i); + } + return start; +} + +static int inferno_pillar_obs_start(int pillar_idx) { + return INF_PLAYER_OBS_SIZE + pillar_idx * 5; +} + +static int inferno_target_mask_slot_offset(int slot_idx) { + return ENCOUNTER_MOVE_ACTIONS + ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION + 1 + slot_idx; +} + +static int inferno_target_mask_none_offset(void) { + return ENCOUNTER_MOVE_ACTIONS + ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION; +} + +static int inferno_step_out_forecast_obs_start(void) { + return INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE + INF_TOTAL_NPC_OBS_SIZE; +} + +static int inferno_pending_hit_obs_start(void) { + return inferno_step_out_forecast_obs_start() + INF_STEP_OUT_FORECAST_OBS_SIZE; +} + +static int inferno_spark_obs_start(void) { + return inferno_pending_hit_obs_start() + + INF_FEATURES_PER_HIT * ENCOUNTER_MAX_PENDING_HITS; +} + +static int inferno_obs_slot_barrage_count_index(int slot_idx) { + int type = inferno_obs_slot_type(slot_idx); + int has_style = (type == INF_NPC_BLOB || type == INF_NPC_JAD); + int has_scan = (type == INF_NPC_BLOB); + int has_los = (type != INF_NPC_NIBBLER && type != INF_NPC_MELEER && + type != INF_NPC_HEALER_JAD && type != INF_NPC_ZUK_SHIELD); + int has_timer = (type != INF_NPC_NIBBLER && type != INF_NPC_HEALER_JAD && + type != INF_NPC_ZUK_SHIELD); + + return inferno_obs_slot_start(slot_idx) + 4 + has_timer + + 3 * has_style + has_los + 3 * has_scan; +} + +static int inferno_obs_slot_size_index(int slot_idx) { + return inferno_obs_slot_start(slot_idx) + 3; +} + +static int inferno_obs_slot_npc_los_index(int slot_idx) { + int type = inferno_obs_slot_type(slot_idx); + int has_timer = (type != INF_NPC_NIBBLER && type != INF_NPC_HEALER_JAD && + type != INF_NPC_ZUK_SHIELD); + int has_style = (type == INF_NPC_BLOB || type == INF_NPC_JAD); + return inferno_obs_slot_start(slot_idx) + 4 + has_timer + 3 * has_style; +} + +static int inferno_obs_slot_edge_distance_index(int slot_idx) { + return inferno_obs_slot_barrage_count_index(slot_idx) + 1; +} + +static int inferno_obs_slot_npc_can_attack_if_ready_index(int slot_idx) { + return inferno_obs_slot_edge_distance_index(slot_idx) + 1; +} + +static int inferno_obs_slot_npc_can_attack_this_tick_index(int slot_idx) { + return inferno_obs_slot_npc_can_attack_if_ready_index(slot_idx) + 1; +} + +static int inferno_obs_slot_frozen_index(int slot_idx) { + return inferno_obs_slot_npc_can_attack_this_tick_index(slot_idx) + 1; +} + +static int inferno_obs_slot_player_can_attack_index(int slot_idx) { + return inferno_obs_slot_frozen_index(slot_idx) + 1; +} + +static int inferno_obs_slot_player_has_los_index(int slot_idx) { + return inferno_obs_slot_player_can_attack_index(slot_idx) + 1; +} + +static int inferno_obs_slot_player_can_mage_index(int slot_idx) { + return inferno_obs_slot_player_has_los_index(slot_idx) + 1; +} + +static int inferno_obs_slot_player_can_long_range_index(int slot_idx) { + return inferno_obs_slot_player_can_mage_index(slot_idx) + 1; +} + +static int inferno_obs_slot_player_can_blowpipe_index(int slot_idx) { + return inferno_obs_slot_player_can_long_range_index(slot_idx) + 1; +} + +static int inferno_obs_slot_mage_range_deficit_index(int slot_idx) { + return inferno_obs_slot_player_can_blowpipe_index(slot_idx) + 1; +} + +static int inferno_obs_slot_long_range_deficit_index(int slot_idx) { + return inferno_obs_slot_mage_range_deficit_index(slot_idx) + 1; +} + +static int inferno_obs_slot_blowpipe_range_deficit_index(int slot_idx) { + return inferno_obs_slot_long_range_deficit_index(slot_idx) + 1; +} + +static int inferno_obs_slot_target_category_start(int slot_idx) { + return inferno_obs_slot_barrage_count_index(slot_idx) + 13; +} + +static int inferno_obs_slot_targeted_index(int slot_idx) { + int type = inferno_obs_slot_type(slot_idx); + int has_target_category = (type != INF_NPC_NIBBLER && + type != INF_NPC_ZUK_SHIELD); + + return inferno_obs_slot_target_category_start(slot_idx) + + 4 * has_target_category; +} + +static int inferno_obs_slot_phantom_index(int slot_idx) { + return inferno_obs_slot_targeted_index(slot_idx) + 1; +} + +static int inferno_obs_slot_dig_index(int slot_idx) { + return inferno_obs_slot_phantom_index(slot_idx) + 1; +} + +static void init_threat_obs_state(InfernoState* state, int player_x, int player_y) { + inf_build_npc_stats(); + *state = make_test_state(player_x, player_y); + state->player.entity_type = ENTITY_PLAYER; + state->player.base_hitpoints = 99; + state->player.current_hitpoints = 99; + state->player.base_attack = 99; + state->player.base_strength = 99; + state->player.base_defence = 99; + state->player.base_ranged = 99; + state->player.base_magic = 99; + state->player.current_attack = 99; + state->player.current_strength = 99; + state->player.current_defence = 99; + state->player.current_ranged = 99; + state->player.current_magic = 99; + state->weapon_set = INF_GEAR_BP; + osrs_interaction_init(&state->interaction); + encounter_compute_loadout_stats(INF_MAX_MAGE_LOADOUT, ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, 30, + &state->loadout_stats[INF_GEAR_MAGE]); + encounter_compute_loadout_stats(INF_MAX_RANGE_LONG_LOADOUT, ATTACK_STYLE_RANGED, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, + &state->loadout_stats[INF_GEAR_LONG_RANGE]); + encounter_compute_loadout_stats(INF_MAX_RANGE_FAST_LOADOUT, ATTACK_STYLE_RANGED, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, + &state->loadout_stats[INF_GEAR_BP]); +} + +static void add_threat_obs_npc( + InfernoState* state, + int slot, + InfNPCType type, + int x, + int y +) { + state->npcs[slot] = make_test_npc(type, x, y, INF_NPC_STATS[type].size); + state->npcs[slot].active = 1; + state->npcs[slot].hp = state->npcs[slot].max_hp = INF_NPC_STATS[type].hp; + state->npcs[slot].attack_timer = 1; +} + +static void test_npc_threat_obs_exposes_frozen_meleer_pressure(void) { + printf("--- npc threat obs exposes frozen meleer pressure ---\n"); + + InfernoState state; + init_threat_obs_state(&state, 10, 10); + add_threat_obs_npc(&state, 0, INF_NPC_MELEER, 11, 10); + state.npcs[0].frozen_ticks = 8; + inf_refresh_current_obs_slots(&state); + + InfNpcPlayerThreat threat = inf_npc_player_threat(&state, &state.npcs[0]); + ASSERT_INT_EQ("frozen adjacent meleer can attack if ready", + threat.can_attack_if_ready, 1); + ASSERT_INT_EQ("frozen adjacent meleer can attack this tick", + threat.can_attack_this_tick, 1); + ASSERT_INT_EQ("immediate threat includes frozen adjacent meleer", + inf_player_has_immediate_threat(&state), 1); + InfNpcPressureSummary pressure = inf_npc_pressure_summary(&state); + ASSERT_INT_EQ("pressure summary counts frozen meleer if ready", + pressure.if_ready_total, 1); + ASSERT_INT_EQ("pressure summary counts frozen meleer this tick", + pressure.this_tick_total, 1); + ASSERT_INT_EQ("pressure summary exposes melee style", + pressure.if_ready_melee, 1); + ASSERT_INT_EQ("pressure summary exposes ready timer", + pressure.timer_lte_1, 1); + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + int obs_slot = inf_find_target_obs_slot(&state, 0); + ASSERT_INT_EQ("frozen meleer has obs slot", obs_slot >= 0, 1); + ASSERT_FLOAT_NEAR("frozen meleer threat if ready obs", + obs[inferno_obs_slot_npc_can_attack_if_ready_index(obs_slot)], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("frozen meleer threat this tick obs", + obs[inferno_obs_slot_npc_can_attack_this_tick_index(obs_slot)], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("frozen ticks obs", + obs[inferno_obs_slot_frozen_index(obs_slot)], + 8.0f / (float)BARRAGE_FREEZE_TICKS, 1e-6f); + ASSERT_FLOAT_NEAR("prayer critical includes frozen melee pressure", + obs[INF_OBS_PRAYER_MELEE], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("pressure obs counts frozen meleer if ready", + obs[INF_OBS_PRESSURE_IF_READY_TOTAL], 1.0f / (float)INF_OBS_NPCS, 1e-6f); + ASSERT_FLOAT_NEAR("pressure obs counts frozen meleer this tick", + obs[INF_OBS_PRESSURE_THIS_TICK_TOTAL], 1.0f / (float)INF_OBS_NPCS, 1e-6f); + ASSERT_FLOAT_NEAR("pressure obs nearest timer", + obs[INF_OBS_PRESSURE_NEAREST_TIMER], 0.1f, 1e-6f); +} + +static void test_npc_threat_obs_respects_overlap_range_and_stun(void) { + printf("--- npc threat obs respects overlap range and stun ---\n"); + + InfernoState under; + init_threat_obs_state(&under, 10, 10); + add_threat_obs_npc(&under, 0, INF_NPC_MELEER, 9, 9); + under.npcs[0].frozen_ticks = 8; + InfNpcPlayerThreat under_threat = inf_npc_player_threat(&under, &under.npcs[0]); + ASSERT_INT_EQ("standing under frozen meleer is not attackable", + under_threat.can_attack_if_ready, 0); + ASSERT_INT_EQ("standing under frozen meleer is not pressure", + inf_npc_pressure_summary(&under).if_ready_total, 0); + + InfernoState diagonal; + init_threat_obs_state(&diagonal, 10, 10); + add_threat_obs_npc(&diagonal, 0, INF_NPC_MELEER, 11, 11); + diagonal.npcs[0].frozen_ticks = 8; + InfNpcPlayerThreat diagonal_threat = + inf_npc_player_threat(&diagonal, &diagonal.npcs[0]); + ASSERT_INT_EQ("frozen meleer diagonal corner contact is not attackable", + diagonal_threat.can_attack_if_ready, 0); + ASSERT_INT_EQ("frozen meleer diagonal corner contact is not pressure", + inf_npc_pressure_summary(&diagonal).if_ready_total, 0); + + InfernoState far; + init_threat_obs_state(&far, 10, 10); + add_threat_obs_npc(&far, 0, INF_NPC_MELEER, 13, 10); + far.npcs[0].frozen_ticks = 8; + InfNpcPlayerThreat far_threat = inf_npc_player_threat(&far, &far.npcs[0]); + ASSERT_INT_EQ("frozen meleer outside melee distance is not attackable", + far_threat.can_attack_if_ready, 0); + + InfernoState stunned; + init_threat_obs_state(&stunned, 10, 10); + add_threat_obs_npc(&stunned, 0, INF_NPC_MELEER, 11, 10); + stunned.npcs[0].stun_timer = 2; + InfNpcPlayerThreat stunned_threat = + inf_npc_player_threat(&stunned, &stunned.npcs[0]); + ASSERT_INT_EQ("stunned adjacent meleer would threaten if ready", + stunned_threat.can_attack_if_ready, 1); + ASSERT_INT_EQ("stunned adjacent meleer cannot attack this tick", + stunned_threat.can_attack_this_tick, 0); + InfNpcPressureSummary stunned_pressure = inf_npc_pressure_summary(&stunned); + ASSERT_INT_EQ("stunned meleer keeps if-ready pressure", + stunned_pressure.if_ready_total, 1); + ASSERT_INT_EQ("stunned meleer is not this-tick pressure", + stunned_pressure.this_tick_total, 0); + ASSERT_INT_EQ("stunned meleer is not imminent pressure", + stunned_pressure.timer_lte_1, 0); +} + +static void test_npc_pressure_summary_respects_los_target_and_mixed_styles(void) { + printf("--- npc pressure summary respects LOS target and mixed styles ---\n"); + + InfernoState blocked; + init_threat_obs_state(&blocked, 10, 10); + add_threat_obs_npc(&blocked, 0, INF_NPC_RANGER, 20, 10); + blocked.los_blockers[0] = (LOSBlocker){15, 10, 1, LOS_FULL_MASK}; + blocked.los_blocker_count = 1; + inf_invalidate_los_cache(&blocked); + ASSERT_INT_EQ("pillar blocks ranged pressure", + inf_npc_pressure_summary(&blocked).if_ready_total, 0); + + InfernoState redirected; + init_threat_obs_state(&redirected, 10, 10); + add_threat_obs_npc(&redirected, 0, INF_NPC_RANGER, 15, 10); + redirected.npcs[1] = make_test_npc( + INF_NPC_ZUK_SHIELD, 12, 10, INF_NPC_STATS[INF_NPC_ZUK_SHIELD].size); + redirected.npcs[1].active = 1; + redirected.npcs[0].aggro_target = 1; + ASSERT_INT_EQ("non-player target does not create pressure", + inf_npc_pressure_summary(&redirected).if_ready_total, 0); + + InfernoState mixed; + init_threat_obs_state(&mixed, 10, 10); + add_threat_obs_npc(&mixed, 0, INF_NPC_RANGER, 15, 10); + add_threat_obs_npc(&mixed, 1, INF_NPC_MAGER, 10, 15); + InfNpcPressureSummary pressure = inf_npc_pressure_summary(&mixed); + ASSERT_INT_EQ("mixed pressure counts two NPCs", + pressure.if_ready_total, 2); + ASSERT_INT_EQ("mixed pressure includes ranged", + pressure.if_ready_ranged, 1); + ASSERT_INT_EQ("mixed pressure includes magic", + pressure.if_ready_magic, 1); + ASSERT_INT_EQ("mixed pressure exposes two styles", + pressure.if_ready_style_count, 2); +} + +static void test_npc_threat_obs_keeps_ranger_mager_diagonal_melee(void) { + printf("--- npc threat obs keeps ranger and mager diagonal melee ---\n"); + + InfernoState ranger_state; + init_threat_obs_state(&ranger_state, 10, 10); + add_threat_obs_npc(&ranger_state, 0, INF_NPC_RANGER, 11, 11); + InfNpcPlayerThreat ranger_threat = + inf_npc_player_threat(&ranger_state, &ranger_state.npcs[0]); + ASSERT_INT_EQ("diagonal ranger can attack player", + ranger_threat.can_attack_if_ready, 1); + ASSERT_INT_EQ("diagonal ranger threat includes melee fallback", + (ranger_threat.style_mask & INF_STYLE_MASK_MELEE) != 0, 1); + + InfernoState mager_state; + init_threat_obs_state(&mager_state, 10, 10); + add_threat_obs_npc(&mager_state, 0, INF_NPC_MAGER, 11, 11); + InfNpcPlayerThreat mager_threat = + inf_npc_player_threat(&mager_state, &mager_state.npcs[0]); + ASSERT_INT_EQ("diagonal mager can attack player", + mager_threat.can_attack_if_ready, 1); + ASSERT_INT_EQ("diagonal mager threat includes melee fallback", + (mager_threat.style_mask & INF_STYLE_MASK_MELEE) != 0, 1); +} + +static void test_player_attackability_obs_exposes_current_loadout_and_los(void) { + printf("--- player attackability obs exposes current loadout and LOS ---\n"); + + InfernoState state; + init_threat_obs_state(&state, 10, 10); + add_threat_obs_npc(&state, 0, INF_NPC_RANGER, 20, 10); + inf_refresh_current_obs_slots(&state); + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + int obs_slot = inf_find_target_obs_slot(&state, 0); + ASSERT_INT_EQ("range test ranger has obs slot", obs_slot >= 0, 1); + ASSERT_FLOAT_NEAR("ranger size obs is normalized", + obs[inferno_obs_slot_size_index(obs_slot)], 3.0f / 7.0f, 1e-6f); + ASSERT_FLOAT_NEAR("blowpipe cannot attack distance ten ranger", + obs[inferno_obs_slot_player_can_attack_index(obs_slot)], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("player has LOS to distance ten ranger", + obs[inferno_obs_slot_player_has_los_index(obs_slot)], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("mage loadout can attack distance ten ranger", + obs[inferno_obs_slot_player_can_mage_index(obs_slot)], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("long-range loadout can attack distance ten ranger", + obs[inferno_obs_slot_player_can_long_range_index(obs_slot)], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("blowpipe loadout cannot attack distance ten ranger", + obs[inferno_obs_slot_player_can_blowpipe_index(obs_slot)], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("mage loadout has no range deficit", + obs[inferno_obs_slot_mage_range_deficit_index(obs_slot)], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("long-range loadout has no range deficit", + obs[inferno_obs_slot_long_range_deficit_index(obs_slot)], 0.0f, 1e-6f); + int bp_deficit = 10 - state.loadout_stats[INF_GEAR_BP].attack_range; + if (bp_deficit < 0) bp_deficit = 0; + float expected_bp_deficit = (float)bp_deficit / 15.0f; + ASSERT_FLOAT_NEAR("blowpipe range deficit is geometric", + obs[inferno_obs_slot_blowpipe_range_deficit_index(obs_slot)], + expected_bp_deficit, 1e-6f); + + state.los_blockers[0] = (LOSBlocker){15, 10, 1, LOS_FULL_MASK}; + state.los_blocker_count = 1; + inf_invalidate_los_cache(&state); + inf_write_obs((EncounterState*)&state, obs); + ASSERT_FLOAT_NEAR("LOS blocker hides player LOS", + obs[inferno_obs_slot_player_has_los_index(obs_slot)], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("LOS blocker blocks mage reachability", + obs[inferno_obs_slot_player_can_mage_index(obs_slot)], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("LOS blocker keeps mage range deficit geometric", + obs[inferno_obs_slot_mage_range_deficit_index(obs_slot)], 0.0f, 1e-6f); +} + +static void test_barrage_total_target_count_matches_resolution_targets(void) { + printf("--- barrage total target count matches resolution targets ---\n"); + + InfernoState state; + init_threat_obs_state(&state, 10, 10); + add_threat_obs_npc(&state, 0, INF_NPC_RANGER, 16, 10); + add_threat_obs_npc(&state, 1, INF_NPC_NIBBLER, 17, 11); + add_threat_obs_npc(&state, 2, INF_NPC_MAGER, 15, 9); + add_threat_obs_npc(&state, 3, INF_NPC_ZUK_SHIELD, 16, 11); + add_threat_obs_npc(&state, 4, INF_NPC_BAT, 17, 10); + state.npcs[4].death_ticks = 2; + + ASSERT_INT_EQ("barrage count includes primary and active non-shields", + inf_barrage_total_target_count(&state, 0), 3); + + inf_refresh_current_obs_slots(&state); + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + int obs_slot = inf_find_target_obs_slot(&state, 0); + ASSERT_FLOAT_NEAR("barrage count obs includes primary target", + obs[inferno_obs_slot_barrage_count_index(obs_slot)], + 3.0f / (float)BARRAGE_MAX_HITS, 1e-6f); + + for (int i = 5; i < 16; i++) { + add_threat_obs_npc(&state, i, INF_NPC_BAT, 15 + (i % 3), 9 + (i % 3)); + } + ASSERT_INT_EQ("barrage count caps at max hits", + inf_barrage_total_target_count(&state, 0), BARRAGE_MAX_HITS); +} + +static void init_step_out_forecast_stack_state(InfernoState* state, int player_x, int player_y) { + reset_test_context(); + inf_build_npc_stats(); + memset(state, 0, sizeof(*state)); + memset(state->npc_los_cache, -1, sizeof(state->npc_los_cache)); + state->rng_state = 20260515u; + state->wave = 59; + state->player.entity_type = ENTITY_PLAYER; + state->player.x = player_x; + state->player.y = player_y; + state->player.base_hitpoints = 99; + state->player.current_hitpoints = 99; + state->player.current_defence = 99; + state->player.current_magic = 99; + state->player.current_prayer = 99; + state->player_last_interaction_target_slot = -1; + state->player_last_interaction_age = 1; + state->player_dest_x = -1; + state->player_dest_y = -1; + test_config()->step_out_forecast_obs_enabled = 1; + test_config()->step_out_forecast_obs_mode = + INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT; + state->weapon_set = INF_GEAR_LONG_RANGE; + osrs_interaction_init(&state->interaction); + for (int p = 0; p < INF_NUM_PILLARS; p++) { + state->pillars[p].x = INF_PILLAR_POS[p][0]; + state->pillars[p].y = INF_PILLAR_POS[p][1]; + state->pillars[p].hp = INF_PILLAR_HP; + state->pillars[p].active = 1; + } + inf_rebuild_los(state); + inf_rebuild_player_collision_flags(state); +} + +static void add_step_out_forecast_npc( + InfernoState* state, int slot, InfNPCType type, int x, int y, int timer +) { + inf_init_npc(state, slot, type, x, y); + state->npcs[slot].attack_timer = timer; + state->npcs[slot].stun_timer = 0; + state->npcs[slot].frozen_ticks = 0; +} + +static void clear_step_out_forecast_pillars(InfernoState* state) { + for (int p = 0; p < INF_NUM_PILLARS; p++) { + state->pillars[p].active = 0; + state->pillars[p].hp = 0; + } + inf_rebuild_los(state); + inf_rebuild_entity_collision_flags(state); +} + +static void assert_step_out_ranger_then_mager( + const char* label, + const InfStepOutForecastAction* action, + int expected_x, + int expected_y +) { + char msg[128]; + snprintf(msg, sizeof(msg), "%s action is valid", label); + ASSERT_INT_EQ(msg, action->valid, 1); + snprintf(msg, sizeof(msg), "%s landing x", label); + ASSERT_INT_EQ(msg, action->land_x, expected_x); + snprintf(msg, sizeof(msg), "%s landing y", label); + ASSERT_INT_EQ(msg, action->land_y, expected_y); + snprintf(msg, sizeof(msg), "%s ranger fires first", label); + ASSERT_INT_EQ(msg, action->ticks[0].ranger_count, 1); + snprintf(msg, sizeof(msg), "%s mager waits first tick", label); + ASSERT_INT_EQ(msg, action->ticks[0].mager_count, 0); + snprintf(msg, sizeof(msg), "%s mager fires second", label); + ASSERT_INT_EQ(msg, action->ticks[1].mager_count, 1); + snprintf(msg, sizeof(msg), "%s ranger does not double-fire", label); + ASSERT_INT_EQ(msg, action->ticks[1].ranger_count, 0); + snprintf(msg, sizeof(msg), "%s exposes off-tick opportunity", label); + ASSERT_INT_EQ(msg, action->ranger_mager_offtick_opportunity, 1); + snprintf(msg, sizeof(msg), "%s avoids same-tick conflict", label); + ASSERT_INT_EQ(msg, action->same_tick_mixed_style_conflict, 0); +} + +static void test_step_out_forecast_matches_movement_head_destinations(void) { + printf("--- step-out forecast matches movement head destinations ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 29, 39); + + InfStepOutForecast forecast; + inf_build_step_out_forecast(&state, &forecast); + + for (int action = 0; action < ENCOUNTER_MOVE_ACTIONS; action++) { + Player moved = state.player; + if (action > 0) { + InfWalkCtx walk_ctx = { &state, inf_legacy_context() }; + encounter_move_to_target( + &moved, + ENCOUNTER_MOVE_TARGET_DX[action], + ENCOUNTER_MOVE_TARGET_DY[action], + inf_tile_walkable, + &walk_ctx); + } + + ASSERT_INT_EQ("forecast movement landing x", + forecast.actions[action].land_x, moved.x); + ASSERT_INT_EQ("forecast movement landing y", + forecast.actions[action].land_y, moved.y); + } +} + +static void assert_inferno_npc_sw_origin_step( + const char* label, + int pillar_idx, + InfNPCType type, + int player_x, + int player_y, + int npc_x, + int npc_y, + int expected_x, + int expected_y +) { + InfernoState state; + init_step_out_forecast_stack_state(&state, player_x, player_y); + for (int p = 0; p < INF_NUM_PILLARS; p++) { + state.pillars[p].active = p == pillar_idx; + state.pillars[p].hp = p == pillar_idx ? INF_PILLAR_HP : 0; + } + inf_rebuild_los(&state); + add_step_out_forecast_npc(&state, 0, type, npc_x, npc_y, 0); + inf_rebuild_entity_collision_flags(&state); + ASSERT_INT_EQ("starting NPC has no LOS", + inf_npc_has_los_direct(&state, 0), 0); + + inf_npc_move(&state, 0); + + char msg[128]; + snprintf(msg, sizeof(msg), "%s x", label); + ASSERT_INT_EQ(msg, state.npcs[0].x, expected_x); + snprintf(msg, sizeof(msg), "%s y", label); + ASSERT_INT_EQ(msg, state.npcs[0].y, expected_y); +} + +static void test_inferno_npc_travel_uses_sw_origin_around_all_pillars(void) { + printf("--- inferno NPC travel uses SW origin around all pillars ---\n"); + + assert_inferno_npc_sw_origin_step( + "south pillar mager", + 0, INF_NPC_MAGER, + 15, 14, + 12, 30, + 13, 29); + assert_inferno_npc_sw_origin_step( + "west pillar ranger", + 1, INF_NPC_RANGER, + 12, 28, + 11, 37, + 12, 37); + assert_inferno_npc_sw_origin_step( + "north pillar ranger", + 2, INF_NPC_RANGER, + 18, 40, + 16, 22, + 17, 23); +} + +static void assert_inferno_jal_npc_uses_edge_clearance( + const char* label, + InfNPCType type, + int player_x, + int player_y, + int npc_x, + int npc_y, + int expected_x, + int expected_y +) { + InfernoState state; + init_step_out_forecast_stack_state(&state, player_x, player_y); + for (int p = 0; p < INF_NUM_PILLARS; p++) { + state.pillars[p].active = p == 0; + state.pillars[p].hp = p == 0 ? INF_PILLAR_HP : 0; + } + inf_rebuild_los(&state); + add_step_out_forecast_npc(&state, 0, type, npc_x, npc_y, 0); + inf_rebuild_entity_collision_flags(&state); + ASSERT_INT_EQ("starting Jal NPC has no LOS", + inf_npc_has_los(&state, 0), 0); + + inf_npc_move(&state, 0); + + char msg[128]; + snprintf(msg, sizeof(msg), "%s x", label); + ASSERT_INT_EQ(msg, state.npcs[0].x, expected_x); + snprintf(msg, sizeof(msg), "%s y", label); + ASSERT_INT_EQ(msg, state.npcs[0].y, expected_y); +} + +static void test_inferno_jal_npcs_use_edge_clearance_at_pillars(void) { + printf("--- inferno Jal NPCs use edge clearance at pillars ---\n"); + + assert_inferno_jal_npc_uses_edge_clearance( + "JalXil south pillar corner", + INF_NPC_RANGER, + 21, 16, + 20, 20, + 20, 19); + assert_inferno_jal_npc_uses_edge_clearance( + "JalZek south pillar corner", + INF_NPC_MAGER, + 21, 20, + 17, 16, + 18, 16); +} + +static void test_step_out_forecast_north_pillar_ranger_mager_order(void) { + printf("--- step-out forecast north pillar ranger/mager order ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 29, 39); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 0); + add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 0); + + InfStepOutForecast forecast; + inf_build_step_out_forecast(&state, &forecast); + + const InfStepOutForecastAction* idle = &forecast.actions[0]; + ASSERT_INT_EQ("idle remains safe from ranged tick one", + idle->ticks[0].ranged_count, 0); + ASSERT_INT_EQ("idle remains safe from magic tick one", + idle->ticks[0].magic_count, 0); + + const InfStepOutForecastAction* run_west = &forecast.actions[11]; + assert_step_out_ranger_then_mager("north pillar run west", run_west, 27, 39); +} + +static void test_step_out_forecast_obs_exposes_compact_action_affordance(void) { + printf("--- step-out forecast obs exposes compact action affordance ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 29, 39); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 0); + add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 0); + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int action_start = inferno_step_out_forecast_obs_start() + + 11 * INF_STEP_OUT_FORECAST_ACTION_FEATURES; + ASSERT_FLOAT_NEAR("run west obs valid", + obs[action_start], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("run west obs first attack tick", + obs[action_start + 1], 1.0f / 4.0f, 1e-6f); + ASSERT_FLOAT_NEAR("run west obs first style mask", + obs[action_start + 2], (float)INF_STYLE_MASK_RANGED / 7.0f, 1e-6f); + ASSERT_FLOAT_NEAR("run west obs has max hit", + obs[action_start + 3], 70.0f / 150.0f, 1e-6f); + ASSERT_FLOAT_NEAR("run west obs same-tick conflict", + obs[action_start + 4], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("run west obs ranger/mager conflict", + obs[action_start + 5], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("run west obs off-tick opportunity", + obs[action_start + 6], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("run west obs melee fallback exposure", + obs[action_start + 7], 0.0f, 1e-6f); +} + +static void test_step_out_forecast_obs_can_be_disabled(void) { + printf("--- step-out forecast obs can be disabled ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 29, 39); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 0); + add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 0); + test_config()->step_out_forecast_obs_enabled = 0; + test_config()->step_out_forecast_obs_mode = INF_STEP_OUT_FORECAST_MODE_OFF; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int forecast_start = inferno_step_out_forecast_obs_start(); + for (int j = 0; j < INF_STEP_OUT_FORECAST_OBS_SIZE; j++) { + ASSERT_FLOAT_NEAR("disabled forecast obs stays zero", + obs[forecast_start + j], 0.0f, 1e-6f); + } +} + +static void test_step_out_forecast_obs_uses_fast_mode(void) { + printf("--- step-out forecast obs uses fast mode ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 29, 39); + clear_step_out_forecast_pillars(&state); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 1); + add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 2); + inf_rebuild_entity_collision_flags(&state); + test_config()->step_out_forecast_obs_enabled = 1; + test_config()->step_out_forecast_obs_mode = + INF_STEP_OUT_FORECAST_MODE_FAST_STATIC_TILE; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int action_start = inferno_step_out_forecast_obs_start(); + ASSERT_FLOAT_NEAR("fast obs idle valid", + obs[action_start], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("fast obs idle first attack tick", + obs[action_start + 1], 1.0f / 4.0f, 1e-6f); + ASSERT_FLOAT_NEAR("fast obs idle first style mask", + obs[action_start + 2], (float)INF_STYLE_MASK_RANGED / 7.0f, 1e-6f); + ASSERT_FLOAT_NEAR("fast obs idle max hit", + obs[action_start + 3], 70.0f / 150.0f, 1e-6f); + ASSERT_FLOAT_NEAR("fast obs idle off-tick opportunity", + obs[action_start + 6], 1.0f, 1e-6f); +} + +static void test_fast_step_out_forecast_matches_movement_head_destinations(void) { + printf("--- fast step-out forecast matches movement head destinations ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 29, 39); + + InfStepOutForecast forecast; + inf_build_step_out_forecast_fast_static_ctx( + &state, inf_legacy_context(), &forecast); + + for (int action = 0; action < ENCOUNTER_MOVE_ACTIONS; action++) { + Player moved = state.player; + if (action > 0) { + InfWalkCtx walk_ctx = { &state, inf_legacy_context() }; + encounter_move_to_target( + &moved, + ENCOUNTER_MOVE_TARGET_DX[action], + ENCOUNTER_MOVE_TARGET_DY[action], + inf_tile_walkable, + &walk_ctx); + } + + ASSERT_INT_EQ("fast forecast movement landing x", + forecast.actions[action].land_x, moved.x); + ASSERT_INT_EQ("fast forecast movement landing y", + forecast.actions[action].land_y, moved.y); + } +} + +static void test_fast_step_out_forecast_immediate_static_threats(void) { + printf("--- fast step-out forecast immediate static threats ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 29, 39); + clear_step_out_forecast_pillars(&state); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 1); + add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 2); + inf_rebuild_entity_collision_flags(&state); + + InfStepOutForecast exact; + InfStepOutForecast fast; + InfStepOutForecastOracleDiff diff; + inf_build_step_out_forecast_exact_ctx(&state, inf_legacy_context(), &exact); + inf_build_step_out_forecast_fast_static_ctx(&state, inf_legacy_context(), &fast); + inf_compare_step_out_forecasts(&exact, &fast, &diff); + + const InfStepOutForecastAction* idle = &fast.actions[0]; + ASSERT_INT_EQ("fast idle ranger fires first", idle->ticks[0].ranger_count, 1); + ASSERT_INT_EQ("fast idle mager fires second", idle->ticks[1].mager_count, 1); + ASSERT_INT_EQ("fast idle exposes off-tick opportunity", + idle->ranger_mager_offtick_opportunity, 1); + ASSERT_INT_EQ("fast static has no dangerous false negatives", + diff.dangerous_false_negatives, 0); +} + +static void test_fast_step_out_forecast_blob_scan_and_melee_fallback(void) { + printf("--- fast step-out forecast blob scan and melee fallback ---\n"); + + InfernoState blob_state; + init_step_out_forecast_stack_state(&blob_state, 29, 39); + clear_step_out_forecast_pillars(&blob_state); + add_step_out_forecast_npc(&blob_state, 0, INF_NPC_BLOB, 29, 30, 1); + blob_state.npcs[0].blob_scanned_prayer = -1; + blob_state.npcs[0].had_los_last_tick = 0; + inf_rebuild_entity_collision_flags(&blob_state); + + InfStepOutForecast blob_forecast; + inf_build_step_out_forecast_fast_static_ctx( + &blob_state, inf_legacy_context(), &blob_forecast); + ASSERT_INT_EQ("fast blob scan tick", + blob_forecast.actions[0].ticks[0].blob_scan_count, 1); + + InfernoState melee_state; + init_step_out_forecast_stack_state(&melee_state, 10, 10); + clear_step_out_forecast_pillars(&melee_state); + add_step_out_forecast_npc(&melee_state, 0, INF_NPC_MAGER, 11, 10, 1); + inf_rebuild_entity_collision_flags(&melee_state); + + InfStepOutForecast melee_forecast; + inf_build_step_out_forecast_fast_static_ctx( + &melee_state, inf_legacy_context(), &melee_forecast); + ASSERT_INT_EQ("fast melee fallback exposure", + melee_forecast.actions[0].melee_fallback_exposure, 1); + ASSERT_INT_EQ("fast melee fallback mixed style", + melee_forecast.actions[0].same_tick_mixed_style_conflict, 1); +} + +static void test_fast_step_out_forecast_does_not_mutate_state(void) { + printf("--- fast step-out forecast does not mutate state ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 29, 39); + clear_step_out_forecast_pillars(&state); + add_step_out_forecast_npc(&state, 0, INF_NPC_BLOB, 29, 30, 1); + state.npcs[0].blob_scanned_prayer = -1; + state.npcs[0].had_los_last_tick = 0; + inf_rebuild_entity_collision_flags(&state); + + InfernoState before = state; + InfStepOutForecast forecast; + inf_build_step_out_forecast_fast_static_ctx( + &state, inf_legacy_context(), &forecast); + + ASSERT_INT_EQ("fast forecast preserves player x", + state.player.x, before.player.x); + ASSERT_INT_EQ("fast forecast preserves player y", + state.player.y, before.player.y); + ASSERT_INT_EQ("fast forecast preserves NPC timer", + state.npcs[0].attack_timer, before.npcs[0].attack_timer); + ASSERT_INT_EQ("fast forecast preserves blob scan state", + state.npcs[0].blob_scanned_prayer, before.npcs[0].blob_scanned_prayer); + ASSERT_INT_EQ("fast forecast preserves LOS cache", + memcmp(state.npc_los_cache, before.npc_los_cache, + sizeof(state.npc_los_cache)), 0); + ASSERT_INT_EQ("fast forecast preserves player collision flags", + memcmp(state.player_collision_flags, before.player_collision_flags, + sizeof(state.player_collision_flags)), 0); + ASSERT_INT_EQ("fast forecast preserves NPC collision flags", + memcmp(state.npc_collision_flags, before.npc_collision_flags, + sizeof(state.npc_collision_flags)), 0); +} + +static void test_readonly_step_out_forecast_matches_movement_head_destinations(void) { + printf("--- readonly step-out forecast matches movement head destinations ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 29, 39); + + InfStepOutForecast forecast; + inf_build_step_out_forecast_fast_readonly_ctx( + &state, inf_legacy_context(), &forecast); + + for (int action = 0; action < ENCOUNTER_MOVE_ACTIONS; action++) { + Player moved = state.player; + if (action > 0) { + InfWalkCtx walk_ctx = { &state, inf_legacy_context() }; + encounter_move_to_target( + &moved, + ENCOUNTER_MOVE_TARGET_DX[action], + ENCOUNTER_MOVE_TARGET_DY[action], + inf_tile_walkable, + &walk_ctx); + } + + ASSERT_INT_EQ("readonly forecast movement landing x", + forecast.actions[action].land_x, moved.x); + ASSERT_INT_EQ("readonly forecast movement landing y", + forecast.actions[action].land_y, moved.y); + } +} + +static void test_readonly_step_out_forecast_does_not_mutate_state(void) { + printf("--- readonly step-out forecast does not mutate state ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 29, 39); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 1); + add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 2); + add_step_out_forecast_npc(&state, 2, INF_NPC_BLOB, 29, 32, 1); + state.npcs[2].blob_scanned_prayer = -1; + state.npcs[2].had_los_last_tick = 0; + inf_rebuild_entity_collision_flags(&state); + + InfernoState before = state; + InfStepOutForecast forecast; + inf_build_step_out_forecast_fast_readonly_ctx( + &state, inf_legacy_context(), &forecast); + + ASSERT_INT_EQ("readonly forecast preserves player x", + state.player.x, before.player.x); + ASSERT_INT_EQ("readonly forecast preserves player y", + state.player.y, before.player.y); + ASSERT_INT_EQ("readonly forecast preserves ranger x", + state.npcs[0].x, before.npcs[0].x); + ASSERT_INT_EQ("readonly forecast preserves ranger y", + state.npcs[0].y, before.npcs[0].y); + ASSERT_INT_EQ("readonly forecast preserves NPC timer", + state.npcs[0].attack_timer, before.npcs[0].attack_timer); + ASSERT_INT_EQ("readonly forecast preserves blob scan state", + state.npcs[2].blob_scanned_prayer, before.npcs[2].blob_scanned_prayer); + ASSERT_INT_EQ("readonly forecast preserves LOS cache", + memcmp(state.npc_los_cache, before.npc_los_cache, + sizeof(state.npc_los_cache)), 0); + ASSERT_INT_EQ("readonly forecast preserves player collision flags", + memcmp(state.player_collision_flags, before.player_collision_flags, + sizeof(state.player_collision_flags)), 0); + ASSERT_INT_EQ("readonly forecast preserves NPC collision flags", + memcmp(state.npc_collision_flags, before.npc_collision_flags, + sizeof(state.npc_collision_flags)), 0); +} + +static void assert_readonly_step_out_matches_exact_action( + const char* label, + InfernoState* state, + int action_idx +) { + InfStepOutForecast exact; + InfStepOutForecast readonly; + InfStepOutForecastOracleDiff diff; + inf_build_step_out_forecast_exact_ctx(state, inf_legacy_context(), &exact); + inf_build_step_out_forecast_fast_readonly_ctx( + state, inf_legacy_context(), &readonly); + inf_compare_step_out_forecasts(&exact, &readonly, &diff); + + ASSERT_INT_EQ("readonly action oracle has no dangerous false negatives", + diff.dangerous_false_negatives, 0); + const InfStepOutForecastAction* exact_action = &exact.actions[action_idx]; + const InfStepOutForecastAction* readonly_action = &readonly.actions[action_idx]; + char msg[128]; + snprintf(msg, sizeof(msg), "%s valid", label); + ASSERT_INT_EQ(msg, readonly_action->valid, exact_action->valid); + snprintf(msg, sizeof(msg), "%s land x", label); + ASSERT_INT_EQ(msg, readonly_action->land_x, exact_action->land_x); + snprintf(msg, sizeof(msg), "%s land y", label); + ASSERT_INT_EQ(msg, readonly_action->land_y, exact_action->land_y); + snprintf(msg, sizeof(msg), "%s first ranger count", label); + ASSERT_INT_EQ(msg, + readonly_action->ticks[0].ranger_count, + exact_action->ticks[0].ranger_count); + snprintf(msg, sizeof(msg), "%s second mager count", label); + ASSERT_INT_EQ(msg, + readonly_action->ticks[1].mager_count, + exact_action->ticks[1].mager_count); +} + +static void test_readonly_step_out_forecast_pillar_step_out_cases(void) { + printf("--- readonly step-out forecast pillar step-out cases ---\n"); + + InfernoState north_state; + init_step_out_forecast_stack_state(&north_state, 29, 39); + add_step_out_forecast_npc(&north_state, 0, INF_NPC_RANGER, 24, 31, 0); + add_step_out_forecast_npc(&north_state, 1, INF_NPC_MAGER, 29, 30, 0); + assert_readonly_step_out_matches_exact_action( + "north pillar run west", &north_state, 11); + + InfernoState south_state; + init_step_out_forecast_stack_state(&south_state, 22, 17); + add_step_out_forecast_npc(&south_state, 0, INF_NPC_RANGER, 17, 25, 0); + add_step_out_forecast_npc(&south_state, 1, INF_NPC_MAGER, 22, 26, 0); + assert_readonly_step_out_matches_exact_action( + "south pillar run west", &south_state, 11); + + InfernoState west_state; + init_step_out_forecast_stack_state(&west_state, 11, 29); + add_step_out_forecast_npc(&west_state, 0, INF_NPC_RANGER, 8, 40, 0); + add_step_out_forecast_npc(&west_state, 1, INF_NPC_MAGER, 16, 42, 0); + assert_readonly_step_out_matches_exact_action( + "west pillar walk north", &west_state, 4); +} + +static void test_step_out_forecast_obs_uses_readonly_mode(void) { + printf("--- step-out forecast obs uses readonly mode ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 29, 39); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 0); + add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 0); + test_config()->step_out_forecast_obs_enabled = 1; + test_config()->step_out_forecast_obs_mode = + INF_STEP_OUT_FORECAST_MODE_FAST_READONLY_MOVE; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int action_start = inferno_step_out_forecast_obs_start() + + 11 * INF_STEP_OUT_FORECAST_ACTION_FEATURES; + ASSERT_FLOAT_NEAR("readonly obs run west valid", + obs[action_start], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("readonly obs run west first attack tick", + obs[action_start + 1], 1.0f / 4.0f, 1e-6f); + ASSERT_FLOAT_NEAR("readonly obs run west first style mask", + obs[action_start + 2], (float)INF_STYLE_MASK_RANGED / 7.0f, 1e-6f); + ASSERT_FLOAT_NEAR("readonly obs run west off-tick opportunity", + obs[action_start + 6], 1.0f, 1e-6f); +} + +static void test_readonly_step_out_forecast_stun_countdown(void) { + printf("--- readonly step-out forecast stun countdown ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 20, 20); + clear_step_out_forecast_pillars(&state); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 20, 25, 1); + state.npcs[0].stun_timer = 2; + inf_rebuild_entity_collision_flags(&state); + + InfStepOutForecast forecast; + inf_build_step_out_forecast_fast_readonly_ctx( + &state, inf_legacy_context(), &forecast); + + ASSERT_INT_EQ("stunned ranger does not fire tick one", + forecast.actions[0].ticks[0].ranger_count, 0); + ASSERT_INT_EQ("stunned ranger does not fire tick two", + forecast.actions[0].ticks[1].ranger_count, 0); + ASSERT_INT_EQ("stunned ranger fires after countdown", + forecast.actions[0].ticks[2].ranger_count, 1); +} + +static void test_readonly_step_out_forecast_frozen_can_attack(void) { + printf("--- readonly step-out forecast frozen can attack ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 20, 20); + clear_step_out_forecast_pillars(&state); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 20, 25, 1); + state.npcs[0].frozen_ticks = 4; + inf_rebuild_entity_collision_flags(&state); + + InfStepOutForecast forecast; + inf_build_step_out_forecast_fast_readonly_ctx( + &state, inf_legacy_context(), &forecast); + + ASSERT_INT_EQ("frozen ranger still attacks with LOS", + forecast.actions[0].ticks[0].ranger_count, 1); +} + +static void test_readonly_step_out_forecast_blob_scanned_fire(void) { + printf("--- readonly step-out forecast blob scanned fire ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 20, 20); + clear_step_out_forecast_pillars(&state); + add_step_out_forecast_npc(&state, 0, INF_NPC_BLOB, 20, 25, 1); + state.npcs[0].blob_scanned_prayer = PRAYER_PROTECT_MAGIC; + state.npcs[0].attack_style = ATTACK_STYLE_RANGED; + state.npcs[0].had_los_last_tick = 1; + inf_rebuild_entity_collision_flags(&state); + + InfStepOutForecast forecast; + inf_build_step_out_forecast_fast_readonly_ctx( + &state, inf_legacy_context(), &forecast); + + ASSERT_INT_EQ("scanned blob fires ranged", + forecast.actions[0].ticks[0].ranged_count, 1); + ASSERT_INT_EQ("scanned blob fire has max hit", + forecast.actions[0].ticks[0].max_hit > 0, 1); +} + +static void test_readonly_step_out_forecast_under_player_overlap_is_danger(void) { + printf("--- readonly step-out forecast under-player overlap is danger ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 20, 20); + clear_step_out_forecast_pillars(&state); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 20, 20, 1); + inf_rebuild_entity_collision_flags(&state); + uint32_t rng_before = state.rng_state; + + InfStepOutForecast forecast; + inf_build_step_out_forecast_fast_readonly_ctx( + &state, inf_legacy_context(), &forecast); + + ASSERT_INT_EQ("under-player overlap marks immediate danger", + inf_step_out_forecast_action_dangerous(&forecast.actions[0]), 1); + ASSERT_INT_EQ("under-player forecast does not consume RNG", + state.rng_state, rng_before); +} + +static void test_readonly_step_out_forecast_invalid_movement_zeroes_payload(void) { + printf("--- readonly step-out forecast invalid movement zeroes payload ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, INF_ARENA_MIN_X, INF_ARENA_MIN_Y); + clear_step_out_forecast_pillars(&state); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, + INF_ARENA_MIN_X + 3, INF_ARENA_MIN_Y + 3, 1); + inf_rebuild_entity_collision_flags(&state); + + InfStepOutForecast forecast; + inf_build_step_out_forecast_fast_readonly_ctx( + &state, inf_legacy_context(), &forecast); + + const InfStepOutForecastAction* action = &forecast.actions[9]; + ASSERT_INT_EQ("invalid run-west action is invalid", action->valid, 0); + ASSERT_INT_EQ("invalid action has no same-tick conflict", + action->same_tick_mixed_style_conflict, 0); + ASSERT_INT_EQ("invalid action has no off-tick opportunity", + action->ranger_mager_offtick_opportunity, 0); + ASSERT_INT_EQ("invalid action has no melee fallback", + action->melee_fallback_exposure, 0); + for (int tick_idx = 0; tick_idx < INF_STEP_OUT_FORECAST_HORIZON; tick_idx++) { + ASSERT_INT_EQ("invalid action has no melee count", + action->ticks[tick_idx].melee_count, 0); + ASSERT_INT_EQ("invalid action has no ranged count", + action->ticks[tick_idx].ranged_count, 0); + ASSERT_INT_EQ("invalid action has no magic count", + action->ticks[tick_idx].magic_count, 0); + ASSERT_INT_EQ("invalid action has no blob scan count", + action->ticks[tick_idx].blob_scan_count, 0); + ASSERT_INT_EQ("invalid action has no max hit", + action->ticks[tick_idx].max_hit, 0); + } +} + +static void test_step_out_forecast_south_pillar_ranger_mager_order(void) { + printf("--- step-out forecast south pillar ranger/mager order ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 22, 17); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 17, 25, 0); + add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 22, 26, 0); + + InfStepOutForecast forecast; + inf_build_step_out_forecast(&state, &forecast); + + const InfStepOutForecastAction* run_west = &forecast.actions[11]; + assert_step_out_ranger_then_mager("south pillar run west", run_west, 20, 17); +} + +static void test_step_out_forecast_west_pillar_ranger_mager_order(void) { + printf("--- step-out forecast west pillar ranger/mager order ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 11, 29); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 8, 40, 0); + add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 16, 42, 0); + + InfStepOutForecast forecast; + inf_build_step_out_forecast(&state, &forecast); + + const InfStepOutForecastAction* walk_north = &forecast.actions[4]; + assert_step_out_ranger_then_mager("west pillar walk north", walk_north, 11, 28); +} + +static void test_step_out_forecast_inactive_pillar_does_not_create_cover(void) { + printf("--- step-out forecast inactive pillar does not create cover ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 29, 39); + state.pillars[2].active = 0; + state.pillars[2].hp = 0; + inf_rebuild_los(&state); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 24, 31, 0); + add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 29, 30, 0); + + InfStepOutForecast forecast; + inf_build_step_out_forecast(&state, &forecast); + + const InfStepOutForecastAction* idle = &forecast.actions[0]; + ASSERT_INT_EQ("inactive north pillar exposes ranger immediately", + idle->ticks[0].ranger_count, 1); + ASSERT_INT_EQ("inactive north pillar exposes mager immediately", + idle->ticks[0].mager_count, 1); + ASSERT_INT_EQ("inactive north pillar shows same-tick conflict", + idle->same_tick_mixed_style_conflict, 1); + ASSERT_INT_EQ("inactive north pillar has no off-tick cover", + idle->ranger_mager_offtick_opportunity, 0); +} + +static void test_step_out_same_tick_ranger_mager_event_logs(void) { + printf("--- step-out same-tick ranger/mager event logs ---\n"); + + InfernoState state; + init_step_out_forecast_stack_state(&state, 14, 35); + add_step_out_forecast_npc(&state, 0, INF_NPC_RANGER, 5, 39, 0); + add_step_out_forecast_npc(&state, 1, INF_NPC_MAGER, 4, 34, 0); + + int actions[INF_NUM_ACTION_HEADS] = {0}; + actions[INF_HEAD_MOVE] = 13; + inf_step((EncounterState*)&state, actions); + + ASSERT_INT_EQ("step-out tick moved the player", + state.tick_scratch.player_moved, 1); + ASSERT_INT_EQ("movement tick does not count attacks before NPCs see new tile", + state.total_step_out_ranger_mager_same_tick_attacks, 0); + + int noop[INF_NUM_ACTION_HEADS] = {0}; + inf_step((EncounterState*)&state, noop); + + ASSERT_INT_EQ("same-tick ranger/mager event counted", + state.total_ranger_mager_same_tick_attacks, 1); + ASSERT_INT_EQ("step-out same-tick ranger/mager event counted", + state.total_step_out_ranger_mager_same_tick_attacks, 1); +} + +static void test_direct_start_waves_spawn_without_empty_gap(void) { + printf("--- direct start waves spawn without empty gap ---\n"); + + EncounterState* raw = inf_create(); + reset_inferno_at_public_wave(raw, 20, 1.0f); + InfernoState* regular = (InfernoState*)raw; + + ASSERT_INT_EQ("late regular start has no empty wave delay", + regular->wave_spawn_delay, 0); + ASSERT_INT_EQ("late regular start spawns mobs immediately", + count_active_npcs(regular) > 0, 1); + + reset_inferno_at_public_wave(raw, 69, 1.0f); + InfernoState* zuk = (InfernoState*)raw; + + ASSERT_INT_EQ("zuk start has no empty wave delay", zuk->wave_spawn_delay, 0); + ASSERT_INT_EQ("zuk spawned immediately", find_active_npc_type(zuk, INF_NPC_ZUK) >= 0, 1); + ASSERT_INT_EQ("zuk shield spawned immediately", + find_active_npc_type(zuk, INF_NPC_ZUK_SHIELD) >= 0, 1); + ASSERT_INT_EQ("zuk start player x", zuk->player.x, INF_ZUK_PLAYER_START_X); + ASSERT_INT_EQ("zuk start player y", zuk->player.y, INF_ZUK_PLAYER_START_Y); + + inf_destroy(raw); +} + +static void test_joseph_start_wave_70_seeds_zuk_jad_checkpoint(void) { + printf("--- Joseph start wave 70 seeds Zuk Jad checkpoint ---\n"); + + EncounterState* raw = inf_create(); + reset_inferno_at_public_wave(raw, 70, 1.0f); + InfernoState* state = (InfernoState*)raw; + int zuk_idx = find_active_npc_type(state, INF_NPC_ZUK); + int jad_idx = find_active_npc_type(state, INF_NPC_JAD); + int shield_idx = find_active_npc_type(state, INF_NPC_ZUK_SHIELD); + + ASSERT_INT_EQ("runtime wave is Zuk", state->wave, INF_NUM_WAVES - 1); + ASSERT_INT_EQ("Zuk exists", zuk_idx >= 0, 1); + ASSERT_INT_EQ("Zuk starts under Jad threshold", state->npcs[zuk_idx].hp, 479); + ASSERT_INT_EQ("Jad checkpoint spawned Jad", jad_idx >= 0, 1); + ASSERT_INT_EQ("Jad targets shield", state->npcs[jad_idx].aggro_target, shield_idx); + ASSERT_INT_EQ("Zuk healers not spawned yet", state->zuk.healer_spawned, 0); + + inf_destroy(raw); +} + +static void test_joseph_start_wave_71_seeds_zuk_healer_checkpoint(void) { + printf("--- Joseph start wave 71 seeds Zuk healer checkpoint ---\n"); + + EncounterState* raw = inf_create(); + reset_inferno_at_public_wave(raw, 71, 1.0f); + InfernoState* state = (InfernoState*)raw; + int zuk_idx = find_active_npc_type(state, INF_NPC_ZUK); + + ASSERT_INT_EQ("runtime wave is Zuk", state->wave, INF_NUM_WAVES - 1); + ASSERT_INT_EQ("Zuk exists", zuk_idx >= 0, 1); + ASSERT_INT_EQ("Zuk starts under healer threshold", state->npcs[zuk_idx].hp, 239); + ASSERT_INT_EQ("Zuk healer checkpoint marks spawned", state->zuk.healer_spawned, 1); + ASSERT_INT_EQ("Zuk healer count", count_active_npc_type(state, INF_NPC_HEALER_ZUK), 4); + for (int i = 0; i < INF_MAX_NPCS; i++) { + if (state->npcs[i].active && state->npcs[i].type == INF_NPC_HEALER_ZUK) { + ASSERT_INT_EQ("Zuk healer targets Zuk", state->npcs[i].aggro_target, zuk_idx); + } + } + + inf_destroy(raw); +} + +static void test_zuk_ready_countdown_holds_npcs_then_releases(void) { + printf("--- zuk ready countdown holds npcs then releases ---\n"); + + EncounterState* raw = inf_create(); + reset_inferno_at_public_wave(raw, 69, 1.0f); + InfernoState* state = (InfernoState*)raw; + int zuk_idx = find_active_npc_type(state, INF_NPC_ZUK); + + ASSERT_INT_EQ("zuk exists during ready countdown", zuk_idx >= 0, 1); + ASSERT_INT_EQ("zuk attack timer starts at reference delay", + state->npcs[zuk_idx].attack_timer, 14); + + for (int i = 0; i < 5; i++) + step_inferno_noop(state); + ASSERT_INT_EQ("ready countdown does not tick zuk early", + state->npcs[zuk_idx].attack_timer, 14); + + step_inferno_noop(state); + ASSERT_INT_EQ("zuk attack timer starts once ready countdown clears", + state->npcs[zuk_idx].attack_timer, 13); + + inf_destroy(raw); +} + +static void init_zuk_timing_state(InfernoState* state) { + reset_test_context(); + memset(state, 0, sizeof(*state)); + memset(state->npc_los_cache, -1, sizeof(state->npc_los_cache)); + state->rng_state = 7; + state->wave = 68; + state->player.entity_type = ENTITY_PLAYER; + state->player.x = INF_ZUK_PLAYER_START_X; + state->player.y = INF_ZUK_PLAYER_START_Y; + state->player.base_hitpoints = 99; + state->player.current_hitpoints = 99; + state->player.base_prayer = 99; + state->player.current_prayer = 99; + state->player.base_defence = 99; + state->player.current_defence = 99; + state->player.base_magic = 99; + state->player.current_magic = 99; + state->player.base_ranged = 99; + state->player.current_ranged = 99; + state->player_dest_x = -1; + state->player_dest_y = -1; + state->player_last_interaction_target_slot = -1; + state->player_last_interaction_age = 1; + state->tick_at_first_zuk_healer_target = -1; + state->tick_at_first_zuk_healer_attack = -1; + state->weapon_set = INF_GEAR_LONG_RANGE; + osrs_interaction_init(&state->interaction); + encounter_compute_loadout_stats(INF_MAX_RANGE_LONG_LOADOUT, ATTACK_STYLE_RANGED, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, + &state->loadout_stats[INF_GEAR_LONG_RANGE]); + + state->npcs[0] = make_test_npc( + INF_NPC_ZUK, INF_ZUK_X, INF_ZUK_Y, INF_NPC_STATS[INF_NPC_ZUK].size); + state->npcs[0].active = 1; + state->npcs[0].hp = state->npcs[0].max_hp = INF_NPC_STATS[INF_NPC_ZUK].hp; + state->npcs[0].attack_timer = 14; + state->npcs[0].stun_timer = 8; + + state->npcs[1] = make_test_npc( + INF_NPC_ZUK_SHIELD, INF_ZUK_SHIELD_X, INF_ZUK_SHIELD_Y, + INF_NPC_STATS[INF_NPC_ZUK_SHIELD].size); + state->npcs[1].active = 1; + state->npcs[1].hp = state->npcs[1].max_hp = INF_NPC_STATS[INF_NPC_ZUK_SHIELD].hp; + + state->zuk.shield_idx = 1; + state->zuk.shield_dir = 1; + state->zuk.set_timer = 72; + state->zuk.set_interval = 350; +} + +static void equip_zuk_timing_state_blowpipe(InfernoState* state) { + state->weapon_set = INF_GEAR_BP; + encounter_apply_loadout(&state->player, INF_MAX_RANGE_FAST_LOADOUT, GEAR_RANGED); + encounter_compute_loadout_stats(INF_MAX_RANGE_FAST_LOADOUT, ATTACK_STYLE_RANGED, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_RAPID, 0, + &state->loadout_stats[INF_GEAR_BP]); +} + +static void test_zuk_shield_does_not_set_collision_flags(void) { + printf("--- zuk shield does not set collision flags ---\n"); + + ASSERT_INT_EQ("zuk shield follows reference CollisionType.NONE", + inf_npc_sets_collision_flag(INF_NPC_ZUK_SHIELD), 0); +} + +static void test_zuk_obs_exposes_attack_timer_summary(void) { + printf("--- zuk obs exposes attack timer summary ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + state.npcs[0].attack_timer = 3; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + ASSERT_FLOAT_NEAR("zuk attack timer uses existing player placeholder", + obs[INF_OBS_ZUK_ATTACK_TIMER], 0.3f, 1e-6f); +} + +static void test_zuk_obs_exposes_pending_sparks(void) { + printf("--- zuk obs exposes exact pending spark landings ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + state.pending_sparks[0] = (InfPendingSpark){ + .active = 1, .src_x = state.player.x + 5, .src_y = state.player.y, + .x = state.player.x + 4, .y = state.player.y, + .damage = 3, .ticks_remaining = 4, + }; + state.pending_sparks[1] = (InfPendingSpark){ + .active = 1, .src_x = state.player.x + 5, .src_y = state.player.y, + .x = state.player.x - 1, .y = state.player.y + 2, + .damage = 7, .ticks_remaining = 2, + }; + state.pending_sparks[2] = (InfPendingSpark){ + .active = 1, .src_x = state.player.x - 3, .src_y = state.player.y + 1, + .x = state.player.x - 2, .y = state.player.y + 1, + .damage = 5, .ticks_remaining = 3, + }; + state.pending_sparks[3] = (InfPendingSpark){ + .active = 1, .src_x = state.player.x + 1, .src_y = state.player.y + 1, + .x = state.player.x + 1, .y = state.player.y + 1, + .damage = 6, .ticks_remaining = 4, + }; + state.pending_sparks[4] = (InfPendingSpark){ + .active = 1, .src_x = state.player.x + 2, .src_y = state.player.y + 2, + .x = state.player.x + 2, .y = state.player.y + 2, + .damage = 8, .ticks_remaining = 5, + }; + state.pending_sparks[5] = (InfPendingSpark){ + .active = 1, .src_x = state.player.x + 3, .src_y = state.player.y + 3, + .x = state.player.x + 3, .y = state.player.y + 3, + .damage = 9, .ticks_remaining = 6, + }; + + int spark_start = inferno_spark_obs_start(); + int spark_features = INF_FEATURES_PER_SPARK; + int spark_slots = INF_SPARK_OBS_SLOTS; + ASSERT_INT_EQ("inferno obs has full spark section", + INF_NUM_OBS >= spark_start + spark_features * spark_slots, 1); + if (INF_NUM_OBS < spark_start + spark_features * spark_slots) + return; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + ASSERT_INT_EQ("spark obs keeps all pending slots", spark_slots, INF_MAX_PENDING_SPARKS); + ASSERT_INT_EQ("spark obs carries landing and source", spark_features, 7); + ASSERT_FLOAT_NEAR("first spark active", obs[spark_start], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("first spark landing x", + obs[spark_start + 1], -1.0f / (float)INF_ARENA_WIDTH, 1e-6f); + ASSERT_FLOAT_NEAR("first spark landing y", + obs[spark_start + 2], 2.0f / (float)INF_ARENA_HEIGHT, 1e-6f); + ASSERT_FLOAT_NEAR("first spark source x", + obs[spark_start + 3], 5.0f / (float)INF_ARENA_WIDTH, 1e-6f); + ASSERT_FLOAT_NEAR("first spark source y", + obs[spark_start + 4], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("first spark timer", + obs[spark_start + 5], 0.2f, 1e-6f); + ASSERT_FLOAT_NEAR("first spark damage", + obs[spark_start + 6], 0.7f, 1e-6f); + ASSERT_FLOAT_NEAR("second spark landing x", + obs[spark_start + spark_features + 1], + -2.0f / (float)INF_ARENA_WIDTH, 1e-6f); + ASSERT_FLOAT_NEAR("third spark sorts same-tick nearest landing first", + obs[spark_start + 2 * spark_features + 1], + 1.0f / (float)INF_ARENA_WIDTH, 1e-6f); +} + +static void assert_human_blowpipe_zuk_chase_endpoint( + int start_x, int start_y, int expected_x, int expected_y, + const char* label +) { + InfernoState endpoint_state; + init_zuk_timing_state(&endpoint_state); + equip_zuk_timing_state_blowpipe(&endpoint_state); + endpoint_state.player.x = start_x; + endpoint_state.player.y = start_y; + endpoint_state.player.attack_timer = 3; + endpoint_state.npcs[0].stun_timer = 64; + endpoint_state.npcs[0].attack_timer = 64; + inf_rebuild_entity_collision_flags(&endpoint_state); + + ASSERT_INT_EQ("Zuk blowpipe endpoint starts out of range", + inf_player_can_attack_npc_from_current_tile(&endpoint_state, 0), 0); + + HumanInput hi = make_human_input(); + human_input_queue_attack_npc(&hi, 0); + inf_step_human_commands((EncounterState*)&endpoint_state, &hi); + human_input_destroy(&hi); + + for (int i = 0; i < 16; i++) { + HumanInput empty = make_human_input(); + inf_step_human_commands((EncounterState*)&endpoint_state, &empty); + human_input_destroy(&empty); + } + + ASSERT_INT_EQ(label, endpoint_state.player.x, expected_x); + ASSERT_INT_EQ("Zuk blowpipe endpoint y matches InfernoTrainer", + endpoint_state.player.y, expected_y); + ASSERT_INT_EQ("Zuk blowpipe endpoint remains out of range", + inf_player_can_attack_npc_from_current_tile(&endpoint_state, 0), 0); + ASSERT_INT_EQ("Zuk blowpipe endpoint does not fire", + endpoint_state.npcs[0].pending_hits.hits[0].active, 0); + ASSERT_INT_EQ("Zuk blowpipe endpoint keeps interaction active", + osrs_interaction_active(&endpoint_state.interaction), 1); +} + +static void test_human_blowpipe_click_chases_zuk_out_of_range(void) { + printf("--- human blowpipe click chases Zuk out of range ---\n"); + + assert_human_blowpipe_zuk_chase_endpoint( + 11, 42, 22, INF_ARENA_MAX_Y, + "left-edge Zuk blowpipe endpoint x matches InfernoTrainer"); + + assert_human_blowpipe_zuk_chase_endpoint( + 39, 42, 28, INF_ARENA_MAX_Y, + "right-edge Zuk blowpipe endpoint x matches InfernoTrainer"); + + InfernoState edge_state; + init_zuk_timing_state(&edge_state); + equip_zuk_timing_state_blowpipe(&edge_state); + edge_state.player.x = 25; + edge_state.player.y = 42; + edge_state.player.attack_timer = 3; + edge_state.npcs[0].stun_timer = 64; + edge_state.npcs[0].attack_timer = 64; + inf_rebuild_entity_collision_flags(&edge_state); + + ASSERT_INT_EQ("north-row Zuk click starts out of blowpipe range", + inf_player_can_attack_npc_from_current_tile(&edge_state, 0), 0); + + HumanInput edge_hi = make_human_input(); + human_input_queue_attack_npc(&edge_hi, 0); + inf_step_human_commands((EncounterState*)&edge_state, &edge_hi); + human_input_destroy(&edge_hi); + + ASSERT_INT_EQ("north-row Zuk click follows reference seek x", edge_state.player.x, 24); + ASSERT_INT_EQ("north-row Zuk click walks to max north row", + edge_state.player.y, INF_ARENA_MAX_Y); + ASSERT_INT_EQ("north-row Zuk click remains outside blowpipe range", + inf_player_can_attack_npc_from_current_tile(&edge_state, 0), 0); + ASSERT_INT_EQ("north-row Zuk cooldown prevents immediate hit", + edge_state.npcs[0].pending_hits.hits[0].active, 0); + ASSERT_INT_EQ("north-row Zuk interaction remains active", + osrs_interaction_active(&edge_state.interaction), 1); + + InfernoState state; + init_zuk_timing_state(&state); + equip_zuk_timing_state_blowpipe(&state); + state.player.x = 25; + state.player.y = 37; + state.player.attack_timer = 3; + state.npcs[0].stun_timer = 64; + state.npcs[0].attack_timer = 64; + inf_rebuild_entity_collision_flags(&state); + + ASSERT_INT_EQ("human Zuk click starts out of blowpipe range", + inf_player_can_attack_npc_from_current_tile(&state, 0), 0); + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + HumanInput hi = make_human_input(); + human_input_queue_attack_npc(&hi, 0); + inf_step_human_commands((EncounterState*)&state, &hi); + human_input_destroy(&hi); + + ASSERT_INT_EQ("human Zuk click keeps interaction active", + osrs_interaction_active(&state.interaction), 1); + ASSERT_INT_EQ("human Zuk click targets Zuk", state.interaction.target_slot, 0); + ASSERT_INT_EQ("human Zuk click keeps player x", state.player.x, 25); + ASSERT_INT_EQ("human Zuk click walks north toward range", state.player.y > 37, 1); + ASSERT_INT_EQ("human Zuk click stays inside arena", state.player.y <= INF_ARENA_MAX_Y, 1); + ASSERT_INT_EQ("cooldown prevents immediate Zuk hit", + state.npcs[0].pending_hits.hits[0].active, 0); + ASSERT_INT_EQ("attack timer decrements after chase", state.player.attack_timer, 2); + + for (int i = 0; i < 8; i++) { + HumanInput empty = make_human_input(); + inf_step_human_commands((EncounterState*)&state, &empty); + human_input_destroy(&empty); + } + + ASSERT_INT_EQ("human Zuk click ends at max north row", state.player.y, INF_ARENA_MAX_Y); + ASSERT_INT_EQ("human Zuk click never reaches blowpipe attack tile", + inf_player_can_attack_npc_from_current_tile(&state, 0), 0); + ASSERT_INT_EQ("human Zuk click does not fire unreachable blowpipe hit", + state.npcs[0].pending_hits.hits[0].active, 0); + ASSERT_INT_EQ("human Zuk click keeps interaction after chase", + osrs_interaction_active(&state.interaction), 1); +} + +static void test_zuk_healer_blowpipe_target_chases_out_of_range(void) { + printf("--- Zuk healer blowpipe target chases out of range ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + equip_zuk_timing_state_blowpipe(&state); + state.tick = 432; + state.player.x = 25; + state.player.y = 42; + state.player.attack_timer = 3; + + state.zuk.healer_spawned = 1; + state.npcs[2] = make_test_npc( + INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[2].aggro_target = 0; + inf_rebuild_entity_collision_flags(&state); + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + int healer_slot = inf_find_target_obs_slot(&state, 2); + ASSERT_INT_EQ("healer appears in target obs", healer_slot >= 0, 1); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_TARGET] = healer_slot + 1; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("healer target keeps interaction active", + osrs_interaction_active(&state.interaction), 1); + ASSERT_INT_EQ("healer target selects healer", state.interaction.target_slot, 2); + ASSERT_INT_EQ("healer target follows reference seek x", state.player.x, 23); + ASSERT_INT_EQ("healer target follows reference seek y", state.player.y, 42); + ASSERT_INT_EQ("healer target remains active while still out of range", + inf_player_can_attack_npc_from_current_tile(&state, 2), 0); + ASSERT_INT_EQ("cooldown prevents immediate healer hit", + state.npcs[2].pending_hits.hits[0].active, 0); + ASSERT_INT_EQ("attack timer decrements after healer chase", + state.player.attack_timer, 2); + ASSERT_INT_EQ("healer target is counted as cooldown after chase", + state.total_zuk_healer_cooldown_ticks, 1); + ASSERT_INT_EQ("healer target is not counted as range blocked after chase", + state.total_zuk_healer_out_of_range_ticks, 0); + + int healer_hit_seen = 0; + for (int i = 0; i < 4 && !healer_hit_seen; i++) { + memset(actions, 0, sizeof(actions)); + inf_tick_player(&state, actions, 1); + healer_hit_seen = state.npcs[2].pending_hits.hits[0].active; + } + + ASSERT_INT_EQ("healer target eventually fires after chase and cooldown", + healer_hit_seen, 1); + ASSERT_INT_EQ("healer target remains attackable after chase", + inf_player_can_attack_npc_from_current_tile(&state, 2), 1); +} + +static void test_render_facing_prefers_attack_target_while_chasing(void) { + printf("--- render facing prefers attack target while chasing ---\n"); + + RenderEntity entity; + memset(&entity, 0, sizeof(entity)); + entity.current_hitpoints = 99; + entity.attack_target_entity_idx = 2; + entity.attack_style_this_tick = ATTACK_STYLE_NONE; + + ASSERT_INT_EQ("attack target facing wins over movement", + render_entity_select_facing_mode(&entity, 1), + RENDER_ENTITY_FACE_ATTACK_TARGET); + + entity.attack_target_entity_idx = -1; + ASSERT_INT_EQ("untargeted movement faces movement direction", + render_entity_select_facing_mode(&entity, 1), + RENDER_ENTITY_FACE_MOVEMENT); +} + +static void test_render_identity_survives_npc_death_compaction(void) { + printf("--- render identity survives NPC death compaction ---\n"); + + RenderEntity previous[4]; + memset(previous, 0, sizeof(previous)); + previous[0].entity_type = ENTITY_PLAYER; + previous[1].entity_type = ENTITY_NPC; + previous[1].npc_slot = 0; + previous[1].npc_def_id = INF_NPC_DEF_IDS[INF_NPC_MAGER]; + previous[2].entity_type = ENTITY_NPC; + previous[2].npc_slot = 1; + previous[2].npc_def_id = INF_NPC_DEF_IDS[INF_NPC_RANGER]; + previous[3].entity_type = ENTITY_NPC; + previous[3].npc_slot = 2; + previous[3].npc_def_id = INF_NPC_DEF_IDS[INF_NPC_MELEER]; + + int used[4] = {0}; + RenderEntity current = previous[2]; + int previous_idx = render_entity_find_previous_identity_index( + previous, 4, used, ¤t); + + ASSERT_INT_EQ("ranger keeps old visual slot after earlier NPC dies", + previous_idx, 2); + used[previous_idx] = 1; + current = previous[3]; + previous_idx = render_entity_find_previous_identity_index( + previous, 4, used, ¤t); + ASSERT_INT_EQ("meleer keeps old visual slot after earlier NPC dies", + previous_idx, 3); + + current.npc_def_id = INF_NPC_DEF_IDS[INF_NPC_RANGER]; + previous_idx = render_entity_find_previous_identity_index( + previous, 4, used, ¤t); + ASSERT_INT_EQ("same slot with different NPC type is not the same identity", + previous_idx, -1); + + used[0] = 0; + used[1] = 0; + used[2] = 0; + used[3] = 0; + previous[1].npc_instance_id = 41; + current = previous[1]; + current.npc_instance_id = 42; + previous_idx = render_entity_find_previous_identity_index( + previous, 4, used, ¤t); + ASSERT_INT_EQ("same slot and type with new spawn id resets visual state", + previous_idx, -1); + + current.npc_instance_id = 41; + previous_idx = render_entity_find_previous_identity_index( + previous, 4, used, ¤t); + ASSERT_INT_EQ("same spawn id keeps visual state", + previous_idx, 1); +} + +static void test_render_identity_matches_two_players_across_tick(void) { + printf("--- render identity matches two players across tick ---\n"); + + RenderEntity previous[2]; + memset(previous, 0, sizeof(previous)); + previous[0].entity_type = ENTITY_PLAYER; + previous[1].entity_type = ENTITY_PLAYER; + + int used[2] = {0, 0}; + RenderEntity current = previous[0]; + int idx0 = render_entity_find_previous_identity_index(previous, 2, used, ¤t); + ASSERT_INT_EQ("first PvP player matches previous[0]", idx0, 0); + + used[idx0] = 1; + current = previous[1]; + int idx1 = render_entity_find_previous_identity_index(previous, 2, used, ¤t); + ASSERT_INT_EQ("second PvP player matches previous[1]", idx1, 1); +} + +static void test_render_identity_two_players_claim_unique_slots(void) { + printf("--- render identity two players claim unique slots ---\n"); + + RenderEntity previous[2]; + memset(previous, 0, sizeof(previous)); + previous[0].entity_type = ENTITY_PLAYER; + previous[1].entity_type = ENTITY_PLAYER; + + int used[2] = {0, 0}; + RenderEntity current; + memset(¤t, 0, sizeof(current)); + current.entity_type = ENTITY_PLAYER; + + int idx0 = render_entity_find_previous_identity_index(previous, 2, used, ¤t); + if (idx0 >= 0) used[idx0] = 1; + int idx1 = render_entity_find_previous_identity_index(previous, 2, used, ¤t); + + ASSERT_INT_EQ("first player gets a previous slot", (idx0 >= 0), 1); + ASSERT_INT_EQ("second player gets a previous slot", (idx1 >= 0), 1); + ASSERT_INT_EQ("two concurrent players claim distinct previous slots", + (idx0 != idx1), 1); +} + +static void test_render_identity_single_player_unchanged(void) { + printf("--- render identity single player unchanged ---\n"); + + RenderEntity previous[1]; + memset(previous, 0, sizeof(previous)); + previous[0].entity_type = ENTITY_PLAYER; + + int used[1] = {0}; + RenderEntity current = previous[0]; + int idx = render_entity_find_previous_identity_index(previous, 1, used, ¤t); + ASSERT_INT_EQ("single-player encounter still matches previous[0]", idx, 0); +} + +static void test_render_motion_speed_ladder_matches_deob(void) { + printf("--- render motion speed ladder matches deob ---\n"); + + int stall_debt = 0; + ASSERT_INT_EQ("depth one walks at 4", + osrs_render_speed_one_client_tick(1, 0, &stall_debt), 4); + ASSERT_INT_EQ("depth two walks at 4", + osrs_render_speed_one_client_tick(2, 0, &stall_debt), 4); + ASSERT_INT_EQ("depth three catches up at 6", + osrs_render_speed_one_client_tick(3, 0, &stall_debt), 6); + ASSERT_INT_EQ("depth four catches up at 8", + osrs_render_speed_one_client_tick(4, 0, &stall_debt), 8); + + ASSERT_INT_EQ("run doubles base to 8", + osrs_render_speed_one_client_tick(1, 1, &stall_debt), 8); + ASSERT_INT_EQ("run doubles depth-three catch-up to 12", + osrs_render_speed_one_client_tick(3, 1, &stall_debt), 12); + ASSERT_INT_EQ("run doubles depth-four catch-up to 16", + osrs_render_speed_one_client_tick(4, 1, &stall_debt), 16); + + stall_debt = 3; + ASSERT_INT_EQ("stall debt with queue depth repays at 8", + osrs_render_speed_one_client_tick(2, 0, &stall_debt), 8); + ASSERT_INT_EQ("stall debt repays one tick", stall_debt, 2); + + stall_debt = 3; + ASSERT_INT_EQ("depth one does not spend stall debt", + osrs_render_speed_one_client_tick(1, 0, &stall_debt), 4); + ASSERT_INT_EQ("depth one keeps stall debt", stall_debt, 3); + + ASSERT_INT_EQ("speed 8 selects run pose", + osrs_render_speed_uses_run_pose(8.0f), 1); + ASSERT_INT_EQ("speed 6 stays on walk pose", + osrs_render_speed_uses_run_pose(6.0f), 0); + ASSERT_INT_EQ("speed 4 stays on walk pose", + osrs_render_speed_uses_run_pose(4.0f), 0); +} + +static void test_render_motion_lone_step_takes_32_client_ticks(void) { + printf("--- render motion lone step takes 32 client ticks ---\n"); + + OsrsRenderWaypointQueue q; + osrs_render_waypoint_queue_clear(&q); + float sub_x = 64.0f, sub_y = 64.0f; + osrs_render_waypoint_push(&q, 64.0f + 128.0f, 64.0f, 0); + + int stall_debt = 0; + int ticks = 0; + while (q.length > 0 && ticks < 100) { + int speed; + float ddx, ddy; + osrs_render_waypoint_advance_one_client_tick( + &q, &sub_x, &sub_y, &stall_debt, &speed, &ddx, &ddy); + ticks++; + } + ASSERT_INT_EQ("isolated 1-tile step takes 32 client ticks (640ms > 600ms tick)", + ticks, 32); + ASSERT_FLOAT_NEAR("arrived at the waypoint", sub_x, 192.0f, 1e-6f); +} + +static void render_motion_continuity_case( + const char* label, float tiles_per_tick, int running +) { + OsrsRenderWaypointQueue q; + osrs_render_waypoint_queue_clear(&q); + float sub_x = 64.0f, sub_y = 64.0f; + float true_x = 64.0f; + int stall_debt = 0; + int started = 0, pauses = 0, max_depth = 0; + + for (int tick = 0; tick < 40; tick++) { + true_x += tiles_per_tick * OSRS_RENDER_SUB_UNITS_PER_TILE; + osrs_render_waypoint_push(&q, true_x, 64.0f, running); + for (int ct = 0; ct < (int)OSRS_RENDER_CLIENT_TICKS_PER_GAME_TICK; ct++) { + int speed; + float ddx, ddy; + int moving = osrs_render_waypoint_advance_one_client_tick( + &q, &sub_x, &sub_y, &stall_debt, &speed, &ddx, &ddy); + if (started && !moving) pauses++; + if (moving) started = 1; + if (q.length > max_depth) max_depth = q.length; + } + } + + char msg[128]; + snprintf(msg, sizeof(msg), "%s never pauses mid-walk", label); + ASSERT_INT_EQ(msg, pauses, 0); + snprintf(msg, sizeof(msg), "%s queue depth stays bounded", label); + ASSERT_INT_EQ(msg, max_depth <= 3, 1); + snprintf(msg, sizeof(msg), "%s visual trails within three tiles", label); + ASSERT_INT_EQ(msg, + (true_x - sub_x) <= 3.0f * OSRS_RENDER_SUB_UNITS_PER_TILE, 1); +} + +static void test_render_motion_continuous_movement_never_pauses(void) { + printf("--- render motion continuous movement never pauses ---\n"); + + render_motion_continuity_case("continuous walk", 1.0f, 0); + render_motion_continuity_case("continuous run", 2.0f, 1); +} + +static void test_render_motion_waypoint_pop_snap_and_overflow(void) { + printf("--- render motion waypoint pop, axis snap, queue overflow ---\n"); + + OsrsRenderWaypointQueue q; + osrs_render_waypoint_queue_clear(&q); + float sub_x = 64.0f, sub_y = 64.0f; + osrs_render_waypoint_push(&q, 192.0f, 64.0f, 0); + osrs_render_waypoint_push(&q, 320.0f, 64.0f, 0); + int stall_debt = 0; + for (int ct = 0; ct < 32; ct++) { + int speed; + float ddx, ddy; + osrs_render_waypoint_advance_one_client_tick( + &q, &sub_x, &sub_y, &stall_debt, &speed, &ddx, &ddy); + } + ASSERT_FLOAT_NEAR("arrival tick clamps at the popped waypoint", + sub_x, 192.0f, 1e-6f); + ASSERT_INT_EQ("first waypoint popped, second still queued", q.length, 1); + + osrs_render_waypoint_queue_clear(&q); + sub_x = 0.0f; + sub_y = 0.0f; + osrs_render_waypoint_push(&q, 300.0f, 40.0f, 0); + { + int speed; + float ddx, ddy; + osrs_render_waypoint_advance_one_client_tick( + &q, &sub_x, &sub_y, &stall_debt, &speed, &ddx, &ddy); + } + ASSERT_FLOAT_NEAR("beyond-2-tile gap snaps x to the waypoint", + sub_x, 300.0f, 1e-6f); + ASSERT_FLOAT_NEAR("beyond-2-tile gap snaps y to the waypoint too", + sub_y, 40.0f, 1e-6f); + ASSERT_INT_EQ("snapped waypoint pops the same cycle", q.length, 0); + + osrs_render_waypoint_queue_clear(&q); + for (int i = 1; i <= 11; i++) + osrs_render_waypoint_push(&q, (float)(i * 128), 0.0f, 0); + ASSERT_INT_EQ("queue caps at 10 waypoints", + q.length, OSRS_RENDER_WAYPOINT_QUEUE_DEPTH); + ASSERT_FLOAT_NEAR("overflow drops the oldest waypoint", + q.x[q.length - 1], 256.0f, 1e-6f); + ASSERT_FLOAT_NEAR("newest waypoint at the queue head", + q.x[0], 11.0f * 128.0f, 1e-6f); +} + +static void test_render_motion_seed_classification_uses_explicit_teleport(void) { + printf("--- render motion seed classification uses explicit teleport ---\n"); + + ASSERT_INT_EQ("persistent normal entity does not seed from distance", + osrs_render_should_seed_visual_position( + 1, 0, 0, RENDER_MOVEMENT_NORMAL), + 0); + ASSERT_INT_EQ("explicit teleport seeds visual position", + osrs_render_should_seed_visual_position( + 1, 0, 0, RENDER_MOVEMENT_TELEPORT), + 1); + ASSERT_INT_EQ("new identity seeds visual position", + osrs_render_should_seed_visual_position( + 1, 1, 0, RENDER_MOVEMENT_NORMAL), + 1); + ASSERT_INT_EQ("invisible to visible appearance seeds visual position", + osrs_render_should_seed_visual_position( + 1, 0, 1, RENDER_MOVEMENT_NORMAL), + 1); +} + +static void test_render_post_tick_removed_distance_snap_branch(void) { + printf("--- render post tick removed distance snap branch ---\n"); + + ASSERT_SOURCE_BLOCK_NOT_CONTAINS( + "render post tick does not snap from tile distance", + "ocean/osrs/osrs_render.h", + "static void render_post_tick", + "static void render_client_tick", + "tile_dx > 2.0f"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "render post tick seeds only by movement kind classification", + "ocean/osrs/osrs_render.h", + "static void render_post_tick", + "static void render_client_tick", + "osrs_render_should_seed_visual_position"); +} + +static void test_render_bridge_marks_genuine_teleports(void) { + printf("--- render bridge marks genuine teleports ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "inferno meleer dig-up marks render teleport", + "ocean/osrs/encounters/inferno/encounter_inferno_obs_mask.inc", + "RenderMovementKind render_movement_kind", + "RenderEntity* re = &out[n++];", + "RENDER_MOVEMENT_TELEPORT"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "zulrah surface marks render teleport", + "ocean/osrs/encounters/encounter_zulrah.h", + "static void zul_fill_render_entities", + "for (int i = 0; i < ZUL_MAX_SNAKELINGS", + "RENDER_MOVEMENT_TELEPORT"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "lab move command snaps only render visual state", + "ocean/osrs/osrs_render.h", + "static void render_lab_snap_line_visuals", + "static const EncounterDef* render_lab_def", + "render_lab_line_command_is(line, \"move_npc\")"); +} + +static void test_entity_model_ground_lift_keeps_floor_planes_above_terrain(void) { + printf("--- entity model ground lift keeps floor planes above terrain ---\n"); + + float ground = 2.0f; + ASSERT_FLOAT_NEAR("model ground is lifted above terrain", + osrs_render_entity_model_ground(ground), + ground + OSRS_RENDER_ENTITY_GROUND_LIFT, + 1e-6f); + ASSERT_INT_EQ("model ground lift is positive", + OSRS_RENDER_ENTITY_GROUND_LIFT > 0.0f, 1); +} + +static void test_spotanim_lookup_prefers_recolored_model_alias(void) { + printf("--- spotanim lookup prefers recolored model alias ---\n"); + + OsrsModel models[2]; + memset(models, 0, sizeof(models)); + models[0].model_id = 3136; + models[1].model_id = OSRS_SPOTANIM_RECOLOR_MODEL_BASE | 1384u; + + ModelCache secondary_cache; + memset(&secondary_cache, 0, sizeof(secondary_cache)); + secondary_cache.models = models; + secondary_cache.count = 2; + + OsrsSpotAnimDef meta; + memset(&meta, 0, sizeof(meta)); + meta.id = 1384; + meta.model_id = 3136; + + OsrsModel* found = effect_find_model(&meta, NULL, &secondary_cache, NULL); + ASSERT_INT_EQ("blob magic spotanim resolves recolored model", + found ? (int)found->model_id : -1, + (int)(OSRS_SPOTANIM_RECOLOR_MODEL_BASE | 1384u)); +} + +static void test_inferno_npc_spawn_id_changes_on_slot_reuse(void) { + printf("--- inferno npc spawn id changes on slot reuse ---\n"); + + InfernoState state = make_test_state(20, 20); + inf_build_npc_stats(); + inf_init_npc(&state, 3, INF_NPC_BLOB_RANGE, 18, 20); + uint32_t first_render_id = state.npcs[3].render_id; + inf_deactivate_npc(&state, 3); + inf_init_npc(&state, 3, INF_NPC_BLOB_RANGE, 19, 21); + uint32_t second_render_id = state.npcs[3].render_id; + + ASSERT_INT_EQ("first render id is nonzero", first_render_id != 0, 1); + ASSERT_INT_EQ("slot reuse gets a new render id", + second_render_id != first_render_id, 1); + + RenderEntity entities[4]; + int count = 0; + inf_fill_render_entities((EncounterState*)&state, entities, 4, &count); + ASSERT_INT_EQ("reused NPC appears in render list", count, 2); + ASSERT_INT_EQ("render entity carries spawn id", + entities[1].npc_instance_id, second_render_id); +} + +static void test_anim_rest_pose_resets_working_vertices(void) { + printf("--- animation rest pose resets working vertices ---\n"); + + uint8_t skins[2] = {0, 0}; + int16_t base[6] = {10, -20, 30, 40, -50, 60}; + AnimModelState* state = anim_model_state_create(skins, 2); + for (int i = 0; i < 6; i++) + state->verts[i] = (int16_t)(999 - i); + + anim_apply_rest_pose(state, base); + + for (int i = 0; i < 6; i++) { + char label[64]; + snprintf(label, sizeof(label), "rest vertex %d", i); + ASSERT_INT_EQ(label, state->verts[i], base[i]); + } + + anim_model_state_free(state); +} + +static void test_zuk_healer_target_action_tags_on_landed_hit(void) { + printf("--- zuk healer target action tags on landed hit ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + state.tick = 321; + state.player.x = 20; + state.player.y = 46; + encounter_apply_loadout(&state.player, INF_MAX_RANGE_LONG_LOADOUT, GEAR_RANGED); + + state.npcs[2] = make_test_npc( + INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[2].aggro_target = 0; + + float obs[INF_NUM_OBS]; + float mask[INF_ACTION_MASK_SIZE]; + inf_write_obs((EncounterState*)&state, obs); + inf_write_mask((EncounterState*)&state, mask); + + int healer_slot = 33; + ASSERT_INT_EQ("zuk healer occupies first healer slot", + state.current_obs_slots[healer_slot], 2); + ASSERT_FLOAT_NEAR("zuk healer target mask is valid", + mask[inferno_target_mask_slot_offset(healer_slot)], 1.0f, 1e-6f); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_TARGET] = healer_slot + 1; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("target action selects zuk healer", + state.interaction.target_slot, 2); + ASSERT_INT_EQ("target action records first healer target tick", + state.tick_at_first_zuk_healer_target, 321); + ASSERT_INT_EQ("target action records healer target tick count", + state.total_zuk_healer_target_ticks, 1); + ASSERT_INT_EQ("player attack queues healer hit", + state.npcs[2].pending_hits.hits[0].active, 1); + ASSERT_INT_EQ("player attack records first healer attack tick", + state.tick_at_first_zuk_healer_attack, 321); + ASSERT_INT_EQ("player attack records healer attack fire count", + state.total_zuk_healer_attack_fires, 1); + ASSERT_INT_EQ("non-magic attack at untagged healer is counted", + state.tick_scratch.zuk_untagged_healer_nonmagic_attacks, 1); + ASSERT_INT_EQ("non-magic attack does not count mage healer fire", + state.tick_scratch.zuk_healer_mage_attack_fires, 0); + ASSERT_INT_EQ("attackable healer target tick counted", + state.total_zuk_healer_attackable_ticks, 1); + ASSERT_INT_EQ("healer target was not blocked by cooldown", + state.total_zuk_healer_cooldown_ticks, 0); + ASSERT_INT_EQ("healer target was not blocked by range", + state.total_zuk_healer_out_of_range_ticks, 0); + + state.npcs[2].pending_hits.hits[0].damage = 0; + state.npcs[2].pending_hits.hits[0].ticks_remaining = 1; + inf_resolve_player_projectiles_on_npcs(&state); + + ASSERT_INT_EQ("landed zero-damage hit tags zuk healer", + state.npcs[2].aggro_target, -1); + ASSERT_INT_EQ("landed zero-damage hit increments tag count", + state.tick_scratch.healer_tags, 1); +} + +static void test_zuk_healer_mage_attack_counts_penalty_event(void) { + printf("--- Zuk healer mage attack counts penalty event ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + state.tick = 654; + state.player.x = 20; + state.player.y = 46; + state.weapon_set = INF_GEAR_MAGE; + state.player.autocast_enabled = 1; + state.player.autocast_spell = ENCOUNTER_SPELL_ICE; + encounter_apply_loadout(&state.player, INF_MAX_MAGE_LOADOUT, GEAR_MAGE); + encounter_compute_loadout_stats(INF_MAX_MAGE_LOADOUT, ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, 30, + &state.loadout_stats[INF_GEAR_MAGE]); + + state.npcs[2] = make_test_npc( + INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[2].aggro_target = 0; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_TARGET] = 34; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("mage healer attack fires once", + state.tick_scratch.zuk_healer_mage_attack_fires, 1); + ASSERT_INT_EQ("mage healer attack gets no non-magic attempt count", + state.tick_scratch.zuk_untagged_healer_nonmagic_attacks, 0); + ASSERT_INT_EQ("mage attack still records total healer fire count", + state.total_zuk_healer_attack_fires, 1); +} + +static void test_zuk_safe_healer_target_mask_requires_fire_window(void) { + printf("--- safe Zuk healer target mask requires fire window ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + test_config()->zuk_safe_untagged_healer_target_mask = 1; + encounter_apply_loadout(&state.player, INF_MAX_RANGE_LONG_LOADOUT, GEAR_RANGED); + + state.npcs[2] = make_test_npc( + INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[2].aggro_target = 0; + + float obs[INF_NUM_OBS]; + float mask[INF_ACTION_MASK_SIZE]; + int healer_slot = 33; + + state.player.x = 20; + state.player.y = 46; + state.player.attack_timer = 0; + inf_write_obs((EncounterState*)&state, obs); + inf_write_mask((EncounterState*)&state, mask); + ASSERT_FLOAT_NEAR("unsafe healer target masked while off shield", + mask[inferno_target_mask_slot_offset(healer_slot)], 0.0f, 1e-6f); + + state.player.x = 24; + state.player.y = 46; + state.player.attack_timer = 0; + inf_write_obs((EncounterState*)&state, obs); + inf_write_mask((EncounterState*)&state, mask); + ASSERT_FLOAT_NEAR("safe fire-ready healer target remains valid", + mask[inferno_target_mask_slot_offset(healer_slot)], 1.0f, 1e-6f); + + state.player.attack_timer = 2; + inf_write_mask((EncounterState*)&state, mask); + ASSERT_FLOAT_NEAR("cooldown healer target is masked", + mask[inferno_target_mask_slot_offset(healer_slot)], 0.0f, 1e-6f); + + state.npcs[2].aggro_target = -1; + inf_write_mask((EncounterState*)&state, mask); + ASSERT_FLOAT_NEAR("tagged healer target remains valid for killing", + mask[inferno_target_mask_slot_offset(healer_slot)], 1.0f, 1e-6f); +} + +static void test_zuk_safe_healer_target_mask_clears_unsafe_target(void) { + printf("--- safe Zuk healer target mask clears unsafe target ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + test_config()->zuk_safe_untagged_healer_target_mask = 1; + state.player.x = 24; + state.player.y = 46; + state.player.attack_timer = 3; + encounter_apply_loadout(&state.player, INF_MAX_RANGE_LONG_LOADOUT, GEAR_RANGED); + + state.npcs[2] = make_test_npc( + INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[2].aggro_target = 0; + osrs_interaction_set(&state.interaction, 2); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("unsafe active healer target cleared", + osrs_interaction_active(&state.interaction), 0); + ASSERT_INT_EQ("cooldown target tick is not counted after clear", + state.total_zuk_healer_cooldown_ticks, 0); +} + +static void test_zuk_force_safe_healer_target_mask_blocks_idle_when_safe(void) { + printf("--- force safe Zuk healer target mask blocks idle when safe ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + test_config()->zuk_force_safe_untagged_healer_target_mask = 1; + encounter_apply_loadout(&state.player, INF_MAX_RANGE_LONG_LOADOUT, GEAR_RANGED); + state.player.x = 24; + state.player.y = 46; + state.player.attack_timer = 0; + + state.npcs[2] = make_test_npc( + INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[2].aggro_target = 0; + state.npcs[3] = make_test_npc( + INF_NPC_MAGER, 28, 48, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[3].active = 1; + state.npcs[3].hp = state.npcs[3].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + + float obs[INF_NUM_OBS]; + float mask[INF_ACTION_MASK_SIZE]; + inf_write_obs((EncounterState*)&state, obs); + inf_write_mask((EncounterState*)&state, mask); + int healer_slot = inf_find_target_obs_slot(&state, 2); + int mager_slot = inf_find_target_obs_slot(&state, 3); + + ASSERT_INT_EQ("healer appears in target obs", healer_slot >= 0, 1); + ASSERT_INT_EQ("mager appears in target obs", mager_slot >= 0, 1); + ASSERT_FLOAT_NEAR("idle target action masked while safe healer exists", + mask[inferno_target_mask_none_offset()], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("safe untagged healer target remains valid", + mask[inferno_target_mask_slot_offset(healer_slot)], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("non-healer target masked while safe healer exists", + mask[inferno_target_mask_slot_offset(mager_slot)], 0.0f, 1e-6f); + + state.player.attack_timer = 2; + inf_write_mask((EncounterState*)&state, mask); + ASSERT_FLOAT_NEAR("idle target action returns when no safe fire window exists", + mask[inferno_target_mask_none_offset()], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("unsafe untagged healer remains masked during cooldown", + mask[inferno_target_mask_slot_offset(healer_slot)], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("non-healer target returns when no safe healer exists", + mask[inferno_target_mask_slot_offset(mager_slot)], 1.0f, 1e-6f); +} + +static void test_zuk_force_safe_healer_target_mask_clears_stale_target(void) { + printf("--- force safe Zuk healer target mask clears stale target ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + test_config()->zuk_force_safe_untagged_healer_target_mask = 1; + encounter_apply_loadout(&state.player, INF_MAX_RANGE_LONG_LOADOUT, GEAR_RANGED); + state.player.x = 24; + state.player.y = 46; + state.player.attack_timer = 0; + + state.npcs[2] = make_test_npc( + INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[2].aggro_target = 0; + state.npcs[3] = make_test_npc( + INF_NPC_MAGER, 28, 48, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[3].active = 1; + state.npcs[3].hp = state.npcs[3].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + osrs_interaction_set(&state.interaction, 3); + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("stale non-healer target cleared", + osrs_interaction_active(&state.interaction), 0); + ASSERT_INT_EQ("stale target did not count as healer target tick", + state.total_zuk_healer_target_ticks, 0); +} + +static void test_zuk_spark_render_matches_pending_spark_state(void) { + printf("--- zuk spark render matches pending spark state ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + state.pending_sparks[0] = (InfPendingSpark){ + .active = 1, .src_x = 16, .src_y = 48, + .x = state.player.x, .y = state.player.y, + .damage = 9, .ticks_remaining = 4, + }; + + EncounterOverlay ov; + memset(&ov, 0, sizeof(ov)); + inf_render_post_tick((EncounterState*)&state, &ov); + + ASSERT_INT_EQ("spark render emits one projectile", ov.projectile_count, 1); + ASSERT_INT_EQ("spark source x", ov.projectiles[0].src_x, 16); + ASSERT_INT_EQ("spark source y", ov.projectiles[0].src_y, 48); + ASSERT_INT_EQ("spark target x", ov.projectiles[0].dst_x, state.player.x); + ASSERT_INT_EQ("spark target y", ov.projectiles[0].dst_y, state.player.y); + ASSERT_INT_EQ("spark visual duration", ov.projectiles[0].duration_ticks, 4 * 30); + ASSERT_INT_EQ("spark projectile model", ov.projectiles[0].model_id, INF_GFX_660_MODEL); + ASSERT_INT_EQ("spark projectile animation", ov.projectiles[0].anim_id, INF_GFX_660_ANIM); + ASSERT_INT_EQ("spark impact spotanim", ov.projectiles[0].impact_gfx_id, 659); + ASSERT_INT_EQ("spark render marks visual emitted", + state.pending_sparks[0].visual_emitted, 1); +} + +static void test_zuk_attack_delay_counts_down_while_stunned(void) { + printf("--- zuk attack delay counts down while stunned ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + + inf_npc_attack(&state, 0); + + ASSERT_INT_EQ("zuk stun decremented", state.npcs[0].stun_timer, 7); + ASSERT_INT_EQ("zuk attack delay decremented during stun", + state.npcs[0].attack_timer, 13); + ASSERT_INT_EQ("zuk does not attack while stunned", + state.npcs[0].attacked_this_tick, 0); +} + +static void test_zuk_set_timer_spawns_on_decrement_to_zero(void) { + printf("--- zuk set timer spawns on decrement to zero ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + state.zuk.set_timer = 1; + + inf_zuk_tick(&state); + + ASSERT_INT_EQ("mager spawned when set timer reaches zero", + find_active_npc_type(&state, INF_NPC_MAGER) >= 0, 1); + ASSERT_INT_EQ("ranger spawned when set timer reaches zero", + find_active_npc_type(&state, INF_NPC_RANGER) >= 0, 1); + ASSERT_INT_EQ("set timer resets to interval", state.zuk.set_timer, 350); +} + +static void test_zuk_hp_threshold_pause_happens_before_set_tick(void) { + printf("--- zuk hp threshold pause happens before set tick ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + state.npcs[0].hp = 601; + state.npcs[0].attack_timer = 100; + state.npcs[0].stun_timer = 0; + state.zuk.set_timer = 10; + state.npcs[0].pending_hits.hits[0].active = 1; + state.npcs[0].pending_hits.hits[0].damage = 2; + state.npcs[0].pending_hits.hits[0].ticks_remaining = 1; + state.npcs[0].pending_hits.hits[0].attack_style = ATTACK_STYLE_RANGED; + state.npcs[0].pending_hits.count = 1; + + step_inferno_noop(&state); + + ASSERT_INT_EQ("zuk damage landed", state.npcs[0].hp, 599); + ASSERT_INT_EQ("zuk set timer paused on same tick", state.zuk.timer_paused, 1); + ASSERT_INT_EQ("zuk set timer did not tick after pause", state.zuk.set_timer, 10); +} + +static void test_set_attack_to_shield_is_projectile_delayed(void) { + printf("--- set attack to shield is projectile delayed ---\n"); + + int found_immediate_damage = 0; + for (uint32_t seed = 1; seed < 200; seed++) { + InfernoState state; + init_zuk_timing_state(&state); + state.rng_state = seed; + state.npcs[2] = make_test_npc( + INF_NPC_MAGER, 20, 36, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + state.npcs[2].attack_timer = 0; + state.npcs[2].aggro_target = 1; + + inf_npc_attack(&state, 2); + if (state.npcs[1].hp < state.npcs[1].max_hp || + state.tick_scratch.shield_damage > 0.0f) { + found_immediate_damage = 1; + break; + } + } + + ASSERT_INT_EQ("set attack does not damage shield on fire tick", + found_immediate_damage, 0); +} + +static void test_npc_target_projectile_delays_match_reference(void) { + printf("--- npc target projectile delays match reference ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + + InfNPC mager = make_test_npc( + INF_NPC_MAGER, 20, 36, INF_NPC_STATS[INF_NPC_MAGER].size); + InfNPC ranger = make_test_npc( + INF_NPC_RANGER, 29, 36, INF_NPC_STATS[INF_NPC_RANGER].size); + InfNPC jad = make_test_npc( + INF_NPC_JAD, 24, 32, INF_NPC_STATS[INF_NPC_JAD].size); + InfNPC* shield = &state.npcs[1]; + + int mager_dist = encounter_projectile_distance( + mager.x, mager.y, mager.size, shield->x, shield->y, shield->size, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + int ranger_dist = encounter_projectile_distance( + ranger.x, ranger.y, ranger.size, shield->x, shield->y, shield->size, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + int jad_dist = encounter_projectile_distance( + jad.x, jad.y, jad.size, shield->x, shield->y, shield->size, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + + ASSERT_INT_EQ("mager shield hit delay", + inf_npc_target_hit_delay(&mager, shield, ATTACK_STYLE_MAGIC), + inf_npc_projectile_timing( + INF_NPC_MAGER, ATTACK_STYLE_MAGIC, mager_dist).damage_delay_ticks); + ASSERT_INT_EQ("ranger shield hit delay includes reduceDelay -2", + inf_npc_target_hit_delay(&ranger, shield, ATTACK_STYLE_RANGED), + inf_npc_projectile_timing( + INF_NPC_RANGER, ATTACK_STYLE_RANGED, ranger_dist).damage_delay_ticks); + ASSERT_INT_EQ("jad magic shield hit delay uses jad path", + inf_npc_target_hit_delay(&jad, shield, ATTACK_STYLE_MAGIC), + INF_JAD_PROJECTILE_DELAY + inf_npc_projectile_timing( + INF_NPC_JAD, ATTACK_STYLE_MAGIC, jad_dist).damage_delay_ticks); + ASSERT_INT_EQ("jad ranged shield hit delay uses jad path", + inf_npc_target_hit_delay(&jad, shield, ATTACK_STYLE_RANGED), + INF_JAD_PROJECTILE_DELAY + inf_npc_projectile_timing( + INF_NPC_JAD, ATTACK_STYLE_RANGED, jad_dist).damage_delay_ticks); +} + +static void test_npc_player_projectile_delays_use_reference_options(void) { + printf("--- npc player projectile delays use reference options ---\n"); + + InfernoState state = make_test_state(10, 10); + InfNPC* ranger = &state.npcs[0]; + *ranger = make_test_npc( + INF_NPC_RANGER, 16, 10, INF_NPC_STATS[INF_NPC_RANGER].size); + ranger->active = 1; + ranger->attack_timer = 0; + ranger->attack_style = ATTACK_STYLE_RANGED; + ranger->had_los_last_tick = 1; + + int dist = encounter_projectile_distance( + ranger->x, ranger->y, ranger->size, state.player.x, state.player.y, 1, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + EncounterProjectileTiming timing = + inf_npc_projectile_timing(INF_NPC_RANGER, ATTACK_STYLE_RANGED, dist); + + inf_npc_attack(&state, 0); + + ASSERT_INT_EQ("ranger queued one pending hit", state.player_pending_hits.count, 1); + + ASSERT_INT_EQ("ranger pending hit lands one tick before the raw projectile delay", + state.player_pending_hits.hits[0].ticks_remaining, timing.damage_delay_ticks - 1); +} + +static void test_player_projectile_timing_uses_reference_options(void) { + printf("--- player projectile timing uses reference options ---\n"); + + int closest = encounter_projectile_distance( + 16, 11, 1, 12, 10, 3, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + int sw_tile = encounter_projectile_distance( + 16, 11, 1, 12, 10, 3, + ENCOUNTER_PROJECTILE_DISTANCE_TARGET_SW_TILE); + ASSERT_INT_EQ("barrage distance uses target SW tile", sw_tile > closest, 1); + + EncounterProjectileTiming barrage = + inf_player_projectile_timing(ATTACK_STYLE_MAGIC, ITEM_KODAI_WAND, 0, sw_tile); + ASSERT_INT_EQ("barrage damage delay uses SW distance", + barrage.damage_delay_ticks, encounter_magic_hit_delay(sw_tile, 1)); + + EncounterProjectileTiming blowpipe_spec = inf_player_projectile_timing( + ATTACK_STYLE_RANGED, ITEM_TOXIC_BLOWPIPE, 1, 12); + ASSERT_INT_EQ("blowpipe spec adds one damage tick", + blowpipe_spec.damage_delay_ticks, encounter_blowpipe_hit_delay(12, 1) + 1); + ASSERT_INT_EQ("blowpipe spec visual delay", blowpipe_spec.visual_start_delay_ticks, 1); + ASSERT_INT_EQ("blowpipe spec visual duration", + blowpipe_spec.visual_duration_ticks, encounter_blowpipe_hit_delay(12, 1) - 1); + + EncounterProjectileTiming tbow = + inf_player_projectile_timing(ATTACK_STYLE_RANGED, ITEM_TWISTED_BOW, 0, 12); + ASSERT_INT_EQ("tbow damage delay", + tbow.damage_delay_ticks, encounter_ranged_hit_delay(12, 1)); + ASSERT_INT_EQ("tbow visual delay", tbow.visual_start_delay_ticks, 1); + ASSERT_INT_EQ("tbow visual duration", + tbow.visual_duration_ticks, encounter_ranged_hit_delay(12, 1) - 1); +} + +static void init_phantom_barrage_test_state( + InfernoState* state, + int death_ticks, + int player_attack_timer +) { + init_zuk_timing_state(state); + memset(state->npcs, 0, sizeof(state->npcs)); + state->zuk.shield_idx = -1; + state->wave = 60; + state->player.x = 20; + state->player.y = 40; + state->player.attack_timer = player_attack_timer; + state->weapon_set = INF_GEAR_MAGE; + state->player.autocast_enabled = 1; + state->player.autocast_spell = ENCOUNTER_SPELL_ICE; + encounter_apply_loadout(&state->player, INF_MAX_MAGE_LOADOUT, GEAR_MAGE); + encounter_compute_loadout_stats(INF_MAX_MAGE_LOADOUT, ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, 30, + &state->loadout_stats[INF_GEAR_MAGE]); + + state->npcs[0] = make_test_npc( + INF_NPC_NIBBLER, 28, 40, INF_NPC_STATS[INF_NPC_NIBBLER].size); + state->npcs[0].active = 1; + state->npcs[0].hp = 0; + state->npcs[0].max_hp = INF_NPC_STATS[INF_NPC_NIBBLER].hp; + state->npcs[0].death_ticks = death_ticks; + + state->npcs[1] = make_test_npc( + INF_NPC_NIBBLER, 28, 41, INF_NPC_STATS[INF_NPC_NIBBLER].size); + state->npcs[1].active = 1; + state->npcs[1].hp = state->npcs[1].max_hp = INF_NPC_STATS[INF_NPC_NIBBLER].hp; +} + +static void test_phantom_barrage_target_is_masked_until_cast_window(void) { + printf("--- phantom barrage target is masked until cast window ---\n"); + + InfernoState state; + float mask[INF_ACTION_MASK_SIZE]; + + init_phantom_barrage_test_state(&state, 2, 1); + inf_write_mask((EncounterState*)&state, mask); + int target_slot = inf_find_target_obs_slot(&state, 0); + ASSERT_INT_EQ("dying target appears before cast window", target_slot >= 0, 1); + ASSERT_FLOAT_NEAR("next tick phantom target is valid", + mask[inferno_target_mask_slot_offset(target_slot)], 1.0f, 1e-6f); + + init_phantom_barrage_test_state(&state, 2, 2); + inf_write_mask((EncounterState*)&state, mask); + target_slot = inf_find_target_obs_slot(&state, 0); + ASSERT_INT_EQ("cooldown dying target still appears", target_slot >= 0, 1); + ASSERT_FLOAT_NEAR("cooldown phantom target is masked", + mask[inferno_target_mask_slot_offset(target_slot)], 0.0f, 1e-6f); +} + +static void test_phantom_barrage_hits_aoe_on_first_cast_window(void) { + printf("--- manual phantom barrage hits AoE on first cast window ---\n"); + + int found_aoe_hit = 0; + for (uint32_t seed = 1; seed < 200 && !found_aoe_hit; seed++) { + InfernoState state; + init_phantom_barrage_test_state(&state, 1, 1); + state.rng_state = seed; + inf_refresh_current_obs_slots(&state); + int target_slot = inf_find_target_obs_slot(&state, 0); + ASSERT_INT_EQ("dying target appears in cast window", target_slot >= 0, 1); + if (target_slot < 0) return; + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_TARGET] = target_slot + 1; + actions[INF_HEAD_SPELL] = 2; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("phantom primary does not receive stale pending hit", + state.npcs[0].pending_hits.hits[0].active, 0); + if (state.tick_scratch.player_attacked && + state.npcs[1].pending_hits.hits[0].active && + state.npcs[1].pending_hits.hits[0].attack_style == ATTACK_STYLE_MAGIC) { + found_aoe_hit = 1; + } + } + + ASSERT_INT_EQ("phantom barrage can queue adjacent AoE hit", found_aoe_hit, 1); +} + +static void test_ranged_attack_cannot_fire_on_dying_target(void) { + printf("--- ranged attack cannot fire on dying target ---\n"); + + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_NIBBLER); + state.weapon_set = INF_GEAR_LONG_RANGE; + encounter_apply_loadout(&state.player, INF_MAX_RANGE_LONG_LOADOUT, GEAR_RANGED); + state.player.autocast_enabled = 0; + state.player.attack_timer = 0; + state.npcs[0].hp = 0; + state.npcs[0].death_ticks = 1; + osrs_interaction_set(&state.interaction, 0); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("ranged attack does not fire on dying target", + state.tick_scratch.player_attacked, 0); + ASSERT_INT_EQ("ranged attack does not queue dying target pending hit", + state.npcs[0].pending_hits.hits[0].active, 0); + ASSERT_INT_EQ("ranged attack does not start cooldown", + state.player.attack_timer, 0); +} + +static void test_autocast_barrage_cannot_fire_on_dying_target(void) { + printf("--- autocast barrage cannot fire on dying target ---\n"); + + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_NIBBLER); + state.player.attack_timer = 0; + state.npcs[0].hp = 0; + state.npcs[0].death_ticks = 1; + osrs_interaction_set(&state.interaction, 0); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("autocast does not fire on dying target", + state.tick_scratch.player_attacked, 0); + ASSERT_INT_EQ("autocast does not queue dying target pending hit", + state.npcs[0].pending_hits.hits[0].active, 0); + ASSERT_INT_EQ("autocast does not start cooldown", + state.player.attack_timer, 0); +} + +static void test_manual_blood_barrage_can_heal_from_dying_primary(void) { + printf("--- manual blood barrage can heal from dying primary ---\n"); + + int found_heal = 0; + for (uint32_t seed = 1; seed < 400 && !found_heal; seed++) { + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_NIBBLER); + state.rng_state = seed; + state.player.current_hitpoints = 80; + state.player.attack_timer = 0; + state.npcs[0].hp = 0; + state.npcs[0].death_ticks = 1; + osrs_interaction_set(&state.interaction, 0); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_SPELL] = 1; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("manual blood barrage fires on dying target", + state.tick_scratch.player_attacked, 1); + ASSERT_INT_EQ("manual blood barrage does not queue dying target pending hit", + state.npcs[0].pending_hits.hits[0].active, 0); + if (state.tick_scratch.blood_heal > 0 && + state.player.current_hitpoints > 80) { + found_heal = 1; + } + } + + ASSERT_INT_EQ("manual blood barrage can heal from dying primary", + found_heal, 1); +} + +static void test_phantom_barrage_close_barrage_timing_cannot_recast(void) { + printf("--- phantom barrage close barrage timing cannot recast ---\n"); + + InfernoState state; + init_phantom_barrage_test_state(&state, 1, 2); + inf_refresh_current_obs_slots(&state); + int target_slot = inf_find_target_obs_slot(&state, 0); + ASSERT_INT_EQ("dying target appears during cooldown", target_slot >= 0, 1); + if (target_slot < 0) return; + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_TARGET] = target_slot + 1; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("cooldown prevents phantom barrage fire", + state.tick_scratch.player_attacked, 0); + ASSERT_INT_EQ("cooldown prevents AoE pending hit", + state.npcs[1].pending_hits.hits[0].active, 0); + ASSERT_INT_EQ("attack timer only decrements", + state.player.attack_timer, 1); +} + +static void test_phantom_barrage_does_not_displace_live_obs_slots(void) { + printf("--- phantom barrage does not displace live obs slots ---\n"); + + InfernoState state; + init_phantom_barrage_test_state(&state, 1, 1); + for (int i = 1; i <= 6; i++) { + state.npcs[i] = make_test_npc( + INF_NPC_NIBBLER, 28 + i, 41, INF_NPC_STATS[INF_NPC_NIBBLER].size); + state.npcs[i].active = 1; + state.npcs[i].hp = state.npcs[i].max_hp = INF_NPC_STATS[INF_NPC_NIBBLER].hp; + } + + inf_refresh_current_obs_slots(&state); + + for (int slot = 16; slot < 22; slot++) { + ASSERT_INT_EQ("live nibbler fills capped obs slot", + state.current_obs_slots[slot] >= 1 && state.current_obs_slots[slot] <= 6, + 1); + } + ASSERT_INT_EQ("dying phantom target does not displace live cap", + inf_find_target_obs_slot(&state, 0), -1); +} + +static void test_phantom_barrage_targetability_obs_requires_ready_attack(void) { + printf("--- phantom barrage targetability obs requires ready attack ---\n"); + + InfernoState state; + init_phantom_barrage_test_state(&state, 1, 1); + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + int target_slot = inf_find_target_obs_slot(&state, 0); + ASSERT_INT_EQ("dying target appears in obs slots", target_slot >= 0, 1); + if (target_slot < 0) return; + + int phantom_idx = inferno_obs_slot_phantom_index(target_slot); + ASSERT_FLOAT_NEAR("cooldown target is not phantom targetable now", + obs[phantom_idx], 0.0f, 1e-6f); + + init_phantom_barrage_test_state(&state, 1, 0); + memset(obs, 0, sizeof(obs)); + inf_write_obs((EncounterState*)&state, obs); + target_slot = inf_find_target_obs_slot(&state, 0); + ASSERT_INT_EQ("ready dying target appears in obs slots", target_slot >= 0, 1); + if (target_slot < 0) return; + phantom_idx = inferno_obs_slot_phantom_index(target_slot); + ASSERT_FLOAT_NEAR("ready barrage target is phantom targetable now", + obs[phantom_idx], 1.0f, 1e-6f); +} + +static void init_confliction_barrage_test_state( + InfernoState* state, + InfNPCType target_type +) { + *state = make_test_state(10, 10); + state->rng_state = 1; + state->weapon_set = INF_GEAR_MAGE; + state->player.entity_type = ENTITY_PLAYER; + state->player.base_hitpoints = 99; + state->player.current_hitpoints = 99; + state->player.base_attack = 99; + state->player.base_strength = 99; + state->player.base_defence = 99; + state->player.base_ranged = 99; + state->player.base_magic = 99; + state->player.current_attack = 99; + state->player.current_strength = 99; + state->player.current_defence = 99; + state->player.current_ranged = 99; + state->player.current_magic = 99; + state->player.autocast_enabled = 1; + state->player.autocast_spell = ENCOUNTER_SPELL_BLOOD; + state->player_dest_x = -1; + state->player_dest_y = -1; + osrs_interaction_init(&state->interaction); + encounter_apply_loadout(&state->player, INF_MAX_MAGE_LOADOUT, GEAR_MAGE); + encounter_compute_loadout_stats(INF_MAX_MAGE_LOADOUT, ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AUTOCAST, 30, + &state->loadout_stats[INF_GEAR_MAGE]); + + state->npcs[0] = make_test_npc( + target_type, 16, 10, INF_NPC_STATS[target_type].size); + state->npcs[0].active = 1; + state->npcs[0].hp = state->npcs[0].max_hp = INF_NPC_STATS[target_type].hp; + osrs_interaction_set(&state->interaction, 0); +} + +static int inferno_fire_blood_barrage_at_slot_zero( + InfernoState* state, + uint32_t seed +) { + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + state->rng_state = seed; + state->player.attack_timer = 0; + encounter_pending_hit_queue_clear(&state->npcs[0].pending_hits); + osrs_interaction_set(&state->interaction, 0); + inf_tick_player(state, actions, 1); + return state->npcs[0].pending_hits.hits[0].hit_success; +} + +static void test_default_autocast_casts_blood_barrage(void) { + printf("--- default autocast casts blood barrage ---\n"); + + InfernoState state; + init_confliction_barrage_test_state(&state, INF_NPC_RANGER); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + state.rng_state = 1; + state.player.attack_timer = 0; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("pending hit records blood barrage", + state.npcs[0].pending_hits.hits[0].spell_type, ENCOUNTER_SPELL_BLOOD); + ASSERT_INT_EQ("player render spell records blood barrage", + state.player.magic_type_this_tick, ENCOUNTER_SPELL_BLOOD); +} + +static void test_ice_barrage_success_freezes_target_and_records_spell(void) { + printf("--- ice barrage success freezes target and records spell ---\n"); + + int found = 0; + for (uint32_t seed = 1; seed < 256 && !found; seed++) { + InfernoState state; + init_confliction_barrage_test_state(&state, INF_NPC_RANGER); + state.player.autocast_spell = ENCOUNTER_SPELL_ICE; + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + state.rng_state = seed; + state.player.attack_timer = 0; + inf_tick_player(&state, actions, 1); + + if (state.npcs[0].pending_hits.hits[0].hit_success) { + found = 1; + ASSERT_INT_EQ("ice pending hit records ice barrage", + state.npcs[0].pending_hits.hits[0].spell_type, ENCOUNTER_SPELL_ICE); + ASSERT_INT_EQ("ice barrage freezes on successful accuracy", + state.npcs[0].frozen_ticks, BARRAGE_FREEZE_TICKS); + ASSERT_INT_EQ("player render spell records ice barrage", + state.player.magic_type_this_tick, ENCOUNTER_SPELL_ICE); + } + } + ASSERT_INT_EQ("found deterministic successful ice barrage seed", found, 1); +} + +static void test_inferno_barrage_primes_confliction_and_reuses_double_accuracy(void) { + printf("--- inferno barrage primes Confliction and reuses double accuracy ---\n"); + + InfernoState state; + uint32_t miss_seed = 0; + for (uint32_t seed = 1; seed < 10000 && miss_seed == 0; seed++) { + init_confliction_barrage_test_state(&state, INF_NPC_RANGER); + int hit = inferno_fire_blood_barrage_at_slot_zero(&state, seed); + if (state.npcs[0].pending_hits.hits[0].active && !hit) + miss_seed = seed; + } + ASSERT_INT_EQ("deterministic miss seed found", miss_seed > 0, 1); + if (miss_seed == 0) return; + + init_confliction_barrage_test_state(&state, INF_NPC_RANGER); + ASSERT_INT_EQ("first barrage splashes", + inferno_fire_blood_barrage_at_slot_zero(&state, miss_seed), 0); + ASSERT_INT_EQ("splash primes Confliction", + state.player.item_effect_state.confliction_is_primed, 1); + ASSERT_INT_EQ("prime target kind", + state.player.item_effect_state.confliction_target.kind, OSRS_TARGET_NPC); + ASSERT_INT_EQ("prime target slot", + state.player.item_effect_state.confliction_target.id, 0); + + const EncounterLoadoutStats* ls = &state.loadout_stats[INF_GEAR_MAGE]; + OsrsPreparedAttackEffects prepared = osrs_prepare_attack_effects( + &state.player.equipment_effect_profile, + &state.player.item_effect_state, + state.player.equipped[GEAR_SLOT_WEAPON], + ATTACK_STYLE_MAGIC, + OSRS_MAGIC_ATTACK_ANCIENT_BLOOD, + (OsrsTargetRef){ .kind = OSRS_TARGET_NPC, .id = 0 }, + 1, + ls->eff_level * (ls->attack_bonus + 64), + ls->max_hit, + osrs_target_effect_context_none(), + state.player.current_hitpoints, + state.player.base_hitpoints + ); + ASSERT_INT_EQ("same target prepares double accuracy", + prepared.use_double_accuracy, 1); + + inferno_fire_blood_barrage_at_slot_zero(&state, miss_seed); + ASSERT_INT_EQ("double accuracy attempt clears Confliction", + state.player.item_effect_state.confliction_is_primed, 0); +} + +static void test_barrage_accuracy_regression_against_ranger_and_mager(void) { + printf("--- barrage accuracy regression against ranger and mager ---\n"); + + InfNPCType targets[2] = { INF_NPC_RANGER, INF_NPC_MAGER }; + for (int i = 0; i < 2; i++) { + int hits = 0; + for (uint32_t seed = 1; seed <= 64; seed++) { + InfernoState state; + init_confliction_barrage_test_state(&state, targets[i]); + hits += inferno_fire_blood_barrage_at_slot_zero(&state, seed); + } + ASSERT_INT_EQ("repeated barrages are not all splashes", hits > 0, 1); + ASSERT_INT_EQ("repeated barrages are not guaranteed hits", hits < 64, 1); + } +} + +static void init_barrage_pending_queue_edge_state(InfernoState* state) { + init_confliction_barrage_test_state(state, INF_NPC_RANGER); + state->player.x = 24; + state->player.y = 14; + state->npcs[0].x = 12; + state->npcs[0].y = 14; + state->npcs[0].hp = state->npcs[0].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + encounter_pending_hit_queue_clear(&state->npcs[0].pending_hits); + osrs_interaction_set(&state->interaction, 0); +} + +static void tick_barrage_pending_queue_edge_state(InfernoState* state) { + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_SPELL] = 1; + state->tick_scratch.player_attacked = 0; + state->npcs[0].hit_landed_this_tick = 0; + state->npcs[0].hit_damage = 0; + state->npcs[0].hit_was_successful_this_tick = 0; + inf_resolve_player_projectiles_on_npcs(state); + inf_tick_player(state, actions, 1); + state->tick++; +} + +static void test_barrage_pending_queue_handles_slow_hit_delay(void) { + printf("--- barrage pending queue handles slow hit delay ---\n"); + + InfernoState state; + init_barrage_pending_queue_edge_state(&state); + + int closest = encounter_projectile_distance( + state.player.x, state.player.y, 1, + state.npcs[0].x, state.npcs[0].y, state.npcs[0].size, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + int sw_tile = encounter_projectile_distance( + state.player.x, state.player.y, 1, + state.npcs[0].x, state.npcs[0].y, state.npcs[0].size, + ENCOUNTER_PROJECTILE_DISTANCE_TARGET_SW_TILE); + ASSERT_INT_EQ("ranger is attackable at closest-tile range ten", closest, 10); + ASSERT_INT_EQ("barrage timing still sees target SW distance twelve", sw_tile, 12); + ASSERT_INT_EQ("player can cast at closest-tile range ten", + inf_player_can_attack_npc_from_current_tile(&state, 0), 1); + + tick_barrage_pending_queue_edge_state(&state); + ASSERT_INT_EQ("first cast queues one hit", + state.npcs[0].pending_hits.count, 1); + ASSERT_INT_EQ("first queued hit uses slow barrage travel", + state.npcs[0].pending_hits.hits[0].ticks_remaining, + encounter_magic_hit_delay(sw_tile, 1)); + + for (int i = 0; i < 5; i++) + tick_barrage_pending_queue_edge_state(&state); + + ASSERT_INT_EQ("second cast queues behind the first in-flight hit", + state.npcs[0].pending_hits.count, 2); + ASSERT_INT_EQ("oldest hit is one tick from landing", + state.npcs[0].pending_hits.hits[0].ticks_remaining, 1); + + tick_barrage_pending_queue_edge_state(&state); + ASSERT_INT_EQ("oldest hit lands instead of being overwritten", + state.npcs[0].hit_landed_this_tick, 1); + ASSERT_INT_EQ("one queued hit remains after first land", + state.npcs[0].pending_hits.count, 1); + ASSERT_INT_EQ("ranger took damage from queued hit", + state.npcs[0].hp < state.npcs[0].max_hp, 1); +} + +static void test_barrage_aoe_queues_hits_on_multiple_npcs(void) { + printf("--- barrage AoE queues hits on multiple NPCs ---\n"); + + InfernoState state; + init_confliction_barrage_test_state(&state, INF_NPC_RANGER); + state.npcs[1] = make_test_npc( + INF_NPC_RANGER, 17, 10, INF_NPC_STATS[INF_NPC_RANGER].size); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + state.player.attack_timer = 0; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("primary ranger has queued barrage hit", + state.npcs[0].pending_hits.count, 1); + ASSERT_INT_EQ("secondary ranger has queued barrage hit", + state.npcs[1].pending_hits.count, 1); + ASSERT_INT_EQ("primary queued hit is magic", + state.npcs[0].pending_hits.hits[0].attack_style, ATTACK_STYLE_MAGIC); + ASSERT_INT_EQ("secondary queued hit is magic", + state.npcs[1].pending_hits.hits[0].attack_style, ATTACK_STYLE_MAGIC); + ASSERT_INT_EQ("secondary queued hit preserves spell type", + state.npcs[1].pending_hits.hits[0].spell_type, ENCOUNTER_SPELL_BLOOD); +} + +static void test_repeated_edge_barrages_kill_ranger(void) { + printf("--- repeated edge barrages kill ranger ---\n"); + + InfernoState state; + init_barrage_pending_queue_edge_state(&state); + + for (int tick = 0; tick < 240 && state.npcs[0].hp > 0; tick++) + tick_barrage_pending_queue_edge_state(&state); + + ASSERT_INT_EQ("edge barrage loop kills the ranger", + state.npcs[0].hp <= 0, 1); +} + +static void test_npc_pending_queue_lands_multiple_hits_in_order(void) { + printf("--- npc pending queue lands multiple hits in order ---\n"); + + InfernoState state = make_test_state(10, 10); + state.npcs[0] = make_test_npc( + INF_NPC_RANGER, 16, 10, INF_NPC_STATS[INF_NPC_RANGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + inf_queue_npc_pending_hit( + &state, 0, 1, 7, ATTACK_STYLE_MAGIC, ENCOUNTER_SPELL_BLOOD, 1); + inf_queue_npc_pending_hit( + &state, 0, 2, 11, ATTACK_STYLE_MAGIC, ENCOUNTER_SPELL_BLOOD, 1); + + inf_resolve_player_projectiles_on_npcs(&state); + ASSERT_INT_EQ("first queued hit lands", + state.npcs[0].hit_damage, 7); + ASSERT_INT_EQ("second hit remains queued", + state.npcs[0].pending_hits.count, 1); + state.npcs[0].hit_landed_this_tick = 0; + state.npcs[0].hit_damage = 0; + + inf_resolve_player_projectiles_on_npcs(&state); + ASSERT_INT_EQ("second queued hit lands", + state.npcs[0].hit_damage, 11); + ASSERT_INT_EQ("queue is empty after both hits", + state.npcs[0].pending_hits.count, 0); +} + +static void test_npc_death_clears_pending_hits(void) { + printf("--- npc death clears pending hits ---\n"); + + InfernoState state = make_test_state(10, 10); + state.npcs[0] = make_test_npc( + INF_NPC_RANGER, 16, 10, INF_NPC_STATS[INF_NPC_RANGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = 5; + state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + inf_queue_npc_pending_hit( + &state, 0, 1, 7, ATTACK_STYLE_MAGIC, ENCOUNTER_SPELL_BLOOD, 1); + inf_queue_npc_pending_hit( + &state, 0, 4, 11, ATTACK_STYLE_MAGIC, ENCOUNTER_SPELL_BLOOD, 1); + + inf_resolve_player_projectiles_on_npcs(&state); + ASSERT_INT_EQ("lethal hit starts death linger", + state.npcs[0].death_ticks > 0, 1); + ASSERT_INT_EQ("death clears remaining pending hits", + state.npcs[0].pending_hits.count, 0); +} + +static void test_lab_dump_reports_npc_pending_hit_queue(void) { + printf("--- lab dump reports npc pending hit queue ---\n"); + + InfernoState state = make_test_state(10, 10); + state.npcs[0] = make_test_npc( + INF_NPC_RANGER, 16, 10, INF_NPC_STATS[INF_NPC_RANGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + inf_queue_npc_pending_hit( + &state, 0, 3, 17, ATTACK_STYLE_MAGIC, ENCOUNTER_SPELL_BLOOD, 1); + + char* dump = inf_lab_alloc_json(&state); + ASSERT_INT_EQ("lab dump includes pending count", + strstr(dump, "\"pending_count\":1") != NULL, 1); + ASSERT_INT_EQ("lab dump includes pending timer", + strstr(dump, "\"pending_earliest_ticks\":3") != NULL, 1); + ASSERT_INT_EQ("lab dump includes pending damage", + strstr(dump, "\"pending_damage\":17") != NULL, 1); + free(dump); +} + +static void test_explicit_spell_cast_does_not_persist(void) { + printf("--- explicit spell cast does not persist ---\n"); + + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_RANGER); + state.player.autocast_spell = ENCOUNTER_SPELL_BLOOD; + + fire_player_action_at_slot_zero(&state, 2); + ASSERT_INT_EQ("manual ice cast records ice pending hit", + state.npcs[0].pending_hits.hits[0].spell_type, ENCOUNTER_SPELL_ICE); + ASSERT_INT_EQ("manual ice render records ice", + state.player.magic_type_this_tick, ENCOUNTER_SPELL_ICE); + + state.player.attack_timer = 0; + encounter_pending_hit_queue_clear(&state.npcs[0].pending_hits); + fire_player_action_at_slot_zero(&state, 0); + ASSERT_INT_EQ("later normal attack falls back to blood autocast", + state.npcs[0].pending_hits.hits[0].spell_type, ENCOUNTER_SPELL_BLOOD); +} + +static void test_spell_without_target_does_not_affect_later_attack(void) { + printf("--- spell without target does not affect later attack ---\n"); + + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_RANGER); + state.player.autocast_spell = ENCOUNTER_SPELL_BLOOD; + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_SPELL] = 2; + inf_tick_player(&state, actions, 1); + ASSERT_INT_EQ("spell without target does not fire", + state.tick_scratch.player_attacked, 0); + + fire_player_action_at_slot_zero(&state, 0); + ASSERT_INT_EQ("next normal attack uses autocast blood", + state.npcs[0].pending_hits.hits[0].spell_type, ENCOUNTER_SPELL_BLOOD); +} + +static void test_target_without_spell_uses_autocast(void) { + printf("--- target without spell uses autocast ---\n"); + + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_RANGER); + state.player.autocast_spell = ENCOUNTER_SPELL_ICE; + + fire_player_action_at_slot_zero(&state, 0); + ASSERT_INT_EQ("no-spell target attack uses ice autocast", + state.npcs[0].pending_hits.hits[0].spell_type, ENCOUNTER_SPELL_ICE); +} + +static void test_manual_spell_overrides_autocast(void) { + printf("--- manual spell overrides autocast ---\n"); + + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_RANGER); + state.player.autocast_spell = ENCOUNTER_SPELL_BLOOD; + + fire_player_action_at_slot_zero(&state, 2); + ASSERT_INT_EQ("manual ice overrides blood autocast", + state.npcs[0].pending_hits.hits[0].spell_type, ENCOUNTER_SPELL_ICE); + + init_spell_cast_test_state(&state, INF_NPC_RANGER); + state.player.autocast_spell = ENCOUNTER_SPELL_ICE; + fire_player_action_at_slot_zero(&state, 1); + ASSERT_INT_EQ("manual blood overrides ice autocast", + state.npcs[0].pending_hits.hits[0].spell_type, ENCOUNTER_SPELL_BLOOD); +} + +static void test_blood_barrage_at_full_hp_is_valid_and_heals_zero(void) { + printf("--- blood barrage at full HP is valid and heals zero ---\n"); + + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_NIBBLER); + float mask[INF_ACTION_MASK_SIZE]; + inf_write_mask((EncounterState*)&state, mask); + ASSERT_FLOAT_NEAR("blood barrage action valid at full HP", + mask[inferno_action_head_mask_offset(INF_HEAD_SPELL) + 1], 1.0f, 1e-6f); + + fire_player_action_at_slot_zero(&state, 1); + state.npcs[0].pending_hits.hits[0].damage = 12; + state.npcs[0].pending_hits.hits[0].ticks_remaining = 1; + state.player.current_hitpoints = state.player.base_hitpoints; + inf_resolve_player_projectiles_on_npcs(&state); + + ASSERT_INT_EQ("full HP blood barrage heals zero", + state.tick_scratch.blood_heal, 0); + ASSERT_INT_EQ("HP stays capped", + state.player.current_hitpoints, state.player.base_hitpoints); +} + +static void test_manual_spell_in_range_gear_uses_range_gear_magic_stats(void) { + printf("--- manual spell in range gear uses range gear magic stats ---\n"); + + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_RANGER); + state.weapon_set = INF_GEAR_BP; + encounter_apply_loadout(&state.player, INF_MAX_RANGE_FAST_LOADOUT, GEAR_RANGED); + state.player.autocast_spell = ENCOUNTER_SPELL_BLOOD; + + InfPlayerAttack attack; + int resolved = inf_resolve_player_attack(&state, ENCOUNTER_SPELL_ICE, &attack); + EncounterLoadoutStats expected; + encounter_compute_player_equipped_stats( + &state.player, ATTACK_STYLE_MAGIC, FIGHT_STYLE_AUTOCAST, 30, &expected); + + ASSERT_INT_EQ("manual spell resolves as attack", + resolved, 1); + ASSERT_INT_EQ("manual spell is magic", + attack.stats.style, ATTACK_STYLE_MAGIC); + ASSERT_INT_EQ("manual spell uses current gear magic attack bonus", + attack.stats.attack_bonus, expected.attack_bonus); + ASSERT_INT_EQ("manual spell uses barrage speed", + attack.stats.attack_speed, 5); + ASSERT_INT_EQ("manual spell uses barrage range", + attack.stats.attack_range, 10); +} + +static void test_phantom_barrage_allows_explicit_spell_from_range_gear(void) { + printf("--- phantom barrage allows explicit spell from range gear ---\n"); + + InfernoState state; + init_phantom_barrage_test_state(&state, 1, 1); + state.weapon_set = INF_GEAR_BP; + encounter_apply_loadout(&state.player, INF_MAX_RANGE_FAST_LOADOUT, GEAR_RANGED); + state.player.autocast_enabled = 1; + state.player.autocast_spell = ENCOUNTER_SPELL_BLOOD; + inf_refresh_current_obs_slots(&state); + + int target_slot = inf_find_target_obs_slot(&state, 0); + ASSERT_INT_EQ("dying target appears in obs slots", target_slot >= 0, 1); + if (target_slot < 0) return; + + float mask[INF_ACTION_MASK_SIZE]; + inf_write_mask((EncounterState*)&state, mask); + ASSERT_FLOAT_NEAR("explicit spell can target phantom from range gear", + mask[inferno_target_mask_slot_offset(target_slot)], 1.0f, 1e-6f); + + int actions[INF_NUM_ACTION_HEADS]; + memset(actions, 0, sizeof(actions)); + actions[INF_HEAD_TARGET] = target_slot + 1; + actions[INF_HEAD_SPELL] = 2; + inf_tick_player(&state, actions, 1); + + ASSERT_INT_EQ("explicit phantom barrage fires from range gear", + state.tick_scratch.player_attacked, 1); + ASSERT_INT_EQ("explicit phantom barrage uses magic style", + state.player_attack_style_id, ATTACK_STYLE_MAGIC); +} + +static void test_zuk_obs_tracks_shield_and_mager_aggro(void) { + printf("--- zuk obs tracks shield hp/death and mager aggro ---\n"); + + InfernoState state = make_test_state(INF_ZUK_PLAYER_START_X, INF_ZUK_PLAYER_START_Y); + state.wave = 68; + state.player.current_defence = 99; + state.player.current_magic = 99; + state.player.current_ranged = 99; + state.player.current_hitpoints = 99; + state.player.base_hitpoints = 99; + state.player.base_prayer = 99; + state.player.current_prayer = 99; + state.player.prayer = PRAYER_NONE; + state.weapon_set = INF_GEAR_LONG_RANGE; + + state.npcs[0] = make_test_npc( + INF_NPC_MAGER, 20, 36, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + state.npcs[0].attack_timer = 4; + + state.npcs[1] = make_test_npc( + INF_NPC_ZUK, 22, 14, INF_NPC_STATS[INF_NPC_ZUK].size); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = INF_NPC_STATS[INF_NPC_ZUK].hp; + + state.npcs[2] = make_test_npc( + INF_NPC_ZUK_SHIELD, 23, 44, INF_NPC_STATS[INF_NPC_ZUK_SHIELD].size); + state.npcs[2].active = 1; + state.npcs[2].hp = 300; + state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_ZUK_SHIELD].hp; + + state.zuk.shield_idx = 2; + state.zuk.shield_dir = -1; + state.zuk.shield_freeze = 3; + state.npcs[0].aggro_target = 2; + + float obs[INF_NUM_OBS]; + float mask[INF_ACTION_MASK_SIZE]; + inf_write_obs((EncounterState*)&state, obs); + inf_write_mask((EncounterState*)&state, mask); + + int mager_slot = 0; + int shield_slot = 26; + int shield_start = inferno_obs_slot_start(shield_slot); + int mager_target_category = inferno_obs_slot_target_category_start(mager_slot); + + ASSERT_INT_EQ("first mager occupies mager slot 0", state.current_obs_slots[mager_slot], 0); + ASSERT_INT_EQ("shield occupies dedicated shield slot", state.current_obs_slots[shield_slot], 2); + ASSERT_FLOAT_NEAR("shield hp ratio visible in shield slot", obs[shield_start], 0.5f, 1e-6f); + ASSERT_FLOAT_NEAR("mager target_player off while on shield", + obs[mager_target_category], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("mager target_shield on while on shield", + obs[mager_target_category + 2], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("shield direction visible while alive", obs[INF_OBS_ZUK_SHIELD_DIR], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("shield freeze visible while alive", obs[INF_OBS_ZUK_SHIELD_FREEZE], 0.6f, 1e-6f); + ASSERT_FLOAT_NEAR("mager target mask is valid", mask[inferno_target_mask_slot_offset(mager_slot)], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("shield target mask stays invalid", mask[inferno_target_mask_slot_offset(shield_slot)], 0.0f, 1e-6f); + + state.npcs[2].active = 0; + state.zuk.shield_idx = -1; + state.npcs[0].aggro_target = -1; + + memset(obs, 0, sizeof(obs)); + memset(mask, 0, sizeof(mask)); + inf_write_obs((EncounterState*)&state, obs); + inf_write_mask((EncounterState*)&state, mask); + + ASSERT_INT_EQ("dead shield drops out of shield slot", state.current_obs_slots[shield_slot], -1); + ASSERT_FLOAT_NEAR("dead shield slot hp zeros out", obs[shield_start], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("dead shield zeroes stale direction", obs[INF_OBS_ZUK_SHIELD_DIR], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("dead shield zeroes stale freeze", obs[INF_OBS_ZUK_SHIELD_FREEZE], 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("mager target_player flips on", + obs[mager_target_category], 1.0f, 1e-6f); +} + +static void test_zuk_healer_obs_marks_untagged_healer_pressure(void) { + printf("--- zuk healer obs marks untagged healer pressure ---\n"); + + InfernoState state = make_test_state(INF_ZUK_PLAYER_START_X, INF_ZUK_PLAYER_START_Y); + state.wave = 68; + state.player.current_hitpoints = 99; + state.player.base_hitpoints = 99; + state.player.base_prayer = 99; + state.player.current_prayer = 99; + state.player.current_defence = 99; + state.player.current_magic = 99; + state.player.current_ranged = 99; + state.weapon_set = INF_GEAR_LONG_RANGE; + state.zuk.healer_spawned = 1; + + state.npcs[0] = make_test_npc( + INF_NPC_ZUK, 22, 14, INF_NPC_STATS[INF_NPC_ZUK].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_ZUK].hp; + + state.npcs[1] = make_test_npc( + INF_NPC_HEALER_ZUK, 20, 42, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + state.npcs[1].attack_timer = 4; + state.npcs[1].aggro_target = 0; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int healer_slot = 33; + int healer_target_category = inferno_obs_slot_target_category_start(healer_slot); + ASSERT_INT_EQ("Zuk healer occupies first Zuk healer obs slot", + state.current_obs_slots[healer_slot], 1); + ASSERT_FLOAT_NEAR("untagged Zuk healer target_zuk is visible", + obs[healer_target_category + 1], 1.0f, 1e-6f); + + state.npcs[1].aggro_target = -1; + memset(obs, 0, sizeof(obs)); + inf_write_obs((EncounterState*)&state, obs); + + ASSERT_FLOAT_NEAR("tagged Zuk healer target_player is visible", + obs[healer_target_category], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("tagged Zuk healer clears target_zuk", + obs[healer_target_category + 1], 0.0f, 1e-6f); +} + +static void test_inferno_obs_target_categories_cover_boss_helpers(void) { + printf("--- inferno obs target categories cover boss helpers ---\n"); + + InfernoState state = make_test_state(20, 20); + state.player.current_hitpoints = 99; + state.player.base_hitpoints = 99; + state.player.base_prayer = 99; + state.player.current_prayer = 99; + state.player.current_defence = 99; + state.player.current_magic = 99; + state.player.current_ranged = 99; + + state.npcs[0] = make_test_npc( + INF_NPC_JAD, 24, 20, INF_NPC_STATS[INF_NPC_JAD].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_JAD].hp; + + state.npcs[1] = make_test_npc( + INF_NPC_HEALER_JAD, 25, 20, INF_NPC_STATS[INF_NPC_HEALER_JAD].size); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = INF_NPC_STATS[INF_NPC_HEALER_JAD].hp; + state.npcs[1].aggro_target = 0; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int healer_slot = 27; + int target_category = inferno_obs_slot_target_category_start(healer_slot); + ASSERT_INT_EQ("Jad healer occupies helper slot", + state.current_obs_slots[healer_slot], 1); + ASSERT_FLOAT_NEAR("Jad healer targeting Jad maps to other NPC", + obs[target_category + 3], 1.0f, 1e-6f); + + state.npcs[1].aggro_target = -1; + memset(obs, 0, sizeof(obs)); + inf_write_obs((EncounterState*)&state, obs); + ASSERT_FLOAT_NEAR("tagged Jad healer maps to player", + obs[target_category], 1.0f, 1e-6f); +} + +static void test_zuk_set_obs_los_uses_current_target(void) { + printf("--- zuk set obs los uses current target ---\n"); + + InfernoState state = make_test_state(11, 14); + state.wave = 68; + state.player.current_hitpoints = 99; + state.player.base_hitpoints = 99; + state.player.base_prayer = 99; + state.player.current_prayer = 99; + state.player.current_defence = 99; + state.player.current_magic = 99; + state.player.current_ranged = 99; + state.weapon_set = INF_GEAR_LONG_RANGE; + + state.npcs[0] = make_test_npc( + INF_NPC_MAGER, 20, 36, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + state.npcs[0].attack_timer = 4; + state.npcs[0].aggro_target = 1; + + state.npcs[1] = make_test_npc( + INF_NPC_ZUK_SHIELD, 23, 44, INF_NPC_STATS[INF_NPC_ZUK_SHIELD].size); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = INF_NPC_STATS[INF_NPC_ZUK_SHIELD].hp; + state.zuk.shield_idx = 1; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + int mager_target_category = inferno_obs_slot_target_category_start(0); + ASSERT_INT_EQ("mager occupies first obs slot", state.current_obs_slots[0], 0); + ASSERT_FLOAT_NEAR("mager los follows shield target", + obs[inferno_obs_slot_npc_los_index(0)], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("mager target_shield stays on", obs[mager_target_category + 2], 1.0f, 1e-6f); +} + +static void test_zuk_set_prayer_critical_ignores_shield_target(void) { + printf("--- zuk set prayer critical ignores shield target ---\n"); + + InfernoState state = make_test_state(20, 34); + state.wave = 68; + state.player.current_hitpoints = 99; + state.player.base_hitpoints = 99; + state.player.base_prayer = 99; + state.player.current_prayer = 99; + state.player.current_defence = 99; + state.player.current_magic = 99; + state.player.current_ranged = 99; + state.weapon_set = INF_GEAR_LONG_RANGE; + + state.npcs[0] = make_test_npc( + INF_NPC_MAGER, 20, 36, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + state.npcs[0].attack_timer = 1; + state.npcs[0].attack_style = ATTACK_STYLE_MAGIC; + state.npcs[0].aggro_target = 1; + + state.npcs[1] = make_test_npc( + INF_NPC_ZUK_SHIELD, 23, 44, INF_NPC_STATS[INF_NPC_ZUK_SHIELD].size); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = INF_NPC_STATS[INF_NPC_ZUK_SHIELD].hp; + state.zuk.shield_idx = 1; + + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + + ASSERT_FLOAT_NEAR("shield-targeted mager does not create prayer deadline", obs[INF_OBS_PRAYER_TIMER], 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("shield-targeted mager has no prayer style", + obs[INF_OBS_PRAYER_MELEE] + obs[INF_OBS_PRAYER_RANGED] + obs[INF_OBS_PRAYER_MAGIC], + 0.0f, 1e-6f); +} + +static void child_inf_put_bad_start_wave(void) { + InfernoState state = make_test_state(0, 0); + inf_put_int((EncounterState*)&state, "start_wave", 0); +} + +static void child_inf_put_unknown_int(void) { + InfernoState state = make_test_state(0, 0); + inf_put_int((EncounterState*)&state, "bogus_key", 1); +} + +static void child_inf_put_removed_win_bonus(void) { + InfernoState state = make_test_state(0, 0); + inf_put_float((EncounterState*)&state, "win_bonus_coeff", 1.0f); +} + +static void child_encounter_emit_projectile_overflow(void) { + EncounterOverlay ov; + memset(&ov, 0, sizeof(ov)); + ov.projectile_count = ENCOUNTER_MAX_OVERLAY_PROJECTILES; + encounter_emit_projectile( + &ov, 0, 0, 1, 1, 0, 0, 30, 0, 0, 0, 0.0f, 0, 1, 1, 0, 0); +} + +static void child_inf_pending_spark_overflow(void) { + InfernoState state = make_test_state(20, 20); + for (int i = 0; i < INF_MAX_PENDING_SPARKS; i++) + state.pending_sparks[i].active = 1; + inf_queue_pending_spark(&state, 0, 0, 20, 20, 1, 1); +} + +static void child_inf_restore_v1_snapshot(void) { + EncounterState* raw = inf_create(); + inf_reset(raw, 123u); + size_t snap_size = inf_snapshot_size(raw); + InfSnapshot* snap = (InfSnapshot*)malloc(snap_size); + inf_snapshot(raw, snap); + snap->version = 1u; + inf_restore(raw, snap, snap_size); + free(snap); + inf_destroy(raw); +} + +static void child_inf_restore_wrong_config_snapshot(void) { + InfernoState state_a; + InfernoState state_b; + InfernoContext ctx_a; + InfernoContext ctx_b; + + inf_init_context_typed(&ctx_a); + inf_init_context_typed(&ctx_b); + ctx_a.config.damage_reward_coeff = 0.01f; + ctx_b.config.damage_reward_coeff = 0.02f; + inf_init_state_typed(&state_a, &ctx_a); + inf_init_state_typed(&state_b, &ctx_b); + inf_reset_ctx((EncounterState*)&state_a, (EncounterContext*)&ctx_a, 123u); + inf_reset_ctx((EncounterState*)&state_b, (EncounterContext*)&ctx_b, 123u); + + InfSnapshot snap; + inf_snapshot_ctx((EncounterState*)&state_a, (EncounterContext*)&ctx_a, &snap); + inf_restore_ctx( + (EncounterState*)&state_b, + (EncounterContext*)&ctx_b, + &snap, + sizeof(snap)); +} + +static void test_fail_fast_boundaries(void) { + printf("--- fail fast boundaries ---\n"); + + assert_child_aborts("invalid inferno start wave aborts", child_inf_put_bad_start_wave); + assert_child_aborts("unknown inferno int config aborts", child_inf_put_unknown_int); + assert_child_aborts("removed win bonus config aborts", child_inf_put_removed_win_bonus); + assert_child_aborts("overlay projectile overflow aborts", child_encounter_emit_projectile_overflow); + assert_child_aborts("inferno pending spark overflow aborts", child_inf_pending_spark_overflow); + assert_child_aborts("inferno v1 snapshot restore aborts", child_inf_restore_v1_snapshot); + assert_child_aborts( + "inferno wrong-config snapshot restore aborts", + child_inf_restore_wrong_config_snapshot); +} + +static void test_human_target_and_potion_translation(void) { + printf("--- inferno human target and potion translation ---\n"); + + InfernoState state = make_test_state(20, 20); + state.player.base_hitpoints = 99; + state.player.current_hitpoints = 80; + state.player.base_prayer = 99; + state.player.current_prayer = 60; + state.player.current_attack = 99; + state.player.current_strength = 99; + state.player.current_defence = 99; + state.player.current_ranged = 99; + state.player.current_magic = 99; + + state.npcs[0] = make_test_npc( + INF_NPC_MAGER, 24, 24, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + + state.npcs[1] = make_test_npc( + INF_NPC_MAGER, 26, 24, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + + state.npcs[2] = make_test_npc( + INF_NPC_MAGER, 28, 24, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + + state.npcs[3] = make_test_npc( + INF_NPC_ZUK_SHIELD, 23, 44, INF_NPC_STATS[INF_NPC_ZUK_SHIELD].size); + state.npcs[3].active = 1; + state.npcs[3].hp = state.npcs[3].max_hp = INF_NPC_STATS[INF_NPC_ZUK_SHIELD].hp; + + { + float obs[INF_NUM_OBS]; + inf_write_obs((EncounterState*)&state, obs); + } + + ASSERT_INT_EQ("first visible mager is targetable", + inf_is_human_targetable_npc_slot((EncounterState*)&state, 0), 1); + ASSERT_INT_EQ("second visible mager is targetable", + inf_is_human_targetable_npc_slot((EncounterState*)&state, 1), 1); + ASSERT_INT_EQ("third capped-out mager is not targetable", + inf_is_human_targetable_npc_slot((EncounterState*)&state, 2), 0); + ASSERT_INT_EQ("shield is never targetable", + inf_is_human_targetable_npc_slot((EncounterState*)&state, 3), 0); + + { + HumanInput hi; + int actions[INF_NUM_ACTION_HEADS]; + + hi = make_human_input(); + hi.pending_target_idx = 0; + inf_translate_human_input(&hi, actions, (EncounterState*)&state); + ASSERT_INT_EQ("visible mager click maps into target head", + actions[INF_HEAD_TARGET], 1); + + hi = make_human_input(); + hi.pending_target_idx = 2; + inf_translate_human_input(&hi, actions, (EncounterState*)&state); + ASSERT_INT_EQ("capped-out mager click is rejected", + actions[INF_HEAD_TARGET], 0); + + hi = make_human_input(); + hi.pending_target_idx = 3; + inf_translate_human_input(&hi, actions, (EncounterState*)&state); + ASSERT_INT_EQ("shield click is rejected", + actions[INF_HEAD_TARGET], 0); + + hi = make_human_input(); + hi.pending_potion = POTION_BREW; + inf_translate_human_input(&hi, actions, (EncounterState*)&state); + ASSERT_INT_EQ("brew still maps to eat head", actions[INF_HEAD_EAT], 1); + ASSERT_INT_EQ("brew does not touch potion head", actions[INF_HEAD_POTION], 0); + + hi = make_human_input(); + hi.pending_potion = POTION_RESTORE; + inf_translate_human_input(&hi, actions, (EncounterState*)&state); + ASSERT_INT_EQ("restore maps to potion 1", actions[INF_HEAD_POTION], 1); + + hi = make_human_input(); + hi.pending_potion = POTION_BASTION; + inf_translate_human_input(&hi, actions, (EncounterState*)&state); + ASSERT_INT_EQ("bastion maps to potion 2", actions[INF_HEAD_POTION], 2); + + hi = make_human_input(); + hi.pending_potion = POTION_STAMINA; + inf_translate_human_input(&hi, actions, (EncounterState*)&state); + ASSERT_INT_EQ("stamina maps to potion 3", actions[INF_HEAD_POTION], 3); + + hi = make_human_input(); + hi.pending_potion = POTION_PRAYER_POT; + inf_translate_human_input(&hi, actions, (EncounterState*)&state); + ASSERT_INT_EQ("prayer pot no longer aliases to restore", + actions[INF_HEAD_POTION], 0); + } +} + +static void test_human_targeting_refreshes_stale_obs_slots(void) { + printf("--- inferno human targeting refreshes stale obs slots ---\n"); + + InfernoState state = make_test_state(20, 20); + for (int i = 0; i < INF_OBS_NPCS; i++) { + state.current_obs_slots[i] = -1; + } + + state.npcs[5] = make_test_npc( + INF_NPC_RANGER, 24, 24, INF_NPC_STATS[INF_NPC_RANGER].size); + state.npcs[5].active = 1; + state.npcs[5].hp = state.npcs[5].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + + ASSERT_INT_EQ("live ranger is targetable without prior obs write", + inf_is_human_targetable_npc_slot((EncounterState*)&state, 5), 1); + + { + HumanInput hi = make_human_input(); + int actions[INF_NUM_ACTION_HEADS]; + hi.pending_target_idx = 5; + state.current_obs_slots[2] = -1; + inf_translate_human_input(&hi, actions, (EncounterState*)&state); + ASSERT_INT_EQ("pending human target refreshes obs slot", + actions[INF_HEAD_TARGET], 3); + } + + { + HumanInput hi; + int actions[INF_NUM_ACTION_HEADS]; + human_input_init(&hi); + human_input_queue_attack_npc(&hi, 5); + state.current_obs_slots[2] = -1; + inf_translate_human_commands(&hi, actions, &state); + ASSERT_INT_EQ("queued human target refreshes obs slot", + actions[INF_HEAD_TARGET], 3); + human_input_destroy(&hi); + } +} + +static void test_human_spell_selection_is_client_local_until_target_click(void) { + printf("--- human spell selection is client local until target click ---\n"); + + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_RANGER); + HumanInput hi; + human_input_init(&hi); + hi.cursor_mode = CURSOR_SPELL_TARGET; + hi.selected_spell = ATTACK_BLOOD; + + int actions[INF_NUM_ACTION_HEADS]; + inf_translate_human_commands(&hi, actions, &state); + ASSERT_INT_EQ("client-only selection queues no command", + hi.commands.count, 0); + ASSERT_INT_EQ("client-only selection sends no spell action", + actions[INF_HEAD_SPELL], 0); + + human_input_queue_spell_target(&hi, ATTACK_BLOOD, 0); + inf_translate_human_commands(&hi, actions, &state); + ASSERT_INT_EQ("spell target command kind", + hi.commands.items[0].kind, HUMAN_COMMAND_SPELL_TARGET); + ASSERT_INT_EQ("spell target command carries blood", + hi.commands.items[0].spell, ATTACK_BLOOD); + ASSERT_INT_EQ("spell target command maps spell action", + actions[INF_HEAD_SPELL], 1); + + human_input_destroy(&hi); +} + +static void test_human_walk_command_sends_no_selected_spell_cast(void) { + printf("--- human walk command sends no selected spell cast ---\n"); + + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_RANGER); + HumanInput hi; + human_input_init(&hi); + hi.cursor_mode = CURSOR_SPELL_TARGET; + hi.selected_spell = ATTACK_ICE; + human_input_queue_walk(&hi, 20, 20); + + int actions[INF_NUM_ACTION_HEADS]; + inf_translate_human_commands(&hi, actions, &state); + ASSERT_INT_EQ("walk command sends no spell action", + actions[INF_HEAD_SPELL], 0); + ASSERT_INT_EQ("walk command sends no target action", + actions[INF_HEAD_TARGET], 0); + + human_input_destroy(&hi); +} + +static void test_human_autocast_selection_persists_across_weapon_switches(void) { + printf("--- human autocast selection persists across weapon switches ---\n"); + + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_RANGER); + inf_legacy_context()->human_command_mode = 1; + state.player.autocast_spell = ENCOUNTER_SPELL_BLOOD; + + HumanInput hi; + human_input_init(&hi); + human_input_queue_set_autocast(&hi, ENCOUNTER_SPELL_ICE, 1); + human_input_queue_equip_inventory_item(&hi, 0, ITEM_TWISTED_BOW, GEAR_SLOT_WEAPON); + human_input_queue_equip_inventory_item(&hi, 0, ITEM_KODAI_WAND, GEAR_SLOT_WEAPON); + inf_legacy_context()->human_commands = hi.commands.items; + inf_legacy_context()->human_command_count = hi.commands.count; + inf_apply_human_player_commands(&state); + + ASSERT_INT_EQ("autocast spell persists after weapon switches", + state.player.autocast_spell, ENCOUNTER_SPELL_ICE); + ASSERT_INT_EQ("autocast stays enabled", + state.player.autocast_enabled, 1); + ASSERT_INT_EQ("defensive autocast persists", + state.player.autocast_defensive, 1); + ASSERT_INT_EQ("kodai returns to defensive autocast stance", + state.player.fight_style, FIGHT_STYLE_DEFENSIVE_AUTOCAST); + + human_input_destroy(&hi); +} + +static void test_autocast_is_inactive_with_non_autocast_weapon(void) { + printf("--- autocast is inactive with non-autocast weapon ---\n"); + + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_RANGER); + state.weapon_set = INF_GEAR_BP; + encounter_apply_loadout(&state.player, INF_MAX_RANGE_FAST_LOADOUT, GEAR_RANGED); + state.player.autocast_enabled = 1; + state.player.autocast_spell = ENCOUNTER_SPELL_ICE; + + InfPlayerAttack attack; + int resolved = inf_resolve_player_attack(&state, ENCOUNTER_SPELL_NONE, &attack); + ASSERT_INT_EQ("normal attack still resolves", + resolved, 1); + ASSERT_INT_EQ("non-autocast weapon ignores remembered autocast", + attack.stats.style, ATTACK_STYLE_RANGED); + ASSERT_INT_EQ("remembered autocast spell remains stored", + state.player.autocast_spell, ENCOUNTER_SPELL_ICE); +} + +static void test_echo_boots_recoil_reflects_to_attacker_only(void) { + printf("--- echo boots recoil reflects to the attacking NPC only ---\n"); + + InfernoState state = make_test_state(20, 20); + memset(state.player.equipped, ITEM_NONE, sizeof(state.player.equipped)); + osrs_item_effect_state_init(&state.player.item_effect_state); + state.player.equipped[GEAR_SLOT_FEET] = ITEM_ECHO_BOOTS; + osrs_refresh_player_equipment(&state.player); + + state.npcs[0] = make_test_npc(INF_NPC_BAT, 21, 20, INF_NPC_STATS[INF_NPC_BAT].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = 10; + state.npcs[1] = make_test_npc(INF_NPC_BAT, 23, 20, INF_NPC_STATS[INF_NPC_BAT].size); + state.npcs[1].active = 1; + state.npcs[1].hp = state.npcs[1].max_hp = 10; + state.npcs[2] = make_test_npc(INF_NPC_ZUK, 20, 20, INF_NPC_STATS[INF_NPC_ZUK].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = 1200; + + inf_apply_echo_boots_recoil(&state, 1, 0); + ASSERT_INT_EQ("zero damage does not consume echo charge", + state.player.item_effect_state.echo_boot_charges, OSRS_ECHO_BOOTS_MAX_CHARGES); + ASSERT_INT_EQ("zero damage does not recoil the attacker", + state.npcs[1].hp, 10); + + inf_apply_echo_boots_recoil(&state, 1, 7); + ASSERT_INT_EQ("positive damage consumes one echo charge", + state.player.item_effect_state.echo_boot_charges, OSRS_ECHO_BOOTS_MAX_CHARGES - 1); + ASSERT_INT_EQ("the attacker takes echo recoil regardless of distance", + state.npcs[1].hp, 9); + ASSERT_INT_EQ("an adjacent bystander is not hit", + state.npcs[0].hp, 10); + ASSERT_FLOAT_NEAR("echo recoil records one damage", + state.tick_scratch.damage_dealt, 1.0f, 1e-6f); + + inf_apply_echo_boots_recoil(&state, 2, 7); + ASSERT_INT_EQ("Zuk attacker avoids echo recoil", + state.npcs[2].hp, 1200); +} + +static void test_redemption_action_maps_without_smite(void) { + printf("--- redemption action maps without smite ---\n"); + + InfernoState state = make_test_state(20, 20); + InfernoContext* ctx = inf_legacy_context(); + state.player.base_prayer = 99; + state.player.current_prayer = 99; + + int actions[INF_NUM_ACTION_HEADS] = {0}; + actions[INF_HEAD_PRAYER] = INF_OVERHEAD_SET_REFRESH_REDEMPTION; + inf_player_pretick(&state, ctx, actions); + + ASSERT_INT_EQ("inferno exposes six overhead actions", + INF_ACTION_DIMS[INF_HEAD_PRAYER], ENCOUNTER_OVERHEAD_DIM_PVE_REDEMPTION); + ASSERT_INT_EQ("local action five is redemption", + state.player.prayer, PRAYER_REDEMPTION); + ASSERT_INT_EQ("inferno action five is not smite", + state.player.prayer == PRAYER_SMITE, 0); +} + +static void test_redemption_zero_hit_landing_heals_and_drains(void) { + printf("--- redemption zero-hit landing heals and drains ---\n"); + + InfernoState state = make_test_state(20, 20); + state.player.base_hitpoints = 99; + state.player.current_hitpoints = 7; + state.player.base_prayer = 99; + state.player.current_prayer = 12; + state.player.prayer = PRAYER_REDEMPTION; + state.player.offensive_prayer = OFFENSIVE_PRAYER_RIGOUR; + + inf_damage_player(&state, 0); + + ASSERT_INT_EQ("zero hit procs redemption at low HP", + state.player.current_hitpoints, 31); + ASSERT_INT_EQ("redemption drains prayer points", + state.player.current_prayer, 0); + ASSERT_INT_EQ("redemption clears overhead", + state.player.prayer, PRAYER_NONE); + ASSERT_INT_EQ("redemption clears offensive prayer", + state.player.offensive_prayer, OFFENSIVE_PRAYER_NONE); + ASSERT_INT_EQ("zero hit still shows a hitsplat", + state.player.hit_landed_this_tick, 1); + ASSERT_INT_EQ("zero hit remains zero damage", + state.player.hit_damage, 0); +} + +static void test_redemption_does_not_prevent_lethal_damage(void) { + printf("--- redemption does not prevent lethal damage ---\n"); + + InfernoState state = make_test_state(20, 20); + state.player.base_hitpoints = 99; + state.player.current_hitpoints = 7; + state.player.base_prayer = 99; + state.player.current_prayer = 12; + state.player.prayer = PRAYER_REDEMPTION; + + inf_damage_player(&state, 8); + + ASSERT_INT_EQ("lethal damage still kills through redemption", + state.player.current_hitpoints, 0); + ASSERT_INT_EQ("lethal damage does not drain redemption", + state.player.current_prayer, 12); +} + +static void test_redemption_procs_on_locked_zero_projectile_landing(void) { + printf("--- redemption procs on locked zero projectile landing ---\n"); + + InfernoState state = make_test_state(20, 20); + state.player.base_hitpoints = 99; + state.player.current_hitpoints = 7; + state.player.base_prayer = 99; + state.player.current_prayer = 12; + state.player.prayer = PRAYER_REDEMPTION; + state.player_pending_hits.count = 1; + state.player_pending_hits.hits[0] = (EncounterPendingHit){ + .active = 1, + .damage = 0, + .ticks_remaining = 1, + .attack_style = ATTACK_STYLE_MAGIC, + .check_prayer = 0, + .prayer_check_delay = 0, + .source_npc_type = INF_NPC_HEALER_ZUK, + .hit_success = 1, + }; + + inf_resolve_player_pending_hits(&state); + + ASSERT_INT_EQ("locked zero projectile lands", + state.player_pending_hits.count, 0); + ASSERT_INT_EQ("redemption heals on landing after protection was locked", + state.player.current_hitpoints, 31); + ASSERT_INT_EQ("redemption drains prayer on landing", + state.player.current_prayer, 0); +} + +static void test_human_autocast_works_with_dragon_hunter_wand(void) { + printf("--- human autocast works with dragon hunter wand ---\n"); + + InfernoState state; + init_spell_cast_test_state(&state, INF_NPC_RANGER); + inf_legacy_context()->human_command_mode = 1; + state.player.equipped[GEAR_SLOT_WEAPON] = ITEM_DRAGON_HUNTER_WAND; + state.player.equipped[GEAR_SLOT_SHIELD] = ITEM_CRYSTAL_SHIELD; + state.player.autocast_enabled = 1; + state.player.autocast_defensive = 1; + state.player.autocast_spell = ENCOUNTER_SPELL_ICE; + state.player.fight_style = FIGHT_STYLE_DEFENSIVE_AUTOCAST; + osrs_refresh_player_equipment(&state.player); + + InfPlayerAttack attack; + int resolved = inf_resolve_player_attack(&state, ENCOUNTER_SPELL_NONE, &attack); + + ASSERT_INT_EQ("dragon hunter wand autocast resolves", + resolved, 1); + ASSERT_INT_EQ("dragon hunter wand attack stays magic", + attack.stats.style, ATTACK_STYLE_MAGIC); + ASSERT_INT_EQ("dragon hunter wand casts remembered ice", + attack.spell, ENCOUNTER_SPELL_ICE); + ASSERT_INT_EQ("dragon hunter wand barrage flag", + attack.is_barrage, 1); + ASSERT_INT_EQ("dragon hunter wand autocast range", + attack.stats.attack_range, 10); +} + +static void test_inferno_snapshot_restore_round_trip(void) { + printf("--- inferno snapshot/restore round trip ---\n"); + + EncounterState* raw = inf_create(); + InfernoState* state = (InfernoState*)raw; + inf_reset(raw, 31415u); + + int actions_a[INF_NUM_ACTION_HEADS] = {0}; + actions_a[INF_HEAD_MOVE] = 1; + actions_a[INF_HEAD_TARGET] = 1; + actions_a[INF_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_MELEE; + + int actions_b[INF_NUM_ACTION_HEADS] = {0}; + actions_b[INF_HEAD_MOVE] = 5; + actions_b[INF_HEAD_TARGET] = 2; + actions_b[INF_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; + + const int N1 = 12; + for (int i = 0; i < N1; i++) inf_step(raw, actions_a); + + size_t snap_size = inf_snapshot_size(raw); + ASSERT_INT_EQ("snapshot size matches sizeof(InfSnapshot)", + (int)snap_size, (int)sizeof(InfSnapshot)); + InfSnapshot* snap_A = (InfSnapshot*)malloc(snap_size); + inf_snapshot(raw, snap_A); + ASSERT_INT_EQ("snapshot magic stamped", + (int)snap_A->magic, (int)INF_SNAPSHOT_MAGIC); + ASSERT_INT_EQ("snapshot version stamped", + (int)snap_A->version, (int)INF_SNAPSHOT_VERSION); + + const int N2 = 18; + for (int i = 0; i < N2; i++) inf_step(raw, actions_b); + + InfSnapshot* snap_B = (InfSnapshot*)malloc(snap_size); + inf_snapshot(raw, snap_B); + + inf_restore(raw, snap_A, snap_size); + ASSERT_INT_EQ("tick reset to N1 after restore", state->tick, N1); + for (int i = 0; i < N2; i++) inf_step(raw, actions_b); + + InfSnapshot* snap_B_prime = (InfSnapshot*)malloc(snap_size); + inf_snapshot(raw, snap_B_prime); + + int diff = memcmp(&snap_B->state, &snap_B_prime->state, sizeof(InfernoState)); + ASSERT_INT_EQ("memcmp(state at N1+N2, state after restore+replay) == 0", diff, 0); + + InfernoState* a = &snap_B->state; + InfernoState* b = &snap_B_prime->state; + ASSERT_INT_EQ("tick", a->tick, b->tick); + ASSERT_INT_EQ("wave", a->wave, b->wave); + ASSERT_INT_EQ("episode_over", a->episode_over, b->episode_over); + ASSERT_INT_EQ("winner", a->winner, b->winner); + ASSERT_INT_EQ("rng_state", (int)a->rng_state, (int)b->rng_state); + ASSERT_INT_EQ("player x", a->player.x, b->player.x); + ASSERT_INT_EQ("player y", a->player.y, b->player.y); + ASSERT_INT_EQ("player hp", a->player.current_hitpoints, b->player.current_hitpoints); + ASSERT_INT_EQ("player prayer", a->player.current_prayer, b->player.current_prayer); + + free(snap_A); + free(snap_B); + free(snap_B_prime); + inf_destroy(raw); +} + +static void test_inferno_snapshot_preserves_loadout_profile(void) { + printf("--- inferno snapshot preserves loadout profile ---\n"); + + EncounterState* raw = inf_create(); + InfernoState* state = (InfernoState*)raw; + + inf_put_int(raw, "loadout_profile_mode", INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); + inf_put_float(raw, "budget_loadout_fraction", 1.0f); + inf_reset(raw, 314u); + + size_t snap_size = inf_snapshot_size(raw); + InfSnapshot* snap = (InfSnapshot*)malloc(snap_size); + inf_snapshot(raw, snap); + + state->active_loadout_profile = INF_LOADOUT_PROFILE_MAX; + + inf_restore(raw, snap, snap_size); + + ASSERT_INT_EQ("restored active budget profile", + state->active_loadout_profile, INF_LOADOUT_PROFILE_BUDGET); + ASSERT_INT_EQ("restore keeps live loadout profile mode", + test_config()->loadout_profile_mode, INF_LOADOUT_PROFILE_MODE_BUDGET_ONLY); + ASSERT_FLOAT_NEAR("restore keeps live budget loadout fraction", + test_config()->budget_loadout_fraction, 1.0f, 1e-6f); + + free(snap); + inf_destroy(raw); +} + +static void test_inferno_restore_builds_npc_stats_before_late_spawn(void) { + printf("--- inferno restore builds NPC stats before late spawn ---\n"); + + EncounterState* raw_a = inf_create(); + reset_inferno_at_public_wave(raw_a, 69, 1.0f); + + InfSnapshot snap; + inf_snapshot(raw_a, &snap); + inf_destroy(raw_a); + + EncounterState* raw_b = inf_create(); + inf_put_int(raw_b, "start_wave", 69); + memset(INF_NPC_STATS, 0, sizeof(INF_NPC_STATS)); + inf_restore(raw_b, &snap, sizeof(snap)); + + InfernoState* state = (InfernoState*)raw_b; + int zuk_idx = find_active_npc_type(state, INF_NPC_ZUK); + ASSERT_INT_EQ("Zuk exists after restore", zuk_idx >= 0, 1); + state->wave_ready_delay = 0; + state->zuk.set_timer = 1; + state->npcs[zuk_idx].attack_timer = 999; + + int actions[INF_NUM_ACTION_HEADS] = {0}; + inf_step(raw_b, actions); + + int mager_idx = find_active_npc_type(state, INF_NPC_MAGER); + int ranger_idx = find_active_npc_type(state, INF_NPC_RANGER); + ASSERT_INT_EQ("restored env spawned mager", mager_idx >= 0, 1); + ASSERT_INT_EQ("restored env spawned ranger", ranger_idx >= 0, 1); + ASSERT_INT_EQ("spawned mager has real HP", + state->npcs[mager_idx].hp, INF_NPC_STATS[INF_NPC_MAGER].hp); + ASSERT_INT_EQ("spawned ranger has real HP", + state->npcs[ranger_idx].hp, INF_NPC_STATS[INF_NPC_RANGER].hp); + ASSERT_INT_EQ("spawned mager has real max HP", + state->npcs[mager_idx].max_hp, INF_NPC_STATS[INF_NPC_MAGER].hp); + ASSERT_INT_EQ("spawned ranger has real max HP", + state->npcs[ranger_idx].max_hp, INF_NPC_STATS[INF_NPC_RANGER].hp); + ASSERT_INT_EQ("spawned mager did not attack on spawn tick", + state->npcs[mager_idx].attacked_this_tick, 0); + ASSERT_INT_EQ("spawned ranger did not attack on spawn tick", + state->npcs[ranger_idx].attacked_this_tick, 0); + + inf_destroy(raw_b); +} + +static void test_inferno_snapshot_preserves_external_pointers(void) { + printf("--- inferno snapshot preserves external pointers across restore ---\n"); + + InfernoState state_a; + InfernoState state_b; + InfernoContext ctx_a; + InfernoContext ctx_b; + Log log_a = {0}; + Log log_b = {0}; + int dummy_a = 1, dummy_b = 2; + + inf_init_context_typed(&ctx_a); + inf_init_context_typed(&ctx_b); + ctx_a.collision_map = (const CollisionMap*)&dummy_a; + ctx_b.collision_map = (const CollisionMap*)&dummy_b; + ctx_a.world_offset_x = 100; + ctx_a.world_offset_y = 200; + ctx_b.world_offset_x = 300; + ctx_b.world_offset_y = 400; + ctx_a.log = &log_a; + ctx_b.log = &log_b; + ctx_a.config.start_wave = 12; + ctx_b.config = ctx_a.config; + ctx_b.human_command_mode = 1; + + inf_reset_ctx((EncounterState*)&state_a, (EncounterContext*)&ctx_a, 7u); + inf_reset_ctx((EncounterState*)&state_b, (EncounterContext*)&ctx_b, 7u); + + size_t snap_size = inf_snapshot_size_ctx( + (EncounterState*)&state_a, (EncounterContext*)&ctx_a); + InfSnapshot* snap = (InfSnapshot*)malloc(snap_size); + inf_snapshot_ctx((EncounterState*)&state_a, (EncounterContext*)&ctx_a, snap); + + inf_restore_ctx((EncounterState*)&state_b, (EncounterContext*)&ctx_b, snap, snap_size); + ASSERT_INT_EQ("env B keeps its own collision_map after restore", + (int)(ctx_b.collision_map == (const CollisionMap*)&dummy_b), 1); + ASSERT_INT_EQ("env A snapshot did not leak its collision_map into B", + (int)(ctx_b.collision_map != (const CollisionMap*)&dummy_a), 1); + ASSERT_INT_EQ("env B keeps world offset x after restore", ctx_b.world_offset_x, 300); + ASSERT_INT_EQ("env B keeps world offset y after restore", ctx_b.world_offset_y, 400); + ASSERT_INT_EQ("env B keeps live log pointer after restore", + (int)(ctx_b.log == &log_b), 1); + ASSERT_INT_EQ("env B keeps live start_wave after restore", + ctx_b.config.start_wave, 12); + ASSERT_INT_EQ("env B keeps live human command mode after restore", + ctx_b.human_command_mode, 1); + + free(snap); +} + +static void test_inferno_state_assignment_copy_replays_trajectory(void) { + printf("--- inferno state assignment copy replays trajectory ---\n"); + + InfernoContext ctx_a; + InfernoContext ctx_b; + InfernoState state_a; + InfernoState state_b; + inf_init_context_typed(&ctx_a); + inf_init_context_typed(&ctx_b); + inf_init_state_typed(&state_a, &ctx_a); + inf_init_state_typed(&state_b, &ctx_b); + inf_reset_ctx((EncounterState*)&state_a, (EncounterContext*)&ctx_a, 987u); + + int prefix[INF_NUM_ACTION_HEADS] = {0}; + prefix[INF_HEAD_MOVE] = 1; + prefix[INF_HEAD_TARGET] = 1; + prefix[INF_HEAD_GEAR] = INF_GEAR_LONG_RANGE; + + for (int i = 0; i < 9; i++) + inf_step_ctx((EncounterState*)&state_a, (EncounterContext*)&ctx_a, prefix); + + state_b = state_a; + inf_refresh_after_state_load(&state_b, &ctx_b); + + int suffix[INF_NUM_ACTION_HEADS] = {0}; + suffix[INF_HEAD_MOVE] = 5; + suffix[INF_HEAD_TARGET] = 2; + suffix[INF_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; + suffix[INF_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_RIGOUR; + + for (int i = 0; i < 17; i++) { + inf_step_ctx((EncounterState*)&state_a, (EncounterContext*)&ctx_a, suffix); + inf_step_ctx((EncounterState*)&state_b, (EncounterContext*)&ctx_b, suffix); + } + + inf_refresh_after_state_load(&state_a, &ctx_a); + inf_refresh_after_state_load(&state_b, &ctx_b); + ASSERT_INT_EQ("assignment copy replay state memcmp", + memcmp(&state_a, &state_b, sizeof(InfernoState)), 0); +} + +static void test_inferno_refresh_after_state_load_rebuilds_derived_state(void) { + printf("--- inferno refresh after state load rebuilds derived state ---\n"); + + InfernoContext ctx; + InfernoState state; + inf_init_context_typed(&ctx); + inf_init_state_typed(&state, &ctx); + inf_reset_ctx((EncounterState*)&state, (EncounterContext*)&ctx, 42u); + + memset(state.current_obs_slots, -1, sizeof(state.current_obs_slots)); + memset(state.npc_collision_flags, 99, sizeof(state.npc_collision_flags)); + memset(state.player_collision_flags, 99, sizeof(state.player_collision_flags)); + memset(state.npc_los_cache, 7, sizeof(state.npc_los_cache)); + state.loadout_stats[INF_GEAR_LONG_RANGE].max_hit = -1; + + inf_refresh_after_state_load(&state, &ctx); + + int visible_count = 0; + for (int i = 0; i < INF_OBS_NPCS; i++) { + if (state.current_obs_slots[i] >= 0) + visible_count++; + } + + ASSERT_INT_EQ("refresh repopulates visible obs slots", visible_count > 0, 1); + ASSERT_INT_EQ("refresh recomputes long-range max hit", + state.loadout_stats[INF_GEAR_LONG_RANGE].max_hit > 0, 1); + ASSERT_INT_EQ("refresh clears npc collision scratch", + state.npc_collision_flags[0][0], 0); + ASSERT_INT_EQ("refresh clears player collision scratch", + state.player_collision_flags[0][0], 0); + ASSERT_INT_EQ("refresh invalidates LOS cache", + state.npc_los_cache[0], -1); +} + +static void test_inferno_cell_key_is_deterministic_and_16_bytes(void) { + printf("--- inferno cell key is deterministic and 16 bytes ---\n"); + + EncounterState* raw = inf_create(); + inf_reset(raw, 99u); + + ASSERT_INT_EQ("sizeof(InfCellKey) == 16", (int)sizeof(InfCellKey), 16); + ASSERT_INT_EQ("inf_cell_key_size returns sizeof(InfCellKey)", + (int)inf_cell_key_size(raw), (int)sizeof(InfCellKey)); + + ASSERT_INT_EQ("progress_score == 0 at reset", + (int)(inf_progress_score(raw) * 1000.0f), 0); + + int actions[INF_NUM_ACTION_HEADS] = {0}; + actions[INF_HEAD_MOVE] = 3; + actions[INF_HEAD_TARGET] = 1; + actions[INF_HEAD_PRAYER] = ENCOUNTER_OVERHEAD_SET_REFRESH_RANGED; + actions[INF_HEAD_OFFENSIVE] = ENCOUNTER_OFFENSIVE_SET_REFRESH_PIETY; + + inf_reset(raw, 99u); + for (int i = 0; i < 20; i++) inf_step(raw, actions); + InfCellKey key_a; + inf_write_cell_key(raw, &key_a); + + inf_reset(raw, 99u); + for (int i = 0; i < 20; i++) inf_step(raw, actions); + InfCellKey key_b; + inf_write_cell_key(raw, &key_b); + + int diff = memcmp(&key_a, &key_b, sizeof(InfCellKey)); + ASSERT_INT_EQ("memcmp(key_a, key_b) == 0 for same seed+actions", diff, 0); + + inf_reset(raw, 99u); + InfCellKey key_at_reset; + inf_write_cell_key(raw, &key_at_reset); + int diff_post = memcmp(&key_at_reset, &key_a, sizeof(InfCellKey)); + ASSERT_INT_EQ("key at reset differs from key after stepping", (diff_post != 0) ? 1 : 0, 1); + + inf_destroy(raw); +} + +static void test_inferno_cell_key_quantization_groups_neighbors(void) { + printf("--- inferno cell key quantization groups neighbor states ---\n"); + + EncounterState* raw_a = inf_create(); + EncounterState* raw_b = inf_create(); + InfernoState* state_a = (InfernoState*)raw_a; + InfernoState* state_b = (InfernoState*)raw_b; + + inf_reset(raw_a, 1u); + inf_reset(raw_b, 1u); + + state_b->player.x = state_a->player.x + 1; + + InfCellKey key_a, key_b; + inf_write_cell_key(raw_a, &key_a); + inf_write_cell_key(raw_b, &key_b); + int diff_within_bin = memcmp(&key_a, &key_b, sizeof(InfCellKey)); + ASSERT_INT_EQ("1-tile player shift stays in same cell", diff_within_bin, 0); + + state_b->player.x = state_a->player.x + 2; + inf_write_cell_key(raw_b, &key_b); + int diff_cross_bin = memcmp(&key_a, &key_b, sizeof(InfCellKey)); + ASSERT_INT_EQ("2-tile player shift changes cell", (diff_cross_bin != 0) ? 1 : 0, 1); + + inf_destroy(raw_a); + inf_destroy(raw_b); +} + +static void test_inferno_cell_key_tracks_set_magers_and_jad_hp_bucket(void) { + printf("--- inferno cell key tracks set magers and jad hp bucket ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + + state.npcs[2] = make_test_npc( + INF_NPC_MAGER, 20, 36, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[2].active = 1; + state.npcs[2].hp = state.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + + state.npcs[3] = make_test_npc( + INF_NPC_RANGER, 26, 36, INF_NPC_STATS[INF_NPC_RANGER].size); + state.npcs[3].active = 1; + state.npcs[3].hp = state.npcs[3].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + + state.npcs[4] = make_test_npc( + INF_NPC_MELEER, 24, 36, INF_NPC_STATS[INF_NPC_MELEER].size); + state.npcs[4].active = 1; + state.npcs[4].hp = state.npcs[4].max_hp = INF_NPC_STATS[INF_NPC_MELEER].hp; + + state.npcs[5] = make_test_npc( + INF_NPC_JAD, 24, 32, INF_NPC_STATS[INF_NPC_JAD].size); + state.npcs[5].active = 1; + state.npcs[5].hp = 253; + state.npcs[5].max_hp = INF_NPC_STATS[INF_NPC_JAD].hp; + state.player.attack_timer = 6; + + InfCellKey key; + inf_write_cell_key((EncounterState*)&state, &key); + + ASSERT_INT_EQ("active_set_count includes mager ranger meleer", + key.active_set_count, 3); + ASSERT_INT_EQ("jad_hp_bin uses live Jad HP / 50", + key.jad_hp_bin, 5); + ASSERT_INT_EQ("cell key stores player attack timer bucket", + key.offensive_prayer_attack_timer >> 4, 6); +} + +static void test_inferno_progress_score_rewards_late_add_transitions(void) { + printf("--- inferno progress score rewards late add transitions ---\n"); + + InfernoState jad_alive; + init_zuk_timing_state(&jad_alive); + jad_alive.zuk.jad_spawned = 1; + jad_alive.min_zuk_hp_seen = 300.0f; + jad_alive.npcs[0].hp = 300; + jad_alive.npcs[2] = make_test_npc( + INF_NPC_JAD, 24, 32, INF_NPC_STATS[INF_NPC_JAD].size); + jad_alive.npcs[2].active = 1; + jad_alive.npcs[2].hp = jad_alive.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_JAD].hp; + + InfernoState jad_dead = jad_alive; + jad_dead.npcs[2].active = 0; + jad_dead.npcs[2].hp = 0; + + float q_jad_alive = inf_progress_score((EncounterState*)&jad_alive); + float q_jad_dead = inf_progress_score((EncounterState*)&jad_dead); + ASSERT_FLOAT_GT("same Zuk HP with Jad dead scores higher", + q_jad_dead, q_jad_alive); + + InfernoState jad_damaged = jad_alive; + jad_damaged.npcs[2].hp = jad_damaged.npcs[2].max_hp / 2; + float q_jad_damaged = inf_progress_score((EncounterState*)&jad_damaged); + ASSERT_FLOAT_GT("same Zuk HP with Jad damaged scores higher", + q_jad_damaged, q_jad_alive); + ASSERT_FLOAT_GT("Jad dead scores above partial Jad damage", + q_jad_dead, q_jad_damaged); + + InfernoState healer_alive = jad_dead; + healer_alive.zuk.healer_spawned = 1; + healer_alive.min_zuk_hp_seen = 180.0f; + healer_alive.npcs[0].hp = 180; + healer_alive.npcs[2] = make_test_npc( + INF_NPC_HEALER_ZUK, 20, 48, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + healer_alive.npcs[2].active = 1; + healer_alive.npcs[2].hp = healer_alive.npcs[2].max_hp = + INF_NPC_STATS[INF_NPC_HEALER_ZUK].hp; + + InfernoState healer_dead = healer_alive; + healer_dead.npcs[2].active = 0; + healer_dead.npcs[2].hp = 0; + + float q_healer_alive = inf_progress_score((EncounterState*)&healer_alive); + float q_healer_dead = inf_progress_score((EncounterState*)&healer_dead); + ASSERT_FLOAT_GT("same Zuk HP with healers cleared scores higher", + q_healer_dead, q_healer_alive); + + InfernoState set_alive; + init_zuk_timing_state(&set_alive); + set_alive.min_zuk_hp_seen = 850.0f; + set_alive.npcs[0].hp = 850; + set_alive.npcs[2] = make_test_npc( + INF_NPC_MAGER, 20, 36, INF_NPC_STATS[INF_NPC_MAGER].size); + set_alive.npcs[2].active = 1; + set_alive.npcs[2].hp = set_alive.npcs[2].max_hp = INF_NPC_STATS[INF_NPC_MAGER].hp; + + InfernoState set_dead = set_alive; + set_dead.npcs[2].active = 0; + set_dead.npcs[2].hp = 0; + + float q_set_alive = inf_progress_score((EncounterState*)&set_alive); + float q_set_dead = inf_progress_score((EncounterState*)&set_dead); + ASSERT_FLOAT_GT("same Zuk HP with set cleared scores higher", + q_set_dead, q_set_alive); + + InfernoState win = healer_alive; + win.episode_over = 1; + win.winner = INF_OUTCOME_PLAYER_WON; + float q_win = inf_progress_score((EncounterState*)&win); + ASSERT_FLOAT_GT("win scores above partial state", q_win, q_healer_dead); +} + +static void test_inferno_healer_transition_stats_track_episode_progress(void) { + printf("--- inferno healer transition stats track episode progress ---\n"); + + InfernoState state = make_test_state(INF_ZUK_PLAYER_START_X, INF_ZUK_PLAYER_START_Y); + state.tick = 120; + state.tick_at_le_240 = -1; + state.tick_at_zuk_healer_spawn = -1; + state.tick_at_first_zuk_healer_tag = -1; + state.tick_at_all_zuk_healers_tagged = -1; + state.tick_at_all_zuk_healers_dead = -1; + state.zuk.healer_spawned = 1; + + state.npcs[0] = make_test_npc(INF_NPC_ZUK, 20, 52, 5); + state.npcs[0].active = 1; + state.npcs[0].hp = 239; + state.npcs[0].max_hp = 1200; + + for (int i = 1; i <= 4; i++) { + state.npcs[i] = make_test_npc(INF_NPC_HEALER_ZUK, 15 + i, 48, 1); + state.npcs[i].active = 1; + state.npcs[i].hp = 100; + state.npcs[i].max_hp = 100; + } + + state.tick_scratch.zuk_healer_tags = 1; + state.tick_scratch.hp_restored = 21.0f; + state.tick_scratch.spark_damage = 7.0f; + inf_update_healer_transition_stats(&state); + + ASSERT_INT_EQ("healer spawn tick recorded", + state.tick_at_zuk_healer_spawn, 120); + ASSERT_INT_EQ("healer stats infer 240 threshold tick", + state.tick_at_le_240, 120); + ASSERT_INT_EQ("first healer tag tick recorded", + state.tick_at_first_zuk_healer_tag, 120); + ASSERT_INT_EQ("one healer tag accumulated", + state.total_zuk_healer_tags, 1); + ASSERT_FLOAT_NEAR("post-240 restored hp accumulated", + state.hp_restored_after_240, 21.0f, 1e-6f); + ASSERT_FLOAT_NEAR("post-240 spark damage accumulated", + state.spark_damage_after_240, 7.0f, 1e-6f); + ASSERT_FLOAT_NEAR("post-spawn max Zuk HP initialized", + state.zuk_hp_max_after_healer_spawn, 239.0f, 1e-6f); + + state.tick = 121; + state.npcs[0].hp = 420; + state.tick_scratch.zuk_healer_tags = 3; + state.tick_scratch.kill_zuk_healer = 2; + state.tick_scratch.hp_restored = 13.0f; + state.tick_scratch.spark_damage = 5.0f; + inf_update_healer_transition_stats(&state); + + ASSERT_INT_EQ("all healer tag tick recorded", + state.tick_at_all_zuk_healers_tagged, 121); + ASSERT_INT_EQ("four healer tags accumulated", + state.total_zuk_healer_tags, 4); + ASSERT_INT_EQ("two healer kills accumulated", + state.total_zuk_healer_kills, 2); + ASSERT_FLOAT_NEAR("post-spawn max Zuk HP tracks healer restore", + state.zuk_hp_max_after_healer_spawn, 420.0f, 1e-6f); + + state.tick = 122; + state.tick_scratch.zuk_healer_tags = 0; + state.tick_scratch.kill_zuk_healer = 2; + state.tick_scratch.hp_restored = 0.0f; + state.tick_scratch.spark_damage = 0.0f; + for (int i = 1; i <= 4; i++) + state.npcs[i].active = 0; + inf_update_healer_transition_stats(&state); + + ASSERT_INT_EQ("all healer dead tick recorded", + state.tick_at_all_zuk_healers_dead, 122); + ASSERT_INT_EQ("four healer kills accumulated", + state.total_zuk_healer_kills, 4); +} + +static void test_inferno_human_equip_does_not_snap_loadout(void) { + printf("--- inferno human equip does not snap full loadout ---\n"); + + EncounterState* raw = inf_create(); + InfernoState* state = (InfernoState*)raw; + inf_reset(raw, 123); + + HumanInput input; + human_input_init(&input); + input.enabled = 1; + + uint8_t old_body = state->player.equipped[GEAR_SLOT_BODY]; + human_input_queue_equip_inventory_item( + &input, 0, ITEM_TOXIC_BLOWPIPE, GEAR_SLOT_WEAPON); + + inf_step_human_commands(raw, &input); + + ASSERT_INT_EQ("weapon changed to clicked blowpipe", + state->player.equipped[GEAR_SLOT_WEAPON], ITEM_TOXIC_BLOWPIPE); + ASSERT_INT_EQ("body slot did not snap to ranged preset", + state->player.equipped[GEAR_SLOT_BODY], old_body); + ASSERT_INT_EQ("2h weapon clears shield", + state->player.equipped[GEAR_SLOT_SHIELD], ITEM_NONE); + ASSERT_INT_EQ("queued command drained", input.commands.count, 0); + + human_input_destroy(&input); + inf_destroy(raw); +} + +static void test_jad_render_uses_style_specific_attack_animation(void) { + printf("--- jad render uses style-specific attack animation ---\n"); + + InfernoState magic_state; + init_jad_timing_test_state(&magic_state, 10, 10, 16, 10); + magic_state.npcs[0].attacked_this_tick = 1; + magic_state.npcs[0].attack_style_this_tick = ATTACK_STYLE_MAGIC; + + RenderEntity magic_entities[4]; + int magic_count = 0; + inf_fill_render_entities((EncounterState*)&magic_state, magic_entities, 4, &magic_count); + + InfernoState range_state; + init_jad_timing_test_state(&range_state, 10, 10, 16, 10); + range_state.npcs[0].attacked_this_tick = 1; + range_state.npcs[0].attack_style_this_tick = ATTACK_STYLE_RANGED; + + RenderEntity range_entities[4]; + int range_count = 0; + inf_fill_render_entities((EncounterState*)&range_state, range_entities, 4, &range_count); + + ASSERT_INT_EQ("jad magic render entity count", magic_count, 2); + ASSERT_INT_EQ("jad ranged render entity count", range_count, 2); + ASSERT_INT_EQ("jad magic attack animation", magic_entities[1].npc_anim_id, 7592); + ASSERT_INT_EQ("jad ranged attack animation", range_entities[1].npc_anim_id, 7593); +} + +static void test_inferno_render_uses_npc_death_animation(void) { + printf("--- inferno render uses NPC death animation ---\n"); + + InfernoState state; + init_phantom_barrage_test_state(&state, 2, 1); + + RenderEntity entities[4]; + int count = 0; + inf_fill_render_entities((EncounterState*)&state, entities, 4, &count); + + ASSERT_INT_EQ("dying NPC still renders", count >= 2, 1); + ASSERT_INT_EQ("nibbler death animation", + entities[1].npc_anim_id, INF_GEN_ANIM_NIBBLER_DEATH); +} + +static void test_jad_magic_render_emits_three_offset_projectiles(void) { + printf("--- jad magic render emits three offset projectiles ---\n"); + + InfernoState state; + init_jad_timing_test_state(&state, 10, 10, 16, 10); + state.npcs[0].attacked_this_tick = 1; + state.npcs[0].attack_style_this_tick = ATTACK_STYLE_MAGIC; + + EncounterOverlay ov; + memset(&ov, 0, sizeof(ov)); + inf_render_post_tick((EncounterState*)&state, &ov); + + ASSERT_INT_EQ("jad magic emits three projectile models", ov.projectile_count, 3); + ASSERT_INT_EQ("jad magic front model", ov.projectiles[0].model_id, INF_GFX_448_MODEL); + ASSERT_INT_EQ("jad magic middle model", ov.projectiles[1].model_id, INF_GFX_449_MODEL); + ASSERT_INT_EQ("jad magic rear model", ov.projectiles[2].model_id, INF_GFX_450_MODEL); + ASSERT_INT_EQ("jad magic front anim", ov.projectiles[0].anim_id, INF_GFX_448_ANIM); + ASSERT_INT_EQ("jad magic middle anim", ov.projectiles[1].anim_id, INF_GFX_449_ANIM); + ASSERT_INT_EQ("jad magic rear anim", ov.projectiles[2].anim_id, INF_GFX_450_ANIM); + ASSERT_INT_EQ("jad magic visible duration is two ticks close range", ov.projectiles[0].duration_ticks, 2 * 30); + ASSERT_INT_EQ("jad magic start delay is three ticks", ov.projectiles[0].start_delay, 3 * 30); + ASSERT_FLOAT_NEAR("jad magic arc height", ov.projectiles[0].arc_height, 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("jad magic front offset", ov.projectiles[0].offset_y, 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("jad magic middle offset", ov.projectiles[1].offset_y, 0.5f, 1e-6f); + ASSERT_FLOAT_NEAR("jad magic rear offset", ov.projectiles[2].offset_y, 0.0f, 1e-6f); +} + +static void test_jad_ranged_render_uses_target_anchored_two_tick_visual(void) { + printf("--- jad ranged render uses target anchored two tick visual ---\n"); + + InfernoState state; + init_jad_timing_test_state(&state, 10, 10, 16, 10); + state.npcs[0].attacked_this_tick = 1; + state.npcs[0].attack_style_this_tick = ATTACK_STYLE_RANGED; + + EncounterOverlay ov; + memset(&ov, 0, sizeof(ov)); + inf_render_post_tick((EncounterState*)&state, &ov); + + ASSERT_INT_EQ("jad ranged emits one projectile", ov.projectile_count, 1); + ASSERT_INT_EQ("jad ranged model", ov.projectiles[0].model_id, INF_GFX_451_MODEL); + ASSERT_INT_EQ("jad ranged anim", ov.projectiles[0].anim_id, INF_GFX_451_ANIM); + ASSERT_INT_EQ("jad ranged target-anchored motion", + ov.projectiles[0].motion_mode, ENCOUNTER_PROJECTILE_MOTION_TARGET_ANCHORED); + ASSERT_INT_EQ("jad ranged start height is player target height", ov.projectiles[0].start_h, 64); + ASSERT_INT_EQ("jad ranged end height is player target height", ov.projectiles[0].end_h, 64); + ASSERT_INT_EQ("jad ranged visible duration is two ticks close range", ov.projectiles[0].duration_ticks, 2 * 30); + ASSERT_INT_EQ("jad ranged start delay is three ticks", ov.projectiles[0].start_delay, 3 * 30); +} + +static void test_jad_projectile_long_distance_visual_duration_uses_reference_formula(void) { + printf("--- jad long-distance projectile visual duration uses reference formula ---\n"); + + InfernoState range_state; + init_jad_timing_test_state(&range_state, 10, 10, 36, 10); + range_state.npcs[0].attacked_this_tick = 1; + range_state.npcs[0].attack_style_this_tick = ATTACK_STYLE_RANGED; + + EncounterOverlay range_ov; + memset(&range_ov, 0, sizeof(range_ov)); + inf_render_post_tick((EncounterState*)&range_state, &range_ov); + + int range_dist = encounter_projectile_distance( + range_state.npcs[0].x, range_state.npcs[0].y, range_state.npcs[0].size, + range_state.player.x, range_state.player.y, 1, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + EncounterProjectileTiming range_timing = + inf_npc_projectile_timing(INF_NPC_JAD, ATTACK_STYLE_RANGED, range_dist); + ASSERT_INT_EQ("jad ranged long-distance duration", + range_ov.projectiles[0].duration_ticks, + range_timing.visual_duration_ticks * 30); + + InfernoState magic_state; + init_jad_timing_test_state(&magic_state, 10, 10, 36, 10); + magic_state.npcs[0].attacked_this_tick = 1; + magic_state.npcs[0].attack_style_this_tick = ATTACK_STYLE_MAGIC; + + EncounterOverlay magic_ov; + memset(&magic_ov, 0, sizeof(magic_ov)); + inf_render_post_tick((EncounterState*)&magic_state, &magic_ov); + + int magic_dist = encounter_projectile_distance( + magic_state.npcs[0].x, magic_state.npcs[0].y, magic_state.npcs[0].size, + magic_state.player.x, magic_state.player.y, 1, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + EncounterProjectileTiming magic_timing = + inf_npc_projectile_timing(INF_NPC_JAD, ATTACK_STYLE_MAGIC, magic_dist); + ASSERT_INT_EQ("jad magic long-distance duration", + magic_ov.projectiles[0].duration_ticks, + magic_timing.visual_duration_ticks * 30); +} + +static void test_inferno_npc_projectile_render_uses_reference_visual_timing(void) { + printf("--- inferno npc projectile render uses reference visual timing ---\n"); + + InfernoState mager_state = make_test_state(10, 10); + mager_state.npcs[0] = make_test_npc( + INF_NPC_MAGER, 16, 10, INF_NPC_STATS[INF_NPC_MAGER].size); + mager_state.npcs[0].active = 1; + mager_state.npcs[0].attacked_this_tick = 1; + mager_state.npcs[0].attack_style_this_tick = ATTACK_STYLE_MAGIC; + + EncounterOverlay mager_ov; + memset(&mager_ov, 0, sizeof(mager_ov)); + inf_render_post_tick((EncounterState*)&mager_state, &mager_ov); + + int mager_dist = encounter_projectile_distance( + mager_state.npcs[0].x, mager_state.npcs[0].y, mager_state.npcs[0].size, + mager_state.player.x, mager_state.player.y, 1, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + EncounterProjectileTiming mager_timing = + inf_npc_projectile_timing(INF_NPC_MAGER, ATTACK_STYLE_MAGIC, mager_dist); + + ASSERT_INT_EQ("mager projectile count", mager_ov.projectile_count, 1); + ASSERT_INT_EQ("mager projectile model", + mager_ov.projectiles[0].model_id, INF_GFX_1376_MODEL); + ASSERT_INT_EQ("mager projectile animation", + mager_ov.projectiles[0].anim_id, INF_GFX_1376_ANIM); + ASSERT_INT_EQ("mager impact spotanim", + mager_ov.projectiles[0].impact_gfx_id, 0); + ASSERT_INT_EQ("mager projectile tracks player", mager_ov.projectiles[0].tracks_target, 1); + ASSERT_INT_EQ("mager projectile target kind", + mager_ov.projectiles[0].target_kind, ENCOUNTER_PROJECTILE_TARGET_PLAYER); + ASSERT_INT_EQ("mager projectile source kind", + mager_ov.projectiles[0].source_kind, ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT); + ASSERT_INT_EQ("mager projectile source slot", + mager_ov.projectiles[0].source_npc_slot, 0); + ASSERT_INT_EQ("mager visual start delay", + mager_ov.projectiles[0].start_delay, mager_timing.visual_start_delay_ticks * 30); + ASSERT_INT_EQ("mager visual duration", + mager_ov.projectiles[0].duration_ticks, mager_timing.visual_duration_ticks * 30); + + InfernoState ranger_state = make_test_state(10, 10); + ranger_state.npcs[0] = make_test_npc( + INF_NPC_RANGER, 16, 10, INF_NPC_STATS[INF_NPC_RANGER].size); + ranger_state.npcs[0].active = 1; + ranger_state.npcs[0].attacked_this_tick = 1; + ranger_state.npcs[0].attack_style_this_tick = ATTACK_STYLE_RANGED; + + EncounterOverlay ranger_ov; + memset(&ranger_ov, 0, sizeof(ranger_ov)); + inf_render_post_tick((EncounterState*)&ranger_state, &ranger_ov); + + int ranger_dist = encounter_projectile_distance( + ranger_state.npcs[0].x, ranger_state.npcs[0].y, ranger_state.npcs[0].size, + ranger_state.player.x, ranger_state.player.y, 1, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + EncounterProjectileTiming ranger_timing = + inf_npc_projectile_timing(INF_NPC_RANGER, ATTACK_STYLE_RANGED, ranger_dist); + + ASSERT_INT_EQ("ranger projectile count", ranger_ov.projectile_count, 1); + ASSERT_INT_EQ("ranger projectile model", + ranger_ov.projectiles[0].model_id, INF_GFX_1377_MODEL); + ASSERT_INT_EQ("ranger impact spotanim", + ranger_ov.projectiles[0].impact_gfx_id, 0); + ASSERT_INT_EQ("ranger projectile tracks player", ranger_ov.projectiles[0].tracks_target, 1); + ASSERT_INT_EQ("ranger projectile target kind", + ranger_ov.projectiles[0].target_kind, ENCOUNTER_PROJECTILE_TARGET_PLAYER); + ASSERT_INT_EQ("ranger projectile source kind", + ranger_ov.projectiles[0].source_kind, ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT); + ASSERT_INT_EQ("ranger projectile source slot", + ranger_ov.projectiles[0].source_npc_slot, 0); + ASSERT_INT_EQ("ranger visual start delay", + ranger_ov.projectiles[0].start_delay, ranger_timing.visual_start_delay_ticks * 30); + ASSERT_INT_EQ("ranger visual duration", + ranger_ov.projectiles[0].duration_ticks, ranger_timing.visual_duration_ticks * 30); + + InfernoState blob_state = make_test_state(10, 10); + blob_state.npcs[3] = make_test_npc( + INF_NPC_BLOB, 16, 10, INF_NPC_STATS[INF_NPC_BLOB].size); + blob_state.npcs[3].active = 1; + blob_state.npcs[3].attacked_this_tick = 1; + blob_state.npcs[3].attack_style_this_tick = ATTACK_STYLE_RANGED; + + EncounterOverlay blob_ov; + memset(&blob_ov, 0, sizeof(blob_ov)); + inf_render_post_tick((EncounterState*)&blob_state, &blob_ov); + + ASSERT_INT_EQ("blob projectile count", blob_ov.projectile_count, 1); + ASSERT_INT_EQ("blob ranged projectile model", + blob_ov.projectiles[0].model_id, INF_GFX_1383_MODEL); + ASSERT_INT_EQ("blob ranged projectile animation", + blob_ov.projectiles[0].anim_id, INF_GFX_1383_ANIM); + ASSERT_INT_EQ("blob ranged impact spotanim", + blob_ov.projectiles[0].impact_gfx_id, 0); + ASSERT_INT_EQ("blob projectile source kind", + blob_ov.projectiles[0].source_kind, ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT); + ASSERT_INT_EQ("blob projectile source slot", blob_ov.projectiles[0].source_npc_slot, 3); + ASSERT_INT_EQ("blob projectile target kind", + blob_ov.projectiles[0].target_kind, ENCOUNTER_PROJECTILE_TARGET_PLAYER); + + InfernoState blob_magic_state = make_test_state(10, 10); + blob_magic_state.npcs[3] = make_test_npc( + INF_NPC_BLOB, 16, 10, INF_NPC_STATS[INF_NPC_BLOB].size); + blob_magic_state.npcs[3].active = 1; + blob_magic_state.npcs[3].attacked_this_tick = 1; + blob_magic_state.npcs[3].attack_style_this_tick = ATTACK_STYLE_MAGIC; + + EncounterOverlay blob_magic_ov; + memset(&blob_magic_ov, 0, sizeof(blob_magic_ov)); + inf_render_post_tick((EncounterState*)&blob_magic_state, &blob_magic_ov); + + ASSERT_INT_EQ("blob magic projectile count", blob_magic_ov.projectile_count, 1); + ASSERT_INT_EQ("blob magic projectile model", + blob_magic_ov.projectiles[0].model_id, INF_GFX_1384_MODEL); + ASSERT_INT_EQ("blob magic projectile has no placeholder animation", + blob_magic_ov.projectiles[0].anim_id, OSRS_COMBAT_PROJECTILE_MISSING); + ASSERT_INT_EQ("blob magic impact spotanim", + blob_magic_ov.projectiles[0].impact_gfx_id, 0); + + InfernoState blob_split_range_state = make_test_state(10, 10); + blob_split_range_state.npcs[3] = make_test_npc( + INF_NPC_BLOB_RANGE, 16, 10, INF_NPC_STATS[INF_NPC_BLOB_RANGE].size); + blob_split_range_state.npcs[3].active = 1; + blob_split_range_state.npcs[3].attacked_this_tick = 1; + blob_split_range_state.npcs[3].attack_style_this_tick = ATTACK_STYLE_RANGED; + + EncounterOverlay blob_split_range_ov; + memset(&blob_split_range_ov, 0, sizeof(blob_split_range_ov)); + inf_render_post_tick((EncounterState*)&blob_split_range_state, &blob_split_range_ov); + + ASSERT_INT_EQ("blob split ranged projectile count", + blob_split_range_ov.projectile_count, 1); + ASSERT_INT_EQ("blob split ranged projectile model", + blob_split_range_ov.projectiles[0].model_id, INF_GFX_1379_MODEL); + ASSERT_INT_EQ("blob split ranged projectile animation", + blob_split_range_ov.projectiles[0].anim_id, INF_GFX_1379_ANIM); + + InfernoState blob_split_magic_state = make_test_state(10, 10); + blob_split_magic_state.npcs[3] = make_test_npc( + INF_NPC_BLOB_MAGE, 16, 10, INF_NPC_STATS[INF_NPC_BLOB_MAGE].size); + blob_split_magic_state.npcs[3].active = 1; + blob_split_magic_state.npcs[3].attacked_this_tick = 1; + blob_split_magic_state.npcs[3].attack_style_this_tick = ATTACK_STYLE_MAGIC; + + EncounterOverlay blob_split_magic_ov; + memset(&blob_split_magic_ov, 0, sizeof(blob_split_magic_ov)); + inf_render_post_tick((EncounterState*)&blob_split_magic_state, &blob_split_magic_ov); + + ASSERT_INT_EQ("blob split magic projectile count", + blob_split_magic_ov.projectile_count, 1); + ASSERT_INT_EQ("blob split magic projectile model", + blob_split_magic_ov.projectiles[0].model_id, INF_GFX_1381_MODEL); + ASSERT_INT_EQ("blob split magic projectile animation", + blob_split_magic_ov.projectiles[0].anim_id, INF_GFX_1381_ANIM); +} + +static void test_inferno_npc_projectile_render_tracks_target_npc_slot(void) { + printf("--- inferno npc projectile render tracks target npc slot ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + state.npcs[2] = make_test_npc( + INF_NPC_MAGER, 20, 36, INF_NPC_STATS[INF_NPC_MAGER].size); + state.npcs[2].active = 1; + state.npcs[2].attacked_this_tick = 1; + state.npcs[2].attack_style_this_tick = ATTACK_STYLE_MAGIC; + state.npcs[2].attack_visual_target = 1; + + EncounterOverlay ov; + memset(&ov, 0, sizeof(ov)); + inf_render_post_tick((EncounterState*)&state, &ov); + + ASSERT_INT_EQ("shield-target projectile count", ov.projectile_count, 1); + ASSERT_INT_EQ("shield-target projectile tracks target", ov.projectiles[0].tracks_target, 1); + ASSERT_INT_EQ("shield-target projectile target kind", + ov.projectiles[0].target_kind, ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT); + ASSERT_INT_EQ("shield-target projectile target slot", ov.projectiles[0].target_npc_slot, 1); + ASSERT_INT_EQ("shield-target projectile source kind", + ov.projectiles[0].source_kind, ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT); + ASSERT_INT_EQ("shield-target projectile source slot", ov.projectiles[0].source_npc_slot, 2); +} + +static void test_inferno_zuk_projectile_render_uses_combat_visual_rows(void) { + printf("--- inferno zuk projectile render uses combat visual rows ---\n"); + + InfernoState state; + init_zuk_timing_state(&state); + state.npcs[0].attacked_this_tick = 1; + state.npcs[0].attack_style_this_tick = ATTACK_STYLE_NONE; + + EncounterOverlay ov; + memset(&ov, 0, sizeof(ov)); + inf_render_post_tick((EncounterState*)&state, &ov); + + ASSERT_INT_EQ("zuk projectile count", ov.projectile_count, 1); + ASSERT_INT_EQ("zuk projectile model", ov.projectiles[0].model_id, INF_GFX_1375_MODEL); + ASSERT_INT_EQ("zuk projectile animation", ov.projectiles[0].anim_id, INF_GFX_1375_ANIM); + ASSERT_INT_EQ("zuk projectile source kind", + ov.projectiles[0].source_kind, ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT); + ASSERT_INT_EQ("zuk projectile source slot", ov.projectiles[0].source_npc_slot, 0); + + InfernoState healer_state; + init_zuk_timing_state(&healer_state); + healer_state.npcs[2] = make_test_npc( + INF_NPC_HEALER_ZUK, 28, 49, INF_NPC_STATS[INF_NPC_HEALER_ZUK].size); + healer_state.npcs[2].active = 1; + healer_state.npcs[2].attacked_this_tick = 1; + healer_state.npcs[2].attack_style_this_tick = ATTACK_STYLE_MAGIC; + healer_state.npcs[2].attack_visual_target = 0; + + EncounterOverlay healer_ov; + memset(&healer_ov, 0, sizeof(healer_ov)); + inf_render_post_tick((EncounterState*)&healer_state, &healer_ov); + + ASSERT_INT_EQ("healer projectile count", healer_ov.projectile_count, 1); + ASSERT_INT_EQ("healer projectile model", + healer_ov.projectiles[0].model_id, INF_GFX_660_MODEL); + ASSERT_INT_EQ("healer projectile animation", + healer_ov.projectiles[0].anim_id, INF_GFX_660_ANIM); + ASSERT_INT_EQ("healer impact spotanim", healer_ov.projectiles[0].impact_gfx_id, 659); + ASSERT_INT_EQ("healer projectile source kind", + healer_ov.projectiles[0].source_kind, ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT); + ASSERT_INT_EQ("healer projectile source slot", healer_ov.projectiles[0].source_npc_slot, 2); +} + +static void test_player_projectile_render_uses_stored_reference_timing(void) { + printf("--- player projectile render uses stored reference timing ---\n"); + + InfernoState blowpipe_state = make_test_state(10, 10); + blowpipe_state.player.equipped[GEAR_SLOT_WEAPON] = ITEM_TOXIC_BLOWPIPE; + blowpipe_state.npcs[0] = make_test_npc( + INF_NPC_JAD, 18, 10, INF_NPC_STATS[INF_NPC_JAD].size); + blowpipe_state.npcs[0].active = 1; + blowpipe_state.tick_scratch.player_attacked = 1; + blowpipe_state.player_attack_npc_idx = 0; + blowpipe_state.player_attack_style_id = ATTACK_STYLE_RANGED; + blowpipe_state.player_attack_dmg = 7; + blowpipe_state.player.used_special_this_tick = 1; + + int blowpipe_dist = encounter_projectile_distance( + blowpipe_state.player.x, blowpipe_state.player.y, 1, + blowpipe_state.npcs[0].x, blowpipe_state.npcs[0].y, blowpipe_state.npcs[0].size, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + blowpipe_state.player_attack_timing = inf_player_projectile_timing( + ATTACK_STYLE_RANGED, ITEM_TOXIC_BLOWPIPE, 1, blowpipe_dist); + + EncounterOverlay blowpipe_ov; + memset(&blowpipe_ov, 0, sizeof(blowpipe_ov)); + inf_render_post_tick((EncounterState*)&blowpipe_state, &blowpipe_ov); + + ASSERT_INT_EQ("blowpipe spec projectile count", blowpipe_ov.projectile_count, 1); + ASSERT_INT_EQ("blowpipe projectile tracks target", blowpipe_ov.projectiles[0].tracks_target, 1); + ASSERT_INT_EQ("blowpipe projectile target kind", + blowpipe_ov.projectiles[0].target_kind, ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT); + ASSERT_INT_EQ("blowpipe projectile target slot", blowpipe_ov.projectiles[0].target_npc_slot, 0); + ASSERT_INT_EQ("blowpipe projectile model", + blowpipe_ov.projectiles[0].model_id, OSRS_PROJECTILE_MODEL_DRAGON_DART); + ASSERT_INT_EQ("blowpipe projectile animation", + blowpipe_ov.projectiles[0].anim_id, OSRS_PROJECTILE_ANIM_DRAGON_DART); + ASSERT_INT_EQ("blowpipe launch spotanim", + blowpipe_ov.projectiles[0].launch_gfx_id, GFX_BLOWPIPE_SPEC); + ASSERT_INT_EQ("blowpipe impact spotanim", + blowpipe_ov.projectiles[0].impact_gfx_id, 0); + ASSERT_INT_EQ("blowpipe projectile start height", + blowpipe_ov.projectiles[0].start_h, 163); + ASSERT_INT_EQ("blowpipe projectile end height", + blowpipe_ov.projectiles[0].end_h, 146); + ASSERT_INT_EQ("blowpipe spec visual start delay", + blowpipe_ov.projectiles[0].start_delay, + blowpipe_state.player_attack_timing.visual_start_delay_ticks * 30); + ASSERT_INT_EQ("blowpipe spec visual duration", + blowpipe_ov.projectiles[0].duration_ticks, + blowpipe_state.player_attack_timing.visual_duration_ticks * 30); + + InfernoState tbow_state = make_test_state(10, 10); + tbow_state.player.equipped[GEAR_SLOT_WEAPON] = ITEM_TWISTED_BOW; + tbow_state.npcs[0] = make_test_npc( + INF_NPC_JAD, 18, 10, INF_NPC_STATS[INF_NPC_JAD].size); + tbow_state.npcs[0].active = 1; + tbow_state.tick_scratch.player_attacked = 1; + tbow_state.player_attack_npc_idx = 0; + tbow_state.player_attack_style_id = ATTACK_STYLE_RANGED; + tbow_state.player_attack_dmg = 7; + + int tbow_dist = encounter_projectile_distance( + tbow_state.player.x, tbow_state.player.y, 1, + tbow_state.npcs[0].x, tbow_state.npcs[0].y, tbow_state.npcs[0].size, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + tbow_state.player_attack_timing = inf_player_projectile_timing( + ATTACK_STYLE_RANGED, ITEM_TWISTED_BOW, 0, tbow_dist); + + EncounterOverlay tbow_ov; + memset(&tbow_ov, 0, sizeof(tbow_ov)); + inf_render_post_tick((EncounterState*)&tbow_state, &tbow_ov); + + ASSERT_INT_EQ("tbow projectile count", tbow_ov.projectile_count, 1); + ASSERT_INT_EQ("tbow projectile tracks target", tbow_ov.projectiles[0].tracks_target, 1); + ASSERT_INT_EQ("tbow projectile target kind", + tbow_ov.projectiles[0].target_kind, ENCOUNTER_PROJECTILE_TARGET_NPC_SLOT); + ASSERT_INT_EQ("tbow projectile target slot", tbow_ov.projectiles[0].target_npc_slot, 0); + ASSERT_INT_EQ("tbow projectile model", + tbow_ov.projectiles[0].model_id, OSRS_PROJECTILE_MODEL_DRAGON_ARROW); + ASSERT_INT_EQ("tbow projectile animation", + tbow_ov.projectiles[0].anim_id, OSRS_PROJECTILE_ANIM_DRAGON_ARROW); + ASSERT_INT_EQ("tbow launch spotanim", + tbow_ov.projectiles[0].launch_gfx_id, GFX_DRAGON_ARROW_LAUNCH); + ASSERT_INT_EQ("tbow impact spotanim", + tbow_ov.projectiles[0].impact_gfx_id, 0); + ASSERT_INT_EQ("tbow visual start delay", + tbow_ov.projectiles[0].start_delay, + tbow_state.player_attack_timing.visual_start_delay_ticks * 30); + ASSERT_INT_EQ("tbow visual duration", + tbow_ov.projectiles[0].duration_ticks, + tbow_state.player_attack_timing.visual_duration_ticks * 30); + + InfernoState bowfa_state = make_test_state(10, 10); + bowfa_state.player.equipped[GEAR_SLOT_WEAPON] = ITEM_BOW_OF_FAERDHINEN; + bowfa_state.npcs[0] = make_test_npc( + INF_NPC_JAD, 18, 10, INF_NPC_STATS[INF_NPC_JAD].size); + bowfa_state.npcs[0].active = 1; + bowfa_state.tick_scratch.player_attacked = 1; + bowfa_state.player_attack_npc_idx = 0; + bowfa_state.player_attack_style_id = ATTACK_STYLE_RANGED; + bowfa_state.player_attack_dmg = 7; + + int bowfa_dist = encounter_projectile_distance( + bowfa_state.player.x, bowfa_state.player.y, 1, + bowfa_state.npcs[0].x, bowfa_state.npcs[0].y, bowfa_state.npcs[0].size, + ENCOUNTER_PROJECTILE_DISTANCE_CLOSEST_TILE); + bowfa_state.player_attack_timing = inf_player_projectile_timing( + ATTACK_STYLE_RANGED, ITEM_BOW_OF_FAERDHINEN, 0, bowfa_dist); + + EncounterOverlay bowfa_ov; + memset(&bowfa_ov, 0, sizeof(bowfa_ov)); + inf_render_post_tick((EncounterState*)&bowfa_state, &bowfa_ov); + + ASSERT_INT_EQ("bowfa projectile count", bowfa_ov.projectile_count, 1); + ASSERT_INT_EQ("bowfa projectile model", + bowfa_ov.projectiles[0].model_id, OSRS_PROJECTILE_MODEL_ARROW); + ASSERT_INT_EQ("bowfa launch spotanim", + bowfa_ov.projectiles[0].launch_gfx_id, GFX_RUNE_ARROW_LAUNCH); + ASSERT_INT_EQ("bowfa impact spotanim", + bowfa_ov.projectiles[0].impact_gfx_id, 0); +} + +static void test_projectile_anchor_effect_subtile_round_trips_entity_center(void) { + printf("--- projectile anchor effect subtile round-trips entity center ---\n"); + + int player_sub_x = 10 * 128 + 64; + int player_sub_y = 17 * 128 + 64; + float anchor_x = osrs_projectile_anchor_coord_from_subtile(player_sub_x); + float anchor_y = osrs_projectile_anchor_coord_from_subtile(player_sub_y); + + ASSERT_FLOAT_NEAR("projectile anchor x is tile origin", + anchor_x, 10.0f, 0.0001f); + ASSERT_FLOAT_NEAR("projectile anchor y is tile origin", + anchor_y, 17.0f, 0.0001f); + ASSERT_FLOAT_NEAR("projectile effect x returns to entity center", + osrs_projectile_subtile_from_anchor_coord(anchor_x), + (float)player_sub_x, 0.0001f); + ASSERT_FLOAT_NEAR("projectile effect y returns to entity center", + osrs_projectile_subtile_from_anchor_coord(anchor_y), + (float)player_sub_y, 0.0001f); +} + +static void test_magic_splash_landing_keeps_spell_visual_context(void) { + printf("--- magic splash landing keeps spell visual context ---\n"); + + ASSERT_INT_EQ("ice barrage visual id matches encounter spell id", + OSRS_COMBAT_VISUAL_SPELL_ICE_BARRAGE, ENCOUNTER_SPELL_ICE); + ASSERT_INT_EQ("blood barrage visual id matches encounter spell id", + OSRS_COMBAT_VISUAL_SPELL_BLOOD_BARRAGE, ENCOUNTER_SPELL_BLOOD); + + InfernoState state = make_test_state(10, 10); + state.npcs[0] = make_test_npc( + INF_NPC_RANGER, 16, 10, INF_NPC_STATS[INF_NPC_RANGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + state.npcs[0].pending_hits.hits[0].active = 1; + state.npcs[0].pending_hits.hits[0].ticks_remaining = 1; + state.npcs[0].pending_hits.hits[0].damage = 0; + state.npcs[0].pending_hits.hits[0].attack_style = ATTACK_STYLE_MAGIC; + state.npcs[0].pending_hits.hits[0].spell_type = ENCOUNTER_SPELL_BLOOD; + state.npcs[0].pending_hits.hits[0].hit_success = 0; + state.npcs[0].pending_hits.count = 1; + + inf_resolve_player_projectiles_on_npcs(&state); + + RenderEntity entities[4]; + int count = 0; + inf_fill_render_entities((EncounterState*)&state, entities, 4, &count); + + ASSERT_INT_EQ("splashing ranger still emits landed visual event", + entities[1].hit_landed_this_tick, 1); + ASSERT_INT_EQ("splashing ranger records failed accuracy", + entities[1].hit_was_successful, 0); + ASSERT_INT_EQ("splashing ranger records blood barrage spell", + entities[1].hit_spell_type, ENCOUNTER_SPELL_BLOOD); +} + +static void test_npc_overkill_hit_caps_splat_hp_and_damage_stats(void) { + printf("--- npc overkill hit caps splat hp and damage stats ---\n"); + + InfernoState state = make_test_state(10, 10); + state.npcs[0] = make_test_npc( + INF_NPC_RANGER, 16, 10, INF_NPC_STATS[INF_NPC_RANGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = 15; + state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + state.npcs[0].pending_hits.hits[0].active = 1; + state.npcs[0].pending_hits.hits[0].ticks_remaining = 1; + state.npcs[0].pending_hits.hits[0].damage = 50; + state.npcs[0].pending_hits.hits[0].attack_style = ATTACK_STYLE_RANGED; + state.npcs[0].pending_hits.hits[0].hit_success = 1; + state.npcs[0].pending_hits.count = 1; + + inf_resolve_player_projectiles_on_npcs(&state); + + RenderEntity entities[4]; + int count = 0; + inf_fill_render_entities((EncounterState*)&state, entities, 4, &count); + + ASSERT_INT_EQ("ranger hp clamps at zero", state.npcs[0].hp, 0); + ASSERT_INT_EQ("ranger hit splat caps to remaining hp", + state.npcs[0].hit_damage, 15); + ASSERT_INT_EQ("render entity hit splat caps to remaining hp", + entities[1].hit_damage, 15); + ASSERT_FLOAT_NEAR("landing does not double count XP-drop damage", + state.tick_scratch.damage_dealt, 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("landing does not double count set damage", + state.tick_scratch.damage_set, 0.0f, 1e-6f); + ASSERT_INT_EQ("overkill still counts the kill", state.tick_scratch.kill_set, 1); +} + +static void test_blood_barrage_overkill_heals_from_capped_damage(void) { + printf("--- blood barrage overkill heals from capped damage ---\n"); + + InfernoState state = make_test_state(10, 10); + state.player.base_hitpoints = 99; + state.player.current_hitpoints = 80; + state.npcs[0] = make_test_npc( + INF_NPC_RANGER, 16, 10, INF_NPC_STATS[INF_NPC_RANGER].size); + state.npcs[0].active = 1; + state.npcs[0].hp = 8; + state.npcs[0].max_hp = INF_NPC_STATS[INF_NPC_RANGER].hp; + state.npcs[0].pending_hits.hits[0].active = 1; + state.npcs[0].pending_hits.hits[0].ticks_remaining = 1; + state.npcs[0].pending_hits.hits[0].damage = 40; + state.npcs[0].pending_hits.hits[0].attack_style = ATTACK_STYLE_MAGIC; + state.npcs[0].pending_hits.hits[0].spell_type = ENCOUNTER_SPELL_BLOOD; + state.npcs[0].pending_hits.hits[0].hit_success = 1; + state.npcs[0].pending_hits.count = 1; + + inf_resolve_player_projectiles_on_npcs(&state); + + ASSERT_INT_EQ("blood barrage hit splat caps to remaining hp", + state.npcs[0].hit_damage, 8); + ASSERT_FLOAT_NEAR("blood barrage landing does not double count damage stat", + state.tick_scratch.damage_dealt, 0.0f, 1e-6f); + ASSERT_INT_EQ("blood barrage heal uses capped damage", + state.tick_scratch.blood_heal, 2); + ASSERT_INT_EQ("player receives capped blood heal", + state.player.current_hitpoints, 82); +} + +static void test_elysian_proc_propagates_to_player_render_entity(void) { + printf("--- elysian proc propagates to player render entity ---\n"); + + InfernoState state = make_test_state(10, 10); + state.player.entity_type = ENTITY_PLAYER; + state.player.base_hitpoints = 99; + state.player.current_hitpoints = 99; + state.player.equipped[GEAR_SLOT_SHIELD] = ITEM_ELYSIAN_SPIRIT_SHIELD; + state.player_pending_hits.count = 1; + state.player_pending_hits.hits[0].active = 1; + state.player_pending_hits.hits[0].ticks_remaining = 1; + state.player_pending_hits.hits[0].damage = 12; + state.player_pending_hits.hits[0].attack_style = ATTACK_STYLE_RANGED; + state.player_pending_hits.hits[0].source_npc_type = INF_NPC_RANGER; + state.player_pending_hits.hits[0].elysian_reduced = 1; + + inf_resolve_player_pending_hits(&state); + + RenderEntity entities[2]; + int count = 0; + inf_fill_render_entities((EncounterState*)&state, entities, 2, &count); + + ASSERT_INT_EQ("player render entity exists", count >= 1, 1); + ASSERT_INT_EQ("elysian proc reaches player state", + state.player.elysian_proc_this_tick, 1); + ASSERT_INT_EQ("elysian proc reaches render entity", + entities[0].elysian_proc_this_tick, 1); +} + +static void test_delayed_player_hit_records_landing_source(void) { + printf("--- delayed player hit records landing source ---\n"); + + InfernoState state = make_test_state(10, 10); + state.player.base_hitpoints = 99; + state.player.current_hitpoints = 10; + state.last_hit_by_type = INF_NPC_MAGER; + state.player_pending_hits.count = 1; + state.player_pending_hits.hits[0] = (EncounterPendingHit){ + .active = 1, + .damage = 20, + .ticks_remaining = 1, + .attack_style = ATTACK_STYLE_NONE, + .check_prayer = 0, + .source_npc_type = INF_NPC_ZUK, + }; + + inf_resolve_player_pending_hits(&state); + + ASSERT_INT_EQ("zuk hit killed player", state.player.current_hitpoints, 0); + ASSERT_INT_EQ("last hit source updated on landing", + state.last_hit_by_type, INF_NPC_ZUK); +} + +static void test_terminal_reward_uses_fixed_win_reward(void) { + printf("--- terminal reward uses fixed win reward ---\n"); + + InfernoState state = make_test_state(10, 10); + state.episode_over = 1; + state.winner = INF_OUTCOME_PLAYER_WON; + ASSERT_FLOAT_NEAR("terminal win reward is fixed", + inf_compute_reward(&state), 1.0f, 1e-6f); + + state.winner = INF_OUTCOME_PLAYER_DIED; + test_config()->death_penalty_coeff = 0.25f; + ASSERT_FLOAT_NEAR("terminal loss uses configured death penalty", + inf_compute_reward(&state), -0.25f, 1e-6f); +} + +static void test_final_wave_completion_emits_terminal_reward(void) { + printf("--- final wave completion emits terminal reward ---\n"); + + InfernoState state = make_test_state(10, 10); + state.player.base_hitpoints = 99; + state.player.current_hitpoints = 99; + state.wave = INF_NUM_WAVES - 1; + + step_inferno_noop(&state); + + ASSERT_INT_EQ("final wave completion ends episode", state.episode_over, 1); + ASSERT_INT_EQ("final wave completion marks win", state.winner, INF_OUTCOME_PLAYER_WON); + ASSERT_INT_EQ("final wave completion marks wave clear", + state.tick_scratch.wave_completed, 1); + ASSERT_FLOAT_NEAR("final wave completion emits clipped terminal reward", + state.reward, 1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("episode return includes terminal reward", + state.episode_return, 1.0f, 1e-6f); +} + +static void test_lethal_pending_hit_banks_damage_stats_before_terminal(void) { + printf("--- lethal pending hit banks damage stats before terminal ---\n"); + + InfernoState state = make_test_state(10, 10); + state.player.base_hitpoints = 99; + state.player.current_hitpoints = 10; + state.player_pending_hits.count = 1; + state.player_pending_hits.hits[0] = (EncounterPendingHit){ + .active = 1, + .damage = 20, + .ticks_remaining = 1, + .attack_style = ATTACK_STYLE_MAGIC, + .check_prayer = 0, + .source_npc_type = INF_NPC_ZUK, + }; + + step_inferno_noop(&state); + + ASSERT_INT_EQ("lethal hit ends episode", state.episode_over, 1); + ASSERT_INT_EQ("lethal hit marks loss", state.winner, INF_OUTCOME_PLAYER_DIED); + ASSERT_INT_EQ("lethal source counted", state.killed_by_type[INF_NPC_ZUK], 1); + ASSERT_FLOAT_NEAR("lethal damage is banked", + state.total_damage_received, 20.0f, 1e-6f); + ASSERT_FLOAT_NEAR("lethal tick emits zero reward", state.reward, 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("episode return matches emitted terminal reward", + state.episode_return, 0.0f, 1e-6f); +} + +static void test_terminal_penalty_applies_to_death_when_enabled(void) { + printf("--- terminal penalty applies to death when enabled ---\n"); + + InfernoState state = make_test_state(10, 10); + test_config()->terminal_penalty_enabled = 1; + state.player.base_hitpoints = 99; + state.player.current_hitpoints = 10; + state.player_pending_hits.count = 1; + state.player_pending_hits.hits[0] = (EncounterPendingHit){ + .active = 1, + .damage = 20, + .ticks_remaining = 1, + .attack_style = ATTACK_STYLE_MAGIC, + .check_prayer = 0, + .source_npc_type = INF_NPC_ZUK, + }; + + step_inferno_noop(&state); + + ASSERT_INT_EQ("lethal hit ends episode", state.episode_over, 1); + ASSERT_INT_EQ("lethal hit marks loss", state.winner, INF_OUTCOME_PLAYER_DIED); + ASSERT_FLOAT_NEAR("lethal tick emits terminal penalty", + state.reward, -1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("episode return includes terminal penalty", + state.episode_return, -1.0f, 1e-6f); +} + +static void test_timeout_reward_matches_episode_return(void) { + printf("--- timeout reward matches episode return ---\n"); + + InfernoState state = make_test_state(10, 10); + state.player.base_hitpoints = 99; + state.player.current_hitpoints = 99; + state.tick = INF_MAX_TICKS - 1; + + step_inferno_noop(&state); + + ASSERT_INT_EQ("timeout ends episode", state.episode_over, 1); + ASSERT_INT_EQ("timeout marks loss", state.winner, INF_OUTCOME_PLAYER_DIED); + ASSERT_FLOAT_NEAR("timeout emits zero reward", state.reward, 0.0f, 1e-6f); + ASSERT_FLOAT_NEAR("episode return matches timeout reward", + state.episode_return, 0.0f, 1e-6f); +} + +static void test_terminal_penalty_applies_to_timeout_when_enabled(void) { + printf("--- terminal penalty applies to timeout when enabled ---\n"); + + InfernoState state = make_test_state(10, 10); + test_config()->terminal_penalty_enabled = 1; + state.player.base_hitpoints = 99; + state.player.current_hitpoints = 99; + state.tick = INF_MAX_TICKS - 1; + + step_inferno_noop(&state); + + ASSERT_INT_EQ("timeout ends episode", state.episode_over, 1); + ASSERT_INT_EQ("timeout marks loss", state.winner, INF_OUTCOME_PLAYER_DIED); + ASSERT_FLOAT_NEAR("timeout emits terminal penalty", state.reward, -1.0f, 1e-6f); + ASSERT_FLOAT_NEAR("episode return includes terminal penalty", + state.episode_return, -1.0f, 1e-6f); +} + +static void test_inferno_render_overlay_reports_death_source(void) { + printf("--- inferno render overlay reports death source ---\n"); + + InfernoState state = make_test_state(10, 10); + state.episode_over = 1; + state.winner = INF_OUTCOME_PLAYER_DIED; + state.last_hit_by_type = INF_NPC_ZUK; + + EncounterOverlay ov; + memset(&ov, 0, sizeof(ov)); + inf_render_post_tick((EncounterState*)&state, &ov); + + ASSERT_INT_EQ("death banner active", ov.status_text_active, 1); + ASSERT_STR_EQ("death banner text", ov.status_text, "Killed by TzKal-Zuk"); +} + +static void test_inferno_binding_forwards_safe_target_reward_coeff(void) { + printf("--- inferno binding forwards safe target reward coeff ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "safe target reward coeff float key", + "ocean/osrs_inferno/osrs_inferno.h", + "float_keys[]", + "};", + "\"zuk_safe_untagged_healer_target_bonus_coeff\""); +} + +static void test_inferno_binding_forwards_healer_attack_shape_coeffs(void) { + printf("--- inferno binding forwards healer attack shape coeffs ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "non-magic healer attack bonus float key", + "ocean/osrs_inferno/osrs_inferno.h", + "float_keys[]", + "};", + "\"zuk_untagged_healer_nonmagic_attack_bonus_coeff\""); + ASSERT_SOURCE_BLOCK_CONTAINS( + "mage healer attack penalty float key", + "ocean/osrs_inferno/osrs_inferno.h", + "float_keys[]", + "};", + "\"zuk_healer_mage_attack_penalty_coeff\""); + ASSERT_SOURCE_BLOCK_CONTAINS( + "non-magic healer attack bonus config", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "zuk_untagged_healer_nonmagic_attack_bonus_coeff ="); + ASSERT_SOURCE_BLOCK_CONTAINS( + "mage healer attack penalty config", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "zuk_healer_mage_attack_penalty_coeff ="); +} + +static void test_inferno_binding_forwards_supply_milestone_rewards(void) { + printf("--- inferno binding forwards supply milestone rewards ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "supply milestone brew float key", + "ocean/osrs_inferno/osrs_inferno.h", + "float_keys[]", + "};", + "\"supply_milestone_brew_reward_coeff\""); + ASSERT_SOURCE_BLOCK_CONTAINS( + "supply milestone restore float key", + "ocean/osrs_inferno/osrs_inferno.h", + "float_keys[]", + "};", + "\"supply_milestone_restore_reward_coeff\""); + ASSERT_SOURCE_BLOCK_CONTAINS( + "supply milestone brew default off", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "supply_milestone_brew_reward_coeff = 0.0"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "supply milestone restore default off", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "supply_milestone_restore_reward_coeff = 0.0"); +} + +static void test_inferno_binding_forwards_offensive_prayer_reward(void) { + printf("--- inferno binding forwards offensive prayer reward ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "offensive prayer float key", + "ocean/osrs_inferno/osrs_inferno.h", + "float_keys[]", + "};", + "\"offensive_prayer_reward_coeff\""); + ASSERT_SOURCE_BLOCK_CONTAINS( + "offensive prayer default config", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "offensive_prayer_reward_coeff = 0.0"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "offensive prayer sweep config", + "config/osrs_inferno.ini", + "[sweep.env.offensive_prayer_reward_coeff]", + "[sweep.env.shield_penalty_coeff]", + "max = 1.0"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "offensive prayer correct metric", + "ocean/osrs_inferno/osrs_inferno.h", + "offensive_prayer_correct_rate", + "brews_remaining", + "offensive_prayer_magic_correct_rate"); +} + +static void test_inferno_binding_forwards_curriculum_supply_config(void) { + printf("--- inferno binding forwards curriculum supply config ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "curriculum supply shared jitter float key", + "ocean/osrs_inferno/osrs_inferno.h", + "float_keys[]", + "};", + "\"curriculum_supply_shared_jitter\""); + ASSERT_SOURCE_BLOCK_CONTAINS( + "curriculum supply jitter mode int key", + "ocean/osrs_inferno/osrs_inferno.h", + "int_keys[]", + "};", + "\"curriculum_supply_jitter_mode\""); + ASSERT_SOURCE_BLOCK_CONTAINS( + "curriculum no-brew mode int key", + "ocean/osrs_inferno/osrs_inferno.h", + "int_keys[]", + "};", + "\"curriculum_no_brew_mode\""); + ASSERT_SOURCE_BLOCK_CONTAINS( + "curriculum agent marker assigned by mixer", + "ocean/osrs_inferno/osrs_inferno.h", + "inferno_env_put_int(env, \"start_wave\", waves[t]);", + "return;", + "inferno_env_put_int(env, \"curriculum_agent\", 1);"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "curriculum supply defaults off", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "curriculum_supply_jitter_mode = 0"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "curriculum no-brew defaults off", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "curriculum_no_brew_frac = 0.0"); +} + +static void test_inferno_binding_forwards_post_healer_set_rewards(void) { + printf("--- inferno binding forwards post-healer set rewards ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "post-healer set damage coeff float key", + "ocean/osrs_inferno/osrs_inferno.h", + "float_keys[]", + "};", + "\"post_healer_set_damage_reward_coeff\""); + ASSERT_SOURCE_BLOCK_CONTAINS( + "post-healer set alive penalty float key", + "ocean/osrs_inferno/osrs_inferno.h", + "float_keys[]", + "};", + "\"post_healer_set_alive_tick_penalty_coeff\""); + ASSERT_SOURCE_BLOCK_CONTAINS( + "post-healer set alive penalty default off", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "post_healer_set_alive_penalty_cap = 0.0"); + ASSERT_SOURCE_BLOCK_NOT_CONTAINS( + "post-healer set alive penalty not swept", + "config/osrs_inferno.ini", + "[sweep]", + "[sweep.train.total_timesteps]", + "post_healer_set_alive_tick_penalty_coeff"); + ASSERT_SOURCE_NOT_CONTAINS( + "post-healer set alive penalty sweep section removed", + "config/osrs_inferno.ini", + "[sweep.env.post_healer_set_alive_tick_penalty_coeff]"); + ASSERT_SOURCE_NOT_CONTAINS( + "post-healer set alive cap sweep section removed", + "config/osrs_inferno.ini", + "[sweep.env.post_healer_set_alive_penalty_cap]"); +} + +static void test_inferno_binding_forwards_joseph_reward_mode(void) { + printf("--- inferno binding forwards Joseph reward mode ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "Joseph reward mode int parsed", + "ocean/osrs_inferno/osrs_inferno.h", + "void puf_init", + "inferno_apply_curriculum(env, kwargs);", + "dict_get(kwargs, \"joseph_reward_mode\")"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "Joseph reward mode config", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "joseph_reward_mode = 1"); +} + +static void test_inferno_binding_forwards_safe_healer_target_mask(void) { + printf("--- inferno binding forwards safe healer target mask ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "safe healer target mask int parsed", + "ocean/osrs_inferno/osrs_inferno.h", + "void puf_init", + "inferno_apply_curriculum(env, kwargs);", + "dict_get(kwargs, \"zuk_safe_untagged_healer_target_mask\")"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "safe healer target mask default config", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "zuk_safe_untagged_healer_target_mask = 0"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "force safe healer target mask int parsed", + "ocean/osrs_inferno/osrs_inferno.h", + "void puf_init", + "inferno_apply_curriculum(env, kwargs);", + "dict_get(kwargs, \"zuk_force_safe_untagged_healer_target_mask\")"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "force safe healer target mask default config", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "zuk_force_safe_untagged_healer_target_mask = 0"); +} + +static void test_inferno_binding_forwards_terminal_penalty_toggle(void) { + printf("--- inferno binding forwards terminal penalty toggle ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "terminal penalty int parsed", + "ocean/osrs_inferno/osrs_inferno.h", + "void puf_init", + "inferno_apply_curriculum(env, kwargs);", + "dict_get(kwargs, \"terminal_penalty_enabled\")"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "terminal penalty default config", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "terminal_penalty_enabled = 0"); + ASSERT_SOURCE_BLOCK_NOT_CONTAINS( + "terminal penalty not swept", + "config/osrs_inferno.ini", + "[sweep]", + "[sweep.train.total_timesteps]", + "terminal_penalty_enabled"); + ASSERT_SOURCE_NOT_CONTAINS( + "terminal penalty sweep section removed", + "config/osrs_inferno.ini", + "[sweep.env.terminal_penalty_enabled]"); +} + +static void test_inferno_binding_forwards_step_out_forecast_obs_toggle(void) { + printf("--- inferno binding forwards step-out forecast obs toggle ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "step-out forecast obs mode int parsed", + "ocean/osrs_inferno/osrs_inferno.h", + "void puf_init", + "inferno_apply_curriculum(env, kwargs);", + "dict_get(kwargs, \"step_out_forecast_obs_mode\")"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "step-out forecast obs mode default config", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "step_out_forecast_obs_mode = 1"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "step-out forecast obs mode sweep axis", + "config/osrs_inferno.ini", + "[sweep.env.step_out_forecast_obs_mode]", + "scale = auto", + "distribution = int_uniform"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "step-out forecast obs mode sweep covers readonly mode", + "config/osrs_inferno.ini", + "[sweep.env.step_out_forecast_obs_mode]", + "scale = auto", + "max = 3"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "step-out forecast readonly mode enum", + "ocean/osrs/encounters/inferno/encounter_inferno_model.inc", + "INF_STEP_OUT_FORECAST_MODE_OFF = 0", + "};", + "INF_STEP_OUT_FORECAST_MODE_FAST_READONLY_MOVE = 3"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "step-out forecast mode hash is source of truth", + "ocean/osrs/encounters/inferno/encounter_inferno_render_snapshot.inc", + "static uint64_t inf_config_fingerprint", + "return h;", + "INF_HASH_CONFIG_FIELD(config, &h, step_out_forecast_obs_mode)"); +} + +static void test_inferno_binding_forwards_loadout_profile_config(void) { + printf("--- inferno binding forwards loadout profile config ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "budget loadout fraction float key", + "ocean/osrs_inferno/osrs_inferno.h", + "float_keys[]", + "};", + "\"budget_loadout_fraction\""); + ASSERT_SOURCE_BLOCK_CONTAINS( + "loadout profile mode int parsed", + "ocean/osrs_inferno/osrs_inferno.h", + "void puf_init", + "inferno_apply_curriculum(env, kwargs);", + "dict_get(kwargs, \"loadout_profile_mode\")"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "loadout profile mode default config", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "loadout_profile_mode = 0"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "budget loadout fraction default config", + "config/osrs_inferno.ini", + "[env]", + "[vec]", + "budget_loadout_fraction = 0.0"); +} + +static void test_inferno_binding_logs_post_healer_set_reward_components(void) { + printf("--- inferno binding logs post-healer set reward components ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "post-healer set coeff metric", + "ocean/osrs_inferno/osrs_inferno.h", + "post_healer_set_damage_reward_coeff_normal", + "action_mask_checks_normal", + "post_healer_set_alive_penalty_coeff_normal"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "post-healer set reward component metric", + "ocean/osrs_inferno/osrs_inferno.h", + "post_healer_set_damage_reward_normal", + "action_mask_checks_normal", + "post_healer_set_alive_penalty_normal"); +} + +static void test_inferno_binding_logs_idle_diagnostics(void) { + printf("--- inferno binding logs idle diagnostics ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "attack-ready idle metric emitted", + "ocean/osrs_inferno/osrs_inferno.h", + "void puf_log", + "float wr = log->wins", + "attack_ready_no_attack_ticks"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "target-available idle metric emitted", + "ocean/osrs_inferno/osrs_inferno.h", + "void puf_log", + "float wr = log->wins", + "target_available_no_attack_ticks"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "safe opportunity idle metric emitted", + "ocean/osrs_inferno/osrs_inferno.h", + "void puf_log", + "float wr = log->wins", + "safe_attack_opportunity_missed_ticks"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "progressless metric emitted", + "ocean/osrs_inferno/osrs_inferno.h", + "void puf_log", + "float wr = log->wins", + "progressless_ticks"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "npc pressure metric emitted", + "ocean/osrs_inferno/osrs_inferno.h", + "void puf_log", + "float wr = log->wins", + "npc_pressure_this_tick_count_per_tick"); + ASSERT_SOURCE_BLOCK_CONTAINS( + "idle phase names emitted", + "ocean/osrs_inferno/osrs_inferno.h", + "inferno_log_idle_metric", + "void puf_log", + "zuk_post_healers"); +} + +static void test_inferno_log_metrics_fit_cuda_dict(void) { + printf("--- inferno log metrics fit CUDA dict ---\n"); + + int metric_count = inferno_my_log_metric_key_count(); + ASSERT_INT_LE("puf_log metric key count plus env/n", metric_count + 1, 64); +} + +static void test_curriculum_supports_wave60_bridge_tier(void) { + printf("--- curriculum supports wave60 bridge tier ---\n"); + + ASSERT_SOURCE_BLOCK_CONTAINS( + "curriculum has room for wave60 bridge", + "ocean/osrs_inferno/osrs_inferno.h", + "wave_keys[] = {", + "frac_keys[] = {", + "\"curriculum_wave_8\""); + ASSERT_SOURCE_BLOCK_CONTAINS( + "curriculum parses fifth wave key", + "ocean/osrs_inferno/osrs_inferno.h", + "wave_keys[] = {", + "frac_keys[] = {", + "\"curriculum_wave_5\""); + ASSERT_SOURCE_BLOCK_CONTAINS( + "curriculum parses fifth fraction key", + "ocean/osrs_inferno/osrs_inferno.h", + "frac_keys[] = {", + "int waves[8]", + "\"curriculum_frac_5\""); +} + +static void test_inferno_reset_uses_osrs_run_energy_units(void) { + printf("--- inferno reset uses osrs run energy units ---\n"); + + InfernoContext ctx; + InfernoState state; + inf_init_context_typed(&ctx); + inf_init_state_typed(&state, &ctx); + inf_reset_ctx((EncounterState*)&state, (EncounterContext*)&ctx, 20260519u); + + ASSERT_INT_EQ("inferno starts at full run energy", + state.player.run_energy, OSRS_RUN_ENERGY_FULL); + ASSERT_INT_EQ("inferno run energy renders as percent", + osrs_run_energy_percent(state.player.run_energy), 100); +} + +int main(void) { + inf_build_npc_stats(); + + test_attack_chase_uses_reachable_approach_tile(); + test_attack_chase_routes_around_los_blocker_while_in_range(); + test_attack_chase_routes_around_los_blocker_for_short_range(); + test_melee_fallback_geometry(); + test_style_mask_preview(); + test_style_choice_sampling(); + test_tagged_jad_healer_melee_geometry(); + test_overlap_shuffle_hold_after_recent_target_click(); + test_overlap_shuffle_respects_npc_collision_flags(); + test_large_npc_overlap_shuffle_can_partially_unclip(); + test_player_movement_ignores_npc_collision_flags(); + test_tagged_jad_healer_stops_at_melee_contact(); + test_tagged_jad_healers_queue_behind_front_healer(); + test_meleer_dig_can_stack_without_losing_collision_flag(); + test_jad_healer_spawn_offsets_match_wave_67_reference(); + test_jad_healer_spawn_offsets_match_zuk_reference(); + test_npc_terrain_blocks_full_footprint_lava_shelf(); + test_zuk_jad_healer_spawn_falls_back_to_passable_arena_tiles(); + test_meleer_dig_landing_order(); + test_final_wave_reward_applies_healer_tags_and_heal_cost(); + test_final_wave_reward_uses_zuk_low_watermark_progress(); + test_final_wave_reward_blocks_zuk_damage_while_healers_heal(); + test_final_wave_reward_pays_zuk_healer_damage(); + test_post_healer_zuk_damage_reward_is_after_clear_only(); + test_zuk_healer_phase_hp_delta_default_preserves_low_watermark(); + test_zuk_healer_phase_hp_delta_pays_healed_back_zuk_damage(); + test_zuk_healer_phase_hp_delta_avoids_double_pay_below_low_watermark(); + test_zuk_healer_phase_hp_delta_penalizes_zuk_healing_once(); + test_zuk_healer_phase_hp_delta_pays_net_same_tick_delta(); + test_zuk_healer_phase_hp_delta_keeps_non_zuk_heal_cost(); + test_post_healer_set_damage_reward_defaults_off(); + test_post_healer_set_damage_reward_pays_after_healer_clear(); + test_post_healer_set_kill_bonus_uses_existing_emitter(); + test_post_healer_set_alive_penalty_caps_per_episode(); + test_zuk_untagged_healer_tick_penalty_defaults_off(); + test_zuk_untagged_healer_tick_penalty_counts_only_untagged_zuk_healers(); + test_zuk_untagged_healer_target_bonus_defaults_off(); + test_zuk_untagged_healer_target_bonus_rewards_distinct_healers(); + test_zuk_safe_untagged_healer_target_bonus_records_safe_subset(); + test_zuk_untagged_healer_target_bonus_excludes_tagged_healers(); + test_zuk_healer_tags_first_reward_mode_blocks_pre_tag_damage(); + test_zuk_healer_tags_first_reward_mode_resumes_after_all_tags(); + test_joseph_reward_mode_pays_tags_while_healers_heal(); + test_zuk_healer_attack_shape_reward_applies_in_joseph_mode(); + test_offensive_prayer_reward_shapes_normal_and_joseph_mode(); + test_offensive_prayer_attack_events_count_real_attacks(); + test_offensive_prayer_barrage_aoe_counts_once(); + test_offensive_prayer_no_attack_no_event(); + test_offensive_prayer_melee_maps_to_piety(); + test_player_reward_damage_uses_xp_drop_tick(); + test_idle_diagnostics_count_missed_attack_opportunities(); + test_idle_diagnostics_phase_split(); + test_joseph_reward_mode_damps_healed_zuk_damage(); + test_jad_damage_reward_pauses_while_jad_healers_heal(); + test_jad_healer_damage_never_gets_damage_reward(); + test_shield_tag_reward_excludes_zuk(); + test_inferno_reset_supplies_match_current_inventory(); + test_inferno_reset_inventory_leaves_one_empty_slot(); + test_inferno_max_profile_reset_uses_existing_gear(); + test_inferno_budget_profile_reset_uses_budget_gear(); + test_inferno_mixed_profile_sampling_respects_fraction(); + test_inferno_gear_actions_map_to_active_profile_slots(); + test_inferno_reset_preserves_reward_config(); + test_supply_milestone_reward_defaults_off(); + test_supply_milestone_reward_pays_surplus_at_anchor_once(); + test_supply_milestone_reward_never_penalizes_shortage(); + test_late_start_supply_profile_anchor_waves(); + test_late_start_supply_profile_interpolation_and_scale(); + test_curriculum_supply_no_brew_is_curriculum_only(); + test_curriculum_supply_modes_gate_zuk_and_pre_zuk(); + test_curriculum_supply_jitter_clamps_to_inventory_bounds(); + test_late_start_supply_observations(); + test_dead_mob_store_eligibility(); + test_resurrected_mob_does_not_reenter_dead_store(); + test_blob_split_waits_for_death_removal(); + test_mager_resurrection_render_event_is_not_magic_projectile(); + test_double_mager_wave_resurrection_limit(); + test_pending_hit_obs_timer_prefers_prayer_window(); + test_blob_attacks_player_on_six_tick_cadence(); + test_jad_has_no_pre_fire_style_preview(); + test_jad_fire_tick_exposes_three_tick_prayer_deadline(); + test_jad_prayer_on_third_tick_blocks(); + test_jad_prayer_first_on_fourth_tick_does_not_block(); + test_jad_long_distance_damage_uses_delayed_projectile_landing(); + test_triple_jad_pending_threats_fit_obs_layout(); + test_inferno_obs_shape_includes_step_out_forecast_features(); + test_inferno_obs_wave_phase_one_hot(); + test_inferno_obs_exposes_pillar_footprint_size(); + test_inferno_obs_exposes_meleer_dig_state(); + test_npc_threat_obs_exposes_frozen_meleer_pressure(); + test_npc_threat_obs_respects_overlap_range_and_stun(); + test_npc_pressure_summary_respects_los_target_and_mixed_styles(); + test_npc_threat_obs_keeps_ranger_mager_diagonal_melee(); + test_player_attackability_obs_exposes_current_loadout_and_los(); + test_barrage_total_target_count_matches_resolution_targets(); + test_jad_special_wave_spawn_cadence_matches_reference(); + test_triple_jad_first_attacks_are_staggered(); + test_jad_melee_stays_instant_and_untelegraphed(); + test_step_out_forecast_matches_movement_head_destinations(); + test_inferno_npc_travel_uses_sw_origin_around_all_pillars(); + test_inferno_jal_npcs_use_edge_clearance_at_pillars(); + test_step_out_forecast_north_pillar_ranger_mager_order(); + test_step_out_forecast_obs_exposes_compact_action_affordance(); + test_step_out_forecast_obs_can_be_disabled(); + test_step_out_forecast_obs_uses_fast_mode(); + test_fast_step_out_forecast_matches_movement_head_destinations(); + test_fast_step_out_forecast_immediate_static_threats(); + test_fast_step_out_forecast_blob_scan_and_melee_fallback(); + test_fast_step_out_forecast_does_not_mutate_state(); + test_readonly_step_out_forecast_matches_movement_head_destinations(); + test_readonly_step_out_forecast_does_not_mutate_state(); + test_readonly_step_out_forecast_pillar_step_out_cases(); + test_step_out_forecast_obs_uses_readonly_mode(); + test_readonly_step_out_forecast_stun_countdown(); + test_readonly_step_out_forecast_frozen_can_attack(); + test_readonly_step_out_forecast_blob_scanned_fire(); + test_readonly_step_out_forecast_under_player_overlap_is_danger(); + test_readonly_step_out_forecast_invalid_movement_zeroes_payload(); + test_step_out_forecast_south_pillar_ranger_mager_order(); + test_step_out_forecast_west_pillar_ranger_mager_order(); + test_step_out_forecast_inactive_pillar_does_not_create_cover(); + test_step_out_same_tick_ranger_mager_event_logs(); + test_direct_start_waves_spawn_without_empty_gap(); + test_joseph_start_wave_70_seeds_zuk_jad_checkpoint(); + test_joseph_start_wave_71_seeds_zuk_healer_checkpoint(); + test_zuk_ready_countdown_holds_npcs_then_releases(); + test_zuk_shield_does_not_set_collision_flags(); + test_zuk_attack_delay_counts_down_while_stunned(); + test_zuk_set_timer_spawns_on_decrement_to_zero(); + test_zuk_hp_threshold_pause_happens_before_set_tick(); + test_set_attack_to_shield_is_projectile_delayed(); + test_npc_target_projectile_delays_match_reference(); + test_npc_player_projectile_delays_use_reference_options(); + test_player_projectile_timing_uses_reference_options(); + test_phantom_barrage_target_is_masked_until_cast_window(); + test_phantom_barrage_hits_aoe_on_first_cast_window(); + test_ranged_attack_cannot_fire_on_dying_target(); + test_autocast_barrage_cannot_fire_on_dying_target(); + test_manual_blood_barrage_can_heal_from_dying_primary(); + test_phantom_barrage_close_barrage_timing_cannot_recast(); + test_phantom_barrage_does_not_displace_live_obs_slots(); + test_phantom_barrage_targetability_obs_requires_ready_attack(); + test_default_autocast_casts_blood_barrage(); + test_ice_barrage_success_freezes_target_and_records_spell(); + test_inferno_barrage_primes_confliction_and_reuses_double_accuracy(); + test_barrage_accuracy_regression_against_ranger_and_mager(); + test_barrage_pending_queue_handles_slow_hit_delay(); + test_barrage_aoe_queues_hits_on_multiple_npcs(); + test_repeated_edge_barrages_kill_ranger(); + test_npc_pending_queue_lands_multiple_hits_in_order(); + test_npc_death_clears_pending_hits(); + test_lab_dump_reports_npc_pending_hit_queue(); + test_explicit_spell_cast_does_not_persist(); + test_spell_without_target_does_not_affect_later_attack(); + test_target_without_spell_uses_autocast(); + test_manual_spell_overrides_autocast(); + test_blood_barrage_at_full_hp_is_valid_and_heals_zero(); + test_manual_spell_in_range_gear_uses_range_gear_magic_stats(); + test_phantom_barrage_allows_explicit_spell_from_range_gear(); + test_zuk_obs_exposes_attack_timer_summary(); + test_zuk_obs_exposes_pending_sparks(); + test_human_blowpipe_click_chases_zuk_out_of_range(); + test_zuk_healer_blowpipe_target_chases_out_of_range(); + test_render_facing_prefers_attack_target_while_chasing(); + test_render_identity_survives_npc_death_compaction(); + test_render_identity_matches_two_players_across_tick(); + test_render_identity_two_players_claim_unique_slots(); + test_render_identity_single_player_unchanged(); + test_render_motion_speed_ladder_matches_deob(); + test_render_motion_lone_step_takes_32_client_ticks(); + test_render_motion_continuous_movement_never_pauses(); + test_render_motion_waypoint_pop_snap_and_overflow(); + test_render_motion_seed_classification_uses_explicit_teleport(); + test_render_post_tick_removed_distance_snap_branch(); + test_render_bridge_marks_genuine_teleports(); + test_entity_model_ground_lift_keeps_floor_planes_above_terrain(); + test_spotanim_lookup_prefers_recolored_model_alias(); + test_inferno_npc_spawn_id_changes_on_slot_reuse(); + test_anim_rest_pose_resets_working_vertices(); + test_zuk_healer_target_action_tags_on_landed_hit(); + test_zuk_healer_mage_attack_counts_penalty_event(); + test_zuk_safe_healer_target_mask_requires_fire_window(); + test_zuk_safe_healer_target_mask_clears_unsafe_target(); + test_zuk_force_safe_healer_target_mask_blocks_idle_when_safe(); + test_zuk_force_safe_healer_target_mask_clears_stale_target(); + test_zuk_spark_render_matches_pending_spark_state(); + test_zuk_obs_tracks_shield_and_mager_aggro(); + test_zuk_healer_obs_marks_untagged_healer_pressure(); + test_inferno_obs_target_categories_cover_boss_helpers(); + test_zuk_set_obs_los_uses_current_target(); + test_zuk_set_prayer_critical_ignores_shield_target(); + test_fail_fast_boundaries(); + test_human_target_and_potion_translation(); + test_human_targeting_refreshes_stale_obs_slots(); + test_human_spell_selection_is_client_local_until_target_click(); + test_human_walk_command_sends_no_selected_spell_cast(); + test_human_autocast_selection_persists_across_weapon_switches(); + test_autocast_is_inactive_with_non_autocast_weapon(); + test_echo_boots_recoil_reflects_to_attacker_only(); + test_redemption_action_maps_without_smite(); + test_redemption_zero_hit_landing_heals_and_drains(); + test_redemption_does_not_prevent_lethal_damage(); + test_redemption_procs_on_locked_zero_projectile_landing(); + test_human_autocast_works_with_dragon_hunter_wand(); + test_inferno_snapshot_restore_round_trip(); + test_inferno_snapshot_preserves_loadout_profile(); + test_inferno_restore_builds_npc_stats_before_late_spawn(); + test_inferno_snapshot_preserves_external_pointers(); + test_inferno_state_assignment_copy_replays_trajectory(); + test_inferno_refresh_after_state_load_rebuilds_derived_state(); + test_inferno_cell_key_is_deterministic_and_16_bytes(); + test_inferno_cell_key_quantization_groups_neighbors(); + test_inferno_cell_key_tracks_set_magers_and_jad_hp_bucket(); + test_inferno_progress_score_rewards_late_add_transitions(); + test_inferno_healer_transition_stats_track_episode_progress(); + test_inferno_human_equip_does_not_snap_loadout(); + test_jad_render_uses_style_specific_attack_animation(); + test_inferno_render_uses_npc_death_animation(); + test_jad_magic_render_emits_three_offset_projectiles(); + test_jad_ranged_render_uses_target_anchored_two_tick_visual(); + test_jad_projectile_long_distance_visual_duration_uses_reference_formula(); + test_inferno_npc_projectile_render_uses_reference_visual_timing(); + test_inferno_npc_projectile_render_tracks_target_npc_slot(); + test_inferno_zuk_projectile_render_uses_combat_visual_rows(); + test_player_projectile_render_uses_stored_reference_timing(); + test_projectile_anchor_effect_subtile_round_trips_entity_center(); + test_magic_splash_landing_keeps_spell_visual_context(); + test_npc_overkill_hit_caps_splat_hp_and_damage_stats(); + test_blood_barrage_overkill_heals_from_capped_damage(); + test_elysian_proc_propagates_to_player_render_entity(); + test_delayed_player_hit_records_landing_source(); + test_terminal_reward_uses_fixed_win_reward(); + test_final_wave_completion_emits_terminal_reward(); + test_lethal_pending_hit_banks_damage_stats_before_terminal(); + test_terminal_penalty_applies_to_death_when_enabled(); + test_timeout_reward_matches_episode_return(); + test_terminal_penalty_applies_to_timeout_when_enabled(); + test_inferno_render_overlay_reports_death_source(); + test_inferno_binding_forwards_safe_target_reward_coeff(); + test_inferno_binding_forwards_healer_attack_shape_coeffs(); + test_inferno_binding_forwards_supply_milestone_rewards(); + test_inferno_binding_forwards_offensive_prayer_reward(); + test_inferno_binding_forwards_curriculum_supply_config(); + test_inferno_binding_forwards_post_healer_set_rewards(); + test_inferno_binding_forwards_joseph_reward_mode(); + test_inferno_binding_forwards_safe_healer_target_mask(); + test_inferno_binding_forwards_terminal_penalty_toggle(); + test_inferno_binding_forwards_step_out_forecast_obs_toggle(); + test_inferno_binding_forwards_loadout_profile_config(); + test_inferno_binding_logs_post_healer_set_reward_components(); + test_inferno_binding_logs_idle_diagnostics(); + test_inferno_log_metrics_fit_cuda_dict(); + test_curriculum_supports_wave60_bridge_tier(); + test_inferno_reset_uses_osrs_run_energy_units(); + + return osrs_test_summary(); +} diff --git a/ocean/osrs/tests/test_inferno_forecast_exact.c b/ocean/osrs/tests/test_inferno_forecast_exact.c new file mode 100644 index 0000000000..b4c89e310b --- /dev/null +++ b/ocean/osrs/tests/test_inferno_forecast_exact.c @@ -0,0 +1,327 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_inferno.h" + +#define EXACT_MAGIC "INFEXACTv1" +#define EXACT_VERSION 1u +#define EXACT_CHUNK_BYTES 65536 +#define EXACT_ROLLOUT_STEPS 64 +#define EXACT_ENV_SEED 0x01FEC0DEu +#define EXACT_ACTION_SEED 0xD1B54A32D192ED03ULL + +typedef struct { + char magic[16]; + uint32_t version; + uint32_t state_size; + uint32_t forecast_size; + uint32_t forecast_obs_size; + uint32_t obs_size; + uint32_t action_features; + uint32_t record_count; +} InfExactFileHeader; + +typedef struct { + uint32_t scenario_id; + uint32_t step_index; + uint32_t tick; + uint32_t public_start_wave; + uint32_t wave; + uint32_t terminal; + uint32_t winner; + uint32_t state_size; + uint32_t obs_size; + uint32_t forecast_size; + uint64_t state_hash; + uint64_t forecast_hash; + uint64_t forecast_obs_hash; + uint64_t obs_hash; + float reward; +} InfExactRecordHeader; + +typedef struct { + FILE* file; + uint32_t record_count; +} InfExactWriter; + +static uint64_t exact_fnv_bytes(uint64_t h, const void* data, size_t size) { + const uint8_t* bytes = (const uint8_t*)data; + for (size_t i = 0; i < size; i++) { + h ^= bytes[i]; + h *= 1099511628211ULL; + } + return h; +} + +static uint64_t exact_hash_bytes(const void* data, size_t size) { + return exact_fnv_bytes(1469598103934665603ULL, data, size); +} + +static void exact_write_all(FILE* file, const void* data, size_t size) { + if (fwrite(data, 1, size, file) != size) { + perror("write inferno exact fixture"); + abort(); + } +} + +static void exact_readable_path( + char* out, + size_t out_size, + const char* dir, + const char* file_name +) { + int n = snprintf(out, out_size, "%s/%s", dir, file_name); + if (n < 0 || (size_t)n >= out_size) { + fprintf(stderr, "fixture path too long: %s/%s\n", dir, file_name); + abort(); + } +} + +static void exact_mkdir_if_needed(const char* dir) { + if (mkdir(dir, 0777) == 0) return; + if (errno == EEXIST) return; + perror("mkdir inferno exact fixture dir"); + abort(); +} + +static void exact_writer_open(InfExactWriter* writer, const char* path) { + memset(writer, 0, sizeof(*writer)); + writer->file = fopen(path, "wb"); + if (!writer->file) { + perror("open inferno exact fixture"); + abort(); + } + + InfExactFileHeader header = {0}; + memcpy(header.magic, EXACT_MAGIC, sizeof(EXACT_MAGIC)); + header.version = EXACT_VERSION; + header.state_size = (uint32_t)sizeof(InfernoState); + header.forecast_size = (uint32_t)sizeof(InfStepOutForecast); + header.forecast_obs_size = INF_STEP_OUT_FORECAST_OBS_SIZE; + header.obs_size = INF_NUM_OBS; + header.action_features = INF_STEP_OUT_FORECAST_ACTION_FEATURES; + exact_write_all(writer->file, &header, sizeof(header)); +} + +static void exact_writer_close(InfExactWriter* writer) { + InfExactFileHeader header = {0}; + memcpy(header.magic, EXACT_MAGIC, sizeof(EXACT_MAGIC)); + header.version = EXACT_VERSION; + header.state_size = (uint32_t)sizeof(InfernoState); + header.forecast_size = (uint32_t)sizeof(InfStepOutForecast); + header.forecast_obs_size = INF_STEP_OUT_FORECAST_OBS_SIZE; + header.obs_size = INF_NUM_OBS; + header.action_features = INF_STEP_OUT_FORECAST_ACTION_FEATURES; + header.record_count = writer->record_count; + if (fseek(writer->file, 0, SEEK_SET) != 0) { + perror("seek inferno exact fixture"); + abort(); + } + exact_write_all(writer->file, &header, sizeof(header)); + if (fclose(writer->file) != 0) { + perror("close inferno exact fixture"); + abort(); + } + writer->file = NULL; +} + +static void exact_capture( + InfExactWriter* writer, + uint32_t scenario_id, + uint32_t step_index, + uint32_t public_start_wave, + InfernoState* s, + InfernoContext* ctx +) { + InfStepOutForecast forecast; + float forecast_obs[INF_STEP_OUT_FORECAST_OBS_SIZE]; + float obs[INF_NUM_OBS]; + + inf_build_step_out_forecast_ctx(s, ctx, &forecast); + inf_write_obs_ctx((EncounterState*)s, (EncounterContext*)ctx, obs); + int forecast_obs_offset = + INF_PLAYER_OBS_SIZE + INF_PILLAR_OBS_SIZE + INF_TOTAL_NPC_OBS_SIZE; + memcpy(forecast_obs, &obs[forecast_obs_offset], sizeof(forecast_obs)); + + InfExactRecordHeader record = {0}; + record.scenario_id = scenario_id; + record.step_index = step_index; + record.tick = (uint32_t)s->tick; + record.public_start_wave = public_start_wave; + record.wave = (uint32_t)s->wave; + record.terminal = (uint32_t)inf_is_terminal_ctx( + (EncounterState*)s, (EncounterContext*)ctx); + record.winner = (uint32_t)s->winner; + record.state_size = (uint32_t)sizeof(*s); + record.obs_size = INF_NUM_OBS; + record.forecast_size = (uint32_t)sizeof(forecast); + record.state_hash = exact_hash_bytes(s, sizeof(*s)); + record.forecast_hash = exact_hash_bytes(&forecast, sizeof(forecast)); + record.forecast_obs_hash = exact_hash_bytes(forecast_obs, sizeof(forecast_obs)); + record.obs_hash = exact_hash_bytes(obs, sizeof(obs)); + record.reward = inf_get_reward_ctx((EncounterState*)s, (EncounterContext*)ctx); + + exact_write_all(writer->file, &record, sizeof(record)); + exact_write_all(writer->file, &forecast, sizeof(forecast)); + exact_write_all(writer->file, forecast_obs, sizeof(forecast_obs)); + exact_write_all(writer->file, obs, sizeof(obs)); + exact_write_all(writer->file, s, sizeof(*s)); + writer->record_count++; +} + +static uint64_t exact_splitmix64(uint64_t* state) { + uint64_t z = (*state += 0x9E3779B97F4A7C15ULL); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + return z ^ (z >> 31); +} + +static void exact_trace_actions( + uint64_t* rng, + int actions[INF_NUM_ACTION_HEADS] +) { + for (int head = 0; head < INF_NUM_ACTION_HEADS; head++) { + actions[head] = (int)(exact_splitmix64(rng) % + (uint64_t)INF_ACTION_DIMS[head]); + } +} + +static void exact_init_state( + InfernoState* s, + InfernoContext* ctx, + int public_start_wave, + uint32_t seed +) { + inf_init_context_typed(ctx); + memset(s, 0, sizeof(*s)); + inf_put_int_ctx( + (EncounterState*)s, + (EncounterContext*)ctx, + "start_wave", + public_start_wave); + inf_put_int_ctx( + (EncounterState*)s, + (EncounterContext*)ctx, + "step_out_forecast_obs_mode", + INF_STEP_OUT_FORECAST_MODE_EXACT_ROLLOUT); + inf_reset_ctx((EncounterState*)s, (EncounterContext*)ctx, seed); +} + +static void exact_run_wave_rollout( + InfExactWriter* writer, + uint32_t scenario_id, + int public_start_wave +) { + InfernoState s; + InfernoContext ctx; + exact_init_state(&s, &ctx, public_start_wave, EXACT_ENV_SEED); + + uint32_t capture_idx = 0; + uint64_t action_rng = EXACT_ACTION_SEED; + int actions[INF_NUM_ACTION_HEADS]; + exact_capture(writer, scenario_id, capture_idx++, public_start_wave, &s, &ctx); + for (int step = 0; step < EXACT_ROLLOUT_STEPS; step++) { + exact_trace_actions(&action_rng, actions); + inf_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + exact_capture(writer, scenario_id, capture_idx++, public_start_wave, &s, &ctx); + if (s.episode_over) break; + } +} + +static void exact_generate_fixture(const char* path) { + InfExactWriter writer; + exact_writer_open(&writer, path); + + exact_run_wave_rollout(&writer, 1u, 1); + exact_run_wave_rollout(&writer, 62u, 62); + exact_run_wave_rollout(&writer, 67u, 67); + exact_run_wave_rollout(&writer, 69u, 69); + + exact_writer_close(&writer); +} + +static int exact_compare_files(const char* expected_path, const char* actual_path) { + FILE* expected = fopen(expected_path, "rb"); + if (!expected) { + perror("open expected inferno exact fixture"); + abort(); + } + FILE* actual = fopen(actual_path, "rb"); + if (!actual) { + perror("open actual inferno exact fixture"); + abort(); + } + + uint8_t expected_buf[EXACT_CHUNK_BYTES]; + uint8_t actual_buf[EXACT_CHUNK_BYTES]; + uint64_t offset = 0; + for (;;) { + size_t ne = fread(expected_buf, 1, sizeof(expected_buf), expected); + size_t na = fread(actual_buf, 1, sizeof(actual_buf), actual); + if (ne != na) { + printf("inferno exact mismatch: size differs at byte %llu\n", + (unsigned long long)offset); + fclose(expected); + fclose(actual); + return 1; + } + if (ne == 0) break; + if (memcmp(expected_buf, actual_buf, ne) != 0) { + for (size_t i = 0; i < ne; i++) { + if (expected_buf[i] == actual_buf[i]) continue; + printf("inferno exact mismatch at byte %llu: expected %u got %u\n", + (unsigned long long)(offset + i), + (unsigned)expected_buf[i], + (unsigned)actual_buf[i]); + fclose(expected); + fclose(actual); + return 1; + } + } + offset += (uint64_t)ne; + } + + fclose(expected); + fclose(actual); + return 0; +} + +int main(int argc, char** argv) { + if (argc != 3 || + (strcmp(argv[1], "--write-golden") != 0 && + strcmp(argv[1], "--compare") != 0)) { + fprintf(stderr, + "usage: %s --write-golden DIR | --compare DIR\n", argv[0]); + return 2; + } + + inf_build_npc_stats(); + + char fixture_path[1024]; + char current_path[1024]; + exact_mkdir_if_needed(argv[2]); + exact_readable_path( + fixture_path, sizeof(fixture_path), argv[2], + "inferno_forecast_exact.bin"); + + if (strcmp(argv[1], "--write-golden") == 0) { + exact_generate_fixture(fixture_path); + printf("inferno exact golden wrote %s\n", fixture_path); + return 0; + } + + exact_readable_path( + current_path, sizeof(current_path), argv[2], + "inferno_forecast_exact.current.bin"); + exact_generate_fixture(current_path); + int failed = exact_compare_files(fixture_path, current_path); + if (failed) return 1; + printf("inferno exact golden compare PASS: %s\n", fixture_path); + return 0; +} diff --git a/ocean/osrs/tests/test_inferno_golden.c b/ocean/osrs/tests/test_inferno_golden.c new file mode 100644 index 0000000000..2a017a4550 --- /dev/null +++ b/ocean/osrs/tests/test_inferno_golden.c @@ -0,0 +1,154 @@ +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_inferno.h" + +#define FNV_OFFSET 1469598103934665603ULL +#define FNV_PRIME 1099511628211ULL + +static inline uint64_t fnv_bytes(uint64_t h, const void* p, size_t n) { + const uint8_t* b = (const uint8_t*)p; + for (size_t i = 0; i < n; i++) { + h ^= b[i]; + h *= FNV_PRIME; + } + return h; +} + +static inline uint64_t fnv_f32(uint64_t h, float v) { + uint32_t bits; + memcpy(&bits, &v, sizeof(bits)); + return fnv_bytes(h, &bits, sizeof(bits)); +} + +static inline uint64_t fnv_i32(uint64_t h, int v) { + int32_t w = (int32_t)v; + return fnv_bytes(h, &w, sizeof(w)); +} + +static inline uint64_t splitmix64(uint64_t* s) { + uint64_t z = (*s += 0x9E3779B97F4A7C15ULL); + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + return z ^ (z >> 31); +} + +static uint64_t run_episode(int start_wave, uint32_t seed, int max_ticks) { + EncounterState* state = inf_create(); + inf_put_int(state, "start_wave", start_wave); + inf_reset(state, seed); + + InfernoState* s = (InfernoState*)state; + + static float obs[INF_NUM_OBS]; + int actions[INF_NUM_ACTION_HEADS]; + + uint64_t arng = ((uint64_t)seed << 20) ^ (uint64_t)(start_wave + 1) ^ 0xD1B54A32D192ED03ULL; + + uint64_t h = FNV_OFFSET; + + for (int t = 0; t < max_ticks; t++) { + for (int head = 0; head < INF_NUM_ACTION_HEADS; head++) { + int dim = INF_ACTION_DIMS[head]; + actions[head] = (int)(splitmix64(&arng) % (uint64_t)dim); + } + + inf_step(state, actions); + inf_write_obs(state, obs); + + for (int i = 0; i < INF_NUM_OBS; i++) { + h = fnv_f32(h, obs[i]); + } + h = fnv_f32(h, s->reward); + h = fnv_i32(h, s->wave); + h = fnv_i32(h, s->tick); + h = fnv_i32(h, s->episode_over); + h = fnv_i32(h, s->winner); + + if (s->episode_over) break; + } + + inf_destroy(state); + return h; +} + +typedef struct { + const char* name; + int start_wave; + uint32_t seed; +} GoldenConfig; + +static const GoldenConfig CONFIGS[] = { + { "wave1_a", 1, 0x0000001u }, + { "wave1_b", 1, 0x0BADF00Du }, + { "wave1_c", 1, 0x1234567u }, + { "meleer_a", 9, 0x0000001u }, + { "meleer_b", 9, 0x0BADF00Du }, + { "ranger_a", 18, 0x0000001u }, + { "ranger_b", 18, 0x0BADF00Du }, + { "mager_a", 35, 0x0000001u }, + { "mager_b", 35, 0x0BADF00Du }, + { "jad_a", 67, 0x0000001u }, + { "jad_b", 67, 0x0BADF00Du }, + { "jad_c", 67, 0x1234567u }, + { "zuk_a", 69, 0x0000001u }, + { "zuk_b", 69, 0x0BADF00Du }, + { "zuk_c", 69, 0x1234567u }, +}; + +#define NUM_CONFIGS ((int)(sizeof(CONFIGS) / sizeof(CONFIGS[0]))) +#define EPISODE_TICKS 2000 + +static const uint64_t BASELINE[NUM_CONFIGS] = { + + 0x9d8970300cea947aULL, + 0xefeefc062898de1bULL, + 0x300b40b9b6c32f47ULL, + 0xf600c7a9f79479faULL, + 0x267ab0fac9ad5b27ULL, + 0x999a41e1a0916ab9ULL, + 0x2ddb91b645db1e75ULL, + 0xd2a1416c4b53157fULL, + 0x84a2ab3540f37ac8ULL, + 0xb98928c437e48005ULL, + 0x3e5a885e173d4674ULL, + 0xd6577872951242fdULL, + 0xc5af52a73611f3f6ULL, + 0x7a84c874f2b7c5ceULL, + 0x28c4ccc92a588192ULL, +}; + +int main(int argc, char** argv) { + int print_mode = (argc > 1 && strcmp(argv[1], "--print") == 0); + + inf_build_npc_stats(); + + printf("inferno golden-master (%d configs, <=%d ticks each)\n\n", + NUM_CONFIGS, EPISODE_TICKS); + + int failed = 0; + for (int c = 0; c < NUM_CONFIGS; c++) { + uint64_t h = run_episode(CONFIGS[c].start_wave, CONFIGS[c].seed, EPISODE_TICKS); + if (print_mode) { + printf(" 0x%016llxULL, /* %s */\n", (unsigned long long)h, CONFIGS[c].name); + } else { + int ok = (h == BASELINE[c]); + printf(" %-12s 0x%016llx %s\n", CONFIGS[c].name, + (unsigned long long)h, ok ? "PASS" : "FAIL"); + if (!ok) { + printf(" expected 0x%016llx\n", + (unsigned long long)BASELINE[c]); + failed++; + } + } + } + + if (print_mode) { + printf("\npaste the array above into BASELINE[].\n"); + return 0; + } + + printf("\n%d/%d configs match baseline\n", NUM_CONFIGS - failed, NUM_CONFIGS); + return failed > 0 ? 1 : 0; +} diff --git a/ocean/osrs/tests/test_inferno_lab.c b/ocean/osrs/tests/test_inferno_lab.c new file mode 100644 index 0000000000..f6ac5ba3ed --- /dev/null +++ b/ocean/osrs/tests/test_inferno_lab.c @@ -0,0 +1,274 @@ +#include +#include +#include + +#include "ocean/osrs/encounters/encounter_inferno.h" + +#include "ocean/osrs/tests/osrs_test_check.h" + +#define ASSERT_CONTAINS(label, haystack, needle) do { \ + tests_run++; \ + if (strstr((haystack), (needle)) != NULL) { \ + tests_passed++; \ + } else { \ + tests_failed++; \ + printf(" FAIL: %s - missing %s\n", (label), (needle)); \ + } \ +} while (0) + +static InfernoState* make_lab_state(void) { + InfernoState* state = (InfernoState*)inf_create(); + inf_put_float((EncounterState*)state, "late_start_supply_profile_scale", 1.0f); + inf_reset((EncounterState*)state, 20260515u); + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_CLEAR_NPCS, + }); + return state; +} + +static void test_lab_typed_commands_mutate_state(void) { + printf("--- inferno lab typed commands mutate state ---\n"); + + InfernoState* state = make_lab_state(); + + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SET_PLAYER, + .as.tile = { .x = 29, .y = 39 }, + }); + ASSERT_INT_EQ("player x", state->player.x, 29); + ASSERT_INT_EQ("player y", state->player.y, 39); + + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SPAWN_NPC, + .as.spawn_npc = { + .slot = 0, + .type = INF_NPC_RANGER, + .x = 24, + .y = 31, + .hp = { .kind = INF_LAB_OPTIONAL_INT_UNSET }, + .timer = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = 3 }, + }, + }); + ASSERT_INT_EQ("ranger active", state->npcs[0].active, 1); + ASSERT_INT_EQ("ranger type", state->npcs[0].type, INF_NPC_RANGER); + ASSERT_INT_EQ("ranger timer", state->npcs[0].attack_timer, 3); + + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_MOVE_NPC, + .as.move_npc = { .slot = 0, .x = 20, .y = 32 }, + }); + ASSERT_INT_EQ("ranger moved x", state->npcs[0].x, 20); + ASSERT_INT_EQ("ranger moved y", state->npcs[0].y, 32); + + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SET_NPC_HP, + .as.npc_hp = { .slot = 0, .hp = 7 }, + }); + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SET_NPC_TIMER, + .as.npc_timer = { .slot = 0, .timer = 0 }, + }); + ASSERT_INT_EQ("ranger hp", state->npcs[0].hp, 7); + ASSERT_INT_EQ("ranger timer zero", state->npcs[0].attack_timer, 0); + + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SET_PILLAR, + .as.pillar = { + .pillar_idx = 2, + .state = INF_LAB_PILLAR_REMOVED, + .hp = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = 0 }, + }, + }); + ASSERT_INT_EQ("north pillar inactive", state->pillars[2].active, 0); + ASSERT_INT_EQ("north pillar removed from LOS blockers", state->los_blocker_count, 2); + + inf_destroy((EncounterState*)state); +} + +static void add_script_line(InfernoState* state, const char* line) { + InfLabLineResult result = inf_lab_apply_script_line(state, line); + ASSERT_INT_EQ("script line does not dump", result, INF_LAB_LINE_NONE); +} + +static void setup_north_pillar_stack_from_script(InfernoState* state) { + add_script_line(state, "clear_npcs"); + add_script_line(state, "player x=29 y=39"); + add_script_line(state, "pillar idx=0 active=1 hp=255"); + add_script_line(state, "pillar idx=1 active=1 hp=255"); + add_script_line(state, "pillar idx=2 active=1 hp=255"); + add_script_line(state, "npc slot=0 type=ranger x=24 y=31 hp=full timer=0"); + add_script_line(state, "npc slot=1 type=mager x=29 y=30 hp=full timer=0"); +} + +static void test_lab_script_reaches_exact_forecast(void) { + printf("--- inferno lab script reaches exact forecast ---\n"); + + InfernoState* state = make_lab_state(); + setup_north_pillar_stack_from_script(state); + + InfLabLineResult result = inf_lab_apply_script_line(state, "forecast"); + ASSERT_INT_EQ("forecast line result", result, INF_LAB_LINE_FORECAST); + + InfStepOutForecast forecast; + inf_build_step_out_forecast(state, &forecast); + const InfStepOutForecastAction* run_west = &forecast.actions[11]; + ASSERT_INT_EQ("run west valid", run_west->valid, 1); + ASSERT_INT_EQ("run west land x", run_west->land_x, 27); + ASSERT_INT_EQ("run west land y", run_west->land_y, 39); + ASSERT_INT_EQ("ranger fires first", run_west->ticks[0].ranger_count, 1); + ASSERT_INT_EQ("mager fires second", run_west->ticks[1].mager_count, 1); + ASSERT_INT_EQ("offtick opportunity", run_west->ranger_mager_offtick_opportunity, 1); + ASSERT_INT_EQ("no same tick conflict", run_west->same_tick_mixed_style_conflict, 0); + + inf_destroy((EncounterState*)state); +} + +static void test_lab_json_contains_state_and_forecast(void) { + printf("--- inferno lab json contains state and forecast ---\n"); + + InfernoState* state = make_lab_state(); + setup_north_pillar_stack_from_script(state); + + char* json = inf_lab_alloc_json(state); + ASSERT_CONTAINS("json has player", json, "\"player\":{\"x\":29,\"y\":39"); + ASSERT_CONTAINS("json has ranger", json, "\"type\":\"ranger\""); + ASSERT_CONTAINS("json has mager", json, "\"type\":\"mager\""); + ASSERT_CONTAINS("json has forecast", json, "\"forecast\""); + ASSERT_CONTAINS("json has rich ticks", json, "\"ticks\":["); + ASSERT_CONTAINS("json has off-tick affordance", json, "\"ranger_mager_offtick\":1"); + free(json); + + char* dump = NULL; + InfLabLineResult result = inf_lab_apply_script_line_alloc_json( + state, "dump", &dump); + ASSERT_INT_EQ("dump line result", result, INF_LAB_LINE_DUMP); + ASSERT_CONTAINS("dump has action array", dump, "\"actions\":["); + free(dump); + + inf_destroy((EncounterState*)state); +} + +static void test_lab_spawn_wave_and_delete(void) { + printf("--- inferno lab spawn wave and delete ---\n"); + + InfernoState* state = make_lab_state(); + + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SPAWN_WAVE, + .as.wave = { .wave = 60 }, + }); + ASSERT_INT_EQ("wave 60 internal index", state->wave, 59); + + int active_count = 0; + for (int i = 0; i < INF_MAX_NPCS; i++) + if (state->npcs[i].active) active_count++; + ASSERT_INT_EQ("wave 60 active NPC count", active_count, 7); + + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_DELETE_NPC, + .as.npc_slot = { .slot = 0 }, + }); + ASSERT_INT_EQ("slot deleted", state->npcs[0].active, 0); + + inf_destroy((EncounterState*)state); +} + +static void test_lab_snapshot_restore_round_trip(void) { + printf("--- inferno lab snapshot restore round trip ---\n"); + + InfernoState* state = make_lab_state(); + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SET_PLAYER, + .as.tile = { .x = 29, .y = 39 }, + }); + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SPAWN_NPC, + .as.spawn_npc = { + .slot = 3, + .type = INF_NPC_MAGER, + .x = 27, + .y = 32, + .hp = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = 99 }, + .timer = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = 2 }, + }, + }); + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SET_PILLAR, + .as.pillar = { + .pillar_idx = 1, + .state = INF_LAB_PILLAR_REMOVED, + .hp = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = 0 }, + }, + }); + state->wave = 11; + state->tick = 321; + state->rng_state = 0x1234abcd; + state->player.brew_doses = 7; + state->player.restore_doses = 11; + state->player_pending_hits.count = 2; + + size_t snapshot_size = ENCOUNTER_INFERNO.snapshot_size( + (EncounterState*)state, (EncounterContext*)inf_legacy_context()); + ASSERT_INT_EQ("snapshot size", (int)snapshot_size, (int)sizeof(InfSnapshot)); + InfSnapshot* snapshot = (InfSnapshot*)malloc(snapshot_size); + ENCOUNTER_INFERNO.snapshot( + (EncounterState*)state, + (EncounterContext*)inf_legacy_context(), + snapshot); + + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SET_PLAYER, + .as.tile = { .x = 18, .y = 18 }, + }); + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_CLEAR_NPCS, + }); + inf_lab_apply_command(state, &(InfernoLabCommand){ + .kind = INF_LAB_COMMAND_SET_PILLAR, + .as.pillar = { + .pillar_idx = 1, + .state = INF_LAB_PILLAR_ACTIVE, + .hp = { .kind = INF_LAB_OPTIONAL_INT_SET, .value = INF_PILLAR_HP }, + }, + }); + state->wave = 60; + state->tick = 999; + state->rng_state = 7; + state->player.brew_doses = 0; + state->player.restore_doses = 0; + encounter_pending_hit_queue_clear(&state->player_pending_hits); + + ENCOUNTER_INFERNO.restore( + (EncounterState*)state, + (EncounterContext*)inf_legacy_context(), + snapshot, + snapshot_size); + + ASSERT_INT_EQ("restored player x", state->player.x, 29); + ASSERT_INT_EQ("restored player y", state->player.y, 39); + ASSERT_INT_EQ("restored mager active", state->npcs[3].active, 1); + ASSERT_INT_EQ("restored mager type", state->npcs[3].type, INF_NPC_MAGER); + ASSERT_INT_EQ("restored mager hp", state->npcs[3].hp, 99); + ASSERT_INT_EQ("restored mager timer", state->npcs[3].attack_timer, 2); + ASSERT_INT_EQ("restored west pillar inactive", state->pillars[1].active, 0); + ASSERT_INT_EQ("restored LOS blockers", state->los_blocker_count, 2); + ASSERT_INT_EQ("restored wave", state->wave, 11); + ASSERT_INT_EQ("restored tick", state->tick, 321); + ASSERT_INT_EQ("restored rng", (int)state->rng_state, (int)0x1234abcd); + ASSERT_INT_EQ("restored brews", state->player.brew_doses, 7); + ASSERT_INT_EQ("restored restores", state->player.restore_doses, 11); + ASSERT_INT_EQ("restored pending hits", state->player_pending_hits.count, 2); + + free(snapshot); + inf_destroy((EncounterState*)state); +} + +int main(void) { + test_lab_typed_commands_mutate_state(); + test_lab_script_reaches_exact_forecast(); + test_lab_json_contains_state_and_forecast(); + test_lab_spawn_wave_and_delete(); + test_lab_snapshot_restore_round_trip(); + + return osrs_test_summary(); +} diff --git a/ocean/osrs/tests/test_inferno_replay_best.c b/ocean/osrs/tests/test_inferno_replay_best.c new file mode 100644 index 0000000000..4ad363c23c --- /dev/null +++ b/ocean/osrs/tests/test_inferno_replay_best.c @@ -0,0 +1,55 @@ +#include +#include + +#include "ocean/osrs_inferno/replay_best.h" + +static int tests_passed = 0; +static int tests_failed = 0; + +#define ASSERT(cond) do { \ + if (!(cond)) { \ + printf("FAIL at %s:%d: %s\n", __FILE__, __LINE__, #cond); \ + tests_failed++; \ + return; \ + } \ +} while(0) + +#define RUN(name) do { \ + printf(" %-50s", #name); \ + name(); \ + printf("PASS\n"); \ + tests_passed++; \ +} while(0) + +static void test_full_run_prefers_higher_wave_then_fewer_ticks_then_lower_seed(void) { + InfernoReplayBest best = inferno_replay_best_initial(); + ASSERT(inferno_replay_is_better(&best, 0, 10, 500, 1200, 9)); + inferno_replay_best_apply(&best, 10, 500, 1200, 9); + + ASSERT(inferno_replay_is_better(&best, 0, 11, 900, 1200, 8)); + ASSERT(inferno_replay_is_better(&best, 0, 10, 400, 1200, 8)); + ASSERT(inferno_replay_is_better(&best, 0, 10, 500, 1200, 8)); + ASSERT(!inferno_replay_is_better(&best, 0, 10, 500, 1200, 10)); +} + +static void test_partial_run_prefers_lower_zuk_hp_then_kill_ticks_then_lower_seed(void) { + InfernoReplayBest best = inferno_replay_best_initial(); + inferno_replay_best_apply(&best, 68, 700, 300, 9); + + ASSERT(inferno_replay_is_better(&best, 68, 68, 900, 200, 8)); + ASSERT(inferno_replay_is_better(&best, 68, 68, 900, 300, 8)); + ASSERT(!inferno_replay_is_better(&best, 68, 68, 900, 600, 8)); + + inferno_replay_best_apply(&best, 68, 700, 0, 9); + ASSERT(inferno_replay_is_better(&best, 68, 68, 600, 0, 10)); + ASSERT(inferno_replay_is_better(&best, 68, 68, 700, 0, 8)); + ASSERT(!inferno_replay_is_better(&best, 68, 68, 700, 0, 10)); +} + +int main(void) { + printf("inferno replay best tests\n\n"); + RUN(test_full_run_prefers_higher_wave_then_fewer_ticks_then_lower_seed); + RUN(test_partial_run_prefers_lower_zuk_hp_then_kill_ticks_then_lower_seed); + printf("%d passed, %d failed\n", tests_passed, tests_failed); + return tests_failed > 0 ? 1 : 0; +} diff --git a/ocean/osrs/tests/test_osrs_combat_rolls.c b/ocean/osrs/tests/test_osrs_combat_rolls.c new file mode 100644 index 0000000000..5d161edf13 --- /dev/null +++ b/ocean/osrs/tests/test_osrs_combat_rolls.c @@ -0,0 +1,45 @@ +#include +#include + +#include "../osrs_combat.h" + +static void test_hit_chance_fraction_matches_osrs_formula(void) { + uint64_t num, den; + + osrs_hit_chance_fraction(100, 200, &num, &den); + assert(num == 100); + assert(den == 402); + + osrs_hit_chance_fraction(250, 100, &num, &den); + assert(num == 400); + assert(den == 502); +} + +static void test_double_hit_chance_fraction_matches_closed_form(void) { + uint64_t num, den; + + osrs_hit_chance_double_fraction(100, 200, &num, &den); + assert(num == 100ull * 405ull); + assert(den == 6ull * 101ull * 201ull); + + osrs_hit_chance_double_fraction(250, 100, &num, &den); + assert(num == 6ull * 251ull * 251ull - 102ull * 203ull); + assert(den == 6ull * 251ull * 251ull); +} + +static void test_roll_ratio_consumes_rng_for_certain_outcomes(void) { + uint32_t rng_zero = 12345; + uint32_t rng_one = 12345; + + assert(encounter_roll_ratio_u16(&rng_zero, 0, 7) == 0); + assert(encounter_roll_ratio_u16(&rng_one, 7, 7) == 1); + assert(rng_zero == rng_one); + assert(rng_zero != 12345); +} + +int main(void) { + test_hit_chance_fraction_matches_osrs_formula(); + test_double_hit_chance_fraction_matches_closed_form(); + test_roll_ratio_consumes_rng_for_certain_outcomes(); + return 0; +} diff --git a/ocean/osrs/tests/test_osrs_inventory_clicks.c b/ocean/osrs/tests/test_osrs_inventory_clicks.c new file mode 100644 index 0000000000..7df12bb975 --- /dev/null +++ b/ocean/osrs/tests/test_osrs_inventory_clicks.c @@ -0,0 +1,349 @@ +#include +#include + +#include "ocean/osrs/osrs_inventory_clicks.h" + +#define CHECK(label, cond) do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s\n", label); \ + return 1; \ + } \ +} while (0) + +static int test_item_index_classification(void) { + CHECK("venator bow item index equips", + osrs_item_click_action(ITEM_VENATOR_BOW) == OSRS_CLICK_EQUIP); + CHECK("empty item index noops", + osrs_item_click_action(ITEM_NONE) == OSRS_CLICK_NONE); + CHECK("gear item has no consumable tag", + osrs_item_click_consumable_kind(ITEM_VENATOR_BOW) == + OSRS_CONSUMABLE_NONE); + return 0; +} + +static int test_raw_consumable_classification(void) { + OsrsConsumableClick brew = + osrs_consumable_click_lookup_raw_osrs_id(6685); + CHECK("sara brew drinks", brew.click_action == OSRS_CLICK_DRINK); + CHECK("sara brew kind", brew.consumable_kind == OSRS_CONSUMABLE_BREW); + CHECK("sara brew four-dose count", brew.dose_count == 4); + + OsrsConsumableClick shark = + osrs_consumable_click_lookup_raw_osrs_id(385); + CHECK("shark eats", shark.click_action == OSRS_CLICK_EAT); + CHECK("shark food kind", shark.consumable_kind == OSRS_CONSUMABLE_SHARK_FOOD); + CHECK("shark has no dose", shark.dose_count == 0); + + OsrsConsumableClick unknown = + osrs_consumable_click_lookup_raw_osrs_id(9999); + CHECK("unknown raw id noops", unknown.click_action == OSRS_CLICK_NONE); + CHECK("unknown raw id has no kind", + unknown.consumable_kind == OSRS_CONSUMABLE_NONE); + CHECK("unknown raw id has no dose", unknown.dose_count == 0); + return 0; +} + +static int test_sara_brew_dose_variants(void) { + OsrsConsumableClick brew4 = + osrs_consumable_click_lookup_raw_osrs_id(6685); + OsrsConsumableClick brew3 = + osrs_consumable_click_lookup_raw_osrs_id(6687); + OsrsConsumableClick brew2 = + osrs_consumable_click_lookup_raw_osrs_id(6689); + OsrsConsumableClick brew1 = + osrs_consumable_click_lookup_raw_osrs_id(6691); + + CHECK("6685 is four-dose", brew4.dose_count == 4); + CHECK("6687 is three-dose", brew3.dose_count == 3); + CHECK("6689 is two-dose", brew2.dose_count == 2); + CHECK("6691 is one-dose", brew1.dose_count == 1); + return 0; +} + +static int test_dose_after_drink(void) { + CHECK("four-dose count decrements to three", + osrs_consumable_dose_count_after_drink(4) == 3); + CHECK("one-dose count decrements to empty", + osrs_consumable_dose_count_after_drink(1) == 0); + CHECK("6685 decrements to 6687", + osrs_consumable_raw_osrs_id_after_drink(6685) == 6687); + CHECK("6687 decrements to 6689", + osrs_consumable_raw_osrs_id_after_drink(6687) == 6689); + CHECK("6691 decrements to empty", + osrs_consumable_raw_osrs_id_after_drink(6691) == 0); + CHECK("guthix rest uses odd id family", + osrs_consumable_raw_osrs_id_after_drink(4417) == 4419); + return 0; +} + +static int test_pure_click_interpreter(void) { + OsrsInventoryClickResolution gear = osrs_inventory_click_interpret( + ITEM_VENATOR_BOW, + 27610, + OSRS_CLICK_TICK_FIRST + ); + CHECK("gear click resolves equip", gear.click_action == OSRS_CLICK_EQUIP); + CHECK("gear click has no consumable kind", + gear.consumable_kind == OSRS_CONSUMABLE_NONE); + + OsrsInventoryClickResolution brew = osrs_inventory_click_interpret( + ITEM_NONE, + 6685, + OSRS_CLICK_TICK_FIRST + ); + CHECK("brew click resolves drink", brew.click_action == OSRS_CLICK_DRINK); + CHECK("brew interpreter kind", brew.consumable_kind == OSRS_CONSUMABLE_BREW); + CHECK("brew interpreter dose", brew.dose_count == 4); + CHECK("brew interpreter next raw id", brew.raw_osrs_id_after_drink == 6687); + + OsrsInventoryClickResolution shark = osrs_inventory_click_interpret( + ITEM_NONE, + 385, + OSRS_CLICK_TICK_FIRST + ); + CHECK("shark click resolves eat", shark.click_action == OSRS_CLICK_EAT); + CHECK("shark interpreter kind", + shark.consumable_kind == OSRS_CONSUMABLE_SHARK_FOOD); + + OsrsInventoryClickResolution duplicate = osrs_inventory_click_interpret( + ITEM_NONE, + 6685, + OSRS_CLICK_TICK_DUPLICATE + ); + CHECK("duplicate click noops", duplicate.click_action == OSRS_CLICK_NONE); + return 0; +} + +static int test_cell_click_attributes_to_slot_item(void) { + OsrsInventoryCell cells[OSRS_INVENTORY_SIZE]; + for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) + cells[i] = osrs_inventory_cell_empty(); + cells[3] = osrs_inventory_cell_from_raw_osrs_id(3024); + cells[4] = osrs_inventory_cell_from_item(ITEM_TWISTED_BOW); + + OsrsInventorySlotSnapshot snapshot = osrs_inventory_slot_snapshot(cells); + OsrsInventoryClickResolution restore = + osrs_inventory_snapshot_click_interpret( + &snapshot, 3, OSRS_CLICK_TICK_FIRST); + OsrsInventoryClickResolution bow = + osrs_inventory_snapshot_click_interpret( + &snapshot, 4, OSRS_CLICK_TICK_FIRST); + + CHECK("slot 3 resolves restore, not adjacent bow", + restore.click_action == OSRS_CLICK_DRINK && + restore.consumable_kind == OSRS_CONSUMABLE_SUPER_RESTORE); + CHECK("slot 4 resolves bow equip, not adjacent restore", + bow.click_action == OSRS_CLICK_EQUIP); + return 0; +} + +static int test_cell_drink_decrements_one_dose(void) { + OsrsInventoryCell restore = osrs_inventory_cell_from_raw_osrs_id(3024); + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_interpret(&restore, OSRS_CLICK_TICK_FIRST); + osrs_inventory_cell_decrement_drink(&restore, resolution); + + CHECK("restore one drink leaves three doses", restore.dose == 3); + CHECK("restore one drink updates raw id to three-dose", + restore.raw_osrs_id == 3026); + return 0; +} + +typedef struct { + int calls; + OsrsConsumableKind kind; +} TestDrinkEffect; + +static void test_record_drink_effect(void* ctx, OsrsConsumableKind kind) { + TestDrinkEffect* effect = (TestDrinkEffect*)ctx; + effect->calls++; + effect->kind = kind; +} + +static int test_shared_drink_consume_owns_timer_and_one_dose(void) { + OsrsInventoryCell restore = osrs_inventory_cell_from_raw_osrs_id(3024); + OsrsInventoryClickResolution resolution = + osrs_inventory_cell_click_interpret(&restore, OSRS_CLICK_TICK_FIRST); + int potion_timer = 0; + TestDrinkEffect effect = {0}; + OsrsInventoryDrinkConsumeResult first = + osrs_inventory_cell_consume_drink_one_dose( + &restore, + resolution, + &potion_timer, + test_record_drink_effect, + &effect); + + CHECK("shared consume accepts first drink", first.consumed == 1); + CHECK("shared consume decrements one dose", + restore.dose == 3 && restore.raw_osrs_id == 3026); + CHECK("shared consume arms potion timer", potion_timer == 3); + CHECK("shared consume applies one effect", + effect.calls == 1 && effect.kind == OSRS_CONSUMABLE_SUPER_RESTORE); + + resolution = + osrs_inventory_cell_click_interpret(&restore, OSRS_CLICK_TICK_FIRST); + OsrsInventoryDrinkConsumeResult gated = + osrs_inventory_cell_consume_drink_one_dose( + &restore, + resolution, + &potion_timer, + test_record_drink_effect, + &effect); + CHECK("shared consume blocks live timer", gated.consumed == 0); + CHECK("timer-gated shared consume leaves cell intact", + restore.dose == 3 && restore.raw_osrs_id == 3026); + CHECK("timer-gated shared consume skips effect", effect.calls == 1); + return 0; +} + +static int test_cell_rearrange_swaps_two_slots(void) { + OsrsInventoryCell cells[OSRS_INVENTORY_SIZE]; + for (int i = 0; i < OSRS_INVENTORY_SIZE; i++) + cells[i] = osrs_inventory_cell_empty(); + cells[1] = osrs_inventory_cell_from_item(ITEM_TWISTED_BOW); + cells[9] = osrs_inventory_cell_from_raw_osrs_id(6685); + + osrs_inventory_swap_cells(cells, 1, 9); + + CHECK("swap moves bow to target slot", + cells[9].item_idx == ITEM_TWISTED_BOW); + CHECK("swap moves brew to source slot", + cells[1].raw_osrs_id == 6685 && cells[1].dose == 4); + return 0; +} + +static int cell_in_unit_range(const float* out) { + for (int i = 0; i < OSRS_INVENTORY_CELL_OBS_FEATURES; i++) { + if (out[i] < -1.0f || out[i] > 1.0f) return 0; + } + return 1; +} + +static int test_enriched_feature_counts(void) { + CHECK("cell obs features is 28", OSRS_INVENTORY_CELL_OBS_FEATURES == 28); + CHECK("equipped obs features is 18", OSRS_EQUIPPED_SELF_OBS_FEATURES == 18); + return 0; +} + +static int test_brew_cell_semantics(void) { + float out[OSRS_INVENTORY_CELL_OBS_FEATURES]; + float zero_deltas[6] = {0}; + + osrs_write_inventory_cell_affordance_features( + out, ITEM_NONE, 6685, 4, 0, zero_deltas, 99, 99, 99); + CHECK("brew is not armor", out[12] == 0.0f); + CHECK("brew is not weapon", out[13] == 0.0f); + CHECK("brew kind is brew", out[14] == 1.0f); + + CHECK("brew hp-heal norm ~= 16/99", + fabsf(out[19] - (16.0f / 99.0f)) < 1e-4f); + CHECK("brew has no prayer restore", out[20] == 0.0f); + CHECK("brew weapon speed/range are zero", out[26] == 0.0f && out[27] == 0.0f); + CHECK("brew cell within [-1,1]", cell_in_unit_range(out)); + return 0; +} + +static int test_weapon_cell_semantics(void) { + float out[OSRS_INVENTORY_CELL_OBS_FEATURES]; + float zero_deltas[6] = {0}; + + uint16_t fang_raw = ITEM_DATABASE[ITEM_OSMUMTENS_FANG].item_id; + osrs_write_inventory_cell_affordance_features( + out, ITEM_OSMUMTENS_FANG, fang_raw, 0, 0, zero_deltas, 99, 99, 99); + CHECK("fang is weapon", out[13] == 1.0f); + CHECK("fang is not armor", out[12] == 0.0f); + CHECK("fang is not consumable", out[14] == 0.0f && out[15] == 0.0f && + out[16] == 0.0f && out[17] == 0.0f && out[18] == 0.0f); + CHECK("fang weapon speed norm > 0", out[26] > 0.0f); + CHECK("fang weapon range norm > 0", out[27] > 0.0f); + CHECK("fang effect class is damage amp", out[23] == 1.0f); + CHECK("fang is not lifesteal/defensive/util", + out[22] == 0.0f && out[24] == 0.0f && out[25] == 0.0f); + CHECK("fang cell within [-1,1]", cell_in_unit_range(out)); + return 0; +} + +static int test_effect_class4_decoder(void) { + float eff4[4]; + osrs_item_effect_class4(OSRS_ITEM_EFFECT_BLOOD_FURY, eff4); + CHECK("blood fury is lifesteal only", + eff4[0] == 1.0f && eff4[1] == 0.0f && eff4[2] == 0.0f && eff4[3] == 0.0f); + osrs_item_effect_class4(OSRS_ITEM_EFFECT_TWISTED_BOW, eff4); + CHECK("twisted bow is damage amp only", + eff4[0] == 0.0f && eff4[1] == 1.0f && eff4[2] == 0.0f && eff4[3] == 0.0f); + osrs_item_effect_class4(OSRS_ITEM_EFFECT_VENATOR_BOUNCE, eff4); + CHECK("venator bounce is damage amp only", + eff4[0] == 0.0f && eff4[1] == 1.0f && eff4[2] == 0.0f && eff4[3] == 0.0f); + osrs_item_effect_class4(OSRS_ITEM_EFFECT_LIGHTBEARER, eff4); + CHECK("lightbearer is util only", + eff4[0] == 0.0f && eff4[1] == 0.0f && eff4[2] == 0.0f && eff4[3] == 1.0f); + + float eq[OSRS_EQUIPPED_SELF_OBS_FEATURES]; + osrs_write_equipped_self_features(eq, ITEM_AMULET_OF_BLOOD_FURY); + CHECK("equipped blood fury lifesteal", eq[12] == 1.0f); + CHECK("equipped blood fury no other class", + eq[13] == 0.0f && eq[14] == 0.0f && eq[15] == 0.0f); + return 0; +} + +static int test_kind6_totality(void) { + CHECK("none -> none", col_consumable_kind6(OSRS_CONSUMABLE_NONE) == COL_CKIND6_NONE); + CHECK("brew -> brew", col_consumable_kind6(OSRS_CONSUMABLE_BREW) == COL_CKIND6_BREW); + CHECK("super restore -> restore", + col_consumable_kind6(OSRS_CONSUMABLE_SUPER_RESTORE) == COL_CKIND6_RESTORE); + CHECK("sanfew -> restore", + col_consumable_kind6(OSRS_CONSUMABLE_SANFEW) == COL_CKIND6_RESTORE); + CHECK("super combat -> combat boost", + col_consumable_kind6(OSRS_CONSUMABLE_SUPER_COMBAT) == COL_CKIND6_COMBAT_BOOST); + CHECK("divine combat -> combat boost", + col_consumable_kind6(OSRS_CONSUMABLE_DIVINE_COMBAT) == COL_CKIND6_COMBAT_BOOST); + CHECK("ranging -> ranged boost", + col_consumable_kind6(OSRS_CONSUMABLE_RANGING) == COL_CKIND6_RANGED_BOOST); + CHECK("divine ranging -> ranged boost", + col_consumable_kind6(OSRS_CONSUMABLE_DIVINE_RANGING) == COL_CKIND6_RANGED_BOOST); + CHECK("surge -> special", + col_consumable_kind6(OSRS_CONSUMABLE_SURGE) == COL_CKIND6_SPECIAL); + CHECK("guthix rest -> special", + col_consumable_kind6(OSRS_CONSUMABLE_GUTHIX_REST) == COL_CKIND6_SPECIAL); + CHECK("saturated heart -> special", + col_consumable_kind6(OSRS_CONSUMABLE_SATURATED_HEART) == COL_CKIND6_SPECIAL); + CHECK("anti-venom+ -> special", + col_consumable_kind6(OSRS_CONSUMABLE_ANTIVENOM_PLUS) == COL_CKIND6_SPECIAL); + CHECK("shark -> food", + col_consumable_kind6(OSRS_CONSUMABLE_SHARK_FOOD) == COL_CKIND6_FOOD); + CHECK("karambwan -> food", + col_consumable_kind6(OSRS_CONSUMABLE_KARAMBWAN) == COL_CKIND6_FOOD); + return 0; +} + +static int test_empty_cell_clamp(void) { + float out[OSRS_INVENTORY_CELL_OBS_FEATURES]; + float zero_deltas[6] = {0}; + osrs_write_inventory_cell_affordance_features( + out, ITEM_NONE, 0, 0, 0, zero_deltas, 99, 99, 99); + CHECK("empty cell not present", out[0] == 0.0f); + CHECK("empty cell within [-1,1]", cell_in_unit_range(out)); + return 0; +} + +int main(void) { + if (test_item_index_classification()) return 1; + if (test_raw_consumable_classification()) return 1; + if (test_sara_brew_dose_variants()) return 1; + if (test_dose_after_drink()) return 1; + if (test_pure_click_interpreter()) return 1; + if (test_cell_click_attributes_to_slot_item()) return 1; + if (test_cell_drink_decrements_one_dose()) return 1; + if (test_shared_drink_consume_owns_timer_and_one_dose()) return 1; + if (test_cell_rearrange_swaps_two_slots()) return 1; + if (test_enriched_feature_counts()) return 1; + if (test_brew_cell_semantics()) return 1; + if (test_weapon_cell_semantics()) return 1; + if (test_effect_class4_decoder()) return 1; + if (test_kind6_totality()) return 1; + if (test_empty_cell_clamp()) return 1; + + printf("test_osrs_inventory_clicks: OK\n"); + return 0; +} diff --git a/ocean/osrs/tests/test_osrs_item_effect_masks.c b/ocean/osrs/tests/test_osrs_item_effect_masks.c new file mode 100644 index 0000000000..6177ea0d96 --- /dev/null +++ b/ocean/osrs/tests/test_osrs_item_effect_masks.c @@ -0,0 +1,113 @@ +#include +#include +#include + +#include "ocean/osrs/osrs_items.h" + +static const struct { + int item; + uint32_t effect; +} EXPECTED_ITEM_EFFECTS[] = { + {ITEM_TWISTED_BOW, OSRS_ITEM_EFFECT_TWISTED_BOW}, + {ITEM_VIRTUS_MASK, OSRS_ITEM_EFFECT_VIRTUS_PIECE}, + {ITEM_VIRTUS_ROBE_TOP, OSRS_ITEM_EFFECT_VIRTUS_PIECE}, + {ITEM_VIRTUS_ROBE_BOTTOM, OSRS_ITEM_EFFECT_VIRTUS_PIECE}, + {ITEM_CONFLICTION_GAUNTLETS, OSRS_ITEM_EFFECT_CONFLICTION}, + {ITEM_SANGUINESTI_STAFF, OSRS_ITEM_EFFECT_SANG_HEAL}, + {ITEM_RING_OF_RECOIL, OSRS_ITEM_EFFECT_RECOIL_RING}, + {ITEM_RING_OF_SUFFERING_RI, OSRS_ITEM_EFFECT_RECOIL_RING}, + {ITEM_LIGHTBEARER, OSRS_ITEM_EFFECT_LIGHTBEARER}, + {ITEM_DHAROKS_HELM, OSRS_ITEM_EFFECT_DHAROK_PIECE}, + {ITEM_DHAROKS_PLATELEGS, OSRS_ITEM_EFFECT_DHAROK_PIECE}, + {ITEM_ELYSIAN_SPIRIT_SHIELD, OSRS_ITEM_EFFECT_ELYSIAN}, + {ITEM_CRYSTAL_HELM, OSRS_ITEM_EFFECT_CRYSTAL_ARMOUR}, + {ITEM_CRYSTAL_BODY, OSRS_ITEM_EFFECT_CRYSTAL_ARMOUR}, + {ITEM_CRYSTAL_LEGS, OSRS_ITEM_EFFECT_CRYSTAL_ARMOUR}, + {ITEM_DRAGON_HUNTER_WAND, OSRS_ITEM_EFFECT_DRAGON_HUNTER_WAND}, + {ITEM_ECHO_BOOTS, OSRS_ITEM_EFFECT_ECHO_BOOTS}, + {ITEM_AMULET_OF_BLOOD_FURY, OSRS_ITEM_EFFECT_BLOOD_FURY}, + {ITEM_SERPENTINE_HELM, OSRS_ITEM_EFFECT_VENOM_IMMUNE}, + {ITEM_OSMUMTENS_FANG, OSRS_ITEM_EFFECT_FANG}, + {ITEM_TUMEKENS_SHADOW, OSRS_ITEM_EFFECT_TUMEKENS_SHADOW}, + {ITEM_VENATOR_BOW, OSRS_ITEM_EFFECT_VENATOR_BOUNCE}, +}; + +static const int EXPECTED_ITEM_EFFECT_FREE_ITEMS[] = { + ITEM_ABYSSAL_TENTACLE, +}; + +int main(void) { + const size_t n_expected = + sizeof(EXPECTED_ITEM_EFFECTS) / sizeof(EXPECTED_ITEM_EFFECTS[0]); + const size_t n_expected_effect_free = + sizeof(EXPECTED_ITEM_EFFECT_FREE_ITEMS) / + sizeof(EXPECTED_ITEM_EFFECT_FREE_ITEMS[0]); + int failures = 0; + + for (size_t e = 0; e < n_expected; e++) { + if (EXPECTED_ITEM_EFFECTS[e].effect == OSRS_ITEM_EFFECT_NONE) { + fprintf(stderr, + "FAIL: EXPECTED_ITEM_EFFECTS row %zu lists OSRS_ITEM_EFFECT_NONE; " + "drop the row instead of pinning NONE\n", + e); + failures++; + } + } + + for (size_t e = 0; e < n_expected_effect_free; e++) { + int item = EXPECTED_ITEM_EFFECT_FREE_ITEMS[e]; + if (item < 0 || item >= NUM_ITEMS) { + fprintf(stderr, + "FAIL: EXPECTED_ITEM_EFFECT_FREE_ITEMS row %zu has invalid " + "item index %d\n", + e, item); + failures++; + continue; + } + if (ITEM_DATABASE[item].effect_mask != OSRS_ITEM_EFFECT_NONE) { + fprintf(stderr, + "FAIL: %s (ITEM idx %d): expected OSRS_ITEM_EFFECT_NONE, " + "got 0x%X\n", + ITEM_DATABASE[item].name, item, + ITEM_DATABASE[item].effect_mask); + failures++; + } + } + + int non_none_seen = 0; + for (int i = 0; i < NUM_ITEMS; i++) { + uint32_t expected = OSRS_ITEM_EFFECT_NONE; + for (size_t e = 0; e < n_expected; e++) { + if (EXPECTED_ITEM_EFFECTS[e].item == i) { + expected = EXPECTED_ITEM_EFFECTS[e].effect; + break; + } + } + + uint32_t actual = ITEM_DATABASE[i].effect_mask; + if (actual != expected) { + fprintf(stderr, + "FAIL: %s (ITEM idx %d): effect_mask=0x%X, expected 0x%X\n", + ITEM_DATABASE[i].name, i, actual, expected); + failures++; + } + if (actual != OSRS_ITEM_EFFECT_NONE) non_none_seen++; + } + + if (non_none_seen != (int)n_expected) { + fprintf(stderr, + "FAIL: %d items carry an effect mask, expected %zu " + "(item DB drifted from the guard table)\n", + non_none_seen, n_expected); + failures++; + } + + if (failures) { + fprintf(stderr, "test_osrs_item_effect_masks: %d failure(s)\n", failures); + return 1; + } + + printf("test_osrs_item_effect_masks: OK (%zu effect items pinned, %d/%d effect-free)\n", + n_expected, NUM_ITEMS - (int)n_expected, NUM_ITEMS); + return 0; +} diff --git a/ocean/osrs/tests/test_osrs_npc_movement.c b/ocean/osrs/tests/test_osrs_npc_movement.c new file mode 100644 index 0000000000..131bea618b --- /dev/null +++ b/ocean/osrs/tests/test_osrs_npc_movement.c @@ -0,0 +1,158 @@ +#include +#include + +#include "ocean/osrs/osrs_encounter.h" + +typedef struct { + uint8_t blocked[16][16]; + int hold_overlap; +} TestNpcMoveGrid; + +#include "ocean/osrs/tests/osrs_test_check.h" + +static int test_npc_move_blocked(void* ctx, int x, int y, int size) { + TestNpcMoveGrid* grid = (TestNpcMoveGrid*)ctx; + for (int dx = 0; dx < size; dx++) { + for (int dy = 0; dy < size; dy++) { + int tx = x + dx; + int ty = y + dy; + if (tx < 0 || tx >= 16 || ty < 0 || ty >= 16) return 1; + if (grid->blocked[tx][ty]) return 1; + } + } + return 0; +} + +static int test_npc_move_hold_overlap(void* ctx) { + TestNpcMoveGrid* grid = (TestNpcMoveGrid*)ctx; + return grid->hold_overlap; +} + +static void test_shared_diagonal_edge_clearance_size_one(void) { + printf("test_shared_diagonal_edge_clearance_size_one\n"); + + TestNpcMoveGrid grid; + memset(&grid, 0, sizeof(grid)); + grid.blocked[6][5] = 1; + int x = 5; + int y = 5; + int moved = encounter_npc_step_toward_policy( + &x, &y, 7, 7, 1, 1, ENCOUNTER_NPC_STEP_TRAVEL_TARGET, + test_npc_move_blocked, &grid, NULL, NULL); + CHECK("east side block prevents 1x1 diagonal corner cut", + moved == 1 && x == 5 && y == 6); + + memset(&grid, 0, sizeof(grid)); + grid.blocked[5][6] = 1; + x = 5; + y = 5; + moved = encounter_npc_step_toward_policy( + &x, &y, 7, 7, 1, 1, ENCOUNTER_NPC_STEP_TRAVEL_TARGET, + test_npc_move_blocked, &grid, NULL, NULL); + CHECK("north side block prevents 1x1 diagonal corner cut", + moved == 1 && x == 6 && y == 5); + + memset(&grid, 0, sizeof(grid)); + x = 5; + y = 5; + moved = encounter_npc_step_toward_policy( + &x, &y, 7, 7, 1, 1, ENCOUNTER_NPC_STEP_TRAVEL_TARGET, + test_npc_move_blocked, &grid, NULL, NULL); + CHECK("clear side edges allow 1x1 diagonal move", + moved == 1 && x == 6 && y == 6); +} + +static void test_shared_aggro_target_overlap_rewrite(void) { + printf("test_shared_aggro_target_overlap_rewrite\n"); + + TestNpcMoveGrid grid; + memset(&grid, 0, sizeof(grid)); + uint32_t rng = 0x12345678u; + int x = 5; + int y = 5; + int moved = encounter_npc_step_toward_policy( + &x, &y, 6, 6, 1, 1, ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET, + test_npc_move_blocked, &grid, NULL, &rng); + CHECK("proposed diagonal into target overlap rewrites to x-only", + moved == 1 && x == 6 && y == 5); + + memset(&grid, 0, sizeof(grid)); + grid.blocked[6][5] = 1; + rng = 0x12345678u; + x = 5; + y = 5; + moved = encounter_npc_step_toward_policy( + &x, &y, 6, 6, 1, 1, ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET, + test_npc_move_blocked, &grid, NULL, &rng); + CHECK("blocked x-only rewrite holds and does not try y-only", + moved == 0 && x == 5 && y == 5); +} + +static void test_shared_melee_policy(void) { + printf("test_shared_melee_policy\n"); + + TestNpcMoveGrid grid; + memset(&grid, 0, sizeof(grid)); + int x = 5; + int y = 5; + int moved = encounter_npc_step_toward_policy( + &x, &y, 6, 5, 1, 1, ENCOUNTER_NPC_STEP_STOP_AT_MELEE, + test_npc_move_blocked, &grid, NULL, NULL); + CHECK("cardinal melee contact with stop-at-melee holds", + moved == 0 && x == 5 && y == 5); + + x = 5; + y = 5; + moved = encounter_npc_step_toward_policy( + &x, &y, 6, 6, 1, 1, ENCOUNTER_NPC_STEP_STOP_AT_MELEE, + test_npc_move_blocked, &grid, NULL, NULL); + CHECK("diagonal melee contact with stop-at-melee tries x-only", + moved == 1 && x == 6 && y == 5); +} + +static void test_shared_current_overlap(void) { + printf("test_shared_current_overlap\n"); + + TestNpcMoveGrid grid; + memset(&grid, 0, sizeof(grid)); + uint32_t rng = 0x12345678u; + int x = 5; + int y = 5; + int moved = encounter_npc_step_toward_policy( + &x, &y, 5, 5, 1, 1, ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET, + test_npc_move_blocked, &grid, NULL, &rng); + int dist = (x > 5 ? x - 5 : 5 - x) + (y > 5 ? y - 5 : 5 - y); + CHECK("current overlap shuffles one cardinal tile", + moved == 1 && dist == 1); + + memset(&grid, 0, sizeof(grid)); + grid.blocked[4][5] = 1; + rng = 12345u; + x = 5; + y = 5; + moved = encounter_npc_step_toward_policy( + &x, &y, 5, 5, 1, 1, ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET, + test_npc_move_blocked, &grid, NULL, &rng); + CHECK("blocked sampled overlap shuffle stays under target", + moved == 0 && x == 5 && y == 5); + + memset(&grid, 0, sizeof(grid)); + grid.hold_overlap = 1; + rng = 0x12345678u; + x = 5; + y = 5; + moved = encounter_npc_step_toward_policy( + &x, &y, 5, 5, 1, 1, ENCOUNTER_NPC_STEP_OSRS_AGGRO_TARGET, + test_npc_move_blocked, &grid, test_npc_move_hold_overlap, &rng); + CHECK("current overlap plus just-clicked target holds current tile", + moved == 0 && x == 5 && y == 5); +} + +int main(void) { + test_shared_diagonal_edge_clearance_size_one(); + test_shared_aggro_target_overlap_rewrite(); + test_shared_melee_policy(); + test_shared_current_overlap(); + + return osrs_test_summary(); +} diff --git a/ocean/osrs/tests/test_osrs_pending_hit_queue.c b/ocean/osrs/tests/test_osrs_pending_hit_queue.c new file mode 100644 index 0000000000..3a89a9bdac --- /dev/null +++ b/ocean/osrs/tests/test_osrs_pending_hit_queue.c @@ -0,0 +1,124 @@ +#include +#include +#include +#include +#include + +#include "ocean/osrs/osrs_encounter.h" + +#include "ocean/osrs/tests/osrs_test_check.h" + +static void assert_child_aborts(const char* label, void (*fn)(void)) { + fflush(NULL); + pid_t pid = fork(); + if (pid == 0) { + fn(); + _exit(0); + } + + int status = 0; + waitpid(pid, &status, 0); + tests_run++; + if (WIFSIGNALED(status) || (WIFEXITED(status) && WEXITSTATUS(status) != 0)) { + tests_passed++; + } else { + tests_failed++; + printf(" FAIL: %s - child returned successfully\n", label); + } +} + +static EncounterPendingHit make_hit(int damage, int ticks_remaining) { + return (EncounterPendingHit){ + .damage = damage, + .ticks_remaining = ticks_remaining, + .attack_style = ATTACK_STYLE_MAGIC, + .spell_type = ENCOUNTER_SPELL_BLOOD, + .hit_success = damage > 0, + }; +} + +static void test_push_preserves_insertion_order(void) { + printf("--- push preserves insertion order ---\n"); + + EncounterPendingHitQueue q = {0}; + EncounterPendingHit* first = encounter_pending_hit_queue_push( + &q, make_hit(11, 4), "test", 7, 1, 2); + EncounterPendingHit* second = encounter_pending_hit_queue_push( + &q, make_hit(19, 2), "test", 7, 1, 2); + + ASSERT_INT_EQ("count after two pushes", q.count, 2); + ASSERT_INT_EQ("first hit active", first->active, 1); + ASSERT_INT_EQ("second hit active", second->active, 1); + ASSERT_INT_EQ("first hit remains first", q.hits[0].damage, 11); + ASSERT_INT_EQ("second hit remains second", q.hits[1].damage, 19); +} + +static void test_remove_compacts_and_clears_tail(void) { + printf("--- remove compacts and clears tail ---\n"); + + EncounterPendingHitQueue q = {0}; + encounter_pending_hit_queue_push(&q, make_hit(1, 5), "test", 7, 1, 2); + encounter_pending_hit_queue_push(&q, make_hit(2, 4), "test", 7, 1, 2); + encounter_pending_hit_queue_push(&q, make_hit(3, 3), "test", 7, 1, 2); + + encounter_pending_hit_queue_remove(&q, 1, "test"); + + ASSERT_INT_EQ("count after remove", q.count, 2); + ASSERT_INT_EQ("first slot kept", q.hits[0].damage, 1); + ASSERT_INT_EQ("third slot compacted", q.hits[1].damage, 3); + ASSERT_INT_EQ("tail active cleared", q.hits[2].active, 0); + ASSERT_INT_EQ("tail damage cleared", q.hits[2].damage, 0); +} + +static void test_earliest_and_damage_sum(void) { + printf("--- earliest and damage sum ---\n"); + + EncounterPendingHitQueue q = {0}; + encounter_pending_hit_queue_push(&q, make_hit(5, 7), "test", 7, 1, 2); + encounter_pending_hit_queue_push(&q, make_hit(9, 2), "test", 7, 1, 2); + encounter_pending_hit_queue_push(&q, make_hit(13, 4), "test", 7, 1, 2); + q.hits[2].active = 0; + + const EncounterPendingHit* earliest = encounter_pending_hit_queue_earliest(&q); + + ASSERT_INT_EQ("earliest hit is second slot", earliest == &q.hits[1], 1); + ASSERT_INT_EQ("damage sum ignores inactive hits", + encounter_pending_hit_queue_damage_sum(&q), 14); +} + +static void test_clear_zeroes_queue(void) { + printf("--- clear zeroes queue ---\n"); + + EncounterPendingHitQueue q = {0}; + encounter_pending_hit_queue_push(&q, make_hit(5, 7), "test", 7, 1, 2); + encounter_pending_hit_queue_push(&q, make_hit(9, 2), "test", 7, 1, 2); + + encounter_pending_hit_queue_clear(&q); + + ASSERT_INT_EQ("count cleared", q.count, 0); + ASSERT_INT_EQ("first slot cleared", q.hits[0].active, 0); + ASSERT_INT_EQ("second slot cleared", q.hits[1].damage, 0); +} + +static void child_queue_overflow(void) { + EncounterPendingHitQueue q = {0}; + for (int i = 0; i < ENCOUNTER_MAX_PENDING_HITS; i++) + encounter_pending_hit_queue_push(&q, make_hit(i + 1, i + 1), "test", 7, 1, 2); + encounter_pending_hit_queue_push(&q, make_hit(99, 1), "test", 7, 1, 2); +} + +static void test_overflow_aborts(void) { + printf("--- overflow aborts ---\n"); + + assert_child_aborts("shared pending-hit overflow aborts", child_queue_overflow); +} + +int main(void) { + test_push_preserves_insertion_order(); + test_remove_compacts_and_clears_tail(); + test_earliest_and_damage_sum(); + test_clear_zeroes_queue(); + test_overflow_aborts(); + + return osrs_test_summary(); +} diff --git a/ocean/osrs/tests/test_osrs_pvp_pending_hits.c b/ocean/osrs/tests/test_osrs_pvp_pending_hits.c new file mode 100644 index 0000000000..93305322af --- /dev/null +++ b/ocean/osrs/tests/test_osrs_pvp_pending_hits.c @@ -0,0 +1,92 @@ +#include +#include +#include +#include +#include + +#include "ocean/osrs/osrs_pvp_combat.h" + +#include "ocean/osrs/tests/osrs_test_check.h" + +static void assert_child_aborts(const char* label, void (*fn)(void)) { + fflush(NULL); + pid_t pid = fork(); + if (pid == 0) { + fn(); + _exit(0); + } + + int status = 0; + waitpid(pid, &status, 0); + tests_run++; + if (WIFSIGNALED(status) || (WIFEXITED(status) && WEXITSTATUS(status) != 0)) { + tests_passed++; + } else { + tests_failed++; + printf(" FAIL: %s - child returned successfully\n", label); + } +} + +static void queue_test_hit(Player* attacker, Player* defender, int damage) { + queue_hit(123, 0, 1, attacker, defender, damage, ATTACK_STYLE_MAGIC, + 4, 0, damage > 0, 0, 0, 0, 0, 0); +} + +static void test_pvp_queue_accepts_capacity(void) { + printf("--- pvp queue accepts capacity ---\n"); + + Player attacker = {0}; + Player defender = {0}; + defender.prayer = PRAYER_NONE; + + for (int i = 0; i < MAX_PENDING_HITS; i++) + queue_test_hit(&attacker, &defender, i + 1); + + ASSERT_INT_EQ("queue reaches capacity", attacker.num_pending_hits, MAX_PENDING_HITS); + ASSERT_INT_EQ("first damage kept", attacker.pending_hits[0].damage, 1); + ASSERT_INT_EQ("last damage kept", attacker.pending_hits[MAX_PENDING_HITS - 1].damage, + MAX_PENDING_HITS); +} + +static void child_pvp_queue_overflow(void) { + Player attacker = {0}; + Player defender = {0}; + defender.prayer = PRAYER_NONE; + + for (int i = 0; i < MAX_PENDING_HITS; i++) + queue_test_hit(&attacker, &defender, i + 1); + queue_test_hit(&attacker, &defender, 99); +} + +static void test_pvp_queue_overflow_aborts(void) { + printf("--- pvp queue overflow aborts ---\n"); + + assert_child_aborts("pvp pending-hit overflow aborts", child_pvp_queue_overflow); +} + +static void test_pvp_remove_compacts_and_clears_tail(void) { + printf("--- pvp remove compacts and clears tail ---\n"); + + Player attacker = {0}; + Player defender = {0}; + defender.prayer = PRAYER_NONE; + queue_test_hit(&attacker, &defender, 3); + queue_test_hit(&attacker, &defender, 7); + queue_test_hit(&attacker, &defender, 11); + + pvp_remove_pending_hit(&attacker, 1); + + ASSERT_INT_EQ("queue count after remove", attacker.num_pending_hits, 2); + ASSERT_INT_EQ("first hit remains", attacker.pending_hits[0].damage, 3); + ASSERT_INT_EQ("third hit compacted", attacker.pending_hits[1].damage, 11); + ASSERT_INT_EQ("tail damage cleared", attacker.pending_hits[2].damage, 0); + ASSERT_INT_EQ("tail timer cleared", attacker.pending_hits[2].ticks_until_hit, 0); +} + +int main(void) { + test_pvp_queue_accepts_capacity(); + test_pvp_queue_overflow_aborts(); + test_pvp_remove_compacts_and_clears_tail(); + + return osrs_test_summary(); +} diff --git a/ocean/osrs/tests/test_osrs_special_attacks.c b/ocean/osrs/tests/test_osrs_special_attacks.c new file mode 100644 index 0000000000..7cccbb0ec2 --- /dev/null +++ b/ocean/osrs/tests/test_osrs_special_attacks.c @@ -0,0 +1,655 @@ +#include +#include +#include + +#include "ocean/osrs/osrs_encounter.h" +#include "ocean/osrs/osrs_special_attacks.h" + +#include "ocean/osrs/tests/osrs_test_check.h" + +static Player make_maxed_player(void) { + Player p; + memset(&p, 0, sizeof(p)); + p.base_hitpoints = 99; p.current_hitpoints = 99; + p.base_prayer = 99; p.current_prayer = 99; + p.base_attack = 99; p.current_attack = 99; + p.base_strength = 99; p.current_strength = 99; + p.base_defence = 99; p.current_defence = 99; + p.base_ranged = 99; p.current_ranged = 99; + p.base_magic = 99; p.current_magic = 99; + return p; +} + +static int loadout_stats_equal(const EncounterLoadoutStats* a, const EncounterLoadoutStats* b) { + return a->attack_bonus == b->attack_bonus && + a->strength_bonus == b->strength_bonus && + a->eff_level == b->eff_level && + a->max_hit == b->max_hit && + a->attack_speed == b->attack_speed && + a->attack_range == b->attack_range && + a->style == b->style && + a->fight_style == b->fight_style && + a->def_stab == b->def_stab && + a->def_slash == b->def_slash && + a->def_crush == b->def_crush && + a->def_magic == b->def_magic && + a->def_ranged == b->def_ranged && + a->att_prayer_mult == b->att_prayer_mult && + a->str_prayer_mult == b->str_prayer_mult && + a->spell_base_damage == b->spell_base_damage; +} + +static void test_consumable_amounts_and_laws(void) { + printf("test_consumable_amounts_and_laws\n"); + + CHECK("super restore amount at 99 is 32", osrs_super_restore_amount(99) == 32); + CHECK("sanfew amount at 99 is 33", osrs_sanfew_restore_amount(99) == 33); + CHECK("sanfew out-restores super restore above level 80", + osrs_sanfew_restore_amount(99) > osrs_super_restore_amount(99) && + osrs_sanfew_restore_amount(60) < osrs_super_restore_amount(60)); + CHECK("super combat boost at 99 is 19", osrs_super_combat_boost_amount(99) == 19); + CHECK("ranging boost at 99 is 13", osrs_ranging_boost_amount(99) == 13); + CHECK("brew heal at 99 is 16", osrs_brew_heal_amount(99) == 16); + CHECK("brew defence boost at 99 is 21", osrs_brew_defence_boost_amount(99) == 21); + + int pvp_float_caps_match_integer_helpers = 1; + for (int level = 1; level <= 99; level++) { + int old_combat = (int)floorf(level * 0.15f) + 5; + int old_ranged = (int)floorf(level * 0.10f) + 4; + int old_brew_defence = (int)floorf(2.0f + (0.20f * level)); + if (old_combat != osrs_super_combat_boost_amount(level) || + old_ranged != osrs_ranging_boost_amount(level) || + old_brew_defence != osrs_brew_defence_boost_amount(level)) { + pvp_float_caps_match_integer_helpers = 0; + } + } + CHECK("PvP float cap expressions match integer helpers for levels 1..99", + pvp_float_caps_match_integer_helpers); + + CHECK("osrs_drink_potion super restore matches the amount helper", + osrs_drink_potion(POTION_SUPER_RESTORE, 0, 99, 0).prayer_restored == + osrs_super_restore_amount(99)); + CHECK("osrs_drink_potion sanfew matches and cures venom", + osrs_drink_potion(POTION_SANFEW, 0, 99, 0).prayer_restored == + osrs_sanfew_restore_amount(99) && + osrs_drink_potion(POTION_SANFEW, 0, 99, 0).venom_cured == 1); + CHECK("osrs_drink_potion super combat matches the amount helper", + osrs_drink_potion(POTION_SUPER_COMBAT, 0, 99, 0).level_boost == + osrs_super_combat_boost_amount(99)); + CHECK("osrs_brew_effect heal matches the amount helper", + osrs_brew_effect(99, 99, 99, 99, 99, 99).hp_healed == osrs_brew_heal_amount(99)); + + BrewResult fresh = osrs_brew_effect(99, 99, 99, 99, 99, 99); + BrewResult drained = osrs_brew_effect(99, 99, 50, 50, 50, 50); + CHECK("brew def boost computes from base defence (21 at 99)", + fresh.def_boost == 21 && drained.def_boost == 21); + CHECK("brew drains diminish with the current level", + drained.att_drain < fresh.att_drain && drained.att_drain == 7); + + Player p = make_maxed_player(); + p.current_attack = 1; p.current_strength = 40; p.current_ranged = 98; + for (int i = 0; i < 20; i++) encounter_restore_stats(&p); + CHECK("restore converges to base and never overshoots", + p.current_attack == 99 && p.current_strength == 99 && + p.current_ranged == 99 && p.current_magic == 99); + + p = make_maxed_player(); + encounter_super_combat_boost(&p); + encounter_super_combat_boost(&p); + CHECK("super combat caps at base + boost (118)", + p.current_attack == 118 && p.current_strength == 118 && p.current_defence == 118); + encounter_ranging_boost(&p); + encounter_ranging_boost(&p); + CHECK("ranging caps at base + boost (112)", p.current_ranged == 112); + + p = make_maxed_player(); + for (int i = 0; i < 4; i++) encounter_brew_drain_stats(&p); + CHECK("brews drain offensive stats", p.current_attack < 99 && p.current_magic < 99); + CHECK("repeated brews cap defence at base + base-level boost (120)", + p.current_defence == 99 + 21); + Player half_def = make_maxed_player(); + half_def.current_defence = 50; + encounter_brew_drain_stats(&half_def); + CHECK("brew def boost is base-derived even when defence is drained", + half_def.current_defence == 50 + 21); + for (int i = 0; i < 20; i++) encounter_restore_stats(&p); + CHECK("restores recover every brewed-down stat to base", + p.current_attack == 99 && p.current_strength == 99 && + p.current_ranged == 99 && p.current_magic == 99); + + Player a = make_maxed_player(); a.current_attack = 1; + Player b = make_maxed_player(); b.current_attack = 1; + encounter_restore_stats(&a); + encounter_sanfew_restore_stats(&b); + CHECK("per-dose stat recovery follows the formulas (33 vs 32 at 99)", + a.current_attack == 33 && b.current_attack == 34); +} + +static void drift_ticks(Player* p, int* timer, EncounterStatDriftPins pins, int ticks) { + for (int i = 0; i < ticks; i++) encounter_tick_stat_drift(p, timer, pins); +} + +static int player_combat_and_hp_at_base(const Player* p) { + return p->current_attack == p->base_attack && + p->current_strength == p->base_strength && + p->current_defence == p->base_defence && + p->current_ranged == p->base_ranged && + p->current_magic == p->base_magic && + p->current_hitpoints == p->base_hitpoints; +} + +static void test_stat_drift_laws(void) { + printf("test_stat_drift_laws\n"); + + Player p = make_maxed_player(); + p.current_attack = 118; + p.current_strength = 80; + p.current_defence = 120; + p.current_ranged = 112; + p.current_magic = 70; + p.current_hitpoints = 50; + int timer = 0; + drift_ticks(&p, &timer, encounter_stat_drift_no_pins(), 99); + CHECK("stat drift waits for a complete 100-tick cycle", + p.current_attack == 118 && p.current_strength == 80 && + p.current_hitpoints == 50 && timer == 99); + drift_ticks(&p, &timer, encounter_stat_drift_no_pins(), 1); + CHECK("stat drift moves every combat stat and HP one toward base", + p.current_attack == 117 && p.current_strength == 81 && + p.current_defence == 119 && p.current_ranged == 111 && + p.current_magic == 71 && p.current_hitpoints == 51 && timer == 0); + + p = make_maxed_player(); + p.current_hitpoints = 115; + timer = 0; + drift_ticks(&p, &timer, encounter_stat_drift_no_pins(), 100); + CHECK("overhealed hitpoints decay one per 100-tick cycle", + p.current_hitpoints == 114); + + p = make_maxed_player(); + p.current_hitpoints = 0; + timer = ENCOUNTER_STAT_DRIFT_TICKS - 1; + drift_ticks(&p, &timer, encounter_stat_drift_no_pins(), 1); + CHECK("natural HP regeneration does not revive a dead player", + p.current_hitpoints == 0); + + p = make_maxed_player(); + p.current_attack = 118; + p.current_strength = 80; + p.current_defence = 120; + p.current_ranged = 112; + p.current_magic = 70; + p.current_hitpoints = 115; + timer = 0; + for (int cycle = 0; cycle < 50; cycle++) + drift_ticks(&p, &timer, encounter_stat_drift_no_pins(), ENCOUNTER_STAT_DRIFT_TICKS); + CHECK("repeated stat drift converges every combat stat and HP to base", + player_combat_and_hp_at_base(&p)); + + p = make_maxed_player(); + encounter_super_combat_boost(&p); + p.current_strength = 122; + p.current_defence = 50; + timer = 0; + EncounterStatDriftPins pins = encounter_divine_super_combat_pins(&p); + drift_ticks(&p, &timer, pins, ENCOUNTER_STAT_DRIFT_TICKS); + CHECK("divine super combat pins boosted stats through drift", + p.current_attack == 118 && p.current_strength == 121 && + p.current_defence == 118); + drift_ticks(&p, &timer, pins, ENCOUNTER_STAT_DRIFT_TICKS * 4); + CHECK("divine super combat floor holds for later drift cycles", + p.current_attack == 118 && p.current_strength == 118 && + p.current_defence == 118); + + p = make_maxed_player(); + encounter_ranging_boost(&p); + timer = 0; + pins = encounter_divine_ranging_pins(&p); + drift_ticks(&p, &timer, pins, ENCOUNTER_STAT_DRIFT_TICKS * 3); + CHECK("divine ranging pins ranged through drift", p.current_ranged == 112); +} + +static void test_spec_costs_and_sgs(void) { + printf("test_spec_costs_and_sgs\n"); + + CHECK("cost table: claws/SGS/elder maul 50, DDS 25, statius 35, non-spec 0", + osrs_spec_cost(ITEM_DRAGON_CLAWS) == 50 && + osrs_spec_cost(ITEM_SGS) == 50 && + osrs_spec_cost(ITEM_ELDER_MAUL) == 50 && + osrs_spec_cost(ITEM_DRAGON_DAGGER) == 25 && + osrs_spec_cost(ITEM_STATIUS_WARHAMMER) == 35 && + osrs_spec_cost(ITEM_SCYTHE_OF_VITUR) == 0); + + uint32_t rng = 4242; + int landed = 0, missed = 0; + for (int i = 0; i < 4000; i++) { + SpecResult r = osrs_resolve_spec(ITEM_SGS, 20000, 50, 12000, 99, &rng); + if (r.total_damage > 0) { + landed++; + int expected_heal = r.total_damage / 2 > 10 ? r.total_damage / 2 : 10; + int expected_pray = r.total_damage / 4 > 5 ? r.total_damage / 4 : 5; + if (r.heal != expected_heal || r.prayer_restore != expected_pray) { + CHECK("SGS heal/prayer follow max(d/2,10) / max(d/4,5)", 0); + return; + } + } else { + missed++; + if (r.heal != 0 || r.prayer_restore != 0) { + CHECK("a missed SGS spec restores nothing", 0); + return; + } + } + } + CHECK("SGS sample hit both outcomes", landed > 0 && missed > 0); + CHECK("SGS heal/prayer follow max(d/2,10) / max(d/4,5)", 1); +} + +static void test_two_handed_loadout_shield_suppression(void) { + printf("test_two_handed_loadout_shield_suppression\n"); + + uint8_t two_handed_with_shield[NUM_GEAR_SLOTS]; + uint8_t two_handed_without_shield[NUM_GEAR_SLOTS]; + uint8_t one_handed_with_shield[NUM_GEAR_SLOTS]; + uint8_t one_handed_without_shield[NUM_GEAR_SLOTS]; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + two_handed_with_shield[slot] = ITEM_NONE; + two_handed_without_shield[slot] = ITEM_NONE; + one_handed_with_shield[slot] = ITEM_NONE; + one_handed_without_shield[slot] = ITEM_NONE; + } + + two_handed_with_shield[GEAR_SLOT_WEAPON] = ITEM_SGS; + two_handed_with_shield[GEAR_SLOT_SHIELD] = ITEM_DRAGON_DEFENDER; + two_handed_without_shield[GEAR_SLOT_WEAPON] = ITEM_SGS; + one_handed_with_shield[GEAR_SLOT_WEAPON] = ITEM_DRAGON_CLAWS; + one_handed_with_shield[GEAR_SLOT_SHIELD] = ITEM_DRAGON_DEFENDER; + one_handed_without_shield[GEAR_SLOT_WEAPON] = ITEM_DRAGON_CLAWS; + + EncounterLoadoutStats sgs_with_shield; + EncounterLoadoutStats sgs_without_shield; + EncounterLoadoutStats claws_with_shield; + EncounterLoadoutStats claws_without_shield; + encounter_compute_loadout_stats(two_handed_with_shield, ATTACK_STYLE_MELEE, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AGGRESSIVE, 0, &sgs_with_shield); + encounter_compute_loadout_stats(two_handed_without_shield, ATTACK_STYLE_MELEE, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AGGRESSIVE, 0, &sgs_without_shield); + encounter_compute_loadout_stats(one_handed_with_shield, ATTACK_STYLE_MELEE, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AGGRESSIVE, 0, &claws_with_shield); + encounter_compute_loadout_stats(one_handed_without_shield, ATTACK_STYLE_MELEE, + OFFENSIVE_PRAYER_NONE, 99, FIGHT_STYLE_AGGRESSIVE, 0, &claws_without_shield); + + CHECK("SGS is marked two-handed and dragon claws are marked one-handed", + item_is_two_handed(ITEM_SGS) == 1 && + item_is_two_handed(ITEM_DRAGON_CLAWS) == 0); + CHECK("2h loadout stats ignore an occupied shield slot", + loadout_stats_equal(&sgs_with_shield, &sgs_without_shield)); + CHECK("1h loadout stats keep dragon defender bonuses", + claws_with_shield.attack_bonus > claws_without_shield.attack_bonus && + claws_with_shield.strength_bonus == + claws_without_shield.strength_bonus + + ITEM_DATABASE[ITEM_DRAGON_DEFENDER].melee_strength); + + uint8_t two_handed_with_effect_shield[NUM_GEAR_SLOTS]; + uint8_t one_handed_with_effect_shield[NUM_GEAR_SLOTS]; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + two_handed_with_effect_shield[slot] = ITEM_NONE; + one_handed_with_effect_shield[slot] = ITEM_NONE; + } + two_handed_with_effect_shield[GEAR_SLOT_WEAPON] = ITEM_SGS; + two_handed_with_effect_shield[GEAR_SLOT_SHIELD] = ITEM_ELYSIAN_SPIRIT_SHIELD; + one_handed_with_effect_shield[GEAR_SLOT_WEAPON] = ITEM_DRAGON_CLAWS; + one_handed_with_effect_shield[GEAR_SLOT_SHIELD] = ITEM_ELYSIAN_SPIRIT_SHIELD; + + OsrsEquipmentEffectProfile two_handed_profile; + OsrsEquipmentEffectProfile one_handed_profile; + encounter_derive_loadout_effect_profile( + two_handed_with_effect_shield, &two_handed_profile); + encounter_derive_loadout_effect_profile( + one_handed_with_effect_shield, &one_handed_profile); + CHECK("2h effect profile ignores shield effect bits", + two_handed_profile.shield_item == ITEM_NONE && + !osrs_effect_profile_has(&two_handed_profile, OSRS_ITEM_EFFECT_ELYSIAN)); + CHECK("1h effect profile keeps shield effect bits", + one_handed_profile.shield_item == ITEM_ELYSIAN_SPIRIT_SHIELD && + osrs_effect_profile_has(&one_handed_profile, OSRS_ITEM_EFFECT_ELYSIAN)); +} + +static void test_magic_effective_attack_level_law(void) { + printf("test_magic_effective_attack_level_law\n"); + + uint8_t powered_staff_loadout[NUM_GEAR_SLOTS]; + for (int slot = 0; slot < NUM_GEAR_SLOTS; slot++) { + powered_staff_loadout[slot] = ITEM_NONE; + } + powered_staff_loadout[GEAR_SLOT_WEAPON] = ITEM_TRIDENT_OF_SWAMP; + + EncounterLoadoutStats accurate; + EncounterLoadoutStats longrange; + encounter_compute_loadout_stats(powered_staff_loadout, ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_AUGURY, 82, FIGHT_STYLE_ACCURATE, 30, &accurate); + encounter_compute_loadout_stats(powered_staff_loadout, ATTACK_STYLE_MAGIC, + OFFENSIVE_PRAYER_AUGURY, 82, FIGHT_STYLE_LONGRANGE, 30, &longrange); + + CHECK("E12 accurate magic eff is floor(82*1.25)+2+9 = 113", + accurate.eff_level == 113); + CHECK("E12 longrange magic eff is floor(82*1.25)+9 = 111", + longrange.eff_level == 111); + + encounter_update_loadout_level(&accurate, OFFENSIVE_PRAYER_AUGURY, 82, 82); + encounter_update_loadout_level(&longrange, OFFENSIVE_PRAYER_AUGURY, 82, 82); + CHECK("E12 dynamic magic recompute preserves accurate and longrange laws", + accurate.eff_level == 113 && longrange.eff_level == 111); +} + +static void test_spec_force_max_laws(void) { + printf("test_spec_force_max_laws\n"); + + typedef struct { + int item; + int total; + int hits; + int def_drain; + int magic_def_drain; + int freeze_ticks; + int attack_speed_override; + int heal; + int prayer_restore; + } ForceMaxCase; + + const ForceMaxCase cases[] = { + { ITEM_AGS, 55, 1, 0, 0, 0, 0, 0, 0 }, + { ITEM_DRAGON_CLAWS, 77, 4, 0, 0, 0, 0, 0, 0 }, + { ITEM_STATIUS_WARHAMMER, 50, 1, 60, 0, 0, 0, 0, 0 }, + { ITEM_BGS, 48, 1, 48, 0, 0, 0, 0, 0 }, + { ITEM_ZGS, 44, 1, 0, 0, 32, 0, 0, 0 }, + { ITEM_SGS, 44, 1, 0, 0, 0, 0, 22, 11 }, + { ITEM_ANCIENT_GS, 44, 1, 0, 0, 0, 0, 0, 0 }, + { ITEM_VESTAS, 48, 1, 0, 0, 0, 0, 0, 0 }, + { ITEM_VOIDWAKER, 60, 1, 0, 0, 0, 0, 0, 0 }, + { ITEM_GRANITE_MAUL, 40, 1, 0, 0, 0, 1, 0, 0 }, + { ITEM_DRAGON_DAGGER, 92, 2, 0, 0, 0, 0, 0, 0 }, + { ITEM_ELDER_MAUL, 40, 1, 70, 0, 0, 0, 0, 0 }, + { ITEM_TOXIC_BLOWPIPE, 60, 1, 0, 0, 0, 0, 30, 0 }, + { ITEM_MAGIC_SHORTBOW_I, 80, 2, 0, 0, 0, 0, 0, 0 }, + { ITEM_DARK_BOW, 96, 2, 0, 0, 0, 0, 0, 0 }, + { ITEM_HEAVY_BALLISTA, 50, 1, 0, 0, 0, 0, 0, 0 }, + { ITEM_ZARYTE_CROSSBOW, 40, 1, 0, 0, 0, 0, 0, 0 }, + { ITEM_MORRIGANS_JAVELIN, 48, 1, 0, 0, 0, 0, 0, 0 }, + { ITEM_ARMADYL_CROSSBOW, 40, 1, 0, 0, 0, 0, 0, 0 }, + { ITEM_VOLATILE_STAFF, 58, 1, 0, 0, 0, 0, 0, 0 }, + { ITEM_EYE_OF_AYAK, 52, 1, 0, 52, 0, 5, 0, 0 }, + }; + + int all_cases_match = 1; + for (int i = 0; i < (int)(sizeof(cases) / sizeof(cases[0])); i++) { + SpecResult r = {0}; + osrs_spec_result_force_max(&r, cases[i].item, 40, 200); + if (r.total_damage != cases[i].total || r.num_hits != cases[i].hits || + r.def_drain != cases[i].def_drain || + r.magic_def_drain != cases[i].magic_def_drain || + r.freeze_ticks != cases[i].freeze_ticks || + r.attack_speed_override != cases[i].attack_speed_override || + r.heal != cases[i].heal || + r.prayer_restore != cases[i].prayer_restore || + r.spec_cost != osrs_spec_cost(cases[i].item)) { + all_cases_match = 0; + } + } + CHECK("force-max covers every resolver weapon's best outcome", all_cases_match); + + SpecResult claws = {0}; + osrs_spec_result_force_max(&claws, ITEM_DRAGON_CLAWS, 40, 200); + CHECK("force-max claws uses first-success t=79 floor split with observable total 77", + claws.damage[0] == 39 && claws.damage[1] == 19 && + claws.damage[2] == 9 && claws.damage[3] == 10 && + claws.total_damage == 77); + + SpecResult sgs = {0}; + osrs_spec_result_force_max(&sgs, ITEM_SGS, 8, 200); + CHECK("force-max SGS recomputes heal/prayer minimums from the forced total", + sgs.total_damage == 8 && sgs.heal == 10 && sgs.prayer_restore == 5); + + SpecResult elder = {0}; + osrs_spec_result_force_max(&elder, ITEM_ELDER_MAUL, 40, 200); + CHECK("force-max elder maul re-applies 35% target defence drain", + elder.total_damage == 40 && elder.def_drain == 70); +} + +static void claws_expected_splats(int branch, int total, int out[4]) { + out[0] = 0; out[1] = 0; out[2] = 0; out[3] = 0; + switch (branch) { + case 0: + out[0] = total / 2; out[1] = total / 4; + out[2] = total / 8; out[3] = total / 8 + 1; + break; + case 1: + out[0] = total / 2; out[1] = total / 4; out[2] = total / 4 + 1; + break; + case 2: + out[0] = total / 2; out[1] = total / 2 + 1; + break; + case 3: + out[0] = total + 1; + break; + } +} + +static int claws_classify(const SpecResult* r, int max_hit, int* out_branch) { + if (r->damage[0] == 0 && r->damage[1] == 0 && + r->damage[2] == 0 && r->damage[3] == 0) { + *out_branch = 5; + return 1; + } + if (r->damage[0] == 1 && r->damage[1] == 1 && + r->damage[2] == 0 && r->damage[3] == 0) { + *out_branch = 4; + return 1; + } + for (int k = 0; k < 4; k++) { + int low = max_hit * (4 - k) / 4; + int high = max_hit + low - 1; + for (int t = low; t <= high; t++) { + int e[4]; + claws_expected_splats(k, t, e); + if (r->damage[0] == e[0] && r->damage[1] == e[1] && + r->damage[2] == e[2] && r->damage[3] == e[3]) { + *out_branch = k; + return 1; + } + } + } + return 0; +} + +static void test_claws_and_def_drains(void) { + printf("test_claws_and_def_drains\n"); + + uint32_t rng = 1337; + int seen_total[40] = {0}; + int classified_ok = 1, branch0 = 0, min_total = 1 << 30, max_total = 0; + for (int i = 0; i < 20000; i++) { + SpecResult r = osrs_resolve_spec(ITEM_DRAGON_CLAWS, 2000000, 40, 1, 99, &rng); + int branch; + if (!claws_classify(&r, 40, &branch)) { classified_ok = 0; break; } + if (branch != 0) continue; + branch0++; + int total = r.damage[0] + r.damage[1] + r.damage[2] + r.damage[3]; + if (total != r.total_damage) { classified_ok = 0; break; } + + for (int t = 40; t <= 79; t++) { + int e[4]; + claws_expected_splats(0, t, e); + if (r.damage[0] == e[0] && r.damage[1] == e[1] && + r.damage[2] == e[2] && r.damage[3] == e[3]) { + seen_total[t - 40] = 1; + } + } + if (total < min_total) min_total = total; + if (total > max_total) max_total = total; + } + int covered = 1; + for (int i = 0; i < 40; i++) covered &= seen_total[i]; + CHECK("claws splats always match a dps-calc branch table", classified_ok); + CHECK("claws first-success branch dominates at p~=1", branch0 >= 19990); + CHECK("claws first-success draws cover every total in [40, 79]", covered); + CHECK("claws observable extremes match the floor splits (41 / 77 at max 40)", + min_total == 20 + 10 + 5 + 6 && max_total == 39 + 19 + 9 + 10); + + rng = 777; + int seen_class[6] = {0}; + int contested_ok = 1; + for (int i = 0; i < 50000; i++) { + SpecResult r = osrs_resolve_spec(ITEM_DRAGON_CLAWS, 10000, 40, 10000, 99, &rng); + int branch; + if (r.num_hits != 4 || !claws_classify(&r, 40, &branch)) { + contested_ok = 0; + break; + } + seen_class[branch] = 1; + } + CHECK("claws contested-accuracy tuples all match dps-calc tables", contested_ok); + CHECK("claws all four branches and both all-miss outcomes observed", + seen_class[0] && seen_class[1] && seen_class[2] && + seen_class[3] && seen_class[4] && seen_class[5]); + + rng = 99; + int maul_drained = 0, statius_drained = 0; + for (int i = 0; i < 2000 && (!maul_drained || !statius_drained); i++) { + SpecResult m = osrs_resolve_spec(ITEM_ELDER_MAUL, 20000, 40, 8000, 200, &rng); + if (m.damage[0] > 0 && !maul_drained) { + CHECK("elder maul drains 35% of target def", m.def_drain == 200 * 35 / 100); + maul_drained = 1; + } + SpecResult st = osrs_resolve_spec(ITEM_STATIUS_WARHAMMER, 20000, 40, 8000, 200, &rng); + if (st.damage[0] > 0 && !statius_drained) { + CHECK("statius drains 30% of target def", st.def_drain == 200 * 30 / 100); + statius_drained = 1; + } + } + CHECK("both drain weapons landed in the sample", maul_drained && statius_drained); +} + +static void test_item_effect_laws(void) { + printf("test_item_effect_laws\n"); + + OsrsItemEffectState state; + osrs_item_effect_state_init(&state); + + OsrsEquipmentEffectProfile empty; + memset(&empty, 0, sizeof(empty)); + OsrsPreparedAttackEffects id = osrs_prepare_attack_effects( + &empty, &state, ITEM_SCYTHE_OF_VITUR, ATTACK_STYLE_MELEE, + OSRS_MAGIC_ATTACK_NONE, osrs_target_ref_none(), 1, 12345, 50, + osrs_target_effect_context_magic(300, 80), 99, 99); + CHECK("empty profile is the identity on rolls", + id.attack_roll == 12345 && id.max_hit == 50 && id.use_double_accuracy == 0); + + OsrsEquipmentEffectProfile tbow_profile; + memset(&tbow_profile, 0, sizeof(tbow_profile)); + tbow_profile.effect_mask = OSRS_ITEM_EFFECT_TWISTED_BOW; + int prev_hit = -1, prev_roll = -1, monotone = 1; + for (int magic = 1; magic <= 350; magic += 7) { + OsrsPreparedAttackEffects e = osrs_prepare_attack_effects( + &tbow_profile, &state, ITEM_TWISTED_BOW, ATTACK_STYLE_RANGED, + OSRS_MAGIC_ATTACK_NONE, osrs_target_ref_none(), 1, 20000, 80, + osrs_target_effect_context_magic(magic, 0), 99, 99); + if (e.max_hit < prev_hit || e.attack_roll < prev_roll) monotone = 0; + prev_hit = e.max_hit; + prev_roll = e.attack_roll; + } + CHECK("tbow scaling is monotone in target magic", monotone); + + CHECK("crystal points are helm 1 / legs 2 / body 3", + osrs_crystal_armour_points(ITEM_CRYSTAL_HELM) == 1 && + osrs_crystal_armour_points(ITEM_CRYSTAL_LEGS) == 2 && + osrs_crystal_armour_points(ITEM_CRYSTAL_BODY) == 3); + OsrsEquipmentEffectProfile crystal; + memset(&crystal, 0, sizeof(crystal)); + crystal.effect_mask = OSRS_ITEM_EFFECT_CRYSTAL_ARMOUR; + crystal.crystal_armour_points = 6; + OsrsPreparedAttackEffects bowfa = osrs_prepare_attack_effects( + &crystal, &state, ITEM_BOW_OF_FAERDHINEN, ATTACK_STYLE_RANGED, + OSRS_MAGIC_ATTACK_NONE, osrs_target_ref_none(), 1, 20000, 40, + osrs_target_effect_context_magic(100, 0), 99, 99); + CHECK("full crystal scales bowfa by 26/20 accuracy and 46/40 damage", + bowfa.attack_roll == 20000 * 26 / 20 && bowfa.max_hit == 40 * 46 / 40); + + OsrsEquipmentEffectProfile fang; + memset(&fang, 0, sizeof(fang)); + fang.effect_mask = OSRS_ITEM_EFFECT_FANG; + CHECK("fang generated item carries the shared fang effect", + ITEM_DATABASE[ITEM_OSMUMTENS_FANG].effect_mask == OSRS_ITEM_EFFECT_FANG); + + int fang_bounds_ok = 1; + for (int max_hit = 1; max_hit <= 99; max_hit++) { + OsrsPreparedAttackEffects prepared = osrs_prepare_attack_effects_for_melee_style( + &fang, &state, ITEM_OSMUMTENS_FANG, ATTACK_STYLE_MELEE, + MELEE_STYLE_STAB, OSRS_MAGIC_ATTACK_NONE, osrs_target_ref_none(), 1, + 12345, max_hit, osrs_target_effect_context_none(), 99, 99); + int shrink = osrs_fang_hit_bound_shrink(max_hit); + if (prepared.min_hit != shrink || prepared.max_hit != max_hit - shrink) { + fang_bounds_ok = 0; + } + } + CHECK("fang min/max bounds follow floor(max*3/20) shrink", fang_bounds_ok); + + OsrsPreparedAttackEffects fang_stab = osrs_prepare_attack_effects_for_melee_style( + &fang, &state, ITEM_OSMUMTENS_FANG, ATTACK_STYLE_MELEE, + MELEE_STYLE_STAB, OSRS_MAGIC_ATTACK_NONE, osrs_target_ref_none(), 1, + 20000, 50, osrs_target_effect_context_none(), 99, 99); + OsrsPreparedAttackEffects fang_slash = osrs_prepare_attack_effects_for_melee_style( + &fang, &state, ITEM_OSMUMTENS_FANG, ATTACK_STYLE_MELEE, + MELEE_STYLE_SLASH, OSRS_MAGIC_ATTACK_NONE, osrs_target_ref_none(), 1, + 20000, 50, osrs_target_effect_context_none(), 99, 99); + CHECK("fang accuracy reroll applies only on stab", + fang_stab.use_fang_accuracy == 1 && fang_slash.use_fang_accuracy == 0); + + int double_accuracy_monotone = 1; + for (int attack_roll = 1; attack_roll <= 20000; attack_roll += 137) { + for (int def_roll = 1; def_roll <= 20000; def_roll += 251) { + if (osrs_hit_chance_double(attack_roll, def_roll) + 0.000001f < + osrs_hit_chance(attack_roll, def_roll)) { + double_accuracy_monotone = 0; + } + } + } + CHECK("fang double-roll accuracy never lowers hit chance", double_accuracy_monotone); + + CHECK("scythe splats: 1 vs 1x1, 2 vs 2x2, 3 vs 3x3+", + osrs_scythe_splats_for_target_size(1) == 1 && + osrs_scythe_splats_for_target_size(2) == 2 && + osrs_scythe_splats_for_target_size(3) == 3 && + osrs_scythe_splats_for_target_size(5) == 3); + + OsrsEquipmentEffectProfile fury; + memset(&fury, 0, sizeof(fury)); + fury.effect_mask = OSRS_ITEM_EFFECT_BLOOD_FURY; + uint32_t rng = 31337; + int procs = 0, bad_heal = 0; + for (int i = 0; i < 2000; i++) { + int heal = osrs_blood_fury_heal_amount( + &fury, ATTACK_STYLE_MELEE, 30, &rng); + if (heal > 0) { + procs++; + if (heal != 9) bad_heal = 1; + } + } + CHECK("blood fury heals exactly 30% on every proc", !bad_heal); + CHECK("blood fury procs near the 20% rate", procs > 280 && procs < 520); + int ranged_heal = osrs_blood_fury_heal_amount( + &fury, ATTACK_STYLE_RANGED, 30, &rng); + CHECK("blood fury never procs on ranged damage", ranged_heal == 0); +} + +int main(void) { + test_consumable_amounts_and_laws(); + test_stat_drift_laws(); + test_spec_costs_and_sgs(); + test_two_handed_loadout_shield_suppression(); + test_magic_effective_attack_level_law(); + test_spec_force_max_laws(); + test_claws_and_def_drains(); + test_item_effect_laws(); + + return osrs_test_summary(); +} diff --git a/ocean/osrs/tests/test_osrs_venator_bow_bounce.c b/ocean/osrs/tests/test_osrs_venator_bow_bounce.c new file mode 100644 index 0000000000..0bc07c0ce8 --- /dev/null +++ b/ocean/osrs/tests/test_osrs_venator_bow_bounce.c @@ -0,0 +1,299 @@ +#include +#include +#include + +#include "../osrs_combat.h" + +#define CHECK(name, cond) do { \ + if (!(cond)) { \ + fprintf(stderr, "FAIL: %s\n", name); \ + abort(); \ + } \ +} while (0) + +static OsrsVenatorMonster live_monster(int slot, int x, int y, int size) { + return osrs_venator_monster( + slot, x, y, size, OSRS_VENATOR_MONSTER_ALIVE); +} + +static void check_chain_slots( + const char* name, + OsrsVenatorChain chain, + OsrsVenatorChainLength length, + int slot0, + int slot1, + int slot2 +) { + CHECK(name, chain.length == length); + CHECK(name, chain.hits[0].slot == slot0); + if (length >= OSRS_VENATOR_CHAIN_LENGTH_TWO) { + CHECK(name, chain.hits[1].slot == slot1); + CHECK(name, chain.hits[1].kind == OSRS_VENATOR_HIT_BOUNCE); + } + if (length >= OSRS_VENATOR_CHAIN_LENGTH_THREE) { + CHECK(name, chain.hits[2].slot == slot2); + CHECK(name, chain.hits[2].kind == OSRS_VENATOR_HIT_BOUNCE); + } +} + +static int chains_equal(OsrsVenatorChain a, OsrsVenatorChain b) { + if (a.length != b.length) return 0; + for (int i = 0; i < (int)a.length; i++) { + if (a.hits[i].slot != b.hits[i].slot) return 0; + if (a.hits[i].kind != b.hits[i].kind) return 0; + if (a.hits[i].footprint.sw_x != b.hits[i].footprint.sw_x) return 0; + if (a.hits[i].footprint.sw_y != b.hits[i].footprint.sw_y) return 0; + if (a.hits[i].footprint.size != b.hits[i].footprint.size) return 0; + } + return 1; +} + +static void test_size1_bounce_law(void) { + OsrsVenatorFootprint a = osrs_venator_footprint(0, 0, 1); + for (int dx = -4; dx <= 4; dx++) { + for (int dy = -4; dy <= 4; dy++) { + OsrsVenatorFootprint b = osrs_venator_footprint(dx, dy, 1); + int expected = chebyshev_distance(0, 0, dx, dy) <= 2; + int ab = osrs_venator_can_bounce(a, b); + int ba = osrs_venator_can_bounce(b, a); + CHECK("size1 bounce equals cheb<=2", ab == expected); + CHECK("size1 bounce is symmetric", ab == ba); + } + } +} + +static void test_bounce_max_hit_law(void) { + const int cases[] = {0, 1, 2, 3, 29, 30, 31, 98}; + const int expected[] = {0, 0, 1, 2, 19, 20, 20, 65}; + for (int i = 0; i < (int)(sizeof(cases) / sizeof(cases[0])); i++) { + CHECK("bounce max is floor(original*2/3)", + osrs_venator_bounce_max_hit(cases[i]) == expected[i]); + } +} + +static void test_chain_laws_and_determinism(void) { + OsrsVenatorMonster primary = live_monster(5, 0, 0, 1); + OsrsVenatorMonster candidates[] = { + live_monster(9, 1, 1, 1), + live_monster(3, -1, 1, 1), + }; + + OsrsVenatorChain first = osrs_venator_resolve_chain( + primary, candidates, 2); + OsrsVenatorChain second = osrs_venator_resolve_chain( + primary, candidates, 2); + + CHECK("chain length is at least one", first.length >= OSRS_VENATOR_CHAIN_LENGTH_ONE); + CHECK("chain length is at most three", first.length <= OSRS_VENATOR_CHAIN_LENGTH_THREE); + CHECK("hit2 is not primary", first.hits[1].slot != primary.slot); + CHECK("hit3 is not hit2", first.hits[2].slot != first.hits[1].slot); + CHECK("lower slot wins equal-distance hit2 tie", first.hits[1].slot == 3); + CHECK("targeting is deterministic", chains_equal(first, second)); +} + +static void test_warband_cluster_and_bounce_back(void) { + OsrsVenatorMonster primary = live_monster(30, 0, 0, 1); + OsrsVenatorMonster candidates[] = { + live_monster(20, 1, 0, 1), + live_monster(10, 2, 0, 1), + }; + OsrsVenatorChain chain = osrs_venator_resolve_chain( + primary, candidates, 2); + check_chain_slots( + "three-monster warband cluster covers all three slots", + chain, + OSRS_VENATOR_CHAIN_LENGTH_THREE, + 30, + 20, + 10); + + OsrsVenatorMonster bounce_primary = live_monster(10, 0, 0, 1); + OsrsVenatorMonster bounce_candidates[] = { + live_monster(20, 1, 0, 1), + live_monster(30, 2, 0, 1), + }; + OsrsVenatorChain bounce_chain = osrs_venator_resolve_chain( + bounce_primary, bounce_candidates, 2); + check_chain_slots( + "warband tie permits deterministic bounce-back to primary", + bounce_chain, + OSRS_VENATOR_CHAIN_LENGTH_THREE, + 10, + 20, + 10); +} + +static void test_no_overreach_past_original_centre(void) { + OsrsVenatorMonster primary = live_monster(100, 0, 0, 1); + OsrsVenatorMonster candidates[] = { + live_monster(50, 2, 0, 1), + live_monster(10, 3, 0, 1), + }; + OsrsVenatorChain chain = osrs_venator_resolve_chain(primary, candidates, 2); + check_chain_slots( + "2nd bounce never reaches a target >2 tiles from the original centre", + chain, + OSRS_VENATOR_CHAIN_LENGTH_THREE, + 100, + 50, + 100); + for (int i = 0; i < (int)chain.length; i++) + CHECK("over-reach target (3,0) is never in the chain", + chain.hits[i].slot != 10); +} + +static void test_rule_matrix_representatives(void) { + OsrsVenatorFootprint sender1 = osrs_venator_footprint(0, 0, 1); + OsrsVenatorFootprint sender2 = osrs_venator_footprint(0, 0, 2); + OsrsVenatorFootprint sender4 = osrs_venator_footprint(0, 0, 4); + + OsrsVenatorFootprint target2_accept_pass_send_fail = + osrs_venator_footprint(2, 0, 2); + OsrsVenatorFootprint target2_pass = + osrs_venator_footprint(1, 0, 2); + OsrsVenatorFootprint target2_fail = + osrs_venator_footprint(3, 0, 2); + CHECK("size2 accept pass", osrs_venator_accepts_bounce( + sender1, target2_accept_pass_send_fail)); + CHECK("size2 accept fail", !osrs_venator_accepts_bounce( + sender1, target2_fail)); + CHECK("size2 send pass", osrs_venator_sends_bounce( + sender1, target2_pass)); + CHECK("size2 send fail", !osrs_venator_sends_bounce( + sender1, target2_accept_pass_send_fail)); + + OsrsVenatorFootprint target3_pass = + osrs_venator_footprint(1, 0, 3); + OsrsVenatorFootprint target3_fail = + osrs_venator_footprint(2, 0, 3); + CHECK("size3 accept pass", osrs_venator_accepts_bounce( + sender1, target3_pass)); + CHECK("size3 accept fail", !osrs_venator_accepts_bounce( + sender1, target3_fail)); + CHECK("size3 send pass", osrs_venator_sends_bounce( + sender2, osrs_venator_footprint(2, 0, 3))); + CHECK("size3 send fail", !osrs_venator_sends_bounce( + sender2, osrs_venator_footprint(3, 0, 3))); + + OsrsVenatorFootprint target4_accept_pass = + osrs_venator_footprint(3, 0, 4); + OsrsVenatorFootprint target4_accept_fail = + osrs_venator_footprint(4, 0, 4); + CHECK("size4 accept pass", osrs_venator_accepts_bounce( + sender4, target4_accept_pass)); + CHECK("size4 accept fail", !osrs_venator_accepts_bounce( + sender4, target4_accept_fail)); + CHECK("size4 send pass", osrs_venator_sends_bounce( + sender2, osrs_venator_footprint(1, 0, 4))); + CHECK("size4 send fail", !osrs_venator_sends_bounce( + sender2, osrs_venator_footprint(2, 0, 4))); + + OsrsVenatorFootprint target5_pass = + osrs_venator_footprint(1, 0, 5); + OsrsVenatorFootprint target5_fail = + osrs_venator_footprint(2, 0, 5); + CHECK("size5 accept pass", osrs_venator_accepts_bounce( + sender2, target5_pass)); + CHECK("size5 accept fail", !osrs_venator_accepts_bounce( + sender2, target5_fail)); + CHECK("size5 send pass", osrs_venator_sends_bounce( + sender2, target5_pass)); + CHECK("size5 send fail", !osrs_venator_sends_bounce( + sender2, target5_fail)); +} + +static void test_boundaries(void) { + OsrsVenatorMonster primary = live_monster(1, 0, 0, 1); + + OsrsVenatorChain no_candidate = osrs_venator_resolve_chain( + primary, NULL, 0); + check_chain_slots( + "no candidate gives one hit", + no_candidate, + OSRS_VENATOR_CHAIN_LENGTH_ONE, + 1, + -1, + -1); + + OsrsVenatorMonster one_candidate[] = { + live_monster(2, 2, 0, 1), + }; + OsrsVenatorChain one = osrs_venator_resolve_chain( + primary, one_candidate, 1); + check_chain_slots( + "one candidate gives two hits", + one, + OSRS_VENATOR_CHAIN_LENGTH_TWO, + 1, + 2, + -1); + + OsrsVenatorMonster excluded_candidate[] = { + live_monster(2, 3, 0, 1), + }; + OsrsVenatorChain excluded = osrs_venator_resolve_chain( + primary, excluded_candidate, 1); + check_chain_slots( + "cheb three excludes candidate", + excluded, + OSRS_VENATOR_CHAIN_LENGTH_ONE, + 1, + -1, + -1); + + CHECK("cheb two can bounce", + osrs_venator_can_bounce( + primary.footprint, one_candidate[0].footprint)); + CHECK("cheb three cannot bounce", + !osrs_venator_can_bounce( + primary.footprint, excluded_candidate[0].footprint)); +} + +static void test_damage_rolls_all_chain_nodes(void) { + OsrsVenatorMonster primary = live_monster(10, 0, 0, 1); + OsrsVenatorMonster candidates[] = { + live_monster(20, 1, 0, 1), + live_monster(30, 2, 0, 1), + }; + OsrsVenatorChain chain = osrs_venator_resolve_chain( + primary, candidates, 2); + int defence_rolls[OSRS_VENATOR_MAX_CHAIN_HITS] = {100, 200, 300}; + uint32_t rng = 12345; + uint32_t expected_rng = rng; + + xorshift32(&expected_rng); + xorshift32(&expected_rng); + xorshift32(&expected_rng); + + OsrsVenatorDamageResult damage = osrs_venator_roll_chain_damage( + &chain, defence_rolls, 0, 31, &rng); + + CHECK("damage result keeps chain length", damage.length == chain.length); + CHECK("all chain nodes roll accuracy even when all miss", + damage.hits[0].roll_state == OSRS_VENATOR_DAMAGE_ROLLED && + damage.hits[1].roll_state == OSRS_VENATOR_DAMAGE_ROLLED && + damage.hits[2].roll_state == OSRS_VENATOR_DAMAGE_ROLLED); + CHECK("zero attack roll misses all chain nodes", + damage.hits[0].accuracy == OSRS_VENATOR_ACCURACY_MISS && + damage.hits[1].accuracy == OSRS_VENATOR_ACCURACY_MISS && + damage.hits[2].accuracy == OSRS_VENATOR_ACCURACY_MISS); + CHECK("all misses do not roll damage", damage.total_damage == 0); + CHECK("primary max hit is original", damage.hits[0].max_hit == 31); + CHECK("bounce max hit is two-thirds of original", damage.hits[1].max_hit == 20); + CHECK("third max hit is two-thirds of original", damage.hits[2].max_hit == 20); + CHECK("independent misses consume one accuracy roll per chain node", + rng == expected_rng); +} + +int main(void) { + test_size1_bounce_law(); + test_bounce_max_hit_law(); + test_chain_laws_and_determinism(); + test_warband_cluster_and_bounce_back(); + test_no_overreach_past_original_centre(); + test_rule_matrix_representatives(); + test_boundaries(); + test_damage_rolls_all_chain_nodes(); + printf("test_osrs_venator_bow_bounce: OK\n"); + return 0; +} diff --git a/ocean/osrs/tests/trace_colo_move.c b/ocean/osrs/tests/trace_colo_move.c new file mode 100644 index 0000000000..c61b6e5d82 --- /dev/null +++ b/ocean/osrs/tests/trace_colo_move.c @@ -0,0 +1,59 @@ +#include +#include + +#include "ocean/osrs/encounters/encounter_colosseum.h" + +static int nearest_live_obs_target(const ColosseumState* s) { + int best_slot = -1, best_dist = 1 << 30; + for (int slot = 0; slot < COLO_OBS_NPCS; slot++) { + int idx = s->current_obs_slots[slot]; + if (idx < 0 || idx >= COLO_MAX_NPCS) continue; + const ColoNPC* npc = &s->npcs[idx]; + if (!col_npc_is_live_target(npc)) continue; + int d = col_npc_dist_to_player(s, npc); + if (d < best_dist) { best_dist = d; best_slot = slot; } + } + return best_slot < 0 ? 0 : col_primary_attack_action_for_obs_slot(best_slot); +} + +static int first_walkable_move(const ColosseumState* s) { + for (int a = 1; a < ENCOUNTER_MOVE_ACTIONS; a++) { + int nx = s->player.x + ENCOUNTER_MOVE_TARGET_DX[a]; + int ny = s->player.y + ENCOUNTER_MOVE_TARGET_DY[a]; + if (col_player_walkable((void*)s, nx, ny)) return a; + } + return 0; +} + +int main(void) { + ColosseumContext ctx; + col_init_context_typed(&ctx); + ctx.config.start_wave = 0; + ctx.config.loadout_profile_mode = COLO_LOADOUT_PROFILE_MODE_MIXED; + ctx.config.beginner_loadout_fraction = 0.5f; + ctx.config.step_out_forecast_obs_enabled = 1; + ctx.config.action_debug_log = 1; + + ColosseumState s; + memset(&s, 0, sizeof(s)); + col_reset_ctx((EncounterState*)&s, (EncounterContext*)&ctx, 0x1234u); + + static float obs[COLO_NUM_OBS]; + int actions[COLO_NUM_ACTION_HEADS] = {0}; + for (int tick = 0; tick < 80 && !s.episode_over; tick++) { + col_write_obs_ctx((EncounterState*)&s, (EncounterContext*)&ctx, obs); + for (int h = 0; h < COLO_NUM_ACTION_HEADS; h++) actions[h] = 0; + if (s.modifiers.draft_pending) { + int opt = 0; + for (int o = 0; o < COLO_MODIFIER_DRAFT_OPTIONS; o++) + if (s.modifiers.draft_options[o] >= 0) { opt = o; break; } + actions[COLO_HEAD_MODIFIER_SELECT] = opt + 1; + } else { + actions[COLO_HEAD_PRIMARY] = (tick & 1) + ? first_walkable_move(&s) + : nearest_live_obs_target(&s); + } + col_step_ctx((EncounterState*)&s, (EncounterContext*)&ctx, actions); + } + return 0; +} diff --git a/ocean/osrs_colosseum/colosseum_profile.h b/ocean/osrs_colosseum/colosseum_profile.h new file mode 100644 index 0000000000..02dfa34cb2 --- /dev/null +++ b/ocean/osrs_colosseum/colosseum_profile.h @@ -0,0 +1,58 @@ +#ifndef PUFFER_COLOSSEUM_PROFILE_H +#define PUFFER_COLOSSEUM_PROFILE_H + +#ifndef COLOSSEUM_ENV_EXPORT +#define COLOSSEUM_ENV_EXPORT __attribute__((visibility("default"))) +#endif + +#define OSRS_ENV_PROFILE_SLOTS(X) \ + X(COLO_PROF_C_STEP_TOTAL, "c_step_total") \ + X(COLO_PROF_C_ACTIONS, "c_actions") \ + X(COLO_PROF_C_ENCOUNTER_STEP, "c_encounter_step") \ + X(COLO_PROF_C_WRITE_OBS, "c_write_obs") \ + X(COLO_PROF_C_WRITE_MASK, "c_write_mask") \ + X(COLO_PROF_C_REWARD_TERMINAL, "c_reward_terminal") \ + X(COLO_PROF_C_TERMINAL_LOG, "c_terminal_log") \ + X(COLO_PROF_C_RESET, "c_reset") \ + X(COLO_PROF_OBS_REFRESH_SLOTS, "obs_refresh_slots") \ + X(COLO_PROF_OBS_PREFIX, "obs_prefix") \ + X(COLO_PROF_OBS_PILLARS, "obs_pillars") \ + X(COLO_PROF_OBS_BESTGEAR, "obs_bestgear_build") \ + X(COLO_PROF_OBS_INVENTORY, "obs_inventory") \ + X(COLO_PROF_OBS_VENATOR, "obs_venator") \ + X(COLO_PROF_OBS_NPC_SLOTS, "obs_npc_slots") \ + X(COLO_PROF_OBS_MODIFIERS, "obs_modifiers") \ + X(COLO_PROF_OBS_BOSS, "obs_boss") \ + X(COLO_PROF_OBS_PENDING_HITS, "obs_pending_hits") \ + X(COLO_PROF_OBS_FORECAST, "obs_forecast") \ + X(COLO_PROF_OBS_THREAT_LOS, "obs_threat_los") \ + X(COLO_PROF_OBS_THRALL_DC, "obs_thrall_dc") \ + X(COLO_PROF_OBS_WEAPON_CHOICE, "obs_weapon_choice") \ + X(COLO_PROF_OBS_SPAWN, "obs_spawn") \ + X(COLO_PROF_OBS_THREAT_FIELD, "obs_threat_field") \ + X(COLO_PROF_FC_SETUP, "fc_setup") \ + X(COLO_PROF_FC_ROLLOUT, "fc_rollout") \ + X(COLO_PROF_FC_SOLARFLARE, "fc_solarflare") \ + X(COLO_PROF_STEP_NPC_TOTAL, "step_npc_total") \ + X(COLO_PROF_STEP_SOL_BOSS, "step_sol_boss") \ + X(COLO_PROF_STEP_JAVELIN_SKYFALL, "step_javelin_skyfall") \ + X(COLO_PROF_STEP_NPC_MOVEMENT, "step_npc_movement") \ + X(COLO_PROF_STEP_NPC_PATHFINDING, "step_npc_pathfinding") \ + X(COLO_PROF_STEP_NPC_ATTACK, "step_npc_attack") \ + X(COLO_PROF_STEP_MANTICORE_BARRAGE, "step_manticore_barrage") \ + X(COLO_PROF_STEP_WARBAND_ATTACK, "step_warband_attack") \ + X(COLO_PROF_STEP_MODIFIERS_HAZARDS, "step_modifiers_hazards") + +#define OSRS_ENV_PROFILE_PREFIX colosseum +#define OSRS_ENV_PROFILE_COUNT COLO_PROF_COUNT +#define OSRS_ENV_PROFILE_SLOT_TYPE ColosseumProfileSlot +#define OSRS_ENV_PROFILE_ENV_VAR "PUFFER_COLOSSEUM_PROFILE" +#define OSRS_ENV_PROFILE_EXPORT COLOSSEUM_ENV_EXPORT +#include "../osrs/osrs_env_profile.h" + +#define COLO_PROFILE_ENABLED() colosseum_profile_enabled() +#define COLO_PROFILE_NOW_MS() colosseum_profile_now_ms() +#define COLO_PROFILE_ADD(slot, ms) colosseum_profile_add((slot), (ms)) +#define COLO_PROFILE_MARK(slot) colosseum_profile_mark(col_prof_enabled, &col_prof_t0, (slot)) + +#endif diff --git a/ocean/osrs_colosseum/osrs_colosseum.c b/ocean/osrs_colosseum/osrs_colosseum.c new file mode 100644 index 0000000000..66b90abb91 --- /dev/null +++ b/ocean/osrs_colosseum/osrs_colosseum.c @@ -0,0 +1,4 @@ +#define OSRS_VISUAL +#define OSRS_VISUAL_DEFAULT_ENCOUNTER "colosseum" +#include "colosseum_profile.h" +#include "../osrs/osrs_visual.c" diff --git a/ocean/osrs_colosseum/osrs_colosseum.h b/ocean/osrs_colosseum/osrs_colosseum.h new file mode 100644 index 0000000000..55aafb2c23 --- /dev/null +++ b/ocean/osrs_colosseum/osrs_colosseum.h @@ -0,0 +1,402 @@ +#pragma once + +#include +#include +#include +#include + +#include "pufferenv.h" + +#define Log OsrsSharedLog +#include "../osrs/encounters/encounter_colosseum.h" +#undef Log + +#define OBS_SIZE COLO_NUM_OBS +#define NUM_ATNS COLO_NUM_ACTION_HEADS +#define ACT_SIZES COLO_ACTION_DIMS_INIT +typedef float obs_t; + +#define COLO_ENV_STATE(env) ((EncounterState*)&(env)->state) +#define COLO_ENV_CONTEXT(env) ((EncounterContext*)&(env)->context) +#define COLO_MAX_CURRICULUM_TIERS 8 + +struct Log { + float episode_return; + float episode_length; + float damage_dealt; + float damage_received; + float wins; + float wave; + float npc_kills; + float prayer_correct; + float prayer_total; + float score; + float sol_min_hp; + float max_depth_reached; + float current_set_argmax_dpt_hit; + float current_set_argmax_dpt_n; + float attacked_argmax_set_hit; + float attacked_argmax_set_n; + float pray_faced_by_type[COLO_NUM_NPC_TYPES]; + float pray_correct_by_type[COLO_NUM_NPC_TYPES]; + float offpray_damage_by_type[COLO_NUM_NPC_TYPES]; + float total_damage_by_type[COLO_NUM_NPC_TYPES]; + float death_by_type[COLO_NUM_NPC_TYPES]; + float typeless_damage_by_type[COLO_NUM_NPC_TYPES]; + float death_fatal_damage; + float offpray_damage_conflict; + float offpray_damage_solo; + float death_on_conflict_tick; + float death_dmg_unprayable; + float death_dmg_offpray; + float death_dmg_prayed; + float death_dmg_self; + float death_heal_remaining; + float farm_damage; + float n; +}; + +struct Env { + Log log; + int num_agents; + unsigned int rng; + Agent agents[1]; + int tag; + int boundary_reached; + + ColosseumState state; + ColosseumContext context; + int config_start_wave; + int acts_staging[COLO_NUM_ACTION_HEADS]; + uint64_t damage_scale_anneal_step; + float max_episode_depth_seen; +}; + +static inline uint32_t col_lowbias32(uint32_t x) { + x ^= x >> 16; + x *= 0x7feb352dU; + x ^= x >> 15; + x *= 0x846ca68bU; + x ^= x >> 16; + return x; +} + +static inline float col_curriculum_uniform(uint32_t env_index) { + uint32_t h = col_lowbias32(env_index ^ 0x9e3779b9U); + return (float)(h >> 8) * (1.0f / 16777216.0f); +} + +static void col_write_action_mask_bytes(Env* env, unsigned char* mask_out) { + if (!mask_out) return; + float mask_f[COLO_ACTION_MASK_SIZE]; + ENCOUNTER_COLOSSEUM.write_mask(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), mask_f); + for (int i = 0; i < COLO_ACTION_MASK_SIZE; i++) { + mask_out[i] = mask_f[i] != 0.0f ? 1 : 0; + } +} + +static inline void col_log_dpt_sample(float* hit_acc, float* n_acc, int sample) { + if (sample < 0) { + return; + } + *hit_acc += sample ? 1.0f : 0.0f; + *n_acc += 1.0f; +} + +static void col_assign_curriculum_wave(Env* env, Dict* kwargs) { + int classic_curriculum_mode = (int)dict_get(kwargs, "classic_curriculum_mode"); + if (classic_curriculum_mode < 0 || classic_curriculum_mode > 1) { + fprintf(stderr, "colosseum: classic_curriculum_mode must be 0 or 1, got %d\n", + classic_curriculum_mode); + abort(); + } + int num_tiers_config = (int)dict_get(kwargs, "curriculum_num_tiers"); + if (num_tiers_config < 0 || num_tiers_config > COLO_MAX_CURRICULUM_TIERS) { + fprintf(stderr, "colosseum: curriculum_num_tiers must be 0..%d, got %d\n", + COLO_MAX_CURRICULUM_TIERS, num_tiers_config); + abort(); + } + + static const char* const wave_keys[COLO_MAX_CURRICULUM_TIERS] = { + "curriculum_wave_1", "curriculum_wave_2", "curriculum_wave_3", "curriculum_wave_4", + "curriculum_wave_5", "curriculum_wave_6", "curriculum_wave_7", "curriculum_wave_8", + }; + static const char* const frac_keys[COLO_MAX_CURRICULUM_TIERS] = { + "curriculum_frac_1", "curriculum_frac_2", "curriculum_frac_3", "curriculum_frac_4", + "curriculum_frac_5", "curriculum_frac_6", "curriculum_frac_7", "curriculum_frac_8", + }; + int waves[COLO_MAX_CURRICULUM_TIERS]; + float fracs[COLO_MAX_CURRICULUM_TIERS]; + int num_tiers = 0; + for (int i = 0; i < num_tiers_config; i++) { + int wave = (int)dict_get(kwargs, wave_keys[i]); + float frac = (float)dict_get(kwargs, frac_keys[i]); + if (frac > 0.0f) { + waves[num_tiers] = wave; + fracs[num_tiers] = frac; + num_tiers++; + } + } + + if (classic_curriculum_mode == 0 || num_tiers == 0) { + return; + } + float draw = col_curriculum_uniform(env->rng); + float cumulative = 0.0f; + for (int t = 0; t < num_tiers; t++) { + cumulative += fracs[t]; + if (draw < cumulative) { + ENCOUNTER_COLOSSEUM.put_int( + COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), "start_wave", waves[t]); + ENCOUNTER_COLOSSEUM.put_int( + COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), "curriculum_agent", 1); + return; + } + } +} + +void puf_init(Env* env, Dict* kwargs) { + env->num_agents = 1; + env->agents[0].policy = 0; + ENCOUNTER_COLOSSEUM.init_context(COLO_ENV_CONTEXT(env)); + ENCOUNTER_COLOSSEUM.init_state(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env)); + + uint32_t seed_offset = 0; + const char* seed_offset_str = getenv("PUFFER_ENV_SEED_OFFSET"); + if (seed_offset_str) { + seed_offset = (uint32_t)strtoul(seed_offset_str, NULL, 10); + } + uint32_t env_seed = col_lowbias32(env->rng + seed_offset); + if (env_seed == 0) { + env_seed = 1; + } + env->state.rng_state = env_seed; + + memset(&env->log, 0, sizeof(Log)); + + int start_wave = (int)dict_get(kwargs, "start_wave"); + ENCOUNTER_COLOSSEUM.put_int( + COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), "start_wave", start_wave); + env->config_start_wave = start_wave - 1; + + static const char* const float_keys[] = { + "damage_reward_coeff", + "wave_clear_bonus", + "win_bonus", + "death_penalty_coeff", + "timeout_penalty", + "boss_damage_reward_coeff", + "boss_phase_bonus", + "argmax_gear_reward_coeff", + "offensive_boost_reward_coeff", + "beginner_loadout_fraction", + "late_start_supply_fraction_per_wave", + "prayer_switch_fail_prob", + "player_damage_received_scale", + "damage_scale_anneal_start", + }; + for (size_t k = 0; k < sizeof(float_keys) / sizeof(*float_keys); k++) { + ENCOUNTER_COLOSSEUM.put_float( + COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), + float_keys[k], (float)dict_get(kwargs, float_keys[k])); + } + + static const char* const int_keys[] = { + "farm_safe_damage_cap", + "farm_cap_waves", + "loadout_profile_mode", + "step_out_forecast_obs_enabled", + "threat_field_obs_enabled", + "forecast_horizon", + "mask_inventory_heads", + "prayer_oracle_mode", + "late_start_state_mode", + "bis_gear_oracle_mode", + "invuln_mode", + "episode_max_ticks_override", + "remove_brews", + "damage_scale_anneal_ticks", + }; + for (size_t k = 0; k < sizeof(int_keys) / sizeof(*int_keys); k++) { + ENCOUNTER_COLOSSEUM.put_int( + COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), + int_keys[k], (int)dict_get(kwargs, int_keys[k])); + } + + col_assign_curriculum_wave(env, kwargs); +} + +void puf_reset(Env* env) { + Agent* agent = &env->agents[0]; + ENCOUNTER_COLOSSEUM.reset(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), 0); + ENCOUNTER_COLOSSEUM.write_obs( + COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), (float*)agent->observations); + col_write_action_mask_bytes(env, agent->action_mask); +} + +void puf_step(Env* env) { + Agent* agent = &env->agents[0]; + for (int i = 0; i < NUM_ATNS; i++) { + env->acts_staging[i] = (int)agent->actions[i]; + } + + int anneal_ticks = env->context.config.damage_scale_anneal_ticks; + float anneal_start = env->context.config.damage_scale_anneal_start; + env->damage_scale_anneal_step++; + if (anneal_ticks > 0 && anneal_start < 1.0f) { + float frac = (float)env->damage_scale_anneal_step / (float)anneal_ticks; + if (frac > 1.0f) frac = 1.0f; + env->context.config.player_damage_received_scale = + anneal_start + (1.0f - anneal_start) * frac; + } + + ENCOUNTER_COLOSSEUM.step(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), env->acts_staging); + + float* obs = (float*)agent->observations; + ENCOUNTER_COLOSSEUM.write_obs(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), obs); + col_write_action_mask_bytes(env, agent->action_mask); + + agent->rewards[0] = ENCOUNTER_COLOSSEUM.get_reward(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env)); + int is_terminal = ENCOUNTER_COLOSSEUM.is_terminal(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env)); + agent->terminals[0] = (float)is_terminal; + + if (env->state.start_wave == env->config_start_wave) { + col_log_dpt_sample( + &env->log.current_set_argmax_dpt_hit, + &env->log.current_set_argmax_dpt_n, + col_current_set_is_argmax_dpt_for_target(&env->state)); + col_log_dpt_sample( + &env->log.attacked_argmax_set_hit, + &env->log.attacked_argmax_set_n, + col_attacked_with_argmax_set(&env->state)); + } + + if (is_terminal) { + ColosseumState* s = &env->state; + ColosseumLog* clog = (ColosseumLog*)ENCOUNTER_COLOSSEUM.get_log( + COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env)); + if (s->start_wave == env->config_start_wave) { + if (clog->max_wave_depth > env->max_episode_depth_seen) { + env->max_episode_depth_seen = clog->max_wave_depth; + } + env->log.max_depth_reached += env->max_episode_depth_seen; + env->log.n += 1.0f; + env->log.episode_return += clog->episode_return; + env->log.episode_length += (float)clog->episode_length; + env->log.wins += (float)clog->win; + env->log.wave += (float)clog->wave_reached; + env->log.damage_dealt += clog->total_damage_dealt; + env->log.damage_received += clog->total_damage_received; + env->log.npc_kills += (float)clog->total_npc_kills; + env->log.score += clog->outcome_score; + env->log.sol_min_hp += (float)s->min_sol_hp_seen; + env->log.prayer_correct += (float)clog->total_prayer_correct; + env->log.prayer_total += (float)clog->total_npc_attacks; + for (int t = 0; t < COLO_NUM_NPC_TYPES; t++) { + env->log.pray_faced_by_type[t] += clog->pray_faced_by_type[t]; + env->log.pray_correct_by_type[t] += clog->pray_correct_by_type[t]; + env->log.offpray_damage_by_type[t] += clog->offpray_damage_by_type[t]; + env->log.total_damage_by_type[t] += clog->total_damage_by_type[t]; + env->log.death_by_type[t] += clog->death_by_type[t]; + env->log.typeless_damage_by_type[t] += clog->typeless_damage_by_type[t]; + } + env->log.death_fatal_damage += clog->death_fatal_damage; + env->log.offpray_damage_conflict += clog->offpray_damage_conflict; + env->log.offpray_damage_solo += clog->offpray_damage_solo; + env->log.death_on_conflict_tick += clog->death_on_conflict_tick; + env->log.death_dmg_unprayable += clog->death_dmg_unprayable; + env->log.death_dmg_offpray += clog->death_dmg_offpray; + env->log.death_dmg_prayed += clog->death_dmg_prayed; + env->log.death_dmg_self += clog->death_dmg_self; + env->log.death_heal_remaining += clog->death_heal_remaining; + env->log.farm_damage += clog->farm_damage; + } + ENCOUNTER_COLOSSEUM.reset(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), 0); + ENCOUNTER_COLOSSEUM.write_obs(COLO_ENV_STATE(env), COLO_ENV_CONTEXT(env), obs); + col_write_action_mask_bytes(env, agent->action_mask); + } +} + +void puf_render(Env* env) { + (void)env; +} + +void puf_close(Env* env) { + ENCOUNTER_COLOSSEUM.destroy_context(COLO_ENV_CONTEXT(env)); +} + +void puf_log(Log* log, Dict* out) { + dict_set(out, "episode_return", log->episode_return); + dict_set(out, "episode_length", log->episode_length); + dict_set(out, "damage_dealt", log->damage_dealt); + dict_set(out, "damage_received", log->damage_received); + dict_set(out, "wins", log->wins); + dict_set(out, "wave", log->wave); + dict_set(out, "npc_kills", log->npc_kills); + + float prayer_rate = log->prayer_total > 0.0f + ? log->prayer_correct / log->prayer_total : 0.0f; + dict_set(out, "prayer_correct_rate", prayer_rate); + + dict_set(out, "score", log->score); + dict_set(out, "sol_min_hp", log->sol_min_hp); + dict_set(out, "max_depth_reached", log->max_depth_reached); + dict_set(out, "current_set_is_argmax_dpt_for_target", + log->current_set_argmax_dpt_n > 0.0f + ? log->current_set_argmax_dpt_hit / log->current_set_argmax_dpt_n : 0.0f); + dict_set(out, "attacked_with_argmax_set", + log->attacked_argmax_set_n > 0.0f + ? log->attacked_argmax_set_hit / log->attacked_argmax_set_n : 0.0f); + + static const char* const OFFPRAY_RATE_KEYS[COLO_NUM_NPC_TYPES] = { + "offpray_rate_berserker", "offpray_rate_archer", "offpray_rate_seer", + "offpray_rate_serpent", "offpray_rate_jaguar", "offpray_rate_javelin", + "offpray_rate_shockwave", "offpray_rate_minotaur", "offpray_rate_manticore", + "offpray_rate_sol", "offpray_rate_totem", "offpray_rate_bee"}; + static const char* const OFFPRAY_DMG_KEYS[COLO_NUM_NPC_TYPES] = { + "offpray_dmg_berserker", "offpray_dmg_archer", "offpray_dmg_seer", + "offpray_dmg_serpent", "offpray_dmg_jaguar", "offpray_dmg_javelin", + "offpray_dmg_shockwave", "offpray_dmg_minotaur", "offpray_dmg_manticore", + "offpray_dmg_sol", "offpray_dmg_totem", "offpray_dmg_bee"}; + static const char* const TOTAL_DMG_KEYS[COLO_NUM_NPC_TYPES] = { + "total_dmg_berserker", "total_dmg_archer", "total_dmg_seer", + "total_dmg_serpent", "total_dmg_jaguar", "total_dmg_javelin", + "total_dmg_shockwave", "total_dmg_minotaur", "total_dmg_manticore", + "total_dmg_sol", "total_dmg_totem", "total_dmg_bee"}; + for (int t = 0; t < COLO_NUM_NPC_TYPES; t++) { + float faced = log->pray_faced_by_type[t]; + float off_rate = faced > 0.0f + ? (faced - log->pray_correct_by_type[t]) / faced : 0.0f; + dict_set(out, OFFPRAY_RATE_KEYS[t], off_rate); + dict_set(out, OFFPRAY_DMG_KEYS[t], log->offpray_damage_by_type[t]); + dict_set(out, TOTAL_DMG_KEYS[t], log->total_damage_by_type[t]); + } + + static const char* const DEATH_BY_KEYS[COLO_NUM_NPC_TYPES] = { + "death_by_berserker", "death_by_archer", "death_by_seer", + "death_by_serpent", "death_by_jaguar", "death_by_javelin", + "death_by_shockwave", "death_by_minotaur", "death_by_manticore", + "death_by_sol", "death_by_totem", "death_by_bee"}; + for (int t = 0; t < COLO_NUM_NPC_TYPES; t++) { + dict_set(out, DEATH_BY_KEYS[t], log->death_by_type[t]); + } + dict_set(out, "death_fatal_damage", log->death_fatal_damage); + dict_set(out, "offpray_dmg_conflict", log->offpray_damage_conflict); + dict_set(out, "offpray_dmg_solo", log->offpray_damage_solo); + dict_set(out, "death_on_conflict_tick", log->death_on_conflict_tick); + + static const char* const TYPELESS_DMG_KEYS[COLO_NUM_NPC_TYPES] = { + "typeless_dmg_berserker", "typeless_dmg_archer", "typeless_dmg_seer", + "typeless_dmg_serpent", "typeless_dmg_jaguar", "typeless_dmg_javelin", + "typeless_dmg_shockwave", "typeless_dmg_minotaur", "typeless_dmg_manticore", + "typeless_dmg_sol", "typeless_dmg_totem", "typeless_dmg_bee"}; + for (int t = 0; t < COLO_NUM_NPC_TYPES; t++) { + dict_set(out, TYPELESS_DMG_KEYS[t], log->typeless_damage_by_type[t]); + } + dict_set(out, "death_dmg_unprayable", log->death_dmg_unprayable); + dict_set(out, "death_dmg_offpray", log->death_dmg_offpray); + dict_set(out, "death_dmg_prayed", log->death_dmg_prayed); + dict_set(out, "death_dmg_self", log->death_dmg_self); + dict_set(out, "death_heal_remaining", log->death_heal_remaining); + dict_set(out, "farm_damage", log->farm_damage); +} diff --git a/ocean/osrs_inferno/inferno_profile.h b/ocean/osrs_inferno/inferno_profile.h new file mode 100644 index 0000000000..fc22626d79 --- /dev/null +++ b/ocean/osrs_inferno/inferno_profile.h @@ -0,0 +1,43 @@ +#ifndef PUFFER_INFERNO_PROFILE_H +#define PUFFER_INFERNO_PROFILE_H + +#ifndef INFERNO_ENV_EXPORT +#define INFERNO_ENV_EXPORT __attribute__((visibility("default"))) +#endif + +#define OSRS_ENV_PROFILE_SLOTS(X) \ + X(INF_PROF_C_STEP_TOTAL, "c_step_total") \ + X(INF_PROF_C_ACTIONS, "c_actions") \ + X(INF_PROF_C_ENCOUNTER_STEP, "c_encounter_step") \ + X(INF_PROF_C_WRITE_OBS, "c_write_obs") \ + X(INF_PROF_C_WRITE_MASK, "c_write_mask") \ + X(INF_PROF_C_REWARD_TERMINAL, "c_reward_terminal") \ + X(INF_PROF_C_TERMINAL_LOG, "c_terminal_log") \ + X(INF_PROF_C_RESET, "c_reset") \ + X(INF_PROF_OBS_PREFIX, "obs_prefix") \ + X(INF_PROF_OBS_REFRESH_SLOTS, "obs_refresh_slots") \ + X(INF_PROF_OBS_NPC_SLOTS, "obs_npc_slots") \ + X(INF_PROF_OBS_FORECAST, "obs_forecast") \ + X(INF_PROF_OBS_PENDING_HITS, "obs_pending_hits") \ + X(INF_PROF_OBS_SPARKS, "obs_sparks") \ + X(INF_PROF_FORECAST_LANDING, "forecast_landing") \ + X(INF_PROF_FORECAST_STATE_COPY, "forecast_state_copy") \ + X(INF_PROF_FORECAST_NPC_MOVE, "forecast_npc_move") \ + X(INF_PROF_FORECAST_NPC_ATTACK, "forecast_npc_attack") \ + X(INF_PROF_FORECAST_CALLS, "forecast_calls") \ + X(INF_PROF_FORECAST_VALID_ACTIONS, "forecast_valid_actions") \ + X(INF_PROF_FORECAST_DISTINCT_LANDINGS, "forecast_distinct_landings") + +#define OSRS_ENV_PROFILE_PREFIX inferno +#define OSRS_ENV_PROFILE_COUNT INF_PROF_COUNT +#define OSRS_ENV_PROFILE_SLOT_TYPE InfernoProfileSlot +#define OSRS_ENV_PROFILE_ENV_VAR "PUFFER_INFERNO_PROFILE" +#define OSRS_ENV_PROFILE_EXPORT INFERNO_ENV_EXPORT +#include "../osrs/osrs_env_profile.h" + +#define INF_PROFILE_ENABLED() inferno_profile_enabled() +#define INF_PROFILE_NOW_MS() inferno_profile_now_ms() +#define INF_PROFILE_ADD(slot, ms) inferno_profile_add((slot), (ms)) +#define INF_PROFILE_MARK(slot) inferno_profile_mark(inf_prof_enabled, &inf_prof_t0, (slot)) + +#endif diff --git a/ocean/osrs_inferno/osrs_inferno.c b/ocean/osrs_inferno/osrs_inferno.c new file mode 100644 index 0000000000..abbbff01b3 --- /dev/null +++ b/ocean/osrs_inferno/osrs_inferno.c @@ -0,0 +1,4 @@ +#define OSRS_VISUAL +#define OSRS_VISUAL_DEFAULT_ENCOUNTER "inferno" +#include "inferno_profile.h" +#include "../osrs/osrs_visual.c" diff --git a/ocean/osrs_inferno/osrs_inferno.h b/ocean/osrs_inferno/osrs_inferno.h new file mode 100644 index 0000000000..fce0fa1d9a --- /dev/null +++ b/ocean/osrs_inferno/osrs_inferno.h @@ -0,0 +1,572 @@ +#ifndef OSRS_INFERNO_ENV_H +#define OSRS_INFERNO_ENV_H + +#include +#include +#include +#include + +#include "pufferenv.h" +#include "inferno_profile.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" +#include "../osrs/encounters/encounter_inferno.h" +#pragma GCC diagnostic pop + +#define OBS_SIZE INF_NUM_OBS +#define NUM_ATNS INF_NUM_ACTION_HEADS +#define ACT_SIZES INF_ACTION_DIMS_INIT +typedef float obs_t; + +#define INF_ENV_STATE(env) ((EncounterState*)&((env)->state)) +#define INF_ENV_CONTEXT(env) ((EncounterContext*)&((env)->context)) +#define INF_ENV_INFERNO(env) (&((env)->state)) +#define INF_ENV_INFERNO_CONTEXT(env) (&((env)->context)) + +struct Env { + Log log; + int num_agents; + unsigned int rng; + Agent agents[1]; + int tag; + int boundary_reached; + + InfernoState state; + InfernoContext context; + int config_start_wave; +}; + +static inline uint32_t inf_lowbias32(uint32_t x) { + x ^= x >> 16; + x *= 0x7feb352dU; + x ^= x >> 15; + x *= 0x846ca68bU; + x ^= x >> 16; + return x; +} + +static void inferno_env_put_float(Env* env, const char* key, float value) { + ENCOUNTER_INFERNO.put_float(INF_ENV_STATE(env), INF_ENV_CONTEXT(env), key, value); +} + +static void inferno_env_put_int(Env* env, const char* key, int value) { + ENCOUNTER_INFERNO.put_int(INF_ENV_STATE(env), INF_ENV_CONTEXT(env), key, value); +} + +static void inferno_zero_phase_reward_coeffs(Env* env) { + static const char* const keys[] = { + "jad_damage_reward_coeff", + "zuk_healer_damage_reward_coeff", + "set_damage_reward_coeff", + "jad_kill_bonus", + "zuk_healer_kill_bonus", + "set_kill_bonus", + "post_healer_zuk_damage_coeff", + "zuk_healer_phase_hp_delta_coeff", + "post_healer_set_damage_reward_coeff", + "post_healer_set_kill_bonus", + "post_healer_set_alive_tick_penalty_coeff", + "post_healer_set_alive_penalty_cap", + "zuk_untagged_healer_tick_penalty_coeff", + "zuk_untagged_healer_target_bonus_coeff", + "zuk_safe_untagged_healer_target_bonus_coeff", + }; + for (size_t i = 0; i < sizeof(keys) / sizeof(*keys); i++) + inferno_env_put_float(env, keys[i], 0.0f); + inferno_env_put_float(env, "post_jad_zuk_multiplier", 1.0f); + inferno_env_put_float(env, "jad_alive_zuk_multiplier", 1.0f); +} + +static void inferno_apply_reward_profile(Env* env, int reward_profile) { + switch (reward_profile) { + case 0: + inferno_env_put_int(env, "joseph_reward_mode", 0); + inferno_env_put_float(env, "damage_reward_coeff", 0.0f); + inferno_env_put_float(env, "shield_penalty_coeff", 0.0f); + inferno_env_put_float(env, "tag_reward_coeff", 0.0f); + inferno_env_put_float(env, "shield_tag_reward_coeff", 0.0f); + inferno_env_put_float( + env, "zuk_untagged_healer_nonmagic_attack_bonus_coeff", 0.0f); + inferno_env_put_float(env, "zuk_healer_mage_attack_penalty_coeff", 0.0f); + inferno_zero_phase_reward_coeffs(env); + break; + case 1: + inferno_env_put_int(env, "joseph_reward_mode", 0); + break; + case 2: + inferno_env_put_int(env, "joseph_reward_mode", 1); + inferno_env_put_float(env, "shield_tag_reward_coeff", 0.0f); + inferno_zero_phase_reward_coeffs(env); + break; + default: + fprintf(stderr, "reward_profile must be in [0,2], got %d\n", reward_profile); + abort(); + } +} + +static void inferno_apply_curriculum(Env* env, Dict* kwargs) { + if ((int)dict_get(kwargs, "classic_curriculum_mode") != 1) + return; + + static const char* const wave_keys[] = { + "curriculum_wave_1", "curriculum_wave_2", "curriculum_wave_3", "curriculum_wave_4", + "curriculum_wave_5", "curriculum_wave_6", "curriculum_wave_7", "curriculum_wave_8", + }; + static const char* const frac_keys[] = { + "curriculum_frac_1", "curriculum_frac_2", "curriculum_frac_3", "curriculum_frac_4", + "curriculum_frac_5", "curriculum_frac_6", "curriculum_frac_7", "curriculum_frac_8", + }; + int waves[8]; + float fracs[8]; + int num_tiers = 0; + float total_frac = 0.0f; + for (int i = 0; i < 8; i++) { + DictItem* w = dict_find(kwargs, wave_keys[i]); + float f = (float)dict_get(kwargs, frac_keys[i]); + if (w && f > 0.0f) { + waves[num_tiers] = (int)w->value; + fracs[num_tiers] = f; + total_frac += f; + num_tiers++; + } + } + if (num_tiers == 0) + return; + + float u = (float)inf_lowbias32(inf_lowbias32((uint32_t)env->rng) ^ 0x9e3779b9U) + / 4294967296.0f; + float cursor = 1.0f - total_frac; + if (u < cursor) + return; + for (int t = 0; t < num_tiers; t++) { + if (u < cursor + fracs[t]) { + inferno_env_put_int(env, "start_wave", waves[t]); + inferno_env_put_int(env, "curriculum_agent", 1); + return; + } + cursor += fracs[t]; + } +} + +void puf_init(Env* env, Dict* kwargs) { + env->num_agents = 1; + env->agents[0].policy = 0; + ENCOUNTER_INFERNO.init_context(INF_ENV_CONTEXT(env)); + ENCOUNTER_INFERNO.init_state(INF_ENV_STATE(env), INF_ENV_CONTEXT(env)); + + uint32_t seed_offset = 0; + const char* seed_offset_str = getenv("PUFFER_ENV_SEED_OFFSET"); + if (seed_offset_str) + seed_offset = (uint32_t)strtoul(seed_offset_str, NULL, 10); + uint32_t env_seed = inf_lowbias32((uint32_t)env->rng + seed_offset); + if (env_seed == 0) + env_seed = 1; + env->state.rng_state = env_seed; + + memset(&env->log, 0, sizeof(Log)); + + int start_wave = (int)dict_get(kwargs, "start_wave"); + inferno_env_put_int(env, "start_wave", start_wave); + inferno_env_put_float(env, "damage_reward_coeff", + (float)dict_get(kwargs, "damage_reward_coeff")); + inferno_env_put_float(env, "shield_penalty_coeff", + (float)dict_get(kwargs, "shield_penalty_coeff")); + inferno_env_put_float(env, "tag_reward_coeff", + (float)dict_get(kwargs, "tag_reward_coeff")); + + static const char* const float_keys[] = { + "shield_tag_reward_coeff", + "budget_loadout_fraction", + "offensive_prayer_reward_coeff", + "death_penalty_coeff", + "phase_900_bonus", "phase_600_bonus", "phase_300_bonus", + "shield_penalty_episode_cap", + "supply_milestone_brew_reward_coeff", + "supply_milestone_restore_reward_coeff", + "jad_damage_reward_coeff", "zuk_healer_damage_reward_coeff", + "set_damage_reward_coeff", + "jad_kill_bonus", "zuk_healer_kill_bonus", "set_kill_bonus", + "post_healer_zuk_damage_coeff", "zuk_healer_phase_hp_delta_coeff", + "post_healer_set_damage_reward_coeff", "post_healer_set_kill_bonus", + "post_healer_set_alive_tick_penalty_coeff", + "post_healer_set_alive_penalty_cap", + "zuk_untagged_healer_tick_penalty_coeff", + "zuk_untagged_healer_target_bonus_coeff", + "zuk_safe_untagged_healer_target_bonus_coeff", + "zuk_untagged_healer_nonmagic_attack_bonus_coeff", + "zuk_healer_mage_attack_penalty_coeff", + "post_jad_zuk_multiplier", "jad_alive_zuk_multiplier", + "curriculum_supply_shared_jitter", + "curriculum_supply_brew_jitter", + "curriculum_supply_restore_jitter", + "curriculum_no_brew_frac", + }; + for (size_t k = 0; k < sizeof(float_keys) / sizeof(*float_keys); k++) + inferno_env_put_float(env, float_keys[k], (float)dict_get(kwargs, float_keys[k])); + + inferno_env_put_float(env, "late_start_supply_profile_scale", + (float)dict_get(kwargs, "late_start_supply_profile_scale")); + inferno_env_put_int(env, "oracle_mode", (int)dict_get(kwargs, "oracle_mode")); + inferno_env_put_int(env, "terminal_penalty_enabled", + (int)dict_get(kwargs, "terminal_penalty_enabled")); + + static const char* const int_keys[] = { + "curriculum_supply_jitter_mode", + "curriculum_no_brew_mode", + }; + for (size_t k = 0; k < sizeof(int_keys) / sizeof(*int_keys); k++) + inferno_env_put_int(env, int_keys[k], (int)dict_get(kwargs, int_keys[k])); + + inferno_env_put_int(env, "step_out_forecast_obs_mode", + (int)dict_get(kwargs, "step_out_forecast_obs_mode")); + inferno_env_put_int(env, "loadout_profile_mode", + (int)dict_get(kwargs, "loadout_profile_mode")); + inferno_env_put_int(env, "zuk_healer_reward_mode", + (int)dict_get(kwargs, "zuk_healer_reward_mode")); + inferno_env_put_int(env, "joseph_reward_mode", + (int)dict_get(kwargs, "joseph_reward_mode")); + inferno_apply_reward_profile(env, (int)dict_get(kwargs, "reward_profile")); + inferno_env_put_int(env, "zuk_safe_untagged_healer_target_mask", + (int)dict_get(kwargs, "zuk_safe_untagged_healer_target_mask")); + inferno_env_put_int(env, "zuk_force_safe_untagged_healer_target_mask", + (int)dict_get(kwargs, "zuk_force_safe_untagged_healer_target_mask")); + + env->config_start_wave = (start_wave > 0) ? start_wave - 1 : 0; + + inferno_apply_curriculum(env, kwargs); +} + +static inline void inferno_env_write_obs(Env* env) { + ENCOUNTER_INFERNO.write_obs(INF_ENV_STATE(env), INF_ENV_CONTEXT(env), + (float*)env->agents[0].observations); +} + +static inline void inferno_env_write_mask(Env* env) { + float mask_f[INF_ACTION_MASK_SIZE]; + ENCOUNTER_INFERNO.write_mask(INF_ENV_STATE(env), INF_ENV_CONTEXT(env), mask_f); + unsigned char* mask = env->agents[0].action_mask; + for (int i = 0; i < INF_ACTION_MASK_SIZE; i++) + mask[i] = mask_f[i] > 0.0f ? 1u : 0u; +} + +void puf_reset(Env* env) { + ENCOUNTER_INFERNO.reset(INF_ENV_STATE(env), INF_ENV_CONTEXT(env), 0); + inferno_env_write_obs(env); + inferno_env_write_mask(env); + env->agents[0].rewards[0] = 0.0f; + env->agents[0].terminals[0] = 0.0f; +} + +void puf_step(Env* env) { + int inf_prof_enabled = INF_PROFILE_ENABLED(); + double inf_prof_total_t0 = inf_prof_enabled ? INF_PROFILE_NOW_MS() : 0.0; + double inf_prof_t0 = inf_prof_total_t0; + + int acts[NUM_ATNS]; + for (int i = 0; i < NUM_ATNS; i++) + acts[i] = (int)env->agents[0].actions[i]; + INF_PROFILE_MARK(INF_PROF_C_ACTIONS); + + ENCOUNTER_INFERNO.step(INF_ENV_STATE(env), INF_ENV_CONTEXT(env), acts); + INF_PROFILE_MARK(INF_PROF_C_ENCOUNTER_STEP); + + inferno_env_write_obs(env); + INF_PROFILE_MARK(INF_PROF_C_WRITE_OBS); + inferno_env_write_mask(env); + INF_PROFILE_MARK(INF_PROF_C_WRITE_MASK); + + env->agents[0].rewards[0] = + ENCOUNTER_INFERNO.get_reward(INF_ENV_STATE(env), INF_ENV_CONTEXT(env)); + + int is_term = ENCOUNTER_INFERNO.is_terminal(INF_ENV_STATE(env), INF_ENV_CONTEXT(env)); + env->agents[0].terminals[0] = (float)is_term; + INF_PROFILE_MARK(INF_PROF_C_REWARD_TERMINAL); + + + if (is_term) { + InfernoState* s = INF_ENV_INFERNO(env); + float min_zuk_hp_term = (s->winner == INF_OUTCOME_PLAYER_WON) + ? 0.0f + : (s->min_zuk_hp_seen > 0.0f ? s->min_zuk_hp_seen : 1200.0f); + + if (s->start_wave != env->config_start_wave) goto skip_log; + + env->log.episode_return += s->episode_return; + env->log.episode_length += (float)s->tick; + env->log.damage_dealt += s->total_damage_dealt; + env->log.zuk_healer_damage += s->total_zuk_healer_damage; + env->log.damage_received += s->total_damage_received; + env->log.hp_restored += s->total_hp_restored; + env->log.wins += (s->winner == INF_OUTCOME_PLAYER_WON) ? 1.0f : 0.0f; + env->log.wave += (float)s->wave; + env->log.prayer_correct += (float)s->total_prayer_correct; + env->log.prayer_total += (float)s->total_npc_attacks; + env->log.offensive_prayer_attacks += + (float)s->total_offensive_prayer_attacks; + env->log.offensive_prayer_correct += + (float)s->total_offensive_prayer_correct; + for (int i = 0; i < 4; i++) { + env->log.offensive_prayer_attacks_by_style[i] += + (float)s->offensive_prayer_attacks_by_style[i]; + env->log.offensive_prayer_correct_by_style[i] += + (float)s->offensive_prayer_correct_by_style[i]; + } + env->log.idle_ticks += (float)s->total_idle_ticks; + env->log.attack_ready_no_attack_ticks += + (float)s->total_attack_ready_no_attack_ticks; + env->log.target_available_no_attack_ticks += + (float)s->total_target_available_no_attack_ticks; + env->log.safe_attack_opportunity_missed_ticks += + (float)s->total_safe_attack_opportunity_missed_ticks; + env->log.progressless_ticks += (float)s->total_progressless_ticks; + env->log.npc_pressure_if_ready_count += + s->total_npc_pressure_if_ready_count; + env->log.npc_pressure_this_tick_count += + s->total_npc_pressure_this_tick_count; + env->log.npc_pressure_max_incoming_hit += + s->max_npc_pressure_incoming_hit; + for (int i = 0; i < OSRS_INFERNO_IDLE_PHASE_COUNT; i++) { + env->log.attack_ready_no_attack_ticks_by_phase[i] += + (float)s->attack_ready_no_attack_ticks_by_phase[i]; + env->log.target_available_no_attack_ticks_by_phase[i] += + (float)s->target_available_no_attack_ticks_by_phase[i]; + env->log.safe_attack_opportunity_missed_ticks_by_phase[i] += + (float)s->safe_attack_opportunity_missed_ticks_by_phase[i]; + env->log.progressless_ticks_by_phase[i] += + (float)s->progressless_ticks_by_phase[i]; + } + env->log.brews_used += (float)s->total_brews_used; + env->log.brews_remaining += (float)s->player.brew_doses; + env->log.restores_remaining += (float)s->player.restore_doses; + env->log.npc_kills += (float)s->total_npc_kills; + env->log.start_wave += (float)env->config_start_wave; + + env->log.behind_shield_pct += (s->total_zuk_ticks > 0) + ? (float)s->behind_shield_ticks / (float)s->total_zuk_ticks : 0.0f; + + env->log.min_zuk_hp_seen += min_zuk_hp_term; + + env->log.n += 1.0f; + + { + env->log.min_zuk_hp_normal += min_zuk_hp_term; + env->log.n_normal += 1.0f; + int won = (s->winner == INF_OUTCOME_PLAYER_WON); + int phase_bucket = won ? 4 + : (min_zuk_hp_term <= 300.0f) ? 3 + : (min_zuk_hp_term <= 600.0f) ? 2 + : (min_zuk_hp_term <= 900.0f) ? 1 : 0; + env->log.phase_reached_normal_sum += (float)phase_bucket; + if (min_zuk_hp_term <= 240.0f) env->log.count_min_hp_le_240_normal += 1.0f; + + if (s->tick_at_all_zuk_healers_dead >= 0) { + env->log.count_all_zuk_healers_dead_normal += 1.0f; + float post_healer_survival = + (float)(s->tick - s->tick_at_all_zuk_healers_dead); + env->log.damage_after_all_zuk_healers_dead_normal_sum += + s->damage_after_all_zuk_healers_dead; + if (post_healer_survival >= 20.0f || won) + env->log.count_healer_resolved_20_normal += 1.0f; + } + env->log.post_healer_set_damage_reward_coeff_normal_sum += + INF_ENV_INFERNO_CONTEXT(env)->config.post_healer_set_damage_reward_coeff; + env->log.post_healer_set_kill_bonus_coeff_normal_sum += + INF_ENV_INFERNO_CONTEXT(env)->config.post_healer_set_kill_bonus; + env->log.post_healer_set_alive_penalty_coeff_normal_sum += + INF_ENV_INFERNO_CONTEXT(env)->config.post_healer_set_alive_tick_penalty_coeff; + env->log.post_healer_set_alive_penalty_cap_normal_sum += + INF_ENV_INFERNO_CONTEXT(env)->config.post_healer_set_alive_penalty_cap; + env->log.post_healer_set_damage_reward_normal_sum += + s->post_healer_set_damage_reward_total; + env->log.post_healer_set_kill_bonus_reward_normal_sum += + s->post_healer_set_kill_bonus_total; + env->log.post_healer_set_alive_penalty_normal_sum += + s->post_healer_set_alive_penalty_total; + env->log.post_healer_set_pressure_normal_sum += + s->post_healer_set_pressure_total; + env->log.action_mask_checks_normal_sum += + (float)s->total_action_mask_checks; + if (s->tick_at_le_240 >= 0) { + env->log.hp_restored_after_240_normal_sum += s->hp_restored_after_240; + env->log.spark_damage_after_240_normal_sum += s->spark_damage_after_240; + } + if (!won) { + int zuk_healer_alive = 0; + for (int n = 0; n < INF_MAX_NPCS; n++) { + if (s->npcs[n].hp <= 0) continue; + if (s->npcs[n].type == INF_NPC_HEALER_ZUK) zuk_healer_alive = 1; + } + if (zuk_healer_alive) env->log.count_died_with_zuk_healer_alive_normal += 1.0f; + if (s->tick_at_le_240 >= 0) + env->log.count_died_after_240_normal += 1.0f; + } + } + skip_log: + INF_PROFILE_MARK(INF_PROF_C_TERMINAL_LOG); + ENCOUNTER_INFERNO.reset(INF_ENV_STATE(env), INF_ENV_CONTEXT(env), 0); + inferno_env_write_obs(env); + inferno_env_write_mask(env); + INF_PROFILE_MARK(INF_PROF_C_RESET); + } + + if (inf_prof_enabled) + INF_PROFILE_ADD(INF_PROF_C_STEP_TOTAL, INF_PROFILE_NOW_MS() - inf_prof_total_t0); +} + +void puf_render(Env* env) { + (void)env; +} + +void puf_close(Env* env) { + ENCOUNTER_INFERNO.destroy_context(INF_ENV_CONTEXT(env)); +} + +static void inferno_log_idle_metric( + Dict* out, + const char* name, + float total, + const float by_phase[OSRS_INFERNO_IDLE_PHASE_COUNT] +) { + static const char* phases[OSRS_INFERNO_IDLE_PHASE_COUNT] = { + "set", + "jad", + "zuk_pre_jad", + "zuk_jad", + "zuk_healers", + "zuk_post_healers", + }; + /* dict_set keeps the key POINTER, so phase keys use a static pool (not a stack + buffer) to stay unique within one puf_log flush. */ + static char key_pool[64][96]; + static int key_pool_next = 0; + dict_set(out, name, total); + for (int i = 0; i < OSRS_INFERNO_IDLE_PHASE_COUNT; i++) { + char* key = key_pool[key_pool_next]; + key_pool_next = (key_pool_next + 1) & 63; + snprintf(key, sizeof(key_pool[0]), "%s_%s", name, phases[i]); + dict_set(out, key, by_phase[i]); + } +} + +void puf_log(Log* log, Dict* out) { + dict_set(out, "episode_return", log->episode_return); + dict_set(out, "damage_dealt", log->damage_dealt); + dict_set(out, "damage_received", log->damage_received); + dict_set(out, "episode_length", log->episode_length); + + float damage_per_tick = log->episode_length > 0.0f + ? log->damage_dealt / log->episode_length : 0.0f; + dict_set(out, "damage_per_100_ticks", damage_per_tick * 100.0f); + dict_set(out, "wins", log->wins); + dict_set(out, "wave", log->wave); + dict_set(out, "idle_ticks", log->idle_ticks); + inferno_log_idle_metric( + out, + "attack_ready_no_attack_ticks", + log->attack_ready_no_attack_ticks, + log->attack_ready_no_attack_ticks_by_phase); + inferno_log_idle_metric( + out, + "target_available_no_attack_ticks", + log->target_available_no_attack_ticks, + log->target_available_no_attack_ticks_by_phase); + inferno_log_idle_metric( + out, + "safe_attack_opportunity_missed_ticks", + log->safe_attack_opportunity_missed_ticks, + log->safe_attack_opportunity_missed_ticks_by_phase); + inferno_log_idle_metric( + out, + "progressless_ticks", + log->progressless_ticks, + log->progressless_ticks_by_phase); + float pressure_denom = log->episode_length > 0.0f + ? log->episode_length : 1.0f; + dict_set(out, "npc_pressure_if_ready_count_per_tick", + log->npc_pressure_if_ready_count / pressure_denom); + dict_set(out, "npc_pressure_this_tick_count_per_tick", + log->npc_pressure_this_tick_count / pressure_denom); + dict_set(out, "npc_pressure_max_incoming_hit", + log->npc_pressure_max_incoming_hit); + dict_set(out, "brews_used", log->brews_used); + + float prayer_rate = (log->prayer_total > 0.0f) + ? log->prayer_correct / log->prayer_total : 0.0f; + dict_set(out, "prayer_correct_rate", prayer_rate); + float offensive_prayer_rate = log->offensive_prayer_attacks > 0.0f + ? log->offensive_prayer_correct / log->offensive_prayer_attacks : 0.0f; + dict_set(out, "offensive_prayer_correct_rate", offensive_prayer_rate); + dict_set(out, "offensive_prayer_attacks", log->offensive_prayer_attacks); + dict_set(out, "offensive_prayer_ranged_correct_rate", + log->offensive_prayer_attacks_by_style[ATTACK_STYLE_RANGED] > 0.0f + ? log->offensive_prayer_correct_by_style[ATTACK_STYLE_RANGED] / + log->offensive_prayer_attacks_by_style[ATTACK_STYLE_RANGED] + : 0.0f); + dict_set(out, "offensive_prayer_magic_correct_rate", + log->offensive_prayer_attacks_by_style[ATTACK_STYLE_MAGIC] > 0.0f + ? log->offensive_prayer_correct_by_style[ATTACK_STYLE_MAGIC] / + log->offensive_prayer_attacks_by_style[ATTACK_STYLE_MAGIC] + : 0.0f); + dict_set(out, "brews_remaining", log->brews_remaining); + dict_set(out, "restores_remaining", log->restores_remaining); + dict_set(out, "behind_shield_pct", log->behind_shield_pct); + dict_set(out, "min_zuk_hp_seen", log->min_zuk_hp_seen); + dict_set(out, "hp_restored", log->hp_restored); + dict_set(out, "zuk_healer_damage", log->zuk_healer_damage); + + float wr = log->wins; + float score; + int start_wave = (int)(log->start_wave + 0.5f); + if (start_wave >= 68) { + score = (1200.0f - log->min_zuk_hp_seen) / 1200.0f; + } else { + float wave_frac = log->wave / (float)INF_NUM_WAVES; + score = wr + (1.0f - wr) * wave_frac * 0.5f; + } + dict_set(out, "score", score); + + if (log->n_normal > 0.0f) { + float min_zuk_hp_normal = log->min_zuk_hp_normal / log->n_normal; + float score_normal = (1200.0f - min_zuk_hp_normal) / 1200.0f; + float frac_all_healers_dead = + log->count_all_zuk_healers_dead_normal / log->n_normal; + float frac_died_after_240 = + log->count_died_after_240_normal / log->n_normal; + float post_healer_zuk_damage = + log->count_all_zuk_healers_dead_normal > 0.0f + ? log->damage_after_all_zuk_healers_dead_normal_sum / + log->count_all_zuk_healers_dead_normal : 0.0f; + float frac_died_with_zuk_healer = + log->count_died_with_zuk_healer_alive_normal / log->n_normal; + float healer_resolve = + log->count_healer_resolved_20_normal / log->n_normal; + dict_set(out, "score_normal", score_normal); + dict_set(out, "phase_reached_normal", + log->phase_reached_normal_sum / log->n_normal); + dict_set(out, "min_zuk_hp_normal", min_zuk_hp_normal); + dict_set(out, "frac_min_hp_le_240_normal", + log->count_min_hp_le_240_normal / log->n_normal); + dict_set(out, "frac_all_zuk_healers_dead_normal", frac_all_healers_dead); + dict_set(out, "frac_died_after_240_normal", frac_died_after_240); + dict_set(out, "post_healer_objective_normal", + healer_resolve + 0.001f * post_healer_zuk_damage - + 0.1f * frac_died_with_zuk_healer); + dict_set(out, "spark_damage_after_240_normal", + log->spark_damage_after_240_normal_sum / log->n_normal); + dict_set(out, "hp_restored_after_240_normal", + log->hp_restored_after_240_normal_sum / log->n_normal); + } else { + dict_set(out, "score_normal", 0.0f); + dict_set(out, "phase_reached_normal", 0.0f); + dict_set(out, "min_zuk_hp_normal", 1200.0f); + dict_set(out, "frac_min_hp_le_240_normal", 0.0f); + dict_set(out, "frac_all_zuk_healers_dead_normal", 0.0f); + dict_set(out, "frac_died_after_240_normal", 0.0f); + dict_set(out, "post_healer_objective_normal", 0.0f); + dict_set(out, "spark_damage_after_240_normal", 0.0f); + dict_set(out, "hp_restored_after_240_normal", 0.0f); + } +} + +#endif diff --git a/ocean/osrs_inferno/replay_best.h b/ocean/osrs_inferno/replay_best.h new file mode 100644 index 0000000000..d06bd4e94a --- /dev/null +++ b/ocean/osrs_inferno/replay_best.h @@ -0,0 +1,54 @@ +#ifndef OSRS_INFERNO_REPLAY_BEST_H +#define OSRS_INFERNO_REPLAY_BEST_H + +#include + +typedef struct { + int wave; + int ticks; + int min_zuk_hp; + uint32_t rng_seed; +} InfernoReplayBest; + +static inline InfernoReplayBest inferno_replay_best_initial(void) { + return (InfernoReplayBest){ + .wave = 0, + .ticks = 999999, + .min_zuk_hp = 999999, + .rng_seed = UINT32_MAX, + }; +} + +static inline int inferno_replay_is_better( + const InfernoReplayBest* best, + int start_wave, + int wave, + int ticks, + int min_zuk_hp, + uint32_t rng_seed +) { + if (start_wave == 0) { + if (wave != best->wave) return wave > best->wave; + if (ticks != best->ticks) return ticks < best->ticks; + return rng_seed < best->rng_seed; + } + + if (min_zuk_hp != best->min_zuk_hp) return min_zuk_hp < best->min_zuk_hp; + if (min_zuk_hp == 0 && ticks != best->ticks) return ticks < best->ticks; + return rng_seed < best->rng_seed; +} + +static inline void inferno_replay_best_apply( + InfernoReplayBest* best, + int wave, + int ticks, + int min_zuk_hp, + uint32_t rng_seed +) { + best->wave = wave; + best->ticks = ticks; + best->min_zuk_hp = min_zuk_hp; + best->rng_seed = rng_seed; +} + +#endif diff --git a/src/algo.cu b/src/algo.cu index 973ebf3669..e5dcb9d00b 100644 --- a/src/algo.cu +++ b/src/algo.cu @@ -1548,7 +1548,7 @@ __device__ __forceinline__ float safe_continuous_logstd(const precision_t* logst // Fused loss function. PPO clipped loss + value + entropy constexpr int PPO_THREADS = 256; -constexpr int MAX_ATN_HEADS = 16; // TODO: use env atn dim directly +constexpr int MAX_ATN_HEADS = NUM_ATNS; enum LossIdx { LOSS_PG = 0, LOSS_VF = 1, LOSS_ENT = 2, LOSS_TOTAL = 3, diff --git a/src/ocean.cu b/src/ocean.cu index df90453b89..6ca617ad38 100644 --- a/src/ocean.cu +++ b/src/ocean.cu @@ -1260,7 +1260,454 @@ static void* nmmo3_encoder_create_weights(void* self) { static void nmmo3_encoder_free_weights(void* weights) { free(weights); } static void nmmo3_encoder_free_activations(void* activations) { free(activations); } -// Override encoder vtable for known ocean environments. No-op for unknown envs. +// ---- Colosseum entity encoder ---- +static constexpr int COLO_ENT_NPC_START = 1030; +static constexpr int COLO_ENT_NUM_NPCS = 24; +static constexpr int COLO_ENT_FEATS = 37; +static constexpr int COLO_ENT_TYPE_ONEHOT = 12; +static constexpr int COLO_ENT_BOTTLENECK = 16; +static constexpr int COLO_ENT_NPC_BLOCK = COLO_ENT_NUM_NPCS * COLO_ENT_FEATS; +static constexpr int COLO_ENT_INV_START = 48; +static constexpr int COLO_ENT_INV_NUM_CELLS = 28; +static constexpr int COLO_ENT_INV_FEATS = 28; +static constexpr int COLO_ENT_INV_PRESENT = 0; +static constexpr int COLO_ENT_INV_BOTTLENECK = 16; +static constexpr int COLO_ENT_INV_BLOCK = COLO_ENT_INV_NUM_CELLS * COLO_ENT_INV_FEATS; + +struct ColosseumEntityEncoderWeights { + PrecisionTensor global_w; + PrecisionTensor entity_l1_w; + PrecisionTensor entity_l2_w; + PrecisionTensor inv_l1_w; + PrecisionTensor inv_l2_w; + int obs_size, hidden; +}; + +struct ColosseumEntityEncoderActivations { + PrecisionTensor out; + PrecisionTensor saved_obs; + PrecisionTensor npc_flat; + PrecisionTensor entity_z1; + PrecisionTensor entity_h1; + PrecisionTensor grad_z1; + IntTensor pool_argmax; + PrecisionTensor global_wgrad; + PrecisionTensor entity_l1_wgrad; + PrecisionTensor entity_l2_wgrad; + PrecisionTensor inv_flat; + PrecisionTensor inv_z1; + PrecisionTensor inv_h1; + PrecisionTensor inv_grad_z1; + IntTensor inv_pool_argmax; + PrecisionTensor inv_l1_wgrad; + PrecisionTensor inv_l2_wgrad; +}; + +__global__ void colo_ent_gather_npcs( + precision_t* __restrict__ npc_flat, const precision_t* __restrict__ obs, + int B, int obs_size) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + int total = B * COLO_ENT_NPC_BLOCK; + if (idx >= total) return; + int b = idx / COLO_ENT_NPC_BLOCK; + int off = idx % COLO_ENT_NPC_BLOCK; + npc_flat[idx] = obs[(int64_t)b * obs_size + COLO_ENT_NPC_START + off]; +} + +__device__ __forceinline__ float colo_ent_gelu_fwd(float x) { + float inner = 0.7978845608028654f * (x + 0.044715f * x * x * x); + return 0.5f * x * (1.0f + tanhf(inner)); +} + +__device__ __forceinline__ float colo_ent_gelu_grad(float x) { + float x3 = x * x * x; + float inner = 0.7978845608028654f * (x + 0.044715f * x3); + float t = tanhf(inner); + float dinner = 0.7978845608028654f * (1.0f + 3.0f * 0.044715f * x * x); + return 0.5f * (1.0f + t) + 0.5f * x * (1.0f - t * t) * dinner; +} + +__global__ void colo_ent_gather_inv( + precision_t* __restrict__ inv_flat, const precision_t* __restrict__ obs, + int B, int obs_size) { + int idx = blockIdx.x * blockDim.x + threadIdx.x; + int total = B * COLO_ENT_INV_BLOCK; + if (idx >= total) return; + int b = idx / COLO_ENT_INV_BLOCK; + int off = idx % COLO_ENT_INV_BLOCK; + inv_flat[idx] = obs[(int64_t)b * obs_size + COLO_ENT_INV_START + off]; +} + +// ---- fused pool kernels ---- +static constexpr int COLO_ENT_BATCH_TILE = 8; +static constexpr int COLO_ENT_HIDDEN_TILE = 32; +static constexpr int COLO_ENT_FC_THREADS = COLO_ENT_BATCH_TILE * COLO_ENT_HIDDEN_TILE; + +__global__ void colo_ent_fused_pool_fwd( + precision_t* __restrict__ out, int* __restrict__ argmax, + precision_t* __restrict__ h1, + const precision_t* __restrict__ z1, const precision_t* __restrict__ rec_flat, + const precision_t* __restrict__ l2_w, + int B, int H, int num_rec, int rec_feats, int active_width) { + extern __shared__ float colo_ent_sh[]; + float* h1_tile = colo_ent_sh; + float* mask_tile = h1_tile + COLO_ENT_BATCH_TILE * num_rec * COLO_ENT_BOTTLENECK; + float* w_tile = mask_tile + COLO_ENT_BATCH_TILE * num_rec; + + int batch_base = blockIdx.x * COLO_ENT_BATCH_TILE; + int hidden_base = blockIdx.y * COLO_ENT_HIDDEN_TILE; + int tx = threadIdx.x; + int ty = threadIdx.y; + int tid = ty * COLO_ENT_HIDDEN_TILE + tx; + + int h1_values = COLO_ENT_BATCH_TILE * num_rec * COLO_ENT_BOTTLENECK; + for (int idx = tid; idx < h1_values; idx += COLO_ENT_FC_THREADS) { + int bt = idx / (num_rec * COLO_ENT_BOTTLENECK); + int rem = idx - bt * num_rec * COLO_ENT_BOTTLENECK; + int gb = batch_base + bt; + float v = gb < B + ? colo_ent_gelu_fwd(to_float(z1[(int64_t)gb * num_rec * COLO_ENT_BOTTLENECK + rem])) + : 0.0f; + h1_tile[idx] = v; + if (h1 && blockIdx.y == 0 && gb < B) + h1[(int64_t)gb * num_rec * COLO_ENT_BOTTLENECK + rem] = from_float(v); + } + int mask_values = COLO_ENT_BATCH_TILE * num_rec; + for (int idx = tid; idx < mask_values; idx += COLO_ENT_FC_THREADS) { + int bt = idx / num_rec; + int n = idx - bt * num_rec; + int gb = batch_base + bt; + float active = 0.0f; + if (gb < B) { + const precision_t* rec = rec_flat + ((int64_t)gb * num_rec + n) * rec_feats; + for (int t = 0; t < active_width; t++) active += to_float(rec[t]); + } + mask_tile[idx] = active; + } + int w_values = COLO_ENT_HIDDEN_TILE * COLO_ENT_BOTTLENECK; + for (int idx = tid; idx < w_values; idx += COLO_ENT_FC_THREADS) { + int d = idx / COLO_ENT_HIDDEN_TILE; + int th = idx - d * COLO_ENT_HIDDEN_TILE; + int gh = hidden_base + th; + w_tile[idx] = gh < H + ? to_float(l2_w[(int64_t)gh * COLO_ENT_BOTTLENECK + d]) + : 0.0f; + } + __syncthreads(); + + int b = batch_base + ty; + int h = hidden_base + tx; + if (b >= B || h >= H) return; + float best = -3.4028234663852886e38f; + int best_n = -1; + for (int n = 0; n < num_rec; n++) { + if (mask_tile[ty * num_rec + n] <= 0.0f) continue; + const float* hp = h1_tile + (ty * num_rec + n) * COLO_ENT_BOTTLENECK; + float sum = 0.0f; +#pragma unroll + for (int d = 0; d < COLO_ENT_BOTTLENECK; d++) + sum += w_tile[d * COLO_ENT_HIDDEN_TILE + tx] * hp[d]; + if (sum > best) { best = sum; best_n = n; } + } + int64_t o = (int64_t)b * H + h; + out[o] = from_float(to_float(out[o]) + (best_n < 0 ? 0.0f : best)); + argmax[o] = best_n; +} + +__global__ void colo_ent_fused_l2_wgrad( + precision_t* __restrict__ wgrad, const precision_t* __restrict__ grad, + const precision_t* __restrict__ h1, const int* __restrict__ argmax, + int B, int H, int num_rec) { + int h = blockIdx.x; + if (h >= H) return; + + float sum[COLO_ENT_BOTTLENECK]; +#pragma unroll + for (int k = 0; k < COLO_ENT_BOTTLENECK; k++) sum[k] = 0.0f; + + for (int b = threadIdx.x; b < B; b += blockDim.x) { + int n = argmax[(int64_t)b * H + h]; + if (n < 0) continue; + float g = to_float(grad[(int64_t)b * H + h]); + const precision_t* hp = h1 + ((int64_t)b * num_rec + n) * COLO_ENT_BOTTLENECK; +#pragma unroll + for (int k = 0; k < COLO_ENT_BOTTLENECK; k++) + sum[k] += g * to_float(hp[k]); + } + + __shared__ float warp_sums[COLO_ENT_BOTTLENECK * 32]; + int lane = threadIdx.x & 31; + int warp = threadIdx.x >> 5; + int num_warps = (blockDim.x + 31) >> 5; +#pragma unroll + for (int k = 0; k < COLO_ENT_BOTTLENECK; k++) { + float s = sum[k]; + for (int offset = 16; offset > 0; offset >>= 1) + s += __shfl_down_sync(0xffffffff, s, offset); + if (lane == 0) warp_sums[k * 32 + warp] = s; + } + __syncthreads(); + + if (warp == 0) { +#pragma unroll + for (int k = 0; k < COLO_ENT_BOTTLENECK; k++) { + float s = lane < num_warps ? warp_sums[k * 32 + lane] : 0.0f; + for (int offset = 16; offset > 0; offset >>= 1) + s += __shfl_down_sync(0xffffffff, s, offset); + if (lane == 0) wgrad[(int64_t)h * COLO_ENT_BOTTLENECK + k] = from_float(s); + } + } +} + +__global__ void colo_ent_fused_grad_z1( + precision_t* __restrict__ grad_z1, const precision_t* __restrict__ grad, + const precision_t* __restrict__ l2_w, const precision_t* __restrict__ z1, + const int* __restrict__ argmax, int B, int H, int num_rec) { + int b = blockIdx.x; + if (b >= B) return; + + extern __shared__ float colo_ent_sh[]; + float* accum = colo_ent_sh; + int* arg_s = (int*)(accum + num_rec * COLO_ENT_BOTTLENECK); + float* grad_s = (float*)(arg_s + blockDim.x); + + for (int idx = threadIdx.x; idx < num_rec * COLO_ENT_BOTTLENECK; idx += blockDim.x) + accum[idx] = 0.0f; + __syncthreads(); + + for (int base = 0; base < H; base += blockDim.x) { + int h = base + threadIdx.x; + if (h < H) { + arg_s[threadIdx.x] = argmax[(int64_t)b * H + h]; + grad_s[threadIdx.x] = to_float(grad[(int64_t)b * H + h]); + } + __syncthreads(); + + int tile = H - base; + if (tile > (int)blockDim.x) tile = blockDim.x; + for (int idx = threadIdx.x; idx < tile * COLO_ENT_BOTTLENECK; idx += blockDim.x) { + int j = idx / COLO_ENT_BOTTLENECK; + int k = idx - j * COLO_ENT_BOTTLENECK; + int n = arg_s[j]; + if (n < 0) continue; + float g = grad_s[j] * to_float(l2_w[(int64_t)(base + j) * COLO_ENT_BOTTLENECK + k]); + atomicAdd(&accum[n * COLO_ENT_BOTTLENECK + k], g); + } + __syncthreads(); + } + + for (int idx = threadIdx.x; idx < num_rec * COLO_ENT_BOTTLENECK; idx += blockDim.x) { + int64_t o = (int64_t)b * num_rec * COLO_ENT_BOTTLENECK + idx; + grad_z1[o] = from_float(accum[idx] * colo_ent_gelu_grad(to_float(z1[o]))); + } +} + +static void colo_ent_launch_fused_fwd( + precision_t* out, int* argmax, precision_t* h1, + const precision_t* z1, const precision_t* rec_flat, + const precision_t* l2_w, int B, int H, int num_rec, int rec_feats, int active_width, + cudaStream_t stream) { + dim3 block(COLO_ENT_HIDDEN_TILE, COLO_ENT_BATCH_TILE); + dim3 grid((B + COLO_ENT_BATCH_TILE - 1) / COLO_ENT_BATCH_TILE, + (H + COLO_ENT_HIDDEN_TILE - 1) / COLO_ENT_HIDDEN_TILE); + size_t shared_bytes = ( + (size_t)COLO_ENT_BATCH_TILE * num_rec * COLO_ENT_BOTTLENECK + + (size_t)COLO_ENT_BATCH_TILE * num_rec + + (size_t)COLO_ENT_HIDDEN_TILE * COLO_ENT_BOTTLENECK) * sizeof(float); + colo_ent_fused_pool_fwd<<>>( + out, argmax, h1, z1, rec_flat, l2_w, B, H, num_rec, rec_feats, active_width); +} + +static void colo_ent_launch_fused_bwd( + precision_t* l2_wgrad, precision_t* grad_z1, const precision_t* grad, + const precision_t* l2_w, const precision_t* z1, const precision_t* h1, + const int* argmax, int B, int H, int num_rec, cudaStream_t stream) { + colo_ent_fused_l2_wgrad<<>>( + l2_wgrad, grad, h1, argmax, B, H, num_rec); + size_t shared_bytes = + ((size_t)num_rec * COLO_ENT_BOTTLENECK + 2 * BLOCK_SIZE) * sizeof(float); + colo_ent_fused_grad_z1<<>>( + grad_z1, grad, l2_w, z1, argmax, B, H, num_rec); +} + +static PrecisionTensor colo_entity_encoder_forward(void* w, void* activations, PrecisionTensor input, cudaStream_t stream) { + ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; + ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; + int B = input.shape[0]; + int H = ew->hidden; + int NB = B * COLO_ENT_NUM_NPCS; + + if (a->saved_obs.data) puf_copy(&a->saved_obs, &input, stream); + + puf_mm(&input, &ew->global_w, &a->out, stream); + + colo_ent_gather_npcs<<>>( + a->npc_flat.data, input.data, B, ew->obs_size); + + PrecisionTensor npc2d = {.data = a->npc_flat.data, .shape = {NB, COLO_ENT_FEATS}}; + puf_mm(&npc2d, &ew->entity_l1_w, &a->entity_z1, stream); + colo_ent_launch_fused_fwd( + a->out.data, a->pool_argmax.data, a->entity_h1.data, + a->entity_z1.data, a->npc_flat.data, + ew->entity_l2_w.data, B, H, COLO_ENT_NUM_NPCS, COLO_ENT_FEATS, + COLO_ENT_TYPE_ONEHOT, stream); + + int IB = B * COLO_ENT_INV_NUM_CELLS; + colo_ent_gather_inv<<>>( + a->inv_flat.data, input.data, B, ew->obs_size); + PrecisionTensor inv2d = {.data = a->inv_flat.data, .shape = {IB, COLO_ENT_INV_FEATS}}; + puf_mm(&inv2d, &ew->inv_l1_w, &a->inv_z1, stream); + static_assert(COLO_ENT_INV_PRESENT == 0, + "fused pool mask reads a prefix; present flag must be cell-local offset 0"); + colo_ent_launch_fused_fwd( + a->out.data, a->inv_pool_argmax.data, a->inv_h1.data, + a->inv_z1.data, a->inv_flat.data, + ew->inv_l2_w.data, B, H, COLO_ENT_INV_NUM_CELLS, COLO_ENT_INV_FEATS, + 1, stream); + return a->out; +} + +static void colo_entity_encoder_backward(void* w, void* activations, PrecisionTensor grad, cudaStream_t stream) { + ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; + ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; + int B = grad.shape[0]; + int H = ew->hidden; + int NB = B * COLO_ENT_NUM_NPCS; + + puf_mm_tn(&grad, &a->saved_obs, &a->global_wgrad, stream); + + colo_ent_launch_fused_bwd( + a->entity_l2_wgrad.data, a->grad_z1.data, grad.data, + ew->entity_l2_w.data, a->entity_z1.data, a->entity_h1.data, + a->pool_argmax.data, B, H, COLO_ENT_NUM_NPCS, stream); + PrecisionTensor npc2d = {.data = a->npc_flat.data, .shape = {NB, COLO_ENT_FEATS}}; + puf_mm_tn(&a->grad_z1, &npc2d, &a->entity_l1_wgrad, stream); + + int IB = B * COLO_ENT_INV_NUM_CELLS; + colo_ent_launch_fused_bwd( + a->inv_l2_wgrad.data, a->inv_grad_z1.data, grad.data, + ew->inv_l2_w.data, a->inv_z1.data, a->inv_h1.data, + a->inv_pool_argmax.data, B, H, COLO_ENT_INV_NUM_CELLS, stream); + PrecisionTensor inv2d = {.data = a->inv_flat.data, .shape = {IB, COLO_ENT_INV_FEATS}}; + puf_mm_tn(&a->inv_grad_z1, &inv2d, &a->inv_l1_wgrad, stream); +} + +static void colo_entity_encoder_init_weights(void* w, uint64_t* seed, cudaStream_t stream) { + ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; + auto init2d = [&](PrecisionTensor& t, int rows, int cols) { + PrecisionTensor wt = {.data = t.data, .shape = {rows, cols}}; + puf_kaiming_init(&wt, std::sqrt(2.0f), (*seed)++, stream); + }; + init2d(ew->global_w, ew->hidden, ew->obs_size); + init2d(ew->entity_l1_w, COLO_ENT_BOTTLENECK, COLO_ENT_FEATS); + init2d(ew->entity_l2_w, ew->hidden, COLO_ENT_BOTTLENECK); + init2d(ew->inv_l1_w, COLO_ENT_INV_BOTTLENECK, COLO_ENT_INV_FEATS); + init2d(ew->inv_l2_w, ew->hidden, COLO_ENT_INV_BOTTLENECK); +} + +static void colo_entity_assert_aligned(int64_t numel, const char* name) { + if (numel % 8 != 0) { + fprintf(stderr, "colosseum entity encoder: %s numel %lld not a multiple of 8; " + "bf16 packing would corrupt weights\n", name, (long long)numel); + abort(); + } +} + +static void colo_entity_encoder_reg_params(void* w, Allocator* alloc) { + ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; + ew->global_w = {.shape = {ew->hidden, ew->obs_size}}; + ew->entity_l1_w = {.shape = {COLO_ENT_BOTTLENECK, COLO_ENT_FEATS}}; + ew->entity_l2_w = {.shape = {ew->hidden, COLO_ENT_BOTTLENECK}}; + colo_entity_assert_aligned(numel(ew->global_w.shape), "global_w"); + colo_entity_assert_aligned(numel(ew->entity_l1_w.shape), "entity_l1_w"); + colo_entity_assert_aligned(numel(ew->entity_l2_w.shape), "entity_l2_w"); + alloc_register(alloc, &ew->global_w); + alloc_register(alloc, &ew->entity_l1_w); + alloc_register(alloc, &ew->entity_l2_w); + ew->inv_l1_w = {.shape = {COLO_ENT_INV_BOTTLENECK, COLO_ENT_INV_FEATS}}; + ew->inv_l2_w = {.shape = {ew->hidden, COLO_ENT_INV_BOTTLENECK}}; + colo_entity_assert_aligned(numel(ew->inv_l1_w.shape), "inv_l1_w"); + colo_entity_assert_aligned(numel(ew->inv_l2_w.shape), "inv_l2_w"); + alloc_register(alloc, &ew->inv_l1_w); + alloc_register(alloc, &ew->inv_l2_w); +} + +static void colo_entity_encoder_reg_train(void* w, void* activations, Allocator* acts, Allocator* grads, int B_TT) { + ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; + ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; + int H = ew->hidden; + int NB = B_TT * COLO_ENT_NUM_NPCS; + *a = {}; + a->out = {.shape = {B_TT, H}}; + a->saved_obs = {.shape = {B_TT, ew->obs_size}}; + a->npc_flat = {.shape = {NB, COLO_ENT_FEATS}}; + a->entity_z1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; + a->entity_h1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; + a->grad_z1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; + a->pool_argmax = {.shape = {B_TT, H}}; + alloc_register(acts, &a->out); + alloc_register(acts, &a->saved_obs); + alloc_register(acts, &a->npc_flat); + alloc_register(acts, &a->entity_z1); + alloc_register(acts, &a->entity_h1); + alloc_register(acts, &a->grad_z1); + alloc_register(acts, &a->pool_argmax); + int IB = B_TT * COLO_ENT_INV_NUM_CELLS; + a->inv_flat = {.shape = {IB, COLO_ENT_INV_FEATS}}; + a->inv_z1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; + a->inv_h1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; + a->inv_grad_z1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; + a->inv_pool_argmax = {.shape = {B_TT, H}}; + alloc_register(acts, &a->inv_flat); + alloc_register(acts, &a->inv_z1); + alloc_register(acts, &a->inv_h1); + alloc_register(acts, &a->inv_grad_z1); + alloc_register(acts, &a->inv_pool_argmax); + a->global_wgrad = {.shape = {H, ew->obs_size}}; + a->entity_l1_wgrad = {.shape = {COLO_ENT_BOTTLENECK, COLO_ENT_FEATS}}; + a->entity_l2_wgrad = {.shape = {H, COLO_ENT_BOTTLENECK}}; + alloc_register(grads, &a->global_wgrad); + alloc_register(grads, &a->entity_l1_wgrad); + alloc_register(grads, &a->entity_l2_wgrad); + a->inv_l1_wgrad = {.shape = {COLO_ENT_INV_BOTTLENECK, COLO_ENT_INV_FEATS}}; + a->inv_l2_wgrad = {.shape = {H, COLO_ENT_INV_BOTTLENECK}}; + alloc_register(grads, &a->inv_l1_wgrad); + alloc_register(grads, &a->inv_l2_wgrad); +} + +static void colo_entity_encoder_reg_rollout(void* w, void* activations, Allocator* alloc, int B) { + ColosseumEntityEncoderWeights* ew = (ColosseumEntityEncoderWeights*)w; + ColosseumEntityEncoderActivations* a = (ColosseumEntityEncoderActivations*)activations; + int H = ew->hidden; + int NB = B * COLO_ENT_NUM_NPCS; + a->out = {.shape = {B, H}}; + a->npc_flat = {.shape = {NB, COLO_ENT_FEATS}}; + a->entity_z1 = {.shape = {NB, COLO_ENT_BOTTLENECK}}; + a->pool_argmax = {.shape = {B, H}}; + alloc_register(alloc, &a->out); + alloc_register(alloc, &a->npc_flat); + alloc_register(alloc, &a->entity_z1); + alloc_register(alloc, &a->pool_argmax); + int IB = B * COLO_ENT_INV_NUM_CELLS; + a->inv_flat = {.shape = {IB, COLO_ENT_INV_FEATS}}; + a->inv_z1 = {.shape = {IB, COLO_ENT_INV_BOTTLENECK}}; + a->inv_pool_argmax = {.shape = {B, H}}; + alloc_register(alloc, &a->inv_flat); + alloc_register(alloc, &a->inv_z1); + alloc_register(alloc, &a->inv_pool_argmax); +} + +static void* colo_entity_encoder_create_weights(void* self) { + Encoder* e = (Encoder*)self; + ColosseumEntityEncoderWeights* ew = + (ColosseumEntityEncoderWeights*)calloc(1, sizeof(ColosseumEntityEncoderWeights)); + ew->obs_size = e->in_dim; + ew->hidden = e->out_dim; + return ew; +} +static void colo_entity_encoder_free_weights(void* weights) { free(weights); } +static void colo_entity_encoder_free_activations(void* activations) { free(activations); } + static void create_custom_encoder(const char* env_name, Encoder* enc) { if (strcmp(env_name, "nmmo3") == 0) { *enc = Encoder{ @@ -1290,5 +1737,19 @@ static void create_custom_encoder(const char* env_name, Encoder* enc) { .in_dim = enc->in_dim, .out_dim = enc->out_dim, .activation_size = sizeof(MinimalEntityEncoderActivations), }; + } else if (strcmp(env_name, "osrs_colosseum") == 0) { + *enc = Encoder{ + .forward = colo_entity_encoder_forward, + .backward = colo_entity_encoder_backward, + .init_weights = colo_entity_encoder_init_weights, + .reg_params = colo_entity_encoder_reg_params, + .reg_train = colo_entity_encoder_reg_train, + .reg_rollout = colo_entity_encoder_reg_rollout, + .create_weights = colo_entity_encoder_create_weights, + .free_weights = colo_entity_encoder_free_weights, + .free_activations = colo_entity_encoder_free_activations, + .in_dim = enc->in_dim, .out_dim = enc->out_dim, + .activation_size = sizeof(ColosseumEntityEncoderActivations), + }; } }