diff --git a/.gitignore b/.gitignore index 73c9de7..c124b12 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ build/ 参考代码/ *.pvf nul +Game/assets/ diff --git a/Frostbite2D/include/frostbite2D/audio/music.h b/Frostbite2D/include/frostbite2D/audio/music.h index cca3737..a937b64 100644 --- a/Frostbite2D/include/frostbite2D/audio/music.h +++ b/Frostbite2D/include/frostbite2D/audio/music.h @@ -15,6 +15,7 @@ namespace frostbite2D { */ class Music : public RefObject { public: + static Ptr loadFromPath(const std::string& path); static Ptr loadFromFile(const std::string& path); static Ptr loadFromMemory(const uint8* data, size_t size); static Ptr loadFromNpk(const std::string& audioPath); diff --git a/Frostbite2D/include/frostbite2D/audio/sound.h b/Frostbite2D/include/frostbite2D/audio/sound.h index 737afb7..0bea8fc 100644 --- a/Frostbite2D/include/frostbite2D/audio/sound.h +++ b/Frostbite2D/include/frostbite2D/audio/sound.h @@ -15,6 +15,7 @@ namespace frostbite2D { */ class Sound : public RefObject { public: + static Ptr loadFromPath(const std::string& path); static Ptr loadFromFile(const std::string& path); static Ptr loadFromMemory(const uint8* data, size_t size); static Ptr loadFromNpk(const std::string& audioPath); diff --git a/Frostbite2D/include/frostbite2D/resource/pvf_archive.h b/Frostbite2D/include/frostbite2D/resource/pvf_archive.h index 884d890..c689175 100644 --- a/Frostbite2D/include/frostbite2D/resource/pvf_archive.h +++ b/Frostbite2D/include/frostbite2D/resource/pvf_archive.h @@ -186,17 +186,13 @@ public: */ bool hasLoadString(const std::string& type, const std::string& key) const; + std::string normalizePath(const std::string& path) const; + std::string resolvePath(const std::string& baseDir, const std::string& path) const; + private: PvfArchive() = default; ~PvfArchive() = default; - /** - * @brief 规范化路径(转为小写) - * @param path 原始路径 - * @return 规范化后的路径 - */ - std::string normalizePath(const std::string& path) const; - /** * @brief 分割字符串 * @param str 要分割的字符串 diff --git a/Frostbite2D/src/frostbite2D/audio/music.cpp b/Frostbite2D/src/frostbite2D/audio/music.cpp index 1b71265..2bf3e2a 100644 --- a/Frostbite2D/src/frostbite2D/audio/music.cpp +++ b/Frostbite2D/src/frostbite2D/audio/music.cpp @@ -8,6 +8,38 @@ namespace frostbite2D { +namespace { + +std::string normalizeAudioLogicalPath(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; +} + +bool startsWithPrefix(const std::string& value, const char* prefix) { + const size_t prefixLength = std::char_traits::length(prefix); + return value.size() >= prefixLength && + value.compare(0, prefixLength, prefix) == 0; +} + +} // namespace + +Ptr Music::loadFromPath(const std::string& path) { + std::string logicalPath = normalizeAudioLogicalPath(path); + if (startsWithPrefix(logicalPath, "music/")) { + return loadFromFile("assets/" + logicalPath); + } + if (startsWithPrefix(logicalPath, "sounds/")) { + return loadFromNpk(logicalPath); + } + + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Unsupported audio path prefix for music: %s", path.c_str()); + return nullptr; +} + Ptr Music::loadFromFile(const std::string& path) { auto& asset = Asset::get(); std::string fullPath = asset.resolvePath(path); @@ -41,13 +73,14 @@ Ptr Music::loadFromMemory(const uint8* data, size_t size) { Ptr Music::loadFromNpk(const std::string& audioPath) { SoundPackArchive& archive = SoundPackArchive::get(); + std::string normalizedPath = normalizeAudioLogicalPath(audioPath); if (!archive.isOpen()) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SoundPackArchive not initialized"); return nullptr; } - auto audioOpt = archive.getAudio(audioPath); + auto audioOpt = archive.getAudio(normalizedPath); if (!audioOpt) { std::string defaultPath = archive.getDefaultAudioPath(); if (!defaultPath.empty()) { diff --git a/Frostbite2D/src/frostbite2D/audio/sound.cpp b/Frostbite2D/src/frostbite2D/audio/sound.cpp index 7113004..495aba3 100644 --- a/Frostbite2D/src/frostbite2D/audio/sound.cpp +++ b/Frostbite2D/src/frostbite2D/audio/sound.cpp @@ -8,6 +8,38 @@ namespace frostbite2D { +namespace { + +std::string normalizeAudioLogicalPath(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; +} + +bool startsWithPrefix(const std::string& value, const char* prefix) { + const size_t prefixLength = std::char_traits::length(prefix); + return value.size() >= prefixLength && + value.compare(0, prefixLength, prefix) == 0; +} + +} // namespace + +Ptr Sound::loadFromPath(const std::string& path) { + std::string logicalPath = normalizeAudioLogicalPath(path); + if (startsWithPrefix(logicalPath, "music/")) { + return loadFromFile("assets/" + logicalPath); + } + if (startsWithPrefix(logicalPath, "sounds/")) { + return loadFromNpk(logicalPath); + } + + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Unsupported audio path prefix for sound: %s", path.c_str()); + return nullptr; +} + Ptr Sound::loadFromFile(const std::string& path) { auto& asset = Asset::get(); std::string fullPath = asset.resolvePath(path); @@ -39,13 +71,14 @@ Ptr Sound::loadFromMemory(const uint8* data, size_t size) { Ptr Sound::loadFromNpk(const std::string& audioPath) { SoundPackArchive& archive = SoundPackArchive::get(); + std::string normalizedPath = normalizeAudioLogicalPath(audioPath); if (!archive.isOpen()) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SoundPackArchive not initialized"); return nullptr; } - auto audioOpt = archive.getAudio(audioPath); + auto audioOpt = archive.getAudio(normalizedPath); if (!audioOpt) { std::string defaultPath = archive.getDefaultAudioPath(); if (!defaultPath.empty()) { diff --git a/Frostbite2D/src/frostbite2D/resource/npk_archive.cpp b/Frostbite2D/src/frostbite2D/resource/npk_archive.cpp index 6bef450..d5820fb 100644 --- a/Frostbite2D/src/frostbite2D/resource/npk_archive.cpp +++ b/Frostbite2D/src/frostbite2D/resource/npk_archive.cpp @@ -5,6 +5,35 @@ namespace frostbite2D { +namespace { + +const char* getZlibErrorName(int code) { + switch (code) { + case Z_OK: + return "Z_OK"; + case Z_STREAM_END: + return "Z_STREAM_END"; + case Z_NEED_DICT: + return "Z_NEED_DICT"; + case Z_ERRNO: + return "Z_ERRNO"; + case Z_STREAM_ERROR: + return "Z_STREAM_ERROR"; + case Z_DATA_ERROR: + return "Z_DATA_ERROR"; + case Z_MEM_ERROR: + return "Z_MEM_ERROR"; + case Z_BUF_ERROR: + return "Z_BUF_ERROR"; + case Z_VERSION_ERROR: + return "Z_VERSION_ERROR"; + default: + return "UNKNOWN_ZLIB_ERROR"; + } +} + +} // namespace + const uint8 NpkArchive::NPK_KEY[256] = { 112, 117, 99, 104, 105, 107, 111, 110, 64, 110, 101, 111, 112, 108, 101, 32, 100, 117, 110, 103, 101, 111, 110, 32, 97, 110, 100, 32, 102, 105, 103, 104, @@ -257,7 +286,23 @@ bool NpkArchive::loadImgData(ImgRef& img) { ); if (uncompressResult != Z_OK) { - SDL_Log("Failed to uncompress image data: %d", uncompressResult); + if (frame.type == 16 && frame.compressionType == 5 && + frame.size == static_cast(deSize)) { + frame.data = std::move(compressedData); + cachedData.memoryUsage += frame.data.size(); + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "NpkArchive: fallback to raw RGBA frame, img=%s, frame=%d, " + "type=%d, compression=%d, size=%dx%d", + img.path.c_str(), i, frame.type, frame.compressionType, + frame.width, frame.height); + continue; + } + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "Failed to uncompress image data: %d (%s), img=%s, npk=%s, frame=%d, " + "type=%d, compression=%d, size=%dx%d, offset=%u, packed=%u, expected=%d", + uncompressResult, getZlibErrorName(uncompressResult), img.path.c_str(), + img.npkFile.c_str(), i, frame.type, frame.compressionType, frame.width, + frame.height, frame.offset, frame.size, deSize); continue; } diff --git a/Frostbite2D/src/frostbite2D/resource/pvf_archive.cpp b/Frostbite2D/src/frostbite2D/resource/pvf_archive.cpp index dbb94c5..4ad4584 100644 --- a/Frostbite2D/src/frostbite2D/resource/pvf_archive.cpp +++ b/Frostbite2D/src/frostbite2D/resource/pvf_archive.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include namespace frostbite2D { @@ -296,12 +297,70 @@ bool PvfArchive::hasLoadString(const std::string& type, const std::string& key) } std::string PvfArchive::normalizePath(const std::string& path) const { - std::string result = path; - std::transform(result.begin(), result.end(), result.begin(), - [](unsigned char c) { return std::tolower(c); }); + if (path.empty()) { + return {}; + } + + 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)); }); + + std::vector parts; + std::stringstream stream(normalized); + std::string part; + while (std::getline(stream, part, '/')) { + if (part.empty() || part == ".") { + continue; + } + if (part == "..") { + if (!parts.empty() && parts.back() != "..") { + parts.pop_back(); + } else { + parts.push_back(part); + } + continue; + } + parts.push_back(part); + } + + std::string result; + for (size_t i = 0; i < parts.size(); ++i) { + if (i > 0) { + result += '/'; + } + result += parts[i]; + } return result; } +std::string PvfArchive::resolvePath(const std::string& baseDir, const std::string& path) const { + if (path.empty()) { + return {}; + } + + std::string rawPath = path; + std::replace(rawPath.begin(), rawPath.end(), '\\', '/'); + std::transform(rawPath.begin(), rawPath.end(), rawPath.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + + size_t slashPos = rawPath.find('/'); + std::string root = slashPos == std::string::npos ? rawPath : rawPath.substr(0, slashPos); + static const char* kLogicalRoots[] = {"map", "sprite", "town", "sound", "audio", + "monster", "character", "ui"}; + for (const char* logicalRoot : kLogicalRoots) { + if (root == logicalRoot) { + return normalizePath(rawPath); + } + } + + std::string normalizedBaseDir = normalizePath(baseDir); + if (!normalizedBaseDir.empty() && normalizedBaseDir.back() != '/') { + normalizedBaseDir.push_back('/'); + } + return normalizePath(normalizedBaseDir + rawPath); +} + std::vector PvfArchive::splitString(const std::string& str, const std::string& delimiter) const { std::vector tokens; size_t pos = 0; diff --git a/Game/assets/SoundPacks/sounds_ui.npk b/Game/assets/SoundPacks/sounds_ui.npk index dc3535d..7104221 100644 Binary files a/Game/assets/SoundPacks/sounds_ui.npk and b/Game/assets/SoundPacks/sounds_ui.npk differ diff --git a/Game/include/Actor/GameDataLoader.h b/Game/include/Actor/GameDataLoader.h new file mode 100644 index 0000000..5d5fa09 --- /dev/null +++ b/Game/include/Actor/GameDataLoader.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include +#include + +namespace frostbite2D::game { + +struct AreaConfig { + std::string mapPath; + std::string mapType; + Vec2 generatePos = Vec2(-1.0f, -1.0f); +}; + +struct TownConfig { + std::string townName; + std::string enteringTitle; + std::string enteringCutscene; + int needQuestId = -1; + int needLevel = -1; + std::map areas; +}; + +struct BackgroundAnimationConfig { + std::string filename; + std::string layer; + std::string order; +}; + +struct MapAnimationConfig { + std::string filename; + std::string layer; + int xPos = 0; + int yPos = 0; + int zPos = 0; +}; + +struct MoveAreaTarget { + int town = -2; + int area = -2; +}; + +struct TileInfo { + std::string spritePath; + int spriteIndex = 0; + int imgPos = 0; +}; + +struct MapConfig { + std::string name; + std::string mapPath; + std::string mapDir; + int backgroundPos = 0; + int farSightScroll = 100; + bool hasCameraLimit = false; + int cameraLimitMinX = -1; + int cameraLimitMaxX = -1; + int cameraLimitMinY = -1; + int cameraLimitMaxY = -1; + std::vector tilePaths; + std::vector extendedTilePaths; + std::vector backgroundAnimations; + std::vector mapAnimations; + std::vector soundIds; + std::vector virtualMovableAreas; + std::vector townMovableAreas; + std::vector townMovableAreaTargets; +}; + +std::map loadTownList(); +bool loadTownConfig(const std::string& townPath, TownConfig& outConfig); +bool loadMapConfig(const std::string& mapPath, MapConfig& outConfig); +bool loadTileInfo(const std::string& path, TileInfo& outInfo); + +} // namespace frostbite2D::game diff --git a/Game/include/Actor/GameMap.h b/Game/include/Actor/GameMap.h new file mode 100644 index 0000000..8b11742 --- /dev/null +++ b/Game/include/Actor/GameMap.h @@ -0,0 +1,55 @@ +#pragma once + +#include "GameDataLoader.h" +#include "GameMapLayer.h" +#include +#include +#include + +namespace frostbite2D { + +class GameMap : public Actor { +public: + using MapMoveArea = game::MoveAreaTarget; + + GameMap(); + ~GameMap() override = default; + + bool LoadMap(const std::string& mapName); + void Enter(); + void Update(float deltaTime) override; + + void AddObject(RefPtr object); + + Vec3 CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const; + MapMoveArea CheckIsItMoveArea(const Vec3& curPos) const; + const std::vector& GetMoveAreaInfo() const; + Rect GetMovablePositionArea(size_t index) const; + + int GetMapLength() const { return mapLength_; } + int GetMapHeight() const { return mapHeight_; } + +private: + void initLayers(); + void clearLayerChildren(); + void InitTile(); + void InitBackgroundAnimation(); + void InitMapAnimation(); + void InitVirtualMovableArea(); + void InitMoveArea(); + void updateCamera(); + void updateLayerPositions(const Vec2& cameraPos); + + game::MapConfig mapConfig_; + std::unordered_map> layerMap_; + std::vector movableArea_; + std::vector moveArea_; + int mapLength_ = 0; + int mapHeight_ = 0; + int mapOffsetY_ = 0; + bool debugMode_ = false; + Vec2 cameraFocus_ = Vec2::Zero(); + Ptr currentMusic_; +}; + +} // namespace frostbite2D diff --git a/Game/include/Actor/GameMapLayer.h b/Game/include/Actor/GameMapLayer.h new file mode 100644 index 0000000..eb40374 --- /dev/null +++ b/Game/include/Actor/GameMapLayer.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace frostbite2D { + +class GameMapLayer : public Actor { +public: + void Render() override; + + void AddDebugFeasibleAreaInfo(const Rect& rect, int type); + void AddObject(RefPtr obj); + +private: + std::vector feasibleAreaInfoList_; + std::vector moveAreaInfoList_; +}; + +} // namespace frostbite2D diff --git a/Game/include/Actor/GameTown.h b/Game/include/Actor/GameTown.h index 83755bf..5e312a3 100644 --- a/Game/include/Actor/GameTown.h +++ b/Game/include/Actor/GameTown.h @@ -1,12 +1,43 @@ #pragma once + +#include "GameDataLoader.h" +#include "GameMap.h" #include +#include namespace frostbite2D { -class GameTown : public Actor { - +class GameTown : public Actor { public: + struct MapInfo { + int areaId = -1; + RefPtr map; + std::string type; + Vec2 generatePos = Vec2(-1.0f, -1.0f); + }; + GameTown(); - ~GameTown(); + ~GameTown() override = default; + + bool Init(int index, const std::string& townPath); + void AddCharacter(RefPtr actor, int areaIndex = -2); + + int GetCurAreaIndex() const { return curMapIndex_; } + RefPtr GetArea(int index) const; + const std::vector& GetAreas() const { return mapList_; } + + void Update(float deltaTime) override; + +private: + std::string name_; + std::string enteringTitle_; + std::string enteringCutscene_; + int needQuestId_ = -1; + int needLevel_ = -1; + int sariaRoomId_ = -1; + Vec2 sariaRoomPos_ = Vec2(-1.0f, -1.0f); + std::vector mapList_; + int curMapIndex_ = -1; }; + } // namespace frostbite2D diff --git a/Game/include/Actor/GameWorld.h b/Game/include/Actor/GameWorld.h index a72d89d..1a8d87f 100644 --- a/Game/include/Actor/GameWorld.h +++ b/Game/include/Actor/GameWorld.h @@ -1,18 +1,32 @@ #pragma once + +#include "GameTown.h" #include #include -#include "GameTown.h" namespace frostbite2D { + class GameWorld : public Scene { -private: - /**城镇Map */ - std::map> m_TownMap; - - public: GameWorld(); - ~GameWorld(); + ~GameWorld() override = default; + + void onEnter() override; + void onExit() override; + + void AddCharacter(RefPtr actor, int townId); + void MoveCharacter(RefPtr actor, int townId, int area); + + static GameWorld* GetWorld(); + +private: + bool InitWorld(); + + std::map townPathMap_; + std::map> townMap_; + RefPtr mainActor_; + int curTown_ = -1; + bool initialized_ = false; }; } // namespace frostbite2D diff --git a/Game/src/Actor/GameDataLoader.cpp b/Game/src/Actor/GameDataLoader.cpp new file mode 100644 index 0000000..2f98142 --- /dev/null +++ b/Game/src/Actor/GameDataLoader.cpp @@ -0,0 +1,301 @@ +#include "Actor/GameDataLoader.h" +#include +#include +#include +#include +#include +#include + +namespace frostbite2D::game { + +namespace { + +std::string toLowerCase(const std::string& str) { + std::string result = str; + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return result; +} + +std::string trimLayer(const std::string& str) { + if (str.size() >= 2 && str.front() == '[' && str.back() == ']') { + return toLowerCase(str.substr(1, str.size() - 2)); + } + return toLowerCase(str); +} + +std::string normalizeWithPrefix(const std::string& prefix, const std::string& path) { + auto& pvf = PvfArchive::get(); + std::string normalizedPath = pvf.normalizePath(path); + if (normalizedPath.empty()) { + return normalizedPath; + } + if (normalizedPath.rfind(prefix + "/", 0) == 0) { + return normalizedPath; + } + return pvf.normalizePath(prefix + "/" + normalizedPath); +} + +std::string resolveMapPath(const std::string& baseDir, const std::string& leaf) { + return PvfArchive::get().resolvePath(baseDir, leaf); +} + +std::string resolveSpritePath(const std::string& baseDir, const std::string& leaf) { + return normalizeWithPrefix("sprite", PvfArchive::get().resolvePath(baseDir, leaf)); +} + +class ScriptTokenStream { +public: + explicit ScriptTokenStream(const std::string& path) + : path_(toLowerCase(path)) { + auto& pvf = PvfArchive::get(); + auto rawData = pvf.getFileRawData(path_); + if (!rawData) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameDataLoader: missing script %s", + path_.c_str()); + return; + } + + ScriptParser parser(*rawData, path_); + if (!parser.isValid()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "GameDataLoader: invalid script payload %s", path_.c_str()); + return; + } + + for (const auto& value : parser.parseAll()) { + tokens_.push_back(value.toString()); + } + valid_ = true; + } + + bool isValid() const { return valid_; } + bool isEnd() const { return index_ >= tokens_.size(); } + const std::string& path() const { return path_; } + + std::string get() { + if (isEnd()) { + return {}; + } + return tokens_[index_++]; + } + +private: + std::string path_; + std::vector tokens_; + size_t index_ = 0; + bool valid_ = false; +}; + +int toInt(const std::string& value, int fallback = 0) { + try { + return std::stoi(value); + } catch (...) { + return fallback; + } +} + +bool readScript(const std::string& path, ScriptTokenStream& outStream) { + (void)path; + return outStream.isValid(); +} + +} // namespace + +std::map loadTownList() { + std::map townList; + ScriptTokenStream stream("town/town.lst"); + if (!stream.isValid()) { + return townList; + } + + while (!stream.isEnd()) { + std::string index = stream.get(); + if (index.empty()) { + break; + } + std::string townPath = stream.get(); + if (townPath.empty()) { + break; + } + townList[toInt(index)] = normalizeWithPrefix("town", townPath); + } + + return townList; +} + +bool loadTownConfig(const std::string& townPath, TownConfig& outConfig) { + ScriptTokenStream stream(townPath); + if (!stream.isValid()) { + return false; + } + + outConfig = TownConfig(); + while (!stream.isEnd()) { + std::string segment = stream.get(); + if (segment == "[entering title]") { + outConfig.enteringTitle = "sprite/" + toLowerCase(stream.get()); + } else if (segment == "[cutscene image]") { + outConfig.enteringCutscene = "sprite/" + toLowerCase(stream.get()); + } else if (segment == "[area]") { + int index = toInt(stream.get(), -1); + std::string mapPath = normalizeWithPrefix("map", stream.get()); + std::string type = trimLayer(stream.get()); + AreaConfig areaConfig; + areaConfig.mapPath = mapPath; + areaConfig.mapType = type; + if (type == "gate") { + areaConfig.generatePos.x = static_cast(toInt(stream.get(), -1)); + areaConfig.generatePos.y = static_cast(toInt(stream.get(), -1)); + } + outConfig.areas[index] = areaConfig; + } else if (segment == "[name]") { + outConfig.townName = stream.get(); + } else if (segment == "[need level]") { + outConfig.needLevel = toInt(stream.get(), -1); + } else if (segment == "[need quest id]") { + outConfig.needQuestId = toInt(stream.get(), -1); + } + } + + return !outConfig.areas.empty(); +} + +bool loadMapConfig(const std::string& mapPath, MapConfig& outConfig) { + ScriptTokenStream stream(mapPath); + if (!stream.isValid()) { + return false; + } + + outConfig = MapConfig(); + outConfig.mapPath = toLowerCase(mapPath); + size_t slashPos = outConfig.mapPath.find_last_of('/'); + outConfig.mapDir = slashPos == std::string::npos ? "" : outConfig.mapPath.substr(0, slashPos + 1); + + while (!stream.isEnd()) { + std::string segment = stream.get(); + if (segment == "[background pos]") { + outConfig.backgroundPos = toInt(stream.get()); + } else if (segment == "[map name]") { + outConfig.name = stream.get(); + } else if (segment == "[limit map camera move]") { + outConfig.hasCameraLimit = true; + outConfig.cameraLimitMinX = toInt(stream.get(), -1); + outConfig.cameraLimitMaxX = toInt(stream.get(), -1); + outConfig.cameraLimitMinY = toInt(stream.get(), -1); + outConfig.cameraLimitMaxY = toInt(stream.get(), -1); + } else if (segment == "[far sight scroll]") { + outConfig.farSightScroll = toInt(stream.get(), 100); + } else if (segment == "[tile]") { + while (!stream.isEnd()) { + std::string token = stream.get(); + if (token == "[/tile]") { + break; + } + outConfig.tilePaths.push_back(resolveMapPath(outConfig.mapDir, token)); + } + } else if (segment == "[extended tile]") { + while (!stream.isEnd()) { + std::string token = stream.get(); + if (token == "[/extended tile]") { + break; + } + outConfig.extendedTilePaths.push_back(resolveMapPath(outConfig.mapDir, token)); + } + } else if (segment == "[background animation]") { + while (!stream.isEnd()) { + std::string token = stream.get(); + if (token == "[/background animation]") { + break; + } + if (token != "[ani info]") { + continue; + } + BackgroundAnimationConfig ani; + stream.get(); + ani.filename = resolveMapPath(outConfig.mapDir, stream.get()); + stream.get(); + ani.layer = trimLayer(stream.get()); + stream.get(); + ani.order = stream.get(); + outConfig.backgroundAnimations.push_back(ani); + } + } else if (segment == "[sound]") { + while (!stream.isEnd()) { + std::string token = stream.get(); + if (token == "[/sound]") { + break; + } + outConfig.soundIds.push_back(token); + } + } else if (segment == "[animation]") { + while (!stream.isEnd()) { + std::string token = stream.get(); + if (token == "[/animation]") { + break; + } + MapAnimationConfig ani; + ani.filename = resolveMapPath(outConfig.mapDir, token); + ani.layer = trimLayer(stream.get()); + ani.xPos = toInt(stream.get()); + ani.yPos = toInt(stream.get()); + ani.zPos = toInt(stream.get()); + outConfig.mapAnimations.push_back(ani); + } + } else if (segment == "[town movable area]") { + while (!stream.isEnd()) { + std::string token = stream.get(); + if (token == "[/town movable area]") { + break; + } + Rect rect(static_cast(toInt(token)), + static_cast(toInt(stream.get())), + static_cast(toInt(stream.get())), + static_cast(toInt(stream.get()))); + MoveAreaTarget target; + target.town = toInt(stream.get(), -2); + target.area = toInt(stream.get(), -2); + outConfig.townMovableAreas.push_back(rect); + outConfig.townMovableAreaTargets.push_back(target); + } + } else if (segment == "[virtual movable area]") { + while (!stream.isEnd()) { + std::string token = stream.get(); + if (token == "[/virtual movable area]") { + break; + } + Rect rect(static_cast(toInt(token)), + static_cast(toInt(stream.get())), + static_cast(toInt(stream.get())), + static_cast(toInt(stream.get()))); + outConfig.virtualMovableAreas.push_back(rect); + } + } + } + + return true; +} + +bool loadTileInfo(const std::string& path, TileInfo& outInfo) { + ScriptTokenStream stream(path); + if (!stream.isValid()) { + return false; + } + + outInfo = TileInfo(); + size_t slashPos = path.find_last_of('/'); + std::string baseDir = slashPos == std::string::npos ? "" : path.substr(0, slashPos + 1); + while (!stream.isEnd()) { + std::string segment = stream.get(); + if (segment == "[image]") { + outInfo.spritePath = resolveSpritePath(baseDir, stream.get()); + outInfo.spriteIndex = toInt(stream.get()); + } else if (segment == "[img pos]") { + outInfo.imgPos = toInt(stream.get()); + } + } + + return true; +} + +} // namespace frostbite2D::game diff --git a/Game/src/Actor/GameMap.cpp b/Game/src/Actor/GameMap.cpp new file mode 100644 index 0000000..4813e9b --- /dev/null +++ b/Game/src/Actor/GameMap.cpp @@ -0,0 +1,364 @@ +#include "Actor/GameMap.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace frostbite2D { + +namespace { + +static const char* kLayerNames[] = { + "contact", "distantback", "middleback", "bottom", "closeback", + "normal", "close", "cover", "max"}; + +static const int kLayerOrders[] = { + 10000, 50000, 100000, 150000, 200000, 250000, 300000, 350000, 400000}; + +constexpr float kScreenWidth = 1280.0f; +constexpr float kScreenHeight = 720.0f; +constexpr float kTileSpacing = 224.0f; +constexpr float kExtendedTileStepY = 120.0f; + +Ptr createMapSprite(const std::string& path, int index) { + auto sprite = Sprite::createFromNpk(path, static_cast(index)); + if (!sprite) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "GameMap: failed to create sprite %s[%d]", + path.c_str(), index); + sprite = MakePtr(); + } + return sprite; +} + +} // namespace + +GameMap::GameMap() { + initLayers(); +} + +void GameMap::initLayers() { + for (size_t i = 0; i < std::size(kLayerNames); ++i) { + auto layer = MakePtr(); + layer->SetName(kLayerNames[i]); + layer->SetZOrder(kLayerOrders[i]); + layerMap_[kLayerNames[i]] = layer; + AddChild(layer); + } +} + +void GameMap::clearLayerChildren() { + for (auto& [_, layer] : layerMap_) { + if (layer) { + layer->RemoveAllChildren(); + } + } + movableArea_.clear(); + moveArea_.clear(); + currentMusic_.Reset(); + mapLength_ = 0; + mapHeight_ = 0; +} + +bool GameMap::LoadMap(const std::string& mapName) { + clearLayerChildren(); + + if (!game::loadMapConfig(mapName, mapConfig_)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameMap: failed to load map %s", + mapName.c_str()); + return false; + } + + mapOffsetY_ = mapConfig_.backgroundPos; + InitTile(); + InitBackgroundAnimation(); + InitMapAnimation(); + InitVirtualMovableArea(); + InitMoveArea(); + + if (cameraFocus_ == Vec2::Zero()) { + if (!moveArea_.empty()) { + cameraFocus_ = moveArea_.front().center(); + } else if (mapLength_ > 0 || mapHeight_ > 0) { + cameraFocus_ = Vec2(mapLength_ * 0.5f, mapHeight_ * 0.5f); + } + } + + return true; +} + +void GameMap::Enter() { + 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; + } + } + } + + updateCamera(); +} + +void GameMap::Update(float deltaTime) { + Actor::Update(deltaTime); + (void)deltaTime; + updateCamera(); +} + +void GameMap::InitTile() { + if (mapConfig_.tilePaths.empty() && mapConfig_.extendedTilePaths.empty()) { + return; + } + + float maxBaseHeight = 0.0f; + float maxTotalBottom = 0.0f; + float maxOffset = 0.0f; + int normalTileCount = static_cast(mapConfig_.tilePaths.size()); + + for (size_t i = 0; i < mapConfig_.tilePaths.size(); ++i) { + game::TileInfo info; + if (!game::loadTileInfo(mapConfig_.tilePaths[i], info)) { + continue; + } + + auto sprite = createMapSprite(info.spritePath.empty() ? "sprite/character/common/circlecooltime.img" + : info.spritePath, + info.spriteIndex); + sprite->SetPosition(static_cast(i) * kTileSpacing, static_cast(info.imgPos)); + layerMap_["bottom"]->AddChild(sprite); + + Vec2 size = sprite->GetSize(); + maxOffset = std::max(maxOffset, static_cast(info.imgPos)); + maxBaseHeight = std::max(maxBaseHeight, size.y); + mapLength_ = std::max(mapLength_, static_cast(i * kTileSpacing + size.x)); + maxTotalBottom = std::max(maxTotalBottom, info.imgPos + size.y); + } + + if (normalTileCount > 0 && mapLength_ == 0) { + mapLength_ = static_cast(normalTileCount * kTileSpacing); + } + + for (size_t i = 0; i < mapConfig_.extendedTilePaths.size(); ++i) { + game::TileInfo info; + if (!game::loadTileInfo(mapConfig_.extendedTilePaths[i], info)) { + continue; + } + + auto sprite = createMapSprite(info.spritePath.empty() ? "sprite/character/common/circlecooltime.img" + : info.spritePath, + info.spriteIndex); + int row = normalTileCount > 0 ? static_cast(i) / normalTileCount : 0; + float posY = maxOffset + maxBaseHeight + row * kExtendedTileStepY; + sprite->SetPosition(static_cast(i) * kTileSpacing, posY); + layerMap_["bottom"]->AddChild(sprite); + + Vec2 size = sprite->GetSize(); + mapLength_ = std::max(mapLength_, static_cast(i * kTileSpacing + size.x)); + maxTotalBottom = std::max(maxTotalBottom, posY + size.y); + } + + mapHeight_ = std::max(mapHeight_, static_cast(maxTotalBottom)); +} + +void GameMap::InitBackgroundAnimation() { + for (const auto& ani : mapConfig_.backgroundAnimations) { + auto animation = MakePtr(ani.filename); + if (!animation || !animation->IsUsable()) { + continue; + } + + int repeatCount = 1; + Vec2 animationSize = animation->GetSize(); + float rate = std::max(1.0f, mapConfig_.farSightScroll / 100.0f); + if (animationSize.x > 0.0f && mapLength_ > 0) { + repeatCount = std::max(1, static_cast((mapLength_ * rate) / animationSize.x) + 1); + } + + auto layerIt = layerMap_.find(ani.layer); + if (layerIt == layerMap_.end()) { + continue; + } + + for (int i = 0; i < repeatCount; ++i) { + auto backgroundAni = MakePtr(ani.filename); + if (!backgroundAni || !backgroundAni->IsUsable()) { + continue; + } + backgroundAni->SetPosition(i * animationSize.x, -120.0f); + backgroundAni->SetZOrder(-1000000); + layerIt->second->AddChild(backgroundAni); + } + } +} + +void GameMap::InitMapAnimation() { + for (const auto& ani : mapConfig_.mapAnimations) { + auto animation = MakePtr(ani.filename); + if (!animation || !animation->IsUsable()) { + continue; + } + + animation->SetPosition(static_cast(ani.xPos), + static_cast(ani.yPos - ani.zPos)); + animation->SetZOrder(ani.yPos); + + auto layerIt = layerMap_.find(ani.layer); + if (layerIt != layerMap_.end()) { + layerIt->second->AddChild(animation); + } + } +} + +void GameMap::InitVirtualMovableArea() { + movableArea_ = mapConfig_.virtualMovableAreas; + if (!debugMode_) { + return; + } + + for (const auto& rect : movableArea_) { + layerMap_["max"]->AddDebugFeasibleAreaInfo(rect, 0); + } +} + +void GameMap::InitMoveArea() { + moveArea_ = mapConfig_.townMovableAreas; + if (!debugMode_) { + return; + } + + for (const auto& rect : moveArea_) { + layerMap_["max"]->AddDebugFeasibleAreaInfo(rect, 1); + } +} + +void GameMap::updateCamera() { + Camera* camera = Renderer::get().getCamera(); + if (!camera) { + return; + } + + float halfWidth = kScreenWidth * 0.5f; + float halfHeight = kScreenHeight * 0.5f; + float targetX = cameraFocus_.x; + float targetY = cameraFocus_.y; + + if (mapLength_ > 0) { + targetX = std::clamp(targetX, halfWidth, std::max(halfWidth, static_cast(mapLength_) - halfWidth)); + } + if (mapHeight_ > 0) { + targetY = std::clamp(targetY, halfHeight, std::max(halfHeight, static_cast(mapHeight_) - halfHeight)); + } + + if (mapConfig_.hasCameraLimit) { + if (mapConfig_.cameraLimitMinX != -1) { + targetX = std::max(targetX, static_cast(mapConfig_.cameraLimitMinX)); + } + if (mapConfig_.cameraLimitMaxX != -1) { + targetX = std::min(targetX, static_cast(mapConfig_.cameraLimitMaxX)); + } + if (mapConfig_.cameraLimitMinY != -1) { + targetY = std::max(targetY, static_cast(mapConfig_.cameraLimitMinY)); + } + if (mapConfig_.cameraLimitMaxY != -1) { + targetY = std::min(targetY, static_cast(mapConfig_.cameraLimitMaxY)); + } + } + + Vec2 cameraPos(targetX - halfWidth, targetY - halfHeight); + camera->setPosition(cameraPos); + updateLayerPositions(Vec2(targetX, targetY)); +} + +void GameMap::updateLayerPositions(const Vec2& cameraTarget) { + float halfWidth = kScreenWidth * 0.5f; + float halfHeight = kScreenHeight * 0.5f; + + for (auto& entry : layerMap_) { + const std::string& name = entry.first; + RefPtr& layer = entry.second; + float posX = -cameraTarget.x + halfWidth; + float posY = -cameraTarget.y + halfHeight + static_cast(mapOffsetY_); + if (name == "distantback") { + posX *= static_cast(mapConfig_.farSightScroll) / 100.0f; + } + layer->SetPosition(posX, posY); + } +} + +void GameMap::AddObject(RefPtr object) { + if (!object) { + return; + } + object->SetZOrder(static_cast(object->GetPosition().y)); + layerMap_["normal"]->AddObject(object); +} + +Vec3 GameMap::CheckIsItMovable(const Vec3& curPos, const Vec3& posOffset) const { + Vec3 result = curPos; + if (movableArea_.empty()) { + result += posOffset; + return result; + } + + float targetX = curPos.x + posOffset.x; + float targetY = curPos.y + posOffset.y; + + bool isXValid = false; + for (const auto& area : movableArea_) { + if (area.containsPoint(Vec2(targetX, curPos.y))) { + isXValid = true; + break; + } + } + + bool isYValid = false; + for (const auto& area : movableArea_) { + if (area.containsPoint(Vec2(curPos.x, targetY))) { + isYValid = true; + break; + } + } + + if (isXValid) { + result.x = targetX; + } + if (isYValid) { + result.y = targetY; + } + result.z += posOffset.z; + return result; +} + +GameMap::MapMoveArea GameMap::CheckIsItMoveArea(const Vec3& curPos) const { + for (size_t i = 0; i < moveArea_.size() && i < mapConfig_.townMovableAreaTargets.size(); ++i) { + if (moveArea_[i].containsPoint(Vec2(curPos.x, curPos.y))) { + return mapConfig_.townMovableAreaTargets[i]; + } + } + return MapMoveArea(); +} + +const std::vector& GameMap::GetMoveAreaInfo() const { + return mapConfig_.townMovableAreaTargets; +} + +Rect GameMap::GetMovablePositionArea(size_t index) const { + if (index >= mapConfig_.townMovableAreas.size()) { + return Rect::Zero(); + } + return mapConfig_.townMovableAreas[index]; +} + +} // namespace frostbite2D diff --git a/Game/src/Actor/GameMapLayer.cpp b/Game/src/Actor/GameMapLayer.cpp new file mode 100644 index 0000000..b3d1f83 --- /dev/null +++ b/Game/src/Actor/GameMapLayer.cpp @@ -0,0 +1,36 @@ +#include "Actor/GameMapLayer.h" +#include + +namespace frostbite2D { + +void GameMapLayer::Render() { + Actor::Render(); + + Vec2 worldOrigin = GetWorldTransform().transformPoint(Vec2::Zero()); + + for (const auto& rect : feasibleAreaInfoList_) { + Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y, + rect.width(), rect.height()); + Renderer::get().drawQuad(drawRect, Color(1.0f, 0.0f, 0.0f, 0.35f)); + } + + for (const auto& rect : moveAreaInfoList_) { + Rect drawRect(worldOrigin.x + rect.origin.x, worldOrigin.y + rect.origin.y, + rect.width(), rect.height()); + Renderer::get().drawQuad(drawRect, Color(0.0f, 0.0f, 1.0f, 0.35f)); + } +} + +void GameMapLayer::AddDebugFeasibleAreaInfo(const Rect& rect, int type) { + if (type == 0) { + feasibleAreaInfoList_.push_back(rect); + } else if (type == 1) { + moveAreaInfoList_.push_back(rect); + } +} + +void GameMapLayer::AddObject(RefPtr obj) { + AddChild(obj); +} + +} // namespace frostbite2D diff --git a/Game/src/Actor/GameTown.cpp b/Game/src/Actor/GameTown.cpp new file mode 100644 index 0000000..e94b8f1 --- /dev/null +++ b/Game/src/Actor/GameTown.cpp @@ -0,0 +1,107 @@ +#include "Actor/GameTown.h" +#include +#include + +namespace frostbite2D { + +GameTown::GameTown() = default; + +bool GameTown::Init(int index, const std::string& townPath) { + game::TownConfig config; + if (!game::loadTownConfig(townPath, config)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameTown: failed to init town %d (%s)", + index, townPath.c_str()); + return false; + } + + name_ = config.townName; + enteringTitle_ = config.enteringTitle; + enteringCutscene_ = config.enteringCutscene; + needQuestId_ = config.needQuestId; + needLevel_ = config.needLevel; + mapList_.clear(); + curMapIndex_ = -1; + sariaRoomId_ = -1; + sariaRoomPos_ = Vec2(-1.0f, -1.0f); + + for (const auto& [areaId, areaConfig] : config.areas) { + auto map = MakePtr(); + if (!map->LoadMap(areaConfig.mapPath)) { + continue; + } + + MapInfo info; + info.areaId = areaId; + info.map = map; + info.type = areaConfig.mapType; + info.generatePos = areaConfig.generatePos; + mapList_.push_back(info); + + if (info.type == "gate") { + sariaRoomId_ = areaId; + sariaRoomPos_ = areaConfig.generatePos; + } + } + + std::sort(mapList_.begin(), mapList_.end(), + [](const MapInfo& a, const MapInfo& b) { return a.areaId < b.areaId; }); + return !mapList_.empty(); +} + +RefPtr GameTown::GetArea(int index) const { + auto it = std::find_if(mapList_.begin(), mapList_.end(), + [index](const MapInfo& info) { return info.areaId == index; }); + if (it == mapList_.end()) { + return nullptr; + } + return it->map; +} + +void GameTown::AddCharacter(RefPtr actor, int areaIndex) { + int targetMapIndex = areaIndex; + if (areaIndex == -2) { + targetMapIndex = sariaRoomId_ != -1 ? sariaRoomId_ : (mapList_.empty() ? -1 : mapList_.front().areaId); + } + + auto mapIt = std::find_if(mapList_.begin(), mapList_.end(), + [targetMapIndex](const MapInfo& info) { + return info.areaId == targetMapIndex; + }); + if (mapIt == mapList_.end()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "GameTown: target area %d not found, fallback to first map", targetMapIndex); + if (mapList_.empty()) { + return; + } + mapIt = mapList_.begin(); + } + + if (curMapIndex_ != -1) { + auto oldMap = GetArea(curMapIndex_); + if (oldMap) { + RemoveChild(oldMap); + } + } + + if (actor && actor->GetParent()) { + actor->GetParent()->RemoveChild(actor); + } + + AddChild(mapIt->map); + mapIt->map->Enter(); + + if (actor) { + if (areaIndex == -2 && sariaRoomPos_.x >= 0.0f && sariaRoomPos_.y >= 0.0f) { + actor->SetPosition(sariaRoomPos_); + } + mapIt->map->AddObject(actor); + } + + curMapIndex_ = mapIt->areaId; +} + +void GameTown::Update(float deltaTime) { + Actor::Update(deltaTime); +} + +} // namespace frostbite2D diff --git a/Game/src/Actor/GameWorld.cpp b/Game/src/Actor/GameWorld.cpp new file mode 100644 index 0000000..f22d80d --- /dev/null +++ b/Game/src/Actor/GameWorld.cpp @@ -0,0 +1,85 @@ +#include "Actor/GameWorld.h" +#include "Actor/GameDataLoader.h" +#include +#include + +namespace frostbite2D { + +GameWorld::GameWorld() = default; + +void GameWorld::onEnter() { + Scene::onEnter(); + if (initialized_) { + return; + } + + SetScale(1.2f); + initialized_ = InitWorld(); +} + +void GameWorld::onExit() { + Scene::onExit(); +} + +bool GameWorld::InitWorld() { + townPathMap_ = game::loadTownList(); + if (townPathMap_.empty()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: no town entries found"); + return false; + } + + for (const auto& [townId, townPath] : townPathMap_) { + auto town = MakePtr(); + if (!town->Init(townId, townPath)) { + continue; + } + townMap_[townId] = town; + } + + if (townMap_.empty()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: failed to create any town"); + return false; + } + + AddCharacter(nullptr, townMap_.begin()->first); + return true; +} + +void GameWorld::AddCharacter(RefPtr actor, int townId) { + auto it = townMap_.find(townId); + if (it == townMap_.end()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: town %d not found", townId); + return; + } + + mainActor_ = actor; + curTown_ = townId; + it->second->AddCharacter(actor); + AddChild(it->second); +} + +void GameWorld::MoveCharacter(RefPtr actor, int townId, int area) { + auto townIt = townMap_.find(townId); + if (townIt == townMap_.end()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "GameWorld: target town %d missing", townId); + return; + } + + if (curTown_ != -1) { + auto currentTown = townMap_.find(curTown_); + if (currentTown != townMap_.end()) { + RemoveChild(currentTown->second); + } + } + + curTown_ = townId; + mainActor_ = actor; + townIt->second->AddCharacter(actor, area); + AddChild(townIt->second); +} + +GameWorld* GameWorld::GetWorld() { + return dynamic_cast(SceneManager::get().GetCurrentScene()); +} + +} // namespace frostbite2D diff --git a/Game/src/main.cpp b/Game/src/main.cpp index 652c2ac..9e894b7 100644 --- a/Game/src/main.cpp +++ b/Game/src/main.cpp @@ -13,11 +13,13 @@ #include #include #include +#include #include #include #include #include #include +#include "Actor/GameWorld.h" using namespace frostbite2D; @@ -96,13 +98,16 @@ int main(int argc, char **argv) { archive.setSoundPackDirectory("assets/SoundPacks"); archive.init(); + auto &audioDatabase = AudioDatabase::get(); + audioDatabase.loadFromFile("assets/audio.xml"); + return "后台资源加载成功"; }, [](std::string message) mutable { SDL_Log("后台资源加载成功"); - auto TestScene = MakePtr(); - SceneManager::get().PushScene(TestScene); + auto gameWorld = MakePtr(); + SceneManager::get().ReplaceScene(gameWorld); }, [](std::exception_ptr error) { try {