From 800ade77021b34adf8daa5ca5de0efaa8df24152 Mon Sep 17 00:00:00 2001 From: Niels Martin Hansen Date: Fri, 22 May 2020 22:22:55 +0200 Subject: [PATCH] Feature: Push-buttons on storybook pages (#7896) Allow more direct player-initiated interaction for Game Scripts, by letting the GS put push-buttons on storybook pages. These buttons can either trigger an immediate event, or require the player to first select a tile on the map, or a vehicle. Additionally this reworks how the storybook pages are layouted and rendered, to allow for slightly more complex layouts, and maybe speeding drawing up a bit. --- src/command.cpp | 2 + src/command_type.h | 1 + src/game/game_instance.cpp | 3 + src/script/api/ai/ai_event.hpp.sq | 3 + src/script/api/game/game_event.hpp.sq | 3 + src/script/api/game/game_event_types.hpp.sq | 53 +++ src/script/api/game/game_story_page.hpp.sq | 108 ++++- src/script/api/game_changelog.hpp | 8 + src/script/api/script_event.hpp | 3 + src/script/api/script_event_types.hpp | 132 ++++++ src/script/api/script_story_page.cpp | 87 +++- src/script/api/script_story_page.hpp | 139 +++++- .../api/template/template_event_types.hpp.sq | 27 ++ .../api/template/template_story_page.hpp.sq | 6 + src/story.cpp | 143 ++++++ src/story_base.h | 109 ++++- src/story_gui.cpp | 414 +++++++++++++++--- 17 files changed, 1159 insertions(+), 82 deletions(-) diff --git a/src/command.cpp b/src/command.cpp index ac1ac25521..8890381784 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -170,6 +170,7 @@ CommandProc CmdShowStoryPage; CommandProc CmdRemoveStoryPage; CommandProc CmdRemoveStoryPageElement; CommandProc CmdScrollViewport; +CommandProc CmdStoryPageButton; CommandProc CmdLevelLand; @@ -330,6 +331,7 @@ static const Command _command_proc_table[] = { DEF_CMD(CmdRemoveStoryPage, CMD_DEITY, CMDT_OTHER_MANAGEMENT ), // CMD_REMOVE_STORY_PAGE DEF_CMD(CmdRemoveStoryPageElement, CMD_DEITY, CMDT_OTHER_MANAGEMENT ), // CMD_REMOVE_STORY_ELEMENT_PAGE DEF_CMD(CmdScrollViewport, CMD_DEITY, CMDT_OTHER_MANAGEMENT ), // CMD_SCROLL_VIEWPORT + DEF_CMD(CmdStoryPageButton, CMD_DEITY, CMDT_OTHER_MANAGEMENT ), // CMD_STORY_PAGE_BUTTON DEF_CMD(CmdLevelLand, CMD_ALL_TILES | CMD_NO_TEST | CMD_AUTO, CMDT_LANDSCAPE_CONSTRUCTION), // CMD_LEVEL_LAND; test run might clear tiles multiple times, in execution that only happens once diff --git a/src/command_type.h b/src/command_type.h index 2e026a63d9..04dbfe893c 100644 --- a/src/command_type.h +++ b/src/command_type.h @@ -295,6 +295,7 @@ enum Commands { CMD_REMOVE_STORY_PAGE, ///< remove a story page CMD_REMOVE_STORY_PAGE_ELEMENT, ///< remove a story page element CMD_SCROLL_VIEWPORT, ///< scroll main viewport of players + CMD_STORY_PAGE_BUTTON, ///< selection via story page button CMD_LEVEL_LAND, ///< level land diff --git a/src/game/game_instance.cpp b/src/game/game_instance.cpp index 57b2213ea9..4424c801c6 100644 --- a/src/game/game_instance.cpp +++ b/src/game/game_instance.cpp @@ -147,6 +147,9 @@ void GameInstance::RegisterAPI() SQGSEventIndustryOpen_Register(this->engine); SQGSEventRoadReconstruction_Register(this->engine); SQGSEventStationFirstVehicle_Register(this->engine); + SQGSEventStoryPageButtonClick_Register(this->engine); + SQGSEventStoryPageTileSelect_Register(this->engine); + SQGSEventStoryPageVehicleSelect_Register(this->engine); SQGSEventSubsidyAwarded_Register(this->engine); SQGSEventSubsidyExpired_Register(this->engine); SQGSEventSubsidyOffer_Register(this->engine); diff --git a/src/script/api/ai/ai_event.hpp.sq b/src/script/api/ai/ai_event.hpp.sq index 6061eeed52..1290008c75 100644 --- a/src/script/api/ai/ai_event.hpp.sq +++ b/src/script/api/ai/ai_event.hpp.sq @@ -49,6 +49,9 @@ void SQAIEvent_Register(Squirrel *engine) SQAIEvent.DefSQConst(engine, ScriptEvent::ET_EXCLUSIVE_TRANSPORT_RIGHTS, "ET_EXCLUSIVE_TRANSPORT_RIGHTS"); SQAIEvent.DefSQConst(engine, ScriptEvent::ET_ROAD_RECONSTRUCTION, "ET_ROAD_RECONSTRUCTION"); SQAIEvent.DefSQConst(engine, ScriptEvent::ET_VEHICLE_AUTOREPLACED, "ET_VEHICLE_AUTOREPLACED"); + SQAIEvent.DefSQConst(engine, ScriptEvent::ET_STORYPAGE_BUTTON_CLICK, "ET_STORYPAGE_BUTTON_CLICK"); + SQAIEvent.DefSQConst(engine, ScriptEvent::ET_STORYPAGE_TILE_SELECT, "ET_STORYPAGE_TILE_SELECT"); + SQAIEvent.DefSQConst(engine, ScriptEvent::ET_STORYPAGE_VEHICLE_SELECT, "ET_STORYPAGE_VEHICLE_SELECT"); SQAIEvent.DefSQMethod(engine, &ScriptEvent::GetEventType, "GetEventType", 1, "x"); diff --git a/src/script/api/game/game_event.hpp.sq b/src/script/api/game/game_event.hpp.sq index 42ea11b8ba..2be056cd8b 100644 --- a/src/script/api/game/game_event.hpp.sq +++ b/src/script/api/game/game_event.hpp.sq @@ -49,6 +49,9 @@ void SQGSEvent_Register(Squirrel *engine) SQGSEvent.DefSQConst(engine, ScriptEvent::ET_EXCLUSIVE_TRANSPORT_RIGHTS, "ET_EXCLUSIVE_TRANSPORT_RIGHTS"); SQGSEvent.DefSQConst(engine, ScriptEvent::ET_ROAD_RECONSTRUCTION, "ET_ROAD_RECONSTRUCTION"); SQGSEvent.DefSQConst(engine, ScriptEvent::ET_VEHICLE_AUTOREPLACED, "ET_VEHICLE_AUTOREPLACED"); + SQGSEvent.DefSQConst(engine, ScriptEvent::ET_STORYPAGE_BUTTON_CLICK, "ET_STORYPAGE_BUTTON_CLICK"); + SQGSEvent.DefSQConst(engine, ScriptEvent::ET_STORYPAGE_TILE_SELECT, "ET_STORYPAGE_TILE_SELECT"); + SQGSEvent.DefSQConst(engine, ScriptEvent::ET_STORYPAGE_VEHICLE_SELECT, "ET_STORYPAGE_VEHICLE_SELECT"); SQGSEvent.DefSQMethod(engine, &ScriptEvent::GetEventType, "GetEventType", 1, "x"); diff --git a/src/script/api/game/game_event_types.hpp.sq b/src/script/api/game/game_event_types.hpp.sq index 62e4f6a089..6a0e89b499 100644 --- a/src/script/api/game/game_event_types.hpp.sq +++ b/src/script/api/game/game_event_types.hpp.sq @@ -308,3 +308,56 @@ void SQGSEventRoadReconstruction_Register(Squirrel *engine) SQGSEventRoadReconstruction.PostRegister(engine); } + + +template <> const char *GetClassName() { return "GSEventStoryPageButtonClick"; } + +void SQGSEventStoryPageButtonClick_Register(Squirrel *engine) +{ + DefSQClass SQGSEventStoryPageButtonClick("GSEventStoryPageButtonClick"); + SQGSEventStoryPageButtonClick.PreRegister(engine, "GSEvent"); + + SQGSEventStoryPageButtonClick.DefSQStaticMethod(engine, &ScriptEventStoryPageButtonClick::Convert, "Convert", 2, ".x"); + + SQGSEventStoryPageButtonClick.DefSQMethod(engine, &ScriptEventStoryPageButtonClick::GetCompanyID, "GetCompanyID", 1, "x"); + SQGSEventStoryPageButtonClick.DefSQMethod(engine, &ScriptEventStoryPageButtonClick::GetStoryPageID, "GetStoryPageID", 1, "x"); + SQGSEventStoryPageButtonClick.DefSQMethod(engine, &ScriptEventStoryPageButtonClick::GetElementID, "GetElementID", 1, "x"); + + SQGSEventStoryPageButtonClick.PostRegister(engine); +} + + +template <> const char *GetClassName() { return "GSEventStoryPageTileSelect"; } + +void SQGSEventStoryPageTileSelect_Register(Squirrel *engine) +{ + DefSQClass SQGSEventStoryPageTileSelect("GSEventStoryPageTileSelect"); + SQGSEventStoryPageTileSelect.PreRegister(engine, "GSEvent"); + + SQGSEventStoryPageTileSelect.DefSQStaticMethod(engine, &ScriptEventStoryPageTileSelect::Convert, "Convert", 2, ".x"); + + SQGSEventStoryPageTileSelect.DefSQMethod(engine, &ScriptEventStoryPageTileSelect::GetCompanyID, "GetCompanyID", 1, "x"); + SQGSEventStoryPageTileSelect.DefSQMethod(engine, &ScriptEventStoryPageTileSelect::GetStoryPageID, "GetStoryPageID", 1, "x"); + SQGSEventStoryPageTileSelect.DefSQMethod(engine, &ScriptEventStoryPageTileSelect::GetElementID, "GetElementID", 1, "x"); + SQGSEventStoryPageTileSelect.DefSQMethod(engine, &ScriptEventStoryPageTileSelect::GetTile, "GetTile", 1, "x"); + + SQGSEventStoryPageTileSelect.PostRegister(engine); +} + + +template <> const char *GetClassName() { return "GSEventStoryPageVehicleSelect"; } + +void SQGSEventStoryPageVehicleSelect_Register(Squirrel *engine) +{ + DefSQClass SQGSEventStoryPageVehicleSelect("GSEventStoryPageVehicleSelect"); + SQGSEventStoryPageVehicleSelect.PreRegister(engine, "GSEvent"); + + SQGSEventStoryPageVehicleSelect.DefSQStaticMethod(engine, &ScriptEventStoryPageVehicleSelect::Convert, "Convert", 2, ".x"); + + SQGSEventStoryPageVehicleSelect.DefSQMethod(engine, &ScriptEventStoryPageVehicleSelect::GetCompanyID, "GetCompanyID", 1, "x"); + SQGSEventStoryPageVehicleSelect.DefSQMethod(engine, &ScriptEventStoryPageVehicleSelect::GetStoryPageID, "GetStoryPageID", 1, "x"); + SQGSEventStoryPageVehicleSelect.DefSQMethod(engine, &ScriptEventStoryPageVehicleSelect::GetElementID, "GetElementID", 1, "x"); + SQGSEventStoryPageVehicleSelect.DefSQMethod(engine, &ScriptEventStoryPageVehicleSelect::GetVehicleID, "GetVehicleID", 1, "x"); + + SQGSEventStoryPageVehicleSelect.PostRegister(engine); +} diff --git a/src/script/api/game/game_story_page.hpp.sq b/src/script/api/game/game_story_page.hpp.sq index 99bc237bf8..b9fbf60260 100644 --- a/src/script/api/game/game_story_page.hpp.sq +++ b/src/script/api/game/game_story_page.hpp.sq @@ -24,21 +24,101 @@ void SQGSStoryPage_Register(Squirrel *engine) SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPET_TEXT, "SPET_TEXT"); SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPET_LOCATION, "SPET_LOCATION"); SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPET_GOAL, "SPET_GOAL"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPET_BUTTON_PUSH, "SPET_BUTTON_PUSH"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPET_BUTTON_TILE, "SPET_BUTTON_TILE"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPET_BUTTON_VEHICLE, "SPET_BUTTON_VEHICLE"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBF_NONE, "SPBF_NONE"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBF_FLOAT_LEFT, "SPBF_FLOAT_LEFT"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBF_FLOAT_RIGHT, "SPBF_FLOAT_RIGHT"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_MOUSE, "SPBC_MOUSE"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_ZZZ, "SPBC_ZZZ"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_BUOY, "SPBC_BUOY"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_QUERY, "SPBC_QUERY"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_HQ, "SPBC_HQ"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_SHIP_DEPOT, "SPBC_SHIP_DEPOT"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_SIGN, "SPBC_SIGN"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_TREE, "SPBC_TREE"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_BUY_LAND, "SPBC_BUY_LAND"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_LEVEL_LAND, "SPBC_LEVEL_LAND"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_TOWN, "SPBC_TOWN"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_INDUSTRY, "SPBC_INDUSTRY"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_ROCKY_AREA, "SPBC_ROCKY_AREA"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_DESERT, "SPBC_DESERT"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_TRANSMITTER, "SPBC_TRANSMITTER"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_AIRPORT, "SPBC_AIRPORT"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_DOCK, "SPBC_DOCK"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_CANAL, "SPBC_CANAL"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_LOCK, "SPBC_LOCK"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_RIVER, "SPBC_RIVER"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_AQUEDUCT, "SPBC_AQUEDUCT"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_BRIDGE, "SPBC_BRIDGE"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_RAIL_STATION, "SPBC_RAIL_STATION"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_TUNNEL_RAIL, "SPBC_TUNNEL_RAIL"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_TUNNEL_ELRAIL, "SPBC_TUNNEL_ELRAIL"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_TUNNEL_MONO, "SPBC_TUNNEL_MONO"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_TUNNEL_MAGLEV, "SPBC_TUNNEL_MAGLEV"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_AUTORAIL, "SPBC_AUTORAIL"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_AUTOELRAIL, "SPBC_AUTOELRAIL"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_AUTOMONO, "SPBC_AUTOMONO"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_AUTOMAGLEV, "SPBC_AUTOMAGLEV"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_WAYPOINT, "SPBC_WAYPOINT"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_RAIL_DEPOT, "SPBC_RAIL_DEPOT"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_ELRAIL_DEPOT, "SPBC_ELRAIL_DEPOT"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_MONO_DEPOT, "SPBC_MONO_DEPOT"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_MAGLEV_DEPOT, "SPBC_MAGLEV_DEPOT"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_CONVERT_RAIL, "SPBC_CONVERT_RAIL"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_CONVERT_ELRAIL, "SPBC_CONVERT_ELRAIL"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_CONVERT_MONO, "SPBC_CONVERT_MONO"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_CONVERT_MAGLEV, "SPBC_CONVERT_MAGLEV"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_AUTOROAD, "SPBC_AUTOROAD"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_AUTOTRAM, "SPBC_AUTOTRAM"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_ROAD_DEPOT, "SPBC_ROAD_DEPOT"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_BUS_STATION, "SPBC_BUS_STATION"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_TRUCK_STATION, "SPBC_TRUCK_STATION"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_ROAD_TUNNEL, "SPBC_ROAD_TUNNEL"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_CLONE_TRAIN, "SPBC_CLONE_TRAIN"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_CLONE_ROADVEH, "SPBC_CLONE_ROADVEH"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_CLONE_SHIP, "SPBC_CLONE_SHIP"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_CLONE_AIRPLANE, "SPBC_CLONE_AIRPLANE"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_DEMOLISH, "SPBC_DEMOLISH"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_LOWERLAND, "SPBC_LOWERLAND"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_RAISELAND, "SPBC_RAISELAND"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_PICKSTATION, "SPBC_PICKSTATION"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_BUILDSIGNALS, "SPBC_BUILDSIGNALS"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_DARK_BLUE, "SPBC_DARK_BLUE"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_PALE_GREEN, "SPBC_PALE_GREEN"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_PINK, "SPBC_PINK"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_YELLOW, "SPBC_YELLOW"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_RED, "SPBC_RED"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_LIGHT_BLUE, "SPBC_LIGHT_BLUE"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_GREEN, "SPBC_GREEN"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_DARK_GREEN, "SPBC_DARK_GREEN"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_BLUE, "SPBC_BLUE"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_CREAM, "SPBC_CREAM"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_MAUVE, "SPBC_MAUVE"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_PURPLE, "SPBC_PURPLE"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_ORANGE, "SPBC_ORANGE"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_BROWN, "SPBC_BROWN"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_GREY, "SPBC_GREY"); + SQGSStoryPage.DefSQConst(engine, ScriptStoryPage::SPBC_WHITE, "SPBC_WHITE"); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::IsValidStoryPage, "IsValidStoryPage", 2, ".i"); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::IsValidStoryPageElement, "IsValidStoryPageElement", 2, ".i"); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::New, "New", 3, ".i."); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::NewElement, "NewElement", 5, ".iii."); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::UpdateElement, "UpdateElement", 4, ".ii."); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::GetPageSortValue, "GetPageSortValue", 2, ".i"); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::GetPageElementSortValue, "GetPageElementSortValue", 2, ".i"); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::GetCompany, "GetCompany", 2, ".i"); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::GetDate, "GetDate", 2, ".i"); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::SetDate, "SetDate", 3, ".ii"); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::SetTitle, "SetTitle", 3, ".i."); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::Show, "Show", 2, ".i"); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::Remove, "Remove", 2, ".i"); - SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::RemoveElement, "RemoveElement", 2, ".i"); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::IsValidStoryPage, "IsValidStoryPage", 2, ".i"); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::IsValidStoryPageElement, "IsValidStoryPageElement", 2, ".i"); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::New, "New", 3, ".i."); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::NewElement, "NewElement", 5, ".iii."); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::UpdateElement, "UpdateElement", 4, ".ii."); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::GetPageSortValue, "GetPageSortValue", 2, ".i"); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::GetPageElementSortValue, "GetPageElementSortValue", 2, ".i"); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::GetCompany, "GetCompany", 2, ".i"); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::GetDate, "GetDate", 2, ".i"); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::SetDate, "SetDate", 3, ".ii"); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::SetTitle, "SetTitle", 3, ".i."); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::Show, "Show", 2, ".i"); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::Remove, "Remove", 2, ".i"); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::RemoveElement, "RemoveElement", 2, ".i"); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::MakePushButtonReference, "MakePushButtonReference", 3, ".ii"); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::MakeTileButtonReference, "MakeTileButtonReference", 4, ".iii"); + SQGSStoryPage.DefSQStaticMethod(engine, &ScriptStoryPage::MakeVehicleButtonReference, "MakeVehicleButtonReference", 5, ".iiii"); SQGSStoryPage.PostRegister(engine); } diff --git a/src/script/api/game_changelog.hpp b/src/script/api/game_changelog.hpp index df8c663a03..69c4d971a5 100644 --- a/src/script/api/game_changelog.hpp +++ b/src/script/api/game_changelog.hpp @@ -17,6 +17,14 @@ * * This version is not yet released. The following changes are not set in stone yet. * + * API additions: + * \li GSEventStoryPageButtonClick + * \li GSEventStoryPageTileSelect + * \li GSEventStoryPageVehicleSelect + * \li GSStoryPage::MakePushButtonReference + * \li GSStoryPage::MakeTileButtonReference + * \li GSStoryPage::MakeVehicleButtonReference + * * \b 1.10.0 * * API additions: diff --git a/src/script/api/script_event.hpp b/src/script/api/script_event.hpp index 886e4f1db8..8e8f181701 100644 --- a/src/script/api/script_event.hpp +++ b/src/script/api/script_event.hpp @@ -54,6 +54,9 @@ public: ET_EXCLUSIVE_TRANSPORT_RIGHTS, ET_ROAD_RECONSTRUCTION, ET_VEHICLE_AUTOREPLACED, + ET_STORYPAGE_BUTTON_CLICK, + ET_STORYPAGE_TILE_SELECT, + ET_STORYPAGE_VEHICLE_SELECT, }; /** diff --git a/src/script/api/script_event_types.hpp b/src/script/api/script_event_types.hpp index 98cca7f502..f0487f48a0 100644 --- a/src/script/api/script_event_types.hpp +++ b/src/script/api/script_event_types.hpp @@ -1098,4 +1098,136 @@ private: VehicleID new_id; ///< The vehicle that has been created in replacement. }; +/** + * Event StoryPageButtonClick, indicating a player clicked a push button on a storybook page. + * @api game + */ +class ScriptEventStoryPageButtonClick : public ScriptEvent { +public: + /** + * @param company_id Which company triggered the event. + * @param page_id Which page was the clicked button on. + * @param element_id Which button element was clicked. + */ + ScriptEventStoryPageButtonClick(CompanyID company_id, StoryPageID page_id, StoryPageElementID element_id) : + ScriptEvent(ET_STORYPAGE_BUTTON_CLICK), + company_id((ScriptCompany::CompanyID)company_id), + page_id(page_id), + element_id(element_id) + { } + + /** + * Convert an ScriptEvent to the real instance. + * @param instance The instance to convert. + * @return The converted instance. + */ + static ScriptEventStoryPageButtonClick *Convert(ScriptEvent *instance) { return (ScriptEventStoryPageButtonClick *)instance; } + + /** Get the CompanyID of the player that selected a tile. */ + ScriptCompany::CompanyID GetCompanyID() { return this->company_id; } + + /** Get the StoryPageID of the storybook page the clicked button is located on. */ + StoryPageID GetStoryPageID() { return this->page_id; } + + /** Get the StoryPageElementID of the button element that was clicked. */ + StoryPageElementID GetElementID() { return this->element_id; } + +private: + ScriptCompany::CompanyID company_id; + StoryPageID page_id; + StoryPageElementID element_id; +}; + +/** + * Event StoryPageTileSelect, indicating a player clicked a tile selection button on a storybook page, and selected a tile. + * @api game + */ +class ScriptEventStoryPageTileSelect : public ScriptEvent { +public: + /** + * @param company_id Which company triggered the event. + * @param page_id Which page is the used selection button on. + * @param element_id Which button element was used to select the tile. + * @param tile_index Which tile was selected by the player. + */ + ScriptEventStoryPageTileSelect(CompanyID company_id, StoryPageID page_id, StoryPageElementID element_id, TileIndex tile_index) : + ScriptEvent(ET_STORYPAGE_TILE_SELECT), + company_id((ScriptCompany::CompanyID)company_id), + page_id(page_id), + element_id(element_id), + tile_index(tile_index) + { } + + /** + * Convert an ScriptEvent to the real instance. + * @param instance The instance to convert. + * @return The converted instance. + */ + static ScriptEventStoryPageTileSelect *Convert(ScriptEvent *instance) { return (ScriptEventStoryPageTileSelect *)instance; } + + /** Get the CompanyID of the player that selected a tile. */ + ScriptCompany::CompanyID GetCompanyID() { return this->company_id; } + + /** Get the StoryPageID of the storybook page the used selection button is located on. */ + StoryPageID GetStoryPageID() { return this->page_id; } + + /** Get the StoryPageElementID of the selection button used to select the tile. */ + StoryPageElementID GetElementID() { return this->element_id; } + + /** Get the TileIndex of the tile the player selected */ + TileIndex GetTile() { return this->tile_index; } + +private: + ScriptCompany::CompanyID company_id; + StoryPageID page_id; + StoryPageElementID element_id; + TileIndex tile_index; +}; + +/** + * Event StoryPageTileSelect, indicating a player clicked a tile selection button on a storybook page, and selected a tile. + * @api game + */ +class ScriptEventStoryPageVehicleSelect : public ScriptEvent { +public: + /** + * @param company_id Which company triggered the event. + * @param page_id Which page is the used selection button on. + * @param element_id Which button element was used to select the tile. + * @param vehicle_id Which vehicle was selected by the player. + */ + ScriptEventStoryPageVehicleSelect(CompanyID company_id, StoryPageID page_id, StoryPageElementID element_id, VehicleID vehicle_id) : + ScriptEvent(ET_STORYPAGE_VEHICLE_SELECT), + company_id((ScriptCompany::CompanyID)company_id), + page_id(page_id), + element_id(element_id), + vehicle_id(vehicle_id) + { } + + /** + * Convert an ScriptEvent to the real instance. + * @param instance The instance to convert. + * @return The converted instance. + */ + static ScriptEventStoryPageVehicleSelect *Convert(ScriptEvent *instance) { return (ScriptEventStoryPageVehicleSelect *)instance; } + + /** Get the CompanyID of the player that selected a tile. */ + ScriptCompany::CompanyID GetCompanyID() { return this->company_id; } + + /** Get the StoryPageID of the storybook page the used selection button is located on. */ + StoryPageID GetStoryPageID() { return this->page_id; } + + /** Get the StoryPageElementID of the selection button used to select the vehicle. */ + StoryPageElementID GetElementID() { return this->element_id; } + + /** Get the VehicleID of the vehicle the player selected */ + VehicleID GetVehicleID() { return this->vehicle_id; } + +private: + ScriptCompany::CompanyID company_id; + StoryPageID page_id; + StoryPageElementID element_id; + VehicleID vehicle_id; +}; + #endif /* SCRIPT_EVENT_TYPES_HPP */ diff --git a/src/script/api/script_story_page.cpp b/src/script/api/script_story_page.cpp index 87b81268c1..39ae3d53ff 100644 --- a/src/script/api/script_story_page.cpp +++ b/src/script/api/script_story_page.cpp @@ -22,6 +22,11 @@ #include "../../safeguards.h" +static inline bool StoryPageElementTypeRequiresText(StoryPageElementType type) +{ + return type == SPET_TEXT || type == SPET_LOCATION || type == SPET_BUTTON_PUSH || type == SPET_BUTTON_TILE || type == SPET_BUTTON_VEHICLE; +} + /* static */ bool ScriptStoryPage::IsValidStoryPage(StoryPageID story_page_id) { return ::StoryPage::IsValidID(story_page_id); @@ -57,18 +62,34 @@ { CCountedPtr counter(text); + ::StoryPageElementType btype = static_cast<::StoryPageElementType>(type); + EnforcePrecondition(STORY_PAGE_ELEMENT_INVALID, ScriptObject::GetCompany() == OWNER_DEITY); EnforcePrecondition(STORY_PAGE_ELEMENT_INVALID, IsValidStoryPage(story_page_id)); - EnforcePrecondition(STORY_PAGE_ELEMENT_INVALID, (type != SPET_TEXT && type != SPET_LOCATION) || (text != nullptr && !StrEmpty(text->GetEncodedText()))); + EnforcePrecondition(STORY_PAGE_ELEMENT_INVALID, !StoryPageElementTypeRequiresText(btype) || (text != nullptr && !StrEmpty(text->GetEncodedText()))); EnforcePrecondition(STORY_PAGE_ELEMENT_INVALID, type != SPET_LOCATION || ::IsValidTile(reference)); EnforcePrecondition(STORY_PAGE_ELEMENT_INVALID, type != SPET_GOAL || ScriptGoal::IsValidGoal((ScriptGoal::GoalID)reference)); EnforcePrecondition(STORY_PAGE_ELEMENT_INVALID, type != SPET_GOAL || !(StoryPage::Get(story_page_id)->company == INVALID_COMPANY && Goal::Get(reference)->company != INVALID_COMPANY)); - if (!ScriptObject::DoCommand(type == SPET_LOCATION ? reference : 0, + uint32 refid = 0; + TileIndex reftile = 0; + switch (type) { + case SPET_LOCATION: + reftile = reference; + break; + case SPET_GOAL: + case SPET_BUTTON_PUSH: + case SPET_BUTTON_TILE: + case SPET_BUTTON_VEHICLE: + refid = reference; + break; + } + + if (!ScriptObject::DoCommand(reftile, story_page_id + (type << 16), - type == SPET_GOAL ? reference : 0, + refid, CMD_CREATE_STORY_PAGE_ELEMENT, - type == SPET_TEXT || type == SPET_LOCATION ? text->GetEncodedText() : nullptr, + StoryPageElementTypeRequiresText(btype) ? text->GetEncodedText() : nullptr, &ScriptInstance::DoCommandReturnStoryPageElementID)) return STORY_PAGE_ELEMENT_INVALID; /* In case of test-mode, we return StoryPageElementID 0 */ @@ -86,16 +107,30 @@ StoryPage *p = StoryPage::Get(pe->page); ::StoryPageElementType type = pe->type; - EnforcePrecondition(false, (type != ::SPET_TEXT && type != ::SPET_LOCATION) || (text != nullptr && !StrEmpty(text->GetEncodedText()))); + EnforcePrecondition(false, !StoryPageElementTypeRequiresText(type) || (text != nullptr && !StrEmpty(text->GetEncodedText()))); EnforcePrecondition(false, type != ::SPET_LOCATION || ::IsValidTile(reference)); EnforcePrecondition(false, type != ::SPET_GOAL || ScriptGoal::IsValidGoal((ScriptGoal::GoalID)reference)); EnforcePrecondition(false, type != ::SPET_GOAL || !(p->company == INVALID_COMPANY && Goal::Get(reference)->company != INVALID_COMPANY)); - return ScriptObject::DoCommand(type == ::SPET_LOCATION ? reference : 0, + uint32 refid = 0; + TileIndex reftile = 0; + switch (type) { + case SPET_LOCATION: + reftile = reference; + break; + case SPET_GOAL: + case SPET_BUTTON_PUSH: + case SPET_BUTTON_TILE: + case SPET_BUTTON_VEHICLE: + refid = reference; + break; + } + + return ScriptObject::DoCommand(reftile, story_page_element_id, - type == ::SPET_GOAL ? reference : 0, + refid, CMD_UPDATE_STORY_PAGE_ELEMENT, - type == ::SPET_TEXT || type == ::SPET_LOCATION ? text->GetEncodedText() : nullptr); + StoryPageElementTypeRequiresText(type) ? text->GetEncodedText() : nullptr); } /* static */ uint32 ScriptStoryPage::GetPageSortValue(StoryPageID story_page_id) @@ -173,3 +208,39 @@ return ScriptObject::DoCommand(0, story_page_element_id, 0, CMD_REMOVE_STORY_PAGE_ELEMENT); } +/* static */ ScriptStoryPage::StoryPageButtonFormatting ScriptStoryPage::MakePushButtonReference(StoryPageButtonColour colour, StoryPageButtonFlags flags) +{ + StoryPageButtonData data; + data.SetColour((Colours)colour); + data.SetFlags((::StoryPageButtonFlags)flags); + if (!data.ValidateColour()) return UINT32_MAX; + if (!data.ValidateFlags()) return UINT32_MAX; + return data.referenced_id; +} + +/* static */ ScriptStoryPage::StoryPageButtonFormatting ScriptStoryPage::MakeTileButtonReference(StoryPageButtonColour colour, StoryPageButtonFlags flags, StoryPageButtonCursor cursor) +{ + StoryPageButtonData data; + data.SetColour((Colours)colour); + data.SetFlags((::StoryPageButtonFlags)flags); + data.SetCursor((::StoryPageButtonCursor)cursor); + if (!data.ValidateColour()) return UINT32_MAX; + if (!data.ValidateFlags()) return UINT32_MAX; + if (!data.ValidateCursor()) return UINT32_MAX; + return data.referenced_id; +} + +/* static */ ScriptStoryPage::StoryPageButtonFormatting ScriptStoryPage::MakeVehicleButtonReference(StoryPageButtonColour colour, StoryPageButtonFlags flags, StoryPageButtonCursor cursor, ScriptVehicle::VehicleType vehtype) +{ + StoryPageButtonData data; + data.SetColour((Colours)colour); + data.SetFlags((::StoryPageButtonFlags)flags); + data.SetCursor((::StoryPageButtonCursor)cursor); + data.SetVehicleType((::VehicleType)vehtype); + if (!data.ValidateColour()) return UINT32_MAX; + if (!data.ValidateFlags()) return UINT32_MAX; + if (!data.ValidateCursor()) return UINT32_MAX; + if (!data.ValidateVehicleType()) return UINT32_MAX; + return data.referenced_id; +} + diff --git a/src/script/api/script_story_page.hpp b/src/script/api/script_story_page.hpp index d7e44fee49..23e4e03b14 100644 --- a/src/script/api/script_story_page.hpp +++ b/src/script/api/script_story_page.hpp @@ -12,6 +12,7 @@ #include "script_company.hpp" #include "script_date.hpp" +#include "script_vehicle.hpp" #include "../../story_type.h" #include "../../story_base.h" @@ -57,9 +58,111 @@ public: * Story page element types. */ enum StoryPageElementType { - SPET_TEXT = ::SPET_TEXT, ///< An element that displays a block of text. - SPET_LOCATION = ::SPET_LOCATION, ///< An element that displays a single line of text along with a button to view the referenced location. - SPET_GOAL = ::SPET_GOAL, ///< An element that displays a goal. + SPET_TEXT = ::SPET_TEXT, ///< An element that displays a block of text. + SPET_LOCATION = ::SPET_LOCATION, ///< An element that displays a single line of text along with a button to view the referenced location. + SPET_GOAL = ::SPET_GOAL, ///< An element that displays a goal. + SPET_BUTTON_PUSH = ::SPET_BUTTON_PUSH, ///< A push button that triggers an immediate event. + SPET_BUTTON_TILE = ::SPET_BUTTON_TILE, ///< A button that allows the player to select a tile, and triggers an event with the tile. + SPET_BUTTON_VEHICLE = ::SPET_BUTTON_VEHICLE, ///< A button that allows the player to select a vehicle, and triggers an event wih the vehicle. + }; + + /** + * Formatting data for button page elements. + */ + typedef uint32 StoryPageButtonFormatting; + + /** + * Formatting and layout flags for story page buttons. + * The SPBF_FLOAT_LEFT and SPBF_FLOAT_RIGHT flags can not be combined. + */ + enum StoryPageButtonFlags { + SPBF_NONE = ::SPBF_NONE, ///< No special formatting for button. + SPBF_FLOAT_LEFT = ::SPBF_FLOAT_LEFT, ///< Button is placed to the left of the following paragraph. + SPBF_FLOAT_RIGHT = ::SPBF_FLOAT_RIGHT, ///< Button is placed to the right of the following paragraph. + }; + + /** + * Mouse cursors usable by story page buttons. + */ + enum StoryPageButtonCursor { + SPBC_MOUSE = ::SPBC_MOUSE, + SPBC_ZZZ = ::SPBC_ZZZ, + SPBC_BUOY = ::SPBC_BUOY, + SPBC_QUERY = ::SPBC_QUERY, + SPBC_HQ = ::SPBC_HQ, + SPBC_SHIP_DEPOT = ::SPBC_SHIP_DEPOT, + SPBC_SIGN = ::SPBC_SIGN, + SPBC_TREE = ::SPBC_TREE, + SPBC_BUY_LAND = ::SPBC_BUY_LAND, + SPBC_LEVEL_LAND = ::SPBC_LEVEL_LAND, + SPBC_TOWN = ::SPBC_TOWN, + SPBC_INDUSTRY = ::SPBC_INDUSTRY, + SPBC_ROCKY_AREA = ::SPBC_ROCKY_AREA, + SPBC_DESERT = ::SPBC_DESERT, + SPBC_TRANSMITTER = ::SPBC_TRANSMITTER, + SPBC_AIRPORT = ::SPBC_AIRPORT, + SPBC_DOCK = ::SPBC_DOCK, + SPBC_CANAL = ::SPBC_CANAL, + SPBC_LOCK = ::SPBC_LOCK, + SPBC_RIVER = ::SPBC_RIVER, + SPBC_AQUEDUCT = ::SPBC_AQUEDUCT, + SPBC_BRIDGE = ::SPBC_BRIDGE, + SPBC_RAIL_STATION = ::SPBC_RAIL_STATION, + SPBC_TUNNEL_RAIL = ::SPBC_TUNNEL_RAIL, + SPBC_TUNNEL_ELRAIL = ::SPBC_TUNNEL_ELRAIL, + SPBC_TUNNEL_MONO = ::SPBC_TUNNEL_MONO, + SPBC_TUNNEL_MAGLEV = ::SPBC_TUNNEL_MAGLEV, + SPBC_AUTORAIL = ::SPBC_AUTORAIL, + SPBC_AUTOELRAIL = ::SPBC_AUTOELRAIL, + SPBC_AUTOMONO = ::SPBC_AUTOMONO, + SPBC_AUTOMAGLEV = ::SPBC_AUTOMAGLEV, + SPBC_WAYPOINT = ::SPBC_WAYPOINT, + SPBC_RAIL_DEPOT = ::SPBC_RAIL_DEPOT, + SPBC_ELRAIL_DEPOT = ::SPBC_ELRAIL_DEPOT, + SPBC_MONO_DEPOT = ::SPBC_MONO_DEPOT, + SPBC_MAGLEV_DEPOT = ::SPBC_MAGLEV_DEPOT, + SPBC_CONVERT_RAIL = ::SPBC_CONVERT_RAIL, + SPBC_CONVERT_ELRAIL = ::SPBC_CONVERT_ELRAIL, + SPBC_CONVERT_MONO = ::SPBC_CONVERT_MONO, + SPBC_CONVERT_MAGLEV = ::SPBC_CONVERT_MAGLEV, + SPBC_AUTOROAD = ::SPBC_AUTOROAD, + SPBC_AUTOTRAM = ::SPBC_AUTOTRAM, + SPBC_ROAD_DEPOT = ::SPBC_ROAD_DEPOT, + SPBC_BUS_STATION = ::SPBC_BUS_STATION, + SPBC_TRUCK_STATION = ::SPBC_TRUCK_STATION, + SPBC_ROAD_TUNNEL = ::SPBC_ROAD_TUNNEL, + SPBC_CLONE_TRAIN = ::SPBC_CLONE_TRAIN, + SPBC_CLONE_ROADVEH = ::SPBC_CLONE_ROADVEH, + SPBC_CLONE_SHIP = ::SPBC_CLONE_SHIP, + SPBC_CLONE_AIRPLANE = ::SPBC_CLONE_AIRPLANE, + SPBC_DEMOLISH = ::SPBC_DEMOLISH, + SPBC_LOWERLAND = ::SPBC_LOWERLAND, + SPBC_RAISELAND = ::SPBC_RAISELAND, + SPBC_PICKSTATION = ::SPBC_PICKSTATION, + SPBC_BUILDSIGNALS = ::SPBC_BUILDSIGNALS, + }; + + /** + * Colour codes usable for story page button elements. + * Place a colour value in the lowest 8 bits of the \c reference parameter to the button. + */ + enum StoryPageButtonColour { + SPBC_DARK_BLUE = ::COLOUR_DARK_BLUE, + SPBC_PALE_GREEN = ::COLOUR_PALE_GREEN, + SPBC_PINK = ::COLOUR_PINK, + SPBC_YELLOW = ::COLOUR_YELLOW, + SPBC_RED = ::COLOUR_RED, + SPBC_LIGHT_BLUE = ::COLOUR_LIGHT_BLUE, + SPBC_GREEN = ::COLOUR_GREEN, + SPBC_DARK_GREEN = ::COLOUR_DARK_GREEN, + SPBC_BLUE = ::COLOUR_BLUE, + SPBC_CREAM = ::COLOUR_CREAM, + SPBC_MAUVE = ::COLOUR_MAUVE, + SPBC_PURPLE = ::COLOUR_PURPLE, + SPBC_ORANGE = ::COLOUR_ORANGE, + SPBC_BROWN = ::COLOUR_BROWN, + SPBC_GREY = ::COLOUR_GREY, + SPBC_WHITE = ::COLOUR_WHITE, }; /** @@ -90,7 +193,11 @@ public: * Create a new story page element. * @param story_page_id The page id of the story page which the page element should be appended to. * @param type Which page element type to create. - * @param reference A reference value to the object that is referred to by some page element types. When type is SPET_GOAL, this is the goal ID. When type is SPET_LOCATION, this is the TileIndex. + * @param reference A reference value to the object that is referred to by some page element types. + * When type is SPET_GOAL, this is the goal ID. + * When type is SPET_LOCATION, this is the TileIndex. + * When type is a button, this is additional parameters for the button, + * use the #BuildPushButtonReference, #BuildTileButtonReference, or #BuildVehicleButtonReference functions to make the values. * @param text The body text of page elements that allow custom text. (SPET_TEXT and SPET_LOCATION) * @return The new StoryPageElementID, or STORY_PAGE_ELEMENT_INVALID if it failed. * @pre No ScriptCompanyMode may be in scope. @@ -204,6 +311,30 @@ public: * @pre IsValidStoryPageElement(story_page_element_id). */ static bool RemoveElement(StoryPageElementID story_page_element_id); + + /** + * Create a reference value for SPET_BUTTON_PUSH element parameters. + * @param colour The colour for the face of the button. + * @return A reference value usable with the #NewElement and #UpdateElement functions. + */ + static StoryPageButtonFormatting MakePushButtonReference(StoryPageButtonColour colour, StoryPageButtonFlags flags); + + /** + * Create a reference value for SPET_BUTTON_TILE element parameters. + * @param colour The colour for the face of the button. + * @param cursor The mouse cursor to use when the player clicks the button and the game is ready for the player to select a tile. + * @return A reference value usable with the #NewElement and #UpdateElement functions. + */ + static StoryPageButtonFormatting MakeTileButtonReference(StoryPageButtonColour colour, StoryPageButtonFlags flags, StoryPageButtonCursor cursor); + + /** + * Create a reference value for SPET_BUTTON_VEHICLE element parameters. + * @param colour The colour for the face of the button. + * @param cursor The mouse cursor to use when the player clicks the button and the game is ready for the player to select a vehicle. + * @param vehtype The type of vehicle that will be selectable, or \c VT_INVALID to allow all types. + * @return A reference value usable with the #NewElement and #UpdateElement functions. + */ + static StoryPageButtonFormatting MakeVehicleButtonReference(StoryPageButtonColour colour, StoryPageButtonFlags flags, StoryPageButtonCursor cursor, ScriptVehicle::VehicleType vehtype); }; #endif /* SCRIPT_STORY_HPP */ diff --git a/src/script/api/template/template_event_types.hpp.sq b/src/script/api/template/template_event_types.hpp.sq index 09cbe43f3f..4e82de63c7 100644 --- a/src/script/api/template/template_event_types.hpp.sq +++ b/src/script/api/template/template_event_types.hpp.sq @@ -273,3 +273,30 @@ namespace SQConvert { template <> inline const ScriptEventVehicleAutoReplaced &GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return *(ScriptEventVehicleAutoReplaced *)instance; } template <> inline int Return(HSQUIRRELVM vm, ScriptEventVehicleAutoReplaced *res) { if (res == nullptr) { sq_pushnull(vm); return 1; } res->AddRef(); Squirrel::CreateClassInstanceVM(vm, "EventVehicleAutoReplaced", res, nullptr, DefSQDestructorCallback, true); return 1; } } // namespace SQConvert + +namespace SQConvert { + /* Allow ScriptEventStoryPageButtonClick to be used as Squirrel parameter */ + template <> inline ScriptEventStoryPageButtonClick *GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return (ScriptEventStoryPageButtonClick *)instance; } + template <> inline ScriptEventStoryPageButtonClick &GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return *(ScriptEventStoryPageButtonClick *)instance; } + template <> inline const ScriptEventStoryPageButtonClick *GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return (ScriptEventStoryPageButtonClick *)instance; } + template <> inline const ScriptEventStoryPageButtonClick &GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return *(ScriptEventStoryPageButtonClick *)instance; } + template <> inline int Return(HSQUIRRELVM vm, ScriptEventStoryPageButtonClick *res) { if (res == nullptr) { sq_pushnull(vm); return 1; } res->AddRef(); Squirrel::CreateClassInstanceVM(vm, "EventStoryPageButtonClick", res, nullptr, DefSQDestructorCallback, true); return 1; } +} // namespace SQConvert + +namespace SQConvert { + /* Allow ScriptEventStoryPageTileSelect to be used as Squirrel parameter */ + template <> inline ScriptEventStoryPageTileSelect *GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return (ScriptEventStoryPageTileSelect *)instance; } + template <> inline ScriptEventStoryPageTileSelect &GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return *(ScriptEventStoryPageTileSelect *)instance; } + template <> inline const ScriptEventStoryPageTileSelect *GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return (ScriptEventStoryPageTileSelect *)instance; } + template <> inline const ScriptEventStoryPageTileSelect &GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return *(ScriptEventStoryPageTileSelect *)instance; } + template <> inline int Return(HSQUIRRELVM vm, ScriptEventStoryPageTileSelect *res) { if (res == nullptr) { sq_pushnull(vm); return 1; } res->AddRef(); Squirrel::CreateClassInstanceVM(vm, "EventStoryPageTileSelect", res, nullptr, DefSQDestructorCallback, true); return 1; } +} // namespace SQConvert + +namespace SQConvert { + /* Allow ScriptEventStoryPageVehicleSelect to be used as Squirrel parameter */ + template <> inline ScriptEventStoryPageVehicleSelect *GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return (ScriptEventStoryPageVehicleSelect *)instance; } + template <> inline ScriptEventStoryPageVehicleSelect &GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return *(ScriptEventStoryPageVehicleSelect *)instance; } + template <> inline const ScriptEventStoryPageVehicleSelect *GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return (ScriptEventStoryPageVehicleSelect *)instance; } + template <> inline const ScriptEventStoryPageVehicleSelect &GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return *(ScriptEventStoryPageVehicleSelect *)instance; } + template <> inline int Return(HSQUIRRELVM vm, ScriptEventStoryPageVehicleSelect *res) { if (res == nullptr) { sq_pushnull(vm); return 1; } res->AddRef(); Squirrel::CreateClassInstanceVM(vm, "EventStoryPageVehicleSelect", res, nullptr, DefSQDestructorCallback, true); return 1; } +} // namespace SQConvert diff --git a/src/script/api/template/template_story_page.hpp.sq b/src/script/api/template/template_story_page.hpp.sq index 106f9a6238..f06d531385 100644 --- a/src/script/api/template/template_story_page.hpp.sq +++ b/src/script/api/template/template_story_page.hpp.sq @@ -17,6 +17,12 @@ namespace SQConvert { template <> inline int Return(HSQUIRRELVM vm, ScriptStoryPage::StoryPageElementID res) { sq_pushinteger(vm, (int32)res); return 1; } template <> inline ScriptStoryPage::StoryPageElementType GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQInteger tmp; sq_getinteger(vm, index, &tmp); return (ScriptStoryPage::StoryPageElementType)tmp; } template <> inline int Return(HSQUIRRELVM vm, ScriptStoryPage::StoryPageElementType res) { sq_pushinteger(vm, (int32)res); return 1; } + template <> inline ScriptStoryPage::StoryPageButtonFlags GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQInteger tmp; sq_getinteger(vm, index, &tmp); return (ScriptStoryPage::StoryPageButtonFlags)tmp; } + template <> inline int Return(HSQUIRRELVM vm, ScriptStoryPage::StoryPageButtonFlags res) { sq_pushinteger(vm, (int32)res); return 1; } + template <> inline ScriptStoryPage::StoryPageButtonCursor GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQInteger tmp; sq_getinteger(vm, index, &tmp); return (ScriptStoryPage::StoryPageButtonCursor)tmp; } + template <> inline int Return(HSQUIRRELVM vm, ScriptStoryPage::StoryPageButtonCursor res) { sq_pushinteger(vm, (int32)res); return 1; } + template <> inline ScriptStoryPage::StoryPageButtonColour GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQInteger tmp; sq_getinteger(vm, index, &tmp); return (ScriptStoryPage::StoryPageButtonColour)tmp; } + template <> inline int Return(HSQUIRRELVM vm, ScriptStoryPage::StoryPageButtonColour res) { sq_pushinteger(vm, (int32)res); return 1; } /* Allow ScriptStoryPage to be used as Squirrel parameter */ template <> inline ScriptStoryPage *GetParam(ForceType, HSQUIRRELVM vm, int index, SQAutoFreePointers *ptr) { SQUserPointer instance; sq_getinstanceup(vm, index, &instance, 0); return (ScriptStoryPage *)instance; } diff --git a/src/story.cpp b/src/story.cpp index 0d465fde85..e60c60a9fa 100644 --- a/src/story.cpp +++ b/src/story.cpp @@ -21,6 +21,10 @@ #include "goal_base.h" #include "window_func.h" #include "gui.h" +#include "vehicle_base.h" +#include "game/game.hpp" +#include "script/api/script_story_page.hpp" +#include "script/api/script_event_types.hpp" #include "safeguards.h" @@ -47,6 +51,8 @@ INSTANTIATE_POOL_METHODS(StoryPage) */ static bool VerifyElementContentParameters(StoryPageID page_id, StoryPageElementType type, TileIndex tile, uint32 reference, const char *text) { + StoryPageButtonData button_data{ reference }; + switch (type) { case SPET_TEXT: if (StrEmpty(text)) return false; @@ -60,6 +66,18 @@ static bool VerifyElementContentParameters(StoryPageID page_id, StoryPageElement /* Reject company specific goals on global pages */ if (StoryPage::Get(page_id)->company == INVALID_COMPANY && Goal::Get((GoalID)reference)->company != INVALID_COMPANY) return false; break; + case SPET_BUTTON_PUSH: + if (!button_data.ValidateColour()) return false; + return true; + case SPET_BUTTON_TILE: + if (!button_data.ValidateColour()) return false; + if (!button_data.ValidateCursor()) return false; + return true; + case SPET_BUTTON_VEHICLE: + if (!button_data.ValidateColour()) return false; + if (!button_data.ValidateCursor()) return false; + if (!button_data.ValidateVehicleType()) return false; + return true; default: return false; } @@ -88,10 +106,94 @@ static void UpdateElement(StoryPageElement &pe, TileIndex tile, uint32 reference case SPET_GOAL: pe.referenced_id = (GoalID)reference; break; + case SPET_BUTTON_PUSH: + case SPET_BUTTON_TILE: + case SPET_BUTTON_VEHICLE: + pe.text = stredup(text); + pe.referenced_id = reference; + break; default: NOT_REACHED(); } } +/** Set the button background colour. */ +void StoryPageButtonData::SetColour(Colours button_colour) +{ + assert(button_colour < COLOUR_END); + SB(this->referenced_id, 0, 8, button_colour); +} + +void StoryPageButtonData::SetFlags(StoryPageButtonFlags flags) +{ + SB(this->referenced_id, 24, 8, flags); +} + +/** Set the mouse cursor used while waiting for input for the button. */ +void StoryPageButtonData::SetCursor(StoryPageButtonCursor cursor) +{ + assert(cursor < SPBC_END); + SB(this->referenced_id, 8, 8, cursor); +} + +/** Set the type of vehicles that are accepted by the button */ +void StoryPageButtonData::SetVehicleType(VehicleType vehtype) +{ + assert(vehtype == VEH_INVALID || vehtype < VEH_COMPANY_END); + SB(this->referenced_id, 16, 8, vehtype); +} + +/** Get the button background colour. */ +Colours StoryPageButtonData::GetColour() const +{ + return Extract(this->referenced_id); +} + +StoryPageButtonFlags StoryPageButtonData::GetFlags() const +{ + return (StoryPageButtonFlags)GB(this->referenced_id, 24, 8); +} + +/** Get the mouse cursor used while waiting for input for the button. */ +StoryPageButtonCursor StoryPageButtonData::GetCursor() const +{ + return Extract(this->referenced_id); +} + +/** Get the type of vehicles that are accepted by the button */ +VehicleType StoryPageButtonData::GetVehicleType() const +{ + return (VehicleType)GB(this->referenced_id, 16, 8); +} + +/** Verify that the data stored a valid Colour value */ +bool StoryPageButtonData::ValidateColour() const +{ + return GB(this->referenced_id, 0, 8) < COLOUR_END; +} + +bool StoryPageButtonData::ValidateFlags() const +{ + byte flags = GB(this->referenced_id, 24, 8); + /* Don't allow float left and right together */ + if ((flags & SPBF_FLOAT_LEFT) && (flags & SPBF_FLOAT_RIGHT)) return false; + /* Don't allow undefined flags */ + if (flags & ~(SPBF_FLOAT_LEFT | SPBF_FLOAT_RIGHT)) return false; + return true; +} + +/** Verify that the data stores a valid StoryPageButtonCursor value */ +bool StoryPageButtonData::ValidateCursor() const +{ + return GB(this->referenced_id, 8, 8) < SPBC_END; +} + +/** Verity that the data stored a valid VehicleType value */ +bool StoryPageButtonData::ValidateVehicleType() const +{ + byte vehtype = GB(this->referenced_id, 16, 8); + return vehtype == VEH_INVALID || vehtype < VEH_COMPANY_END; +} + /** * Create a new story page. * @param tile unused. @@ -358,3 +460,44 @@ CommandCost CmdRemoveStoryPageElement(TileIndex tile, DoCommandFlag flags, uint3 return CommandCost(); } +/** + * Clicked/used a button on a story page. + * @param tile Tile selected, for tile selection buttons, otherwise unused. + * @param flags Type of operation. + * @param p1 Bit 0..15 = story page element id of button. + * @param p2 ID of selected item for buttons that select an item (e.g. vehicle), otherwise unused. + * @param text Unused. + * @return The cost of the operation, or an error. + */ +CommandCost CmdStoryPageButton(TileIndex tile, DoCommandFlag flags, uint32 p1, uint32 p2, const char *text) +{ + StoryPageElementID page_element_id = (StoryPageElementID)GB(p1, 0, 16); + + if (!StoryPageElement::IsValidID(page_element_id)) return CMD_ERROR; + const StoryPageElement *const pe = StoryPageElement::Get(page_element_id); + + /* Check the player belongs to the company that owns the page. */ + const StoryPage *const sp = StoryPage::Get(pe->page); + if (sp->company != INVALID_COMPANY && sp->company != _current_company) return CMD_ERROR; + + switch (pe->type) { + case SPET_BUTTON_PUSH: + /* No validation required */ + if (flags & DC_EXEC) Game::NewEvent(new ScriptEventStoryPageButtonClick(_current_company, pe->page, page_element_id)); + break; + case SPET_BUTTON_TILE: + if (!IsValidTile(tile)) return CMD_ERROR; + if (flags & DC_EXEC) Game::NewEvent(new ScriptEventStoryPageTileSelect(_current_company, pe->page, page_element_id, tile)); + break; + case SPET_BUTTON_VEHICLE: + if (!Vehicle::IsValidID(p2)) return CMD_ERROR; + if (flags & DC_EXEC) Game::NewEvent(new ScriptEventStoryPageVehicleSelect(_current_company, pe->page, page_element_id, (VehicleID)p2)); + break; + default: + /* Invalid page element type, not a button. */ + return CMD_ERROR; + } + + return CommandCost(); +} + diff --git a/src/story_base.h b/src/story_base.h index e82b9d6964..e4d677cbf7 100644 --- a/src/story_base.h +++ b/src/story_base.h @@ -13,6 +13,8 @@ #include "company_type.h" #include "story_type.h" #include "date_type.h" +#include "gfx_type.h" +#include "vehicle_type.h" #include "core/pool_type.hpp" typedef Pool StoryPageElementPool; @@ -26,9 +28,12 @@ extern uint32 _story_page_next_sort_value; * Each story page element is one of these types. */ enum StoryPageElementType : byte { - SPET_TEXT = 0, ///< A text element. - SPET_LOCATION, ///< An element that references a tile along with a one-line text. - SPET_GOAL, ///< An element that references a goal. + SPET_TEXT = 0, ///< A text element. + SPET_LOCATION, ///< An element that references a tile along with a one-line text. + SPET_GOAL, ///< An element that references a goal. + SPET_BUTTON_PUSH, ///< A push button that triggers an immediate event. + SPET_BUTTON_TILE, ///< A button that allows the player to select a tile, and triggers an event with the tile. + SPET_BUTTON_VEHICLE, ///< A button that allows the player to select a vehicle, and triggers an event wih the vehicle. SPET_END, INVALID_SPET = 0xFF, }; @@ -36,18 +41,108 @@ enum StoryPageElementType : byte { /** Define basic enum properties */ template <> struct EnumPropsT : MakeEnumPropsT {}; +/** Flags available for buttons */ +enum StoryPageButtonFlags : byte { + SPBF_NONE = 0, + SPBF_FLOAT_LEFT = 1 << 0, + SPBF_FLOAT_RIGHT = 1 << 1, +}; +DECLARE_ENUM_AS_BIT_SET(StoryPageButtonFlags) + +/** Mouse cursors usable by story page buttons. */ +enum StoryPageButtonCursor : byte { + SPBC_MOUSE, + SPBC_ZZZ, + SPBC_BUOY, + SPBC_QUERY, + SPBC_HQ, + SPBC_SHIP_DEPOT, + SPBC_SIGN, + SPBC_TREE, + SPBC_BUY_LAND, + SPBC_LEVEL_LAND, + SPBC_TOWN, + SPBC_INDUSTRY, + SPBC_ROCKY_AREA, + SPBC_DESERT, + SPBC_TRANSMITTER, + SPBC_AIRPORT, + SPBC_DOCK, + SPBC_CANAL, + SPBC_LOCK, + SPBC_RIVER, + SPBC_AQUEDUCT, + SPBC_BRIDGE, + SPBC_RAIL_STATION, + SPBC_TUNNEL_RAIL, + SPBC_TUNNEL_ELRAIL, + SPBC_TUNNEL_MONO, + SPBC_TUNNEL_MAGLEV, + SPBC_AUTORAIL, + SPBC_AUTOELRAIL, + SPBC_AUTOMONO, + SPBC_AUTOMAGLEV, + SPBC_WAYPOINT, + SPBC_RAIL_DEPOT, + SPBC_ELRAIL_DEPOT, + SPBC_MONO_DEPOT, + SPBC_MAGLEV_DEPOT, + SPBC_CONVERT_RAIL, + SPBC_CONVERT_ELRAIL, + SPBC_CONVERT_MONO, + SPBC_CONVERT_MAGLEV, + SPBC_AUTOROAD, + SPBC_AUTOTRAM, + SPBC_ROAD_DEPOT, + SPBC_BUS_STATION, + SPBC_TRUCK_STATION, + SPBC_ROAD_TUNNEL, + SPBC_CLONE_TRAIN, + SPBC_CLONE_ROADVEH, + SPBC_CLONE_SHIP, + SPBC_CLONE_AIRPLANE, + SPBC_DEMOLISH, + SPBC_LOWERLAND, + SPBC_RAISELAND, + SPBC_PICKSTATION, + SPBC_BUILDSIGNALS, + SPBC_END, + INVALID_SPBC = 0xFF +}; + +/** Define basic enum properties */ +template <> struct EnumPropsT : MakeEnumPropsT {}; + +/** Helper to construct packed "id" values for button-type StoryPageElement */ +struct StoryPageButtonData { + uint32 referenced_id; + + void SetColour(Colours button_colour); + void SetFlags(StoryPageButtonFlags flags); + void SetCursor(StoryPageButtonCursor cursor); + void SetVehicleType(VehicleType vehtype); + Colours GetColour() const; + StoryPageButtonFlags GetFlags() const; + StoryPageButtonCursor GetCursor() const; + VehicleType GetVehicleType() const; + bool ValidateColour() const; + bool ValidateFlags() const; + bool ValidateCursor() const; + bool ValidateVehicleType() const; +}; + /** * Struct about story page elements. * Each StoryPage is composed of one or more page elements that provide * page content. Each element only contain one type of content. **/ struct StoryPageElement : StoryPageElementPool::PoolItem<&_story_page_element_pool> { - uint32 sort_value; ///< A number that increases for every created story page element. Used for sorting. The id of a story page element is the pool index. - StoryPageID page; ///< Id of the page which the page element belongs to + uint32 sort_value; ///< A number that increases for every created story page element. Used for sorting. The id of a story page element is the pool index. + StoryPageID page; ///< Id of the page which the page element belongs to StoryPageElementType type; ///< Type of page element - uint32 referenced_id; ///< Id of referenced object (location, goal etc.) - char *text; ///< Static content text of page element + uint32 referenced_id; ///< Id of referenced object (location, goal etc.) + char *text; ///< Static content text of page element /** * We need an (empty) constructor so struct isn't zeroed (as C++ standard states) diff --git a/src/story_gui.cpp b/src/story_gui.cpp index 677d88bba8..96c411fe9c 100644 --- a/src/story_gui.cpp +++ b/src/story_gui.cpp @@ -23,26 +23,47 @@ #include "viewport_func.h" #include "window_func.h" #include "company_base.h" +#include "tilehighlight_func.h" +#include "vehicle_base.h" #include "widgets/story_widget.h" #include "table/strings.h" #include "table/sprites.h" +#include + #include "safeguards.h" +static CursorID TranslateStoryPageButtonCursor(StoryPageButtonCursor cursor); + typedef GUIList GUIStoryPageList; typedef GUIList GUIStoryPageElementList; struct StoryBookWindow : Window { protected: + struct LayoutCacheElement { + const StoryPageElement *pe; + Rect bounds; + }; + typedef std::vector LayoutCache; + + enum class ElementFloat { + None, + Left, + Right, + }; + Scrollbar *vscroll; ///< Scrollbar of the page text. + mutable LayoutCache layout_cache; ///< Cached element layout. GUIStoryPageList story_pages; ///< Sorted list of pages. GUIStoryPageElementList story_page_elements; ///< Sorted list of page elements that belong to the current page. StoryPageID selected_page_id; ///< Pool index of selected page. char selected_generic_title[255]; ///< If the selected page doesn't have a custom title, this buffer is used to store a generic page title. + StoryPageElementID active_button_id; ///< Which button element the player is currently using + static GUIStoryPageList::SortFunction * const page_sorter_funcs[]; static GUIStoryPageElementList::SortFunction * const page_element_sorter_funcs[]; @@ -91,6 +112,7 @@ protected: } this->story_page_elements.Sort(); + this->InvalidateStoryPageElementLayout(); } /** Sort story page elements by order value. */ @@ -174,6 +196,8 @@ protected: this->story_page_elements.ForceRebuild(); this->BuildStoryPageElementList(); + if (this->active_button_id != INVALID_STORY_PAGE_ELEMENT) ResetObjectToPlace(); + this->vscroll->SetCount(this->GetContentHeight()); this->SetWidgetDirty(WID_SB_SCROLLBAR); this->SetWidgetDirty(WID_SB_SEL_PAGE); @@ -251,9 +275,9 @@ protected: /** * Get the width available for displaying content on the page panel. */ - uint GetAvailablePageContentWidth() + uint GetAvailablePageContentWidth() const { - return this->GetWidget(WID_SB_PAGE_PANEL)->current_x - WD_FRAMETEXT_LEFT - WD_FRAMERECT_RIGHT; + return this->GetWidget(WID_SB_PAGE_PANEL)->current_x - WD_FRAMETEXT_LEFT - WD_FRAMETEXT_RIGHT - 1; } /** @@ -304,20 +328,26 @@ protected: * @param max_width Available width to display content. * @return the height in pixels. */ - uint GetPageElementHeight(const StoryPageElement &pe, int max_width) + uint GetPageElementHeight(const StoryPageElement &pe, int max_width) const { switch (pe.type) { case SPET_TEXT: SetDParamStr(0, pe.text); return GetStringHeight(STR_BLACK_RAW_STRING, max_width); - break; case SPET_GOAL: case SPET_LOCATION: { Dimension sprite_dim = GetSpriteSize(GetPageElementSprite(pe)); return sprite_dim.height; - break; } + + case SPET_BUTTON_PUSH: + case SPET_BUTTON_TILE: + case SPET_BUTTON_VEHICLE: { + Dimension dim = GetStringBoundingBox(pe.text, FS_NORMAL); + return dim.height + WD_BEVEL_TOP + WD_BEVEL_BOTTOM + WD_FRAMETEXT_TOP + WD_FRAMETEXT_BOTTOM; + } + default: NOT_REACHED(); } @@ -325,27 +355,161 @@ protected: } /** - * Get the total height of the content displayed - * in this window. + * Get the float style of a page element. + * @param pe The story page element. + * @return The float style. + */ + ElementFloat GetPageElementFloat(const StoryPageElement &pe) const + { + switch (pe.type) { + case SPET_BUTTON_PUSH: + case SPET_BUTTON_TILE: + case SPET_BUTTON_VEHICLE: { + StoryPageButtonFlags flags = StoryPageButtonData{ pe.referenced_id }.GetFlags(); + if (flags & SPBF_FLOAT_LEFT) return ElementFloat::Left; + if (flags & SPBF_FLOAT_RIGHT) return ElementFloat::Right; + return ElementFloat::None; + } + + default: + return ElementFloat::None; + } + } + + /** + * Get the width a page element would use if it was floating left or right. + * @param pe The story page element. + * @return The calculated width of the element. + */ + int GetPageElementFloatWidth(const StoryPageElement &pe) const + { + switch (pe.type) { + case SPET_BUTTON_PUSH: + case SPET_BUTTON_TILE: + case SPET_BUTTON_VEHICLE: { + Dimension dim = GetStringBoundingBox(pe.text, FS_NORMAL); + return dim.width + WD_BEVEL_LEFT + WD_BEVEL_RIGHT + WD_FRAMETEXT_LEFT + WD_FRAMETEXT_RIGHT; + } + + default: + NOT_REACHED(); // only buttons can float + } + } + + /** Invalidate the current page layout */ + void InvalidateStoryPageElementLayout() + { + this->layout_cache.clear(); + } + + /** Create the page layout if it is missing */ + void EnsureStoryPageElementLayout() const + { + /* Assume if the layout cache has contents it is valid */ + if (!this->layout_cache.empty()) return; + + StoryPage *page = this->GetSelPage(); + if (page == nullptr) return; + int max_width = GetAvailablePageContentWidth(); + int element_dist = FONT_HEIGHT_NORMAL; + + /* Make space for the header */ + int main_y = GetHeadHeight(max_width) + element_dist; + + /* Current bottom of left/right column */ + int left_y = main_y; + int right_y = main_y; + /* Current width of left/right column, 0 indicates no content in column */ + int left_width = 0; + int right_width = 0; + /* Indexes into element cache for yet unresolved floats */ + std::vector left_floats; + std::vector right_floats; + + /* Build layout */ + for (const StoryPageElement *pe : this->story_page_elements) { + ElementFloat fl = this->GetPageElementFloat(*pe); + + if (fl == ElementFloat::None) { + /* Verify available width */ + const int min_required_width = 10 * FONT_HEIGHT_NORMAL; + int left_offset = (left_width == 0) ? 0 : (left_width + element_dist); + int right_offset = (right_width == 0) ? 0 : (right_width + element_dist); + if (left_offset + right_offset + min_required_width >= max_width) { + /* Width of floats leave too little for main content, push down */ + main_y = max(main_y, left_y); + main_y = max(main_y, right_y); + left_width = right_width = 0; + left_offset = right_offset = 0; + /* Do not add element_dist here, to keep together elements which were supposed to float besides each other. */ + } + /* Determine height */ + const int available_width = max_width - left_offset - right_offset; + const int height = GetPageElementHeight(*pe, available_width); + /* Check for button that needs extra margin */ + if (left_offset == 0 && right_offset == 0) { + switch (pe->type) { + case SPET_BUTTON_PUSH: + case SPET_BUTTON_TILE: + case SPET_BUTTON_VEHICLE: + left_offset = right_offset = available_width / 5; + break; + default: + break; + } + } + /* Position element in main column */ + LayoutCacheElement ce{ pe }; + ce.bounds.left = left_offset; + ce.bounds.right = max_width - right_offset; + ce.bounds.top = main_y; + main_y += height; + ce.bounds.bottom = main_y; + this->layout_cache.push_back(ce); + main_y += element_dist; + /* Clear all floats */ + left_width = right_width = 0; + left_y = right_y = main_y = max(main_y, max(left_y, right_y)); + left_floats.clear(); + right_floats.clear(); + } else { + /* Prepare references to correct column */ + int &cur_width = (fl == ElementFloat::Left) ? left_width : right_width; + int &cur_y = (fl == ElementFloat::Left) ? left_y : right_y; + std::vector &cur_floats = (fl == ElementFloat::Left) ? left_floats : right_floats; + /* Position element */ + cur_width = max(cur_width, this->GetPageElementFloatWidth(*pe)); + LayoutCacheElement ce{ pe }; + ce.bounds.left = (fl == ElementFloat::Left) ? 0 : (max_width - cur_width); + ce.bounds.right = (fl == ElementFloat::Left) ? cur_width : max_width; + ce.bounds.top = cur_y; + cur_y += GetPageElementHeight(*pe, cur_width); + ce.bounds.bottom = cur_y; + cur_floats.push_back(this->layout_cache.size()); + this->layout_cache.push_back(ce); + cur_y += element_dist; + /* Update floats in column to all have the same width */ + for (size_t index : cur_floats) { + LayoutCacheElement &ce = this->layout_cache[index]; + ce.bounds.left = (fl == ElementFloat::Left) ? 0 : (max_width - cur_width); + ce.bounds.right = (fl == ElementFloat::Left) ? cur_width : max_width; + } + } + } + } + + /** + * Get the total height of the content displayed in this window. * @return the height in pixels */ uint GetContentHeight() { - StoryPage *page = this->GetSelPage(); - if (page == nullptr) return 0; - int max_width = GetAvailablePageContentWidth(); - uint element_vertical_dist = FONT_HEIGHT_NORMAL; + this->EnsureStoryPageElementLayout(); - /* Head */ - uint height = GetHeadHeight(max_width); + /* The largest bottom coordinate of any element is the height of the content */ + uint max_y = std::accumulate(this->layout_cache.begin(), this->layout_cache.end(), 0, [](uint max_y, const LayoutCacheElement &ce) -> uint { return max(max_y, ce.bounds.bottom); }); - /* Body */ - for (const StoryPageElement *pe : this->story_page_elements) { - height += element_vertical_dist; - height += GetPageElementHeight(*pe, max_width); - } - - return height; + return max_y; } /** @@ -396,6 +560,39 @@ protected: ShowGoalsList((CompanyID)this->window_number); break; + case SPET_BUTTON_PUSH: + if (this->active_button_id != INVALID_STORY_PAGE_ELEMENT) ResetObjectToPlace(); + this->active_button_id = pe.index; + this->SetTimeout(); + this->SetWidgetDirty(WID_SB_PAGE_PANEL); + + DoCommandP(0, pe.index, 0, CMD_STORY_PAGE_BUTTON); + break; + + case SPET_BUTTON_TILE: + if (this->active_button_id == pe.index) { + ResetObjectToPlace(); + this->active_button_id = INVALID_STORY_PAGE_ELEMENT; + } else { + CursorID cursor = TranslateStoryPageButtonCursor(StoryPageButtonData{ pe.referenced_id }.GetCursor()); + SetObjectToPlaceWnd(cursor, PAL_NONE, HT_RECT, this); + this->active_button_id = pe.index; + } + this->SetWidgetDirty(WID_SB_PAGE_PANEL); + break; + + case SPET_BUTTON_VEHICLE: + if (this->active_button_id == pe.index) { + ResetObjectToPlace(); + this->active_button_id = INVALID_STORY_PAGE_ELEMENT; + } else { + CursorID cursor = TranslateStoryPageButtonCursor(StoryPageButtonData{ pe.referenced_id }.GetCursor()); + SetObjectToPlaceWnd(cursor, PAL_NONE, HT_VEHICLE, this); + this->active_button_id = pe.index; + } + this->SetWidgetDirty(WID_SB_PAGE_PANEL); + break; + default: NOT_REACHED(); } @@ -422,6 +619,8 @@ public: this->selected_generic_title[0] = '\0'; this->selected_page_id = INVALID_STORY_PAGE; + this->active_button_id = INVALID_STORY_PAGE_ELEMENT; + this->OnInvalidateData(-1); } @@ -443,6 +642,8 @@ public: void SetSelectedPage(uint16 page_index) { if (this->selected_page_id != page_index) { + if (this->active_button_id) ResetObjectToPlace(); + this->active_button_id = INVALID_STORY_PAGE_ELEMENT; this->selected_page_id = page_index; this->RefreshSelectedPage(); this->UpdatePrevNextDisabledState(); @@ -504,7 +705,8 @@ public: /* Draw content (now coordinates given to Draw** are local to the new clipping region). */ int line_height = FONT_HEIGHT_NORMAL; - int y_offset = - this->vscroll->GetPosition(); + const int scrollpos = this->vscroll->GetPosition(); + int y_offset = -scrollpos; /* Date */ if (page->date != INVALID_DATE) { @@ -518,28 +720,46 @@ public: y_offset = DrawStringMultiLine(0, right - x, y_offset, bottom - y, STR_STORY_BOOK_TITLE, TC_BLACK, SA_TOP | SA_HOR_CENTER); /* Page elements */ - for (const StoryPageElement *const pe : this->story_page_elements) { - y_offset += line_height; // margin to previous element - - switch (pe->type) { + this->EnsureStoryPageElementLayout(); + for (const LayoutCacheElement &ce : this->layout_cache) { + y_offset = ce.bounds.top - scrollpos; + switch (ce.pe->type) { case SPET_TEXT: - SetDParamStr(0, pe->text); - y_offset = DrawStringMultiLine(0, right - x, y_offset, bottom - y, STR_JUST_RAW_STRING, TC_BLACK, SA_TOP | SA_LEFT); + SetDParamStr(0, ce.pe->text); + y_offset = DrawStringMultiLine(ce.bounds.left, ce.bounds.right, ce.bounds.top - scrollpos, ce.bounds.bottom - scrollpos, STR_JUST_RAW_STRING, TC_BLACK, SA_TOP | SA_LEFT); break; case SPET_GOAL: { - Goal *g = Goal::Get((GoalID) pe->referenced_id); + Goal *g = Goal::Get((GoalID) ce.pe->referenced_id); StringID string_id = g == nullptr ? STR_STORY_BOOK_INVALID_GOAL_REF : STR_JUST_RAW_STRING; if (g != nullptr) SetDParamStr(0, g->text); - DrawActionElement(y_offset, right - x, line_height, GetPageElementSprite(*pe), string_id); + DrawActionElement(y_offset, ce.bounds.right - ce.bounds.left, line_height, GetPageElementSprite(*ce.pe), string_id); break; } case SPET_LOCATION: - SetDParamStr(0, pe->text); - DrawActionElement(y_offset, right - x, line_height, GetPageElementSprite(*pe)); + SetDParamStr(0, ce.pe->text); + DrawActionElement(y_offset, ce.bounds.right - ce.bounds.left, line_height, GetPageElementSprite(*ce.pe)); break; + case SPET_BUTTON_PUSH: + case SPET_BUTTON_TILE: + case SPET_BUTTON_VEHICLE: { + const int height = FONT_HEIGHT_NORMAL; + const int tmargin = WD_BEVEL_TOP + WD_FRAMETEXT_TOP; + const int bmargin = WD_BEVEL_BOTTOM + WD_FRAMETEXT_BOTTOM; + const int width = ce.bounds.right - ce.bounds.left; + const int hmargin = width / 5; + const FrameFlags frame = this->active_button_id == ce.pe->index ? FR_LOWERED : FR_NONE; + const Colours bgcolour = StoryPageButtonData{ ce.pe->referenced_id }.GetColour(); + + DrawFrameRect(ce.bounds.left, ce.bounds.top - scrollpos, ce.bounds.right, ce.bounds.bottom - scrollpos - 1, bgcolour, frame); + + SetDParamStr(0, ce.pe->text); + DrawString(ce.bounds.left + WD_BEVEL_LEFT, ce.bounds.right - WD_BEVEL_RIGHT, ce.bounds.top + tmargin - scrollpos, STR_JUST_RAW_STRING, TC_WHITE, SA_CENTER); + break; + } + default: NOT_REACHED(); } } @@ -593,6 +813,7 @@ public: void OnResize() override { + this->InvalidateStoryPageElementLayout(); this->vscroll->SetCapacityFromWidget(this, WID_SB_PAGE_PANEL, WD_FRAMETEXT_TOP + WD_FRAMETEXT_BOTTOM); this->vscroll->SetCount(this->GetContentHeight()); } @@ -625,27 +846,14 @@ public: break; case WID_SB_PAGE_PANEL: { - uint clicked_y = this->vscroll->GetScrolledRowFromWidget(pt.y, this, WID_SB_PAGE_PANEL, WD_FRAMETEXT_TOP); - uint max_width = GetAvailablePageContentWidth(); + int clicked_y = this->vscroll->GetScrolledRowFromWidget(pt.y, this, WID_SB_PAGE_PANEL, WD_FRAMETEXT_TOP); + this->EnsureStoryPageElementLayout(); - /* Skip head rows. */ - uint head_height = this->GetHeadHeight(max_width); - if (clicked_y < head_height) return; - - /* Detect if a page element was clicked. */ - uint y = head_height; - uint element_vertical_dist = FONT_HEIGHT_NORMAL; - for (const StoryPageElement *const pe : this->story_page_elements) { - - y += element_vertical_dist; // margin row - - uint content_height = GetPageElementHeight(*pe, max_width); - if (clicked_y >= y && clicked_y < y + content_height) { - this->OnPageElementClick(*pe); + for (const LayoutCacheElement &ce : this->layout_cache) { + if (clicked_y >= ce.bounds.top && clicked_y < ce.bounds.bottom && pt.x >= ce.bounds.left && pt.x < ce.bounds.right) { + this->OnPageElementClick(*ce.pe); return; } - - y += content_height; } } } @@ -700,6 +908,52 @@ public: this->RefreshSelectedPage(); } } + + void OnTimeout() override + { + this->active_button_id = INVALID_STORY_PAGE_ELEMENT; + this->SetWidgetDirty(WID_SB_PAGE_PANEL); + } + + void OnPlaceObject(Point pt, TileIndex tile) override + { + const StoryPageElement *const pe = StoryPageElement::GetIfValid(this->active_button_id); + if (pe == nullptr || pe->type != SPET_BUTTON_TILE) { + ResetObjectToPlace(); + this->active_button_id = INVALID_STORY_PAGE_ELEMENT; + this->SetWidgetDirty(WID_SB_PAGE_PANEL); + return; + } + + DoCommandP(tile, pe->index, 0, CMD_STORY_PAGE_BUTTON); + ResetObjectToPlace(); + } + + bool OnVehicleSelect(const Vehicle *v) override + { + const StoryPageElement *const pe = StoryPageElement::GetIfValid(this->active_button_id); + if (pe == nullptr || pe->type != SPET_BUTTON_VEHICLE) { + ResetObjectToPlace(); + this->active_button_id = INVALID_STORY_PAGE_ELEMENT; + this->SetWidgetDirty(WID_SB_PAGE_PANEL); + return false; + } + + /* Check that the vehicle matches the requested type */ + StoryPageButtonData data{ pe->referenced_id }; + VehicleType wanted_vehtype = data.GetVehicleType(); + if (wanted_vehtype != VEH_INVALID && wanted_vehtype != v->type) return false; + + DoCommandP(0, pe->index, v->index, CMD_STORY_PAGE_BUTTON); + ResetObjectToPlace(); + return true; + } + + void OnPlaceObjectAbort() override + { + this->active_button_id = INVALID_STORY_PAGE_ELEMENT; + this->SetWidgetDirty(WID_SB_PAGE_PANEL); + } }; GUIStoryPageList::SortFunction * const StoryBookWindow::page_sorter_funcs[] = { @@ -742,6 +996,68 @@ static WindowDesc _story_book_desc( _nested_story_book_widgets, lengthof(_nested_story_book_widgets) ); +static CursorID TranslateStoryPageButtonCursor(StoryPageButtonCursor cursor) +{ + switch (cursor) { + case SPBC_MOUSE: return SPR_CURSOR_MOUSE; + case SPBC_ZZZ: return SPR_CURSOR_ZZZ; + case SPBC_BUOY: return SPR_CURSOR_BUOY; + case SPBC_QUERY: return SPR_CURSOR_QUERY; + case SPBC_HQ: return SPR_CURSOR_HQ; + case SPBC_SHIP_DEPOT: return SPR_CURSOR_SHIP_DEPOT; + case SPBC_SIGN: return SPR_CURSOR_SIGN; + case SPBC_TREE: return SPR_CURSOR_TREE; + case SPBC_BUY_LAND: return SPR_CURSOR_BUY_LAND; + case SPBC_LEVEL_LAND: return SPR_CURSOR_LEVEL_LAND; + case SPBC_TOWN: return SPR_CURSOR_TOWN; + case SPBC_INDUSTRY: return SPR_CURSOR_INDUSTRY; + case SPBC_ROCKY_AREA: return SPR_CURSOR_ROCKY_AREA; + case SPBC_DESERT: return SPR_CURSOR_DESERT; + case SPBC_TRANSMITTER: return SPR_CURSOR_TRANSMITTER; + case SPBC_AIRPORT: return SPR_CURSOR_AIRPORT; + case SPBC_DOCK: return SPR_CURSOR_DOCK; + case SPBC_CANAL: return SPR_CURSOR_CANAL; + case SPBC_LOCK: return SPR_CURSOR_LOCK; + case SPBC_RIVER: return SPR_CURSOR_RIVER; + case SPBC_AQUEDUCT: return SPR_CURSOR_AQUEDUCT; + case SPBC_BRIDGE: return SPR_CURSOR_BRIDGE; + case SPBC_RAIL_STATION: return SPR_CURSOR_RAIL_STATION; + case SPBC_TUNNEL_RAIL: return SPR_CURSOR_TUNNEL_RAIL; + case SPBC_TUNNEL_ELRAIL: return SPR_CURSOR_TUNNEL_ELRAIL; + case SPBC_TUNNEL_MONO: return SPR_CURSOR_TUNNEL_MONO; + case SPBC_TUNNEL_MAGLEV: return SPR_CURSOR_TUNNEL_MAGLEV; + case SPBC_AUTORAIL: return SPR_CURSOR_AUTORAIL; + case SPBC_AUTOELRAIL: return SPR_CURSOR_AUTOELRAIL; + case SPBC_AUTOMONO: return SPR_CURSOR_AUTOMONO; + case SPBC_AUTOMAGLEV: return SPR_CURSOR_AUTOMAGLEV; + case SPBC_WAYPOINT: return SPR_CURSOR_WAYPOINT; + case SPBC_RAIL_DEPOT: return SPR_CURSOR_RAIL_DEPOT; + case SPBC_ELRAIL_DEPOT: return SPR_CURSOR_ELRAIL_DEPOT; + case SPBC_MONO_DEPOT: return SPR_CURSOR_MONO_DEPOT; + case SPBC_MAGLEV_DEPOT: return SPR_CURSOR_MAGLEV_DEPOT; + case SPBC_CONVERT_RAIL: return SPR_CURSOR_CONVERT_RAIL; + case SPBC_CONVERT_ELRAIL: return SPR_CURSOR_CONVERT_ELRAIL; + case SPBC_CONVERT_MONO: return SPR_CURSOR_CONVERT_MONO; + case SPBC_CONVERT_MAGLEV: return SPR_CURSOR_CONVERT_MAGLEV; + case SPBC_AUTOROAD: return SPR_CURSOR_AUTOROAD; + case SPBC_AUTOTRAM: return SPR_CURSOR_AUTOTRAM; + case SPBC_ROAD_DEPOT: return SPR_CURSOR_ROAD_DEPOT; + case SPBC_BUS_STATION: return SPR_CURSOR_BUS_STATION; + case SPBC_TRUCK_STATION: return SPR_CURSOR_TRUCK_STATION; + case SPBC_ROAD_TUNNEL: return SPR_CURSOR_ROAD_TUNNEL; + case SPBC_CLONE_TRAIN: return SPR_CURSOR_CLONE_TRAIN; + case SPBC_CLONE_ROADVEH: return SPR_CURSOR_CLONE_ROADVEH; + case SPBC_CLONE_SHIP: return SPR_CURSOR_CLONE_SHIP; + case SPBC_CLONE_AIRPLANE: return SPR_CURSOR_CLONE_AIRPLANE; + case SPBC_DEMOLISH: return ANIMCURSOR_DEMOLISH; + case SPBC_LOWERLAND: return ANIMCURSOR_LOWERLAND; + case SPBC_RAISELAND: return ANIMCURSOR_RAISELAND; + case SPBC_PICKSTATION: return ANIMCURSOR_PICKSTATION; + case SPBC_BUILDSIGNALS: return ANIMCURSOR_BUILDSIGNALS; + default: return SPR_CURSOR_QUERY; + } +} + /** * Raise or create the story book window for \a company, at page \a page_id. * @param company 'Owner' of the story book, may be #INVALID_COMPANY.