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:
-
settings now persist to a platform user file
-
particle pools can resize safely at runtime
-
combat feedback budgets are configurable and enforced
-
companion identity/liveliness got deeper
-
intro companion entrances are data-driven by dialogue speaker
-
undead death audio got split and throttled by role
-
dialogue choices now use explicit body/name font roles
-
mid-run Auditor spawn behavior got test coverage
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:
-
shipped moddable defaults (
Assets/Data/settings.toml) -
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:
-
particle pools can now change capacity safely (
set_capacity) -
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:
-
new pure liveliness kernel (
src/game/npc_life.rs) for idle gaze/sway temperament -
richer per-identity behavior hooks in runtime (guard intercept, aura ticks, evasive reposition, cleave-supporting attack logic)
-
intro entrances tied to dialogue speaker lines via cinematic data
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:
-
runtime audio now rejects unsupported decode formats early (
.wav/.oggexpected), instead of letting bad manifests crash startup -
undead deaths are routed by role: short randomized slices for common kills, throttled groan for boss-like undead
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:
-
version bumped to
0.36.0 -
changelog updated with the covered slices
-
new/updated tests for:
- 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:
-
user preference boundary (player settings should persist)
-
signal/noise boundary (combat info should stay legible under load)
-
authoring/runtime boundary (cinematic entrances belong in data)
-
scripting/safety boundary (bad audio formats shouldn’t panic startup)
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.

Reader notes
No notes yet.
Sign in with GitHub to leave a note.
Continue with GitHub