sdl_runtime_compiler git · main
SDL3 game for running and compiling code at runtime
C++ 72.3% C 26.2%git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gzsrc/states/game_state.cpp raw
#include <algorithm>
#include <print>
#include <unordered_set>
#include "core/game.hpp"
#include "entities/solid_box.hpp"
#include "entities/world.hpp"
#include "game_state.hpp"
#include "rendering/gl_renderer.hpp"
#include "systems/aim.hpp"
#include "systems/carry_lex.hpp"
#include "systems/celebration_system.hpp"
#include "systems/cpp_highlight_system.hpp"
#include "systems/game_mod_abi.h"
#include "systems/input_system.hpp"
#include "systems/physics_system.hpp"
#include "systems/pick_system.hpp"
#include "systems/source_tokens.hpp"
#include "text/token.hpp"
#include "utils/config.hpp"
#include "utils/frame_profiler.hpp"
#include "wall/label.hpp"
#include "wall/wall.hpp"
namespace
Nimbin
{
GameState::GameState(size_t starting_level)
{
_ctx.player = cpputils::make_unique<Player>();
_ctx.world = cpputils::make_unique<World> ();
_ctx.camera = cpputils::make_unique<Camera>();
_ctx.light = cpputils::make_unique<Light> ();
_input_sys = cpputils::make_unique<InputSystem> ();
_freelook_sys = cpputils::make_unique<FreelookSystem> ();
_compiler_sys = cpputils::make_unique<CompilerSystem> ();
_program_sys = cpputils::make_unique<ProgramSystem> ();
_dyn_loader_sys = cpputils::make_unique<DynLoaderSystem> ();
_physics_sys = cpputils::make_unique<PhysicsSystem> ();
_celebration_sys = cpputils::make_unique<CelebrationSystem> ();
_projectiles_sys = cpputils::make_unique<ProjectileSystem> ();
_particle_sys = cpputils::make_unique<ParticleSystem> ();
_word_anim_sys = cpputils::make_unique<WordAnimSystem> ();
_highlight_sys = cpputils::make_unique<CppHighlightSystem>();
_wall_labels = Wall::Label::generate_All(World::get_BoxScale());
_wall_editor = Text::Handler(Wall::Role::Editor, World::get_BoxScale(),
Text::LayoutMode::Editing);
_wall_editor.set_ShowNumbers(true, true);
_text_walls = { &_wall_editor };
_wall_program = Text::Handler (Wall::Role::Program, World::get_BoxScale(),
Text::LayoutMode::Display);
_wall_program.set_Lexing (false);
_wall_program.set_ShowNumbers (false, false);
_wall_program.set_DefaultStyle (Text::Token::depth_wall, 0.003f,
static_cast<double>(ProgramSystem::line_height),
Config::Wall::free_space_dist);
_wall_program.set_AppendBaseline (static_cast<double>(ProgramSystem::start_y));
_wall_program.set_FaceMode (Text::FaceMode::Pitch);
_wall_compiler = Text::Handler (Wall::Role::Compiler, World::get_BoxScale(),
Text::LayoutMode::Display);
_wall_compiler.set_Lexing (false);
_wall_compiler.set_ShowNumbers (false, false);
_wall_compiler.set_DefaultStyle (Text::Token::depth_compiler, 0.003f,
static_cast<double>(CompilerSystem::line_height),
Config::Wall::free_space_dist);
_wall_compiler.set_AppendBaseline(static_cast<double>(CompilerSystem::start_y));
_wall_compiler.set_AppendColor (Config::Color::white);
_wall_compiler.set_FaceMode (Text::FaceMode::Pitch);
_wall_info = Text::Handler (Wall::Role::Info, World::get_BoxScale(),
Text::LayoutMode::Display);
_wall_info.set_Lexing (false);
_wall_info.set_ShowNumbers (false, false);
_wall_info.set_DefaultStyle (Text::Token::depth_wall, 0.003f,
static_cast<double>(Config::Wall::text_line_spacing),
Config::Wall::free_space_dist);
_wall_info.set_AppendBaseline(World::get_FloorY<double>() + 1.0);
_wall_info.set_AppendColor (Config::Color::white);
_wall_info.set_FaceMode (Text::FaceMode::Pitch);
_text_walls = { &_wall_editor, &_wall_program, &_wall_compiler, &_wall_info };
_ctx.editor_wall = &_wall_editor;
_ctx.info_wall = &_wall_info;
_pult_sys = cpputils::make_unique<PultSystem> ();
_pult_sys->init(_ctx.scenery, _ctx.buttons, World::get_BoxScale());
if (starting_level >= Level::get_Count()) starting_level = 0;
_level_director = cpputils::make_unique<Level::Director>(Level::get_Data(starting_level));
_level_director->enter_Step(_ctx);
_word_ai_sys = cpputils::make_unique<WordAiSystem>();
_word_ai_sys->set_MaxActiveAi(_level_director->get_MaxAiForStep());
_hint_sys = cpputils::make_unique<HintSystem>();
if (!_level_director->is_LevelComplete())
_hint_sys->set_Hint(_level_director->get_CurrentHint());
if (_level_director->get_Runner().is_DynamicLib())
sandbox_LoadFileToWall();
}
void
GameState::debug_SpawnWords([[maybe_unused]] size_t n)
{
DynArray<std::string> tokens = Text::Token::get_TestArrayAllKeywords();
static constexpr float spacing = 3.0f;
static constexpr int per_row = 8;
static constexpr float row_gap = 3.0f;
const double hover_y = World::get_FloorY<double>() + 1.5;
for (size_t i = 0; i < tokens.size(); i++) {
Vec3 pos { -static_cast<float>(per_row) * spacing * 0.5f
+ static_cast<float>(i % per_row) * spacing,
hover_y,
static_cast<float>(i / per_row) * row_gap };
Text::TokenPtr obj = Text::make_Token(tokens[i],
Text::Token::depth_floor,
0.003f, pos);
obj->body.hover_height = hover_y;
_ctx.words.push_back(std::move(obj));
}
}
void
GameState::carriedline_Update()
{
if (_carried.empty()) return;
const FreelookState& fs = _freelook_sys->state;
if (fs.active) return;
const Vec3 anchor = CarriedLine::get_LineAnchor(*_ctx.camera, Config::Carry::dist);
const Vec3 right = _ctx.camera->right;
const double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z);
DynArray<Vec3> slots = _carried.get_SlotPosition(anchor, right, Config::Carry::gap);
DynArray<CarriedWordNode*>
nodes = _carried.reverse_nodes();
const bool easing = fs.lerping && !_freelook_return_from.empty();
double s = 1.0;
if (easing) {
double t = fs.lerp_t;
t = (t < 0.0) ? 0.0 : (t > 1.0 ? 1.0 : t);
s = t * t * (3.0 - 2.0 * t);
}
for (size_t i = 0; i < nodes.size() && i < slots.size(); i++) {
if (!nodes[i]->word) continue;
Vec3 target = slots[i];
if (easing && i < _freelook_return_from.size()) {
const Vec3& from = _freelook_return_from[i];
nodes[i]->word->pos = { from.x + (target.x - from.x) * s,
from.y + (target.y - from.y) * s,
from.z + (target.z - from.z) * s };
}
else {
nodes[i]->word->pos = target;
}
nodes[i]->word->yaw = static_cast<float>(glyph_yaw);
}
}
void
GameState::pult_RunAction(PultAction act)
{
_level_director->record_Button(act);
switch (act) {
case PultAction::Compile: on_Compile (); break;
case PultAction::RunProgram: on_Run (); break;
case PultAction::ScrollEditorUp: on_EditorScroll( 1); break;
case PultAction::ScrollEditorDown: on_EditorScroll(-1); break;
case PultAction::ClearEditor: on_EditorClear (); break;
case PultAction::ConnectEditor: wallconnect_BeginEditor (); break;
case PultAction::ConnectProgram: wallconnect_BeginProgram(); break;
}
}
void
GameState::on_Click()
{
if (_focus_active) {
const float ws = World::get_BoxScale<float>();
const Wall::Transform
transform = Wall::get_Transform(_focus_role, ws);
const Vec3 normal = Wall::inward_Normal(transform.wall_position);
const Vec3& cam_pos = _ctx.camera->pos;
const Vec3& cam_fwd = _ctx.camera->fwd;
const double denom = cam_fwd.x*normal.x + cam_fwd.y*normal.y + cam_fwd.z*normal.z;
if (denom < -1e-6) {
const double t = ((transform.position.x-cam_pos.x)*normal.x
+ (transform.position.y-cam_pos.y)*normal.y
+ (transform.position.z-cam_pos.z)*normal.z) / denom;
if (t > 0.0) { // crosshair points on wall
const Vec3 impact { cam_pos.x + cam_fwd.x*t, cam_pos.y + cam_fwd.y*t, cam_pos.z + cam_fwd.z*t };
if (_focus_role == Wall::Role::Editor) {
const Text::Handler::CursorHit hit =
_wall_editor.resolve_Impact(impact);
wallconnect_BeginEditor();
_wall_editor.place_CursorInsert(hit);
}
else if (_focus_role == Wall::Role::Program) {
wallconnect_BeginProgram();
}
}
}
return;
}
if (_wall_editor.edit_Active()) {
const Wall::Transform
wall_transform = Wall::get_Transform(Wall::Role::Editor, World::get_BoxScale());
const double wall_tans_x = wall_transform.position.x;
const Vec3& cam_pos = _ctx.camera->pos;
const Vec3& cam_fwd = _ctx.camera->fwd;
if (SDL_fabs(cam_fwd.x) > 1e-6) {
const double t = (wall_tans_x - cam_pos.x) / cam_fwd.x;
if (t > 0.0) {
Vec3 impact { wall_tans_x, cam_pos.y + cam_fwd.y * t, cam_pos.z + cam_fwd.z * t };
if (_wall_editor.cursor_AtImpact(impact)) return;
}
}
if (Text::Token::Data* tk = PickSystem::pick_WallToken(*_ctx.camera, _wall_editor))
_wall_editor.cursor_ToToken(tk);
return;
}
PickResult result = PickSystem::pick(*_ctx.camera, _ctx.words, _ctx.scenery, _text_walls);
if (!result.hit()) return;
if (result.kind == PickKind::Scenery && result.scenery_obj) {
if (std::optional<PultAction> act =
_pult_sys->try_Activate(result.scenery_obj, _ctx.buttons); act)
pult_RunAction(*act);
}
}
bool
GameState::on_Pick()
{
PickResult result = PickSystem::pick(*_ctx.camera, _ctx.words, _ctx.scenery, _text_walls);
if (!result.hit()) return false;
if (result.kind == PickKind::WallWord && result.wall_handler && result.wall_token) {
Text::TokenPtr picked = result.wall_handler->detach(result.wall_token);
if (!picked) return false;
_level_director->record_PickedUp(picked->text);
picked->set_Look (Config::Color::keep, Text::Token::depth_floor);
Wall::show_TokenInfo (_wall_info, picked->text);
carry_BeginAnim (std::move(picked), true);
return true;
}
if (result.kind == PickKind::Word) {
Text::TokenPtr picked = std::move(_ctx.words[result.word_index]);
_ctx.words.erase(_ctx.words.begin() + static_cast<std::ptrdiff_t>(result.word_index));
if (!picked) return false;
_level_director->record_PickedUp(picked->text);
picked->set_Look (Config::Color::keep, Text::Token::depth_floor);
Wall::show_TokenInfo(_wall_info, picked->text);
carry_BeginAnim (std::move(picked), false);
return true;
}
return false;
}
void
GameState::on_DropLast()
{
if (_carried.empty()) return;
Text::TokenPtr dropped = _carried.pop_back();
if (!dropped) return;
CarryLex::revert_ToSingle(dropped.get());
dropped->body.mode = PhysicsMode::Dynamic;
dropped->body.vel = { _ctx.camera->fwd.x * 1.5, 0.8, _ctx.camera->fwd.z * 1.5 };
WordAnim_Drop drop;
drop.word = std::move(dropped);
_anims_drop.push_back(std::move(drop));
if (!_carried.empty()) {
Wall::show_TokenInfo(_wall_info, _carried.last->word->text);
}
else _level_director->show_Details(_ctx);
_level_director->record_Dropped();
}
bool
GameState::on_ShootOne()
{
if (_carried.empty()) return false;
Text::TokenPtr word = _carried.pop_back();
if (!word) return false;
CarryLex::revert_ToSingle(word.get());
_freelook_sys->state.active = false;
_freelook_sys->state.lerping = false;
DynArray<Text::TokenPtr> single;
single.push_back(std::move(word));
Aim::Result aim = Aim::crosshair_Target(*_ctx.camera, _ctx.words, _ctx.scenery,
_text_walls, World::get_BoxScale());
_projectiles_sys->launch(_projectiles, std::move(single),
_ctx.camera->fwd, aim);
if (!_carried.empty() && _carried.last && _carried.last->word) {
Wall::show_TokenInfo(_wall_info, _carried.last->word->text);
}
else _level_director->show_Details(_ctx);
_level_director->record_Shot();
return true;
}
void
GameState::on_ShootAll()
{
if (_carried.empty()) return;
_freelook_sys->state.active = false;
_freelook_sys->state.lerping = false;
DynArray<Text::TokenPtr> all_words;
for (CarriedWordNode* node : _carried.nodes())
if (node->word) {
CarryLex::revert_ToSingle (node->word.get());
all_words.push_back(std::move(node->word));
}
_carried = CarriedLine();
Aim::Result aim;
if (all_words.empty())
return;
aim = Aim::crosshair_Target(*_ctx.camera, _ctx.words, _ctx.scenery,
_text_walls, World::get_BoxScale());
_projectiles_sys->launch(_projectiles, std::move(all_words),
_ctx.camera->fwd, aim);
_level_director->show_Details(_ctx);
_level_director->record_Shot ();
}
std::string
GameState::get_EditorSource() const
{
std::string src;
for (const std::string& l : _wall_editor.as_Lines())
{ src += l; src += '\n'; }
return src;
}
void GameState::on_Compile() {
std::string src = get_EditorSource();
if ( !_level_director->is_LevelComplete() &&
_level_director->get_Runner().is_DynamicLib()) {
const std::string path = sandbox_GetSourcePath();
std::string stripped;
for (char c : src)
if (!std::isspace((unsigned char)c))
{ stripped = src; break; }
if (!path.empty() && !stripped.empty())
SourceTokens::write_File(path, src);
_dyn_loader_sys->build(_wall_compiler, src, true);
}
else {
_compiler_sys->compile(_wall_compiler, src);
}
}
void
GameState::on_Run()
{
_program_sys->run(_wall_program);
}
void
GameState::on_DebugPrint()
{
std::print("TEXT::: {0}\n", get_EditorSource());
}
void
GameState::on_FocusToggle()
{
_focus_on = !_focus_on;
}
void
GameState::on_Jump()
{
if (_focus_active) {
_ctx.player->pos = _ctx.camera->pos;
_ctx.player->prev_pos = _ctx.camera->pos;
_ctx.player->vel = Vec3{0, 0, 0};
_ctx.player->on_ground = false;
return;
}
if (_ctx.player->on_ground) {
_ctx.player->vel.y = Config::Physics::player_jump_velocity;
_ctx.player->on_ground = false;
}
}
void
GameState::on_FreelookStart()
{
_freelook_sys->begin();
}
void
GameState::on_FreelookEnd()
{
if (!_freelook_sys->state.active) return;
_freelook_return_from.clear();
for (CarriedWordNode* node : _carried.reverse_nodes())
if (node->word) _freelook_return_from.push_back(node->word->pos);
_freelook_sys->end();
}
void
GameState::on_NumberAdjust(int delta)
{
if (_carried.empty() || !_carried.last || !_carried.last->word)
return;
Text::Token::Data* word = _carried.last->word.get();
const std::string& text = word->text;
if (text.empty()) return;
size_t start = (text[0] == '-') ? 1 : 0;
if (start == text.size()) return;
for (size_t i = start; i < text.size(); i++)
if (!std::isdigit(static_cast<unsigned char>(text[i])))
return;
long val = std::strtol(text.c_str(), nullptr, 10);
val += delta;
word->text = std::to_string(val);
word->rebuild_Mesh();
}
void
GameState::on_TypingStart(FrameInput& input)
{
input.typing_mode = true;
input.typing_buffer.clear();
_typing_words.clear();
_typing_last .clear();
}
void
GameState::on_TypingCommit(FrameInput& input)
{
input.typing_mode = false;
input.typing_buffer.clear();
_typing_words.clear();
_typing_last .clear();
}
void
GameState::on_TypingCancel(FrameInput& input)
{
typing_ClearPreview();
input.typing_mode = false;
input.typing_buffer.clear();
_typing_last.clear();
}
void
GameState::projectile_LandOnProgramInput(ProjectileResult& result)
{
std::string line;
for (const Text::TokenPtr& word : result.words) {
if (!word || word->type != ObjectType::Text) continue;
if (!line.empty()) line += ' ';
line += word->text;
}
if (!line.empty())
_program_sys->feed_Input(line);
_program_sys->echo_Words(_wall_program, result.words);
}
void
GameState::projectile_LandOnFloor(ProjectileResult& result)
{
for (Text::TokenPtr& word : result.words) {
if (!word || word->type != ObjectType::Text) continue;
word->set_Look(Config::Color::keep, Text::Token::depth_floor);
word->body.mode = PhysicsMode::Dynamic;
word->body.vel = result.vel;
_ctx.words.push_back(std::move(word));
}
}
void
GameState::projectile_LandOnWall(ProjectileResult& result)
{
DynArray<Text::TokenPtr> edit_block, prog_block, comp_block, info_block;
for (Text::TokenPtr& word : result.words) {
if (!word) continue;
word->body.mode = PhysicsMode::Static;
word->body.vel = Vec3{0, 0, 0};
Wall::Drop drop = Wall::attach_ToNearestWall(std::move(word));
if (!drop.token) continue;
switch (drop.role) {
case Wall::Role::Editor: edit_block.push_back(std::move(drop.token)); break;
case Wall::Role::Program: prog_block.push_back(std::move(drop.token)); break;
case Wall::Role::Compiler: comp_block.push_back(std::move(drop.token)); break;
case Wall::Role::Info: info_block.push_back(std::move(drop.token)); break;
}
}
if (!edit_block.empty()) {
Vec3 impact{0, 0, 0};
for (const Text::TokenPtr& t : edit_block)
{ impact.x += t->pos.x; impact.y += t->pos.y; impact.z += t->pos.z; }
const double inv = 1.0 / static_cast<double>(edit_block.size());
impact.x *= inv;
impact.y *= inv;
impact.z *= inv;
_wall_editor.attach(std::move(edit_block), impact);
}
{ FP_ZONE("flush.append");
if (!prog_block.empty()) _wall_program .append_Tokens(std::move(prog_block));
if (!comp_block.empty()) _wall_compiler.append_Tokens(std::move(comp_block));
if (!info_block.empty()) _wall_info .append_Tokens(std::move(info_block));
}
}
void
GameState::projectile_LandOnWallText(ProjectileResult& result)
{
const size_t idx = result.wall_text_index;
if (idx >= LABEL_HIT_BASE) {
for (Text::TokenPtr& word : result.words)
if (word) _ctx.drop_WordToFloor(std::move(word), result.vel);
return;
}
if (idx >= WALL_TOK_BASE) {
const size_t rel = idx - WALL_TOK_BASE;
const size_t handler_id = rel >> 28;
const size_t line_idx = (rel >> 14) & 0x3FFF;
const size_t token_idx = rel & 0x3FFF;
Text::Handler* handler = (handler_id < _text_walls.size())
? _text_walls[handler_id]
: nullptr;
if (handler) {
const auto& lines = handler->get_Lines();
if ( line_idx < lines.size()
&& token_idx < lines[line_idx].tokens.size()
&& lines[line_idx].tokens[token_idx]) {
if (handler->is_Editing()) {
Vec3 impact = lines[line_idx].tokens[token_idx]->pos;
for (const Text::TokenPtr& word : result.words)
if (word) { impact = word->pos; break; }
if (result.use_aim) impact = result.aim_point;
const Text::Handler::EditorHit hit = handler->classify_Impact(impact);
if (hit.kind == Text::Handler::HitKind::DropToken) {
const auto& ls = handler->get_Lines();
if ( hit.line_idx < ls.size()
&& hit.token_idx < ls[hit.line_idx].tokens.size()
&& ls[hit.line_idx].tokens[hit.token_idx]) {
if (Text::TokenPtr orphan = handler->detach(ls[hit.line_idx].tokens[hit.token_idx].get())) {
orphan->body.hover_height = World::get_FloorY<double>() + 1.5;
orphan->yaw = 0.0f;
_ctx.drop_WordToFloor(std::move(orphan), Vec3{0.0, 1.5, 0.0});
}
}
for (Text::TokenPtr& word : result.words)
if (word) _ctx.drop_WordToFloor(std::move(word), result.vel);
}
else {
DynArray<Text::TokenPtr> block;
for (Text::TokenPtr& word : result.words)
if (word) block.push_back(std::move(word));
handler->attach(std::move(block), impact);
}
return;
}
if (Text::TokenPtr orphan = handler->detach(lines[line_idx].tokens[token_idx].get())) {
orphan->body.hover_height = World::get_FloorY<double>() + 1.5;
orphan->yaw = 0.0f;
_ctx.drop_WordToFloor(std::move(orphan), Vec3{0.0, 1.5, 0.0});
}
}
}
for (Text::TokenPtr& word : result.words)
if (word) _ctx.drop_WordToFloor(std::move(word), result.vel);
return;
}
if (idx >= Text::Handler::HIT_BASE) {
const size_t lid = Text::Handler::line_Of(idx);
const double delta = _wall_editor.scroll_DeltaToEye(lid, _ctx.camera->pos.y);
_wall_editor.scroll(delta);
_wall_editor.bounce(lid);
projectile_LandOnFloor(result);
return;
}
for (Text::TokenPtr& word : result.words)
if (word) _ctx.drop_WordToFloor(std::move(word), result.vel);
}
void
GameState::handle_Event(Game& game, const SDL_Event& event)
{
_input_sys->handle_Event(game, event, _frame_input);
}
void
GameState::update(double dt, const GlRenderer* gl)
{
if (gl) { _fov_y = gl->get_Fov_y(); _aspect = gl->get_Aspect(); }
FrameInput& input = _frame_input;
{ FP_ZONE("update.input" ); update_Input (input, dt); }
{ FP_ZONE("update.world_sim" ); update_Physics (dt); }
{ FP_ZONE("update.present" ); update_Objects (dt, gl); }
{ FP_ZONE("update.subsystems" ); update_Compiler (); }
{ FP_ZONE("update.progression"); update_Progression (dt); }
{ FP_ZONE("update.highlight" ); update_Highlighting(); }
input.look_x = 0;
input.look_y = 0;
input.scroll_x = 0;
input.scroll_y = 0;
input.commands.clear();
}
void
GameState::update_Input(FrameInput& input, double dt)
{
for (InputCommand cmd : input.commands) {
switch (cmd) {
case InputCommand::Jump: on_Jump(); break;
case InputCommand::Click: on_Click(); break;
case InputCommand::Drop: on_DropLast(); break;
case InputCommand::ShootAll: on_ShootAll(); break;
case InputCommand::Compile: on_Compile(); break;
case InputCommand::Run: on_Run(); break;
case InputCommand::DebugPrint: on_DebugPrint(); break;
case InputCommand::FocusToggle: on_FocusToggle(); break;
case InputCommand::FreelookStart: on_FreelookStart(); break;
case InputCommand::FreelookEnd: on_FreelookEnd(); break;
case InputCommand::NumberInc: on_NumberAdjust(+1); break;
case InputCommand::NumberDec: on_NumberAdjust(-1); break;
case InputCommand::TypingStart: on_TypingStart (input); break;
case InputCommand::TypingCommit: on_TypingCommit(input); break;
case InputCommand::TypingCancel: on_TypingCancel(input); break;
case InputCommand::SandboxRestore: on_SandboxRestore(); break;
case InputCommand::ConnectPultSubmit: on_WallConnectSubmit(); break;
case InputCommand::ConnectPultExit: on_WallConnectExit(); break;
}
}
Wall::Connection::Target
target = _wall_connection.get_Target();
if (target == Wall::Connection::Target::Editor) {
{ FP_ZONE("handle_input.Editor");
Text::Handler::EditReq req = _wall_editor.feed_Keys(input.vim_keys, _ctx.camera->pos.y);
input.vim_keys.clear();
switch (req) {
case Text::Handler::EditReq::Write:
_wall_editor.mark_Saved();
break;
case Text::Handler::EditReq::WriteQuit:
case Text::Handler::EditReq::Exit:
case Text::Handler::EditReq::Discard:
_wall_editor.end_Edit();
_wall_connection.end();
_frame_input.capture_vim = false;
_focus_locked = false;
break;
case Text::Handler::EditReq::None:
break;
}
}
}
else if (target == Wall::Connection::Target::Program) {
{ FP_ZONE("handle_input.Program");
_wall_connection.set_ProgramBuffer(input.typing_buffer);
}
}
else if (input.typing_mode) {
{ FP_ZONE("handle_input.typing_mode");
input.forward = input.backward = input.left = input.right = false;
if (input.typing_buffer != _typing_last) {
typing_RebuildPreview(input.typing_buffer);
_typing_last = input.typing_buffer;
}
}
}
if (_pick_cooldown > 0.0) _pick_cooldown -= dt;
if (input.pick && _pick_cooldown <= 0.0)
if (on_Pick()) _pick_cooldown = 0.15;
if (_shoot_cooldown > 0.0) _shoot_cooldown -= dt;
if (input.shoot && _shoot_cooldown <= 0.0)
{ FP_ZONE("handle_input.onShootO"); if (on_ShootOne()) _shoot_cooldown = 0.15; }
const bool want_focus = _focus_on || _focus_locked;
if (!_freelook_sys->state.active && !want_focus)
_ctx.player->add_Look(input.look_x, input.look_y);
_freelook_sys->update(dt);
if (want_focus) {
const bool f = input.forward, b = input.backward,
l = input.left, r = input.right;
input.forward = input.backward = input.left = input.right = false;
_ctx.player->compute_Movement(input);
input.forward = f; input.backward = b; input.left = l; input.right = r;
}
else {
_ctx.player->compute_Movement(input);
}
_ctx.camera->sync_FromPlayer(*_ctx.player);
focus_UpdateWall(input, dt);
}
void
GameState::update_Objects(double dt, const GlRenderer* gl)
{
for (Wall::Label& label : _wall_labels)
label.update(dt, *_ctx.camera);
carriedline_Update();
_word_anim_sys->update(_anims_add, _anims_drop, _carried, _ctx.words, dt);
if (_carried.size != _carried_last_size) {
if (_carried.size > 0)
CarryLex::lex_CarriedLine(_carried);
_carried_last_size = _carried.size;
}
_pult_sys ->update(dt, *_ctx.camera);
_plate_sys->update(dt, *_ctx.player, _level_director->get_StepPlates());
for (Text::Handler* handler : _text_walls)
if (handler) handler->update(dt, *_ctx.camera);
_wall_connection.update(dt, *_ctx.camera, gl->get_Aspect(), gl->get_Fov_y());
focus_UpdateLabel (dt, *_ctx.camera, gl->get_Aspect(), gl->get_Fov_y());
DynArray<GameObject*> particle_colliders;
particle_colliders.reserve(_ctx.scenery.size());
for (const GameObjectPtr& o : _ctx.scenery)
if (o) particle_colliders.push_back(o.get());
_particle_sys->update(dt, particle_colliders, World::get_BoxScale());
}
void
GameState::update_Compiler()
{
_compiler_sys->update(_wall_compiler);
if (_compiler_sys->last_compile_ok) {
_compiler_sys->last_compile_ok = false;
_level_director->record_CompileOk();
}
// TODO: the dyn path may still blocks the frame in dynlib_Apply
_dyn_loader_sys->update(_wall_compiler);
if (_dyn_loader_sys->last_build_ok) {
_dyn_loader_sys->last_build_ok = false;
dynlib_Apply(_dyn_loader_sys->load_Objects());
SourceTokens::copy_File(sandbox_GetSourcePath(), sandbox_BackupPath());
}
_program_sys->update(_wall_program);
}
void
GameState::update_Highlighting()
{
if (_wall_editor.is_Dirty())
_highlight_sys->request(_wall_editor.render_Source(), _wall_editor.generation());
DynArray<Text::HiSpan> spans;
uint64_t gen;
if (_highlight_sys->poll(spans, gen))
_wall_editor.apply_Highlight(spans, gen);
}
void
GameState::update_Progression(double dt)
{
_celebration_sys->tick(dt, *_particle_sys);
level_TryAdvance(dt);
}
void
GameState::render(SDL_Window& window, GlRenderer& renderer)
{
int w, h;
SDL_GetWindowSizeInPixels(&window, &w, &h);
{ FP_ZONE("render.light_setup");
const double yaw_r = Config::Physics::binrad * static_cast<double>(_ctx.camera->yaw);
const double pitch_r = Config::Physics::binrad * static_cast<double>(_ctx.camera->pitch);
const double cy = SDL_cos(yaw_r), sy = SDL_sin(yaw_r);
const double cp = SDL_cos(pitch_r), sp = SDL_sin(pitch_r);
_ctx.light->flashlight_pos = _ctx.camera->pos;
_ctx.light->flashlight_dir = Vec3{ -sy*cp, sp, -cy*cp };
}
{ FP_ZONE("render.begin_Frame"); renderer.begin_Frame(w, h); }
{ FP_ZONE("render.set_Light" ); renderer.set_Light(*_ctx.light); }
{ FP_ZONE("render.world" ); _ctx.world ->draw(renderer, *_ctx.camera, *_ctx.light); }
{ FP_ZONE("render.text_walls" );
for (Text::Handler* handler : _text_walls)
if (handler) handler->render(renderer, *_ctx.camera, *_ctx.light);
}
{ FP_ZONE("render.scenery" );
for (const GameObjectPtr& scenery : _ctx.scenery)
if (scenery) scenery->draw(renderer, *_ctx.camera, *_ctx.light);
}
{ FP_ZONE("render.words" );
for (const Text::TokenPtr& word : _ctx.words)
if (word) word->draw(renderer, *_ctx.camera, *_ctx.light);
}
{ FP_ZONE("render.wall_labels");
for (const Wall::Label& label : _wall_labels)
label.draw(renderer, *_ctx.camera, *_ctx.light);
}
{ FP_ZONE("render._carried" );
DynArray<CarriedWordNode*> nodes = _carried.nodes();
for (const CarriedWordNode* node : nodes)
if (node && node->word)
node->word->draw(renderer, *_ctx.camera, *_ctx.light);
}
{ FP_ZONE("render.anims" );
for (const WordAnim_Add& anim : _anims_add)
if (anim.word) anim.word->draw(renderer, *_ctx.camera, *_ctx.light);
for (const WordAnim_Drop& drop : _anims_drop)
if (drop.word) drop.word->draw(renderer, *_ctx.camera, *_ctx.light);
}
{ FP_ZONE("render.ai.stolen" );
for (const Text::TokenPtr& word : _ctx.words) {
if (!word) continue;
for (const Text::TokenPtr& s : word->ai.stolen)
if (s) s->draw(renderer, *_ctx.camera, *_ctx.light);
}
}
{ FP_ZONE("render._projectiles");
for (const Projectile& proj : _projectiles)
for (const Text::TokenPtr& word : proj.words)
if (word) word->draw(renderer, *_ctx.camera, *_ctx.light);
}
{ FP_ZONE("render.hint" ); _hint_sys ->draw(renderer, *_ctx.camera, *_ctx.light); }
{ FP_ZONE("render.pult_labels"); _pult_sys ->draw_Labels(renderer, *_ctx.camera, *_ctx.light); }
{ FP_ZONE("render.particles" ); _particle_sys ->draw(renderer, *_ctx.camera, *_ctx.light); }
{ FP_ZONE("render.celebration"); _celebration_sys ->draw(renderer, *_ctx.camera, *_ctx.light); }
{ FP_ZONE("render.wall_edit" ); _wall_connection.render(renderer, *_ctx.camera, *_ctx.light); }
{ FP_ZONE("render.focus_label");
if (_focus_active && _focus_label) {
const double hw = static_cast<double>(_focus_label->half_extent);
_focus_label->pos = Vec3{
_focus_label_pos.x + _ctx.camera->right.x * hw,
_focus_label_pos.y + _ctx.camera->right.y * hw,
_focus_label_pos.z + _ctx.camera->right.z * hw,
};
_focus_label->yaw = _focus_label_yaw;
_focus_label->pitch = 0.0f;
renderer.clear_Depth();
_focus_label->draw(renderer, *_ctx.camera, *_ctx.light);
}
}
{ FP_ZONE("render.end_Frame" ); renderer.end_Frame(&window); }
}
void
GameState::update_Physics(double dt)
{
_ctx.player->prev_pos = _ctx.player->pos;
{ FP_ZONE("updateworldsim.phys_player" );
_physics_sys->update_Player(*_ctx.player,
_ctx.player->get_AccX(),
_ctx.player->get_AccZ(),
dt);
}
DynArray<GameObject*> colliders;
colliders.reserve(_ctx.scenery.size() + _ctx.words.size());
for (const GameObjectPtr& o : _ctx.scenery) if (o) colliders.push_back(o.get());
for (const Text::TokenPtr& w : _ctx.words) if (w) colliders.push_back(w.get());
{ FP_ZONE("updateworldsim.phys_objects");
_physics_sys->update_Objects(colliders, dt, 25.0);
}
{ FP_ZONE("updateworldsim.word_ai" );
DynArray<Text::TokenPtr> ai_dropped;
_word_ai_sys->update(_ctx.words, *_ctx.player, _carried, ai_dropped, dt);
for (Text::TokenPtr& drop : ai_dropped)
if (drop) _ctx.words.push_back(std::move(drop));
}
{ FP_ZONE("updateworldsim.phys_projectiles");
_physics_sys->update_Projectiles(_projectiles, dt, _ctx.words);
}
DynArray<Text::Handler*> all_labels = _pult_sys->get_Labels();
{ FP_ZONE("updateworldsim.hint_and_labels" );
_hint_sys->update(dt, *_ctx.camera);
for (Text::Handler* label : all_labels)
label->update(dt, *_ctx.camera);
all_labels.push_back(&_hint_sys->get_Label());
for (Wall::Label& label : _wall_labels)
all_labels.push_back(&label.label);
}
DynArray<ProjectileContactEvent> projectile_contacts;
{ FP_ZONE("updateworldsim.resolve_projectiles");
_physics_sys->resolve_Projectiles(_projectiles, &colliders, &all_labels, &projectile_contacts);
}
DynArray<WallTextHitTest> wall_text_hits;
{ FP_ZONE("updateworldsim.build_wall_hits" );
for (size_t i = 0; i < _text_walls.size(); ++i) {
Text::Handler* handler = _text_walls[i];
if (!handler) continue;
const auto& elines = handler->get_Lines();
for (size_t line_idx = 0; line_idx < elines.size(); ++line_idx) {
const DynArray<Text::TokenPtr>& toks = elines[line_idx].tokens;
for (size_t token_idx = 0; token_idx < toks.size(); ++token_idx) {
if (!toks[token_idx]) continue;
wall_text_hits.push_back({
toks[token_idx]->aabb_Min(),
toks[token_idx]->aabb_Max(),
WALL_TOK_BASE | (i << 28) | (line_idx << 14) | token_idx,
Wall::Position::NegX,
});
}
}
}
for (const Text::Handler::NumberHit& nh : _wall_editor.number_Hitboxes())
wall_text_hits.push_back({
nh.aabb_Min,
nh.aabb_Max,
nh.index,
Wall::Position::NegX,
});
for (size_t line_idx = 0; line_idx < _wall_labels.size(); ++line_idx) {
const DynArray<Text::TokenPtr>& words = _wall_labels[line_idx].label.get_Words();
for (size_t wi = 0; wi < words.size(); ++wi) {
if (!words[wi]) continue;
wall_text_hits.push_back({
words[wi]->aabb_Min(),
words[wi]->aabb_Max(),
LABEL_HIT_BASE | (line_idx << 16) | wi,
Wall::position_Of(_wall_labels[line_idx].role),
});
}
}
}
DynArray<ProjectileResult> proj_results;
{ FP_ZONE("updateworldsim.projectile_sys" );
_projectiles_sys->update(_projectiles, proj_results,
wall_text_hits, dt,
_program_sys->is_Running());
}
{ FP_ZONE("updateworldsim.projectile_land");
for (ProjectileResult& result : proj_results) {
switch (result.landing) {
case ProjectileLanding::Floor: projectile_LandOnFloor (result); break;
case ProjectileLanding::ProgramInput:
projectile_LandOnProgramInput(result); break;
case ProjectileLanding::Wall: projectile_LandOnWall (result); break;
case ProjectileLanding::WallText: projectile_LandOnWallText (result); break;
case ProjectileLanding::None: break;
}
}
for (const ProjectileContactEvent& contact : projectile_contacts) {
if (std::optional<PultAction> action =
_pult_sys->handle_ProjectileContact(contact, _ctx.buttons); action) {
pult_RunAction(*action);
continue;
}
if (contact.other && contact.other->type == ObjectType::Text) {
Text::Token::Data* word = static_cast<Text::Token::Data*>(contact.other);
_level_director->record_AiDefeated(word->text);
word->ai.state = Text::Token::Ai::State::Stunned;
word->ai.state_timer = 0.0;
word->body.vel = Vec3{0, 0, 0};
word->set_Color (Config::Color::stunned);
word->set_ColorSource(Text::ColorSource::Static);
}
}
}
{ FP_ZONE("updateworldsim.player_collision");
PlayerCollision col = _ctx.world->resolve_Player(*_ctx.player);
_ctx.player->apply_Collision(col);
_physics_sys->resolve_PlayerVsObjects(*_ctx.player, _carried, colliders);
if (std::optional<PultAction> act =
_pult_sys->handle_PlayerContact(*_ctx.player, _ctx.buttons); act)
pult_RunAction(*act);
}
}
void
GameState::level_TryAdvance(double dt)
{
Level::Director::AdvanceResult res =
_level_director->try_Advance(_ctx, dt, *_celebration_sys, *_particle_sys);
if (!res.stepped) return;
dynlib_Clear();
if (res.reset_carry) {
_ctx.words .clear();
_projectiles.clear();
_anims_add .clear();
_anims_drop .clear();
_carried = CarriedLine();
}
if (res.advanced) {
_level_director->enter_Step(_ctx);
_word_ai_sys->set_MaxActiveAi(_level_director->get_MaxAiForStep());
_hint_sys ->set_Hint (_level_director->get_CurrentHint());
} else {
_hint_sys ->clear();
_level_director->show_Details(_ctx);
}
}
void
GameState::dynlib_Clear()
{
if (_dyn_scenery.empty()) { return; }
std::unordered_set<GameObject*>
set(_dyn_scenery.begin(), _dyn_scenery.end());
_ctx.scenery.erase( std::remove_if(_ctx.scenery.begin(), _ctx.scenery.end(),
[&set](const GameObjectPtr& sp) { return set.count(sp.get()) > 0; }),
_ctx.scenery.end() );
_ctx.words .erase( std::remove_if(_ctx.words.begin(), _ctx.words.end(),
[&set](const Text::TokenPtr& wp) { return set.count(wp.get()) > 0; }),
_ctx.words.end() );
_dyn_scenery.clear();
}
void
GameState::dynlib_Apply(const DynArray<DynObject>& objs)
{
dynlib_Clear();
for (const DynObject& o : objs) {
if (o.kind == DYN_BOX) {
auto box = cpputils::make_unique<SolidBox>(
Vec3{ o.x , o.y , o.z },
Vec3{ o.hx, o.hy, o.hz },
SDL_FColor{ o.r, o.g, o.b, 1.0f });
box->body.mode = o.dynamic
? PhysicsMode::Dynamic
: PhysicsMode::Static;
_dyn_scenery.push_back(box.get());
_ctx.scenery.push_back(std::move(box));
}
else {
Text::TokenPtr t = Text::make_Token(o.text, Text::Token::depth_floor, 0.003f,
Vec3{ o.x, o.y, o.z },
SDL_FColor{ o.r, o.g, o.b, 1.0f });
t->body.mode = PhysicsMode::Static;
_dyn_scenery.push_back(t.get());
_ctx.words .push_back(std::move(t));
}
}
}
std::string
GameState::sandbox_GetSourcePath() const {
return _level_director
? _level_director->get_Runner().get_SourceFile()
: std::string{};
}
std::string
GameState::sandbox_BackupPath() const {
std::string c = sandbox_GetSourcePath();
return c.empty() ? c : (c + ".last_ok");
}
void
GameState::sandbox_LoadFileToWall()
{
const std::string path = sandbox_GetSourcePath();
if (path.empty()) return;
std::string src;
if (!SourceTokens::read_File(path, src)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Sandbox: cannot read %s", path.c_str());
return;
}
DynArray<std::string> lines;
std::string cur;
for (char c : src) {
if (c == '\n') { lines.push_back(cur); cur.clear(); }
else if (c != '\r') { cur += c; }
}
if (!cur .empty()) lines.push_back(cur);
if (lines.empty()) lines.push_back("");
_wall_editor.set_Lines(lines);
}
void
GameState::on_SandboxRestore() {
if (!_level_director || !_level_director->get_Runner().is_DynamicLib())
return;
if (SourceTokens::copy_File(sandbox_BackupPath(), sandbox_GetSourcePath()))
sandbox_LoadFileToWall();
}
void
GameState::typing_RebuildPreview(const std::string& typed)
{
typing_ClearPreview();
const Vec3 anchor = CarriedLine::get_LineAnchor(*_ctx.camera, Config::Carry::dist);
const double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z);
std::istringstream in(typed);
std::string tok_str;
while (in >> tok_str) {
Text::TokenPtr token = Text::make_Token(tok_str, Text::Token::depth_floor, 0.003f,
anchor, Config::Color::floor);
token->yaw = static_cast<float>(glyph_yaw);
token->body.mode = PhysicsMode::Carried;
_typing_words.push_back(token.get());
_carried.push(std::move(token));
}
CarryLex::lex_CarriedLine(_carried);
}
void
GameState::typing_ClearPreview()
{
for (size_t i = 0; i < _typing_words.size(); i++) {
if (_carried.empty()) break;
_carried.pop_back();
}
_typing_words.clear();
CarryLex::lex_CarriedLine(_carried);
}
void
GameState::carry_BeginAnim(Text::TokenPtr word, bool with_pitch)
{
WordAnim_Add anim;
anim.start_pos = word->pos;
anim.yaw_start = word->yaw;
if (with_pitch) {
anim.pitch_start = word->pitch;
anim.pitch_target = 0.0;
}
anim.word = std::move(word);
anim.t = 0.0;
anim.complete = false;
const Vec3 anchor = CarriedLine::get_LineAnchor(*_ctx.camera, Config::Carry::dist);
const double glyph_yaw = SDL_atan2(-_ctx.camera->fwd.x, -_ctx.camera->fwd.z);
anim.target_pos = anchor;
anim.yaw_target = static_cast<float>(glyph_yaw);
_anims_add.push_back(std::move(anim));
}
void
GameState::wallconnect_BeginProgram()
{
_frame_input.capture_typing = true;
_frame_input.typing_buffer.clear();
_wall_connection.begin_Program(_pult_sys->get_ConnectProgramPos());
}
void
GameState::wallconnect_BeginEditor()
{
_wall_editor.begin_Edit(_ctx.camera->pos.y);
_frame_input.capture_vim = true;
_frame_input.vim_keys.clear();
_wall_connection.begin_Editor(_pult_sys->get_ConnectEditorPos(), _wall_editor);
}
void
GameState::on_WallConnectSubmit()
{
std::string line = _frame_input.typing_buffer;
_frame_input.typing_buffer.clear();
_wall_connection.clear_ProgramPreview();
if (line == ":exit") { on_WallConnectExit(); return; }
if (line == ":help") {
for (const char* h : { ":exit leave the connection",
"ctrl+WASD move while connected",
"<text> + Enter send a line to the program (stdin)" })
_program_sys->echo_Input(_wall_program, h);
return;
}
_program_sys->feed_Input(line);
_program_sys->echo_Input(_wall_program, line);
}
void
GameState::on_WallConnectExit()
{
_wall_connection.end();
_frame_input.capture_typing = false;
_frame_input.typing_buffer.clear();
_focus_locked = false;
}
void
GameState::on_EditorClear()
{
_wall_editor.clear_ToFloor(_ctx.words);
}
void
GameState::on_EditorScroll(int8_t dt)
{
if (dt > 0) _wall_editor.scroll( Config::Wall::scroll_step);
else _wall_editor.scroll(-Config::Wall::scroll_step);
}
Text::Handler*
GameState::get_Handler(Wall::Role role)
{
switch (role) {
case Wall::Role::Editor: return &_wall_editor;
case Wall::Role::Program: return &_wall_program;
case Wall::Role::Compiler: return &_wall_compiler;
case Wall::Role::Info: return &_wall_info;
}
return nullptr;
}
bool
GameState::focus_ComputeTarget()
{
const Camera& cam = *_ctx.camera;
const float ws = World::get_BoxScale<float>();
const Wall::Role roles[4] = { Wall::Role::Editor, Wall::Role::Program,
Wall::Role::Info, Wall::Role::Compiler };
Wall::Role best_role = Wall::Role::Editor;
bool found = false;
double best_t = 1e30;
for (Wall::Role role : roles) {
const Vec3 n = Wall::inward_Normal(Wall::position_Of(role));
const Vec3 p = Wall::get_Position(role, ws);
const double denom = cam.fwd.x*n.x + cam.fwd.y*n.y + cam.fwd.z*n.z;
if (denom < -1e-4) {
const double t = ((p.x-cam.pos.x)*n.x + (p.y-cam.pos.y)*n.y + (p.z-cam.pos.z)*n.z) / denom;
if (t > 0.1 && t < best_t) { best_t = t; best_role = role; found = true; }
}
}
if (!found) return false;
const Wall::Transform tf = Wall::get_Transform(best_role, ws);
const Vec3 n = Wall::inward_Normal(tf.wall_position);
const bool horiz_is_z = (tf.wall_position == Wall::Position::PosX ||
tf.wall_position == Wall::Position::NegX);
double y_min=1e30, y_max=-1e30, h_min=1e30, h_max=-1e30; bool any=false;
if (Text::Handler* hw = get_Handler(best_role))
for (const Text::Line& line : hw->get_Lines())
for (const Text::TokenPtr& tp : line.tokens) {
if (!tp || tp->is_Whitespace()) continue;
const double hc = horiz_is_z ? tp->pos.z : tp->pos.x;
y_min = SDL_min(y_min, tp->pos.y - static_cast<double>(tp->half_extent_y));
y_max = SDL_max(y_max, tp->pos.y + static_cast<double>(tp->half_extent_y));
h_min = SDL_min(h_min, hc - static_cast<double>(tp->half_extent));
h_max = SDL_max(h_max, hc + static_cast<double>(tp->half_extent));
any = true;
}
double cy, ch, text_w, text_h;
if (any) { cy=(y_min+y_max)*0.5; ch=(h_min+h_max)*0.5; text_w=h_max-h_min; text_h=y_max-y_min; }
else { cy=tf.position.y; ch=0.0; text_w=2.0; text_h=2.0; }
const double fov = (_fov_y > 0.01f) ? (double)_fov_y : (SDL_PI_D / 3.0);
const double asp = (_aspect > 0.01f) ? (double)_aspect : 1.7;
const double half_tan = SDL_tan(fov * 0.5);
constexpr double FIT = 0.85;
const double d_v = (text_h * 0.5) / (FIT * half_tan);
const double d_h = (text_w * 0.5) / (FIT * half_tan * asp);
Vec3 center = tf.position;
center.y = cy;
if (horiz_is_z) center.z = ch; else center.x = ch;
_focus_center = center;
_focus_normal = n;
_focus_dist = SDL_max(SDL_max(d_v, d_h), 2.0);
_focus_yaw_r = tf.yaw;
_focus_role = best_role;
return true;
}
void
GameState::focus_UpdateWall(FrameInput& input, double dt)
{
const bool want = _focus_on || _focus_locked;
constexpr double DUR = 0.25;
if (want) {
if (!_focus_active) {
if (!focus_ComputeTarget()) return;
_focus_active = true;
}
_focus_t = SDL_min(1.0, _focus_t + dt / DUR);
const double ws = World::get_BoxScale();
// dolly (scroll wheel): wheel up = toward the wall, down = away. Clamped
// inside the room. Scale by distance so each notch feels proportional.
constexpr double DOLLY = 1.2, MIN_D = 0.6;
const double MAX_D = 2.0 * ws - 0.6;
_focus_dist -= input.scroll_y * DOLLY * (_focus_dist * 0.15 + 1.0);
_focus_dist = SDL_clamp(_focus_dist, MIN_D, MAX_D);
// pan via mouse motion — smooth, any direction (look is suppressed while
// focused, so the mouse deltas are ours). Scale by distance so the pan
// feels the same in screen space whether zoomed in or out.
constexpr double PAN_SENS = 0.0016;
const double k = PAN_SENS * _focus_dist;
const double rx = SDL_cos(_focus_yaw_r); // focus camera right axis (y = 0)
const double rz = -SDL_sin(_focus_yaw_r);
const double pan_h = (double)input.look_x * k;
const double pan_v = (double)input.look_y * k;
const double lim = ws - 0.5;
_focus_center.x = SDL_clamp(_focus_center.x + rx * pan_h, -lim, lim);
_focus_center.z = SDL_clamp(_focus_center.z + rz * pan_h, -lim, lim);
_focus_center.y = SDL_clamp(_focus_center.y - pan_v, -lim, lim);
} else {
if (!_focus_active) return;
_focus_t -= dt / DUR;
if (_focus_t <= 0.0) { _focus_t = 0.0; _focus_active = false; return; }
}
const Vec3 focus_pos = { _focus_center.x + _focus_normal.x * _focus_dist,
_focus_center.y + _focus_normal.y * _focus_dist,
_focus_center.z + _focus_normal.z * _focus_dist };
Camera& cam = *_ctx.camera;
const double s = _focus_t * _focus_t * (3.0 - 2.0 * _focus_t);
double dyaw = _focus_yaw_r - cam.yaw_r;
while (dyaw > SDL_PI_D) dyaw -= 2.0 * SDL_PI_D;
while (dyaw < -SDL_PI_D) dyaw += 2.0 * SDL_PI_D;
const double yaw_r = cam.yaw_r + dyaw * s;
const double pitch_r = cam.pitch_r + (0.0 - cam.pitch_r) * s;
const Vec3 pos = { cam.pos.x + (focus_pos.x - cam.pos.x) * s,
cam.pos.y + (focus_pos.y - cam.pos.y) * s,
cam.pos.z + (focus_pos.z - cam.pos.z) * s };
const double binrad = Config::Physics::binrad;
cam.pos = pos;
cam.yaw_r = yaw_r;
cam.pitch_r = pitch_r;
cam.yaw = (uint32_t)SDL_lround(yaw_r / binrad);
cam.pitch = (int32_t) SDL_lround(pitch_r / binrad);
cam.fwd = { -SDL_sin(yaw_r)*SDL_cos(pitch_r), SDL_sin(pitch_r), -SDL_cos(yaw_r)*SDL_cos(pitch_r) };
cam.right = { SDL_cos(yaw_r), 0.0, -SDL_sin(yaw_r) };
}
void
GameState::focus_UpdateLabel(double dt, const Camera& cam, float aspect, float fov_y)
{
if (!_focus_active) { _focus_label_init = false; return; }
if (!_focus_label)
_focus_label = Text::make_Token("focus", Text::Token::depth_wall, 0.0016f,
Vec3{0, 0, 0}, SDL_FColor{0.35f, 0.55f, 1.0f, 1.0f});
const Vec3 up = Math::cross(cam.right, cam.fwd);
const double dist = 2.2;
const double half_h = dist * static_cast<double>(SDL_tanf(fov_y * 0.5f));
const double half_w = half_h * static_cast<double>(aspect);
const double fx = 0.82;
const double fy = 0.80;
const double left = half_w * fx;
const double upamt= half_h * fy;
const Vec3 target {
cam.pos.x + cam.fwd.x*dist - cam.right.x*left + up.x*upamt,
cam.pos.y + cam.fwd.y*dist - cam.right.y*left + up.y*upamt,
cam.pos.z + cam.fwd.z*dist - cam.right.z*left + up.z*upamt,
};
if (!_focus_label_init) { _focus_label_pos = target; _focus_label_init = true; }
const double k = 1.0 - SDL_exp(-6.0 * dt); // exponential trail = "delay in movement"
_focus_label_pos.x += (target.x - _focus_label_pos.x) * k;
_focus_label_pos.y += (target.y - _focus_label_pos.y) * k;
_focus_label_pos.z += (target.z - _focus_label_pos.z) * k;
_focus_label_yaw = static_cast<float>(SDL_atan2(-cam.fwd.x, -cam.fwd.z));
}
} // namespace Nimbin