New game works almost as intended, quest system

This commit is contained in:
2026-02-05 18:21:25 +03:00
parent 4fb7e94fed
commit 0405214388
16 changed files with 826 additions and 517 deletions

View File

@@ -33,7 +33,6 @@ class ActionNodeActions {
const ActionNodeList &alist =
ECS::get<ActionNodeList>();
state.apply(effects);
// node position can change in case of character
const Ogre::Vector3 &nodePosition =
alist.dynamicNodes[node].position;
state.setPosition(nodePosition);
@@ -49,13 +48,8 @@ class ActionNodeActions {
{
const ActionNodeList &alist =
ECS::get<ActionNodeList>();
// const TownNPCs &npcs = bb.town.get<TownNPCs>();
// const TownAI &ai = bb.town.get<TownAI>();
const Ogre::Vector3 &nodePosition =
alist.dynamicNodes[node].position;
// flecs::entity e = npcs.npcs.at(bb.index).e;
// bool validActive = e.is_valid() &&
// e.has<CharacterBase>();
const Ogre::Vector3 &npcPosition =
bb.getPosition();
float dist = npcPosition.squaredDistance(
@@ -359,9 +353,6 @@ CharacterAIModule::CharacterAIModule(flecs::world &ecs)
std::lock_guard<std::mutex> lock(
ecs_mutex);
alist.build();
updateBlackboardsBits(
town, alist, npcs, ai);
updateBlackboards(town, alist,
npcs, ai);
}
@@ -370,12 +361,12 @@ CharacterAIModule::CharacterAIModule(flecs::world &ecs)
->addMainThreadTask([this, town,
&alist]() {
town.modified<TownAI>();
town.modified<TownNPCs>();
ECS::modified<
ActionNodeList>();
});
});
});
#if 1
ecs.system<TownAI, TownNPCs>("PlanAI")
.kind(flecs::OnUpdate)
.interval(0.5f)
@@ -394,7 +385,6 @@ CharacterAIModule::CharacterAIModule(flecs::world &ecs)
});
});
});
#endif
}
void CharacterAIModule::createAI(flecs::entity town)
@@ -466,7 +456,6 @@ void CharacterAIModule::buildPlans(flecs::entity town, const TownNPCs &npcs,
<< std::endl;
ai.plans[plan_tasks.front().blackboard.index].push_back(
plan_tasks.front().plan);
// OgreAssert(false, "plan...");
}
plan_tasks.pop_front();
} else
@@ -504,20 +493,24 @@ void CharacterAIModule::createBlackboards(flecs::entity town,
strength += 10;
dexterity += 10;
}
// FIXME: use separate "memory" for stats
// Do not keep actual stats in blackboard
nlohmann::json memory;
memory["strength"] = strength;
memory["dexterity"] = dexterity;
memory["health"] = health;
memory["stamina"] = stamina;
memory["needs_hunger"] = 0;
memory["needs_thirst"] = 0;
memory["needs_toilet"] = 0;
nlohmann::json bb;
bb["strength"] = strength;
bb["dexterity"] = dexterity;
bb["health"] = health;
bb["stamina"] = stamina;
bb["needs_hunger"] = 0;
bb["needs_thirst"] = 0;
bb["needs_toilet"] = 0;
bb["have_water"] = 0;
bb["have_food"] = 0;
bb["have_medicine"] = 0;
bb["at_object"] = 0;
ai.memory[it->first] = memory;
ai.blackboards[it->first] = Blackboard(bb);
ai.blackboards[it->first].index = it->first;
ai.blackboards.at(it->first).town = town;
@@ -525,93 +518,103 @@ void CharacterAIModule::createBlackboards(flecs::entity town,
ai.blackboards.at(it->first).actionRefResize(0);
ai.blackboards.at(it->first).actionRefAddActions(
ai.actions);
ai.conditions =
TownAI::BitEvolutionBuilder()
.addRule(
"healthy",
[](const nlohmann::json &data)
-> bool {
return data["health"]
.get<int>() >
20;
})
->addRule(
"hungry",
[](const nlohmann::json &data)
-> bool {
return data["needs_hunger"]
.get<int>() >
2000;
})
->addRule(
"thirsty",
[](const nlohmann::json &data)
-> bool {
return data["needs_thirst"]
.get<int>() >
1000;
})
->addRule(
"toilet",
[](const nlohmann::json &data)
-> bool {
return data["needs_toilet"]
.get<int>() >
1500;
})
->build();
ai.memoryUpdates =
TownAI::MemoryUpdateBuilder()
.addUpdate(
"health",
[](int index,
const std::string &name,
int value) -> int {
if (value < 50)
return value +
1;
else
return value;
})
.addUpdate("needs_hunger",
[](int index,
const std::string &name,
int value) -> int {
return value + 1;
})
.addUpdate("needs_thirst",
[](int index,
const std::string &name,
int value) -> int {
return value + 1;
})
.addUpdate("needs_toilet",
[](int index,
const std::string &name,
int value) -> int {
return value + 1;
})
.build();
}
}
}
void CharacterAIModule::updateBlackboardsBits(flecs::entity town,
ActionNodeList &alist,
const TownNPCs &npcs, TownAI &ai)
{
OgreAssert(town.is_valid(), "Bad town entity");
std::lock_guard<std::mutex> lock(*ai.mutex);
std::vector<struct Blackboard::UpdateBit> updateBits = {
{ "health", 0, 0, 100, 20, "healthy" },
{ "needs_hunger", 1, 0, 10000, 2000, "hungry" },
{ "needs_thirst", 1, 0, 10000, 1000, "thirsty" },
{ "needs_toilet", 1, 0, 10000, 1500, "toilet" }
};
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
if (ai.blackboards.find(it->first) == ai.blackboards.end())
continue;
auto &bb = ai.blackboards.at(it->first);
bb.commit();
}
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
if (ai.blackboards.find(it->first) == ai.blackboards.end())
continue;
ai.blackboards.at(it->first).index = it->first;
ai.blackboards.at(it->first).town = town;
auto &bb = ai.blackboards.at(it->first);
bb.updateBits(updateBits, it->second.props);
}
}
void CharacterAIModule::updateBlackboards(flecs::entity town,
const ActionNodeList &alist,
ActionNodeList &alist,
const TownNPCs &npcs, TownAI &ai)
{
std::lock_guard<std::mutex> lock(*ai.mutex);
OgreAssert(town.is_valid(), "Bad town entity");
alist.build();
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
if (ai.blackboards.find(it->first) == ai.blackboards.end())
continue;
ai.blackboards.at(it->first).index = it->first;
ai.blackboards.at(it->first).town = town;
for (auto e : ai.memoryUpdates) {
std::string key = e.name;
auto &memory = ai.memory.at(it->first);
if (memory.find(key) == memory.end())
memory[key] = 0;
int value = memory[key].get<int>();
int new_value = e.update(it->first, key, value);
if (value != new_value)
memory[key] = new_value;
}
auto &bb = ai.blackboards.at(it->first);
bb.updateBits(ai, ai.memory.at(it->first), it->second.props);
bb.query_ai();
bb.fixupBooleanKeys();
}
prepareActions(ai);
}
void CharacterAIModule::prepareActions(TownAI &ai)
{
// baking actions
for (auto &action : ai.actions) {
action->effects.commit();
action->prereq.commit();
}
for (auto &naction : ai.nodeActions)
for (auto &action : naction.second) {
action->effects.commit();
action->prereq.commit();
}
#if 0
for (const auto &action : ai.actions) {
std::cout << action->get_name() << std::endl;
std::cout << "effects: " << std::endl;
action->effects.dump_bits();
std::cout << "prereq: " << std::endl;
action->prereq.dump_bits();
}
for (auto &naction : ai.nodeActions)
for (const auto &action : naction.second) {
std::cout << action->get_name() << std::endl;
std::cout << action->get_name() << std::endl;
std::cout << "effects: " << std::endl;
action->effects.dump_bits();
std::cout << "prereq: " << std::endl;
action->prereq.dump_bits();
}
std::cout << "end dump" << std::endl;
std::cout << "dump" << std::endl;
for (auto &m : Blackboard::mapping)
std::cout << m.first << ": " << m.second << std::endl;
std::cout << "end dump" << std::endl;
// OgreAssert(false, "baking actions");
#endif
}
void Blackboard::_actionRefResize(int count)
@@ -628,8 +631,7 @@ void Blackboard::_actionRefResize(int count)
}
Blackboard::Blackboard()
: stats(nlohmann::json::object())
, object(-1)
: object(-1)
, index(-1)
, actionRefCount(0)
, mutex(std::make_shared<std::mutex>())
@@ -642,7 +644,6 @@ std::unordered_map<std::string, size_t> Blackboard::mapping;
Blackboard::Blackboard(const nlohmann::json &stats)
: Blackboard()
{
this->stats = stats;
populate(stats, mapping);
}
@@ -679,11 +680,6 @@ void Blackboard::populate(const nlohmann::json &stats,
bool Blackboard::operator==(const Blackboard &other) const
{
OgreAssert(mask != 0 && other.mask != 0, "blackboard not prepared");
#if 0
if (mask == 0 || other.mask == 0)
return is_satisfied_by(this->stats, other.stats);
else
#endif
return (bits & other.mask) == (other.bits & other.mask);
}
@@ -695,7 +691,7 @@ bool Blackboard::operator!=(const Blackboard &other) const
void Blackboard::apply(const Blackboard &other)
{
std::lock_guard<std::mutex> lock(*mutex);
stats.update(other.stats);
// stats.update(other.stats);
// 1. Clear bits in 'values' that are about to be overwritten (where effect mask is 1)
// 2. OR the result with the effect values (filtered by the effect mask)
bits = (bits & ~other.mask) | (other.bits & other.mask);
@@ -713,8 +709,10 @@ Ogre::String Blackboard::dumpActions()
for (count = 0; count < actionRefCount; count++) {
auto &action = actionRef[count];
ret += "name: " + action->get_name() + "\n";
ret += "\tprereq:\n" + action->prereq.stats.dump(4) + "\n";
ret += "\teffects:\n" + action->effects.stats.dump(4) + "\n";
ret += "\tprereq:\n";
action->prereq.dump_bits();
ret += "\teffects:\n";
action->effects.dump_bits();
}
return ret;
}
@@ -724,23 +722,6 @@ void Blackboard::printActions()
std::cout << dumpActions() << std::endl;
}
void Blackboard::fixupBooleanKeys()
{
std::lock_guard<std::mutex> lock(*mutex);
int count;
for (count = 0; count < actionRefCount; count++) {
auto &action = actionRef[count];
const nlohmann::json &prereq = action->prereq.stats;
const nlohmann::json &effects = action->effects.stats;
for (auto it = prereq.begin(); it != prereq.end(); it++)
if (stats.find(it.key()) == stats.end())
stats[it.key()] = 0;
for (auto it = effects.begin(); it != effects.end(); it++)
if (stats.find(it.key()) == stats.end())
stats[it.key()] = 0;
}
}
void Blackboard::actionRefResize(int count)
{
std::lock_guard<std::mutex> lock(*mutex);
@@ -793,34 +774,14 @@ void Blackboard::actionRefAddActions(goap::BaseAction<Blackboard> **actions,
}
}
void Blackboard::updateBits(const std::vector<UpdateBit> &updateBits,
void Blackboard::updateBits(const TownAI &ai, const nlohmann::json &memory,
const nlohmann::json &props)
{
for (const auto &mbits : updateBits) {
// OgreAssert(mapping.find(mbits.checkValue) != mapping.end(),
// "No key " + mbits.checkValue);
OgreAssert(mapping.find(mbits.writeValue) != mapping.end(),
"No key " + mbits.writeValue);
int value = stats[mbits.checkValue].get<int>();
int maxValue = mbits.maxValue;
int minValue = mbits.minValue;
int threshold = mbits.threshold;
if (props.find(mbits.checkValue + "_max") != props.end())
maxValue = props[mbits.checkValue + "_max"].get<int>();
if (props.find(mbits.checkValue + "_threshold") != props.end())
threshold = props[mbits.checkValue + "_threshold"]
.get<int>();
value += mbits.recover;
if (value > maxValue)
value = maxValue;
if (value >= threshold)
stats[mbits.writeValue] = 1;
else
stats[mbits.writeValue] = 0;
if (value < mbits.minValue)
value = mbits.minValue;
stats[mbits.checkValue] = value;
for (const auto &mcond : ai.conditions) {
if (mapping.find(mcond.key) == mapping.end())
populate(nlohmann::json::object({ { mcond.key, 0 } }),
mapping);
bits.set(mapping[mcond.key], mcond.condition(memory));
}
}
@@ -869,34 +830,6 @@ int Blackboard::distance_to(const Blackboard &goal) const
int distance = 0;
OgreAssert(mask != 0 && goal.mask != 0, "blackboard not prepared");
return ((bits ^ goal.bits) & goal.mask).count();
#if 0
OgreAssert(goal.stats.is_object(),
"Not an object:\n" + goal.stats.dump(4));
for (auto it = goal.stats.begin(); it != goal.stats.end(); ++it) {
const std::string &key = it.key();
const auto &goalVal = it.value();
// If current state doesn't have the key, treat it as a maximum difference
if (stats.find(key) == stats.end()) {
distance += 100; // Example: High cost for missing state
continue;
}
const auto &currentVal = stats[key];
if (goalVal.is_number() && currentVal.is_number()) {
// Add numerical difference
distance += std::abs(goalVal.get<float>() -
currentVal.get<float>());
} else {
// Check non-numeric equality
if (goalVal != currentVal) {
distance += 1; // Penalty for mismatch
}
}
}
return distance;
#endif
}
void Blackboard::setPosition(const Ogre::Vector3 &position)
@@ -922,7 +855,6 @@ void Blackboard::query_ai()
ActionNodeList &alist = ECS::get_mut<ActionNodeList>();
alist.query_ai(position, distance, points, distances);
_actionRefResize(ai.actions.size());
int actionRefIndex = ai.actions.size();
int nodeActionCount = 0;
for (size_t point : points) {
Ogre::Vector3 &p = alist.dynamicNodes[point].position;
@@ -931,10 +863,10 @@ void Blackboard::query_ai()
if (object >= 0 && (size_t)object == point &&
distance > radius * radius) {
object = -1;
stats["at_object"] = 0;
bits.set(mapping["at_object"], false);
} else if (object >= 0 && (size_t)object == point &&
distance <= radius * radius)
stats["at_object"] = 1;
bits.set(mapping["at_object"], true);
/* some nodes do not have usable actions */
if (ai.nodeActions[point].size() > 0) {
OgreAssert(ai.nodeActions[point].size() > 0,
@@ -951,7 +883,6 @@ void Blackboard::query_ai()
j["global_position_y"] = p.y;
j["global_position_z"] = p.z;
nodes.push_back(j);
stats["nodes"] = nodes;
}
}
}