diff --git a/doc/editor/Contents.html b/doc/editor/Contents.html index 47502503f..3a9cec7a2 100644 --- a/doc/editor/Contents.html +++ b/doc/editor/Contents.html @@ -6,7 +6,6 @@

Blades Scenario Editor Instructions

-

Copyright 1998, Spiderweb Software, Inc.

This document contains all the information you need to play use the Blades of Exile Scenario Editor and make new scenarios for Blades of Exile!

This menu can also take you to the different sections of this document. To find the thing you're looking for, try the table of contents...

Table of Contents

@@ -98,15 +97,5 @@

Technical support

  • Blades of Exile Forum at Spiderweb Software
  • Blades of Exile IRC channel
  • -

    Spiderweb Software

    - diff --git a/pkg/credits/Funding.txt b/pkg/credits/Funding.txt index aca5479d8..a5ef8089d 100644 --- a/pkg/credits/Funding.txt +++ b/pkg/credits/Funding.txt @@ -10,6 +10,7 @@ Confirmed: - Bret Rodabaugh - Dylan Nugent - Evan Mulrooney +- Glen Chudley - Jake Harrelson - Jared Forcinito - Jeff Potter @@ -25,6 +26,7 @@ Confirmed: - K L - Laura Nelson - Mariann Krizsan +- Maryanne Wachter - Mike Lapinsky - Nathan Rickey - Nick Chaimov diff --git a/proj/xc12/BoE.xcodeproj/project.pbxproj b/proj/xc12/BoE.xcodeproj/project.pbxproj index c8a6baba7..6573d4b63 100755 --- a/proj/xc12/BoE.xcodeproj/project.pbxproj +++ b/proj/xc12/BoE.xcodeproj/project.pbxproj @@ -2516,7 +2516,7 @@ "-Wno-quoted-include-in-framework-header", "-Wno-shorten-64-to-32", "-Wno-comma", - "-Werror=implicit-fallthrough", + "-Wimplicit-fallthrough", ); }; name = Debug; @@ -2631,7 +2631,7 @@ "-Wno-quoted-include-in-framework-header", "-Wno-shorten-64-to-32", "-Wno-comma", - "-Werror=implicit-fallthrough", + "-Wimplicit-fallthrough", ); }; name = Release; diff --git a/rsrc/dialogs/about-boe.xml b/rsrc/dialogs/about-boe.xml index f290fba02..b703a0de1 100644 --- a/rsrc/dialogs/about-boe.xml +++ b/rsrc/dialogs/about-boe.xml @@ -18,11 +18,11 @@ ORIGINAL GAME:





    - OPEN SOURCE CREDITS:











































































    + OPEN SOURCE CREDITS:













































































    SCENARIO FIXES AND UPDATES:

    - +
    Concept, Design, Programming:
    Graphics:
    @@ -34,11 +34,11 @@ Graphics:






    Consulting:



    Testing and Troubleshooting:

    - Funding:













































    + Funding:

















































    Bandit Busywork:
    - +
    Jeff Vogel
    Andrew Hunter
    @@ -84,6 +84,7 @@ Bret Rodabaugh
    Dylan Nugent
    Evan Mulrooney
    + Glen Chudley
    Jake Harrelson
    Jared Forcinito
    Jeff Potter
    @@ -99,6 +100,7 @@ K L
    Laura Nelson
    Mariann Krizsan
    + Maryanne Wachter
    Mike Lapinsky
    Nathan Rickey
    Nick Chaimov
    diff --git a/rsrc/dialogs/get-items.xml b/rsrc/dialogs/get-items.xml index 53c053514..c5c26d2c1 100644 --- a/rsrc/dialogs/get-items.xml +++ b/rsrc/dialogs/get-items.xml @@ -4,6 +4,8 @@ + Get all gold diff --git a/rsrc/dialogs/pick-scenario.xml b/rsrc/dialogs/pick-scenario.xml index ead9e7e95..d5ecbc9e5 100644 --- a/rsrc/dialogs/pick-scenario.xml +++ b/rsrc/dialogs/pick-scenario.xml @@ -75,6 +75,7 @@ + - - Shift to this town's entrance in this outdoor section? + + Shift to this town's entrance in outdoor section {sec} at {loc} ({loc_str})? - diff --git a/rsrc/graphics/startanim.png b/rsrc/graphics/startanim.png index e4443d9a8..247de61eb 100644 Binary files a/rsrc/graphics/startanim.png and b/rsrc/graphics/startanim.png differ diff --git a/rsrc/strings/specials-text-town.txt b/rsrc/strings/specials-text-town.txt index db809e25a..b016130ca 100644 --- a/rsrc/strings/specials-text-town.txt +++ b/rsrc/strings/specials-text-town.txt @@ -353,7 +353,7 @@ Number of town to place party in Skip dialog and always change level? Trigger Limitations Special to Call in New Town -A dialog comes up text you supply (saying, perhaps, they've found a stairway). The dialog buttons are Climb and Leave. If the Leave button is pressed, the Jump To special is used, and the party is not allowed to enter the space. If the Climb button is pushed, the party is moved to another town. +A dialog comes up with text you supply (saying, perhaps, they've found a stairway). The dialog buttons are Climb and Leave. If the Leave button is pressed, the Jump To special is used, and the party is not allowed to enter the space. If the Climb button is pushed, the party is moved to another town. -------------------- Relocate Outdoors Unused diff --git a/src/dialogxml/dialogs/dialog.cpp b/src/dialogxml/dialogs/dialog.cpp index 728f3368e..c671becc2 100644 --- a/src/dialogxml/dialogs/dialog.cpp +++ b/src/dialogxml/dialogs/dialog.cpp @@ -795,6 +795,8 @@ void cDialog::handle_one_event(const sf::Event& currentEvent, cFramerateLimiter& } BOOST_FALLTHROUGH; case sf::Event::MouseMoved:{ + int x = currentEvent.mouseMove.x / get_ui_scale(); + int y = currentEvent.mouseMove.y / get_ui_scale(); // Did the window move, potentially dirtying the canvas below it? if(check_window_moved(win, winLastX, winLastY)) if (redraw_everything != NULL) @@ -802,7 +804,7 @@ void cDialog::handle_one_event(const sf::Event& currentEvent, cFramerateLimiter& bool inField = false; for(auto& ctrl : controls) { - if(ctrl.second->getType() == CTRL_FIELD && ctrl.second->getBounds().contains(currentEvent.mouseMove.x, currentEvent.mouseMove.y)) { + if(ctrl.second->getType() == CTRL_FIELD && ctrl.second->getBounds().contains(x, y)) { set_cursor(text_curs); inField = true; break; diff --git a/src/dialogxml/dialogs/strchoice.cpp b/src/dialogxml/dialogs/strchoice.cpp index be546087b..3cbb7d029 100644 --- a/src/dialogxml/dialogs/strchoice.cpp +++ b/src/dialogxml/dialogs/strchoice.cpp @@ -143,6 +143,8 @@ bool cStringChoice::onLeft(){ if(page == 0) page = lastPage(); else page--; fillPage(); + clearHighlights(); + highlightSearch(); return true; } @@ -154,6 +156,8 @@ bool cStringChoice::onRight(){ if(page == lastPage()) page = 0; else page++; fillPage(); + clearHighlights(); + highlightSearch(); return true; } @@ -244,6 +248,7 @@ void cStringChoice::clearHighlights() { bool cStringChoice::highlightSearch() { bool match_on_page = false; + if(search_str.empty()) return false; for(int offset = 0; offset < per_page; ++offset){ std::string led_id = "led" + std::to_string(offset + 1); diff --git a/src/dialogxml/widgets/field.cpp b/src/dialogxml/widgets/field.cpp index 6f18e3bf9..70a7315db 100644 --- a/src/dialogxml/widgets/field.cpp +++ b/src/dialogxml/widgets/field.cpp @@ -560,6 +560,7 @@ void cTextField::handleInput(cKey key, bool record) { if(current_action) history.add(current_action), current_action.reset(); if(haveSelection && !select) selectionPoint = insertionPoint = std::min(selectionPoint,insertionPoint); + if(snippets.empty()) break; if(snippets[ip_row].at.y == snippets[0].at.y) { key.k = key_top; if(select) key.mod += mod_shift; @@ -574,6 +575,7 @@ void cTextField::handleInput(cKey key, bool record) { if(current_action) history.add(current_action), current_action.reset(); if(haveSelection && !select) selectionPoint = insertionPoint = std::max(selectionPoint,insertionPoint); + if(snippets.empty()) break; if(snippets[ip_row].at.y == snippets.back().at.y) { key.k = key_bottom; if(select) key.mod += mod_shift; diff --git a/src/fileio/fileio_party.cpp b/src/fileio/fileio_party.cpp index f22a83fd8..716702146 100644 --- a/src/fileio/fileio_party.cpp +++ b/src/fileio/fileio_party.cpp @@ -284,6 +284,7 @@ bool load_party_v1(fs::path file_to_load, cUniverse& real_univ, bool town_restor univ.file = path; }else{ univ.party.scen_name = ""; + store_party.scen_name[0] = '\0'; } univ.party.import_legacy(store_party, univ); diff --git a/src/fileio/fileio_scen.cpp b/src/fileio/fileio_scen.cpp index 030c8e3e7..7aad56549 100644 --- a/src/fileio/fileio_scen.cpp +++ b/src/fileio/fileio_scen.cpp @@ -266,8 +266,11 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario return false; } port_item_list(&item_data); - scenario.import_legacy(temp_scenario); - scenario.import_legacy(item_data); + scenario.import_legacy(temp_scenario, load_type == eLoadScenario::ONLY_HEADER); + if(load_type == eLoadScenario::FULL){ + scenario.ter_types[23].fly_over = false; + scenario.import_legacy(item_data); + } // TODO: Consider skipping the fread and assignment when len is 0 scenario.special_items.resize(50); @@ -295,8 +298,6 @@ bool load_scenario_v1(fs::path file_to_load, cScenario& scenario, eLoadScenario fclose(file_id); - scenario.ter_types[23].fly_over = false; - scenario.scen_file = file_to_load; if(load_type == eLoadScenario::ONLY_HEADER) return true; load_spec_graphics_v1(scenario.scen_file); diff --git a/src/game/boe.actions.cpp b/src/game/boe.actions.cpp index 8bb52761f..12854267a 100644 --- a/src/game/boe.actions.cpp +++ b/src/game/boe.actions.cpp @@ -1409,6 +1409,11 @@ void handle_victory(bool force, bool record) { univ.exportGraphics(); univ.exportSummons(); univ.clear_stored_pcs(); + // Saved monsters are not valid now + for(auto& pop : univ.party.creature_save){ + pop.which_town = 200; + pop.clear(); + } reload_startup(); overall_mode = MODE_STARTUP; draw_startup(0); diff --git a/src/game/boe.dlgutil.cpp b/src/game/boe.dlgutil.cpp index c99ba0722..5fe3847e2 100644 --- a/src/game/boe.dlgutil.cpp +++ b/src/game/boe.dlgutil.cpp @@ -662,15 +662,16 @@ std::vector preset_words = { "Ask About...", }; +std::vector preset_word_locs = { + {4, 366}, {70, 366}, {136, 366}, + {4, 389}, {70, 389}, {121, 389}, + {210, 389}, {190, 366}, + {4, 343} +}; + static void reset_talk_words() { // first initialise talk_words here talk_words.clear(); - static const std::vector preset_word_locs = { - {4, 366}, {70, 366}, {136, 366}, - {4, 389}, {70, 389}, {121, 389}, - {210, 389}, {190, 366}, - {4, 343} - }; TextStyle style; style.font = FONT_DUNGEON; style.pointSize = TALK_WORD_SIZE; @@ -1798,8 +1799,16 @@ class cChooseScenario { for(auto& hdr : scen_headers){ // I just checked, and the scenario editor will let you name your scenario "" or " "! std::string name = name_alphabetical(hdr.name); - if(!name.empty()) - me[name.substr(0, 1)].show(); + if(!name.empty()){ + // Starts with a letter: + if(me.hasControl(name.substr(0, 1))){ + me[name.substr(0, 1)].show(); + } + // Starts with a digit: + else if(name[0] >= '0' && name[0] <= '9'){ + me["#"].show(); + } + } } } @@ -1882,6 +1891,7 @@ class cChooseScenario { stk.setPage(0); return true; }); + // Letter buttons scroll to an alphabetical position: for(int i = 0; i < 26; ++i){ std::string letter(1, (char)('a' + i)); me[letter].attachClickHandler([this](cDialog& me, std::string letter, eKeyMod) -> bool { @@ -1895,6 +1905,12 @@ class cChooseScenario { return true; }); } + // Number button scrolls to scenarios that start with symbols or digits: + me["#"].attachClickHandler([this](cDialog& me, std::string letter, eKeyMod) -> bool { + auto& stk = dynamic_cast(me["list"]); + stk.setPage(1); + return true; + }); put_scen_info(); diff --git a/src/game/boe.fileio.cpp b/src/game/boe.fileio.cpp index a53c900de..5d9c39216 100644 --- a/src/game/boe.fileio.cpp +++ b/src/game/boe.fileio.cpp @@ -341,6 +341,8 @@ std::string name_alphabetical(std::string a) { // The scenario editor will let you prepend whitespace to a scenario name :( boost::algorithm::trim_left(a); std::transform(a.begin(), a.end(), a.begin(), tolower); + // Some party makers start with the name of the corresponding scenario in quotes + if(a.substr(0,1) == "\"") a.erase(a.begin(), a.begin() + 1); if(a.substr(0,2) == "a ") a.erase(a.begin(), a.begin() + 2); else if(a.substr(0,4) == "the ") a.erase(a.begin(), a.begin() + 4); return a; @@ -359,11 +361,9 @@ std::vector build_scen_headers() { std::string scen_file; while(std::getline(in, scen_file)){ scen_header_type scen_head; - fs::path full_path; - for(fs::path scenDir : all_scen_dirs()){ - full_path = scenDir / scen_file; - if (fs::exists(full_path)) - break; + fs::path full_path = locate_scenario(scen_file, true); + if(full_path.empty()){ + LOG("Scenario missing! " + scen_file); } if(load_scenario_header(full_path, scen_head)){ scen_headers.push_back(scen_head); diff --git a/src/game/boe.graphics.cpp b/src/game/boe.graphics.cpp index d960a7bbd..ab3e06d1a 100644 --- a/src/game/boe.graphics.cpp +++ b/src/game/boe.graphics.cpp @@ -1186,17 +1186,21 @@ void place_trim(short q,short r,location where,ter_num_t ter_type) { } static void init_trim_mask(std::unique_ptr& mask, rectangle src_rect) { - sf::RenderTexture render; + static sf::RenderTexture render; + static bool init = false; + if(!init){ + render.create(28, 36); + init = true; + } rectangle dest_rect; dest_rect.top = src_rect.top % 36; dest_rect.bottom = (src_rect.bottom - 1) % 36 + 1; dest_rect.left = src_rect.left % 28; dest_rect.right = (src_rect.right - 1) % 28 + 1; - std::tie(dest_rect.top, dest_rect.bottom) = std::make_tuple(36 - dest_rect.top, 36 - dest_rect.bottom); render.create(28, 36); render.clear(sf::Color::White); rect_draw_some_item(*ResMgr::graphics.get("trim"), src_rect, render, dest_rect); - render.display(); + // render.display(); // Using it as a mask, we don't need to flip mask.reset(new sf::Texture); mask->create(28, 36); mask->update(render.getTexture().copyToImage()); @@ -1246,6 +1250,13 @@ void draw_trim(short q,short r,short which_trim,ter_num_t ground_ter) { } sf::Color test_color = {0,0,0}, store_color; + location targ; + targ.x = q; + targ.y = r; + if(supressing_some_spaces && (targ != ok_space[0]) && (targ != ok_space[1]) && + (targ != ok_space[2]) && (targ != ok_space[3])) + return; + unsigned short pic = univ.scenario.ter_types[ground_ter].picture; if(pic < 960){ int which_sheet = pic / 50; @@ -1368,67 +1379,6 @@ void place_road(short q,short r,location where,bool here) { to_rect.offset(13 + q * 28,13 + r * 36); rect_draw_some_item (roads_gworld, road_rects[0], terrain_screen_gworld(), to_rect); } - }else{ - // TODO: I suspect this branch is now irrelevant. - ter_num_t ter = coord_to_ter(where.x, where.y); - ter_num_t ref = coord_to_ter(where.x,where.y); - bool horz = false, vert = false; - eTrimType trim = eTrimType::NONE, vertTrim = eTrimType::NONE; - if(ref < univ.scenario.ter_types.size()) { - trim = univ.scenario.ter_types[ref].trim_type; - } - if(ter < univ.scenario.ter_types.size()) { - vertTrim = univ.scenario.ter_types[ter].trim_type; - } - if(where.y > 0) - ter = coord_to_ter(where.x,where.y - 1); - if((where.y == 0) || connect_roads(ter)) - vert = can_build_roads_on(ref); - else if((vertTrim == eTrimType::S && trim == eTrimType::N) || (vertTrim == eTrimType::N && trim == eTrimType::S)) - vert = can_build_roads_on(ref); - - if(((is_out()) && (where.x < 96)) || (!(is_out()) && (where.x < univ.town->max_dim - 1))) - ter = coord_to_ter(where.x + 1,where.y); - eTrimType horzTrim = univ.scenario.ter_types[ter].trim_type; - if(((is_out()) && (where.x == 96)) || (!(is_out()) && (where.x == univ.town->max_dim - 1)) - || connect_roads(ter)) - horz = can_build_roads_on(ref); - else if((horzTrim == eTrimType::W && trim == eTrimType::E) || (horzTrim == eTrimType::E && trim == eTrimType::W)) - horz = can_build_roads_on(ref); - - if(vert){ - if(((is_out()) && (where.y < 96)) || (!(is_out()) && (where.y < univ.town->max_dim - 1))) - ter = coord_to_ter(where.x,where.y + 1); - eTrimType vertTrim = univ.scenario.ter_types[ter].trim_type; - if(((is_out()) && (where.y == 96)) || (!(is_out()) && (where.y == univ.town->max_dim - 1)) - || connect_roads(ter)) - vert = can_build_roads_on(ref); - else if((vertTrim == eTrimType::S && trim == eTrimType::N) || (vertTrim == eTrimType::N && trim == eTrimType::S)) - vert = can_build_roads_on(ref); - else vert = false; - } - - if(horz){ - if(where.x > 0) - ter = coord_to_ter(where.x - 1,where.y); - eTrimType horzTrim = univ.scenario.ter_types[ter].trim_type; - if((where.x == 0) || connect_roads(ter)) - horz = can_build_roads_on(ref); - else if((horzTrim == eTrimType::W && trim == eTrimType::E) || (horzTrim == eTrimType::E && trim == eTrimType::W)) - horz = can_build_roads_on(ref); - else horz = false; - } - - if(horz){ - to_rect = road_dest_rects[5]; - to_rect.offset(13 + q * 28,13 + r * 36); - rect_draw_some_item (roads_gworld, road_rects[2], terrain_screen_gworld(), to_rect); - } - if(vert){ - to_rect = road_dest_rects[4]; - to_rect.offset(13 + q * 28,13 + r * 36); - rect_draw_some_item (roads_gworld, road_rects[3], terrain_screen_gworld(), to_rect); - } } } diff --git a/src/game/boe.graphutil.cpp b/src/game/boe.graphutil.cpp index 1b432dc95..619d28b08 100644 --- a/src/game/boe.graphutil.cpp +++ b/src/game/boe.graphutil.cpp @@ -134,7 +134,7 @@ void draw_monsters() { for(short k = 0; k < width * height; k++) { std::shared_ptr src_gw; graf_pos_ref(src_gw, source_rect) = spec_scen_g.find_graphic(picture_wanted % 1000 + - ((enc.direction < 4) ? 0 : (width * height)) + k); + ((enc.direction >= 4) ? 0 : (width * height)) + k); to_rect = monst_rects[(width - 1) * 2 + height - 1][k]; to_rect.offset(13 + 28 * where_draw.x,13 + 36 * where_draw.y); rect_draw_some_item(*src_gw, source_rect, terrain_screen_gworld(),to_rect, sf::BlendAlpha); @@ -142,7 +142,7 @@ void draw_monsters() { } if(picture_wanted < 1000) { for(short k = 0; k < width * height; k++) { - source_rect = get_monster_template_rect(picture_wanted,(enc.direction < 4) ? 0 : 1,k); + source_rect = get_monster_template_rect(picture_wanted,(enc.direction >= 4) ? 0 : 1,k); to_rect = monst_rects[(width - 1) * 2 + height - 1][k]; to_rect.offset(13 + 28 * where_draw.x,13 + 36 * where_draw.y); int which_sheet = m_pic_index[picture_wanted].i / 20; @@ -192,7 +192,7 @@ void draw_monsters() { Draw_Some_Item(*src_gw, source_rect, terrain_screen_gworld(), store_loc, 1, 0); } else { pic_num_t this_monst = monst.picture_num; - int pic_mode = (monst.direction) < 4 ? 0 : 1; + int pic_mode = (monst.direction) >= 4 ? 0 : 1; pic_mode += (combat_posing_monster == i + 100) ? 10 : 0; source_rect = get_monster_template_rect(this_monst, pic_mode, k); int which_sheet = (m_pic_index[this_monst].i+k) / 20; @@ -448,7 +448,7 @@ void draw_party_symbol(location center) { return; if(!univ.party.is_alive()) return; - if((is_town()) && (univ.party.town_loc.x > 70)) + if((is_town()) && (univ.party.town_loc.x > (univ.scenario.is_legacy ? 70 : LOC_UNUSED))) return; if(is_town() && center != univ.party.town_loc) { diff --git a/src/game/boe.items.cpp b/src/game/boe.items.cpp index 6e59dfe43..9c024364d 100644 --- a/src/game/boe.items.cpp +++ b/src/game/boe.items.cpp @@ -396,6 +396,16 @@ static void put_item_graphics(cDialog& me, size_t& first_item_shown, short& curr me["down"].hide(); else me["down"].show(); + me["gold"].hide(); + me["gold-label"].hide(); + for(cItem* item : item_array){ + if(item->variety == eItemType::GOLD && !item->property){ + me["gold"].show(); + me["gold-label"].show(); + break; + } + } + for(short i = 0; i < ITEMS_IN_WINDOW; i++) { std::ostringstream sout; sout << "item" << i + 1; @@ -459,7 +469,22 @@ static bool display_item_event_filter(cDialog& me, std::string id, size_t& first first_item_shown += ITEMS_IN_WINDOW; put_item_graphics(me, first_item_shown, current_getting_pc, item_array); } - } else if(id.substr(0,2) == "pc") { + } else if(id == "gold"){ + // Get all gold + for(int i = item_array.size() - 1; i >= 0; --i){ + item = *item_array[i]; + if(item.variety != eItemType::GOLD || item.property) continue; + if(item.item_level > 3000) + item.item_level = 3000; + set_item_flag(&item); + give_gold(item.item_level,false); + *item_array[i] = cItem(); + item_array.erase(item_array.begin() + i); + } + play_sound(39); // formerly force_play_sound + put_item_graphics(me, first_item_shown, current_getting_pc, item_array); + } + else if(id.substr(0,2) == "pc") { current_getting_pc = id[2] - '1'; put_item_graphics(me, first_item_shown, current_getting_pc, item_array); } else { @@ -560,7 +585,7 @@ bool show_get_items(std::string titleText, std::vector& itemRefs, short cDialog itemDialog(*ResMgr::dialogs.get("get-items")); auto handler = std::bind(display_item_event_filter, _1, _2, std::ref(first_item), std::ref(pc_getting), std::ref(itemRefs), overload); - itemDialog.attachClickHandlers(handler, {"done", "up", "down"}); + itemDialog.attachClickHandlers(handler, {"done", "up", "down", "gold"}); itemDialog.attachClickHandlers(handler, {"pc1", "pc2", "pc3", "pc4", "pc5", "pc6"}); itemDialog.setResult(false); cTextMsg& title = dynamic_cast(itemDialog["title"]); diff --git a/src/game/boe.locutils.cpp b/src/game/boe.locutils.cpp index 259a4d3fd..c04c1a30d 100644 --- a/src/game/boe.locutils.cpp +++ b/src/game/boe.locutils.cpp @@ -138,7 +138,9 @@ bool loc_off_world(location p1) { } bool loc_off_act_area(location p1) { - if((p1.x > univ.town->in_town_rect.left) && (p1.x < univ.town->in_town_rect.right) && + if(is_out() && univ.out->is_on_map(p1)) + return false; + else if((p1.x > univ.town->in_town_rect.left) && (p1.x < univ.town->in_town_rect.right) && (p1.y > univ.town->in_town_rect.top) && (p1.y < univ.town->in_town_rect.bottom)) return false; return true; diff --git a/src/game/boe.main.cpp b/src/game/boe.main.cpp index 2a5142992..a6b83bc05 100644 --- a/src/game/boe.main.cpp +++ b/src/game/boe.main.cpp @@ -128,7 +128,8 @@ std::map> feature_flags = { {"store-spell-target", {"fixed"}}, {"store-spell-caster", {"fixed"}}, // Game balance - {"magic-resistance", {"fixed"}} // Resist Magic used to not help with magic damage! + {"magic-resistance", {"fixed"}}, // Resist Magic used to not help with magic damage! + {"door-town-difficulty", {"fixed"}} }; struct cParseEntrance { diff --git a/src/game/boe.monster.cpp b/src/game/boe.monster.cpp index 5db2d17b2..8e1d0e61e 100644 --- a/src/game/boe.monster.cpp +++ b/src/game/boe.monster.cpp @@ -341,7 +341,7 @@ bool monst_hate_spot(short which_m,location *good_loc) { hate_spot = true; } if(hate_spot) { - prospect = find_clear_spot(loc,1); + prospect = find_clear_spot(loc,1,univ.town.monst[which_m].x_width,univ.town.monst[which_m].y_width); if(prospect.x > 0) { *good_loc = prospect; return true; @@ -708,10 +708,12 @@ bool try_move(short i,location start,short x,short y) { } bool combat_move_monster(short which,location destination) { - if(!monst_can_be_there(destination,which)) + if(!monst_can_be_there(destination,which)){ return false; - else if(!monst_check_special_terrain(destination,2,which)) + } + else if(!monst_check_special_terrain(destination,2,which)){ return false; + } else { univ.town.monst[which].direction = set_direction(univ.town.monst[which].cur_loc, destination); univ.town.monst[which].cur_loc = destination; @@ -728,12 +730,26 @@ bool combat_move_monster(short which,location destination) { // Looks at all spaces within 2, looking for a spot which is clear of nastiness and beings // returns {0,0} if none found // TODO: NO WAIT IT DOESN'T LOOK AT ALL SPACES!!! -// TODO: THIS MAKES NO ADJUSTMENTS FOR BIG MONSTERS!!! //mode; // 0 - normal 1 - prefer adjacent space -location find_clear_spot(location from_where,short mode) { +location find_clear_spot(location from_where,short mode,short x_width,short y_width) { location loc,store_loc; short num_tries = 0,r1; - + auto is_clear = [from_where](location loc) -> bool { + return !loc_off_act_area(loc) && !is_blocked(loc) + && can_see_light(from_where,loc,combat_obscurity) == 0 + && (!is_combat() || univ.target_there(loc,TARG_PC) == nullptr) + && (!(is_town()) || (loc != univ.party.town_loc)) + && (!univ.town.is_summon_safe(loc.x, loc.y)); + }; + auto all_is_clear = [from_where, x_width, y_width, is_clear](location loc) -> bool { + for(int x = 0; x < x_width; ++x){ + for(int y = 0; y < y_width; ++y){ + if(!is_clear({loc.x + x, loc.y + y})) + return false; + } + } + return true; + }; while(num_tries < 75) { num_tries++; loc = from_where; @@ -741,11 +757,7 @@ location find_clear_spot(location from_where,short mode) { loc.x = loc.x + r1; r1 = get_ran(1,-2,2); loc.y = loc.y + r1; - if(!loc_off_act_area(loc) && !is_blocked(loc) - && can_see_light(from_where,loc,combat_obscurity) == 0 - && (!is_combat() || univ.target_there(loc,TARG_PC) == nullptr) - && (!(is_town()) || (loc != univ.party.town_loc)) - && (!univ.town.is_summon_safe(loc.x, loc.y))) { + if(all_is_clear(loc)) { if((mode == 0) || ((mode == 1) && (adjacent(from_where,loc)))) return loc; else store_loc = loc; @@ -810,58 +822,65 @@ void monst_inflict_fields(short which_monst) { which_m = &univ.town.monst[which_monst]; bool have_radiate = which_m->abil[eMonstAbil::RADIATE].active; eFieldType which_radiate = which_m->abil[eMonstAbil::RADIATE].radiate.type; + // Judgment call: big monsters should only get damaged once per damage type if they're on + // multiple of the same field. (Except webs.) + bool quickfire = false; + bool blade_wall = false; + bool force_wall = false; + bool sleep_cloud = false; + bool ice_wall = false; + bool stink_cloud = false; + bool fire_wall = false; for(short i = 0; i < univ.town.monst[which_monst].x_width; i++) for(short j = 0; j < univ.town.monst[which_monst].y_width; j++) if(univ.town.monst[which_monst].is_alive()) { where_check.x = univ.town.monst[which_monst].cur_loc.x + i; where_check.y = univ.town.monst[which_monst].cur_loc.y + j; - // TODO: If the goal is to damage the monster by any fields it's on, why all the break statements? - if(univ.town.is_quickfire(where_check.x,where_check.y)) { + if(!quickfire && univ.town.is_quickfire(where_check.x,where_check.y)) { + quickfire = true; r1 = get_ran(2,1,8); damage_monst(*which_m,7,r1,eDamageType::FIRE); - break; } - if(univ.town.is_blade_wall(where_check.x,where_check.y)) { + if(!blade_wall && univ.town.is_blade_wall(where_check.x,where_check.y)) { + blade_wall = true; r1 = get_ran(6,1,8); - if(have_radiate && which_radiate != eFieldType::WALL_BLADES) + if(!have_radiate || which_radiate != eFieldType::WALL_BLADES) damage_monst(*which_m,7,r1,eDamageType::WEAPON); - break; } - if(univ.town.is_force_wall(where_check.x,where_check.y)) { + if(!force_wall && univ.town.is_force_wall(where_check.x,where_check.y)) { + force_wall = true; r1 = get_ran(3,1,6); - if(have_radiate && which_radiate != eFieldType::WALL_FORCE) + if(!have_radiate || which_radiate != eFieldType::WALL_FORCE) damage_monst(*which_m,7,r1,eDamageType::MAGIC); - break; } - if(univ.town.is_sleep_cloud(where_check.x,where_check.y)) { - if(have_radiate && which_radiate != eFieldType::CLOUD_SLEEP) + if(!sleep_cloud && univ.town.is_sleep_cloud(where_check.x,where_check.y)) { + sleep_cloud = true; + if(!have_radiate || which_radiate != eFieldType::CLOUD_SLEEP) which_m->sleep(eStatus::ASLEEP,3,0); - break; } - if(univ.town.is_ice_wall(where_check.x,where_check.y)) { + if(!ice_wall && univ.town.is_ice_wall(where_check.x,where_check.y)) { + ice_wall = true; r1 = get_ran(3,1,6); - if(have_radiate && which_radiate != eFieldType::WALL_ICE) + if(!have_radiate || which_radiate != eFieldType::WALL_ICE) damage_monst(*which_m,7,r1,eDamageType::COLD); - break; } - if(univ.town.is_scloud(where_check.x,where_check.y)) { + if(!stink_cloud && univ.town.is_scloud(where_check.x,where_check.y)) { + stink_cloud = true; r1 = get_ran(1,2,3); - if(have_radiate && which_radiate != eFieldType::CLOUD_STINK) + if(!have_radiate || which_radiate != eFieldType::CLOUD_STINK) which_m->curse(r1); - break; } if(univ.town.is_web(where_check.x,where_check.y) && which_m->m_type != eRace::BUG) { which_m->spell_note(eSpellNote::WEBBED); r1 = get_ran(1,2,3); which_m->web(r1); univ.town.set_web(where_check.x,where_check.y,false); - break; } - if(univ.town.is_fire_wall(where_check.x,where_check.y)) { + if(!fire_wall && univ.town.is_fire_wall(where_check.x,where_check.y)) { + fire_wall = true; r1 = get_ran(2,1,6); - if(have_radiate && which_radiate != eFieldType::WALL_FIRE) + if(!have_radiate || which_radiate != eFieldType::WALL_FIRE) damage_monst(*which_m,7,r1,eDamageType::FIRE); - break; } if(univ.town.is_force_cage(where_check.x,where_check.y)) process_force_cage(where_check, univ.get_target_i(*which_m)); @@ -1193,7 +1212,7 @@ void activate_monsters(short code,short /*attitude*/) { if(code == 0) return; for(short i = 0; i < univ.town->creatures.size(); i++) - if(univ.town->creatures[i].spec_enc_code == code) { + if(univ.town->creatures[i].number > 0 && univ.town->creatures[i].spec_enc_code == code) { cTownperson& monst = univ.town->creatures[i]; univ.town.monst.assign(i, monst, univ.scenario.scen_monsters[monst.number], univ.party.easy_mode, univ.difficulty_adjust()); univ.town.monst[i].spec_enc_code = 0; diff --git a/src/game/boe.monster.hpp b/src/game/boe.monster.hpp index be3fb0656..8b68f8a2f 100644 --- a/src/game/boe.monster.hpp +++ b/src/game/boe.monster.hpp @@ -24,7 +24,7 @@ bool seek_party(short i,location l1,location l2); bool flee_party(short i,location l1,location l2); bool try_move(short i,location start,short x,short y); bool combat_move_monster(short which,location destination); -location find_clear_spot(location from_where,short mode); +location find_clear_spot(location from_where,short mode,short x_width = 1, short y_width = 1); location random_shift(location start); bool outdoor_move_monster(short num,location dest); bool town_move_monster(short num,location dest); diff --git a/src/game/boe.newgraph.cpp b/src/game/boe.newgraph.cpp index f192cf752..5db0708a2 100644 --- a/src/game/boe.newgraph.cpp +++ b/src/game/boe.newgraph.cpp @@ -35,6 +35,7 @@ #include "replay.hpp" #include #include +#include short monsters_faces[190] = { 0,1,2,3,4,5,6,7,8,9, @@ -127,10 +128,7 @@ terrain_screen_rects_t terrain_screen_rects() { to.offset(current_terrain_ul); rectangle in_frame = to; - in_frame.top += 11; - in_frame.left += 11; - in_frame.bottom -= 11; - in_frame.right -= 11; + in_frame.inset(14, 14); return {from, to, in_frame}; } @@ -360,10 +358,20 @@ void do_missile_anim(short num_steps,location missile_origin,short sound_num) { return; } - // Eliminate missiles traveling 0 distance + bool can_see_origin = party_can_see(missile_origin) < 6; + bool missile_flying = false; for(short i = 0; i < 30; i++) { - if((store_missiles[i].missile_type >= 0) && (missile_origin == store_missiles[i].dest)) - store_missiles[i].missile_type = -1; + if((store_missiles[i].missile_type >= 0)){ + // Eliminate missiles traveling 0 distance + if(missile_origin == store_missiles[i].dest){ + store_missiles[i].missile_type = -1; + } + // Eliminate missiles whose whole arc is out of view + if(!can_see_origin && party_can_see(store_missiles[i].dest) == 6){ + store_missiles[i].missile_type = -1; + missile_flying = true; + } + } } std::vector missile_targets; @@ -376,7 +384,11 @@ void do_missile_anim(short num_steps,location missile_origin,short sound_num) { missile_targets.push_back(store_missiles[i].dest); } - if(missile_targets.empty()) return; + if(missile_targets.empty()){ + if(missile_flying) + play_sound(-1 * sound_num); + return; + } if(missile_targets.size() == 1){ tracking_missile = missile_indices[0]; @@ -387,9 +399,14 @@ void do_missile_anim(short num_steps,location missile_origin,short sound_num) { tracking_missile = missile_indices[closest_point_idx(missile_targets, camera_dest)]; } - // Start the camera as close as possible to containing the origin and the camera destination - // on the same screen - center = between_anchor_points(missile_origin, camera_dest); + if(can_see_origin){ + // Start the camera as close as possible to containing the origin and the camera destination + // on the same screen + center = between_anchor_points(missile_origin, camera_dest); + }else{ + // Can't see the origin, so showing the whole flight path when we don't have to is kind of a spoiler. + center = camera_dest; + } // make terrain_template contain current terrain all nicely draw_terrain(1); @@ -576,7 +593,7 @@ void do_explosion_anim(short /*sound_num*/,short special_draw, short snd) { draw_terrain(1); if(special_draw != 2) { auto ter_rects = terrain_screen_rects(); - rect_draw_some_item(terrain_screen_gworld().getTexture(),ter_rects.from,mainPtr(),ter_rects.to); + rect_draw_some_item(terrain_screen_gworld().getTexture(),ter_rects.from,mainPtr(),ter_rects.to, ter_rects.in_frame); } TextStyle style; @@ -995,6 +1012,7 @@ void place_talk_str(std::string str_to_place,std::string str_to_place2,short col // First determine the offsets of clickable words. // The added spaces ensure that end-of-word boundaries are found std::string str = str_to_place + " |" + str_to_place2 + " "; + std::vector hilites; std::vector nodes; int wordStart = 0, wordEnd = 0; @@ -1014,6 +1032,16 @@ void place_talk_str(std::string str_to_place,std::string str_to_place2,short col } } + // If the text will overflow onto the preset talk words, shrink it + extern std::vector preset_word_locs; + auto break_info = calculate_line_wrapping(word_place_rect, str, style); + short lines = break_info.size(); + short height = lines * (style.lineHeight+1); + if(height >= word_place_rect.height()){ + short overflow = height - preset_word_locs.back().y; + style.lineHeight -= ceil(overflow / (float) lines); + } + std::vector word_rects = draw_string_hilite(talk_gworld(), word_place_rect, str, style, hilites, color ? CUSTOM_WORD_ON : CUSTOM_WORD_OFF); if(!talk_end_forced) { diff --git a/src/game/boe.party.cpp b/src/game/boe.party.cpp index 7c4bf839e..aff1bc34b 100644 --- a/src/game/boe.party.cpp +++ b/src/game/boe.party.cpp @@ -1385,10 +1385,10 @@ void cast_town_spell(location where) { case eSpell::UNLOCK: // TODO: Is the unlock spell supposed to have a max range? if(univ.scenario.ter_types[ter].special == eTerSpec::UNLOCKABLE){ - if(univ.scenario.ter_types[ter].flag2 == 10) + if(univ.scenario.ter_types[ter].flag2 == 10){ r1 = 10000; - else{ - r1 = get_ran(1,1,100) - 5 * adj + 5 * univ.town->difficulty; + }else{ + r1 = get_ran(1,1,100) - 5 * adj + 5 * univ.town.door_diff_adjust(); r1 += univ.scenario.ter_types[ter].flag2 * 7; } if(r1 < (135 - combat_percent[min(19,level)])) { diff --git a/src/game/boe.specials.cpp b/src/game/boe.specials.cpp index 62d80e8a4..22078abdc 100644 --- a/src/game/boe.specials.cpp +++ b/src/game/boe.specials.cpp @@ -1937,7 +1937,6 @@ void special_increase_age(long length, bool 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) @@ -1955,7 +1954,6 @@ void special_increase_age(long length, bool 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) @@ -1971,6 +1969,7 @@ void special_increase_age(long length, bool queue) { 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); + // Party timers are triggered by special nodes and only run once! univ.party.party_event_timers[i].time = 0; univ.party.party_event_timers[i].node = -1; stat_area = true; @@ -1987,8 +1986,8 @@ void special_increase_age(long length, bool queue) { draw_terrain(0); } -void queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, location spec_loc) { - if(spec < 0) return; +bool queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, location spec_loc) { + if(spec < 0) return false; pending_special_type queued_special; queued_special.spec = spec; queued_special.where = spec_loc; @@ -2000,6 +1999,7 @@ void queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, loca run_special(queued_special, nullptr, nullptr, nullptr); else special_queue.push(queued_special); + return true; } void run_special(pending_special_type spec, short* a, short* b, bool* redraw) { @@ -2009,6 +2009,8 @@ void run_special(pending_special_type spec, short* a, short* b, bool* redraw) { univ.party.age = std::max(univ.party.age, store_time); } +extern bool need_enter_town_autosave; + // 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) @@ -2166,6 +2168,10 @@ void run_special(eSpecCtx which_mode, eSpecCtxType which_type, spec_num_t start_ erase_out_specials(); else erase_town_specials(); special_in_progress = false; + if(which_mode == eSpecCtx::ENTER_TOWN && need_enter_town_autosave){ + try_auto_save("EnterTown"); + need_enter_town_autosave = false; + } // TODO: Should find a way to do this that doesn't risk stack overflow if(ctx.next_spec == -1 && !special_queue.empty()) { diff --git a/src/game/boe.specials.hpp b/src/game/boe.specials.hpp index 32523b9f3..203f35d37 100644 --- a/src/game/boe.specials.hpp +++ b/src/game/boe.specials.hpp @@ -20,7 +20,7 @@ void teleport_party(short x,short y,short mode); bool run_stone_circle(short which); void change_level(short town_num,short x,short y); void push_things(); -void queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, location spec_loc); +bool queue_special(eSpecCtx mode, eSpecCtxType which_type, spec_num_t spec, location spec_loc); void run_special(eSpecCtx which_mode, eSpecCtxType which_type, spec_num_t start_spec, location spec_loc, short* a = nullptr, short* b = nullptr, bool* redraw = nullptr); void run_special(pending_special_type spec, short* a, short* b, bool* redraw); diff --git a/src/game/boe.text.cpp b/src/game/boe.text.cpp index 7b0e35a6d..a1ea6e268 100644 --- a/src/game/boe.text.cpp +++ b/src/game/boe.text.cpp @@ -1130,7 +1130,7 @@ void print_buf () { line_style.applyTo(text, get_ui_scale()); // A spacing factor of 1.0 within multiline messages doesn't actually line up with other single buffer lines text.setLineSpacing(0.85); - text.setString(message); + text.setString(sf::String::fromUtf8(message.begin(), message.end())); text.setPosition(moveTo); draw_scale_aware_text(text_area_gworld(), text); } @@ -1262,19 +1262,13 @@ std::string get_location(cUniverse* specific_univ) { std::string loc_str = ""; location loc = outdoors ? global_to_local(specific_univ->party.out_loc) : specific_univ->party.town_loc; - if(outdoors) { - loc_str = specific_univ->out->name; - for(short i = 0; i < specific_univ->out->area_desc.size(); i++) - if(loc.in(specific_univ->out->area_desc[i])) { - loc_str = specific_univ->out->area_desc[i].descr; - } - } - if(town){ - loc_str = specific_univ->town->name; - for(short i = 0; i < specific_univ->town->area_desc.size(); i++) - if(loc.in(specific_univ->town->area_desc[i])) { - loc_str = specific_univ->town->area_desc[i].descr; - } + const std::vector& area_desc = (outdoors ? specific_univ->out->area_desc : specific_univ->town->area_desc); + + loc_str = outdoors ? specific_univ->out->name : specific_univ->town->name; + for(const info_rect_t& area : area_desc){ + if(!area.empty() && loc.in(area)) { + loc_str = area.descr; + } } return loc_str; } diff --git a/src/game/boe.town.cpp b/src/game/boe.town.cpp index 4c799cfec..a9b79de34 100644 --- a/src/game/boe.town.cpp +++ b/src/game/boe.town.cpp @@ -69,6 +69,8 @@ void force_town_enter(short which_town,location where_start) { town_force_loc = where_start; } +bool need_enter_town_autosave = false; + //short entry_dir; // if 9, go to forced void start_town_mode(short which_town, short entry_dir, bool debug_enter) { short town_number; @@ -334,8 +336,9 @@ void start_town_mode(short which_town, short entry_dir, bool debug_enter) { if(no_thrash.count(&monst) == 0) monst.active = eCreatureStatus::DEAD; } + bool specials_queued = false; if(!debug_enter) - handle_town_specials(town_number, (short) town_toast,(entry_dir < 9) ? univ.town->start_locs[entry_dir] : town_force_loc); + specials_queued = handle_town_specials(town_number, (short) town_toast,(entry_dir < 9) ? univ.town->start_locs[entry_dir] : town_force_loc); // Flush excess doomguards and viscous goos for(short i = 0; i < univ.town.monst.size(); i++) @@ -492,24 +495,6 @@ void start_town_mode(short which_town, short entry_dir, bool debug_enter) { monst.targ_loc.y = 0; } - // check horses - for(short i = 0; i < univ.party.boats.size(); i++) { - if(univ.scenario.boats[i].which_town >= 0 && univ.scenario.boats[i].loc.x >= 0) { - if(!univ.party.boats[i].exists) { - univ.party.boats[i] = univ.scenario.boats[i]; - univ.party.boats[i].exists = true; - } - } - } - for(short i = 0; i < univ.party.horses.size(); i++) { - if(univ.scenario.horses[i].which_town >= 0 && univ.scenario.horses[i].loc.x >= 0) { - if(!univ.party.horses[i].exists) { - univ.party.horses[i] = univ.scenario.horses[i]; - univ.party.horses[i].exists = true; - } - } - } - clear_map(); reset_item_max(); town_force = 200; @@ -517,7 +502,9 @@ void start_town_mode(short which_town, short entry_dir, bool debug_enter) { // ... except it actually doesn't, because the town enter special is only queued, not run immediately. draw_terrain(1); - try_auto_save("EnterTown"); + // If special nodes still need to be called, we can't do the autosave yet. + if(specials_queued) need_enter_town_autosave = true; + else try_auto_save("EnterTown"); } @@ -644,8 +631,8 @@ location end_town_mode(bool switching_level,location destination, bool debug_lea return to_return; } -void handle_town_specials(short /*town_number*/, bool town_dead,location /*start_loc*/) { - queue_special(eSpecCtx::ENTER_TOWN, eSpecCtxType::TOWN, town_dead ? univ.town->spec_on_entry_if_dead : univ.town->spec_on_entry, univ.party.town_loc); +bool handle_town_specials(short /*town_number*/, bool town_dead,location /*start_loc*/) { + return queue_special(eSpecCtx::ENTER_TOWN, eSpecCtxType::TOWN, town_dead ? univ.town->spec_on_entry_if_dead : univ.town->spec_on_entry, univ.party.town_loc); } void handle_leave_town_specials(short /*town_number*/, short which_spec,location /*start_loc*/) { @@ -1161,7 +1148,7 @@ void pick_lock(location where,short pc_num) { if(r1 < 75) will_break = true; - r1 = get_ran(1,1,100) - 5 * univ.party[pc_num].stat_adj(eSkill::DEXTERITY) + univ.town->difficulty * 7 + r1 = get_ran(1,1,100) - 5 * univ.party[pc_num].stat_adj(eSkill::DEXTERITY) + univ.town.door_diff_adjust() * 7 - 5 * univ.party[pc_num].skill(eSkill::LOCKPICKING) - which_item->abil_strength * 7; // Nimble? @@ -1194,7 +1181,7 @@ void bash_door(location where,short pc_num) { short r1,unlock_adjust; terrain = univ.town->terrain(where.x,where.y); - r1 = get_ran(1,1,100) - 15 * univ.party[pc_num].stat_adj(eSkill::STRENGTH) + univ.town->difficulty * 4; + r1 = get_ran(1,1,100) - 15 * univ.party[pc_num].stat_adj(eSkill::STRENGTH) + univ.town.door_diff_adjust() * 4; if(univ.scenario.ter_types[terrain].special != eTerSpec::UNLOCKABLE) { add_string_to_buf(" Wrong terrain type."); @@ -1470,7 +1457,7 @@ void draw_map(bool need_refresh) { if(is_out() ? univ.out->roads[where.x][where.y] : univ.town.is_road(where.x,where.y)) { draw_rect.inset(1,1); - rect_draw_some_item(*ResMgr::graphics.get("trim"),{8,112,12,116},map_gworld(),draw_rect); + rect_draw_some_item(*ResMgr::graphics.get("fields"),{80,28,84,32},map_gworld(),draw_rect); } } } diff --git a/src/game/boe.town.hpp b/src/game/boe.town.hpp index 9467ad2c7..2c866387c 100644 --- a/src/game/boe.town.hpp +++ b/src/game/boe.town.hpp @@ -6,7 +6,7 @@ void force_town_enter(short which_town,location where_start); void start_town_mode(short which_town, short entry_dir, bool debug_enter = false); location end_town_mode(bool switching_level,location destination,bool debug_leave=false); // returns new party location void handle_leave_town_specials(short town_number, short which_spec,location start_loc) ; -void handle_town_specials(short town_number, bool town_dead,location start_loc) ; +bool handle_town_specials(short town_number, bool town_dead,location start_loc) ; bool abil_exists(eItemAbil abil); void start_town_combat(eDirection direction); diff --git a/src/gfx/render_text.cpp b/src/gfx/render_text.cpp index a72c2558a..23745d48c 100644 --- a/src/gfx/render_text.cpp +++ b/src/gfx/render_text.cpp @@ -117,7 +117,7 @@ break_info_t calculate_line_wrapping(rectangle dest_rect, std::string str, TextS short str_len = str.length(); unsigned short last_line_break = 0,last_word_break = 0; - str_to_draw.setString(str); + str_to_draw.setString(sf::String::fromUtf8(str.begin(), str.end())); // Even if the text is only one line, break_info is required for calculating word boundaries. // So we can't skip the rest of this. @@ -241,10 +241,6 @@ std::string truncate_with_ellipsis(std::string str, const TextStyle& style, int return str; } -std::map substitutions = { - {"–", "--"} -}; - static void win_draw_string(sf::RenderTarget& dest_window,rectangle dest_rect,std::string str,text_params_t& options) { if(str.empty()) return; // Nothing to do! short line_height = options.style.lineHeight; @@ -256,10 +252,7 @@ static void win_draw_string(sf::RenderTarget& dest_window,rectangle dest_rect,st // TODO: Why the heck are we drawing a whole line higher than requested!? adjust_y -= str_to_draw.getLocalBounds().height; - for(auto it : substitutions){ - boost::replace_all(str, it.first, it.second); - } - str_to_draw.setString(str); + str_to_draw.setString(sf::String::fromUtf8(str.begin(), str.end())); short total_width = str_to_draw.getLocalBounds().width; options.style.applyTo(str_to_draw, get_ui_scale()); @@ -333,7 +326,7 @@ static void win_draw_string(sf::RenderTarget& dest_window,rectangle dest_rect,st } for(auto& snippet : options.snippets) { - str_to_draw.setString(snippet.text); + str_to_draw.setString(sf::String::fromUtf8(snippet.text.begin(), snippet.text.end())); str_to_draw.setPosition(snippet.at); if(snippet.hilited) { rectangle bounds = str_to_draw.getGlobalBounds(); @@ -405,7 +398,7 @@ size_t string_length(std::string str, const TextStyle& style, short* height){ sf::Text text; style.applyTo(text); - text.setString(str); + text.setString(sf::String::fromUtf8(str.begin(), str.end())); total_width = text.getLocalBounds().width; if(strings_to_cache.count(str)){ location measurement; diff --git a/src/pcedit/pc.fileio.cpp b/src/pcedit/pc.fileio.cpp index caa33faac..e4d8ee222 100644 --- a/src/pcedit/pc.fileio.cpp +++ b/src/pcedit/pc.fileio.cpp @@ -26,6 +26,11 @@ void remove_party_from_scen() { univ.exportGraphics(); univ.exportSummons(); univ.party.scen_name = ""; + // Saved monsters are not valid now + for(auto& pop : univ.party.creature_save){ + pop.which_town = 200; + pop.clear(); + } party_in_scen = false; load_base_item_defs(); } diff --git a/src/scenario/area.hpp b/src/scenario/area.hpp index 8308235d1..7d308087b 100644 --- a/src/scenario/area.hpp +++ b/src/scenario/area.hpp @@ -54,6 +54,17 @@ class cArea { bool is_on_map(location loc) const { return loc.x < max_dim && loc.y < max_dim && loc.x >= 0 && loc.y >= 0; } + + std::string loc_str(location where) { + std::string str = name; + for(info_rect_t rect : area_desc){ + if(!rect.empty() && rect.contains(where)){ + str += ": " + rect.descr; + break; + } + } + return str; + } }; #endif diff --git a/src/scenario/scenario.cpp b/src/scenario/scenario.cpp index 0337292ac..2e10063d5 100644 --- a/src/scenario/scenario.cpp +++ b/src/scenario/scenario.cpp @@ -119,6 +119,7 @@ cScenario::cScenario(const cScenario& other) , intro_strs(other.intro_strs) , journal_strs(other.journal_strs) , spec_strs(other.spec_strs) + , sdf_names(other.sdf_names) , snd_names(other.snd_names) , adjust_diff(other.adjust_diff) , is_legacy(other.is_legacy) @@ -231,7 +232,7 @@ cScenario::cItemStorage::cItemStorage() : ter_type(-1), property(0) { item_odds[i] = 0; } -void cScenario::import_legacy(legacy::scenario_data_type& old){ +void cScenario::import_legacy(legacy::scenario_data_type& old, bool header_only){ is_legacy = true; // TODO eventually the absence of feature flags here will replace is_legacy altogether feature_flags = {}; @@ -267,6 +268,9 @@ void cScenario::import_legacy(legacy::scenario_data_type& old){ rating = eContentRating(old.rating); // TODO: Is this used anywhere? uses_custom_graphics = old.uses_custom_graphics; + + if(header_only) return; + boats.resize(30); horses.resize(30); for(short i = 0; i < 30; i++) { @@ -640,15 +644,15 @@ std::string cScenario::get_feature_flag(std::string flag) { return iter->second; } -std::string cScenario::get_sdf_name(int row, int col) { +std::string cScenario::get_sdf_name(int row, int col) const { if(row < 0 || row >= SDF_ROWS || col < 0 || col >= SDF_COLUMNS){ throw "Tried to access SDF name for out-of-bounds flag (" + std::to_string(row) + ", " + std::to_string(col) + ")"; } if(sdf_names.find(row) == sdf_names.end()) return ""; - if(sdf_names[row].find(col) == sdf_names[row].end()) + if(sdf_names.at(row).find(col) == sdf_names.at(row).end()) return ""; - return sdf_names[row][col]; + return sdf_names.at(row).at(col); } bool cScenario::cItemStorage::operator==(const cScenario::cItemStorage& other) const { diff --git a/src/scenario/scenario.hpp b/src/scenario/scenario.hpp index d52585b2f..ea6f1e17f 100644 --- a/src/scenario/scenario.hpp +++ b/src/scenario/scenario.hpp @@ -139,7 +139,7 @@ class cScenario { std::vector ic_names; std::vector itf_names; std::map> sdf_names; - std::string get_sdf_name(int row, int col); + std::string get_sdf_name(int row, int col) const; bool adjust_diff; bool is_legacy; @@ -151,7 +151,7 @@ class cScenario { towns.back()->init_start(); } - void import_legacy(legacy::scenario_data_type& old); + void import_legacy(legacy::scenario_data_type& old, bool header_only = false); void import_legacy(legacy::scen_item_data_type& old); void writeTo(cTagFile& file) const; void readFrom(const cTagFile& file); diff --git a/src/scenario/special.cpp b/src/scenario/special.cpp index 05bb0d5af..f40bfb480 100644 --- a/src/scenario/special.cpp +++ b/src/scenario/special.cpp @@ -21,6 +21,7 @@ #include "skills_traits.hpp" #include "damage.hpp" #include "fields.hpp" +#include "scenario.hpp" bool cTimer::is_valid() const { if(time < 0) return false; @@ -529,6 +530,22 @@ void cSpecial::import_legacy(legacy::special_node_type& old){ } } +// In the editor node list, be as helpful as possible about what the specific node instance does +std::string cSpecial::editor_hint(const cScenario& scenario) const { + std::string hint = (*type).name(); + + switch(type){ + case eSpecType::TOWN_STAIR: + case eSpecType::TOWN_GENERIC_STAIR: + hint += " to "; + if(ex2a < scenario.towns.size()) hint += scenario.towns[ex2a]->loc_str(loc(ex1a, ex1b)); + else hint += "INVALID TOWN"; + break; + default: break; + } + return hint; +} + static eSpecCat getNodeCategory(eSpecType node) { for(int i = 0; i <= int(eSpecCat::OUTDOOR); i++) { eSpecCat cat = eSpecCat(i); diff --git a/src/scenario/special.hpp b/src/scenario/special.hpp index 34683ed14..6f9427a09 100644 --- a/src/scenario/special.hpp +++ b/src/scenario/special.hpp @@ -16,6 +16,7 @@ #include "dialogxml/widgets/pictypes.hpp" namespace legacy { struct special_node_type; }; +class cScenario; static const short SDF_COMPLETE = 250; @@ -108,6 +109,7 @@ class cSpecial { return true; } bool operator!=(const cSpecial& other) const { return !(*this == other); } + std::string editor_hint(const cScenario& scenario) const; }; enum class eSpecCtxType { diff --git a/src/scenario/vehicle.cpp b/src/scenario/vehicle.cpp index 7a2d0c3b5..5716bf3e8 100644 --- a/src/scenario/vehicle.cpp +++ b/src/scenario/vehicle.cpp @@ -25,7 +25,7 @@ cVehicle::cVehicle() : void cVehicle::import_legacy(legacy::horse_record_type& old){ which_town = old.which_town; - exists = old.exists; + exists = which_town >= 0 && which_town <= 200; property = old.property; if(which_town < 200) { loc.x = old.horse_loc.x; @@ -40,7 +40,7 @@ void cVehicle::import_legacy(legacy::horse_record_type& old){ void cVehicle::import_legacy(legacy::boat_record_type& old){ which_town = old.which_town; - exists = old.exists; + exists = which_town >= 0 && which_town <= 200; property = old.property; if(which_town < 200) { loc.x = old.boat_loc.x; diff --git a/src/scenedit/scen.actions.cpp b/src/scenedit/scen.actions.cpp index 91e96bd71..74096acc1 100644 --- a/src/scenedit/scen.actions.cpp +++ b/src/scenedit/scen.actions.cpp @@ -809,7 +809,7 @@ void commit_stroke() { }else if(!current_creatures_placed.empty()){ undo_list.add(action_ptr(new aPlaceEraseCreature(current_creatures_placed.size() > 1 ? "Place Creatures" : "Place Creature", true, current_creatures_placed))); update_edit_menu(); - current_items_placed.clear(); + current_creatures_placed.clear(); }else if(!current_fields_cleared.empty()){ undo_list.add(action_ptr(new aClearFields(current_fields_cleared))); update_edit_menu(); @@ -2335,7 +2335,10 @@ void handle_editor_screen_shift(int dx, int dy) { if(town_entrances.size() == 1){ town_entrance_t only_entrance = town_entrances[0]; cChoiceDlog shift_prompt("shift-town-entrance", {"yes", "no"}); - shift_prompt->getControl("out-sec").setText(boost::lexical_cast(only_entrance.out_sec)); + cControl& text = shift_prompt->getControl("prompt"); + text.replaceText("{sec}", boost::lexical_cast(only_entrance.out_sec)); + text.replaceText("{loc}", boost::lexical_cast(only_entrance.loc)); + text.replaceText("{loc_str}", scenario.outdoors[only_entrance.out_sec.x][only_entrance.out_sec.y]->loc_str(only_entrance.loc)); if(shift_prompt.show() == "yes"){ set_current_out(only_entrance.out_sec, true); @@ -2350,9 +2353,9 @@ void handle_editor_screen_shift(int dx, int dy) { std::vector entrance_strings; for(town_entrance_t entrance : town_entrances){ std::ostringstream sstr; - sstr << "Entrance in section " << entrance.out_sec << " at " << entrance.loc; + sstr << "Entrance in section " << entrance.out_sec << " at " << entrance.loc + << " (" <loc_str(entrance.loc) << ")"; entrance_strings.push_back(sstr.str()); - } cStringChoice dlog(entrance_strings, "Shift to one of this town's entrances in the outdoors?"); size_t choice = dlog.show(-1); @@ -2463,15 +2466,18 @@ void frill_up_terrain() { for(short i = 0; i < cur_area->max_dim; i++) for(short j = 0; j < cur_area->max_dim; j++) { terrain_type = cur_area->terrain(i,j); + bool changed = false; for(size_t k = 0; k < scenario.ter_types.size(); k++) { if(terrain_type == k) continue; cTerrain& ter = scenario.ter_types[k]; - if(terrain_type == ter.frill_for && get_ran(1,1,100) < ter.frill_chance) + if(terrain_type == ter.frill_for && get_ran(1,1,100) < ter.frill_chance){ terrain_type = k; + changed = true; + } } - set_terrain(loc(i, j), terrain_type, changes); + if(changed) set_terrain(loc(i, j), terrain_type, changes); } undo_list.add(action_ptr(new aDrawTerrain("Frill Up Terrain", changes))); update_edit_menu(); @@ -2888,10 +2894,11 @@ void place_edit_special(location loc) { bool is_new = false; if(i == specials.size()){ specials.emplace_back(-1,-1,-1); + get_current_area()->specials.emplace_back(); is_new = true; } if(specials[i].spec < 0) { - if(edit_spec_enc(i, editing_town ? 2: 1, nullptr, is_new)) { + if(edit_spec_enc(get_current_area()->specials.size()-1, editing_town ? 2: 1, nullptr, is_new)) { specials[i] = loc; specials[i].spec = i; undo_list.add(action_ptr(new aPlaceEraseSpecial("Place Special Encounter", true, specials[i]))); diff --git a/src/scenedit/scen.core.cpp b/src/scenedit/scen.core.cpp index 641b9c662..9cb0c24c8 100644 --- a/src/scenedit/scen.core.cpp +++ b/src/scenedit/scen.core.cpp @@ -1897,6 +1897,7 @@ static bool edit_item_type_event_filter(cDialog& me, std::string hit, cItem& ite } if(commit_changes){ + temp_item = item; // We actually can't make the action undoable while the dialog is still open if(is_new){ undo_list.add(action_ptr(new aCreateDeleteItem(true, temp_item))); @@ -3344,6 +3345,8 @@ bool build_scenario() { scenario.feature_flags = { {"scenario-meta-format", "V2"}, {"resurrection-balm", "required"}, + // Town difficulty, due to a bug, used to not be added to door difficulty + {"door-town-difficulty", "fixed"} }; fs::path basePath = progDir/"Blades of Exile Base"/(grass ? "bladbase.boes" : "cavebase.boes"); diff --git a/src/scenedit/scen.graphics.cpp b/src/scenedit/scen.graphics.cpp index 40f5bad0c..c7ebf7c3d 100644 --- a/src/scenedit/scen.graphics.cpp +++ b/src/scenedit/scen.graphics.cpp @@ -535,15 +535,15 @@ static void apply_mode_buttons() { std::ostringstream strb; switch(mode) { case 0: - strb << i << " - " << (*scenario.scen_specials[i].type).name(); + strb << i << " - " << scenario.scen_specials[i].editor_hint(scenario); set_rb(i,RB_SCEN_SPEC, i, strb.str()); break; case 1: - strb << i << " - " << (*current_terrain->specials[i].type).name(); + strb << i << " - " << current_terrain->specials[i].editor_hint(scenario); set_rb(i,RB_OUT_SPEC, i, strb.str()); break; case 2: - strb << i << " - " << (*town->specials[i].type).name(); + strb << i << " - " << town->specials[i].editor_hint(scenario); set_rb(i,RB_TOWN_SPEC, i, strb.str()); break; } @@ -1484,7 +1484,6 @@ void draw_one_tiny_terrain_spot (short i,short j,ter_num_t terrain_to_draw,short } else { sf::Texture& small_ter_gworld = *ResMgr::graphics.get("termap"); if(picture_wanted >= 960) { - picture_wanted -= 960; from_rect.offset(12 * 20, (picture_wanted - 960) * 12); rect_draw_some_item(small_ter_gworld, from_rect, mainPtr(), dest_rect); } else { diff --git a/src/scenedit/scen.keydlgs.cpp b/src/scenedit/scen.keydlgs.cpp index 96bf2ac8b..37da1f11c 100644 --- a/src/scenedit/scen.keydlgs.cpp +++ b/src/scenedit/scen.keydlgs.cpp @@ -967,7 +967,7 @@ static bool discard_spec_enc(cDialog& me, node_stack_t& edit_stack) { } auto cur = edit_stack.back(); auto& list = cur.mode == 0 ? scenario.scen_specials : (cur.mode == 1 ? current_terrain->specials : town->specials); - if(cur.which == list.size() - 1 && list[cur.which].type == eSpecType::NONE && list[cur.which].jumpto == -1) + if(cur.is_new && cur.which == list.size() - 1 && list[cur.which].type == eSpecType::NONE && list[cur.which].jumpto == -1) list.pop_back(); edit_stack.pop_back(); if(action == "cancel") { diff --git a/src/scenedit/scen.undo.cpp b/src/scenedit/scen.undo.cpp index 7f6bffcf1..261d9ae4d 100644 --- a/src/scenedit/scen.undo.cpp +++ b/src/scenedit/scen.undo.cpp @@ -65,6 +65,9 @@ void cTerrainAction::showChangeSite() { if(!((abs((short) (cen_x - area.where.x)) <=4) && (abs((short) (cen_y - area.where.y)) <= 4))){ cen_x = area.where.x; cen_y = area.where.y; + size_t max_dim = area.is_town ? town->max_dim : current_terrain->max_dim; + cen_x = minmax(4, max_dim - 5, cen_x); + cen_y = minmax(4, max_dim - 5, cen_y); } redraw_screen(); } @@ -288,7 +291,7 @@ aPlaceEraseItem::aPlaceEraseItem(std::string name, bool place, size_t index, cTo {} aPlaceEraseItem::aPlaceEraseItem(std::string name, bool place, item_changes_t items) - : cTerrainAction(name, items.begin()->second.loc, !place) + : cTerrainAction(name, closest_to_view(items), !place) , items(items) {} @@ -1246,3 +1249,19 @@ bool aCreateAreaRect::redo_me() { area->area_desc[which] = rect; return true; } + +location closest_to_view(stroke_ter_changes_t changes) { + std::vector locs; + for(auto& it : changes){ + locs.push_back(it.first); + } + return closest_point(locs, loc(cen_x, cen_y)); +} + +location closest_to_view(item_changes_t changes) { + std::vector locs; + for(auto& it : changes){ + locs.push_back(it.second.loc); + } + return closest_point(locs, loc(cen_x, cen_y)); +} \ No newline at end of file diff --git a/src/scenedit/scen.undo.hpp b/src/scenedit/scen.undo.hpp index fbf792141..0f21d2842 100644 --- a/src/scenedit/scen.undo.hpp +++ b/src/scenedit/scen.undo.hpp @@ -45,6 +45,9 @@ typedef std::map outdoor_sections_t; typedef std::map,loc_compare> clear_field_stroke_t; typedef std::set field_stroke_t; +location closest_to_view(stroke_ter_changes_t changes); +location closest_to_view(item_changes_t changes); + // Action that modified something in town or outdoor terrain, so we should show the modified area when undoing or redoing class cTerrainAction : public cAction { public: @@ -100,7 +103,7 @@ class aSetSpecial : public cTerrainAction { class aDrawTerrain : public cTerrainAction { public: aDrawTerrain(std::string name, stroke_ter_changes_t stroke_changes) : - cTerrainAction(name, stroke_changes.begin()->first), // Use arbitrary changed tile as site of change + cTerrainAction(name, closest_to_view(stroke_changes)), changes(stroke_changes) {} bool undo_me() override; bool redo_me() override; diff --git a/src/universe/party.cpp b/src/universe/party.cpp index 030ec8566..d8235d33f 100644 --- a/src/universe/party.cpp +++ b/src/universe/party.cpp @@ -274,14 +274,19 @@ void cParty::import_legacy(legacy::stored_items_list_type& old,short which_list) } void cParty::import_legacy(legacy::setup_save_type& old){ - for(int n = 0; n < 4; n++) + for(int n = 0; n < 4; n++){ + setup[n].resize(64, 64); for(int i = 0; i < 64; i++) for(int j = 0; j < 64; j++) setup[n][i][j] = old.setup[n][i][j]; + } } void cParty::cConvers::import_legacy(legacy::talk_save_type old, const cScenario& scenario){ - who_said = scenario.towns[old.personality / 10]->talking.people[old.personality % 10].title; + size_t town = old.personality / 10; + size_t npc = old.personality % 10; + if(town >= scenario.towns.size()) return; + who_said = scenario.towns[town]->talking.people[npc].title; in_town = scenario.towns[old.town_num]->name; int strnums[2] = {old.str1, old.str2}; std::string* strs[2] = {&the_str1, &the_str2}; diff --git a/src/universe/pc.cpp b/src/universe/pc.cpp index c490c81ba..3c4d3ac63 100644 --- a/src/universe/pc.cpp +++ b/src/universe/pc.cpp @@ -850,7 +850,7 @@ const cInvenSlot cPlayer::has_class_equip(unsigned int item_class) const { cInvenSlot cPlayer::has_class(unsigned int item_class, bool require_charges) { return find_item_matching([item_class, require_charges](int, const cItem& item) { - return item.special_class == item_class && (!require_charges || item.charges > 0); + return item.special_class == item_class && (!require_charges || item.charges > 0 || item.max_charges == 0); }); } diff --git a/src/universe/universe.cpp b/src/universe/universe.cpp index 9ba324289..327df733f 100644 --- a/src/universe/universe.cpp +++ b/src/universe/universe.cpp @@ -116,6 +116,10 @@ const cTown& cCurTown::operator * () const { return *record(); } +int cCurTown::door_diff_adjust() { + return univ.scenario.has_feature_flag("door-town-difficulty") ? arena->difficulty : 0; +} + void cCurTown::place_preset_fields() { // Initialize barriers, etc. Note non-sfx gets forgotten if this is a town recently visited. fields.resize(record()->max_dim, record()->max_dim); diff --git a/src/universe/universe.hpp b/src/universe/universe.hpp index a40246766..17e9f8af7 100644 --- a/src/universe/universe.hpp +++ b/src/universe/universe.hpp @@ -52,6 +52,7 @@ class cCurTown { void import_legacy(unsigned char(& old_sfx)[64][64], unsigned char(& old_misc_i)[64][64]); void import_legacy(legacy::big_tr_type& old); + int door_diff_adjust(); cTown* operator -> (); cTown& operator * (); const cTown* operator -> () const;