#include #include #include #include #include "boe.global.hpp" #include "universe/universe.hpp" #include "boe.party.hpp" #include "boe.town.hpp" #include "boe.text.hpp" #include "boe.infodlg.hpp" #include "boe.items.hpp" #include "boe.combat.hpp" #include "boe.monster.hpp" #include "boe.locutils.hpp" #include "boe.actions.hpp" #include "sounds.hpp" #include "boe.townspec.hpp" #include "boe.graphics.hpp" #include "boe.fileio.hpp" #include "boe.specials.hpp" #include "boe.newgraph.hpp" #include "boe.dlgutil.hpp" #include "mathutil.hpp" #include "boe.main.hpp" #include "dialogxml/dialogs/strdlog.hpp" #include "dialogxml/dialogs/choicedlog.hpp" #include "dialogxml/dialogs/3choice.hpp" #include "fileio/fileio.hpp" #include #include "spell.hpp" #include "boe.menus.hpp" #include "replay.hpp" #include #include "winutil.hpp" extern eGameMode overall_mode; extern eItemWinMode stat_window; extern short which_combat_type; extern location center; extern bool processing_fields,monsters_going,boom_anim_active; extern effect_pat_type current_pat; extern cOutdoors::cWandering store_wandering_special; extern eSpell spell_being_cast, town_spell; extern eSpecCtxType spec_target_type; extern short spell_caster, spec_target_fail, spec_target_options; extern short fast_bang; extern bool end_scenario; extern cUniverse univ; extern std::queue special_queue; extern short combat_posing_monster; bool can_draw_pcs = true; bool fog_lifted = false; bool cartoon_happening = false; std::map boom_gr = { {eDamageType::WEAPON, 3}, {eDamageType::FIRE, 0}, {eDamageType::POISON, 2}, {eDamageType::MAGIC, 1}, {eDamageType::UNBLOCKABLE, 5}, {eDamageType::COLD, 4}, {eDamageType::UNDEAD, 3}, {eDamageType::DEMON, 3}, {eDamageType::SPECIAL, 1}, }; short store_item_spell_level = 10; extern short store_spell_target; extern std::map skill_max; // global values for when processing special encounters bool special_in_progress = false; struct runtime_state { eSpecCtx which_mode; cSpecial cur_spec; eSpecCtxType cur_spec_type; location spec_loc; mutable short next_spec; mutable eSpecCtxType next_spec_type; mutable iLiving* cur_target; short* ret_a; short* ret_b; bool* redraw; }; // The internals of the special nodes runtime static cSpecial get_node(spec_num_t cur_spec, eSpecCtxType cur_spec_type); static iLiving& current_pc_picked_in_spec_enc(const runtime_state& ctx); static void resolve_pointers(cSpecial& cur_node); static void setsd(short a, short b, short val); static bool isValidField(int fld, bool allowSpecial); static void handle_message(const runtime_state& ctx, const std::string& title = "", pic_num_t pic = -1, ePicType pt = PIC_SCEN); // Sub-runtimes for each category of node void general_spec(const runtime_state& ctx); void ifthen_spec(const runtime_state& ctx); void affect_spec(const runtime_state& ctx); void oneshot_spec(const runtime_state& ctx); void townmode_spec(const runtime_state& ctx); void rect_spec(const runtime_state& ctx); void outdoor_spec(const runtime_state& ctx); static void start_cartoon() { if(!cartoon_happening && !is_combat()) { for(int i = 0; i < 6; i++) univ.party[i].combat_pos = univ.party.town_loc; } cartoon_happening = true; } //short mode; // 0 - pre 1 - end by victory 2 - end by flight bool handle_wandering_specials(short mode) { // TODO: Is loc_in_sec the correct location to pass here? // (I'm pretty sure it is, but I should verify it somehow.) // (It's either that or univ.party.p_loc.) switch(mode) { case 0: // pre-combat, possibly prevent the fight // TODO: If s2 > 0, encounter is forced (monsters don't flee even if they're weak) if((mode == 0) && (store_wandering_special.spec_on_meet >= 0)) { // When encountering short prevent; run_special(eSpecCtx::OUTDOOR_ENC,eSpecCtxType::OUTDOOR,store_wandering_special.spec_on_meet,univ.party.loc_in_sec,&prevent); if(prevent > 0) return false; } break; case 1: // end by victory run_special(eSpecCtx::WIN_ENCOUNTER,eSpecCtxType::OUTDOOR,store_wandering_special.spec_on_win,univ.party.loc_in_sec); break; case 2: // end by loss (flight or total party kill) run_special(eSpecCtx::FLEE_ENCOUNTER,eSpecCtxType::OUTDOOR,store_wandering_special.spec_on_flee,univ.party.loc_in_sec); break; } return true; } // returns true if can enter this space // sets forced to true if definitely can enter bool check_special_terrain(location where_check,eSpecCtx mode,cPlayer& which_pc,bool *forced) { ter_num_t ter; short r1,door_pc,pic_type = 0; eTerSpec ter_special; std::string choice; int ter_flag1,ter_flag2,ter_flag3; eDamageType dam_type = eDamageType::WEAPON; bool can_enter = true; location out_where,from_loc,to_loc; short s1 = 0,s2 = 0; short spec_num = -1; *forced = false; switch(mode) { case eSpecCtx::OUT_MOVE: ter = univ.out[where_check.x][where_check.y]; from_loc = univ.party.out_loc; break; case eSpecCtx::TOWN_MOVE: ter = univ.town->terrain(where_check.x,where_check.y); from_loc = univ.party.town_loc; break; case eSpecCtx::COMBAT_MOVE: ter = univ.town->terrain(where_check.x,where_check.y); from_loc = univ.current_pc().combat_pos; break; default: // No movement happened, so just return false. // TODO: Should there be an error message here? std::cout << "Note: Improper mode passed to check_special_terrain: " << int(mode) << std::endl; return false; } ter_special = univ.scenario.ter_types[ter].special; ter_flag1 = univ.scenario.ter_types[ter].flag1; ter_flag2 = univ.scenario.ter_types[ter].flag2; ter_flag3 = univ.scenario.ter_types[ter].flag3; // TODO: Why not support conveyors outdoors, too? if(mode != eSpecCtx::OUT_MOVE && ter_special == eTerSpec::CONVEYOR) { if( ((ter_flag1 == DIR_N) && (where_check.y > from_loc.y)) || ((ter_flag1 == DIR_E) && (where_check.x < from_loc.x)) || ((ter_flag1 == DIR_S) && (where_check.y < from_loc.y)) || ((ter_flag1 == DIR_W) && (where_check.x > from_loc.x)) ) { ASB("The moving floor prevents you."); return false; } } if(mode == eSpecCtx::OUT_MOVE) { out_where = global_to_local(where_check); for(short i = 0; i < univ.out->special_locs.size(); i++) if(out_where == univ.out->special_locs[i]) { spec_num = univ.out->special_locs[i].spec; // call special run_special(mode, eSpecCtxType::OUTDOOR, spec_num, out_where, &s1, &s2); if(s1 > 0) can_enter = false; else if(s2 > 0) *forced = true; erase_out_specials(); put_pc_screen(); put_item_screen(stat_window); } } if((is_combat()) && (univ.town.is_spot(where_check.x, where_check.y) || (univ.scenario.ter_types[coord_to_ter(where_check.x, where_check.y)].trim_type == eTrimType::CITY))) { ASB("Move: Can't trigger this special in combat."); return false; // TODO: Maybe replace eTrimType::CITY check with a blockage == clear/special && is_special() check? } if(mode != eSpecCtx::OUT_MOVE && univ.town.is_force_barr(where_check.x,where_check.y)) { add_string_to_buf(" Magic barrier!"); can_enter = false; } if(mode != eSpecCtx::OUT_MOVE && univ.town.is_force_cage(where_check.x,where_check.y)) { add_string_to_buf(" Force cage!"); can_enter = false; } if((mode == eSpecCtx::TOWN_MOVE || (mode == eSpecCtx::COMBAT_MOVE && which_combat_type == 1)) && can_enter && univ.town.is_special(where_check.x,where_check.y)) { for(short i = 0; i < univ.town->special_locs.size(); i++) if(where_check == univ.town->special_locs[i]) { spec_num = univ.town->special_locs[i].spec; bool runSpecial = false; if(!is_blocked(where_check)) runSpecial = true; if(ter_special == eTerSpec::CHANGE_WHEN_STEP_ON) runSpecial = true; if(ter_special == eTerSpec::CALL_SPECIAL) runSpecial = true; if(univ.town->specials[spec_num].type == eSpecType::CANT_ENTER) runSpecial = true; if(!univ.scenario.is_legacy && univ.party.in_boat >= 0 && univ.scenario.ter_types[ter].boat_over) runSpecial = true; if(runSpecial) { give_help(54,0); run_special(mode, eSpecCtxType::TOWN, spec_num, where_check, &s1, &s2); if(s1 > 0) can_enter = false; else if(s2 > 0) *forced = true; } } put_pc_screen(); put_item_screen(stat_window); } // TODO: Just verify that yes, it works with this and doesn't work without it. if(mode == eSpecCtx::COMBAT_MOVE && town_boat_there(where_check)) { add_string_to_buf("Blocked: Can't enter boats in combat"); can_enter = false; } if(!can_enter) return false; if(!is_out()) { check_fields(where_check,mode,which_pc); if(univ.town.is_web(where_check.x,where_check.y) && univ.current_pc().race != eRace::BUG) { add_string_to_buf(" Webs!"); if(mode != eSpecCtx::COMBAT_MOVE) { for(short i = 0; i < 6; i++) { r1 = get_ran(1,2,3); univ.party[i].web(r1); } } else univ.current_pc().web(get_ran(1,2,3)); put_pc_screen(); univ.town.set_web(where_check.x,where_check.y,false); } if(univ.town.is_crate(where_check.x,where_check.y)) { add_string_to_buf(" You push the crate."); to_loc = push_loc(from_loc,where_check); univ.town.set_crate(where_check.x,where_check.y,false); if(to_loc.x > 0) univ.town.set_crate(to_loc.x,to_loc.y,true); for(short i = 0; i < univ.town.items.size(); i++) if(univ.town.items[i].variety != eItemType::NO_ITEM && univ.town.items[i].item_loc == where_check && univ.town.items[i].contained && univ.town.items[i].held) univ.town.items[i].item_loc = to_loc; } if(univ.town.is_barrel(where_check.x,where_check.y)) { add_string_to_buf(" You push the barrel."); to_loc = push_loc(from_loc,where_check); univ.town.set_barrel(where_check.x,where_check.y,false); if(to_loc.x > 0) univ.town.set_barrel(to_loc.x,to_loc.y,true); for(short i = 0; i < univ.town.items.size(); i++) if(univ.town.items[i].variety != eItemType::NO_ITEM && univ.town.items[i].item_loc == where_check && univ.town.items[i].contained && univ.town.items[i].held) univ.town.items[i].item_loc = to_loc; } if(univ.town.is_block(where_check.x,where_check.y)) { add_string_to_buf(" You push the stone block."); to_loc = push_loc(from_loc,where_check); univ.town.set_block(where_check.x,where_check.y,false); if(to_loc.x > 0) univ.town.set_block(to_loc.x,to_loc.y,true); } } switch(ter_special) { // First, put the specials that aren't activated on moving into the space here. // This is to silence compiler warnings while still preserving the warning if a new special is added. case eTerSpec::NONE: case eTerSpec::BRIDGE: case eTerSpec::BED: case eTerSpec::UNUSED1: case eTerSpec::CRUMBLING: case eTerSpec::LOCKABLE: case eTerSpec::UNUSED2: case eTerSpec::IS_A_SIGN: case eTerSpec::UNUSED3: case eTerSpec::IS_A_CONTAINER: case eTerSpec::WATERFALL_CAVE: case eTerSpec::WATERFALL_SURFACE: case eTerSpec::CONVEYOR: case eTerSpec::BLOCKED_TO_MONSTERS: case eTerSpec::TOWN_ENTRANCE: case eTerSpec::CHANGE_WHEN_USED: case eTerSpec::CALL_SPECIAL_WHEN_USED: break; case eTerSpec::CHANGE_WHEN_STEP_ON: alter_space(where_check.x,where_check.y,ter_flag1); if(ter_flag2 >= 0) { play_sound(-1 * ter_flag2); } give_help(47,65); if(univ.scenario.ter_types[ter].blocksMove()) can_enter = false; break; case eTerSpec::DAMAGING: // if the party is flying, in a boat, or entering a boat, they cannot be harmed by terrain if(flying() || univ.party.in_boat >= 0) break; if(mode == eSpecCtx::TOWN_MOVE && town_boat_there(where_check)) break; if(mode == eSpecCtx::OUT_MOVE && out_boat_there(where_check)) break; if(ter_flag3 > 0 && ter_flag3 < 8) dam_type = (eDamageType) ter_flag3; else dam_type = eDamageType::WEAPON; r1 = get_ran(ter_flag2,1,ter_flag1); switch(dam_type){ case eDamageType::FIRE: add_string_to_buf(" It's hot!"); pic_type = 0; // TODO: Would be nice to have something similar to this but for other damaging terrains... if(univ.party.status[ePartyStatus::FIREWALK] > 0) { add_string_to_buf(" It doesn't affect you."); r1 = -1; } break; case eDamageType::COLD: add_string_to_buf(" You feel cold!"); pic_type = 4; break; case eDamageType::SPECIAL: dam_type = eDamageType::UNBLOCKABLE; BOOST_FALLTHROUGH; case eDamageType::MAGIC: case eDamageType::UNBLOCKABLE: add_string_to_buf(" Something shocks you!"); pic_type = 1; break; case eDamageType::WEAPON: add_string_to_buf(" You feel pain!"); pic_type = 3; break; case eDamageType::POISON: add_string_to_buf(" You suddenly feel very ill for a moment..."); pic_type = 2; break; case eDamageType::UNDEAD: case eDamageType::DEMON: add_string_to_buf(" A dark wind blows through you!"); pic_type = 1; // TODO: Verify that this is correct break; case eDamageType::MARKED: break; } if(r1 < 0) break; // "It doesn't affect you." if(mode != eSpecCtx::COMBAT_MOVE) hit_party(r1,dam_type); fast_bang = 1; if(mode == eSpecCtx::COMBAT_MOVE) damage_pc(which_pc,r1,dam_type,eRace::UNKNOWN); else boom_space(univ.party.out_loc,overall_mode,pic_type,r1,12); fast_bang = 0; break; case eTerSpec::DANGEROUS: // if the party is flying, in a boat, or entering a boat, they cannot be harmed by terrain if(flying() || univ.party.in_boat >= 0) break; if(mode == eSpecCtx::TOWN_MOVE && town_boat_there(where_check)) break; if(mode == eSpecCtx::OUT_MOVE && out_boat_there(where_check)) break; //one_sound(17); for(short i = mode == eSpecCtx::COMBAT_MOVE ? univ.get_target_i(which_pc) : 0 ; i < 6; i++) if(univ.party[i].main_status == eMainStatus::ALIVE) { if(get_ran(1,1,100) <= ter_flag2) { switch((eStatus)ter_flag3){ // TODO: Should we disallow some statuses when outdoors? case eStatus::POISONED_WEAPON: // TODO: Do something here if(get_ran(1,1,100) > 60) add_string_to_buf("It's eerie here..."); break; case eStatus::BLESS_CURSE: // Should say "You feel awkward." / "You feel blessed."? univ.party[i].curse(ter_flag1); break; case eStatus::POISON: univ.party[i].poison(ter_flag1); break; case eStatus::HASTE_SLOW: // Should say "You feel sluggish." / "You feel speedy."? univ.party[i].slow(ter_flag1); break; case eStatus::INVULNERABLE: // Should say "You feel odd." / "You feel protected."? univ.party[i].apply_status(eStatus::INVULNERABLE,ter_flag1); break; case eStatus::MAGIC_RESISTANCE: // Should say "You feel odd." / "You feel protected."? univ.party[i].apply_status(eStatus::MAGIC_RESISTANCE,ter_flag1); break; case eStatus::WEBS: // Should say "You feel sticky." / "Your skin tingles."? univ.party[i].web(ter_flag1); break; case eStatus::DISEASE: // Should say "You feel healthy." / "You feel sick."? univ.party[i].disease(ter_flag1); break; case eStatus::INVISIBLE: if(ter_flag1 < 0) add_string_to_buf("You feel obscure."); else add_string_to_buf("You feel exposed."); univ.party[i].apply_status(eStatus::INVISIBLE,ter_flag1); break; case eStatus::DUMB: // Should say "You feel clearheaded." / "You feel confused."? univ.party[i].dumbfound(ter_flag1); break; case eStatus::MARTYRS_SHIELD: // Should say "You feel dull." / "You start to glow slightly."? univ.party[i].apply_status(eStatus::MARTYRS_SHIELD,ter_flag1); break; case eStatus::ASLEEP: // Should say "You feel alert." / "You feel very tired."? univ.party[i].sleep(eStatus::ASLEEP,ter_flag1,ter_flag1 / 2); break; case eStatus::PARALYZED: // Should say "You find it easier to move." / "You feel very stiff."? univ.party[i].sleep(eStatus::PARALYZED,ter_flag1,ter_flag1 / 2); break; case eStatus::ACID: // Should say "Your skin tingles pleasantly." / "Your skin burns!"? univ.party[i].acid(ter_flag1); break; case eStatus::FORCECAGE: if(is_out()) break; univ.party[i].sleep(eStatus::FORCECAGE,ter_flag1,ter_flag1 / 2); break; case eStatus::MAIN: case eStatus::CHARM: // These magic values are illegal in this context break; } if(mode == eSpecCtx::COMBAT_MOVE) break; // only damage once in combat! } } put_pc_screen(); if(ter_flag3 == int(eStatus::DUMB)) adjust_spell_menus(); //print_nums(1,which_pc,current_pc); break; case eTerSpec::CALL_SPECIAL: { eSpecCtxType spec_type = eSpecCtxType::SCEN; if(ter_flag2 == 1){ if(mode == eSpecCtx::TOWN_MOVE || (mode == eSpecCtx::COMBAT_MOVE && which_combat_type == 1)) spec_type = eSpecCtxType::TOWN; else spec_type = eSpecCtxType::OUTDOOR; } run_special(mode, spec_type, ter_flag1, where_check, &s1, &s2); if(s1 > 0) can_enter = false; break; } // Locked doors case eTerSpec::UNLOCKABLE: if(is_combat()) { // No lockpicking in combat add_string_to_buf(" Can't enter: It's locked."); break; } // See what party wants to do. choice = cChoiceDlog("locked-door-action",{"leave","pick","bash"}).show(); can_enter = false; if(choice == "leave") break; if(choice == "pick"){ if((door_pc = select_pc(eSelectPC::ONLY_CAN_LOCKPICK, "Who will pick the lock?", eSkill::LOCKPICKING)) < 6) pick_lock(where_check,door_pc); }else{ if((door_pc = select_pc(eSelectPC::ONLY_LIVING, "Who will bash?", eSkill::STRENGTH)) < 6) bash_door(where_check,door_pc); } break; case eTerSpec::WILDERNESS_CAVE: case eTerSpec::WILDERNESS_SURFACE: handle_hunting(); break; } // Action may change terrain, so update what's been seen if(is_town()) update_explored(univ.party.town_loc); if(is_combat()) update_explored(univ.current_pc().combat_pos); return can_enter; } // This procedure find the effects of fields that would affect a PC who moves into // a space or waited in that same space // In town mode, process_fields() is responsible for actually dealing the damage // All this does is print a message void check_fields(location where_check,eSpecCtx mode,cPlayer& which_pc) { short r1; if(mode != eSpecCtx::COMBAT_MOVE && mode != eSpecCtx::TOWN_MOVE && mode != eSpecCtx::OUT_MOVE) { std::cout << "Note: Improper mode passed to check_special_terrain: " << int(mode) << std::endl; return; } if(is_out()) return; if(univ.town.is_fire_wall(where_check.x,where_check.y)) { add_string_to_buf(" Fire wall!"); r1 = get_ran(1,1,6) + 1; if(mode == eSpecCtx::COMBAT_MOVE) damage_pc(which_pc,r1,eDamageType::FIRE,eRace::UNKNOWN); } if(univ.town.is_force_wall(where_check.x,where_check.y)) { add_string_to_buf(" Force wall!"); r1 = get_ran(2,1,6); if(mode == eSpecCtx::COMBAT_MOVE) damage_pc(which_pc,r1,eDamageType::MAGIC,eRace::UNKNOWN); } if(univ.town.is_ice_wall(where_check.x,where_check.y)) { add_string_to_buf(" Ice wall!"); r1 = get_ran(2,1,6); if(mode == eSpecCtx::COMBAT_MOVE) damage_pc(which_pc,r1,eDamageType::COLD,eRace::UNKNOWN); if(!is_combat()) boom_space(univ.party.town_loc,overall_mode,4,r1,7); } if(univ.town.is_blade_wall(where_check.x,where_check.y)) { add_string_to_buf(" Blade wall!"); r1 = get_ran(4,1,8); if(mode == eSpecCtx::COMBAT_MOVE) damage_pc(which_pc,r1,eDamageType::WEAPON,eRace::UNKNOWN); } if(univ.town.is_quickfire(where_check.x,where_check.y)) { add_string_to_buf(" Quickfire!"); r1 = get_ran(2,1,8); if(mode == eSpecCtx::COMBAT_MOVE) damage_pc(which_pc,r1,eDamageType::FIRE,eRace::UNKNOWN); } if(univ.town.is_scloud(where_check.x,where_check.y)) { add_string_to_buf(" Stinking cloud!"); which_pc.curse(get_ran(1,2,3)); } if(univ.town.is_sleep_cloud(where_check.x,where_check.y)) { add_string_to_buf(" Sleep cloud!"); which_pc.sleep(eStatus::ASLEEP,3,0); } if(univ.town.is_fire_barr(where_check.x,where_check.y)) { add_string_to_buf(" Magic barrier!"); r1 = get_ran(2,1,10); if(is_town()) fast_bang = 1; if(mode == eSpecCtx::COMBAT_MOVE) damage_pc(which_pc,r1,eDamageType::MAGIC,eRace::UNKNOWN); else hit_party(r1,eDamageType::MAGIC,0); fast_bang = 0; } put_pc_screen(); } void use_spec_item(short item, bool& need_redraw) { if(recording){ record_action("use_spec_item", boost::lexical_cast(item)); } run_special(eSpecCtx::USE_SPEC_ITEM, eSpecCtxType::SCEN, univ.scenario.special_items[item].special, univ.party.get_loc()); need_redraw = true; } void use_item(short pc,short item) { bool take_charge = true; short level,str,r1; short sp[3] = {}; // Dummy values to pass to run_special; not actually used std::string str1, str2; // Used by books eItemUse type; location user_loc; const cItem& item_rec = univ.party[pc].items[item]; eItemAbil abil = item_rec.ability; bool inept_ok = !item_rec.use_magic(); level = univ.party[pc].items[item].item_level; (void) level; // TODO: Is it correct to never use the level? // Maybe it is, since abilities have their own level? if(is_out()) user_loc = univ.party.out_loc; if(is_town()) user_loc = univ.party.town_loc; if(is_combat()) user_loc = univ.current_pc().combat_pos; if(!item_rec.can_use()) { add_string_to_buf("Use: Can't use this item."); take_charge = false; } if(item_rec.rechargeable && item_rec.charges == 0) { add_string_to_buf("Use: No charges left."); take_charge = false; } if(univ.party[pc].traits[eTrait::MAGICALLY_INEPT] && !inept_ok){ add_string_to_buf("Use: Can't - magically inept."); take_charge = false; } if(take_charge) { if(overall_mode == MODE_OUTDOORS && !item_rec.use_outdoors()) { add_string_to_buf("Use: Not while outdoors."); take_charge = false; } if((overall_mode != MODE_OUTDOORS) && !item_rec.use_in_town() && !item_rec.use_in_combat()){ add_string_to_buf("Use: Only outdoors."); take_charge = false; } if((overall_mode == MODE_TOWN) && !item_rec.use_in_town()) { add_string_to_buf("Use: Not while in town."); take_charge = false; } if((overall_mode == MODE_COMBAT) && !item_rec.use_in_combat()) { add_string_to_buf("Use: Not in combat."); take_charge = false; } } if(take_charge) { cItem& the_item = univ.party[pc].items[item]; std::string name; if(!the_item.ident) name = the_item.name.c_str(); else name = the_item.full_name.c_str(); add_string_to_buf("Use: " + name); if(the_item.variety == eItemType::POTION) play_sound(56); str = the_item.abil_strength; store_item_spell_level = str; type = the_item.magic_use_type; switch(abil) { case eItemAbil::POISON_WEAPON: // poison weapon take_charge = poison_weapon(pc,str,false); break; case eItemAbil::AFFECT_STATUS: { auto status = the_item.abil_data.status; switch(status) { case eStatus::MAIN: case eStatus::CHARM: // These don't make any sense in this context. break; case eStatus::POISONED_WEAPON: if(the_item.abil_harms()) { ASB(" Weapon poison lost."); if(the_item.abil_group()) univ.party.apply_status(eStatus::POISONED_WEAPON,-str); else univ.party[pc].apply_status(eStatus::POISONED_WEAPON,-str); } else if(the_item.abil_group()) { for(short i = 0; i < 6; i++) take_charge = take_charge || poison_weapon(i,str,true); } else take_charge = poison_weapon(pc,str,true); break; case eStatus::BLESS_CURSE: play_sound(4); if(!the_item.abil_harms()) { ASB(" You feel blessed."); str = str * -1; }else ASB(" You feel awkward."); if(the_item.abil_group()) univ.party.curse(str); else univ.party[pc].curse(str); break; case eStatus::HASTE_SLOW: // TODO: Is this the right sound? play_sound(75); if(!the_item.abil_harms()) { ASB(" You feel speedy."); str = str * -1; }else ASB(" You feel sluggish."); if(the_item.abil_group()) univ.party.slow(str); else univ.party[pc].slow(str); break; case eStatus::INVULNERABLE: // TODO: Is this the right sound? play_sound(68); if(the_item.abil_harms()) { ASB(" You feel odd."); str = str * -1; }else ASB(" You feel protected."); if(the_item.abil_group()) univ.party.apply_status(status,str); else univ.party[pc].apply_status(status,str); break; case eStatus::MAGIC_RESISTANCE: // TODO: Is this the right sound? // TODO: This ignores resistances if it's negative play_sound(51); if(the_item.abil_harms()) { ASB(" You feel odd."); str = str * -1; }else ASB(" You feel protected."); if(the_item.abil_group()) univ.party.apply_status(status,str); else univ.party[pc].apply_status(status,str); break; case eStatus::WEBS: if(the_item.abil_harms()) ASB(" You feel sticky."); else { ASB(" Your skin tingles."); str = str * -1; } if(the_item.abil_group()) univ.party.web(str); else univ.party[pc].web(str); break; case eStatus::INVISIBLE: // TODO: Is this the right sound? play_sound(43); if(the_item.abil_harms()) { ASB(" You feel exposed."); str = str * -1; }else ASB(" You feel obscure."); if(the_item.abil_group()) univ.party.apply_status(status,str); else univ.party[pc].apply_status(status,str); break; case eStatus::MARTYRS_SHIELD: // TODO: Is this the right sound? play_sound(43); if(the_item.abil_harms()) { ASB(" You feel dull."); str = str * -1; }else ASB(" You start to glow slightly."); if(the_item.abil_group()) univ.party.apply_status(status,str); else univ.party[pc].apply_status(status,str); break; case eStatus::POISON: switch(type) { case eItemUse::HELP_ONE: ASB(" You feel better."); univ.party[pc].cure(str); break; case eItemUse::HARM_ONE: ASB(" You feel ill."); univ.party[pc].poison(str); break; case eItemUse::HELP_ALL: ASB(" You all feel better."); univ.party.cure(str); break; case eItemUse::HARM_ALL: ASB(" You all feel ill."); univ.party.poison(str); break; } break; case eStatus::DISEASE: switch(type) { case eItemUse::HELP_ONE: ASB(" You feel healthy."); univ.party[pc].apply_status(eStatus::DISEASE,-1 * str); break; case eItemUse::HARM_ONE: ASB(" You feel sick."); univ.party[pc].disease(str); break; case eItemUse::HELP_ALL: ASB(" You all feel healthy."); univ.party.apply_status(eStatus::DISEASE,-1 * str); break; case eItemUse::HARM_ALL: ASB(" You all feel sick."); univ.party.disease(str); break; } break; case eStatus::DUMB: switch(type) { case eItemUse::HELP_ONE: ASB(" You feel clear headed."); univ.party[pc].apply_status(eStatus::DUMB,-1 * str); break; case eItemUse::HARM_ONE: ASB(" You feel confused."); univ.party[pc].dumbfound(str); break; case eItemUse::HELP_ALL: ASB(" You all feel clear headed."); univ.party.apply_status(eStatus::DUMB,-1 * str); break; case eItemUse::HARM_ALL: ASB(" You all feel confused."); univ.party.dumbfound(str); break; } adjust_spell_menus(); break; case eStatus::ASLEEP: switch(type) { case eItemUse::HELP_ONE: ASB(" You feel alert."); univ.party[pc].apply_status(eStatus::ASLEEP,-1 * str); break; case eItemUse::HARM_ONE: ASB(" You feel very tired."); univ.party[pc].sleep(eStatus::ASLEEP,str + 1,200); break; case eItemUse::HELP_ALL: ASB(" You all feel alert."); univ.party.apply_status(eStatus::ASLEEP,-1 * str); break; case eItemUse::HARM_ALL: ASB(" You all feel very tired."); univ.party.sleep(eStatus::ASLEEP,str + 1,200); break; } break; case eStatus::PARALYZED: switch(type) { case eItemUse::HELP_ONE: ASB(" You find it easier to move."); univ.party[pc].apply_status(eStatus::PARALYZED,-1 * str * 100); break; case eItemUse::HARM_ONE: ASB(" You feel very stiff."); univ.party[pc].sleep(eStatus::PARALYZED,str * 20 + 10,200); break; case eItemUse::HELP_ALL: ASB(" You all find it easier to move."); univ.party.apply_status(eStatus::PARALYZED,-1 * str * 100); break; case eItemUse::HARM_ALL: ASB(" You all feel very stiff."); univ.party.sleep(eStatus::PARALYZED,str * 20 + 10,200); break; } break; case eStatus::ACID: switch(type) { case eItemUse::HELP_ONE: ASB(" Your skin tingles pleasantly."); univ.party[pc].apply_status(eStatus::ACID,-1 * str); break; case eItemUse::HARM_ONE: ASB(" Your skin burns!"); univ.party[pc].acid(str); break; case eItemUse::HELP_ALL: ASB(" You all tingle pleasantly."); univ.party.apply_status(eStatus::ACID,-1 * str); break; case eItemUse::HARM_ALL: ASB(" Everyone's skin burns!"); univ.party.acid(str); break; } break; case eStatus::FORCECAGE: switch(type) { case eItemUse::HELP_ONE: process_force_cage(univ.party[pc].get_loc(), pc, str); break; case eItemUse::HARM_ONE: univ.party[pc].sleep(eStatus::FORCECAGE, str, str / 2); break; case eItemUse::HELP_ALL: for(short i = 0; i < 6; i++) process_force_cage(univ.party[i].get_loc(), i, str); break; case eItemUse::HARM_ALL: univ.party.sleep(eStatus::FORCECAGE, str, str / 2); break; } break; } } break; case eItemAbil::BLISS_DOOM: switch(type) { case eItemUse::HELP_ONE: ASB(" You feel wonderful!"); univ.party[pc].heal(str * 20); univ.party[pc].apply_status(eStatus::BLESS_CURSE,str); break; case eItemUse::HARM_ONE: ASB(" You feel terrible."); drain_pc(univ.party[pc],str * 5); damage_pc(univ.party[pc],20 * str,eDamageType::UNBLOCKABLE,eRace::HUMAN); univ.party[pc].disease(2 * str); univ.party[pc].dumbfound(2 * str); break; case eItemUse::HELP_ALL: ASB(" Everyone feels wonderful!"); univ.party.heal(str*20); univ.party.apply_status(eStatus::BLESS_CURSE,str); break; case eItemUse::HARM_ALL: ASB(" You all feel terrible."); for(short i = 0; i < 6; i++) { drain_pc(univ.party[i],str * 5); damage_pc(univ.party[i],20 * str,eDamageType::UNBLOCKABLE,eRace::HUMAN); univ.party[i].disease(2 * str); univ.party[i].dumbfound(2 * str); } break; } if(the_item.abil_harms()) adjust_spell_menus(); break; case eItemAbil::AFFECT_EXPERIENCE: switch(type) { case eItemUse::HELP_ONE: ASB(" You feel much smarter."); award_xp(pc,str * 5); break; case eItemUse::HARM_ONE: ASB(" You feel forgetful."); drain_pc(univ.party[pc],str * 5); break; case eItemUse::HELP_ALL: ASB(" You all feel much smarter."); award_party_xp(str * 5); break; case eItemUse::HARM_ALL: ASB(" You all feel forgetful."); for(short i = 0; i < 6; i++) drain_pc(univ.party[i],str * 5); break; } break; case eItemAbil::AFFECT_SKILL_POINTS: // TODO: Is this the right sound? play_sound(68); switch(type) { case eItemUse::HELP_ONE: ASB(" You feel much smarter."); univ.party[pc].skill_pts += str; break; case eItemUse::HARM_ONE: ASB(" You feel forgetful."); univ.party[pc].skill_pts = max(0,univ.party[pc].skill_pts - str); break; case eItemUse::HELP_ALL: ASB(" You all feel much smarter."); for(short i = 0; i < 6; i++) univ.party[i].skill_pts += str; break; case eItemUse::HARM_ALL: ASB(" You all feel forgetful."); for(short i = 0; i < 6; i++) univ.party[i].skill_pts = max(0,univ.party[i].skill_pts - str); break; } break; case eItemAbil::AFFECT_HEALTH: switch(type) { case eItemUse::HELP_ONE: ASB(" You feel better."); univ.party[pc].heal(str * 20); break; case eItemUse::HARM_ONE: ASB(" You feel sick."); damage_pc(univ.party[pc],20 * str,eDamageType::UNBLOCKABLE,eRace::HUMAN); break; case eItemUse::HELP_ALL: ASB(" You all feel better."); univ.party.heal(str * 20); break; case eItemUse::HARM_ALL: ASB(" You all feel sick."); hit_party(20 * str,eDamageType::UNBLOCKABLE); break; } break; case eItemAbil::AFFECT_SPELL_POINTS: switch(type) { case eItemUse::HELP_ONE: ASB(" You feel energized."); univ.party[pc].restore_sp(str * 5); break; case eItemUse::HARM_ONE: ASB(" You feel drained."); univ.party[pc].cur_sp = max(0,univ.party[pc].cur_sp - str * 5); break; case eItemUse::HELP_ALL: ASB(" You all feel energized."); univ.party.restore_sp(str * 5); break; case eItemUse::HARM_ALL: ASB(" You all feel drained."); for(short i = 0; i < 6; i++) univ.party[i].cur_sp = max(0,univ.party[i].cur_sp - str * 5); break; } break; case eItemAbil::LIGHT: if(!the_item.abil_harms()) { ASB(" You have more light."); increase_light(50 * str); } else { ASB(" It gets darker."); increase_light(-50 * str); } break; case eItemAbil::AFFECT_PARTY_STATUS: if(the_item.abil_harms()) { ePartyStatus status = the_item.abil_data.party; int i = univ.party.status[status]; switch(status) { case ePartyStatus::STEALTH: ASB(" Your footsteps become louder."); str *= 5; break; case ePartyStatus::FIREWALK: ASB(" The chill recedes from your feet."); str *= 2; break; case ePartyStatus::DETECT_LIFE: ASB(" Your vision of life becomes blurry."); break; case ePartyStatus::FLIGHT: if(i <= str) { if(univ.scenario.ter_types[univ.out[univ.party.out_loc.x][univ.party.out_loc.y]].blocksMove()) { add_string_to_buf(" You plummet to your deaths."); slay_party(eMainStatus::DEAD); print_buf(); pause(150); } else if(i > 1) { add_string_to_buf(" You plummet to the ground."); hit_party(get_ran(i,1,12), eDamageType::SPECIAL); } else add_string_to_buf(" You land safely."); } else add_string_to_buf(" You start to descend."); break; } if(str > i) str = i; str *= -1; } else switch(the_item.abil_data.party) { case ePartyStatus::STEALTH: ASB(" Your footsteps become quieter."); str *= 5; break; case ePartyStatus::FIREWALK: ASB(" You feel chilly."); str *= 2; break; case ePartyStatus::DETECT_LIFE: ASB(" You detect life."); break; case ePartyStatus::FLIGHT: if(univ.party.status[ePartyStatus::FLIGHT] > 0) { add_string_to_buf(" Not while already flying."); take_charge = false; } else if(univ.party.in_boat >= 0) { add_string_to_buf(" Leave boat first."); take_charge = false; } else if(univ.party.in_horse >= 0) { add_string_to_buf(" Leave horse first."); take_charge = false; } else ASB(" You rise into the air!"); break; } if(take_charge) univ.party.status[the_item.abil_data.party] += str; break; case eItemAbil::HEALTH_POISON: switch(type) { case eItemUse::HELP_ONE: ASB(" You feel wonderful."); univ.party[pc].heal(str*25); univ.party[pc].cure(str); break; case eItemUse::HARM_ONE: ASB(" You feel terrible."); damage_pc(univ.party[pc], str*25, eDamageType::UNBLOCKABLE, eRace::UNKNOWN); univ.party[pc].poison(str); break; case eItemUse::HELP_ALL: ASB(" You all feel wonderful."); univ.party.heal(str*25); univ.party.cure(str); break; case eItemUse::HARM_ALL: ASB(" You all feel terrible."); hit_party(str*25, eDamageType::UNBLOCKABLE); univ.party.poison(str); break; } break; case eItemAbil::CALL_SPECIAL: // TODO: Should this have its own separate eSpecCtx? run_special(eSpecCtx::USE_SPEC_ITEM, eSpecCtxType::SCEN, str, user_loc, &sp[0]); if(sp[0] > 0) take_charge = false; break; case eItemAbil::CAST_SPELL: { if(univ.town.is_antimagic(user_loc.x, user_loc.y)) { add_string_to_buf(" Not in antimagic field."); take_charge = false; break; } auto spell = the_item.abil_data.spell; switch(spell) { case eSpell::FLAME: add_string_to_buf(" It fires a bolt of flame."); break; case eSpell::FIREBALL: add_string_to_buf(" It shoots a fireball."); break; case eSpell::FIRESTORM: add_string_to_buf(" It shoots a huge fireball. "); break; case eSpell::KILL: add_string_to_buf(" It shoots a black ray."); break; case eSpell::ICE_BOLT: add_string_to_buf(" It fires a ball of ice."); break; case eSpell::SLOW: add_string_to_buf(" It fires a purple ray."); break; case eSpell::DISPEL_UNDEAD: add_string_to_buf(" It shoots a white ray."); break; case eSpell::RAVAGE_SPIRIT: add_string_to_buf(" It shoots a golden ray."); break; case eSpell::ACID_SPRAY: add_string_to_buf(" Acid sprays from the tip!"); break; case eSpell::FOUL_VAPOR: add_string_to_buf(" It creates a cloud of gas."); break; case eSpell::CLOUD_SLEEP: add_string_to_buf(" It creates a shimmering cloud."); break; case eSpell::POISON: add_string_to_buf(" A green ray emerges."); break; case eSpell::SHOCKSTORM: add_string_to_buf(" Sparks fly."); break; case eSpell::PARALYZE_BEAM: add_string_to_buf(" It shoots a silvery beam."); break; case eSpell::GOO_BOMB: add_string_to_buf(" It explodes!"); break; case eSpell::STRENGTHEN_TARGET: add_string_to_buf(" It shoots a fiery red ray."); break; case eSpell::CHARM_MASS: ASB("It throbs, and emits odd rays."); break; case eSpell::DISPEL_BARRIER: add_string_to_buf(" It fires a blinding ray."); break; case eSpell::WALL_ICE_BALL: add_string_to_buf(" It shoots a blue sphere."); break; case eSpell::CHARM_FOE: add_string_to_buf(" It fires a lovely, sparkling beam."); break; case eSpell::ANTIMAGIC: add_string_to_buf(" Your hair stands on end."); break; default: add_string_to_buf(" It casts a spell: " + (*spell).name()); break; } switch((*spell).need_select) { case SELECT_NO: break; case SELECT_ACTIVE: store_spell_target = select_pc(eSelectPC::ONLY_LIVING); break; case SELECT_ANY: store_spell_target = select_pc(eSelectPC::ANY); break; } if(overall_mode == MODE_COMBAT) { bool priest = (*spell).is_priest(); switch((*spell).refer) { case REFER_YES: if(priest) do_priest_spell(univ.cur_pc, spell, true); else do_mage_spell(univ.cur_pc, spell, true); break; case REFER_TARGET: start_spell_targeting(spell, true); break; case REFER_FANCY: start_fancy_spell_targeting(spell, true); break; case REFER_IMMED: if(priest) combat_immed_priest_cast(univ.cur_pc, spell, true); else combat_immed_mage_cast(univ.cur_pc, spell, true); break; } } else if((*spell).is_priest()) do_priest_spell(univ.cur_pc, spell, true); else do_mage_spell(univ.cur_pc, spell, true); } break; case eItemAbil::SUMMONING: if(!summon_monster(the_item.abil_data.value,user_loc,str,eAttitude::FRIENDLY,true)) add_string_to_buf(" Summon failed."); break; case eItemAbil::MASS_SUMMONING: r1 = get_ran(str,1,4); (void) r1; // TODO: This value was never used, so why is it here? r1 = get_ran(1,3,5); for(short i = 0; i < r1; i++) if(!summon_monster(the_item.abil_data.value,user_loc,r1,eAttitude::FRIENDLY,true)) add_string_to_buf(" Summon failed."); break; case eItemAbil::QUICKFIRE: add_string_to_buf("Fire pours out!"); univ.town.set_quickfire(user_loc.x,user_loc.y,true); break; case eItemAbil::MESSAGE: take_charge = false; r1 = the_item.desc.find("|||"); str1 = the_item.desc.substr(r1 + 3); r1 = str1.find("|||"); if(r1 != std::string::npos) { str2 = str1.substr(r1 + 3); str1 = str1.substr(0, r1); } r1 = the_item.graphic_num; cStrDlog(str1, str2, "Reading " + the_item.name, r1, PIC_ITEM).show(); break; // Now for all the non-usable abilities. These are enumerated here so that the compiler can catch if we've missed one. case eItemAbil::ACCURACY: case eItemAbil::ANTIMAGIC_WEAPON: case eItemAbil::ASPTONGUE: case eItemAbil::BOOST_MAGIC: case eItemAbil::BOOST_STAT: case eItemAbil::BOOST_WAR: case eItemAbil::CAUSES_FEAR: case eItemAbil::COMFREY: case eItemAbil::DAMAGE_PROTECTION: case eItemAbil::DAMAGING_WEAPON: case eItemAbil::DISTANCE_MISSILE: case eItemAbil::DRAIN_MISSILES: case eItemAbil::EMBERF: case eItemAbil::ENCUMBERING: case eItemAbil::EVASION: case eItemAbil::EXPLODING_WEAPON: case eItemAbil::FREE_ACTION: case eItemAbil::FULL_PROTECTION: case eItemAbil::WILL: case eItemAbil::GIANT_STRENGTH: case eItemAbil::GRAYMOLD: case eItemAbil::HEALING_WEAPON: case eItemAbil::HEAVIER_OBJECT: case eItemAbil::HIT_CALL_SPECIAL: case eItemAbil::HOLLY: case eItemAbil::LIFE_SAVING: case eItemAbil::LIGHTER_OBJECT: case eItemAbil::LOCKPICKS: case eItemAbil::MANDRAKE: case eItemAbil::MARTYRS_SHIELD: case eItemAbil::MAGERY: case eItemAbil::NETTLE: case eItemAbil::NONE: case eItemAbil::OCCASIONAL_STATUS: case eItemAbil::POISON_AUGMENT: case eItemAbil::PROTECT_FROM_PETRIFY: case eItemAbil::PROTECT_FROM_SPECIES: case eItemAbil::RADIANT: case eItemAbil::REGENERATE: case eItemAbil::RESURRECTION_BALM: case eItemAbil::RETURNING_MISSILE: case eItemAbil::SAPPHIRE: case eItemAbil::SEEKING_MISSILE: case eItemAbil::SKILL: case eItemAbil::SLAYER_WEAPON: case eItemAbil::SLOW_WEARER: case eItemAbil::SMOKY_CRYSTAL: case eItemAbil::SOULSUCKER: case eItemAbil::SPEED: case eItemAbil::STATUS_PROTECTION: case eItemAbil::STATUS_WEAPON: case eItemAbil::THIEVING: case eItemAbil::WEAK_WEAPON: case eItemAbil::WEAPON_CALL_SPECIAL: case eItemAbil::WORMGRASS: case eItemAbil::UNUSED: case eItemAbil::DROP_CALL_SPECIAL: case eItemAbil::HP_DAMAGE: case eItemAbil::HP_DAMAGE_REVERSE: case eItemAbil::SP_DAMAGE: case eItemAbil::SP_DAMAGE_REVERSE: break; } } put_pc_screen(); if((take_charge) && (univ.party[pc].items[item].charges > 0)) univ.party[pc].remove_charge(item); if(stat_window == pc) put_item_screen(stat_window); if(!take_charge) { draw_terrain(0); put_item_screen(stat_window); } } // Returns true if an action is actually carried out. This can only be reached in town. bool use_space(location where) { ter_num_t ter; location from_loc,to_loc; ter = univ.town->terrain(where.x,where.y); from_loc = univ.party.town_loc; add_string_to_buf("Use..."); if(univ.town.is_web(where.x,where.y)) { add_string_to_buf(" You clear the webs."); univ.town.set_web(where.x,where.y,false); return true; } if(univ.town.is_crate(where.x,where.y)) { to_loc = push_loc(from_loc,where); if(from_loc == to_loc) { add_string_to_buf(" Can't push crate."); return false; } add_string_to_buf(" You push the crate."); univ.town.set_crate(where.x,where.y,false); univ.town.set_crate(to_loc.x,to_loc.y,true); for(short i = 0; i < univ.town.items.size(); i++) if(univ.town.items[i].variety != eItemType::NO_ITEM && univ.town.items[i].item_loc == where && univ.town.items[i].contained && univ.town.items[i].held) univ.town.items[i].item_loc = to_loc; } if(univ.town.is_barrel(where.x,where.y)) { to_loc = push_loc(from_loc,where); if(from_loc == to_loc) { add_string_to_buf(" Can't push barrel."); return false; } add_string_to_buf(" You push the barrel."); univ.town.set_barrel(where.x, where.y,false); univ.town.set_barrel(to_loc.x,to_loc.y,true); for(short i = 0; i < univ.town.items.size(); i++) if(univ.town.items[i].variety != eItemType::NO_ITEM && univ.town.items[i].item_loc == where && univ.town.items[i].contained && univ.town.items[i].held) univ.town.items[i].item_loc = to_loc; } if(univ.town.is_block(where.x,where.y)) { to_loc = push_loc(from_loc,where); if(from_loc == to_loc) { add_string_to_buf(" Can't push block."); return false; } add_string_to_buf(" You push the block."); univ.town.set_block(where.x,where.y,false); univ.town.set_block(to_loc.x,to_loc.y,true); } if(univ.scenario.ter_types[ter].special == eTerSpec::CHANGE_WHEN_USED) { if(where == from_loc) { add_string_to_buf(" Not while on space."); return false; } add_string_to_buf(" OK."); alter_space(where.x,where.y,univ.scenario.ter_types[ter].flag1); if(univ.scenario.ter_types[ter].flag2 >= 0) play_sound(univ.scenario.ter_types[ter].flag2); return true; } else if(univ.scenario.ter_types[ter].special == eTerSpec::CALL_SPECIAL_WHEN_USED) { eSpecCtxType spec_type = eSpecCtxType::SCEN; if(univ.scenario.ter_types[ter].flag2 == 1){ if(is_town() || (is_combat() && which_combat_type == 1)) spec_type = eSpecCtxType::TOWN; else spec_type = eSpecCtxType::OUTDOOR; } run_special(eSpecCtx::USE_SPACE, spec_type, univ.scenario.ter_types[ter].flag1, where); return true; } add_string_to_buf(" Nothing to use."); return false; } // Note ... if this is a container, the code must first process any specials. If // specials return false, can't get items inside. If true, can get items inside. // Can't get items out in combat. // TODO this always returns false. Why? bool adj_town_look(location where) { ter_num_t terrain; bool can_open = true,item_there = false,got_special = false; short s1 = 0; for(short i = 0; i < univ.town.items.size(); i++) if(univ.town.items[i].variety != eItemType::NO_ITEM && (univ.town.items[i].contained) && (where == univ.town.items[i].item_loc)) item_there = true; terrain = univ.town->terrain(where.x,where.y); if(univ.town.is_special(where.x,where.y)) {// && (get_blockage(terrain) > 0)) { if(!adjacent(univ.party.town_loc,where)) add_string_to_buf(" Not close enough to search."); else { for(short i = 0; i < univ.town->special_locs.size(); i++) if(where == univ.town->special_locs[i]) { if(get_blockage(univ.town->terrain(where.x,where.y)) > 0) { // tell party you find something, if looking at a space they can't step in add_string_to_buf(" Search: You find something!"); } run_special(eSpecCtx::TOWN_LOOK, eSpecCtxType::TOWN, univ.town->special_locs[i].spec, where, &s1); if(s1 > 0) can_open = false; got_special = true; } put_item_screen(stat_window); } } if(is_container(where) && item_there && can_open) { get_item(where,6,true); }else if(univ.scenario.ter_types[terrain].special == eTerSpec::CHANGE_WHEN_USED || univ.scenario.ter_types[terrain].special == eTerSpec::CALL_SPECIAL_WHEN_USED) { add_string_to_buf(" (Use this space to do something with it.)", 2); }else{ if(!got_special) add_string_to_buf(" Search: You don't find anything."); return false; } return false; } void out_move_party(short x,short y) { location l; l.x = x;l.y = y; l = local_to_global(l); univ.party.out_loc = l; center = l; update_explored(l); } // mode - 0=full teleport flash 1=no teleport flash 2=only fade flash 3=only arrival flash void teleport_party(short x,short y,short mode) { // TODO: Teleport sound? (Sound 10) location l; bool fadeIn = false, fadeOut = false; if(is_combat()) mode = 1; if(mode == 0 || mode == 2) fadeOut = true; if(mode == 0 || mode == 3) fadeIn = true; // Clear forcecage status for(int i = 0; i < 6; i++) univ.party[i].status[eStatus::FORCECAGE] = 0; l = univ.party.town_loc; update_explored(l); if(fadeOut) { start_missile_anim(); for(short i = 0; i < 9; i++) add_explosion(l,-1,1,1,0,0); do_explosion_anim(5,1); can_draw_pcs = false; do_explosion_anim(5,2); end_missile_anim(); } center.x = x; center.y = y; if(is_combat()) { univ.current_pc().combat_pos.x = x; univ.current_pc().combat_pos.y = y; } l.x = x; l.y = y; univ.party.town_loc.x = x; univ.party.town_loc.y = y; update_explored(l); draw_terrain(0); if(fadeIn) { start_missile_anim(); for(short i = 0; i < 14; i++) add_explosion(center,-1,1,1,0,0); do_explosion_anim(5,1); } can_draw_pcs = true; if(fadeIn) { do_explosion_anim(5,2); end_missile_anim(); } draw_map(true); } void fade_party() { location l; l = univ.party.town_loc; start_missile_anim(); for(short i = 0; i < 14; i++) add_explosion(l,-1,1,1,0,0); do_explosion_anim(5,1); univ.party.town_loc.x = 100; univ.party.town_loc.y = 100; do_explosion_anim(5,2); end_missile_anim(); } void change_level(short town_num,short x,short y) { location l(x,y); if((town_num < 0) || (town_num >= univ.scenario.towns.size())) { showError("The scenario special encounter tried to put you into a town that doesn't exist."); return; } // Clear forcecage status for(int i = 0; i < 6; i++) univ.party[i].status[eStatus::FORCECAGE] = 0; force_town_enter(town_num,l); end_town_mode(1,l); start_town_mode(town_num,9); } short get_sound_type(eDamageType dam_type, short forced_sound_type) { short sound_type = forced_sound_type; if(sound_type == -1){ sound_type = 0; if(dam_type == eDamageType::FIRE || dam_type == eDamageType::UNBLOCKABLE) sound_type = 5; else if(dam_type == eDamageType::COLD) sound_type = 7; else if(dam_type == eDamageType::MAGIC) sound_type = 12; else if(dam_type == eDamageType::POISON) sound_type = 11; } return sound_type; } short get_boom_type(eDamageType dam_type){ short boom_type = 2; if(dam_type == eDamageType::FIRE) boom_type = 0; else if(dam_type == eDamageType::UNBLOCKABLE) boom_type = 4; else if(dam_type == eDamageType::COLD) boom_type = 5; return boom_type; } // Damaging and killing monsters needs to be here because several have specials attached to them. short damage_monst(cCreature& victim, short who_hit, short how_much, eDamageType dam_type, short sound_type, bool do_print) { short r1,which_spot; location where_put; //print_num(which_m,(short)univ.town.monst[which_m].m_loc.x,(short)univ.town.monst[which_m].m_loc.y); if(victim.active == eCreatureStatus::DEAD) return false; // Note: sound type 0 can now be forced for UNBLOCKABLE by passing sound_type 0, // but -1 is the new value for "use default" sound_type = get_sound_type(dam_type, sound_type); if(dam_type < eDamageType::SPECIAL) { how_much = percent(how_much, victim.resist[dam_type]); } // Absorb damage? if((dam_type == eDamageType::FIRE || dam_type == eDamageType::MAGIC || dam_type == eDamageType::COLD) && victim.abil[eMonstAbil::ABSORB_SPELLS].active && get_ran(1,1,1000) <= victim.abil[eMonstAbil::ABSORB_SPELLS].special.extra1) { if(32767 - victim.health > how_much) victim.health = 32767; else victim.health += how_much; ASB(" Magic absorbed."); return false; } // Saving throw if((dam_type == eDamageType::FIRE || dam_type == eDamageType::COLD) && get_ran(1,0,20) <= victim.level) how_much /= 2; if(dam_type == eDamageType::MAGIC && (get_ran(1,0,24) <= victim.level)) how_much /= 2; // Invulnerable? if(dam_type != eDamageType::SPECIAL && victim.invuln) how_much = how_much / 10; if(dam_type != eDamageType::SPECIAL && victim.status[eStatus::INVULNERABLE] > 0) how_much /= 10; // Mag. res helps w. fire and cold // TODO: Why doesn't this help with magic damage!? if(dam_type == eDamageType::FIRE || dam_type == eDamageType::COLD) { int magic_res = victim.status[eStatus::MAGIC_RESISTANCE]; if(magic_res > 0) how_much /= 2; else if(magic_res < 0) how_much *= 2; } // TODO: So, player armour blocks demon/undead damage, but monster armour doesn't? if(dam_type == eDamageType::WEAPON) { r1 = get_ran(1,0,(victim.armor * 5) / 4); r1 += victim.level / 4; how_much -= r1; } if(boom_anim_active) { if(how_much < 0) how_much = 0; victim.marked_damage += how_much; add_explosion(victim.cur_loc,how_much,0,get_boom_type(dam_type),14 * (victim.x_width - 1),18 * (victim.y_width - 1)); // Note: Windows version printed an "undamaged" message here if applicable, but I don't think that's right. if(how_much == 0) return false; else return true; } if(how_much <= 0) { if(is_combat()) victim.spell_note(7); if(how_much <= 0 && (dam_type == eDamageType::WEAPON || dam_type == eDamageType::UNDEAD || dam_type == eDamageType::DEMON)) { draw_terrain(2); play_sound(2); } // sprintf ((char *) create_line, " No damage. "); // add_string_to_buf((char *) create_line); return false; } if(do_print) victim.damaged_msg(how_much,0); victim.health = victim.health - how_much; if(univ.debug_mode) victim.health = -1; // splitting monsters if(victim.abil[eMonstAbil::SPLITS].active && victim.health > 0 && get_ran(1,1,1000) < victim.abil[eMonstAbil::SPLITS].special.extra1){ where_put = find_clear_spot(victim.cur_loc,1); if(where_put.x > 0) if((which_spot = place_monster(victim.number,where_put)) < univ.town.monst.size()) { static_cast(univ.town.monst[which_spot]) = victim; univ.town.monst[which_spot].health = victim.health; victim.spell_note(27); } } if(who_hit < 7) univ.party.total_dam_done += how_much; // Monster damages. Make it hostile. victim.active = eCreatureStatus::ALERTED; if(dam_type != eDamageType::MARKED) { if(party_can_see_monst(univ.get_target_i(victim) - 100)) { boom_space(victim.cur_loc,100,boom_gr[dam_type],how_much,sound_type); } else { boom_space(victim.cur_loc,overall_mode, boom_gr[dam_type],how_much,sound_type); } } if(victim.health < 0) { victim.killed_msg(); kill_monst(victim,who_hit); } else { if(how_much > 0) victim.morale = victim.morale - 1; if(how_much > 5) victim.morale = victim.morale - 1; if(how_much > 10) victim.morale = victim.morale - 1; if(how_much > 20) victim.morale = victim.morale - 2; } if(victim.is_friendly() && who_hit < 7 && ((!processing_fields && !monsters_going) || (processing_fields && !univ.party.hostiles_present))) { add_string_to_buf("Damaged an innocent."); victim.attitude = eAttitude::HOSTILE_A; make_town_hostile(); } return how_much; } void petrify_monst(cCreature& which_m,int strength) { which_m.spell_note(9); short r1 = get_ran(1,0,20); r1 += which_m.level / 4; r1 += which_m.status[eStatus::BLESS_CURSE]; r1 -= strength; // TODO: This should probably do something similar to charm_monst with the magic resistance if(r1 > 14 || which_m.resist[eDamageType::MAGIC] == 0) which_m.spell_note(10); else { which_m.spell_note(8); kill_monst(which_m,7,eMainStatus::STONE); } } void kill_monst(cCreature& which_m,short who_killed,eMainStatus type) { short xp,i,j; location l; if(isHumanoid(which_m.m_type)) { if(which_m.m_type == eRace::GOBLIN) i = 4; else i = get_ran(1,0,1); play_sound(29 + i); } else switch(which_m.m_type) { case eRace::GIANT: play_sound(29); break; // TODO: Should birds be considered beasts? If there are any birds in the bladbase, probably; otherwise, better to have new sound case eRace::REPTILE: case eRace::BEAST: case eRace::DEMON: case eRace::UNDEAD: case eRace::SKELETAL: case eRace::STONE: i = get_ran(1,0,1); play_sound(31 + i); break; // TODO: I feel like dragons should have a different sound. default: play_sound(33); break; } // Special killing effects if(univ.party.sd_legit(which_m.spec1,which_m.spec2)) PSD[which_m.spec1][which_m.spec2] = 1; if(which_m.special_on_kill >= 0) run_special(eSpecCtx::KILL_MONST, eSpecCtxType::TOWN, which_m.special_on_kill, which_m.cur_loc); if(which_m.abil[eMonstAbil::DEATH_TRIGGER].active) run_special(eSpecCtx::KILL_MONST,eSpecCtxType::SCEN, which_m.abil[eMonstAbil::DEATH_TRIGGER].special.extra1, which_m.cur_loc); if(!univ.debug_mode && (which_m.summon_time == 0 || !which_m.party_summoned)) { // no xp for party-summoned monsters xp = which_m.level * 2; if(who_killed < 6) award_xp(who_killed,xp); else if(who_killed == 6) award_party_xp(xp / 6 + 1); if(who_killed < 7) { univ.party.total_m_killed++; i = max((xp / 6),1); award_party_xp(i); } l = which_m.cur_loc; place_glands(l,which_m.number); } if(!univ.debug_mode && which_m.summon_time == 0) place_treasure(which_m.cur_loc, which_m.level / 2, which_m.treasure, 0); i = which_m.cur_loc.x; j = which_m.cur_loc.y; if(type == eMainStatus::DUST) univ.town.set_ash(i,j,true); else if(type == eMainStatus::ABSENT || type == eMainStatus::STONE); else switch(which_m.m_type) { case eRace::DEMON: univ.town.set_ash(i,j,true); break; case eRace::UNDEAD: break; case eRace::SKELETAL: univ.town.set_bones(i,j,true); break; case eRace::SLIME: case eRace::PLANT: case eRace::BUG: univ.town.set_sm_slime(i,j,true); break; case eRace::STONE: univ.town.set_rubble(i,j,true); break; default: univ.town.set_sm_blood(i,j,true); break; } if((is_town() || which_combat_type == 1) && which_m.summon_time == 0) { univ.town->m_killed++; } which_m.spec1 = 0; // make sure, if this is a spec. activated monster, it won't come back which_m.active = eCreatureStatus::DEAD; } // Pushes party and monsters around by moving walls and conveyor belts. // This is very fragile, and only hands a few cases. void push_things() { bool redraw = false; ter_num_t ter; location l; if(is_out()) // TODO: Make these work outdoors return; if(!univ.town.belt_present) return; for(short i = 0; i < univ.town.monst.size(); i++) if(univ.town.monst[i].is_alive()) { l = univ.town.monst[i].cur_loc; ter = univ.town->terrain(l.x,l.y); if (univ.scenario.ter_types[ter].special==eTerSpec::CONVEYOR) { switch(univ.scenario.ter_types[ter].flag1) { // TODO: Implement the other 4 possible directions case DIR_N: l.y--; break; case DIR_E: l.x++; break; case DIR_S: l.y++; break; case DIR_W: l.x--; break; } } if(l != univ.town.monst[i].cur_loc) { univ.town.monst[i].cur_loc = l; if((point_onscreen(center,univ.town.monst[i].cur_loc)) || (point_onscreen(center,l))) redraw = true; } } for(short i = 0; i < univ.town.items.size(); i++) if(univ.town.items[i].variety != eItemType::NO_ITEM) { l = univ.town.items[i].item_loc; ter = univ.town->terrain(l.x,l.y); if (univ.scenario.ter_types[ter].special==eTerSpec::CONVEYOR) { switch(univ.scenario.ter_types[ter].flag1) { // TODO: Implement the other 4 possible directions case DIR_N: l.y--; break; case DIR_E: l.x++; break; case DIR_S: l.y++; break; case DIR_W: l.x--; break; } } if(l != univ.town.items[i].item_loc) { univ.town.items[i].item_loc = l; if((point_onscreen(center,univ.town.items[i].item_loc)) || (point_onscreen(center,l))) redraw = true; } } if(is_town()) { ter = univ.town->terrain(univ.party.town_loc.x,univ.party.town_loc.y); l = univ.party.town_loc; if (univ.scenario.ter_types[ter].special==eTerSpec::CONVEYOR) { switch(univ.scenario.ter_types[ter].flag1) { // TODO: Implement the other 4 possible directions case DIR_N: l.y--; break; case DIR_E: l.x++; break; case DIR_S: l.y++; break; case DIR_W: l.x--; break; } } if(l != univ.party.town_loc) { // TODO: Will this push you into a placed forcecage or barrier? Should it? ASB("You get pushed."); if(univ.scenario.ter_types[ter].special == eTerSpec::CONVEYOR) draw_terrain(0); center = l; univ.party.town_loc = l; update_explored(l); ter = univ.town->terrain(univ.party.town_loc.x,univ.party.town_loc.y); (void) ter; // Though it's never read currently, it at least keeps things consistent draw_map(true); if(univ.town.is_barrel(univ.party.town_loc.x,univ.party.town_loc.y)) { univ.town.set_barrel(univ.party.town_loc.x,univ.party.town_loc.y,false); ASB("You smash the barrel."); } if(univ.town.is_crate(univ.party.town_loc.x,univ.party.town_loc.y)) { univ.town.set_crate(univ.party.town_loc.x,univ.party.town_loc.y,false); ASB("You smash the crate."); } if(univ.town.is_block(univ.party.town_loc.x,univ.party.town_loc.y)) { ASB("You crash into the block."); hit_party(get_ran(1, 1, 6), eDamageType::WEAPON); } for(short k = 0; k < univ.town.items.size(); k++) if(univ.town.items[k].variety != eItemType::NO_ITEM && univ.town.items[k].held && (univ.town.items[k].item_loc == univ.party.town_loc)) univ.town.items[k].contained = univ.town.items[k].held = false; redraw = true; } } if(is_combat()) { for(short i = 0; i < 6; i++) if(univ.party[i].main_status == eMainStatus::ALIVE) { ter = univ.town->terrain(univ.party[i].combat_pos.x,univ.party[i].combat_pos.y); l = univ.party[i].combat_pos; if (univ.scenario.ter_types[ter].special==eTerSpec::CONVEYOR) { switch(univ.scenario.ter_types[ter].flag1) { // TODO: Implement the other 4 possible directions case DIR_N: l.y--; break; case DIR_E: l.x++; break; case DIR_S: l.y++; break; case DIR_W: l.x--; break; } } if(l != univ.party[i].combat_pos) { ASB("Someone gets pushed."); ter = univ.town->terrain(l.x,l.y); if(univ.scenario.ter_types[ter].special == eTerSpec::CONVEYOR) draw_terrain(0); univ.party[i].combat_pos = l; update_explored(l); draw_map(true); if(univ.town.is_barrel(univ.party[i].combat_pos.x,univ.party[i].combat_pos.y)) { univ.town.set_barrel(univ.party[i].combat_pos.x,univ.party[i].combat_pos.y,false); ASB("You smash the barrel."); } if(univ.town.is_crate(univ.party[i].combat_pos.x,univ.party[i].combat_pos.y)) { univ.town.set_crate(univ.party[i].combat_pos.x,univ.party[i].combat_pos.y,false); ASB("You smash the crate."); } if(univ.town.is_block(univ.party[i].combat_pos.x,univ.party[i].combat_pos.y)) { ASB("You crash into the block."); damage_pc(univ.party[i],get_ran(1, 1, 6), eDamageType::WEAPON,eRace::UNKNOWN); } for(short k = 0; k < univ.town.items.size(); k++) if(univ.town.items[k].variety != eItemType::NO_ITEM && univ.town.items[k].held && (univ.town.items[k].item_loc == univ.party[i].combat_pos)) univ.town.items[k].contained = univ.town.items[k].held = false; redraw = true; } } } if(redraw) { print_buf(); draw_terrain(0); } } void special_increase_age(long length, bool queue) { bool redraw = false,stat_area = false; location trigger_loc; unsigned long age_before = univ.party.age - length; unsigned long current_age = univ.party.age; bool failed_job = false; if(is_combat()) { extern short combat_active_pc; trigger_loc = univ.party[combat_active_pc].combat_pos; } else if(is_town()) { trigger_loc = univ.party.town_loc; } else if(is_out()) { trigger_loc = univ.party.out_loc; } for(auto& p : univ.party.active_quests) { if(p.second.status != eQuestStatus::STARTED) continue; cQuest& quest = univ.scenario.quests[p.first]; if(quest.deadline <= 0) continue; bool is_relative = quest.deadline_is_relative; int deadline = quest.deadline + is_relative * p.second.start; if(day_reached(deadline + 1, quest.event)) { p.second.status = eQuestStatus::FAILED; if(p.second.source >= 0) { int bank = p.second.source; // Safety valve in case it was given by a special node if(bank >= univ.party.job_banks.size()) univ.party.job_banks.resize(bank + 1); int add_anger = 1; if(quest.deadline_is_relative) { if(quest.deadline < 20) add_anger++; if(quest.deadline < 10) add_anger++; if(quest.deadline < 5) add_anger++; } else if(quest.deadline - p.second.start > 20) add_anger++; univ.party.job_banks[bank].anger += add_anger; } failed_job = true; } } if(failed_job) { add_string_to_buf("The deadline for one of your quests has passed.",2); print_buf(); if(stat_window == ITEM_WIN_QUESTS) set_stat_window(stat_window); } // Angered job boards slowly forgive you if(univ.party.age % 30 == 0) for(short i = 0; i < univ.party.job_banks.size(); i++) move_to_zero(univ.party.job_banks[i].anger); if(is_town() || (is_combat() && which_combat_type == 1)) { for(short i = 0; i < univ.town->timers.size(); i++) if(univ.town->timers[i].time > 0) { short time = univ.town->timers[i].time; bool need_redraw = false; for(unsigned long j = age_before + 1; j <= current_age; j++) if(j % time == 0) { if(queue) { univ.party.age = j; queue_special(eSpecCtx::TOWN_TIMER, eSpecCtxType::TOWN, univ.town->timers[i].node, trigger_loc); } else run_special(eSpecCtx::TOWN_TIMER, eSpecCtxType::TOWN, univ.town->timers[i].node, trigger_loc, nullptr, nullptr, &need_redraw); univ.town->timers[i].time = 0; } stat_area = true; if(need_redraw) redraw = true; } } univ.party.age = current_age; for(short i = 0; i < univ.scenario.scenario_timers.size(); i++) if(univ.scenario.scenario_timers[i].time > 0) { short time = univ.scenario.scenario_timers[i].time; bool need_redraw = false; for(unsigned long j = age_before + 1; j <= current_age; j++) if(j % time == 0) { if(queue) { univ.party.age = j; queue_special(eSpecCtx::SCEN_TIMER, eSpecCtxType::SCEN, univ.scenario.scenario_timers[i].node, trigger_loc); } else run_special(eSpecCtx::SCEN_TIMER, eSpecCtxType::SCEN, univ.scenario.scenario_timers[i].node, trigger_loc, nullptr, nullptr,&need_redraw); univ.scenario.scenario_timers[i].time = 0; } stat_area = true; if(need_redraw) redraw = true; } univ.party.age = current_age; auto party_timers = univ.party.party_event_timers; for(short i = 0; i < party_timers.size(); i++) { if(party_timers[i].time <= length) { univ.party.age = age_before + party_timers[i].time; auto which_type = party_timers[i].node_type; bool need_redraw = false; if(queue) queue_special(eSpecCtx::PARTY_TIMER, which_type, party_timers[i].node, trigger_loc); else run_special(eSpecCtx::PARTY_TIMER, which_type, party_timers[i].node, trigger_loc, nullptr, nullptr, &need_redraw); univ.party.party_event_timers[i].time = 0; univ.party.party_event_timers[i].node = -1; stat_area = true; if(need_redraw) redraw = true; } else univ.party.party_event_timers[i].time -= length; } univ.party.age = current_age; if(stat_area) { put_pc_screen(); put_item_screen(stat_window); } if(redraw) draw_terrain(0); } void queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, location spec_loc) { if(spec < 0) return; pending_special_type queued_special; queued_special.spec = spec; queued_special.where = spec_loc; queued_special.type = which_type; queued_special.mode = mode; queued_special.trigger_time = univ.party.age; // FIXME: I forced calling the leave special just to avoid calling them outside, ie. with town_num=200 if (mode==eSpecCtx::LEAVE_TOWN) run_special(queued_special, nullptr, nullptr, nullptr); else special_queue.push(queued_special); } void run_special(pending_special_type spec, short* a, short* b, bool* redraw) { unsigned long store_time = univ.party.age; univ.party.age = spec.trigger_time; run_special(spec.mode, spec.type, spec.spec, spec.where, a, b, redraw); univ.party.age = std::max(univ.party.age, store_time); } // This is the big painful one, the main special engine entry point // which_mode - says when it was called // which_type - where the special is stored (town, out, scenario) // start_spec - the number of the first spec to call // a,b - 2 values that can be returned // Movement: a=1 if blocked, b=1 if forced // Look: a=1 if search blocked (can't get items in container), b unused // Talk: a,b are response strings // Encounter: a=1 if monsters flee, b=1 if forced // redraw - true if now need redraw void run_special(eSpecCtx which_mode, eSpecCtxType which_type, spec_num_t start_spec, location spec_loc, short* a, short* b, bool* redraw) { int num_nodes = 0; // Make sure return pointers are valid short dummy[2]; bool dummy2; if(a == nullptr) a = &dummy[0]; if(b == nullptr) b = &dummy[1]; if(redraw == nullptr) redraw = &dummy2; // We can only have one special node chain running at a time. // If the chain triggers an action that calls another chain, delay the chain until the current one has completed. if(special_in_progress && start_spec >= 0) { queue_special(which_mode, which_type, start_spec, spec_loc); return; } // Initialize runtime state runtime_state ctx; ctx.which_mode = which_mode; ctx.next_spec = start_spec; ctx.next_spec_type = which_type; ctx.spec_loc = spec_loc; ctx.ret_a = a; ctx.ret_b = b; ctx.redraw = redraw; ctx.cur_target = nullptr; special_in_progress = true; if(end_scenario) { special_in_progress = false; return; } // Store the special's location in reserved pointers univ.party.force_ptr(10, spec_loc.x); univ.party.force_ptr(11, spec_loc.y); // Also store the terrain type on that location univ.party.force_ptr(12, coord_to_ter(spec_loc.x, spec_loc.y)); while(ctx.next_spec >= 0) { short cur_spec = ctx.next_spec; ctx.cur_spec_type = ctx.next_spec_type; ctx.next_spec = -1; ctx.cur_spec = get_node(cur_spec, ctx.cur_spec_type); if(univ.debug_mode && univ.node_step_through) { give_help(68,69); std::string debug = "Step: "; debug += (*ctx.cur_spec.type).name(); debug += " - "; debug += std::to_string(cur_spec); add_string_to_buf(debug); redraw_screen(REFRESH_TRANS); sf::Event evt; extern boost::optional replay_fps_limit; cFramerateLimiter fps_limiter; while(true) { if(replaying && has_next_action("step_through_continue")){ pop_next_action(); break; }else if(pollEvent(mainPtr(), evt) && (evt.type == sf::Event::KeyPressed || evt.type == sf::Event::MouseButtonPressed)){ if(recording){ record_action("step_through_continue", ""); } break; } if(replaying && replay_fps_limit.has_value()){ replay_fps_limit->frame_finished(); }else{ fps_limiter.frame_finished(); } } if(replaying && has_next_action("step_through_exit")){ pop_next_action(); univ.node_step_through = false; }else if(!replaying && evt.type == sf::Event::KeyPressed && evt.key.code == sf::Keyboard::Escape){ record_action("step_through_exit", ""); univ.node_step_through = false; } } // Convert pointer values to reference values // TODO: Might need to make a database of which nodes don't allow pointers in which slots. // This is because some nodes now use -2 as a meaningful value. If that's all, then // just disallowing single-digit pointers should suffice, but what about arithmetic? // (Of course, currently all SDFs are positive, so allowing negative arithmetic is useless.) cSpecial& cur_node = ctx.cur_spec; resolve_pointers(cur_node); //print_nums(1111,cur_spec_type,cur_node.type); if(cur_node.type == eSpecType::INVALID) { special_in_progress = false; return; } switch((*cur_node.type).cat) { case eSpecCat::GENERAL: if(cur_node.type == eSpecType::NONE && univ.debug_mode) { std::string type("???"); switch(ctx.cur_spec_type) { case eSpecCtxType::SCEN: type = "scenario"; break; case eSpecCtxType::OUTDOOR: type = "outdoors" ; break; case eSpecCtxType::TOWN: type = "town"; break; } add_string_to_buf("Warning: Null " + type + " special called (ID " + std::to_string(cur_spec) + ") - was this intended?", 4); } general_spec(ctx); break; case eSpecCat::ONCE: oneshot_spec(ctx); break; case eSpecCat::AFFECT: affect_spec(ctx); break; case eSpecCat::IF_THEN: ifthen_spec(ctx); break; case eSpecCat::TOWN: townmode_spec(ctx); break; case eSpecCat::RECT: rect_spec(ctx); break; case eSpecCat::OUTDOOR: outdoor_spec(ctx); break; case eSpecCat::INVALID: // TODO: Should it print some kind of error message? special_in_progress = false; return; } num_nodes++; if(check_for_interrupt()){ add_string_to_buf("SPECIAL ENCOUNTER INTERRUPTED.", 3); ctx.next_spec = -1; } } if(is_out()) erase_out_specials(); else erase_town_specials(); special_in_progress = false; // TODO: Should find a way to do this that doesn't risk stack overflow if(ctx.next_spec == -1 && !special_queue.empty()) { pending_special_type pending = special_queue.front(); special_queue.pop(); run_special(pending, a, b, redraw); } } cSpecial get_node(spec_num_t cur_spec, eSpecCtxType cur_spec_type) { cSpecial dummy_node; dummy_node.type = eSpecType::INVALID; switch(cur_spec_type) { case eSpecCtxType::SCEN: if(cur_spec != minmax(0,univ.scenario.scen_specials.size() - 1,cur_spec)) { showError("The scenario called a scenario special node out of range: " + std::to_string(cur_spec)); return dummy_node; } return univ.scenario.scen_specials[cur_spec]; case eSpecCtxType::OUTDOOR: if(!is_out()) { showError("The scenario called an outdoors special node while not outdoors."); return dummy_node; } if(cur_spec != minmax(0,univ.out->specials.size() - 1,cur_spec)) { showError("The scenario called an outdoor special node out of range: " + std::to_string(cur_spec)); return dummy_node; } return univ.out->specials[cur_spec]; case eSpecCtxType::TOWN: if(is_out()) { showError("The scenario called a town special node while outdoors."); return dummy_node; } if(cur_spec != minmax(0,univ.town->specials.size() - 1,cur_spec)) { showError("The scenario called a town special node out of range: " + std::to_string(cur_spec)); return dummy_node; } return univ.town->specials[cur_spec]; } return dummy_node; } // TODO: Make cur_spec_type an enum void general_spec(const runtime_state& ctx) { bool check_mess = false; std::string str1,str2; short store_val = 0; cSpecial cur_node = ctx.cur_spec, spec = ctx.cur_spec; ctx.next_spec = cur_node.jumpto; switch(cur_node.type) { case eSpecType::NONE: break; // null spec case eSpecType::SET_SDF: check_mess = true; setsd(cur_node.sd1,cur_node.sd2,cur_node.ex1a); break; case eSpecType::INC_SDF: check_mess = true; setsd(cur_node.sd1, cur_node.sd2, PSD[cur_node.sd1][cur_node.sd2] + ((cur_node.ex1b == 0) ? 1 : -1) * cur_node.ex1a); break; case eSpecType::DISPLAY_MSG: check_mess = true; break; case eSpecType::TITLED_MSG: univ.get_str(str1, ctx.cur_spec_type, cur_node.m3); handle_message(ctx, str1, cur_node.pic, ePicType(cur_node.pictype)); break; case eSpecType::DISPLAY_SM_MSG: univ.get_strs(str1, str2, ctx.cur_spec_type, cur_node.m1,cur_node.m2); if(cur_node.m1 >= 0) add_string_to_buf(str1, 4); if(cur_node.m2 >= 0) add_string_to_buf(str2, 4); break; case eSpecType::FLIP_SDF: setsd(cur_node.sd1,cur_node.sd2, ((PSD[cur_node.sd1][cur_node.sd2] == 0) ? 1 : 0) ); check_mess = true; break; case eSpecType::CANT_ENTER: check_mess = true; if(ctx.which_mode == eSpecCtx::TALK) { extern bool talk_end_forced; talk_end_forced = cur_node.ex1a; } else if(cur_node.ex1a != 0) *ctx.ret_a = 1; else { *ctx.ret_a = 0; if(cur_node.ex2a != 0) *ctx.ret_b = 1; } break; case eSpecType::CHANGE_TIME: check_mess = true; univ.party.age += cur_node.ex1a; // TODO: Should this trigger special events, timers, etc? break; case eSpecType::SCEN_TIMER_START: check_mess = true; univ.party.start_timer(spec.ex1a, spec.ex1b, eSpecCtxType::SCEN); break; case eSpecType::PLAY_SOUND: if(spec.ex1b) play_sound(cur_node.ex1a); else play_sound(-cur_node.ex1a); break; case eSpecType::CHANGE_HORSE_OWNER: check_mess = true; if(spec.ex1a != minmax(0, univ.party.horses.size() - 1, cur_node.ex1a)) showError("Horse out of range."); else univ.party.horses[cur_node.ex1a].property = (spec.ex2a == 0) ? 1 : 0; break; case eSpecType::CHANGE_BOAT_OWNER: check_mess = true; if(spec.ex1a != minmax(0,univ.party.boats.size() - 1,spec.ex1a)) showError("Boat out of range."); else univ.party.boats[cur_node.ex1a].property = (spec.ex2a == 0) ? 1 : 0; break; case eSpecType::SET_TOWN_VISIBILITY: check_mess = true; if(spec.ex1a != minmax(0,univ.scenario.towns.size() - 1,spec.ex1a)) showError("Town out of range."); else univ.scenario.towns[spec.ex1a]->can_find = spec.ex2a; *ctx.redraw = true; break; case eSpecType::MAJOR_EVENT_OCCURRED: check_mess = true; if(spec.ex1a != minmax(1,10,spec.ex1a)) showError("Event code out of range."); else if(univ.party.key_times.count(spec.ex1a) == 0) univ.party.key_times[spec.ex1a] = univ.party.calc_day(); break; case eSpecType::FORCED_GIVE: check_mess = true; if(spec.ex1a < 0 || spec.ex1a >= univ.scenario.scen_items.size()) break; if(!univ.party.forced_give(univ.scenario.scen_items[spec.ex1a],eItemAbil::NONE) && spec.ex1b >= 0) ctx.next_spec = spec.ex1b; break; case eSpecType::BUY_ITEMS_OF_TYPE: for(short i = 0; i < 144; i++) if(univ.party.take_class(spec.ex1a)) store_val++; if(store_val == 0) { if(spec.ex1b >= 0) ctx.next_spec = spec.ex1b; } else { check_mess = true; give_gold(store_val * spec.ex2a,true); } break; case eSpecType::CALL_GLOBAL: ctx.next_spec_type = eSpecCtxType::SCEN; break; case eSpecType::SET_SDF_ROW: if(spec.sd1 != minmax(0,299,spec.sd1)) showError("Stuff Done flag out of range."); else for(short i = 0; i < 50; i++) PSD[spec.sd1][i] = spec.ex1a; break; case eSpecType::COPY_SDF: if(!univ.party.sd_legit(spec.sd1,spec.sd2) || !univ.party.sd_legit(spec.ex1a,spec.ex1b)) showError("Stuff Done flag out of range."); else PSD[spec.sd1][spec.sd2] = PSD[spec.ex1a][spec.ex1b]; break; case eSpecType::REST: check_mess = true; do_rest(max(spec.ex1a, 0), max(spec.ex1b, 0), max(spec.ex1b, 0)); break; case eSpecType::END_SCENARIO: end_scenario = true; break; case eSpecType::SET_POINTER: if(spec.ex1a < 0) showError("Attempted to assign a pointer out of range (100..199)"); else try { if(spec.sd1 < 0 && spec.sd2 < 0) univ.party.clear_ptr(spec.ex1a); else univ.party.set_ptr(spec.ex1a,spec.sd1,spec.sd2); } catch(std::range_error x) { showError(x.what()); } break; case eSpecType::SET_CAMP_FLAG: if(!univ.party.sd_legit(spec.sd1,spec.sd2)) showError("Stuff Done flag out of range (x - 0..299, y - 0..49)."); else { set_campaign_flag(spec.sd1,spec.sd2,spec.ex1a,spec.ex1b,spec.m1,spec.ex2a); } break; case eSpecType::DISPLAY_PICTURE: // TODO: In addition to the large picture, there's a small icon; should that be customizable? check_mess = false; univ.get_str(str1, ctx.cur_spec_type, spec.m1); custom_pic_dialog(str1, spec.ex1a); break; case eSpecType::SDF_RANDOM: check_mess = true; short rand; // Automatically fix the range in case some idiot puts it in backwards, or the same (WHY) if(cur_node.ex1a == cur_node.ex1b) { rand = cur_node.ex1b; } else { rand = get_ran(1, min(cur_node.ex1a,cur_node.ex1b), max(cur_node.ex1a,cur_node.ex1b) ); } setsd(cur_node.sd1,cur_node.sd2,rand); //print_nums(rand, cur_node.ex1a, cur_node.ex1b); break; // SDF arithmetic! :D /* SDF1, SDF2 - Output SDF (for division, the quotient) Ex1a, Ex1b - Input SDF (left operand) - if ex1b is -1, takes ex1a as a literal value (which must be positive) Ex2a, Ex2b - Input SDF (right operand) - if ex2b is -1, takes ex2a as a literal value (which must be positive) Ex1c, Ex2c - For division only, output SDF to store the remainder. */ case eSpecType::SDF_ADD: case eSpecType::SDF_DIFF: case eSpecType::SDF_TIMES: case eSpecType::SDF_POWER: case eSpecType::SDF_DIVIDE: { check_mess = true; int i = spec.ex1b == -1 ? spec.ex1a : PSD[spec.ex1a][spec.ex1b]; int j = spec.ex2b == -1 ? spec.ex2a : PSD[spec.ex2a][spec.ex2b]; switch(spec.type) { case eSpecType::SDF_ADD: setsd(spec.sd1, spec.sd2, i + j); break; case eSpecType::SDF_DIFF: setsd(spec.sd1, spec.sd2, i - j); break; case eSpecType::SDF_TIMES: setsd(spec.sd1, spec.sd2, i * j); break; case eSpecType::SDF_DIVIDE: if(univ.party.sd_legit(spec.sd1, spec.sd2)) setsd(spec.sd1, spec.sd2, i / j); if(univ.party.sd_legit(spec.ex1c, spec.ex2c)) setsd(spec.ex1c, spec.ex2c, i % j); break; case eSpecType::SDF_POWER: if(i == 2) setsd(spec.sd1, spec.sd2, 1 << j); else setsd(spec.sd1, spec.sd2, pow(i, j)); break; default: // Unreachable case break; } break; } case eSpecType::PRINT_NUMS: if(!univ.debug_mode) break; check_mess = false; univ.get_strs(str1,str2, ctx.cur_spec_type,cur_node.m1, cur_node.m2); if(cur_node.m1 >= 0) ASB("debug: " + str1, 7); if(cur_node.m2 >= 0) ASB("debug: " + str2, 7); // TODO: Give more options? switch(spec.pic) { case 0: // Print SDF contents print_nums(spec.sd1, spec.sd2, univ.party.stuff_done[spec.sd1][spec.sd2]); break; case 1: // Print three literal values (which might be pointers!) print_nums(spec.ex1a, spec.ex1b, spec.ex1c); break; case 2: // Print monster health and spell points if(spec.ex1a >= univ.town.monst.size()) break; print_nums(spec.ex1a, univ.town.monst[spec.ex1a].health, univ.town.monst[spec.ex1a].mp); break; } break; case eSpecType::CHANGE_TER: alter_space(spec.ex1a,spec.ex1b,spec.ex2a); *ctx.redraw = true; draw_map(true); check_mess = true; break; case eSpecType::SWAP_TER: swap_ter(spec.ex1a,spec.ex1b,spec.ex2a,spec.ex2b); *ctx.redraw = true; draw_map(true); check_mess = true; break; case eSpecType::TRANS_TER: alter_space(spec.ex1a,spec.ex1b,univ.scenario.ter_types[coord_to_ter(spec.ex1a,spec.ex1b)].trans_to_what); *ctx.redraw = true; draw_map(true); check_mess = true; break; case eSpecType::ENTER_SHOP: univ.get_str(str1,ctx.cur_spec_type,spec.m1); spec.ex1b = minmax(0,6,spec.ex2b); start_shop_mode(spec.ex1a, spec.ex1b, str1); ctx.next_spec = -1; break; case eSpecType::STORY_DIALOG: univ.get_str(str1,ctx.cur_spec_type,spec.m1); story_dialog(str1, spec.m2, spec.m3, ctx.cur_spec_type, spec.pic, ePicType(spec.pictype), spec.ex1c, spec.ex2c); break; case eSpecType::CLEAR_BUF: univ.get_buf().clear(); break; case eSpecType::APPEND_STRING: univ.get_str(str1,ctx.cur_spec_type,spec.ex1a); if(spec.pic) univ.get_buf() += ' '; univ.get_buf() += str1; break; case eSpecType::APPEND_NUM: if(spec.pic) univ.get_buf() += ' '; univ.get_buf() += std::to_string(spec.ex1a); break; case eSpecType::APPEND_MONST: if(spec.pic) univ.get_buf() += ' '; if(spec.ex1a == 0) { int pc = univ.get_target_i(current_pc_picked_in_spec_enc(ctx)); if(pc == 6) univ.get_buf() += "Your party"; else if(pc < 100) univ.get_buf() += univ.party[pc].name; else if(!is_out()) univ.get_buf() += univ.town.monst[pc - 100].m_name; } else univ.get_buf() += univ.scenario.scen_monsters[spec.ex1a].m_name; break; case eSpecType::APPEND_ITEM: if(spec.pic) univ.get_buf() += ' '; if(spec.ex1b == 1) univ.get_buf() += univ.scenario.scen_items[spec.ex1a].full_name; else if(spec.ex1b == 2) univ.get_buf() += univ.scenario.scen_items[spec.ex1a].interesting_string(); else univ.get_buf() += univ.scenario.scen_items[spec.ex1a].name; break; case eSpecType::APPEND_TER: if(spec.pic) univ.get_buf() += ' '; univ.get_buf() += univ.scenario.ter_types[spec.ex1a].name; break; case eSpecType::SWAP_STR_BUF: univ.swap_buf(spec.ex1a); break; case eSpecType::STR_BUF_TO_SIGN: if(spec.ex1a < 0) break; if(is_out()) { if(spec.ex1a >= univ.out->sign_locs.size()) break; std::swap(univ.out->sign_locs[spec.ex1a].text, univ.get_buf()); } else { if(spec.ex1a >= univ.town->sign_locs.size()) break; std::swap(univ.town->sign_locs[spec.ex1a].text, univ.get_buf()); } break; case eSpecType::PAUSE: if(spec.ex1a < 0) break; redraw_screen(REFRESH_TERRAIN | REFRESH_STATS); sf::sleep(sf::milliseconds(spec.ex1a)); break; case eSpecType::START_TALK: { int i = univ.get_target_i(current_pc_picked_in_spec_enc(ctx)); if(i >= 100) i -= 100; else i = -1; start_talk_mode(i, spec.ex1a, spec.ex1b, spec.pic); ctx.next_spec = -1; break; } case eSpecType::UPDATE_QUEST: { check_mess = true; if(spec.ex1a < 0 || spec.ex1a >= univ.scenario.quests.size()) { showError("The scenario tried to update a non-existent quest."); break; } if(spec.ex1b < 0 || spec.ex1b > 3) { showError("Invalid quest status (range 0 .. 3)."); break; } auto& job = univ.party.active_quests[spec.ex1a]; auto& quest = univ.scenario.quests[spec.ex1a]; if(spec.ex1b == int(eQuestStatus::STARTED) && job.status != eQuestStatus::STARTED) { job.start = univ.party.calc_day(); job.source = max(-1,spec.ex2a); if(job.source >= univ.party.job_banks.size()) univ.party.job_banks.resize(job.source + 1); } job.status = eQuestStatus(spec.ex1b); switch(job.status) { case eQuestStatus::STARTED: add_string_to_buf("You have received a quest."); break; case eQuestStatus::AVAILABLE: break; // TODO: Should this award XP/gold if the quest was previously started? case eQuestStatus::FAILED: add_string_to_buf("You have failed to complete a quest."); if(job.source >= 0 && job.source < univ.party.job_banks.size()) univ.party.job_banks[job.source].anger += spec.ex2a < 0 ? 1 : spec.ex2a; break; case eQuestStatus::COMPLETED: add_string_to_buf("You have completed a quest!"); if(quest.gold > 0) { int gold = quest.gold; add_string_to_buf(" Received " + std::to_string(gold) + " as a reward."); give_gold(gold, true); } if(quest.xp > 0) award_party_xp(quest.xp); break; } break; } default: showError("Special node type \"" + (*cur_node.type).name() + "\" is either miscategorized or unimplemented!"); break; } if(check_mess) handle_message(ctx); } void oneshot_spec(const runtime_state& ctx) { bool check_mess = true,set_sd = true; std::array strs; std::array buttons = {-1,-1,-1}; cSpecial spec = ctx.cur_spec, cur_node = ctx.cur_spec; cItem store_i; location l; int dlg_res; ctx.next_spec = cur_node.jumpto; if((univ.party.sd_legit(spec.sd1,spec.sd2)) && (PSD[spec.sd1][spec.sd2] == 250)) { ctx.next_spec = -1; return; } switch(cur_node.type) { case eSpecType::ONCE_GIVE_ITEM: if(spec.ex1a >= 0 && spec.ex1a < univ.scenario.scen_items.size() && !univ.party.forced_give(univ.scenario.scen_items[spec.ex1a],eItemAbil::NONE)) { set_sd = false; if( spec.ex2b >= 0) ctx.next_spec = spec.ex2b; } else { give_gold(spec.ex1b,true); give_food(spec.ex2a,true); } break; case eSpecType::ONCE_GIVE_SPEC_ITEM: if(spec.ex1a != minmax(0,49,spec.ex1a)) { showError("Special item is out of range."); set_sd = false; } else if(spec.ex1b == 0) univ.party.spec_items.insert(spec.ex1a); else univ.party.spec_items.erase(spec.ex1a); if(stat_window == ITEM_WIN_SPECIAL) set_stat_window(ITEM_WIN_SPECIAL); *ctx.redraw = 1; break; case eSpecType::ONCE_NULL: set_sd = false; check_mess = false; break; case eSpecType::ONCE_SET_SDF: check_mess = false; break; case eSpecType::ONCE_DIALOG: check_mess = false; dlg_res = once_dialog(univ, spec, ctx.cur_spec_type); if(dlg_res < 0) break; if(spec.m3 > 0) { if(dlg_res == 1) { if((spec.ex1a >= 0) || (spec.ex2a >= 0)) { set_sd = false; } } if(dlg_res == 2) ctx.next_spec = spec.ex1b; if(dlg_res == 3) ctx.next_spec = spec.ex2b; } else { if(dlg_res == 2) ctx.next_spec = spec.ex1b; if(dlg_res == 3) ctx.next_spec = spec.ex2b; } break; case eSpecType::ONCE_GIVE_ITEM_DIALOG: check_mess = false; if(spec.m1 < 0) break; univ.get_strs(strs, ctx.cur_spec_type, spec.m1); // Leave / Take buttons[0] = 9; buttons[1] = 19; dlg_res = custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c); if(dlg_res == 1) {set_sd = false; ctx.next_spec = -1;} else { store_i = univ.scenario.get_stored_item(spec.ex1a); if((spec.ex1a >= 0) && (!univ.party.give_item(store_i,true))) { set_sd = false; ctx.next_spec = -1; } else { give_gold(spec.ex1b,true); give_food(spec.ex2a,true); if((spec.m3 >= 0) && (spec.m3 < 50)) { if(!univ.party.spec_items.count(spec.m3)) ASB("You get a special item."); univ.party.spec_items.insert(spec.m3); *ctx.redraw = true; if(stat_window == ITEM_WIN_SPECIAL) set_stat_window(ITEM_WIN_SPECIAL); } if(spec.ex2b >= 0) ctx.next_spec = spec.ex2b; } } break; case eSpecType::ONCE_OUT_ENCOUNTER: if(spec.ex1a != minmax(0,3,spec.ex1a)) { showError("Special outdoor enc. is out of range. Must be 0-3."); set_sd = false; } else { l = global_to_local(univ.party.out_loc); place_outd_wand_monst(l, univ.out->special_enc[spec.ex1a],true); } break; case eSpecType::ONCE_TOWN_ENCOUNTER: activate_monsters(spec.ex1a,0); break; case eSpecType::ONCE_TRAP: check_mess = false; if((spec.m1 >= 0) || (spec.m2 >= 0)) { univ.get_strs(strs[0],strs[1], ctx.cur_spec_type, spec.m1, spec.m2); buttons[0] = 3; buttons[1] = 2; dlg_res = custom_choice_dialog(strs,spec.pic,ePicType(spec.pictype),buttons, true, spec.ex1c, spec.ex2c); // TODO: Make custom_choice_dialog return string? } else dlg_res = cChoiceDlog("basic-trap",{"yes","no"}).show() == "no"; if(dlg_res == 1) { set_sd = false; ctx.next_spec = -1; *ctx.ret_a = 1; } else { if(!is_combat()) { dlg_res = select_pc(eSelectPC::ONLY_LIVING,"Trap! Who will disarm?", eSkill::DISARM_TRAPS); if(dlg_res == 6){ *ctx.ret_a = 1; set_sd = false; } } else dlg_res = univ.cur_pc; bool disarmed = run_trap(dlg_res,eTrapType(spec.ex1a),spec.ex1b,spec.ex2a); if(!disarmed && spec.ex1a == TRAP_CUSTOM) { if(spec.jumpto >= 0) queue_special(ctx.which_mode, ctx.cur_spec_type, spec.jumpto, loc(univ.party.get_ptr(10), univ.party.get_ptr(11))); ctx.next_spec = spec.ex2b; ctx.next_spec_type = eSpecCtxType::SCEN; } } break; case eSpecType::ONCE_DISPLAY_MSG: break; // Nothing to do here, but need to include it to prevent the below error from showing. default: showError("Special node type \"" + (*cur_node.type).name() + "\" is either miscategorized or unimplemented!"); break; } if(check_mess) { handle_message(ctx); } if((set_sd) && (univ.party.sd_legit(spec.sd1,spec.sd2))) PSD[spec.sd1][spec.sd2] = 250; } void affect_spec(const runtime_state& ctx) { bool check_mess = true; short r1; iLiving& pc = current_pc_picked_in_spec_enc(ctx); short pc_num = univ.get_target_i(pc); std::string str; cSpecial spec = ctx.cur_spec, cur_node = ctx.cur_spec; ctx.next_spec = cur_node.jumpto; switch(cur_node.type) { case eSpecType::SELECT_TARGET: check_mess = false; // If this <= 0, pick PC normally // TODO: I think this is for compatibility with old scenarios? If so, remove it and just convert data on load. // (Actually, I think the only compatibility thing is that it's <= instead of ==) if(spec.ex2a <= 0) { int i = 0; if(spec.ex1a == 2) ctx.cur_target = &univ.party; else if(spec.ex1a == 1) { i = select_pc(eSelectPC::ONLY_LIVING); if(i != 6) ctx.cur_target = &univ.party[i]; } else if(spec.ex1a == 0) { i = select_pc(eSelectPC::ANY); if(i != 6) ctx.cur_target = &univ.party[i]; } else if(spec.ex1a == 3) { i = select_pc(eSelectPC::ONLY_DEAD); if(i != 6) ctx.cur_target = &univ.party[i]; } else if(spec.ex1a == 4) { i = select_pc(eSelectPC::ONLY_LIVING_WITH_ITEM_SLOT); if(i != 6) ctx.cur_target = &univ.party[i]; } if(i == 6)// && (spec.ex1b >= 0)) ctx.next_spec = spec.ex1b; } else if(spec.ex2a == 2) { // Select a specific PC short pc = spec.ex2b; bool can_pick = true; if(pc >= 0 && pc < 6) { // Honour the request for alive PCs only. if(univ.party[pc].main_status == eMainStatus::ABSENT) can_pick = false; else if(spec.ex1a % 4 == 0 && univ.party[pc].main_status != eMainStatus::ALIVE) can_pick = false; else if(spec.ex1a == 3 && univ.party[pc].main_status == eMainStatus::ALIVE) can_pick = false; else if(spec.ex1a == 4 && !univ.party[pc].has_space()) can_pick = false; } else if(pc >= 100 && pc < univ.town.monst.size() + 100) { short monst = pc - 100; // Honour the request for alive only if(spec.ex1a == 0 && univ.town.monst[monst].active == eCreatureStatus::DEAD) can_pick = false; else if(spec.ex1a == 3 && univ.town.monst[monst].is_alive()) can_pick = false; } else if(pc >= 1000 || pc == -1) { // Select PC by unique ID; might be a stored PC rather than a party member if(pc == -1) pc = 1000 + spec.ex2c; can_pick = false; // Assume ID is invalid unless proven otherwise cPlayer* found = nullptr; for(short i = 0; i < 6; i++) { if(univ.party[i].unique_id == pc) { can_pick = true; found = &univ.party[i]; break; } } if(!can_pick) { for(auto& p : univ.stored_pcs) { if(p.first == pc && p.second->unique_id == pc) { can_pick = true; found = p.second.get(); break; } } } if(can_pick) { // Honour the request for alive PCs only. if(found->main_status == eMainStatus::ABSENT) can_pick = false; else if(spec.ex1a % 4 == 0 && found->main_status != eMainStatus::ALIVE) can_pick = false; else if(spec.ex1a == 3 && found->main_status == eMainStatus::ALIVE) can_pick = false; else if(spec.ex1a == 4 && !found->has_space()) can_pick = false; } if(can_pick) ctx.cur_target = found; else ctx.next_spec = spec.ex1b; break; // To avoid the call to get_target } else can_pick = false; // Because it's an invalid index if(can_pick) ctx.cur_target = &univ.get_target(pc); else ctx.next_spec = spec.ex1b; } else if(spec.ex2a == 1) { // Pick random PC (from *i) int i; if(spec.ex1a == 0) { bool can_pick = false; int tries = 0; while(!can_pick && tries < 100) { i = get_ran(1,0,5); can_pick = true; if(univ.party[i].main_status == eMainStatus::ABSENT) can_pick = false; else if(spec.ex1a % 4 == 0 && univ.party[i].main_status != eMainStatus::ALIVE) can_pick = false; else if(spec.ex1a == 3 && univ.party[i].main_status == eMainStatus::ALIVE) can_pick = false; else if(spec.ex1a == 4 && !univ.party[i].has_space()) can_pick = false; tries++; } if(can_pick) ctx.cur_target = &univ.party[i]; else ctx.next_spec = spec.ex1b; } else { i = get_ran(1,0,5); ctx.cur_target = &univ.party[i]; } } break; case eSpecType::DAMAGE: { r1 = get_ran(spec.ex1a,1,spec.ex1b) + spec.ex2a; eDamageType dam_type = (eDamageType) spec.ex2b; int snd_type = spec.ex2c <= 0 ? 0 : -spec.ex2c; if(pc_num == 6) hit_party(r1, dam_type, snd_type); else damage_target(pc_num, r1, dam_type, snd_type); break; } case eSpecType::AFFECT_HP: if(spec.ex1b == 0) pc.heal(spec.ex1a); else pc.heal(-spec.ex1a); if(cCreature* who = dynamic_cast(&pc)) { if(spec.ex1b == 0) who->spell_note(41); else who->spell_note(42); } break; case eSpecType::AFFECT_SP: if(spec.ex1b == 0) pc.restore_sp(spec.ex1a); else pc.drain_sp(spec.ex1a, spec.ex1c); if(cCreature* who = dynamic_cast(&pc)) { if(spec.ex1b == 0) who->spell_note(43); else who->spell_note(44); } break; case eSpecType::AFFECT_XP: if(pc_num >= 100) break; for(short i = 0; i < 6; i++) if(pc_num == 6 || pc_num == i) { if(spec.ex1a < 0) univ.party[i].experience = univ.party[i].level * univ.party[i].get_tnl(); else if(spec.ex1b == 0) award_xp(i,spec.ex1a,true); else drain_pc(univ.party[i],spec.ex1a); } break; case eSpecType::AFFECT_SKILL_PTS: if(pc_num >= 100) break; for(short i = 0; i < 6; i++) if(pc_num == 6 || pc_num == i) univ.party[i].skill_pts = minmax(0, 100, univ.party[i].skill_pts + spec.ex1a * ((spec.ex1b != 0) ? -1: 1)); break; case eSpecType::AFFECT_DEADNESS: if(pc_num < 100) { for(short i = 0; i < 6; i++) if(pc_num == 6 || pc_num == i) { if(spec.ex1b == 0) { if(spec.ex1a == 3 && is_combat() && which_combat_type == 0 && univ.party[i].main_status == eMainStatus::FLED) univ.party[i].main_status = eMainStatus::ALIVE; else if(spec.ex1a == 4) univ.party[i].main_status -= eMainStatus::SPLIT; else if(spec.ex1a == 5) univ.party[i].main_status = eMainStatus::ALIVE; else if((univ.party[i].main_status > eMainStatus::ABSENT) && (univ.party[i].main_status < eMainStatus::SPLIT)) univ.party[i].main_status = eMainStatus::ALIVE; } else if(univ.party[i].main_status == eMainStatus::ABSENT); else switch(spec.ex1a){ // When passed to kill_pc, the SPLIT party status actually means "no saving throw". case 0: kill_pc(univ.party[i],spec.ex1c > 0 ? eMainStatus::DEAD : eMainStatus::SPLIT_DEAD); break; case 1: kill_pc(univ.party[i],spec.ex1c > 0 ? eMainStatus::DUST : eMainStatus::SPLIT_DUST); break; case 2: if(spec.ex1c > 0) petrify_pc(univ.party[i],spec.ex1c); else kill_pc(univ.party[i],eMainStatus::SPLIT_STONE); break; case 3: if(!is_combat() || which_combat_type != 0) break; if(univ.party[i].main_status == eMainStatus::ALIVE) univ.party[i].main_status = eMainStatus::FLED; break; case 4: if(party_size(true) > 1) univ.party[i].main_status += eMainStatus::SPLIT; break; case 5: kill_pc(univ.party[i],spec.ex1c > 0 ? eMainStatus::ABSENT : eMainStatus::SPLIT_ABSENT); break; } } *ctx.redraw = true; } else { // Kill monster cCreature& who = univ.town.monst[pc_num - 100]; if(who.is_alive() && spec.ex1b > 0) { switch(spec.ex1a) { case 0: who.spell_note(46); kill_monst(who,7,eMainStatus::DEAD); break; case 1: who.spell_note(51); kill_monst(who,7,eMainStatus::DUST); break; case 2: if(spec.ex1c > 0) petrify_monst(who,spec.ex1c); else { who.spell_note(8); kill_monst(who,7,eMainStatus::STONE); } break; case 5: who.active = eCreatureStatus::DEAD; break; } } // Bring back to life else if(who.active == eCreatureStatus::DEAD && spec.ex1b == 0) { who.active = eCreatureStatus::IDLE; who.spell_note(45); } } break; case eSpecType::AFFECT_STATUS: switch(eStatus(spec.ex1c)) { case eStatus::POISON: if(spec.ex1b == 0) pc.cure(spec.ex1a); else pc.poison(spec.ex1a); break; case eStatus::HASTE_SLOW: if(spec.ex1b == 0) pc.slow(-spec.ex1a); else pc.slow(spec.ex1a); break; case eStatus::INVULNERABLE: if(spec.ex1b == 0) pc.apply_status(eStatus::INVULNERABLE, spec.ex1a); else pc.apply_status(eStatus::INVULNERABLE, -spec.ex1a); break; case eStatus::MAGIC_RESISTANCE: if(spec.ex1b == 0) pc.apply_status(eStatus::MAGIC_RESISTANCE, spec.ex1a); else pc.apply_status(eStatus::MAGIC_RESISTANCE, -spec.ex1a); break; case eStatus::WEBS: if(spec.ex1b == 0) pc.apply_status(eStatus::WEBS, -spec.ex1a); else pc.web(spec.ex1a); break; case eStatus::DISEASE: if(spec.ex1b == 0) pc.apply_status(eStatus::DISEASE, -spec.ex1a); else pc.disease(spec.ex1a); break; case eStatus::INVISIBLE: if(spec.ex1b == 0) pc.apply_status(eStatus::INVISIBLE, spec.ex1a); else pc.apply_status(eStatus::INVISIBLE, -spec.ex1a); break; case eStatus::BLESS_CURSE: if(spec.ex1b == 0) pc.curse(-spec.ex1a); else pc.curse(spec.ex1a); break; case eStatus::DUMB: if(spec.ex1b == 0) pc.apply_status(eStatus::DUMB, -spec.ex1a); else pc.dumbfound(spec.ex1a); break; case eStatus::ASLEEP: if(spec.ex1b == 0) pc.apply_status(eStatus::ASLEEP, -spec.ex1a); else pc.sleep(eStatus::ASLEEP, spec.ex1a, 10); break; case eStatus::PARALYZED: if(spec.ex1b == 0) pc.apply_status(eStatus::PARALYZED, -spec.ex1a); else pc.sleep(eStatus::PARALYZED, spec.ex1a, 10); break; case eStatus::POISONED_WEAPON: if(pc_num >= 100) break; for(short i = 0; i < 6; i++) if(pc_num == 6 || pc_num == i) { if(spec.ex1b == 0) poison_weapon(i, spec.ex1a, true); else univ.party[i].apply_status(eStatus::POISONED_WEAPON, -spec.ex1a); } break; case eStatus::MARTYRS_SHIELD: if(spec.ex1b == 0) pc.apply_status(eStatus::MARTYRS_SHIELD, spec.ex1a); else pc.apply_status(eStatus::MARTYRS_SHIELD, -spec.ex1a); break; case eStatus::ACID: if(spec.ex1b == 0) pc.apply_status(eStatus::ACID, -spec.ex1a); else pc.acid(spec.ex1a); break; case eStatus::FORCECAGE: if(is_out()) break; if(spec.ex1b == 0) pc.apply_status(eStatus::FORCECAGE, -spec.ex1a); else pc.sleep(eStatus::FORCECAGE, spec.ex1a, 10); break; // Invalid values case eStatus::MAIN: case eStatus::CHARM: break; } put_pc_screen(); if(spec.ex2a == int(eStatus::DUMB)) adjust_spell_menus(); break; case eSpecType::AFFECT_STAT: if(pc_num >= 100) break; if(spec.ex2a != minmax(0,20,spec.ex2a)) { showError("Skill is out of range."); break; } for(short i = 0; i < 6; i++) if((pc_num == 6 || pc_num == i) && get_ran(1,1,100) < spec.pic) { eSkill skill = eSkill(spec.ex2a); int adj = spec.ex1a * (spec.ex1b != 0 ? -1: 1); if(skill == eSkill::MAX_HP) univ.party[i].max_health = minmax(6, 250, univ.party[i].max_health + adj); else if(skill == eSkill::MAX_SP) univ.party[i].max_sp = minmax(0, 150, univ.party[i].max_sp + adj); else univ.party[i].skills[skill] = minmax(0, skill_max[skill], univ.party[i].skills[skill] + adj); } break; case eSpecType::AFFECT_LEVEL: if(pc_num >= 100) { int lvl = pc.get_level(); if(spec.ex1b == 0) lvl += spec.ex1a; else lvl -= spec.ex1a; dynamic_cast(&pc)->level = lvl; } else for(short i = 0; i < 6; i++) if(pc_num == 6 || pc_num == i) { int cur = univ.party[i].get_level(); int lvl = cur; if(spec.ex1b == 0) lvl += spec.ex1a; else lvl -= spec.ex1a; univ.party[i].level = lvl; } break; case eSpecType::AFFECT_MORALE: pc.scare(spec.ex1a * (spec.ex1b != 0 ? 1 : -1)); break; case eSpecType::AFFECT_MONST_ATT: if(pc_num < 100) break; if(spec.ex1a < 0 || spec.ex1a > 2) { showError("Invalid monster attack (0-2)"); break; } if(spec.ex2a == 0) { dynamic_cast(&pc)->a[spec.ex1a].dice += spec.ex1b; dynamic_cast(&pc)->a[spec.ex1a].sides += spec.ex1c; } else { dynamic_cast(&pc)->a[spec.ex1a].dice -= spec.ex1b; dynamic_cast(&pc)->a[spec.ex1a].sides -= spec.ex1c; } break; case eSpecType::AFFECT_MONST_STAT: if(pc_num < 100) break; if(spec.ex2a < 0 || spec.ex2a > 7) { showError("Invalid monster stat (0-7)"); break; } // Blah, let's hackily reuse pc_num... pc_num = spec.ex1a; if(spec.ex1b > 0) pc_num = -pc_num; switch(spec.ex2a) { case 0: dynamic_cast(&pc)->m_health += pc_num; break; case 1: dynamic_cast(&pc)->max_mp += pc_num; break; case 2: dynamic_cast(&pc)->armor += pc_num; break; case 3: dynamic_cast(&pc)->skill += pc_num; break; case 4: dynamic_cast(&pc)->speed += pc_num; break; case 5: dynamic_cast(&pc)->mu += pc_num; break; case 6: dynamic_cast(&pc)->cl += pc_num; break; } break; case eSpecType::AFFECT_MAGE_SPELL: if(pc_num >= 100) break; if(spec.ex1a != minmax(0,61,spec.ex1a)) { showError("Mage spell is out of range (0 - 61). See docs."); break; } for(short i = 0; i < 6; i++) if(pc_num == 6 || pc_num == i) univ.party[i].mage_spells[spec.ex1a] = !spec.ex1b; break; case eSpecType::AFFECT_PRIEST_SPELL: if(pc_num >= 100) break; if(spec.ex1a != minmax(0,61,spec.ex1a)) { showError("Priest spell is out of range (0 - 61). See docs."); break; } for(short i = 0; i < 6; i++) if(pc_num == 6 || pc_num == i) univ.party[i].priest_spells[spec.ex1a] = !spec.ex1b; break; case eSpecType::AFFECT_GOLD: if(spec.ex1b == 0) give_gold(spec.ex1a,true); else if(univ.party.gold < spec.ex1a) univ.party.gold = 0; else take_gold(spec.ex1a,false); break; case eSpecType::AFFECT_FOOD: if(spec.ex1b == 0) give_food(spec.ex1a,true); else if(univ.party.food < spec.ex1a) univ.party.food = 0; else take_food(spec.ex1a,false); break; case eSpecType::AFFECT_ALCHEMY: if(spec.ex1a != minmax(0,19,spec.ex1a)) { showError("Alchemy is out of range."); break; } univ.party.alchemy[spec.ex1a] = !spec.ex1b; break; case eSpecType::AFFECT_SOUL_CRYSTAL: if(pc_num < 100) break; if(spec.ex1a == 0) record_monst(dynamic_cast(&pc), spec.ex1b); else for(mon_num_t& monst : univ.party.imprisoned_monst) { if(monst == dynamic_cast(&pc)->number) monst = 0; } break; case eSpecType::AFFECT_PARTY_STATUS: if(spec.ex2a < 0 || spec.ex2a > 3) break; if(spec.ex1b == 0 && spec.ex2a == 1 && univ.party.in_boat >= 0) add_string_to_buf(" Can't fly when on a boat."); else if(spec.ex1b == 0 && spec.ex2a == 1 && univ.party.in_horse >= 0) add_string_to_buf(" Can't fly when on a horse."); r1 = univ.party.status[ePartyStatus(spec.ex2a)]; if(spec.ex1b == 0) r1 = minmax(0,250,r1 + spec.ex1a); else r1 = minmax(0,250,r1 - spec.ex1a); univ.party.status[ePartyStatus::STEALTH] = r1; break; case eSpecType::AFFECT_TRAITS: if(pc_num >= 100) break; if(spec.ex1a < 0 || spec.ex1a > 16) { showError("Trait is out of range (0 - 16)."); break; } for(short i = 0; i < 6; i++) if(pc_num == 6 || pc_num == i) univ.party[i].traits[eTrait(spec.ex1a)] = !spec.ex1b; break; case eSpecType::AFFECT_AP: if(!is_combat()) break; if(pc_num == 6) { for(short i = 0; i < 6; i++) { if(spec.ex1b) univ.party[i].ap += spec.ex1a; else univ.party[i].ap -= spec.ex1a; if(univ.party[i].ap < 0) univ.party[i].ap = 0; } } else { if(spec.ex1b) pc.ap += spec.ex1a; else pc.ap -= spec.ex1a; if(pc.ap < 0) pc.ap = 0; } break; case eSpecType::AFFECT_NAME: univ.get_str(str, ctx.cur_spec_type, spec.m3); if(cPlayer* who = dynamic_cast(&pc)) who->name = str; else if(cCreature* monst = dynamic_cast(&pc)) monst->m_name = str; else if(dynamic_cast(&pc)) for(short i = 0; i < 6; i++) univ.party[i].name = str; break; case eSpecType::CREATE_NEW_PC: if(spec.ex1c < 0 || spec.ex1c > 19) { showError("Race out of range (0 - 19)."); break; } pc_num = univ.party.free_space(); if(pc_num == 6) { add_string_to_buf("No room for new PC."); ctx.next_spec = spec.pictype; check_mess = false; break; } univ.get_str(str, ctx.cur_spec_type, spec.m3); univ.party.new_pc(pc_num); univ.party[pc_num].name = str; univ.party[pc_num].which_graphic = spec.pic; univ.party[pc_num].cur_health = univ.party[pc_num].max_health = spec.ex1a; univ.party[pc_num].cur_sp = univ.party[pc_num].max_sp = spec.ex1b; univ.party[pc_num].race = eRace(spec.ex1c); univ.party[pc_num].skills[eSkill::STRENGTH] = spec.ex2a; univ.party[pc_num].skills[eSkill::DEXTERITY] = spec.ex2b; univ.party[pc_num].skills[eSkill::INTELLIGENCE] = spec.ex2c; ctx.cur_target = &univ.get_target(pc_num); if(univ.party.sd_legit(spec.sd1, spec.sd2)) univ.party.stuff_done[spec.sd1][spec.sd2] = univ.party[pc_num].unique_id - 1000; break; case eSpecType::STORE_PC: if(cPlayer* who = dynamic_cast(&pc)) { if(univ.party.sd_legit(spec.sd1, spec.sd2)) univ.party.stuff_done[spec.sd1][spec.sd2] = univ.party[pc_num].unique_id - 1000; if(spec.ex1a == 1) break; who->main_status += eMainStatus::SPLIT; univ.stored_pcs[who->unique_id] = univ.party.remove_pc(pc_num); univ.party.new_pc(pc_num); } break; case eSpecType::UNSTORE_PC: if(spec.ex1a < 1000) spec.ex1a += 1000; if(univ.stored_pcs.find(spec.ex1a) == univ.stored_pcs.end()) { showError("Scenario tried to unstore a nonexistent PC!"); break; } pc_num = univ.party.free_space(); if(pc_num == 6) { add_string_to_buf("No room for PC."); ctx.next_spec = spec.ex1b; check_mess = false; break; } univ.party.replace_pc(pc_num, std::move(univ.stored_pcs[spec.ex1a])); ctx.cur_target = &univ.get_target(pc_num); univ.party[pc_num].main_status -= eMainStatus::SPLIT; univ.stored_pcs.erase(spec.ex1a); break; case eSpecType::AFFECT_MONST_TARG: if(pc_num < 100) break; // TODO: Verify this actually works! It's possible the monster ignores this and just recalculates its target each turn. dynamic_cast(&pc)->target = spec.ex1a; break; case eSpecType::GIVE_ITEM: if(pc_num >= 100) break; if(spec.ex1a >= 0 && spec.ex1a < univ.scenario.scen_items.size()) { cItem to_give = univ.scenario.scen_items[spec.ex1a]; if(spec.ex1b >= 0 && spec.ex1b <= 6) { to_give.enchant_weapon(eEnchant(spec.ex1b)); } if(to_give.charges > 0 && spec.ex1c >= 0) to_give.charges = spec.ex1c; if(spec.ex2a == 1 || spec.ex2a == 2) to_give.ident = true; else if(spec.ex2a == 0) to_give.ident = false; if(spec.ex2a == 2) to_give.concealed = false; if(spec.ex2b == 1) to_give.cursed = to_give.unsellable = true; else if(spec.ex2b == 0) to_give.cursed = to_give.unsellable = false; int equip_type = 0; if(spec.ex2c == 0) equip_type = GIVE_EQUIP_SOFT; else if(spec.ex2c == 1) equip_type = GIVE_EQUIP_TRY; else if(spec.ex2c >= 2) equip_type = GIVE_EQUIP_FORCE; bool success = true; for(short i = 0; i < 6; i++) if(pc_num == 6 || pc_num == i) success = success && univ.party[i].give_item(to_give, equip_type | GIVE_ALLOW_OVERLOAD) == eBuyStatus::OK; if(!success) ctx.next_spec = spec.pic; } break; default: showError("Special node type \"" + (*cur_node.type).name() + "\" is either miscategorized or unimplemented!"); break; } if(check_mess) { handle_message(ctx); } } bool isValidField(int fld, bool allowSpecial) { if(fld <= SPECIAL_EXPLORED) return false; if(fld == SPECIAL_SPOT) return false; if(fld >= WALL_FORCE && fld <= BARRIER_CAGE) return true; if(!allowSpecial) return false; if(fld == FIELD_DISPEL || fld == FIELD_SMASH) return true; return false; } void ifthen_spec(const runtime_state& ctx) { bool check_mess = false; std::string str1, str2, str3; cSpecial spec = ctx.cur_spec, cur_node = ctx.cur_spec; location l; ctx.next_spec = cur_node.jumpto; switch(cur_node.type) { case eSpecType::IF_SDF: if(univ.party.sd_legit(spec.sd1,spec.sd2)) { if((spec.ex1a >= 0) && (PSD[spec.sd1][spec.sd2] >= spec.ex1a)) ctx.next_spec = spec.ex1b; else if((spec.ex2a >= 0) && (PSD[spec.sd1][spec.sd2] < spec.ex2a)) ctx.next_spec = spec.ex2b; } break; case eSpecType::IF_TOWN_NUM: if(((is_town()) || (is_combat())) && (univ.party.town_num == spec.ex1a)) ctx.next_spec = spec.ex1b; break; case eSpecType::IF_RANDOM: if(get_ran(1,1,100) < spec.ex1a) ctx.next_spec = spec.ex1b; break; case eSpecType::IF_HAVE_SPECIAL_ITEM: if(spec.ex1a != minmax(0,49,spec.ex1a)) { showError("Special item is out of range."); } else if(univ.party.spec_items.count(spec.ex1a) > 0) ctx.next_spec = spec.ex1b; break; case eSpecType::IF_SDF_COMPARE: if((univ.party.sd_legit(spec.sd1,spec.sd2)) && (univ.party.sd_legit(spec.ex1a,spec.ex1b))) { if(PSD[spec.ex1a][spec.ex1b] < PSD[spec.sd1][spec.sd2]) ctx.next_spec = spec.ex2b; } else showError("A Stuff Done flag is out of range."); break; case eSpecType::IF_TER_TYPE: l.x = spec.ex1a; l.y = spec.ex1b; l = local_to_global(l); if((is_town() || is_combat()) && univ.town->terrain(spec.ex1a,spec.ex1b) == spec.ex2a) ctx.next_spec = spec.ex2b; else if(is_out() && univ.out[l.x][l.y] == spec.ex2a) ctx.next_spec = spec.ex2b; break; case eSpecType::IF_HAS_GOLD: if(univ.party.gold >= spec.ex1a) { if(spec.ex2a > 0) take_gold(spec.ex1a,true); ctx.next_spec = spec.ex1b; } break; case eSpecType::IF_HAS_FOOD: if(univ.party.food >= spec.ex1a) { if(spec.ex2a > 0) take_food(spec.ex1a,true); ctx.next_spec = spec.ex1b; } break; case eSpecType::IF_ITEM_CLASS_ON_SPACE: if(is_out()) break; l.x = spec.ex1a; l.y = spec.ex1b; for(short i = 0; i < univ.town.items.size(); i++) if(univ.town.items[i].variety != eItemType::NO_ITEM && univ.town.items[i].special_class == (unsigned)spec.ex2a && l == univ.town.items[i].item_loc) { ctx.next_spec = spec.ex2b; if(spec.ex2c > 0) { *ctx.redraw = true; univ.town.items[i].variety = eItemType::NO_ITEM; } } break; case eSpecType::IF_HAVE_ITEM_CLASS: if(spec.ex2a > 0 && univ.party.take_class(spec.ex1a)) ctx.next_spec = spec.ex1b; else if(spec.ex2a == 0 && univ.party.has_class(spec.ex1a)) ctx.next_spec = spec.ex1b; break; case eSpecType::IF_EQUIP_ITEM_CLASS: for(cPlayer& pc : univ.party) if(pc.main_status == eMainStatus::ALIVE) if(cInvenSlot item = pc.has_class_equip(spec.ex1a)) { ctx.next_spec = spec.ex1b; if(spec.ex2a > 0) { *ctx.redraw = true; pc.take_item(item.slot); if(&pc == &univ.party[stat_window]) put_item_screen(stat_window); } } break; case eSpecType::IF_DAY_REACHED: if(univ.party.calc_day() >= spec.ex1a) ctx.next_spec = spec.ex1b; break; case eSpecType::IF_FIELDS: if(is_out()) break; if(!isValidField(spec.m1, false)) { showError("Scenario tried to check for invalid field type (1...24)"); } else { int i = 0; for(short j = spec.ex1b; j < min(spec.ex2b, univ.town->max_dim); j++) for(short k = spec.ex1a; k < min(spec.ex2a, univ.town->max_dim); k++) { // If pict non-zero, exclude rectangle interior if(spec.pic > 0 && i > spec.ex1b && i < spec.ex2b && j > spec.ex1a && j < spec.ex2a) continue; switch(eFieldType(spec.m1)) { // These values are not allowed case SPECIAL_EXPLORED: case SPECIAL_SPOT: case SPECIAL_ROAD: case FIELD_DISPEL: case FIELD_SMASH: break; // Walls case WALL_FIRE: i += univ.town.is_fire_wall(i,j); break; case WALL_FORCE: i += univ.town.is_force_wall(i,j); break; case WALL_ICE: i += univ.town.is_ice_wall(i,j); break; case WALL_BLADES: i += univ.town.is_blade_wall(i,j); break; // Clouds case CLOUD_STINK: i += univ.town.is_scloud(i,j); break; case CLOUD_SLEEP: i += univ.town.is_sleep_cloud(i,j); break; // Advanced case FIELD_QUICKFIRE: i += univ.town.is_quickfire(i,j); break; case FIELD_ANTIMAGIC: i += univ.town.is_antimagic(i,j); break; case BARRIER_FIRE: i += univ.town.is_fire_barr(i,j); break; case BARRIER_FORCE: i += univ.town.is_force_barr(i,j); break; case BARRIER_CAGE: i += univ.town.is_force_cage(i,j); break; // Objects case FIELD_WEB: i += univ.town.is_web(i,j); break; case OBJECT_BARREL: i += univ.town.is_barrel(i,j); break; case OBJECT_CRATE: i += univ.town.is_crate(i,j); break; case OBJECT_BLOCK: i += univ.town.is_block(i,j); break; // Sfx case SFX_SMALL_BLOOD: i += univ.town.is_sm_blood(i,j); break; case SFX_MEDIUM_BLOOD: i += univ.town.is_med_blood(i,j); break; case SFX_LARGE_BLOOD: i += univ.town.is_lg_blood(i,j); break; case SFX_SMALL_SLIME: i += univ.town.is_sm_slime(i,j); break; case SFX_LARGE_SLIME: i += univ.town.is_lg_slime(i,j); break; case SFX_ASH: i += univ.town.is_ash(i,j); break; case SFX_BONES: i += univ.town.is_bones(i,j); break; case SFX_RUBBLE: i += univ.town.is_rubble(i,j); break; } } if(i >= spec.sd1 && i <= spec.sd2) ctx.next_spec = spec.m2; } break; case eSpecType::IF_PARTY_SIZE: if(spec.ex2a < 1) { if(party_size(spec.ex1a <= 0) == spec.ex2b) ctx.next_spec = spec.ex1b; } else { if(party_size(spec.ex1a <= 0) >= spec.ex2b) ctx.next_spec = spec.ex1b; } break; case eSpecType::IF_EVENT_OCCURRED: if(day_reached(spec.ex1a,spec.ex1b)) ctx.next_spec = spec.ex2b; break; case eSpecType::IF_SPECIES: if(spec.ex1a < 0 || spec.ex1a > 21) { showError("Species out of range (0-21)"); } else { int i = race_present(eRace(spec.ex1a)), j = minmax(1, party_size(true), spec.ex2a); if(spec.ex2b == -2 && i <= j) ctx.next_spec = spec.ex1b; if(spec.ex2b == -1 && i < j) ctx.next_spec = spec.ex1b; if(spec.ex2b == 0 && i == j) ctx.next_spec = spec.ex1b; if(spec.ex2b == 1 && i > j) ctx.next_spec = spec.ex1b; if(spec.ex2b == 2 && i >= j) ctx.next_spec = spec.ex1b; } break; case eSpecType::IF_TRAIT: if(spec.ex1a < 0 || spec.ex1a > 16) { showError("Invalid trait (0...16)"); } else { int i = trait_present(eTrait(spec.ex1a)), j = minmax(1, party_size(true), spec.ex2a); if(spec.ex2b == -2 && i <= j) ctx.next_spec = spec.ex1b; if(spec.ex2b == -1 && i < j) ctx.next_spec = spec.ex1b; if(spec.ex2b == 0 && i == j) ctx.next_spec = spec.ex1b; if(spec.ex2b == 1 && i > j) ctx.next_spec = spec.ex1b; if(spec.ex2b == 2 && i >= j) ctx.next_spec = spec.ex1b; } break; case eSpecType::IF_STATISTIC: if((spec.ex2a < 0 || spec.ex2a > 20) && (spec.ex2a < 100 || spec.ex2a > 104)) { showError("Attempted to check an invalid statistic (0...20 or 100...104)."); break; } if(spec.ex2b < -1 || spec.ex2b > 3) { showError("Invalid statistic-checking mode (-1...3); will fall back to cumulative check."); spec.ex2b = 0; } if(spec.ex2b == -1) { // Check specific PC's stat (uses the active PC from Select PC node) short pc = univ.get_target_i(current_pc_picked_in_spec_enc(ctx)); if(pc == 6 && univ.party.is_split()) pc = univ.get_target_i(univ.party.pc_present()); if(pc >= 0 && pc < 6) { if(check_party_stat(eSkill(spec.ex2a), 10 + pc) >= spec.ex1a) ctx.next_spec = spec.ex1b; break; } spec.ex2b = 0; } if(check_party_stat(eSkill(spec.ex2a), spec.ex2b) >= spec.ex1a) ctx.next_spec = spec.ex1b; break; case eSpecType::IF_TEXT_RESPONSE: check_mess = false; univ.get_str(str1,eSpecCtxType::SCEN,spec.m1); str3 = get_text_response(str1); spec.pic = minmax(0,50,spec.pic); univ.get_strs(str1,str2,eSpecCtxType::SCEN,spec.ex1a,spec.ex2a); if(spec.ex1a >= 0 && str3.compare(0, spec.pic, str1, 0, spec.pic) == 0) ctx.next_spec = spec.ex1b; if(spec.ex2a >= 0 && str3.compare(0, spec.pic, str2, 0, spec.pic) == 0) ctx.next_spec = spec.ex2b; break; /* This is a little complicated. m1 - points to a prompt string m2,m3 - If nonequal, specifies a range of allowed responses. pic - Comparison mode: 0 - in range, 1 - not in range, 2 - simple compare pictype - Special to jump to if both tests pass ex1a, ex1b, ex1c - Test 1 values (see below) ex2a, ex2b, ex2c - Test 2 values (see below) Test values: If pic = 0 or 1: ex#a - Lower bound ex#b - Upper bound Enabled if ex#a < ex#b. If ex#a >= ex#b, this test is ignored. If pic = 2: ex#a - Value to compare to. ex#b - Set to 0 to enable. If -1, this test is ignored. ex#c - Special to jump to if test # succeeds but the other test fails. jumpto - Special to jump to if both tests fail. */ case eSpecType::IF_NUM_RESPONSE: { check_mess = false; if(spec.m2 > spec.m3) std::swap(spec.m2,spec.m3); univ.get_str(str1,eSpecCtxType::SCEN,spec.m1); int i = get_num_response(spec.m2,spec.m3,str1); setsd(spec.sd1, spec.sd2, abs(i)); int j = 0; spec.pic = minmax(0,2,spec.pic); switch(spec.pic) { // Comparison mode case 0: // Is in range? if(spec.ex1a >= 0 && spec.ex1a < spec.ex1b && i == minmax(spec.ex1a,spec.ex1b,i)) j += 1; if(spec.ex2a >= 0 && spec.ex2a < spec.ex2b && i == minmax(spec.ex2a,spec.ex2b,i)) j += 2; break; case 1: // Not in range? if(spec.ex1a >= 0 && spec.ex1a < spec.ex1b && i != minmax(spec.ex1a,spec.ex1b,i)) j += 1; if(spec.ex2a >= 0 && spec.ex2a < spec.ex2b && i != minmax(spec.ex2a,spec.ex2b,i)) j += 2; break; case 2: // Simple comparison? switch(spec.ex1b) { case -2: if(spec.ex1a >= 0 && i <= spec.ex1a) j += 1; break; case -1: if(spec.ex1a >= 0 && i < spec.ex1a) j += 1; break; case 0: if(spec.ex1a >= 0 && i == spec.ex1a) j += 1; break; case 1: if(spec.ex1a >= 0 && i > spec.ex1a) j += 1; break; case 2: if(spec.ex1a >= 0 && i >= spec.ex1a) j += 1; break; } switch(spec.ex2b) { case -2: if(spec.ex2a >= 0 && i <= spec.ex2a) j += 2; break; case -1: if(spec.ex2a >= 0 && i < spec.ex2a) j += 2; break; case 0: if(spec.ex2a >= 0 && i == spec.ex2a) j += 2; break; case 1: if(spec.ex2a >= 0 && i > spec.ex2a) j += 2; break; case 2: if(spec.ex2a >= 0 && i >= spec.ex2a) j += 2; break; } break; } switch(j) { case 1: ctx.next_spec = spec.ex1c; break; case 2: ctx.next_spec = spec.ex2c; break; case 3: ctx.next_spec = spec.pictype; break; } break; } case eSpecType::IF_SDF_EQ: if(univ.party.sd_legit(spec.sd1,spec.sd2)) { if(PSD[spec.sd1][spec.sd2] == spec.ex1a) ctx.next_spec = spec.ex1b; } break; case eSpecType::IF_ALIVE: if(spec.ex1a == -1 && current_pc_picked_in_spec_enc(ctx).is_alive()) ctx.next_spec = spec.ex1b; else if(dynamic_cast(¤t_pc_picked_in_spec_enc(ctx))) { int pc = univ.get_target_i(current_pc_picked_in_spec_enc(ctx)); eMainStatus stat = eMainStatus::ALIVE; switch(spec.ex1a) { case 0: stat = eMainStatus::DEAD; break; case 1: stat = eMainStatus::DUST; break; case 2: stat = eMainStatus::STONE; break; case 3: stat = eMainStatus::FLED; break; case 4: stat = eMainStatus::SPLIT; break; case 5: stat = eMainStatus::ABSENT; break; } bool pass = false; if(stat == eMainStatus::SPLIT && univ.party.is_split()) pass = true; else if(pc < 6 && univ.party[pc].main_status == stat) pass = true; else { int n = std::count_if(univ.party.begin(), univ.party.end(), [stat](const cPlayer& who) { return who.main_status == stat; }); pass = n > 0; } if(pass) ctx.next_spec = spec.ex1b; } break; case eSpecType::IF_MAGE_SPELL: if(cPlayer* who = dynamic_cast(¤t_pc_picked_in_spec_enc(ctx))) { int pc = univ.get_target_i(*who); if(spec.ex1a < 0 || spec.ex1a >= 62) break; bool pass = false; if(pc < 6 && univ.party[pc].mage_spells[spec.ex1a]) pass = true; else { int n = std::count_if(univ.party.begin(), univ.party.end(), [&spec](const cPlayer& who) { return who.mage_spells[spec.ex1a]; }); pass = n + 0; } if(pass) ctx.next_spec = spec.ex1b; } else { // TODO: Implement for monsters // Currently they have a fixed spell list depending solely on caster level } break; case eSpecType::IF_PRIEST_SPELL: if(cPlayer* who = dynamic_cast(¤t_pc_picked_in_spec_enc(ctx))) { int pc = univ.get_target_i(*who); if(spec.ex1a < 0 || spec.ex1a >= 62) break; bool pass = false; if(pc < 6 && univ.party[pc].priest_spells[spec.ex1a]) pass = true; else { int n = std::count_if(univ.party.begin(), univ.party.end(), [&spec](const cPlayer& who) { return who.priest_spells[spec.ex1a]; }); pass = n + 0; } if(pass) ctx.next_spec = spec.ex1b; } else { // TODO: Implement for monsters // Currently they have a fixed spell list depending solely on caster level } break; case eSpecType::IF_RECIPE: if(spec.ex1a < 0 || spec.ex1a >= 20) { showError("Alchemy recipe out of range (0 - 19)."); break; } if(univ.party.alchemy[spec.ex1a]) ctx.next_spec = spec.ex1b; break; case eSpecType::IF_STATUS: if(spec.ex1a < 0 || spec.ex1a > 14) { showError("Invalid status effect (0...14)"); } else { int k = 0; if(dynamic_cast(¤t_pc_picked_in_spec_enc(ctx))) { k = spec.ex2b == 2 ? std::numeric_limits::max() : 0; int j = 0; for(short i = 0; i < 6; i++, j++) if(univ.party[i].main_status == eMainStatus::ALIVE) { eStatus stat = eStatus(spec.ex1a); if(spec.ex2b < 2) k += univ.party[i].status[stat]; else if(spec.ex2b == 3) k = max(univ.party[i].status[stat], k); else if(spec.ex2b == 2) k = min(univ.party[i].status[stat], k); } if(spec.ex2b == 1 && j > 0) k /= j; } else { if(current_pc_picked_in_spec_enc(ctx).is_alive()) k = current_pc_picked_in_spec_enc(ctx).status[eStatus(spec.ex1a)]; else k = 0; } int j = spec.ex2a; if(spec.ex2c == -2 && k <= j) ctx.next_spec = spec.ex1b; if(spec.ex2c == -1 && k < j) ctx.next_spec = spec.ex1b; if(spec.ex2c == 0 && k == j) ctx.next_spec = spec.ex1b; if(spec.ex2c == 1 && k > j) ctx.next_spec = spec.ex1b; if(spec.ex2c == 2 && k >= j) ctx.next_spec = spec.ex1b; } break; case eSpecType::IF_QUEST: if(spec.ex1a < 0 || spec.ex1a >= univ.scenario.quests.size()) { showError("The scenario tried to update a non-existent quest."); break; } if(spec.ex1b < 0 || spec.ex1b > 3) { showError("Invalid quest status (range 0 .. 3)."); break; } if(univ.party.active_quests[spec.ex1a].status == eQuestStatus(spec.ex1b)) ctx.next_spec = spec.ex1c; break; case eSpecType::IF_CONTEXT: // TODO: Test this. In particular, test that the legacy behaviour is correct. if(univ.scenario.is_legacy) check_mess = true; if(ctx.which_mode == eSpecCtx(spec.ex1a)) { if(ctx.which_mode <= eSpecCtx::COMBAT_MOVE) { *ctx.ret_a = bool(spec.ex1b); // Should block move? 1 = yes, 0 = no if(*ctx.ret_a) { if(ctx.which_mode == eSpecCtx::OUT_MOVE) ASB("Can't go here while outdoors."); else if(ctx.which_mode == eSpecCtx::TOWN_MOVE) ASB("Can't go here while in town mode."); else if(ctx.which_mode == eSpecCtx::COMBAT_MOVE) ASB("Can't go here during combat."); } } else if(ctx.which_mode == eSpecCtx::TARGET && spec.ex1b >= 0) { // TODO: I'm not quite sure if this covers every way of determining which spell was cast if(is_town() && int(town_spell) != spec.ex1b) break; if(is_combat() && int(spell_being_cast) != spec.ex1b) break; } ctx.next_spec = spec.ex1c; } break; case eSpecType::IF_LOOKING: if(ctx.which_mode == eSpecCtx::OUT_LOOK || ctx.which_mode == eSpecCtx::TOWN_LOOK) ctx.next_spec = spec.ex1c; break; case eSpecType::IF_IN_BOAT: if(univ.party.in_boat >= 0 && (spec.ex1b < 0 || spec.ex1b == univ.party.in_boat)) ctx.next_spec = spec.ex1c; break; case eSpecType::IF_ON_HORSE: if(univ.party.in_horse >= 0 && (spec.ex1b < 0 || spec.ex1b == univ.party.in_horse)) ctx.next_spec = spec.ex1c; break; default: showError("Special node type \"" + (*cur_node.type).name() + "\" is either miscategorized or unimplemented!"); break; } if(check_mess) { handle_message(ctx); } } void townmode_spec(const runtime_state& ctx) { static const char*const stairDlogs[8] = { "basic-stair-up", "basic-stair-down", "basic-slope-up", "basic-slope-down", "slimy-stair-up", "slimy-stair-down", "dark-slope-up", "dark-slope-down" }; bool check_mess = true; std::array strs; short r1; std::array buttons = {-1,-1,-1}; cSpecial spec = ctx.cur_spec, cur_node = ctx.cur_spec; location l; ter_num_t ter; cItem store_i; effect_pat_type pat; ctx.next_spec = cur_node.jumpto; l.x = spec.ex1a; l.y = spec.ex1b; if(is_out()) return; switch(cur_node.type) { case eSpecType::MAKE_TOWN_HOSTILE: if(spec.ex2a < 0 || spec.ex2a > 3){ showError("Invalid attitude (0-Friendly Docile, 1-Hostile A, 2-Friendly Will Fight, 3-Hostile B)."); break; } set_town_attitude(spec.ex1a,spec.ex1b,eAttitude(spec.ex2a)); break; case eSpecType::TOWN_MOVE_PARTY: if(is_combat()) { ASB("Not while in combat."); if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; ctx.next_spec = -1; check_mess = false; } else { // 1 no *ctx.ret_a = 1; if(ctx.which_mode == eSpecCtx::TALK || spec.ex2a == 0) teleport_party(spec.ex1a,spec.ex1b,1); else teleport_party(spec.ex1a,spec.ex1b,0); } *ctx.redraw = true; break; case eSpecType::TOWN_HIT_SPACE: if(ctx.which_mode == eSpecCtx::TALK) break; hit_space(l,spec.ex2a,(eDamageType) spec.ex2b,1,1); *ctx.redraw = true; break; case eSpecType::TOWN_EXPLODE_SPACE: if(ctx.which_mode == eSpecCtx::TALK) break; radius_damage(l,spec.pic, spec.ex2a, (eDamageType) spec.ex2b); *ctx.redraw = true; break; case eSpecType::TOWN_LOCK_SPACE: ter = coord_to_ter(spec.ex1a,spec.ex1b); if(univ.scenario.ter_types[ter].special == eTerSpec::LOCKABLE) alter_space(l.x,l.y,univ.scenario.ter_types[ter].flag1); *ctx.redraw = true; break; case eSpecType::TOWN_UNLOCK_SPACE: ter = coord_to_ter(spec.ex1a,spec.ex1b); if(univ.scenario.ter_types[ter].special == eTerSpec::UNLOCKABLE) alter_space(l.x,l.y,univ.scenario.ter_types[ter].flag1); *ctx.redraw = true; break; case eSpecType::TOWN_SFX_BURST: if(ctx.which_mode == eSpecCtx::TALK) break; if(spec.ex2b == 1) mondo_boom(l,spec.ex2a,spec.ex2c); else run_a_boom(l,spec.ex2a,0,0,spec.ex2c); break; case eSpecType::TOWN_CREATE_WANDERING: create_wand_monst(); *ctx.redraw = true; break; case eSpecType::TOWN_PLACE_MONST: place_monster(spec.ex2a,l,spec.ex2b); *ctx.redraw = true; break; case eSpecType::TOWN_DESTROY_MONST: if(spec.ex1a >= 0 && spec.ex1b >= 0) { iLiving* monst = univ.target_there(l, TARG_MONST); if(monst != nullptr) dynamic_cast(monst)->active = eCreatureStatus::DEAD; } *ctx.redraw = true; break; case eSpecType::TOWN_NUKE_MONSTS: for(short i = 0; i < univ.town.monst.size(); i++) if(univ.town.monst[i].is_alive() && (univ.town.monst[i].number == spec.ex1a || spec.ex1a == 0 || (spec.ex1a == -1 && univ.town.monst[i].is_friendly()) || (spec.ex1a == -2 && !univ.town.monst[i].is_friendly()))) { univ.town.monst[i].active = eCreatureStatus::DEAD; } *ctx.redraw = true; break; case eSpecType::TOWN_GENERIC_LEVER: if(ctx.which_mode != eSpecCtx::OUT_MOVE && ctx.which_mode != eSpecCtx::TOWN_MOVE && ctx.which_mode != eSpecCtx::COMBAT_MOVE && ctx.which_mode != eSpecCtx::OUT_LOOK && ctx.which_mode != eSpecCtx::TOWN_LOOK) { ASB("Can't use lever now."); check_mess = false; ctx.next_spec = -1; } else { if(handle_lever(loc(univ.party.get_ptr(10), univ.party.get_ptr(11)))) ctx.next_spec = spec.ex1b; } break; case eSpecType::TOWN_GENERIC_PORTAL: if(is_combat()) { ASB("Not while in combat."); if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; ctx.next_spec = -1; check_mess = false; } else if(ctx.which_mode != eSpecCtx::TOWN_MOVE && ctx.which_mode != eSpecCtx::TOWN_LOOK) { ASB("Can't teleport now."); if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; ctx.next_spec = -1; check_mess = false; } else if(cChoiceDlog("basic-portal",{"yes","no"}).show() == "yes") { *ctx.ret_a = 1; if(ctx.which_mode == eSpecCtx::TALK) teleport_party(spec.ex1a,spec.ex1b,1); else teleport_party(spec.ex1a,spec.ex1b,spec.ex2a); } break; case eSpecType::TOWN_GENERIC_BUTTON: if(cChoiceDlog("basic-button",{"yes","no"}).show() == "yes") ctx.next_spec = spec.ex1b; break; case eSpecType::TOWN_GENERIC_STAIR: check_mess = false; if(spec.ex2c != 1 && spec.ex2c != 2 && is_combat()) { ASB("Can't change level in combat."); if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; ctx.next_spec = -1; } else if(spec.ex2c != 2 && spec.ex2c != 3 && ctx.which_mode != eSpecCtx::TOWN_MOVE) { ASB("Can't change level now."); if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; ctx.next_spec = -1; } else { *ctx.ret_a = 1; if(spec.ex2b < 0) spec.ex2b = 0; if((spec.ex2b >= 8) || (cChoiceDlog(stairDlogs[spec.ex2b],{"climb","leave"}).show() == "climb")) { bool was_in_combat = false; short was_active; if(overall_mode == MODE_TALKING) end_talk_mode(); else if(is_combat()) { was_in_combat = true; if(which_combat_type == 0) { // outdoor combat ASB("Can't change level in combat."); ctx.next_spec = -1; break; } else { was_active = univ.cur_pc; eDirection dir = end_town_combat(); if(dir == DIR_HERE) { ASB("Can't change level now."); break; } univ.party.direction = dir; } } *ctx.ret_a = 1; change_level(spec.ex2a,l.x,l.y); if(was_in_combat) { start_town_combat(univ.party.direction); univ.cur_pc = was_active; } } else ctx.next_spec = -1; } break; case eSpecType::TOWN_LEVER: check_mess = false; if(spec.m1 < 0) break; if(ctx.which_mode != eSpecCtx::OUT_MOVE && ctx.which_mode != eSpecCtx::TOWN_MOVE && ctx.which_mode != eSpecCtx::COMBAT_MOVE && ctx.which_mode != eSpecCtx::OUT_LOOK && ctx.which_mode != eSpecCtx::TOWN_LOOK) { ASB("Can't use lever now."); check_mess = false; ctx.next_spec = -1; } else { univ.get_strs(strs,ctx.cur_spec_type, spec.m1); buttons[0] = 9; buttons[1] = 35; if(custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c) == 1) ctx.next_spec = -1; else { int x = univ.party.get_ptr(10), y = univ.party.get_ptr(11); ter = coord_to_ter(x, y); alter_space(x,y,univ.scenario.ter_types[ter].trans_to_what); ctx.next_spec = spec.ex1b; } } break; case eSpecType::TOWN_PORTAL: check_mess = false; if(spec.m1 < 0) break; if(is_combat()) { ASB("Not while in combat."); if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; ctx.next_spec = -1; check_mess = false; } else if(ctx.which_mode != eSpecCtx::TOWN_MOVE && ctx.which_mode != eSpecCtx::TOWN_LOOK) { ASB("Can't teleport now."); if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; ctx.next_spec = -1; check_mess = false; } else { univ.get_strs(strs, ctx.cur_spec_type,spec.m1); buttons[0] = 9; buttons[1] = 8; if(custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons, true, spec.ex1c, spec.ex2c) == 1) { ctx.next_spec = -1; if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; } else { *ctx.ret_a = 1; if(ctx.which_mode == eSpecCtx::TALK) teleport_party(spec.ex1a,spec.ex1b,1); else teleport_party(spec.ex1a,spec.ex1b,spec.ex2a); } } break; case eSpecType::TOWN_STAIR: check_mess = false; if((spec.m1 < 0) && (spec.ex2b != 1)) break; if(spec.ex2c != 1 && spec.ex2c != 2 && is_combat()) { ASB("Can't change level in combat."); if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; ctx.next_spec = -1; } else if(spec.ex2c != 2 && spec.ex2c != 3 && ctx.which_mode != eSpecCtx::TOWN_MOVE) { ASB("Can't change level now."); if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; ctx.next_spec = -1; } else { univ.get_strs(strs,ctx.cur_spec_type, spec.m1); buttons[0] = 20; buttons[1] = 24; int i = spec.ex2b == 1 ? 2 : custom_choice_dialog(strs, spec.pic, ePicType(spec.pictype), buttons); *ctx.ret_a = 1; if(i == 1) { ctx.next_spec = -1; } else { bool was_in_combat = false; short was_active; if(overall_mode == MODE_TALKING) end_talk_mode(); else if(is_combat()) { was_in_combat = true; if(which_combat_type == 0) { // outdoor combat ASB("Can't change level in combat."); ctx.next_spec = -1; break; } else { was_active = univ.cur_pc; univ.party.direction = end_town_combat(); } } *ctx.ret_a = 1; change_level(spec.ex2a,l.x,l.y); if(was_in_combat) { start_town_combat(univ.party.direction); univ.cur_pc = was_active; } } } break; case eSpecType::TOWN_RELOCATE: position_party(spec.ex1a,spec.ex1b,spec.ex2a,spec.ex2b); break; case eSpecType::TOWN_PLACE_ITEM: store_i = univ.scenario.get_stored_item(spec.ex2a); place_item(store_i,l,spec.ex2b); break; case eSpecType::TOWN_SPLIT_PARTY: if(ctx.which_mode == eSpecCtx::TALK) break; if(is_combat()) { ASB("Not while in combat."); if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; ctx.next_spec = -1; check_mess = false; break; } if(univ.party.is_split()) { ASB("Party is already split."); if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; ctx.next_spec = -1; check_mess = false; break; } r1 = select_pc(eSelectPC::ONLY_LIVING,"Which character goes?"); if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; if(r1 != 6) { univ.cur_pc = r1; ctx.next_spec = -1; if(!univ.party.start_split(spec.ex1a,spec.ex1b,spec.ex2a,r1)) ASB("Party already split!"); update_explored(univ.party.town_loc); center = univ.party.town_loc; } else check_mess = false; break; case eSpecType::TOWN_REUNITE_PARTY: if(is_combat()) { ASB("Not while in combat."); break; } if(ctx.which_mode == eSpecCtx::OUT_MOVE || ctx.which_mode == eSpecCtx::TOWN_MOVE || ctx.which_mode == eSpecCtx::COMBAT_MOVE) *ctx.ret_a = 1; ctx.next_spec = -1; check_mess = false; if(univ.party.end_split(spec.ex1a)) ASB("Party already together!"); else ASB("You are reunited."); if(spec.ex2a); // This means reunite the party by bringing the others to the current location rather than the reverse. else if(univ.party.left_in == size_t(-1) || univ.party.town_num == univ.party.left_in) { univ.party.town_loc = univ.party.left_at; update_explored(univ.party.town_loc); center = univ.party.town_loc; // Clear forcecage status for(int i = 0; i < 6; i++) univ.party[i].status[eStatus::FORCECAGE] = 0; } else change_level(univ.party.left_in, univ.party.left_at.x, univ.party.left_at.y); break; case eSpecType::TOWN_TIMER_START: univ.party.start_timer(spec.ex1a, spec.ex1b, eSpecCtxType::TOWN); break; // OBoE: Change town lighting case eSpecType::TOWN_CHANGE_LIGHTING: // Change global town lighting if(spec.ex1a >= 0 && spec.ex1a <= 3) { univ.town->lighting_type = (eLighting) spec.ex1a; draw_terrain(); } // Change party light level if(spec.ex2a > 0) { if(spec.ex2b == 0) increase_light(spec.ex2a); else increase_light(-spec.ex2a); } break; case eSpecType::TOWN_SET_ATTITUDE: if((spec.ex1a < 0) || (spec.ex1a > 59)){ showError("Tried to change the attitude of a nonexistent monster (should be 0...59)."); break; } if(spec.ex1b < 0 || spec.ex1b > 3){ showError("Invalid attitude (0-Friendly Docile, 1-Hostile A, 2-Friendly Will Fight, 3-Hostile B)."); break; } univ.town.monst[spec.ex1a].attitude = eAttitude(spec.ex1b); break; case eSpecType::TOWN_RUN_MISSILE: if(ctx.which_mode != eSpecCtx::TALK) { int i; if(iLiving* targ = univ.target_there(loc(spec.ex2a, spec.ex2b), TARG_MONST)) { cCreature* who = dynamic_cast(targ); i = 14 * who->x_width - 1; r1 = 18 * who->y_width - 1; } else i = r1 = 0; run_a_missile(l, loc(spec.ex2a, spec.ex2b), spec.pic, spec.ex1c, spec.ex2c, i, r1, 100); } break; case eSpecType::TOWN_BOOM_SPACE: // TODO: This should work, but does it need a bit of extra logic? if(ctx.which_mode == eSpecCtx::TALK) break; boom_space(l, 100, spec.ex2a, spec.ex2b, -spec.ex2c); break; case eSpecType::TOWN_MONST_ATTACK: // TODO: I'm not certain if this will work. if(ctx.which_mode != eSpecCtx::TALK) { int i = combat_posing_monster; if(l.y >= 0) { iLiving* who = univ.target_there(l); if(who != nullptr) combat_posing_monster = univ.get_target_i(*who); if(combat_posing_monster == 6) combat_posing_monster = -1; } else combat_posing_monster = spec.ex1a; if(combat_posing_monster < 0 || combat_posing_monster - 100 >= univ.town.monst.size()) { combat_posing_monster = i; break; } redraw_screen(REFRESH_TERRAIN); combat_posing_monster = i; } break; case eSpecType::TOWN_SET_CENTER: if(l.x >= 0 && l.y >= 0) center = l; else center = is_combat() ? univ.current_pc().combat_pos : univ.party.town_loc; start_cartoon(); redraw_screen(REFRESH_TERRAIN); break; case eSpecType::TOWN_LIFT_FOG: fog_lifted = spec.ex1a; redraw_screen(REFRESH_TERRAIN); break; case eSpecType::TOWN_START_TARGETING: ctx.next_spec = -1; if(spec.ex1a < 0 || spec.ex1a > 7) { showError("Invalid spell pattern (0 - 7)."); break; } if(spec.ex1c > 1 && !is_combat()) { add_string_to_buf(" Target: Only in combat"); break; } if(!is_combat()) start_town_targeting(eSpell::NONE, spec.jumpto, true, eSpellPat(spec.ex1a)); else if(spec.ex1c > 1) start_fancy_spell_targeting(eSpell::NONE, true, spec.ex1b, eSpellPat(spec.ex1a), spec.ex1c); else start_spell_targeting(eSpell::NONE, true, spec.ex1b, eSpellPat(spec.ex1a)); spell_caster = spec.jumpto; spec_target_type = ctx.cur_spec_type; spec_target_fail = spec.ex2a; spec_target_options = 0; if(spec.ex2b > 0) spec_target_options += 1; if(spec.ex2c > 0) spec_target_options += 20; else if(spec.ex2c == 0) spec_target_options += 10; break; case eSpecType::TOWN_SPELL_PAT_FIELD: if(spec.ex1c < -1 || spec.ex1c > 14) { showError("Invalid spell pattern (-1 - 14)."); break; } if(spec.ex2a == -1 && (spec.ex1c == PAT_PROT || (spec.ex1c == PAT_CURRENT && current_pat == cPattern::get_builtin(PAT_PROT).pattern))) ; // This means to use the default effect of Protective Circle else if((spec.ex2a < 1 || spec.ex2a == 9 || spec.ex2a > 24) && spec.ex2a != 32 && spec.ex2a != 33) { showError("Invalid field type (see docs)."); break; } if(spec.ex1c == -1) { pat = current_pat; } else if(spec.ex1c < PAT_WALL) { pat = cPattern::get_builtin(eSpellPat(spec.ex1c)).pattern; } else if(spec.ex1c < PAT_PROT) { pat = cPattern::get_builtin(PAT_WALL).patterns[spec.ex1c - PAT_WALL]; } else if(spec.ex1c == PAT_PROT) { pat = cPattern::get_builtin(PAT_PROT).pattern; } if(spec.ex2a == -1) place_spell_pattern(pat, l, 6); else place_spell_pattern(pat, l, eFieldType(spec.ex2a), 6); break; case eSpecType::TOWN_SPELL_PAT_BOOM: if(spec.ex1c < -1 || spec.ex1c > 14) { showError("Invalid spell pattern (-1 - 14)."); break; } if(spec.ex2a == -1 && (spec.ex1c == PAT_PROT || (spec.ex1c == PAT_CURRENT && current_pat == cPattern::get_builtin(PAT_PROT).pattern))) ; // This means to use the default effect of Protective Circle else if(spec.ex2a < 0 || spec.ex2a > 7) { showError("Invalid damage type (0 - 7)."); break; } if(spec.ex1c == -1) { pat = current_pat; } else if(spec.ex1c < PAT_WALL) { pat = cPattern::get_builtin(eSpellPat(spec.ex1c)).pattern; } else if(spec.ex1c < PAT_PROT) { pat = cPattern::get_builtin(PAT_WALL).patterns[spec.ex1c - PAT_WALL]; } else if(spec.ex1c == PAT_PROT) { pat = cPattern::get_builtin(PAT_PROT).pattern; } if(spec.ex2c) start_missile_anim(); if(spec.ex2a == -1) place_spell_pattern(pat, l, 6); else place_spell_pattern(pat, l, eDamageType(spec.ex2a), spec.ex2b, 6); if(spec.ex2c) { do_explosion_anim(0, 0); end_missile_anim(); } break; case eSpecType::TOWN_RELOCATE_CREATURE: if(spec.ex2b > 5) { showError("Invalid positioning mode (0-5)"); } else { int i = spec.ex2a < 0 ? univ.get_target_i(current_pc_picked_in_spec_enc(ctx)) : spec.ex2a; if(spec.ex2b == 5) { spec.ex2b = 0; if(i >= 100) { std::set checked; std::queue to_check; location cur_check = l; for(int tries = 0; tries < 100 && !monst_can_be_there(cur_check, i - 100); tries++) { for(int x = -1; x <= 1; x++) { for(int y = -1; y <= 1; y++) { if(x == 0 && y == 0) continue; location next(l.x+x,l.y+y); if(!univ.town.is_on_map(next.x, next.y)) continue; if(!checked.count(next)) to_check.push(next); checked.insert(cur_check); } } cur_check = to_check.front(); to_check.pop(); } if(monst_can_be_there(cur_check, i - 100)) l = cur_check; } } if(spec.ex2b > 1) { if(spec.ex2b <= 3) l.x *= -1; if(spec.ex2b >= 3) l.y *= -1; } if(i < 6) { start_cartoon(); if(spec.ex2b == 0) univ.party[i].combat_pos = l; else { univ.party[i].combat_pos.x += l.x; univ.party[i].combat_pos.y += l.y; } } else if(i >= 100) { i -= 100; if(spec.ex2b == 0) univ.town.monst[i].cur_loc = l; else { univ.town.monst[i].cur_loc.x += l.x; univ.town.monst[i].cur_loc.y += l.y; } } else { showError("Invalid positioning target!"); break; } redraw_screen(REFRESH_TERRAIN); if(spec.ex2c > 0) sf::sleep(sf::milliseconds(spec.ex2c)); *ctx.redraw = true; } break; case eSpecType::TOWN_PLACE_LABEL: check_mess = false; if(l.y < 0) { if(l.x < 0) l.x = univ.get_target_i(current_pc_picked_in_spec_enc(ctx)); if(l.x < 6) l = (is_combat() || cartoon_happening) ? univ.party[l.x].combat_pos : univ.party.town_loc; else if(l.x == 6) l = univ.party.town_loc; else if(l.x >= 100 && l.x - 100 < univ.town.monst.size()) l = univ.town.monst[l.x - 100].cur_loc; else { showError("Invalid label target!"); break; } } univ.get_strs(strs[0], strs[1], ctx.cur_spec_type, spec.m1, spec.m1); place_text_label(strs[0], l, spec.ex2a); redraw_screen(REFRESH_TERRAIN); if(spec.ex2b > 0) // TODO: Add preferences setting to increase this delay, for slow readers sf::sleep(sf::seconds(spec.ex2b)); break; default: showError("Special node type \"" + (*cur_node.type).name() + "\" is either miscategorized or unimplemented!"); break; } if(check_mess) { handle_message(ctx); } } void rect_spec(const runtime_state& ctx){ bool check_mess = true; cSpecial spec = ctx.cur_spec, cur_node = ctx.cur_spec; location l; ter_num_t ter; ctx.next_spec = cur_node.jumpto; *ctx.redraw = true; bool need_redraw_map=false; for(short i = spec.ex1b;i <= spec.ex2b;i++) for(short j = spec.ex1a; j <= spec.ex2a; j++) { l.x = i; l.y = j; // If pict non-zero, exclude rectangle interior if(spec.pic > 0 && i > spec.ex1b && i < spec.ex2b && j > spec.ex1a && j < spec.ex2a) continue; switch(cur_node.type) { case eSpecType::RECT_PLACE_FIELD: if(is_out()) return; if(!isValidField(spec.sd2, true)) { showError("Scenario tried to place an invalid field type (1...24)"); goto END; // Break out of the switch AND both loops, but still handle messages } if(spec.sd2 == FIELD_DISPEL || get_ran(1,1,100) <= spec.sd1) switch(eFieldType(spec.sd2)) { // These values are not allowed. case SPECIAL_EXPLORED: case SPECIAL_SPOT: case SPECIAL_ROAD: break; // Walls case WALL_FIRE: univ.town.set_fire_wall(i,j,true); break; case WALL_FORCE: univ.town.set_force_wall(i,j,true); break; case WALL_ICE: univ.town.set_ice_wall(i,j,true); break; case WALL_BLADES: univ.town.set_blade_wall(i,j,true); break; // Clouds case CLOUD_STINK: univ.town.set_scloud(i,j,true); break; case CLOUD_SLEEP: univ.town.set_sleep_cloud(i,j,true); break; // Advanced case FIELD_QUICKFIRE: univ.town.set_quickfire(i,j,true); break; case FIELD_ANTIMAGIC: univ.town.set_antimagic(i,j,true); break; case BARRIER_FIRE: univ.town.set_fire_barr(i,j,true); break; case BARRIER_FORCE: univ.town.set_force_barr(i,j,true); break; case BARRIER_CAGE: univ.town.set_force_cage(i,j,true); break; // Cleanse case FIELD_DISPEL: if(spec.sd1 == 0) dispel_fields(i,j,1); else dispel_fields(i,j,2); break; // Objects case FIELD_WEB: univ.town.set_web(i,j,true); break; case OBJECT_BARREL: univ.town.set_barrel(i,j,true); break; case OBJECT_CRATE: univ.town.set_crate(i,j,true); break; case OBJECT_BLOCK: univ.town.set_block(i,j,true); break; // Sfx case SFX_SMALL_BLOOD: univ.town.set_sm_blood(i,j,true); break; case SFX_MEDIUM_BLOOD: univ.town.set_med_blood(i,j,true); break; case SFX_LARGE_BLOOD: univ.town.set_lg_blood(i,j,true); break; case SFX_SMALL_SLIME: univ.town.set_sm_slime(i,j,true); break; case SFX_LARGE_SLIME: univ.town.set_lg_slime(i,j,true); break; case SFX_ASH: univ.town.set_ash(i,j,true); break; case SFX_BONES: univ.town.set_bones(i,j,true); break; case SFX_RUBBLE: univ.town.set_rubble(i,j,true); break; // Special value: Move Mountains! case FIELD_SMASH: crumble_wall(loc(i,j)); break; } break; case eSpecType::RECT_MOVE_ITEMS: { if(is_out()) return; bool isContainer = is_container(loc(spec.sd1,spec.sd2)); bool moveContainer = spec.pictype > 0; for(auto& item : univ.town.items) if(item.variety != eItemType::NO_ITEM && item.item_loc == l) { if(item.contained && !moveContainer) continue; item.item_loc.x = spec.sd1; item.item_loc.y = spec.sd2; if(isContainer && spec.m3 > 0) { item.contained = is_container(item.item_loc); if(univ.town.is_crate(spec.sd1,spec.sd2) || univ.town.is_barrel(spec.sd1,spec.sd2)) item.held = true; } } break; } case eSpecType::RECT_DESTROY_ITEMS: if(is_out()) return; for(short k = 0; k < univ.town.items.size(); k++) if(univ.town.items[k].variety != eItemType::NO_ITEM && univ.town.items[k].item_loc == l) { univ.town.items[k].variety = eItemType::NO_ITEM; } break; case eSpecType::RECT_CHANGE_TER: if(get_ran(1,1,100) <= spec.sd2){ alter_space(l.x,l.y,spec.sd1); *ctx.redraw = need_redraw_map= true; } break; case eSpecType::RECT_SWAP_TER: swap_ter(l.x,l.y,spec.sd1,spec.sd2); *ctx.redraw = need_redraw_map= true; break; case eSpecType::RECT_TRANS_TER: ter = coord_to_ter(i,j); alter_space(l.x,l.y,univ.scenario.ter_types[ter].trans_to_what); *ctx.redraw = need_redraw_map= true; break; case eSpecType::RECT_LOCK: ter = coord_to_ter(i,j); if(univ.scenario.ter_types[ter].special == eTerSpec::LOCKABLE){ alter_space(l.x,l.y,univ.scenario.ter_types[ter].flag1); *ctx.redraw = need_redraw_map= true; } break; case eSpecType::RECT_UNLOCK: ter = coord_to_ter(i,j); if(univ.scenario.ter_types[ter].special == eTerSpec::UNLOCKABLE){ alter_space(l.x,l.y,univ.scenario.ter_types[ter].flag1); *ctx.redraw = need_redraw_map= true; break; } break; case eSpecType::RECT_SET_EXPLORED: if(spec.sd1) make_explored(l.x, l.y); else take_explored(l.x, l.y); break; default: showError("Special node type \"" + (*cur_node.type).name() + "\" is either miscategorized or unimplemented!"); break; } } END: if (need_redraw_map) draw_map(true); if(check_mess) { handle_message(ctx); } } void outdoor_spec(const runtime_state& ctx){ bool check_mess = false; std::string str1, str2; cSpecial spec = ctx.cur_spec, cur_node = ctx.cur_spec; location l; ctx.next_spec = cur_node.jumpto; if(!is_out()) return; switch(cur_node.type) { case eSpecType::OUT_MAKE_WANDER: create_wand_monst(); *ctx.redraw = true; break; case eSpecType::OUT_PLACE_ENCOUNTER: if(spec.ex1a != minmax(0,3,spec.ex1a)) { showError("Special outdoor enc. is out of range. Must be 0-3."); //set_sd = false; } else { l = global_to_local(univ.party.out_loc); place_outd_wand_monst(l, univ.out->special_enc[spec.ex1a],true); check_mess = true; } break; case eSpecType::OUT_MOVE_PARTY: check_mess = true; out_move_party(spec.ex1a,spec.ex1b); *ctx.redraw = true; *ctx.ret_a = 1; break; case eSpecType::OUT_FORCE_TOWN: { l = {spec.ex2a, spec.ex2b}; int i = 0; if(l.x < 0 || l.y < 0 || l.x >= 64 || l.y >= 64) i = 9; else if(spec.ex1b == 0) i = 2; else if(spec.ex1b == 4) i = 0; else if(spec.ex1b < 4) i = 3; else i = 1; if(i == 9) force_town_enter(spec.ex1a, l); start_town_mode(spec.ex1a, i); } break; default: showError("Special node type \"" + (*cur_node.type).name() + "\" is either miscategorized or unimplemented!"); break; } if(check_mess) { handle_message(ctx); } } void setsd(short a, short b, short val) { if(!univ.party.sd_legit(a,b)) { showError("The scenario attempted to change an out of range Stuff Done flag."); return; } PSD[a][b] = val; } void handle_message(const runtime_state& ctx, const std::string& title, pic_num_t pic, ePicType pt) { if(pic == -1) { pic = univ.scenario.intro_pic; pt = PIC_SCEN; } eEncNoteType note_type; switch(ctx.cur_spec_type) { case eSpecCtxType::SCEN: note_type = NOTE_SCEN; break; case eSpecCtxType::OUTDOOR: note_type = NOTE_OUT; break; case eSpecCtxType::TOWN: note_type = NOTE_TOWN; break; } if(ctx.cur_spec.m1 < 0 && ctx.cur_spec.m2 < 0) return; if(ctx.which_mode == eSpecCtx::TALK) { *ctx.ret_a = ctx.cur_spec.m1; *ctx.ret_b = ctx.cur_spec.m2; return; } std::string str1, str2; univ.get_strs(str1, str2, ctx.cur_spec_type, ctx.cur_spec.m1, ctx.cur_spec.m2); if(str1.empty() && str2.empty()) return; std::string placename = is_out() ? univ.out->name : univ.town->name; cStrDlog display_strings(str1, str2, title, pic, pt, 0); display_strings.setSound(57); display_strings.setRecordHandler(cStringRecorder(ctx.cur_spec_type, note_type).string1(ctx.cur_spec.m1).string2(ctx.cur_spec.m2).at(placename)); display_strings.show(); } // This function sets/retrieves values to/from campaign flags void set_campaign_flag(short sdf_a, short sdf_b, short cpf_a, short cpf_b, short str, bool get_send) { // get_send = false: Send value in SDF to Campaign Flag // get_send = true: Retrieve value from Campaign Flag and put in SDF try { if(str >= 0 && str < univ.scenario.spec_strs.size()) { std::string cp_id = univ.scenario.spec_strs[str]; if(get_send) univ.party.stuff_done[sdf_a][sdf_b] = univ.cpn_flag(cpf_a, cpf_b, cp_id); else univ.cpn_flag(cpf_a, cpf_b, cp_id) = univ.party.stuff_done[sdf_a][sdf_b]; } else { if(get_send) univ.party.stuff_done[sdf_a][sdf_b] = univ.cpn_flag(cpf_a, cpf_b); else univ.cpn_flag(cpf_a, cpf_b) = univ.party.stuff_done[sdf_a][sdf_b]; } } catch(std::range_error x) { showError(x.what()); } } void resolve_pointers(cSpecial& cur_node) { if(cur_node.sd1 <= -10) cur_node.sd1 = univ.party.get_ptr(-cur_node.sd1); if(cur_node.sd2 <= -10) cur_node.sd2 = univ.party.get_ptr(-cur_node.sd2); if(cur_node.m1 <= -10) cur_node.m1 = univ.party.get_ptr(-cur_node.m1); if(cur_node.m2 <= -10) cur_node.m2 = univ.party.get_ptr(-cur_node.m2); if(cur_node.m3 <= -10) cur_node.m3 = univ.party.get_ptr(-cur_node.m3); if(cur_node.pic <= -10) cur_node.pic = univ.party.get_ptr(-cur_node.pic); if(cur_node.pictype <= -10) cur_node.pictype = univ.party.get_ptr(-cur_node.pictype); if(cur_node.ex1a <= -10) cur_node.ex1a = univ.party.get_ptr(-cur_node.ex1a); if(cur_node.ex1b <= -10) cur_node.ex1b = univ.party.get_ptr(-cur_node.ex1b); if(cur_node.ex1c <= -10) cur_node.ex1c = univ.party.get_ptr(-cur_node.ex1c); if(cur_node.ex2a <= -10) cur_node.ex2a = univ.party.get_ptr(-cur_node.ex2a); if(cur_node.ex2b <= -10) cur_node.ex2b = univ.party.get_ptr(-cur_node.ex2b); if(cur_node.ex2c <= -10) cur_node.ex2c = univ.party.get_ptr(-cur_node.ex2c); if(cur_node.jumpto <= -10) cur_node.jumpto = univ.party.get_ptr(-cur_node.jumpto); } iLiving& current_pc_picked_in_spec_enc(const runtime_state& ctx) { if(ctx.cur_target != nullptr) return *ctx.cur_target; switch(ctx.which_mode) { case eSpecCtx::OUT_MOVE: case eSpecCtx::TOWN_MOVE: case eSpecCtx::COMBAT_MOVE: case eSpecCtx::OUT_LOOK: case eSpecCtx::TOWN_LOOK: case eSpecCtx::ENTER_TOWN: case eSpecCtx::LEAVE_TOWN: case eSpecCtx::TALK: case eSpecCtx::USE_SPEC_ITEM: case eSpecCtx::TOWN_HOSTILE: case eSpecCtx::TOWN_TIMER: case eSpecCtx::SCEN_TIMER: case eSpecCtx::PARTY_TIMER: case eSpecCtx::OUTDOOR_ENC: case eSpecCtx::FLEE_ENCOUNTER: case eSpecCtx::WIN_ENCOUNTER: case eSpecCtx::DROP_ITEM: case eSpecCtx::SHOPPING: case eSpecCtx::STARTUP: // Default behaviour - select entire party, or active member if split or in combat // We also have a legacy flag - originally, it always defaulted to whole party if(is_combat() && !univ.scenario.is_legacy) return univ.current_pc(); else { if(univ.party.is_split() && ctx.cur_spec.type != eSpecType::AFFECT_DEADNESS) return univ.party.pc_present(); else return univ.party; } break; case eSpecCtx::KILL_MONST: case eSpecCtx::SEE_MONST: case eSpecCtx::MONST_SPEC_ABIL: case eSpecCtx::ATTACKED_MELEE: case eSpecCtx::ATTACKING_MELEE: case eSpecCtx::ATTACKED_RANGE: case eSpecCtx::ATTACKING_RANGE: if(univ.scenario.is_legacy) return univ.party; // The monster/PC on the trigger space is the target if(auto targ = univ.target_there(ctx.spec_loc)) return *targ; else return univ.party; case eSpecCtx::TARGET: case eSpecCtx::USE_SPACE: case eSpecCtx::HAIL: // If there's a monster on the space, select that as the target if(auto targ = univ.target_there(ctx.spec_loc, TARG_MONST)) return *targ; else return univ.party; } }