Brandling Safari: LAN safari-zone creature catching on the HD-2D engine
- Authoritative 30 Hz server (ENet, SQLite): accounts, characters, wild AI, tall grass / water / wandering spawns, fishing, captures, follower chains, clock + weather, LAN beacon - Client: title / join / character creator / play screens, prediction + reconciliation, interpolated players and parties, HD-2D UI (dialogue with Professor Henry, HUD, collection) - Shared deterministic zone generator, movement, protocol, roster, appearance - Paper-doll character art pipeline (PixelLab base walk + per-item animation edits) - Brandling roster from the company list (logo colors, jev types), Blender safari props - Headless test bot
This commit is contained in:
559
server/Creatures.cpp
Normal file
559
server/Creatures.cpp
Normal file
@@ -0,0 +1,559 @@
|
||||
// Wild creature population and behaviour, tall-grass encounters, captures and
|
||||
// party followers.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include "Log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
|
||||
namespace bs {
|
||||
|
||||
namespace {
|
||||
constexpr float kPi = 3.14159265f;
|
||||
constexpr int kWanderers = 16;
|
||||
constexpr int kSwimmers = 5;
|
||||
constexpr int kMaxEmergedPerPlayer = 4;
|
||||
constexpr float kWanderSpeed = 1.4f;
|
||||
constexpr float kSwimSpeed = 0.35f;
|
||||
constexpr float kFleeSpeed = 4.5f;
|
||||
} // namespace
|
||||
|
||||
Creature& Game::addCreature(uint16_t species, glm::vec2 pos, COrigin origin) {
|
||||
uint32_t id = nextEntity_++;
|
||||
Creature& c = creatures_[id];
|
||||
c.id = id;
|
||||
c.species = species;
|
||||
c.origin = origin;
|
||||
c.pos = pos;
|
||||
c.y = zone_.groundHeight(pos);
|
||||
c.yaw = rng_.range(-kPi, kPi);
|
||||
return c;
|
||||
}
|
||||
|
||||
void Game::removeCreature(uint32_t id) {
|
||||
auto it = creatures_.find(id);
|
||||
if (it != creatures_.end()) it->second.dead = true;
|
||||
}
|
||||
|
||||
float Game::nearestPlayerDist(glm::vec2 p, Player** who) {
|
||||
float best = 1e9f;
|
||||
if (who) *who = nullptr;
|
||||
for (auto& [peer, pl] : players_) {
|
||||
if (!pl->spawned) continue;
|
||||
float d = glm::length(pl->move.pos - p);
|
||||
if (d < best) {
|
||||
best = d;
|
||||
if (who) *who = pl.get();
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
void Game::announceAppeared(const Creature& c) {
|
||||
for (auto& [peer, p] : players_) {
|
||||
if (!p->spawned || glm::length(p->move.pos - c.pos) > 10.0f) continue;
|
||||
auto it = creatures_.find(c.id);
|
||||
if (it != creatures_.end()) it->second.seenBy.insert(p->entity);
|
||||
noteSeen(*p, c.species);
|
||||
}
|
||||
}
|
||||
|
||||
void Game::faceNearestPlayer(Creature& c) {
|
||||
Player* who = nullptr;
|
||||
float d = nearestPlayerDist(c.pos, &who);
|
||||
if (who && d < 12.0f && d > 0.05f) c.yaw = yawFromDir(who->move.pos - c.pos);
|
||||
}
|
||||
|
||||
// Grass creatures that popped up for this player and are still around it (stale ones left
|
||||
// behind in another patch do not block new encounters).
|
||||
int Game::emergedCount(const Player& p) const {
|
||||
int n = 0;
|
||||
for (auto& [id, c] : creatures_)
|
||||
n += !c.dead && c.kind == EntityKind::Wild && c.origin == COrigin::Grass && c.emergedFor == p.entity &&
|
||||
glm::length(c.pos - p.move.pos) < 15.0f;
|
||||
return n;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ population
|
||||
|
||||
bool Game::spawnWanderer() {
|
||||
uint16_t species;
|
||||
if (!tables_.pick(rng_, SpawnHabitat::Land, species)) return false;
|
||||
glm::vec2 spot, fallback;
|
||||
bool have = false, haveFallback = false;
|
||||
for (int i = 0; i < 8 && !have; ++i) {
|
||||
glm::vec2 p;
|
||||
if (!zone_.randomWanderSpot(rng_, p)) continue;
|
||||
if (nearestPlayerDist(p) > 14.0f) spot = p, have = true;
|
||||
else if (!haveFallback) fallback = p, haveFallback = true;
|
||||
}
|
||||
if (!have && !haveFallback) return false;
|
||||
Creature& c = addCreature(species, have ? spot : fallback, COrigin::Wanderer);
|
||||
c.mode = CMode::Wander;
|
||||
c.anim = Anim::Idle;
|
||||
c.timer = rng_.range(0.5f, 4.0f);
|
||||
c.lifetime = rng_.range(120.0f, 300.0f);
|
||||
announceAppeared(c);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Game::spawnSwimmer(bool announce) {
|
||||
uint16_t species;
|
||||
glm::vec2 p;
|
||||
if (!tables_.pick(rng_, SpawnHabitat::Water, species) || !zone_.randomWater(rng_, p, true)) return false;
|
||||
Creature& c = addCreature(species, p, COrigin::Swimmer);
|
||||
c.y = Zone::kWaterLevel;
|
||||
c.lifetime = 1e9f; // swimmers rotate by diving
|
||||
if (announce) {
|
||||
c.mode = CMode::Emerge;
|
||||
c.anim = Anim::Emerge;
|
||||
c.timer = 0.9f;
|
||||
GameEvent s;
|
||||
s.kind = EventKind::Splash;
|
||||
s.p = glm::vec3(p.x, Zone::kWaterLevel, p.y);
|
||||
sendEvent(s);
|
||||
GameEvent e;
|
||||
e.kind = EventKind::Emerge;
|
||||
e.a = c.id;
|
||||
e.i = 1;
|
||||
e.p = s.p;
|
||||
sendEvent(e);
|
||||
} else {
|
||||
c.mode = CMode::Swim;
|
||||
c.anim = Anim::Swim;
|
||||
}
|
||||
announceAppeared(c);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Game::maintainPopulation(float dt) {
|
||||
int wanderers = 0, swimmers = 0;
|
||||
for (auto& [id, c] : creatures_) {
|
||||
if (c.dead || c.kind != EntityKind::Wild) continue;
|
||||
wanderers += c.origin == COrigin::Wanderer;
|
||||
swimmers += c.origin == COrigin::Swimmer;
|
||||
}
|
||||
wanderSpawnTimer_ -= dt;
|
||||
if (wanderers < kWanderers && wanderSpawnTimer_ <= 0.0f) {
|
||||
spawnWanderer();
|
||||
wanderSpawnTimer_ = 1.5f;
|
||||
}
|
||||
|
||||
// One swimmer dives (Splash, despawn) every 10-25 s and resurfaces elsewhere a few seconds later.
|
||||
swimmerTimer_ -= dt;
|
||||
if (swimmerTimer_ <= 0.0f) {
|
||||
swimmerTimer_ = rng_.range(10.0f, 25.0f);
|
||||
std::vector<uint32_t> candidates;
|
||||
for (auto& [id, c] : creatures_)
|
||||
if (!c.dead && c.kind == EntityKind::Wild && c.origin == COrigin::Swimmer && c.mode == CMode::Swim)
|
||||
candidates.push_back(id);
|
||||
if (!candidates.empty()) {
|
||||
Creature& c = creatures_[candidates[size_t(rng_.range(0, int(candidates.size()) - 1))]];
|
||||
GameEvent s;
|
||||
s.kind = EventKind::Splash;
|
||||
s.p = glm::vec3(c.pos.x, Zone::kWaterLevel, c.pos.y);
|
||||
sendEvent(s);
|
||||
removeCreature(c.id);
|
||||
--swimmers;
|
||||
swimmerRespawn_ = rng_.range(2.0f, 5.0f);
|
||||
}
|
||||
}
|
||||
if (swimmerRespawn_ > 0.0f) {
|
||||
swimmerRespawn_ -= dt;
|
||||
if (swimmerRespawn_ <= 0.0f && swimmers < kSwimmers) spawnSwimmer(true), ++swimmers;
|
||||
} else if (swimmers < kSwimmers) {
|
||||
swimmerRespawn_ = rng_.range(2.0f, 5.0f); // replace caught swimmers
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ tall grass
|
||||
|
||||
void Game::grassEncounters(Player& p) {
|
||||
while (p.grassDist >= 0.5f) {
|
||||
p.grassDist -= 0.5f;
|
||||
float chance = 0.07f * (p.move.running ? 2.2f : 1.0f);
|
||||
uint16_t species;
|
||||
glm::vec2 spot;
|
||||
if (rng_.chance(chance) && emergedCount(p) < kMaxEmergedPerPlayer &&
|
||||
tables_.pick(rng_, SpawnHabitat::Land, species) &&
|
||||
zone_.randomTallGrassNear(rng_, p.move.pos, 2.0f, 6.0f, spot)) {
|
||||
Creature& c = addCreature(species, spot, COrigin::Grass);
|
||||
c.mode = CMode::Emerge;
|
||||
c.anim = Anim::Emerge;
|
||||
c.timer = 0.9f;
|
||||
c.emergedFor = p.entity;
|
||||
c.yaw = yawFromDir(p.move.pos - spot);
|
||||
c.lifetime = rng_.range(60.0f, 120.0f);
|
||||
GameEvent e;
|
||||
e.kind = EventKind::Emerge;
|
||||
e.a = c.id;
|
||||
e.i = 0;
|
||||
e.p = glm::vec3(spot.x, c.y, spot.y);
|
||||
sendEvent(e);
|
||||
announceAppeared(c);
|
||||
const Species* sp = roster_.get(species);
|
||||
logf("a wild %s (entity %u) emerged near %s", sp ? sp->name.c_str() : "?", c.id, p.row.name.c_str());
|
||||
continue;
|
||||
}
|
||||
if (p.rustleCooldown <= 0.0f && rng_.chance(0.25f) &&
|
||||
zone_.randomTallGrassNear(rng_, p.move.pos, 1.5f, 6.0f, spot)) {
|
||||
p.rustleCooldown = rng_.range(0.8f, 1.6f);
|
||||
GameEvent e;
|
||||
e.kind = EventKind::Rustle;
|
||||
e.p = glm::vec3(spot.x, zone_.groundHeight(spot), spot.y);
|
||||
sendEvent(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ behaviour
|
||||
|
||||
bool Game::pickWanderTarget(Creature& c) {
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
glm::vec2 t;
|
||||
if (c.origin == COrigin::Grass && zone_.inTallGrass(c.pos)) {
|
||||
if (!zone_.randomTallGrassNear(rng_, c.pos, 2.0f, 6.0f, t)) continue;
|
||||
} else {
|
||||
float a = rng_.range(-kPi, kPi), r = rng_.range(2.0f, 6.0f);
|
||||
t = c.pos + glm::vec2(std::cos(a), std::sin(a)) * r;
|
||||
}
|
||||
bool ok = zone_.creatureWalkable(t, false);
|
||||
for (int k = 1; k < 5 && ok; ++k) ok = zone_.creatureWalkable(glm::mix(c.pos, t, float(k) / 5.0f), false);
|
||||
if (!ok) continue;
|
||||
c.target = t;
|
||||
c.walking = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Game::wander(Creature& c, float dt, float speed) {
|
||||
if (c.walking) {
|
||||
glm::vec2 d = c.target - c.pos;
|
||||
float len = glm::length(d);
|
||||
glm::vec2 next = c.pos + d / std::max(len, 1e-4f) * std::min(len, speed * dt);
|
||||
if (len < 0.05f || !zone_.creatureWalkable(next, false)) {
|
||||
c.walking = false;
|
||||
c.timer = rng_.range(1.0f, 4.0f);
|
||||
c.anim = Anim::Idle;
|
||||
} else {
|
||||
c.pos = next;
|
||||
c.yaw = yawFromDir(d);
|
||||
c.anim = Anim::Walk;
|
||||
}
|
||||
} else {
|
||||
c.anim = Anim::Idle;
|
||||
c.timer -= dt;
|
||||
if (c.timer <= 0.0f && !pickWanderTarget(c)) c.timer = 1.0f;
|
||||
}
|
||||
c.y = zone_.groundHeight(c.pos);
|
||||
}
|
||||
|
||||
void Game::updateWild(Creature& c, float dt) {
|
||||
// dex: wanderers (and anything else) register as seen once a player comes within 6 units
|
||||
for (auto& [peer, p] : players_) {
|
||||
if (!p->spawned || c.seenBy.count(p->entity) || glm::length(p->move.pos - c.pos) > 6.0f) continue;
|
||||
c.seenBy.insert(p->entity);
|
||||
noteSeen(*p, c.species);
|
||||
}
|
||||
|
||||
switch (c.mode) {
|
||||
case CMode::Emerge:
|
||||
c.timer -= dt;
|
||||
if (c.timer <= 0.0f) {
|
||||
if (c.origin == COrigin::Swimmer) {
|
||||
c.mode = CMode::Swim;
|
||||
c.anim = Anim::Swim;
|
||||
} else {
|
||||
c.mode = CMode::Alert;
|
||||
c.anim = Anim::Alert;
|
||||
c.timer = 2.5f;
|
||||
Player* who = byEntity(c.emergedFor);
|
||||
if (who) c.yaw = yawFromDir(who->move.pos - c.pos);
|
||||
else faceNearestPlayer(c);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CMode::Alert:
|
||||
c.anim = Anim::Alert;
|
||||
faceNearestPlayer(c);
|
||||
c.timer -= dt;
|
||||
if (c.timer <= 0.0f) {
|
||||
c.hookBonus = false;
|
||||
c.walking = false;
|
||||
c.timer = rng_.range(0.5f, 2.0f);
|
||||
bool water = zone_.isWater(c.pos);
|
||||
c.mode = water ? CMode::Swim : CMode::Wander;
|
||||
c.anim = water ? Anim::Swim : Anim::Idle;
|
||||
}
|
||||
break;
|
||||
case CMode::Wander:
|
||||
wander(c, dt, kWanderSpeed);
|
||||
break;
|
||||
case CMode::Swim: {
|
||||
c.anim = Anim::Swim;
|
||||
c.y = Zone::kWaterLevel;
|
||||
if (!c.walking) {
|
||||
glm::vec2 t;
|
||||
if (zone_.randomWaterNear(rng_, c.pos, 3.0f, t) && zone_.isWater(glm::mix(c.pos, t, 0.5f))) {
|
||||
c.target = t;
|
||||
c.walking = true;
|
||||
}
|
||||
} else {
|
||||
glm::vec2 d = c.target - c.pos;
|
||||
float len = glm::length(d);
|
||||
glm::vec2 next = c.pos + d / std::max(len, 1e-4f) * std::min(len, kSwimSpeed * dt);
|
||||
if (len < 0.05f || !zone_.isWater(next)) c.walking = false;
|
||||
else c.pos = next, c.yaw = yawFromDir(d);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CMode::Capturing:
|
||||
c.captureTimer -= dt;
|
||||
if (c.captureTimer <= 0.0f) resolveCapture(c);
|
||||
break;
|
||||
case CMode::Flee: {
|
||||
c.timer -= dt;
|
||||
glm::vec2 d = c.target - c.pos;
|
||||
float len = glm::length(d);
|
||||
if (len > 0.05f) {
|
||||
glm::vec2 next = c.pos + d / len * std::min(len, kFleeSpeed * dt);
|
||||
bool water = zone_.isWater(c.pos);
|
||||
if (zone_.creatureWalkable(next, water)) c.pos = next;
|
||||
c.yaw = yawFromDir(d);
|
||||
}
|
||||
if (!zone_.isWater(c.pos)) c.y = zone_.groundHeight(c.pos);
|
||||
if (c.timer <= 0.0f) removeCreature(c.id);
|
||||
break;
|
||||
}
|
||||
case CMode::Hooked: {
|
||||
c.timer -= dt;
|
||||
float t = std::clamp(1.0f - c.timer / 1.0f, 0.0f, 1.0f);
|
||||
c.pos = glm::mix(c.arcFrom, c.arcTo, t);
|
||||
float groundY = zone_.isWater(c.arcTo) ? Zone::kWaterLevel : zone_.groundHeight(c.arcTo);
|
||||
c.y = glm::mix(c.arcFromY, groundY, t) + 1.6f * std::sin(kPi * t);
|
||||
if (glm::length(c.arcTo - c.arcFrom) > 0.05f) c.yaw = yawFromDir(c.arcTo - c.arcFrom);
|
||||
if (c.timer <= 0.0f) {
|
||||
c.pos = c.arcTo;
|
||||
c.y = groundY;
|
||||
c.mode = CMode::Alert;
|
||||
c.anim = Anim::Alert;
|
||||
c.timer = 6.0f;
|
||||
c.hookBonus = true;
|
||||
c.owner = 0;
|
||||
faceNearestPlayer(c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CMode::Follow: break;
|
||||
}
|
||||
|
||||
// Idle creatures nobody is near eventually leave (captures/hooks/flee never time out here).
|
||||
if (c.mode == CMode::Wander || c.mode == CMode::Swim) {
|
||||
c.lifetime -= dt;
|
||||
if (c.lifetime <= 0.0f && nearestPlayerDist(c.pos) > 12.0f) removeCreature(c.id);
|
||||
}
|
||||
}
|
||||
|
||||
void Game::updateCreatures(float dt) {
|
||||
for (auto& [id, c] : creatures_)
|
||||
if (!c.dead && c.kind == EntityKind::Wild) updateWild(c, dt);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ capture
|
||||
|
||||
void Game::resolveCapture(Creature& c) {
|
||||
Player* p = byEntity(c.owner);
|
||||
const Species* sp = roster_.get(c.species);
|
||||
if (!p || !sp) { // thrower left: the capsule pops open
|
||||
c.mode = c.resumeMode;
|
||||
c.anim = c.mode == CMode::Swim ? Anim::Swim : Anim::Alert;
|
||||
c.timer = 2.5f;
|
||||
c.owner = 0;
|
||||
return;
|
||||
}
|
||||
GameEvent r;
|
||||
r.kind = EventKind::CaptureResult;
|
||||
r.a = p->entity;
|
||||
r.b = c.id;
|
||||
r.i = c.captureSuccess ? 1 : 0;
|
||||
r.j = c.wobbles;
|
||||
r.p = glm::vec3(c.pos.x, c.y, c.pos.y);
|
||||
sendEvent(r);
|
||||
|
||||
if (c.captureSuccess) {
|
||||
int party = int(std::count_if(p->creatures.begin(), p->creatures.end(), [](auto& o) { return o.inParty; }));
|
||||
bool inParty = party < kMaxParty;
|
||||
bool first = p->caught.count(c.species) == 0;
|
||||
OwnedCreature oc;
|
||||
oc.species = c.species;
|
||||
oc.inParty = inParty;
|
||||
oc.caughtAt = int64_t(std::time(nullptr));
|
||||
oc.uid = db_.insertCreature(p->row.id, oc.species, oc.caughtAt, inParty);
|
||||
if (!oc.uid) {
|
||||
removeCreature(c.id);
|
||||
sendToast(*p, "The capsule malfunctioned and the creature escaped");
|
||||
return;
|
||||
}
|
||||
p->creatures.push_back(oc);
|
||||
p->caught.insert(c.species);
|
||||
p->seen.insert(c.species);
|
||||
db_.markCaught(p->row.id, c.species);
|
||||
sendProfile(*p);
|
||||
GameEvent e;
|
||||
e.kind = EventKind::Caught;
|
||||
e.a = p->entity;
|
||||
e.b = c.species;
|
||||
e.i = first ? 1 : 0;
|
||||
e.p = r.p;
|
||||
sendEvent(e);
|
||||
logf("%s caught %s (uid %u, %s)%s", p->row.name.c_str(), sp->name.c_str(), oc.uid,
|
||||
inParty ? "party" : "box", first ? " - new species!" : "");
|
||||
if (inParty) {
|
||||
// the wild entity turns into the follower (same entity id)
|
||||
c.kind = EntityKind::Follower;
|
||||
c.origin = COrigin::Caught;
|
||||
c.mode = CMode::Follow;
|
||||
c.anim = Anim::Idle;
|
||||
c.owner = p->entity;
|
||||
c.uid = oc.uid;
|
||||
c.y = zone_.groundHeight(c.pos);
|
||||
c.hookBonus = false;
|
||||
syncFollowers(*p);
|
||||
} else {
|
||||
removeCreature(c.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
++c.failedThrows;
|
||||
float flee = sp->fleeRate * (c.failedThrows >= 2 ? 1.5f : 1.0f);
|
||||
if (rng_.chance(flee)) {
|
||||
GameEvent e;
|
||||
e.kind = EventKind::Fled;
|
||||
e.a = c.id;
|
||||
e.p = r.p;
|
||||
sendEvent(e);
|
||||
c.mode = CMode::Flee;
|
||||
c.anim = Anim::Flee;
|
||||
c.timer = 1.5f;
|
||||
glm::vec2 away = c.pos - p->move.pos;
|
||||
if (glm::length(away) < 0.05f) away = dirFromYaw(c.yaw);
|
||||
c.target = c.pos + glm::normalize(away) * 8.0f;
|
||||
c.yaw = yawFromDir(away);
|
||||
c.owner = p->entity;
|
||||
logf("%s fled from %s", sp->name.c_str(), p->row.name.c_str());
|
||||
} else {
|
||||
c.mode = c.resumeMode;
|
||||
c.anim = c.mode == CMode::Swim ? Anim::Swim : Anim::Alert;
|
||||
c.timer = 2.5f;
|
||||
c.owner = 0;
|
||||
c.walking = false;
|
||||
if (c.mode == CMode::Alert) c.yaw = yawFromDir(p->move.pos - c.pos);
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ followers
|
||||
|
||||
void Game::syncFollowers(Player& p) {
|
||||
std::vector<const OwnedCreature*> party;
|
||||
for (const OwnedCreature& o : p.creatures)
|
||||
if (o.inParty && party.size() < size_t(kMaxParty)) party.push_back(&o);
|
||||
for (auto& [id, c] : creatures_) {
|
||||
if (c.dead || c.kind != EntityKind::Follower || c.owner != p.entity) continue;
|
||||
auto it = std::find_if(party.begin(), party.end(), [&](const OwnedCreature* o) { return o->uid == c.uid; });
|
||||
if (it == party.end()) removeCreature(id);
|
||||
else c.order = uint8_t(it - party.begin());
|
||||
}
|
||||
for (size_t k = 0; k < party.size(); ++k) {
|
||||
bool exists = false;
|
||||
for (auto& [id, c] : creatures_)
|
||||
if (!c.dead && c.kind == EntityKind::Follower && c.owner == p.entity && c.uid == party[k]->uid) exists = true;
|
||||
if (exists) continue;
|
||||
// trail points are >= 0.15 apart (seeded 0.3 apart): ~3 points back per chain slot
|
||||
size_t back = p.trail.empty() ? 0 : std::min(p.trail.size() - 1, (k + 1) * 3);
|
||||
Creature& c = addCreature(party[k]->species, p.trail.empty() ? p.move.pos : p.trail[back], COrigin::Caught);
|
||||
c.kind = EntityKind::Follower;
|
||||
c.mode = CMode::Follow;
|
||||
c.anim = Anim::Idle;
|
||||
c.owner = p.entity;
|
||||
c.uid = party[k]->uid;
|
||||
c.order = uint8_t(k);
|
||||
c.yaw = p.move.yaw;
|
||||
c.lifetime = 1e9f;
|
||||
}
|
||||
}
|
||||
|
||||
void Game::updateFollowers(Player& p, float dt) {
|
||||
for (auto& [id, c] : creatures_) {
|
||||
if (c.dead || c.kind != EntityKind::Follower || c.owner != p.entity) continue;
|
||||
// target: ~0.95 * (order + 1) units back along the breadcrumb trail
|
||||
float want = 0.95f * float(c.order + 1);
|
||||
glm::vec2 prev = p.move.pos, target = prev;
|
||||
float acc = 0.0f;
|
||||
bool found = false;
|
||||
for (const glm::vec2& t : p.trail) {
|
||||
float seg = glm::length(t - prev);
|
||||
if (seg > 1e-4f && acc + seg >= want) {
|
||||
target = prev + (t - prev) * ((want - acc) / seg);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
acc += seg;
|
||||
prev = t;
|
||||
}
|
||||
if (!found) target = prev;
|
||||
|
||||
glm::vec2 d = target - c.pos;
|
||||
float len = glm::length(d);
|
||||
float moved = 0.0f;
|
||||
if (len > 8.0f) {
|
||||
c.pos = target;
|
||||
} else if (len > 0.08f) {
|
||||
float speed = (p.move.running ? kRunSpeed : kWalkSpeed) * 1.15f;
|
||||
moved = std::min(len, speed * dt);
|
||||
c.pos += d / len * moved;
|
||||
c.yaw = yawFromDir(d);
|
||||
}
|
||||
float v = moved / dt;
|
||||
c.anim = v < 0.3f ? Anim::Idle : v > kWalkSpeed * 1.1f ? Anim::Run : Anim::Walk;
|
||||
c.y = zone_.groundHeight(c.pos);
|
||||
}
|
||||
}
|
||||
|
||||
Creature* Game::spawnForConsole(uint16_t species) {
|
||||
Player* first = nullptr;
|
||||
for (auto& [peer, p] : players_)
|
||||
if (p->spawned) {
|
||||
first = p.get();
|
||||
break;
|
||||
}
|
||||
if (!first) return nullptr;
|
||||
glm::vec2 spot;
|
||||
bool ok = zone_.randomTallGrassNear(rng_, first->move.pos, 2.0f, 5.0f, spot);
|
||||
for (int i = 0; i < 40 && !ok; ++i) {
|
||||
float a = rng_.range(-kPi, kPi), r = rng_.range(2.0f, 5.0f);
|
||||
spot = first->move.pos + glm::vec2(std::cos(a), std::sin(a)) * r;
|
||||
ok = zone_.creatureWalkable(spot, false);
|
||||
}
|
||||
if (!ok) spot = first->move.pos + dirFromYaw(first->move.yaw) * 2.0f;
|
||||
Creature& c = addCreature(species, spot, COrigin::Console);
|
||||
c.mode = CMode::Emerge;
|
||||
c.anim = Anim::Emerge;
|
||||
c.timer = 0.9f;
|
||||
c.emergedFor = first->entity;
|
||||
c.yaw = yawFromDir(first->move.pos - spot);
|
||||
c.lifetime = rng_.range(120.0f, 300.0f);
|
||||
GameEvent e;
|
||||
e.kind = EventKind::Emerge;
|
||||
e.a = c.id;
|
||||
e.p = glm::vec3(spot.x, c.y, spot.y);
|
||||
sendEvent(e);
|
||||
announceAppeared(c);
|
||||
return &c;
|
||||
}
|
||||
|
||||
} // namespace bs
|
||||
Reference in New Issue
Block a user