#include #include "entities/camera.hpp" #include "entities/world.hpp" #include "rendering/gl_renderer.hpp" #include "systems/cpp_highlight_system.hpp" #include "text/token.hpp" #include "utils/globals.hpp" namespace Nimbin { namespace Text { static SDL_FColor aiGlintColor(Token::Ai::State s) { switch (s) { case Token::Ai::State::Attack: return {1.0f, 0.85f, 0.75f, 1.0f}; case Token::Ai::State::Flee: return {1.0f, 1.0f, 0.80f, 1.0f}; case Token::Ai::State::Stunned: return {0.85f, 0.85f, 0.95f, 1.0f}; case Token::Ai::State::Idle: return {1.0f, 1.0f, 1.0f, 1.0f}; } return {1.0f, 1.0f, 1.0f, 1.0f}; } static Vec3 v_norm(const Vec3& v) { double l = SDL_sqrt(v.x*v.x + v.y*v.y + v.z*v.z); return (l < 1e-9) ? Vec3{0,0,1} : Vec3{v.x/l, v.y/l, v.z/l}; } // Camera-facing filled disc (triangle fan) static void append_disc(DynArray& v, DynArray& idx, const Vec3& c, const Vec3& right, const Vec3& up, const Vec3& camN, double radius) { constexpr int SEG = 10; const int base = static_cast(v.size()) / 9; auto push = [&](const Vec3& p) { v.push_back((float)p.x); v.push_back((float)p.y); v.push_back((float)p.z); v.push_back((float)camN.x); v.push_back((float)camN.y); v.push_back((float)camN.z); v.push_back(1.0f); v.push_back(0.0f); v.push_back(0.0f); }; push(c); for (int i = 0; i < SEG; i++) { double ang = static_cast(i) * (2.0 * SDL_PI_D / SEG); double dx = SDL_cos(ang) * radius, dy = SDL_sin(ang) * radius; push(Vec3{ c.x + right.x*dx + up.x*dy, c.y + right.y*dx + up.y*dy, c.z + right.z*dx + up.z*dy }); } for (int i = 0; i < SEG; i++) { idx.push_back(base + 0); idx.push_back(base + 1 + i); idx.push_back(base + 1 + ((i + 1) % SEG)); } } // Fast integer hash → float in [0,1). Used for all per-sparkle randomness. static double hash01(uint32_t x) { x ^= x >> 16; x *= 0x7feb352dU; x ^= x >> 15; x *= 0x846ca68bU; x ^= x >> 16; return (x & 0x00ffffffU) / static_cast(0x01000000); } static SDL_FColor mix_col(SDL_FColor a, SDL_FColor b, float k) { return { a.r + (b.r - a.r) * k, a.g + (b.g - a.g) * k, a.b + (b.b - a.b) * k, 1.0f }; } // A whitespace token's text is a run of spaces or a tab. We keep the real text // (" " / "\t") for serialization, but build the MESH from an ASCII ghost glyph // so the token is visible as a tangible thing when it's loose in the world. // (The mesh path is single-byte, so the ghost must be ASCII — not "·"/"→".) static bool text_Is_Whitespace(const std::string& t, bool& has_tab) { if (t.empty()) return false; has_tab = false; for (char c : t) { if (c == '\t') has_tab = true; else if (c != ' ') return false; } return true; } static std::string ghost_Glyph(const std::string& t) { return (t.find('\t') != std::string::npos) ? ">" : "."; } namespace Token { Data::Data(const std::string& txt, float dpth, float scle, Vec3 position, SDL_FColor colr) : GameObject(position, ObjectType::Text, CollisionShape{}, RigidBody{PhysicsMode::Hover}, true, true, true), text(txt), color(colr), depth(dpth), scale(scle) { bool has_tab = false; if (text_Is_Whitespace(text, has_tab)) { is_tab = has_tab; is_space = !has_tab; } single_color = Text::Token::get_Color(CppHighlight::classify_Word(text)); context_color = single_color; // until a lexer overrides if (is_Whitespace()) { // Faint glyph that glows when loose in the world (bloomed ghost). single_color = SDL_FColor{ 0.45f, 0.55f, 0.70f, 1.0f }; context_color = single_color; glow_enabled = true; glow_strength = 0.6f; } // `color` (Static slot) already initialised to `colr` in the member init list. rebuild_Mesh(); body.hover_phase = Random::unit() * SDL_PI_D * 2.0; ai.aggressiveness = Random::unit(); ai.wiggle_phase = Random::unit() * SDL_PI_D * 2.0; ai.wiggle_freq = 3.0 + Random::unit() * 4.0; ai.speed = 2.5 + Random::unit() * 2.5; } Data::~Data() { if (gpu_vbo || gpu_ebo) GlRenderer::free_MeshBuffers(gpu_vbo, gpu_ebo); if (gpu_vbo_sparkle || gpu_ebo_sparkle) GlRenderer::free_MeshBuffers(gpu_vbo_sparkle, gpu_ebo_sparkle); } void Data::set_Color(SDL_FColor c) // the Static fixed colour { color = c; } void Data::set_Context(SDL_FColor c) // lexer result inside a document { context_color = c; has_context = true; } void Data::clear_Context() // leaving a document → fall back to single { has_context = false; } void Data::set_Look(SDL_FColor c, float new_depth) { if (c.r == -1.0f) single_color = Text::Token::get_Color(CppHighlight::classify_Word(text)); else if (c.g == -1.0f) { /* keep current colours */ } else color = c; // explicit colour → Static slot set_Depth(new_depth); } void Data::recolor() // recompute the tokenized (single) colour { single_color = Text::Token::get_Color(CppHighlight::classify_Word(text)); if (!has_context) context_color = single_color; } void Data::retext(const std::string& s) // text changed → new single colour + new mesh { if (text == s) return; text = s; bool has_tab = false; const bool ws = text_Is_Whitespace(text, has_tab); is_tab = ws && has_tab; is_space = ws && !has_tab; single_color = Text::Token::get_Color(CppHighlight::classify_Word(text)); if (!has_context) context_color = single_color; rebuild_Mesh(); } void Data::set_Depth(float new_depth) { depth = new_depth; shape.half_extents.z = (double)depth * 0.5; } Vec3 Data::words_centroid(const DynArray& words) { Vec3 c{}; double n = 0.0; for (const TokenPtr& w : words) { if (!w) continue; c += w->pos; n += 1.0; } if (n > 0.0) c = c / n; return c; } Vec3 Data::get_Distance(Vec3 position) const { return pos - position; } void Data::rebuild_Mesh() { // SPIKE("rebuild_mesh"); // Whitespace keeps its real text (" "/"\t") for serialization, but renders // an ASCII ghost glyph so it's a visible thing when loose in the world. const std::string build_str = is_Whitespace() ? ghost_Glyph(text) : text; loaded = mesh.build(font_path, build_str, 64, depth, scale); if (!loaded) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Text::Token::Data: mesh build failed for \"%s\"", text.c_str()); return; } float minX = 1e9f, maxX = -1e9f; float minY = 1e9f, maxY = -1e9f; for (const Vec3& v : mesh.verts) { float vx = static_cast(v.x); float vy = static_cast(v.y); if (vx < minX) minX = vx; if (vx > maxX) maxX = vx; if (vy < minY) minY = vy; if (vy > maxY) maxY = vy; } half_extent = (maxX - minX) * 0.5f; half_extent_y = (maxY - minY) * 0.5f; shape.half_extents = { static_cast(half_extent), static_cast(half_extent_y), static_cast(depth) * 0.5 }; packed_verts. clear(); packed_indices.clear(); const size_t nv = mesh.verts.size(); packed_verts.reserve(nv * 9); for (size_t i = 0; i < nv; i++) { const Vec3& v = mesh.verts[i]; const Vec3& n = mesh.vertex_normals[i]; const float f = (i < mesh.is_front.size()) ? mesh.is_front[i] : 0.0f; packed_verts.push_back((float)v.x); packed_verts.push_back((float)v.y); packed_verts.push_back((float)v.z); packed_verts.push_back((float)n.x); packed_verts.push_back((float)n.y); packed_verts.push_back((float)n.z); packed_verts.push_back(f); packed_verts.push_back(0.0f); packed_verts.push_back(0.0f); } packed_indices.reserve(mesh.tris.size() * 3); for (const MeshTri& t : mesh.tris) { packed_indices.push_back(t.a); packed_indices.push_back(t.b); packed_indices.push_back(t.c); } gpu_dirty = true; } void Data::update(const Vec3& camera_pos, const double view_mat[9], float carry_dist) { float fx = -static_cast(view_mat[6]); float fy = -static_cast(view_mat[7]); float fz = -static_cast(view_mat[8]); pos.x = camera_pos.x + static_cast(fx * carry_dist); pos.y = camera_pos.y + static_cast(fy * carry_dist); pos.z = camera_pos.z + static_cast(fz * carry_dist); yaw = SDL_atan2f(-fx, -fz); } void Data::draw(GlRenderer& gl, const Camera& camera, const Light& light) const { if (!loaded || packed_indices.empty()) return; Mat4 proj = gl.get_Projection(); Mat4 view = buildViewMat4(camera); Mat4 model_rot = buildModelMat4(pos, yaw, pitch); Mat4 model = model_rot; model.m[8] *= depth; model.m[9] *= depth; model.m[10] *= depth; Mat4 mvp = Mat4::mul(proj, Mat4::mul(view, model)); const SDL_FColor ac = get_ActiveColor(); gl.set_Wireframe(false); gl.set_Light (light); gl.set_MVP (model, mvp); gl.set_NormalMat(model_rot); gl.set_FaceShade(ac.r, ac.g, ac.b, back_dim, front_tint); if (glow_enabled && glow_strength > 0.0f) gl.set_Emissive(ac.r, ac.g, ac.b, glow_strength); else gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f); gl.draw_MeshCached(gpu_vbo, gpu_ebo, gpu_index_count, gpu_dirty, packed_verts.data(), (int)packed_verts.size() / 9, packed_indices.data(), (int)packed_indices.size()); gpu_dirty = false; gl.set_Emissive (0.0f, 0.0f, 0.0f, 0.0f); gl.set_FaceShade(0.0f, 0.0f, 0.0f, 1.0f, false); // ── AI sparkle tail ─────────────────────────────────────────────────── if (ai.state == Ai::State::Idle) return; const Uint64 now = SDL_GetTicks(); // Sample the word's path into a history ring → the trailing tail. if (sparkle_trail_count == 0) { for (int i = 0; i < SPARKLE_TRAIL; i++) sparkle_trail[i] = pos; sparkle_trail_count = SPARKLE_TRAIL; sparkle_last_ms = now; } else if (now - sparkle_last_ms >= 33) { // ~30 Hz for (int i = SPARKLE_TRAIL - 1; i > 0; --i) sparkle_trail[i] = sparkle_trail[i - 1]; sparkle_trail[0] = pos; sparkle_last_ms = now; } const double t = static_cast(now) * 0.001; const Vec3 fwd = v_norm(camera.fwd); const Vec3 right = v_norm(camera.right); const Vec3 up = v_norm(Vec3{ right.y*fwd.z - right.z*fwd.y, right.z*fwd.x - right.x*fwd.z, right.x*fwd.y - right.y*fwd.x }); const Vec3 camN = { -fwd.x, -fwd.y, -fwd.z }; const double sz = SDL_max(half_extent, 0.25); const uint32_t wseed = static_cast(body.hover_phase * 10037.0) | 1u; static const SDL_FColor PAL[] = { {1.00f, 0.72f, 0.10f, 1.0f}, // gold {1.00f, 0.25f, 0.65f, 1.0f}, // magenta-pink {0.20f, 0.80f, 1.00f, 1.0f}, // cyan {0.45f, 1.00f, 0.30f, 1.0f}, // lime {1.00f, 0.45f, 0.10f, 1.0f}, // orange {0.65f, 0.40f, 1.00f, 1.0f}, // violet }; constexpr int PALN = sizeof(PAL) / sizeof(PAL[0]); DynArray sv[PALN]; DynArray si[PALN]; constexpr int PER_NODE = 2; for (int j = 0; j < sparkle_trail_count; ++j) { const double age = (sparkle_trail_count > 1) ? double(j) / double(sparkle_trail_count - 1) : 0.0; const Vec3& node = sparkle_trail[j]; for (int s = 0; s < PER_NODE; ++s) { uint32_t h = wseed ^ (uint32_t(j) * 2654435761u) ^ (uint32_t(s) * 40503u); if (hash01(h) < 0.6) continue; // sparser → calmer double phase = hash01(h ^ 0xA5A5u) * 6.28318; double tw = SDL_pow(SDL_max(0.0, SDL_sin(t * 1.6 + phase)), 3.0); // slower, softer if (tw < 0.04) continue; double spread = sz * (0.6 + age * 1.4); // older = wider scatter double jx = (hash01(h ^ 0x1111u) - 0.5) * spread; double jy = (hash01(h ^ 0x2222u) - 0.5) * spread; // gentle independent wander so each sparkle drifts in place double wamp = sz * 0.22; // how far it roams double fx = 0.6 + hash01(h ^ 0x7777u) * 0.8; // per-sparkle drift rates double fy = 0.6 + hash01(h ^ 0x8888u) * 0.8; jx += SDL_sin(t * fx + hash01(h ^ 0x5555u) * 6.28318) * wamp; jy += SDL_cos(t * fy + hash01(h ^ 0x6666u) * 6.28318) * wamp; Vec3 ctr = { node.x + right.x*jx + up.x*jy + camN.x*0.03, node.y + right.y*jx + up.y*jy + camN.y*0.03, node.z + right.z*jx + up.z*jy + camN.z*0.03 }; double size_rng = 0.06 + 0.05 * hash01(h ^ 0x3333u); // smaller + varied double outer = size_rng * sz * tw * (1.0 - age * 0.6); if (outer < 1e-4) continue; int c = int(hash01(h ^ 0x4444u) * PALN) % PALN; append_disc(sv[c], si[c], ctr, right, up, camN, static_cast(outer)); } } Mat4 ident = buildModelMat4(Vec3{0,0,0}, 0.0f, 0.0f); gl.set_MVP (ident, Mat4::mul(proj, view)); gl.set_NormalMat(ident); const SDL_FColor glint = aiGlintColor(ai.state); for (int c = 0; c < PALN; ++c) { if (si[c].empty()) continue; SDL_FColor g = mix_col(PAL[c], glint, 0.15f); // keep only a hint of state gl.set_FaceShade (0.0f, 0.0f, 0.0f, 1.0f, false); // no lit term → no white-out gl.set_Emissive (g.r, g.g, g.b, 0.85f); // pure colour, below clip gl.draw_MeshCached(gpu_vbo_sparkle, gpu_ebo_sparkle, gpu_sparkle_index_count, /*dirty=*/true, sv[c].data(), (int)sv[c].size() / 9, si[c].data(), (int)si[c].size()); } gl.set_Emissive (0.0f, 0.0f, 0.0f, 0.0f); gl.set_FaceShade(0.0f, 0.0f, 0.0f, 1.0f, false); } } // namespace Token } // namespace Text } // namespace Nimbin