From b5c432e77afd8ead73a2271c9cc8734925236acd Mon Sep 17 00:00:00 2001 From: Lenheart <947330670@qq.com> Date: Thu, 2 Apr 2026 23:32:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=B8=B8=E6=88=8F?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=8C=85=E6=8B=AC?= =?UTF-8?q?=E5=9C=B0=E5=9B=BE=E3=80=81=E8=A7=92=E8=89=B2=E3=80=81=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=E5=92=8C=E4=B8=96=E7=95=8C=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现游戏基础架构,包含以下主要功能: - 地图系统:支持地图加载、图层管理和相机控制 - 角色系统:实现角色装备、动画和行为管理 - 场景系统:提供测试场景和世界场景切换 - 世界管理:处理城镇和区域切换逻辑 - 数据加载:添加角色和装备配置加载器 这些改动为游戏开发奠定了基础框架,支持后续功能扩展 --- .../{Camera => camera}/GameCameraController.h | 2 +- Game/include/character/CharacterAnimation.h | 44 ++ Game/include/character/CharacterDataLoader.h | 44 ++ .../character/CharacterEquipmentManager.h | 22 + Game/include/character/CharacterObject.h | 43 ++ Game/include/{Actor => map}/GameDataLoader.h | 0 Game/include/{Actor => map}/GameMap.h | 4 +- Game/include/{Actor => map}/GameMapLayer.h | 0 .../{Actor => scene}/GameMapTestScene.h | 6 +- Game/include/{Actor => world}/GameTown.h | 6 +- Game/include/{Actor => world}/GameWorld.h | 2 +- Game/src/{ => bootstrap}/main.cpp | 4 +- .../GameCameraController.cpp | 4 +- Game/src/character/CharacterAnimation.cpp | 180 +++++++ Game/src/character/CharacterDataLoader.cpp | 447 ++++++++++++++++++ .../character/CharacterEquipmentManager.cpp | 50 ++ Game/src/character/CharacterObject.cpp | 61 +++ Game/src/{Actor => map}/GameDataLoader.cpp | 2 +- Game/src/{Actor => map}/GameMap.cpp | 2 +- Game/src/{Actor => map}/GameMapLayer.cpp | 2 +- .../src/{Actor => scene}/GameMapTestScene.cpp | 29 +- Game/src/{Actor => world}/GameTown.cpp | 2 +- Game/src/{Actor => world}/GameWorld.cpp | 4 +- 23 files changed, 934 insertions(+), 26 deletions(-) rename Game/include/{Camera => camera}/GameCameraController.h (98%) create mode 100644 Game/include/character/CharacterAnimation.h create mode 100644 Game/include/character/CharacterDataLoader.h create mode 100644 Game/include/character/CharacterEquipmentManager.h create mode 100644 Game/include/character/CharacterObject.h rename Game/include/{Actor => map}/GameDataLoader.h (100%) rename Game/include/{Actor => map}/GameMap.h (98%) rename Game/include/{Actor => map}/GameMapLayer.h (100%) rename Game/include/{Actor => scene}/GameMapTestScene.h (74%) rename Game/include/{Actor => world}/GameTown.h (90%) rename Game/include/{Actor => world}/GameWorld.h (95%) rename Game/src/{ => bootstrap}/main.cpp (98%) rename Game/src/{Camera => camera}/GameCameraController.cpp (98%) create mode 100644 Game/src/character/CharacterAnimation.cpp create mode 100644 Game/src/character/CharacterDataLoader.cpp create mode 100644 Game/src/character/CharacterEquipmentManager.cpp create mode 100644 Game/src/character/CharacterObject.cpp rename Game/src/{Actor => map}/GameDataLoader.cpp (99%) rename Game/src/{Actor => map}/GameMap.cpp (99%) rename Game/src/{Actor => map}/GameMapLayer.cpp (96%) rename Game/src/{Actor => scene}/GameMapTestScene.cpp (54%) rename Game/src/{Actor => world}/GameTown.cpp (99%) rename Game/src/{Actor => world}/GameWorld.cpp (96%) diff --git a/Game/include/Camera/GameCameraController.h b/Game/include/camera/GameCameraController.h similarity index 98% rename from Game/include/Camera/GameCameraController.h rename to Game/include/camera/GameCameraController.h index fbee82d..322195e 100644 --- a/Game/include/Camera/GameCameraController.h +++ b/Game/include/camera/GameCameraController.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include #include diff --git a/Game/include/character/CharacterAnimation.h b/Game/include/character/CharacterAnimation.h new file mode 100644 index 0000000..815a0b6 --- /dev/null +++ b/Game/include/character/CharacterAnimation.h @@ -0,0 +1,44 @@ +#pragma once + +#include "character/CharacterDataLoader.h" +#include "character/CharacterEquipmentManager.h" +#include +#include +#include +#include +#include +#include + +namespace frostbite2D { + +class CharacterObject; + +class CharacterAnimation : public Actor { +public: + using ActionAnimationList = std::map>>; + + bool Init(CharacterObject* parent, + const character::CharacterConfig& config, + const CharacterEquipmentManager& equipmentManager); + + void SetAction(const std::string& actionName); + void SetDirection(int direction); + const std::string& GetCurrentAction() const { return currentActionTag_; } + +private: + static std::string FormatImgPath(std::string path, Animation::ReplaceData data); + + void CreateAnimationBySlot(const std::string& actionName, + const std::string& slotName, + const std::string& actionPath, + const character::CharacterConfig& config, + const CharacterEquipmentManager& equipmentManager); + void ApplyFlipRecursive(Actor* actor, bool flipped) const; + + CharacterObject* parent_ = nullptr; + ActionAnimationList actionAnimations_; + std::string currentActionTag_; + int direction_ = 1; +}; + +} // namespace frostbite2D diff --git a/Game/include/character/CharacterDataLoader.h b/Game/include/character/CharacterDataLoader.h new file mode 100644 index 0000000..c2d43f0 --- /dev/null +++ b/Game/include/character/CharacterDataLoader.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace frostbite2D::character { + +struct JobConfig { + std::string name; + std::map defaultAvatarList; +}; + +struct CharacterConfig { + int jobId = -1; + std::string jobTag; + std::string baseJobPath; + JobConfig baseJobConfig; + std::map animationPath; +}; + +struct EquipmentVariation { + int layer = 0; + std::string animationGroup; + std::array imgFormat = {0, 0}; +}; + +struct EquipmentConfig { + int id = -1; + std::string path; + std::string name; + std::vector usableJobs; + std::map> jobAnimations; +}; + +bool loadCharacterIndex(std::map& outIndex); +std::optional loadCharacterConfig(int jobId); + +bool loadEquipmentIndex(std::map& outIndex); +std::optional loadEquipmentConfig(int equipmentId); + +} // namespace frostbite2D::character diff --git a/Game/include/character/CharacterEquipmentManager.h b/Game/include/character/CharacterEquipmentManager.h new file mode 100644 index 0000000..ced0c0c --- /dev/null +++ b/Game/include/character/CharacterEquipmentManager.h @@ -0,0 +1,22 @@ +#pragma once + +#include "character/CharacterDataLoader.h" +#include +#include + +namespace frostbite2D { + +class CharacterEquipmentManager { +public: + bool Init(const character::JobConfig& jobConfig); + + const character::EquipmentConfig* GetEquip(const std::string& slotName) const; + const std::map& GetDefaultEquipment() const { + return defaultEquipment_; + } + +private: + std::map defaultEquipment_; +}; + +} // namespace frostbite2D diff --git a/Game/include/character/CharacterObject.h b/Game/include/character/CharacterObject.h new file mode 100644 index 0000000..cd73e99 --- /dev/null +++ b/Game/include/character/CharacterObject.h @@ -0,0 +1,43 @@ +#pragma once + +#include "character/CharacterAnimation.h" +#include "character/CharacterDataLoader.h" +#include "character/CharacterEquipmentManager.h" +#include +#include +#include + +namespace frostbite2D { + +class CharacterObject : public Actor { +public: + CharacterObject() = default; + ~CharacterObject() override = default; + + bool Construction(int jobId); + + void SetAction(const std::string& actionName); + void SetDirection(int direction); + void SetCharacterPosition(const Vec2& pos); + + int GetJobId() const { return jobId_; } + int GetGrowType() const { return growType_; } + int GetDirection() const { return direction_; } + const std::string& GetCurrentAction() const { return currentAction_; } + + const character::CharacterConfig* GetConfig() const { + return config_ ? &config_.value() : nullptr; + } + const CharacterEquipmentManager& GetEquipmentManager() const { return equipmentManager_; } + +private: + int jobId_ = -1; + int growType_ = -1; + int direction_ = 1; + std::string currentAction_; + std::optional config_; + CharacterEquipmentManager equipmentManager_; + RefPtr animationManager_ = nullptr; +}; + +} // namespace frostbite2D diff --git a/Game/include/Actor/GameDataLoader.h b/Game/include/map/GameDataLoader.h similarity index 100% rename from Game/include/Actor/GameDataLoader.h rename to Game/include/map/GameDataLoader.h diff --git a/Game/include/Actor/GameMap.h b/Game/include/map/GameMap.h similarity index 98% rename from Game/include/Actor/GameMap.h rename to Game/include/map/GameMap.h index bb18290..a74acf2 100644 --- a/Game/include/Actor/GameMap.h +++ b/Game/include/map/GameMap.h @@ -1,7 +1,7 @@ #pragma once -#include "GameDataLoader.h" -#include "GameMapLayer.h" +#include "map/GameDataLoader.h" +#include "map/GameMapLayer.h" #include #include #include diff --git a/Game/include/Actor/GameMapLayer.h b/Game/include/map/GameMapLayer.h similarity index 100% rename from Game/include/Actor/GameMapLayer.h rename to Game/include/map/GameMapLayer.h diff --git a/Game/include/Actor/GameMapTestScene.h b/Game/include/scene/GameMapTestScene.h similarity index 74% rename from Game/include/Actor/GameMapTestScene.h rename to Game/include/scene/GameMapTestScene.h index d71ebbf..23efda2 100644 --- a/Game/include/Actor/GameMapTestScene.h +++ b/Game/include/scene/GameMapTestScene.h @@ -1,7 +1,8 @@ #pragma once -#include "Camera/GameCameraController.h" -#include "GameMap.h" +#include "camera/GameCameraController.h" +#include "character/CharacterObject.h" +#include "map/GameMap.h" #include namespace frostbite2D { @@ -17,6 +18,7 @@ public: private: GameCameraController cameraController_; + RefPtr character_; bool initialized_ = false; RefPtr map_; }; diff --git a/Game/include/Actor/GameTown.h b/Game/include/world/GameTown.h similarity index 90% rename from Game/include/Actor/GameTown.h rename to Game/include/world/GameTown.h index 18d5afa..251a285 100644 --- a/Game/include/Actor/GameTown.h +++ b/Game/include/world/GameTown.h @@ -1,8 +1,8 @@ #pragma once -#include "Camera/GameCameraController.h" -#include "GameDataLoader.h" -#include "GameMap.h" +#include "camera/GameCameraController.h" +#include "map/GameDataLoader.h" +#include "map/GameMap.h" #include #include diff --git a/Game/include/Actor/GameWorld.h b/Game/include/world/GameWorld.h similarity index 95% rename from Game/include/Actor/GameWorld.h rename to Game/include/world/GameWorld.h index 1a8d87f..27ce05e 100644 --- a/Game/include/Actor/GameWorld.h +++ b/Game/include/world/GameWorld.h @@ -1,6 +1,6 @@ #pragma once -#include "GameTown.h" +#include "world/GameTown.h" #include #include diff --git a/Game/src/main.cpp b/Game/src/bootstrap/main.cpp similarity index 98% rename from Game/src/main.cpp rename to Game/src/bootstrap/main.cpp index 21fb7ad..9c451c9 100644 --- a/Game/src/main.cpp +++ b/Game/src/bootstrap/main.cpp @@ -19,8 +19,8 @@ #include #include #include -#include "Actor/GameMapTestScene.h" -#include "Actor/GameWorld.h" +#include "scene/GameMapTestScene.h" +#include "world/GameWorld.h" using namespace frostbite2D; diff --git a/Game/src/Camera/GameCameraController.cpp b/Game/src/camera/GameCameraController.cpp similarity index 98% rename from Game/src/Camera/GameCameraController.cpp rename to Game/src/camera/GameCameraController.cpp index f5d3a5a..39bc465 100644 --- a/Game/src/Camera/GameCameraController.cpp +++ b/Game/src/camera/GameCameraController.cpp @@ -1,5 +1,5 @@ -#include "Camera/GameCameraController.h" -#include "Actor/GameMap.h" +#include "camera/GameCameraController.h" +#include "map/GameMap.h" #include #include #include diff --git a/Game/src/character/CharacterAnimation.cpp b/Game/src/character/CharacterAnimation.cpp new file mode 100644 index 0000000..c7dc6b3 --- /dev/null +++ b/Game/src/character/CharacterAnimation.cpp @@ -0,0 +1,180 @@ +#include "character/CharacterAnimation.h" +#include "character/CharacterObject.h" +#include +#include +#include +#include +#include + +namespace frostbite2D { +namespace { + +const std::array kAvatarParts = { + "weapon_avatar", "aurora_avatar", "hair_avatar", "hat_avatar", + "face_avatar", "breast_avatar", "coat_avatar", "skin_avatar", + "waist_avatar", "pants_avatar", "shoes_avatar", "weapon"}; + +std::string truncatePath(const std::string& path) { + size_t slashPos = path.find_last_of('/'); + if (slashPos == std::string::npos) { + return {}; + } + return path.substr(0, slashPos); +} + +std::string actionTail(const std::string& path) { + size_t slashPos = path.find_last_of('/'); + if (slashPos == std::string::npos) { + return path; + } + return path.substr(slashPos); +} + +} // namespace + +bool CharacterAnimation::Init(CharacterObject* parent, + const character::CharacterConfig& config, + const CharacterEquipmentManager& equipmentManager) { + parent_ = parent; + actionAnimations_.clear(); + currentActionTag_.clear(); + direction_ = 1; + + for (const auto& [actionName, actionPath] : config.animationPath) { + for (const char* slotName : kAvatarParts) { + CreateAnimationBySlot(actionName, slotName, actionPath, config, equipmentManager); + } + } + + if (actionAnimations_.empty()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterAnimation: no usable action animations for job %d", + config.jobId); + return false; + } + + if (actionAnimations_.count("rest") > 0) { + SetAction("rest"); + } else { + SetAction(actionAnimations_.begin()->first); + } + + return true; +} + +std::string CharacterAnimation::FormatImgPath(std::string path, Animation::ReplaceData data) { + size_t pos = path.find("%04d"); + if (pos != std::string::npos) { + path.replace(pos, 4, "%02d%02d"); + } + + char buffer[512] = {}; + std::snprintf(buffer, sizeof(buffer), path.c_str(), data.param1, data.param2); + return std::string(buffer); +} + +void CharacterAnimation::CreateAnimationBySlot( + const std::string& actionName, + const std::string& slotName, + const std::string& actionPath, + const character::CharacterConfig& config, + const CharacterEquipmentManager& equipmentManager) { + if (slotName == std::string("skin_avatar")) { + Animation::ReplaceData replaceData(0, 0); + if (const auto* equip = equipmentManager.GetEquip(slotName)) { + auto it = equip->jobAnimations.find(config.jobId); + if (it != equip->jobAnimations.end() && !it->second.empty()) { + replaceData.param1 = it->second.front().imgFormat[0]; + replaceData.param2 = it->second.front().imgFormat[1]; + } + } + + auto animation = MakePtr(actionPath, FormatImgPath, replaceData); + if (!animation || !animation->IsUsable()) { + return; + } + animation->SetVisible(false); + AddChild(animation); + actionAnimations_[actionName].push_back(animation); + return; + } + + const auto* equip = equipmentManager.GetEquip(slotName); + if (!equip) { + return; + } + + auto jobAniIt = equip->jobAnimations.find(config.jobId); + if (jobAniIt == equip->jobAnimations.end()) { + return; + } + + std::string equipDir = truncatePath(equip->path); + std::string actionPathTail = actionTail(actionPath); + for (const auto& variation : jobAniIt->second) { + if (variation.animationGroup.empty()) { + continue; + } + + std::string aniPath = equipDir + "/" + variation.animationGroup + actionPathTail; + auto animation = MakePtr( + aniPath, FormatImgPath, + Animation::ReplaceData(variation.imgFormat[0], variation.imgFormat[1])); + if (!animation || !animation->IsUsable()) { + continue; + } + + animation->SetVisible(false); + animation->SetZOrder(variation.layer); + AddChild(animation); + actionAnimations_[actionName].push_back(animation); + } +} + +void CharacterAnimation::SetAction(const std::string& actionName) { + auto nextIt = actionAnimations_.find(actionName); + if (nextIt == actionAnimations_.end()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterAnimation: action %s missing, keep %s", actionName.c_str(), + currentActionTag_.c_str()); + return; + } + + if (!currentActionTag_.empty()) { + auto currentIt = actionAnimations_.find(currentActionTag_); + if (currentIt != actionAnimations_.end()) { + for (auto& animation : currentIt->second) { + animation->Reset(); + animation->SetVisible(false); + } + } + } + + for (auto& animation : nextIt->second) { + animation->Reset(); + animation->SetVisible(true); + } + currentActionTag_ = actionName; + SetDirection(direction_); +} + +void CharacterAnimation::SetDirection(int direction) { + direction_ = direction >= 0 ? 1 : -1; + ApplyFlipRecursive(this, direction_ < 0); +} + +void CharacterAnimation::ApplyFlipRecursive(Actor* actor, bool flipped) const { + if (!actor) { + return; + } + + if (auto* sprite = dynamic_cast(actor)) { + sprite->SetFlippedX(flipped); + } + + for (const auto& child : actor->GetChildren()) { + ApplyFlipRecursive(child.Get(), flipped); + } +} + +} // namespace frostbite2D diff --git a/Game/src/character/CharacterDataLoader.cpp b/Game/src/character/CharacterDataLoader.cpp new file mode 100644 index 0000000..20da9a0 --- /dev/null +++ b/Game/src/character/CharacterDataLoader.cpp @@ -0,0 +1,447 @@ +#include "character/CharacterDataLoader.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace frostbite2D::character { +namespace { + +constexpr char kCharacterListPath[] = "character/character.lst"; +constexpr char kEquipmentListPath[] = "equipment/equipment.lst"; + +constexpr auto kTagJobName = u8"[职业名称]"; +constexpr auto kTagDefaultAvatar = u8"[默认时装]"; +constexpr auto kTagDefaultAvatarEnd = u8"[/默认时装]"; +constexpr auto kTagJobLabel = u8"[职业标签]"; +constexpr auto kTagBaseJob = u8"[基础职业属性]"; +constexpr auto kTagActionAnimation = u8"[动作动画]"; +constexpr auto kTagActionAnimationEnd = u8"[/动作动画]"; +constexpr char kTagName[] = "[name]"; +constexpr char kTagUsableJob[] = "[usable job]"; +constexpr char kTagUsableJobEnd[] = "[/usable job]"; +constexpr char kTagAnimationJob[] = "[animation job]"; +constexpr char kTagVariation[] = "[variation]"; +constexpr char kTagLayerVariation[] = "[layer variation]"; +constexpr char kTagEquipmentAniScript[] = "[equipment ani script]"; + +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; +} + +int toInt(const std::string& value, int fallback = 0) { + try { + return std::stoi(value); + } catch (...) { + return fallback; + } +} + +std::string trim(const std::string& value) { + size_t start = 0; + size_t end = value.size(); + while (start < end && std::isspace(static_cast(value[start]))) { + ++start; + } + while (end > start && + std::isspace(static_cast(value[end - 1]))) { + --end; + } + return value.substr(start, end - start); +} + +bool isToken(const std::string& token, const char* expected) { + return trim(token) == expected; +} + +class ScriptTokenStream { +public: + explicit ScriptTokenStream(const std::string& path) + : path_(toLowerCase(path)) { + auto& pvf = PvfArchive::get(); + auto rawData = pvf.getFileRawData(path_); + if (!rawData) { + return; + } + + ScriptParser parser(*rawData, path_); + if (!parser.isValid()) { + 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(); } + + std::string get() { + if (isEnd()) { + return {}; + } + return tokens_[index_++]; + } + + void back() { + if (index_ > 0) { + --index_; + } + } + +private: + std::string path_; + std::vector tokens_; + size_t index_ = 0; + bool valid_ = false; +}; + +int jobNameToIndex(const std::string& jobName) { + static const std::unordered_map kJobMap = { + {"[swordman]", 0}, {"[fighter]", 1}, {"[gunner]", 2}, + {"[mage]", 3}, {"[priest]", 4}, {"[atgunner]", 5}, + {"[thief]", 6}, {"[atfighter]", 7}, {"[atmage]", 8}, + {"[demonic swordman]", 9}, {"[creatormage]", 10}, + }; + + auto it = kJobMap.find(toLowerCase(jobName)); + return it == kJobMap.end() ? -1 : it->second; +} + +std::string normalizeCharacterPath(const std::string& path) { + auto& pvf = PvfArchive::get(); + std::string normalized = pvf.normalizePath(path); + if (normalized.rfind("character/", 0) == 0) { + return normalized; + } + return pvf.normalizePath("character/" + normalized); +} + +void logTokenPreview(const char* label, const std::vector& tokens) { + std::string preview; + size_t previewCount = std::min(tokens.size(), 8); + for (size_t i = 0; i < previewCount; ++i) { + if (!preview.empty()) { + preview += " | "; + } + preview += tokens[i]; + } + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "%s: %s", label, preview.c_str()); +} + +std::optional parseJobConfig(const std::string& path) { + ScriptTokenStream stream(path); + if (!stream.isValid()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterDataLoader: missing job config %s", path.c_str()); + return std::nullopt; + } + + JobConfig config; + while (!stream.isEnd()) { + std::string segment = stream.get(); + if (isToken(segment, kTagJobName)) { + config.name = stream.get(); + } else if (isToken(segment, kTagDefaultAvatar)) { + while (!stream.isEnd()) { + std::string slotName = stream.get(); + if (isToken(slotName, kTagDefaultAvatarEnd)) { + break; + } + config.defaultAvatarList[slotName] = toInt(stream.get(), -1); + } + } + } + + return config; +} + +std::map& characterIndexCache() { + static std::map cache; + static bool loaded = false; + if (!loaded) { + ScriptTokenStream stream(kCharacterListPath); + if (stream.isValid()) { + while (!stream.isEnd()) { + std::string indexToken = stream.get(); + if (indexToken.empty()) { + break; + } + std::string pathToken = stream.get(); + if (pathToken.empty()) { + break; + } + cache[toInt(indexToken, -1)] = normalizeCharacterPath(pathToken); + } + } + loaded = true; + } + return cache; +} + +std::map& equipmentIndexCache() { + static std::map cache; + static bool loaded = false; + if (!loaded) { + ScriptTokenStream stream(kEquipmentListPath); + if (stream.isValid()) { + while (!stream.isEnd()) { + std::string indexToken = stream.get(); + if (indexToken.empty()) { + break; + } + std::string pathToken = stream.get(); + if (pathToken.empty()) { + break; + } + cache[toInt(indexToken, -1)] = + PvfArchive::get().normalizePath("equipment/" + pathToken); + } + } else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterDataLoader: equipment index %s not found, fallback to skin-only character rendering", + kEquipmentListPath); + } + loaded = true; + } + return cache; +} + +std::map& characterConfigCache() { + static std::map cache; + return cache; +} + +std::map& equipmentConfigCache() { + static std::map cache; + return cache; +} + +void appendVariationForJobs(EquipmentConfig& config, + const std::vector& jobIndices, + int param1, + int param2, + ScriptTokenStream& stream) { + std::vector variations; + + while (!stream.isEnd()) { + std::string token = stream.get(); + if (token == kTagLayerVariation) { + EquipmentVariation variation; + variation.layer = toInt(stream.get(), 0); + variation.animationGroup = PvfArchive::get().normalizePath(stream.get()); + variation.imgFormat = {param1, param2}; + variations.push_back(variation); + + if (!stream.isEnd()) { + std::string maybeTag = stream.get(); + if (maybeTag == kTagEquipmentAniScript) { + if (!stream.isEnd()) { + std::string ignoredScriptPath = stream.get(); + (void)ignoredScriptPath; + } + } else { + stream.back(); + } + } + continue; + } + + if (config.path.find("skin") != std::string::npos) { + EquipmentVariation variation; + variation.layer = 0; + variation.animationGroup.clear(); + variation.imgFormat = {param1, param2}; + variations.push_back(variation); + } + + stream.back(); + break; + } + + for (int jobIndex : jobIndices) { + if (jobIndex < 0) { + continue; + } + auto& target = config.jobAnimations[jobIndex]; + target.insert(target.end(), variations.begin(), variations.end()); + } +} + +} // namespace + +bool loadCharacterIndex(std::map& outIndex) { + outIndex = characterIndexCache(); + return !outIndex.empty(); +} + +std::optional loadCharacterConfig(int jobId) { + auto cacheIt = characterConfigCache().find(jobId); + if (cacheIt != characterConfigCache().end()) { + return cacheIt->second; + } + + auto indexIt = characterIndexCache().find(jobId); + if (indexIt == characterIndexCache().end()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterDataLoader: character job %d not found in index", jobId); + return std::nullopt; + } + + ScriptTokenStream stream(indexIt->second); + if (!stream.isValid()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterDataLoader: unable to load character config %s", + indexIt->second.c_str()); + return std::nullopt; + } + + CharacterConfig config; + config.jobId = jobId; + std::vector tokenPreview; + while (!stream.isEnd()) { + std::string segment = stream.get(); + if (tokenPreview.size() < 8) { + tokenPreview.push_back(segment); + } + + if (isToken(segment, kTagJobLabel)) { + std::string jobTag = stream.get(); + if (jobTag.size() >= 2 && jobTag.front() == '[' && jobTag.back() == ']') { + jobTag = jobTag.substr(1, jobTag.size() - 2); + } + config.jobTag = toLowerCase(jobTag); + } else if (isToken(segment, kTagBaseJob)) { + config.baseJobPath = PvfArchive::get().normalizePath(stream.get()); + auto baseJobConfig = parseJobConfig(config.baseJobPath); + if (baseJobConfig) { + config.baseJobConfig = *baseJobConfig; + } else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterDataLoader: base job config missing for job %d from %s", + jobId, config.baseJobPath.c_str()); + } + } else if (isToken(segment, kTagActionAnimation)) { + while (!stream.isEnd()) { + std::string actionName = stream.get(); + if (isToken(actionName, kTagActionAnimationEnd)) { + break; + } + std::string actionPath = stream.get(); + config.animationPath[actionName] = PvfArchive::get().normalizePath( + "character/" + config.jobTag + "/" + actionPath); + } + } + } + + if (config.jobTag.empty() || config.animationPath.empty()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterDataLoader: character job %d config incomplete (jobTag=%s, baseJobPath=%s, actionCount=%d, script=%s)", + jobId, config.jobTag.c_str(), config.baseJobPath.c_str(), + static_cast(config.animationPath.size()), indexIt->second.c_str()); + logTokenPreview("CharacterDataLoader token preview", tokenPreview); + return std::nullopt; + } + + characterConfigCache()[jobId] = config; + return config; +} + +bool loadEquipmentIndex(std::map& outIndex) { + outIndex = equipmentIndexCache(); + return !outIndex.empty(); +} + +std::optional loadEquipmentConfig(int equipmentId) { + auto cacheIt = equipmentConfigCache().find(equipmentId); + if (cacheIt != equipmentConfigCache().end()) { + return cacheIt->second; + } + + auto indexIt = equipmentIndexCache().find(equipmentId); + if (indexIt == equipmentIndexCache().end()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterDataLoader: equipment %d missing from equipment.lst", + equipmentId); + return std::nullopt; + } + + ScriptTokenStream stream(indexIt->second); + if (!stream.isValid()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterDataLoader: unable to load equipment %d from %s", + equipmentId, indexIt->second.c_str()); + return std::nullopt; + } + + EquipmentConfig config; + config.id = equipmentId; + config.path = indexIt->second; + SDL_Log("CharacterDataLoader: equipment %d -> %s", equipmentId, config.path.c_str()); + + while (!stream.isEnd()) { + std::string segment = stream.get(); + if (segment == kTagName) { + config.name = stream.get(); + continue; + } + + if (segment == kTagUsableJob) { + while (!stream.isEnd()) { + std::string jobName = stream.get(); + if (jobName == kTagUsableJobEnd) { + break; + } + int jobIndex = jobNameToIndex(jobName); + if (jobIndex >= 0) { + config.usableJobs.push_back(jobIndex); + } + } + continue; + } + + if (segment != kTagAnimationJob) { + if (segment == kTagVariation) { + int param1 = toInt(stream.get(), 0); + int param2 = toInt(stream.get(), 0); + appendVariationForJobs(config, config.usableJobs, param1, param2, stream); + } + continue; + } + + std::string jobName = stream.get(); + int jobIndex = jobNameToIndex(jobName); + if (jobIndex < 0) { + continue; + } + + if (stream.isEnd()) { + break; + } + + std::string variationTag = stream.get(); + if (variationTag != kTagVariation) { + continue; + } + + int param1 = toInt(stream.get(), 0); + int param2 = toInt(stream.get(), 0); + appendVariationForJobs(config, {jobIndex}, param1, param2, stream); + } + + equipmentConfigCache()[equipmentId] = config; + return config; +} + +} // namespace frostbite2D::character diff --git a/Game/src/character/CharacterEquipmentManager.cpp b/Game/src/character/CharacterEquipmentManager.cpp new file mode 100644 index 0000000..d7f6e83 --- /dev/null +++ b/Game/src/character/CharacterEquipmentManager.cpp @@ -0,0 +1,50 @@ +#include "character/CharacterEquipmentManager.h" +#include + +namespace frostbite2D { + +bool CharacterEquipmentManager::Init(const character::JobConfig& jobConfig) { + defaultEquipment_.clear(); + + if (jobConfig.defaultAvatarList.empty()) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterEquipmentManager: no default avatar entries, fallback to skin-only character rendering"); + return true; + } + + int loadedCount = 0; + for (const auto& [slotName, equipmentId] : jobConfig.defaultAvatarList) { + if (equipmentId < 0) { + continue; + } + + auto equipmentConfig = character::loadEquipmentConfig(equipmentId); + if (!equipmentConfig) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterEquipmentManager: equipment %d for slot %s unavailable, skip layered part", + equipmentId, slotName.c_str()); + continue; + } + + defaultEquipment_[slotName] = *equipmentConfig; + ++loadedCount; + } + + if (loadedCount == 0) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterEquipmentManager: no avatar equipment loaded, fallback to skin-only character rendering"); + } + + return true; +} + +const character::EquipmentConfig* CharacterEquipmentManager::GetEquip( + const std::string& slotName) const { + auto it = defaultEquipment_.find(slotName); + if (it == defaultEquipment_.end()) { + return nullptr; + } + return &it->second; +} + +} // namespace frostbite2D diff --git a/Game/src/character/CharacterObject.cpp b/Game/src/character/CharacterObject.cpp new file mode 100644 index 0000000..376ed09 --- /dev/null +++ b/Game/src/character/CharacterObject.cpp @@ -0,0 +1,61 @@ +#include "character/CharacterObject.h" +#include + +namespace frostbite2D { + +bool CharacterObject::Construction(int jobId) { + RemoveAllChildren(); + animationManager_ = nullptr; + config_.reset(); + currentAction_.clear(); + + auto config = character::loadCharacterConfig(jobId); + if (!config) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, + "CharacterObject: failed to load job %d config", jobId); + return false; + } + + jobId_ = jobId; + growType_ = -1; + direction_ = 1; + config_ = *config; + equipmentManager_.Init(config_->baseJobConfig); + + animationManager_ = MakePtr(); + if (!animationManager_->Init(this, *config_, equipmentManager_)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "CharacterObject: no layered avatar animations available, character will stay empty"); + } + AddChild(animationManager_); + + if (config_->animationPath.count("rest") > 0) { + SetAction("rest"); + } else if (!config_->animationPath.empty()) { + SetAction(config_->animationPath.begin()->first); + } + + SetDirection(1); + return true; +} + +void CharacterObject::SetAction(const std::string& actionName) { + currentAction_ = actionName; + if (animationManager_) { + animationManager_->SetAction(actionName); + } +} + +void CharacterObject::SetDirection(int direction) { + direction_ = direction >= 0 ? 1 : -1; + if (animationManager_) { + animationManager_->SetDirection(direction_); + } +} + +void CharacterObject::SetCharacterPosition(const Vec2& pos) { + SetPosition(pos); + SetZOrder(static_cast(pos.y)); +} + +} // namespace frostbite2D diff --git a/Game/src/Actor/GameDataLoader.cpp b/Game/src/map/GameDataLoader.cpp similarity index 99% rename from Game/src/Actor/GameDataLoader.cpp rename to Game/src/map/GameDataLoader.cpp index 2d620d2..00ebb81 100644 --- a/Game/src/Actor/GameDataLoader.cpp +++ b/Game/src/map/GameDataLoader.cpp @@ -1,4 +1,4 @@ -#include "Actor/GameDataLoader.h" +#include "map/GameDataLoader.h" #include #include #include diff --git a/Game/src/Actor/GameMap.cpp b/Game/src/map/GameMap.cpp similarity index 99% rename from Game/src/Actor/GameMap.cpp rename to Game/src/map/GameMap.cpp index a479a5a..6cd9c92 100644 --- a/Game/src/Actor/GameMap.cpp +++ b/Game/src/map/GameMap.cpp @@ -1,4 +1,4 @@ -#include "Actor/GameMap.h" +#include "map/GameMap.h" #include #include #include diff --git a/Game/src/Actor/GameMapLayer.cpp b/Game/src/map/GameMapLayer.cpp similarity index 96% rename from Game/src/Actor/GameMapLayer.cpp rename to Game/src/map/GameMapLayer.cpp index b3d1f83..9785406 100644 --- a/Game/src/Actor/GameMapLayer.cpp +++ b/Game/src/map/GameMapLayer.cpp @@ -1,4 +1,4 @@ -#include "Actor/GameMapLayer.h" +#include "map/GameMapLayer.h" #include namespace frostbite2D { diff --git a/Game/src/Actor/GameMapTestScene.cpp b/Game/src/scene/GameMapTestScene.cpp similarity index 54% rename from Game/src/Actor/GameMapTestScene.cpp rename to Game/src/scene/GameMapTestScene.cpp index 777c56d..4a38e54 100644 --- a/Game/src/Actor/GameMapTestScene.cpp +++ b/Game/src/scene/GameMapTestScene.cpp @@ -1,6 +1,6 @@ -#include "Actor/GameMapTestScene.h" +#include "scene/GameMapTestScene.h" #include -#include + namespace frostbite2D { namespace { @@ -25,15 +25,30 @@ void GameMapTestScene::onEnter() { return; } AddChild(map_); - + + character_ = MakePtr(); + if (!character_->Construction(0)) { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, + "GameMapTestScene: failed to construct default character"); + character_.Reset(); + } else { + Vec2 spawnPos = map_->ClampCameraFocus(map_->GetDefaultCameraFocus(), 1.2f); + character_->SetCharacterPosition(spawnPos); + map_->AddObject(character_); + } + cameraController_.SetMap(map_.Get()); cameraController_.SetZoom(1.2f); - cameraController_.ClearTarget(); - cameraController_.SetDebugEnabled(true); - cameraController_.SnapToDefaultFocus(); + cameraController_.SetTarget(character_.Get()); + cameraController_.SetDebugEnabled(false); + if (character_) { + cameraController_.SetFocus(character_->GetPosition()); + } else { + cameraController_.ClearTarget(); + cameraController_.SnapToDefaultFocus(); + } map_->Enter(); initialized_ = true; - } void GameMapTestScene::onExit() { diff --git a/Game/src/Actor/GameTown.cpp b/Game/src/world/GameTown.cpp similarity index 99% rename from Game/src/Actor/GameTown.cpp rename to Game/src/world/GameTown.cpp index 9719012..a6f3e33 100644 --- a/Game/src/Actor/GameTown.cpp +++ b/Game/src/world/GameTown.cpp @@ -1,4 +1,4 @@ -#include "Actor/GameTown.h" +#include "world/GameTown.h" #include #include diff --git a/Game/src/Actor/GameWorld.cpp b/Game/src/world/GameWorld.cpp similarity index 96% rename from Game/src/Actor/GameWorld.cpp rename to Game/src/world/GameWorld.cpp index 92bd6d8..2b85bca 100644 --- a/Game/src/Actor/GameWorld.cpp +++ b/Game/src/world/GameWorld.cpp @@ -1,5 +1,5 @@ -#include "Actor/GameWorld.h" -#include "Actor/GameDataLoader.h" +#include "world/GameWorld.h" +#include "map/GameDataLoader.h" #include #include