diff --git a/CMake/Assets.cmake b/CMake/Assets.cmake index fb58bf831..e3ead57bc 100644 --- a/CMake/Assets.cmake +++ b/CMake/Assets.cmake @@ -158,6 +158,7 @@ set(devilutionx_assets lua_internal/get_lua_function_signature.lua lua/devilutionx/events.lua lua/inspect.lua + lua/mods/adria_refills_mana/init.lua lua/mods/clock/init.lua "lua/mods/Floating Numbers - Damage/init.lua" "lua/mods/Floating Numbers - XP/init.lua" diff --git a/Source/engine/demomode.cpp b/Source/engine/demomode.cpp index d284d6a31..1179c3421 100644 --- a/Source/engine/demomode.cpp +++ b/Source/engine/demomode.cpp @@ -127,7 +127,6 @@ struct { bool autoElixirPickup = false; bool autoOilPickup = false; bool autoPickupInTown = false; - bool adriaRefillsMana = false; bool autoEquipWeapons = false; bool autoEquipArmor = false; bool autoEquipHelms = false; @@ -166,7 +165,7 @@ void ReadSettings(FILE *in, uint8_t version) // NOLINT(readability-identifier-le DemoSettings.autoElixirPickup = ReadByte(in) != 0; DemoSettings.autoOilPickup = ReadByte(in) != 0; DemoSettings.autoPickupInTown = ReadByte(in) != 0; - DemoSettings.adriaRefillsMana = ReadByte(in) != 0; + (void)ReadByte(in); // adriaRefillsMana (removed feature, kept for backward compatibility) DemoSettings.autoEquipWeapons = ReadByte(in) != 0; DemoSettings.autoEquipArmor = ReadByte(in) != 0; DemoSettings.autoEquipHelms = ReadByte(in) != 0; @@ -195,7 +194,6 @@ void ReadSettings(FILE *in, uint8_t version) // NOLINT(readability-identifier-le { _("Auto Elixir Pickup"), DemoSettings.autoGoldPickup }, { _("Auto Oil Pickup"), DemoSettings.autoOilPickup }, { _("Auto Pickup in Town"), DemoSettings.autoPickupInTown }, - { _("Adria Refills Mana"), DemoSettings.adriaRefillsMana }, { _("Auto Equip Weapons"), DemoSettings.autoEquipWeapons }, { _("Auto Equip Armor"), DemoSettings.autoEquipArmor }, { _("Auto Equip Helms"), DemoSettings.autoEquipHelms }, @@ -231,7 +229,7 @@ void WriteSettings(FILE *out) WriteByte(out, static_cast(*options.Gameplay.autoElixirPickup)); WriteByte(out, static_cast(*options.Gameplay.autoOilPickup)); WriteByte(out, static_cast(*options.Gameplay.autoPickupInTown)); - WriteByte(out, static_cast(*options.Gameplay.adriaRefillsMana)); + WriteByte(out, 0); // adriaRefillsMana (removed feature, kept for backward compatibility) WriteByte(out, static_cast(*options.Gameplay.autoEquipWeapons)); WriteByte(out, static_cast(*options.Gameplay.autoEquipArmor)); WriteByte(out, static_cast(*options.Gameplay.autoEquipHelms)); @@ -650,7 +648,6 @@ void OverrideOptions() options.Gameplay.autoElixirPickup.SetValue(DemoSettings.autoElixirPickup); options.Gameplay.autoOilPickup.SetValue(DemoSettings.autoOilPickup); options.Gameplay.autoPickupInTown.SetValue(DemoSettings.autoPickupInTown); - options.Gameplay.adriaRefillsMana.SetValue(DemoSettings.adriaRefillsMana); options.Gameplay.autoEquipWeapons.SetValue(DemoSettings.autoEquipWeapons); options.Gameplay.autoEquipArmor.SetValue(DemoSettings.autoEquipArmor); options.Gameplay.autoEquipHelms.SetValue(DemoSettings.autoEquipHelms); diff --git a/Source/lua/lua_event.hpp b/Source/lua/lua_event.hpp new file mode 100644 index 000000000..b7c016107 --- /dev/null +++ b/Source/lua/lua_event.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace devilution { + +/** + * @brief Triggers a Lua event by name. + * This is a minimal header for code that only needs to trigger events. + */ +void LuaEvent(std::string_view name); +void LuaEvent(std::string_view name, std::string_view arg); + +} // namespace devilution diff --git a/Source/lua/lua_global.cpp b/Source/lua/lua_global.cpp index 4fc69075e..bd8358aa8 100644 --- a/Source/lua/lua_global.cpp +++ b/Source/lua/lua_global.cpp @@ -205,6 +205,9 @@ sol::environment CreateLuaSandbox() sandbox["require"] = lua["requireGen"](sandbox, CurrentLuaState->commonPackages, LuaLoadScriptFromAssets); + // Expose commonly used enums globally for mods + sandbox["SfxID"] = lua["SfxID"]; + return sandbox; } @@ -347,6 +350,21 @@ void LuaEvent(std::string_view name, const Player *player, uint32_t arg1) CallLuaEvent(name, player, arg1); } +void LuaEvent(std::string_view name, std::string_view arg) +{ + if (!CurrentLuaState.has_value()) { + return; + } + + const auto trigger = CurrentLuaState->events.traverse_get>(name, "trigger"); + if (!trigger.has_value() || !trigger->is()) { + LogError("events.{}.trigger is not a function", name); + return; + } + const sol::protected_function fn = trigger->as(); + SafeCallResult(fn(arg), /*optional=*/true); +} + sol::state &GetLuaState() { return CurrentLuaState->sol; diff --git a/Source/lua/lua_global.hpp b/Source/lua/lua_global.hpp index 252075335..24f70901e 100644 --- a/Source/lua/lua_global.hpp +++ b/Source/lua/lua_global.hpp @@ -15,6 +15,7 @@ void LuaInitialize(); void LuaReloadActiveMods(); void LuaShutdown(); void LuaEvent(std::string_view name); +void LuaEvent(std::string_view name, std::string_view arg); void LuaEvent(std::string_view name, const Player *player, int arg1, int arg2); void LuaEvent(std::string_view name, const Monster *monster, int arg1, int arg2); void LuaEvent(std::string_view name, const Player *player, uint32_t arg1); diff --git a/Source/lua/modules/audio.cpp b/Source/lua/modules/audio.cpp index f8ab0a7e2..73d5d5faa 100644 --- a/Source/lua/modules/audio.cpp +++ b/Source/lua/modules/audio.cpp @@ -1,9 +1,11 @@ #include "lua/modules/audio.hpp" +#include #include #include "effects.h" #include "lua/metadoc.hpp" +#include "sound_effect_enums.h" namespace devilution { @@ -14,10 +16,27 @@ bool IsValidSfx(int16_t psfx) return psfx >= 0 && psfx <= static_cast(SfxID::LAST); } +void RegisterSfxIDEnum(sol::state_view &lua) +{ + constexpr auto enumValues = magic_enum::enum_values(); + sol::table enumTable = lua.create_table(); + for (const auto enumValue : enumValues) { + const std::string_view name = magic_enum::enum_name(enumValue); + if (!name.empty() && name != "LAST" && name != "None") { + enumTable[name] = static_cast(enumValue); + } + } + // Add LAST and None explicitly + enumTable["LAST"] = static_cast(SfxID::LAST); + enumTable["None"] = static_cast(SfxID::None); + lua["SfxID"] = enumTable; +} + } // namespace sol::table LuaAudioModule(sol::state_view &lua) { + RegisterSfxIDEnum(lua); sol::table table = lua.create_table(); LuaSetDocFn(table, "playSfx", "(id: number)", @@ -25,6 +44,8 @@ sol::table LuaAudioModule(sol::state_view &lua) LuaSetDocFn(table, "playSfxLoc", "(id: number, x: number, y: number)", [](int16_t psfx, int x, int y) { if (IsValidSfx(psfx)) PlaySfxLoc(static_cast(psfx), { x, y }); }); + // Expose SfxID enum through the module table + table["SfxID"] = lua["SfxID"]; return table; } diff --git a/Source/lua/modules/player.cpp b/Source/lua/modules/player.cpp index 58b3ea359..3cc83ce35 100644 --- a/Source/lua/modules/player.cpp +++ b/Source/lua/modules/player.cpp @@ -4,6 +4,8 @@ #include +#include "effects.h" +#include "engine/backbuffer_state.hpp" #include "engine/point.hpp" #include "engine/random.hpp" #include "inv.h" @@ -103,6 +105,12 @@ void InitPlayerUserType(sol::state_view &lua) player._pMana = player._pMaxMana; player._pManaBase = player._pMaxManaBase; }); + LuaSetDocReadonlyProperty(playerType, "mana", "number", + "Current mana (readonly)", + [](Player &player) { return player._pMana >> 6; }); + LuaSetDocReadonlyProperty(playerType, "maxMana", "number", + "Maximum mana (readonly)", + [](Player &player) { return player._pMaxMana >> 6; }); } } // namespace diff --git a/Source/options.cpp b/Source/options.cpp index 86cc0f5b6..65ae9a892 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -72,7 +72,7 @@ namespace { void DiscoverMods() { // Add mods available by default: - std::unordered_set modNames = { "clock", "Floating Numbers - Damage", "Floating Numbers - XP" }; + std::unordered_set modNames = { "clock", "adria_refills_mana", "Floating Numbers - Damage", "Floating Numbers - XP" }; if (HaveHellfire()) { modNames.insert("Hellfire"); @@ -852,7 +852,6 @@ GameplayOptions::GameplayOptions() , autoElixirPickup("Auto Elixir Pickup", OptionEntryFlags::None, N_("Auto Elixir Pickup"), N_("Elixirs are automatically collected when in close proximity to the player."), false) , autoOilPickup("Auto Oil Pickup", OptionEntryFlags::OnlyHellfire, N_("Auto Oil Pickup"), N_("Oils are automatically collected when in close proximity to the player."), false) , autoPickupInTown("Auto Pickup in Town", OptionEntryFlags::None, N_("Auto Pickup in Town"), N_("Automatically pickup items in town."), false) - , adriaRefillsMana("Adria Refills Mana", OptionEntryFlags::None, N_("Adria Refills Mana"), N_("Adria will refill your mana when you visit her shop."), false) , autoEquipWeapons("Auto Equip Weapons", OptionEntryFlags::None, N_("Auto Equip Weapons"), N_("Weapons will be automatically equipped on pickup or purchase if enabled."), true) , autoEquipArmor("Auto Equip Armor", OptionEntryFlags::None, N_("Auto Equip Armor"), N_("Armor will be automatically equipped on pickup or purchase if enabled."), false) , autoEquipHelms("Auto Equip Helms", OptionEntryFlags::None, N_("Auto Equip Helms"), N_("Helms will be automatically equipped on pickup or purchase if enabled."), false) @@ -913,7 +912,6 @@ std::vector GameplayOptions::GetEntries() &numFullRejuPotionPickup, &autoPickupInTown, &disableCripplingShrines, - &adriaRefillsMana, &grabInput, &pauseOnFocusLoss, &skipLoadingScreenThresholdMs, diff --git a/Source/options.h b/Source/options.h index 3b8847054..7e4da902d 100644 --- a/Source/options.h +++ b/Source/options.h @@ -599,8 +599,6 @@ struct GameplayOptions : OptionCategoryBase { OptionEntryBoolean autoOilPickup; /** @brief Enable or Disable auto-pickup in town */ OptionEntryBoolean autoPickupInTown; - /** @brief Recover mana when talking to Adria. */ - OptionEntryBoolean adriaRefillsMana; /** @brief Automatically attempt to equip weapon-type items when picking them up. */ OptionEntryBoolean autoEquipWeapons; /** @brief Automatically attempt to equip armor-type items when picking them up. */ diff --git a/Source/stores.cpp b/Source/stores.cpp index 289efdaab..70c468f7c 100644 --- a/Source/stores.cpp +++ b/Source/stores.cpp @@ -21,6 +21,7 @@ #include "engine/render/text_render.hpp" #include "engine/trn.hpp" #include "game_mode.hpp" +#include "lua/lua_event.hpp" #include "minitext.h" #include "multi.h" #include "options.h" @@ -653,24 +654,8 @@ void StartSmithRepair() AddItemListBackButton(); } -void FillManaPlayer() -{ - if (!*GetOptions().Gameplay.adriaRefillsMana) - return; - - Player &myPlayer = *MyPlayer; - - if (myPlayer._pMana != myPlayer._pMaxMana) { - PlaySFX(SfxID::CastHealing); - } - myPlayer._pMana = myPlayer._pMaxMana; - myPlayer._pManaBase = myPlayer._pMaxManaBase; - RedrawComponent(PanelDrawComponent::Mana); -} - void StartWitch() { - FillManaPlayer(); IsTextFullSize = false; HasScrollbar = false; AddSText(0, 2, _("Witch's shack"), UiFlags::ColorWhitegold | UiFlags::AlignCenter, false); @@ -2215,6 +2200,37 @@ void StartStore(TalkID s) CloseGoldDrop(); ClearSText(0, NumStoreLines); ReleaseStoreBtn(); + + // Fire StoreOpened Lua event for main store entries + switch (s) { + case TalkID::Smith: + LuaEvent("StoreOpened", "griswold"); + break; + case TalkID::Witch: + LuaEvent("StoreOpened", "adria"); + break; + case TalkID::Boy: + LuaEvent("StoreOpened", "wirt"); + break; + case TalkID::Healer: + LuaEvent("StoreOpened", "pepin"); + break; + case TalkID::Storyteller: + LuaEvent("StoreOpened", "cain"); + break; + case TalkID::Tavern: + LuaEvent("StoreOpened", "ogden"); + break; + case TalkID::Drunk: + LuaEvent("StoreOpened", "farnham"); + break; + case TalkID::Barmaid: + LuaEvent("StoreOpened", "gillian"); + break; + default: + break; + } + switch (s) { case TalkID::Smith: StartSmith(); diff --git a/assets/lua/devilutionx/events.lua b/assets/lua/devilutionx/events.lua index de3f95331..dc795b27e 100644 --- a/assets/lua/devilutionx/events.lua +++ b/assets/lua/devilutionx/events.lua @@ -69,6 +69,10 @@ local events = { GameDrawComplete = CreateEvent(), __doc_GameDrawComplete = "Called every frame at the end.", + ---Called when opening a towner store. Passes the towner name as argument (e.g., "griswold", "adria", "pepin", "wirt", "cain"). + StoreOpened = CreateEvent(), + __doc_StoreOpened = "Called when opening a towner store. Passes the towner name as argument.", + ---Called when a Monster takes damage. OnMonsterTakeDamage = CreateEvent(), __doc_OnMonsterTakeDamage = "Called when a Monster takes damage.", diff --git a/assets/lua/mods/adria_refills_mana/init.lua b/assets/lua/mods/adria_refills_mana/init.lua new file mode 100644 index 000000000..f5ca744e0 --- /dev/null +++ b/assets/lua/mods/adria_refills_mana/init.lua @@ -0,0 +1,23 @@ +-- Adria Refills Mana Mod +-- When you visit Adria's shop, your mana is restored to full. + +local events = require("devilutionx.events") +local player = require("devilutionx.player") +local audio = require("devilutionx.audio") + +events.StoreOpened.add(function(townerName) + if townerName ~= "adria" then + return + end + + local p = player.self() + if p == nil then + return + end + + -- Restore mana if player has mana capacity and it's not already full + if p.maxMana > 0 and p.mana < p.maxMana then + audio.playSfx(audio.SfxID.CastHealing) + p:restoreFullMana() + end +end)