mirror of
https://github.com/fallout2-ce/fallout2-ce.git
synced 2026-07-27 16:47:11 -07:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9936e10ff | ||
|
|
6bd1daae0b | ||
|
|
d8336761cc | ||
|
|
5055e2b0d4 | ||
|
|
a0954c8df7 | ||
|
|
7abab85e0a | ||
|
|
511d53a615 | ||
|
|
f17dd19116 | ||
|
|
c28c62727f | ||
|
|
b14f518714 | ||
|
|
6a0bed4433 | ||
|
|
ce27eb55a1 | ||
|
|
c240657d0d | ||
|
|
0a73abe67d | ||
|
|
54228248ef | ||
|
|
4eba423399 | ||
|
|
de98136699 | ||
|
|
b27b0f03fc | ||
|
|
9d6f86424e | ||
|
|
bb65d8d32a | ||
|
|
c9139f762d | ||
|
|
beb3c537a4 | ||
|
|
b71f1e7d18 | ||
|
|
b7f2b32b92 | ||
|
|
75776e4b5b | ||
|
|
5304823f8f | ||
|
|
5860ee2ffd | ||
|
|
550e6f035a | ||
|
|
26978dedc5 | ||
|
|
47434c4038 | ||
|
|
23e9eab72d |
@@ -289,6 +289,8 @@ target_sources(${EXECUTABLE_NAME} PUBLIC
|
||||
"src/sfall_script_hooks.cc"
|
||||
"src/sfall_arrays.cc"
|
||||
"src/sfall_arrays.h"
|
||||
"src/sfall_animation.cc"
|
||||
"src/sfall_animation.h"
|
||||
"src/sfall_callbacks.cc"
|
||||
"src/sfall_callbacks.h"
|
||||
"src/touch.cc"
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# Sfall Compatibility
|
||||
|
||||
This document tracks Fallout 2 CE compatibility with sfall. This is for modders who need to know which Sfall features work in CE.
|
||||
|
||||
For now, this covers opcodes/metarules, and hooks. In the future, it will include other ways of modifying the engine (like ini files), and other Sfall-specific behaviour.
|
||||
|
||||
## Opcodes / Metarules
|
||||
|
||||
See [`https://sfall-team.github.io/sfall/`](https://sfall-team.github.io/sfall/) for documentation on specific functions.
|
||||
|
||||
| Group | Opcodes In Group | Compatibility | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| Direct memory access| read_byte,short,int,string<br>write_byte,short,int,string<br>call_offset_vX | đźš« | Not possible. Open an issue if you need functionality not covered by other opcodes. |
|
||||
| Stats | get/set_pc_base_stat<br>get/set_pc_extra_stat<br>get/set_critter_base_stat<br>get/set_critter_extra_stat | âś… | CE uses engine stat helpers here instead of sfall's direct proto-field behavior, so derived-stat update behavior can differ. |
|
||||
| Stats / Alter min/max | get/set_stat_min/max<br>set_pc_stat_min/max<br>set_npc_stat_min/max | not implemented | - |
|
||||
| Skills | get/set_critter_skill_points<br>get/set_available_skill_points<br>set_skill_max<br>set_critter_skill_mod<br>set_base_skill_mod<br>mod_skill_points_per_level | not implemented | - |
|
||||
| Graphics | graphics_funcs_available<br>force_graphics_refresh<br>get_screen_width<br>get_screen_height<br>set_palette | implemented: only get_screen_width, get_screen_height | - |
|
||||
| Shaders | load_shader<br>free_shader<br>activate_shader<br>deactivate_shader<br>set/get_shader_* | đźš« | likely will not implement direct compatibility
|
||||
| Perks and traits | set_perk_image<br>set_perk_*<br>set_pyromaniac_mod<br>apply_heaveho_fix<br>set_swiftlearner_mod<br>set_fake_perk<br>set_fake_trait<br>set_selectable_perk<br>set_perkbox_title<br>hide_real_perks<br>show_real_perks<br>perk_add_mode<br>clear_selectable_perks<br>has_fake_perk<br>has_fake_trait<br>add_trait<br>remove_trait<br>seq_perk_freq | not implemented | - |
|
||||
| Virtual file system | fs_create<br>fs_copy<br>fs_find<br>fs_read/write_*<br>fs_delete<br>fs_size<br>fs_pos<br>fs_seek<br>fs_resize | đźš« | Open an issue if you have a use case for these |
|
||||
| Combat / Knockback | set_weapon_knockback<br>set_target_knockback<br>set_attacker_knockback<br>remove_weapon_knockback<br>remove_target_knockback<br>remove_attacker_knockback | not implemented | - |
|
||||
| Maps and encounters | in_world_map<br>force_encounter<br>force_encounter_with_flags<br>set_map_time_multi<br>get/set_map_enter_position<br>exec_map_update_scripts<br>get/set_terrain_name<br>set_town_title<br>get/set_can_rest_on_map<br>set_rest_heal_time<br>set_rest_mode<br>set_worldmap_heal_time | implemented: in_world_map, force_encounter, force_encounter_with_flags, set_map_time_multi | - |
|
||||
| Maps and encounters / Worldmap | get_world_map_x/y_pos<br>set_world_map_pos | âś… | - |
|
||||
| Audio | eax_available<br>set_eax_environment<br>play_sfall_sound<br>stop_sfall_sound | not implemented | *eax* opcodes will not be implemented |
|
||||
| Combat / Weapons and ammo | get/set_weapon_ammo_pid<br>get/set_weapon_ammo_count | âś… | - |
|
||||
| Sfall / Version | sfall_ver_major<br>sfall_ver_minor<br>sfall_ver_build | âś… | CE currently reports `4.3.4` |
|
||||
| Utility / Math | log, exponent, round, sqrt, abs, sin, cos, tan, arctan, ceil, ^, floor2, div | âś… | - |
|
||||
| Keyboard and mouse | key_pressed<br>tap_key<br>get_mouse_x/y<br>get_mouse_buttons | âś… | - |
|
||||
| Lists | list_begin<br>list_next<br>list_end<br>list_as_array<br>party_member_list | âś… | - |
|
||||
| Explosions | metarule2_explosions<br>set_attack_explosion_pattern<br>set_attack_explosion_art<br>set_attack_explosion_radius<br>set_attack_is_explosion_fire<br>set_explosion_radius<br>set_dynamite_damage<br>set_plastic_damage<br>get_explosion_damage<br>set_explosion_max_targets<br>item_make_explosive | âś… except item_make_explosive | - |
|
||||
| Animations | reg_anim_combat_check<br>reg_anim_destroy<br>reg_anim_animate_and_hide<br>reg_anim_light<br>reg_anim_change_fid<br>reg_anim_take_out<br>reg_anim_turn_towards<br>reg_anim_callback<br>reg_anim_animate_and_move | âś… except reg_anim_callback | - |
|
||||
| Art and appearance | art_exists<br>refresh_pc_art<br>art_cache_clear<br>set_hero_race<br>set_hero_style | implemented: art_exists, refresh_pc_art, art_cache_clear | - |
|
||||
| Tiles and paths | get_tile_fid<br>tile_under_cursor<br>tile_light<br>tile_get_objs<br>tile_refresh_display<br>obj_blocking_tile<br>tile_by_position<br>get_tile_ground_fid<br>get_tile_roof_fid<br>obj_blocking_line<br>path_find_to<br>objects_in_radius | âś… except objects_in_radius | - |
|
||||
| Utility | sprintf<br>typeof<br>atoi<br>atof | âś… | - |
|
||||
| Utility / Strings | string_split<br>substr<br>strlen<br>charcode<br>get_string_pointer<br>string_find<br>string_find_from<br>string_format<br>string_format_array<br>string_replace<br>string_to_case<br>string_compare | âś… except get_string_pointer | `get_string_pointer` is deprecated and intentionally omitted. |
|
||||
| Interface / Tags | show_iface_tag<br>hide_iface_tag<br>is_iface_tag_active<br>set_iface_tag_text<br>add_iface_tag | implemented: all except set_iface_tag_text, add_iface_tag | CE only handles built-in interface tags here; custom tag creation/text is not supported yet. |
|
||||
| Global variables | set_sfall_global<br>get_sfall_global_int<br>get_sfall_global_float | implemented: all except get_sfall_global_float | Current CE storage is int-backed; `set_sfall_global` stores integer values and there is no float getter yet. |
|
||||
| Hooks / Hook functions | init_hook<br>get_sfall_arg<br>get_sfall_args<br>get_sfall_arg_at<br>set_sfall_return<br>set_sfall_arg<br>register_hook<br>register_hook_proc<br>register_hook_proc_spec | âś… except init_hook | See below for implemented hooks. `init_hook` is deprecated and will not be implemented. register_hook_proc and register_hook_proc_spec both add hooks to the *end* of the hook list, instead of beginning and end, respectively. |
|
||||
| Arrays / Array functions | create_array<br>temp_array<br>fix_array<br>get/set_array<br>resize_array<br>free_array<br>scan_array<br>len_array<br>save/load_array<br>array_key<br>arrayexpr | âś… except save_array, load_array | - |
|
||||
| Perks and traits / NPC perks | set_fake_perk_npc<br>set_fake_trait_npc<br>set_selectable_perk_npc<br>has_fake_perk_npc<br>has_fake_trait_npc | not implemented | - |
|
||||
| Global scripts / Global script functions | set_global_script_repeat<br>set_global_script_type<br>available_global_script_types | implemented: all except available_global_script_types | `GlobalScriptPaths` masks now match archive-backed scripts regardless of `/` vs `\` separators, matching Windows-style `.dat` contents on non-Windows hosts. |
|
||||
| Combat | attack_is_aimed<br>block_combat<br>force_aimed_shots<br>disable_aimed_shots<br>get_attack_type<br>get/set_bodypart_hit_modifier<br>combat_data<br>get/set/reset_critical_table<br>get_last_target<br>get_last_attacker<br>set_critter_burst_disable<br>get/set_critter_current_ap<br>set_spray_settings<br>get/set_combat_free_move | implemented: only get_attack_type, get_bodypart_hit_modifier, combat_data, set_bodypart_hit_modifier | - |
|
||||
| Car | set_car_current_town<br>car_gas_amount<br>set_car_intface_art | implemented: all except set_car_intface_art | - |
|
||||
| Interface / Windows and images | art_frame_data<br>interface_art_draw<br>interface_print<br>draw_image<br>draw_image_scaled<br>get_window_under_mouse<br>create_win<br>get_window_attribute<br>message_box<br>set_window_flag<br>win_fill_color<br>interface_overlay<br>dialog_message<br>get_text_width<br>hide_window<br>show_window | implemented: only message_box, get_text_width, show_window | - |
|
||||
| Interface / Outline | outlined_object<br>get_outline<br>set_outline | implemented: all except get_outline | - |
|
||||
| Interface / Main interface | intface_is_hidden<br>intface_redraw<br>intface_hide<br>intface_show<br>set_quest_failure_value | implemented: only intface_redraw | `intface_redraw` only supports the zero-argument form; the optional-argument path is explicitly unimplemented. |
|
||||
| Interface / Inventory | display_stats<br>inventory_redraw<br>critter_inven_obj2<br>get_current_inven_size<br>item_weight | implemented: only critter_inven_obj2 | - |
|
||||
| Interface / Cursor | get/set_cursor_mode | âś… | - |
|
||||
| Locks | lock_is_jammed<br>unjam_lock<br>set_unjam_locks_time | not implemented | - |
|
||||
| INI settings | modified_ini<br>get_ini_setting<br>get_ini_string<br>get_ini_section<br>get_ini_sections<br>get_ini_config<br>get_ini_config_db<br>set_ini_setting | âś… except modified_ini, get_ini_config, get_ini_config_db | `modified_ini` is intentionally omitted as deprecated. |
|
||||
| Objects and scripts | set_self<br>set_dude_obj<br>real_dude_obj<br>remove_script<br>set_script<br>get_script<br>obj_is_carrying_obj<br>loot_obj<br>dialog_obj<br>obj_under_cursor<br>get_object_data<br>set_object_data<br>get_flags<br>set_flags<br>set_unique_id<br>set_scr_name<br>obj_is_openable<br>get/set_proto_data<br>get_object_ai_data | implemented: set_self, get_script, loot_obj, dialog_obj, obj_under_cursor, get_object_data, get_flags, set_flags, get_proto_data, set_proto_data | - |
|
||||
| Other / Game management | set_movie_path<br>stop_game<br>resume_game<br>mark_movie_played<br>game_loaded<br>get_game_mode<br>get_uptime<br>signal_close_game | implemented: game_loaded, get_game_mode, get_uptime | - |
|
||||
| Other | input_funcs_available<br>get_year<br>set_dm_model<br>set_df_model<br>set_pipboy_available<br>get_kill_counter<br>mod_kill_counter<br>active_hand<br>toggle_active_hand<br>set_pickpocket_max<br>set_hit_chance_max<br>set_xp_mod<br>set_critter_hit_chance_mod<br>set_base_hit_chance_mod<br>inc_npc_level<br>get_npc_level<br>get/set_viewport_x/y<br>set_hp_per_level_mod<br>get/set_unspent_ap_bonus<br>get/set_unspent_ap_perk_bonus<br>hero_select_win<br>create_message_window<br>get_light_level<br>gdialog_get_barter_mod<br>set_inven_ap_cost<br>set_base_pickpocket_mod<br>set_critter_pickpocket_mod<br>message_str_game<br>sneak_success<br>create_spatial<br>unwield_slot<br>get_inven_ap_cost<br>add_g_timer_event<br>add_extra_msg_file<br>get_metarule_table<br>metarule_exist<br>npc_engine_level_up<br>remove_timer_event<br>set_drugs_data<br>spatial_radius | implemented: get_year, active_hand, toggle_active_hand, create_message_window, message_str_game, add_extra_msg_file, metarule_exist | `input_funcs_available`, `nb_create_char` are deprecated in sfall and intentionally absent in CE. `add_extra_msg_file` does not support the explicit `fileNumber` form in CE. |
|
||||
|
||||
## Hooks
|
||||
|
||||
| Hook | ID | Compatibility | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| ToHit | `HOOK_TOHIT` | âś… | - |
|
||||
| AfterHitRoll | `HOOK_AFTERHITROLL` | đźš« | Et tu |
|
||||
| CalcAPCost | `HOOK_CALCAPCOST` | đźš« | - |
|
||||
| DeathAnim1 | `HOOK_DEATHANIM1` | đźš« | Use DEATHANIM2 instead |
|
||||
| DeathAnim2 | `HOOK_DEATHANIM2` | âś… | - |
|
||||
| CombatDamage | `HOOK_COMBATDAMAGE` | âś… | CE passes the raw `Attack*` as the final mixed argument, matching sfall's shape. |
|
||||
| OnDeath | `HOOK_ONDEATH` | đźš« | - |
|
||||
| FindTarget | `HOOK_FINDTARGET` | đźš« | (maybe) |
|
||||
| UseObjOn | `HOOK_USEOBJON` | âś… | - |
|
||||
| UseObj | `HOOK_USEOBJ` | âś… | CE notes an sfall-matching inconsistency around return code `2` behavior between interface contexts. |
|
||||
| RemoveInvenObj | `HOOK_REMOVEINVENOBJ` | đźš« | - |
|
||||
| BarterPrice | `HOOK_BARTERPRICE` | âś… | - |
|
||||
| MoveCost | `HOOK_MOVECOST` | đźš« | - |
|
||||
| ItemDamage | `HOOK_ITEMDAMAGE` | đźš« | - |
|
||||
| AmmoCost | `HOOK_AMMOCOST` | đźš« | Et tu |
|
||||
| KeyPress | `HOOK_KEYPRESS` | âś… | Third hook arg is currently `0`; CE notes that sfall used VK codes there. |
|
||||
| MouseClick | `HOOK_MOUSECLICK` | đźš« | - |
|
||||
| UseSkill | `HOOK_USESKILL` | đźš« | - |
|
||||
| Steal | `HOOK_STEAL` | đźš« | Et tu |
|
||||
| WithinPerception | `HOOK_WITHINPERCEPTION` | đźš« | Et tu |
|
||||
| InventoryMove | `HOOK_INVENTORYMOVE` | đźš« | Et tu |
|
||||
| InvenWield | `HOOK_INVENWIELD` | đźš« | - |
|
||||
| AdjustFID | `HOOK_ADJUSTFID` | đźš« | - |
|
||||
| CombatTurn | `HOOK_COMBATTURN` | đźš« | - |
|
||||
| StdProcedure | `HOOK_STDPROCEDURE` | đźš« | Et tu |
|
||||
| StdProcedureEnd | `HOOK_STDPROCEDURE_END` | đźš« | - |
|
||||
| CarTravel | `HOOK_CARTRAVEL` | đźš« | - |
|
||||
| SetGlobalVar | `HOOK_SETGLOBALVAR` | đźš« | - |
|
||||
| RestTimer | `HOOK_RESTTIMER` | đźš« | Et tu |
|
||||
| GameModeChange | `HOOK_GAMEMODECHANGE` | âś… | - |
|
||||
| UseAnimObj | `HOOK_USEANIMOBJ` | đźš« | Et tu; (maybe) |
|
||||
| ExplosiveTimer | `HOOK_EXPLOSIVETIMER` | đźš« | - |
|
||||
| DescriptionObj | `HOOK_DESCRIPTIONOBJ` | đźš« | Et tu |
|
||||
| UseSkillOn | `HOOK_USESKILLON` | đźš« | Et tu |
|
||||
| OnExplosion | `HOOK_ONEXPLOSION` | đźš« | (maybe) |
|
||||
| SubCombatDamage | `HOOK_SUBCOMBATDAMAGE` | đźš« | (maybe) |
|
||||
| SetLighting | `HOOK_SETLIGHTING` | đźš« | Et tu; (maybe) |
|
||||
| Sneak | `HOOK_SNEAK` | đźš« | - |
|
||||
| TargetObject | `HOOK_TARGETOBJECT` | đźš« | (maybe) |
|
||||
| Encounter | `HOOK_ENCOUNTER` | đźš« | Et tu |
|
||||
| AdjustPoison | `HOOK_ADJUSTPOISON` | đźš« | (maybe) |
|
||||
| AdjustRads | `HOOK_ADJUSTRADS` | đźš« | (maybe) |
|
||||
| RollCheck | `HOOK_ROLLCHECK` | đźš« | - |
|
||||
| BestWeapon | `HOOK_BESTWEAPON` | đźš« | - |
|
||||
| CanUseWeapon | `HOOK_CANUSEWEAPON` | đźš« | - |
|
||||
| BuildSfxWeapon | `HOOK_BUILDSFXWEAPON` | đźš« | - |
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "sfall.h"
|
||||
#include "dik.h"
|
||||
#include "define_lite.h"
|
||||
#include "define_extra.h"
|
||||
#include "test_utils.h"
|
||||
|
||||
variable refresh_pc_art_test_done := false;
|
||||
|
||||
procedure run_refresh_pc_art_test begin
|
||||
variable old_gender := get_critter_stat(dude_obj, STAT_gender);
|
||||
variable new_gender := 1 - old_gender;
|
||||
variable dude_pid := obj_pid(dude_obj);
|
||||
variable old_proto_fid := get_proto_data(dude_pid, PROTO_FID);
|
||||
|
||||
set_pc_base_stat(STAT_gender, new_gender);
|
||||
call assertEquals("gender changes immediately", get_critter_stat(dude_obj, STAT_gender), new_gender);
|
||||
call assertEquals("proto fid unchanged before refresh", get_proto_data(dude_pid, PROTO_FID), old_proto_fid);
|
||||
|
||||
refresh_pc_art;
|
||||
|
||||
variable refreshed_proto_fid := get_proto_data(dude_pid, PROTO_FID);
|
||||
call assertNotEquals("refresh_pc_art updates dude proto fid", refreshed_proto_fid, old_proto_fid);
|
||||
|
||||
if (not critter_inven_obj(dude_obj, INVEN_TYPE_WORN)) then begin
|
||||
call assertEquals("refresh_pc_art updates unarmored dude art", obj_art_fid(dude_obj) bwand 0xFFF, refreshed_proto_fid bwand 0xFFF);
|
||||
end
|
||||
|
||||
set_pc_base_stat(STAT_gender, old_gender);
|
||||
refresh_pc_art;
|
||||
call assertEquals("refresh_pc_art restores original proto fid", get_proto_data(dude_pid, PROTO_FID), old_proto_fid);
|
||||
|
||||
refresh_pc_art_test_done := true;
|
||||
call report_test_results("hook_refresh_pc_art");
|
||||
end
|
||||
|
||||
procedure keypress_handler begin
|
||||
variable pressed := get_sfall_arg_at(0);
|
||||
variable key := get_sfall_arg_at(1);
|
||||
|
||||
if (not pressed) then return;
|
||||
if (key != DIK_X) then return;
|
||||
|
||||
if (refresh_pc_art_test_done) then begin
|
||||
display_msg("refresh_pc_art test already ran");
|
||||
return;
|
||||
end
|
||||
|
||||
call run_refresh_pc_art_test;
|
||||
end
|
||||
|
||||
procedure start begin
|
||||
if (not game_loaded) then return;
|
||||
|
||||
display_msg("refresh_pc_art test ready: press X");
|
||||
register_hook_proc(HOOK_KEYPRESS, keypress_handler);
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "test_utils.h"
|
||||
#include "sfall.h"
|
||||
|
||||
// to test, copy test_extra_msg_file.msg into <game folder>/text/English/game/
|
||||
|
||||
procedure start begin
|
||||
variable auto_id;
|
||||
variable duplicate_auto_id;
|
||||
|
||||
display_msg("Testing add_extra_msg_file...");
|
||||
|
||||
call assertEquals("metarule exists", metarule_exist("add_extra_msg_file"), 1);
|
||||
|
||||
auto_id := add_extra_msg_file("test_extra_msg_file.msg");
|
||||
call assertNotEquals("auto id is not load error", auto_id, -2);
|
||||
call assertTrue("auto id lower bound", auto_id >= 0x3000);
|
||||
call assertTrue("auto id upper bound", auto_id <= 0x3FFF);
|
||||
call assertEquals("auto message", message_str_game(auto_id, 100), "auto extra msg");
|
||||
|
||||
duplicate_auto_id := add_extra_msg_file("TEST_EXTRA_MSG_FILE.MSG");
|
||||
call assertEquals("duplicate file returns existing id", duplicate_auto_id, auto_id);
|
||||
|
||||
call assertEquals("missing file", add_extra_msg_file("missing_extra_msg_file.msg"), -2);
|
||||
|
||||
call report_test_results("add_extra_msg_file");
|
||||
end
|
||||
@@ -9,9 +9,10 @@ procedure ini_test_suite begin
|
||||
|
||||
// Test Case 1: Valid file, valid section
|
||||
result_array := get_ini_section("test.ini", "ValidSection");
|
||||
expected_array := {"Key1": "Value1", "Key2": "2"};
|
||||
call assertEquals("TC1 Size", len_array(result_array), 2);
|
||||
call assertEquals("TC1 Content", arrays_equal(result_array, expected_array), true);
|
||||
call assertEquals("TC1 Size", len_array(result_array), 3);
|
||||
call assertEquals("TC1 Key1", result_array["Key1"], "Value1");
|
||||
call assertEquals("TC1 Key2", result_array["Key2"], "2");
|
||||
call assertEquals("TC1 EmptyValue", result_array["EmptyValue"], "");
|
||||
count := 0;
|
||||
foreach key_val_pair in result_array begin
|
||||
count += 1;
|
||||
@@ -25,19 +26,22 @@ procedure ini_test_suite begin
|
||||
call assertEquals("TC2 get_ini_string string", get_ini_string("test.ini|ValidSection|Key1"), "Value1");
|
||||
call assertEquals("TC2 get_ini_setting int", get_ini_setting("test.ini|ValidSection|Key2"), 2);
|
||||
call assertEquals("TC2 get_ini_string int", get_ini_string("test.ini|ValidSection|Key2"), "2");
|
||||
call assertEquals("TC2 get_ini_setting empty int", get_ini_setting("test.ini|ValidSection|EmptyValue"), 0);
|
||||
call assertEquals("TC2 get_ini_string empty string", get_ini_string("test.ini|ValidSection|EmptyValue"), "");
|
||||
|
||||
// INI file not found
|
||||
result_array := get_ini_section("nonexistent.ini", "AnySection");
|
||||
call assertEquals("TC3 Size", len_array(result_array), 0);
|
||||
call assertEquals("TC3 Is Map", array_key(result_array, -1), 1); // Should still be an associative array
|
||||
call assertEquals("TC3 get_ini_setting", get_ini_setting("notexist.ini|ValidSection|Key2"), 0);
|
||||
call assertEquals("TC3 get_ini_setting missing file", get_ini_setting("notexist.ini|ValidSection|Key2"), -1);
|
||||
call assertEquals("TC3 get_ini_setting missing key", get_ini_setting("test.ini|ValidSection|MissingKey"), -1);
|
||||
call assertEquals("TC3 get_ini_string", get_ini_string("notexist.ini|ValidSection|Key2"), "");
|
||||
|
||||
// Valid file, section not found
|
||||
result_array := get_ini_section("test.ini", "NonExistentSection");
|
||||
call assertEquals("TC4 Size", len_array(result_array), 0);
|
||||
call assertEquals("TC4 Is Map", array_key(result_array, -1), 1);
|
||||
call assertEquals("TC4 get_ini_setting", get_ini_setting("test.ini|NonExistentSection|Key2"), 0);
|
||||
call assertEquals("TC4 get_ini_setting", get_ini_setting("test.ini|NonExistentSection|Key2"), -1);
|
||||
call assertEquals("TC4 get_ini_string", get_ini_string("test.ini|NonExistentSection|Key2"), "");
|
||||
|
||||
|
||||
@@ -45,7 +49,7 @@ procedure ini_test_suite begin
|
||||
result_array := get_ini_section("test.ini", "EmptySection");
|
||||
call assertEquals("TC5 Size", len_array(result_array), 0);
|
||||
call assertEquals("TC5 Is Map", array_key(result_array, -1), 1);
|
||||
call assertEquals("TC5 get_ini_setting", get_ini_setting("test.ini|EmptySection|Key2"), 0);
|
||||
call assertEquals("TC5 get_ini_setting", get_ini_setting("test.ini|EmptySection|Key2"), -1);
|
||||
call assertEquals("TC5 get_ini_string", get_ini_string("test.ini|EmptySection|Key2"), "");
|
||||
|
||||
// set ini setting
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
#include "sfall.h"
|
||||
#include "dik.h"
|
||||
#include "command_lite.h"
|
||||
#include "define_extra.h"
|
||||
#include "test_utils.h"
|
||||
|
||||
#define REG_ANIM_BEGIN 1
|
||||
#define REG_ANIM_END 3
|
||||
#define ANIMATION_REQUEST_UNRESERVED 1
|
||||
#define TEST_ITEM_PID 40
|
||||
#define TEST_FID_SOURCE_PID 76
|
||||
#define TEST_LIGHT_VALUE 0x12340002
|
||||
#define TEST_TAKE_OUT_FRAME WPN_ANIM_PISTOL
|
||||
#define TEST_STAND_ANIM 0
|
||||
#define TEST_WALK_ANIM 1
|
||||
|
||||
#define reg_anim_begin() reg_anim_func(REG_ANIM_BEGIN, ANIMATION_REQUEST_UNRESERVED)
|
||||
#define reg_anim_end() reg_anim_func(REG_ANIM_END, 0)
|
||||
|
||||
variable hook_registered := false;
|
||||
variable pending_test := 0;
|
||||
variable pending_frames := 0;
|
||||
|
||||
variable test_move_critter_obj := 0;
|
||||
variable test_takeout_critter_obj := 0;
|
||||
variable test_light_obj := 0;
|
||||
variable test_fid_obj := 0;
|
||||
variable test_fid_source_obj := 0;
|
||||
variable test_hide_obj := 0;
|
||||
variable test_destroy_obj := 0;
|
||||
|
||||
variable expected_turn_tile := 0;
|
||||
variable expected_move_tile := 0;
|
||||
variable initial_rotation := 0;
|
||||
variable initial_tile := 0;
|
||||
variable initial_fid := 0;
|
||||
variable expected_fid := 0;
|
||||
variable initial_hide_visibility := 0;
|
||||
variable initial_destroy_visibility := 0;
|
||||
variable test_objects_ready := false;
|
||||
variable pending_timeout_frames := 0;
|
||||
variable noncombat_test_completed := false;
|
||||
|
||||
// Manual checks:
|
||||
// X:
|
||||
// - No registered animation should run in combat.
|
||||
// - The temp critters should not move or draw a weapon.
|
||||
// V:
|
||||
// - `reg_anim_combat_check(0)` should allow the registered animations in combat.
|
||||
// N:
|
||||
// - Outside combat, the full opcode surface should run and be checked.
|
||||
|
||||
procedure setup_test_objects begin
|
||||
variable elev := elevation(dude_obj);
|
||||
|
||||
test_move_critter_obj := create_object_sid(obj_pid(dude_obj), tile_num_in_direction(dude_tile, dude_cur_rot, 3), elev, -1);
|
||||
test_takeout_critter_obj := create_object_sid(obj_pid(dude_obj), tile_num_in_direction(dude_tile, (dude_cur_rot + 1) % 6, 3), elev, -1);
|
||||
test_light_obj := create_object_sid(TEST_ITEM_PID, tile_num_in_direction(dude_tile, (dude_cur_rot + 2) % 6, 4), elev, -1);
|
||||
test_fid_obj := create_object_sid(TEST_ITEM_PID, tile_num_in_direction(dude_tile, (dude_cur_rot + 3) % 6, 4), elev, -1);
|
||||
test_fid_source_obj := create_object_sid(TEST_FID_SOURCE_PID, tile_num_in_direction(dude_tile, (dude_cur_rot + 4) % 6, 4), elev, -1);
|
||||
test_hide_obj := create_object_sid(TEST_ITEM_PID, tile_num_in_direction(dude_tile, (dude_cur_rot + 5) % 6, 4), elev, -1);
|
||||
test_destroy_obj := create_object_sid(TEST_ITEM_PID, tile_num_in_direction(dude_tile, dude_cur_rot, 5), elev, -1);
|
||||
|
||||
initial_rotation := obj_get_rot(test_move_critter_obj);
|
||||
initial_tile := tile_num(test_move_critter_obj);
|
||||
expected_turn_tile := tile_num_in_direction(initial_tile, (initial_rotation + 3) % 6, 1);
|
||||
expected_move_tile := tile_num_in_direction(initial_tile, initial_rotation, 1);
|
||||
initial_fid := get_object_data(test_fid_obj, OBJ_DATA_FID);
|
||||
expected_fid := get_object_data(test_fid_source_obj, OBJ_DATA_FID);
|
||||
initial_hide_visibility := obj_is_visible_flag(test_hide_obj);
|
||||
initial_destroy_visibility := obj_is_visible_flag(test_destroy_obj);
|
||||
pending_frames := 0;
|
||||
test_objects_ready := true;
|
||||
end
|
||||
|
||||
procedure ensure_test_objects begin
|
||||
if (test_objects_ready) then return;
|
||||
call setup_test_objects();
|
||||
end
|
||||
|
||||
procedure queue_full_sequence begin
|
||||
reg_anim_begin();
|
||||
reg_anim_turn_towards(test_move_critter_obj, expected_turn_tile, -1);
|
||||
reg_anim_take_out(test_takeout_critter_obj, TEST_TAKE_OUT_FRAME, -1);
|
||||
reg_anim_light(test_light_obj, TEST_LIGHT_VALUE, -1);
|
||||
reg_anim_change_fid(test_fid_obj, expected_fid, -1);
|
||||
reg_anim_animate_and_hide(test_hide_obj, TEST_STAND_ANIM, -1);
|
||||
reg_anim_destroy(test_destroy_obj);
|
||||
reg_anim_animate_and_move(test_move_critter_obj, expected_move_tile, TEST_WALK_ANIM, -1);
|
||||
reg_anim_end();
|
||||
end
|
||||
|
||||
procedure start_blocked_test begin
|
||||
pending_test := 1;
|
||||
pending_timeout_frames := 30;
|
||||
call ensure_test_objects();
|
||||
call queue_full_sequence();
|
||||
display_msg("Started blocked-case test.");
|
||||
end
|
||||
|
||||
procedure start_enabled_test begin
|
||||
pending_test := 2;
|
||||
pending_timeout_frames := 120;
|
||||
call ensure_test_objects();
|
||||
reg_anim_combat_check(0);
|
||||
call queue_full_sequence();
|
||||
display_msg("Started enabled-case test.");
|
||||
end
|
||||
|
||||
procedure start_noncombat_test begin
|
||||
pending_test := 3;
|
||||
pending_timeout_frames := 120;
|
||||
call ensure_test_objects();
|
||||
call queue_full_sequence();
|
||||
display_msg("Started non-combat opcode test.");
|
||||
end
|
||||
|
||||
procedure keypress_handler begin
|
||||
variable
|
||||
pressed := get_sfall_arg_at(0),
|
||||
key := get_sfall_arg_at(1);
|
||||
|
||||
if (not pressed) then return;
|
||||
|
||||
if (pending_test != 0) then begin
|
||||
if (key == DIK_X or key == DIK_V or key == DIK_N) then
|
||||
display_msg("A reg_anim_combat_check test is already running.");
|
||||
return;
|
||||
end
|
||||
|
||||
if (combat_data and key == DIK_X) then begin
|
||||
display_msg("X: combat should block all reg_anim calls in this sequence.");
|
||||
call start_blocked_test();
|
||||
end else if (combat_data and key == DIK_V) then begin
|
||||
display_msg("V: reg_anim_combat_check(0) should allow the registered animations in combat.");
|
||||
call start_enabled_test();
|
||||
end else if ((not combat_data) and key == DIK_N) then begin
|
||||
if (noncombat_test_completed) then begin
|
||||
display_msg("N already ran once. Reload the map or game before running it again.");
|
||||
return;
|
||||
end
|
||||
display_msg("N: outside combat, the full reg_anim opcode sequence should run.");
|
||||
call start_noncombat_test();
|
||||
end else if (key == DIK_X or key == DIK_V) then begin
|
||||
display_msg("Enter combat before running X or V.");
|
||||
end else if (key == DIK_N) then begin
|
||||
display_msg("Leave combat before running N.");
|
||||
end
|
||||
end
|
||||
|
||||
procedure report_blocked_results begin
|
||||
variable errors_before := test_suite_errors;
|
||||
|
||||
call assertEquals("turn_towards blocked", obj_get_rot(test_move_critter_obj), initial_rotation);
|
||||
call assertEquals("animate_and_move blocked", tile_num(test_move_critter_obj), initial_tile);
|
||||
call assertEquals("change_fid blocked", get_object_data(test_fid_obj, OBJ_DATA_FID), initial_fid);
|
||||
call assertEquals("animate_and_hide blocked", obj_is_visible_flag(test_hide_obj), initial_hide_visibility);
|
||||
call assertEquals("destroy blocked", obj_is_visible_flag(test_destroy_obj), initial_destroy_visibility);
|
||||
|
||||
if (test_suite_errors == errors_before) then
|
||||
display_msg("PASS blocked automatic checks");
|
||||
else
|
||||
display_msg("FAIL blocked automatic checks");
|
||||
|
||||
call report_test_results("reg_anim_combat_check_blocked");
|
||||
end
|
||||
|
||||
procedure report_enabled_results begin
|
||||
variable errors_before := test_suite_errors;
|
||||
|
||||
call assertNotEquals("turn_towards runs in combat when enabled", obj_get_rot(test_move_critter_obj), initial_rotation);
|
||||
call assertEquals("animate_and_move runs in combat when enabled", tile_num(test_move_critter_obj), expected_move_tile);
|
||||
call assertEquals("change_fid runs in combat when enabled", get_object_data(test_fid_obj, OBJ_DATA_FID), expected_fid);
|
||||
|
||||
if (test_suite_errors == errors_before) then
|
||||
display_msg("PASS enabled automatic checks");
|
||||
else
|
||||
display_msg("FAIL enabled automatic checks");
|
||||
|
||||
call report_test_results("reg_anim_combat_check_enabled");
|
||||
end
|
||||
|
||||
procedure report_noncombat_results begin
|
||||
variable errors_before := test_suite_errors;
|
||||
|
||||
noncombat_test_completed := true;
|
||||
|
||||
call assertNotEquals("turn_towards outside combat", obj_get_rot(test_move_critter_obj), initial_rotation);
|
||||
call assertEquals("animate_and_move outside combat", tile_num(test_move_critter_obj), expected_move_tile);
|
||||
call assertEquals("change_fid outside combat", get_object_data(test_fid_obj, OBJ_DATA_FID), expected_fid);
|
||||
call assertEquals("animate_and_hide outside combat", obj_is_visible_flag(test_hide_obj), 0);
|
||||
|
||||
if (test_suite_errors == errors_before) then
|
||||
display_msg("PASS non-combat automatic checks");
|
||||
else
|
||||
display_msg("FAIL non-combat automatic checks");
|
||||
|
||||
call report_test_results("reg_anim_full_noncombat");
|
||||
display_msg("Manual check: outside combat, the other clone should play take out, light should visibly apply if supported by the object type, and destroy_obj should be gone.");
|
||||
end
|
||||
|
||||
procedure poll_pending_test begin
|
||||
if (pending_test == 0) then return;
|
||||
|
||||
pending_frames += 1;
|
||||
|
||||
if (pending_frames < pending_timeout_frames) then return;
|
||||
|
||||
variable current_test := pending_test;
|
||||
pending_test := 0;
|
||||
pending_frames := 0;
|
||||
pending_timeout_frames := 0;
|
||||
|
||||
if (current_test == 1) then begin
|
||||
call report_blocked_results();
|
||||
end else if (current_test == 2) then begin
|
||||
call report_enabled_results();
|
||||
end else if (current_test == 3) then begin
|
||||
call report_noncombat_results();
|
||||
end
|
||||
end
|
||||
|
||||
procedure start begin
|
||||
set_global_script_repeat(1);
|
||||
|
||||
if (not hook_registered and game_loaded) then begin
|
||||
hook_registered := true;
|
||||
call ensure_test_objects();
|
||||
register_hook_proc(HOOK_KEYPRESS, keypress_handler);
|
||||
display_msg("reg_anim_combat_check test loaded. Enter combat for X/V, or stay out of combat for N (one-shot).");
|
||||
end
|
||||
|
||||
call poll_pending_test();
|
||||
end
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "sfall.h"
|
||||
#include "test_utils.h"
|
||||
|
||||
variable blocker_obj := 0;
|
||||
|
||||
procedure cleanup begin
|
||||
if (blocker_obj) then begin
|
||||
destroy_object(blocker_obj);
|
||||
blocker_obj := 0;
|
||||
end
|
||||
end
|
||||
|
||||
procedure start begin
|
||||
variable elev := elevation(dude_obj);
|
||||
variable dude_tile := tile_num(dude_obj);
|
||||
variable dir;
|
||||
variable step_tile := -1;
|
||||
variable step_dir := -1;
|
||||
variable block_tile := -1;
|
||||
variable path;
|
||||
variable objs;
|
||||
variable ground_fid;
|
||||
variable roof_fid;
|
||||
|
||||
display_msg("Testing tile/path functions...");
|
||||
call cleanup();
|
||||
|
||||
for (dir := 0; dir < 6; dir += 1) begin
|
||||
variable candidate := tile_num_in_direction(dude_tile, dir, 1);
|
||||
if (candidate != dude_tile and obj_blocking_tile(candidate, elev, BLOCKING_TYPE_BLOCK) == 0) then begin
|
||||
if (step_tile == -1) then begin
|
||||
step_tile := candidate;
|
||||
step_dir := dir;
|
||||
end
|
||||
|
||||
if (block_tile == -1 and candidate != step_tile) then begin
|
||||
block_tile := candidate;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
call assertNotEquals("found reachable neighboring tile", step_tile, -1);
|
||||
if (step_tile != -1) then begin
|
||||
path := path_find_to(dude_obj, step_tile, BLOCKING_TYPE_BLOCK);
|
||||
call assertEquals("one-step path length", len_array(path), 1);
|
||||
if (len_array(path) == 1) then
|
||||
call assertEquals("one-step path direction", get_array(path, 0), step_dir);
|
||||
end
|
||||
|
||||
if (block_tile != -1) then begin
|
||||
blocker_obj := create_object_sid(obj_pid(dude_obj), block_tile, elev, -1);
|
||||
call assertNotEquals("spawned blocking critter", blocker_obj, 0);
|
||||
if (blocker_obj) then begin
|
||||
path := path_find_to(dude_obj, tile_num(blocker_obj), BLOCKING_TYPE_BLOCK);
|
||||
call assertEquals("occupied destination returns empty path", len_array(path), 0);
|
||||
end
|
||||
end else begin
|
||||
display_msg("Skipping occupied-destination path check: no free adjacent tile for blocker.");
|
||||
end
|
||||
|
||||
objs := tile_get_objs(dude_tile, elev);
|
||||
call assertTrue("tile_get_objs contains dude", scan_array(objs, dude_obj) != -1);
|
||||
|
||||
ground_fid := get_tile_ground_fid(dude_tile, elev);
|
||||
roof_fid := get_tile_roof_fid(dude_tile, elev);
|
||||
call assertTrue("ground fid is non-negative", ground_fid >= 0);
|
||||
call assertTrue("roof fid is non-negative", roof_fid >= 0);
|
||||
call assertEquals("ground helper matches ext", ground_fid, get_tile_fid_ext(dude_tile, elev, 0));
|
||||
call assertEquals("roof helper matches ext", roof_fid, get_tile_fid_ext(dude_tile, elev, 1));
|
||||
|
||||
call assertTrue("tile_light returns non-negative intensity", tile_light(elev, dude_tile) >= 0);
|
||||
call assertEquals("tile_by_position metarule exists", metarule_exist("tile_by_position"), 1);
|
||||
call assertEquals("tile_by_position matches tile_under_cursor", tile_by_position(get_mouse_x, get_mouse_y), tile_under_cursor);
|
||||
|
||||
tile_refresh_display;
|
||||
call cleanup();
|
||||
call report_test_results("tile_path");
|
||||
end
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "../sfall/define_lite.h"
|
||||
#include "../test_utils.h"
|
||||
|
||||
#include "../sfall/lib.arrays.h"
|
||||
|
||||
variable last_game_mode;
|
||||
variable gamemodechange_assertions_enabled := false;
|
||||
variable gamemodechange_reported := false;
|
||||
@@ -94,6 +96,47 @@ procedure gamemodechange_handler begin
|
||||
last_game_mode := current_mode;
|
||||
end
|
||||
|
||||
procedure combatdamage_handler begin
|
||||
variable args := get_sfall_args;
|
||||
|
||||
display_msg(sprintf("combatdamage %s", debug_array_str(args)));
|
||||
|
||||
set_sfall_return(10); // target dmg
|
||||
set_sfall_return(5);
|
||||
set_sfall_return(0);
|
||||
set_sfall_return(DAM_HIT); // attacker flag
|
||||
set_sfall_return(10); // knockback
|
||||
end
|
||||
|
||||
|
||||
procedure barterprice_handler begin
|
||||
variable
|
||||
args := get_sfall_args,
|
||||
value := args[2],
|
||||
caps := args[4],
|
||||
offerValue := args[7],
|
||||
offerTable := args[6],
|
||||
offerCaps := item_caps_total(offerTable);
|
||||
|
||||
display_msg(sprintf("barterprice %s", debug_array_str(args)));
|
||||
|
||||
set_sfall_return((value - caps) * 2 + caps);
|
||||
set_sfall_return((offerValue - offerCaps) / 2 + offerCaps);
|
||||
end
|
||||
|
||||
#define ANIM_fall_back (20)
|
||||
#define ANIM_fall_front (21)
|
||||
#define ANIM_fire_dance (33)
|
||||
|
||||
procedure deathanim_handler begin
|
||||
variable
|
||||
args := get_sfall_args;
|
||||
|
||||
display_msg(sprintf("deathanim %s", debug_array_str(args)));
|
||||
|
||||
set_sfall_return(ANIM_fire_dance);
|
||||
end
|
||||
|
||||
procedure start begin
|
||||
//call test_with_args(777);
|
||||
display_msg("start hook test");
|
||||
@@ -112,6 +155,9 @@ procedure start begin
|
||||
register_hook_proc(HOOK_USEOBJ, useobj_handler);
|
||||
register_hook_proc(HOOK_USEOBJON, useobjon_handler);
|
||||
register_hook_proc(HOOK_GAMEMODECHANGE, gamemodechange_handler);
|
||||
register_hook_proc(HOOK_COMBATDAMAGE, combatdamage_handler);
|
||||
register_hook_proc(HOOK_BARTERPRICE, barterprice_handler);
|
||||
register_hook_proc(HOOK_DEATHANIM2, deathanim_handler);
|
||||
end
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[ValidSection]
|
||||
Key1=Value1
|
||||
Key2=2
|
||||
EmptyValue=
|
||||
|
||||
[SectionWithSpaces]
|
||||
LeadingSpaceKey = LeadingSpaceValue
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{100}{}{auto extra msg}
|
||||
+81
-68
@@ -29,6 +29,7 @@
|
||||
#include "scripts.h"
|
||||
#include "settings.h"
|
||||
#include "sfall_config.h"
|
||||
#include "sfall_script_hooks.h"
|
||||
#include "skill.h"
|
||||
#include "stat.h"
|
||||
#include "text_object.h"
|
||||
@@ -76,20 +77,20 @@ static const int gMaximumBloodDeathAnimations[DAMAGE_TYPE_COUNT] = {
|
||||
// Note: some of these are callbacks that always take two Object*, but may not use them.
|
||||
// Ignored parameters are marked with underscores.
|
||||
static int actionKnockdown(Object* obj, int* anim, int maxDistance, int rotation, int delay);
|
||||
static int _action_blood(Object* obj, int anim, int delay);
|
||||
static int _pick_death(Object* attacker, Object* defender, Object* weapon, int damage, int attackerAnimation, bool hitFromFront);
|
||||
static int _check_death(Object* obj, int anim, int minViolenceLevel, bool hitFromFront);
|
||||
static int actionBlood(Object* obj, int anim, int delay);
|
||||
static int pickDeathAnim(Object* attacker, Object* defender, Object* weapon, int damage, int attackerAnimation, bool hitFromFront);
|
||||
static int checkDeathAnim(Object* obj, int anim, int minViolenceLevel, bool hitFromFront);
|
||||
static int _internal_destroy(Object* _, Object* toDestroy);
|
||||
static void _show_damage_to_object(Object* defender, int damage, int flags, Object* weapon, bool hitFromFront, int knockbackDistance, int knockbackRotation, int attackerAnimation, Object* attacker, int delay);
|
||||
static void showDamageToObject(Object* defender, int damage, int flags, Object* weapon, bool hitFromFront, int knockbackDistance, int knockbackRotation, int attackerAnimation, Object* attacker, int delay);
|
||||
static int _show_death(Object* obj, int anim);
|
||||
static int _show_damage_extras(Attack* attack);
|
||||
static void _show_damage(Attack* attack, int attackerAnimation, int delay);
|
||||
static int showDamageToExtras(Attack* attack);
|
||||
static void showDamage(Attack* attack, int attackerAnimation, int delay);
|
||||
static int _action_melee(Attack* attack, int anim);
|
||||
static int _action_ranged(Attack* attack, int anim);
|
||||
static int _is_next_to(Object* obj1, Object* obj2);
|
||||
static int _action_climb_ladder(Object* critter, Object* ladder);
|
||||
static int _action_use_skill_in_combat_error(Object* critter);
|
||||
static int _pick_fall(Object* obj, int anim);
|
||||
static int pickFallAnim(Object* obj, int anim);
|
||||
static int _report_explosion(Attack* attack, Object* sourceObj);
|
||||
static int _finished_explosion(Object*, Object*);
|
||||
static int _compute_explosion_damage(int min, int max, Object* defender, int* knockbackDistancePtr);
|
||||
@@ -155,8 +156,10 @@ int actionKnockdown(Object* obj, int* anim, int maxDistance, int rotation, int d
|
||||
return tile;
|
||||
}
|
||||
|
||||
// 0x410568
|
||||
int _action_blood(Object* obj, int anim, int delay)
|
||||
// Plays bleeding animation (after critter has fallen with ANIM_FALL_BACK or ANIM_FALL_FRONT).
|
||||
//
|
||||
// 0x410568 action_blood
|
||||
int actionBlood(Object* obj, int anim, int delay)
|
||||
{
|
||||
if (settings.preferences.violence_level == VIOLENCE_LEVEL_NONE) {
|
||||
return anim;
|
||||
@@ -181,9 +184,19 @@ int _action_blood(Object* obj, int anim, int delay)
|
||||
return bloodyAnim;
|
||||
}
|
||||
|
||||
// 0x41060C
|
||||
int _pick_death(Object* attacker, Object* defender, Object* weapon, int damage, int attackerAnimation, bool hitFromFront)
|
||||
// 0x41060C pick_death
|
||||
int pickDeathAnim(Object* attacker, Object* defender, Object* weapon, int damage, int attackerAnimation, bool hitFromFront)
|
||||
{
|
||||
if (attacker->fid == buildFid(OBJ_TYPE_MISC, 10, 0, 0, 0)) { // roktxpd.frm
|
||||
return checkDeathAnim(defender, ANIM_EXPLODED_TO_NOTHING, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront);
|
||||
}
|
||||
if (attacker->pid == PROTO_ID_FORCE_FIELD_NS) { // Forcefield North/South
|
||||
return checkDeathAnim(defender, ANIM_ELECTRIFIED_TO_NOTHING, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront);
|
||||
}
|
||||
if (attacker->fid == FRAME_ID_FORCE_FIELD_NS) { // ffield03.frm
|
||||
return checkDeathAnim(defender, attackerAnimation, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront);
|
||||
}
|
||||
|
||||
int normalViolenceLevelDamageThreshold = 15;
|
||||
int maximumBloodViolenceLevelDamageThreshold = 45;
|
||||
|
||||
@@ -209,7 +222,7 @@ int _pick_death(Object* attacker, Object* defender, Object* weapon, int damage,
|
||||
int violenceLevel = settings.preferences.violence_level;
|
||||
|
||||
if (critterFlagCheck(defender->pid, CRITTER_SPECIAL_DEATH)) {
|
||||
return _check_death(defender, ANIM_EXPLODED_TO_NOTHING, VIOLENCE_LEVEL_NORMAL, hitFromFront);
|
||||
return checkDeathAnim(defender, ANIM_EXPLODED_TO_NOTHING, VIOLENCE_LEVEL_NORMAL, hitFromFront);
|
||||
}
|
||||
|
||||
bool hasBloodyMess = false;
|
||||
@@ -241,7 +254,7 @@ int _pick_death(Object* attacker, Object* defender, Object* weapon, int damage,
|
||||
if (violenceLevel > VIOLENCE_LEVEL_MINIMAL && (hasBloodyMess || normalViolenceLevelDamageThreshold <= damage)) {
|
||||
if (violenceLevel > VIOLENCE_LEVEL_NORMAL && (hasBloodyMess || maximumBloodViolenceLevelDamageThreshold <= damage)) {
|
||||
deathAnim = gMaximumBloodDeathAnimations[damageType];
|
||||
if (_check_death(defender, deathAnim, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront) != deathAnim) {
|
||||
if (checkDeathAnim(defender, deathAnim, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront) != deathAnim) {
|
||||
deathAnim = gNormalDeathAnimations[damageType];
|
||||
}
|
||||
} else {
|
||||
@@ -255,11 +268,14 @@ int _pick_death(Object* attacker, Object* defender, Object* weapon, int damage,
|
||||
deathAnim = ANIM_FALL_FRONT;
|
||||
}
|
||||
|
||||
return _check_death(defender, deathAnim, VIOLENCE_LEVEL_NONE, hitFromFront);
|
||||
return checkDeathAnim(defender, deathAnim, VIOLENCE_LEVEL_NONE, hitFromFront);
|
||||
}
|
||||
|
||||
// 0x410814
|
||||
int _check_death(Object* obj, int anim, int minViolenceLevel, bool hitFromFront)
|
||||
// Returns anim if art for it exists and selected violence_level >= minViolenceLevel.
|
||||
// Otherwise, returns neutral fall animation (front or back, based on hitFromFront).
|
||||
//
|
||||
// 0x410814 check_death
|
||||
int checkDeathAnim(Object* obj, int anim, int minViolenceLevel, bool hitFromFront)
|
||||
{
|
||||
int fid;
|
||||
|
||||
@@ -275,7 +291,8 @@ int _check_death(Object* obj, int anim, int minViolenceLevel, bool hitFromFront)
|
||||
}
|
||||
|
||||
fid = buildFid(OBJ_TYPE_CRITTER, obj->fid & 0xFFF, ANIM_FALL_FRONT, (obj->fid & 0xF000) >> 12, obj->rotation + 1);
|
||||
if (artExists(fid)) {
|
||||
// CE: fixed vanilla logic that returned ANIM_FALL_BACK if artExists for ANIM_FALL_FRONT returned true.
|
||||
if (!artExists(fid)) {
|
||||
return ANIM_FALL_BACK;
|
||||
}
|
||||
|
||||
@@ -290,8 +307,8 @@ int _internal_destroy(Object* _, Object* toDestroy)
|
||||
|
||||
// TODO: Check very carefully, lots of conditions and jumps.
|
||||
//
|
||||
// 0x4108D0
|
||||
void _show_damage_to_object(Object* defender, int damage, int flags, Object* weapon, bool hitFromFront, int knockbackDistance, int knockbackRotation, int attackerAnimation, Object* attacker, int delay)
|
||||
// 0x4108D0 show_damage_to_object
|
||||
void showDamageToObject(Object* defender, int damage, int flags, Object* weapon, bool hitFromFront, int knockbackDistance, int knockbackRotation, int attackerAnimation, Object* attacker, int delay)
|
||||
{
|
||||
int anim;
|
||||
int fid;
|
||||
@@ -304,30 +321,22 @@ void _show_damage_to_object(Object* defender, int damage, int flags, Object* wea
|
||||
anim = FID_ANIM_TYPE(defender->fid);
|
||||
if (!critterIsProne(defender)) {
|
||||
if ((flags & DAM_DEAD) != 0) {
|
||||
fid = buildFid(OBJ_TYPE_MISC, 10, 0, 0, 0);
|
||||
if (fid == attacker->fid) {
|
||||
anim = _check_death(defender, ANIM_EXPLODED_TO_NOTHING, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront);
|
||||
} else if (attacker->pid == PROTO_ID_0x20001EB) {
|
||||
anim = _check_death(defender, ANIM_ELECTRIFIED_TO_NOTHING, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront);
|
||||
} else if (attacker->fid == FID_0x20001F5) {
|
||||
anim = _check_death(defender, attackerAnimation, VIOLENCE_LEVEL_MAXIMUM_BLOOD, hitFromFront);
|
||||
} else {
|
||||
anim = _pick_death(attacker, defender, weapon, damage, attackerAnimation, hitFromFront);
|
||||
}
|
||||
anim = pickDeathAnim(attacker, defender, weapon, damage, attackerAnimation, hitFromFront);
|
||||
scriptHooks_DeathAnim(attacker, defender, weapon, damage, &anim);
|
||||
|
||||
if (anim != ANIM_FIRE_DANCE) {
|
||||
if (knockbackDistance != 0 && (anim == ANIM_FALL_FRONT || anim == ANIM_FALL_BACK)) {
|
||||
actionKnockdown(defender, &anim, knockbackDistance, knockbackRotation, delay);
|
||||
anim = _action_blood(defender, anim, -1);
|
||||
anim = actionBlood(defender, anim, -1);
|
||||
} else {
|
||||
sfx_name = sfxBuildCharName(defender, anim, CHARACTER_SOUND_EFFECT_DIE);
|
||||
animationRegisterPlaySoundEffect(defender, sfx_name, delay);
|
||||
|
||||
anim = _pick_fall(defender, anim);
|
||||
anim = pickFallAnim(defender, anim);
|
||||
animationRegisterAnimate(defender, anim, 0);
|
||||
|
||||
if (anim == ANIM_FALL_FRONT || anim == ANIM_FALL_BACK) {
|
||||
anim = _action_blood(defender, anim, -1);
|
||||
anim = actionBlood(defender, anim, -1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -406,7 +415,7 @@ void _show_damage_to_object(Object* defender, int damage, int flags, Object* wea
|
||||
if (knockbackDistance != 0) {
|
||||
actionKnockdown(defender, &anim, knockbackDistance, knockbackRotation, 0);
|
||||
} else {
|
||||
anim = _pick_fall(defender, anim);
|
||||
anim = pickFallAnim(defender, anim);
|
||||
animationRegisterAnimate(defender, anim, 0);
|
||||
}
|
||||
} else if ((flags & DAM_ON_FIRE) != 0 && artExists(buildFid(OBJ_TYPE_CRITTER, defender->fid & 0xFFF, ANIM_FIRE_DANCE, (defender->fid & 0xF000) >> 12, defender->rotation + 1))) {
|
||||
@@ -439,7 +448,7 @@ void _show_damage_to_object(Object* defender, int damage, int flags, Object* wea
|
||||
}
|
||||
} else {
|
||||
if ((flags & DAM_DEAD) != 0 && (defender->data.critter.combat.results & DAM_DEAD) == 0) {
|
||||
anim = _action_blood(defender, anim, delay);
|
||||
anim = actionBlood(defender, anim, delay);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -508,8 +517,10 @@ int _show_death(Object* obj, int anim)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 0x410FEC
|
||||
int _show_damage_extras(Attack* attack)
|
||||
// Animates damage to extras in a given attack (secondary targets).
|
||||
//
|
||||
// 0x410FEC _show_damage_extras
|
||||
int showDamageToExtras(Attack* attack)
|
||||
{
|
||||
for (int index = 0; index < attack->extrasLength; index++) {
|
||||
Object* obj = attack->extras[index];
|
||||
@@ -520,7 +531,7 @@ int _show_damage_extras(Attack* attack)
|
||||
_register_priority(1);
|
||||
int attackerAnimation = critterGetAnimationForHitMode(attack->attacker, attack->hitMode);
|
||||
int knockbackRotation = tileGetRotationTo(attack->attacker->tile, obj->tile);
|
||||
_show_damage_to_object(obj, attack->extrasDamage[index], attack->extrasFlags[index], attack->weapon, hitFromFront, attack->extrasKnockback[index], knockbackRotation, attackerAnimation, attack->attacker, 0);
|
||||
showDamageToObject(obj, attack->extrasDamage[index], attack->extrasFlags[index], attack->weapon, hitFromFront, attack->extrasKnockback[index], knockbackRotation, attackerAnimation, attack->attacker, 0);
|
||||
reg_anim_end();
|
||||
}
|
||||
}
|
||||
@@ -528,8 +539,16 @@ int _show_damage_extras(Attack* attack)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 0x4110AC
|
||||
void _show_damage(Attack* attack, int attackerAnimation, int delay)
|
||||
// Shows animation to attacker, in case of critical failure, backwash, etc.
|
||||
static void showDamageToAttacker(Attack* attack, int attackerAnimation)
|
||||
{
|
||||
showDamageToObject(attack->attacker, attack->attackerDamage, attack->attackerFlags, attack->weapon, true, 0, 0, attackerAnimation, attack->attacker, -1);
|
||||
}
|
||||
|
||||
// Animates damage to defender (or attacker, in some cases) as a result of a given attack.
|
||||
//
|
||||
// 0x4110AC show_damage
|
||||
void showDamage(Attack* attack, int attackerAnimation, int delay)
|
||||
{
|
||||
for (int index = 0; index < attack->extrasLength; index++) {
|
||||
Object* object = attack->extras[index];
|
||||
@@ -540,33 +559,26 @@ void _show_damage(Attack* attack, int attackerAnimation, int delay)
|
||||
}
|
||||
|
||||
if ((attack->attackerFlags & DAM_HIT) == 0) {
|
||||
if ((attack->attackerFlags & DAM_CRITICAL) != 0) {
|
||||
_show_damage_to_object(attack->attacker, attack->attackerDamage, attack->attackerFlags, attack->weapon, 1, 0, 0, attackerAnimation, attack->attacker, -1);
|
||||
} else if ((attack->attackerFlags & DAM_BACKWASH) != 0) {
|
||||
_show_damage_to_object(attack->attacker, attack->attackerDamage, attack->attackerFlags, attack->weapon, 1, 0, 0, attackerAnimation, attack->attacker, -1);
|
||||
if ((attack->attackerFlags & DAM_CRITICAL) != 0 || (attack->attackerFlags & DAM_BACKWASH) != 0) {
|
||||
showDamageToAttacker(attack, attackerAnimation);
|
||||
}
|
||||
} else {
|
||||
if (attack->defender != nullptr) {
|
||||
// NOTE: Uninline.
|
||||
bool hitFromFront = _is_hit_from_front(attack->defender, attack->attacker);
|
||||
bool hitFromFront = _is_hit_from_front(attack->attacker, attack->defender);
|
||||
|
||||
if (FID_TYPE(attack->defender->fid) == OBJ_TYPE_CRITTER) {
|
||||
if (attack->attacker->fid == FID_0x20001F5) {
|
||||
int knockbackRotation = tileGetRotationTo(attack->attacker->tile, attack->defender->tile);
|
||||
_show_damage_to_object(attack->defender, attack->defenderDamage, attack->defenderFlags, attack->weapon, hitFromFront, attack->defenderKnockback, knockbackRotation, attackerAnimation, attack->attacker, delay);
|
||||
} else {
|
||||
int weaponAnimation = critterGetAnimationForHitMode(attack->attacker, attack->hitMode);
|
||||
int knockbackRotation = tileGetRotationTo(attack->attacker->tile, attack->defender->tile);
|
||||
_show_damage_to_object(attack->defender, attack->defenderDamage, attack->defenderFlags, attack->weapon, hitFromFront, attack->defenderKnockback, knockbackRotation, weaponAnimation, attack->attacker, delay);
|
||||
}
|
||||
} else {
|
||||
tileGetRotationTo(attack->attacker->tile, attack->defender->tile);
|
||||
critterGetAnimationForHitMode(attack->attacker, attack->hitMode);
|
||||
int knockbackRotation = tileGetRotationTo(attack->attacker->tile, attack->defender->tile);
|
||||
int attackerAnimForShow = attack->attacker->fid == FRAME_ID_FORCE_FIELD_NS
|
||||
? attackerAnimation
|
||||
: critterGetAnimationForHitMode(attack->attacker, attack->hitMode);
|
||||
|
||||
showDamageToObject(attack->defender, attack->defenderDamage, attack->defenderFlags, attack->weapon, hitFromFront, attack->defenderKnockback, knockbackRotation, attackerAnimForShow, attack->attacker, delay);
|
||||
}
|
||||
}
|
||||
|
||||
if ((attack->attackerFlags & DAM_DUD) != 0) {
|
||||
_show_damage_to_object(attack->attacker, attack->attackerDamage, attack->attackerFlags, attack->weapon, 1, 0, 0, attackerAnimation, attack->attacker, -1);
|
||||
showDamageToAttacker(attack, attackerAnimation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -643,7 +655,7 @@ int _action_melee(Attack* attack, int anim)
|
||||
|
||||
animationRegisterAnimate(attack->attacker, anim, 0);
|
||||
animationRegisterPlaySoundEffect(attack->attacker, sfx_name_temp, delay);
|
||||
_show_damage(attack, anim, 0);
|
||||
showDamage(attack, anim, 0);
|
||||
} else {
|
||||
if (attack->defender->data.critter.combat.results & 0x03) {
|
||||
animationRegisterPlaySoundEffect(attack->attacker, sfx_name_temp, -1);
|
||||
@@ -685,7 +697,7 @@ int _action_melee(Attack* attack, int anim)
|
||||
return -1;
|
||||
}
|
||||
|
||||
_show_damage_extras(attack);
|
||||
showDamageToExtras(attack);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -914,7 +926,7 @@ int _action_ranged(Attack* attack, int anim)
|
||||
}
|
||||
}
|
||||
|
||||
_show_damage(attack, anim, delay);
|
||||
showDamage(attack, anim, delay);
|
||||
|
||||
if ((attack->attackerFlags & DAM_HIT) == 0) {
|
||||
_combatai_msg(attack->defender, attack, AI_MESSAGE_TYPE_MISS, -1);
|
||||
@@ -981,7 +993,7 @@ int _action_ranged(Attack* attack, int anim)
|
||||
return -1;
|
||||
}
|
||||
|
||||
_show_damage_extras(attack);
|
||||
showDamageToExtras(attack);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1511,7 +1523,7 @@ int actionUseSkill(Object* user, Object* target, int skill)
|
||||
}
|
||||
|
||||
// 0x412BC4
|
||||
bool _is_hit_from_front(Object* attacker, Object* defender)
|
||||
bool _is_hit_from_front(const Object* attacker, const Object* defender)
|
||||
{
|
||||
int diff = attacker->rotation - defender->rotation;
|
||||
if (diff < 0) {
|
||||
@@ -1534,9 +1546,10 @@ bool _can_see(Object* source, Object* target)
|
||||
return diff == 0 || diff == 1 || diff == 5;
|
||||
}
|
||||
|
||||
// looks like it tries to change fall animation depending on object's current rotation
|
||||
// 0x412C1C
|
||||
int _pick_fall(Object* obj, int anim)
|
||||
// Tries to change between ANIM_FALL_FRONT and ANIM_FALL_BACK based on available free space in front or behind and presence of art frames.
|
||||
//
|
||||
// 0x412C1C pick_fall
|
||||
int pickFallAnim(Object* obj, int anim)
|
||||
{
|
||||
int i;
|
||||
int rotation;
|
||||
@@ -1666,7 +1679,7 @@ int actionExplode(int tile, int elevation, int minDamage, int maxDamage, Object*
|
||||
animationRegisterPlaySoundEffect(explosion, "whn1xxx1", 0);
|
||||
animationRegisterUnsetFlag(explosion, OBJECT_HIDDEN, 0);
|
||||
animationRegisterAnimateAndHide(explosion, ANIM_STAND, 0);
|
||||
_show_damage(attack, 0, 1);
|
||||
showDamage(attack, 0, 1);
|
||||
|
||||
for (int rotation = 0; rotation < ROTATION_COUNT; rotation++) {
|
||||
animationRegisterUnsetFlag(adjacentExplosions[rotation], OBJECT_HIDDEN, 0);
|
||||
@@ -1697,7 +1710,7 @@ int actionExplode(int tile, int elevation, int minDamage, int maxDamage, Object*
|
||||
return -1;
|
||||
}
|
||||
|
||||
_show_damage_extras(attack);
|
||||
showDamageToExtras(attack);
|
||||
} else {
|
||||
if (critter != nullptr) {
|
||||
if ((attack->defenderFlags & DAM_DEAD) != 0) {
|
||||
@@ -1897,7 +1910,7 @@ void actionDamage(int tile, int elevation, int minDamage, int maxDamage, int dam
|
||||
}
|
||||
|
||||
Object* attacker;
|
||||
if (objectCreateWithFidPid(&attacker, FID_0x20001F5, -1) == -1) {
|
||||
if (objectCreateWithFidPid(&attacker, FRAME_ID_FORCE_FIELD_NS, -1) == -1) {
|
||||
internal_free(attack);
|
||||
return;
|
||||
}
|
||||
@@ -1932,7 +1945,7 @@ void actionDamage(int tile, int elevation, int minDamage, int maxDamage, int dam
|
||||
if (animated) {
|
||||
reg_anim_begin(ANIMATION_REQUEST_RESERVED);
|
||||
animationRegisterPlaySoundEffect(attacker, "whc1xxx1", 0);
|
||||
_show_damage(attack, gMaximumBloodDeathAnimations[damageType], 0);
|
||||
showDamage(attack, gMaximumBloodDeathAnimations[damageType], 0);
|
||||
animationRegisterCallbackForced(attack, nullptr, (AnimationCallback*)_report_dmg, 0);
|
||||
animationRegisterHideObjectForced(attacker);
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ int actionPickUp(Object* critter, Object* item);
|
||||
int _action_loot_container(Object* critter, Object* container);
|
||||
int _action_skill_use(int skill);
|
||||
int actionUseSkill(Object* user, Object* target, int skill);
|
||||
bool _is_hit_from_front(Object* attacker, Object* defender);
|
||||
bool _is_hit_from_front(const Object* attacker, const Object* defender);
|
||||
bool _can_see(Object* source, Object* target);
|
||||
bool _action_explode_running();
|
||||
int actionExplode(int tile, int elevation, int minDamage, int maxDamage, Object* sourceObj, bool animate);
|
||||
|
||||
+20
-1
@@ -226,6 +226,24 @@ typedef struct AnimationSequence {
|
||||
AnimationDescription animations[ANIMATION_DESCRIPTION_LIST_CAPACITY];
|
||||
} AnimationSequence;
|
||||
|
||||
// sfall: "in combat" check for animations is overridable
|
||||
static bool gRegAnimCombatCheck = true;
|
||||
|
||||
bool animationCheckCombatMode()
|
||||
{
|
||||
return gRegAnimCombatCheck && isInCombat();
|
||||
}
|
||||
|
||||
void animationSetCombatCheck(bool enable)
|
||||
{
|
||||
gRegAnimCombatCheck = enable;
|
||||
}
|
||||
|
||||
void animationResetCombatCheck()
|
||||
{
|
||||
gRegAnimCombatCheck = true;
|
||||
}
|
||||
|
||||
typedef struct PathNode {
|
||||
int tile;
|
||||
int from;
|
||||
@@ -937,6 +955,7 @@ int animationRegisterAnimateAndHide(Object* owner, int anim, int delay)
|
||||
}
|
||||
|
||||
// 0x414C50
|
||||
// TODO: original function likely handled both tiles and object
|
||||
int animationRegisterRotateToTile(Object* owner, int tile)
|
||||
{
|
||||
if (_check_registry(owner) == -1) {
|
||||
@@ -1206,7 +1225,7 @@ int animationRegisterTakeOutWeapon(Object* owner, int weaponAnimationCode, int d
|
||||
AnimationDescription* animationDescription = &(animationSequence->animations[gAnimationDescriptionCurrentIndex]);
|
||||
animationDescription->kind = ANIM_KIND_TAKE_OUT_WEAPON;
|
||||
animationDescription->anim = ANIM_TAKE_OUT;
|
||||
animationDescription->delay = 0;
|
||||
animationDescription->delay = 0; // TODO: should use `delay`?
|
||||
animationDescription->owner = owner;
|
||||
animationDescription->weaponAnimationCode = weaponAnimationCode;
|
||||
|
||||
|
||||
@@ -112,6 +112,9 @@ typedef Object* PathBuilderCallback(Object* object, int tile, int elevation);
|
||||
void animationInit();
|
||||
void animationReset();
|
||||
void animationExit();
|
||||
bool animationCheckCombatMode();
|
||||
void animationSetCombatCheck(bool enable);
|
||||
void animationResetCombatCheck();
|
||||
int reg_anim_begin(int a1);
|
||||
int _register_priority(int a1);
|
||||
int reg_anim_clear(Object* a1);
|
||||
|
||||
+26
-22
@@ -88,8 +88,8 @@ typedef struct DamageCalculationContext {
|
||||
int damageResistance;
|
||||
int damageThreshold;
|
||||
int damageBonus;
|
||||
int bonusDamageMultiplier;
|
||||
int combatDifficultyDamageModifier;
|
||||
int baseDamageMult;
|
||||
int difficultyDamagePercent;
|
||||
} DamageCalculationContext;
|
||||
|
||||
static bool _combat_safety_invalidate_weapon_func(Object* attacker, Object* weapon, int hitMode, Object* defender, int* safeDistancePtr, Object* attackerFriend);
|
||||
@@ -119,7 +119,7 @@ static int _attackFindInvalidFlags(Object* a1, Object* a2);
|
||||
static int attackComputeCriticalFailure(Attack* attack);
|
||||
static void _do_random_cripple(int* flagsPtr);
|
||||
static int attackDetermineToHit(Object* attacker, int tile, Object* defender, int hitLocation, int hitMode, bool useDistance);
|
||||
static void attackComputeDamage(Attack* attack, int ammoQuantity, int a3);
|
||||
static void attackComputeDamage(Attack* attack, int numRounds, int baseDamageMult);
|
||||
static void _check_for_death(Object* a1, int a2, int* a3);
|
||||
static void _set_new_results(Object* a1, int a2);
|
||||
static void _damage_object(Object* a1, int damage, bool animated, int a4, Object* a5);
|
||||
@@ -4232,8 +4232,8 @@ static int attackComputeCriticalFailure(Attack* attack)
|
||||
attack->attackerFlags &= ~v17;
|
||||
|
||||
if ((attack->attackerFlags & DAM_HIT_SELF) != 0) {
|
||||
int ammoQuantity = attackType == ATTACK_TYPE_RANGED ? attack->ammoQuantity : 1;
|
||||
attackComputeDamage(attack, ammoQuantity, 2);
|
||||
int rounds = attackType == ATTACK_TYPE_RANGED ? attack->ammoQuantity : 1;
|
||||
attackComputeDamage(attack, rounds, 2);
|
||||
} else if ((attack->attackerFlags & DAM_EXPLODE) != 0) {
|
||||
attackComputeDamage(attack, 1, 2);
|
||||
}
|
||||
@@ -4262,8 +4262,8 @@ static int attackComputeCriticalFailure(Attack* attack)
|
||||
attack->defenderHitLocation = HIT_LOCATION_TORSO;
|
||||
attack->attackerFlags &= ~DAM_CRITICAL;
|
||||
|
||||
int ammoQuantity = attackType == ATTACK_TYPE_RANGED ? attack->ammoQuantity : 1;
|
||||
attackComputeDamage(attack, ammoQuantity, 2);
|
||||
int rounds = attackType == ATTACK_TYPE_RANGED ? attack->ammoQuantity : 1;
|
||||
attackComputeDamage(attack, rounds, 2);
|
||||
} else {
|
||||
attack->defender = attack->oops;
|
||||
}
|
||||
@@ -4506,7 +4506,7 @@ static int attackDetermineToHit(Object* attacker, int tile, Object* defender, in
|
||||
}
|
||||
|
||||
// 0x4247B8
|
||||
static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamageMultiplier)
|
||||
static void attackComputeDamage(Attack* attack, int numRounds, int baseDamageMult)
|
||||
{
|
||||
int* damagePtr;
|
||||
Object* critter;
|
||||
@@ -4528,6 +4528,8 @@ static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamag
|
||||
*damagePtr = 0;
|
||||
|
||||
if (FID_TYPE(critter->fid) != OBJ_TYPE_CRITTER) {
|
||||
// This is to match sfall behavior as it wraps attackComputeDamage call and always invokes hook, even in this case.
|
||||
scriptHooks_ComputeDamage(attack, numRounds, baseDamageMult);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4557,14 +4559,14 @@ static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamag
|
||||
damageBonus = 0;
|
||||
}
|
||||
|
||||
int combatDifficultyDamageModifier = 100;
|
||||
int difficultyDamagePercent = 100;
|
||||
if (attack->attacker->data.critter.combat.team != gDude->data.critter.combat.team) {
|
||||
switch (settings.preferences.combat_difficulty) {
|
||||
case COMBAT_DIFFICULTY_EASY:
|
||||
combatDifficultyDamageModifier = 75;
|
||||
difficultyDamagePercent = 75;
|
||||
break;
|
||||
case COMBAT_DIFFICULTY_HARD:
|
||||
combatDifficultyDamageModifier = 125;
|
||||
difficultyDamagePercent = 125;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -4576,8 +4578,8 @@ static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamag
|
||||
context.damageResistance = damageResistance;
|
||||
context.damageThreshold = damageThreshold;
|
||||
context.damageBonus = damageBonus;
|
||||
context.bonusDamageMultiplier = bonusDamageMultiplier;
|
||||
context.combatDifficultyDamageModifier = combatDifficultyDamageModifier;
|
||||
context.baseDamageMult = baseDamageMult;
|
||||
context.difficultyDamagePercent = difficultyDamagePercent;
|
||||
|
||||
if (gDamageCalculationType == DAMAGE_CALCULATION_TYPE_GLOVZ || gDamageCalculationType == DAMAGE_CALCULATION_TYPE_GLOVZ_WITH_DAMAGE_MULTIPLIER_TWEAK) {
|
||||
damageModCalculateGlovz(&context);
|
||||
@@ -4591,10 +4593,10 @@ static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamag
|
||||
damageResistance = 0;
|
||||
}
|
||||
|
||||
int damageMultiplier = bonusDamageMultiplier * weaponGetAmmoDamageMultiplier(attack->weapon);
|
||||
int damageMultiplier = baseDamageMult * weaponGetAmmoDamageMultiplier(attack->weapon);
|
||||
int damageDivisor = weaponGetAmmoDamageDivisor(attack->weapon);
|
||||
|
||||
for (int index = 0; index < ammoQuantity; index++) {
|
||||
for (int index = 0; index < numRounds; index++) {
|
||||
int damage = weaponGetDamage(attack->attacker, attack->hitMode);
|
||||
|
||||
damage += damageBonus;
|
||||
@@ -4608,7 +4610,7 @@ static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamag
|
||||
// TODO: Why we're halving it?
|
||||
damage /= 2;
|
||||
|
||||
damage *= combatDifficultyDamageModifier;
|
||||
damage *= difficultyDamagePercent;
|
||||
damage /= 100;
|
||||
|
||||
damage -= damageThreshold;
|
||||
@@ -4665,6 +4667,8 @@ static void attackComputeDamage(Attack* attack, int ammoQuantity, int bonusDamag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scriptHooks_ComputeDamage(attack, numRounds, baseDamageMult);
|
||||
}
|
||||
|
||||
// 0x424BAC
|
||||
@@ -6692,9 +6696,9 @@ static void damageModCalculateGlovz(DamageCalculationContext* context)
|
||||
|
||||
int calculatedDamageResistance = context->damageResistance;
|
||||
if (calculatedDamageResistance > 0) {
|
||||
if (context->combatDifficultyDamageModifier > 100) {
|
||||
if (context->difficultyDamagePercent > 100) {
|
||||
calculatedDamageResistance -= 20;
|
||||
} else if (context->combatDifficultyDamageModifier < 100) {
|
||||
} else if (context->difficultyDamagePercent < 100) {
|
||||
calculatedDamageResistance += 20;
|
||||
}
|
||||
|
||||
@@ -6740,9 +6744,9 @@ static void damageModCalculateGlovz(DamageCalculationContext* context)
|
||||
}
|
||||
|
||||
if (gDamageCalculationType == DAMAGE_CALCULATION_TYPE_GLOVZ_WITH_DAMAGE_MULTIPLIER_TWEAK) {
|
||||
damage += damageModGlovzDivRound(damage * context->bonusDamageMultiplier * 25, 100);
|
||||
damage += damageModGlovzDivRound(damage * context->baseDamageMult * 25, 100);
|
||||
} else {
|
||||
damage += damage * context->bonusDamageMultiplier / 2;
|
||||
damage += damage * context->baseDamageMult / 2;
|
||||
}
|
||||
|
||||
if (damage > 0) {
|
||||
@@ -6775,7 +6779,7 @@ static int damageModGlovzDivRound(int dividend, int divisor)
|
||||
|
||||
static void damageModCalculateYaam(DamageCalculationContext* context)
|
||||
{
|
||||
int damageMultiplier = context->bonusDamageMultiplier * weaponGetAmmoDamageMultiplier(context->attack->weapon);
|
||||
int damageMultiplier = context->baseDamageMult * weaponGetAmmoDamageMultiplier(context->attack->weapon);
|
||||
int damageDivisor = weaponGetAmmoDamageDivisor(context->attack->weapon);
|
||||
|
||||
int ammoDamageResistance = weaponGetAmmoDamageResistanceModifier(context->attack->weapon);
|
||||
@@ -6812,7 +6816,7 @@ static void damageModCalculateYaam(DamageCalculationContext* context)
|
||||
}
|
||||
|
||||
damage /= 2;
|
||||
damage *= context->combatDifficultyDamageModifier;
|
||||
damage *= context->difficultyDamagePercent;
|
||||
damage /= 100;
|
||||
|
||||
damage -= damage * damageResistance / 100;
|
||||
|
||||
@@ -641,7 +641,7 @@ int fileNameListInit(const char* pattern, char*** fileNameListPtr)
|
||||
char dir[COMPAT_MAX_DIR];
|
||||
char fileName[COMPAT_MAX_FNAME];
|
||||
char extension[COMPAT_MAX_EXT];
|
||||
compat_windows_path_to_native(name);
|
||||
compat_path_to_native(name);
|
||||
compat_splitpath(name, nullptr, dir, fileName, extension);
|
||||
|
||||
if (!isWildcard || *dir == '\0' || (strchr(dir, '\\') == nullptr && strchr(dir, '/') == nullptr)) {
|
||||
|
||||
+26
-4
@@ -42,6 +42,7 @@ static DFile* dfileOpenInternal(DBase* dbase, const char* filename, const char*
|
||||
static int dfileReadCharInternal(DFile* stream);
|
||||
static bool dfileReadCompressed(DFile* stream, void* ptr, size_t size);
|
||||
static void dfileUngetCompressed(DFile* stream, int ch);
|
||||
static bool dfilePathMatchesPattern(const char* pattern, const char* path);
|
||||
|
||||
// Reads .DAT file contents.
|
||||
//
|
||||
@@ -201,11 +202,15 @@ bool dbaseClose(DBase* dbase)
|
||||
// 0x4E5308
|
||||
bool dbaseFindFirstEntry(DBase* dbase, DFileFindData* findFileData, const char* pattern)
|
||||
{
|
||||
// fpattern-matching requires native-separators
|
||||
assert(pattern != nullptr);
|
||||
compat_strlcpy(findFileData->pattern, pattern, sizeof(findFileData->pattern));
|
||||
compat_path_to_native(findFileData->pattern);
|
||||
|
||||
for (int index = 0; index < dbase->entriesLength; index++) {
|
||||
DBaseEntry* entry = &(dbase->entries[index]);
|
||||
if (fpattern_match(pattern, entry->path)) {
|
||||
if (dfilePathMatchesPattern(findFileData->pattern, entry->path)) {
|
||||
strcpy(findFileData->fileName, entry->path);
|
||||
strcpy(findFileData->pattern, pattern);
|
||||
findFileData->index = index;
|
||||
return true;
|
||||
}
|
||||
@@ -219,7 +224,7 @@ bool dbaseFindNextEntry(DBase* dbase, DFileFindData* findFileData)
|
||||
{
|
||||
for (int index = findFileData->index + 1; index < dbase->entriesLength; index++) {
|
||||
DBaseEntry* entry = &(dbase->entries[index]);
|
||||
if (fpattern_match(findFileData->pattern, entry->path)) {
|
||||
if (dfilePathMatchesPattern(findFileData->pattern, entry->path)) {
|
||||
strcpy(findFileData->fileName, entry->path);
|
||||
findFileData->index = index;
|
||||
return true;
|
||||
@@ -638,7 +643,15 @@ static int dbaseFindEntryByFilePath(const void* file, const void* entryName)
|
||||
// 0x4E5D9C
|
||||
static DFile* dfileOpenInternal(DBase* dbase, const char* filePath, const char* mode, DFile* dfile)
|
||||
{
|
||||
DBaseEntry* entry = (DBaseEntry*)bsearch(filePath, dbase->entries, dbase->entriesLength, sizeof(*dbase->entries), dbaseFindEntryByFilePath);
|
||||
assert(filePath != nullptr);
|
||||
|
||||
// .dat files contain windows path separators
|
||||
char normalizedFilePath[COMPAT_MAX_PATH];
|
||||
compat_strlcpy(normalizedFilePath, filePath, sizeof(normalizedFilePath));
|
||||
compat_path_to_windows(normalizedFilePath);
|
||||
|
||||
DBaseEntry* entry;
|
||||
entry = (DBaseEntry*)bsearch(normalizedFilePath, dbase->entries, dbase->entriesLength, sizeof(*dbase->entries), dbaseFindEntryByFilePath);
|
||||
if (entry == nullptr) {
|
||||
goto err;
|
||||
}
|
||||
@@ -860,4 +873,13 @@ static void dfileUngetCompressed(DFile* stream, int ch)
|
||||
stream->position--;
|
||||
}
|
||||
|
||||
// pattern must be normalized to native paths, since that's what fpattern requires
|
||||
static bool dfilePathMatchesPattern(const char* pattern, const char* path)
|
||||
{
|
||||
char normalizedPath[COMPAT_MAX_PATH];
|
||||
compat_strlcpy(normalizedPath, path, sizeof(normalizedPath));
|
||||
compat_path_to_native(normalizedPath);
|
||||
return fpattern_match(pattern, normalizedPath);
|
||||
}
|
||||
|
||||
} // namespace fallout
|
||||
|
||||
+11
-7
@@ -3,6 +3,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include "art.h"
|
||||
#include "color.h"
|
||||
@@ -229,9 +230,9 @@ void displayMonitorExit()
|
||||
}
|
||||
|
||||
// 0x43186C
|
||||
void displayMonitorAddMessage(char* str)
|
||||
void displayMonitorAddMessage(const char* str)
|
||||
{
|
||||
if (!gDisplayMonitorInitialized) {
|
||||
if (!gDisplayMonitorInitialized || str == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -256,10 +257,13 @@ void displayMonitorAddMessage(char* str)
|
||||
}
|
||||
}
|
||||
|
||||
std::string mutableMessage(str);
|
||||
char* mutableStr = mutableMessage.data();
|
||||
|
||||
// TODO: Refactor these two loops.
|
||||
char* splitPos = nullptr;
|
||||
while (true) {
|
||||
while (fontGetStringWidth(str) < DISPLAY_MONITOR_WIDTH - _max_disp - knobWidth) {
|
||||
while (fontGetStringWidth(mutableStr) < DISPLAY_MONITOR_WIDTH - _max_disp - knobWidth) {
|
||||
char* temp = gDisplayMonitorLines[_disp_start];
|
||||
int length;
|
||||
if (knob != '\0') {
|
||||
@@ -270,7 +274,7 @@ void displayMonitorAddMessage(char* str)
|
||||
} else {
|
||||
length = DISPLAY_MONITOR_LINE_LENGTH - 1;
|
||||
}
|
||||
strncpy(temp, str, length);
|
||||
strncpy(temp, mutableStr, length);
|
||||
gDisplayMonitorLines[_disp_start][DISPLAY_MONITOR_LINE_LENGTH - 1] = '\0';
|
||||
_disp_start = (_disp_start + 1) % gDisplayMonitorLinesCapacity;
|
||||
|
||||
@@ -281,12 +285,12 @@ void displayMonitorAddMessage(char* str)
|
||||
return;
|
||||
}
|
||||
|
||||
str = splitPos + 1;
|
||||
mutableStr = splitPos + 1;
|
||||
*splitPos = ' ';
|
||||
splitPos = nullptr;
|
||||
}
|
||||
|
||||
char* space = strrchr(str, ' ');
|
||||
char* space = strrchr(mutableStr, ' ');
|
||||
if (space == nullptr) {
|
||||
break;
|
||||
}
|
||||
@@ -311,7 +315,7 @@ void displayMonitorAddMessage(char* str)
|
||||
} else {
|
||||
length = DISPLAY_MONITOR_LINE_LENGTH - 1;
|
||||
}
|
||||
strncpy(temp, str, length);
|
||||
strncpy(temp, mutableStr, length);
|
||||
|
||||
gDisplayMonitorLines[_disp_start][DISPLAY_MONITOR_LINE_LENGTH - 1] = '\0';
|
||||
_disp_start = (_disp_start + 1) % gDisplayMonitorLinesCapacity;
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace fallout {
|
||||
int displayMonitorInit();
|
||||
int displayMonitorReset();
|
||||
void displayMonitorExit();
|
||||
void displayMonitorAddMessage(char* string);
|
||||
void displayMonitorAddMessage(const char* string);
|
||||
void displayMonitorDisable();
|
||||
void displayMonitorEnable();
|
||||
|
||||
|
||||
+19
-14
@@ -128,6 +128,23 @@ int gSplashScreenScaling = 0;
|
||||
// CE: Sonora folks like to store objects in global variables.
|
||||
static void** gGameGlobalPointers = nullptr;
|
||||
|
||||
static void debugModeInit()
|
||||
{
|
||||
// CE: Handle debug mode (exactly as seen in `mapper2.exe`).
|
||||
const char* debugMode = settings.debug.mode.c_str();
|
||||
if (compat_stricmp(debugMode, "environment") == 0) {
|
||||
_debug_register_env();
|
||||
} else if (compat_stricmp(debugMode, "screen") == 0) {
|
||||
_debug_register_screen();
|
||||
} else if (compat_stricmp(debugMode, "log") == 0) {
|
||||
_debug_register_log("debug.log", "wt");
|
||||
} else if (compat_stricmp(debugMode, "mono") == 0) {
|
||||
_debug_register_mono();
|
||||
} else if (compat_stricmp(debugMode, "gnw") == 0) {
|
||||
_debug_register_func(_win_debug);
|
||||
}
|
||||
}
|
||||
|
||||
// 0x442580
|
||||
int gameInitWithOptions(const char* windowTitle, bool isMapper, int font, int flags, int argc, char** argv)
|
||||
{
|
||||
@@ -146,6 +163,8 @@ int gameInitWithOptions(const char* windowTitle, bool isMapper, int font, int fl
|
||||
|
||||
settingsInit(isMapper, argc, argv);
|
||||
|
||||
debugModeInit();
|
||||
|
||||
gIsMapper = isMapper;
|
||||
|
||||
if (gameDbInit() == -1) {
|
||||
@@ -192,20 +211,6 @@ int gameInitWithOptions(const char* windowTitle, bool isMapper, int font, int fl
|
||||
showSplash();
|
||||
}
|
||||
|
||||
// CE: Handle debug mode (exactly as seen in `mapper2.exe`).
|
||||
const char* debugMode = settings.debug.mode.c_str();
|
||||
if (compat_stricmp(debugMode, "environment") == 0) {
|
||||
_debug_register_env();
|
||||
} else if (compat_stricmp(debugMode, "screen") == 0) {
|
||||
_debug_register_screen();
|
||||
} else if (compat_stricmp(debugMode, "log") == 0) {
|
||||
_debug_register_log("debug.log", "wt");
|
||||
} else if (compat_stricmp(debugMode, "mono") == 0) {
|
||||
_debug_register_mono();
|
||||
} else if (compat_stricmp(debugMode, "gnw") == 0) {
|
||||
_debug_register_func(_win_debug);
|
||||
}
|
||||
|
||||
interfaceFontsInit();
|
||||
fontManagerAdd(&gModernFontManager);
|
||||
fontSetCurrent(font);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user