diff --git a/Frostbite2D/include/frostbite2D/audio/music.h b/Frostbite2D/include/frostbite2D/audio/music.h index a937b64..5f5eb41 100644 --- a/Frostbite2D/include/frostbite2D/audio/music.h +++ b/Frostbite2D/include/frostbite2D/audio/music.h @@ -19,6 +19,8 @@ public: static Ptr loadFromFile(const std::string& path); static Ptr loadFromMemory(const uint8* data, size_t size); static Ptr loadFromNpk(const std::string& audioPath); + static bool isPathPlaying(const std::string& path); + static std::string getCurrentPlayingPath(); ~Music(); diff --git a/Frostbite2D/include/frostbite2D/resource/audio_database.h b/Frostbite2D/include/frostbite2D/resource/audio_database.h index ffa09a7..9871060 100644 --- a/Frostbite2D/include/frostbite2D/resource/audio_database.h +++ b/Frostbite2D/include/frostbite2D/resource/audio_database.h @@ -13,10 +13,11 @@ namespace frostbite2D { * @brief 音频项类型枚举 */ enum class AudioEntryType { - None, ///< 无效 - Effect, ///< 音效 - Music, ///< 背景音乐 - Random ///< 随机音效组 + None, ///< invalid + Effect, ///< one-shot sound effect + Ambient, ///< looping ambient track + Music, ///< background music + Random ///< random sound group }; /** @@ -39,6 +40,16 @@ struct AudioMusic { int32 loopDelay = -1; ///< 循环延迟(-1 表示未设置) }; +/** + * @brief ????????? + */ +struct AudioAmbient { + std::string id; ///< ?????ID + std::string file; ///< ?????? + int32 loopDelay = -1; ///< ????????1 ????????? + int32 loopDelayRange = -1;///< ???????????1 ????????? +}; + /** * @brief 随机音效项 */ @@ -62,6 +73,7 @@ struct AudioEntry { AudioEntryType type = AudioEntryType::None; ///< 条目类型 const AudioEffect* effect = nullptr; ///< 音效指针(type == Effect 时有效) + const AudioAmbient* ambient = nullptr;///< ???????????ype == Ambient ?????? const AudioMusic* music = nullptr; ///< 音乐指针(type == Music 时有效) const AudioRandom* random = nullptr; ///< 随机组指针(type == Random 时有效) @@ -71,6 +83,9 @@ struct AudioEntry { /// @brief 判断是否为音效 bool isEffect() const { return type == AudioEntryType::Effect; } + /// @brief ???????????? + bool isAmbient() const { return type == AudioEntryType::Ambient; } + /// @brief 判断是否为音乐 bool isMusic() const { return type == AudioEntryType::Music; } @@ -81,6 +96,8 @@ struct AudioEntry { std::string getFilePath() const { if (type == AudioEntryType::Effect && effect) return effect->file; + if (type == AudioEntryType::Ambient && ambient) + return ambient->file; if (type == AudioEntryType::Music && music) return music->file; return ""; @@ -203,6 +220,13 @@ public: */ std::optional getEffect(const std::string& id); + /** + * @brief ???????????? + * @param id ?????ID + * @return ????????????????????? std::nullopt + */ + std::optional getAmbient(const std::string& id); + /** * @brief 获取音乐配置 * @param id 音乐 ID @@ -226,6 +250,11 @@ public: */ size_t effectCount() const; + /** + * @brief ??????????? + */ + size_t ambientCount() const; + /** * @brief 获取音乐数量 */ @@ -312,6 +341,7 @@ private: std::string selectFromRandom(const AudioRandom& randomGroup); std::unordered_map effectMap_; ///< 音效映射 + std::unordered_map ambientMap_; ///< ???????? std::unordered_map musicMap_; ///< 音乐映射 std::unordered_map randomMap_; ///< 随机组映射 diff --git a/Frostbite2D/src/frostbite2D/audio/music.cpp b/Frostbite2D/src/frostbite2D/audio/music.cpp index 2bf3e2a..c52c105 100644 --- a/Frostbite2D/src/frostbite2D/audio/music.cpp +++ b/Frostbite2D/src/frostbite2D/audio/music.cpp @@ -10,6 +10,9 @@ namespace frostbite2D { namespace { +Mix_Music* gCurrentMusicHandle = nullptr; +std::string gCurrentMusicPath; + std::string normalizeAudioLogicalPath(const std::string& path) { std::string normalized = path; std::replace(normalized.begin(), normalized.end(), '\\', '/'); @@ -24,6 +27,30 @@ bool startsWithPrefix(const std::string& value, const char* prefix) { value.compare(0, prefixLength, prefix) == 0; } +std::string canonicalizeAudioPlaybackPath(const std::string& path) { + std::string normalized = normalizeAudioLogicalPath(path); + if (startsWithPrefix(normalized, "assets/music/") || + startsWithPrefix(normalized, "assets/sounds/")) { + return normalized.substr(7); + } + return normalized; +} + +bool hasActiveMusicPlayback() { + return Mix_PlayingMusic() == 1 || Mix_PausedMusic() == 1; +} + +void clearCurrentMusicState() { + gCurrentMusicHandle = nullptr; + gCurrentMusicPath.clear(); +} + +void clearCurrentMusicStateIfStopped() { + if (!hasActiveMusicPlayback()) { + clearCurrentMusicState(); + } +} + } // namespace Ptr Music::loadFromPath(const std::string& path) { @@ -98,18 +125,37 @@ Ptr Music::loadFromNpk(const std::string& audioPath) { return nullptr; } - return loadFromMemory(dataOpt->data(), dataOpt->size()); + Ptr music = loadFromMemory(dataOpt->data(), dataOpt->size()); + if (music) { + music->path_ = canonicalizeAudioPlaybackPath(normalizedPath); + } + return music; +} + +bool Music::isPathPlaying(const std::string& path) { + clearCurrentMusicStateIfStopped(); + return hasActiveMusicPlayback() && + gCurrentMusicPath == canonicalizeAudioPlaybackPath(path); +} + +std::string Music::getCurrentPlayingPath() { + clearCurrentMusicStateIfStopped(); + return gCurrentMusicPath; } Music::Music(Mix_Music* music, const std::string& path) - : music_(music), path_(path) { + : music_(music), path_(canonicalizeAudioPlaybackPath(path)) { } Music::Music(Mix_Music* music, std::vector data, const std::string& path) - : music_(music), path_(path), data_(std::move(data)) { + : music_(music), path_(canonicalizeAudioPlaybackPath(path)), + data_(std::move(data)) { } Music::~Music() { + if (gCurrentMusicHandle == music_) { + clearCurrentMusicState(); + } if (music_) { Mix_FreeMusic(music_); music_ = nullptr; @@ -131,8 +177,11 @@ bool Music::play(int loops) { Mix_VolumeMusic(vol); if (Mix_PlayMusic(music_, loops) == 0) { + gCurrentMusicHandle = music_; + gCurrentMusicPath = path_; return true; } else { + clearCurrentMusicStateIfStopped(); SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to play music: %s", Mix_GetError()); return false; } @@ -151,7 +200,14 @@ bool Music::fadeIn(int ms, int loops) { int vol = static_cast(volume_ * AudioSystem::get().getMusicVolume() * AudioSystem::get().getMasterVolume() * MIX_MAX_VOLUME); Mix_VolumeMusic(vol); - return Mix_FadeInMusic(music_, loops, ms) == 0; + if (Mix_FadeInMusic(music_, loops, ms) == 0) { + gCurrentMusicHandle = music_; + gCurrentMusicPath = path_; + return true; + } + + clearCurrentMusicStateIfStopped(); + return false; } void Music::pause() { @@ -173,13 +229,20 @@ void Music::stop() { return; } Mix_HaltMusic(); + if (gCurrentMusicHandle == music_) { + clearCurrentMusicState(); + } } bool Music::fadeOut(int ms) { if (!music_) { return false; } - return Mix_FadeOutMusic(ms) == 0; + bool result = Mix_FadeOutMusic(ms) == 0; + if (result && gCurrentMusicHandle == music_) { + clearCurrentMusicState(); + } + return result; } void Music::setVolume(float volume) { diff --git a/Frostbite2D/src/frostbite2D/resource/audio_database.cpp b/Frostbite2D/src/frostbite2D/resource/audio_database.cpp index 6c2fb10..bcfdbdc 100644 --- a/Frostbite2D/src/frostbite2D/resource/audio_database.cpp +++ b/Frostbite2D/src/frostbite2D/resource/audio_database.cpp @@ -95,6 +95,42 @@ bool AudioDatabase::loadFromString(const std::string& xmlContent) { } effectMap_[effect.id] = std::move(effect); + } else if (strcmp(name, "AMBIENT") == 0) { + rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID"); + rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE"); + + if (!idAttr || !fileAttr) { + continue; + } + + if (strlen(idAttr->value()) == 0) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "AudioDatabase: skip invalid AMBIENT entry (empty ID)"); + continue; + } + + if (strlen(fileAttr->value()) == 0) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "AudioDatabase: skip AMBIENT entry with empty FILE: %s", + idAttr->value()); + continue; + } + + AudioAmbient ambient; + ambient.id = idAttr->value(); + ambient.file = fileAttr->value(); + + rapidxml::xml_attribute<>* loopDelayAttr = node->first_attribute("LOOP_DELAY"); + if (loopDelayAttr) { + ambient.loopDelay = std::stoi(loopDelayAttr->value()); + } + + rapidxml::xml_attribute<>* loopDelayRangeAttr = node->first_attribute("LOOP_DELAY_RANGE"); + if (loopDelayRangeAttr) { + ambient.loopDelayRange = std::stoi(loopDelayRangeAttr->value()); + } + + ambientMap_[ambient.id] = std::move(ambient); } else if (strcmp(name, "MUSIC") == 0) { rapidxml::xml_attribute<>* idAttr = node->first_attribute("ID"); rapidxml::xml_attribute<>* fileAttr = node->first_attribute("FILE"); @@ -162,15 +198,17 @@ bool AudioDatabase::loadFromString(const std::string& xmlContent) { } loaded_ = true; - - SDL_Log("AudioDatabase: 加载完成 - %zu 个音效, %zu 个音乐, %zu 个随机组", - effectMap_.size(), musicMap_.size(), randomMap_.size()); + + SDL_Log("AudioDatabase: loaded - %zu effects, %zu ambients, %zu musics, %zu random groups", + effectMap_.size(), ambientMap_.size(), musicMap_.size(), + randomMap_.size()); return true; } void AudioDatabase::clear() { effectMap_.clear(); + ambientMap_.clear(); musicMap_.clear(); randomMap_.clear(); loaded_ = false; @@ -194,6 +232,13 @@ std::optional AudioDatabase::get(const std::string& id) { return entry; } + auto ambientIt = ambientMap_.find(id); + if (ambientIt != ambientMap_.end()) { + entry.type = AudioEntryType::Ambient; + entry.ambient = &ambientIt->second; + return entry; + } + auto musicIt = musicMap_.find(id); if (musicIt != musicMap_.end()) { entry.type = AudioEntryType::Music; @@ -235,6 +280,10 @@ std::optional AudioDatabase::getFilePath(const std::string& id) { auto soundIdOpt = getSound(id); if (!soundIdOpt) { + auto ambientIt = ambientMap_.find(id); + if (ambientIt != ambientMap_.end()) { + return ambientIt->second.file; + } auto musicIt = musicMap_.find(id); if (musicIt != musicMap_.end()) { return musicIt->second.file; @@ -263,6 +312,18 @@ std::optional AudioDatabase::getEffect(const std::string& id return std::nullopt; } +std::optional AudioDatabase::getAmbient(const std::string& id) { + if (!loaded_) { + return std::nullopt; + } + + auto it = ambientMap_.find(id); + if (it != ambientMap_.end()) { + return &it->second; + } + return std::nullopt; +} + std::optional AudioDatabase::getMusic(const std::string& id) { if (!loaded_) { return std::nullopt; @@ -291,6 +352,10 @@ size_t AudioDatabase::effectCount() const { return effectMap_.size(); } +size_t AudioDatabase::ambientCount() const { + return ambientMap_.size(); +} + size_t AudioDatabase::musicCount() const { return musicMap_.size(); } @@ -364,7 +429,8 @@ bool AudioDatabase::has(const std::string& id) { if (!loaded_) { return false; } - return effectMap_.count(id) > 0 || musicMap_.count(id) > 0 || randomMap_.count(id) > 0; + return effectMap_.count(id) > 0 || ambientMap_.count(id) > 0 || + musicMap_.count(id) > 0 || randomMap_.count(id) > 0; } std::string AudioDatabase::filePath(const std::string& id) { @@ -395,6 +461,9 @@ AudioEntryType AudioDatabase::typeOf(const std::string& id) { if (effectMap_.count(id) > 0) { return AudioEntryType::Effect; } + if (ambientMap_.count(id) > 0) { + return AudioEntryType::Ambient; + } if (musicMap_.count(id) > 0) { return AudioEntryType::Music; } diff --git a/Game/include/audio/MapAudioController.h b/Game/include/audio/MapAudioController.h new file mode 100644 index 0000000..0bee039 --- /dev/null +++ b/Game/include/audio/MapAudioController.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace frostbite2D { + +struct MapAudioTrack { + std::string id; + std::string path; +}; + +struct MapAudioState { + std::optional bgmTrack; + std::vector ambientLoops; +}; + +class MapAudioController { +public: + static MapAudioController& get(); + + void ApplyMapAudio(const MapAudioState& state); + void ClearMapAudio(); + +private: + struct ActiveAmbientLoop { + MapAudioTrack track; + RefPtr sound; + int channel = -1; + }; + + MapAudioController() = default; + + void ApplyBgmTrack(const std::optional& bgmTrack); + void ApplyAmbientLoops(const std::vector& ambientLoops); + void StopAmbientLoop(ActiveAmbientLoop& loop); + static std::string NormalizeTrackKey(const MapAudioTrack& track); + + std::optional currentBgmTrack_; + RefPtr currentBgmMusic_; + std::unordered_map activeAmbientLoops_; +}; + +} // namespace frostbite2D diff --git a/Game/include/character/CharacterActionTypes.h b/Game/include/character/CharacterActionTypes.h index 161fec2..6f484ba 100644 --- a/Game/include/character/CharacterActionTypes.h +++ b/Game/include/character/CharacterActionTypes.h @@ -1,5 +1,6 @@ #pragma once +#include "common/math/GameMath.h" #include #include #include @@ -81,8 +82,8 @@ struct CharacterMotor { float gravity = 1600.0f; void SetGroundPosition(const Vec2& groundPosition) { - position.x = RoundWorldCoordinate(groundPosition.x); - position.y = RoundWorldCoordinate(groundPosition.y); + position.x = gameMath::RoundWorldCoordinate(groundPosition.x); + position.y = gameMath::RoundWorldCoordinate(groundPosition.y); positionRemainder_ = Vec2::Zero(); } @@ -167,10 +168,6 @@ struct CharacterMotor { } private: - static int32 RoundWorldCoordinate(float value) { - return static_cast(std::lround(value)); - } - static int32 ConsumeWholeUnits(float& remainder) { if (remainder >= 1.0f) { int32 wholeUnits = static_cast(std::floor(remainder)); diff --git a/Game/include/common/GameDebugActor.h b/Game/include/common/debug/GameDebugActor.h similarity index 100% rename from Game/include/common/GameDebugActor.h rename to Game/include/common/debug/GameDebugActor.h diff --git a/Game/include/common/math/GameMath.h b/Game/include/common/math/GameMath.h new file mode 100644 index 0000000..0461c78 --- /dev/null +++ b/Game/include/common/math/GameMath.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace frostbite2D::gameMath { + +int32 RoundWorldCoordinate(float value); +Vec2 MakeIntegerWorldPoint(int x, int y); +Vec3 MakeIntegerWorldPosition(int x, int y, int z); + +bool IsPointOnSegment(const Vec2& point, const Vec2& start, const Vec2& end); +bool IsPointInPolygon(const std::vector& polygon, const Vec2& point); +bool IsPointMovable(const std::vector& polygon, int x, int y); + +std::vector BuildRectPolygon(const Rect& rect); +std::vector BuildPolygonFillRects(const std::vector& polygon); + +} // namespace frostbite2D::gameMath diff --git a/Game/include/map/GameMap.h b/Game/include/map/GameMap.h index 763ad6d..8bcf485 100644 --- a/Game/include/map/GameMap.h +++ b/Game/include/map/GameMap.h @@ -49,6 +49,7 @@ public: size_t FindMoveAreaIndex(const Vec3& curPos) const; const std::vector& GetMoveAreaInfo() const; size_t GetMoveAreaCount() const { return moveArea_.size(); } + game::MoveAreaBounds GetMovablePositionBounds(size_t index) const; Rect GetMovablePositionArea(size_t index) const; const std::string& GetMapPath() const { return mapConfig_.mapPath; } void SetDebugHighlightedMoveAreaIndex(size_t index); diff --git a/Game/include/world/GameTown.h b/Game/include/world/GameTown.h index 251a285..bf0611f 100644 --- a/Game/include/world/GameTown.h +++ b/Game/include/world/GameTown.h @@ -25,6 +25,7 @@ public: int GetCurAreaIndex() const { return curMapIndex_; } RefPtr GetArea(int index) const; + RefPtr GetCurrentArea() const; const std::vector& GetAreas() const { return mapList_; } void Update(float deltaTime) override; diff --git a/Game/include/world/GameWorld.h b/Game/include/world/GameWorld.h index 4272a75..c1b0ce9 100644 --- a/Game/include/world/GameWorld.h +++ b/Game/include/world/GameWorld.h @@ -7,6 +7,11 @@ namespace frostbite2D { +class Actor; +class CharacterObject; +class GameDebugUIScene; +class GameMap; + class GameWorld : public Scene { public: GameWorld(); @@ -19,6 +24,13 @@ public: void AddCharacter(RefPtr actor, int townId); void MoveCharacter(RefPtr actor, int townId, int area); void RequestMoveCharacter(RefPtr actor, int townId, int area); + void UpdateMoveAreaSuppression(GameMap* map, size_t moveAreaIndex); + bool ShouldSuppressMoveArea(GameMap* map, size_t moveAreaIndex) const; + + Actor* GetMainActor() const; + CharacterObject* GetMainCharacter() const; + GameTown* GetCurrentTown() const; + GameMap* GetCurrentMap() const; static GameWorld* GetWorld(); @@ -29,13 +41,25 @@ private: int area = -1; }; + struct SuppressedMoveArea { + GameMap* map = nullptr; + size_t moveAreaIndex = GameMap::kInvalidMoveAreaIndex; + }; + bool InitWorld(); + bool InitMainCharacter(int townId); void ProcessPendingCharacterMove(); + void EnsureDebugScene(); + void RefreshDebugContext(); + void SetSuppressedMoveArea(GameMap* map, size_t moveAreaIndex); + void ClearSuppressedMoveArea(); std::map townPathMap_; std::map> townMap_; RefPtr mainActor_; + RefPtr debugScene_; std::optional pendingCharacterMove_; + std::optional suppressedMoveArea_; int curTown_ = -1; bool initialized_ = false; }; diff --git a/Game/src/audio/MapAudioController.cpp b/Game/src/audio/MapAudioController.cpp new file mode 100644 index 0000000..0d72090 --- /dev/null +++ b/Game/src/audio/MapAudioController.cpp @@ -0,0 +1,172 @@ +#include "audio/MapAudioController.h" +#include +#include +#include + +namespace frostbite2D { + +namespace { + +std::string NormalizeAudioPath(const std::string& path) { + std::string normalized = path; + std::replace(normalized.begin(), normalized.end(), '\\', '/'); + std::transform(normalized.begin(), normalized.end(), normalized.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return normalized; +} + +} // namespace + +MapAudioController& MapAudioController::get() { + static MapAudioController instance; + return instance; +} + +void MapAudioController::ApplyMapAudio(const MapAudioState& state) { + ApplyBgmTrack(state.bgmTrack); + ApplyAmbientLoops(state.ambientLoops); +} + +void MapAudioController::ClearMapAudio() { + if (currentBgmMusic_) { + currentBgmMusic_->stop(); + currentBgmMusic_.Reset(); + } + currentBgmTrack_.reset(); + + for (auto& [_, loop] : activeAmbientLoops_) { + StopAmbientLoop(loop); + } + activeAmbientLoops_.clear(); +} + +void MapAudioController::ApplyBgmTrack( + const std::optional& bgmTrack) { + if (!bgmTrack || bgmTrack->path.empty()) { + if (currentBgmMusic_) { + currentBgmMusic_->stop(); + currentBgmMusic_.Reset(); + } + currentBgmTrack_.reset(); + return; + } + + const std::string desiredKey = NormalizeTrackKey(*bgmTrack); + const bool sameTrack = + currentBgmTrack_ && NormalizeTrackKey(*currentBgmTrack_) == desiredKey; + if (sameTrack && Music::isPathPlaying(bgmTrack->path)) { + return; + } + + RefPtr music = Music::loadFromPath(bgmTrack->path); + if (!music) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "MapAudioController: failed to load bgm %s", + bgmTrack->path.c_str()); + return; + } + + if (!music->play(-1)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "MapAudioController: failed to play bgm %s", + bgmTrack->path.c_str()); + return; + } + + currentBgmMusic_ = music; + currentBgmTrack_ = *bgmTrack; +} + +void MapAudioController::ApplyAmbientLoops( + const std::vector& ambientLoops) { + std::unordered_map desiredLoops; + desiredLoops.reserve(ambientLoops.size()); + for (const auto& loop : ambientLoops) { + if (loop.path.empty()) { + continue; + } + desiredLoops.emplace(NormalizeTrackKey(loop), loop); + } + + for (auto it = activeAmbientLoops_.begin(); it != activeAmbientLoops_.end();) { + if (desiredLoops.find(it->first) != desiredLoops.end()) { + ++it; + continue; + } + + StopAmbientLoop(it->second); + it = activeAmbientLoops_.erase(it); + } + + for (const auto& [key, track] : desiredLoops) { + auto activeIt = activeAmbientLoops_.find(key); + if (activeIt != activeAmbientLoops_.end()) { + ActiveAmbientLoop& loop = activeIt->second; + if (NormalizeAudioPath(loop.track.path) == NormalizeAudioPath(track.path) && + Sound::isPlaying(loop.channel)) { + continue; + } + + StopAmbientLoop(loop); + loop.track = track; + if (!loop.sound || + NormalizeAudioPath(loop.sound->getPath()) != + NormalizeAudioPath(track.path)) { + loop.sound = Sound::loadFromPath(track.path); + } + if (!loop.sound) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "MapAudioController: failed to load ambient %s", + track.path.c_str()); + continue; + } + + int channel = loop.sound->playLoop(); + if (channel == -1) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "MapAudioController: failed to play ambient %s", + track.path.c_str()); + continue; + } + + loop.channel = channel; + continue; + } + + ActiveAmbientLoop loop; + loop.track = track; + loop.sound = Sound::loadFromPath(track.path); + if (!loop.sound) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "MapAudioController: failed to load ambient %s", + track.path.c_str()); + continue; + } + + loop.channel = loop.sound->playLoop(); + if (loop.channel == -1) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "MapAudioController: failed to play ambient %s", + track.path.c_str()); + continue; + } + + activeAmbientLoops_.emplace(key, std::move(loop)); + } +} + +void MapAudioController::StopAmbientLoop(ActiveAmbientLoop& loop) { + if (loop.channel >= 0) { + Sound::stop(loop.channel); + loop.channel = -1; + } +} + +std::string MapAudioController::NormalizeTrackKey(const MapAudioTrack& track) { + if (!track.id.empty()) { + return NormalizeAudioPath(track.id); + } + return NormalizeAudioPath(track.path); +} + +} // namespace frostbite2D diff --git a/Game/src/bootstrap/main.cpp b/Game/src/bootstrap/main.cpp index df100e0..f5eba43 100644 --- a/Game/src/bootstrap/main.cpp +++ b/Game/src/bootstrap/main.cpp @@ -20,7 +20,6 @@ #include #include #include -#include "scene/GameMapTestScene.h" #include "world/GameWorld.h" using namespace frostbite2D; @@ -144,10 +143,9 @@ int main(int argc, char **argv) { StartupTrace::mark("before SceneManager::ReplaceScene"); { - ScopedStartupTrace stageTrace( - "SceneManager::ReplaceScene(GameMapTestScene)"); - auto testMapScene = MakePtr(); - SceneManager::get().ReplaceScene(testMapScene); + ScopedStartupTrace stageTrace("SceneManager::ReplaceScene(GameWorld)"); + auto gameWorld = MakePtr(); + SceneManager::get().ReplaceScene(gameWorld); } StartupTrace::mark("after SceneManager::ReplaceScene"); diff --git a/Game/src/character/CharacterObject.cpp b/Game/src/character/CharacterObject.cpp index 8445b4a..0d6875c 100644 --- a/Game/src/character/CharacterObject.cpp +++ b/Game/src/character/CharacterObject.cpp @@ -1,8 +1,8 @@ #include "character/CharacterObject.h" +#include "common/math/GameMath.h" #include "map/GameMap.h" #include "world/GameWorld.h" #include -#include #include #include #include @@ -43,10 +43,6 @@ Vec3 ToWorldVector(const CharacterWorldPosition& position) { static_cast(position.z)); } -int32 RoundWorldCoordinate(float value) { - return static_cast(std::lround(value)); -} - } // namespace bool CharacterObject::Construction(int jobId) { @@ -199,9 +195,9 @@ void CharacterObject::ApplyMapMovementConstraints( static_cast(motor_.position.y - previousPosition.y), static_cast(motor_.position.z - previousPosition.z)); Vec3 resolvedWorldPos = map->CheckIsItMovable(previousWorldPos, worldOffset); - motor_.position.x = RoundWorldCoordinate(resolvedWorldPos.x); - motor_.position.y = RoundWorldCoordinate(resolvedWorldPos.y); - motor_.position.z = RoundWorldCoordinate(resolvedWorldPos.z); + motor_.position.x = gameMath::RoundWorldCoordinate(resolvedWorldPos.x); + motor_.position.y = gameMath::RoundWorldCoordinate(resolvedWorldPos.y); + motor_.position.z = gameMath::RoundWorldCoordinate(resolvedWorldPos.z); } void CharacterObject::QueueMapTransitionIfNeeded() { @@ -210,16 +206,24 @@ void CharacterObject::QueueMapTransitionIfNeeded() { return; } - GameMap::MapMoveArea target; - if (!map->TryGetMoveAreaTarget(ToWorldVector(motor_.position), target)) { - return; - } - GameWorld* world = GameWorld::GetWorld(); if (!world) { return; } + size_t moveAreaIndex = map->FindMoveAreaIndex(ToWorldVector(motor_.position)); + world->UpdateMoveAreaSuppression(map, moveAreaIndex); + if (moveAreaIndex == GameMap::kInvalidMoveAreaIndex || + world->ShouldSuppressMoveArea(map, moveAreaIndex)) { + return; + } + + const auto& moveAreaInfo = map->GetMoveAreaInfo(); + if (moveAreaIndex >= moveAreaInfo.size()) { + return; + } + + const GameMap::MapMoveArea& target = moveAreaInfo[moveAreaIndex]; world->RequestMoveCharacter(RefPtr(this), target.town, target.area); } diff --git a/Game/src/common/GameDebugActor.cpp b/Game/src/common/debug/GameDebugActor.cpp similarity index 99% rename from Game/src/common/GameDebugActor.cpp rename to Game/src/common/debug/GameDebugActor.cpp index 1d46e3f..740cc61 100644 --- a/Game/src/common/GameDebugActor.cpp +++ b/Game/src/common/debug/GameDebugActor.cpp @@ -1,4 +1,4 @@ -#include "common/GameDebugActor.h" +#include "common/debug/GameDebugActor.h" #include "character/CharacterObject.h" #include "map/GameMap.h" #include "ui/NineSliceActor.h" diff --git a/Game/src/common/math/GameMath.cpp b/Game/src/common/math/GameMath.cpp new file mode 100644 index 0000000..ec4e9a0 --- /dev/null +++ b/Game/src/common/math/GameMath.cpp @@ -0,0 +1,148 @@ +#include "common/math/GameMath.h" +#include +#include + +namespace frostbite2D::gameMath { +namespace { + +constexpr double kPolygonEpsilon = 0.001; + +} // namespace + +int32 RoundWorldCoordinate(float value) { + return static_cast(std::lround(value)); +} + +Vec2 MakeIntegerWorldPoint(int x, int y) { + return Vec2(static_cast(x), static_cast(y)); +} + +Vec3 MakeIntegerWorldPosition(int x, int y, int z) { + return Vec3(static_cast(x), static_cast(y), + static_cast(z)); +} + +bool IsPointOnSegment(const Vec2& point, const Vec2& start, const Vec2& end) { + Vec2 segment = end - start; + Vec2 toPoint = point - start; + double cross = static_cast(segment.cross(toPoint)); + if (std::abs(cross) > kPolygonEpsilon) { + return false; + } + + double minX = std::min(start.x, end.x) - kPolygonEpsilon; + double maxX = std::max(start.x, end.x) + kPolygonEpsilon; + double minY = std::min(start.y, end.y) - kPolygonEpsilon; + double maxY = std::max(start.y, end.y) + kPolygonEpsilon; + return point.x >= minX && point.x <= maxX && point.y >= minY && + point.y <= maxY; +} + +bool IsPointInPolygon(const std::vector& polygon, const Vec2& point) { + if (polygon.size() < 3) { + return false; + } + + bool inside = false; + for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) { + const Vec2& start = polygon[j]; + const Vec2& end = polygon[i]; + if (IsPointOnSegment(point, start, end)) { + return true; + } + + bool crossesScanline = (start.y > point.y) != (end.y > point.y); + if (!crossesScanline) { + continue; + } + + double intersectX = + static_cast(start.x) + + (static_cast(point.y) - static_cast(start.y)) * + (static_cast(end.x) - static_cast(start.x)) / + (static_cast(end.y) - static_cast(start.y)); + if (intersectX >= static_cast(point.x) - kPolygonEpsilon) { + inside = !inside; + } + } + + return inside; +} + +bool IsPointMovable(const std::vector& polygon, int x, int y) { + return IsPointInPolygon(polygon, MakeIntegerWorldPoint(x, y)); +} + +std::vector BuildRectPolygon(const Rect& rect) { + if (rect.empty()) { + return {}; + } + + return { + Vec2(rect.left(), rect.top()), + Vec2(rect.right(), rect.top()), + Vec2(rect.right(), rect.bottom()), + Vec2(rect.left(), rect.bottom()), + }; +} + +std::vector BuildPolygonFillRects(const std::vector& polygon) { + std::vector fillRects; + if (polygon.size() < 3) { + return fillRects; + } + + float minY = polygon.front().y; + float maxY = polygon.front().y; + for (const auto& point : polygon) { + minY = std::min(minY, point.y); + maxY = std::max(maxY, point.y); + } + + int scanlineMinY = static_cast(std::floor(minY)); + int scanlineMaxY = static_cast(std::ceil(maxY)) - 1; + if (scanlineMaxY < scanlineMinY) { + return fillRects; + } + + for (int y = scanlineMinY; y <= scanlineMaxY; ++y) { + float scanY = static_cast(y) + 0.5f; + std::vector intersections; + intersections.reserve(polygon.size()); + + for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) { + const Vec2& start = polygon[j]; + const Vec2& end = polygon[i]; + if (start.y == end.y) { + continue; + } + + float edgeMinY = std::min(start.y, end.y); + float edgeMaxY = std::max(start.y, end.y); + if (scanY < edgeMinY || scanY >= edgeMaxY) { + continue; + } + + float t = (scanY - start.y) / (end.y - start.y); + intersections.push_back(start.x + (end.x - start.x) * t); + } + + if (intersections.size() < 2) { + continue; + } + + std::sort(intersections.begin(), intersections.end()); + for (size_t i = 0; i + 1 < intersections.size(); i += 2) { + float left = std::floor(intersections[i]); + float right = std::ceil(intersections[i + 1]); + float width = right - left; + if (width > 0.0f) { + fillRects.emplace_back(left, static_cast(y), width, 1.0f); + } + } + } + + return fillRects; +} + +} // namespace frostbite2D::gameMath diff --git a/Game/src/map/GameMap.cpp b/Game/src/map/GameMap.cpp index 8de1d6a..8086ab7 100644 --- a/Game/src/map/GameMap.cpp +++ b/Game/src/map/GameMap.cpp @@ -1,3 +1,5 @@ +#include "audio/MapAudioController.h" +#include "common/math/GameMath.h" #include "map/GameMap.h" #include #include @@ -24,71 +26,6 @@ static const int kLayerOrders[] = { constexpr int kTileRootZOrder = -1000000; constexpr float kExtendedTileStepY = 120.0f; -constexpr double kPolygonEpsilon = 0.001; - -int RoundWorldCoordinate(float value) { - return static_cast(std::lround(value)); -} - -Vec2 MakeIntegerWorldPoint(int x, int y) { - return Vec2(static_cast(x), static_cast(y)); -} - -Vec3 MakeIntegerWorldPosition(int x, int y, int z) { - return Vec3(static_cast(x), static_cast(y), - static_cast(z)); -} - -bool IsPointOnSegment(const Vec2& point, const Vec2& start, const Vec2& end) { - Vec2 segment = end - start; - Vec2 toPoint = point - start; - double cross = static_cast(segment.cross(toPoint)); - if (std::abs(cross) > kPolygonEpsilon) { - return false; - } - - double minX = std::min(start.x, end.x) - kPolygonEpsilon; - double maxX = std::max(start.x, end.x) + kPolygonEpsilon; - double minY = std::min(start.y, end.y) - kPolygonEpsilon; - double maxY = std::max(start.y, end.y) + kPolygonEpsilon; - return point.x >= minX && point.x <= maxX && point.y >= minY && - point.y <= maxY; -} - -bool IsPointInPolygon(const std::vector& polygon, const Vec2& point) { - if (polygon.size() < 3) { - return false; - } - - bool inside = false; - for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) { - const Vec2& start = polygon[j]; - const Vec2& end = polygon[i]; - if (IsPointOnSegment(point, start, end)) { - return true; - } - - bool crossesScanline = (start.y > point.y) != (end.y > point.y); - if (!crossesScanline) { - continue; - } - - double intersectX = - static_cast(start.x) + - (static_cast(point.y) - static_cast(start.y)) * - (static_cast(end.x) - static_cast(start.x)) / - (static_cast(end.y) - static_cast(start.y)); - if (intersectX >= static_cast(point.x) - kPolygonEpsilon) { - inside = !inside; - } - } - - return inside; -} - -bool IsPointMovable(const std::vector& polygon, int x, int y) { - return IsPointInPolygon(polygon, MakeIntegerWorldPoint(x, y)); -} // 地图里的 tile/img 统一走这里创建,失败时返回空 Sprite // 占位,避免整张地图中断。 @@ -102,6 +39,49 @@ Ptr createMapSprite(const std::string& path, int index) { return sprite; } +MapAudioState BuildMapAudioState(const game::MapConfig& mapConfig) { + MapAudioState state; + if (mapConfig.soundIds.empty()) { + return state; + } + + auto& audioDatabase = AudioDatabase::get(); + for (const auto& soundId : mapConfig.soundIds) { + AudioEntryType type = audioDatabase.typeOf(soundId); + std::string filePath = audioDatabase.filePath(soundId); + if (filePath.empty()) { + continue; + } + + if (type == AudioEntryType::Music) { + if (!state.bgmTrack) { + state.bgmTrack = MapAudioTrack{soundId, filePath}; + } + continue; + } + + if (type == AudioEntryType::Ambient) { + state.ambientLoops.push_back(MapAudioTrack{soundId, filePath}); + continue; + } + + if (type == AudioEntryType::Effect) { + SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, + "GameMap: ignore one-shot map sound %s in [%s]", + soundId.c_str(), mapConfig.mapPath.c_str()); + continue; + } + + if (type == AudioEntryType::Random) { + SDL_LogVerbose(SDL_LOG_CATEGORY_APPLICATION, + "GameMap: ignore random map audio %s in [%s]", + soundId.c_str(), mapConfig.mapPath.c_str()); + } + } + + return state; +} + } // namespace GameMap::GameMap() { @@ -172,23 +152,7 @@ bool GameMap::LoadMap(const std::string &mapName) { } void GameMap::Enter() { - // 地图进入时尝试找到第一个可播放的 BGM 并循环播放。 - if (!mapConfig_.soundIds.empty()) { - auto& audioDatabase = AudioDatabase::get(); - for (const auto& soundId : mapConfig_.soundIds) { - if (audioDatabase.typeOf(soundId) != AudioEntryType::Music) { - continue; - } - std::string filePath = audioDatabase.filePath(soundId); - if (filePath.empty()) { - continue; - } - currentMusic_ = Music::loadFromPath(filePath); - if (currentMusic_ && currentMusic_->play(-1)) { - break; - } - } - } + MapAudioController::get().ApplyMapAudio(BuildMapAudioState(mapConfig_)); } void GameMap::Update(float deltaTime) { Actor::Update(deltaTime); } @@ -448,28 +412,28 @@ void GameMap::AddObject(RefPtr object) { } Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const { - int currentX = RoundWorldCoordinate(curPos.x); - int currentY = RoundWorldCoordinate(curPos.y); - int targetX = RoundWorldCoordinate(curPos.x + posOffset.x); - int targetY = RoundWorldCoordinate(curPos.y + posOffset.y); - int targetZ = RoundWorldCoordinate(curPos.z + posOffset.z); - Vec3 result = MakeIntegerWorldPosition(currentX, currentY, targetZ); + int currentX = gameMath::RoundWorldCoordinate(curPos.x); + int currentY = gameMath::RoundWorldCoordinate(curPos.y); + int targetX = gameMath::RoundWorldCoordinate(curPos.x + posOffset.x); + int targetY = gameMath::RoundWorldCoordinate(curPos.y + posOffset.y); + int targetZ = gameMath::RoundWorldCoordinate(curPos.z + posOffset.z); + Vec3 result = gameMath::MakeIntegerWorldPosition(currentX, currentY, targetZ); if (!movableAreaCheckEnabled_) { - return MakeIntegerWorldPosition(targetX, targetY, targetZ); + return gameMath::MakeIntegerWorldPosition(targetX, targetY, targetZ); } if (movablePolygon_.size() < 3) { - return MakeIntegerWorldPosition(targetX, targetY, targetZ); + return gameMath::MakeIntegerWorldPosition(targetX, targetY, targetZ); } // Prefer the full destination first; only fall back to single-axis sliding // when the combined move would leave the movable polygon. - if (IsPointMovable(movablePolygon_, targetX, targetY)) { + if (gameMath::IsPointMovable(movablePolygon_, targetX, targetY)) { result.x = static_cast(targetX); result.y = static_cast(targetY); return result; } - bool isXValid = IsPointMovable(movablePolygon_, targetX, currentY); - bool isYValid = IsPointMovable(movablePolygon_, currentX, targetY); + bool isXValid = gameMath::IsPointMovable(movablePolygon_, targetX, currentY); + bool isYValid = gameMath::IsPointMovable(movablePolygon_, currentX, targetY); if (isXValid && isYValid) { int moveX = std::abs(targetX - currentX); @@ -500,12 +464,12 @@ GameMap::MapMoveArea GameMap::CheckIsItMoveArea(const Vec3& curPos) const { } size_t GameMap::FindMoveAreaIndex(const Vec3& curPos) const { - int currentX = RoundWorldCoordinate(curPos.x); - int currentY = RoundWorldCoordinate(curPos.y); + int currentX = gameMath::RoundWorldCoordinate(curPos.x); + int currentY = gameMath::RoundWorldCoordinate(curPos.y); for (size_t i = 0; i < moveArea_.size() && i < mapConfig_.townMovableAreaTargets.size(); ++i) { - if (moveArea_[i].containsPoint(MakeIntegerWorldPoint(currentX, currentY))) { + if (moveArea_[i].containsPoint(gameMath::MakeIntegerWorldPoint(currentX, currentY))) { return i; } } @@ -534,6 +498,13 @@ const std::vector& GameMap::GetMoveAreaInfo() const { return mapConfig_.townMovableAreaTargets; } +game::MoveAreaBounds GameMap::GetMovablePositionBounds(size_t index) const { + if (index >= mapConfig_.townMovableAreaBounds.size()) { + return game::MoveAreaBounds(); + } + return mapConfig_.townMovableAreaBounds[index]; +} + Rect GameMap::GetMovablePositionArea(size_t index) const { if (index >= mapConfig_.townMovableAreas.size()) { return Rect::Zero(); diff --git a/Game/src/map/GameMapLayer.cpp b/Game/src/map/GameMapLayer.cpp index 8f1884d..e005825 100644 --- a/Game/src/map/GameMapLayer.cpp +++ b/Game/src/map/GameMapLayer.cpp @@ -1,4 +1,5 @@ #include "map/GameMapLayer.h" +#include "common/math/GameMath.h" #include #include #include @@ -13,78 +14,6 @@ constexpr float kDebugEdgePointSize = 5.0f; constexpr float kDebugVertexSize = 9.0f; constexpr float kDebugEdgeStep = 4.0f; -std::vector BuildRectPolygon(const Rect& rect) { - if (rect.empty()) { - return {}; - } - - return { - Vec2(rect.left(), rect.top()), - Vec2(rect.right(), rect.top()), - Vec2(rect.right(), rect.bottom()), - Vec2(rect.left(), rect.bottom()), - }; -} - -std::vector BuildPolygonFillRects(const std::vector& polygon) { - std::vector fillRects; - if (polygon.size() < 3) { - return fillRects; - } - - float minY = polygon.front().y; - float maxY = polygon.front().y; - for (const auto& point : polygon) { - minY = std::min(minY, point.y); - maxY = std::max(maxY, point.y); - } - - int scanlineMinY = static_cast(std::floor(minY)); - int scanlineMaxY = static_cast(std::ceil(maxY)) - 1; - if (scanlineMaxY < scanlineMinY) { - return fillRects; - } - - for (int y = scanlineMinY; y <= scanlineMaxY; ++y) { - float scanY = static_cast(y) + 0.5f; - std::vector intersections; - intersections.reserve(polygon.size()); - - for (size_t i = 0, j = polygon.size() - 1; i < polygon.size(); j = i++) { - const Vec2& start = polygon[j]; - const Vec2& end = polygon[i]; - if (start.y == end.y) { - continue; - } - - float edgeMinY = std::min(start.y, end.y); - float edgeMaxY = std::max(start.y, end.y); - if (scanY < edgeMinY || scanY >= edgeMaxY) { - continue; - } - - float t = (scanY - start.y) / (end.y - start.y); - intersections.push_back(start.x + (end.x - start.x) * t); - } - - if (intersections.size() < 2) { - continue; - } - - std::sort(intersections.begin(), intersections.end()); - for (size_t i = 0; i + 1 < intersections.size(); i += 2) { - float left = std::floor(intersections[i]); - float right = std::ceil(intersections[i + 1]); - float width = right - left; - if (width > 0.0f) { - fillRects.emplace_back(left, static_cast(y), width, 1.0f); - } - } - } - - return fillRects; -} - void DrawPolygonOutline(const std::vector& polygon, const Vec2& worldOrigin, const Color& color) { if (polygon.size() < 2) { @@ -155,7 +84,7 @@ void GameMapLayer::Render() { } for (auto& moveArea : moveAreaInfoList_) { - std::vector polygon = BuildRectPolygon(moveArea.rect); + std::vector polygon = gameMath::BuildRectPolygon(moveArea.rect); if (polygon.empty()) { continue; } @@ -170,7 +99,7 @@ void GameMapLayer::Render() { void GameMapLayer::SetDebugFeasibleAreaPolygon(const std::vector& polygon) { feasibleAreaPolygon_ = polygon; - feasibleAreaFillRects_ = BuildPolygonFillRects(feasibleAreaPolygon_); + feasibleAreaFillRects_ = gameMath::BuildPolygonFillRects(feasibleAreaPolygon_); } void GameMapLayer::AddDebugMoveAreaInfo(const Rect& rect, size_t index) { diff --git a/Game/src/scene/GameDebugUIScene.cpp b/Game/src/scene/GameDebugUIScene.cpp index 23e7191..7471714 100644 --- a/Game/src/scene/GameDebugUIScene.cpp +++ b/Game/src/scene/GameDebugUIScene.cpp @@ -1,6 +1,6 @@ #include "scene/GameDebugUIScene.h" #include "character/CharacterObject.h" -#include "common/GameDebugActor.h" +#include "common/debug/GameDebugActor.h" #include "map/GameMap.h" namespace frostbite2D { diff --git a/Game/src/world/GameTown.cpp b/Game/src/world/GameTown.cpp index 699415c..301ff67 100644 --- a/Game/src/world/GameTown.cpp +++ b/Game/src/world/GameTown.cpp @@ -15,7 +15,9 @@ Vec2 RoundWorldPoint(const Vec2& pos) { } // namespace -GameTown::GameTown() = default; +GameTown::GameTown() { + EnableEventReceive(); +} bool GameTown::Init(int index, const std::string& townPath) { game::TownConfig config; @@ -68,10 +70,25 @@ RefPtr GameTown::GetArea(int index) const { return it->map; } +RefPtr GameTown::GetCurrentArea() const { + if (curMapIndex_ == -1) { + return nullptr; + } + + return GetArea(curMapIndex_); +} + void GameTown::AddCharacter(RefPtr actor, int areaIndex) { int targetMapIndex = areaIndex; if (areaIndex == -2) { - targetMapIndex = sariaRoomId_ != -1 ? sariaRoomId_ : (mapList_.empty() ? -1 : mapList_.front().areaId); + if (sariaRoomId_ != -1) { + targetMapIndex = sariaRoomId_; + } else { + targetMapIndex = mapList_.empty() ? -1 : mapList_.front().areaId; + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "GameTown: no gate area configured, fallback to first area %d", + targetMapIndex); + } } auto mapIt = std::find_if(mapList_.begin(), mapList_.end(), diff --git a/Game/src/world/GameWorld.cpp b/Game/src/world/GameWorld.cpp index da7689b..0f40803 100644 --- a/Game/src/world/GameWorld.cpp +++ b/Game/src/world/GameWorld.cpp @@ -1,22 +1,57 @@ #include "world/GameWorld.h" +#include "character/CharacterObject.h" #include "map/GameDataLoader.h" +#include "scene/GameDebugUIScene.h" #include #include namespace frostbite2D { +namespace { + +constexpr int kDefaultTownId = 1; + +Vec2 ComputeMoveAreaCenter(const game::MoveAreaBounds& bounds) { + return Vec2((bounds.leftTop.x + bounds.rightBottom.x) * 0.5f, + (bounds.leftTop.y + bounds.rightBottom.y) * 0.5f); +} + +void SetActorGroundPosition(RefPtr actor, const Vec2& position) { + if (!actor) { + return; + } + + if (auto* character = dynamic_cast(actor.Get())) { + character->SetCharacterPosition(position); + return; + } + + actor->SetPosition(position); +} + +} // namespace + GameWorld::GameWorld() = default; void GameWorld::onEnter() { Scene::onEnter(); - if (initialized_) { - return; + EnsureDebugScene(); + + if (!initialized_) { + initialized_ = InitWorld(); } - initialized_ = InitWorld(); + RefreshDebugContext(); } void GameWorld::onExit() { + pendingCharacterMove_.reset(); + ClearSuppressedMoveArea(); + if (debugScene_) { + debugScene_->ClearDebugContext(); + SceneManager::get().RemoveUIScene(debugScene_.Get()); + } + Scene::onExit(); } @@ -26,6 +61,14 @@ void GameWorld::Update(float deltaTime) { } bool GameWorld::InitWorld() { + RemoveAllChildren(); + townPathMap_.clear(); + townMap_.clear(); + mainActor_.Reset(); + pendingCharacterMove_.reset(); + ClearSuppressedMoveArea(); + curTown_ = -1; + townPathMap_ = game::loadTownList(); if (townPathMap_.empty()) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: no town entries found"); @@ -45,7 +88,36 @@ bool GameWorld::InitWorld() { return false; } - AddCharacter(nullptr, townMap_.begin()->first); + int defaultTownId = kDefaultTownId; + if (townMap_.find(defaultTownId) == townMap_.end()) { + defaultTownId = townMap_.begin()->first; + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "GameWorld: default town %d missing, fallback to town %d", + kDefaultTownId, defaultTownId); + } + + if (!InitMainCharacter(defaultTownId)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "GameWorld: falling back to empty town %d without main character", + defaultTownId); + AddCharacter(nullptr, defaultTownId); + } + + return true; +} + +bool GameWorld::InitMainCharacter(int townId) { + auto mainCharacter = MakePtr(); + if (!mainCharacter->Construction(0)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "GameWorld: failed to construct default main character"); + return false; + } + + mainCharacter->EnableEventReceive(); + mainCharacter->SetEventPriority(-100); + mainCharacter->SetInputEnabled(true); + AddCharacter(mainCharacter, townId); return true; } @@ -60,6 +132,7 @@ void GameWorld::AddCharacter(RefPtr actor, int townId) { curTown_ = townId; it->second->AddCharacter(actor); AddChild(it->second); + RefreshDebugContext(); } void GameWorld::MoveCharacter(RefPtr actor, int townId, int area) { @@ -69,6 +142,44 @@ void GameWorld::MoveCharacter(RefPtr actor, int townId, int area) { return; } + ClearSuppressedMoveArea(); + + int sourceTownId = curTown_; + int sourceAreaId = -1; + if (curTown_ != -1) { + auto currentTown = townMap_.find(curTown_); + if (currentTown != townMap_.end()) { + sourceAreaId = currentTown->second->GetCurAreaIndex(); + } + } + + RefPtr targetMap = townIt->second->GetArea(area); + if (actor && targetMap && sourceTownId != -1 && sourceAreaId != -1) { + bool foundTransitionEntry = false; + const auto& moveAreaInfo = targetMap->GetMoveAreaInfo(); + for (size_t i = 0; i < moveAreaInfo.size(); ++i) { + if (moveAreaInfo[i].town != sourceTownId || moveAreaInfo[i].area != sourceAreaId) { + continue; + } + + game::MoveAreaBounds bounds = targetMap->GetMovablePositionBounds(i); + SetActorGroundPosition(actor, ComputeMoveAreaCenter(bounds)); + SetSuppressedMoveArea(targetMap.Get(), i); + foundTransitionEntry = true; + break; + } + + if (!foundTransitionEntry) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "GameWorld: no matching move area in town %d area %d for source town %d area %d", + townId, area, sourceTownId, sourceAreaId); + } + } else if (actor && !targetMap) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "GameWorld: target area %d missing in town %d, use town fallback spawn", + area, townId); + } + if (curTown_ != -1) { auto currentTown = townMap_.find(curTown_); if (currentTown != townMap_.end()) { @@ -80,6 +191,7 @@ void GameWorld::MoveCharacter(RefPtr actor, int townId, int area) { mainActor_ = actor; townIt->second->AddCharacter(actor, area); AddChild(townIt->second); + RefreshDebugContext(); } void GameWorld::RequestMoveCharacter(RefPtr actor, int townId, int area) { @@ -92,6 +204,22 @@ void GameWorld::RequestMoveCharacter(RefPtr actor, int townId, int area) pendingCharacterMove_ = PendingCharacterMove{actor, townId, area}; } +void GameWorld::UpdateMoveAreaSuppression(GameMap* map, size_t moveAreaIndex) { + if (!suppressedMoveArea_) { + return; + } + + if (suppressedMoveArea_->map != map || + suppressedMoveArea_->moveAreaIndex != moveAreaIndex) { + ClearSuppressedMoveArea(); + } +} + +bool GameWorld::ShouldSuppressMoveArea(GameMap* map, size_t moveAreaIndex) const { + return suppressedMoveArea_ && suppressedMoveArea_->map == map && + suppressedMoveArea_->moveAreaIndex == moveAreaIndex; +} + void GameWorld::ProcessPendingCharacterMove() { if (!pendingCharacterMove_) { return; @@ -102,6 +230,63 @@ void GameWorld::ProcessPendingCharacterMove() { MoveCharacter(move.actor, move.townId, move.area); } +void GameWorld::EnsureDebugScene() { + if (!debugScene_) { + debugScene_ = MakePtr(); + } + + SceneManager::get().RemoveUIScene(debugScene_.Get()); + SceneManager::get().PushUIScene(debugScene_); +} + +void GameWorld::RefreshDebugContext() { + if (!debugScene_) { + return; + } + + debugScene_->SetDebugContext(GetCurrentMap(), GetMainCharacter()); +} + +void GameWorld::SetSuppressedMoveArea(GameMap* map, size_t moveAreaIndex) { + if (!map || moveAreaIndex == GameMap::kInvalidMoveAreaIndex) { + ClearSuppressedMoveArea(); + return; + } + + suppressedMoveArea_ = SuppressedMoveArea{map, moveAreaIndex}; +} + +void GameWorld::ClearSuppressedMoveArea() { + suppressedMoveArea_.reset(); +} + +Actor* GameWorld::GetMainActor() const { + return mainActor_.Get(); +} + +CharacterObject* GameWorld::GetMainCharacter() const { + return dynamic_cast(mainActor_.Get()); +} + +GameTown* GameWorld::GetCurrentTown() const { + auto it = townMap_.find(curTown_); + if (it == townMap_.end()) { + return nullptr; + } + + return it->second.Get(); +} + +GameMap* GameWorld::GetCurrentMap() const { + GameTown* town = GetCurrentTown(); + if (!town) { + return nullptr; + } + + RefPtr area = town->GetCurrentArea(); + return area.Get(); +} + GameWorld* GameWorld::GetWorld() { return dynamic_cast(SceneManager::get().GetCurrentScene()); }