shisaku ~/devlog
$ auth github

Author sign in

Sign in with GitHub to comment on devlog entries.

Continue with GitHub
← all posts

Devlog #8 – Making the Defaults Respect the Player (and Other Quiet Fixes)

Persisting pause-menu settings properly, tightening combat feedback budgets, and improving companion/audio/readability behavior without turning the runtime into spaghetti.

Devlog #8 – Making the Defaults Respect the Player (and Other Quiet Fixes)

Date: June 28, 2026

Focus: Persisting pause-menu settings properly, tightening combat feedback budgets, and improving companion/audio/readability behavior without turning the runtime into spaghetti.


Honest status first

This one is based on the latest commit (75d375c42e92…), and it’s a wide commit.

Not “new subsystem” wide — more like “a lot of quality slices that all needed to happen at once” wide:

A lot of this is the kind of work players notice only when it’s missing. Which usually means it was worth doing.


The biggest practical fix: settings finally belong to the player

Previously, pause-menu changes were session-local in spirit. They felt like “tweaks” rather than true player preferences. This commit closes that gap.

Runtime settings now load with this order:

  1. shipped moddable defaults (Assets/Data/settings.toml)

  2. user settings override (platform data dir settings.toml) when present

and write-back is now real: debounced, atomic, and flushed on resume/restart/quit.


fn load_runtime_settings() -> SettingsConfig {

    let defaults = load_settings_config();

    *match* load_user_settings() {

        Ok(settings) => settings,

        Err(SaveError::Io(error)) *if* error.kind() == std::io::ErrorKind::NotFound => defaults,

        Err(_) => defaults,

    }

}

fn persist_user_settings(&mut self) {

    *if* self.settings == self.settings_last_persisted {

        *return*;

    }

    *match* save_user_settings(&self.settings) {

        Ok(()) => self.settings_last_persisted = self.settings.clone(),

        Err(_) => { */* retry via debounce timer */* }

    }

}

And save paths were corrected to proper platform data locations (including better Linux/macOS behavior), instead of dropping everything under raw $HOME.

This is one of those “boring” improvements that makes the game feel much less prototype-like immediately.


Combat readability: less noise, same truth

Two related changes landed together:

  1. particle pools can now change capacity safely (set_capacity)

  2. damage-number emission is budgeted with token-rate controls from ui.toml

The useful part is not “fewer particles.” It’s “important combat info survives crowding.”


pub fn set_capacity(&mut self, capacity: usize) {

    self.capacity = capacity;

    *while* self.items.len() > self.capacity {

        self.items.pop_front();

    }

}

fn reserve_damage_number_slot(kind: DamageKind, active: usize, max_active: usize, tokens: &mut f32) -> bool {

    *if* kind == DamageKind::PlayerHurt { *return* true; } *// never hide this*

    *if* max_active == 0 || active >= max_active || *tokens < 1.0 { *return* false; }

    *tokens -= 1.0;

    true

}

It now intentionally degrades ambient feedback first, while preserving player-hurt readability.

That’s a good trade.


Companions: from “units” toward “people”

This commit deepened companion identity in three ways:

The liveliness module is intentionally pure and renderer-agnostic, which is exactly right for something we’ll likely tune repeatedly.


pub struct Temperament {

    pub watch_threat: f32,

    pub watch_player: f32,

    pub idle_scan: f32,

    pub scan_speed: f32,

    pub sway: f32,

    pub gaze_ease: f32,

    pub seed: f32,

}

On intro staging, companion entrances can now be hidden and revealed on speaker match:


pub fn start_intro_entrance_for_speaker(&mut self, speaker: &str, npcs: &mut [NpcRuntime]) -> bool {

    *// find matching [[intro.companion_entrance]], start approach once*

}

That gives authors a cleaner first-run scene without hardcoding timing in Rust.


Audio got safer and less fatiguing

There are two wins here:


fn is_supported_audio_path(path: &str) -> bool {

    matches!(ext.as_str(), "ogg" | "wav")

}

*if* enemy_is_boss_like_undead(enemy) {

    play_throttled(SFX_ZOMBIE_GROAN, ...);

} *else* *if* enemy_is_undead(enemy) {

    play_throttled(random_variant, ...);

}

That solves two different “audio pain” classes: startup fragility and combat mix fatigue.


Small but important UI correction: dialogue choice font roles

DialogueChoiceView now separates name_font and body_font explicitly, and dialogue choice text paths use body font consistently.

Not glamorous, but this removes a persistent typography mismatch and makes dialogue UI styling less accidental.


What was verified in this commit

By code-level evidence in the commit:

  - settings round-trip persistence

  - particle pool capacity reduction behavior

  - shipped Lua spawn includes one mid-run JudgeAuditor

  - portrait speaker mapping for companions

  - audio extension guard

  - new stat-key pathways / default enemy coverage / UI feedback defaults

So this wasn’t just “implemented and hoped for.” The behavior got anchored with tests where it mattered.


The humbling part as always: the commit is still unstaged. I’m writing this devlog while the changes are still in that nervous in-between state: written, tested, documented — but without a checkpoint to fall back to.

The commit isn’t a flashy one. It’s mostly about respecting boundaries:

That kind of work is easy to postpone because the game still “runs” without it.

But this is exactly the layer that turns a prototype into something people can live in for more than one session.

The next visual or combat feature will land on better ground because these edges were cleaned now, not later.

$ comments

Reader notes

0 notes

No notes yet.

If you're reading this as a developer: this devlog is built in the open.

If you're reading this as a modder: the direction is source-visible, inspectable systems.