diff --git a/data/saland/hair/hair.json b/data/saland/hair/hair.json new file mode 100644 index 0000000..09123c9 --- /dev/null +++ b/data/saland/hair/hair.json @@ -0,0 +1,28 @@ +{ + "hair": [ + { "hairid": "standard_hair", "displayName": "Messy Redhead", "bg": "", "restriction": ["male"] }, + { "hairid": "standard_hair", "displayName": "Ponytail Redhead", "bg": "standard_hair_bg", "restriction": ["female"] }, + { "hairid": "hair_blonde", "displayName": "Blonde", "bg": "", "restriction": ["male"] }, + { "hairid": "hair_blonde", "displayName": "Ponytail Blonde", "bg": "hair_blonde_bg", "restriction": ["female"] }, + { "hairid": "hair_brunette", "displayName": "Brunette", "bg": "", "restriction": ["male"] }, + { "hairid": "hair_brunette", "displayName": "Ponytail Brunette", "bg": "hair_brunette_bg", "restriction": ["female"] }, + { "hairid": "hair_raven", "displayName": "Black", "bg": "", "restriction": ["male"] }, + { "hairid": "hair_raven", "displayName": "Ponytail Black", "bg": "hair_raven_bg", "restriction": ["female"] }, + { "hairid": "hair_blue", "displayName": "Blue", "bg": "", "restriction": ["male"] }, + { "hairid": "hair_blue", "displayName": "Ponytail Blue", "bg": "hair_blue_bg", "restriction": ["female"] }, + { "hairid": "hair_1", "displayName": "Short Blue", "bg": "" }, + { "hairid": "hair_sara_black", "displayName": "Sara (Black)", "bg": "hair_sara_bg_black" }, + { "hairid": "hair_sara_blonde", "displayName": "Sara (Blonde)", "bg": "hair_sara_bg_blonde" }, + { "hairid": "hair_sara_blue", "displayName": "Sara (Blue)", "bg": "hair_sara_bg_blue" }, + { "hairid": "hair_sara_brown", "displayName": "Sara (Brown)", "bg": "hair_sara_bg_brown" }, + { "hairid": "hair_sara_ginger", "displayName": "Sara (Ginger)", "bg": "hair_sara_bg_ginger" }, + { "hairid": "hair_sara_gold", "displayName": "Sara (Gold)", "bg": "hair_sara_bg_gold" }, + { "hairid": "hair_sara_gray", "displayName": "Sara (Gray)", "bg": "hair_sara_bg_gray" }, + { "hairid": "hair_sara_green", "displayName": "Sara (Green)", "bg": "hair_sara_bg_green" }, + { "hairid": "hair_sara_pink", "displayName": "Sara (Pink)", "bg": "hair_sara_bg_pink" }, + { "hairid": "hair_sara_purple", "displayName": "Sara (Purple)", "bg": "hair_sara_bg_purple" }, + { "hairid": "hair_sara_raven", "displayName": "Sara (Raven)", "bg": "hair_sara_bg_raven" }, + { "hairid": "hair_sara_red", "displayName": "Sara (Red)", "bg": "hair_sara_bg_red" }, + { "hairid": "hair_sara_white", "displayName": "Sara (White)", "bg": "hair_sara_bg_white" } + ] +} diff --git a/src/ImGuiPlayerSelect.cpp b/src/ImGuiPlayerSelect.cpp index 29d55af..e482e28 100644 --- a/src/ImGuiPlayerSelect.cpp +++ b/src/ImGuiPlayerSelect.cpp @@ -33,29 +33,6 @@ https://github.com/sago007/saland #include ImGuiPlayerSelect::ImGuiPlayerSelect() { - // Define available hair options - hairOptions = { - {"standard_hair", "Redhead", ""}, - {"hair_blonde", "Blonde", ""}, - {"hair_brunette", "Brunette", ""}, - {"hair_raven", "Black", ""}, - {"hair_blue", "Blue", ""}, - {"hair_1", "Short Blue", ""}, - {"hair_sara_black", "Sara (Black)", "hair_sara_bg_black"}, - {"hair_sara_blonde", "Sara (Blonde)", "hair_sara_bg_blonde"}, - {"hair_sara_blue", "Sara (Blue)", "hair_sara_bg_blue"}, - {"hair_sara_brown", "Sara (Brown)", "hair_sara_bg_brown"}, - {"hair_sara_ginger", "Sara (Ginger)", "hair_sara_bg_ginger"}, - {"hair_sara_gold", "Sara (Gold)", "hair_sara_bg_gold"}, - {"hair_sara_gray", "Sara (Gray)", "hair_sara_bg_gray"}, - {"hair_sara_green", "Sara (Green)", "hair_sara_bg_green"}, - {"hair_sara_pink", "Sara (Pink)", "hair_sara_bg_pink"}, - {"hair_sara_purple", "Sara (Purple)", "hair_sara_bg_purple"}, - {"hair_sara_raven", "Sara (Raven)", "hair_sara_bg_raven"}, - {"hair_sara_red", "Sara (Red)", "hair_sara_bg_red"}, - {"hair_sara_white", "Sara (White)", "hair_sara_bg_white"}, - }; - LoadPlayerList(); LoadSelectedPlayer(); } @@ -107,10 +84,13 @@ void ImGuiPlayerSelect::LoadSelectedPlayer() { // Set race index selectedRaceIndex = (editingPlayer.race == "male") ? 0 : 1; + // Load hair options filtered by current race + hairOptions = getHairOptions(editingPlayer.race); + // Set hair index selectedHairIndex = 0; for (size_t i = 0; i < hairOptions.size(); i++) { - if (hairOptions[i].id == editingPlayer.hair) { + if (hairOptions[i].hairid == editingPlayer.hair) { selectedHairIndex = i; break; } @@ -249,6 +229,11 @@ void ImGuiPlayerSelect::Draw(SDL_Renderer* target) { const char* races[] = { "Male", "Female" }; if (ImGui::Combo("##race", &selectedRaceIndex, races, 2)) { editingPlayer.race = (selectedRaceIndex == 0) ? "male" : "female"; + hairOptions = getHairOptions(editingPlayer.race); + selectedHairIndex = 0; + if (!hairOptions.empty()) { + editingPlayer.hair = hairOptions[0].hairid; + } needsSave = true; } @@ -261,8 +246,7 @@ void ImGuiPlayerSelect::Draw(SDL_Renderer* target) { hairNames.push_back(option.displayName.c_str()); } if (ImGui::Combo("##hair", &selectedHairIndex, hairNames.data(), hairNames.size())) { - editingPlayer.hair = hairOptions[selectedHairIndex].id; - editingPlayer.hair_bg = hairOptions[selectedHairIndex].bg_id; + editingPlayer.hair = hairOptions[selectedHairIndex].hairid; needsSave = true; } @@ -304,7 +288,7 @@ void ImGuiPlayerSelect::Draw(SDL_Renderer* target) { const sago::SagoSprite& baseSprite = globalData.spriteHolder->GetSprite(spriteName); // Draw back hair (bg) before the body so it appears behind the character - std::string hair_bg = editingPlayer.get_visible_hair_bg(); + std::string hair_bg = getHairDef(editingPlayer.hair, editingPlayer.race).bg; if (hair_bg.length()) { std::string hairBgSpriteName = editingPlayer.get_visible_race() + "_walkcycle_" + hair_bg + "_W"; const sago::SagoSprite& hairBgSprite = globalData.spriteHolder->GetSprite(hairBgSpriteName); diff --git a/src/ImGuiPlayerSelect.hpp b/src/ImGuiPlayerSelect.hpp index 360f5c8..7909c8f 100644 --- a/src/ImGuiPlayerSelect.hpp +++ b/src/ImGuiPlayerSelect.hpp @@ -26,6 +26,7 @@ https://github.com/sago007/saland #include "sago/GameStateInterface.hpp" #include "saland/model/Player.hpp" +#include "saland/GameHair.hpp" #include #include @@ -58,13 +59,7 @@ private: char nameBuffer[64] = ""; int selectedRaceIndex = 0; int selectedHairIndex = 0; - - struct HairOption { - std::string id; - std::string displayName; - std::string bg_id; // Optional background hair sprite id - }; - std::vector hairOptions; + std::vector hairOptions; bool needsSave = false; bool needsRefresh = false; diff --git a/src/saland/Game.cpp b/src/saland/Game.cpp index 40a602f..6eecb4c 100644 --- a/src/saland/Game.cpp +++ b/src/saland/Game.cpp @@ -341,7 +341,6 @@ Game::Game() { ResetWorld(0, 0, false); data->human->pants = globalData.player.get_visible_bottom(); data->human->hair = globalData.player.get_visible_hair(); - data->human->hair_bg = globalData.player.get_visible_hair_bg(); data->human->race = globalData.player.get_visible_race(); data->human->top = globalData.player.get_visible_top(); @@ -448,7 +447,6 @@ void Game::Draw(SDL_Renderer* target) { } data->human->pants = globalData.player.get_visible_bottom(); data->human->hair = globalData.player.get_visible_hair(); - data->human->hair_bg = globalData.player.get_visible_hair_bg(); data->human->race = globalData.player.get_visible_race(); data->human->top = globalData.player.get_visible_top(); std::sort(data->gameRegion.placeables.begin(), data->gameRegion.placeables.end(),sort_placeable); diff --git a/src/saland/GameDraw.cpp b/src/saland/GameDraw.cpp index 195bcfd..ebb1b33 100644 --- a/src/saland/GameDraw.cpp +++ b/src/saland/GameDraw.cpp @@ -22,6 +22,7 @@ https://github.com/sago007/saland */ #include "GameDraw.hpp" +#include "GameHair.hpp" #include #include #include "../sago/SagoTextField.hpp" @@ -215,9 +216,10 @@ void DrawHumanEntity(SDL_Renderer* target, sago::SagoSpriteHolder* sHolder, cons int y = std::round(entity->Y) - offsetY; DrawCollision(target, entity, offsetX, offsetY, drawCollision, resize); // Draw back hair (bg) before the body so it appears behind the character - if (entity->hair_bg.length() > 0) { + std::string hairBgId = getHairDef(entity->hair, entity->race).bg; + if (hairBgId.length() > 0) { std::string hairBgAnimation = animation; - const sago::SagoSprite& myHairBg = sHolder->GetSprite(entity->race + "_"+hairBgAnimation+"_"+entity->hair_bg+"_"+std::string(1,entity->direction)); + const sago::SagoSprite& myHairBg = sHolder->GetSprite(entity->race + "_"+hairBgAnimation+"_"+hairBgId+"_"+std::string(1,entity->direction)); if (relativeAnimation) { myHairBg.DrawProgressive(target, relativeAnimationState, x, y, resize); } diff --git a/src/saland/GameHair.cpp b/src/saland/GameHair.cpp new file mode 100644 index 0000000..318c489 --- /dev/null +++ b/src/saland/GameHair.cpp @@ -0,0 +1,126 @@ +/* +=========================================================================== + * Saland Adventures +Copyright (C) 2014-2026 Poul Sander + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see http://www.gnu.org/licenses/ + +Source information and contacts persons can be found at +https://github.com/sago007/saland +=========================================================================== +*/ + +#include "GameHair.hpp" +#include +#include +#include "../sago/SagoMisc.hpp" +#include "rapidjson/document.h" + +const char* const hair_dir = "saland/hair/"; + +static HairDef hairDefDefault; + +static std::vector all_hair; + +static bool matchesRace(const HairDef& hair, const std::string& race) { + if (hair.restriction.empty()) { + return true; + } + for (const auto& r : hair.restriction) { + if (r == race) { + return true; + } + } + return false; +} + +static void ReadHairFile(const std::string& filename) { + std::string content = sago::GetFileContent(filename.c_str()); + rapidjson::Document document; + document.Parse(content.c_str()); + if (!document.IsObject()) { + std::cerr << "Failed to parse: " << filename << "\n"; + return; + } + for (auto& m : document.GetObject()) { + const std::string& header = m.name.GetString(); + if (header == "hair") { + const auto& items = m.value; + if (!items.IsArray()) { + std::cerr << "Failure reading " << filename << ": 'hair' must be an array\n"; + continue; + } + for (const auto& item : items.GetArray()) { + if (item.IsObject()) { + HairDef new_hair = hairDefDefault; + for (const auto& member : item.GetObject()) { + if (member.name == "hairid") { + new_hair.hairid = member.value.GetString(); + } + if (member.name == "displayName") { + new_hair.displayName = member.value.GetString(); + } + if (member.name == "bg") { + new_hair.bg = member.value.GetString(); + } + if (member.name == "restriction") { + if (member.value.IsArray()) { + for (const auto& r : member.value.GetArray()) { + if (r.IsString()) { + new_hair.restriction.push_back(r.GetString()); + } + } + } + } + } + all_hair.push_back(new_hair); + } + } + } + } +} + +static void initHair() { + std::vector file_list = sago::GetFileList(hair_dir); + for (const std::string& file : file_list) { + std::string filename = std::format("{}{}", hair_dir, file); + printf("Hair file: %s\n", filename.c_str()); + ReadHairFile(filename); + } +} + +const HairDef& getHairDef(const std::string& hairName, const std::string& race) { + if (all_hair.empty()) { + initHair(); + } + for (const auto& hair : all_hair) { + if (hair.hairid == hairName && matchesRace(hair, race)) { + return hair; + } + } + return hairDefDefault; +} + +std::vector getHairOptions(const std::string& race) { + if (all_hair.empty()) { + initHair(); + } + std::vector result; + for (const auto& hair : all_hair) { + if (matchesRace(hair, race)) { + result.push_back(hair); + } + } + return result; +} diff --git a/src/saland/GameHair.hpp b/src/saland/GameHair.hpp new file mode 100644 index 0000000..f41bb6c --- /dev/null +++ b/src/saland/GameHair.hpp @@ -0,0 +1,40 @@ +/* +=========================================================================== + * Saland Adventures +Copyright (C) 2014-2026 Poul Sander + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see http://www.gnu.org/licenses/ + +Source information and contacts persons can be found at +https://github.com/sago007/saland +=========================================================================== +*/ + +#ifndef GAMEHAIR_HPP +#define GAMEHAIR_HPP + +#include +#include + +struct HairDef { + std::string hairid; + std::string displayName; + std::string bg; + std::vector restriction; +}; + +const HairDef& getHairDef(const std::string& hairName, const std::string& race); +std::vector getHairOptions(const std::string& race); + +#endif /* GAMEHAIR_HPP */ diff --git a/src/saland/model/Player.cpp b/src/saland/model/Player.cpp index dac9764..1a0951d 100644 --- a/src/saland/model/Player.cpp +++ b/src/saland/model/Player.cpp @@ -24,7 +24,7 @@ https://github.com/sago007/saland #include "Player.hpp" void to_json(nlohmann::json& j, const Player& p) { - j = nlohmann::json{ {"race", p.race}, {"hair", p.hair}, {"hair_bg", p.hair_bg}, {"item_inventory", p.item_inventory}, {"equipped_items", p.equipped_items} }; + j = nlohmann::json{ {"race", p.race}, {"hair", p.hair}, {"item_inventory", p.item_inventory}, {"equipped_items", p.equipped_items} }; } void from_json(const nlohmann::json& j, Player& p) { @@ -32,9 +32,6 @@ void from_json(const nlohmann::json& j, Player& p) { if (j.contains("hair")) { j.at("hair").get_to(p.hair); } - if (j.contains("hair_bg")) { - j.at("hair_bg").get_to(p.hair_bg); - } j.at("item_inventory").get_to(p.item_inventory); if (j.contains("equipped_items")) { j.at("equipped_items").get_to(p.equipped_items); diff --git a/src/saland/model/Player.hpp b/src/saland/model/Player.hpp index 9eb8c61..370e3e1 100644 --- a/src/saland/model/Player.hpp +++ b/src/saland/model/Player.hpp @@ -34,7 +34,6 @@ struct Player { std::string save_name="player1"; std::string race = "female"; std::string hair = "standard_hair"; - std::string hair_bg; std::string visible_bottom; std::string visible_top; std::string get_visible_race() { @@ -43,9 +42,6 @@ struct Player { std::string get_visible_hair() { return this->hair; } - std::string get_visible_hair_bg() { - return this->hair_bg; - } std::string get_visible_bottom() { if (visible_bottom != "") { return visible_bottom; diff --git a/src/saland/model/placeables.hpp b/src/saland/model/placeables.hpp index c4b7a96..4fd0918 100644 --- a/src/saland/model/placeables.hpp +++ b/src/saland/model/placeables.hpp @@ -97,7 +97,6 @@ class Human : public Creature { public: std::string race = "male"; std::string hair = ""; - std::string hair_bg = ""; std::string pants = ""; std::string top = ""; std::string weapon = "";