commit bb80162f
Add option for defining hair in a json file.
Changed files
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 <format>
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 <string>
#include <vector>
@@ -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<HairOption> hairOptions;
+ std::vector<HairDef> 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 <SDL2/SDL2_gfxPrimitives.h>
#include <cmath>
#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 <iostream>
+#include <format>
+#include "../sago/SagoMisc.hpp"
+#include "rapidjson/document.h"
+
+const char* const hair_dir = "saland/hair/";
+
+static HairDef hairDefDefault;
+
+static std::vector<HairDef> 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<std::string> 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<HairDef> getHairOptions(const std::string& race) {
+ if (all_hair.empty()) {
+ initHair();
+ }
+ std::vector<HairDef> 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 <string>
+#include <vector>
+
+struct HairDef {
+ std::string hairid;
+ std::string displayName;
+ std::string bg;
+ std::vector<std::string> restriction;
+};
+
+const HairDef& getHairDef(const std::string& hairName, const std::string& race);
+std::vector<HairDef> 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 = "";